Source code for usim._basics._resource_level

from abc import abstractmethod
from weakref import WeakValueDictionary
from typing import Iterable, Tuple, Type, Generic, TypeVar


T = TypeVar('T')


[docs]class ResourceLevels(Generic[T]): """ Common class for named resource levels Representation for the levels of multiple named resources. Every set of resources, such as :py:class:`usim.Resources` or :py:class:`usim.Capacities`, specializes a :py:class:`~.ResourceLevels` subclass with one attribute for each named resource. For example, ``Resources(a=3, b=4)`` uses a :py:class:`~.ResourceLevels` with attributes ``a`` and ``b``. .. code:: python3 from usim import Resources resources = Resources(a=3, b=4) print(resources.levels.a) # 3 print(resources.levels.b) # 4 print(resources.levels.c) # raises AttributeError :py:class:`~.ResourceLevels` subtypes allow no additional attributes other than their initial resources, but their values may be changed. Instantiating a subtype requires resource levels to be specified by keyword; missing resource are set to zero. Each resource always uses the same :py:class:`~.ResourceLevels` subtype. Binary operators for comparisons and arithmetic can be applied for instances of the same subtype. .. describe:: levels_a + levels_b levels_a - levels_b Elementwise addition/subtraction of values. .. describe:: levels_a > levels_b levels_a >= levels_b levels_a <= levels_b levels_a < levels_b Strict elementwise comparison of values. :py:data:`True` if the comparison is satisfied by each element pair, :py:data:`False` otherwise. .. describe:: levels_a == levels_b Total elementwise equality of values. :py:data:`True` if each element pair is equal, :py:data:`False` otherwise. The inverse of ``levels_a != levels_b``. .. describe:: levels_a != levels_b Partial elementwise unequality of values. :py:data:`False` if each element pair is equal, :py:data:`True` otherwise. The inverse of ``levels_a == levels_b``. In addition, iteration on a :py:class:`~.ResourceLevels` subtype yields ``field, value`` pairs. This is similar to :py:meth:`dict.items`. .. describe:: for field, value in levels_a Iterate over the current ``field, value`` pairs. .. describe:: dict(levels_a) Create :py:class:`dict` of ``field: value`` pairs. """ __slots__ = () __fields__: Tuple[str] = () #: cache of currently used specialisations to avoid #: recreating/duplicating commonly used types __specialisation_cache__ = WeakValueDictionary() def __init__(self, **kwargs: T): spec_name = f'{__specialise__.__module__}.{__specialise__.__qualname__}' raise TypeError( f'Base class {self.__class__.__name__} cannot be instantiated.\n' '\n' f'The {self.__class__.__name__} type is intended to be automatically\n' 'subclassed by resources. You should not encounter the base class during\n' 'well-behaved simulations.\n' '\n' f'Use {spec_name} to declare subtypes with valid resource level names.\n' ) @abstractmethod def __add__(self, other: 'ResourceLevels[T]') -> 'ResourceLevels[T]': raise NotImplementedError @abstractmethod def __sub__(self, other: 'ResourceLevels[T]') -> 'ResourceLevels[T]': raise NotImplementedError @abstractmethod def __gt__(self, other: 'ResourceLevels[T]') -> bool: raise NotImplementedError @abstractmethod def __ge__(self, other: 'ResourceLevels[T]') -> bool: raise NotImplementedError @abstractmethod def __le__(self, other: 'ResourceLevels[T]') -> bool: raise NotImplementedError @abstractmethod def __lt__(self, other: 'ResourceLevels[T]') -> bool: raise NotImplementedError @abstractmethod def __eq__(self, other: 'ResourceLevels[T]') -> bool: raise NotImplementedError @abstractmethod def __ne__(self, other: 'ResourceLevels[T]') -> bool: raise NotImplementedError def __iter__(self): for field in self.__fields__: yield field, getattr(self, field) def __repr__(self): content = ', '.join( f'{key}={item}' for key, item in self ) return f'{self.__class__.__name__}({content})'
def __specialise__(zero: T, names: Iterable[str]) -> Type[ResourceLevels[T]]: """ Create a specialisation of :py:class:`~.ResourceLevels` :param zero: zero value for all fields :param names: names of fields """ fields = tuple(sorted(names)) try: return ResourceLevels.__specialisation_cache__[fields] except KeyError: pass class SpecialisedResourceLevels(ResourceLevels): __slots__ = fields __fields__ = fields __init__ = __make_init__(zero, fields) __add__ = __binary_op__('__add__', '+', fields) __sub__ = __binary_op__('__sub__', '-', fields) __gt__ = __comparison_op__('__gt__', '>', fields) __ge__ = __comparison_op__('__ge__', '>=', fields) __le__ = __comparison_op__('__le__', '<=', fields) __lt__ = __comparison_op__('__le__', '<', fields) __eq__ = __comparison_op__('__eq__', '==', fields) def __ne__(self, other): return not self == other ResourceLevels.__specialisation_cache__[fields] = SpecialisedResourceLevels return SpecialisedResourceLevels def __make_init__(zero, names: Tuple[str, ...]): """Make an ``__init__`` with ``names`` as keywords and defaults of ``zero``""" namespace = {} args_list = f'={zero}, '.join(names) exec( '\n'.join( [ f"""def __init__(self, *, {args_list}={zero}):""" ] + [ f""" self.{name} = {name}""" for name in names ] ), namespace ) return namespace['__init__'] def __binary_op__(op_name: str, op_symbol: str, names: Tuple[str, ...]): """ Make an operator method ``op_name`` to apply ``op_symbol`` to all fields ``names`` .. code:: python3 __add__ = __make_binary_op__("__add__", '+', ('foo', 'bar')) def __add__(self, other): return type(self)( foo = self.foo + other.foo, bar = self.bar + other.bar, ) """ namespace = {} exec( '\n'.join( [ f"""def {op_name}(self, other):""", """ assert type(self) is type(other),\\""", """ 'resource levels specialisations cannot be mixed'""", """ return type(self)(""", ] + [ f""" {name} = self.{name} {op_symbol} other.{name},""" for name in names ] + [ """ )""" ] ), namespace ) return namespace[op_name] def __comparison_op__(op_name: str, op_symbol: str, names: Tuple[str]): """ Make a comparison method ``op_name`` to apply ``op_symbol`` to all fields ``names`` .. code:: python3 __eq__ = __make_binary_op__("__eq__", '==', ('foo', 'bar')) def __add__(self, other): return ( self.foo + other.foo and self.bar + other.bar ) """ namespace = {} exec( '\n'.join( [ f"""def {op_name}(self, other):""", """ assert type(self) is type(other),\\""", """ 'resource levels specialisations cannot be mixed'""", """ return (""", f""" self.{names[0]} {op_symbol} other.{names[0]}""" ] + [ f""" and self.{name} {op_symbol} other.{name}""" for name in names[1:] ] + [ """ )""" ] ), namespace ) return namespace[op_name]