classcore 1.12__tar.gz → 1.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {classcore-1.12 → classcore-1.14}/PKG-INFO +3 -5
  2. {classcore-1.12 → classcore-1.14}/pyproject.toml +12 -8
  3. {classcore-1.12 → classcore-1.14}/sources/classcore/README.md +15 -0
  4. classcore-1.14/sources/classcore/__/nomina.py +95 -0
  5. {classcore-1.12 → classcore-1.14}/sources/classcore/__init__.py +1 -1
  6. {classcore-1.12 → classcore-1.14}/sources/classcore/factories.py +14 -5
  7. classcore-1.14/sources/classcore/standard/__.py +106 -0
  8. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/__init__.py +1 -0
  9. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/behaviors.py +163 -64
  10. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/classes.py +27 -13
  11. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/decorators.py +32 -2
  12. classcore-1.14/sources/classcore/standard/explanations.py +253 -0
  13. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/nomina.py +8 -0
  14. {classcore-1.12 → classcore-1.14}/sources/classcore/utilities.py +28 -3
  15. classcore-1.12/sources/classcore/__/nomina.py +0 -36
  16. classcore-1.12/sources/classcore/standard/__.py +0 -42
  17. {classcore-1.12 → classcore-1.14}/.gitignore +0 -0
  18. {classcore-1.12 → classcore-1.14}/LICENSE.txt +0 -0
  19. {classcore-1.12 → classcore-1.14}/README.rst +0 -0
  20. {classcore-1.12 → classcore-1.14}/sources/classcore/__/__init__.py +0 -0
  21. {classcore-1.12 → classcore-1.14}/sources/classcore/__/doctab.py +0 -0
  22. {classcore-1.12 → classcore-1.14}/sources/classcore/__/imports.py +0 -0
  23. {classcore-1.12 → classcore-1.14}/sources/classcore/decorators.py +0 -0
  24. {classcore-1.12 → classcore-1.14}/sources/classcore/exceptions.py +0 -0
  25. {classcore-1.12 → classcore-1.14}/sources/classcore/nomina.py +0 -0
  26. {classcore-1.12 → classcore-1.14}/sources/classcore/py.typed +0 -0
  27. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/README.md +0 -0
  28. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/dynadoc.py +0 -0
  29. {classcore-1.12 → classcore-1.14}/sources/classcore/standard/modules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: classcore
3
- Version: 1.12
3
+ Version: 1.14
4
4
  Summary: Foundational class factories and decorators.
5
5
  Project-URL: Homepage, https://github.com/emcd/python-classcore
6
6
  Project-URL: Documentation, https://emcd.github.io/python-classcore
@@ -15,15 +15,13 @@ Classifier: Development Status :: 5 - Production/Stable
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: Apache Software License
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
18
  Classifier: Programming Language :: Python :: 3.12
21
19
  Classifier: Programming Language :: Python :: 3.13
22
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
23
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
- Classifier: Programming Language :: Python :: Implementation :: PyPy
25
23
  Classifier: Topic :: Software Development
26
- Requires-Python: >=3.10
24
+ Requires-Python: >=3.12
27
25
  Requires-Dist: dynadoc~=1.4
28
26
  Requires-Dist: typing-extensions
29
27
  Description-Content-Type: text/x-rst
@@ -13,7 +13,7 @@ description = 'Foundational class factories and decorators.'
13
13
  dynamic = [ 'version' ]
14
14
  license = 'Apache-2.0'
15
15
  readme = { 'file' = 'README.rst', 'content-type' = 'text/x-rst' }
