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

175 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-08 04:17 +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 error_class_provider: _nomina.ErrorClassProvider, 

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

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

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

203 

204 def postprocess( 

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

206 ) -> None: 

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

208 clscls = type( cls ) 

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

210 if not dynadoc_cfg: # either metaclass argument or attribute 

211 dynadoc_cfg_name = ( 

212 attributes_namer( 'classes', 'dynadoc_configuration' ) ) 

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

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

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

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

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

218 instances_assigner = arguments.get( 

219 'instances_assigner_core', assign_attribute_if_mutable ) 

220 instances_deleter = arguments.get( 

221 'instances_deleter_core', delete_attribute_if_mutable ) 

222 instances_surveyor = arguments.get( 

223 'instances_surveyor_core', survey_visible_attributes ) 

224 instances_mutables = arguments.get( 

225 'instances_mutables', __.mutables_default ) 

226 instances_visibles = arguments.get( 

227 'instances_visibles', __.visibles_default ) 

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

229 from .decorators import dataclass_with_standard_behaviors 

230 decorator_factory = dataclass_with_standard_behaviors 

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

232 instances_mutables = instances_mutables or '*' 

233 else: 

234 from .decorators import with_standard_behaviors 

235 decorator_factory = with_standard_behaviors 

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

237 attributes_namer = attributes_namer, 

238 error_class_provider = error_class_provider, 

239 assigner_core = instances_assigner, 

240 deleter_core = instances_deleter, 

241 surveyor_core = instances_surveyor, 

242 mutables = instances_mutables, 

243 visibles = instances_visibles ) 

244 decorators.append( decorator ) 

245 

246 return postprocess 

247 

248 

249def produce_class_initialization_completer( 

250 attributes_namer: _nomina.AttributesNamer 

251) -> _nomina.ClassInitializationCompleter: 

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

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

254 

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

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

257 getattr( cls, arguments_name, None ) ) 

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

259 arguments = arguments or { } 

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

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

262 behaviors: set[ str ] = set( ) 

263 record_behavior( 

264 cls, attributes_namer = attributes_namer, 

265 level = 'class', basename = 'mutables', 

266 label = _nomina.immutability_label, behaviors = behaviors, 

267 verifiers = mutables ) 

268 record_behavior( 

269 cls, attributes_namer = attributes_namer, 

270 level = 'class', basename = 'visibles', 

271 label = _nomina.concealment_label, behaviors = behaviors, 

272 verifiers = visibles ) 

273 # Set behaviors attribute last since it enables enforcement. 

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

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

276 

277 return complete 

278 

279 

280def record_behavior( # noqa: PLR0913 

281 cls: type, /, *, 

282 attributes_namer: _nomina.AttributesNamer, 

283 level: str, 

284 basename: str, 

285 label: str, 

286 behaviors: set[ str ], 

287 verifiers: _nomina.BehaviorExclusionVerifiersOmni, 

288) -> None: 

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

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

291 if verifiers == '*': 

292 setattr( cls, names_name, '*' ) 

293 return 

294 names_omni: _nomina.BehaviorExclusionNamesOmni = ( 

295 getattr( cls, names_name, frozenset( ) ) ) 

296 if names_omni == '*': return 

297 names, regexes, predicates = ( 

298 classify_behavior_exclusion_verifiers( verifiers ) ) 

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

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

301 names_: _nomina.BehaviorExclusionNames = ( 

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

303 regexes_: _nomina.BehaviorExclusionRegexes = ( 

304 _deduplicate_merge_sequences( 

305 regexes, getattr( cls, regexes_name, ( ) ) ) ) 

306 predicates_: _nomina.BehaviorExclusionPredicates = ( 

307 _deduplicate_merge_sequences( 

308 predicates, getattr( cls, predicates_name, ( ) ) ) ) 

309 setattr( cls, names_name, names_ ) 

310 setattr( cls, regexes_name, regexes_ ) 

311 setattr( cls, predicates_name, predicates_ ) 

312 # TODO? Add regexes match cache. 

313 # TODO? Add predicates match cache. 

314 behaviors.add( label ) 

315 

316 

317def record_class_construction_arguments( 

318 attributes_namer: _nomina.AttributesNamer, 

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

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

321) -> None: 

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

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

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

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

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

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

328 if arguments_: return 

329 arguments_ = { } 

330 for name in ( 

331 'class_mutables', 'class_visibles', 

332 'instances_assigner_core', 

333 'instances_deleter_core', 

334 'instances_surveyor_core', 

335 'instances_mutables', 'instances_visibles', 

336 'dynadoc_configuration', 

337 ): 

338 if name not in arguments: continue 

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

340 namespace[ arguments_name ] = arguments_ 

341 

342 

343def _deduplicate_merge_sequences( 

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

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

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

347 result = list( augends ) 

348 augends_ = set( augends ) 

349 for addend in addends: 

350 if addend in augends_: continue 

351 result.append( addend ) 

352 return tuple( result )