Neotraits

Neotraits is an API that lets you declare class attributes with type checking and introspection abilities.

If you have used sqlalchemy declarative, django models or marshmallow you will not find neotraits surprising. It is quite similar to traitlets.

Quick example

You subclass HasTraits and declare some attributes:

from tvb.basic.neotraits.api import HasTraits, Attr

class Foo(HasTraits):
    """ Foo is a traited class """
    bar = Attr(str, doc='an iron bar', default='iron')
    baz = Attr(int)

The default __init__ takes kwargs for each declared attribute.

>>> foo = Foo(baz=42)

For each declared attribute an instance attribute of the same name is created.

# foo.baz is an instance attribute of foo.
>>> print foo.baz
42
# foo.bar is initialized with the default
>>> foo.bar
'iron'

Neotraits will enforce the type of the attribute

>>> foo.baz = 'oops'
TraitTypeError: Attribute can't be set to an instance of <type 'str'>
    attribute __main__.Foo.baz = Attr(field_type=<type 'int'>, default=None, required=True)

Attributes

A neotrait attribute is an instance of Attr that is defined in a class that derives from HasTraits.

Attributes have attribute-specific parameters. Here is a more complex example.

class Foo(HasTraits):
    food = Attr(str, choices=('fish', 'chicken'), required=False)
    eye = NArray(dtype=int, default=numpy.eye(4))
    boat = Float(
        default=42.42,
        label='boaty mcboatface',
        doc='''
        The aquatic vehicle par exellence.
        '''
    )

See the Attribute reference for all the available attributes and their parameters.

Composing and inheriting

Attributes can refer to other traited classes. You simply define an attribute with the type of the referred class :

class A(HasTraits):
    fi = Attr(int)

class B(A):
    ra = Attr(str)
    vi = Float()

class Comp(HasTraits):
    a = Attr(A)
    b = Attr(B)

comp = Comp(a=A(), b=B())

comp.a.fi = 234

Inheritance also works as expected.

class A(HasTraits):
    a = Attr(str)

class B(HasTraits):
    b = Attr(int)
    c = Attr(float)

class Inherit(B, A):
    c = Attr(str)

# c has been correctly overridden and is of type str not float
>>> t = Inherit(a="ana", c="are", b=2)
>>> t.c
are

Introspection

Neotraits keeps a registry of all traited classes that have been created.

>>> HasTraits.get_known_subclasses()
(<class 'tvb.basic.neotraits._core.HasTraits'>, <class '__main__.Foo'>)

A traited class keeps a tuple of all the declared attributes:

>>> Foo.own_declarative_attrs
('baz', 'bar')
# this includes inherited declarative attributes
>>> Foo.declarative_attrs
('baz', 'bar', 'gid')

The declarative Attr objects can still be found on the class. And you can get the declaration details:

>>> print Foo.bar
__main__.Foo.bar = Attr(field_type=<type 'str'>, default='iron', required=True)
>>> Foo.bar.required
True

With these capabilities one can generically list all int attributes:

>>> some_traited_type = Foo
>>> for k in some_traited_type.declarative_attrs:
...    attr = getattr(some_traited_type, k)
...    if attr.field_type == int:
...        print k
baz

Introspection is used by the default __init__, jupyter pretty printing and automatic docstring generation:

Introspection can be used by tools to generate serializers, db mappings, gui’s and other capabilities.

>>> print Foo.__doc__

Traited class [__main__.Foo]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

 Foo is a traited class

Attributes declared
"""""""""""""""""""
baz : __main__.Foo.baz = Attr(field_type=<type 'int'>, default=None, required=True)
bar : __main__.Foo.bar = Attr(field_type=<type 'str'>, default='iron', required=True)
    an iron bar
gid : tvb.basic.neotraits._core.HasTraits.gid = Attr(field_type=<class 'uuid.UUID'>, default=None, required=True)

Properties

It is useful to have properties that are also annotated with attributes. They behave like read only python properties but they also retain the declarations and can be used by introspection.

class A(HasTraits):
    x = NArray(doc='the mighty x')

    @trait_property(NArray(label='3 * x', doc='use the docstring, it is nicer'))
    def x3(self):
        """
        Encouraged place for doc
        """
        return self.x * 3

>>> a = A()
>>> a.x = numpy.arange(4)
>>> a.x3
array([0, 3, 6, 9])

The class remembers which properties are trait_property-ies

