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

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

38 

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

40 

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 

57 

58 

59from . import __ 

60 

61 

62_behavior = 'accretion' 

63 

64 

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 

72 

73 

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

78 

79 

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

86 

87 

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

94 

95 

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. 

104 

105 Cannot be applied to classes which define their own __setattr__ 

106 or __delattr__ methods. 

107 

108 This decorator can be used in different ways: 

109 

110 1. Simple decorator: 

111 

112 >>> @accretive 

113 ... class Config: 

114 ... pass 

115 

116 2. With parameters: 

117 

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 ) 

132 

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 ) 

146 

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 

155 

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 ) 

164 

165 cls.__init__ = __init__ 

166 cls.__delattr__ = __delattr__ 

167 cls.__setattr__ = __setattr__ 

168 return cls 

169 

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

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

172 

173 

174@accretive 

175class Object: 

176 ''' Accretive objects. ''' 

177 

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

179 

180 def __repr__( self ) -> str: 

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

182 

183Object.__doc__ = __.generate_docstring( 

184 Object, 'instance attributes accretion' )