Source code for easyscience.Objects.ObjectClasses

from __future__ import annotations

__author__ = 'github.com/wardsimon'
__version__ = '0.1.0'

#  SPDX-FileCopyrightText: 2023 EasyScience contributors  <core@easyscience.software>
#  SPDX-License-Identifier: BSD-3-Clause
#  © 2021-2023 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience

from inspect import getfullargspec
from typing import TYPE_CHECKING
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Set
from typing import TypeVar
from typing import Union

from easyscience import global_object
from easyscience.Utils.classTools import addLoggedProp

from .core import ComponentSerializer
from .new_variable import Parameter as NewParameter
from .new_variable.descriptor_base import DescriptorBase
from .Variable import Descriptor
from .Variable import Parameter

if TYPE_CHECKING:
    from easyscience.Constraints import C
    from easyscience.Objects.Inferface import iF
    from easyscience.Objects.Variable import V


[docs] class BasedBase(ComponentSerializer): __slots__ = ['_name', '_global_object', 'user_data', '_kwargs'] _REDIRECT = {} def __init__(self, name: str, interface: Optional[iF] = None, unique_name: Optional[str] = None): self._global_object = global_object if unique_name is None: unique_name = self._global_object.generate_unique_name(self.__class__.__name__) self._unique_name = unique_name self._name = name self._global_object.map.add_vertex(self, obj_type='created') self.interface = interface self.user_data: dict = {} @property def _arg_spec(self) -> Set[str]: base_cls = getattr(self, '__old_class__', self.__class__) spec = getfullargspec(base_cls.__init__) names = set(spec.args[1:]) return names def __reduce__(self): """ Make the class picklable. Due to the nature of the dynamic class definitions special measures need to be taken. :return: Tuple consisting of how to make the object :rtype: tuple """ state = self.encode() cls = getattr(self, '__old_class__', self.__class__) return cls.from_dict, (state,) @property def unique_name(self) -> str: """Get the unique name of the object.""" return self._unique_name @unique_name.setter def unique_name(self, new_unique_name: str): """Set a new unique name for the object. The old name is still kept in the map. :param new_unique_name: New unique name for the object""" if not isinstance(new_unique_name, str): raise TypeError('Unique name has to be a string.') self._unique_name = new_unique_name self._global_object.map.add_vertex(self) @property def name(self) -> str: """ Get the common name of the object. :return: Common name of the object """ return self._name @name.setter def name(self, new_name: str): """ Set a new common name for the object. :param new_name: New name for the object :return: None """ self._name = new_name @property def interface(self) -> iF: """ Get the current interface of the object """ return self._interface @interface.setter def interface(self, new_interface: iF): """ Set the current interface to the object and generate bindings if possible. iF.e. ``` def __init__(self, bar, interface=None, **kwargs): super().__init__(self, **kwargs) self.foo = bar self.interface = interface # As final step after initialization to set correct bindings. ``` """ self._interface = new_interface if new_interface is not None: self.generate_bindings()
[docs] def generate_bindings(self): """ Generate or re-generate bindings to an interface (if exists) :raises: AttributeError """ if self.interface is None: raise AttributeError('Interface error for generating bindings. `interface` has to be set.') interfaceable_children = [ key for key in self._global_object.map.get_edges(self) if issubclass(type(self._global_object.map.get_item_by_key(key)), BasedBase) ] for child_key in interfaceable_children: child = self._global_object.map.get_item_by_key(child_key) child.interface = self.interface self.interface.generate_bindings(self)
[docs] def switch_interface(self, new_interface_name: str): """ Switch or create a new interface. """ if self.interface is None: raise AttributeError('Interface error for generating bindings. `interface` has to be set.') self.interface.switch(new_interface_name) self.generate_bindings()
@property def constraints(self) -> List[C]: pars = self.get_parameters() constraints = [] for par in pars: con: Dict[str, C] = par.user_constraints for key in con.keys(): constraints.append(con[key]) return constraints ## TODO clean when full move to new_variable
[docs] def get_parameters(self) -> Union[List[Parameter], List[NewParameter]]: """ Get all parameter objects as a list. :return: List of `Parameter` objects. """ par_list = [] for key, item in self._kwargs.items(): if hasattr(item, 'get_parameters'): par_list = [*par_list, *item.get_parameters()] elif isinstance(item, Parameter) or isinstance(item, NewParameter): par_list.append(item) return par_list
## TODO clean when full move to new_variable def _get_linkable_attributes(self) -> List[V]: """ Get all objects which can be linked against as a list. :return: List of `Descriptor`/`Parameter` objects. """ item_list = [] for key, item in self._kwargs.items(): if hasattr(item, '_get_linkable_attributes'): item_list = [*item_list, *item._get_linkable_attributes()] elif issubclass(type(item), (Descriptor, DescriptorBase)): item_list.append(item) return item_list ## TODO clean when full move to new_variable
[docs] def get_fit_parameters(self) -> Union[List[Parameter], List[NewParameter]]: """ Get all objects which can be fitted (and are not fixed) as a list. :return: List of `Parameter` objects which can be used in fitting. """ fit_list = [] for key, item in self._kwargs.items(): if hasattr(item, 'get_fit_parameters'): fit_list = [*fit_list, *item.get_fit_parameters()] elif isinstance(item, Parameter) or isinstance(item, NewParameter): if item.enabled and not item.fixed: fit_list.append(item) return fit_list
def __dir__(self) -> Iterable[str]: """ This creates auto-completion and helps out in iPython notebooks. :return: list of function and parameter names for auto-completion """ new_class_objs = list(k for k in dir(self.__class__) if not k.startswith('_')) return sorted(new_class_objs) def __copy__(self) -> BasedBase: """Return a copy of the object.""" temp = self.as_dict(skip=['unique_name']) new_obj = self.__class__.from_dict(temp) return new_obj
if TYPE_CHECKING: B = TypeVar('B', bound=BasedBase) BV = TypeVar('BV', bound=ComponentSerializer)
[docs] class BaseObj(BasedBase): """ This is the base class for which all higher level classes are built off of. NOTE: This object is serializable only if parameters are supplied as: `BaseObj(a=value, b=value)`. For `Parameter` or `Descriptor` objects we can cheat with `BaseObj(*[Descriptor(...), Parameter(...), ...])`. """ ## TODO clean when full move to new_variable def __init__( self, name: str, unique_name: Optional[str] = None, *args: Optional[BV], **kwargs: Optional[BV], ): """ Set up the base class. :param name: Name of this object :param args: Any arguments? :param kwargs: Fields which this class should contain """ super(BaseObj, self).__init__(name=name, unique_name=unique_name) # If Parameter or Descriptor is given as arguments... for arg in args: if issubclass(type(arg), (BaseObj, Descriptor, DescriptorBase)): kwargs[getattr(arg, 'name')] = arg # Set kwargs, also useful for serialization known_keys = self.__dict__.keys() self._kwargs = kwargs for key in kwargs.keys(): if key in known_keys: raise AttributeError('Kwargs cannot overwrite class attributes in BaseObj.') if issubclass(type(kwargs[key]), (BasedBase, Descriptor, DescriptorBase)) or 'BaseCollection' in [ c.__name__ for c in type(kwargs[key]).__bases__ ]: self._global_object.map.add_edge(self, kwargs[key]) self._global_object.map.reset_type(kwargs[key], 'created_internal') addLoggedProp( self, key, self.__getter(key), self.__setter(key), get_id=key, my_self=self, test_class=BaseObj, ) def _add_component(self, key: str, component: BV) -> None: """ Dynamically add a component to the class. This is an internal method, though can be called remotely. The recommended alternative is to use typing, i.e. class Foo(Bar): def __init__(self, foo: Parameter, bar: Parameter): super(Foo, self).__init__(bar=bar) self._add_component("foo", foo) Goes to: class Foo(Bar): foo: ClassVar[Parameter] def __init__(self, foo: Parameter, bar: Parameter): super(Foo, self).__init__(bar=bar) self.foo = foo :param key: Name of component to be added :param component: Component to be added :return: None """ self._kwargs[key] = component self._global_object.map.add_edge(self, component) self._global_object.map.reset_type(component, 'created_internal') addLoggedProp( self, key, self.__getter(key), self.__setter(key), get_id=key, my_self=self, test_class=BaseObj, ) ## TODO clean when full move to new_variable def __setattr__(self, key: str, value: BV) -> None: # Assume that the annotation is a ClassVar old_obj = None if ( hasattr(self.__class__, '__annotations__') and key in self.__class__.__annotations__ and hasattr(self.__class__.__annotations__[key], '__args__') and issubclass( getattr(value, '__old_class__', value.__class__), self.__class__.__annotations__[key].__args__, ) ): if issubclass(type(getattr(self, key, None)), (BasedBase, Descriptor, DescriptorBase)): old_obj = self.__getattribute__(key) self._global_object.map.prune_vertex_from_edge(self, old_obj) self._add_component(key, value) else: if hasattr(self, key) and issubclass(type(value), (BasedBase, Descriptor, DescriptorBase)): old_obj = self.__getattribute__(key) self._global_object.map.prune_vertex_from_edge(self, old_obj) self._global_object.map.add_edge(self, value) super(BaseObj, self).__setattr__(key, value) # Update the interface bindings if something changed (BasedBase and Descriptor) if old_obj is not None: old_interface = getattr(self, 'interface', None) if old_interface is not None: self.generate_bindings() def __repr__(self) -> str: return f"{self.__class__.__name__} `{getattr(self, 'name')}`" @staticmethod def __getter(key: str) -> Callable[[BV], BV]: def getter(obj: BV) -> BV: return obj._kwargs[key] return getter @staticmethod def __setter(key: str) -> Callable[[BV], None]: def setter(obj: BV, value: float) -> None: if issubclass(obj._kwargs[key].__class__, (Descriptor, DescriptorBase)) and not issubclass( value.__class__, (Descriptor, DescriptorBase) ): obj._kwargs[key].value = value else: obj._kwargs[key] = value return setter
# @staticmethod # def __setter(key: str) -> Callable[[Union[B, V]], None]: # def setter(obj: Union[V, B], value: float) -> None: # if issubclass(obj._kwargs[key].__class__, Descriptor): # if issubclass(obj._kwargs[key].__class__, Descriptor): # obj._kwargs[key] = value # else: # obj._kwargs[key].value = value # else: # obj._kwargs[key] = value # # return setter