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