Coverage for sources/classcore/standard/behaviors.py: 100%

172 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-05 22:50 +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''' Implementations for standard behaviors. ''' 

22# TODO? Support introspection of PEP 593 annotations for behavior exclusions. 

23# Maybe enum for mutability and visibility. 

24 

25 

26from .. import utilities as _utilities 

27from . import __ 

28from . import nomina as _nomina 

29 

30 

31def assign_attribute_if_mutable( # noqa: PLR0913 

32 obj: object, /, *, 

33 ligation: _nomina.AssignerLigation, 

34 attributes_namer: _nomina.AttributesNamer, 

35 error_class_provider: _nomina.ErrorClassProvider, 

36 level: str, 

37 name: str, 

38 value: __.typx.Any, 

39) -> None: 

40 ''' Assigns attribute if it is mutable, else raises error. ''' 

41 leveli = 'instance' if level == 'instances' else level 

42 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

43 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) 

44 if _nomina.immutability_label not in behaviors: 

45 ligation( name, value ) 

46 return 

47 names_name = attributes_namer( level, 'mutables_names' ) 

48 names: _nomina.BehaviorExclusionNamesOmni = ( 

49 getattr( obj, names_name, frozenset( ) ) ) 

50 if names == '*' or name in names: 

51 ligation( name, value ) 

52 return 

53 predicates_name = attributes_namer( level, 'mutables_predicates' ) 

54 predicates: _nomina.BehaviorExclusionPredicates = ( 

55 getattr( obj, predicates_name, ( ) ) ) 

56 for predicate in predicates: 

57 if predicate( name ): 

58 # TODO? Cache predicate hit. 

59 ligation( name, value ) 

60 return 

61 regexes_name = attributes_namer( level, 'mutables_regexes' ) 

62 regexes: _nomina.BehaviorExclusionRegexes = ( 

63 getattr( obj, regexes_name, ( ) ) ) 

64 for regex in regexes: 

65 if regex.fullmatch( name ): 

66 # TODO? Cache regex hit. 

67 ligation( name, value ) 

68 return 

69 target = _utilities.describe_object( obj ) 

70 raise error_class_provider( 'AttributeImmutability' )( name, target ) 

71 

72 

73def delete_attribute_if_mutable( # noqa: PLR0913 

74 obj: object, /, *, 

75 ligation: _nomina.DeleterLigation, 

76 attributes_namer: _nomina.AttributesNamer, 

77 error_class_provider: _nomina.ErrorClassProvider, 

78 level: str, 

79 name: str, 

80) -> None: 

81 ''' Deletes attribute if it is mutable, else raises error. ''' 

82 leveli = 'instance' if level == 'instances' else level 

83 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

84 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) 

85 if _nomina.immutability_label not in behaviors: 

86 ligation( name ) 

87 return 

88 names_name = attributes_namer( level, 'mutables_names' ) 

89 names: _nomina.BehaviorExclusionNamesOmni = ( 

90 getattr( obj, names_name, frozenset( ) ) ) 

91 if names == '*' or name in names: 

92 ligation( name ) 

93 return 

94 predicates_name = attributes_namer( level, 'mutables_predicates' ) 

95 predicates: _nomina.BehaviorExclusionPredicates = ( 

96 getattr( obj, predicates_name, ( ) ) ) 

97 for predicate in predicates: 

98 if predicate( name ): 

99 # TODO? Cache predicate hit. 

100 ligation( name ) 

101 return 

102 regexes_name = attributes_namer( level, 'mutables_regexes' ) 

103 regexes: _nomina.BehaviorExclusionRegexes = ( 

104 getattr( obj, regexes_name, ( ) ) ) 

105 for regex in regexes: 

106 if regex.fullmatch( name ): 

107 # TODO? Cache regex hit. 

108 ligation( name ) 

109 return 

110 target = _utilities.describe_object( obj ) 

111 raise error_class_provider( 'AttributeImmutability' )( name, target ) 

112 

113 

114def survey_visible_attributes( 

115 obj: object, /, *, 

116 ligation: _nomina.SurveyorLigation, 

117 attributes_namer: _nomina.AttributesNamer, 

118 level: str, 

119) -> __.cabc.Iterable[ str ]: 

120 ''' Returns sequence of visible attributes. ''' 

121 names_base = ligation( ) 

122 leveli = 'instance' if level == 'instances' else level 

123 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

124 behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) 

125 if _nomina.concealment_label not in behaviors: return names_base 

126 names_name = attributes_namer( level, 'visibles_names' ) 

127 names: _nomina.BehaviorExclusionNamesOmni = ( 

128 getattr( obj, names_name, frozenset( ) ) ) 

129 if names == '*': return names_base # pragma: no branch 

130 regexes_name = attributes_namer( level, 'visibles_regexes' ) 