>>> A.declarative_props
('x3', )
>>> A.x3.attr.label
'3 * x'

Cached properties are similar to properties. But the method only gets called once. The second reference to a cached property will return the cached value.

class A(HasTraits):

    @cached_trait_property(NArray(label='expensive'))
    def expensive_once(self):
        return numpy.eye(1001)

Attribute options

All attributes can have a default value. This value is a class attribute and is shared by all instances. For mutable data this might lead to surprising behaviour. In that case you can use a factory function as a default.

class A(HasTraits):
    a = Attr(field_type=dict, default=lambda: {'factory': 'default'})

By default attributes are required. Reading an uninitialized one will fail

  class A(HasTraits):
      a = Attr(str)
      b = Attr(str, default='hello')
      c = Attr(str, required=False)

  >>> ainst = A()
  >>> ainst.a
  TraitAttributeError: required attribute referenced before assignment. Use a default or assign a value before reading it
attribute __main__.A.a = Attr(field_type=<type 'str'>, default=None, required=True)

If a default is present then you get that

>>> ainst.b
'hello'

An attribute may be declared as not required. When you read an optional attribute with no default you get None.

>>> ainst.c is None
True

Numpy arrays and dimensions

Narray attributes can declare a symbolic shape. Note that when attributes are declared no instance is yet created. So you cannot declare a concrete shape.

We define the shape by referring to other attributes defined in the same class.

class A(HasTraits):
    n_nodes = Dim(doc='number of nodes')
    n_sv = Dim()
    w = NArray(shape=(n_nodes, n_nodes))

These Dim’s have to be initialized once when an instance is created. Accessing arrays before the dimensions have been initialized is an error.

# dims are ordinary attributes, you can initialize them in init
>>> ainst = A(n_sv=2)
# w references the undefined n_nodes
>>> ainst.w = numpy.eye(2)
tvb.basic.neotraits.ex.TraitAttributeError: Narray's shape references undefined dimension <n_nodes>. Set it before accessing this array
  attribute __main__.A.w = NArray(label='', dtype=float64, default=None, dim_names=(), ndim=2, required=True)

Once the dimensions have been set on an instance they enforce the shapes of the arrays that use them.

>>> ainst.n_nodes = 42
>>> ainst.w = numpy.eye(2)
tvb.basic.neotraits.ex.TraitValueError: Shape mismatch. Expected (42, 42). Given (2L, 2L). Not broadcasting

We don’t allow arithmetic with symbolic dimensions.

class A(HasTraits):
    n_nodes = Dim(doc='number of nodes')
    flat_w = NArray(shape=(n_nodes * n_nodes, ))

TypeError: unsupported operand type(s) for *: 'Dim' and 'Dim'

Validation

The Attr declarations already do checks that apply to all instances of a class. For example the type of an Attr is enforced.

But you might need to do per instance specific validation.

Let’s say that for the class below, we want to enforce that labels[::-1] == reversed_labels

class A(HasTraits):
    labels = NArray(dtype='S64')
    reversed_labels = NArray(dtype='S64')

We can override the validate method and do the checks in it. Validate is not automatically called, but users of the DataType are encouraged to call it once they are done with populating the instance.

class A(HasTraits):
    labels = NArray(dtype='S64')
    reversed_labels = NArray(dtype='S64')

    def validate(self):
        super(A, self).validate()
        if (self.labels[::-1] != self.reversed_labels).any():
            raise ValueError

>>> a = A()
>>> a.labels = numpy.array(['a', 'b'])
>>> a.reversed_labels = numpy.array(['a', 'b'])
>>> a.validate()
ValueError

This late optional validation might not be a good fit. If you want an error as soon as you assign a bad array then you can promote reversed_labels from a declarative attribute to a declarative property.

class A(HasTraits):
    labels = NArray(dtype='S64')

    def __init__(self, **kwargs):
        super(A, self).__init__(**kwargs)
        self._reversed = None

    @trait_property(NArray(dtype='S64'))
    def reversed_labels(self):
        return self._reversed

    @reversed_labels.setter
    def reversed_labels(self, value):
        if (value[::-1] != self.labels).any():
            raise ValueError
        self._reversed = value

    >>> a = A()
    >>> a.labels = numpy.array(['a', 'b'])
    >>> a.reversed_labels = numpy.array(['a', 'b'])
    >>> a.validate()
    ValueError

Attribute reference

