Source code for propertyestimator.workflow.decorators

"""
A collection of descriptors used to mark-up elements in a workflow, such
as the inputs or outputs of workflow protocols.
"""
import abc
import copy

from enum import Enum

from propertyestimator import unit
from propertyestimator.utils.quantities import EstimatedQuantity
from propertyestimator.workflow.typing import is_instance_of_type, is_supported_type
from propertyestimator.workflow.utils import PlaceholderInput


class UndefinedAttribute:
    """A custom type used to differentiate between ``None`` values,
    and an undeclared optional value."""

    def __eq__(self, other):
        return type(other) == UndefinedAttribute

    def __ne__(self, other):
        return not self.__eq__(other)

    def __getstate__(self):
        return {}

    def __setstate__(self, state):
        return


UNDEFINED = UndefinedAttribute()


class BaseMergeBehaviour(Enum):
    """A base class for enums which will describes how attributes should
    be handled when attempting to merge similar protocols.
    """
    pass


[docs]class MergeBehaviour(BaseMergeBehaviour): """A enum which describes how attributes should be handled when attempting to merge similar protocols. This enum may take values of * ExactlyEqual: This attribute must be exactly equal between two protocols for them to be able to merge. """ ExactlyEqual = 'ExactlyEqual'
[docs]class InequalityMergeBehaviour(BaseMergeBehaviour): """A enum which describes how attributes which can be compared with inequalities should be merged. This enum may take values of * SmallestValue: When two protocols are merged, the smallest value of this attribute from either protocol is retained. * LargestValue: When two protocols are merged, the largest value of this attribute from either protocol is retained. """ SmallestValue = 'SmallestValue' LargestValue = 'LargestValue'
[docs]class BaseProtocolAttribute(abc.ABC): """A custom descriptor used to mark class attributes as being either a required input, or provided output of a protocol. Notes ----- This decorator expects the protocol to have a matching private field in addition to the public attribute. For example if a protocol has an attribute `substance`, by default the protocol must also have a `_substance` field. """
[docs] def __init__(self, docstring, type_hint): """Initializes a new BaseProtocolAttribute object. Parameters ---------- docstring: str A docstring describing the attributes purpose. This will automatically be decorated with additional information such as type hints, default values, etc. type_hint: type, typing.Union The expected type of this attribute. This will be used to help the workflow engine ensure that expected input types match corresponding output values. """ if not is_supported_type(type_hint): raise ValueError(f'The {type_hint} type is not supported by the ' f'workflow type hinting system.') if hasattr(type_hint, '__qualname__'): if type_hint.__qualname__ == 'build_quantity_class.<locals>.Quantity': typed_docstring = f'Quantity: {docstring}' elif type_hint.__qualname__ == 'build_quantity_class.<locals>.Unit': typed_docstring = f'Unit: {docstring}' else: typed_docstring = f'{type_hint.__qualname__}: {docstring}' elif hasattr(type_hint, '__name__'): typed_docstring = f'{type_hint.__name__}: {docstring}' else: typed_docstring = f'{str(type_hint)}: {docstring}' self.__doc__ = typed_docstring self.type_hint = type_hint
def __set_name__(self, owner, name): self._private_attribute_name = '_' + name def __get__(self, instance, owner=None): if instance is None: # Handle the case where this is called on the class directly, # rather than an instance. return self try: return getattr(instance, self._private_attribute_name) except AttributeError: return UNDEFINED def __set__(self, instance, value): if (not is_instance_of_type(value, self.type_hint) and not isinstance(value, PlaceholderInput) and not value == UNDEFINED): raise ValueError(f'The {self._private_attribute_name[1:]} attribute can only accept ' f'values of type {self.type_hint}') setattr(instance, self._private_attribute_name, value)
class ProtocolInputAttribute(BaseProtocolAttribute): """A descriptor used to mark an attribute of a protocol as an input to that protocol. Examples ---------- To mark an attribute as an input: >>> from propertyestimator.workflow.protocols import BaseProtocol >>> from propertyestimator.workflow.decorators import protocol_input >>> >>> class MyProtocol(BaseProtocol): >>> >>> my_input = protocol_input( >>> docstring='An input will be used.', >>> type_hint=float, >>> default_value=0.1 >>> ) """ def __init__(self, docstring, type_hint, default_value, optional=False, merge_behavior=MergeBehaviour.ExactlyEqual): """Initializes a new protocol_input object. Parameters ---------- default_value: Any The default value for this attribute. optional: bool Defines whether this is an optional input of a class. If true, the `default_value` must be set to `UNDEFINED`. merge_behavior: BaseMergeBehaviour An enum describing how this input should be handled when considering whether to, and actually merging two different protocols. """ docstring = f'**Protocol Input** - {docstring}' if not isinstance(merge_behavior, BaseMergeBehaviour): raise ValueError('The merge behaviour must inherit from `BaseMergeBehaviour`') # Automatically extend the docstrings. if (isinstance(default_value, (int, float, str, unit.Quantity, EstimatedQuantity, Enum)) or (isinstance(default_value, (list, tuple, set, frozenset)) and len(default_value) <= 4)): docstring = f'{docstring} The default value of this attribute ' \ f'is ``{str(default_value)}``.' elif default_value == UNDEFINED: optional_string = '' if optional else ' and must be set by the user.' docstring = f'{docstring} The default value of this attribute ' \ f'is not set{optional_string}.' if (merge_behavior == InequalityMergeBehaviour.SmallestValue or merge_behavior == InequalityMergeBehaviour.LargestValue): merge_docstring = '' if merge_behavior == InequalityMergeBehaviour.SmallestValue: merge_docstring = 'When two protocols are merged, the smallest value of ' \ 'this attribute from either protocol is retained.' if merge_behavior == InequalityMergeBehaviour.SmallestValue: merge_docstring = 'When two protocols are merged, the largest value of ' \ 'this attribute from either protocol is retained.' docstring = f'{docstring} {merge_docstring}' if optional is True: docstring = f'{docstring} This input is *optional*.' super().__init__(docstring, type_hint) self.optional = optional self.merge_behavior = merge_behavior self._default_value = default_value def __get__(self, instance, owner=None): if instance is None: # Handle the case where this is called on the class directly, # rather than an instance. return self if not hasattr(instance, self._private_attribute_name): # Make sure to only ever pass a copy of the default value to ensure # mutable values such as lists don't get set by reference. setattr(instance, self._private_attribute_name, copy.deepcopy(self._default_value)) return getattr(instance, self._private_attribute_name) class ProtocolOutputAttribute(BaseProtocolAttribute): """A descriptor used to mark an attribute of a protocol as an output of that protocol. Examples ---------- To mark an attribute as an output: >>> from propertyestimator.workflow.protocols import BaseProtocol >>> from propertyestimator.workflow.decorators import protocol_output, >>> >>> class MyProtocol(BaseProtocol): >>> >>> my_output = protocol_output( >>> docstring='An output that will be filled.', >>> type_hint=float >>> ) """ def __init__(self, docstring, type_hint): """Initializes a new protocol_output object. """ docstring = f'**Protocol Output** - {docstring}' super().__init__(docstring, type_hint) protocol_input = ProtocolInputAttribute protocol_output = ProtocolOutputAttribute