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

197 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-08 23:42 +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 slots and behaviors_name in slots: return 

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

187 slots_ = dict( slots ) 

188 slots_[ behaviors_name ] = 'Active behaviors.' 

189 slots_ = __.types.MappingProxyType( slots_ ) 

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

191 slots_ = list( slots ) 

192 slots_.append( behaviors_name ) 

193 slots_ = tuple( slots_ ) 

194 else: return # pragma: no cover 

195 namespace[ '__slots__' ] = slots_ 

196 

197 

198def classify_behavior_exclusion_verifiers( 

199 verifiers: _nomina.BehaviorExclusionVerifiers 

200) -> tuple[ 

201 _nomina.BehaviorExclusionNames, 

202 _nomina.BehaviorExclusionRegexes, 

203 _nomina.BehaviorExclusionPredicates, 

204]: 

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

206 names: set[ str ] = set( ) 

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

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

209 for verifier in verifiers: 

210 if isinstance( verifier, str ): 

211 names.add( verifier ) 

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

213 regexes.append( verifier ) 

214 elif callable( verifier ): 

215 predicates.append( verifier ) 

216 else: 

217 from ..exceptions import BehaviorExclusionInvalidity 

218 raise BehaviorExclusionInvalidity( verifier ) 

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

220 

221 

222def produce_class_construction_preprocessor( 

223 attributes_namer: _nomina.AttributesNamer 

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

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

226 

227 def preprocess( # noqa: PLR0913 

228 clscls: type, 

229 name: str, 

230 bases: list[ type ], 

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

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

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

234 ) -> None: 

235 record_class_construction_arguments( 

236 attributes_namer, namespace, arguments ) 

237 if '__slots__' in namespace: 

238 augment_class_attributes_allocations( attributes_namer, namespace ) 

239 

240 return preprocess 

241 

242 

243def produce_class_construction_postprocessor( 

244 attributes_namer: _nomina.AttributesNamer, 

245 error_class_provider: _nomina.ErrorClassProvider, 

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

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

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

249 cores_default = dict( 

250 assigner = assign_attribute_if_mutable, 

251 deleter = delete_attribute_if_mutable, 

252 surveyor = survey_visible_attributes ) 

253 

254 def postprocess( 

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

256 ) -> None: 

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

258 clscls = type( cls ) 

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

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

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

262 cores = { } 

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

264 core_function = access_core_function( 

265 cls, 

266 attributes_namer = attributes_namer, 

267 arguments = arguments, 

268 level = 'instances', name = core_name, 

269 default = cores_default[ core_name ] ) 

270 cores[ core_name ] = core_function 

271 instances_mutables = arguments.get( 

272 'instances_mutables', __.mutables_default ) 

273 instances_visibles = arguments.get( 

274 'instances_visibles', __.visibles_default ) 

275 instances_ignore_init_arguments = arguments.get( 

276 'instances_ignore_init_arguments', False ) 

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

278 from .decorators import dataclass_with_standard_behaviors 

279 decorator_factory = dataclass_with_standard_behaviors 

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

281 instances_mutables = instances_mutables or '*' 

282 else: 

283 from .decorators import with_standard_behaviors 

284 decorator_factory = with_standard_behaviors 

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

286 attributes_namer = attributes_namer, 

287 error_class_provider = error_class_provider, 

288 assigner_core = __.typx.cast( 

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

290 deleter_core = __.typx.cast( 

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

292 surveyor_core = __.typx.cast( 

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

294 ignore_init_arguments = instances_ignore_init_arguments, 

295 mutables = instances_mutables, 

296 visibles = instances_visibles ) 

297 decorators.append( decorator ) 

298 # Dynadoc tracks objects in weakset. 

299 # Must decorate after any potential class replacements. 

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

301 if not dynadoc_cfg: # either metaclass argument or attribute 

302 dynadoc_cfg_name = ( 

303 attributes_namer( 'classes', 'dynadoc_configuration' ) ) 

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

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

306 

307 return postprocess 

308 

309 

310def produce_class_initialization_completer( 

311 attributes_namer: _nomina.AttributesNamer 

312) -> _nomina.ClassInitializationCompleter: 

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

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

315 

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

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

318 getattr( cls, arguments_name, None ) ) 

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

320 arguments = arguments or { } 

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

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

323 behaviors: set[ str ] = set( ) 

324 record_behavior( 

325 cls, attributes_namer = attributes_namer, 

326 level = 'class', basename = 'mutables', 

327 label = _nomina.immutability_label, behaviors = behaviors, 

328 verifiers = mutables ) 

329 record_behavior( 

330 cls, attributes_namer = attributes_namer, 

331 level = 'class', basename = 'visibles', 

332 label = _nomina.concealment_label, behaviors = behaviors, 

333 verifiers = visibles ) 

334 # Set behaviors attribute last since it enables enforcement. 

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

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

337 

338 return complete 

339 

340 

341def record_behavior( # noqa: PLR0913 

342 cls: type, /, *, 

343 attributes_namer: _nomina.AttributesNamer, 

344 level: str, 

345 basename: str, 

346 label: str, 

347 behaviors: set[ str ], 

348 verifiers: _nomina.BehaviorExclusionVerifiersOmni, 

349) -> None: 

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

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

352 if verifiers == '*': 

353 setattr( cls, names_name, '*' ) 

354 return 

355 names_omni: _nomina.BehaviorExclusionNamesOmni = ( 

356 getattr( cls, names_name, frozenset( ) ) ) 

357 if names_omni == '*': return 

358 names, regexes, predicates = ( 

359 classify_behavior_exclusion_verifiers( verifiers ) ) 

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

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

362 names_: _nomina.BehaviorExclusionNames = ( 

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

364 regexes_: _nomina.BehaviorExclusionRegexes = ( 

365 _deduplicate_merge_sequences( 

366 regexes, getattr( cls, regexes_name, ( ) ) ) ) 

367 predicates_: _nomina.BehaviorExclusionPredicates = ( 

368 _deduplicate_merge_sequences( 

369 predicates, getattr( cls, predicates_name, ( ) ) ) ) 

370 setattr( cls, names_name, names_ ) 

371 setattr( cls, regexes_name, regexes_ ) 

372 setattr( cls, predicates_name, predicates_ ) 

373 # TODO? Add regexes match cache. 

374 # TODO? Add predicates match cache. 

375 behaviors.add( label ) 

376 

377 

378def record_class_construction_arguments( 

379 attributes_namer: _nomina.AttributesNamer, 

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

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

382) -> None: 

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

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

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

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

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

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

389 if arguments_: return 

390 arguments_ = { } 

391 for name in ( 

392 'class_mutables', 'class_visibles', 

393 'dynadoc_configuration', 

394 'instances_assigner_core', 

395 'instances_deleter_core', 

396 'instances_surveyor_core', 

397 'instances_ignore_init_arguments', 

398 'instances_mutables', 'instances_visibles', 

399 ): 

400 if name not in arguments: continue 

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

402 namespace[ arguments_name ] = arguments_ 

403 

404 

405def _deduplicate_merge_sequences( 

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

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

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

409 result = list( augends ) 

410 augends_ = set( augends ) 

411 for addend in addends: 

412 if addend in augends_: continue 

413 result.append( addend ) 

414 return tuple( result )