class tvb.basic.neotraits.api.Attr(field_type: type, default: Any | None = None, doc: str = '', label: str = '', required: bool = True, final: bool = False, choices: tuple | None = None)[source]

An Attr declares the following about the attribute it describes: * the type * a default value shared by all instances * if the value might be missing * documentation It will resolve to attributes on the instance.

__init__(field_type: type, default: Any | None = None, doc: str = '', label: str = '', required: bool = True, final: bool = False, choices: tuple | None = None) None[source]
Parameters:
  • field_type – the python type of this attribute

  • default – A shared default value. Behaves like class level attribute assignment. Take care with mutable defaults.

  • doc – Documentation for this field.

  • label – A short description.

  • required – required fields should not be None.

  • final – Final fields can only be assigned once.

  • choices – A tuple of the values that this field is allowed to take.

class tvb.basic.neotraits.api.NArray(default: ~numpy.ndarray | None = None, required: bool = True, doc: str = '', label: str = '', dtype: ~numpy.dtype | type | str = <class 'numpy.float64'>, shape: ~typing.Tuple[~tvb.basic.neotraits._attr.Dim, ...] | None = None, dim_names: ~typing.Tuple[str, ...] = (), domain: ~typing.Container | None = None)[source]

Declares a numpy array. dtype enforces the dtype. The default dtype is float64. An optional symbolic shape can be given, as a tuple of Dim attributes from the owning class. The shape will be enforced, but no broadcasting will be done. domain declares what values are allowed in this array. It can be any object that can be checked for membership Defaults are checked if they are in the declared domain. For performance reasons this does not happen on every attribute set.

__init__(default: ~numpy.ndarray | None = None, required: bool = True, doc: str = '', label: str = '', dtype: ~numpy.dtype | type | str = <class 'numpy.float64'>, shape: ~typing.Tuple[~tvb.basic.neotraits._attr.Dim, ...] | None = None, dim_names: ~typing.Tuple[str, ...] = (), domain: ~typing.Container | None = None) None[source]
Parameters:
  • dtype – The numpy datatype. Defaults to float64. This is checked by neotraits.

  • shape – An optional symbolic shape, tuple of Dim’s declared on the owning class

  • dim_names – Optional names for the names of the dimensions

  • domain – Any type that can be checked for membership like xrange. Represents the expected domain of the values in the array.

class tvb.basic.neotraits.api.Final(default=None, field_type=None, doc='', label='')[source]

An attribute that can only be set once. If a default is provided it counts as a set, so it cannot be written to. Note that if the default is a mutable type, the value is shared with all instances of the owning class. We cannot enforce true constancy in python

__init__(default=None, field_type=None, doc='', label='')[source]
Parameters:

default – The constant value

class tvb.basic.neotraits.api.Dim(doc='')[source]

A symbol that defines a dimension in a numpy array shape. It can only be set once. It is an int. Dimensions have to be set before any NArrays that reference them are used.

__init__(doc='')[source]
Parameters:

default – The constant value

class tvb.basic.neotraits.api.Int(field_type=<class 'int'>, default=0, doc='', label='', required=True, final=False, choices=None)[source]

Declares an integer This is different from Attr(field_type=int). The former enforces int subtypes This allows all integer types, including numpy ones that can be safely cast to the declared type according to numpy rules

__init__(field_type=<class 'int'>, default=0, doc='', label='', required=True, final=False, choices=None)[source]
Parameters:
  • field_type – the python type of this attribute

  • default – A shared default value. Behaves like class level attribute assignment. Take care with mutable defaults.

  • doc – Documentation for this field.

  • label – A short description.

  • required – required fields should not be None.

  • final – Final fields can only be assigned once.

  • choices – A tuple of the values that this field is allowed to take.

class tvb.basic.neotraits.api.Float(field_type=<class 'float'>, default=0, doc='', label='', required=True, final=False, choices=None)[source]

Declares a float. This is different from Attr(field_type=float). The former enforces float subtypes. This allows any type that can be safely cast to the declared float type according to numpy rules.

Reading and writing this attribute is slower than a plain python attribute. In performance sensitive code you might want to use plain python attributes or even better local variables.

__init__(field_type=<class 'float'>, default=0, doc='', label='', required=True, final=False, choices=None)[source]
Parameters:
  • field_type – the python type of this attribute

  • default – A shared default value. Behaves like class level attribute assignment. Take care with mutable defaults.

  • doc – Documentation for this field.

  • label – A short description.

  • required – required fields should not be None.

  • final – Final fields can only be assigned once.

  • choices – A tuple of the values that this field is allowed to take.

