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

189 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-11 04:29 +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 augment_class_attributes_allocations( 

155 attributes_namer: _nomina.AttributesNamer, 

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

157) -> None: 

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

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

160 slots: __.typx.Union[ 

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

162 __.cabc.Sequence[ str ], 

163 None 

164 ] = namespace.get( '__slots__' ) 

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

166 slots_ = dict( slots ) 

167 slots_[ behaviors_name ] = 'Active behaviors.' 

168 slots_ = __.types.MappingProxyType( slots_ ) 

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

170 slots_ = list( slots ) 

171 slots_.append( behaviors_name ) 

172 slots_ = tuple( slots_ ) 

173 else: return # pragma: no cover 

174 namespace[ '__slots__' ] = slots_ 

175 

176 

177def classify_behavior_exclusion_verifiers( 

178 verifiers: _nomina.BehaviorExclusionVerifiers 

179) -> tuple[ 

180 _nomina.BehaviorExclusionNames, 

181 _nomina.BehaviorExclusionRegexes, 

182 _nomina.BehaviorExclusionPredicates, 

183]: 

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

185 names: set[ str ] = set( ) 

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

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

188 for verifier in verifiers: 

189 if isinstance( verifier, str ): 

190 names.add( verifier ) 

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

192 regexes.append( verifier ) 

193 elif callable( verifier ): 

194 predicates.append( verifier ) 

195 else: 

196 from ..exceptions import BehaviorExclusionInvalidity 

197 raise BehaviorExclusionInvalidity( verifier ) 

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

199 

200 

201def produce_class_construction_preprocessor( 

202 attributes_namer: _nomina.AttributesNamer 

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

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

205 

206 def preprocess( # noqa: PLR0913 

207 clscls: type, 

208 name: str, 

209 bases: list[ type ], 

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

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

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

213 ) -> None: 

214 record_class_construction_arguments( 

215 attributes_namer, namespace, arguments ) 

216 if '__slots__' in namespace: 

217 augment_class_attributes_allocations( attributes_namer, namespace ) 

218 

219 return preprocess 

220 

221 

222def produce_class_construction_postprocessor( 

223 attributes_namer: _nomina.AttributesNamer, 

224 error_class_provider: _nomina.ErrorClassProvider, 

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

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

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

228 

229 def postprocess( 

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

231 ) -> None: 

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

233 clscls = type( cls ) 

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

235 if not dynadoc_cfg: # either metaclass argument or attribute 

236 dynadoc_cfg_name = ( 

237 attributes_namer( 'classes', 'dynadoc_configuration' ) ) 

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

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

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

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

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

243 instances_assigner = arguments.get( 

244 'instances_assigner_core', assign_attribute_if_mutable ) 

245 instances_deleter = arguments.get( 

246 'instances_deleter_core', delete_attribute_if_mutable ) 

247 instances_surveyor = arguments.get( 

248 'instances_surveyor_core', survey_visible_attributes ) 

249 instances_mutables = arguments.get( 

250 'instances_mutables', __.mutables_default ) 

251 instances_visibles = arguments.get( 

252 'instances_visibles', __.visibles_default ) 

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

254 from .decorators import dataclass_with_standard_behaviors 

255 decorator_factory = dataclass_with_standard_behaviors 

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

257 instances_mutables = instances_mutables or '*' 

258 else: 

259 from .decorators import with_standard_behaviors 

260 decorator_factory = with_standard_behaviors 

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

262 attributes_namer = attributes_namer, 

263 error_class_provider = error_class_provider, 

264 assigner_core = instances_assigner, 

265 deleter_core = instances_deleter, 

266 surveyor_core = instances_surveyor, 

267 mutables = instances_mutables, 

268 visibles = instances_visibles ) 

269 decorators.append( decorator ) 

270 

271 return postprocess 

272 

273 

274def produce_class_initialization_completer( 

275 attributes_namer: _nomina.AttributesNamer 

276) -> _nomina.ClassInitializationCompleter: 

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

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

279 

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

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

282 getattr( cls, arguments_name, None ) ) 

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

284 arguments = arguments or { } 

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

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

287 behaviors: set[ str ] = set( ) 

288 record_behavior( 

289 cls, attributes_namer = attributes_namer, 

290 level = 'class', basename = 'mutables', 

291 label = _nomina.immutability_label, behaviors = behaviors, 

292 verifiers = mutables ) 

293 record_behavior( 

294 cls, attributes_namer = attributes_namer, 

295 level = 'class', basename = 'visibles', 

296 label = _nomina.concealment_label, behaviors = behaviors, 

297 verifiers = visibles ) 

298 # Set behaviors attribute last since it enables enforcement. 

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

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

301 

302 return complete 

303 

304 

305def record_behavior( # noqa: PLR0913 

306 cls: type, /, *, 

307 attributes_namer: _nomina.AttributesNamer, 

308 level: str, 

309 basename: str, 

310 label: str, 

311 behaviors: set[ str ], 

312 verifiers: _nomina.BehaviorExclusionVerifiersOmni, 

313) -> None: 

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

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

316 if verifiers == '*': 

317 setattr( cls, names_name, '*' ) 

318 return 

319 names_omni: _nomina.BehaviorExclusionNamesOmni = ( 

320 getattr( cls, names_name, frozenset( ) ) ) 

321 if names_omni == '*': return 

322 names, regexes, predicates = ( 

323 classify_behavior_exclusion_verifiers( verifiers ) ) 

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

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

326 names_: _nomina.BehaviorExclusionNames = ( 

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

328 regexes_: _nomina.BehaviorExclusionRegexes = ( 

329 _deduplicate_merge_sequences( 

330 regexes, getattr( cls, regexes_name, ( ) ) ) ) 

331 predicates_: _nomina.BehaviorExclusionPredicates = ( 

332 _deduplicate_merge_sequences( 

333 predicates, getattr( cls, predicates_name, ( ) ) ) ) 

334 setattr( cls, names_name, names_ ) 

335 setattr( cls, regexes_name, regexes_ ) 

336 setattr( cls, predicates_name, predicates_ ) 

337 # TODO? Add regexes match cache. 

338 # TODO? Add predicates match cache. 

339 behaviors.add( label ) 

340 

341 

342def record_class_construction_arguments( 

343 attributes_namer: _nomina.AttributesNamer, 

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

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

346) -> None: 

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

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

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

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

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

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

353 if arguments_: return 

354 arguments_ = { } 

355 for name in ( 

356 'class_mutables', 'class_visibles', 

357 'dynadoc_configuration', 

358 'instances_assigner_core', 

359 'instances_deleter_core', 

360 'instances_surveyor_core', 

361 'instances_mutables', 'instances_visibles', 

362 ): 

363 if name not in arguments: continue 

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

365 namespace[ arguments_name ] = arguments_ 

366 

367 

368def _deduplicate_merge_sequences( 

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

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

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

372 result = list( augends ) 

373 augends_ = set( augends ) 

374 for addend in addends: 

375 if addend in augends_: continue 

376 result.append( addend ) 

377 return tuple( result )