Standard Behaviors

Mutability

Mutability can be customized for both the attributes of a class and the attributes of its instances. This is done by supplying an argument which is a sequence of mutability verifiers. These verifiers may be attribute names, compiled regular expressions, or predicates. Attribute names are strings; compiled regular expressions are re.Pattern objects which match attribute names; predicates evaluate an attribute name argument. Also, normal Python mutability can be granted to a class. This is done by supplying the omnimutability marker ('*') as an argument instead of a sequence.

Attribute names are gathered into a set and checked first. Predicates are gathered into a sequence, matching the order in which they were supplied; they are checked if nothing in the attribute names set matches. Pattern objects are gathered into a sequence, matching the order in which they were supplied; they are checked if no predicate matches.

>>> import dataclasses
>>> import classcore.standard as ccstd

Argument Names

The class_mutables metaclass argument controls mutability of attributes on the produced class itself.

>>> class Point2d( ccstd.DataclassObject, class_mutables = '*' ):
...     x: float
...     y: float
...
>>> Point2d.x, Point2d.y = 3, 4
>>> Point2d.x, Point2d.y
(3, 4)
>>> del Point2d.x

The instances_mutables metaclass argument controls mutablity of attributes on instances of the produced class.

>>> class Point2d( ccstd.DataclassObject, instances_mutables = '*' ):
...     x: float
...     y: float
...
>>> point = Point2d( x = 3, y = 4 )
>>> point.x, point.y
(3, 4)
>>> point.x, point.y = 5, 12
>>> point.x, point.y
(5, 12)
>>> del point.x

The mutables decorator factory argument controls mutability of attributes on instances of the decorated class.

>>> @ccstd.dataclass_with_standard_behaviors( mutables = '*' )
... class Point2d:
...     x: float
...     y: float
...
>>> point = Point2d( x = 5, y = 12 )
>>> point.x, point.y
(5, 12)
>>> point.x, point.y = 8, 15
>>> point.x, point.y
(8, 15)
>>> del point.x

Selective Mutability

Explicit attribute names for selective mutability:

>>> @ccstd.dataclass_with_standard_behaviors( mutables = ( 'x', 'y' ) )
... class Point2d:
...     x: float
...     y: float
...
>>> point = Point2d( x = 8, y = 15 )
>>> point.x, point.y = 7, 24
>>> point.x, point.y
(7, 24)
>>> del point.x
>>> point.__slots__ = ( )
Traceback (most recent call last):
...
classcore.exceptions.AttributeImmutability: Could not assign or delete attribute '__slots__' on instance of class ...

With a regular expression in the mix:

>>> import re
>>> regex = re.compile( r'''cache_.*''' )
>>> @ccstd.dataclass_with_standard_behaviors( mutables = ( 'x', 'y', regex ) )
... class Point2d:
...     x: float
...     y: float
...     cache_area: float = dataclasses.field( init = False )
...     cache_hypotenuse: float = dataclasses.field( init = False )
...
>>> point = Point2d( x = 7, y = 24 )
>>> point.x, point.y = 20, 21
>>> point.x, point.y
(20, 21)
>>> point.cache_hypotenuse = 29
>>> del point.cache_hypotenuse
>>> point.__slots__ = ( )
Traceback (most recent call last):
...
classcore.exceptions.AttributeImmutability: Could not assign or delete attribute '__slots__' on instance of class ...
>>> del point.__annotations__
Traceback (most recent call last):
...
classcore.exceptions.AttributeImmutability: Could not assign or delete attribute '__annotations__' on instance of class ...

Or with a predicate:

>>> def predicate( name: str ) -> bool:
...     return not name.startswith( '_' ) or name.startswith( 'cache_' )
...
>>> @ccstd.dataclass_with_standard_behaviors( mutables = ( predicate, ) )
... class Point2d:
...     x: float
...     y: float
...     cache_area: float = dataclasses.field( init = False )
...     cache_hypotenuse: float = dataclasses.field( init = False )
...
>>> point = Point2d( x = 20, y = 21 )
>>> point.x, point.y = 12, 35
>>> point.x, point.y
(12, 35)
>>> point.cache_hypotenuse = 37
>>> del point.cache_hypotenuse
>>> point.__slots__ = ( )
Traceback (most recent call last):
...
classcore.exceptions.AttributeImmutability: Could not assign or delete attribute '__slots__' on instance of class ...
>>> del point.__annotations__
Traceback (most recent call last):
...
classcore.exceptions.AttributeImmutability: Could not assign or delete attribute '__annotations__' on instance of class ...

Invalid mutability verifiers will cause an error to be raised:

>>> @ccstd.with_standard_behaviors( mutables = ( 13, ) )
... class C: pass
...
Traceback (most recent call last):
...
classcore.exceptions.BehaviorExclusionInvalidity: Invalid behavior exclusion verifier: 13

Inheritance

Classes inherit and merge mutability from their bases.

>>> @ccstd.dataclass_with_standard_behaviors( mutables = ( 'x', 'y' ) )
... class Point2d:
...     x: float
...     y: float
...
>>> @ccstd.dataclass_with_standard_behaviors( mutables = ( 'z', ) )
... class Point3d( Point2d ):
...     z: float
...
>>> point3 = Point3d( x = 12, y = 35, z = 47 )
>>> point3.x, point3.y, point3.z = 9, 40, 49
>>> point3.x, point3.y, point3.z
(9, 40, 49)

Omnimutability is also inherited; it short-circuits all other mutablity evaluations.

>>> @ccstd.dataclass_with_standard_behaviors( mutables = '*' )
... class Point2d:
...     x: float
...     y: float
...
>>> @ccstd.dataclass_with_standard_behaviors( mutables = ( 'z', ) )
... class Point3d( Point2d ):
...     z: float
...
>>> point3 = Point3d( x = 9, y = 40, z = 49 )
>>> point3.x, point3.y, point3.z = 28, 45, 73
>>> point3.x, point3.y, point3.z
(28, 45, 73)

Visibility

Visibility can be customized for both the attributes of a class and the attributes of its instances. This is done by supplying an argument which is a sequence of visibility verifiers. These verifiers may be attribute names, compiled regular expressions, or predicates. Attribute names are strings; compiled regular expressions are re.Pattern objects which match attribute names; predicates evaluate an attribute name argument. Also, normal Python visibility can be granted to a class. This is done by supplying the omnivisibility marker ('*') as an argument instead of a sequence.

Attribute names are gathered into a set and checked first. Predicates are gathered into a sequence, matching the order in which they were supplied; they are checked if nothing in the attribute names set matches. Pattern objects are gathered into a sequence, matching the order in which they were supplied; they are checked if no predicate matches.

>>> import classcore.standard as ccstd

Argument Names

The class_visibles metaclass argument controls visibility of attributes on the produced class itself.

>>> class Point2d( ccstd.DataclassObject, class_visibles = '*' ):
...     x: float
...     y: float
...
>>> '__annotations__' in dir( Point2d )
True

The instances_visibles metaclass argument controls visiblity of attributes on instances of the produced class.

>>> class Point2d( ccstd.DataclassObject, instances_visibles = '*' ):
...     x: float
...     y: float
...
>>> point = Point2d( x = 3, y = 4 )
>>> '__slots__' in dir( point )
True

The visibles decorator factory argument controls visibility of attributes on instances of the decorated class.

>>> @ccstd.dataclass_with_standard_behaviors( visibles = '*' )
... class Point2d:
...     x: float
...     y: float
...
>>> point = Point2d( x = 5, y = 12 )
>>> '__slots__' in dir( point )
True

Selective Visibility

Explicit attribute names for selective visibility:

>>> @ccstd.dataclass_with_standard_behaviors( visibles = ( 'x', '__slots__' ) )
... class Point2d:
...     x: float
...     y: float
...
>>> point = Point2d( x = 8, y = 15 )
>>> dir( point )
['__slots__', 'x']
>>> point.y
15

With a regular expression in the mix:

>>> import re
>>> regex = re.compile( r'''__dataclass_.*__''' )
>>> @ccstd.dataclass_with_standard_behaviors( visibles = ( 'x', 'y', regex ) )
... class Point2d:
...     x: float
...     y: float
...
>>> point = Point2d( x = 7, y = 24 )
>>> dir( point )
['__dataclass_fields__', '__dataclass_params__', 'x', 'y']

Or with a predicate:

>>> def predicate( name: str ) -> bool:
...     return not name.startswith( '_' ) or name.startswith( '__dataclass' )
...
>>> @ccstd.dataclass_with_standard_behaviors( visibles = ( predicate, ) )
... class Point2d:
...     x: float
...     y: float
...
>>> point = Point2d( x = 20, y = 21 )
>>> dir( point )
['__dataclass_fields__', '__dataclass_params__', 'x', 'y']

Invalid visibility verifiers will cause an error to be raised:

>>> @ccstd.with_standard_behaviors( visibles = ( 13, ) )
... class C: pass
...
Traceback (most recent call last):
...
classcore.exceptions.BehaviorExclusionInvalidity: Invalid behavior exclusion verifier: 13

Inheritance

Classes inherit and merge visibility from their bases.

>>> @ccstd.dataclass_with_standard_behaviors( visibles = ( '__slots__', ) )
... class Point3d( Point2d ):
...     z: float
...
>>> point3 = Point3d( x = 12, y = 35, z = 47 )
>>> dir( point3 )
['__dataclass_fields__', '__dataclass_params__', '__slots__', 'x', 'y', 'z']

Omnivisibility is also inherited; it short-circuits all other visiblity evaluations.

>>> @ccstd.dataclass_with_standard_behaviors( visibles = '*' )
... class Point2d:
...     x: float
...     y: float
...
>>> @ccstd.dataclass_with_standard_behaviors( visibles = ( 'z', ) )
... class Point3d( Point2d ):
...     z: float
...
>>> point3 = Point3d( x = 9, y = 40, z = 49 )
>>> '__slots__' in dir( point3 )
True

Inline Decoration

Class decorators often mutate the state of the classes which they decorate. If a class is immutable, then this can be problematic. Fortunately, there are several workarounds, depending on the scenario:

  • Apply mutating decorators before the standard behaviors decorator.

  • Supply mutating decorators to the standard behaviors decorator so that it can apply them inline before enforcing immutability.

>>> import abc
>>> import urllib.parse
>>> import typing_extensions as typx
>>> import classcore.standard as ccstd

For example, one can make a decorated protocol by stacking decorators:

>>> @ccstd.with_standard_behaviors( )
... @typx.runtime_checkable
... class FileAccessor( typx.Protocol ):
...     urlparts: urllib.parse.ParseResult
...     @abc.abstractmethod
...     async def acquire( self ) -> bytes: raise NotImplementedError
...     @abc.abstractmethod
...     async def update( self, content: bytes ) -> None: raise NotImplementedError

Or, by inline decoration:

>>> @ccstd.with_standard_behaviors( decorators = ( typx.runtime_checkable, ) )
... class FileAccessor( typx.Protocol ):
...     urlparts: urllib.parse.ParseResult
...     @abc.abstractmethod
...     async def acquire( self ) -> bytes: raise NotImplementedError
...     @abc.abstractmethod
...     async def update( self, content: bytes ) -> None: raise NotImplementedError

If a class is being produced from a standard behaviors metaclass, then there is no option to apply mutating decorators first, since class initialization would be complete by the time that are applied. In this case, mutating decorators must be supplied metaclass argument, so that they can be applied inline.

>>> class FileAccessor( ccstd.Protocol, typx.Protocol, decorators = ( typx.runtime_checkable, ) ):
...     urlparts: urllib.parse.ParseResult
...     @abc.abstractmethod
...     async def acquire( self ) -> bytes: raise NotImplementedError
...     @abc.abstractmethod
...     async def update( self, content: bytes ) -> None: raise NotImplementedError