# -*- coding: utf-8 -*-
"""Provides base for Stone Soup components.
To aid creation of components in Stone Soup, a declarative approach is used to
declare properties of components. These declared properties are then used to
generate the signature for the class, populate documentation, and generate
forms for the user interface.
An example would be:
.. code-block:: python
class Foo(Base):
'''Example Foo class'''
foo: str = Property(doc="foo string parameter")
bar: int = Property(default=10, doc="bar int parameter, default is 10")
This is equivalent to the following:
.. code-block:: python
class Foo:
'''Example Foo class
Parameters
----------
foo : str
foo string parameter
bar : int, optional
bar int parameter, default is 10
'''
def __init__(self, foo, bar=10):
self.foo = foo
self.bar = 10
.. note::
The init method is actually part of :class:`Base` class so in the case of
having to customise initialisation, :func:`super` should be used e.g.:
.. code-block:: python
class Foo(Base):
'''Example Foo class'''
foo: str = Property(doc="foo string parameter")
bar: int = Property(default=10, doc="bar int parameter, default is 10")
def __init__(self, foo, bar=bar.default, *args, **kwargs):
if bar < 0:
raise ValueError("...")
super().__init__(foo, bar, *args, **kwargs)
"""
import inspect
from reprlib import Repr
from abc import ABCMeta
from collections import OrderedDict
from copy import copy
from types import MappingProxyType
[docs]class Property:
"""Property(cls, default=inspect.Parameter.empty)
Property class for definition of attributes on component classes.
A class must be provided such that the framework is aware of how components
are put together to create a valid run within the framework. Additionally,
the class is used by the user interface to generate configuration options
to the users. The class is not used for any type checking, in the spirit of
Python's duck typing.
A default value can be specified to signify the property on the class is
optional. As ``None`` and ``False`` are reasonable default values,
:class:`inspect.Parameter.empty` is used to signify the argument is
mandatory. (Also aliased to :attr:`Property.empty` for ease)
A description string can also be provided which will be rendered in the
documentation.
A property can be specified as read only using the (optional) ``readonly``
flag. Such properties can be written only once (when the parent object is
instantiated). Any subsequent write raises an ``AttributeError``
Property also can be used in similar way to Python standard `property`
using `getter`, `setter` and `deleter` decorators.
Parameters
----------
cls : class, optional
A Python class. Where not specified, a type annotation is required,
and providing both will raise an error.
default : any, optional
A default value, which should be same type as class or None. Defaults
to :class:`inspect.Parameter.empty` (alias :attr:`Property.empty`)
doc : str, optional
Doc string for property
readonly : bool, optional
If `True`, then property can only be set during initialisation.
Attributes
----------
cls
default
doc
readonly
empty : :class:`inspect.Parameter.empty`
Alias to :class:`inspect.Parameter.empty`
"""
empty = inspect.Parameter.empty
def __init__(self, cls=None, *, default=inspect.Parameter.empty, doc=None,
readonly=False):
self.cls = cls
self.default = default
self.doc = self.__doc__ = doc
# Fix for when ":" in doc string being interpreted as type in NumpyDoc
if doc is not None and ':' in doc:
self.__doc__ = ": " + doc
self._property_name = None
self._setter = None
self._getter = None
self._deleter = None
self.readonly = readonly
def __get__(self, instance, owner):
if instance is None:
return self
if self._getter is None:
return getattr(instance, self._property_name)
else:
return self._getter(instance)
def __set__(self, instance, value):
if self.readonly:
if not hasattr(instance, self._property_name):
setattr(instance, self._property_name, value)
else:
# if the value has been set, raise an AttributeError
raise AttributeError(
'{} is readonly'.format(self._property_name))
if self._setter is None:
setattr(instance, self._property_name, value)
else:
self._setter(instance, value)
def __delete__(self, instance):
if self._deleter is None:
delattr(instance, self._property_name)
else:
self._deleter(instance, self._property_name)
def __set_name__(self, owner, name):
if not isinstance(owner, BaseMeta):
raise AttributeError("Cannot use Property on this class type")
self._property_name = "_property_{}".format(name)
def deleter(self, method): # real signature unknown
""" Descriptor to change the deleter on a property. """
new_property = copy(self)
new_property._deleter = method
return new_property
def getter(self, method): # real signature unknown
""" Descriptor to change the getter on a property. """
new_property = copy(self)
new_property._getter = method
return new_property
def setter(self, method): # real signature unknown
""" Descriptor to change the setter on a property. """
new_property = copy(self)
new_property._setter = method
return new_property
class BaseRepr(Repr):
def __init__(self):
self.maxlevel = 10
self.maxtuple = 10
self.maxlist = 10
self.maxarray = 10
self.maxdict = 20
self.maxset = 10
self.maxfrozenset = 10
self.maxdeque = 10
self.maxstring = 500
self.maxlong = 40
self.maxother = 50000
def repr_list(self, obj, level):
if len(obj) > self.maxlist:
max_len = round(self.maxlist/2)
first = ',\n '.join(self.repr1(x, level - 1) for x in obj[:max_len])
last = ',\n '.join(self.repr1(x, level - 1) for x in obj[-max_len:])
return f'[{first},\n ...\n ...\n ...\n {last}]'
else:
return '[{}]'.format(',\n '.join(self.repr1(x, level - 1) for x in obj))
@classmethod
def whitespace_remove(cls, maxlen_whitespace, val):
"""Remove excess whitespace, replacing with ellipses"""
large_whitespace = ' ' * (maxlen_whitespace+1)
fixed_whitespace = ' ' * maxlen_whitespace
if large_whitespace in val:
excess = val.find(large_whitespace) # Find the excess whitespace
line_end = ''.join(val[excess:].partition('\n')[1:])
val = ''.join([val[0:excess], fixed_whitespace, '...', line_end])
return cls.whitespace_remove(maxlen_whitespace, val)
else:
return val
[docs]class Base(metaclass=BaseMeta):
"""Base class for framework components.
This is the base class which should be used for any Stone Soup components.
Building on the :class:`BaseMeta` this provides a init method which
populates the declared properties with their values.
Subclasses can override this method, but they should either call this via
:func:`super()` or ensure they manually populated the properties as
declared."""
def __init__(self, *args, **kwargs):
init_signature = inspect.signature(self.__init__)
bound_arguments = init_signature.bind(*args, **kwargs)
bound_arguments.apply_defaults()
for name, value in bound_arguments.arguments.items():
setattr(self, name, value)
def __repr__(self):
whitespace = ' ' * 4 # Indents every line
max_len_whitespace = 80 # Ensures whitespace doesn't get rid of space on RHS too much
max_out = 50000 # Keeps total length from being too excessive
params = []
for name in type(self).properties:
value = getattr(self, name)
extra_whitespace = ' ' * (len(name) + 1) + whitespace # Lines up rows of arrays
repr_value = Base._repr.repr(value)
if '\n' in repr_value:
value = repr_value.replace('\n', '\n' + extra_whitespace)
params.append(f'{whitespace}{name}={value}')
value = "{}(\n{})".format(type(self).__name__, ",\n".join(params))
rep = Base._repr.whitespace_remove(max_len_whitespace, value)
truncate = '\n...\n... (truncated due to length)\n...'
return ''.join([rep[:max_out], truncate]) if len(rep) > max_out else rep