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

195 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-30 04:05 +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 dcls_spec = getattr( cls, '__dataclass_transform__', None ) 

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

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

261 cores = { } 

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

263 core_function = access_core_function( 

264 cls, 

265 attributes_namer = attributes_namer, 

266 arguments = arguments, 

267 level = 'instances', name = core_name, 

268 default = cores_default[ core_name ] ) 

269 cores[ core_name ] = core_function 

270 instances_mutables = arguments.get( 

271 'instances_mutables', __.mutables_default ) 

272 instances_visibles = arguments.get( 

273 'instances_visibles', __.visibles_default ) 

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

275 from .decorators import dataclass_with_standard_behaviors 

276 decorator_factory = dataclass_with_standard_behaviors 

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

278 instances_mutables = instances_mutables or '*' 

279 else: 

280 from .decorators import with_standard_behaviors 

281 decorator_factory = with_standard_behaviors 

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

283 attributes_namer = attributes_namer, 

284 error_class_provider = error_class_provider, 

285 assigner_core = __.typx.cast( 

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

287 deleter_core = __.typx.cast( 

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

289 surveyor_core = __.typx.cast( 

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

291 mutables = instances_mutables, 

292 visibles = instances_visibles ) 

293 decorators.append( decorator ) 

294 # Dynadoc tracks objects in weakset. 

295 # Must decorate after any potential class replacements. 

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

297 if not dynadoc_cfg: # either metaclass argument or attribute 

298 dynadoc_cfg_name = ( 

299 attributes_namer( 'classes', 'dynadoc_configuration' ) ) 

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

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

302 

303 return postprocess 

304 

305 

306def produce_class_initialization_completer( 

307 attributes_namer: _nomina.AttributesNamer 

308) -> _nomina.ClassInitializationCompleter: 

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

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

311 

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

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

314 getattr( cls, arguments_name, None ) ) 

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

316 arguments = arguments or { } 

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

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

319 behaviors: set[ str ] = set( ) 

320 record_behavior( 

321 cls, attributes_namer = attributes_namer, 

322 level = 'class', basename = 'mutables', 

323 label = _nomina.immutability_label, behaviors = behaviors, 

324 verifiers = mutables ) 

325 record_behavior( 

326 cls, attributes_namer = attributes_namer, 

327 level = 'class', basename = 'visibles', 

328 label = _nomina.concealment_label, behaviors = behaviors, 

329 verifiers = visibles ) 

330 # Set behaviors attribute last since it enables enforcement. 

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

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

333 

334 return complete 

335 

336 

337def record_behavior( # noqa: PLR0913 

338 cls: type, /, *, 

339 attributes_namer: _nomina.AttributesNamer, 

340 level: str, 

341 basename: str, 

342 label: str, 

343 behaviors: set[ str ], 

344 verifiers: _nomina.BehaviorExclusionVerifiersOmni, 

345) -> None: 

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

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

348 if verifiers == '*': 

349 setattr( cls, names_name, '*' ) 

350 return 

351 names_omni: _nomina.BehaviorExclusionNamesOmni = ( 

352 getattr( cls, names_name, frozenset( ) ) ) 

353 if names_omni == '*': return 

354 names, regexes, predicates = ( 

355 classify_behavior_exclusion_verifiers( verifiers ) ) 

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

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

358 names_: _nomina.BehaviorExclusionNames = ( 

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

360 regexes_: _nomina.BehaviorExclusionRegexes = ( 

361 _deduplicate_merge_sequences( 

362 regexes, getattr( cls, regexes_name, ( ) ) ) ) 

363 predicates_: _nomina.BehaviorExclusionPredicates = ( 

364 _deduplicate_merge_sequences( 

365 predicates, getattr( cls, predicates_name, ( ) ) ) ) 

366 setattr( cls, names_name, names_ ) 

367 setattr( cls, regexes_name, regexes_ ) 

368 setattr( cls, predicates_name, predicates_ ) 

369 # TODO? Add regexes match cache. 

370 # TODO? Add predicates match cache. 

371 behaviors.add( label ) 

372 

373 

374def record_class_construction_arguments( 

375 attributes_namer: _nomina.AttributesNamer, 

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

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

378) -> None: 

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

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

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

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

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

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

385 if arguments_: return 

386 arguments_ = { } 

387 for name in ( 

388 'class_mutables', 'class_visibles', 

389 'dynadoc_configuration', 

390 'instances_assigner_core', 

391 'instances_deleter_core', 

392 'instances_surveyor_core', 

393 'instances_mutables', 'instances_visibles', 

394 ): 

395 if name not in arguments: continue 

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

397 namespace[ arguments_name ] = arguments_ 

398 

399 

400def _deduplicate_merge_sequences( 

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

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

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

404 result = list( augends ) 

405 augends_ = set( augends ) 

406 for addend in addends: 

407 if addend in augends_: continue 

408 result.append( addend ) 

409 return tuple( result )