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

195 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-26 18: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''' 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 access_core_function( # noqa: PLR0913 

32 cls: type, /, *, 

33 attributes_namer: _nomina.AttributesNamer, 

34 arguments: __.cabc.Mapping[ str, __.typx.Any ], 

35 level: str, 

36 name: str, 

37 default: __.cabc.Callable[ ..., __.typx.Any ], 

38) -> __.cabc.Callable[ ..., __.typx.Any ]: 

39 ''' Accesses core behavior function. 

40 

41 First checks for override argument, then checks for heritable 

42 attribute. Finally, falls back to provided default. 

43 ''' 

44 argument_name = f"{level}_{name}_core" 

45 attribute_name = attributes_namer( level, f"{name}_core" ) 

46 return ( 

47 arguments.get( argument_name ) 

48 or getattr( cls, attribute_name, default ) ) 

49 

50 

51def assign_attribute_if_mutable( # noqa: PLR0913 

52 obj: object, /, *, 

53 ligation: _nomina.AssignerLigation, 

54 attributes_namer: _nomina.AttributesNamer, 

55 error_class_provider: _nomina.ErrorClassProvider, 

56 level: str, 

57 name: str, 

58 value: __.typx.Any, 

59) -> None: 

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

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

62 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

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

64 if _nomina.immutability_label not in behaviors: 

65 ligation( name, value ) 

66 return 

67 names_name = attributes_namer( level, 'mutables_names' ) 

68 names: _nomina.BehaviorExclusionNamesOmni = ( 

69 getattr( obj, names_name, frozenset( ) ) ) 

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

71 ligation( name, value ) 

72 return 

73 predicates_name = attributes_namer( level, 'mutables_predicates' ) 

74 predicates: _nomina.BehaviorExclusionPredicates = ( 

75 getattr( obj, predicates_name, ( ) ) ) 

76 for predicate in predicates: 

77 if predicate( name ): 

78 # TODO? Cache predicate hit. 

79 ligation( name, value ) 

80 return 

81 regexes_name = attributes_namer( level, 'mutables_regexes' ) 

82 regexes: _nomina.BehaviorExclusionRegexes = ( 

83 getattr( obj, regexes_name, ( ) ) ) 

84 for regex in regexes: 

85 if regex.fullmatch( name ): 

86 # TODO? Cache regex hit. 

87 ligation( name, value ) 

88 return 

89 target = _utilities.describe_object( obj ) 

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

91 

92 

93def delete_attribute_if_mutable( # noqa: PLR0913 

94 obj: object, /, *, 

95 ligation: _nomina.DeleterLigation, 

96 attributes_namer: _nomina.AttributesNamer, 

97 error_class_provider: _nomina.ErrorClassProvider, 

98 level: str, 

99 name: str, 

100) -> None: 

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

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

103 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

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

105 if _nomina.immutability_label not in behaviors: 

106 ligation( name ) 

107 return 

108 names_name = attributes_namer( level, 'mutables_names' ) 

109 names: _nomina.BehaviorExclusionNamesOmni = ( 

110 getattr( obj, names_name, frozenset( ) ) ) 

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

112 ligation( name ) 

113 return 

114 predicates_name = attributes_namer( level, 'mutables_predicates' ) 

115 predicates: _nomina.BehaviorExclusionPredicates = ( 

116 getattr( obj, predicates_name, ( ) ) ) 

117 for predicate in predicates: 

118 if predicate( name ): 

119 # TODO? Cache predicate hit. 

120 ligation( name ) 

121 return 

122 regexes_name = attributes_namer( level, 'mutables_regexes' ) 

123 regexes: _nomina.BehaviorExclusionRegexes = ( 

124 getattr( obj, regexes_name, ( ) ) ) 

125 for regex in regexes: 

126 if regex.fullmatch( name ): 

127 # TODO? Cache regex hit. 

128 ligation( name ) 

129 return 

130 target = _utilities.describe_object( obj ) 

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

132 

133 

134def survey_visible_attributes( 

135 obj: object, /, *, 

136 ligation: _nomina.SurveyorLigation, 

137 attributes_namer: _nomina.AttributesNamer, 

138 level: str, 

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

140 ''' Returns sequence of visible attributes. ''' 

141 names_base = ligation( ) 

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

143 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

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

145 if _nomina.concealment_label not in behaviors: return names_base 

146 names_name = attributes_namer( level, 'visibles_names' ) 

147 names: _nomina.BehaviorExclusionNamesOmni = ( 

148 getattr( obj, names_name, frozenset( ) ) ) 

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

150 regexes_name = attributes_namer( level, 'visibles_regexes' ) 

151 regexes: _nomina.BehaviorExclusionRegexes = ( 

152 getattr( obj, regexes_name, ( ) ) ) 

153 predicates_name = attributes_namer( level, 'visibles_predicates' ) 

154 predicates: _nomina.BehaviorExclusionPredicates = ( 

155 getattr( obj, predicates_name, ( ) ) ) 

156 names_: list[ str ] = [ ] 

157 for name in names_base: 

158 if name in names: 

159 names_.append( name ) 

160 continue 

161 for predicate in predicates: 

162 if predicate( name ): 

163 # TODO? Cache predicate hit. 

164 names_.append( name ) 

165 continue 

166 for regex in regexes: 

167 if regex.fullmatch( name ): 

168 # TODO? Cache regex hit. 

169 names_.append( name ) 

170 continue 

171 return names_ 

172 

173 

174def augment_class_attributes_allocations( 

175 attributes_namer: _nomina.AttributesNamer, 

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

177) -> None: 

178 ''' Adds necessary slots for record-keeping attributes. ''' 

179 behaviors_name = attributes_namer( 'instance', 'behaviors' ) 

180 slots: __.typx.Union[ 

181 __.cabc.Mapping[ str, __.typx.Any ], 

182 __.cabc.Sequence[ str ], 

183 None 

184 ] = namespace.get( '__slots__' ) 

185 if isinstance( slots, __.cabc.Mapping ): 

186 slots_ = dict( slots ) 

187 slots_[ behaviors_name ] = 'Active behaviors.' 

188 slots_ = __.types.MappingProxyType( slots_ ) 

189 elif isinstance( slots, __.cabc.Sequence ): 

190 slots_ = list( slots ) 

191 slots_.append( behaviors_name ) 

192 slots_ = tuple( slots_ ) 

193 else: return # pragma: no cover 

194 namespace[ '__slots__' ] = slots_ 

195 

196 

197def classify_behavior_exclusion_verifiers( 

198 verifiers: _nomina.BehaviorExclusionVerifiers 

199) -> tuple[ 

200 _nomina.BehaviorExclusionNames, 

201 _nomina.BehaviorExclusionRegexes, 

202 _nomina.BehaviorExclusionPredicates, 

203]: 

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

205 names: set[ str ] = set( ) 

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

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

208 for verifier in verifiers: 

209 if isinstance( verifier, str ): 

210 names.add( verifier ) 

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

212 regexes.append( verifier ) 

213 elif callable( verifier ): 

214 predicates.append( verifier ) 

215 else: 

216 from ..exceptions import BehaviorExclusionInvalidity 

217 raise BehaviorExclusionInvalidity( verifier ) 

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

219 

220 

221def produce_class_construction_preprocessor( 

222 attributes_namer: _nomina.AttributesNamer 

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

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

225 

226 def preprocess( # noqa: PLR0913 

227 clscls: type, 

228 name: str, 

229 bases: list[ type ], 

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

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

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

233 ) -> None: 

234 record_class_construction_arguments( 

235 attributes_namer, namespace, arguments ) 

236 if '__slots__' in namespace: 

237 augment_class_attributes_allocations( attributes_namer, namespace ) 

238 

239 return preprocess 

240 

241 

242def produce_class_construction_postprocessor( 

243 attributes_namer: _nomina.AttributesNamer, 

244 error_class_provider: _nomina.ErrorClassProvider, 

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

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

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

248 cores_default = dict( 

249 assigner = assign_attribute_if_mutable, 

250 deleter = delete_attribute_if_mutable, 

251 surveyor = survey_visible_attributes ) 

252 

253 def postprocess( 

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

255 ) -> None: 

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

257 clscls = type( cls ) 

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

259 if not dynadoc_cfg: # either metaclass argument or attribute 

260 dynadoc_cfg_name = ( 

261 attributes_namer( 'classes', 'dynadoc_configuration' ) ) 

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

263 decorators.append( __.ddoc.with_docstring( **dynadoc_cfg ) ) 

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

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

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

267 cores = { } 

268 for core_name in ( 'assigner', 'deleter', 'surveyor' ): 

269 core_function = access_core_function( 

270 cls, 

271 attributes_namer = attributes_namer, 

272 arguments = arguments, 

273 level = 'instances', name = core_name, 

274 default = cores_default[ core_name ] ) 

275 cores[ core_name ] = core_function 

276 instances_mutables = arguments.get( 

277 'instances_mutables', __.mutables_default ) 

278 instances_visibles = arguments.get( 

279 'instances_visibles', __.visibles_default ) 

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

281 from .decorators import dataclass_with_standard_behaviors 

282 decorator_factory = dataclass_with_standard_behaviors 

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

284 instances_mutables = instances_mutables or '*' 

285 else: 

286 from .decorators import with_standard_behaviors 

287 decorator_factory = with_standard_behaviors 

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

289 attributes_namer = attributes_namer, 

290 error_class_provider = error_class_provider, 

291 assigner_core = __.typx.cast( 

292 _nomina.AssignerCore, cores[ 'assigner' ] ), 

293 deleter_core = __.typx.cast( 

294 _nomina.DeleterCore, cores[ 'deleter' ] ), 

295 surveyor_core = __.typx.cast( 

296 _nomina.SurveyorCore, cores[ 'surveyor' ] ), 

297 mutables = instances_mutables, 

298 visibles = instances_visibles ) 

299 decorators.append( decorator ) 

300 

301 return postprocess 

302 

303 

304def produce_class_initialization_completer( 

305 attributes_namer: _nomina.AttributesNamer 

306) -> _nomina.ClassInitializationCompleter: 

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

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

309 

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

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

312 getattr( cls, arguments_name, None ) ) 

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

314 arguments = arguments or { } 

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

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

317 behaviors: set[ str ] = set( ) 

318 record_behavior( 

319 cls, attributes_namer = attributes_namer, 

320 level = 'class', basename = 'mutables', 

321 label = _nomina.immutability_label, behaviors = behaviors, 

322 verifiers = mutables ) 

323 record_behavior( 

324 cls, attributes_namer = attributes_namer, 

325 level = 'class', basename = 'visibles', 

326 label = _nomina.concealment_label, behaviors = behaviors, 

327 verifiers = visibles ) 

328 # Set behaviors attribute last since it enables enforcement. 

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

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

331 

332 return complete 

333 

334 

335def record_behavior( # noqa: PLR0913 

336 cls: type, /, *, 

337 attributes_namer: _nomina.AttributesNamer, 

338 level: str, 

339 basename: str, 

340 label: str, 

341 behaviors: set[ str ], 

342 verifiers: _nomina.BehaviorExclusionVerifiersOmni, 

343) -> None: 

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

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

346 if verifiers == '*': 

347 setattr( cls, names_name, '*' ) 

348 return 

349 names_omni: _nomina.BehaviorExclusionNamesOmni = ( 

350 getattr( cls, names_name, frozenset( ) ) ) 

351 if names_omni == '*': return 

352 names, regexes, predicates = ( 

353 classify_behavior_exclusion_verifiers( verifiers ) ) 

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

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

356 names_: _nomina.BehaviorExclusionNames = ( 

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

358 regexes_: _nomina.BehaviorExclusionRegexes = ( 

359 _deduplicate_merge_sequences( 

360 regexes, getattr( cls, regexes_name, ( ) ) ) ) 

361 predicates_: _nomina.BehaviorExclusionPredicates = ( 

362 _deduplicate_merge_sequences( 

363 predicates, getattr( cls, predicates_name, ( ) ) ) ) 

364 setattr( cls, names_name, names_ ) 

365 setattr( cls, regexes_name, regexes_ ) 

366 setattr( cls, predicates_name, predicates_ ) 

367 # TODO? Add regexes match cache. 

368 # TODO? Add predicates match cache. 

369 behaviors.add( label ) 

370 

371 

372def record_class_construction_arguments( 

373 attributes_namer: _nomina.AttributesNamer, 

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

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

376) -> None: 

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

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

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

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

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

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

383 if arguments_: return 

384 arguments_ = { } 

385 for name in ( 

386 'class_mutables', 'class_visibles', 

387 'dynadoc_configuration', 

388 'instances_assigner_core', 

389 'instances_deleter_core', 

390 'instances_surveyor_core', 

391 'instances_mutables', 'instances_visibles', 

392 ): 

393 if name not in arguments: continue 

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

395 namespace[ arguments_name ] = arguments_ 

396 

397 

398def _deduplicate_merge_sequences( 

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

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

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

402 result = list( augends ) 

403 augends_ = set( augends ) 

404 for addend in addends: 

405 if addend in augends_: continue 

406 result.append( addend ) 

407 return tuple( result )