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

169 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-29 23:23 +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 __future__ import annotations 

27 

28from .. import utilities as _utilities 

29from . import __ 

30from . import nomina as _nomina 

31 

32 

33concealment_label = 'concealment' 

34immutability_label = 'immutability' 

35 

36 

37def assign_attribute_if_mutable( # noqa: PLR0913 

38 obj: object, /, *, 

39 ligation: _nomina.AssignerLigation, 

40 attributes_namer: _nomina.AttributesNamer, 

41 error_class_provider: _nomina.ErrorClassProvider, 

42 level: str, 

43 name: str, 

44 value: __.typx.Any, 

45) -> None: 

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

47 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

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

49 if immutability_label not in behaviors: 

50 ligation( name, value ) 

51 return 

52 names_name = attributes_namer( level, 'mutables_names' ) 

53 names: _nomina.BehaviorExclusionNamesOmni = ( 

54 getattr( obj, names_name, frozenset( ) ) ) 

55 if names == '*' or name in names: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true

56 ligation( name, value ) 

57 return 

58 predicates_name = attributes_namer( level, 'mutables_predicates' ) 

59 predicates: _nomina.BehaviorExclusionPredicates = ( 

60 getattr( obj, predicates_name, ( ) ) ) 

61 for predicate in predicates: 61 ↛ 62line 61 didn't jump to line 62 because the loop on line 61 never started

62 if predicate( name ): 

63 # TODO? Cache predicate hit. 

64 ligation( name, value ) 

65 return 

66 regexes_name = attributes_namer( level, 'mutables_regexes' ) 

67 regexes: _nomina.BehaviorExclusionRegexes = ( 

68 getattr( obj, regexes_name, ( ) ) ) 

69 for regex in regexes: 69 ↛ 70line 69 didn't jump to line 70 because the loop on line 69 never started

70 if regex.fullmatch( name ): 

71 # TODO? Cache regex hit. 

72 ligation( name, value ) 

73 return 

74 target = _utilities.describe_object( obj ) 

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

76 

77 

78def delete_attribute_if_mutable( # noqa: PLR0913 

79 obj: object, /, *, 

80 ligation: _nomina.DeleterLigation, 

81 attributes_namer: _nomina.AttributesNamer, 

82 error_class_provider: _nomina.ErrorClassProvider, 

83 level: str, 

84 name: str, 

85) -> None: 

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

87 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

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

89 if immutability_label not in behaviors: 

90 ligation( name ) 

91 return 

92 names_name = attributes_namer( level, 'mutables_names' ) 

93 names: _nomina.BehaviorExclusionNamesOmni = ( 

94 getattr( obj, names_name, frozenset( ) ) ) 

95 if names == '*' or name in names: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true

96 ligation( name ) 

97 return 

98 predicates_name = attributes_namer( level, 'mutables_predicates' ) 

99 predicates: _nomina.BehaviorExclusionPredicates = ( 

100 getattr( obj, predicates_name, ( ) ) ) 

101 for predicate in predicates: 101 ↛ 102line 101 didn't jump to line 102 because the loop on line 101 never started

102 if predicate( name ): 

103 # TODO? Cache predicate hit. 

104 ligation( name ) 

105 return 

106 regexes_name = attributes_namer( level, 'mutables_regexes' ) 

107 regexes: _nomina.BehaviorExclusionRegexes = ( 

108 getattr( obj, regexes_name, ( ) ) ) 

109 for regex in regexes: 109 ↛ 110line 109 didn't jump to line 110 because the loop on line 109 never started

110 if regex.fullmatch( name ): 

111 # TODO? Cache regex hit. 

112 ligation( name ) 

113 return 

114 target = _utilities.describe_object( obj ) 

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

116 

117 

