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.
- {classcore-1.12 → classcore-1.14}/PKG-INFO +3 -5
- {classcore-1.12 → classcore-1.14}/pyproject.toml +12 -8
- {classcore-1.12 → classcore-1.14}/sources/classcore/README.md +15 -0
- classcore-1.14/sources/classcore/__/nomina.py +95 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/__init__.py +1 -1
- {classcore-1.12 → classcore-1.14}/sources/classcore/factories.py +14 -5
- classcore-1.14/sources/classcore/standard/__.py +106 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/standard/__init__.py +1 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/standard/behaviors.py +163 -64
- {classcore-1.12 → classcore-1.14}/sources/classcore/standard/classes.py +27 -13
- {classcore-1.12 → classcore-1.14}/sources/classcore/standard/decorators.py +32 -2
- classcore-1.14/sources/classcore/standard/explanations.py +253 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/standard/nomina.py +8 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/utilities.py +28 -3
- classcore-1.12/sources/classcore/__/nomina.py +0 -36
- classcore-1.12/sources/classcore/standard/__.py +0 -42
- {classcore-1.12 → classcore-1.14}/.gitignore +0 -0
- {classcore-1.12 → classcore-1.14}/LICENSE.txt +0 -0
- {classcore-1.12 → classcore-1.14}/README.rst +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/__/__init__.py +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/__/doctab.py +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/__/imports.py +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/decorators.py +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/exceptions.py +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/nomina.py +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/py.typed +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/standard/README.md +0 -0
- {classcore-1.12 → classcore-1.14}/sources/classcore/standard/dynadoc.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
'
|
|
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_"
|
|
@@ -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
|
-
|
|
58
|
-
in_progress =
|
|
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
|
-
|
|
85
|
-
in_progress =
|
|
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 ) )
|
|
@@ -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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
120
|
-
|
|
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(
|
|
173
|
+
getattr( objct, regexes_name, ( ) ) )
|
|
125
174
|
for regex in regexes:
|
|
126
175
|
if regex.fullmatch( name ):
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
#
|
|
51
|
-
#
|
|
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
|
-
#
|
|
99
|
-
#
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|