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

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

26 

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. 

29 

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: 

33 

34 * Configuration objects 

35 * Plugin interfaces 

36 * Immutable data containers 

37 * Objects requiring attribute stability 

38 

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

47 

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

49 

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 

66 

67 

68from . import __ 

69 

70 

71_behavior = 'accretion' 

72 

73 

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 

81 

82 

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

87 

88 

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

95 

96 

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

103 

104 

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. 

113 

114 Cannot be applied to classes which define their own __setattr__ 

115 or __delattr__ methods. 

116 

117 This decorator can be used in different ways: 

118 

119 1. Simple decorator: 

120 

121 >>> @accretive 

122 ... class Config: 

123 ... pass 

124 

125 2. With parameters: 

126 

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 ) 

141 

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 ) 

155 

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 

164 

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 ) 

173 

174 cls.__init__ = __init__ 

175 cls.__delattr__ = __delattr__ 

176 cls.__setattr__ = __setattr__ 

177 return cls 

178 

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

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

181 

182 

183@accretive 

184class Object: 

185 ''' Accretive objects. ''' 

186 

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

188 

189 def __repr__( self ) -> str: 

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

191 

192Object.__doc__ = __.generate_docstring( 

193 Object, 'instance attributes accretion' )