Coverage for sources/frigid/objects.py: 100%

60 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-05 02:47 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

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#============================================================================# 

19 

20 

21# ruff: noqa: F811 

22 

23 

24# pylint: disable=line-too-long 

25''' Immutable objects. 

26 

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. 

30 

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'. 

47 

48 The `immutable` decorator can be used to make any class immutable: 

49 

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 

63 

64 

65from . import __ 

66 

67 

68_behavior = 'immutability' 

69 

70 

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 

78 

79 

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__' ) 

84 

85 

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 ]: ... 

92 

93 

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 ] ]: ... 

100 

101 

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. 

110 

111 Cannot be applied to classes which define their own __setattr__ 

112 or __delattr__ methods. 

113 

114 This decorator can be used in different ways: 

115 

116 1. Simple decorator: 

117 

118 >>> @immutable 

119 ... class Config: 

120 ... pass 

121 

122 2. With parameters: 

123 

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 ) 

138 

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 ) 

152 

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 

161 

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 ) 

170 

171 cls.__init__ = __init__ 

172 cls.__delattr__ = __delattr__ 

173 cls.__setattr__ = __setattr__ 

174 return cls 

175 

176 if not __.is_absent( class_ ): return decorator( class_ ) 

177 return decorator # No class to decorate; keyword arguments only. 

178 

179 

180@immutable 

181class Object: 

182 ''' Immutable objects. ''' 

183 

184 __slots__ = ( '__dict__', '_behaviors_' ) 

185 

186 def __repr__( self ) -> str: 

187 return "{fqname}( )".format( fqname = __.calculate_fqname( self ) ) 

188 

189Object.__doc__ = __.generate_docstring( 

190 Object, 'instance attributes immutability' )