131 regexes: _nomina.BehaviorExclusionRegexes = ( 

132 getattr( obj, regexes_name, ( ) ) ) 

133 predicates_name = attributes_namer( level, 'visibles_predicates' ) 

134 predicates: _nomina.BehaviorExclusionPredicates = ( 

135 getattr( obj, predicates_name, ( ) ) ) 

136 names_: list[ str ] = [ ] 

137 for name in names_base: 

138 if name in names: 

139 names_.append( name ) 

140 continue 

141 for predicate in predicates: 

142 if predicate( name ): 

143 # TODO? Cache predicate hit. 

144 names_.append( name ) 

145 continue 

146 for regex in regexes: 

147 if regex.fullmatch( name ): 

148 # TODO? Cache regex hit. 

149 names_.append( name ) 

150 continue 

151 return names_ 

152 

153 

154def classify_behavior_exclusion_verifiers( 

155 verifiers: _nomina.BehaviorExclusionVerifiers 

156) -> tuple[ 

157 _nomina.BehaviorExclusionNames, 

158 _nomina.BehaviorExclusionRegexes, 

159 _nomina.BehaviorExclusionPredicates, 

160]: 

161 ''' Threshes sequence of behavior exclusion verifiers into bins. ''' 

162 names: set[ str ] = set( ) 

163 regexes: list[ __.re.Pattern[ str ] ] = [ ] 

164 predicates: list[ __.cabc.Callable[ ..., bool ] ] = [ ] 

165 for verifier in verifiers: 

166 if isinstance( verifier, str ): 

167 names.add( verifier ) 

168 elif isinstance( verifier, __.re.Pattern ): 

169 regexes.append( verifier ) 

170 elif callable( verifier ): 

171 predicates.append( verifier ) 

172 else: 

173 from ..exceptions import BehaviorExclusionInvalidity 

174 raise BehaviorExclusionInvalidity( verifier ) 

175 return frozenset( names ), tuple( regexes ), tuple( predicates ) 

176 

177 

178def produce_class_construction_preprocessor( 

179 attributes_namer: _nomina.AttributesNamer 

180) -> _nomina.ClassConstructionPreprocessor[ __.U ]: 

181 ''' Produces construction processor which handles metaclass arguments. ''' 

182 

183 def preprocess( # noqa: PLR0913 

184 clscls: type, 

185 name: str, 

186 bases: list[ type ], 

187 namespace: dict[ str, __.typx.Any ], 

188 arguments: dict[ str, __.typx.Any ], 

189 decorators: _nomina.DecoratorsMutable[ __.U ], 

190 ) -> None: 

191 record_class_construction_arguments( 

192 attributes_namer, namespace, arguments ) 

193 

194 return preprocess 

195 

196 

197def produce_class_construction_postprocessor( 

198 attributes_namer: _nomina.AttributesNamer 

199) -> _nomina.ClassConstructionPostprocessor[ __.U ]: 

200 ''' Produces construction processor which determines class decorators. ''' 

201 arguments_name = attributes_namer( 'class', 'construction_arguments' ) 

202 

203 def postprocess( 

204 cls: type, decorators: _nomina.DecoratorsMutable[ __.U ] 

205 ) -> None: 

206 arguments = getattr( cls, arguments_name, { } ) 

207 clscls = type( cls ) 

208 dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } ) 

209 if not dynadoc_cfg: # either metaclass argument or attribute 

210 dynadoc_cfg_name = ( 

211 attributes_namer( 'classes', 'dynadoc_configuration' ) ) 

212 dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } ) 

213 decorators.append( __.dynadoc.with_docstring( **dynadoc_cfg ) ) 

214 dcls_spec = getattr( cls, '__dataclass_transform__', None ) 

215 if not dcls_spec: # either base class or metaclass may be marked 

216 dcls_spec = getattr( clscls, '__dataclass_transform__', None ) 

217 instances_mutables = arguments.get( 

218 'instances_mutables', __.mutables_default ) 

219 instances_visibles = arguments.get( 

220 'instances_visibles', __.visibles_default ) 

221 if dcls_spec and dcls_spec.get( 'kw_only_default', False ): 

222 from .decorators import dataclass_with_standard_behaviors 

223 decorator_factory = dataclass_with_standard_behaviors 

224 if not dcls_spec.get( 'frozen_default', True ): 

225 instances_mutables = instances_mutables or '*' 

226 else: 

227 from .decorators import with_standard_behaviors 

228 decorator_factory = with_standard_behaviors 

229 decorator: _nomina.Decorator[ __.U ] = decorator_factory( 

230 mutables = instances_mutables, visibles = instances_visibles ) 

231 decorators.append( decorator ) 

232 

233 return postprocess 

234 

235 

236def produce_class_initialization_completer( 

237 attributes_namer: _nomina.AttributesNamer 

238) -> _nomina.ClassInitializationCompleter: 