16
- requires-python = '>= 3.10'
16
+ requires-python = '>= 3.12'
17
17
  dependencies = [
18
18
  'dynadoc ~= 1.4',
19
19
  'typing-extensions',
@@ -26,13 +26,11 @@ classifiers = [ # https://pypi.org/classifiers
26
26
  'License :: OSI Approved :: Apache Software License',
27
27
  'Programming Language :: Python :: 3 :: Only',
28
28
  # --- BEGIN: Injected by Copier ---
29
- 'Programming Language :: Python :: 3.10',
30
- 'Programming Language :: Python :: 3.11',
31
29
  'Programming Language :: Python :: 3.12',
32
30
  'Programming Language :: Python :: 3.13',
33
31
  'Programming Language :: Python :: 3.14',
32
+ 'Programming Language :: Python :: 3.15',
34
33
  'Programming Language :: Python :: Implementation :: CPython',
35
- 'Programming Language :: Python :: Implementation :: PyPy',
36
34
  # --- END: Injected by Copier ---
37
35
  'Topic :: Software Development',
38
36
  ]
@@ -91,7 +89,7 @@ strict-naming = false
91
89
  # --- BEGIN: Injected by Copier ---
92
90
  # --- END: Injected by Copier ---
93
91
  [tool.hatch.envs.default]
94
- python = '3.10'
92
+ python = '3.12'
95
93
  [tool.hatch.envs.develop]
96
94
  description = ''' Development environment. '''
97
95
  builder = true
@@ -127,6 +125,12 @@ docsgen = [
127
125
  """sphinx-build -a -d .auxiliary/caches/sphinx --quiet \
128
126
  documentation .auxiliary/artifacts/sphinx-html""",
129
127
  ]
128
+ fragments = [
129
+ """python .auxiliary/scripts/validate-fragments.py""",
130
+ ]
131
+ module-sizes = [
132
+ """python .auxiliary/scripts/validate-module-sizes.py""",
133
+ ]
130
134
  linters = [
131
135
  """ruff check --quiet sources documentation tests""",
132
136
  # """vibelinter check""",
@@ -134,6 +138,8 @@ linters = [
134
138
  # --- END: Injected by Copier ---
135
139
  """isort --check-only --diff sources tests""",
136
140
  """pyright sources""",
141
+ 'fragments',
142
+ 'module-sizes',
137
143
  ]
138
144
  packagers = [
139
145
  """hatch build""",
@@ -170,12 +176,10 @@ description = ''' Quality assurance environment. '''
170
176
  template = 'develop'
171
177
  [[tool.hatch.envs.qa.matrix]]
172
178
  python = [
173
- '3.10',
174
- '3.11',
175
179
  '3.12',
176
180
  '3.13',
177
181
  '3.14',
178
- 'pypy3.11',
182
+ '3.15',
179
183
  ]
180
184
  # [tool.hatch.metadata]
181
185
  # allow-direct-references = true
@@ -297,6 +297,7 @@ sources/
297
297
  ├── classes.py # Standard metaclasses and base classes
298
298
  ├── decorators.py # Standard class decorators
299
299
  ├── dynadoc.py # Documentation configuration
300
+ ├── explanations.py # Attribute behavior explanations
300
301
  ├── modules.py # Module reclassification utilities
301
302
  └── nomina.py # Standard-specific naming types
302
303
  ```
@@ -378,6 +379,20 @@ sources/
378
379
  - `survey_visible_attributes`: Filter attributes for `dir()`
379
380
  - Verifier processing for names, regexes, and predicates
380
381
 
382
+ `explanations.py`
383
+ : Explanations of attribute behavior decisions:
384
+
385
+ - `explain_attribute`: Decision trace for one attribute of one target
386
+ - `AttributeExplanation` and the `Verdict` records (`AssignVerdict`,
387
+ `DeleteVerdict`, `SurveyVerdict`) with derived permissibility
388
+ - The closed `Decision` hierarchy: `PermitByInapplicability`,
389
+ `PermitByOmni`, `PermitByNames`, `PermitByPredicate`,
390
+ `PermitByRegex`, `Prohibit`, with typed payloads
391
+ - Internal marking via metaclass-carried contributions: the namer
392
+ detector for framework names, the abstract-base wiring for stdlib
393
+ ABC machinery (only where that machinery operates), and downstream
394
+ factory namers' detectors for downstream names
395
+
381
396
  `classes.py`
382
397
  : Standard metaclasses and base classes:
383
398
 
@@ -0,0 +1,95 @@
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
+ ''' Common names and type aliases. '''
22
+
23
+
24
+ from . import imports as __
25
+
26
+
27
+ ComparisonResult: __.typx.TypeAlias = bool | __.types.NotImplementedType
28
+ NominativeArguments: __.typx.TypeAlias = __.cabc.Mapping[ str, __.typx.Any ]
29
+ PositionalArguments: __.typx.TypeAlias = __.cabc.Sequence[ __.typx.Any ]
30
+
31
+
32
+ package_name = __name__.split( '.', maxsplit = 1 )[ 0 ]
33
+
34
+
35
+ _levels = frozenset( ( 'class', 'classes', 'instance', 'instances' ) )
36
+ _cores = frozenset( (
37
+ 'behaviors',
38
+ 'construction_arguments',
39
+ 'dynadoc_configuration',
40
+ 'in_progress',
41
+ 'mutables_names', 'mutables_predicates', 'mutables_regexes',
42
+ 'visibles_names', 'visibles_predicates', 'visibles_regexes',
43
+ 'assigner_core', 'deleter_core', 'surveyor_core' ) )
44
+ _hex_digits = frozenset( '0123456789abcdef' )
45
+ _digest_length = 64
46
+ _mangled_cores = frozenset( ( 'class_behaviors', 'class_in_progress' ) )
47
+
48
+
49
+ class AttrnameCalculator:
50
+ ''' Calculates framework attribute names from level and core.
51
+
52
+ Reference implementation of the attribute namer convention:
53
+ call instances to produce names, and consult ``is_internal_name``
54
+ to detect names this framework generated. Downstream packages
55
+ may subclass (or otherwise provide callables exposing the same
56
+ optional detector) to contribute their own internal names.
57
+ '''
58
+
59
+ def is_internal_name( self, name: str ) -> bool:
60
+ prefix = f"_{package_name}_"
61
+ if not name.startswith( prefix ):
62
+ return False
63
+ body = name[ len( prefix ): ]
64
+ # Static stems: _{package}_{level}_{core}_, trailing underscore.
65
+ # The class-level behaviors and in_progress stems are stored
66
+ # mangled only, so they are excluded from static acceptance.
67
+ if body.endswith( '_' ):
68
+ level, _, core = body[ :-1 ].partition( '_' )
69
+ if level in _levels and core in _cores:
70
+ return not (
71
+ 'class' == level
72
+ and f"{level}_{core}" in _mangled_cores )
73
+ return False
74
+ # Mangled forms: one of the mangled cores + 64 lowercase hex,
75
+ # no trailing underscore. The mangling accessors append digests
76
+ # to these stems only.
77
+ for mangled_core in _mangled_cores:
78
+ mangled_prefix = f"{mangled_core}_"
79
+ if body.startswith( mangled_prefix ):
80
+ suffix = body[ len( mangled_prefix ): ]
81
+ return (
82
+ _digest_length == len( suffix )
83
+ and all( c in _hex_digits for c in suffix ) )
84
+ return False
85
+
86
+ def __call__( self, level: str, core: str ) -> str:
87
+ return f"_{package_name}_{level}_{core}_"
88
+
89
+
90
+ calculate_attrname = AttrnameCalculator( )
91
+
92
+
93
+ def calculate_contribution_name( ) -> str:
94
+ ''' Returns metaclass attribute name holding internal-name detectors. '''
95
+ return f"_{package_name}_internal_names_"
@@ -62,7 +62,7 @@ from .factories import *
62
62
 
63
63
 
64
64
  __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ]
65
- __version__ = '1.12'
65
+ __version__ = '1.14'
66
66
 
67
67
 
68
68
  standard.finalize_module(
@@ -54,12 +54,20 @@ def produce_class_constructor(
54
54
  # Some decorators create new classes, which invokes this method again.
55
55
  # Short-circuit to prevent recursive decoration and other tangles.
56
56
  progress_name = attributes_namer( 'class', 'in_progress' )
57
- progress_name_m = _utilities.mangle_name( cls, progress_name )
58
- in_progress = getattr( cls, progress_name_m, False )
57
+ progress_names = _utilities.survey_mangled_names( cls, progress_name )
58
+ in_progress = any(
59
+ getattr( cls, key, False ) for key in progress_names )
59
60
  if in_progress: return cls
61
+ progress_name_m = _utilities.mangle_name( cls, progress_name )
60
62
  setattr( cls, progress_name_m, True )
61
63
  for postprocessor in postprocessors: postprocessor( cls, decorators_ )
62
64
  cls = _decorators.apply_decorators( cls, decorators_ )
65
+ # Decoration may have replaced the class. Clear every marker
66
+ # variant, including ones inherited from the original namespace,
67
+ # so initialization completes regardless of qualname restoration.
68
+ progress_names = _utilities.survey_mangled_names( cls, progress_name )
69
+ for progress_name_ in progress_names:
70
+ setattr( cls, progress_name_, False )
63
71
  setattr( cls, progress_name_m, False )
64
72
  return cls
65
73
 
@@ -81,10 +89,11 @@ def produce_class_initializer(
81
89
  ''' Initializes class, applying hooks. '''
82
90
  superf( *posargs, **nomargs )
83
91
  progress_name = attributes_namer( 'class', 'in_progress' )
84
- progress_name_m = _utilities.mangle_name( cls, progress_name )
85
- in_progress = getattr( cls, progress_name_m, False )
92
+ progress_names = _utilities.survey_mangled_names( cls, progress_name )
93
+ in_progress = any(
94
+ getattr( cls, key, False ) for key in progress_names )
86
95
  if in_progress: return # If non-empty, then not top-level.
87
- delattr( cls, progress_name_m )
96
+ for progress_name_m in progress_names: delattr( cls, progress_name_m )
88
97
  for completer in completers: completer( cls )
89
98
 
90
99
  return initialize
@@ -0,0 +1,106 @@
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
+ ''' Common constants, imports, and utilities. '''
22
+
23
+ # ruff: noqa: F403, F405
24
+
25
+
26
+ from ..__ import *
27
+ from ..nomina import is_public_identifier
28
+
29
+
30
+ def provide_error_class( name: str ) -> type[ Exception ]:
31
+ ''' Produces error class for this package. '''
32
+ match name:
33
+ case 'AttributeImmutability':
34
+ from ..exceptions import AttributeImmutability as error
35
+ case _:
36
+ from ..exceptions import ErrorProvideFailure
37
+ raise ErrorProvideFailure( name, reason = 'Does not exist.' )
38
+ return error
39
+
40
+
41
+ mutables_default = ( )
42
+ visibles_default = ( is_public_identifier, )
43
+
44
+ # Standard library ABC machinery attribute names which the class-level
45
+ # immutability behavior permits. The C _abc implementation writes these
46
+ # cache attributes through direct dict access, so they must remain
47
+ # assignable on standard classes. Also used to mark such names as
48
+ # internal in attribute explanations.
49
+ abc_class_mutables = (
50
+ '_abc_cache',
51
+ '_abc_negative_cache',
52
+ '_abc_negative_cache_version',
53
+ '_abc_registry',
54
+ )
55
+ # Standard library typing machinery attribute names which the
56
+ # class-level immutability behavior permits on protocol classes.
57
+ # 'typing' sets these protocol bookkeeping attributes during class
58
+ # decoration: '_is_runtime_protocol' and '__non_callable_proto_members__'
59
+ # in the 'runtime_checkable' decorator, and
60
+ # '__typing_is_deprecated_inherited_runtime_protocol__' (CPython 3.15+)
61
+ # during class decoration and in 'runtime_checkable'. Without this
62
+ # exemption, decorating a runtime protocol which inherits runtime
63
+ # checkability fails. Also used to mark such names as internal in
64
+ # attribute explanations.
65
+ typing_class_mutables = (
66
+ '_is_runtime_protocol',
67
+ '__non_callable_proto_members__',
68
+ '__typing_is_deprecated_inherited_runtime_protocol__', # Python 3.15+
69
+ )
70
+
71
+
72
+ def is_same_detector(
73
+ detector: cabc.Callable[ [ str ], bool ],
74
+ other: cabc.Callable[ [ str ], bool ], /,
75
+ ) -> bool:
76
+ ''' Returns whether two detector references are the same detector.
77
+
78
+ Bound methods are the same detector only when both the
79
+ underlying function and the bound instance are identical, so
80
+ distinct namer instances with distinct configured behavior keep
81
+ separate contributions. Plain callables compare by identity.
82
+ '''
83
+ if detector is other: return True
84
+ function = getattr( detector, '__func__', None )
85
+ other_function = getattr( other, '__func__', None )
86
+ if function is None or other_function is None: return False
87
+ return (
88
+ function is other_function
89
+ and getattr( detector, '__self__', None )
90
+ is getattr( other, '__self__', None ) )
91
+
92
+
93
+ def augment_internal_names(
94
+ cls: type, /, detector: cabc.Callable[ [ str ], bool ]
95
+ ) -> None:
96
+ ''' Adds an internal-name detector to existing contributions.
97
+
98
+ Private wiring point for machinery mixins: the abstract-base
99
+ metaclass adds the stdlib machinery set after factory
100
+ decoration, without expanding public factory arguments.
101
+ '''
102
+ detectors = getattr( cls, calculate_contribution_name( ), ( ) )
103
+ if any( is_same_detector( detector, d ) for d in detectors ): return
104
+ setattr(
105
+ cls, calculate_contribution_name( ),
106
+ ( *detectors, detector ) )
@@ -49,4 +49,5 @@ from . import nomina
49
49
 
50
50
  from .classes import *
51
51
  from .decorators import *
52
+ from .explanations import *
52
53
  from .modules import *
@@ -58,34 +58,17 @@ def assign_attribute_if_mutable( # noqa: PLR0913
58
58
  value: __.typx.Any,
59
59
  ) -> None:
60
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( ) )
61
+ behaviors = survey_active_behaviors(
62
+ obj, attributes_namer = attributes_namer, level = level )
64
63
  if _nomina.immutability_label not in behaviors:
65
64
  ligation( name, value )
66
65
  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:
66
+ rule = provide_permitter_if_applicable(
67
+ obj, attributes_namer = attributes_namer,
68
+ level = level, basename = 'mutables', name = name )
69
+ if rule is not None:
71
70
  ligation( name, value )
72
71
  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
72
  target = _utilities.describe_object( obj )
90
73
  raise error_class_provider( 'AttributeImmutability' )( name, target )
91
74
 
@@ -99,36 +82,147 @@ def delete_attribute_if_mutable( # noqa: PLR0913
99
82
  name: str,
100
83
  ) -> None:
101
84
  ''' 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( ) )
85
+ behaviors = survey_active_behaviors(
86
+ obj, attributes_namer = attributes_namer, level = level )
105
87
  if _nomina.immutability_label not in behaviors:
106
88
  ligation( name )
107
89
  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:
90
+ rule = provide_permitter_if_applicable(
91
+ obj, attributes_namer = attributes_namer,
92
+ level = level, basename = 'mutables', name = name )
93
+ if rule is not None:
112
94
  ligation( name )
113
95
  return
114
- predicates_name = attributes_namer( level, 'mutables_predicates' )
96
+ target = _utilities.describe_object( obj )
97
+ raise error_class_provider( 'AttributeImmutability' )( name, target )
98
+
99
+
100
+ def determine_relevant_permitters(
101
+ objct: object, /, *,
102
+ attributes_namer: _nomina.AttributesNamer,
103
+ level: str,
104
+ basename: str,
105
+ name: str,
106
+ ) -> tuple[ tuple[ str, __.typx.Any ], ... ]:
107
+ ''' Returns exclusion rules matching name, in evaluation order.
108
+
109
+ A names match short-circuits, matching the survey core's
110
+ historical control flow: predicates and regexes are not evaluated
111
+ for a name found in the exclusion names. Otherwise a name may
112
+ match any number of predicates and any number of regexes. The
113
+ wildcard is not considered here; callers handle its
114
+ short-circuit separately.
115
+
116
+ Rules pair a kind ('names', 'predicate', 'regex') with the raw
117
+ matched verifier (name, predicate, or regex). Matching never
118
+ renders diagnostic text: enforcement and survey consult kind
119
+ and presence only, so exotic callables cannot disrupt them.
120
+ Render verifiers with 'render_verifier_text' at the record
121
+ layer when producing explanation records.
122
+ '''
123
+ names_name = attributes_namer( level, f"{basename}_names" )
124
+ names: _nomina.BehaviorExclusionNamesOmni = (
125
+ getattr( objct, names_name, frozenset( ) ) )
126
+ if name in names: return ( ( 'names', name ), )
127
+ predicates_name = attributes_namer( level, f"{basename}_predicates" )
115
128
  predicates: _nomina.BehaviorExclusionPredicates = (
116
- getattr( obj, predicates_name, ( ) ) )
129
+ getattr( objct, predicates_name, ( ) ) )
130
+ matched: list[ tuple[ str, __.typx.Any ] ] = [
131
+ ( 'predicate', predicate )
132
+ for predicate in predicates if predicate( name ) ]
133
+ regexes_name = attributes_namer( level, f"{basename}_regexes" )
134
+ regexes: _nomina.BehaviorExclusionRegexes = (
135
+ getattr( objct, regexes_name, ( ) ) )
136
+ matched.extend(
137
+ ( 'regex', regex )
138
+ for regex in regexes if regex.fullmatch( name ) )
139
+ return tuple( matched )
140
+
141
+
142
+ def provide_permitter_if_applicable(
143
+ objct: object, /, *,
144
+ attributes_namer: _nomina.AttributesNamer,
145
+ level: str,
146
+ basename: str,
147
+ name: str,
148
+ ) -> __.typx.Optional[ tuple[ str, __.typx.Any ] ]:
149
+ ''' Returns first exclusion rule permitting operation, else None.
150
+
151
+ Rules apply by precedence: omni or names membership, then
152
+ the first matching predicate, then the first matching regex.
153
+ Each rule pairs a kind ('omni', 'names', 'predicate', 'regex')
154
+ with the raw matched verifier. Matching never renders
155
+ diagnostic text: enforcement consults kind and presence only,
156
+ so exotic callables cannot disrupt it. Render the verifier
157
+ with 'render_verifier_text' at the record layer when producing
158
+ explanation records.
159
+ '''
160
+ names_name = attributes_namer( level, f"{basename}_names" )
161
+ names: _nomina.BehaviorExclusionNamesOmni = (
162
+ getattr( objct, names_name, frozenset( ) ) )
163
+ if names == '*': return ( 'omni', '*' )
164
+ if name in names: return ( 'names', name )
165
+ predicates_name = attributes_namer( level, f"{basename}_predicates" )
166
+ predicates: _nomina.BehaviorExclusionPredicates = (
167
+ getattr( objct, predicates_name, ( ) ) )
117
168
  for predicate in predicates:
118
169
  if predicate( name ):
119
- # TODO? Cache predicate hit.
120
- ligation( name )
121
- return
122
- regexes_name = attributes_namer( level, 'mutables_regexes' )
170
+ return ( 'predicate', predicate )
171
+ regexes_name = attributes_namer( level, f"{basename}_regexes" )
123
172
  regexes: _nomina.BehaviorExclusionRegexes = (
124
- getattr( obj, regexes_name, ( ) ) )
173
+ getattr( objct, regexes_name, ( ) ) )
125
174
  for regex in regexes:
126
175
  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 )
176
+ return ( 'regex', regex )
177
+ return None
178
+
179
+
180
+ def render_verifier_text( verifier: __.typx.Any, / ) -> str:
181
+ ''' Returns stable detail text for an exclusion verifier.
182
+
183
+ Regexes render as pattern text; predicates render as
184
+ fully-qualified name, with an anonymous fallback when no
185
+ qualified name is available. Strings do not reach this
186
+ function; they are classified as names before rendering. A
187
+ non-string 'pattern' attribute is not trusted as text; such
188
+ verifiers fall through to qualified-name rendering, where a
189
+ non-string qualified name falls back to '<anonymous>' while
190
+ a non-string module name preserves the qualified name.
191
+ Hostile metadata (raising descriptors, custom
192
+ '__getattribute__') yields '<unrenderable>' rather than
193
+ raising, so diagnostic rendering never disrupts its callers.
194
+ The broad 'except Exception' is deliberate: rendering is
195
+ diagnostics, and diagnostics must never break enforcement
196
+ or survey paths.
197
+
198
+ Note: the inspect utilities were considered and rejected here;
199
+ they resolve modules to objects and provide no qualified-name
200
+ rendering with an anonymous fallback, while this getattr chain
201
+ stays defensive for exotic callables (e.g., partials).
202
+ '''
203
+ try:
204
+ text = _render_verifier_text( verifier )
205
+ except Exception:
206
+ text = '<unrenderable>'
207
+ return text
208
+
209
+
210
+ def survey_active_behaviors(
211
+ objct: object, /, *,
212
+ attributes_namer: _nomina.AttributesNamer,
213
+ level: str,
214
+ ) -> __.cabc.Set[ str ]:
215
+ ''' Returns active behaviors set for object at level.
216
+
217
+ This is the shared entry point of the behavior decision cascade:
218
+ every core first determines whether the governing behavior is
219
+ active before consulting exclusion configuration.
220
+ '''
221
+ leveli = 'instance' if level == 'instances' else level
222
+ behaviors_name = attributes_namer( leveli, 'behaviors' )
223
+ behaviors: __.cabc.Set[ str ] = (
224
+ _utilities.getattr0( objct, behaviors_name, frozenset( ) ) )
225
+ return behaviors
132
226
 
133
227
 
134
228
  def survey_visible_attributes(
@@ -139,35 +233,22 @@ def survey_visible_attributes(
139
233
  ) -> __.cabc.Iterable[ str ]:
140
234
  ''' Returns sequence of visible attributes. '''
141
235
  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( ) )
236
+ behaviors = survey_active_behaviors(
237
+ obj, attributes_namer = attributes_namer, level = level )
145
238
  if _nomina.concealment_label not in behaviors: return names_base
146
239
  names_name = attributes_namer( level, 'visibles_names' )
147
240
  names: _nomina.BehaviorExclusionNamesOmni = (
148
241
  getattr( obj, names_name, frozenset( ) ) )
149
242
  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
243
  names_: list[ str ] = [ ]
157
244
  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
245
+ matched = determine_relevant_permitters(
246
+ obj, attributes_namer = attributes_namer,
247
+ level = level, basename = 'visibles', name = name )
248
+ # Current behavior: one append per matched rule, so a name
249
+ # matching several rules is yielded several times. Preserved
250
+ # deliberately; normalization is a separate repair.
251
+ names_.extend( name for rule in matched )
171
252
  return names_
172
253
 
173
254
 
@@ -412,3 +493,21 @@ def _deduplicate_merge_sequences(
412
493
  if addend in augends_: continue
413
494
  result.append( addend )
414
495
  return tuple( result )
496
+
497
+
498
+ def _render_verifier_text( verifier: __.typx.Any, / ) -> str:
499
+ ''' Returns detail text for an exclusion verifier, possibly raising.
500
+
501
+ Total rendering lives in 'render_verifier_text', which maps
502
+ hostile-metadata failures to the documented fallback. This
503
+ partial function holds the getattr chain so the wrapper has
504
+ no returns inside its guarded block.
505
+ '''
506
+ pattern = getattr( verifier, 'pattern', None )
507
+ if isinstance( pattern, str ): return pattern
508
+ qualname = getattr( verifier, '__qualname__', None )
509
+ if not isinstance( qualname, str ): return '<anonymous>'
510
+ module = getattr( verifier, '__module__', None )
511
+ if not isinstance( module, str ) or '<locals>' in qualname:
512
+ return qualname
513
+ return f"{module}.{qualname}"
@@ -27,14 +27,6 @@ from . import dynadoc as _dynadoc
27
27
  from . import nomina as _nomina
28
28
 
29
29
 
30
- _abc_class_mutables = (
31
- '_abc_cache',
32
- '_abc_negative_cache',
33
- '_abc_negative_cache_version',
34
- '_abc_registry',
35
- '_is_runtime_protocol',
36
- '__non_callable_proto_members__',
37
- )
38
30
  _protocol_cls_set = frozenset( { __.typx.Protocol } )
39
31
  # Attributes never considered declared protocol members: typing internals,
40
32
  # class-creation machinery, and framework attributes declared in classcore
@@ -47,7 +39,12 @@ _protocol_attr_excluded = frozenset( {
47
39
  # Injected during class creation by ABC, Generic, and typing machinery.
48
40
  # '__init__' is '_no_init' on protocol bases; as a structural member it
49
41
  # is vacuous since every candidate object has an '__init__'.
42
+ # '__typing_is_deprecated_inherited_runtime_protocol__' is set by
43
+ # CPython 3.15+ typing on protocol classes which inherit runtime
44
+ # checkability; as a structural member it is vacuous since it records
45
+ # machinery provenance rather than a callable member.
50
46
  '__abstractmethods__', '__parameters__', '__init__', '__orig_bases__',
47
+ '__typing_is_deprecated_inherited_runtime_protocol__', # Python 3.15+
51
48
  } )
52
49
  _protocol_attr_prefixes = ( '_classcore_', '_abc_' )
53
50
  # Standard Python attributes present in every pre-decoration namespace.
@@ -130,6 +127,10 @@ class AbstractClass( Class, __.abc.ABCMeta ):
130
127
  registration) via a diamond hierarchy. `Class` itself remains
131
128
  backed by plain `type`, so ABC machinery applies only where this
132
129
  metaclass is used.
130
+
131
+ Contributes the stdlib ABC and typing machinery names as internal
132
+ for the classes it builds, mirroring the class_mutables exemption
133
+ scoping.
133
134
  '''
134
135
 
135
136
  _dynadoc_fragments_ = (
@@ -313,7 +314,7 @@ class ObjectMutable( metaclass = Class, instances_mutables = '*' ):
313
314
 
314
315
  class AbstractObject(
315
316
  metaclass = AbstractClass,
316
- class_mutables = _abc_class_mutables,
317
+ class_mutables = __.abc_class_mutables,
317
318
  ):
318
319
  ''' Base class for abstract classes with standard behaviors.
319
320
 
@@ -348,7 +349,8 @@ class DataclassObjectMutable( metaclass = DataclassMutable ):
348
349
  class Protocol(
349
350
  __.typx.Protocol,
350
351
  metaclass = ProtocolClass,
351
- class_mutables = _abc_class_mutables,
352
+ class_mutables = (
353
+ *__.abc_class_mutables, *__.typing_class_mutables ),
352
354
  ):
353
355
  ''' Standard base protocol class. '''
354
356
 
@@ -361,7 +363,8 @@ class Protocol(
361
363
  class ProtocolMutable(
362
364
  __.typx.Protocol,
363
365
  metaclass = ProtocolClass,
364
- class_mutables = _abc_class_mutables,
366
+ class_mutables = (
367
+ *__.abc_class_mutables, *__.typing_class_mutables ),
365
368
  instances_mutables = '*',
366
369
  ):
367
370
  ''' Base protocol class with mutable instance attributes. '''
@@ -375,7 +378,8 @@ class ProtocolMutable(
375
378
  class DataclassProtocol(
376
379
  __.typx.Protocol,
377
380
  metaclass = ProtocolDataclass,
378
- class_mutables = _abc_class_mutables,
381
+ class_mutables = (
382
+ *__.abc_class_mutables, *__.typing_class_mutables ),
379
383
  ):
380
384
  ''' Standard base protocol dataclass. '''
381
385
 
@@ -388,7 +392,8 @@ class DataclassProtocol(
388
392
  class DataclassProtocolMutable(
389
393
  __.typx.Protocol,
390
394
  metaclass = ProtocolDataclassMutable,
391
- class_mutables = _abc_class_mutables,
395
+ class_mutables = (
396
+ *__.abc_class_mutables, *__.typing_class_mutables ),
392
397
  ):
393
398
  ''' Base protocol dataclass with mutable instance attributes. '''
394
399
 
@@ -434,3 +439,12 @@ class _CanaryProtocolImpl( _CanaryProtocolUse ):
434
439
  _canary_abstract: int = _CanaryAbstractObject( ).provide( )
435
440
  _canary_protocol_iface: _CanaryProtocolUse = _CanaryProtocolImpl( )
436
441
  _canary_greeting: str = _canary_protocol_iface.greet( )
442
+
443
+
444
+ def _is_stdlib_machinery_name( name: str ) -> bool:
445
+ return (
446
+ name in __.abc_class_mutables
447
+ or name in __.typing_class_mutables )
448
+
449
+
450
+ __.augment_internal_names( AbstractClass, _is_stdlib_machinery_name )
@@ -47,8 +47,10 @@ def prepare_dataclass_for_instances(
47
47
  ''' Annotates dataclass in support of instantiation machinery. '''
48
48
  annotations = __.inspect.get_annotations( cls )
49
49
  behaviors_name = attributes_namer( 'instance', 'behaviors' )
50
- # TODO: Only use mangling if not slotted.
51
- # behaviors_name_ = _utilities.mangle_name( cls, behaviors_name )
50
+ # NOTE: The behaviors field is a dataclass field, so its name must
51
+ # stay unmangled; mangling would desynchronize the field from
52
+ # the slot or class attribute installed by the dataclass
53
+ # machinery, which addresses it by its plain name.
52
54
  behaviors_name_ = behaviors_name
53
55
  annotations[ behaviors_name_ ] = set[ str ]
54
56
  setattr( cls, '__annotations__', annotations ) # in case of absence
@@ -83,6 +85,30 @@ def apply_cfc_core_functions(
83
85
  setattr( clscls, core_aname, core_function )
84
86
 
85
87
 
88
+ def apply_internal_names(
89
+ cls: type, /,
90
+ attributes_namer: _nomina.AttributesNamer,
91
+ ) -> None:
92
+ ''' Records the namer's internal-name contribution, if any.
93
+
94
+ Works for metaclasses (contributions inherit to every class the
95
+ metaclass builds) and for decorator-path classes (contributions
96
+ ride the class itself, since plain decorated classes have no
97
+ custom metaclass). Parent wiring may already contribute the
98
+ same detector (e.g., a diamond re-running the factory with the
99
+ same namer); keep one of each by identity.
100
+ '''
101
+ detector: __.typx.Optional[ _nomina.InternalNameDetector ] = (
102
+ getattr( attributes_namer, 'is_internal_name', None ) )
103
+ if detector is None: return
104
+ detectors: _nomina.InternalNameDetectors = getattr(
105
+ cls, __.calculate_contribution_name( ), ( ) )
106
+ if any( __.is_same_detector( detector, d ) for d in detectors ): return
107
+ setattr(
108
+ cls, __.calculate_contribution_name( ),
109
+ ( *detectors, detector ) )
110
+
111
+
86
112
  def apply_cfc_dynadoc_configuration(
87
113
  clscls: type[ __.T ], /,
88
114
  attributes_namer: _nomina.AttributesNamer,
@@ -192,6 +218,8 @@ def class_factory( # noqa: PLR0913, PLR0917
192
218
  assigner_core = assigner_core,
193
219
  deleter_core = deleter_core,
194
220
  surveyor_core = surveyor_core )
221
+ apply_internal_names(
222
+ clscls, attributes_namer = attributes_namer )
195
223
  apply_cfc_dynadoc_configuration(
196
224
  clscls,
197
225
  attributes_namer = attributes_namer,
@@ -243,6 +271,8 @@ def produce_instances_inception_decorator( # noqa: PLR0913, PLR0917
243
271
  surveyor = _behaviors.survey_visible_attributes )
244
272
 
245
273
  def decorate( cls: type[ __.U ] ) -> type[ __.U ]:
274
+ apply_internal_names(
275
+ cls, attributes_namer = attributes_namer )
246
276
  for core_name in ( 'assigner', 'deleter', 'surveyor' ):
247
277
  core_function = _behaviors.access_core_function(
248
278
  cls,
@@ -0,0 +1,253 @@
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
+ # 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
+ ''' Explanations of attribute behavior decisions. '''
22
+
23
+
24
+ from .. import utilities as _utilities
25
+ from . import __
26
+ from . import behaviors as _behaviors
27
+ from . import decorators as _decorators
28
+ from . import nomina as _nomina
29
+
30
+
31
+ _framework_names_prefix = '_classcore_'
32
+
33
+
34
+ @_decorators.dataclass_with_standard_behaviors( )
35
+ class Decision:
36
+ ''' Decision which one operation reached for one attribute.
37
+
38
+ The decision subclasses form a closed hierarchy; the decision
39
+ type is the discriminant. Verdict permissibility is derived
40
+ from the decision type.
41
+ '''
42
+
43
+
44
+ @_decorators.dataclass_with_standard_behaviors( )
45
+ class PermitByInapplicability( Decision ):
46
+ ''' Permission because the governing behavior is inactive. '''
47
+
48
+
49
+ @_decorators.dataclass_with_standard_behaviors( )
50
+ class PermitByOmni( Decision ):
51
+ ''' Permission by the omni marker, which permits every name. '''
52
+
53
+
54
+ @_decorators.dataclass_with_standard_behaviors( )
55
+ class PermitByNames( Decision ):
56
+ ''' Permission by exclusion names membership. '''
57
+ name: str
58
+
59
+
60
+ @_decorators.dataclass_with_standard_behaviors( )
61
+ class PermitByPredicate( Decision ):
62
+ ''' Permission by an exclusion predicate. '''
63
+ predicate: str
64
+
65
+
66
+ @_decorators.dataclass_with_standard_behaviors( )
67
+ class PermitByRegex( Decision ):
68
+ ''' Permission by an exclusion regex. '''
69
+ pattern: str
70
+
71
+
72
+ @_decorators.dataclass_with_standard_behaviors( )
73
+ class Prohibit( Decision ):
74
+ ''' Prohibition: the behavior is active and no rule permits. '''
75
+
76
+
77
+ @_decorators.dataclass_with_standard_behaviors( )
78
+ class Verdict:
79
+ ''' Verdict for one operation on one attribute. '''
80
+ operation: str
81
+ decision: Decision
82
+
83
+ @property
84
+ def permissible( self ) -> bool:
85
+ ''' Returns whether the operation is permissible. '''
86
+ return not isinstance( self.decision, Prohibit )
87
+
88
+
89
+ @_decorators.dataclass_with_standard_behaviors( )
90
+ class AssignVerdict( Verdict ):
91
+ ''' Verdict for attribute assignment. '''
92
+
93
+
94
+ @_decorators.dataclass_with_standard_behaviors( )
95
+ class DeleteVerdict( Verdict ):
96
+ ''' Verdict for attribute deletion. '''
97
+
98
+
99
+ @_decorators.dataclass_with_standard_behaviors( )
100
+ class SurveyVerdict( Verdict ):
101
+ ''' Verdict for attribute visibility in survey results. '''
102
+
103
+
104
+ @_decorators.dataclass_with_standard_behaviors( )
105
+ class AttributeExplanation:
106
+ ''' Decision trace for one attribute of one target. '''
107
+ target: str
108
+ name: str
109
+ behaviors: __.cabc.Mapping[ str, __.cabc.Set[ str ] ]
110
+ operations: __.cabc.Mapping[ str, Verdict ]
111
+ internal: bool
112
+
113
+ def __repr__( self ) -> str:
114
+ ''' Returns summary of the decision trace. '''
115
+ lines = [
116
+ f"{self.name!r} on {self.target}"
117
+ + ( ' [internal]' if self.internal else '' ) ]
118
+ for level, labels in self.behaviors.items( ):
119
+ lines.append(
120
+ f"behaviors: {', '.join( sorted( labels ) )}"
121
+ + f" ({level})" )
122
+ for operation in ( 'assign', 'delete', 'survey' ):
123
+ verdict = self.operations.get( operation )
124
+ if verdict is None: continue # pragma: no cover
125
+ lines.append( f"{operation}: {self._render( verdict.decision )}" )
126
+ return '\n'.join( lines )
127
+
128
+ def _render( self, decision: Decision, / ) -> str:
129
+ if isinstance( decision, PermitByInapplicability ):
130
+ return 'permitted (behavior inapplicable)'
131
+ if isinstance( decision, PermitByOmni ):
132
+ return "permitted by omni '*'"
133
+ if isinstance( decision, PermitByNames ):
134
+ return f"permitted by names {decision.name!r}"
135
+ if isinstance( decision, PermitByPredicate ):
136
+ return f"permitted by predicate {decision.predicate!r}"
137
+ if isinstance( decision, PermitByRegex ):
138
+ return f"permitted by regex {decision.pattern!r}"
139
+ if isinstance( decision, Prohibit ):
140
+ return 'prohibited (no permitting rule)'
141
+ raise ValueError( type( decision ) )
142
+
143
+
144
+ def explain_attribute(
145
+ target: object, name: str, /, *,
146
+ attributes_namer: _nomina.AttributesNamer = __.calculate_attrname,
147
+ ) -> AttributeExplanation:
148
+ ''' Returns decision trace for attribute name on target.
149
+
150
+ All operations follow precedence semantics: omni, then names
151
+ membership, then the first matching predicate, then the first
152
+ matching regex; prohibition when the governing behavior is
153
+ active and nothing permits; permission by inapplicability when
154
+ the behavior is inactive. For class targets, the classes-level
155
+ configuration is evaluated; for instance targets, the
156
+ instances-level configuration is evaluated against the
157
+ instance's class hierarchy — matching the levels the behavior
158
+ cores evaluate. Records report the normalized level names
159
+ ('class' and 'instance') at which behaviors and exclusion
160
+ configuration were found. Explanations are observational: they
161
+ neither bypass nor alter concealment or immutability. They are
162
+ not side-effect-free for arbitrary callbacks, however:
163
+ producing a record invokes user predicates and internal-name
164
+ detectors to determine the same outcome the cores would.
165
+
166
+ Survey explanations describe normalized first-match semantics;
167
+ the survey core's current once-per-matching-rule duplication is
168
+ characterized as a defect whose repair is deferred separately.
169
+ '''
170
+ class_target = __.inspect.isclass( target )
171
+ level = 'class' if class_target else 'instances'
172
+ level_normalized = 'class' if class_target else 'instance'
173
+ behaviors = _behaviors.survey_active_behaviors(
174
+ target, attributes_namer = attributes_namer, level = level )
175
+ immutability = _nomina.immutability_label in behaviors
176
+ concealment = _nomina.concealment_label in behaviors
177
+ operations: dict[ str, Verdict ] = { }
178
+ for operation, verdict_class, basename, active in (
179
+ ( 'assign', AssignVerdict, 'mutables', immutability ),
180
+ ( 'delete', DeleteVerdict, 'mutables', immutability ),
181
+ ( 'survey', SurveyVerdict, 'visibles', concealment ),
182
+ ):
183
+ rule = (
184
+ _behaviors.provide_permitter_if_applicable(
185
+ target, attributes_namer = attributes_namer,
186
+ level = level, basename = basename, name = name )
187
+ if active else None )
188
+ operations[ operation ] = verdict_class(
189
+ operation = operation,
190
+ decision = produce_decision( rule, active ) )
191
+ return AttributeExplanation(
192
+ target = _utilities.describe_object( target ),
193
+ name = name,
194
+ behaviors = __.types.MappingProxyType(
195
+ { level_normalized: frozenset( behaviors ) } ),
196
+ operations = __.types.MappingProxyType( operations ),
197
+ internal = is_internal_name( target, name ) )
198
+
199
+
200
+ def is_internal_name( target: object, name: str, / ) -> bool:
201
+ ''' Returns whether name is internal to the target's machinery.
202
+
203
+ Contributions are discovered on the metaclass chain: for a
204
+ class target, its metaclass method resolution order; for an
205
+ instance target, the metaclass resolution order of its type
206
+ (the metaclass chain is reached through the double type
207
+ transformation — the instance's own type resolution order
208
+ enumerates the class bases, never the metaclass). Decorator-
209
+ based classes carry their contributions on the class itself,
210
+ so the class resolution order is consulted as well. Each
211
+ contribution is a detector produced by the machinery which
212
+ built the class, so a name is internal only where the
213
+ machinery that generates it operates. Classification scope is
214
+ the machinery chain and is independent of per-class mutability
215
+ exemptions: a name may be internal on a class where it is not
216
+ mutable, and mutable where it is not internal.
217
+ '''
218
+ cls = target if __.inspect.isclass( target ) else type( target )
219
+ chains = (
220
+ type( cls ).__mro__, cls.__mro__ ) # Metaclass chain, then class.
221
+ contribution_name = __.calculate_contribution_name( )
222
+ for chain in chains:
223
+ for holder in chain:
224
+ contributions = holder.__dict__.get( contribution_name )
225
+ if not contributions: continue
226
+ for detector in contributions:
227
+ if detector( name ): return True
228
+ return False
229
+
230
+
231
+ def produce_decision(
232
+ rule: __.typx.Optional[ tuple[ str, __.typx.Any ] ], active: bool, /
233
+ ) -> Decision:
234
+ ''' Returns decision for governing-behavior activity and first rule.
235
+
236
+ Rules are the (kind, raw verifier) pairs produced by
237
+ provide_permitter_if_applicable; the kind vocabulary matches the
238
+ omni/names/predicate/regex precedence stages. Verifier detail
239
+ text is rendered here, at the record layer, so enforcement
240
+ paths which consult the same rules never depend on rendering.
241
+ '''
242
+ if not active: return PermitByInapplicability( )
243
+ if rule is None: return Prohibit( )
244
+ kind, verifier = rule
245
+ if kind == 'omni': return PermitByOmni( )
246
+ if kind == 'names': return PermitByNames( name = verifier )
247
+ if kind == 'predicate':
248
+ return PermitByPredicate(
249
+ predicate = _behaviors.render_verifier_text( verifier ) )
250
+ if kind == 'regex':
251
+ return PermitByRegex(
252
+ pattern = _behaviors.render_verifier_text( verifier ) )
253
+ raise ValueError( kind )
@@ -29,6 +29,10 @@ concealment_label = 'concealment'
29
29
  immutability_label = 'immutability'
30
30
 
31
31
 
32
+ abc_class_mutables = __.abc_class_mutables
33
+ typing_class_mutables = __.typing_class_mutables
34
+
35
+
32
36
  BehaviorExclusionNames: __.typx.TypeAlias = __.cabc.Set[ str ]
33
37
  BehaviorExclusionNamesOmni: __.typx.TypeAlias = (
34
38
  BehaviorExclusionNames | __.typx.Literal[ '*' ] )
@@ -45,6 +49,10 @@ BehaviorExclusionVerifiers: __.typx.TypeAlias = (
45
49
  __.cabc.Sequence[ BehaviorExclusionVerifier ] )
46
50
  BehaviorExclusionVerifiersOmni: __.typx.TypeAlias = (
47
51
  BehaviorExclusionVerifiers | __.typx.Literal[ '*' ] )
52
+ InternalNameDetector: __.typx.TypeAlias = (
53
+ __.cabc.Callable[ [ str ], bool ] )
54
+ InternalNameDetectors: __.typx.TypeAlias = (
55
+ __.cabc.Sequence[ InternalNameDetector ] )
48
56
  ErrorClassProvider: __.typx.TypeAlias = __.typx.Annotated[
49
57
  __.cabc.Callable[ [ str ], type[ Exception ] ],
50
58
  __.ddoc.Doc(
@@ -95,17 +95,42 @@ def mangle_name( objct: object, /, name: str ) -> str:
95
95
  Effectively provides name of private member attribute,
96
96
  which is unique across class inheritance.
97
97
  '''
98
- # TODO: Replace expensive SHA-256 hash with simple 'id'.
99
- # Need to debug weird issue with using 'id' early on dataclasses.
98
+ # NOTE: Mangling is based on the fully-qualified class name, not on id,
99
+ # because class reproductions (e.g., from dataclass slots) must
100
+ # retain identity of private attributes. A reproduction is a new
101
+ # object, so id-based mangling would break such identity.
100
102
  if not __.inspect.isclass( objct ):
101
103
  return mangle_name( type( objct ), name )
102
- # return "{name}{uid}".format( name = name, uid = id( objct ) )
103
104
  namehash = __.hashlib.sha256( )
104
105
  namehash.update( qualify_class_name( objct ).encode( ) )
105
106
  namehash_hex = namehash.hexdigest( )
106
107
  return f"{name}{namehash_hex}"
107
108
 
108
109
 
110
+ def survey_mangled_names( cls: type, /, name: str ) -> tuple[ str, ... ]:
111
+ ''' Returns mangled variants of name present in class's own namespace.
112
+
113
+ A class reproduction (e.g., from ``dataclass( slots = ... )``) is
114
+ created with a bare ``__qualname__``, which is restored only after
115
+ the reproduction is constructed. During that window, mangled names
116
+ computed from the reproduction differ from those computed from the
117
+ original. Detection by prefix survives such qualname drift.
118
+ '''
119
+ suffix_start = len( name )
120
+ return tuple(
121
+ key for key in cls.__dict__
122
+ if key.startswith( name ) and _is_digest( key[ suffix_start: ] ) )
123
+
124
+
125
+ _digest_length = 64 # Length of a hexadecimal SHA-256 digest.
126
+
127
+
128
+ def _is_digest( text: str ) -> bool:
129
+ ''' Returns whether text is a hexadecimal SHA-256 digest. '''
130
+ return _digest_length == len( text ) and all(
131
+ character in '0123456789abcdef' for character in text )
132
+
133
+
109
134
  def qualify_class_name( cls: type ) -> str:
110
135
  ''' Returns fully-qualified class name. '''
111
136
  return f"{cls.__module__}.{cls.__qualname__}"
@@ -1,36 +0,0 @@
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
- ''' Common names and type aliases. '''
22
-
23
-
24
- from . import imports as __
25
-
26
-
27
- ComparisonResult: __.typx.TypeAlias = bool | __.types.NotImplementedType
28
- NominativeArguments: __.typx.TypeAlias = __.cabc.Mapping[ str, __.typx.Any ]
29
- PositionalArguments: __.typx.TypeAlias = __.cabc.Sequence[ __.typx.Any ]
30
-
31
-
32
- package_name = __name__.split( '.', maxsplit = 1 )[ 0 ]
33
-
34
-
35
- def calculate_attrname( level: str, core: str ) -> str:
36
- return f"_{package_name}_{level}_{core}_"
@@ -1,42 +0,0 @@
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
- ''' Common constants, imports, and utilities. '''
22
-
23
- # ruff: noqa: F403
24
-
25
-
26
- from ..__ import *
27
- from ..nomina import is_public_identifier
28
-
29
-
30
- def provide_error_class( name: str ) -> type[ Exception ]:
31
- ''' Produces error class for this package. '''
32
- match name:
33
- case 'AttributeImmutability':
34
- from ..exceptions import AttributeImmutability as error
35
- case _:
36
- from ..exceptions import ErrorProvideFailure
37
- raise ErrorProvideFailure( name, reason = 'Does not exist.' )
38
- return error
39
-
40
-
41
- mutables_default = ( )
42
- visibles_default = ( is_public_identifier, )
File without changes
File without changes
File without changes