class tvb.basic.neotraits.api.List(of: type = <class 'object'>, default: tuple = (), doc: str = '', label: str = '', final: bool = False, choices: tuple | None = None)[source]

The attribute is a list of values. Choices and type are reinterpreted as applying not to the list but to the elements of it

__init__(of: type = <class 'object'>, default: tuple = (), doc: str = '', label: str = '', final: bool = False, choices: tuple | None = None) None[source]
Parameters:
  • field_type – the python type of this attribute

  • default – A shared default value. Behaves like class level attribute assignment. Take care with mutable defaults.

  • doc – Documentation for this field.

  • label – A short description.

  • required – required fields should not be None.

  • final – Final fields can only be assigned once.

  • choices – A tuple of the values that this field is allowed to take.

Reference

class tvb.basic.neotraits.api.HasTraits(**kwargs)[source]

Traited class [tvb.basic.neotraits._core.HasTraits]

Attributes declared

gid : tvb.basic.neotraits._core.HasTraits.gid = Attr(field_type=<class ‘uuid.UUID’>, default=None, required=True)

TYPES_TO_DEEPCOPY = (<class 'numpy.random.mtrand.RandomState'>, <class 'scipy.sparse._csc.csc_matrix'>, <class 'scipy.sparse._base.spmatrix'>, <class 'list'>, <class 'tuple'>)
configure(*args, **kwargs)[source]

Ensures that invariant of the class are satisfied. Override to compute uninitialized state of the class.

duplicate()[source]
gid

gid identifies a specific instance of the hastraits it is used by serializers as an identifier. For non-datatype HasTraits this is less usefull but still provides a unique id for example for a model configuration

set_title()[source]
summary_info() Dict[str, str][source]

A more structured __str__ A 2 column table represented as a dict of str->str The default __str__ and html representations of this object are derived from this table. Override this method and return such a table filled with instance information that informs the user about your instance

tag(tag_name: str, tag_value: str | None = None) None[source]

Add a tag to this trait instance. The tags are for user to recognize and categorize the instances They should never influence the behaviour of the program :param tag_name: an arbitrary tag :param tag_value: an optional tag value

tags

a generic collections of tags. The trait system is not using them nor should any other code. They should not alter behaviour They should describe the instance for the user

validate()[source]

Check that the internal invariants of this class are satisfied. Not meant to ensure that that is the case. Use configure for that. The default configure calls this before it returns. It complains about missing required attrs Can be overridden in subclasses

tvb.basic.neotraits.api.trait_property(attr: Attr) Callable[[Callable], TraitProperty][source]

A read only property that has a declarative attribute associated with. :param attr: the declarative attribute that describes this property

tvb.basic.neotraits.api.cached_trait_property(attr: Attr) Callable[[Callable], CachedTraitProperty][source]

A lazy evaluated attribute. Transforms the decorated method into a cached property. The method will be called once to compute a value. The value will be stored in an instance attribute with the same name as the decorated function. :param attr: the declarative attribute that describes this property

class tvb.basic.neotraits.api.Range(lo, hi, step=1.0)[source]

Defines a domain like the one that numpy.arange generates Points are precisely equidistant but the largest point is <= hi

class tvb.basic.neotraits.api.LinspaceRange(lo, hi, npoints=50)[source]

Defines a domain with precise endpoints but the points are not precisely equidistant Similar to numpy.linspace

tvb.basic.neotraits.api.narray_describe(ar: ndarray) str[source]
tvb.basic.neotraits.api.narray_summary_info(ar: ndarray, ar_name: str = '', condensed: bool = False) Dict[str, str][source]

A 2 column table represented as a dict of str->str

exception tvb.basic.neotraits.ex.TraitAttributeError(msg='', trait=None, attr=None)[source]
exception tvb.basic.neotraits.ex.TraitError(msg='', trait=None, attr=None)[source]
exception tvb.basic.neotraits.ex.TraitFinalAttributeError(msg='', trait=None, attr=None)[source]
exception tvb.basic.neotraits.ex.TraitTypeError(msg='', trait=None, attr=None)[source]
exception tvb.basic.neotraits.ex.TraitValueError(msg='', trait=None, attr=None)[source]