239 ''' Produces initialization completer which finalizes class behaviors. ''' 

240 arguments_name = attributes_namer( 'class', 'construction_arguments' ) 

241 

242 def complete( cls: type ) -> None: 

243 arguments: __.typx.Optional[ dict[ str, __.typx.Any ] ] = ( 

244 getattr( cls, arguments_name, None ) ) 

245 if arguments is not None: delattr( cls, arguments_name ) 

246 arguments = arguments or { } 

247 mutables = arguments.get( 'class_mutables', __.mutables_default ) 

248 visibles = arguments.get( 'class_visibles', __.visibles_default ) 

249 behaviors: set[ str ] = set( ) 

250 record_behavior( 

251 cls, attributes_namer = attributes_namer, 

252 level = 'class', basename = 'mutables', 

253 label = _nomina.immutability_label, behaviors = behaviors, 

254 verifiers = mutables ) 

255 record_behavior( 

256 cls, attributes_namer = attributes_namer, 

257 level = 'class', basename = 'visibles', 

258 label = _nomina.concealment_label, behaviors = behaviors, 

259 verifiers = visibles ) 

260 # Set behaviors attribute last since it enables enforcement. 

261 behaviors_name = attributes_namer( 'class', 'behaviors' ) 

262 _utilities.setattr0( cls, behaviors_name, frozenset( behaviors ) ) 

263 

264 return complete 

265 

266 

267def record_behavior( # noqa: PLR0913 

268 cls: type, /, *, 

269 attributes_namer: _nomina.AttributesNamer, 

270 level: str, 

271 basename: str, 

272 label: str, 

273 behaviors: set[ str ], 

274 verifiers: _nomina.BehaviorExclusionVerifiersOmni, 

275) -> None: 

276 ''' Records details of particular class behavior, such as immutability. ''' 

277 names_name = attributes_namer( level, f"{basename}_names" ) 

278 if verifiers == '*': 

279 setattr( cls, names_name, '*' ) 

280 return 

281 names_omni: _nomina.BehaviorExclusionNamesOmni = ( 

282 getattr( cls, names_name, frozenset( ) ) ) 

283 if names_omni == '*': return 

284 names, regexes, predicates = ( 

285 classify_behavior_exclusion_verifiers( verifiers ) ) 

286 regexes_name = attributes_namer( level, f"{basename}_regexes" ) 

287 predicates_name = attributes_namer( level, f"{basename}_predicates" ) 

288 names_: _nomina.BehaviorExclusionNames = ( 

289 frozenset( { *names, *names_omni } ) ) 

290 regexes_: _nomina.BehaviorExclusionRegexes = ( 

291 _deduplicate_merge_sequences( 

292 regexes, getattr( cls, regexes_name, ( ) ) ) ) 

293 predicates_: _nomina.BehaviorExclusionPredicates = ( 

294 _deduplicate_merge_sequences( 

295 predicates, getattr( cls, predicates_name, ( ) ) ) ) 

296 setattr( cls, names_name, names_ ) 

297 setattr( cls, regexes_name, regexes_ ) 

298 setattr( cls, predicates_name, predicates_ ) 

299 # TODO? Add regexes match cache. 

300 # TODO? Add predicates match cache. 

301 behaviors.add( label ) 

302 

303 

304def record_class_construction_arguments( 

305 attributes_namer: _nomina.AttributesNamer, 

306 namespace: dict[ str, __.typx.Any ], 

307 arguments: dict[ str, __.typx.Any ], 

308) -> None: 

309 ''' Captures metaclass arguments as class attribute for later use. ''' 

310 arguments_name = attributes_namer( 'class', 'construction_arguments' ) 

311 arguments_ = namespace.get( arguments_name, { } ) 

312 # Decorators, which replace classes, will cause construction of the 

313 # replacements without arguments. If we had previously recorded them in 

314 # the class namespace, then we do not want to clobber them. 

315 if arguments_: return 

316 arguments_ = { } 

317 for name in ( 

318 'class_mutables', 'class_visibles', 

319 'instances_mutables', 'instances_visibles', 

320 'dynadoc_configuration', 

321 ): 

322 if name not in arguments: continue 

323 arguments_[ name ] = arguments.pop( name ) 

324 namespace[ arguments_name ] = arguments_ 

325 

326 

327def _deduplicate_merge_sequences( 

328 addends: __.cabc.Sequence[ __.typx.Any ], 

329 augends: __.cabc.Sequence[ __.typx.Any ], 

330) -> __.cabc.Sequence[ __.typx.Any ]: 

331 result = list( augends ) 

332 augends_ = set( augends ) 

333 for addend in addends: 

334 if addend in augends_: continue 

335 result.append( addend ) 

336 return tuple( result )