From e5dbe10d9599b9a53fa07e6fadfaf427ba6d69e3 Mon Sep 17 00:00:00 2001 From: Vratko Polak Date: Tue, 17 Oct 2023 16:31:35 +0200 Subject: feat(MLRsearch): MLRsearch v7 Replaces MLRv2, suitable for "big bang" upgrade across CSIT. PyPI metadata updated only partially (full edits will come separately). Pylint wants less complexity, but the differences are only minor. + Use the same (new CSIT) defaults everywhere, also in Python library. + Update also PLRsearch to use the new result class. + Make upper bound optional in UTI. + Fix ASTF approximate duration detection. + Do not keep approximated_receive_rate (for MRR) in result structure. Change-Id: I03406f32d5c93f56b527cb3f93791b61955dfd74 Signed-off-by: Vratko Polak --- .../python/MLRsearch/dataclass/__init__.py | 19 +++ .../python/MLRsearch/dataclass/dc_property.py | 173 +++++++++++++++++++++ .../libraries/python/MLRsearch/dataclass/field.py | 44 ++++++ 3 files changed, 236 insertions(+) create mode 100644 resources/libraries/python/MLRsearch/dataclass/__init__.py create mode 100644 resources/libraries/python/MLRsearch/dataclass/dc_property.py create mode 100644 resources/libraries/python/MLRsearch/dataclass/field.py (limited to 'resources/libraries/python/MLRsearch/dataclass') diff --git a/resources/libraries/python/MLRsearch/dataclass/__init__.py b/resources/libraries/python/MLRsearch/dataclass/__init__.py new file mode 100644 index 0000000000..e546b090c9 --- /dev/null +++ b/resources/libraries/python/MLRsearch/dataclass/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +__init__ file for Python package "dataclass_property". +""" + +from .dc_property import DataclassProperty +from .field import secondary_field diff --git a/resources/libraries/python/MLRsearch/dataclass/dc_property.py b/resources/libraries/python/MLRsearch/dataclass/dc_property.py new file mode 100644 index 0000000000..7f3b49aeb8 --- /dev/null +++ b/resources/libraries/python/MLRsearch/dataclass/dc_property.py @@ -0,0 +1,173 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module defining DataclassProperty class. + +The main issue that needs support is dataclasses with properties +(including setters) and with (immutable) default values. + +First, this explains how property ends up passed as default constructor value: +https://florimond.dev/en/posts/2018/10/ +/reconciling-dataclasses-and-properties-in-python/ +TL;DR: By the time __init__ is generated, original class variable (type hint) +is replaced by property (method definition). + +Second, there are ways to deal with that: +https://stackoverflow.com/a/61480946 +TL;DR: It relies on the underscored field being replaced by the value. + +But that does not work for field which use default_factory (or no default) +(the underscored class field is deleted instead). +So another way is needed to cover those cases, +ideally without the need to define both original and underscored field. + +This implementation relies on a fact that decorators are executed +when the class fields do yet exist, and decorated function +does know its name, so the decorator can get the value stored in +the class field, and store it as an additional attribute of the getter function. +Then for setter, the property contains the getter (as an unbound function), +so it can access the additional attribute to get the value. + +This approach circumvents the precautions dataclasses take to prevent mishaps +when a single mutable object is shared between multiple instances. +So it is up to setters to create an appropriate copy of the default object +if the default value is mutable. + +The default value cannot be MISSING nor Field nor DataclassProperty, +otherwise the intended logic breaks. +""" + +from __future__ import annotations + +from dataclasses import Field, MISSING +from functools import wraps +from inspect import stack +from typing import Callable, Optional, TypeVar, Union + + +Self = TypeVar("Self") +"""Type for the dataclass instances being created using properties.""" +Value = TypeVar("Value") +"""Type for the value the property (getter, setter) handles.""" + + +def _calling_scope_variable(name: str) -> Value: + """Get a variable from a higher scope. + + This feels dirty, but without this the syntactic sugar + would not be sweet enough. + + The implementation is copied from https://stackoverflow.com/a/14694234 + with the difference of raising RuntimeError (instead of returning None) + if no variable of that name is found in any of the scopes. + + :param name: Name of the variable to access. + :type name: str + :returns: The value of the found variable. + :rtype: Value + :raises RuntimeError: If the variable is not found in any calling scope. + """ + frame = stack()[1][0] + while name not in frame.f_locals: + frame = frame.f_back + if frame is None: + raise RuntimeError(f"Field {name} value not found.") + return frame.f_locals[name] + + +class DataclassProperty(property): + """Subclass of property, handles default values for dataclass fields. + + If a dataclass field does not specify a default value (nor default_factory), + this is not needed, and in fact it will not work (so use built-in property). + + This implementation seemlessly finds and inserts the default value + (can be mutable) into a new attribute of the getter function. + Before calling a setter function in init (recognized by type), + the default value is retrieved and passed transparently to the setter. + It is the responsibilty of the setter to appropriately clone the value, + in order to prevent multiple instances sharing the same mutable value. + """ + + def __init__( + self, + fget: Optional[Callable[[], Value]] = None, + fset: Optional[Callable[[Self, Value], None]] = None, + fdel: Optional[Callable[[], None]] = None, + doc: Optional[str] = None, + ): + """Find and store the default value, construct the property. + + See this for how the superclass property works: + https://docs.python.org/3/howto/descriptor.html#properties + + :param fget: Getter (unbound) function to use, if any. + :param fset: Setter (unbound) function to use, if any. + :param fdel: Deleter (unbound) function to use, if any. + :param doc: Docstring to display when examining the property. + :type fget: Optional[Callable[[Self], Value]] + :type fset: Optional[Callable[[Self, Value], None]] + :type fdel: Optional[Callable[[Self], None]] + :type doc: Optional[str] + """ + variable_found = _calling_scope_variable(fget.__name__) + if not isinstance(variable_found, DataclassProperty): + if isinstance(variable_found, Field): + if variable_found.default is not MISSING: + fget.default_value = variable_found.default + # Else do not store any default value. + else: + fget.default_value = variable_found + # Else this is the second time init is called (when setting setter), + # in which case the default is already stored into fget. + super().__init__(fget=fget, fset=fset, fdel=fdel, doc=doc) + + def setter( + self, + fset: Optional[Callable[[Self, Value], None]], + ) -> DataclassProperty: + """Return new instance with a wrapped setter function set. + + If the argument is None, call superclass method. + + The wrapped function recognizes when it is called in init + (by the fact the value argument is of type DataclassProperty) + and in that case it extracts the stored default and passes that + to the user-defined setter function. + + :param fset: Setter function to wrap and apply. + :type fset: Optional[Callable[[Self, Value], None]] + :returns: New property instance with correct setter function set. + :rtype: DataclassProperty + """ + if fset is None: + return super().setter(fset) + + @wraps(fset) + def wrapped(sel_: Self, val: Union[Value, DataclassProperty]) -> None: + """Extract default from getter if needed, call the user setter. + + The sel_ parameter is listed explicitly, to signify + this is an unbound function, not a bounded method yet. + + :param sel_: Instance of dataclass (not of DataclassProperty) + to set the value on. + :param val: Set this value, or the default value stored there. + :type sel_: Self + :type val: Union[Value, DataclassProperty] + """ + if isinstance(val, DataclassProperty): + val = val.fget.default_value + fset(sel_, val) + + return super().setter(wrapped) diff --git a/resources/libraries/python/MLRsearch/dataclass/field.py b/resources/libraries/python/MLRsearch/dataclass/field.py new file mode 100644 index 0000000000..55d9d0879f --- /dev/null +++ b/resources/libraries/python/MLRsearch/dataclass/field.py @@ -0,0 +1,44 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module defining secondary_field function. + +Just a shrothand for frequently repeated expression. + +The main point is that this dataclass field is not used in init. +Maybe it is a derived value of a frozen dataclass. +Maybe it is a cache to help avoiding repeated computation. +Maybe it is a temporary value stored in one method and read in another method. +In any case, the caller does not need to know it is here, +so it is excluded from repr, hashing, ordering and similar. +""" + +from dataclasses import Field, field + + +def secondary_field() -> Field: + """Return newly created Field with non-default arguments + + In practice, it seems to be fine to reuse the resulting Field instance + when defining multiple dataclass fields, + but we keep this as a function to improve readability. + + :returns: A new Field instance useful for secondary fields. + :rtype: Field + """ + return field( + default=None, + init=False, + repr=False, + compare=False, + ) -- cgit 1.2.3-korg