Coverage for sources/accretive/objects.py: 100%
60 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 04:28 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 04:28 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
4#============================================================================#
5# #
6# Licensed under the Apache License, Version 2.0 (the "License"); #
7# you may not use this file except in compliance with the License. #
8# You may obtain a copy of the License at #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
12# Unless required by applicable law or agreed to in writing, software #
13# distributed under the License is distributed on an "AS IS" BASIS, #
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15# See the License for the specific language governing permissions and #
16# limitations under the License. #
17# #
18#============================================================================#
21# ruff: noqa: F811
24# pylint: disable=line-too-long
25''' Accretive objects.
27 Provides the base class for objects with accretive attributes. Once an
28 attribute is set on an instance, it cannot be reassigned or deleted.
30 >>> from accretive import Object
31 >>> obj = Object( )
32 >>> obj.x = 1 # Add new instance attribute
33 >>> obj.y = 2 # Add another instance attribute
34 >>> obj.x = 3 # Attempt modification
35 Traceback (most recent call last):
36 ...
37 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete existing attribute 'x'.
39 The `accretive` decorator can be used to make any class accretive:
41 >>> from accretive import accretive
42 >>> @accretive
43 ... class Config:
44 ... def __init__( self, debug = False ):
45 ... self.debug = debug
46 ...
47 >>> config = Config( debug = True )
48 >>> config.debug # Access existing attribute
49 True
50 >>> config.verbose = True # Add new attribute
51 >>> config.debug = False # Attempt to modify existing attribute
52 Traceback (most recent call last):
53 ...
54 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete existing attribute 'debug'.
55'''
56# pylint: enable=line-too-long
59from . import __
62_behavior = 'accretion'
65def _check_behavior( obj: object ) -> bool:
66 behaviors: __.cabc.MutableSet[ str ]
67 if _check_dict( obj ):
68 attributes = getattr( obj, '__dict__' )
69 behaviors = attributes.get( '_behaviors_', set( ) )
70 else: behaviors = getattr( obj, '_behaviors_', set( ) )
71 return _behavior in behaviors
74def _check_dict( obj: object ) -> bool:
75 # Return False even if '__dict__' in '__slots__'.
76 if hasattr( obj, '__slots__' ): return False
77 return hasattr( obj, '__dict__' )
80@__.typx.overload
81def accretive( # pragma: no branch
82 class_: type[ __.C ], *,
83 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
84 mutables: __.cabc.Collection[ str ] = ( )
85) -> type[ __.C ]: ...
88@__.typx.overload
89def accretive( # pragma: no branch
90 class_: __.AbsentSingleton, *,
91 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
92 mutables: __.cabc.Collection[ str ] = ( )
93) -> __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]: ...
96def accretive( # pylint: disable=too-complex,too-many-statements
97 class_: __.Absential[ type[ __.C ] ] = __.absent, *,
98 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
99 mutables: __.cabc.Collection[ str ] = ( )
100) -> __.typx.Union[
101 type[ __.C ], __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]
102]:
103 ''' Decorator which makes class accretive after initialization.
105 Cannot be applied to classes which define their own __setattr__
106 or __delattr__ methods.
108 This decorator can be used in different ways:
110 1. Simple decorator:
112 >>> @accretive
113 ... class Config:
114 ... pass
116 2. With parameters:
118 >>> @accretive( mutables = ( 'version', ) )
119 ... class Config:
120 ... pass
121 '''
122 def decorator( cls: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-many-statements
123 if not __.is_absent( docstring ): cls.__doc__ = docstring
124 for method in ( '__setattr__', '__delattr__' ):
125 if method in cls.__dict__:
126 from .exceptions import DecoratorCompatibilityError
127 raise DecoratorCompatibilityError( cls.__name__, method )
128 original_init = next(
129 base.__dict__[ '__init__' ] for base in cls.__mro__
130 if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison
131 mutables_ = frozenset( mutables )
133 def __init__(
134 self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any
135 ) -> None:
136 original_init( self, *posargs, **nomargs )
137 behaviors: __.cabc.MutableSet[ str ]
138 if _check_dict( self ):
139 attributes = getattr( self, '__dict__' )
140 behaviors = attributes.get( '_behaviors_', set( ) )
141 if not behaviors: attributes[ '_behaviors_' ] = behaviors
142 else:
143 behaviors = getattr( self, '_behaviors_', set( ) )
144 if not behaviors: setattr( self, '_behaviors_', behaviors )
145 behaviors.add( _behavior )
147 def __delattr__( self: object, name: str ) -> None:
148 if name in mutables_:
149 super( cls, self ).__delattr__( name )
150 return
151 if _check_behavior( self ): # pragma: no branch
152 from .exceptions import AttributeImmutabilityError
153 raise AttributeImmutabilityError( name )
154 super( cls, self ).__delattr__( name ) # pragma: no cover
156 def __setattr__( self: object, name: str, value: __.typx.Any ) -> None:
157 if name in mutables_:
158 super( cls, self ).__setattr__( name, value )
159 return
160 if _check_behavior( self ) and hasattr( self, name ):
161 from .exceptions import AttributeImmutabilityError
162 raise AttributeImmutabilityError( name )
163 super( cls, self ).__setattr__( name, value )
165 cls.__init__ = __init__
166 cls.__delattr__ = __delattr__
167 cls.__setattr__ = __setattr__
168 return cls
170 if not __.is_absent( class_ ): return decorator( class_ )
171 return decorator # No class to decorate; keyword arguments only.
174@accretive
175class Object:
176 ''' Accretive objects. '''
178 __slots__ = ( '__dict__', '_behaviors_' )
180 def __repr__( self ) -> str:
181 return "{fqname}( )".format( fqname = __.calculate_fqname( self ) )
183Object.__doc__ = __.generate_docstring(
184 Object, 'instance attributes accretion' )