Coverage for sources/accretive/objects.py: 100%
60 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-01 20:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-01 20:09 +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 The implementation uses a special dictionary type for attribute storage
31 that enforces the accretive behavior. This makes it suitable as a base
32 class for:
34 * Configuration objects
35 * Plugin interfaces
36 * Immutable data containers
37 * Objects requiring attribute stability
39 >>> from accretive import Object
40 >>> obj = Object( )
41 >>> obj.x = 1 # Add new instance attribute
42 >>> obj.y = 2 # Add another instance attribute
43 >>> obj.x = 3 # Attempt modification
44 Traceback (most recent call last):
45 ...
46 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete existing attribute 'x'.
48 The `accretive` decorator can be used to make any class accretive:
50 >>> from accretive import accretive
51 >>> @accretive
52 ... class Config:
53 ... def __init__( self, debug = False ):
54 ... self.debug = debug
55 ...
56 >>> config = Config( debug = True )
57 >>> config.debug # Access existing attribute
58 True
59 >>> config.verbose = True # Add new attribute
60 >>> config.debug = False # Attempt to modify existing attribute
61 Traceback (most recent call last):
62 ...
63 accretive.exceptions.AttributeImmutabilityError: Cannot reassign or delete existing attribute 'debug'.
64'''
65# pylint: enable=line-too-long
68from . import __
71_behavior = 'accretion'
74def _check_behavior( obj: object ) -> bool:
75 behaviors: __.cabc.MutableSet[ str ]
76 if _check_dict( obj ):
77 attributes = getattr( obj, '__dict__' )
78 behaviors = attributes.get( '_behaviors_', set( ) )
79 else: behaviors = getattr( obj, '_behaviors_', set( ) )
80 return _behavior in behaviors
83def _check_dict( obj: object ) -> bool:
84 # Return False even if '__dict__' in '__slots__'.
85 if hasattr( obj, '__slots__' ): return False
86 return hasattr( obj, '__dict__' )
89@__.typx.overload
90def accretive( # pragma: no branch
91 class_: type[ __.C ], *,
92 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
93 mutables: __.cabc.Collection[ str ] = ( )
94) -> type[ __.C ]: ...
97@__.typx.overload
98def accretive( # pragma: no branch
99 class_: __.AbsentSingleton, *,
100 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
101 mutables: __.cabc.Collection[ str ] = ( )
102) -> __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]: ...
105def accretive( # pylint: disable=too-complex,too-many-statements
106 class_: __.Absential[ type[ __.C ] ] = __.absent, *,
107 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
108 mutables: __.cabc.Collection[ str ] = ( )
109) -> __.typx.Union[
110 type[ __.C ], __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]
111]:
112 ''' Decorator which makes class accretive after initialization.
114 Cannot be applied to classes which define their own __setattr__
115 or __delattr__ methods.
117 This decorator can be used in different ways:
119 1. Simple decorator:
121 >>> @accretive
122 ... class Config:
123 ... pass
125 2. With parameters:
127 >>> @accretive( mutables = ( 'version', ) )
128 ... class Config:
129 ... pass
130 '''
131 def decorator( cls: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-many-statements
132 if not __.is_absent( docstring ): cls.__doc__ = docstring
133 for method in ( '__setattr__', '__delattr__' ):
134 if method in cls.__dict__:
135 from .exceptions import DecoratorCompatibilityError
136 raise DecoratorCompatibilityError( cls.__name__, method )
137 original_init = next(
138 base.__dict__[ '__init__' ] for base in cls.__mro__
139 if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison
140 mutables_ = frozenset( mutables )
142 def __init__(
143 self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any
144 ) -> None:
145 original_init( self, *posargs, **nomargs )
146 behaviors: __.cabc.MutableSet[ str ]
147 if _check_dict( self ):
148 attributes = getattr( self, '__dict__' )
149 behaviors = attributes.get( '_behaviors_', set( ) )
150 if not behaviors: attributes[ '_behaviors_' ] = behaviors
151 else:
152 behaviors = getattr( self, '_behaviors_', set( ) )
153 if not behaviors: setattr( self, '_behaviors_', behaviors )
154 behaviors.add( _behavior )
156 def __delattr__( self: object, name: str ) -> None:
157 if name in mutables_:
158 super( cls, self ).__delattr__( name )
159 return
160 if _check_behavior( self ): # pragma: no branch
161 from .exceptions import AttributeImmutabilityError
162 raise AttributeImmutabilityError( name )
163 super( cls, self ).__delattr__( name ) # pragma: no cover
165 def __setattr__( self: object, name: str, value: __.typx.Any ) -> None:
166 if name in mutables_:
167 super( cls, self ).__setattr__( name, value )
168 return
169 if _check_behavior( self ) and hasattr( self, name ):
170 from .exceptions import AttributeImmutabilityError
171 raise AttributeImmutabilityError( name )
172 super( cls, self ).__setattr__( name, value )
174 cls.__init__ = __init__
175 cls.__delattr__ = __delattr__
176 cls.__setattr__ = __setattr__
177 return cls
179 if not __.is_absent( class_ ): return decorator( class_ )
180 return decorator # No class to decorate; keyword arguments only.
183@accretive
184class Object:
185 ''' Accretive objects. '''
187 __slots__ = ( '__dict__', '_behaviors_' )
189 def __repr__( self ) -> str:
190 return "{fqname}( )".format( fqname = __.calculate_fqname( self ) )
192Object.__doc__ = __.generate_docstring(
193 Object, 'instance attributes accretion' )