118def survey_visible_attributes( 

119 obj: object, /, *, 

120 ligation: _nomina.SurveyorLigation, 

121 attributes_namer: _nomina.AttributesNamer, 

122 level: str, 

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

124 names_base = ligation( ) 

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

126 behaviors_name = attributes_namer( leveli, 'behaviors' ) 

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

128 if concealment_label not in behaviors: return names_base 128 ↛ exitline 128 didn't return from function 'survey_visible_attributes' because the return on line 128 wasn't executed

129 names_name = attributes_namer( level, 'visibles_names' ) 

130 names: _nomina.BehaviorExclusionNamesOmni = ( 

131 getattr( obj, names_name, frozenset( ) ) ) 

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

133 regexes_name = attributes_namer( level, 'visibles_regexes' ) 

134 regexes: _nomina.BehaviorExclusionRegexes = ( 

135 getattr( obj, regexes_name, ( ) ) ) 

136 predicates_name = attributes_namer( level, 'visibles_predicates' ) 

137 predicates: _nomina.BehaviorExclusionPredicates = ( 

138 getattr( obj, predicates_name, ( ) ) ) 

139 names_: list[ str ] = [ ] 

140 for name in names_base: 

141 if name in names: 

142 names_.append( name ) 

143 continue 

144 for predicate in predicates: 

145 if predicate( name ): 

146 # TODO? Cache predicate hit. 

147 names_.append( name ) 

148 continue 

149 for regex in regexes: 149 ↛ 150line 149 didn't jump to line 150 because the loop on line 149 never started

150 if regex.fullmatch( name ): 

151 # TODO? Cache regex hit. 

152 names_.append( name ) 

153 continue 

154 return names_ 

155 

156 

157def classify_behavior_exclusion_verifiers( 

158 verifiers: _nomina.BehaviorExclusionVerifiers 

159) -> tuple[ 

160 _nomina.BehaviorExclusionNames, 

161 _nomina.BehaviorExclusionRegexes, 

162 _nomina.BehaviorExclusionPredicates, 

163]: 

164 names: set[ str ] = set( ) 

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

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

167 for verifier in verifiers: 

168 if isinstance( verifier, str ): 

169 names.add( verifier ) 

170 elif isinstance( verifier, __.re.Pattern ): 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true

171 regexes.append( verifier ) 

172 elif callable( verifier ): 172 ↛ 175line 172 didn't jump to line 175 because the condition on line 172 was always true

173 predicates.append( verifier ) 

174 else: 

175 from ..exceptions import BehaviorExclusionInvalidity 

176 raise BehaviorExclusionInvalidity( verifier ) 

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

178 

179 

180def produce_class_construction_preprocessor( 

181 attributes_namer: _nomina.AttributesNamer 

182) -> _nomina.ClassConstructionPreprocessor: 

183 

184 def preprocess( # noqa: PLR0913 

185 clscls: type, 

186 name: str, 

187 bases: list[ type ], 

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

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

190 decorators: _nomina.DecoratorsMutable, 

191 ) -> None: 

192 record_class_construction_arguments( 

193 attributes_namer, namespace, arguments ) 

194 

195 return preprocess 

196 

197 

198def produce_class_construction_postprocessor( 

199 attributes_namer: _nomina.AttributesNamer 

200) -> _nomina.ClassConstructionPostprocessor: 

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

202 

203 def postprocess( 

204 cls: type, decorators: _nomina.DecoratorsMutable 

205 ) -> None: 

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

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

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

209 clscls = type( cls ) 

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

211 instances_mutables = arguments.get( 

212 'instances_mutables', __.mutables_default ) 

213 instances_visibles = arguments.get( 

214 'instances_visibles', __.visibles_default ) 

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

216 from .decorators import dataclass_with_standard_behaviors 

217 decorator_factory = dataclass_with_standard_behaviors 

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

219 instances_mutables = instances_mutables or '*' 

220 else: 

221 from .decorators import with_standard_behaviors 

222 decorator_factory = with_standard_behaviors 

223 decorator = decorator_factory( 

224 mutables = instances_mutables, visibles = instances_visibles ) 

225 decorators.append( decorator ) 

226 

227 return postprocess 

228 

229 

230def produce_class_initialization_completer( 

231 attributes_namer: _nomina.AttributesNamer 

232) -> _nomina.ClassInitializationCompleter: 

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

234 

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

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

237 getattr( cls, arguments_name, None ) ) 

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

239 arguments = arguments or { } 

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

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

242 behaviors: set[ str ] = set( ) 

243 record_behavior( 

244 cls, attributes_namer = attributes_namer, 

245 level = 'class', basename = 'mutables', 

246 label = immutability_label, behaviors = behaviors, 

247 verifiers = mutables ) 

248 record_behavior( 

249 cls, attributes_namer = attributes_namer, 

250 level = 'class', basename = 'visibles', 

251 label = concealment_label, behaviors = behaviors, 

252 verifiers = visibles ) 

253 # Set behaviors attribute last since it enables enforcement. 

254 setattr( cls, attributes_namer( 'class', 'behaviors' ), behaviors ) 

255 

256 return complete 

257 

258 

259def record_behavior( # noqa: PLR0913 

260 cls: type, /, *, 

261 attributes_namer: _nomina.AttributesNamer, 

262 level: str, 

263 basename: str, 

264 label: str, 

265 behaviors: set[ str ], 

266 verifiers: _nomina.BehaviorExclusionVerifiersOmni, 

267) -> None: 

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

269 if verifiers == '*': 

270 setattr( cls, names_name, '*' ) 

271 return 

272 names_omni: _nomina.BehaviorExclusionNamesOmni = ( 

273 getattr( cls, names_name, frozenset( ) ) ) 

274 if names_omni == '*': return 274 ↛ exitline 274 didn't return from function 'record_behavior' because the return on line 274 wasn't executed

275 names, regexes, predicates = ( 

276 classify_behavior_exclusion_verifiers( verifiers ) ) 

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

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

279 names_: _nomina.BehaviorExclusionNames = ( 

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

281 regexes_: _nomina.BehaviorExclusionRegexes = ( 

282 _deduplicate_merge_sequences( 

283 regexes, getattr( cls, regexes_name, ( ) ) ) ) 

284 predicates_: _nomina.BehaviorExclusionPredicates = ( 

285 _deduplicate_merge_sequences( 

286 predicates, getattr( cls, predicates_name, ( ) ) ) ) 

287 setattr( cls, names_name, names_ ) 

288 setattr( cls, regexes_name, regexes_ ) 

289 setattr( cls, predicates_name, predicates_ ) 

290 # TODO? Add regexes match cache. 

291 # TODO? Add predicates match cache. 

292 behaviors.add( label ) 

293 

294 

295def record_class_construction_arguments( 

296 attributes_namer: _nomina.AttributesNamer, 

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

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

299) -> None: 

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

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

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

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

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

305 if arguments_: return 305 ↛ exitline 305 didn't return from function 'record_class_construction_arguments' because the return on line 305 wasn't executed

306 arguments_ = { } 

307 for name in ( 

308 'class_mutables', 'class_visibles', 

309 'instances_mutables', 'instances_visibles', 

310 ): 

311 if name not in arguments: continue 

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

313 namespace[ arguments_name ] = arguments_ 

314 

315 

316def _deduplicate_merge_sequences( 

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

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

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

320 result = list( augends ) 

321 augends_ = set( augends ) 

322 for addend in addends: 

323 if addend in augends_: continue 

324 result.append( addend ) 

325 return tuple( result )