Coverage for sources/frigid/objects.py: 100%
60 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 03:33 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-05 03:33 +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''' Immutable objects.
27 Provides a base class and decorator for creating objects with immutable
28 attributes. Once an object is initialized, its attributes cannot be modified
29 or deleted.
31 >>> from frigid import Object
32 >>> class Point( Object ):
33 ... def __init__( self, x, y ):
34 ... self.x = x
35 ... self.y = y
36 ... super( ).__init__( )
37 ...
38 >>> obj = Point( 1, 2 ) # Initialize with attributes
39 >>> obj.z = 3 # Attempt to add attribute
40 Traceback (most recent call last):
41 ...
42 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'z'.
43 >>> obj.x = 4 # Attempt modification
44 Traceback (most recent call last):
45 ...
46 frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.
48 The `immutable` decorator can be used to make any class immutable:
50 >>> from frigid import immutable
51 >>> @immutable
52 ... class Config:
53 ... def __init__( self, verbose = False ):
54 ... self.verbose = verbose
55 ...
56 >>> config = Config( verbose = True )
57 >>> config.verbose = False # Attempt to modify attribute
58 Traceback (most recent call last):
59 ...
60 frigid.exceptions.AttributeImmutabilityError: ...
61'''
62# pylint: enable=line-too-long
65from . import __
68_behavior = 'immutability'
71def _check_behavior( obj: object ) -> bool:
72 behaviors: __.cabc.MutableSet[ str ]
73 if _check_dict( obj ):
74 attributes = getattr( obj, '__dict__' )
75 behaviors = attributes.get( '_behaviors_', set( ) )
76 else: behaviors = getattr( obj, '_behaviors_', set( ) )
77 return _behavior in behaviors
80def _check_dict( obj: object ) -> bool:
81 # Return False even if '__dict__' in '__slots__'.
82 if hasattr( obj, '__slots__' ): return False
83 return hasattr( obj, '__dict__' )
86@__.typx.overload
87def immutable( # pragma: no branch
88 class_: type[ __.C ], *,
89 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
90 mutables: __.cabc.Collection[ str ] = ( )
91) -> type[ __.C ]: ...
94@__.typx.overload
95def immutable( # pragma: no branch
96 class_: __.AbsentSingleton, *,
97 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
98 mutables: __.cabc.Collection[ str ] = ( )
99) -> __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]: ...
102def immutable( # pylint: disable=too-complex,too-many-statements
103 class_: __.Absential[ type[ __.C ] ] = __.absent, *,
104 docstring: __.Absential[ __.typx.Optional[ str ] ] = __.absent,
105 mutables: __.cabc.Collection[ str ] = ( )
106) -> __.typx.Union[
107 type[ __.C ], __.typx.Callable[ [ type[ __.C ] ], type[ __.C ] ]
108]:
109 ''' Decorator which makes class immutable after initialization.
111 Cannot be applied to classes which define their own __setattr__
112 or __delattr__ methods.
114 This decorator can be used in different ways:
116 1. Simple decorator:
118 >>> @immutable
119 ... class Config:
120 ... pass
122 2. With parameters:
124 >>> @immutable( mutables = ( 'version', ) )
125 ... class Config:
126 ... pass
127 '''
128 def decorator( cls: type[ __.C ] ) -> type[ __.C ]: # pylint: disable=too-many-statements
129 if not __.is_absent( docstring ): cls.__doc__ = docstring
130 for method in ( '__setattr__', '__delattr__' ):
131 if method in cls.__dict__:
132 from .exceptions import DecoratorCompatibilityError
133 raise DecoratorCompatibilityError( cls.__name__, method )
134 original_init = next(
135 base.__dict__[ '__init__' ] for base in cls.__mro__
136 if '__init__' in base.__dict__ ) # pylint: disable=magic-value-comparison
137 mutables_ = frozenset( mutables )
139 def __init__(
140 self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any
141 ) -> None:
142 original_init( self, *posargs, **nomargs )
143 behaviors: __.cabc.MutableSet[ str ]
144 if _check_dict( self ):
145 attributes = getattr( self, '__dict__' )
146 behaviors = attributes.get( '_behaviors_', set( ) )
147 if not behaviors: attributes[ '_behaviors_' ] = behaviors
148 else:
149 behaviors = getattr( self, '_behaviors_', set( ) )
150 if not behaviors: setattr( self, '_behaviors_', behaviors )
151 behaviors.add( _behavior )
153 def __delattr__( self: object, name: str ) -> None:
154 if name in mutables_:
155 super( cls, self ).__delattr__( name )
156 return
157 if _check_behavior( self ): # pragma: no branch
158 from .exceptions import AttributeImmutabilityError
159 raise AttributeImmutabilityError( name )
160 super( cls, self ).__delattr__( name ) # pragma: no cover
162 def __setattr__( self: object, name: str, value: __.typx.Any ) -> None:
163 if name in mutables_:
164 super( cls, self ).__setattr__( name, value )
165 return
166 if _check_behavior( self ):
167 from .exceptions import AttributeImmutabilityError
168 raise AttributeImmutabilityError( name )
169 super( cls, self ).__setattr__( name, value )
171 cls.__init__ = __init__
172 cls.__delattr__ = __delattr__
173 cls.__setattr__ = __setattr__
174 return cls
176 if not __.is_absent( class_ ): return decorator( class_ )
177 return decorator # No class to decorate; keyword arguments only.
180@immutable
181class Object:
182 ''' Immutable objects. '''
184 __slots__ = ( '__dict__', '_behaviors_' )
186 def __repr__( self ) -> str:
187 return "{fqname}( )".format( fqname = __.calculate_fqname( self ) )
189Object.__doc__ = __.generate_docstring(
190 Object, 'instance attributes immutability' )