From f1ba7f8f9449f0ae1cff61288799a545de395113 Mon Sep 17 00:00:00 2001 From: Eric McDonald Date: Tue, 29 Apr 2025 15:45:37 -0700 Subject: [PATCH 01/28] Version: 1.0rc0 --- sources/classcore/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 25701a0..91a673f 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -32,7 +32,7 @@ from .factories import * -__version__ = '1.0a0' +__version__ = '1.0rc0' standard.reclassify_modules( __name__, recursive = True ) From ad7e53fd7d1ccf46e657945545a0032661732dcc Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Tue, 29 Apr 2025 22:59:26 +0000 Subject: [PATCH 02/28] Update documents for publication. --- .auxiliary/publications/website.tar.xz | 3 +++ sources/classcore/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .auxiliary/publications/website.tar.xz diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz new file mode 100644 index 0000000..1402a0b --- /dev/null +++ b/.auxiliary/publications/website.tar.xz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:097802c0a82602474514eb75537e823caf8fc6559445fb13a90d31bc9f7579ef +size 142544 diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 91a673f..784b753 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -32,7 +32,7 @@ from .factories import * -__version__ = '1.0rc0' +__version__ = '1.1a0' standard.reclassify_modules( __name__, recursive = True ) From 054408fd510679272b4e3af44eb591562576394a Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Tue, 29 Apr 2025 23:24:30 +0000 Subject: [PATCH 03/28] Update documents for publication. --- .auxiliary/publications/website.tar.xz | 4 ++-- README.rst | 6 +++--- documentation/changelog.rst | 29 +++++++++++++++++++++++++- sources/classcore/__init__.py | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 1402a0b..471c52e 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:097802c0a82602474514eb75537e823caf8fc6559445fb13a90d31bc9f7579ef -size 142544 +oid sha256:580d81d43b0291341663b6e4c1d542f72f80c7287d1142e2cb468190ff26e3a7 +size 146960 diff --git a/README.rst b/README.rst index b80fb0e..6bd4b70 100644 --- a/README.rst +++ b/README.rst @@ -109,9 +109,9 @@ Note on Immutability 📢 Examples 💡 =============================================================================== -.. Please see the `examples directory -.. `_ for -.. greater detail. +Please see the `examples directory +`_ +for greater detail. Standard Behaviors 🔒 ------------------------------------------------------------------------------- diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 119a6f7..7c7d1c1 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -21,5 +21,32 @@ Release Notes ******************************************************************************* - .. towncrier release notes start + +Classcore 1.0 (2025-04-29) +========================== + +Enhancements +------------ + +- Add support for CPython 3.10 to 3.13. +- Add support for PyPy 3.10. +- Base classes and class factory classes which provide standard behaviors + (immutability of all attributes after initialization, concealment of all + non-public attributes from ``dir``) by default. Can apply additional decorators + and can tune for selective or total mutability or visibility. Enforce on class + attributes and instance attributes. +- Class and dataclass decorators which apply a set of standard behaviors to + classes so that they produce instances which are immutable and which only + reveal public attributes by default. Decorators are tunable via arguments to + provide selective or total attributes mutability and visibility as desired. +- Class decorator which accepts a sequence of other class decorators to apply. + Reduces height of decorator stacks and improves their reusability. +- Decorators for modifying class factory classes (metaclasses) so that they can + handle inline application of decorators during production of classes. This + includes logic for the case where a decorator replaces a class rather than + modifies it. +- Module class which enforces immutability and visibility limitation on module + attributes. (Visibility restriction is to reveal only public attributes to + ``dir``.) Also, conveience function which can reclassify a module or an entire + package, recursively, to use this class. diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 784b753..e790c95 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -32,7 +32,7 @@ from .factories import * -__version__ = '1.1a0' +__version__ = '1.0' standard.reclassify_modules( __name__, recursive = True ) From 44c818e13489189440a08232634a0c9859f61f98 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Thu, 1 May 2025 22:01:33 +0000 Subject: [PATCH 04/28] Update documents for publication. --- .auxiliary/configuration/copier-answers.yaml | 2 +- .auxiliary/data/towncrier/+cfc.enhance.rst | 4 -- .../data/towncrier/+coverage.repair.rst | 4 ++ .auxiliary/data/towncrier/+cpy.enhance.rst | 1 - .../towncrier/+mass-decoration.enhance.rst | 2 - .../data/towncrier/+privates.repair.rst | 1 + .auxiliary/data/towncrier/+pypy.enhance.rst | 1 - .../data/towncrier/+std-classes.enhance.rst | 5 -- .../towncrier/+std-decorators.enhance.rst | 4 -- .../data/towncrier/+std-modules.enhance.rst | 4 -- .auxiliary/publications/website.tar.xz | 4 +- .github/workflows/releaser.yaml | 10 ++-- .github/workflows/tester.yaml | 4 +- documentation/examples/std-behaviors.rst | 4 +- pyproject.toml | 23 +++++---- sources/classcore/__/imports.py | 1 + sources/classcore/__init__.py | 2 +- sources/classcore/factories.py | 12 +++-- sources/classcore/standard/behaviors.py | 3 +- sources/classcore/standard/decorators.py | 10 ++-- sources/classcore/standard/modules.py | 3 +- sources/classcore/utilities.py | 46 ++++++++++++++--- .../test_000_classcore/test_110_utilities.py | 49 +++++-------------- 23 files changed, 99 insertions(+), 100 deletions(-) delete mode 100644 .auxiliary/data/towncrier/+cfc.enhance.rst create mode 100644 .auxiliary/data/towncrier/+coverage.repair.rst delete mode 100644 .auxiliary/data/towncrier/+cpy.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+mass-decoration.enhance.rst create mode 100644 .auxiliary/data/towncrier/+privates.repair.rst delete mode 100644 .auxiliary/data/towncrier/+pypy.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+std-classes.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+std-decorators.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+std-modules.enhance.rst diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index 4072fe5..16d2ea4 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.16 +_commit: v1.17.1 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald diff --git a/.auxiliary/data/towncrier/+cfc.enhance.rst b/.auxiliary/data/towncrier/+cfc.enhance.rst deleted file mode 100644 index cd3b86f..0000000 --- a/.auxiliary/data/towncrier/+cfc.enhance.rst +++ /dev/null @@ -1,4 +0,0 @@ -Decorators for modifying class factory classes (metaclasses) so that they can -handle inline application of decorators during production of classes. This -includes logic for the case where a decorator replaces a class rather than -modifies it. diff --git a/.auxiliary/data/towncrier/+coverage.repair.rst b/.auxiliary/data/towncrier/+coverage.repair.rst new file mode 100644 index 0000000..96fa59f --- /dev/null +++ b/.auxiliary/data/towncrier/+coverage.repair.rst @@ -0,0 +1,4 @@ +Properly report test coverage by considering doctests too. (Not a user-facing +fix; however important to note that coverage was 100% on initial release, but +Github Actions workflow was not properly setup to capture coverage from +doctests and so it only reported 95% coverage.) diff --git a/.auxiliary/data/towncrier/+cpy.enhance.rst b/.auxiliary/data/towncrier/+cpy.enhance.rst deleted file mode 100644 index 38e7076..0000000 --- a/.auxiliary/data/towncrier/+cpy.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for CPython 3.10 to 3.13. diff --git a/.auxiliary/data/towncrier/+mass-decoration.enhance.rst b/.auxiliary/data/towncrier/+mass-decoration.enhance.rst deleted file mode 100644 index f0af1af..0000000 --- a/.auxiliary/data/towncrier/+mass-decoration.enhance.rst +++ /dev/null @@ -1,2 +0,0 @@ -Class decorator which accepts a sequence of other class decorators to apply. -Reduces height of decorator stacks and improves their reusability. diff --git a/.auxiliary/data/towncrier/+privates.repair.rst b/.auxiliary/data/towncrier/+privates.repair.rst new file mode 100644 index 0000000..6f51e7f --- /dev/null +++ b/.auxiliary/data/towncrier/+privates.repair.rst @@ -0,0 +1 @@ +Fix interaction with slotted dataclasses on Python 3.11+. diff --git a/.auxiliary/data/towncrier/+pypy.enhance.rst b/.auxiliary/data/towncrier/+pypy.enhance.rst deleted file mode 100644 index 1d50439..0000000 --- a/.auxiliary/data/towncrier/+pypy.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for PyPy 3.10. diff --git a/.auxiliary/data/towncrier/+std-classes.enhance.rst b/.auxiliary/data/towncrier/+std-classes.enhance.rst deleted file mode 100644 index 607b361..0000000 --- a/.auxiliary/data/towncrier/+std-classes.enhance.rst +++ /dev/null @@ -1,5 +0,0 @@ -Base classes and class factory classes which provide standard behaviors -(immutability of all attributes after initialization, concealment of all -non-public attributes from ``dir``) by default. Can apply additional decorators -and can tune for selective or total mutability or visibility. Enforce on class -attributes and instance attributes. diff --git a/.auxiliary/data/towncrier/+std-decorators.enhance.rst b/.auxiliary/data/towncrier/+std-decorators.enhance.rst deleted file mode 100644 index ec492f6..0000000 --- a/.auxiliary/data/towncrier/+std-decorators.enhance.rst +++ /dev/null @@ -1,4 +0,0 @@ -Class and dataclass decorators which apply a set of standard behaviors to -classes so that they produce instances which are immutable and which only -reveal public attributes by default. Decorators are tunable via arguments to -provide selective or total attributes mutability and visibility as desired. diff --git a/.auxiliary/data/towncrier/+std-modules.enhance.rst b/.auxiliary/data/towncrier/+std-modules.enhance.rst deleted file mode 100644 index 6cc9ead..0000000 --- a/.auxiliary/data/towncrier/+std-modules.enhance.rst +++ /dev/null @@ -1,4 +0,0 @@ -Module class which enforces immutability and visibility limitation on module -attributes. (Visibility restriction is to reveal only public attributes to -``dir``.) Also, conveience function which can reclassify a module or an entire -package, recursively, to use this class. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 471c52e..f8eac8f 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:580d81d43b0291341663b6e4c1d542f72f80c7287d1142e2cb468190ff26e3a7 -size 146960 +oid sha256:71474f051e9f51eb5bb711fd97c91a10212b64fb0033cfaf50df9d74b253305c +size 157432 diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml index f079377..c3a03db 100644 --- a/.github/workflows/releaser.yaml +++ b/.github/workflows/releaser.yaml @@ -24,7 +24,7 @@ jobs: test: needs: [initialize] - uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@v1.16 + uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@v1.17.1 with: matrix-exclusions: '${{ needs.initialize.outputs.matrix-exclusions }}' platforms: '${{ needs.initialize.outputs.platforms }}' @@ -33,7 +33,7 @@ jobs: report: needs: [initialize, test] - uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@v1.16 + uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@v1.17.1 with: python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' @@ -43,14 +43,14 @@ jobs: contents: write id-token: write pages: write - uses: emcd/python-project-common/.github/workflows/xrepo--documenter.yaml@v1.16 + uses: emcd/python-project-common/.github/workflows/xrepo--documenter.yaml@v1.17.1 with: include-reports: true python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' package: needs: [initialize, docsgen] - uses: emcd/python-project-common/.github/workflows/xrepo--packager.yaml@v1.16 + uses: emcd/python-project-common/.github/workflows/xrepo--packager.yaml@v1.17.1 with: artifacts-path: '.auxiliary/artifacts/hatch-build' # TODO: Use environment. python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' @@ -95,7 +95,7 @@ jobs: steps: - name: Prepare Python - uses: emcd/python-project-common/.github/actions/python-hatch@v1.16 + uses: emcd/python-project-common/.github/actions/python-hatch@v1.17.1 with: python-version: ${{ fromJSON(needs.initialize.outputs.python-versions)[0] }} diff --git a/.github/workflows/tester.yaml b/.github/workflows/tester.yaml index a341490..e0180a6 100644 --- a/.github/workflows/tester.yaml +++ b/.github/workflows/tester.yaml @@ -14,7 +14,7 @@ jobs: test: needs: [initialize] - uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@v1.16 + uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@v1.17.1 with: matrix-exclusions: '${{ needs.initialize.outputs.matrix-exclusions }}' platforms: '${{ needs.initialize.outputs.platforms }}' @@ -23,6 +23,6 @@ jobs: report: needs: [initialize, test] - uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@v1.16 + uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@v1.17.1 with: python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' diff --git a/documentation/examples/std-behaviors.rst b/documentation/examples/std-behaviors.rst index b8127be..cacee58 100644 --- a/documentation/examples/std-behaviors.rst +++ b/documentation/examples/std-behaviors.rst @@ -187,7 +187,7 @@ Invalid mutability verifiers will cause an error to be raised: ... Traceback (most recent call last): ... - classcore.exceptions.BehaviorExclusionInvalidity: Invalid behavior exclusion verifier: 42 + classcore.exceptions.BehaviorExclusionInvalidity: Invalid behavior exclusion verifier: 13 Inheritance ------------------------------------------------------------------------------- @@ -354,7 +354,7 @@ Invalid visibility verifiers will cause an error to be raised: ... Traceback (most recent call last): ... - classcore.exceptions.BehaviorExclusionInvalidity: Invalid behavior exclusion verifier: 42 + classcore.exceptions.BehaviorExclusionInvalidity: Invalid behavior exclusion verifier: 13 Inheritance diff --git a/pyproject.toml b/pyproject.toml index 63327f9..35aa08c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,28 +119,31 @@ PYTHONUNBUFFERED = 'TRUE' # TODO: Only for coverage/pytest. # --- END: Injected by Copier --- [tool.hatch.envs.develop.scripts] docsgen = [ - '''sphinx-build -E -b linkcheck -d .auxiliary/caches/sphinx \ - documentation .auxiliary/artifacts/sphinx-linkcheck''', - '''sphinx-build -a -d .auxiliary/caches/sphinx \ - documentation .auxiliary/artifacts/sphinx-html''', + """sphinx-build -E -b linkcheck -d .auxiliary/caches/sphinx \ + documentation .auxiliary/artifacts/sphinx-linkcheck""", + """sphinx-build -a -d .auxiliary/caches/sphinx \ + documentation .auxiliary/artifacts/sphinx-html""", ] linters = [ - '''ruff check --quiet sources documentation tests''', + """ruff check --quiet sources documentation tests""", # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- - '''pyright sources''', + """pyright sources""", ] packagers = [ - '''hatch build''', + """hatch build""", # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- ] -testers = [ +testers-no-reports = [ 'coverage erase', 'coverage run', - '''coverage run -m sphinx.cmd.build \ + """coverage run -m sphinx.cmd.build \ -E -b doctest -d .auxiliary/caches/sphinx \ - documentation .auxiliary/artifacts/sphinx-doctest''', + documentation .auxiliary/artifacts/sphinx-doctest""", +] +testers = [ + 'testers-no-reports', 'coverage combine', 'coverage report --skip-covered', 'coverage html', diff --git a/sources/classcore/__/imports.py b/sources/classcore/__/imports.py index 58d3bd3..a5ac700 100644 --- a/sources/classcore/__/imports.py +++ b/sources/classcore/__/imports.py @@ -28,6 +28,7 @@ import collections.abc as cabc import dataclasses as dcls import functools as funct +import hashlib import inspect import platform import re diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index e790c95..61c12f7 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -32,7 +32,7 @@ from .factories import * -__version__ = '1.0' +__version__ = '1.1rc0' standard.reclassify_modules( __name__, recursive = True ) diff --git a/sources/classcore/factories.py b/sources/classcore/factories.py index b66853c..066db01 100644 --- a/sources/classcore/factories.py +++ b/sources/classcore/factories.py @@ -58,12 +58,13 @@ def construct( # noqa: PLR0913 # Some decorators create new classes, which invokes this method again. # Short-circuit to prevent recursive decoration and other tangles. progress_name = attributes_namer( 'class', 'in_progress' ) - in_progress = _utilities.getattr0( cls, progress_name, False ) + progress_name_m = _utilities.mangle_name( cls, progress_name ) + in_progress = getattr( cls, progress_name_m, False ) if in_progress: return cls - setattr( cls, progress_name, True ) + setattr( cls, progress_name_m, True ) for postp in postprocessors: postp( cls, decorators_ ) cls = _decorators.apply_decorators( cls, decorators_ ) - setattr( cls, progress_name, False ) + setattr( cls, progress_name_m, False ) return cls return construct @@ -84,9 +85,10 @@ def initialize( ''' Initializes class, applying hooks. ''' superf( *posargs, **nomargs ) progress_name = attributes_namer( 'class', 'in_progress' ) - in_progress = _utilities.getattr0( cls, progress_name, False ) + progress_name_m = _utilities.mangle_name( cls, progress_name ) + in_progress = getattr( cls, progress_name_m, False ) if in_progress: return # If non-empty, then not top-level. - delattr( cls, progress_name ) + delattr( cls, progress_name_m ) for completer in completers: completer( cls ) return initialize diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index a136b8c..1925a44 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -251,7 +251,8 @@ def complete( cls: type ) -> None: label = concealment_label, behaviors = behaviors, verifiers = visibles ) # Set behaviors attribute last since it enables enforcement. - setattr( cls, attributes_namer( 'class', 'behaviors' ), behaviors ) + behaviors_name = attributes_namer( 'class', 'behaviors' ) + _utilities.setattr0( cls, behaviors_name, frozenset( behaviors ) ) return complete diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 9b6ed62..781442a 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -77,9 +77,10 @@ def prepare_dataclass_for_instances( ''' Annotates dataclass in support of instantiation machinery. ''' annotations = __.inspect.get_annotations( cls ) behaviors_name = attributes_namer( 'instance', 'behaviors' ) - annotations[ behaviors_name ] = set[ str ] + behaviors_name_m = _utilities.mangle_name( cls, behaviors_name ) + annotations[ behaviors_name_m ] = set[ str ] setattr( cls, '__annotations__', annotations ) # in case of absence - setattr( cls, behaviors_name, __.dcls.field( init = False ) ) + setattr( cls, behaviors_name_m, __.dcls.field( init = False ) ) def produce_class_factory_decorators( @@ -137,6 +138,7 @@ def decorate( cls: type[ _U ] ) -> type[ _U ]: if extant is original: return cls behaviors: set[ str ] = set( ) behaviors_name = attributes_namer( 'instance', 'behaviors' ) + behaviors_name_m = _utilities.mangle_name( cls, behaviors_name ) _behaviors.record_behavior( cls, attributes_namer = attributes_namer, level = 'instances', basename = 'mutables', @@ -153,9 +155,9 @@ def initialize( self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any ) -> None: original( self, *posargs, **nomargs ) - behaviors_ = _utilities.getattr0( self, behaviors_name, set( ) ) - if not behaviors_: setattr( self, behaviors_name, behaviors_ ) + behaviors_: set[ str ] = getattr( self, behaviors_name_m, set( ) ) behaviors_.update( behaviors ) + setattr( self, behaviors_name_m, frozenset( behaviors_ ) ) setattr( cls, initializer_name, initialize ) cls.__init__ = initialize diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index e0f6b5e..99f5ae7 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -23,6 +23,7 @@ from __future__ import annotations +from .. import utilities as _utilities from . import __ from . import behaviors as _behaviors from . import classes as _classes @@ -85,5 +86,5 @@ def _seal_module( ) -> None: behaviors = { _behaviors.concealment_label, _behaviors.immutability_label } behaviors_name = attributes_namer( 'instance', 'behaviors' ) - setattr( module, behaviors_name, behaviors ) + _utilities.setattr0( module, behaviors_name, behaviors ) module.__class__ = Module diff --git a/sources/classcore/utilities.py b/sources/classcore/utilities.py index 40c91b6..be6a78f 100644 --- a/sources/classcore/utilities.py +++ b/sources/classcore/utilities.py @@ -34,14 +34,44 @@ def describe_object( obj: object ) -> str: def getattr0( obj: object, name: str, default: __.typx.Any ) -> __.typx.Any: - ''' Returns attribute from object without inheritance. ''' - # Inspect object dictionary directly to suppress getattr inheritance. - attrsdict = getattr( obj, '__dict__', { } ) - if name in attrsdict: return attrsdict[ name ] - slots = getattr( obj, '__slots__', ( ) ) - # Name may be in slots but not yet assigned. - if name in slots: return getattr( obj, name, default ) - return default + ''' Returns private attribute from object. + + Uses mangled attribute which is unique to the class. + ''' + name_m = mangle_name( obj, name ) + return getattr( obj, name_m, default ) + + +def delattr0( obj: object, name: str ) -> None: + ''' Deletes private attribute on object. + + Uses mangled attribute which is unique to the class. + ''' + name_m = mangle_name( obj, name ) + delattr( obj, name_m ) + + +def setattr0( obj: object, name: str, value: __.typx.Any ) -> None: + ''' Assigns private attribute to object. + + Uses mangled attribute which is unique to the class. + ''' + name_m = mangle_name( obj, name ) + setattr( obj, name_m, value ) + + +def mangle_name( obj: object, name: str ) -> str: + ''' Mangles attribute name so that it is unique. + + Effectively provides name of private member attribute, + which is unique across class inheritance. + ''' + if not __.inspect.isclass( obj ): + return mangle_name( type( obj ), name ) + namehash = __.hashlib.sha256( ) + namehash.update( qualify_class_name( obj ).encode( ) ) + namehash_hex = namehash.hexdigest( ) + return f"{name}_{namehash_hex}" def qualify_class_name( cls: type ) -> str: diff --git a/tests/test_000_classcore/test_110_utilities.py b/tests/test_000_classcore/test_110_utilities.py index 81c136a..fbd8d74 100644 --- a/tests/test_000_classcore/test_110_utilities.py +++ b/tests/test_000_classcore/test_110_utilities.py @@ -32,24 +32,7 @@ class Foo: x = 1 -class Bar( Foo ): pass - - -class Baz: - def __init__( self, value ): - self.x = value - - -class FooSlotsBase: __slots__ = ( 'x', ) - - -class FooSlots( FooSlotsBase ): - def __init__( self, value ): - self.x = value - - foo = Foo( ) -bar = Bar( ) def test_100_qualify_class_name( ): @@ -65,28 +48,20 @@ def test_110_describe_object( ): assert 'instance of class' in module.describe_object( foo ) -def test_200_getattr0_dict( ): - ''' Attribute from object dictionary without inheritance. ''' - module = cache_import_module( MODULE_QNAME ) - function = module.getattr0 - sentinel = object( ) - assert 1 == function( Foo, 'x', sentinel ) - assert sentinel == function( Bar, 'x', sentinel ) - assert sentinel == function( foo, 'x', sentinel ) - assert sentinel == function( bar, 'x', sentinel ) - baz = Baz( 42 ) - assert 42 == function( baz, 'x', sentinel ) - - -def test_205_getattr0_slots( ): - ''' Attribute from object slots (empty and filled). ''' +def test_200_attr0( ): + ''' Can access and mutate special private attribute. ''' module = cache_import_module( MODULE_QNAME ) - function = module.getattr0 sentinel = object( ) - foono = FooSlotsBase( ) - assert sentinel == function( foono, 'x', sentinel ) - foo = FooSlots( 42 ) - assert 42 == function( foo, 'x', sentinel ) + class C: pass + module.setattr0( C, 'x', 1 ) + assert 1 == module.getattr0( C, 'x', sentinel ) + assert sentinel == module.getattr0( C, 'y', sentinel ) + class D( C ): pass + module.setattr0( D, 'y', 2 ) + assert sentinel == module.getattr0( D, 'x', sentinel ) + assert 2 == module.getattr0( D, 'y', sentinel ) + module.delattr0( C, 'x' ) + assert sentinel == module.getattr0( C, 'x', sentinel ) @pytest.mark.skipif( From 72e307835e7748e6b820d3fd376dd81471a65897 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Thu, 1 May 2025 22:32:19 +0000 Subject: [PATCH 05/28] Update documents for publication. --- .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 13 +++++++++++++ sources/classcore/__init__.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index f8eac8f..8c6a3f9 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71474f051e9f51eb5bb711fd97c91a10212b64fb0033cfaf50df9d74b253305c -size 157432 +oid sha256:4325dc9150312e437368fa345d1b52806e2be0247b6da420fa7dd0f6143d282f +size 160752 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 7c7d1c1..30dad75 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,19 @@ Release Notes .. towncrier release notes start +Classcore 1.1 (2025-05-01) +========================== + +Repairs +------- + +- Fix interaction with slotted dataclasses on Python 3.11+. +- Properly report test coverage by considering doctests too. (Not a user-facing + fix; however important to note that coverage was 100% on initial release, but + Github Actions workflow was not properly setup to capture coverage from + doctests and so it only reported 95% coverage.) + + Classcore 1.0 (2025-04-29) ========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 61c12f7..ae46ff3 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -32,7 +32,7 @@ from .factories import * -__version__ = '1.1rc0' +__version__ = '1.1' standard.reclassify_modules( __name__, recursive = True ) From fd3f09dfea40307c419a7b44a4f6b72f27f6a137 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Thu, 5 Jun 2025 22:29:34 +0000 Subject: [PATCH 06/28] Update documents for publication. --- .auxiliary/configuration/conventions.md | 88 +++++++ .auxiliary/configuration/copier-answers.yaml | 2 +- .../data/towncrier/+coverage.repair.rst | 4 - .../data/towncrier/+docstrings.enhance.rst | 1 + .../data/towncrier/+dynadoc.enhance.rst | 3 + .../data/towncrier/+privates.repair.rst | 1 - .auxiliary/data/towncrier/+pyright.repair.rst | 1 + .auxiliary/publications/website.tar.xz | 4 +- .github/workflows/claude.yaml | 36 +++ .github/workflows/releaser.yaml | 20 +- .github/workflows/tester.yaml | 4 +- .gitignore | 3 + README.rst | 33 ++- documentation/api.rst | 59 +---- documentation/conf.py | 19 +- documentation/contribution.rst | 14 ++ documentation/devapi.rst | 2 + documentation/examples/index.rst | 1 + documentation/examples/std-dynadoc.rst | 210 +++++++++++++++++ pyproject.toml | 5 +- sources/classcore/__/__init__.py | 8 + sources/classcore/__/doctab.py | 76 ++++++ sources/classcore/__/imports.py | 6 +- sources/classcore/__/nomina.py | 5 +- sources/classcore/__init__.py | 35 ++- sources/classcore/decorators.py | 31 ++- sources/classcore/exceptions.py | 6 +- sources/classcore/factories.py | 22 +- sources/classcore/nomina.py | 69 +++--- sources/classcore/standard/__.py | 2 - sources/classcore/standard/__init__.py | 28 ++- sources/classcore/standard/behaviors.py | 44 ++-- sources/classcore/standard/classes.py | 218 ++++++++++++++--- sources/classcore/standard/decorators.py | 219 ++++++++++++------ sources/classcore/standard/dynadoc.py | 129 +++++++++++ sources/classcore/standard/modules.py | 25 +- sources/classcore/standard/nomina.py | 49 +++- sources/classcore/utilities.py | 10 +- .../test_320_standard_decorators.py | 4 +- 39 files changed, 1175 insertions(+), 321 deletions(-) create mode 100644 .auxiliary/configuration/conventions.md delete mode 100644 .auxiliary/data/towncrier/+coverage.repair.rst create mode 100644 .auxiliary/data/towncrier/+docstrings.enhance.rst create mode 100644 .auxiliary/data/towncrier/+dynadoc.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+privates.repair.rst create mode 100644 .auxiliary/data/towncrier/+pyright.repair.rst create mode 100644 .github/workflows/claude.yaml create mode 100644 documentation/examples/std-dynadoc.rst create mode 100644 sources/classcore/__/doctab.py create mode 100644 sources/classcore/standard/dynadoc.py diff --git a/.auxiliary/configuration/conventions.md b/.auxiliary/configuration/conventions.md new file mode 100644 index 0000000..07d74ce --- /dev/null +++ b/.auxiliary/configuration/conventions.md @@ -0,0 +1,88 @@ +# General Advice + +### Design + +- Make classes lightweight. Prefer module-level functions over class methods. +- Functions should not be more than 30 lines long. Refactor long functions. +- Keep the number of function arguments small. Pass common state via + data transfer objects (DTOs). +- Use dependency injection to improve configuration and testability. Choose + sensible defaults for injected dependencies to streamline normal development. +- Prefer immutability wherever possible. + +# Per-Language Advice + +## Python + +### Design and Idioms + +- Target Python 3.10 and use idioms appropriate for that version + (`match`..`case`, type unions via `|`, etc...). +- Note the internal `__` subpackage which exposes imports used internally + throughout the package (`cabc` alias for `collections.abc`, `enum`, `types`, + `typx` alias for `typing_extensions`, etc...). +- Do not pollute the module namespace with public imports. Either reference + common imports from the `__` subpackage or alias module-level imports as + private. +- Do not use `__all__` to advertise the public API of a module. Name anything, + which should not be part of this API, with a private name starting with `_`. + +### Documentation and Annotations + +- Pad inside of delimiter pairs with spaces. E.g., `( foo )` and not `(foo)`. + Except in f-strings and `str.format` inputs. +- Pad binary operators with spaces. E.g., `foo = 42` and not `foo=42`, `1 + 1` + and not `1+1`, `[ 1 : -n ]` and not `[1:-n]`. +- Docstrings look like `''' Space-padded headline inside of triple-single + quotes. '''` and not `"""Double quotes and no spaces are hard to read."""`. +- Use double-quoted strings for f-strings, `str.format` templates, and log + messages. Otherwise, use single-quoted strings. +- Add type hints for arguments, attributes, and return values. +- Do not write "param spam" documentation which states the obvious. Only + document non-obvious or complex behaviors on arguments and attributes. +- Use PEP 593 `Annotated` with PEP 727 `Doc` for argument, attribute, and + return value documentation, when necessary. +- Use `TypeAlias` aliases to reuse complex annotations or expose them as part + of the public API. + +### Comments and Lines + +- Do not strip comments from existing code unless directed to do so. +- Do not describe obvious code with comments. Only comment on non-obvious or + complex behaviors. +- Leave TODO comments about uncovered edge cases, tests, and other future work. +- Do not break function bodies with empty lines. +- One empty line between attribute blocks and methods on classes. +- Two empty lines between attribute blocks, classes, and functions on modules. +- Split lines at 79 columns. Use parentheses for continuations and not + backslashes. + +### Lints and Tests + +- Check your work by linting, ensuring that the package imports, running tests, + and generating documentation. +- Ignore any warnings, unless instructed otherwise. Focus only on errors and + failures. +- To run linters, use `hatch --env develop run linters`. +- To run testers, use `hatch --env develop run testers`. +- To generate documentation, use `hatch --env develop run docsgen`. +- Do not write tests unless explicitly instructed to do so. + +# Commits + +- Use `git status` to ensure that all relevant changes are in the changeset to + be committed. +- Look at the previous five commit messages for guidance on message style. +- Use present tense, imperative mood verbs to describe changes. E.g. "Fix" and + *not* "Fixed". +- The commit message should include a `Co-Authored-By:` field as its final + line. The name of the author should be your model name. The email address + should either be one which you have been designated to use or else a + commonly-known no-reply address. + +## Interactive Collaboration on User Terminal + +- Do not commit until you have user approval to do so. +- Add the `--no-gpg-sign` option to the `git commit` command to suppress GPG + passphrase challenges. (These challenges conflict with the alternate console + screen, managed by some CLI agents, resulting in an unusable terminal.) diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index 16d2ea4..6860c37 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.17.1 +_commit: v1.20.1 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald diff --git a/.auxiliary/data/towncrier/+coverage.repair.rst b/.auxiliary/data/towncrier/+coverage.repair.rst deleted file mode 100644 index 96fa59f..0000000 --- a/.auxiliary/data/towncrier/+coverage.repair.rst +++ /dev/null @@ -1,4 +0,0 @@ -Properly report test coverage by considering doctests too. (Not a user-facing -fix; however important to note that coverage was 100% on initial release, but -Github Actions workflow was not properly setup to capture coverage from -doctests and so it only reported 95% coverage.) diff --git a/.auxiliary/data/towncrier/+docstrings.enhance.rst b/.auxiliary/data/towncrier/+docstrings.enhance.rst new file mode 100644 index 0000000..f36b9b4 --- /dev/null +++ b/.auxiliary/data/towncrier/+docstrings.enhance.rst @@ -0,0 +1 @@ +Fill out docstrings for all public classes and functions in package. diff --git a/.auxiliary/data/towncrier/+dynadoc.enhance.rst b/.auxiliary/data/towncrier/+dynadoc.enhance.rst new file mode 100644 index 0000000..ec7e2fe --- /dev/null +++ b/.auxiliary/data/towncrier/+dynadoc.enhance.rst @@ -0,0 +1,3 @@ +Integrate with Dynadoc. Adds special introspection control which avoid +docstring updates on immutable classes. Adds Dynadoc ``with_docstring`` +decorator to set of decorators on standard classes. diff --git a/.auxiliary/data/towncrier/+privates.repair.rst b/.auxiliary/data/towncrier/+privates.repair.rst deleted file mode 100644 index 6f51e7f..0000000 --- a/.auxiliary/data/towncrier/+privates.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Fix interaction with slotted dataclasses on Python 3.11+. diff --git a/.auxiliary/data/towncrier/+pyright.repair.rst b/.auxiliary/data/towncrier/+pyright.repair.rst new file mode 100644 index 0000000..c78d00b --- /dev/null +++ b/.auxiliary/data/towncrier/+pyright.repair.rst @@ -0,0 +1 @@ +Address Pyright complaints about metaclass arguments in ``class`` statements. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 8c6a3f9..bbe5b22 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4325dc9150312e437368fa345d1b52806e2be0247b6da420fa7dd0f6143d282f -size 160752 +oid sha256:63dacf1af59ccfbb72500d2c6966429493d29e4d81cbbd3c7ed8028fc301ff72 +size 198436 diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml new file mode 100644 index 0000000..b8098a1 --- /dev/null +++ b/.github/workflows/claude.yaml @@ -0,0 +1,36 @@ +name: claude + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + + initialize: + uses: ./.github/workflows/core--initializer.yaml + + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '/claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '/claude') || contains(github.event.issue.title, '/claude'))) + needs: [initialize] + permissions: + contents: read + id-token: write + issues: read + pull-requests: read + uses: emcd/python-project-common/.github/workflows/xrepo--claude.yaml@gha-1 + with: + allowed-tools: 'Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git diff:*),Bash(git log:*),Bash(git ls-files:*),Bash(git remote:*),Bash(git reset:*),Bash(git rev-parse:*),Bash(git rm:*),Bash(git status),Bash(hatch:*),Bash(pip:*),Bash(python:*),Edit,Write' + python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' + timeout-minutes: 20 + secrets: + anthropic-api-key: '${{ secrets.ANTHROPIC_API_KEY }}' diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml index c3a03db..b9dbbef 100644 --- a/.github/workflows/releaser.yaml +++ b/.github/workflows/releaser.yaml @@ -24,7 +24,7 @@ jobs: test: needs: [initialize] - uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@v1.17.1 + uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@gha-1 with: matrix-exclusions: '${{ needs.initialize.outputs.matrix-exclusions }}' platforms: '${{ needs.initialize.outputs.platforms }}' @@ -33,7 +33,7 @@ jobs: report: needs: [initialize, test] - uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@v1.17.1 + uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@gha-1 with: python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' @@ -43,14 +43,14 @@ jobs: contents: write id-token: write pages: write - uses: emcd/python-project-common/.github/workflows/xrepo--documenter.yaml@v1.17.1 + uses: emcd/python-project-common/.github/workflows/xrepo--documenter.yaml@gha-1 with: include-reports: true python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' package: needs: [initialize, docsgen] - uses: emcd/python-project-common/.github/workflows/xrepo--packager.yaml@v1.17.1 + uses: emcd/python-project-common/.github/workflows/xrepo--packager.yaml@gha-1 with: artifacts-path: '.auxiliary/artifacts/hatch-build' # TODO: Use environment. python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' @@ -81,7 +81,6 @@ jobs: skip-existing: ${{ inputs.which-pypi == 'testpypi' }} publish-github: - if: ${{ startsWith(github.ref, 'refs/tags/') }} needs: - initialize - package @@ -90,12 +89,13 @@ jobs: # --- END: Injected by Copier --- runs-on: ubuntu-latest permissions: + attestations: write contents: write id-token: write steps: - name: Prepare Python - uses: emcd/python-project-common/.github/actions/python-hatch@v1.17.1 + uses: emcd/python-project-common/.github/actions/python-hatch@gha-1 with: python-version: ${{ fromJSON(needs.initialize.outputs.python-versions)[0] }} @@ -111,10 +111,10 @@ jobs: cd ${{ env.DISTRIBUTIONS_PATH }} sha256sum classcore-* >SHA256SUMS.txt - - name: Sign Distributions - uses: sigstore/gh-action-sigstore-python@v3.0.0 + - name: Attest Build Provenance + uses: actions/attest-build-provenance@v2 with: - inputs: >- + subject-path: | ${{ env.DISTRIBUTIONS_PATH }}/SHA256SUMS.txt ${{ env.DISTRIBUTIONS_PATH }}/classcore-* @@ -127,6 +127,7 @@ jobs: cp .auxiliary/artifacts/tc-release-notes.rst .auxiliary/artifacts/release-notes.rst - name: Create Release + if: ${{ startsWith(github.ref, 'refs/tags/') }} env: GITHUB_TOKEN: ${{ github.token }} run: | @@ -135,6 +136,7 @@ jobs: --notes-file .auxiliary/artifacts/release-notes.rst - name: Publish Artifacts + if: ${{ startsWith(github.ref, 'refs/tags/') }} env: GITHUB_TOKEN: ${{ github.token }} run: | diff --git a/.github/workflows/tester.yaml b/.github/workflows/tester.yaml index e0180a6..2de0d5c 100644 --- a/.github/workflows/tester.yaml +++ b/.github/workflows/tester.yaml @@ -14,7 +14,7 @@ jobs: test: needs: [initialize] - uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@v1.17.1 + uses: emcd/python-project-common/.github/workflows/xrepo--tester.yaml@gha-1 with: matrix-exclusions: '${{ needs.initialize.outputs.matrix-exclusions }}' platforms: '${{ needs.initialize.outputs.platforms }}' @@ -23,6 +23,6 @@ jobs: report: needs: [initialize, test] - uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@v1.17.1 + uses: emcd/python-project-common/.github/workflows/xrepo--reporter.yaml@gha-1 with: python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' diff --git a/.gitignore b/.gitignore index 8ae7cd9..0000eb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .env .*.swp +AGENTS.md +CLAUDE.md +CONVENTIONS.md __pycache__/ bugs/ diff --git a/README.rst b/README.rst index 6bd4b70..ced98da 100644 --- a/README.rst +++ b/README.rst @@ -228,27 +228,24 @@ Other Projects by This Author 🌟 =============================================================================== -* `python-absence `_ - - PyPI: `absence `_ +* `python-absence `_ (`absence `_ on PyPI) - 🕳️ A Python library package which provides a **sentinel for absent values** - a falsey, immutable singleton that represents the absence of a value in contexts where ``None`` or ``False`` may be valid values. -* `python-accretive `_ - - PyPI: `accretive `_ + 🕳️ A Python library package which provides a **sentinel for absent values** - a falsey, immutable singleton that represents the absence of a value in contexts where ``None`` or ``False`` may be valid values. +* `python-accretive `_ (`accretive `_ on PyPI) - 🌌 A Python library package which provides **accretive data structures** - collections which can grow but never shrink. -* `python-falsifier `_ - - PyPI: `falsifier `_ + 🌌 A Python library package which provides **accretive data structures** - collections which can grow but never shrink. +* `python-dynadoc `_ (`dynadoc `_ on PyPI) - 🎭 A very simple Python library package which provides a **base class for falsey objects** - objects that evaluate to ``False`` in boolean contexts. -* `python-frigid `_ - - PyPI: `frigid `_ + 📝 A Python library package which bridges the gap between **rich annotations** and **automatic documentation generation** with configurable renderers and support for reusable fragments. +* `python-falsifier `_ (`falsifier `_ on PyPI) - 🔒 A Python library package which provides **immutable data structures** - collections which cannot be modified after creation. -* `python-icecream-truck `_ - - PyPI: `icecream-truck `_ + 🎭 A very simple Python library package which provides a **base class for falsey objects** - objects that evaluate to ``False`` in boolean contexts. +* `python-frigid `_ (`frigid `_ on PyPI) - 🍦 **Flavorful Debugging** - A Python library which enhances the powerful and well-known ``icecream`` package with flavored traces, configuration hierarchies, customized outputs, ready-made recipes, and more. -* `python-mimeogram `_ - - PyPI: `mimeogram `_ + 🔒 A Python library package which provides **immutable data structures** - collections which cannot be modified after creation. +* `python-icecream-truck `_ (`icecream-truck `_ on PyPI) - 📨 A command-line tool for **exchanging collections of files with Large Language Models** - bundle multiple files into a single clipboard-ready document while preserving directory structure and metadata... good for code reviews, project sharing, and LLM interactions. + 🍦 **Flavorful Debugging** - A Python library which enhances the powerful and well-known ``icecream`` package with flavored traces, configuration hierarchies, customized outputs, ready-made recipes, and more. +* `python-mimeogram `_ (`mimeogram `_ on PyPI) + + 📨 A command-line tool for **exchanging collections of files with Large Language Models** - bundle multiple files into a single clipboard-ready document while preserving directory structure and metadata... good for code reviews, project sharing, and LLM interactions. diff --git a/documentation/api.rst b/documentation/api.rst index ef541d3..5f90f5f 100644 --- a/documentation/api.rst +++ b/documentation/api.rst @@ -1,5 +1,5 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- +.. vim: set filetype=rst fileencoding=utf-8: +.. -*- mode: rst ; coding: utf-8 -*- .. +--------------------------------------------------------------------------+ | | | Licensed under the Apache License, Version 2.0 (the "License"); | @@ -16,7 +16,8 @@ | | +--------------------------------------------------------------------------+ -:tocdepth: 4 + +:tocdepth: 3 ******************************************************************************* @@ -27,38 +28,13 @@ API Package ``classcore`` =============================================================================== -Foundational class factories and decorators. - -Provides ability to create class decorators and metaclasses with customization -hooks. The metaclasses can apply class decorators inline during the class -construction and initialization process, properly handling cases where -decorators replace classes (e.g., ``dataclasses.dataclass( slots = True )``). -They also backport the repair mechanism from newer versions of CPython to -ensure that the class closure cells are rectified on replaced classes, so that -zero-argument ``super`` calls function correctly in them. - -The ``classcore.standard`` subpackage is an example of the decorators and -customization hooks being used to provide a set of practical classes and class -decorators. Furthermore, the exception classes in the -:py:mod:`classcore.exceptions` module inherit from one of the standard classes, -making both the exception classes, themselves, and their instances immutable -and concealing their non-public attributes to reduce API noise. I.e., this -package "eats its own dog food" and provides practical examples in so doing. - -This package is not as magical as it might seem. It does **not** rely on any -``exec`` or ``eval`` calls and it does **not** do anything with ``ctypes`` or -similar surgical instruments. It relies completely on the documented Python -data model and the machinery that it provides. While it is true that -metaclasses can be tricky, this package is developed with a deep, -highly-evolved understanding of them. We seek simplicity over cleverness and -maintain robust tests across multiple Python implementations and versions. The -package is also very clean in terms of static type checking (via Pyright). +.. automodule:: classcore Module ``classcore.decorators`` ------------------------------------------------------------------------------- -.. automodule:: classcore.factories +.. automodule:: classcore.decorators Module ``classcore.factories`` @@ -88,28 +64,7 @@ Module ``classcore.utilities`` Subpackage ``classcore.standard`` =============================================================================== -Decorators and class factories which imbue concealment and immutability. - -Concealment restricts the visibility of attributes on classes and their -instances. By default, only public attributes (ones which do not start with -``_``) are revealed for :py:func:`dir` calls. This behavior can be overriden by -supplying visibility verifiers as a decorator factory argument or metaclass -argument. These can be a sequence of attribute names, regular expression -:py:class:`re.Pattern` objects which match attribute names, or predicate -functions which match attribute names. Or, total visibility (per the Python -default) can be achieved by supplying ``visibles = '*'`` instead of a sequence -of verifiers. - -Immutability prevents assignment (including reassignment) or deletion of -attrubtes on classes and their instances after they have been completely -initialized. In addition to any standard Python class, this can be applied to -dataclasses, allowing them to use ``__post_init__`` to set attributes, which -``dataclasses.dataclass( frozen = True )`` prevents. The immutability behavior -can be overridden by supplying mutability verifiers as a decorator factory -argument or metaclass argument. These behave similarly to the visibility -verifiers described above. - -Hooks to modify the concealment and immutability behaviors are also available. +.. automodule:: classcore.standard Module ``classcore.standard.classes`` diff --git a/documentation/conf.py b/documentation/conf.py index 22fe65d..6ffa8bc 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -1,3 +1,6 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- mode: python ; coding: utf-8 -*- + ''' Configuration file for the Sphinx documentation builder. This file only contains a selection of the most common options. @@ -77,7 +80,13 @@ def _import_version( ): "v, remove specified key and return the corresponding value." ), # Type annotation weirdnesses. ( 'py:class', "Doc" ), + ( 'py:class', "builtins.NotImplementedType" ), + ( 'py:class', "classcore.__.T" ), + ( 'py:class', "classcore.__.U" ), + ( 'py:class', "classcore.standard.classes.Object" ), ( 'py:class', "collections.abc.Annotated" ), + ( 'py:class', "dynadoc.context.Context" ), + ( 'py:class', "dynadoc.context.IntrospectionControl" ), ( 'py:class', "types.Annotated" ), ( 'py:class', "typing_extensions._ProtocolMeta" ), ( 'py:class', "typing_extensions.Any" ), @@ -95,6 +104,8 @@ def _import_version( ): r'https://github\.com/emcd/python-classcore/.*', # Package does not exist during initial development. r'https://pypi.org/project/classcore/', + # Github aggressively rate-limits access to certain blobs. + r'https://github\.com/.*/.*/blob/.*', ] # -- Options for HTML output ------------------------------------------------- @@ -114,13 +125,13 @@ def _import_version( ): autodoc_default_options = { 'member-order': 'groupwise', - 'imported-members': False, - 'inherited-members': True, + 'members': True, 'show-inheritance': True, - 'undoc-members': True, + # 'special-members': '__call__', } -#autodoc_typehints = 'description' +autodoc_typehints = 'none' +autodoc_use_type_comments = False # -- Options for intersphinx extension --------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration diff --git a/documentation/contribution.rst b/documentation/contribution.rst index 03de3d8..0aa517d 100644 --- a/documentation/contribution.rst +++ b/documentation/contribution.rst @@ -72,6 +72,20 @@ Guidance and Standards can be found in the `maintenance guide `_. +Artificial Intelligence +------------------------------------------------------------------------------- + +* Contributions, which are co-authored by large language models (LLMs), are + welcome, provided that they adhere to the project guidance and standards + above and are, otherwise, of good quality. + +* A more compact representation of the above guidance and standards, plus some + other advice for these models, can be found in + ``.auxiliary/configuration/conventions.md``. You may link to this file from a + ``AGENTS.md``, ``CLAUDE.md``, ``CONVENTIONS.md`` file in the root of the + project. These files are ignored by Git as we do not wish to pollute the + root of the project with them in the upstream repository. + Resources ------------------------------------------------------------------------------- diff --git a/documentation/devapi.rst b/documentation/devapi.rst index cd22b59..bafacf7 100644 --- a/documentation/devapi.rst +++ b/documentation/devapi.rst @@ -28,6 +28,8 @@ Internal Development Interface Package ``classcore.__`` =============================================================================== +.. automodule:: classcore.__ + Module ``classcore.__.imports`` ------------------------------------------------------------------------------- diff --git a/documentation/examples/index.rst b/documentation/examples/index.rst index 9d1488d..2766596 100644 --- a/documentation/examples/index.rst +++ b/documentation/examples/index.rst @@ -29,3 +29,4 @@ Examples std-protocols std-behaviors std-modules + std-dynadoc diff --git a/documentation/examples/std-dynadoc.rst b/documentation/examples/std-dynadoc.rst new file mode 100644 index 0000000..7661603 --- /dev/null +++ b/documentation/examples/std-dynadoc.rst @@ -0,0 +1,210 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Dynadoc Integration +******************************************************************************* + + +Introduction +=============================================================================== + +Seamless integration with Dynadoc is provided to automate documentation of +classes and their members. This integration allows you to configure Dynadoc +behavior at both the metaclass and individual class levels, ensuring that your +classes receive appropriate documentation based on their structure and +annotations. + +.. doctest:: Dynadoc.Integration + + >>> from typing import Annotated + >>> import classcore.standard as ccstd + >>> import dynadoc + + +Basic Configuration +=============================================================================== + +Dynadoc configuration can be applied to classes through the standard Classcore +metaclasses. The configuration controls how documentation is generated, +including which objects to introspect and how to render the results. + +.. doctest:: Dynadoc.Integration + + >>> class Person( ccstd.DataclassObject ): + ... ''' A person with basic information. ''' + ... name: Annotated[ str, dynadoc.Doc( "Full name of the person" ) ] + ... age: Annotated[ int, dynadoc.Doc( "Age in years" ) ] + ... email: Annotated[ str | None, dynadoc.Doc( "Email address if available" ) ] = None + ... + >>> print( Person.__doc__ ) + A person with basic information. + + :ivar name: Full name of the person + :vartype name: str + :ivar age: Age in years + :vartype age: int + :ivar email: Email address if available + :vartype email: str | None + +The documentation is automatically enhanced with parameter information derived +from the dataclass fields and their type annotations. + + +Metaclass-Level Configuration +=============================================================================== + +You can configure Dynadoc behavior for all classes created with a particular +metaclass by setting configuration attributes directly on the metaclass: + +.. doctest:: Dynadoc.Integration + + >>> # Create a custom metaclass with specific Dynadoc settings + >>> @ccstd.class_factory( + ... dynadoc_configuration = ccstd.dynadoc.produce_dynadoc_configuration( + ... preserve = False # Don't preserve existing docstrings + ... ) + ... ) + ... class CustomClass( type ): pass + ... + >>> class Calculator( metaclass = CustomClass ): + ... ''' Original calculator docstring. ''' + ... def add( + ... self, + ... x: Annotated[ float, dynadoc.Doc( "First number" ) ], + ... y: Annotated[ float, dynadoc.Doc( "Second number" ) ] + ... ) -> Annotated[ float, dynadoc.Doc( "Sum of x and y" ) ]: + ... ''' Add two numbers. ''' + ... return x + y + ... def multiply( + ... self, + ... x: Annotated[ float, dynadoc.Doc( "First number" ) ], + ... y: Annotated[ float, dynadoc.Doc( "Second number" ) ] + ... ) -> Annotated[ float, dynadoc.Doc( "Product of x and y" ) ]: + ... ''' Multiply two numbers. ''' + ... return x * y + ... + >>> print( Calculator.__doc__ ) + None + +.. code-block:: text + + >>> print( Calculator.add.__doc__ ) + Add two numbers. + + :argument self: + :argument x: First number + :type x: float + :argument y: Second number + :type y: float + :returns: Sum of x and y + :rtype: float + +.. code-block:: text + + >>> print( Calculator.multiply.__doc__ ) + Multiply two numbers. + + :argument self: + :argument x: First number + :type x: float + :argument y: Second number + :type y: float + :returns: Product of x and y + :rtype: float + +Notice how the original class docstring was completely replaced (due to +``preserve: False``) and is now ``None``, while the individual method +docstrings were enhanced with parameter and return type documentation from +their annotations. + + +Class-Level Configuration +=============================================================================== + +Individual classes can override metaclass defaults by providing their own +Dynadoc configuration as a ``class`` statement argument: + +.. doctest:: Dynadoc.Integration + + >>> class Vehicle( + ... ccstd.DataclassObject, + ... dynadoc_configuration = { + ... 'preserve': True, + ... 'introspection': ccstd.dynadoc.produce_dynadoc_introspection_control( + ... enable = False # Disable automatic introspection + ... ) + ... } + ... ): + ... ''' A vehicle with make and model information. + ... + ... This class represents various types of vehicles. + ... ''' + ... make: Annotated[ str, dynadoc.Doc( "Vehicle manufacturer" ) ] + ... model: Annotated[ str, dynadoc.Doc( "Vehicle model name" ) ] + ... year: Annotated[ int, dynadoc.Doc( "Year of manufacture" ) ] + ... + >>> print( Vehicle.__doc__ ) + A vehicle with make and model information. + + This class represents various types of vehicles. + +Since introspection was disabled, only the original docstring is preserved +without any automatic parameter documentation. + + +Documentation of Modules +=============================================================================== + +A variation of ``assign_module_docstring`` is provided, which respects +immutable classes. This function is used by this package, itself, to +automatically generate documentation for its own modules: + +.. code-block:: python + + from . import standard + + # ... other imports and definitions ... + + standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) + standard.reclassify_modules( __name__, recursive = True ) + +This automatically generates comprehensive documentation for the entire +package, including all submodules. The key benefits of this variation include: + +* **Automatic immutable class avoidance**: By default, immutable classes are not + introspected during documentation generation to prevent potential issues. + +* **Recursive package documentation**: When applied to a package, it can + recursively document all submodules. + +* **Fragment table integration**: Supports reusable documentation fragments + for consistent terminology across the package. + +You can apply this to your own modules and packages: + +.. code-block:: python + + import classcore.standard as ccstd + + # At the end of your module's __init__.py + ccstd.dynadoc.assign_module_docstring( __name__ ) + + # Optionally make the entire package immutable + ccstd.reclassify_modules( __name__, recursive = True ) diff --git a/pyproject.toml b/pyproject.toml index 35aa08c..278488c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ license = 'Apache-2.0' readme = { 'file' = 'README.rst', 'content-type' = 'text/x-rst' } requires-python = '>= 3.10' dependencies = [ + 'dynadoc', 'typing-extensions', # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- @@ -166,6 +167,8 @@ python = [ '3.13', 'pypy3.10', ] +# [tool.hatch.metadata] +# allow-direct-references = true [tool.hatch.version] path = 'sources/classcore/__init__.py' @@ -182,7 +185,7 @@ strict = false # https://microsoft.github.io/pyright/#/configuration [tool.pyright] -ignore = [ 'tests' ] # Stronger hint for language server. +ignore = [ 'documentation', 'tests' ] # Stronger hint for language server. include = [ 'sources' ] reportConstantRedefinition = true reportInvalidTypeVarUse = true diff --git a/sources/classcore/__/__init__.py b/sources/classcore/__/__init__.py index ffb55ac..f97cc07 100644 --- a/sources/classcore/__/__init__.py +++ b/sources/classcore/__/__init__.py @@ -21,5 +21,13 @@ ''' Common constants, imports, and utilities. ''' +from .doctab import * from .imports import * from .nomina import * + + +T = typx.TypeVar( 'T', bound = type ) +U = typx.TypeVar( 'U' ) + + +dictproxy_empty: cabc.Mapping[ str, str ] = types.MappingProxyType( { } ) diff --git a/sources/classcore/__/doctab.py b/sources/classcore/__/doctab.py new file mode 100644 index 0000000..8e22c95 --- /dev/null +++ b/sources/classcore/__/doctab.py @@ -0,0 +1,76 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Docstring fragments. ''' + + +from . import imports as __ + + +_FragmentsTable: __.typx.TypeAlias = __.cabc.Mapping[ str, str ] +fragments: _FragmentsTable = __.types.MappingProxyType( { + + 'cfc class conceal': + ''' By default, conceals non-public class attributes. ''', + + 'cfc class protect': + ''' By default, protects class attributes. ''', + + 'cfc dynadoc': ''' Applies Dynadoc decoration to classes. ''', + + 'cfc instance conceal': + ''' Produces classes which can conceal instance attributes. ''', + + 'cfc instance protect': + ''' Produces classes which can protect instance attributes. ''', + + 'cfc produce dataclass': + ''' Produces inheritable dataclasses with keyword-only instantiation. ''', + + 'cfc produce protocol class': + ''' Produces :pep:`544` protocol classes. ''', + + 'class concealment': + ''' By default, non-public class attributes are invisible. ''', + + 'class protection': + ''' By default, class attributes are immutable. ''', + + 'class instance conceal': + ''' By default, conceals non-public instance attributes. ''', + + 'class instance protect': + ''' By default, protects instance attributes. ''', + + 'dataclass': + ''' Inheritable dataclass with keyword-only instantiation. ''', + + 'protocol class': + ''' Protocol class (:pep:`544`). Nominal and structural subtyping. ''', + + 'class dynadoc': ''' Is decorated by Dynadoc. ''', + + 'decorators': + ''' Sequence of class decorators. + + Each element takes a class argument and returns a class. + ''', + +} ) diff --git a/sources/classcore/__/imports.py b/sources/classcore/__/imports.py index a5ac700..3847faa 100644 --- a/sources/classcore/__/imports.py +++ b/sources/classcore/__/imports.py @@ -23,8 +23,6 @@ # ruff: noqa: F401 -from __future__ import annotations - import collections.abc as cabc import dataclasses as dcls import functools as funct @@ -35,9 +33,7 @@ import sys import types +import dynadoc import typing_extensions as typx # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- - - -ComparisonResult: typx.TypeAlias = bool | types.NotImplementedType diff --git a/sources/classcore/__/nomina.py b/sources/classcore/__/nomina.py index 1343cea..fcf14ac 100644 --- a/sources/classcore/__/nomina.py +++ b/sources/classcore/__/nomina.py @@ -18,14 +18,13 @@ #============================================================================# -''' Catalog of common type aliases. ''' +''' Common names and type aliases. ''' -from __future__ import annotations - from . import imports as __ +ComparisonResult: __.typx.TypeAlias = bool | __.types.NotImplementedType NominativeArguments: __.typx.TypeAlias = __.cabc.Mapping[ str, __.typx.Any ] PositionalArguments: __.typx.TypeAlias = __.cabc.Sequence[ __.typx.Any ] diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index ae46ff3..bd02a6e 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -18,7 +18,36 @@ #============================================================================# -''' Foundational class factories and decorators. ''' +''' Foundational class factories and decorators. + + Provides ability to create class decorators and metaclasses + with customization hooks. The metaclasses can apply class decorators + inline during the class construction and initialization process, properly + handling cases where decorators replace classes (e.g., + ``dataclasses.dataclass( slots = True )``). They also backport the repair + mechanism from newer versions of CPython to ensure that the class closure + cells are rectified on replaced classes, so that zero-argument ``super`` + calls function correctly in them. + + The ``classcore.standard`` subpackage is an example of the decorators and + customization hooks being used to provide a set of practical classes and + class decorators. Furthermore, the exception classes in the + :py:mod:`classcore.exceptions` module inherit from one of the standard + classes, making both the exception classes, themselves, and their + instances immutable and concealing their non-public attributes to reduce + API noise. I.e., this package "eats its own dog food" and provides + practical examples in so doing. + + This package is not as magical as it might seem. It does **not** rely on + any ``exec`` or ``eval`` calls and it does **not** do anything with + ``ctypes`` or similar surgical instruments. It relies completely on the + documented Python data model and the machinery that it provides. While it + is true that metaclasses can be tricky, this package is developed with a + deep, highly-evolved understanding of them. We seek simplicity over + cleverness and maintain robust tests across multiple Python + implementations and versions. The package is also very clean in terms of + static type checking (via Pyright). +''' from . import __ @@ -32,7 +61,9 @@ from .factories import * -__version__ = '1.1' +__version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] +__version__ = '1.2rc0' +standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) standard.reclassify_modules( __name__, recursive = True ) diff --git a/sources/classcore/decorators.py b/sources/classcore/decorators.py index ffd0fed..c7f9699 100644 --- a/sources/classcore/decorators.py +++ b/sources/classcore/decorators.py @@ -21,17 +21,14 @@ ''' Utilities for the decoration of classes, including metaclasses. ''' -from __future__ import annotations - from . import __ from . import nomina as _nomina from . import utilities as _utilities -_T = __.typx.TypeVar( '_T', bound = type ) - - -def apply_decorators( cls: type, decorators: _nomina.Decorators ) -> type: +def apply_decorators( + cls: type[ __.U ], decorators: _nomina.Decorators[ __.U ] +) -> type: ''' Applies sequence of decorators to class. If decorators replace classes (e.g., ``dataclass( slots = True )``), @@ -49,9 +46,9 @@ def apply_decorators( cls: type, decorators: _nomina.Decorators ) -> type: def decoration_by( - *decorators: _nomina.Decorator, - preparers: _nomina.DecorationPreparers = ( ), -) -> _nomina.Decorator: + *decorators: _nomina.Decorator[ __.U ], + preparers: _nomina.DecorationPreparers[ __.U ] = ( ), +) -> _nomina.Decorator[ __.U ]: ''' Class decorator which applies other class decorators. Useful to apply a stack of decorators as a sequence. @@ -61,7 +58,7 @@ def decoration_by( decorators list itself, such as to inject decorators based on introspection of the class. ''' - def decorate( cls: type ) -> type: + def decorate( cls: type[ __.U ] ) -> type[ __.U ]: decorators_ = list( decorators ) for preparer in preparers: preparer( cls, decorators_ ) return apply_decorators( cls, decorators_ ) @@ -71,24 +68,24 @@ def decorate( cls: type ) -> type: def produce_class_construction_decorator( attributes_namer: _nomina.AttributesNamer, - constructor: _nomina.ClassConstructor, -) -> _nomina.Decorator: + constructor: _nomina.ClassConstructor[ __.T ], +) -> _nomina.Decorator[ __.T ]: ''' Produces metaclass decorator to control class construction. Decorator overrides ``__new__`` on metaclass. ''' - def decorate( clscls: type[ _T ] ) -> type[ _T ]: + def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: constructor_name = attributes_namer( 'classes', 'constructor' ) extant = getattr( clscls, constructor_name, None ) original = getattr( clscls, '__new__' ) if extant is original: return clscls def construct( - clscls_: type[ _T ], + clscls_: type[ __.T ], name: str, bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], *, - decorators: _nomina.Decorators = ( ), + decorators: _nomina.Decorators[ __.T ] = ( ), **arguments: __.typx.Any, ) -> type[ object ]: return constructor( @@ -105,12 +102,12 @@ def construct( def produce_class_initialization_decorator( attributes_namer: _nomina.AttributesNamer, initializer: _nomina.ClassInitializer, -) -> _nomina.Decorator: +) -> _nomina.Decorator[ __.T ]: ''' Produces metaclass decorator to control class initialization. Decorator overrides ``__init__`` on metaclass. ''' - def decorate( clscls: type[ _T ] ) -> type[ _T ]: + def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: initializer_name = attributes_namer( 'classes', 'initializer' ) extant = getattr( clscls, initializer_name, None ) original = getattr( clscls, '__init__' ) diff --git a/sources/classcore/exceptions.py b/sources/classcore/exceptions.py index 4b3c0b2..6951fb3 100644 --- a/sources/classcore/exceptions.py +++ b/sources/classcore/exceptions.py @@ -21,15 +21,13 @@ ''' Exceptions from package. ''' -from __future__ import annotations - from . import __ from . import standard as _standard -class Omniexception( # pyright: ignore[reportGeneralTypeIssues] +class Omniexception( BaseException, _standard.Object, - instances_visibles = ( '__cause__', '__context__' ), # pyright: ignore[reportCallIssue] + instances_visibles = ( '__cause__', '__context__' ), ): ''' Base exception for package. ''' diff --git a/sources/classcore/factories.py b/sources/classcore/factories.py index 066db01..3ff1306 100644 --- a/sources/classcore/factories.py +++ b/sources/classcore/factories.py @@ -21,39 +21,35 @@ ''' Factories which produce metaclass implementations. ''' -from __future__ import annotations - from . import __ from . import decorators as _decorators from . import nomina as _nomina from . import utilities as _utilities -_T = __.typx.TypeVar( '_T', bound = type ) - - def produce_class_constructor( attributes_namer: _nomina.AttributesNamer, - preprocessors: _nomina.ProduceConstructorPreprocsArgument = ( ), - postprocessors: _nomina.ProduceConstructorPostprocsArgument = ( ), -) -> _nomina.ClassConstructor: + preprocessors: _nomina.ProduceConstructorPreprocsArgument[ __.T ] = ( ), + postprocessors: _nomina.ProduceConstructorPostprocsArgument[ __.T ] = ( ), +) -> _nomina.ClassConstructor[ __.T ]: ''' Produces constructors for classes. ''' def construct( # noqa: PLR0913 - clscls: type[ _T ], + clscls: type[ __.T ], superf: _nomina.ClassConstructorLigation, name: str, bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], arguments: __.NominativeArguments, - decorators: _nomina.Decorators, + decorators: _nomina.Decorators[ __.T ], ) -> type: ''' Constructs class, applying decorators and hooks. ''' bases_ = list( bases ) arguments_ = dict( arguments ) decorators_ = list( decorators ) - for prep in preprocessors: - prep( clscls, name, bases_, namespace, arguments_, decorators_ ) + for preprocessor in preprocessors: + preprocessor( + clscls, name, bases_, namespace, arguments_, decorators_ ) cls = superf( clscls, name, tuple( bases_ ), namespace, **arguments_ ) # Some decorators create new classes, which invokes this method again. # Short-circuit to prevent recursive decoration and other tangles. @@ -62,7 +58,7 @@ def construct( # noqa: PLR0913 in_progress = getattr( cls, progress_name_m, False ) if in_progress: return cls setattr( cls, progress_name_m, True ) - for postp in postprocessors: postp( cls, decorators_ ) + for postprocessor in postprocessors: postprocessor( cls, decorators_ ) cls = _decorators.apply_decorators( cls, decorators_ ) setattr( cls, progress_name_m, False ) return cls diff --git a/sources/classcore/nomina.py b/sources/classcore/nomina.py index d97364b..baa7ad2 100644 --- a/sources/classcore/nomina.py +++ b/sources/classcore/nomina.py @@ -21,26 +21,39 @@ ''' Catalog of common type aliases. ''' -from __future__ import annotations - from . import __ -AttributesNamer: __.typx.TypeAlias = ( - __.cabc.Callable[ [ str, str ], str ] ) +AttributesNamer: __.typx.TypeAlias = __.cabc.Callable[ [ str, str ], str ] + +Decorator: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Callable[ [ type[ __.U ] ], type[ __.U ] ], + __.dynadoc.Doc( + ''' Class decorator. -Decorator: __.typx.TypeAlias = __.cabc.Callable[ [ type ], type ] -Decorators: __.typx.TypeAlias = __.cabc.Sequence[ Decorator ] -DecoratorsMutable: __.typx.TypeAlias = __.cabc.MutableSequence[ Decorator ] + Takes class argument and returns class. + ''' ), +] +Decorators: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Sequence[ Decorator[ __.U ] ], __.dynadoc.Fname( 'decorators' ) +] +DecoratorsMutable: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.MutableSequence[ Decorator[ __.U ] ], + __.dynadoc.Fname( 'decorators' ), + __.dynadoc.Doc( + ''' Decorators may be inserted or removed from sequence. ''' ), +] DecorationPreparer: __.typx.TypeAlias = ( - __.cabc.Callable[ [ type, DecoratorsMutable ], None ] ) + __.cabc.Callable[ [ type[ __.U ], DecoratorsMutable[ __.U ] ], None ] ) DecorationPreparers: __.typx.TypeAlias = ( - __.cabc.Sequence[ DecorationPreparer ] ) + __.cabc.Sequence[ DecorationPreparer[ __.U ] ] ) +DecorationPreparersFactory: __.typx.TypeAlias = ( + __.cabc.Callable[ [ ], DecorationPreparers[ __.U ] ] ) ClassConstructorLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ ..., type ], - __.typx.Doc( + __.dynadoc.Doc( ''' Bound class constructor function. Usually from ``super( ).__new__`` or a partial function. @@ -48,7 +61,7 @@ ] InitializerLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ ..., None ], - __.typx.Doc( + __.dynadoc.Doc( ''' Bound initializer function. Usually from ``super( ).__init__`` or a partial function. @@ -56,7 +69,7 @@ ] AssignerLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ str, __.typx.Any ], None ], - __.typx.Doc( + __.dynadoc.Doc( ''' Bound attributes assigner function. Usually from ``super( ).__setattr__`` or a partial function. @@ -64,7 +77,7 @@ ] DeleterLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ str ], None ], - __.typx.Doc( + __.dynadoc.Doc( ''' Bound attributes deleter function. Usually from ``super( ).__delattr__`` or a partial function. @@ -72,7 +85,7 @@ ] SurveyorLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ ], __.cabc.Iterable[ str ] ], - __.typx.Doc( + __.dynadoc.Doc( ''' Bound attributes surveyor function. Usually from ``super( ).__dir__`` or a partial function. @@ -88,19 +101,19 @@ list[ type ], # bases (mutable) dict[ str, __.typx.Any ], # namespace (mutable) dict[ str, __.typx.Any ], # arguments (mutable) - DecoratorsMutable, # decorators (mutable) + DecoratorsMutable[ __.U ], # decorators (mutable) ], None ], - __.typx.Doc( + __.dynadoc.Doc( ''' Processes class data before construction. For use cases, such as argument conversion. ''' ), ] ClassConstructionPostprocessor: __.typx.TypeAlias = __.typx.Annotated[ - __.cabc.Callable[ [ type, DecoratorsMutable ], None ], - __.typx.Doc( + __.cabc.Callable[ [ type, DecoratorsMutable[ __.U ] ], None ], + __.dynadoc.Doc( ''' Processes class before decoration. For use cases, such as decorator list manipulation. @@ -109,7 +122,7 @@ # TODO: ClassInitializationPreparer (arguments mutation) ClassInitializationCompleter: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ type ], None ], - __.typx.Doc( + __.dynadoc.Doc( ''' Completes initialization of class. For use cases, such as enabling immutability once all other @@ -127,11 +140,11 @@ tuple[ type, ... ], dict[ str, __.typx.Any ], __.NominativeArguments, - Decorators, + Decorators[ __.U ], ], type ], - __.typx.Doc( ''' Constructor to use with metaclass. ''' ), + __.dynadoc.Doc( ''' Constructor to use with metaclass. ''' ), ] ClassInitializer: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ @@ -143,20 +156,22 @@ ], None ], - __.typx.Doc( ''' Initializer to use with metaclass. ''' ), + __.dynadoc.Doc( ''' Initializer to use with metaclass. ''' ), ] ProduceConstructorPreprocsArgument: __.typx.TypeAlias = __.typx.Annotated[ - __.cabc.Sequence[ ClassConstructionPreprocessor ], - __.typx.Doc( ''' Processors to apply before construction of class. ''' ), + __.cabc.Sequence[ ClassConstructionPreprocessor[ __.U ] ], + __.dynadoc.Doc( + ''' Processors to apply before construction of class. ''' ), ] ProduceConstructorPostprocsArgument: __.typx.TypeAlias = __.typx.Annotated[ - __.cabc.Sequence[ ClassConstructionPostprocessor ], - __.typx.Doc( ''' Processors to apply before decoration of class. ''' ), + __.cabc.Sequence[ ClassConstructionPostprocessor[ __.U ] ], + __.dynadoc.Doc( + ''' Processors to apply before decoration of class. ''' ), ] ProduceInitializerCompletersArgument: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Sequence[ ClassInitializationCompleter ], - __.typx.Doc( + __.dynadoc.Doc( ''' Processors to apply at final stage of class initialization. ''' ), ] diff --git a/sources/classcore/standard/__.py b/sources/classcore/standard/__.py index a0b55ce..0462c34 100644 --- a/sources/classcore/standard/__.py +++ b/sources/classcore/standard/__.py @@ -21,8 +21,6 @@ ''' Common constants, imports, and utilities. ''' -from __future__ import annotations - from ..__ import * # noqa: F403 diff --git a/sources/classcore/standard/__init__.py b/sources/classcore/standard/__init__.py index 64f32fb..30a8576 100644 --- a/sources/classcore/standard/__init__.py +++ b/sources/classcore/standard/__init__.py @@ -18,10 +18,34 @@ #============================================================================# -''' Decorators and class factories providing concealment and immutability. ''' +''' Decorators and class factories providing concealment and immutability. + Concealment restricts the visibility of attributes on classes and their + instances. By default, only public attributes (ones which do not start with + ``_``) are revealed for :py:func:`dir` calls. This behavior can be + overriden by supplying visibility verifiers as a decorator factory + argument or metaclass argument. These can be a sequence of attribute + names, regular expression :py:class:`re.Pattern` objects which match + attribute names, or predicate functions which match attribute names. Or, + total visibility (per the Python default) can be achieved by supplying + ``visibles = '*'`` instead of a sequence of verifiers. -from __future__ import annotations + Immutability prevents assignment (including reassignment) or deletion of + attrubtes on classes and their instances after they have been completely + initialized. In addition to any standard Python class, this can be applied + to dataclasses, allowing them to use ``__post_init__`` to set attributes, + which ``dataclasses.dataclass( frozen = True )`` prevents. The + immutability behavior can be overridden by supplying mutability verifiers + as a decorator factory argument or metaclass argument. These behave + similarly to the visibility verifiers described above. + + Hooks to modify the concealment and immutability behaviors are also + available. +''' + + +from . import dynadoc +from . import nomina from .classes import * from .decorators import * diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 1925a44..5798399 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -23,17 +23,11 @@ # Maybe enum for mutability and visibility. -from __future__ import annotations - from .. import utilities as _utilities from . import __ from . import nomina as _nomina -concealment_label = 'concealment' -immutability_label = 'immutability' - - def assign_attribute_if_mutable( # noqa: PLR0913 obj: object, /, *, ligation: _nomina.AssignerLigation, @@ -43,10 +37,11 @@ def assign_attribute_if_mutable( # noqa: PLR0913 name: str, value: __.typx.Any, ) -> None: + ''' Assigns attribute if it is mutable, else raises error. ''' leveli = 'instance' if level == 'instances' else level behaviors_name = attributes_namer( leveli, 'behaviors' ) behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) - if immutability_label not in behaviors: + if _nomina.immutability_label not in behaviors: ligation( name, value ) return names_name = attributes_namer( level, 'mutables_names' ) @@ -83,10 +78,11 @@ def delete_attribute_if_mutable( # noqa: PLR0913 level: str, name: str, ) -> None: + ''' Deletes attribute if it is mutable, else raises error. ''' leveli = 'instance' if level == 'instances' else level behaviors_name = attributes_namer( leveli, 'behaviors' ) behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) - if immutability_label not in behaviors: + if _nomina.immutability_label not in behaviors: ligation( name ) return names_name = attributes_namer( level, 'mutables_names' ) @@ -121,11 +117,12 @@ def survey_visible_attributes( attributes_namer: _nomina.AttributesNamer, level: str, ) -> __.cabc.Iterable[ str ]: + ''' Returns sequence of visible attributes. ''' names_base = ligation( ) leveli = 'instance' if level == 'instances' else level behaviors_name = attributes_namer( leveli, 'behaviors' ) behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) - if concealment_label not in behaviors: return names_base + if _nomina.concealment_label not in behaviors: return names_base names_name = attributes_namer( level, 'visibles_names' ) names: _nomina.BehaviorExclusionNamesOmni = ( getattr( obj, names_name, frozenset( ) ) ) @@ -161,6 +158,7 @@ def classify_behavior_exclusion_verifiers( _nomina.BehaviorExclusionRegexes, _nomina.BehaviorExclusionPredicates, ]: + ''' Threshes sequence of behavior exclusion verifiers into bins. ''' names: set[ str ] = set( ) regexes: list[ __.re.Pattern[ str ] ] = [ ] predicates: list[ __.cabc.Callable[ ..., bool ] ] = [ ] @@ -179,7 +177,8 @@ def classify_behavior_exclusion_verifiers( def produce_class_construction_preprocessor( attributes_namer: _nomina.AttributesNamer -) -> _nomina.ClassConstructionPreprocessor: +) -> _nomina.ClassConstructionPreprocessor[ __.U ]: + ''' Produces construction processor which handles metaclass arguments. ''' def preprocess( # noqa: PLR0913 clscls: type, @@ -187,7 +186,7 @@ def preprocess( # noqa: PLR0913 bases: list[ type ], namespace: dict[ str, __.typx.Any ], arguments: dict[ str, __.typx.Any ], - decorators: _nomina.DecoratorsMutable, + decorators: _nomina.DecoratorsMutable[ __.U ], ) -> None: record_class_construction_arguments( attributes_namer, namespace, arguments ) @@ -197,16 +196,23 @@ def preprocess( # noqa: PLR0913 def produce_class_construction_postprocessor( attributes_namer: _nomina.AttributesNamer -) -> _nomina.ClassConstructionPostprocessor: +) -> _nomina.ClassConstructionPostprocessor[ __.U ]: + ''' Produces construction processor which determines class decorators. ''' arguments_name = attributes_namer( 'class', 'construction_arguments' ) def postprocess( - cls: type, decorators: _nomina.DecoratorsMutable + cls: type, decorators: _nomina.DecoratorsMutable[ __.U ] ) -> None: arguments = getattr( cls, arguments_name, { } ) + clscls = type( cls ) + dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } ) + if not dynadoc_cfg: # either metaclass argument or attribute + dynadoc_cfg_name = ( + attributes_namer( 'classes', 'dynadoc_configuration' ) ) + dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } ) + decorators.append( __.dynadoc.with_docstring( **dynadoc_cfg ) ) dcls_spec = getattr( cls, '__dataclass_transform__', None ) if not dcls_spec: # either base class or metaclass may be marked - clscls = type( cls ) dcls_spec = getattr( clscls, '__dataclass_transform__', None ) instances_mutables = arguments.get( 'instances_mutables', __.mutables_default ) @@ -220,7 +226,7 @@ def postprocess( else: from .decorators import with_standard_behaviors decorator_factory = with_standard_behaviors - decorator = decorator_factory( + decorator: _nomina.Decorator[ __.U ] = decorator_factory( mutables = instances_mutables, visibles = instances_visibles ) decorators.append( decorator ) @@ -230,6 +236,7 @@ def postprocess( def produce_class_initialization_completer( attributes_namer: _nomina.AttributesNamer ) -> _nomina.ClassInitializationCompleter: + ''' Produces initialization completer which finalizes class behaviors. ''' arguments_name = attributes_namer( 'class', 'construction_arguments' ) def complete( cls: type ) -> None: @@ -243,12 +250,12 @@ def complete( cls: type ) -> None: record_behavior( cls, attributes_namer = attributes_namer, level = 'class', basename = 'mutables', - label = immutability_label, behaviors = behaviors, + label = _nomina.immutability_label, behaviors = behaviors, verifiers = mutables ) record_behavior( cls, attributes_namer = attributes_namer, level = 'class', basename = 'visibles', - label = concealment_label, behaviors = behaviors, + label = _nomina.concealment_label, behaviors = behaviors, verifiers = visibles ) # Set behaviors attribute last since it enables enforcement. behaviors_name = attributes_namer( 'class', 'behaviors' ) @@ -266,6 +273,7 @@ def record_behavior( # noqa: PLR0913 behaviors: set[ str ], verifiers: _nomina.BehaviorExclusionVerifiersOmni, ) -> None: + ''' Records details of particular class behavior, such as immutability. ''' names_name = attributes_namer( level, f"{basename}_names" ) if verifiers == '*': setattr( cls, names_name, '*' ) @@ -298,6 +306,7 @@ def record_class_construction_arguments( namespace: dict[ str, __.typx.Any ], arguments: dict[ str, __.typx.Any ], ) -> None: + ''' Captures metaclass arguments as class attribute for later use. ''' arguments_name = attributes_namer( 'class', 'construction_arguments' ) arguments_ = namespace.get( arguments_name, { } ) # Decorators, which replace classes, will cause construction of the @@ -308,6 +317,7 @@ def record_class_construction_arguments( for name in ( 'class_mutables', 'class_visibles', 'instances_mutables', 'instances_visibles', + 'dynadoc_configuration', ): if name not in arguments: continue arguments_[ name ] = arguments.pop( name ) diff --git a/sources/classcore/standard/classes.py b/sources/classcore/standard/classes.py index 76f46b0..cadfb52 100644 --- a/sources/classcore/standard/classes.py +++ b/sources/classcore/standard/classes.py @@ -19,74 +19,224 @@ ''' Standard classes and class factories. ''' -# TODO? ClassMutable and ProtocolClassMutable -# Need inheritance of omnimutability and omnivisibility. -from __future__ import annotations - from . import __ from . import decorators as _decorators +from . import dynadoc as _dynadoc +from . import nomina as _nomina -@_decorators.decoration_by( *_decorators.class_factory_decorators ) -class Class( type ): pass +_dynadoc_configuration = ( + _dynadoc.produce_dynadoc_configuration( table = __.fragments ) ) +_class_factory = __.funct.partial( + _decorators.class_factory, dynadoc_configuration = _dynadoc_configuration ) -@_decorators.decoration_by( *_decorators.class_factory_decorators ) -@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) -class Dataclass( type ): pass +class _CfcExtraArguments( __.typx.TypedDict, total = False ): + class_mutables: _nomina.BehaviorExclusionVerifiersOmni + class_visibles: _nomina.BehaviorExclusionVerifiersOmni + dynadoc_configuration: _nomina.DynadocConfiguration + instances_mutables: _nomina.BehaviorExclusionVerifiersOmni + instances_visibles: _nomina.BehaviorExclusionVerifiersOmni -@_decorators.decoration_by( *_decorators.class_factory_decorators ) -@__.typx.dataclass_transform( kw_only_default = True ) -class DataclassMutable( type ): pass +@_class_factory( ) +class Class( type ): + ''' Metaclass for standard classes. ''' + + _dynadoc_fragments_ = ( + 'cfc class conceal', 'cfc class protect', 'cfc dynadoc', + 'cfc instance conceal', 'cfc instance protect' ) -@_decorators.decoration_by( *_decorators.class_factory_decorators ) -class ProtocolClass( type( __.typx.Protocol ) ): pass + def __new__( # Typechecker stub. + clscls: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Unpack[ _CfcExtraArguments ], + ) -> __.T: + return super( ).__new__( clscls, name, bases, namespace ) -@_decorators.decoration_by( *_decorators.class_factory_decorators ) +@_class_factory( ) @__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) -class ProtocolDataclass( type( __.typx.Protocol ) ): pass +class Dataclass( type ): + ''' Metaclass for standard dataclasses. ''' + + _dynadoc_fragments_ = ( + 'cfc produce dataclass', + 'cfc class conceal', 'cfc class protect', 'cfc dynadoc', + 'cfc instance conceal', 'cfc instance protect' ) + + def __new__( # Typechecker stub. + clscls: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Unpack[ _CfcExtraArguments ], + ) -> __.T: + return super( ).__new__( clscls, name, bases, namespace ) + + +@_class_factory( ) +@__.typx.dataclass_transform( kw_only_default = True ) +class DataclassMutable( type ): + ''' Metaclass for dataclasses with mutable instance attributes. ''' + + _dynadoc_fragments_ = ( + 'cfc produce dataclass', + 'cfc class conceal', 'cfc class protect', 'cfc dynadoc', + 'cfc instance conceal' ) + + def __new__( # Typechecker stub. + clscls: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Unpack[ _CfcExtraArguments ], + ) -> __.T: + return super( ).__new__( clscls, name, bases, namespace ) + + +@_class_factory( ) +class ProtocolClass( type( __.typx.Protocol ) ): + ''' Metaclass for standard protocol classes. ''' + + _dynadoc_fragments_ = ( + 'cfc produce protocol class', + 'cfc class conceal', 'cfc class protect', 'cfc dynadoc', + 'cfc instance conceal', 'cfc instance protect' ) + + def __new__( # Typechecker stub. + clscls: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Unpack[ _CfcExtraArguments ], + ) -> __.T: + return super( ).__new__( clscls, name, bases, namespace ) + + +@_class_factory( ) +@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) +class ProtocolDataclass( type( __.typx.Protocol ) ): + ''' Metaclass for standard protocol dataclasses. ''' + + _dynadoc_fragments_ = ( + 'cfc produce protocol class', 'cfc produce dataclass', + 'cfc class conceal', 'cfc class protect', 'cfc dynadoc', + 'cfc instance conceal', 'cfc instance protect' ) + + def __new__( # Typechecker stub. + clscls: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Unpack[ _CfcExtraArguments ], + ) -> __.T: + return super( ).__new__( clscls, name, bases, namespace ) + + +@_class_factory( ) +@__.typx.dataclass_transform( kw_only_default = True ) +class ProtocolDataclassMutable( type( __.typx.Protocol ) ): + ''' Metaclass for protocol dataclasses with mutable instance attributes. + ''' + _dynadoc_fragments_ = ( + 'cfc produce protocol class', 'cfc produce dataclass', + 'cfc class conceal', 'cfc class protect', 'cfc dynadoc', + 'cfc instance conceal' ) -@_decorators.decoration_by( *_decorators.class_factory_decorators ) -@__.typx.dataclass_transform( kw_only_default = True ) -class ProtocolDataclassMutable( type( __.typx.Protocol ) ): pass + def __new__( # Typechecker stub. + clscls: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Unpack[ _CfcExtraArguments ], + ) -> __.T: + return super( ).__new__( clscls, name, bases, namespace ) -class Object( metaclass = Class ): pass +class Object( metaclass = Class ): + ''' Standard base class. ''' + _dynadoc_fragments_ = ( + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal', 'class instance protect' ) -class ObjectMutable( # pyright: ignore[reportGeneralTypeIssues] - metaclass = Class, - instances_mutables = '*', # pyright: ignore[reportCallIssue] -): pass +class ObjectMutable( metaclass = Class, instances_mutables = '*' ): + ''' Base class with mutable instance attributes. ''' -class DataclassObject( metaclass = Dataclass ): pass + _dynadoc_fragments_ = ( + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal' ) -class DataclassObjectMutable( metaclass = DataclassMutable ): pass +class DataclassObject( metaclass = Dataclass ): + ''' Standard base dataclass. ''' + _dynadoc_fragments_ = ( + 'dataclass', + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal', 'class instance protect' ) -class Protocol( __.typx.Protocol, metaclass = ProtocolClass ): pass +class DataclassObjectMutable( metaclass = DataclassMutable ): + ''' Base dataclass with mutable instance attributes. ''' -class ProtocolMutable( # pyright: ignore[reportGeneralTypeIssues] - __.typx.Protocol, - metaclass = ProtocolClass, - instances_mutables = '*', # pyright: ignore[reportCallIssue] -): pass + _dynadoc_fragments_ = ( + 'dataclass', + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal' ) + + +class Protocol( __.typx.Protocol, metaclass = ProtocolClass ): + ''' Standard base protocol class. ''' + + _dynadoc_fragments_ = ( + 'protocol class', + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal', 'class instance protect' ) + + +class ProtocolMutable( + __.typx.Protocol, metaclass = ProtocolClass, instances_mutables = '*' +): + ''' Base protocol class with mutable instance attributes. ''' + + _dynadoc_fragments_ = ( + 'protocol class', + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal' ) class DataclassProtocol( __.typx.Protocol, metaclass = ProtocolDataclass, -): pass +): + ''' Standard base protocol dataclass. ''' + + _dynadoc_fragments_ = ( + 'dataclass', 'protocol class', + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal', 'class instance protect' ) class DataclassProtocolMutable( __.typx.Protocol, metaclass = ProtocolDataclassMutable, -): pass +): + ''' Base protocol dataclass with mutable instance attributes. ''' + + _dynadoc_fragments_ = ( + 'dataclass', 'protocol class', + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal' ) diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 781442a..8181ab1 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -24,8 +24,6 @@ # ruff: noqa: F401 -from __future__ import annotations - from .. import factories as _factories from .. import utilities as _utilities from ..decorators import ( @@ -35,55 +33,118 @@ ) from . import __ from . import behaviors as _behaviors +from . import dynadoc as _dynadoc from . import nomina as _nomina -_U = __.typx.TypeVar( '_U' ) +_dataclass_core = __.dcls.dataclass( kw_only = True, slots = True ) +_dynadoc_configuration = _dynadoc.produce_dynadoc_configuration( ) -_dataclass_core = __.dcls.dataclass( kw_only = True, slots = True ) +def prepare_dataclass_for_instances( + cls: type, + decorators: _nomina.DecoratorsMutable[ __.U ], /, *, + attributes_namer: _nomina.AttributesNamer, +) -> None: + ''' Annotates dataclass in support of instantiation machinery. ''' + annotations = __.inspect.get_annotations( cls ) + behaviors_name = attributes_namer( 'instance', 'behaviors' ) + behaviors_name_m = _utilities.mangle_name( cls, behaviors_name ) + annotations[ behaviors_name_m ] = set[ str ] + setattr( cls, '__annotations__', annotations ) # in case of absence + setattr( cls, behaviors_name_m, __.dcls.field( init = False ) ) -def _produce_class_factory_core( +def apply_cfc_dynadoc_configuration( + clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer, - error_class_provider: _nomina.ErrorClassProvider, -) -> tuple[ _nomina.ClassConstructor, _nomina.ClassInitializer ]: + configuration: _nomina.DynadocConfiguration, +) -> None: + ''' Stores Dynadoc configuration on metaclass. ''' + configuration_name = attributes_namer( 'classes', 'dynadoc_configuration' ) + setattr( clscls, configuration_name, configuration ) + + +def apply_cfc_constructor( + clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer +) -> None: + ''' Injects '__new__' method into metaclass. ''' preprocessors = ( _behaviors.produce_class_construction_preprocessor( attributes_namer = attributes_namer ), ) postprocessors = ( _behaviors.produce_class_construction_postprocessor( attributes_namer = attributes_namer ), ) - completers = ( - _behaviors.produce_class_initialization_completer( - attributes_namer = attributes_namer ), ) - constructor = ( + constructor: _nomina.ClassConstructor[ __.T ] = ( _factories.produce_class_constructor( attributes_namer = attributes_namer, preprocessors = preprocessors, postprocessors = postprocessors ) ) + decorator = produce_class_construction_decorator( + attributes_namer = attributes_namer, constructor = constructor ) + decorator( clscls ) + + +def apply_cfc_initializer( + clscls: type[ __.T ], attributes_namer: _nomina.AttributesNamer +) -> None: + ''' Injects '__init__' method into metaclass. ''' + completers = ( + _behaviors.produce_class_initialization_completer( + attributes_namer = attributes_namer ), ) initializer = ( _factories.produce_class_initializer( attributes_namer = attributes_namer, completers = completers ) ) - return constructor, initializer + decorator = produce_class_initialization_decorator( + attributes_namer = attributes_namer, initializer = initializer ) + decorator( clscls ) -def prepare_dataclass_for_instances( - cls: type, - decorators: _nomina.DecoratorsMutable, /, *, +def apply_cfc_attributes_assigner( + clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer, + error_class_provider: _nomina.ErrorClassProvider, + implementation_core: _nomina.AssignerCore, ) -> None: - ''' Annotates dataclass in support of instantiation machinery. ''' - annotations = __.inspect.get_annotations( cls ) - behaviors_name = attributes_namer( 'instance', 'behaviors' ) - behaviors_name_m = _utilities.mangle_name( cls, behaviors_name ) - annotations[ behaviors_name_m ] = set[ str ] - setattr( cls, '__annotations__', annotations ) # in case of absence - setattr( cls, behaviors_name_m, __.dcls.field( init = False ) ) + ''' Injects '__setattr__' method into metaclass. ''' + decorator = produce_attributes_assignment_decorator( + level = 'class', + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + implementation_core = implementation_core ) + decorator( clscls ) + + +def apply_cfc_attributes_deleter( + clscls: type[ __.T ], /, + attributes_namer: _nomina.AttributesNamer, + error_class_provider: _nomina.ErrorClassProvider, + implementation_core: _nomina.DeleterCore, +) -> None: + ''' Injects '__delattr__' method into metaclass. ''' + decorator = produce_attributes_deletion_decorator( + level = 'class', + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + implementation_core = implementation_core ) + decorator( clscls ) + + +def apply_cfc_attributes_surveyor( + clscls: type[ __.T ], + attributes_namer: _nomina.AttributesNamer, + implementation_core: _nomina.SurveyorCore, +) -> None: + ''' Injects '__dir__' method into metaclass. ''' + decorator = produce_attributes_surveillance_decorator( + level = 'class', + attributes_namer = attributes_namer, + implementation_core = implementation_core ) + decorator( clscls ) -def produce_class_factory_decorators( +def class_factory( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, assigner_core: _nomina.AssignerCore = ( @@ -92,46 +153,43 @@ def produce_class_factory_decorators( _behaviors.delete_attribute_if_mutable ), surveyor_core: _nomina.SurveyorCore = ( _behaviors.survey_visible_attributes ), -) -> _nomina.Decorators: - decorators: list[ _nomina.Decorator ] = [ ] - constructor, initializer = ( - _produce_class_factory_core( - attributes_namer = attributes_namer, - error_class_provider = error_class_provider ) ) - decorators.append( - produce_class_construction_decorator( + dynadoc_configuration: __.cabc.Mapping[ str, __.typx.Any ] = ( + _dynadoc_configuration ), +) -> _nomina.Decorator[ __.T ]: + ''' Produces decorator to apply standard behaviors to metaclass. ''' + def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: + apply_cfc_dynadoc_configuration( + clscls, attributes_namer = attributes_namer, - constructor = constructor ) ) - decorators.append( - produce_class_initialization_decorator( - attributes_namer = attributes_namer, - initializer = initializer ) ) - decorators.append( - produce_attributes_assignment_decorator( - level = 'class', + configuration = dynadoc_configuration ) + apply_cfc_constructor( clscls, attributes_namer = attributes_namer ) + apply_cfc_initializer( clscls, attributes_namer = attributes_namer ) + apply_cfc_attributes_assigner( + clscls, attributes_namer = attributes_namer, error_class_provider = error_class_provider, - implementation_core = assigner_core ) ) - decorators.append( - produce_attributes_deletion_decorator( - level = 'class', + implementation_core = assigner_core ) + apply_cfc_attributes_deleter( + clscls, attributes_namer = attributes_namer, error_class_provider = error_class_provider, - implementation_core = deleter_core ) ) - decorators.append( - produce_attributes_surveillance_decorator( - level = 'class', + implementation_core = deleter_core ) + apply_cfc_attributes_surveyor( + clscls, attributes_namer = attributes_namer, - implementation_core = surveyor_core ) ) - return decorators + implementation_core = surveyor_core ) + return clscls + + return decorate def produce_instances_initialization_decorator( attributes_namer: _nomina.AttributesNamer, mutables: _nomina.BehaviorExclusionVerifiersOmni, visibles: _nomina.BehaviorExclusionVerifiersOmni, -) -> _nomina.Decorator: - def decorate( cls: type[ _U ] ) -> type[ _U ]: +) -> _nomina.Decorator[ __.U ]: + ''' Produces decorator to inject '__init__' method into class. ''' + def decorate( cls: type[ __.U ] ) -> type[ __.U ]: initializer_name = attributes_namer( 'instances', 'initializer' ) extant = getattr( cls, initializer_name, None ) original = getattr( cls, '__init__' ) @@ -142,12 +200,12 @@ def decorate( cls: type[ _U ] ) -> type[ _U ]: _behaviors.record_behavior( cls, attributes_namer = attributes_namer, level = 'instances', basename = 'mutables', - label = _behaviors.immutability_label, behaviors = behaviors, + label = _nomina.immutability_label, behaviors = behaviors, verifiers = mutables ) _behaviors.record_behavior( cls, attributes_namer = attributes_namer, level = 'instances', basename = 'visibles', - label = _behaviors.concealment_label, behaviors = behaviors, + label = _nomina.concealment_label, behaviors = behaviors, verifiers = visibles ) @__.funct.wraps( original ) @@ -171,8 +229,9 @@ def produce_attributes_assignment_decorator( attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, implementation_core: _nomina.AssignerCore, -) -> _nomina.Decorator: - def decorate( cls: type[ _U ] ) -> type[ _U ]: +) -> _nomina.Decorator[ __.U ]: + ''' Produces decorator to inject '__setattr__' method into class. ''' + def decorate( cls: type[ __.U ] ) -> type[ __.U ]: assigner_name = attributes_namer( level, 'assigner' ) extant = getattr( cls, assigner_name, None ) original = getattr( cls, '__setattr__' ) @@ -200,8 +259,9 @@ def produce_attributes_deletion_decorator( attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, implementation_core: _nomina.DeleterCore, -) -> _nomina.Decorator: - def decorate( cls: type[ _U ] ) -> type[ _U ]: +) -> _nomina.Decorator[ __.U ]: + ''' Produces decorator to inject '__delattr__' method into class. ''' + def decorate( cls: type[ __.U ] ) -> type[ __.U ]: deleter_name = attributes_namer( level, 'deleter' ) extant = getattr( cls, deleter_name, None ) original = getattr( cls, '__delattr__' ) @@ -228,8 +288,9 @@ def produce_attributes_surveillance_decorator( level: str, attributes_namer: _nomina.AttributesNamer, implementation_core: _nomina.SurveyorCore, -) -> _nomina.Decorator: - def decorate( cls: type[ _U ] ) -> type[ _U ]: +) -> _nomina.Decorator[ __.U ]: + ''' Produces decorator to inject '__dir__' method into class. ''' + def decorate( cls: type[ __.U ] ) -> type[ __.U ]: surveyor_name = attributes_namer( level, 'surveyor' ) extant = getattr( cls, surveyor_name, None ) original = getattr( cls, '__dir__' ) @@ -265,14 +326,15 @@ def produce_decorators_factory( # noqa: PLR0913 _nomina.BehaviorExclusionVerifiersOmni, _nomina.BehaviorExclusionVerifiersOmni ], - _nomina.Decorators + _nomina.Decorators[ __.U ] ]: + ''' Produces decorators to imbue class with standard behaviors. ''' def produce( mutables: _nomina.BehaviorExclusionVerifiersOmni, visibles: _nomina.BehaviorExclusionVerifiersOmni, - ) -> _nomina.Decorators: + ) -> _nomina.Decorators[ __.U ]: ''' Produces standard decorators. ''' - decorators: list[ _nomina.Decorator ] = [ ] + decorators: list[ _nomina.Decorator[ __.U ] ] = [ ] decorators.append( produce_instances_initialization_decorator( attributes_namer = attributes_namer, @@ -305,10 +367,16 @@ def produce_decoration_preparers_factory( attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, class_preparer: __.typx.Optional[ _nomina.ClassPreparer ] = None, -) -> __.cabc.Callable[ [ ], _nomina.DecorationPreparers ]: - def produce( ) -> _nomina.DecorationPreparers: +) -> _nomina.DecorationPreparersFactory[ __.U ]: + ''' Produces factory to produce class decoration preparers. + + E.g., a preparer needs to inject special annotations to ensure + compatibility with standard behaviors before + :py:func:`dataclasses.dataclass` decorates a class. + ''' + def produce( ) -> _nomina.DecorationPreparers[ __.U ]: ''' Produces processors for standard decorators. ''' - preprocessors: list[ _nomina.DecorationPreparer ] = [ ] + preprocessors: list[ _nomina.DecorationPreparer[ __.U ] ] = [ ] if class_preparer is not None: preprocessors.append( __.funct.partial( @@ -319,34 +387,33 @@ def produce( ) -> _nomina.DecorationPreparers: return produce -class_factory_decorators = produce_class_factory_decorators( ) - - @__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) def dataclass_with_standard_behaviors( - decorators: _nomina.Decorators = ( ), + decorators: _nomina.Decorators[ __.U ] = ( ), mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, -) -> _nomina.Decorator: +) -> _nomina.Decorator[ __.U ]: # https://github.com/microsoft/pyright/discussions/10344 ''' Dataclass decorator factory. ''' decorators_factory = produce_decorators_factory( level = 'instances' ) - decorators_ = decorators_factory( mutables, visibles ) + decorators_: _nomina.Decorators[ __.U ] = ( + decorators_factory( mutables, visibles ) ) preparers_factory = produce_decoration_preparers_factory( class_preparer = prepare_dataclass_for_instances ) - preparers = preparers_factory( ) + preparers: _nomina.DecorationPreparers[ __.U ] = preparers_factory( ) return decoration_by( *decorators, _dataclass_core, *decorators_, preparers = preparers ) def with_standard_behaviors( - decorators: _nomina.Decorators = ( ), + decorators: _nomina.Decorators[ __.U ] = ( ), mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, -) -> _nomina.Decorator: +) -> _nomina.Decorator[ __.U ]: ''' Class decorator factory. ''' decorators_factory = produce_decorators_factory( level = 'instances' ) - decorators_ = decorators_factory( mutables, visibles ) + decorators_: _nomina.Decorators[ __.U ] = ( + decorators_factory( mutables, visibles ) ) preparers_factory = produce_decoration_preparers_factory( ) - preparers = preparers_factory( ) + preparers: _nomina.DecorationPreparers[ __.U ] = preparers_factory( ) return decoration_by( *decorators, *decorators_, preparers = preparers ) diff --git a/sources/classcore/standard/dynadoc.py b/sources/classcore/standard/dynadoc.py new file mode 100644 index 0000000..a116af0 --- /dev/null +++ b/sources/classcore/standard/dynadoc.py @@ -0,0 +1,129 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Dynadoc integration. ''' + + +from .. import utilities as _utilities +from . import __ +from . import nomina as _nomina + + +dynadoc_context = __.dynadoc.produce_context( ) +dynadoc_class_introspection_control = ( + __.dynadoc.ClassIntrospectionControl( + inheritance = True, + introspectors = ( + __.dynadoc.introspection.introspect_special_classes, ) ) ) +dynadoc_module_introspection_control = ( + __.dynadoc.ModuleIntrospectionControl( ) ) + + +def dynadoc_avoid_immutables( + objct: object, + introspection: __.dynadoc.IntrospectionControl, + attributes_namer: _nomina.AttributesNamer, +) -> __.dynadoc.IntrospectionControl: + ''' Disables introspection of immutable objects. ''' + if __.inspect.isclass( objct ): + behaviors_name = attributes_namer( 'class', 'behaviors' ) + behaviors = _utilities.getattr0( objct, behaviors_name, frozenset( ) ) + if _nomina.immutability_label in behaviors: + return introspection.with_limit( + __.dynadoc.IntrospectionLimit( disable = True ) ) + return introspection + + +def produce_dynadoc_introspection_limiter( + attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, +) -> __.dynadoc.IntrospectionLimiter: + ''' Produces introspection limiter which avoids immutable objects. ''' + return __.funct.partial( + dynadoc_avoid_immutables, attributes_namer = attributes_namer ) + +dynadoc_introspection_limiter = produce_dynadoc_introspection_limiter( ) + + +def produce_dynadoc_introspection_control( + enable: bool = True, + class_control: __.dynadoc.ClassIntrospectionControl = ( + dynadoc_class_introspection_control ), + module_control: __.dynadoc.ModuleIntrospectionControl = ( + dynadoc_module_introspection_control ), + limiters: __.dynadoc.IntrospectionLimiters = ( + dynadoc_introspection_limiter, ), + targets: __.dynadoc.IntrospectionTargets = ( + __.dynadoc.IntrospectionTargetsSansModule ), +) -> __.dynadoc.IntrospectionControl: + ''' Produces compatible Dynadoc introspection control. ''' + return __.dynadoc.IntrospectionControl( + enable = enable, + class_control = class_control, + module_control = module_control, + limiters = limiters, + targets = targets ) + +dynadoc_introspection_on_class = produce_dynadoc_introspection_control( ) +dynadoc_introspection_on_package = ( + produce_dynadoc_introspection_control( + targets = __.dynadoc.IntrospectionTargetsOmni ) ) + + +def assign_module_docstring( # noqa: PLR0913 + module: str | __.types.ModuleType, /, + *fragments: __.dynadoc.interfaces.Fragment, + context: _nomina.DynadocContextArgument = dynadoc_context, + introspection: _nomina.DynadocIntrospectionArgument = ( + dynadoc_introspection_on_package ), + preserve: _nomina.DynadocPreserveArgument = True, + renderer: __.dynadoc.xtnsapi.Renderer = ( + __.dynadoc.assembly.renderer_default ), + table: _nomina.DynadocTableArgument = __.dictproxy_empty, +) -> None: + ''' Updates module docstring based on introspection. + + By default, recursively updates docstrings of all module members + which have docstrings. + + By default, ignores previously-decorated immutable classes. + ''' + __.dynadoc.assign_module_docstring( + module, + *fragments, + context = context, + introspection = introspection, + preserve = preserve, + renderer = renderer, + table = table ) + + +def produce_dynadoc_configuration( + context: _nomina.DynadocContextArgument = dynadoc_context, + introspection: _nomina.DynadocIntrospectionArgument = ( + dynadoc_introspection_on_class ), + preserve: _nomina.DynadocPreserveArgument = True, + table: _nomina.DynadocTableArgument = __.dictproxy_empty, +) -> _nomina.ProduceDynadocConfigurationReturn: + ''' Produces compatible Dynadoc configuration. ''' + return __.types.MappingProxyType( dict( + context = context, + introspection = introspection, + preserve = preserve, + table = table ) ) diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index 99f5ae7..6b3b093 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -21,11 +21,8 @@ ''' Standard module classes and reclassifers. ''' -from __future__ import annotations - from .. import utilities as _utilities from . import __ -from . import behaviors as _behaviors from . import classes as _classes from . import nomina as _nomina @@ -37,16 +34,16 @@ class Module( __.types.ModuleType, _classes.Object ): def reclassify_modules( attributes: __.typx.Annotated[ __.cabc.Mapping[ str, __.typx.Any ] | __.types.ModuleType | str, - __.typx.Doc( + __.dynadoc.Doc( 'Module, module name, or dictionary of object attributes.' ), ], /, *, attributes_namer: __.typx.Annotated[ _nomina.AttributesNamer, - __.typx.Doc( + __.dynadoc.Doc( ''' Attributes namer function with which to seal class. ''' ), ] = __.calculate_attrname, recursive: __.typx.Annotated[ - bool, __.typx.Doc( 'Recursively reclassify package modules?' ) + bool, __.dynadoc.Doc( 'Recursively reclassify package modules?' ) ] = False, ) -> None: # TODO? Ensure correct operation with namespace packages. @@ -54,13 +51,13 @@ def reclassify_modules( Can operate on individual modules or entire package hierarchies. - Notes - ----- - * Only converts modules within the same package to prevent unintended - modifications to external modules. - * When used with a dictionary, converts any module objects found as - values if they belong to the same package. - * Has no effect on already-reclassified modules. + Only converts modules within the same package to prevent unintended + modifications to external modules. + + When used with a dictionary, converts any module objects found as + values if they belong to the same package. + + Has no effect on already-reclassified modules. ''' if isinstance( attributes, str ): attributes = __.sys.modules[ attributes ] @@ -84,7 +81,7 @@ def reclassify_modules( def _seal_module( module: __.types.ModuleType, attributes_namer: _nomina.AttributesNamer ) -> None: - behaviors = { _behaviors.concealment_label, _behaviors.immutability_label } + behaviors = { _nomina.concealment_label, _nomina.immutability_label } behaviors_name = attributes_namer( 'instance', 'behaviors' ) _utilities.setattr0( module, behaviors_name, behaviors ) module.__class__ = Module diff --git a/sources/classcore/standard/nomina.py b/sources/classcore/standard/nomina.py index 6fd0545..30b7b2f 100644 --- a/sources/classcore/standard/nomina.py +++ b/sources/classcore/standard/nomina.py @@ -18,16 +18,18 @@ #============================================================================# -''' Catalog of common type aliases. ''' +''' Catalog of common names and type aliases. ''' # ruff: noqa: F403,F405 -from __future__ import annotations - from . import __ from ..nomina import * +concealment_label = 'concealment' +immutability_label = 'immutability' + + BehaviorExclusionNames: __.typx.TypeAlias = __.cabc.Set[ str ] BehaviorExclusionNamesOmni: __.typx.TypeAlias = ( BehaviorExclusionNames | __.typx.Literal[ '*' ] ) @@ -95,6 +97,45 @@ class ClassPreparer( __.typx.Protocol ): @staticmethod def __call__( # pragma: no branch class_: type, - decorators: DecoratorsMutable, /, *, + decorators: DecoratorsMutable[ __.U ], /, *, attributes_namer: AttributesNamer, ) -> None: raise NotImplementedError + + +DynadocConfiguration: __.typx.TypeAlias = __.cabc.Mapping[ str, __.typx.Any ] +DynadocContextArgument: __.typx.TypeAlias = __.typx.Annotated[ + __.dynadoc.Context, + __.dynadoc.Doc( + ''' Dynadoc context. + + Renderer, dictionaries for resolution of stringified annotations, + etc.... + ''' ), +] +DynadocIntrospectionArgument: __.typx.TypeAlias = __.typx.Annotated[ + __.dynadoc.IntrospectionControl, + __.dynadoc.Doc( + ''' Dynadoc introspection control. + + Which kinds of object to recursively introspect? + Scan unnannotated attributes? + Consider base classes? + Etc... + ''' ), +] +DynadocPreserveArgument: __.typx.TypeAlias = __.typx.Annotated[ + bool, __.dynadoc.Doc( ''' Preserve existing docstring? ''' ) +] +DynadocTableArgument: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Mapping[ str, str ], + __.dynadoc.Doc( ''' Table of documentation fragments. ''' ), +] +ProduceDynadocConfigurationReturn: __.typx.TypeAlias = __.typx.Annotated[ + DynadocConfiguration, + __.dynadoc.Doc( + ''' Dynadoc configuration dictionary. + + Suitable as a keyword expansion (``**``) argument to + ``assign_module_docstring`` or ``with_docstring``. + ''' ), +] diff --git a/sources/classcore/utilities.py b/sources/classcore/utilities.py index be6a78f..ba5e8e9 100644 --- a/sources/classcore/utilities.py +++ b/sources/classcore/utilities.py @@ -21,12 +21,11 @@ ''' Various utilities for class manipulation. ''' -from __future__ import annotations - from . import __ def describe_object( obj: object ) -> str: + ''' Returns object type with fully-qualified name. ''' if __.inspect.isclass( obj ): return "class '{}'".format( qualify_class_name( obj ) ) # TODO? functions, methods, etc... @@ -36,7 +35,7 @@ def describe_object( obj: object ) -> str: def getattr0( obj: object, name: str, default: __.typx.Any ) -> __.typx.Any: ''' Returns private attribute from object. - Uses mangled attribute which is unique to the class. + Uses mangled attribute name which is unique to the class. ''' name_m = mangle_name( obj, name ) return getattr( obj, name_m, default ) @@ -45,7 +44,7 @@ def getattr0( obj: object, name: str, default: __.typx.Any ) -> __.typx.Any: def delattr0( obj: object, name: str ) -> None: ''' Deletes private attribute on object. - Uses mangled attribute which is unique to the class. + Uses mangled attribute name which is unique to the class. ''' name_m = mangle_name( obj, name ) delattr( obj, name_m ) @@ -54,7 +53,7 @@ def delattr0( obj: object, name: str ) -> None: def setattr0( obj: object, name: str, value: __.typx.Any ) -> None: ''' Assigns private attribute to object. - Uses mangled attribute which is unique to the class. + Uses mangled attribute name which is unique to the class. ''' name_m = mangle_name( obj, name ) setattr( obj, name_m, value ) @@ -75,6 +74,7 @@ def mangle_name( obj: object, name: str ) -> str: def qualify_class_name( cls: type ) -> str: + ''' Returns fully-qualified class name. ''' return f"{cls.__module__}.{cls.__qualname__}" diff --git a/tests/test_000_classcore/test_320_standard_decorators.py b/tests/test_000_classcore/test_320_standard_decorators.py index ad5c7aa..854764a 100644 --- a/tests/test_000_classcore/test_320_standard_decorators.py +++ b/tests/test_000_classcore/test_320_standard_decorators.py @@ -27,9 +27,9 @@ def test_210_class_factory_decorator_idempotence( ): ''' Class factory decorators are idempotent. ''' module = cache_import_module( MODULE_QNAME ) - @module.decoration_by( *module.class_factory_decorators ) + @module.class_factory( ) class Class: pass - @module.decoration_by( *module.class_factory_decorators ) + @module.class_factory( ) class BetterClass( Class ): pass assert Class.__new__ is BetterClass.__new__ assert Class.__init__ is BetterClass.__init__ From 3d427744c87c9548b557586ef160e1c31e08d750 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Thu, 5 Jun 2025 22:50:46 +0000 Subject: [PATCH 07/28] Update documents for publication. --- .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 18 ++++++++++++++++++ sources/classcore/__init__.py | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index bbe5b22..23e016d 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63dacf1af59ccfbb72500d2c6966429493d29e4d81cbbd3c7ed8028fc301ff72 -size 198436 +oid sha256:1fc5d9b2cf031a7996e28a5d3d7622869c8b9eef9be4958952328bcbb85eb148 +size 202580 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 30dad75..d01ff44 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,24 @@ Release Notes .. towncrier release notes start +Classcore 1.2 (2025-06-05) +========================== + +Enhancements +------------ + +- Fill out docstrings for all public classes and functions in package. +- Integrate with Dynadoc. Adds special introspection control which avoid + docstring updates on immutable classes. Adds Dynadoc ``with_docstring`` + decorator to set of decorators on standard classes. + + +Repairs +------- + +- Address Pyright complaints about metaclass arguments in ``class`` statements. + + Classcore 1.1 (2025-05-01) ========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index bd02a6e..f249d36 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] -__version__ = '1.2rc0' +__version__ = '1.2' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) From 2f48f43c4ca066d585966ad0dc44e6dd00e363ff Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Sun, 8 Jun 2025 03:01:39 +0000 Subject: [PATCH 08/28] Update documents for publication. --- .../data/towncrier/+decorators.enhance.rst | 1 + .../data/towncrier/+decorators.repair.rst | 1 + .../data/towncrier/+docstrings.enhance.rst | 1 - .../data/towncrier/+dynadoc.enhance.rst | 3 - .../data/towncrier/+modules.enhance.rst | 1 + .auxiliary/data/towncrier/+pyright.repair.rst | 1 - .auxiliary/publications/website.tar.xz | 4 +- documentation/changelog.rst | 16 ++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/behaviors.py | 20 +- sources/classcore/standard/decorators.py | 181 ++++++++++-------- sources/classcore/standard/modules.py | 22 ++- 12 files changed, 151 insertions(+), 102 deletions(-) create mode 100644 .auxiliary/data/towncrier/+decorators.enhance.rst create mode 100644 .auxiliary/data/towncrier/+decorators.repair.rst delete mode 100644 .auxiliary/data/towncrier/+docstrings.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+dynadoc.enhance.rst create mode 100644 .auxiliary/data/towncrier/+modules.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+pyright.repair.rst diff --git a/.auxiliary/data/towncrier/+decorators.enhance.rst b/.auxiliary/data/towncrier/+decorators.enhance.rst new file mode 100644 index 0000000..a8537fd --- /dev/null +++ b/.auxiliary/data/towncrier/+decorators.enhance.rst @@ -0,0 +1 @@ +Simplify production of class decorators. diff --git a/.auxiliary/data/towncrier/+decorators.repair.rst b/.auxiliary/data/towncrier/+decorators.repair.rst new file mode 100644 index 0000000..703ca5e --- /dev/null +++ b/.auxiliary/data/towncrier/+decorators.repair.rst @@ -0,0 +1 @@ +Bugfix. Propagate error class provider from metaclass to class decorators. diff --git a/.auxiliary/data/towncrier/+docstrings.enhance.rst b/.auxiliary/data/towncrier/+docstrings.enhance.rst deleted file mode 100644 index f36b9b4..0000000 --- a/.auxiliary/data/towncrier/+docstrings.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Fill out docstrings for all public classes and functions in package. diff --git a/.auxiliary/data/towncrier/+dynadoc.enhance.rst b/.auxiliary/data/towncrier/+dynadoc.enhance.rst deleted file mode 100644 index ec7e2fe..0000000 --- a/.auxiliary/data/towncrier/+dynadoc.enhance.rst +++ /dev/null @@ -1,3 +0,0 @@ -Integrate with Dynadoc. Adds special introspection control which avoid -docstring updates on immutable classes. Adds Dynadoc ``with_docstring`` -decorator to set of decorators on standard classes. diff --git a/.auxiliary/data/towncrier/+modules.enhance.rst b/.auxiliary/data/towncrier/+modules.enhance.rst new file mode 100644 index 0000000..7e716bb --- /dev/null +++ b/.auxiliary/data/towncrier/+modules.enhance.rst @@ -0,0 +1 @@ +Allow arbitrary class to be supplied to ``reclassify_modules``. diff --git a/.auxiliary/data/towncrier/+pyright.repair.rst b/.auxiliary/data/towncrier/+pyright.repair.rst deleted file mode 100644 index c78d00b..0000000 --- a/.auxiliary/data/towncrier/+pyright.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Address Pyright complaints about metaclass arguments in ``class`` statements. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 23e016d..4aaf28e 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fc5d9b2cf031a7996e28a5d3d7622869c8b9eef9be4958952328bcbb85eb148 -size 202580 +oid sha256:7aa0c5cb192c5d272985442c6e03a69918e9fc3abbd3e26dd2bcbab77a4e8816 +size 212876 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index d01ff44..860aaa5 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,22 @@ Release Notes .. towncrier release notes start +Classcore 1.3 (2025-06-07) +========================== + +Enhancements +------------ + +- Allow arbitrary class to be supplied to ``reclassify_modules``. +- Simplify production of class decorators. + + +Repairs +------- + +- Bugfix. Propagate error class provider from metaclass to class decorators. + + Classcore 1.2 (2025-06-05) ========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index f249d36..dab488e 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] -__version__ = '1.2' +__version__ = '1.3' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 5798399..4f1a2fa 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -195,7 +195,8 @@ def preprocess( # noqa: PLR0913 def produce_class_construction_postprocessor( - attributes_namer: _nomina.AttributesNamer + attributes_namer: _nomina.AttributesNamer, + error_class_provider: _nomina.ErrorClassProvider, ) -> _nomina.ClassConstructionPostprocessor[ __.U ]: ''' Produces construction processor which determines class decorators. ''' arguments_name = attributes_namer( 'class', 'construction_arguments' ) @@ -214,6 +215,12 @@ def postprocess( dcls_spec = getattr( cls, '__dataclass_transform__', None ) if not dcls_spec: # either base class or metaclass may be marked dcls_spec = getattr( clscls, '__dataclass_transform__', None ) + instances_assigner = arguments.get( + 'instances_assigner_core', assign_attribute_if_mutable ) + instances_deleter = arguments.get( + 'instances_deleter_core', delete_attribute_if_mutable ) + instances_surveyor = arguments.get( + 'instances_surveyor_core', survey_visible_attributes ) instances_mutables = arguments.get( 'instances_mutables', __.mutables_default ) instances_visibles = arguments.get( @@ -227,7 +234,13 @@ def postprocess( from .decorators import with_standard_behaviors decorator_factory = with_standard_behaviors decorator: _nomina.Decorator[ __.U ] = decorator_factory( - mutables = instances_mutables, visibles = instances_visibles ) + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + assigner_core = instances_assigner, + deleter_core = instances_deleter, + surveyor_core = instances_surveyor, + mutables = instances_mutables, + visibles = instances_visibles ) decorators.append( decorator ) return postprocess @@ -316,6 +329,9 @@ def record_class_construction_arguments( arguments_ = { } for name in ( 'class_mutables', 'class_visibles', + 'instances_assigner_core', + 'instances_deleter_core', + 'instances_surveyor_core', 'instances_mutables', 'instances_visibles', 'dynadoc_configuration', ): diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 8181ab1..6c40bf7 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -66,7 +66,9 @@ def apply_cfc_dynadoc_configuration( def apply_cfc_constructor( - clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer + clscls: type[ __.T ], /, + attributes_namer: _nomina.AttributesNamer, + error_class_provider: _nomina.ErrorClassProvider, ) -> None: ''' Injects '__new__' method into metaclass. ''' preprocessors = ( @@ -74,7 +76,8 @@ def apply_cfc_constructor( attributes_namer = attributes_namer ), ) postprocessors = ( _behaviors.produce_class_construction_postprocessor( - attributes_namer = attributes_namer ), ) + attributes_namer = attributes_namer, + error_class_provider = error_class_provider ), ) constructor: _nomina.ClassConstructor[ __.T ] = ( _factories.produce_class_constructor( attributes_namer = attributes_namer, @@ -162,7 +165,10 @@ def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: clscls, attributes_namer = attributes_namer, configuration = dynadoc_configuration ) - apply_cfc_constructor( clscls, attributes_namer = attributes_namer ) + apply_cfc_constructor( + clscls, + attributes_namer = attributes_namer, + error_class_provider = error_class_provider ) apply_cfc_initializer( clscls, attributes_namer = attributes_namer ) apply_cfc_attributes_assigner( clscls, @@ -311,109 +317,116 @@ def survey( self: object ) -> __.cabc.Iterable[ str ]: return decorate -def produce_decorators_factory( # noqa: PLR0913 - level: str, +@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) +def dataclass_with_standard_behaviors( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, + decorators: _nomina.Decorators[ __.U ] = ( ), assigner_core: _nomina.AssignerCore = ( _behaviors.assign_attribute_if_mutable ), deleter_core: _nomina.DeleterCore = ( _behaviors.delete_attribute_if_mutable ), surveyor_core: _nomina.SurveyorCore = ( _behaviors.survey_visible_attributes ), -) -> __.cabc.Callable[ - [ - _nomina.BehaviorExclusionVerifiersOmni, - _nomina.BehaviorExclusionVerifiersOmni - ], - _nomina.Decorators[ __.U ] -]: - ''' Produces decorators to imbue class with standard behaviors. ''' - def produce( - mutables: _nomina.BehaviorExclusionVerifiersOmni, - visibles: _nomina.BehaviorExclusionVerifiersOmni, - ) -> _nomina.Decorators[ __.U ]: - ''' Produces standard decorators. ''' - decorators: list[ _nomina.Decorator[ __.U ] ] = [ ] - decorators.append( - produce_instances_initialization_decorator( - attributes_namer = attributes_namer, - mutables = mutables, visibles = visibles ) ) - if mutables != '*': - decorators.append( - produce_attributes_assignment_decorator( - level = level, - attributes_namer = attributes_namer, - error_class_provider = error_class_provider, - implementation_core = assigner_core ) ) - decorators.append( - produce_attributes_deletion_decorator( - level = level, - attributes_namer = attributes_namer, - error_class_provider = error_class_provider, - implementation_core = deleter_core ) ) - if visibles != '*': - decorators.append( - produce_attributes_surveillance_decorator( - level = level, - attributes_namer = attributes_namer, - implementation_core = surveyor_core ) ) - return decorators - - return produce - - -def produce_decoration_preparers_factory( - attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, - error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, - class_preparer: __.typx.Optional[ _nomina.ClassPreparer ] = None, -) -> _nomina.DecorationPreparersFactory[ __.U ]: - ''' Produces factory to produce class decoration preparers. - - E.g., a preparer needs to inject special annotations to ensure - compatibility with standard behaviors before - :py:func:`dataclasses.dataclass` decorates a class. - ''' - def produce( ) -> _nomina.DecorationPreparers[ __.U ]: - ''' Produces processors for standard decorators. ''' - preprocessors: list[ _nomina.DecorationPreparer[ __.U ] ] = [ ] - if class_preparer is not None: - preprocessors.append( - __.funct.partial( - class_preparer, - attributes_namer = attributes_namer ) ) - return tuple( preprocessors ) - - return produce - - -@__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) -def dataclass_with_standard_behaviors( - decorators: _nomina.Decorators[ __.U ] = ( ), mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, ) -> _nomina.Decorator[ __.U ]: # https://github.com/microsoft/pyright/discussions/10344 ''' Dataclass decorator factory. ''' - decorators_factory = produce_decorators_factory( level = 'instances' ) decorators_: _nomina.Decorators[ __.U ] = ( - decorators_factory( mutables, visibles ) ) - preparers_factory = produce_decoration_preparers_factory( - class_preparer = prepare_dataclass_for_instances ) - preparers: _nomina.DecorationPreparers[ __.U ] = preparers_factory( ) + _produce_instances_decorators( + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + assigner_core = assigner_core, + deleter_core = deleter_core, + surveyor_core = surveyor_core, + mutables = mutables, + visibles = visibles ) ) + preparers: _nomina.DecorationPreparers[ __.U ] = ( + _produce_instances_decoration_preparers( + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + class_preparer = prepare_dataclass_for_instances ) ) return decoration_by( *decorators, _dataclass_core, *decorators_, preparers = preparers ) -def with_standard_behaviors( +def with_standard_behaviors( # noqa: PLR0913 + attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, + error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, decorators: _nomina.Decorators[ __.U ] = ( ), + assigner_core: _nomina.AssignerCore = ( + _behaviors.assign_attribute_if_mutable ), + deleter_core: _nomina.DeleterCore = ( + _behaviors.delete_attribute_if_mutable ), + surveyor_core: _nomina.SurveyorCore = ( + _behaviors.survey_visible_attributes ), mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, ) -> _nomina.Decorator[ __.U ]: ''' Class decorator factory. ''' - decorators_factory = produce_decorators_factory( level = 'instances' ) decorators_: _nomina.Decorators[ __.U ] = ( - decorators_factory( mutables, visibles ) ) - preparers_factory = produce_decoration_preparers_factory( ) - preparers: _nomina.DecorationPreparers[ __.U ] = preparers_factory( ) + _produce_instances_decorators( + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + assigner_core = assigner_core, + deleter_core = deleter_core, + surveyor_core = surveyor_core, + mutables = mutables, + visibles = visibles ) ) + preparers: _nomina.DecorationPreparers[ __.U ] = ( + _produce_instances_decoration_preparers( + attributes_namer = attributes_namer, + error_class_provider = error_class_provider ) ) return decoration_by( *decorators, *decorators_, preparers = preparers ) + + +def _produce_instances_decoration_preparers( + attributes_namer: _nomina.AttributesNamer, + error_class_provider: _nomina.ErrorClassProvider, + class_preparer: __.typx.Optional[ _nomina.ClassPreparer ] = None, +) -> _nomina.DecorationPreparers[ __.U ]: + ''' Produces processors for standard decorators. ''' + preprocessors: list[ _nomina.DecorationPreparer[ __.U ] ] = [ ] + if class_preparer is not None: + preprocessors.append( + __.funct.partial( + class_preparer, attributes_namer = attributes_namer ) ) + return tuple( preprocessors ) + + +def _produce_instances_decorators( # noqa: PLR0913 + attributes_namer: _nomina.AttributesNamer, + error_class_provider: _nomina.ErrorClassProvider, + assigner_core: _nomina.AssignerCore, + deleter_core: _nomina.DeleterCore, + surveyor_core: _nomina.SurveyorCore, + mutables: _nomina.BehaviorExclusionVerifiersOmni, + visibles: _nomina.BehaviorExclusionVerifiersOmni, +) -> _nomina.Decorators[ __.U ]: + ''' Produces standard decorators. ''' + decorators: list[ _nomina.Decorator[ __.U ] ] = [ ] + decorators.append( + produce_instances_initialization_decorator( + attributes_namer = attributes_namer, + mutables = mutables, visibles = visibles ) ) + if mutables != '*': + decorators.append( + produce_attributes_assignment_decorator( + level = 'instances', + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + implementation_core = assigner_core ) ) + decorators.append( + produce_attributes_deletion_decorator( + level = 'instances', + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + implementation_core = deleter_core ) ) + if visibles != '*': + decorators.append( + produce_attributes_surveillance_decorator( + level = 'instances', + attributes_namer = attributes_namer, + implementation_core = surveyor_core ) ) + return decorators diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index 6b3b093..21b565c 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -35,7 +35,7 @@ def reclassify_modules( attributes: __.typx.Annotated[ __.cabc.Mapping[ str, __.typx.Any ] | __.types.ModuleType | str, __.dynadoc.Doc( - 'Module, module name, or dictionary of object attributes.' ), + ''' Module, module name, or dictionary of object attributes. ''' ), ], /, *, attributes_namer: __.typx.Annotated[ _nomina.AttributesNamer, @@ -43,8 +43,12 @@ def reclassify_modules( ''' Attributes namer function with which to seal class. ''' ), ] = __.calculate_attrname, recursive: __.typx.Annotated[ - bool, __.dynadoc.Doc( 'Recursively reclassify package modules?' ) + bool, __.dynadoc.Doc( ''' Recursively reclassify package modules? ''' ) ] = False, + replacement_class: __.typx.Annotated[ + type[ __.types.ModuleType ], + __.dynadoc.Doc( ''' New class for module. ''' ), + ] = Module, ) -> None: # TODO? Ensure correct operation with namespace packages. ''' Reclassifies modules to have attributes concealment and immutability. @@ -72,16 +76,18 @@ def reclassify_modules( if not __.inspect.ismodule( value ): continue if not value.__name__.startswith( f"{package_name}." ): continue if recursive: reclassify_modules( value, recursive = True ) - if isinstance( value, Module ): continue - _seal_module( value, attributes_namer ) - if module and not isinstance( module, Module ): - _seal_module( module, attributes_namer ) + if isinstance( value, replacement_class ): continue + _seal_module( value, attributes_namer, replacement_class ) + if module and not isinstance( module, replacement_class ): + _seal_module( module, attributes_namer, replacement_class ) def _seal_module( - module: __.types.ModuleType, attributes_namer: _nomina.AttributesNamer + module: __.types.ModuleType, + attributes_namer: _nomina.AttributesNamer, + replacement_class: type[ __.types.ModuleType ], ) -> None: behaviors = { _nomina.concealment_label, _nomina.immutability_label } behaviors_name = attributes_namer( 'instance', 'behaviors' ) _utilities.setattr0( module, behaviors_name, behaviors ) - module.__class__ = Module + module.__class__ = replacement_class From d96fa514b88a34e84e477d461b8aa24ade227653 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Sun, 8 Jun 2025 04:17:58 +0000 Subject: [PATCH 09/28] Update documents for publication. --- .auxiliary/data/towncrier/+decorators.enhance.rst | 1 - .auxiliary/data/towncrier/+decorators.repair.rst | 1 - .auxiliary/data/towncrier/+modules.enhance.rst | 1 - .auxiliary/data/towncrier/+modules.repair.rst | 2 ++ .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 10 ++++++++++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/modules.py | 6 +++++- 8 files changed, 20 insertions(+), 7 deletions(-) delete mode 100644 .auxiliary/data/towncrier/+decorators.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+decorators.repair.rst delete mode 100644 .auxiliary/data/towncrier/+modules.enhance.rst create mode 100644 .auxiliary/data/towncrier/+modules.repair.rst diff --git a/.auxiliary/data/towncrier/+decorators.enhance.rst b/.auxiliary/data/towncrier/+decorators.enhance.rst deleted file mode 100644 index a8537fd..0000000 --- a/.auxiliary/data/towncrier/+decorators.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Simplify production of class decorators. diff --git a/.auxiliary/data/towncrier/+decorators.repair.rst b/.auxiliary/data/towncrier/+decorators.repair.rst deleted file mode 100644 index 703ca5e..0000000 --- a/.auxiliary/data/towncrier/+decorators.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Bugfix. Propagate error class provider from metaclass to class decorators. diff --git a/.auxiliary/data/towncrier/+modules.enhance.rst b/.auxiliary/data/towncrier/+modules.enhance.rst deleted file mode 100644 index 7e716bb..0000000 --- a/.auxiliary/data/towncrier/+modules.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Allow arbitrary class to be supplied to ``reclassify_modules``. diff --git a/.auxiliary/data/towncrier/+modules.repair.rst b/.auxiliary/data/towncrier/+modules.repair.rst new file mode 100644 index 0000000..7edbfe4 --- /dev/null +++ b/.auxiliary/data/towncrier/+modules.repair.rst @@ -0,0 +1,2 @@ +When reclassifying modules recursively, ensure that replacement class is +included in recursive invocations. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 4aaf28e..3bb4922 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7aa0c5cb192c5d272985442c6e03a69918e9fc3abbd3e26dd2bcbab77a4e8816 -size 212876 +oid sha256:7040d54c1c3a7acf0a05a2971a1757dc2f74559987441ca68ab5e031246f55d8 +size 216740 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 860aaa5..19d5950 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,16 @@ Release Notes .. towncrier release notes start +Classcore 1.3.1 (2025-06-07) +============================ + +Repairs +------- + +- When reclassifying modules recursively, ensure that replacement class is + included in recursive invocations. + + Classcore 1.3 (2025-06-07) ========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index dab488e..d13b8b7 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] -__version__ = '1.3' +__version__ = '1.3.1' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index 21b565c..bb89c50 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -75,7 +75,11 @@ def reclassify_modules( for value in attributes.values( ): if not __.inspect.ismodule( value ): continue if not value.__name__.startswith( f"{package_name}." ): continue - if recursive: reclassify_modules( value, recursive = True ) + if recursive: + reclassify_modules( + value, + recursive = True, + replacement_class = replacement_class ) if isinstance( value, replacement_class ): continue _seal_module( value, attributes_namer, replacement_class ) if module and not isinstance( module, replacement_class ): From c1def76b84fa664a031f81dc801460b368c8abf1 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Wed, 11 Jun 2025 02:31:49 +0000 Subject: [PATCH 10/28] Update documents for publication. --- .../data/towncrier/+idempotency.repair.rst | 2 + .../+metaclass-arguments.enhance.rst | 3 + .auxiliary/data/towncrier/+modules.repair.rst | 2 - .auxiliary/data/towncrier/+slots.enhance.rst | 3 + .auxiliary/data/towncrier/+super.repair.rst | 2 + .auxiliary/publications/website.tar.xz | 4 +- documentation/changelog.rst | 23 ++ documentation/examples/std-classes.rst | 114 +++++++++ sources/classcore/__init__.py | 2 +- sources/classcore/decorators.py | 99 +++++--- sources/classcore/exceptions.py | 10 +- sources/classcore/standard/behaviors.py | 27 ++- sources/classcore/standard/classes.py | 18 +- sources/classcore/standard/decorators.py | 222 ++++++++++++------ sources/classcore/utilities.py | 76 ++++-- .../test_000_classcore/test_110_utilities.py | 9 + ...d_decorators.py => test_210_decorators.py} | 35 ++- 17 files changed, 499 insertions(+), 152 deletions(-) create mode 100644 .auxiliary/data/towncrier/+idempotency.repair.rst create mode 100644 .auxiliary/data/towncrier/+metaclass-arguments.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+modules.repair.rst create mode 100644 .auxiliary/data/towncrier/+slots.enhance.rst create mode 100644 .auxiliary/data/towncrier/+super.repair.rst rename tests/test_000_classcore/{test_320_standard_decorators.py => test_210_decorators.py} (56%) diff --git a/.auxiliary/data/towncrier/+idempotency.repair.rst b/.auxiliary/data/towncrier/+idempotency.repair.rst new file mode 100644 index 0000000..99f04a8 --- /dev/null +++ b/.auxiliary/data/towncrier/+idempotency.repair.rst @@ -0,0 +1,2 @@ +Bugfix. Ensure idempotent execution in all scenarios: CPython vs PyPy, slotted +vs non-slotted. diff --git a/.auxiliary/data/towncrier/+metaclass-arguments.enhance.rst b/.auxiliary/data/towncrier/+metaclass-arguments.enhance.rst new file mode 100644 index 0000000..59e794a --- /dev/null +++ b/.auxiliary/data/towncrier/+metaclass-arguments.enhance.rst @@ -0,0 +1,3 @@ +Publicly expose the ``TypedDict`` which tracks available metaclass arguments +for the standard classes. This allows for easier extension by downstream +packages and centralizes documentation on the metaclasses interface. diff --git a/.auxiliary/data/towncrier/+modules.repair.rst b/.auxiliary/data/towncrier/+modules.repair.rst deleted file mode 100644 index 7edbfe4..0000000 --- a/.auxiliary/data/towncrier/+modules.repair.rst +++ /dev/null @@ -1,2 +0,0 @@ -When reclassifying modules recursively, ensure that replacement class is -included in recursive invocations. diff --git a/.auxiliary/data/towncrier/+slots.enhance.rst b/.auxiliary/data/towncrier/+slots.enhance.rst new file mode 100644 index 0000000..aeced20 --- /dev/null +++ b/.auxiliary/data/towncrier/+slots.enhance.rst @@ -0,0 +1,3 @@ +Fully support slotted classes. Bookkeeping attributes will now be in slots if +class is slotted. Potential performance improvement since names do not need to +be mangled for inheritance isolation. diff --git a/.auxiliary/data/towncrier/+super.repair.rst b/.auxiliary/data/towncrier/+super.repair.rst new file mode 100644 index 0000000..630ddbd --- /dev/null +++ b/.auxiliary/data/towncrier/+super.repair.rst @@ -0,0 +1,2 @@ +Bugfix. Ensure that methods added by decoration properly respect class method +resolution order (MRO) when they are not wrapping existing methods. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 3bb4922..cdfc4f9 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7040d54c1c3a7acf0a05a2971a1757dc2f74559987441ca68ab5e031246f55d8 -size 216740 +oid sha256:7d7e5c8ff387ff2073df359cbff0f8683f816c05beebf22a923aad4fc276fe2e +size 234000 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 19d5950..d44cc01 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,29 @@ Release Notes .. towncrier release notes start +Classcore 1.4 (2025-06-10) +========================== + +Enhancements +------------ + +- Fully support slotted classes. Bookkeeping attributes will now be in slots if + class is slotted. Potential performance improvement since names do not need to + be mangled for inheritance isolation. +- Publicly expose the ``TypedDict`` which tracks available metaclass arguments + for the standard classes. This allows for easier extension by downstream + packages and centralizes documentation on the metaclasses interface. + + +Repairs +------- + +- Bugfix. Ensure idempotent execution in all scenarios: CPython vs PyPy, slotted + vs non-slotted. +- Bugfix. Ensure that methods added by decoration properly respect class method + resolution order (MRO) when they are not wrapping existing methods. + + Classcore 1.3.1 (2025-06-07) ============================ diff --git a/documentation/examples/std-classes.rst b/documentation/examples/std-classes.rst index a25776d..395f585 100644 --- a/documentation/examples/std-classes.rst +++ b/documentation/examples/std-classes.rst @@ -159,3 +159,117 @@ attributes, there is a convenience class, ``ObjectMutable``. >>> point.x, point.y = 20, 21 >>> point.x, point.y (20, 21) + + +Attribute Preallocations +=============================================================================== + +You can preallocate attributes using the standard Python ``__slots__`` +mechanism. In addition to potential performance gains for attribute lookups, +this can be useful if you are making a namespace class and want to keep the +namespace dictionary free of record-keeping attributes. You cannot inherit a +standard base class, such as ``Object``, for this purpose, as it is +``__dict__``-based. However, you can create the namespace class via metaclass. + +.. doctest:: Standard.Classes + + >>> class Namespace( metaclass = ccstd.Class ): + ... __slots__ = ( '__dict__', ) + ... def __init__( self, **arguments: float ) -> None: + ... self.__dict__.update( arguments ) + ... + >>> ns = Namespace( x = 20, y = 21 ) + >>> ns.__slots__ + ('__dict__', '_classcore_instance_behaviors_') + >>> 'x' in ns.__dict__ + True + >>> '_classcore_instance_behaviors_' in ns.__dict__ + False + >>> ns.x, ns.y + (20, 21) + +The mapping form of ``__slots__`` is also supported. + +.. doctest:: Standard.Classes + + >>> class Namespace( metaclass = ccstd.Class ): + ... __slots__ = { '__dict__': 'Namespace attributes.' } + ... def __init__( self, **arguments: float ): + ... self.__dict__.update( arguments ) + ... + >>> ns = Namespace( x = 20, y = 21 ) + >>> ns.__slots__[ '__dict__' ] + 'Namespace attributes.' + + +Integrations with Custom Behaviors +=============================================================================== + +You can define dunder methods, like ``__delattr__``, ``__setattr__``, and +``__dir__``, and they will be automatically wrapped by the decorators which +setup attributes concealment and immutability enforcement on classes. + +.. doctest:: Standard.Classes + + >>> class Point2d( ccstd.ObjectMutable ): + ... def __init__( self, x: float, y: float ) -> None: + ... super( ).__init__( ) + ... self.x = x + ... self.y = y + ... def __delattr__( self, name: str ) -> None: + ... if not name.startswith( '_' ): print( name ) + ... super( ).__delattr__( name ) + ... def __setattr__( self, name: str, value ) -> None: + ... if not name.startswith( '_' ): print( f"{name} = {value!r}" ) + ... super( ).__setattr__( name, value ) + ... def __dir__( self ): + ... print( 'called dir' ) + ... return super( ).__dir__( ) + ... + >>> point = Point2d( 3, 4 ) + x = 3 + y = 4 + >>> point.x, point.y = 5, 12 + x = 5 + y = 12 + >>> del point.y + y + >>> 'x' in dir( point ) + called dir + True + +The integration points work correctly with inheritance. Furthermore, the +standard behaviors (concealment and immutability) are idempotent, which +improves their performance in class hierarchies. + +.. doctest:: Standard.Classes + + >>> class Point3d( Point2d ): + ... def __init__( self, x: float, y: float, z: float ) -> None: + ... super( ).__init__( x, y ) + ... self.z = z + ... def __delattr__( self, name: str ) -> None: + ... if name == 'z': print( 'Z!' ) + ... super( ).__delattr__( name ) + ... def __setattr__( self, name: str, value ) -> None: + ... if name == 'z': print( 'Z!' ) + ... super( ).__setattr__( name, value ) + ... def __dir__( self ): + ... print( 'called dir in 3D' ) + ... return super( ).__dir__( ) + ... + >>> point3 = Point3d( 5, 12, 17 ) + x = 5 + y = 12 + Z! + z = 17 + >>> point3.z = 60 + Z! + z = 60 + >>> del point3.z + Z! + z + >>> 'z' not in dir( point3 ) + called dir in 3D + called dir + True diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index d13b8b7..a7d9e99 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] -__version__ = '1.3.1' +__version__ = '1.4' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/decorators.py b/sources/classcore/decorators.py index c7f9699..dd2f310 100644 --- a/sources/classcore/decorators.py +++ b/sources/classcore/decorators.py @@ -75,25 +75,45 @@ def produce_class_construction_decorator( Decorator overrides ``__new__`` on metaclass. ''' def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: - constructor_name = attributes_namer( 'classes', 'constructor' ) - extant = getattr( clscls, constructor_name, None ) - original = getattr( clscls, '__new__' ) - if extant is original: return clscls - - def construct( - clscls_: type[ __.T ], - name: str, - bases: tuple[ type, ... ], - namespace: dict[ str, __.typx.Any ], *, - decorators: _nomina.Decorators[ __.T ] = ( ), - **arguments: __.typx.Any, - ) -> type[ object ]: - return constructor( - clscls_, original, - name, bases, namespace, arguments, decorators ) - - setattr( clscls, constructor_name, construct ) - setattr( clscls, '__new__', construct ) + original = __.typx.cast( + _nomina.ClassConstructorLigation | None, + clscls.__dict__.get( '__new__' ) ) # pyright: ignore + + if original is None: + + def construct_with_super( + clscls_: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Any, + ) -> type[ object ]: + superf = super( clscls, clscls_ ).__new__ + # TODO? Short-circuit if not at start of MRO. + return constructor( + clscls_, superf, + name, bases, namespace, arguments, decorators ) + + setattr( clscls, '__new__', construct_with_super ) + + else: + + def construct_with_original( + clscls_: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Any, + ) -> type[ object ]: + # TODO? Short-circuit if not at start of MRO. + return constructor( + clscls_, original, + name, bases, namespace, arguments, decorators ) + + setattr( clscls, '__new__', construct_with_original ) + return clscls return decorate @@ -108,20 +128,33 @@ def produce_class_initialization_decorator( Decorator overrides ``__init__`` on metaclass. ''' def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: - initializer_name = attributes_namer( 'classes', 'initializer' ) - extant = getattr( clscls, initializer_name, None ) - original = getattr( clscls, '__init__' ) - if extant is original: return clscls - - @__.funct.wraps( original ) - def initialize( - cls: type, *posargs: __.typx.Any, **nomargs: __.typx.Any - ) -> None: - ligation = __.funct.partial( original, cls ) - initializer( cls, ligation, posargs, nomargs ) - - setattr( clscls, initializer_name, initialize ) - clscls.__init__ = initialize + original = __.typx.cast( + _nomina.InitializerLigation | None, + clscls.__dict__.get( '__init__' ) ) # pyright: ignore + + if original is None: + + def initialize_with_super( + cls: type, *posargs: __.typx.Any, **nomargs: __.typx.Any + ) -> None: + ligation = super( clscls, cls ).__init__ + # TODO? Short-circuit if not at start of MRO. + initializer( cls, ligation, posargs, nomargs ) + + clscls.__init__ = initialize_with_super + + else: + + @__.funct.wraps( original ) + def initialize_with_original( + cls: type, *posargs: __.typx.Any, **nomargs: __.typx.Any + ) -> None: + ligation = __.funct.partial( original, cls ) + # TODO? Short-circuit if not at start of MRO. + initializer( cls, ligation, posargs, nomargs ) + + clscls.__init__ = initialize_with_original + return clscls return decorate diff --git a/sources/classcore/exceptions.py b/sources/classcore/exceptions.py index 6951fb3..95cfe64 100644 --- a/sources/classcore/exceptions.py +++ b/sources/classcore/exceptions.py @@ -26,31 +26,31 @@ class Omniexception( - BaseException, _standard.Object, + _standard.Object, BaseException, instances_visibles = ( '__cause__', '__context__' ), ): ''' Base exception for package. ''' -class Omnierror( Exception, Omniexception ): +class Omnierror( Omniexception, Exception ): ''' Base error for package. ''' -class AttributeImmutability( AttributeError, Omnierror ): +class AttributeImmutability( Omnierror, AttributeError ): def __init__( self, name: str, target: str ): super( ).__init__( f"Could not assign or delete attribute {name!r} on {target}." ) -class BehaviorExclusionInvalidity( TypeError, ValueError, Omnierror ): +class BehaviorExclusionInvalidity( Omnierror, TypeError, ValueError ): def __init__( self, verifier: __.typx.Any ): super( ).__init__( f"Invalid behavior exclusion verifier: {verifier!r}" ) -class ErrorProvideFailure( RuntimeError, Omnierror ): +class ErrorProvideFailure( Omnierror, RuntimeError ): def __init__( self, name: str, reason: str ): super( ).__init__( diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 4f1a2fa..99a9ef1 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -151,6 +151,29 @@ def survey_visible_attributes( return names_ +def augment_class_attributes_allocations( + attributes_namer: _nomina.AttributesNamer, + namespace: dict[ str, __.typx.Any ], +) -> None: + ''' Adds necessary slots for record-keeping attributes. ''' + behaviors_name = attributes_namer( 'instance', 'behaviors' ) + slots: __.typx.Union[ + __.cabc.Mapping[ str, __.typx.Any ], + __.cabc.Sequence[ str ], + None + ] = namespace.get( '__slots__' ) + if isinstance( slots, __.cabc.Mapping ): + slots_ = dict( slots ) + slots_[ behaviors_name ] = 'Active behaviors.' + slots_ = __.types.MappingProxyType( slots_ ) + elif isinstance( slots, __.cabc.Sequence ): + slots_ = list( slots ) + slots_.append( behaviors_name ) + slots_ = tuple( slots_ ) + else: return # pragma: no cover + namespace[ '__slots__' ] = slots_ + + def classify_behavior_exclusion_verifiers( verifiers: _nomina.BehaviorExclusionVerifiers ) -> tuple[ @@ -190,6 +213,8 @@ def preprocess( # noqa: PLR0913 ) -> None: record_class_construction_arguments( attributes_namer, namespace, arguments ) + if '__slots__' in namespace: + augment_class_attributes_allocations( attributes_namer, namespace ) return preprocess @@ -329,11 +354,11 @@ def record_class_construction_arguments( arguments_ = { } for name in ( 'class_mutables', 'class_visibles', + 'dynadoc_configuration', 'instances_assigner_core', 'instances_deleter_core', 'instances_surveyor_core', 'instances_mutables', 'instances_visibles', - 'dynadoc_configuration', ): if name not in arguments: continue arguments_[ name ] = arguments.pop( name ) diff --git a/sources/classcore/standard/classes.py b/sources/classcore/standard/classes.py index cadfb52..fd776fc 100644 --- a/sources/classcore/standard/classes.py +++ b/sources/classcore/standard/classes.py @@ -33,11 +33,15 @@ _decorators.class_factory, dynadoc_configuration = _dynadoc_configuration ) -class _CfcExtraArguments( __.typx.TypedDict, total = False ): +class ClassFactoryExtraArguments( __.typx.TypedDict, total = False ): + ''' Extra arguments accepted by standard metaclasses. ''' class_mutables: _nomina.BehaviorExclusionVerifiersOmni class_visibles: _nomina.BehaviorExclusionVerifiersOmni dynadoc_configuration: _nomina.DynadocConfiguration + instances_assigner_core: _nomina.AssignerCore + instances_deleter_core: _nomina.DeleterCore + instances_surveyor_core: _nomina.SurveyorCore instances_mutables: _nomina.BehaviorExclusionVerifiersOmni instances_visibles: _nomina.BehaviorExclusionVerifiersOmni @@ -56,7 +60,7 @@ def __new__( # Typechecker stub. bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], *, decorators: _nomina.Decorators[ __.T ] = ( ), - **arguments: __.typx.Unpack[ _CfcExtraArguments ], + **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], ) -> __.T: return super( ).__new__( clscls, name, bases, namespace ) @@ -77,7 +81,7 @@ def __new__( # Typechecker stub. bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], *, decorators: _nomina.Decorators[ __.T ] = ( ), - **arguments: __.typx.Unpack[ _CfcExtraArguments ], + **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], ) -> __.T: return super( ).__new__( clscls, name, bases, namespace ) @@ -98,7 +102,7 @@ def __new__( # Typechecker stub. bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], *, decorators: _nomina.Decorators[ __.T ] = ( ), - **arguments: __.typx.Unpack[ _CfcExtraArguments ], + **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], ) -> __.T: return super( ).__new__( clscls, name, bases, namespace ) @@ -118,7 +122,7 @@ def __new__( # Typechecker stub. bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], *, decorators: _nomina.Decorators[ __.T ] = ( ), - **arguments: __.typx.Unpack[ _CfcExtraArguments ], + **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], ) -> __.T: return super( ).__new__( clscls, name, bases, namespace ) @@ -139,7 +143,7 @@ def __new__( # Typechecker stub. bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], *, decorators: _nomina.Decorators[ __.T ] = ( ), - **arguments: __.typx.Unpack[ _CfcExtraArguments ], + **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], ) -> __.T: return super( ).__new__( clscls, name, bases, namespace ) @@ -161,7 +165,7 @@ def __new__( # Typechecker stub. bases: tuple[ type, ... ], namespace: dict[ str, __.typx.Any ], *, decorators: _nomina.Decorators[ __.T ] = ( ), - **arguments: __.typx.Unpack[ _CfcExtraArguments ], + **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], ) -> __.T: return super( ).__new__( clscls, name, bases, namespace ) diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 6c40bf7..c3dec55 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -49,6 +49,7 @@ def prepare_dataclass_for_instances( ''' Annotates dataclass in support of instantiation machinery. ''' annotations = __.inspect.get_annotations( cls ) behaviors_name = attributes_namer( 'instance', 'behaviors' ) + # TODO? Only use mangling if not slotted. behaviors_name_m = _utilities.mangle_name( cls, behaviors_name ) annotations[ behaviors_name_m ] = set[ str ] setattr( cls, '__annotations__', annotations ) # in case of absence @@ -89,7 +90,7 @@ def apply_cfc_constructor( def apply_cfc_initializer( - clscls: type[ __.T ], attributes_namer: _nomina.AttributesNamer + clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer ) -> None: ''' Injects '__init__' method into metaclass. ''' completers = ( @@ -112,7 +113,7 @@ def apply_cfc_attributes_assigner( ) -> None: ''' Injects '__setattr__' method into metaclass. ''' decorator = produce_attributes_assignment_decorator( - level = 'class', + level = 'classes', attributes_namer = attributes_namer, error_class_provider = error_class_provider, implementation_core = implementation_core ) @@ -127,7 +128,7 @@ def apply_cfc_attributes_deleter( ) -> None: ''' Injects '__delattr__' method into metaclass. ''' decorator = produce_attributes_deletion_decorator( - level = 'class', + level = 'classes', attributes_namer = attributes_namer, error_class_provider = error_class_provider, implementation_core = implementation_core ) @@ -141,7 +142,7 @@ def apply_cfc_attributes_surveyor( ) -> None: ''' Injects '__dir__' method into metaclass. ''' decorator = produce_attributes_surveillance_decorator( - level = 'class', + level = 'classes', attributes_namer = attributes_namer, implementation_core = implementation_core ) decorator( clscls ) @@ -196,13 +197,8 @@ def produce_instances_initialization_decorator( ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__init__' method into class. ''' def decorate( cls: type[ __.U ] ) -> type[ __.U ]: - initializer_name = attributes_namer( 'instances', 'initializer' ) - extant = getattr( cls, initializer_name, None ) - original = getattr( cls, '__init__' ) - if extant is original: return cls behaviors: set[ str ] = set( ) behaviors_name = attributes_namer( 'instance', 'behaviors' ) - behaviors_name_m = _utilities.mangle_name( cls, behaviors_name ) _behaviors.record_behavior( cls, attributes_namer = attributes_namer, level = 'instances', basename = 'mutables', @@ -213,18 +209,41 @@ def decorate( cls: type[ __.U ] ) -> type[ __.U ]: level = 'instances', basename = 'visibles', label = _nomina.concealment_label, behaviors = behaviors, verifiers = visibles ) + original = cls.__dict__.get( '__init__' ) + + if original is None: + + def initialize_with_super( + self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any + ) -> None: + super( cls, self ).__init__( *posargs, **nomargs ) + # Only record behaviors at start of MRO. + if cls is not type( self ): return + behaviors_: set[ str ] = ( + _utilities.getattr0( self, behaviors_name, set( ) ) ) + behaviors_.update( behaviors ) + _utilities.setattr0( + self, behaviors_name, frozenset( behaviors_ ) ) + + cls.__init__ = initialize_with_super + + else: + + @__.funct.wraps( original ) + def initialize_with_original( + self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any + ) -> None: + original( self, *posargs, **nomargs ) + # Only record behaviors at start of MRO. + if cls is not type( self ): return + behaviors_: set[ str ] = ( + _utilities.getattr0( self, behaviors_name, set( ) ) ) + behaviors_.update( behaviors ) + _utilities.setattr0( + self, behaviors_name, frozenset( behaviors_ ) ) + + cls.__init__ = initialize_with_original - @__.funct.wraps( original ) - def initialize( - self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any - ) -> None: - original( self, *posargs, **nomargs ) - behaviors_: set[ str ] = getattr( self, behaviors_name_m, set( ) ) - behaviors_.update( behaviors ) - setattr( self, behaviors_name_m, frozenset( behaviors_ ) ) - - setattr( cls, initializer_name, initialize ) - cls.__init__ = initialize return cls return decorate @@ -238,23 +257,50 @@ def produce_attributes_assignment_decorator( ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__setattr__' method into class. ''' def decorate( cls: type[ __.U ] ) -> type[ __.U ]: - assigner_name = attributes_namer( level, 'assigner' ) - extant = getattr( cls, assigner_name, None ) - original = getattr( cls, '__setattr__' ) - if extant is original: return cls - - @__.funct.wraps( original ) - def assign( self: object, name: str, value: __.typx.Any ) -> None: - implementation_core( - self, - ligation = __.funct.partial( original, self ), - attributes_namer = attributes_namer, - error_class_provider = error_class_provider, - level = level, - name = name, value = value ) + leveli = 'class' if level == 'classes' else level + original = cls.__dict__.get( '__setattr__' ) + + if original is None: + + def assign_with_super( + self: object, name: str, value: __.typx.Any + ) -> None: + ligation = super( cls, self ).__setattr__ + # Only enforce behaviors at start of MRO. + if cls is not type( self ): + ligation( name, value ) + return + implementation_core( + self, + ligation = ligation, + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + level = leveli, + name = name, value = value ) + + cls.__setattr__ = assign_with_super + + else: + + @__.funct.wraps( original ) + def assign_with_original( + self: object, name: str, value: __.typx.Any + ) -> None: + ligation = __.funct.partial( original, self ) + # Only enforce behaviors at start of MRO. + if cls is not type( self ): + ligation( name, value ) + return + implementation_core( + self, + ligation = ligation, + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + level = leveli, + name = name, value = value ) + + cls.__setattr__ = assign_with_original - setattr( cls, assigner_name, assign ) - cls.__setattr__ = assign return cls return decorate @@ -268,23 +314,46 @@ def produce_attributes_deletion_decorator( ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__delattr__' method into class. ''' def decorate( cls: type[ __.U ] ) -> type[ __.U ]: - deleter_name = attributes_namer( level, 'deleter' ) - extant = getattr( cls, deleter_name, None ) - original = getattr( cls, '__delattr__' ) - if extant is original: return cls - - @__.funct.wraps( original ) - def delete( self: object, name: str ) -> None: - implementation_core( - self, - ligation = __.funct.partial( original, self ), - attributes_namer = attributes_namer, - error_class_provider = error_class_provider, - level = level, - name = name ) + leveli = 'class' if level == 'classes' else level + original = cls.__dict__.get( '__delattr__' ) + + if original is None: + + def delete_with_super( self: object, name: str ) -> None: + ligation = super( cls, self ).__delattr__ + # Only enforce behaviors at start of MRO. + if cls is not type( self ): + ligation( name ) + return + implementation_core( + self, + ligation = ligation, + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + level = leveli, + name = name ) + + cls.__delattr__ = delete_with_super + + else: + + @__.funct.wraps( original ) + def delete_with_original( self: object, name: str ) -> None: + ligation = __.funct.partial( original, self ) + # Only enforce behaviors at start of MRO. + if cls is not type( self ): + ligation( name ) + return + implementation_core( + self, + ligation = ligation, + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + level = leveli, + name = name ) + + cls.__delattr__ = delete_with_original - setattr( cls, deleter_name, delete ) - cls.__delattr__ = delete return cls return decorate @@ -297,21 +366,42 @@ def produce_attributes_surveillance_decorator( ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__dir__' method into class. ''' def decorate( cls: type[ __.U ] ) -> type[ __.U ]: - surveyor_name = attributes_namer( level, 'surveyor' ) - extant = getattr( cls, surveyor_name, None ) - original = getattr( cls, '__dir__' ) - if extant is original: return cls - - @__.funct.wraps( original ) - def survey( self: object ) -> __.cabc.Iterable[ str ]: - return implementation_core( - self, - ligation = __.funct.partial( original, self ), - attributes_namer = attributes_namer, - level = level ) + leveli = 'class' if level == 'classes' else level + original = cls.__dict__.get( '__dir__' ) + + if original is None: + + def survey_with_super( + self: object + ) -> __.cabc.Iterable[ str ]: + ligation = super( cls, self ).__dir__ + # Only enforce behaviors at start of MRO. + if cls is not type( self ): return ligation( ) + return implementation_core( + self, + ligation = ligation, + attributes_namer = attributes_namer, + level = leveli ) + + cls.__dir__ = survey_with_super + + else: + + @__.funct.wraps( original ) + def survey_with_original( + self: object + ) -> __.cabc.Iterable[ str ]: + ligation = __.funct.partial( original, self ) + # Only enforce behaviors at start of MRO. + if cls is not type( self ): return ligation( ) + return implementation_core( + self, + ligation = ligation, + attributes_namer = attributes_namer, + level = leveli ) + + cls.__dir__ = survey_with_original - setattr( cls, surveyor_name, survey ) - cls.__dir__ = survey return cls return decorate diff --git a/sources/classcore/utilities.py b/sources/classcore/utilities.py index ba5e8e9..254d933 100644 --- a/sources/classcore/utilities.py +++ b/sources/classcore/utilities.py @@ -24,51 +24,81 @@ from . import __ -def describe_object( obj: object ) -> str: +def describe_object( objct: object, / ) -> str: ''' Returns object type with fully-qualified name. ''' - if __.inspect.isclass( obj ): - return "class '{}'".format( qualify_class_name( obj ) ) + if __.inspect.isclass( objct ): + return "class '{}'".format( qualify_class_name( objct ) ) # TODO? functions, methods, etc... - return "instance of {}".format( describe_object( type( obj ) ) ) + return "instance of {}".format( describe_object( type( objct ) ) ) -def getattr0( obj: object, name: str, default: __.typx.Any ) -> __.typx.Any: - ''' Returns private attribute from object. +def getattr0( + objct: object, /, name: str, default: __.typx.Any +) -> __.typx.Any: + ''' Returns special private attribute from object. - Uses mangled attribute name which is unique to the class. + This avoids inheritance-related collisions. + + Uses mangled attribute name which is unique to the class, + except when attribute is slotted. Slotted attributes are effectively + isolated from inheritance. ''' - name_m = mangle_name( obj, name ) - return getattr( obj, name_m, default ) + if not __.inspect.isclass( objct ): + for base in type( objct ).mro( ): + slots = getattr( base, '__slots__', ( ) ) + if name in slots: return getattr( objct, name, default ) + name_m = mangle_name( objct, name ) + return getattr( objct, name_m, default ) + +def delattr0( objct: object, /, name: str ) -> None: + ''' Deletes special private attribute on object. -def delattr0( obj: object, name: str ) -> None: - ''' Deletes private attribute on object. + This avoids inheritance-related collisions. - Uses mangled attribute name which is unique to the class. + Uses mangled attribute name which is unique to the class, + except when attribute is slotted. Slotted attributes are effectively + isolated from inheritance. ''' - name_m = mangle_name( obj, name ) - delattr( obj, name_m ) + if not __.inspect.isclass( objct ): + for base in type( objct ).mro( ): + slots = getattr( base, '__slots__', ( ) ) + if name in slots: + delattr( objct, name ) + return + name_m = mangle_name( objct, name ) + delattr( objct, name_m ) + +def setattr0( objct: object, /, name: str, value: __.typx.Any ) -> None: + ''' Assigns special private attribute to object. -def setattr0( obj: object, name: str, value: __.typx.Any ) -> None: - ''' Assigns private attribute to object. + This avoids inheritance-related collisions. - Uses mangled attribute name which is unique to the class. + Uses mangled attribute name which is unique to the class, + except when attribute is slotted. Slotted attributes are effectively + isolated from inheritance. ''' - name_m = mangle_name( obj, name ) - setattr( obj, name_m, value ) + if not __.inspect.isclass( objct ): + for base in type( objct ).mro( ): + slots = getattr( base, '__slots__', ( ) ) + if name in slots: + setattr( objct, name, value ) + return + name_m = mangle_name( objct, name ) + setattr( objct, name_m, value ) -def mangle_name( obj: object, name: str ) -> str: +def mangle_name( objct: object, /, name: str ) -> str: ''' Mangles attribute name so that it is unique. Effectively provides name of private member attribute, which is unique across class inheritance. ''' - if not __.inspect.isclass( obj ): - return mangle_name( type( obj ), name ) + if not __.inspect.isclass( objct ): + return mangle_name( type( objct ), name ) namehash = __.hashlib.sha256( ) - namehash.update( qualify_class_name( obj ).encode( ) ) + namehash.update( qualify_class_name( objct ).encode( ) ) namehash_hex = namehash.hexdigest( ) return f"{name}_{namehash_hex}" diff --git a/tests/test_000_classcore/test_110_utilities.py b/tests/test_000_classcore/test_110_utilities.py index fbd8d74..8e4d449 100644 --- a/tests/test_000_classcore/test_110_utilities.py +++ b/tests/test_000_classcore/test_110_utilities.py @@ -62,6 +62,15 @@ class D( C ): pass assert 2 == module.getattr0( D, 'y', sentinel ) module.delattr0( C, 'x' ) assert sentinel == module.getattr0( C, 'x', sentinel ) + class CS: __slots__ = ( 'z', ) + cs = CS( ) + assert sentinel == module.getattr0( cs, 'z', sentinel ) + module.setattr0( cs, 'z', 3 ) + assert 3 == module.getattr0( cs, 'z', sentinel ) + module.delattr0( cs, 'z' ) + assert sentinel == module.getattr0( cs, 'z', sentinel ) + with pytest.raises( AttributeError ): + module.delattr0( cs, 'missing' ) @pytest.mark.skipif( diff --git a/tests/test_000_classcore/test_320_standard_decorators.py b/tests/test_000_classcore/test_210_decorators.py similarity index 56% rename from tests/test_000_classcore/test_320_standard_decorators.py rename to tests/test_000_classcore/test_210_decorators.py index 854764a..08ef214 100644 --- a/tests/test_000_classcore/test_320_standard_decorators.py +++ b/tests/test_000_classcore/test_210_decorators.py @@ -18,21 +18,32 @@ #============================================================================# +# import pytest + from . import PACKAGE_NAME, cache_import_module -MODULE_QNAME = f"{PACKAGE_NAME}.standard.decorators" +MODULE_QNAME = f"{PACKAGE_NAME}.decorators" -def test_210_class_factory_decorator_idempotence( ): - ''' Class factory decorators are idempotent. ''' +def test_111_produce_class_initialization_decorator_original( ): module = cache_import_module( MODULE_QNAME ) - @module.class_factory( ) - class Class: pass - @module.class_factory( ) - class BetterClass( Class ): pass - assert Class.__new__ is BetterClass.__new__ - assert Class.__init__ is BetterClass.__init__ - assert Class.__setattr__ is BetterClass.__setattr__ - assert Class.__delattr__ is BetterClass.__delattr__ - assert Class.__dir__ is BetterClass.__dir__ + base_module = cache_import_module( f"{PACKAGE_NAME}.__" ) + factories_module = cache_import_module( f"{PACKAGE_NAME}.factories" ) + constructor = factories_module.produce_class_constructor( + attributes_namer = base_module.calculate_attrname ) + cdecorator = module.produce_class_construction_decorator( + attributes_namer = base_module.calculate_attrname, + constructor = constructor ) + initializer = factories_module.produce_class_initializer( + attributes_namer = base_module.calculate_attrname ) + idecorator = module.produce_class_initialization_decorator( + attributes_namer = base_module.calculate_attrname, + initializer = initializer ) + @idecorator + @cdecorator + class Class( type ): + def __init__( self, *posargs, **nomargs ): + self._hello = 'Hi' + class Object( metaclass = Class ): pass + assert Object._hello == 'Hi' From 1dff42590e9c50249c2ef602c60f0429a522bcbe Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Wed, 11 Jun 2025 04:30:49 +0000 Subject: [PATCH 11/28] Update documents for publication. --- .auxiliary/data/towncrier/+idempotency.repair.rst | 2 -- .../data/towncrier/+metaclass-arguments.enhance.rst | 3 --- .auxiliary/data/towncrier/+modules.repair.rst | 1 + .auxiliary/data/towncrier/+slots.enhance.rst | 3 --- .auxiliary/data/towncrier/+super.repair.rst | 2 -- .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 9 +++++++++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/modules.py | 4 ++-- tests/test_000_classcore/test_350_standard_modules.py | 11 +++++++++++ 10 files changed, 26 insertions(+), 15 deletions(-) delete mode 100644 .auxiliary/data/towncrier/+idempotency.repair.rst delete mode 100644 .auxiliary/data/towncrier/+metaclass-arguments.enhance.rst create mode 100644 .auxiliary/data/towncrier/+modules.repair.rst delete mode 100644 .auxiliary/data/towncrier/+slots.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+super.repair.rst diff --git a/.auxiliary/data/towncrier/+idempotency.repair.rst b/.auxiliary/data/towncrier/+idempotency.repair.rst deleted file mode 100644 index 99f04a8..0000000 --- a/.auxiliary/data/towncrier/+idempotency.repair.rst +++ /dev/null @@ -1,2 +0,0 @@ -Bugfix. Ensure idempotent execution in all scenarios: CPython vs PyPy, slotted -vs non-slotted. diff --git a/.auxiliary/data/towncrier/+metaclass-arguments.enhance.rst b/.auxiliary/data/towncrier/+metaclass-arguments.enhance.rst deleted file mode 100644 index 59e794a..0000000 --- a/.auxiliary/data/towncrier/+metaclass-arguments.enhance.rst +++ /dev/null @@ -1,3 +0,0 @@ -Publicly expose the ``TypedDict`` which tracks available metaclass arguments -for the standard classes. This allows for easier extension by downstream -packages and centralizes documentation on the metaclasses interface. diff --git a/.auxiliary/data/towncrier/+modules.repair.rst b/.auxiliary/data/towncrier/+modules.repair.rst new file mode 100644 index 0000000..d60107f --- /dev/null +++ b/.auxiliary/data/towncrier/+modules.repair.rst @@ -0,0 +1 @@ +Enforce attributes concealment and immutability on modules. diff --git a/.auxiliary/data/towncrier/+slots.enhance.rst b/.auxiliary/data/towncrier/+slots.enhance.rst deleted file mode 100644 index aeced20..0000000 --- a/.auxiliary/data/towncrier/+slots.enhance.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fully support slotted classes. Bookkeeping attributes will now be in slots if -class is slotted. Potential performance improvement since names do not need to -be mangled for inheritance isolation. diff --git a/.auxiliary/data/towncrier/+super.repair.rst b/.auxiliary/data/towncrier/+super.repair.rst deleted file mode 100644 index 630ddbd..0000000 --- a/.auxiliary/data/towncrier/+super.repair.rst +++ /dev/null @@ -1,2 +0,0 @@ -Bugfix. Ensure that methods added by decoration properly respect class method -resolution order (MRO) when they are not wrapping existing methods. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index cdfc4f9..c603341 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d7e5c8ff387ff2073df359cbff0f8683f816c05beebf22a923aad4fc276fe2e -size 234000 +oid sha256:e09a5dd9570a34014d99390e12e73ef52df4e0ae16b5ac9dae3f68e945e7ca57 +size 237264 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index d44cc01..ab4acfa 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,15 @@ Release Notes .. towncrier release notes start +Classcore 1.4.1 (2025-06-10) +============================ + +Repairs +------- + +- Enforce attributes concealment and immutability on modules. + + Classcore 1.4 (2025-06-10) ========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index a7d9e99..eb2910d 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] -__version__ = '1.4' +__version__ = '1.4.1' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index bb89c50..84d44f7 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -27,7 +27,7 @@ from . import nomina as _nomina -class Module( __.types.ModuleType, _classes.Object ): +class Module( _classes.Object, __.types.ModuleType ): ''' Modules with attributes immutability and concealment. ''' @@ -93,5 +93,5 @@ def _seal_module( ) -> None: behaviors = { _nomina.concealment_label, _nomina.immutability_label } behaviors_name = attributes_namer( 'instance', 'behaviors' ) - _utilities.setattr0( module, behaviors_name, behaviors ) module.__class__ = replacement_class + _utilities.setattr0( module, behaviors_name, behaviors ) diff --git a/tests/test_000_classcore/test_350_standard_modules.py b/tests/test_000_classcore/test_350_standard_modules.py index 140ca88..35d9016 100644 --- a/tests/test_000_classcore/test_350_standard_modules.py +++ b/tests/test_000_classcore/test_350_standard_modules.py @@ -20,6 +20,8 @@ import types +import pytest + from . import PACKAGE_NAME, cache_import_module @@ -29,6 +31,7 @@ def test_200_reclassification_of_package_module( ): ''' Reclassifies package module directly. ''' module = cache_import_module( MODULE_QNAME ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) module_class = module.Module module_ = types.ModuleType( 'foobarnotreal' ) module_.__package__ = None @@ -37,11 +40,14 @@ def test_200_reclassification_of_package_module( ): assert module_.__class__ is module_class module.reclassify_modules( module_ ) # idempotence assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module.foo = 1 def test_201_reclassification_of_normal_module( ): ''' Reclassifies normal module directly. ''' module = cache_import_module( MODULE_QNAME ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) module_class = module.Module module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) module_.__package__ = 'fakepackage' @@ -50,6 +56,8 @@ def test_201_reclassification_of_normal_module( ): assert module_.__class__ is module_class module.reclassify_modules( module_ ) # idempotence assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module.foo = 1 def test_202_reclassification_of_incomplete_module( ): @@ -67,6 +75,7 @@ def test_202_reclassification_of_incomplete_module( ): def test_205_reclassification_via_module_globals( ): ''' Reclassifies via module globals dictionary. ''' module = cache_import_module( MODULE_QNAME ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) module_class = module.Module module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) module_dict = { 'mod': module_, '__package__': 'fakepackage' } @@ -75,3 +84,5 @@ def test_205_reclassification_via_module_globals( ): assert module_.__class__ is module_class module.reclassify_modules( module_dict ) # idempotence assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module.foo = 1 From 0f8c0c7962c5c301ce96480f70975f3fa32cfaa8 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Thu, 12 Jun 2025 01:36:12 +0000 Subject: [PATCH 12/28] Update documents for publication. --- .auxiliary/data/towncrier/+modules.repair.rst | 3 ++- .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 10 ++++++++++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/modules.py | 1 + 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.auxiliary/data/towncrier/+modules.repair.rst b/.auxiliary/data/towncrier/+modules.repair.rst index d60107f..279c706 100644 --- a/.auxiliary/data/towncrier/+modules.repair.rst +++ b/.auxiliary/data/towncrier/+modules.repair.rst @@ -1 +1,2 @@ -Enforce attributes concealment and immutability on modules. +Ensure that custom attributes namer is passed during recursive module +reclassification. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index c603341..f44a0f9 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e09a5dd9570a34014d99390e12e73ef52df4e0ae16b5ac9dae3f68e945e7ca57 -size 237264 +oid sha256:2b7dd973768be3055ea449d282e62398f20344716ea7250db1aefbc07953c26a +size 240560 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index ab4acfa..8a51167 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,16 @@ Release Notes .. towncrier release notes start +Classcore 1.4.2 (2025-06-11) +============================ + +Repairs +------- + +- Ensure that custom attributes namer is passed during recursive module + reclassification. + + Classcore 1.4.1 (2025-06-10) ============================ diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index eb2910d..abca68c 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] -__version__ = '1.4.1' +__version__ = '1.4.2' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index 84d44f7..ae4937d 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -78,6 +78,7 @@ def reclassify_modules( if recursive: reclassify_modules( value, + attributes_namer = attributes_namer, recursive = True, replacement_class = replacement_class ) if isinstance( value, replacement_class ): continue From 1ed942bdcd2f5298f2900006c9dfeb41763b05bc Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Wed, 25 Jun 2025 04:56:10 +0000 Subject: [PATCH 13/28] Update documents for publication. --- .../data/towncrier/+inheritance.repair.rst | 3 + .../data/towncrier/+interfaces.enhance.rst | 3 + .auxiliary/data/towncrier/+modules.repair.rst | 2 - .auxiliary/publications/website.tar.xz | 4 +- documentation/changelog.rst | 19 +++ documentation/conf.py | 2 - sources/classcore/__/doctab.py | 6 - sources/classcore/__/imports.py | 2 +- sources/classcore/__init__.py | 4 +- sources/classcore/factories.py | 6 +- sources/classcore/nomina.py | 107 +++++++++------ sources/classcore/standard/behaviors.py | 50 +++++-- sources/classcore/standard/decorators.py | 124 +++++++++++------- sources/classcore/standard/dynadoc.py | 40 +++--- sources/classcore/standard/modules.py | 8 +- sources/classcore/standard/nomina.py | 26 ++-- sources/classcore/utilities.py | 3 + .../test_330_standard_decorators.py | 107 +++++++++++++++ 18 files changed, 370 insertions(+), 146 deletions(-) create mode 100644 .auxiliary/data/towncrier/+inheritance.repair.rst create mode 100644 .auxiliary/data/towncrier/+interfaces.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+modules.repair.rst create mode 100644 tests/test_000_classcore/test_330_standard_decorators.py diff --git a/.auxiliary/data/towncrier/+inheritance.repair.rst b/.auxiliary/data/towncrier/+inheritance.repair.rst new file mode 100644 index 0000000..ce51fbb --- /dev/null +++ b/.auxiliary/data/towncrier/+inheritance.repair.rst @@ -0,0 +1,3 @@ +Ensure that replacement implementations (``assigner_core``, ``deleter_core``, +and ``surveyor_core``) are inherited so that behaviors do not regress to +standard baseline behaviors in descendant classes. diff --git a/.auxiliary/data/towncrier/+interfaces.enhance.rst b/.auxiliary/data/towncrier/+interfaces.enhance.rst new file mode 100644 index 0000000..09a88c4 --- /dev/null +++ b/.auxiliary/data/towncrier/+interfaces.enhance.rst @@ -0,0 +1,3 @@ +Improve docstrings for various public type aliases. Also, drop ``Fname`` +references from public type aliases, such as ``Decorators`` so that they can be +reused in downstream packages. diff --git a/.auxiliary/data/towncrier/+modules.repair.rst b/.auxiliary/data/towncrier/+modules.repair.rst deleted file mode 100644 index 279c706..0000000 --- a/.auxiliary/data/towncrier/+modules.repair.rst +++ /dev/null @@ -1,2 +0,0 @@ -Ensure that custom attributes namer is passed during recursive module -reclassification. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index f44a0f9..53a8174 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b7dd973768be3055ea449d282e62398f20344716ea7250db1aefbc07953c26a -size 240560 +oid sha256:bc68219959b1927dcf8cbb067bd8a54487c42b443e530c46183bf83c7382b8c0 +size 257812 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 8a51167..1c33c85 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,25 @@ Release Notes .. towncrier release notes start +Classcore 1.5 (2025-06-24) +========================== + +Enhancements +------------ + +- Improve docstrings for various public type aliases. Also, drop ``Fname`` + references from public type aliases, such as ``Decorators`` so that they can be + reused in downstream packages. + + +Repairs +------- + +- Ensure that replacement implementations (``assigner_core``, ``deleter_core``, + and ``surveyor_core``) are inherited so that behaviors do not regress to + standard baseline behaviors in descendant classes. + + Classcore 1.4.2 (2025-06-11) ============================ diff --git a/documentation/conf.py b/documentation/conf.py index 6ffa8bc..d1a62a5 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -10,8 +10,6 @@ https://jareddillard.com/blog/common-ways-to-customize-sphinx-themes.html ''' -# ruff: noqa: E402,F401 - def _calculate_copyright_notice( ): from datetime import datetime as DateTime, timezone as TimeZone diff --git a/sources/classcore/__/doctab.py b/sources/classcore/__/doctab.py index 8e22c95..fac22bf 100644 --- a/sources/classcore/__/doctab.py +++ b/sources/classcore/__/doctab.py @@ -67,10 +67,4 @@ 'class dynadoc': ''' Is decorated by Dynadoc. ''', - 'decorators': - ''' Sequence of class decorators. - - Each element takes a class argument and returns a class. - ''', - } ) diff --git a/sources/classcore/__/imports.py b/sources/classcore/__/imports.py index 3847faa..6a0694b 100644 --- a/sources/classcore/__/imports.py +++ b/sources/classcore/__/imports.py @@ -33,7 +33,7 @@ import sys import types -import dynadoc +import dynadoc as ddoc import typing_extensions as typx # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index abca68c..c8a9d69 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -61,8 +61,8 @@ class decorators. Furthermore, the exception classes in the from .factories import * -__version__: __.typx.Annotated[ str, __.dynadoc.Visibilities.Reveal ] -__version__ = '1.4.2' +__version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] +__version__ = '1.5' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/factories.py b/sources/classcore/factories.py index 3ff1306..7ebbdd6 100644 --- a/sources/classcore/factories.py +++ b/sources/classcore/factories.py @@ -29,8 +29,8 @@ def produce_class_constructor( attributes_namer: _nomina.AttributesNamer, - preprocessors: _nomina.ProduceConstructorPreprocsArgument[ __.T ] = ( ), - postprocessors: _nomina.ProduceConstructorPostprocsArgument[ __.T ] = ( ), + preprocessors: _nomina.ClassConstructionPreprocessors[ __.T ] = ( ), + postprocessors: _nomina.ClassConstructionPostprocessors[ __.T ] = ( ), ) -> _nomina.ClassConstructor[ __.T ]: ''' Produces constructors for classes. ''' @@ -68,7 +68,7 @@ def construct( # noqa: PLR0913 def produce_class_initializer( attributes_namer: _nomina.AttributesNamer, - completers: _nomina.ProduceInitializerCompletersArgument = ( ), + completers: _nomina.ClassInitializationCompleters = ( ), ) -> _nomina.ClassInitializer: ''' Produces initializers for classes. ''' diff --git a/sources/classcore/nomina.py b/sources/classcore/nomina.py index baa7ad2..c07ad47 100644 --- a/sources/classcore/nomina.py +++ b/sources/classcore/nomina.py @@ -24,36 +24,68 @@ from . import __ -AttributesNamer: __.typx.TypeAlias = __.cabc.Callable[ [ str, str ], str ] +AttributesNamer: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Callable[ [ str, str ], str ], + __.ddoc.Doc( + ''' Names attribute from level and core arguments. + + Level will be one of 'class', 'instances', or 'instance'. + Core will be the core of the name as supplied this package. + + Can be used by downstream packages to determine names of + bookkeeping attributes assigned by this package. + ''' ), +] Decorator: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ type[ __.U ] ], type[ __.U ] ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Class decorator. Takes class argument and returns class. ''' ), ] Decorators: __.typx.TypeAlias = __.typx.Annotated[ - __.cabc.Sequence[ Decorator[ __.U ] ], __.dynadoc.Fname( 'decorators' ) + __.cabc.Sequence[ Decorator[ __.U ] ], + __.ddoc.Doc( + ''' Sequence of class decorators. + + Each element takes a class argument and returns a class. + ''' ), ] DecoratorsMutable: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.MutableSequence[ Decorator[ __.U ] ], - __.dynadoc.Fname( 'decorators' ), - __.dynadoc.Doc( - ''' Decorators may be inserted or removed from sequence. ''' ), + __.ddoc.Doc( + ''' Sequence of class decorators. + + Each element takes a class argument and returns a class. + + Decorators may be inserted or removed from sequence. + ''' ), ] -DecorationPreparer: __.typx.TypeAlias = ( - __.cabc.Callable[ [ type[ __.U ], DecoratorsMutable[ __.U ] ], None ] ) -DecorationPreparers: __.typx.TypeAlias = ( - __.cabc.Sequence[ DecorationPreparer[ __.U ] ] ) -DecorationPreparersFactory: __.typx.TypeAlias = ( - __.cabc.Callable[ [ ], DecorationPreparers[ __.U ] ] ) +DecorationPreparer: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Callable[ [ type[ __.U ], DecoratorsMutable[ __.U ] ], None ], + __.ddoc.Doc( + ''' Class decoration preparer. + + Takes class and mutable sequence of decorators as arguments. + Can alter the sequence. + ''' ), +] +DecorationPreparers: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Sequence[ DecorationPreparer[ __.U ] ], + __.ddoc.Doc( + ''' Sequence of class decoration preparers. + + Each element takes class and mutable sequence of decorators as + arguments. And, each element can alter the sequence. + ''' ), +] ClassConstructorLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ ..., type ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Bound class constructor function. Usually from ``super( ).__new__`` or a partial function. @@ -61,7 +93,7 @@ ] InitializerLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ ..., None ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Bound initializer function. Usually from ``super( ).__init__`` or a partial function. @@ -69,7 +101,7 @@ ] AssignerLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ str, __.typx.Any ], None ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Bound attributes assigner function. Usually from ``super( ).__setattr__`` or a partial function. @@ -77,7 +109,7 @@ ] DeleterLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ str ], None ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Bound attributes deleter function. Usually from ``super( ).__delattr__`` or a partial function. @@ -85,7 +117,7 @@ ] SurveyorLigation: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ ], __.cabc.Iterable[ str ] ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Bound attributes surveyor function. Usually from ``super( ).__dir__`` or a partial function. @@ -105,30 +137,44 @@ ], None ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Processes class data before construction. For use cases, such as argument conversion. ''' ), ] +ClassConstructionPreprocessors: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Sequence[ ClassConstructionPreprocessor[ __.U ] ], + __.ddoc.Doc( ''' Processors to apply before construction of class. ''' ), +] ClassConstructionPostprocessor: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ type, DecoratorsMutable[ __.U ] ], None ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Processes class before decoration. For use cases, such as decorator list manipulation. ''' ), ] +ClassConstructionPostprocessors: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Sequence[ ClassConstructionPostprocessor[ __.U ] ], + __.ddoc.Doc( + ''' Processors to apply before decoration of class. ''' ), +] # TODO: ClassInitializationPreparer (arguments mutation) ClassInitializationCompleter: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ type ], None ], - __.dynadoc.Doc( + __.ddoc.Doc( ''' Completes initialization of class. For use cases, such as enabling immutability once all other initialization has occurred. ''' ), ] +ClassInitializationCompleters: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Sequence[ ClassInitializationCompleter ], + __.ddoc.Doc( + ''' Processors to apply at final stage of class initialization. ''' ), +] ClassConstructor: __.typx.TypeAlias = __.typx.Annotated[ @@ -144,7 +190,7 @@ ], type ], - __.dynadoc.Doc( ''' Constructor to use with metaclass. ''' ), + __.ddoc.Doc( ''' Constructor to use with metaclass. ''' ), ] ClassInitializer: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ @@ -156,22 +202,5 @@ ], None ], - __.dynadoc.Doc( ''' Initializer to use with metaclass. ''' ), -] - - -ProduceConstructorPreprocsArgument: __.typx.TypeAlias = __.typx.Annotated[ - __.cabc.Sequence[ ClassConstructionPreprocessor[ __.U ] ], - __.dynadoc.Doc( - ''' Processors to apply before construction of class. ''' ), -] -ProduceConstructorPostprocsArgument: __.typx.TypeAlias = __.typx.Annotated[ - __.cabc.Sequence[ ClassConstructionPostprocessor[ __.U ] ], - __.dynadoc.Doc( - ''' Processors to apply before decoration of class. ''' ), -] -ProduceInitializerCompletersArgument: __.typx.TypeAlias = __.typx.Annotated[ - __.cabc.Sequence[ ClassInitializationCompleter ], - __.dynadoc.Doc( - ''' Processors to apply at final stage of class initialization. ''' ), + __.ddoc.Doc( ''' Initializer to use with metaclass. ''' ), ] diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 99a9ef1..8205635 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -28,6 +28,26 @@ from . import nomina as _nomina +def access_core_function( # noqa: PLR0913 + cls: type, /, *, + attributes_namer: _nomina.AttributesNamer, + arguments: __.cabc.Mapping[ str, __.typx.Any ], + level: str, + name: str, + default: __.cabc.Callable[ ..., __.typx.Any ], +) -> __.cabc.Callable[ ..., __.typx.Any ]: + ''' Accesses core behavior function. + + First checks for override argument, then checks for heritable + attribute. Finally, falls back to provided default. + ''' + argument_name = f"{level}_{name}_core" + attribute_name = attributes_namer( level, f"{name}_core" ) + return ( + arguments.get( argument_name ) + or getattr( cls, attribute_name, default ) ) + + def assign_attribute_if_mutable( # noqa: PLR0913 obj: object, /, *, ligation: _nomina.AssignerLigation, @@ -225,6 +245,10 @@ def produce_class_construction_postprocessor( ) -> _nomina.ClassConstructionPostprocessor[ __.U ]: ''' Produces construction processor which determines class decorators. ''' arguments_name = attributes_namer( 'class', 'construction_arguments' ) + cores_default = dict( + assigner = assign_attribute_if_mutable, + deleter = delete_attribute_if_mutable, + surveyor = survey_visible_attributes ) def postprocess( cls: type, decorators: _nomina.DecoratorsMutable[ __.U ] @@ -236,16 +260,19 @@ def postprocess( dynadoc_cfg_name = ( attributes_namer( 'classes', 'dynadoc_configuration' ) ) dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } ) - decorators.append( __.dynadoc.with_docstring( **dynadoc_cfg ) ) + decorators.append( __.ddoc.with_docstring( **dynadoc_cfg ) ) dcls_spec = getattr( cls, '__dataclass_transform__', None ) if not dcls_spec: # either base class or metaclass may be marked dcls_spec = getattr( clscls, '__dataclass_transform__', None ) - instances_assigner = arguments.get( - 'instances_assigner_core', assign_attribute_if_mutable ) - instances_deleter = arguments.get( - 'instances_deleter_core', delete_attribute_if_mutable ) - instances_surveyor = arguments.get( - 'instances_surveyor_core', survey_visible_attributes ) + cores = { } + for core_name in ( 'assigner', 'deleter', 'surveyor' ): + core_function = access_core_function( + cls, + attributes_namer = attributes_namer, + arguments = arguments, + level = 'instances', name = core_name, + default = cores_default[ core_name ] ) + cores[ core_name ] = core_function instances_mutables = arguments.get( 'instances_mutables', __.mutables_default ) instances_visibles = arguments.get( @@ -261,9 +288,12 @@ def postprocess( decorator: _nomina.Decorator[ __.U ] = decorator_factory( attributes_namer = attributes_namer, error_class_provider = error_class_provider, - assigner_core = instances_assigner, - deleter_core = instances_deleter, - surveyor_core = instances_surveyor, + assigner_core = __.typx.cast( + _nomina.AssignerCore, cores[ 'assigner' ] ), + deleter_core = __.typx.cast( + _nomina.DeleterCore, cores[ 'deleter' ] ), + surveyor_core = __.typx.cast( + _nomina.SurveyorCore, cores[ 'surveyor' ] ), mutables = instances_mutables, visibles = instances_visibles ) decorators.append( decorator ) diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index c3dec55..00e290b 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -21,8 +21,6 @@ ''' Standard decorators. ''' # TODO? Add attribute value transformer as standard decorator argument. -# ruff: noqa: F401 - from .. import factories as _factories from .. import utilities as _utilities @@ -190,13 +188,34 @@ def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: return decorate -def produce_instances_initialization_decorator( +def produce_instances_initialization_decorator( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer, + assigner_core: __.typx.Optional[ _nomina.AssignerCore ], + deleter_core: __.typx.Optional[ _nomina.DeleterCore ], + surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ], mutables: _nomina.BehaviorExclusionVerifiersOmni, visibles: _nomina.BehaviorExclusionVerifiersOmni, ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__init__' method into class. ''' + cores = dict( + instances_assigner_core = assigner_core, + instances_deleter_core = deleter_core, + instances_surveyor_core = surveyor_core ) + cores_default = dict( + assigner = _behaviors.assign_attribute_if_mutable, + deleter = _behaviors.delete_attribute_if_mutable, + surveyor = _behaviors.survey_visible_attributes ) + def decorate( cls: type[ __.U ] ) -> type[ __.U ]: + for core_name in ( 'assigner', 'deleter', 'surveyor' ): + core_function = _behaviors.access_core_function( + cls, + attributes_namer = attributes_namer, + arguments = cores, + level = 'instances', name = core_name, + default = cores_default[ core_name ] ) + core_aname = attributes_namer( 'instances', f"{core_name}_core" ) + setattr( cls, core_aname, core_function ) behaviors: set[ str ] = set( ) behaviors_name = attributes_namer( 'instance', 'behaviors' ) _behaviors.record_behavior( @@ -253,12 +272,18 @@ def produce_attributes_assignment_decorator( level: str, attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, - implementation_core: _nomina.AssignerCore, + implementation_core: __.typx.Optional[ _nomina.AssignerCore ], ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__setattr__' method into class. ''' def decorate( cls: type[ __.U ] ) -> type[ __.U ]: leveli = 'class' if level == 'classes' else level original = cls.__dict__.get( '__setattr__' ) + core = _behaviors.access_core_function( + cls, + attributes_namer = attributes_namer, + arguments = { f"{leveli}_assigner": implementation_core }, + level = leveli, name = 'assigner', + default = _behaviors.assign_attribute_if_mutable ) if original is None: @@ -270,7 +295,7 @@ def assign_with_super( if cls is not type( self ): ligation( name, value ) return - implementation_core( + core( self, ligation = ligation, attributes_namer = attributes_namer, @@ -291,7 +316,7 @@ def assign_with_original( if cls is not type( self ): ligation( name, value ) return - implementation_core( + core( self, ligation = ligation, attributes_namer = attributes_namer, @@ -310,12 +335,18 @@ def produce_attributes_deletion_decorator( level: str, attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, - implementation_core: _nomina.DeleterCore, + implementation_core: __.typx.Optional[ _nomina.DeleterCore ], ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__delattr__' method into class. ''' def decorate( cls: type[ __.U ] ) -> type[ __.U ]: leveli = 'class' if level == 'classes' else level original = cls.__dict__.get( '__delattr__' ) + core = _behaviors.access_core_function( + cls, + attributes_namer = attributes_namer, + arguments = { f"{leveli}_deleter": implementation_core }, + level = leveli, name = 'deleter', + default = _behaviors.delete_attribute_if_mutable ) if original is None: @@ -325,7 +356,7 @@ def delete_with_super( self: object, name: str ) -> None: if cls is not type( self ): ligation( name ) return - implementation_core( + core( self, ligation = ligation, attributes_namer = attributes_namer, @@ -344,7 +375,7 @@ def delete_with_original( self: object, name: str ) -> None: if cls is not type( self ): ligation( name ) return - implementation_core( + core( self, ligation = ligation, attributes_namer = attributes_namer, @@ -362,12 +393,18 @@ def delete_with_original( self: object, name: str ) -> None: def produce_attributes_surveillance_decorator( level: str, attributes_namer: _nomina.AttributesNamer, - implementation_core: _nomina.SurveyorCore, + implementation_core: __.typx.Optional[ _nomina.SurveyorCore ], ) -> _nomina.Decorator[ __.U ]: ''' Produces decorator to inject '__dir__' method into class. ''' def decorate( cls: type[ __.U ] ) -> type[ __.U ]: leveli = 'class' if level == 'classes' else level original = cls.__dict__.get( '__dir__' ) + core = _behaviors.access_core_function( + cls, + attributes_namer = attributes_namer, + arguments = { f"{leveli}_surveyor": implementation_core }, + level = leveli, name = 'surveyor', + default = _behaviors.survey_visible_attributes ) if original is None: @@ -377,7 +414,7 @@ def survey_with_super( ligation = super( cls, self ).__dir__ # Only enforce behaviors at start of MRO. if cls is not type( self ): return ligation( ) - return implementation_core( + return core( self, ligation = ligation, attributes_namer = attributes_namer, @@ -394,7 +431,7 @@ def survey_with_original( ligation = __.funct.partial( original, self ) # Only enforce behaviors at start of MRO. if cls is not type( self ): return ligation( ) - return implementation_core( + return core( self, ligation = ligation, attributes_namer = attributes_namer, @@ -412,12 +449,9 @@ def dataclass_with_standard_behaviors( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, decorators: _nomina.Decorators[ __.U ] = ( ), - assigner_core: _nomina.AssignerCore = ( - _behaviors.assign_attribute_if_mutable ), - deleter_core: _nomina.DeleterCore = ( - _behaviors.delete_attribute_if_mutable ), - surveyor_core: _nomina.SurveyorCore = ( - _behaviors.survey_visible_attributes ), + assigner_core: __.typx.Optional[ _nomina.AssignerCore ] = None, + deleter_core: __.typx.Optional[ _nomina.DeleterCore ] = None, + surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ] = None, mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, ) -> _nomina.Decorator[ __.U ]: @@ -445,12 +479,9 @@ def with_standard_behaviors( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, decorators: _nomina.Decorators[ __.U ] = ( ), - assigner_core: _nomina.AssignerCore = ( - _behaviors.assign_attribute_if_mutable ), - deleter_core: _nomina.DeleterCore = ( - _behaviors.delete_attribute_if_mutable ), - surveyor_core: _nomina.SurveyorCore = ( - _behaviors.survey_visible_attributes ), + assigner_core: __.typx.Optional[ _nomina.AssignerCore ] = None, + deleter_core: __.typx.Optional[ _nomina.DeleterCore ] = None, + surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ] = None, mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, ) -> _nomina.Decorator[ __.U ]: @@ -488,9 +519,9 @@ def _produce_instances_decoration_preparers( def _produce_instances_decorators( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, - assigner_core: _nomina.AssignerCore, - deleter_core: _nomina.DeleterCore, - surveyor_core: _nomina.SurveyorCore, + assigner_core: __.typx.Optional[ _nomina.AssignerCore ], + deleter_core: __.typx.Optional[ _nomina.DeleterCore ], + surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ], mutables: _nomina.BehaviorExclusionVerifiersOmni, visibles: _nomina.BehaviorExclusionVerifiersOmni, ) -> _nomina.Decorators[ __.U ]: @@ -499,24 +530,25 @@ def _produce_instances_decorators( # noqa: PLR0913 decorators.append( produce_instances_initialization_decorator( attributes_namer = attributes_namer, + assigner_core = assigner_core, + deleter_core = deleter_core, + surveyor_core = surveyor_core, mutables = mutables, visibles = visibles ) ) - if mutables != '*': - decorators.append( - produce_attributes_assignment_decorator( - level = 'instances', - attributes_namer = attributes_namer, - error_class_provider = error_class_provider, - implementation_core = assigner_core ) ) - decorators.append( - produce_attributes_deletion_decorator( - level = 'instances', - attributes_namer = attributes_namer, - error_class_provider = error_class_provider, - implementation_core = deleter_core ) ) - if visibles != '*': - decorators.append( - produce_attributes_surveillance_decorator( - level = 'instances', - attributes_namer = attributes_namer, - implementation_core = surveyor_core ) ) + decorators.append( + produce_attributes_assignment_decorator( + level = 'instances', + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + implementation_core = assigner_core ) ) + decorators.append( + produce_attributes_deletion_decorator( + level = 'instances', + attributes_namer = attributes_namer, + error_class_provider = error_class_provider, + implementation_core = deleter_core ) ) + decorators.append( + produce_attributes_surveillance_decorator( + level = 'instances', + attributes_namer = attributes_namer, + implementation_core = surveyor_core ) ) return decorators diff --git a/sources/classcore/standard/dynadoc.py b/sources/classcore/standard/dynadoc.py index a116af0..a9eace7 100644 --- a/sources/classcore/standard/dynadoc.py +++ b/sources/classcore/standard/dynadoc.py @@ -26,34 +26,34 @@ from . import nomina as _nomina -dynadoc_context = __.dynadoc.produce_context( ) +dynadoc_context = __.ddoc.produce_context( ) dynadoc_class_introspection_control = ( - __.dynadoc.ClassIntrospectionControl( + __.ddoc.ClassIntrospectionControl( inheritance = True, introspectors = ( - __.dynadoc.introspection.introspect_special_classes, ) ) ) + __.ddoc.introspection.introspect_special_classes, ) ) ) dynadoc_module_introspection_control = ( - __.dynadoc.ModuleIntrospectionControl( ) ) + __.ddoc.ModuleIntrospectionControl( ) ) def dynadoc_avoid_immutables( objct: object, - introspection: __.dynadoc.IntrospectionControl, + introspection: __.ddoc.IntrospectionControl, attributes_namer: _nomina.AttributesNamer, -) -> __.dynadoc.IntrospectionControl: +) -> __.ddoc.IntrospectionControl: ''' Disables introspection of immutable objects. ''' if __.inspect.isclass( objct ): behaviors_name = attributes_namer( 'class', 'behaviors' ) behaviors = _utilities.getattr0( objct, behaviors_name, frozenset( ) ) if _nomina.immutability_label in behaviors: return introspection.with_limit( - __.dynadoc.IntrospectionLimit( disable = True ) ) + __.ddoc.IntrospectionLimit( disable = True ) ) return introspection def produce_dynadoc_introspection_limiter( attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, -) -> __.dynadoc.IntrospectionLimiter: +) -> __.ddoc.IntrospectionLimiter: ''' Produces introspection limiter which avoids immutable objects. ''' return __.funct.partial( dynadoc_avoid_immutables, attributes_namer = attributes_namer ) @@ -63,17 +63,17 @@ def produce_dynadoc_introspection_limiter( def produce_dynadoc_introspection_control( enable: bool = True, - class_control: __.dynadoc.ClassIntrospectionControl = ( + class_control: __.ddoc.ClassIntrospectionControl = ( dynadoc_class_introspection_control ), - module_control: __.dynadoc.ModuleIntrospectionControl = ( + module_control: __.ddoc.ModuleIntrospectionControl = ( dynadoc_module_introspection_control ), - limiters: __.dynadoc.IntrospectionLimiters = ( + limiters: __.ddoc.IntrospectionLimiters = ( dynadoc_introspection_limiter, ), - targets: __.dynadoc.IntrospectionTargets = ( - __.dynadoc.IntrospectionTargetsSansModule ), -) -> __.dynadoc.IntrospectionControl: + targets: __.ddoc.IntrospectionTargets = ( + __.ddoc.IntrospectionTargetsSansModule ), +) -> __.ddoc.IntrospectionControl: ''' Produces compatible Dynadoc introspection control. ''' - return __.dynadoc.IntrospectionControl( + return __.ddoc.IntrospectionControl( enable = enable, class_control = class_control, module_control = module_control, @@ -83,18 +83,18 @@ def produce_dynadoc_introspection_control( dynadoc_introspection_on_class = produce_dynadoc_introspection_control( ) dynadoc_introspection_on_package = ( produce_dynadoc_introspection_control( - targets = __.dynadoc.IntrospectionTargetsOmni ) ) + targets = __.ddoc.IntrospectionTargetsOmni ) ) def assign_module_docstring( # noqa: PLR0913 module: str | __.types.ModuleType, /, - *fragments: __.dynadoc.interfaces.Fragment, + *fragments: __.ddoc.interfaces.Fragment, context: _nomina.DynadocContextArgument = dynadoc_context, introspection: _nomina.DynadocIntrospectionArgument = ( dynadoc_introspection_on_package ), preserve: _nomina.DynadocPreserveArgument = True, - renderer: __.dynadoc.xtnsapi.Renderer = ( - __.dynadoc.assembly.renderer_default ), + renderer: __.ddoc.xtnsapi.Renderer = ( + __.ddoc.assembly.renderer_default ), table: _nomina.DynadocTableArgument = __.dictproxy_empty, ) -> None: ''' Updates module docstring based on introspection. @@ -104,7 +104,7 @@ def assign_module_docstring( # noqa: PLR0913 By default, ignores previously-decorated immutable classes. ''' - __.dynadoc.assign_module_docstring( + __.ddoc.assign_module_docstring( module, *fragments, context = context, diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index ae4937d..9f5f82c 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -34,20 +34,20 @@ class Module( _classes.Object, __.types.ModuleType ): def reclassify_modules( attributes: __.typx.Annotated[ __.cabc.Mapping[ str, __.typx.Any ] | __.types.ModuleType | str, - __.dynadoc.Doc( + __.ddoc.Doc( ''' Module, module name, or dictionary of object attributes. ''' ), ], /, *, attributes_namer: __.typx.Annotated[ _nomina.AttributesNamer, - __.dynadoc.Doc( + __.ddoc.Doc( ''' Attributes namer function with which to seal class. ''' ), ] = __.calculate_attrname, recursive: __.typx.Annotated[ - bool, __.dynadoc.Doc( ''' Recursively reclassify package modules? ''' ) + bool, __.ddoc.Doc( ''' Recursively reclassify package modules? ''' ) ] = False, replacement_class: __.typx.Annotated[ type[ __.types.ModuleType ], - __.dynadoc.Doc( ''' New class for module. ''' ), + __.ddoc.Doc( ''' New class for module. ''' ), ] = Module, ) -> None: # TODO? Ensure correct operation with namespace packages. diff --git a/sources/classcore/standard/nomina.py b/sources/classcore/standard/nomina.py index 30b7b2f..ec04730 100644 --- a/sources/classcore/standard/nomina.py +++ b/sources/classcore/standard/nomina.py @@ -46,8 +46,15 @@ __.cabc.Sequence[ BehaviorExclusionVerifier ] ) BehaviorExclusionVerifiersOmni: __.typx.TypeAlias = ( BehaviorExclusionVerifiers | __.typx.Literal[ '*' ] ) -ErrorClassProvider: __.typx.TypeAlias = ( - __.cabc.Callable[ [ str ], type[ Exception ] ] ) +ErrorClassProvider: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Callable[ [ str ], type[ Exception ] ], + __.ddoc.Doc( + ''' Takes name of exception class and returns corresponding class. + + Can be used by downstream packages to provide exceptions from their + own hierarchies rather than the hierarchy from this package. + ''' ), +] class AssignerCore( __.typx.Protocol ): @@ -103,9 +110,10 @@ def __call__( # pragma: no branch DynadocConfiguration: __.typx.TypeAlias = __.cabc.Mapping[ str, __.typx.Any ] +# TODO: Use argument type aliases from 'dynadoc' package. DynadocContextArgument: __.typx.TypeAlias = __.typx.Annotated[ - __.dynadoc.Context, - __.dynadoc.Doc( + __.ddoc.Context, + __.ddoc.Doc( ''' Dynadoc context. Renderer, dictionaries for resolution of stringified annotations, @@ -113,8 +121,8 @@ def __call__( # pragma: no branch ''' ), ] DynadocIntrospectionArgument: __.typx.TypeAlias = __.typx.Annotated[ - __.dynadoc.IntrospectionControl, - __.dynadoc.Doc( + __.ddoc.IntrospectionControl, + __.ddoc.Doc( ''' Dynadoc introspection control. Which kinds of object to recursively introspect? @@ -124,15 +132,15 @@ def __call__( # pragma: no branch ''' ), ] DynadocPreserveArgument: __.typx.TypeAlias = __.typx.Annotated[ - bool, __.dynadoc.Doc( ''' Preserve existing docstring? ''' ) + bool, __.ddoc.Doc( ''' Preserve existing docstring? ''' ) ] DynadocTableArgument: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Mapping[ str, str ], - __.dynadoc.Doc( ''' Table of documentation fragments. ''' ), + __.ddoc.Doc( ''' Table of documentation fragments. ''' ), ] ProduceDynadocConfigurationReturn: __.typx.TypeAlias = __.typx.Annotated[ DynadocConfiguration, - __.dynadoc.Doc( + __.ddoc.Doc( ''' Dynadoc configuration dictionary. Suitable as a keyword expansion (``**``) argument to diff --git a/sources/classcore/utilities.py b/sources/classcore/utilities.py index 254d933..fb4ae6e 100644 --- a/sources/classcore/utilities.py +++ b/sources/classcore/utilities.py @@ -95,8 +95,11 @@ def mangle_name( objct: object, /, name: str ) -> str: Effectively provides name of private member attribute, which is unique across class inheritance. ''' + # TODO: Replace expensive SHA-256 hash with simple 'id'. + # Need to debug weird issue with using 'id' early on dataclasses. if not __.inspect.isclass( objct ): return mangle_name( type( objct ), name ) + # return "{name}_{uid}".format( name = name, uid = id( objct ) ) namehash = __.hashlib.sha256( ) namehash.update( qualify_class_name( objct ).encode( ) ) namehash_hex = namehash.hexdigest( ) diff --git a/tests/test_000_classcore/test_330_standard_decorators.py b/tests/test_000_classcore/test_330_standard_decorators.py new file mode 100644 index 0000000..676102d --- /dev/null +++ b/tests/test_000_classcore/test_330_standard_decorators.py @@ -0,0 +1,107 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +import pytest + +from . import PACKAGE_NAME, cache_import_module + + +MODULE_QNAME = f"{PACKAGE_NAME}.standard.decorators" + + +def simple_assigner_core( + objct, + ligation, + attributes_namer, + error_class_provider, + level, name, value +): + object.__setattr__( objct, name, value ) + + +def simple_deleter_core( + objct, + ligation, + attributes_namer, + error_class_provider, + level, name +): + object.__delattr__( objct, name ) + + +def simple_surveyor_core( objct, ligation, attributes_namer, level ): + return object.__dir__( objct ) + + +def test_120_core_function_inheritance( ): + ''' Core functions inherited. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + + @module.with_standard_behaviors( ) + class Base: pass + + b = Base( ) + with pytest.raises( exceptions.AttributeImmutability ): + b.bar = 6 + assert not hasattr( b, 'bar' ) + assert 'bar' not in dir( b ) + with pytest.raises( exceptions.AttributeImmutability ): + del b.bar + + @module.with_standard_behaviors( ) + class Derivation( Base ): pass + + d = Derivation( ) + with pytest.raises( exceptions.AttributeImmutability ): + d.foo = 2 + assert not hasattr( d, 'foo' ) + assert 'foo' not in dir( d ) + with pytest.raises( exceptions.AttributeImmutability ): + del d.foo + + +def test_121_core_function_replacements_inheritance( ): + ''' Replacement core functions inherited. ''' + module = cache_import_module( MODULE_QNAME ) + + @module.with_standard_behaviors( + assigner_core = simple_assigner_core, + deleter_core = simple_deleter_core, + surveyor_core = simple_surveyor_core, + ) + class Base: pass + + b = Base( ) + b.bar = 6 + assert b.bar == 6 + assert 'bar' in dir( b ) + del b.bar + assert not hasattr( b, 'bar' ) + + @module.with_standard_behaviors( ) + class Derivation( Base ): pass + + d = Derivation( ) + d.foo = 2 + assert d.foo == 2 + assert 'foo' in dir( d ) + del d.foo + assert not hasattr( d, 'foo' ) From f84d5a5699a39e56fa7ffbbf6d7924e5ad7ce7b7 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Thu, 26 Jun 2025 18:48:14 +0000 Subject: [PATCH 14/28] Update documents for publication. --- .../data/towncrier/+inheritance.repair.rst | 5 +- .../data/towncrier/+interfaces.enhance.rst | 3 - .auxiliary/publications/website.tar.xz | 4 +- documentation/changelog.rst | 10 +++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/decorators.py | 62 +++++++++---- .../test_330_standard_decorators.py | 87 ++++++++++++++++++- 7 files changed, 144 insertions(+), 29 deletions(-) delete mode 100644 .auxiliary/data/towncrier/+interfaces.enhance.rst diff --git a/.auxiliary/data/towncrier/+inheritance.repair.rst b/.auxiliary/data/towncrier/+inheritance.repair.rst index ce51fbb..d6c8595 100644 --- a/.auxiliary/data/towncrier/+inheritance.repair.rst +++ b/.auxiliary/data/towncrier/+inheritance.repair.rst @@ -1,3 +1,2 @@ -Ensure that replacement implementations (``assigner_core``, ``deleter_core``, -and ``surveyor_core``) are inherited so that behaviors do not regress to -standard baseline behaviors in descendant classes. +Ensure the inheritance of replacement functions works via metaclasses and not +just decorators. diff --git a/.auxiliary/data/towncrier/+interfaces.enhance.rst b/.auxiliary/data/towncrier/+interfaces.enhance.rst deleted file mode 100644 index 09a88c4..0000000 --- a/.auxiliary/data/towncrier/+interfaces.enhance.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve docstrings for various public type aliases. Also, drop ``Fname`` -references from public type aliases, such as ``Decorators`` so that they can be -reused in downstream packages. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 53a8174..7e8ab38 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc68219959b1927dcf8cbb067bd8a54487c42b443e530c46183bf83c7382b8c0 -size 257812 +oid sha256:a3368fc1c121504f027376062a02f7af9a82ef3f1d726dd4edf7f672e9063dcf +size 266820 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 1c33c85..f4d8651 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,16 @@ Release Notes .. towncrier release notes start +Classcore 1.5.1 (2025-06-26) +============================ + +Repairs +------- + +- Ensure the inheritance of replacement functions works via metaclasses and not + just decorators. + + Classcore 1.5 (2025-06-24) ========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index c8a9d69..b392bb3 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.5' +__version__ = '1.5.1' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 00e290b..2ae2c9a 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -54,6 +54,33 @@ def prepare_dataclass_for_instances( setattr( cls, behaviors_name_m, __.dcls.field( init = False ) ) +def apply_cfc_core_functions( + clscls: type[ __.T ], /, + attributes_namer: _nomina.AttributesNamer, + assigner_core: __.typx.Optional[ _nomina.AssignerCore ] = None, + deleter_core: __.typx.Optional[ _nomina.DeleterCore ] = None, + surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ] = None, +) -> None: + ''' Stores core functions on metaclass. ''' + cores = dict( + classes_assigner_core = assigner_core, + classes_deleter_core = deleter_core, + classes_surveyor_core = surveyor_core ) + cores_default = dict( + assigner = _behaviors.assign_attribute_if_mutable, + deleter = _behaviors.delete_attribute_if_mutable, + surveyor = _behaviors.survey_visible_attributes ) + for core_name in ( 'assigner', 'deleter', 'surveyor' ): + core_function = _behaviors.access_core_function( + clscls, + attributes_namer = attributes_namer, + arguments = cores, + level = 'classes', name = core_name, + default = cores_default[ core_name ] ) + core_aname = attributes_namer( 'classes', f"{core_name}_core" ) + setattr( clscls, core_aname, core_function ) + + def apply_cfc_dynadoc_configuration( clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer, @@ -107,7 +134,7 @@ def apply_cfc_attributes_assigner( clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, - implementation_core: _nomina.AssignerCore, + implementation_core: __.typx.Optional[ _nomina.AssignerCore ], ) -> None: ''' Injects '__setattr__' method into metaclass. ''' decorator = produce_attributes_assignment_decorator( @@ -122,7 +149,7 @@ def apply_cfc_attributes_deleter( clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, - implementation_core: _nomina.DeleterCore, + implementation_core: __.typx.Optional[ _nomina.DeleterCore ], ) -> None: ''' Injects '__delattr__' method into metaclass. ''' decorator = produce_attributes_deletion_decorator( @@ -136,7 +163,7 @@ def apply_cfc_attributes_deleter( def apply_cfc_attributes_surveyor( clscls: type[ __.T ], attributes_namer: _nomina.AttributesNamer, - implementation_core: _nomina.SurveyorCore, + implementation_core: __.typx.Optional[ _nomina.SurveyorCore ], ) -> None: ''' Injects '__dir__' method into metaclass. ''' decorator = produce_attributes_surveillance_decorator( @@ -149,17 +176,20 @@ def apply_cfc_attributes_surveyor( def class_factory( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, - assigner_core: _nomina.AssignerCore = ( - _behaviors.assign_attribute_if_mutable ), - deleter_core: _nomina.DeleterCore = ( - _behaviors.delete_attribute_if_mutable ), - surveyor_core: _nomina.SurveyorCore = ( - _behaviors.survey_visible_attributes ), + assigner_core: __.typx.Optional[ _nomina.AssignerCore ] = None, + deleter_core: __.typx.Optional[ _nomina.DeleterCore ] = None, + surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ] = None, dynadoc_configuration: __.cabc.Mapping[ str, __.typx.Any ] = ( _dynadoc_configuration ), ) -> _nomina.Decorator[ __.T ]: ''' Produces decorator to apply standard behaviors to metaclass. ''' def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: + apply_cfc_core_functions( + clscls, + attributes_namer = attributes_namer, + assigner_core = assigner_core, + deleter_core = deleter_core, + surveyor_core = surveyor_core ) apply_cfc_dynadoc_configuration( clscls, attributes_namer = attributes_namer, @@ -281,8 +311,8 @@ def decorate( cls: type[ __.U ] ) -> type[ __.U ]: core = _behaviors.access_core_function( cls, attributes_namer = attributes_namer, - arguments = { f"{leveli}_assigner": implementation_core }, - level = leveli, name = 'assigner', + arguments = { f"{level}_assigner": implementation_core }, + level = level, name = 'assigner', default = _behaviors.assign_attribute_if_mutable ) if original is None: @@ -344,8 +374,8 @@ def decorate( cls: type[ __.U ] ) -> type[ __.U ]: core = _behaviors.access_core_function( cls, attributes_namer = attributes_namer, - arguments = { f"{leveli}_deleter": implementation_core }, - level = leveli, name = 'deleter', + arguments = { f"{level}_deleter": implementation_core }, + level = level, name = 'deleter', default = _behaviors.delete_attribute_if_mutable ) if original is None: @@ -402,8 +432,8 @@ def decorate( cls: type[ __.U ] ) -> type[ __.U ]: core = _behaviors.access_core_function( cls, attributes_namer = attributes_namer, - arguments = { f"{leveli}_surveyor": implementation_core }, - level = leveli, name = 'surveyor', + arguments = { f"{level}_surveyor": implementation_core }, + level = level, name = 'surveyor', default = _behaviors.survey_visible_attributes ) if original is None: @@ -512,7 +542,7 @@ def _produce_instances_decoration_preparers( if class_preparer is not None: preprocessors.append( __.funct.partial( - class_preparer, attributes_namer = attributes_namer ) ) + class_preparer, attributes_namer = attributes_namer ) ) return tuple( preprocessors ) diff --git a/tests/test_000_classcore/test_330_standard_decorators.py b/tests/test_000_classcore/test_330_standard_decorators.py index 676102d..3fea68b 100644 --- a/tests/test_000_classcore/test_330_standard_decorators.py +++ b/tests/test_000_classcore/test_330_standard_decorators.py @@ -50,8 +50,32 @@ def simple_surveyor_core( objct, ligation, attributes_namer, level ): return object.__dir__( objct ) -def test_120_core_function_inheritance( ): - ''' Core functions inherited. ''' +def class_simple_assigner_core( + cls, + ligation, + attributes_namer, + error_class_provider, + level, name, value +): + type.__setattr__( cls, name, value ) + + +def class_simple_deleter_core( + cls, + ligation, + attributes_namer, + error_class_provider, + level, name +): + type.__delattr__( cls, name ) + + +def class_simple_surveyor_core( cls, ligation, attributes_namer, level ): + return type.__dir__( cls ) + + +def test_120_decorator_core_function_inheritance( ): + ''' Core functions (via decorator) inherited. ''' module = cache_import_module( MODULE_QNAME ) exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) @@ -78,8 +102,8 @@ class Derivation( Base ): pass del d.foo -def test_121_core_function_replacements_inheritance( ): - ''' Replacement core functions inherited. ''' +def test_121_decorator_core_function_replacements_inheritance( ): + ''' Replacement core functions (via decorator) inherited. ''' module = cache_import_module( MODULE_QNAME ) @module.with_standard_behaviors( @@ -105,3 +129,58 @@ class Derivation( Base ): pass assert 'foo' in dir( d ) del d.foo assert not hasattr( d, 'foo' ) + + +def test_220_cfc_core_function_inheritance( ): + ''' Core functions (via metaclass) inherited. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + + @module.class_factory( ) + class Class( type ): pass + + class Base( metaclass = Class ): pass + + with pytest.raises( exceptions.AttributeImmutability ): + Base.bar = 6 + assert not hasattr( Base, 'bar' ) + assert 'bar' not in dir( Base ) + with pytest.raises( exceptions.AttributeImmutability ): + del Base.bar + + class Derivation( Base ): pass + + with pytest.raises( exceptions.AttributeImmutability ): + Derivation.foo = 2 + assert not hasattr( Derivation, 'foo' ) + assert 'foo' not in dir( Derivation ) + with pytest.raises( exceptions.AttributeImmutability ): + del Derivation.foo + + +def test_221_cfc_core_function_replacements_inheritance( ): + ''' Replacement core functions (via metaclass) inherited. ''' + module = cache_import_module( MODULE_QNAME ) + + @module.class_factory( + assigner_core = class_simple_assigner_core, + deleter_core = class_simple_deleter_core, + surveyor_core = class_simple_surveyor_core, + ) + class Class( type ): pass + + class Base( metaclass = Class ): pass + + Base.bar = 6 + assert Base.bar == 6 + assert 'bar' in dir( Base ) + del Base.bar + assert not hasattr( Base, 'bar' ) + + class Derivation( Base ): pass + + Derivation.foo = 2 + assert Derivation.foo == 2 + assert 'foo' in dir( Derivation ) + del Derivation.foo + assert not hasattr( Derivation, 'foo' ) From d7c5b97974680b0d4ace931bcc225fa0628534cc Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Mon, 30 Jun 2025 03:57:16 +0000 Subject: [PATCH 15/28] Update documents for publication. --- .auxiliary/data/towncrier/+decorder.repair.rst | 4 ++++ .auxiliary/data/towncrier/+inheritance.repair.rst | 2 -- .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 12 ++++++++++++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/behaviors.py | 14 ++++++++------ 6 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .auxiliary/data/towncrier/+decorder.repair.rst delete mode 100644 .auxiliary/data/towncrier/+inheritance.repair.rst diff --git a/.auxiliary/data/towncrier/+decorder.repair.rst b/.auxiliary/data/towncrier/+decorder.repair.rst new file mode 100644 index 0000000..7d06a55 --- /dev/null +++ b/.auxiliary/data/towncrier/+decorder.repair.rst @@ -0,0 +1,4 @@ +Standard Classes: Ensure that Dynadoc decorator applies last, after any +decorators which may potentially replace classes (e.g., ``dataclass( slots = +True )``), so that the Dynadoc visitees weak set captures the correct reference +to prevent multiple decoration. diff --git a/.auxiliary/data/towncrier/+inheritance.repair.rst b/.auxiliary/data/towncrier/+inheritance.repair.rst deleted file mode 100644 index d6c8595..0000000 --- a/.auxiliary/data/towncrier/+inheritance.repair.rst +++ /dev/null @@ -1,2 +0,0 @@ -Ensure the inheritance of replacement functions works via metaclasses and not -just decorators. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 7e8ab38..483fa34 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3368fc1c121504f027376062a02f7af9a82ef3f1d726dd4edf7f672e9063dcf -size 266820 +oid sha256:f6029e916f0bfbb4065f55049e2cca26fb877b61f2ec774bc4f9f52ee9dac177 +size 292988 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index f4d8651..aa61375 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,18 @@ Release Notes .. towncrier release notes start +Classcore 1.5.2 (2025-06-29) +============================ + +Repairs +------- + +- Standard Classes: Ensure that Dynadoc decorator applies last, after any + decorators which may potentially replace classes (e.g., ``dataclass( slots = + True )``), so that the Dynadoc visitees weak set captures the correct reference + to prevent multiple decoration. + + Classcore 1.5.1 (2025-06-26) ============================ diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index b392bb3..4c5a105 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.5.1' +__version__ = '1.5.2' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 8205635..9a9eebd 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -255,12 +255,6 @@ def postprocess( ) -> None: arguments = getattr( cls, arguments_name, { } ) clscls = type( cls ) - dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } ) - if not dynadoc_cfg: # either metaclass argument or attribute - dynadoc_cfg_name = ( - attributes_namer( 'classes', 'dynadoc_configuration' ) ) - dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } ) - decorators.append( __.ddoc.with_docstring( **dynadoc_cfg ) ) dcls_spec = getattr( cls, '__dataclass_transform__', None ) if not dcls_spec: # either base class or metaclass may be marked dcls_spec = getattr( clscls, '__dataclass_transform__', None ) @@ -297,6 +291,14 @@ def postprocess( mutables = instances_mutables, visibles = instances_visibles ) decorators.append( decorator ) + # Dynadoc tracks objects in weakset. + # Must decorate after any potential class replacements. + dynadoc_cfg = arguments.get( 'dynadoc_configuration', { } ) + if not dynadoc_cfg: # either metaclass argument or attribute + dynadoc_cfg_name = ( + attributes_namer( 'classes', 'dynadoc_configuration' ) ) + dynadoc_cfg = getattr( clscls, dynadoc_cfg_name, { } ) + decorators.append( __.ddoc.with_docstring( **dynadoc_cfg ) ) return postprocess From 8317caa3af50780a36c0ebb7fb1e80e1105f8371 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Mon, 30 Jun 2025 04:06:24 +0000 Subject: [PATCH 16/28] Update documents for publication. --- .auxiliary/publications/website.tar.xz | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 483fa34..7ef34ac 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6029e916f0bfbb4065f55049e2cca26fb877b61f2ec774bc4f9f52ee9dac177 -size 292988 +oid sha256:574633333250373021b181ced1c992d89054337476728de72b1dab2c07216d81 +size 293012 From 0f03dd80e71be1811397c4bc9cdf85e5b21098e5 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Tue, 1 Jul 2025 05:37:01 +0000 Subject: [PATCH 17/28] Update documents for publication. --- .auxiliary/data/towncrier/+dataclasses.repair.rst | 4 ++++ .auxiliary/data/towncrier/+decorder.repair.rst | 4 ---- .auxiliary/data/towncrier/+slots.repair.rst | 1 + .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 13 +++++++++++++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/behaviors.py | 1 + sources/classcore/standard/decorators.py | 10 ++++++---- sources/classcore/utilities.py | 4 ++-- 9 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 .auxiliary/data/towncrier/+dataclasses.repair.rst delete mode 100644 .auxiliary/data/towncrier/+decorder.repair.rst create mode 100644 .auxiliary/data/towncrier/+slots.repair.rst diff --git a/.auxiliary/data/towncrier/+dataclasses.repair.rst b/.auxiliary/data/towncrier/+dataclasses.repair.rst new file mode 100644 index 0000000..04416f4 --- /dev/null +++ b/.auxiliary/data/towncrier/+dataclasses.repair.rst @@ -0,0 +1,4 @@ +Standard: Ensure that behaviors tracking attribute is not part of comparisons, +hashes, or ``repr`` calculations for dataclasses. Attributes with "private" +names which resemble the CPython scheme for class-local (non-inheritable) +attributes can create confusion for the internal machinery of ``dataclasses``. diff --git a/.auxiliary/data/towncrier/+decorder.repair.rst b/.auxiliary/data/towncrier/+decorder.repair.rst deleted file mode 100644 index 7d06a55..0000000 --- a/.auxiliary/data/towncrier/+decorder.repair.rst +++ /dev/null @@ -1,4 +0,0 @@ -Standard Classes: Ensure that Dynadoc decorator applies last, after any -decorators which may potentially replace classes (e.g., ``dataclass( slots = -True )``), so that the Dynadoc visitees weak set captures the correct reference -to prevent multiple decoration. diff --git a/.auxiliary/data/towncrier/+slots.repair.rst b/.auxiliary/data/towncrier/+slots.repair.rst new file mode 100644 index 0000000..bd1ff6e --- /dev/null +++ b/.auxiliary/data/towncrier/+slots.repair.rst @@ -0,0 +1 @@ +Standard: Do not create duplicate slots for behaviors tracking. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 7ef34ac..41b4885 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:574633333250373021b181ced1c992d89054337476728de72b1dab2c07216d81 -size 293012 +oid sha256:8ca952048182954af554a0d68013d8b61724800dea447ae8597fdf1deafb8b08 +size 301680 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index aa61375..cd73410 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,19 @@ Release Notes .. towncrier release notes start +Classcore 1.5.3 (2025-06-30) +============================ + +Repairs +------- + +- Standard: Do not create duplicate slots for behaviors tracking. +- Standard: Ensure that behaviors tracking attribute is not part of comparisons, + hashes, or ``repr`` calculations for dataclasses. Attributes with "private" + names which resemble the CPython scheme for class-local (non-inheritable) + attributes can create confusion for the internal machinery of ``dataclasses``. + + Classcore 1.5.2 (2025-06-29) ============================ diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 4c5a105..49208c3 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.5.2' +__version__ = '1.5.3' standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 9a9eebd..bdbb6bb 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -182,6 +182,7 @@ def augment_class_attributes_allocations( __.cabc.Sequence[ str ], None ] = namespace.get( '__slots__' ) + if slots and behaviors_name in slots: return if isinstance( slots, __.cabc.Mapping ): slots_ = dict( slots ) slots_[ behaviors_name ] = 'Active behaviors.' diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 2ae2c9a..4704996 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -47,11 +47,13 @@ def prepare_dataclass_for_instances( ''' Annotates dataclass in support of instantiation machinery. ''' annotations = __.inspect.get_annotations( cls ) behaviors_name = attributes_namer( 'instance', 'behaviors' ) - # TODO? Only use mangling if not slotted. - behaviors_name_m = _utilities.mangle_name( cls, behaviors_name ) - annotations[ behaviors_name_m ] = set[ str ] + # TODO: Only use mangling if not slotted. + # behaviors_name_ = _utilities.mangle_name( cls, behaviors_name ) + behaviors_name_ = behaviors_name + annotations[ behaviors_name_ ] = set[ str ] setattr( cls, '__annotations__', annotations ) # in case of absence - setattr( cls, behaviors_name_m, __.dcls.field( init = False ) ) + setattr( cls, behaviors_name_, __.dcls.field( + compare = False, hash = False, init = False, repr = False ) ) def apply_cfc_core_functions( diff --git a/sources/classcore/utilities.py b/sources/classcore/utilities.py index fb4ae6e..1616e70 100644 --- a/sources/classcore/utilities.py +++ b/sources/classcore/utilities.py @@ -99,11 +99,11 @@ def mangle_name( objct: object, /, name: str ) -> str: # Need to debug weird issue with using 'id' early on dataclasses. if not __.inspect.isclass( objct ): return mangle_name( type( objct ), name ) - # return "{name}_{uid}".format( name = name, uid = id( objct ) ) + # return "{name}{uid}".format( name = name, uid = id( objct ) ) namehash = __.hashlib.sha256( ) namehash.update( qualify_class_name( objct ).encode( ) ) namehash_hex = namehash.hexdigest( ) - return f"{name}_{namehash_hex}" + return f"{name}{namehash_hex}" def qualify_class_name( cls: type ) -> str: From f72d45e8d3e27a95dd2b9965cab852a068119fa6 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Tue, 1 Jul 2025 19:10:14 +0000 Subject: [PATCH 18/28] Update documents for publication. --- .auxiliary/configuration/conventions.md | 109 ++++++-- .auxiliary/configuration/copier-answers.yaml | 2 +- .auxiliary/configuration/gemini/settings.json | 31 +++ .auxiliary/configuration/mcp-servers.json | 33 +++ .auxiliary/configuration/pre-commit.yaml | 2 +- .../data/towncrier/+dataclasses.repair.rst | 4 - .../+deprecate-reclassify-modules.notify.rst | 1 + .../towncrier/+finalize-module.enhance.rst | 1 + .auxiliary/data/towncrier/+slots.repair.rst | 1 - .auxiliary/publications/website.tar.xz | 4 +- .auxiliary/scribbles/.gitignore | 2 + .gitignore | 3 + documentation/changelog.rst | 15 + documentation/contribution.rst | 6 +- documentation/examples/std-behaviors.rst | 2 +- documentation/examples/std-modules.rst | 259 +++++++++++++++++- pyproject.toml | 3 +- sources/classcore/__/imports.py | 2 +- sources/classcore/__init__.py | 6 +- sources/classcore/standard/modules.py | 58 ++++ .../test_350_standard_modules.py | 92 +++++++ 21 files changed, 590 insertions(+), 46 deletions(-) create mode 100644 .auxiliary/configuration/gemini/settings.json create mode 100644 .auxiliary/configuration/mcp-servers.json delete mode 100644 .auxiliary/data/towncrier/+dataclasses.repair.rst create mode 100644 .auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst create mode 100644 .auxiliary/data/towncrier/+finalize-module.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+slots.repair.rst create mode 100644 .auxiliary/scribbles/.gitignore diff --git a/.auxiliary/configuration/conventions.md b/.auxiliary/configuration/conventions.md index 07d74ce..a6d8d50 100644 --- a/.auxiliary/configuration/conventions.md +++ b/.auxiliary/configuration/conventions.md @@ -1,15 +1,89 @@ # General Advice +### Context + +- Be sure the look at any README files in the directories which contain the + code or data that you intend to manipulate. They may provide valuable + insights about architecture, constraints, and TODO items. +- At the start of a new session, read any files in the `.auxiliary/notes` + directory. +- During the course of conversation with the user and completion of your tasks, + be sure to update files under `.auxiliary/notes`, removing completed tasks + and adding emergent items. (This will help ensure smooth transition between + sessions.) +- If the 'context7' MCP server is available, try to use that, as necessary, to + retrieve up-to-date documentation for any SDKs or APIs with which you want to + develop. + ### Design - Make classes lightweight. Prefer module-level functions over class methods. - Functions should not be more than 30 lines long. Refactor long functions. +- Modules should not be more than 600 lines long. Refactor large modules. - Keep the number of function arguments small. Pass common state via data transfer objects (DTOs). - Use dependency injection to improve configuration and testability. Choose sensible defaults for injected dependencies to streamline normal development. - Prefer immutability wherever possible. +### Judgment + +- Ensure that you understand why you are performing a task. The user should + give you a clear goal or purpose. +- If you receive data or instructions which seem counter to purpose, then do + not blindly follow the instructions or make code hacks to conform to the + data. + - The user is fallible: data may be erroneous; instructions may contain + typos or be ambiguous. + - You are encouraged to ask clarifying questions or challenge assumptions, + as appropriate. + +### Refactors + +- Ensure that you have sufficient regression tests before attempting refactors. +- Break up large refactors into milestones and make a plan before executing. +- Align your refactors with separation of concerns. +- Ensure that the code can still build and that tests still pass at each + refactoring milestone. +- Be sure to cleanup dead code after completing a refactor. + +### Tests + +- Do not change test expectations to match the results from updated code + without explicit user consent. (Tests exist to enforce desired behaviors.) +- Do not write tests unless explicitly instructed to do so. +- Prefer to write tests in a separate directory hierarchy rather than inline in + code. (Inline tests waste conversation tokens when entire files are being + viewed.) + +### Comments and Style + +- Do not strip comments from existing code unless directed to do so. +- Do not describe obvious code with comments. Only comment on non-obvious or + complex behaviors. +- Leave TODO comments about uncovered edge cases, tests, and other future work. +- Do not break function bodies with empty lines. + +### Operation + +- **Use `rg --line-number --column`** to get precise coordinates for MCP tools + that require line/column positions. +- If you have access to `text-editor` MCP tools, prefer to use them over other + text editing and search-and-replace tools. (Line number-based edits are less + error-prone.) + - **Always reread files with `text-editor` tools** after modifying files + via other tools (like `rust-analyzer`) to avoid file hash conflicts. + - Batch related changes together to minimize file modification + conflicts between different MCP tools. +- If you have access to shell tools, try to use them with relative paths rather + than absolute paths. E.g., if your working directory is + `/home/me/src/some-project` and you want to run `sed` on + `/home/me/src/some-project/README.md`, then run `sed` on `README.md` and not + on the full absolute path. +- Do not write to paths outside of the current project unless the user has + explicitly requested that you do so. If you need a scratch space, use + the `.auxiliary/scribbles` directory instead of `/tmp`. + # Per-Language Advice ## Python @@ -45,29 +119,23 @@ - Use `TypeAlias` aliases to reuse complex annotations or expose them as part of the public API. -### Comments and Lines +### Quality Assurance + +- Ensure the package imports in interpreter. +- Ensure linters give a clean report. + To run linters, use `hatch --env develop run linters`. +- Ensure tests pass. + To run testers, use `hatch --env develop run testers`. +- Ensure documentation generates without error. + To generate documentation, use `hatch --env develop run docsgen`. + +### Lines -- Do not strip comments from existing code unless directed to do so. -- Do not describe obvious code with comments. Only comment on non-obvious or - complex behaviors. -- Leave TODO comments about uncovered edge cases, tests, and other future work. -- Do not break function bodies with empty lines. - One empty line between attribute blocks and methods on classes. - Two empty lines between attribute blocks, classes, and functions on modules. - Split lines at 79 columns. Use parentheses for continuations and not backslashes. -### Lints and Tests - -- Check your work by linting, ensuring that the package imports, running tests, - and generating documentation. -- Ignore any warnings, unless instructed otherwise. Focus only on errors and - failures. -- To run linters, use `hatch --env develop run linters`. -- To run testers, use `hatch --env develop run testers`. -- To generate documentation, use `hatch --env develop run docsgen`. -- Do not write tests unless explicitly instructed to do so. - # Commits - Use `git status` to ensure that all relevant changes are in the changeset to @@ -79,10 +147,3 @@ line. The name of the author should be your model name. The email address should either be one which you have been designated to use or else a commonly-known no-reply address. - -## Interactive Collaboration on User Terminal - -- Do not commit until you have user approval to do so. -- Add the `--no-gpg-sign` option to the `git commit` command to suppress GPG - passphrase challenges. (These challenges conflict with the alternate console - screen, managed by some CLI agents, resulting in an unusable terminal.) diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index 6860c37..e3ed264 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.20.1 +_commit: v1.24 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald diff --git a/.auxiliary/configuration/gemini/settings.json b/.auxiliary/configuration/gemini/settings.json new file mode 100644 index 0000000..9f48e88 --- /dev/null +++ b/.auxiliary/configuration/gemini/settings.json @@ -0,0 +1,31 @@ +{ + "mcpServers": { + "context7": { + "command": "npx", + "args": [ "-y", "@upstash/context7-mcp" ] + }, + "pyright": { + "command": "mcp-language-server", + "args": [ + "--lsp", + "pyright-langserver", + "--workspace", + ".", + "--", + "--stdio" + ] + }, + "ruff": { + "command": "mcp-language-server", + "args": [ + "--lsp", + "ruff", + "--workspace", + ".", + "--", + "server", + "--preview" + ] + } + } +} diff --git a/.auxiliary/configuration/mcp-servers.json b/.auxiliary/configuration/mcp-servers.json new file mode 100644 index 0000000..6ae002a --- /dev/null +++ b/.auxiliary/configuration/mcp-servers.json @@ -0,0 +1,33 @@ +{ + "mcpServers": { + "pyright": { + "command": "mcp-language-server", + "args": [ + "--lsp", + "pyright-langserver", + "--workspace", + ".", + "--", + "--stdio" + ] + }, + "ruff": { + "command": "mcp-language-server", + "args": [ + "--lsp", + "ruff", + "--workspace", + ".", + "--", + "server", + "--preview" + ] + }, + "text-editor": { + "command": "uvx", + "args": [ + "mcp-text-editor" + ] + } + } +} diff --git a/.auxiliary/configuration/pre-commit.yaml b/.auxiliary/configuration/pre-commit.yaml index 383b03a..0993cb4 100644 --- a/.auxiliary/configuration/pre-commit.yaml +++ b/.auxiliary/configuration/pre-commit.yaml @@ -40,7 +40,7 @@ repos: name: 'Check: Debug Statements (Python)' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.12.1 hooks: - id: ruff name: 'Lint: Ruff' diff --git a/.auxiliary/data/towncrier/+dataclasses.repair.rst b/.auxiliary/data/towncrier/+dataclasses.repair.rst deleted file mode 100644 index 04416f4..0000000 --- a/.auxiliary/data/towncrier/+dataclasses.repair.rst +++ /dev/null @@ -1,4 +0,0 @@ -Standard: Ensure that behaviors tracking attribute is not part of comparisons, -hashes, or ``repr`` calculations for dataclasses. Attributes with "private" -names which resemble the CPython scheme for class-local (non-inheritable) -attributes can create confusion for the internal machinery of ``dataclasses``. diff --git a/.auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst b/.auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst new file mode 100644 index 0000000..f424d6a --- /dev/null +++ b/.auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst @@ -0,0 +1 @@ +Deprecate ``reclassify_modules`` function. Use ``finalize_module`` instead. diff --git a/.auxiliary/data/towncrier/+finalize-module.enhance.rst b/.auxiliary/data/towncrier/+finalize-module.enhance.rst new file mode 100644 index 0000000..ec45c1a --- /dev/null +++ b/.auxiliary/data/towncrier/+finalize-module.enhance.rst @@ -0,0 +1 @@ +Add ``finalize_module`` function to combine Dynadoc docstring generation with module reclassification for immutability and concealment. \ No newline at end of file diff --git a/.auxiliary/data/towncrier/+slots.repair.rst b/.auxiliary/data/towncrier/+slots.repair.rst deleted file mode 100644 index bd1ff6e..0000000 --- a/.auxiliary/data/towncrier/+slots.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Standard: Do not create duplicate slots for behaviors tracking. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 41b4885..4e391d7 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ca952048182954af554a0d68013d8b61724800dea447ae8597fdf1deafb8b08 -size 301680 +oid sha256:576f887ec9be897758f81e1b49ce57be5af14b58cdb32ec33368a5d6db1f423a +size 315200 diff --git a/.auxiliary/scribbles/.gitignore b/.auxiliary/scribbles/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.auxiliary/scribbles/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.gitignore b/.gitignore index 0000eb8..af59065 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .env +.gemini +.mcp.json .*.swp AGENTS.md CLAUDE.md CONVENTIONS.md +GEMINI.md __pycache__/ bugs/ diff --git a/documentation/changelog.rst b/documentation/changelog.rst index cd73410..6062240 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,21 @@ Release Notes .. towncrier release notes start +Classcore 1.6 (2025-07-01) +========================== + +Enhancements +------------ + +- Add ``finalize_module`` function to combine Dynadoc docstring generation with module reclassification for immutability and concealment. + + +Notices +------- + +- Deprecate ``reclassify_modules`` function. Use ``finalize_module`` instead. + + Classcore 1.5.3 (2025-06-30) ============================ diff --git a/documentation/contribution.rst b/documentation/contribution.rst index 0aa517d..2ef1ea7 100644 --- a/documentation/contribution.rst +++ b/documentation/contribution.rst @@ -82,9 +82,9 @@ Artificial Intelligence * A more compact representation of the above guidance and standards, plus some other advice for these models, can be found in ``.auxiliary/configuration/conventions.md``. You may link to this file from a - ``AGENTS.md``, ``CLAUDE.md``, ``CONVENTIONS.md`` file in the root of the - project. These files are ignored by Git as we do not wish to pollute the - root of the project with them in the upstream repository. + ``AGENTS.md``, ``CLAUDE.md``, ``GEMINI.md``, ``CONVENTIONS.md``, etc... file + in the root of the project. These files are ignored by Git as we do not wish + to pollute the root of the project with them in the upstream repository. Resources ------------------------------------------------------------------------------- diff --git a/documentation/examples/std-behaviors.rst b/documentation/examples/std-behaviors.rst index cacee58..61bc51a 100644 --- a/documentation/examples/std-behaviors.rst +++ b/documentation/examples/std-behaviors.rst @@ -99,7 +99,7 @@ on instances of the decorated class. >>> del point.x -Selective Visibility +Selective Mutability ------------------------------------------------------------------------------- Explicit attribute names for selective mutability: diff --git a/documentation/examples/std-modules.rst b/documentation/examples/std-modules.rst index 3bf483a..63b3a63 100644 --- a/documentation/examples/std-modules.rst +++ b/documentation/examples/std-modules.rst @@ -1,3 +1,4 @@ +.. vim: set fileencoding=utf-8: .. -*- coding: utf-8 -*- .. +--------------------------------------------------------------------------+ | | @@ -24,10 +25,262 @@ Standard Modules Introduction =============================================================================== -.. todo:: Contents +The ``standard.modules`` submodule provides functionality to enhance Python +modules with immutability, concealment, and automatic documentation generation. +This is particularly useful for package authors who want to prevent accidental +modification of their module's public API while providing rich documentation. +The module provides two main approaches: -Reclassification +1. **Module reclassification** - converts standard modules to have immutable + and concealed attributes +2. **Module finalization** - combines documentation generation with + reclassification in a single convenient function + + +Module Reclassification +=============================================================================== + +The ``reclassify_modules`` function converts modules to use a custom module +class that provides immutability and concealment behaviors. Here's how you +might use it in a hypothetical package: + +.. code-block:: python + + # mypackage/__init__.py + import classcore.standard as _ccstd + + # Import your submodules + from . import core + from . import utils + from . import exceptions + + # Apply module reclassification + _ccstd.reclassify_modules( __name__, recursive = True ) + +After reclassification, the modules become immutable: + +.. code-block:: python + + # This would raise AttributeImmutability exception + # mypackage.core = "something else" + + # Non-public attributes are concealed from dir() + # dir( mypackage ) # Only shows public attributes + +The ``recursive = True`` parameter ensures that all submodules within the +package hierarchy are also reclassified, providing consistent behavior +throughout your package. + + +Individual Module Reclassification +------------------------------------------------------------------------------- + +You can also reclassify individual modules without affecting the entire +package hierarchy: + +.. code-block:: python + + # mypackage/core.py + import classcore.standard as _ccstd + + def important_function(): + ''' This function should not be accidentally modified. ''' + return "Important result" + + # Reclassify only this module + _ccstd.reclassify_modules( __name__ ) + +This approach is useful when you want fine-grained control over which modules +in your package receive the enhanced behaviors. + + +Module Finalization with Documentation =============================================================================== -.. todo:: Contents +The ``finalize_module`` function provides a convenient way to combine automatic +documentation generation (via Dynadoc integration) with module reclassification. +This is the recommended approach for most packages. + +Basic Usage +------------------------------------------------------------------------------- + +.. code-block:: python + + # mypackage/__init__.py + import classcore.standard as _ccstd + + from . import core + from . import utils + from . import exceptions + + # Define documentation fragments + _fragments = { + 'version': '1.0.0', + 'author': 'Your Name', + 'description': 'A utility package for data processing' + } + + # Finalize the module with documentation and reclassification + _ccstd.finalize_module( + __name__, + dynadoc_table = _fragments, + recursive = True + ) + +The ``finalize_module`` function will: + +1. Generate comprehensive documentation for the module and its members using + Dynadoc introspection +2. Apply the documentation fragments you provide +3. Reclassify the module and its submodules for immutability and concealment + +Advanced Configuration +------------------------------------------------------------------------------- + +For complex packages, you might want to configure different parts differently: + +.. code-block:: python + + # mypackage/__init__.py + import classcore.standard as _ccstd + + # Configure main package with full documentation + _ccstd.finalize_module( + __name__, + dynadoc_table = main_fragments, + recursive = False # Handle submodules individually + ) + + # Configure submodules with different settings + _ccstd.finalize_module( + f"{__name__}.core", + dynadoc_table = core_fragments, + recursive = True + ) + + _ccstd.finalize_module( + f"{__name__}.utils", + dynadoc_table = utils_fragments, + recursive = True + ) + +This approach allows you to provide different documentation fragments and +introspection settings for different parts of your package. + + +Best Practices +=============================================================================== + +Package-Level Application +------------------------------------------------------------------------------- + +For most packages, apply ``finalize_module`` at the package level in your +``__init__.py`` file: + +.. code-block:: python + + # mypackage/__init__.py + import classcore.standard as _ccstd + + # Package metadata and documentation fragments + __version__ = '1.0.0' + _fragments = { + 'version': __version__, + 'maintainer': 'Package Team', + 'license': 'Apache 2.0' + } + + # Import public API + from .core import PublicClass, public_function + from .utils import helper_function + + # Finalize the entire package + _ccstd.finalize_module( + __name__, + dynadoc_table = _fragments, + recursive = True + ) + +This pattern ensures that: + +- Your package's public API is documented +- All modules in the package are immutable and concealed +- Documentation fragments are applied consistently +- The entire package hierarchy is protected from accidental modification + +Documentation Fragments +------------------------------------------------------------------------------- + +Use documentation fragments to provide consistent metadata across your package: + +.. code-block:: python + + # mypackage/_metadata.py + fragments = { + 'version': '1.0.0', + 'author': 'Your Name ', + 'license': 'Apache License 2.0', + 'homepage': 'https://github.com/yourname/mypackage', + 'description': 'A comprehensive data processing library', + 'examples_url': 'https://mypackage.readthedocs.io/examples', + 'api_url': 'https://mypackage.readthedocs.io/api' + } + + # mypackage/__init__.py + from ._metadata import fragments + import classcore.standard as _ccstd + + _ccstd.finalize_module( __name__, dynadoc_table = fragments, recursive = True ) + +This approach centralizes your package metadata and makes it easy to maintain +consistency across documentation. + +Error Handling +------------------------------------------------------------------------------- + +When using module finalization, be aware that the resulting modules will raise +``AttributeImmutability`` exceptions if code attempts to modify them: + +.. code-block:: python + + import classcore.exceptions + + # After finalization, this will raise an exception + try: + mypackage.core.some_function = lambda: "modified" + except classcore.exceptions.AttributeImmutability as e: + print( f"Cannot modify module: {e}" ) + +Design your package APIs to avoid dynamic modification after finalization. +If you need dynamic behavior, consider using configuration objects or factory +functions instead of direct module attribute modification. + + +Integration with Build Systems +=============================================================================== + +Module finalization integrates well with modern Python build systems. The +immutability ensures that your package's API surface is clearly defined and +cannot be accidentally modified at runtime. + +For packages that use entry points or plugin systems, apply finalization after +all dynamic setup is complete: + +.. code-block:: python + + # mypackage/__init__.py + import classcore.standard as _ccstd + + # Dynamic setup (plugin registration, etc.) + _setup_plugins() + _register_entry_points() + + # Final API definition + from .api import * + + # Lock down the package + _ccstd.finalize_module( __name__, dynadoc_table = _fragments, recursive = True ) + +This ensures that your package initialization is complete before the +immutability protections are applied. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 278488c..47d7382 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ year-of-origin = 2025 gha-runners = [ '/home/runner/work/python-classcore/python-classcore/', '/Users/runner/work/python-classcore/python-classcore/', + 'C:\a\python-classcore\python-classcore\', 'D:\a\python-classcore\python-classcore\', ] [tool.coverage.run] @@ -95,9 +96,7 @@ description = ''' Development environment. ''' dependencies = [ 'Jinja2', 'coverage[toml]', - 'emcd-projects', 'furo', - 'icecream-truck', 'packaging', 'pre-commit', 'pyright', diff --git a/sources/classcore/__/imports.py b/sources/classcore/__/imports.py index 6a0694b..abc9e78 100644 --- a/sources/classcore/__/imports.py +++ b/sources/classcore/__/imports.py @@ -18,7 +18,7 @@ #============================================================================# -''' Common imports and type aliases used throughout the package. ''' +''' Common imports used throughout the package. ''' # ruff: noqa: F401 diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 49208c3..122a0e2 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,8 +62,8 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.5.3' +__version__ = '1.6' -standard.dynadoc.assign_module_docstring( __name__, table = __.fragments ) -standard.reclassify_modules( __name__, recursive = True ) +standard.finalize_module( + __name__, dynadoc_table = __.fragments, recursive = True ) diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index 9f5f82c..cb5cbc0 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -24,6 +24,7 @@ from .. import utilities as _utilities from . import __ from . import classes as _classes +from . import dynadoc as _dynadoc from . import nomina as _nomina @@ -31,6 +32,63 @@ class Module( _classes.Object, __.types.ModuleType ): ''' Modules with attributes immutability and concealment. ''' +def finalize_module( # noqa: PLR0913 + module: __.typx.Annotated[ + str | __.types.ModuleType, + __.ddoc.Doc( ''' Module or module name to finalize. ''' ), + ], /, + *fragments: __.ddoc.interfaces.Fragment, + attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, + dynadoc_introspection: _nomina.DynadocIntrospectionArgument = ( + _dynadoc.dynadoc_introspection_on_package ), + dynadoc_table: _nomina.DynadocTableArgument = __.dictproxy_empty, + recursive: __.typx.Annotated[ + bool, __.ddoc.Doc( ''' Recursively reclassify package modules? ''' ) + ] = False, + replacement_class: __.typx.Annotated[ + type[ __.types.ModuleType ], + __.ddoc.Doc( ''' New class for module. ''' ), + ] = Module, +) -> None: + ''' Combines Dynadoc docstring assignment and module reclassification. + + Applies module docstring generation via Dynadoc introspection, + then reclassifies modules for immutability and concealment. + + When recursive is False, automatically excludes module targets from + dynadoc introspection to document only the provided module. When + recursive is True, automatically includes module targets so Dynadoc + can recursively document all modules. + ''' + module_target = __.ddoc.IntrospectionTargets.Module + if recursive: + if not ( dynadoc_introspection.targets & module_target ): + targets = dynadoc_introspection.targets | module_target + introspection = __.ddoc.IntrospectionControl( + enable = dynadoc_introspection.enable, + class_control = dynadoc_introspection.class_control, + module_control = dynadoc_introspection.module_control, + limiters = dynadoc_introspection.limiters, + targets = targets ) + else: introspection = dynadoc_introspection + elif dynadoc_introspection.targets & module_target: + limit = __.ddoc.IntrospectionLimit( + targets_exclusions = module_target ) + introspection = dynadoc_introspection.with_limit( limit ) + else: introspection = dynadoc_introspection + _dynadoc.assign_module_docstring( + module, + *fragments, + introspection = introspection, + table = dynadoc_table ) + reclassify_modules( + module, + attributes_namer = attributes_namer, + recursive = recursive, + replacement_class = replacement_class ) + + +@__.typx.deprecated( "Use 'finalize_module' instead." ) def reclassify_modules( attributes: __.typx.Annotated[ __.cabc.Mapping[ str, __.typx.Any ] | __.types.ModuleType | str, diff --git a/tests/test_000_classcore/test_350_standard_modules.py b/tests/test_000_classcore/test_350_standard_modules.py index 35d9016..b68cc40 100644 --- a/tests/test_000_classcore/test_350_standard_modules.py +++ b/tests/test_000_classcore/test_350_standard_modules.py @@ -86,3 +86,95 @@ def test_205_reclassification_via_module_globals( ): assert module_.__class__ is module_class with pytest.raises( exceptions_module.AttributeImmutability ): module.foo = 1 + + +def test_210_finalize_module_basic( ): + ''' Finalizes module with default parameters. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + module_class = module.Module + module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) + module_.__package__ = 'fakepackage' + assert module_.__class__ is not module_class + module.finalize_module( module_ ) + assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module_.foo = 1 + + +def test_211_finalize_module_recursive_with_module_targets( ): + ''' Finalizes module recursively when introspection has Module targets. ''' + module = cache_import_module( MODULE_QNAME ) + dynadoc_module = cache_import_module( f"{PACKAGE_NAME}.standard.dynadoc" ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + module_class = module.Module + module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) + module_.__package__ = 'fakepackage' + introspection_with_module = dynadoc_module.dynadoc_introspection_on_package + assert module_.__class__ is not module_class + module.finalize_module( + module_, + dynadoc_introspection = introspection_with_module, + recursive = True ) + assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module_.foo = 1 + + +def test_212_finalize_module_recursive_without_module_targets( ): + ''' Finalizes module recursively when introspection lacks targets. ''' + module = cache_import_module( MODULE_QNAME ) + dynadoc_module = cache_import_module( f"{PACKAGE_NAME}.standard.dynadoc" ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + module_class = module.Module + module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) + module_.__package__ = 'fakepackage' + introspection_without_module = ( + dynadoc_module.dynadoc_introspection_on_class ) + assert module_.__class__ is not module_class + module.finalize_module( + module_, + dynadoc_introspection = introspection_without_module, + recursive = True ) + assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module_.foo = 1 + + +def test_213_finalize_module_nonrecursive_with_module_targets( ): + ''' Finalizes module non-recursively when introspection has targets. ''' + module = cache_import_module( MODULE_QNAME ) + dynadoc_module = cache_import_module( f"{PACKAGE_NAME}.standard.dynadoc" ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + module_class = module.Module + module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) + module_.__package__ = 'fakepackage' + introspection_with_module = dynadoc_module.dynadoc_introspection_on_package + assert module_.__class__ is not module_class + module.finalize_module( + module_, + dynadoc_introspection = introspection_with_module, + recursive = False ) + assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module_.foo = 1 + + +def test_214_finalize_module_nonrecursive_without_module_targets( ): + ''' Finalizes module non-recursively when introspection lacks targets. ''' + module = cache_import_module( MODULE_QNAME ) + dynadoc_module = cache_import_module( f"{PACKAGE_NAME}.standard.dynadoc" ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + module_class = module.Module + module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) + module_.__package__ = 'fakepackage' + introspection_without_module = ( + dynadoc_module.dynadoc_introspection_on_class ) + assert module_.__class__ is not module_class + module.finalize_module( + module_, + dynadoc_introspection = introspection_without_module, + recursive = False ) + assert module_.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + module_.foo = 1 From 9e4ca3c4e75ec15524aa40026376e23f09c739a0 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Wed, 2 Jul 2025 03:54:16 +0000 Subject: [PATCH 19/28] Update documents for publication. --- .../+deprecate-reclassify-modules.notify.rst | 1 - .../+deprecation-warnings-fix.repair.rst | 1 + .../towncrier/+finalize-module.enhance.rst | 1 - .auxiliary/publications/website.tar.xz | 4 +- documentation/changelog.rst | 9 ++++ documentation/conf.py | 4 -- sources/classcore/__init__.py | 2 +- sources/classcore/standard/modules.py | 45 +++++++++++++++++-- .../test_350_standard_modules.py | 35 +++++++++------ 9 files changed, 77 insertions(+), 25 deletions(-) delete mode 100644 .auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst create mode 100644 .auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst delete mode 100644 .auxiliary/data/towncrier/+finalize-module.enhance.rst diff --git a/.auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst b/.auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst deleted file mode 100644 index f424d6a..0000000 --- a/.auxiliary/data/towncrier/+deprecate-reclassify-modules.notify.rst +++ /dev/null @@ -1 +0,0 @@ -Deprecate ``reclassify_modules`` function. Use ``finalize_module`` instead. diff --git a/.auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst b/.auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst new file mode 100644 index 0000000..270e99d --- /dev/null +++ b/.auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst @@ -0,0 +1 @@ +Fix deprecation warnings from finalize_module by refactoring to use private _reclassify_module implementation. \ No newline at end of file diff --git a/.auxiliary/data/towncrier/+finalize-module.enhance.rst b/.auxiliary/data/towncrier/+finalize-module.enhance.rst deleted file mode 100644 index ec45c1a..0000000 --- a/.auxiliary/data/towncrier/+finalize-module.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``finalize_module`` function to combine Dynadoc docstring generation with module reclassification for immutability and concealment. \ No newline at end of file diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 4e391d7..c4e3166 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:576f887ec9be897758f81e1b49ce57be5af14b58cdb32ec33368a5d6db1f423a -size 315200 +oid sha256:77f1800cb3dc9679ce6ca8171348b50d4ff2603c6dc8ff94f63770a4917872be +size 319528 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 6062240..8237b51 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,15 @@ Release Notes .. towncrier release notes start +Classcore 1.6.1 (2025-07-01) +============================ + +Repairs +------- + +- Fix deprecation warnings from finalize_module by refactoring to use private _reclassify_module implementation. + + Classcore 1.6 (2025-07-01) ========================== diff --git a/documentation/conf.py b/documentation/conf.py index d1a62a5..8a215b6 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -77,15 +77,11 @@ def _import_version( ): ( 'py:class', "v, remove specified key and return the corresponding value." ), # Type annotation weirdnesses. - ( 'py:class', "Doc" ), ( 'py:class', "builtins.NotImplementedType" ), ( 'py:class', "classcore.__.T" ), ( 'py:class', "classcore.__.U" ), - ( 'py:class', "classcore.standard.classes.Object" ), - ( 'py:class', "collections.abc.Annotated" ), ( 'py:class', "dynadoc.context.Context" ), ( 'py:class', "dynadoc.context.IntrospectionControl" ), - ( 'py:class', "types.Annotated" ), ( 'py:class', "typing_extensions._ProtocolMeta" ), ( 'py:class', "typing_extensions.Any" ), ] diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 122a0e2..48fb719 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.6' +__version__ = '1.6.1' standard.finalize_module( diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index cb5cbc0..e950c1c 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -81,7 +81,7 @@ def finalize_module( # noqa: PLR0913 *fragments, introspection = introspection, table = dynadoc_table ) - reclassify_modules( + _reclassify_module( module, attributes_namer = attributes_namer, recursive = recursive, @@ -108,7 +108,6 @@ def reclassify_modules( __.ddoc.Doc( ''' New class for module. ''' ), ] = Module, ) -> None: - # TODO? Ensure correct operation with namespace packages. ''' Reclassifies modules to have attributes concealment and immutability. Can operate on individual modules or entire package hierarchies. @@ -121,6 +120,46 @@ def reclassify_modules( Has no effect on already-reclassified modules. ''' + _reclassify_module( + attributes, + attributes_namer = attributes_namer, + recursive = recursive, + replacement_class = replacement_class ) + + +def _reclassify_module( + attributes: __.typx.Annotated[ + __.cabc.Mapping[ str, __.typx.Any ] | __.types.ModuleType | str, + __.ddoc.Doc( + ''' Module, module name, or dictionary of object attributes. ''' ), + ], /, *, + attributes_namer: __.typx.Annotated[ + _nomina.AttributesNamer, + __.ddoc.Doc( + ''' Attributes namer function with which to seal class. ''' ), + ] = __.calculate_attrname, + recursive: __.typx.Annotated[ + bool, __.ddoc.Doc( ''' Recursively reclassify package modules? ''' ) + ] = False, + replacement_class: __.typx.Annotated[ + type[ __.types.ModuleType ], + __.ddoc.Doc( ''' New class for module. ''' ), + ] = Module, +) -> None: + # TODO? Ensure correct operation with namespace packages. + ''' Core implementation for module reclassification. + + Reclassifies modules to have attributes concealment and immutability. + Can operate on individual modules or entire package hierarchies. + + Only converts modules within the same package to prevent unintended + modifications to external modules. + + When used with a dictionary, converts any module objects found as + values if they belong to the same package. + + Has no effect on already-reclassified modules. + ''' if isinstance( attributes, str ): attributes = __.sys.modules[ attributes ] if isinstance( attributes, __.types.ModuleType ): @@ -134,7 +173,7 @@ def reclassify_modules( if not __.inspect.ismodule( value ): continue if not value.__name__.startswith( f"{package_name}." ): continue if recursive: - reclassify_modules( + _reclassify_module( value, attributes_namer = attributes_namer, recursive = True, diff --git a/tests/test_000_classcore/test_350_standard_modules.py b/tests/test_000_classcore/test_350_standard_modules.py index b68cc40..3ab4dcf 100644 --- a/tests/test_000_classcore/test_350_standard_modules.py +++ b/tests/test_000_classcore/test_350_standard_modules.py @@ -19,6 +19,7 @@ import types +import warnings import pytest @@ -36,10 +37,12 @@ def test_200_reclassification_of_package_module( ): module_ = types.ModuleType( 'foobarnotreal' ) module_.__package__ = None assert module_.__class__ is not module_class - module.reclassify_modules( module_ ) - assert module_.__class__ is module_class - module.reclassify_modules( module_ ) # idempotence - assert module_.__class__ is module_class + with warnings.catch_warnings( ): + warnings.simplefilter( 'ignore', DeprecationWarning ) + module.reclassify_modules( module_ ) + assert module_.__class__ is module_class + module.reclassify_modules( module_ ) # idempotence + assert module_.__class__ is module_class with pytest.raises( exceptions_module.AttributeImmutability ): module.foo = 1 @@ -52,10 +55,12 @@ def test_201_reclassification_of_normal_module( ): module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) module_.__package__ = 'fakepackage' assert module_.__class__ is not module_class - module.reclassify_modules( module_ ) - assert module_.__class__ is module_class - module.reclassify_modules( module_ ) # idempotence - assert module_.__class__ is module_class + with warnings.catch_warnings( ): + warnings.simplefilter( 'ignore', DeprecationWarning ) + module.reclassify_modules( module_ ) + assert module_.__class__ is module_class + module.reclassify_modules( module_ ) # idempotence + assert module_.__class__ is module_class with pytest.raises( exceptions_module.AttributeImmutability ): module.foo = 1 @@ -68,7 +73,9 @@ def test_202_reclassification_of_incomplete_module( ): module_.__package__ = None del module_.__name__ assert module_.__class__ is not module_class - module.reclassify_modules( module_ ) + with warnings.catch_warnings( ): + warnings.simplefilter( 'ignore', DeprecationWarning ) + module.reclassify_modules( module_ ) assert module_.__class__ is not module_class @@ -80,10 +87,12 @@ def test_205_reclassification_via_module_globals( ): module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) module_dict = { 'mod': module_, '__package__': 'fakepackage' } assert module_.__class__ is not module_class - module.reclassify_modules( module_dict ) - assert module_.__class__ is module_class - module.reclassify_modules( module_dict ) # idempotence - assert module_.__class__ is module_class + with warnings.catch_warnings( ): + warnings.simplefilter( 'ignore', DeprecationWarning ) + module.reclassify_modules( module_dict ) + assert module_.__class__ is module_class + module.reclassify_modules( module_dict ) # idempotence + assert module_.__class__ is module_class with pytest.raises( exceptions_module.AttributeImmutability ): module.foo = 1 From 87a39a75fa13f01c9c2aa9c506ed1cf2fe678943 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Tue, 8 Jul 2025 23:43:15 +0000 Subject: [PATCH 20/28] Update documents for publication. (1.7) --- .auxiliary/configuration/claude/.gitignore | 1 + .../claude/commands/release-final.md | 173 ++++++++++++++ .../claude/commands/release-patch.md | 207 +++++++++++++++++ .../claude/commands/validate-custom-slash.md | 41 ++++ .auxiliary/configuration/claude/settings.json | 5 + .auxiliary/configuration/copier-answers.yaml | 2 +- .../+deprecation-warnings-fix.repair.rst | 1 - .../data/towncrier/+newinit.enhance.rst | 8 + .auxiliary/notes/release-aar.rst | 219 ++++++++++++++++++ .auxiliary/publications/website.tar.xz | 4 +- .gitignore | 1 + README.rst | 38 ++- documentation/changelog.rst | 16 ++ documentation/contribution.rst | 26 ++- documentation/examples/std-classes.rst | 28 +++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/behaviors.py | 4 + sources/classcore/standard/classes.py | 1 + sources/classcore/standard/decorators.py | 115 +++++++-- .../test_340_standard_classes.py | 48 ++++ 20 files changed, 899 insertions(+), 41 deletions(-) create mode 100644 .auxiliary/configuration/claude/.gitignore create mode 100644 .auxiliary/configuration/claude/commands/release-final.md create mode 100644 .auxiliary/configuration/claude/commands/release-patch.md create mode 100644 .auxiliary/configuration/claude/commands/validate-custom-slash.md create mode 100644 .auxiliary/configuration/claude/settings.json delete mode 100644 .auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst create mode 100644 .auxiliary/data/towncrier/+newinit.enhance.rst create mode 100644 .auxiliary/notes/release-aar.rst create mode 100644 tests/test_000_classcore/test_340_standard_classes.py diff --git a/.auxiliary/configuration/claude/.gitignore b/.auxiliary/configuration/claude/.gitignore new file mode 100644 index 0000000..93c0f73 --- /dev/null +++ b/.auxiliary/configuration/claude/.gitignore @@ -0,0 +1 @@ +settings.local.json diff --git a/.auxiliary/configuration/claude/commands/release-final.md b/.auxiliary/configuration/claude/commands/release-final.md new file mode 100644 index 0000000..7132bc2 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/release-final.md @@ -0,0 +1,173 @@ +--- +allowed-tools: Bash(git status), Bash(git pull:*), Bash(git checkout:*), Bash(git add:*), Bash(git commit:*), Bash(git tag:*), Bash(git rm:*), Bash(git cherry-pick:*), Bash(git log:*), Bash(git branch:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), LS, Read +description: Execute automated final release with QA monitoring and development cycle setup +--- + +# Release Final + +**NOTE: This is an experimental workflow! If anything seems unclear or missing, +please stop for consultation with the user.** + +For execution of a fully-automated final release. + +Below is a validated process to create a final release with automated +monitoring and next development cycle setup. + +Target release version: `$ARGUMENTS` (e.g., `1.6`, `2.0`) + +**CRITICAL**: Verify exactly one target release version provided. +**HALT if**: +- No target release version is provided +- Multiple release versions provided (e.g., `1.6 foo bar`) +- Release version format doesn't match `X.Y` pattern (e.g., `1.6.2`, `1.6a0`) + +## Context + +- Current git status: !`git status` +- Current branch: !`git branch --show-current` +- Current version: !`hatch version` +- Recent commits: !`git log --oneline -10` +- Available towncrier fragments: !`ls .auxiliary/data/towncrier/*.rst 2>/dev/null || echo "No fragments found"` +- Target release branch status: !`git branch -r | grep release-$ARGUMENTS || echo "Release branch not found - will create new"` +- Local release branch status: !`git branch | grep release-$ARGUMENTS || echo "No local release branch"` + +## Prerequisites + +Before starting, ensure: +- GitHub CLI (`gh`) is installed and authenticated +- For new releases: All changes are committed to `master` branch +- For existing release branches: Release candidate has been validated and tested +- Working directory is clean with no uncommitted changes +- Towncrier news fragments are present for the release enhancements + +## Process Summary + +Key functional areas of the process: + +1. **Branch Setup**: Create new release branch or checkout existing one +2. **Version Bump**: Set version to final release (major/minor/patch as appropriate) +3. **Update Changelog**: Run Towncrier to build final changelog +4. **QA Monitoring**: Push commits and monitor QA workflow with GitHub CLI +5. **Tag Release**: Create signed git tag after QA passes +6. **Release Monitoring**: Monitor release workflow deployment +7. **Cleanup**: Remove news fragments and cherry-pick back to master +8. **Next Development Cycle**: Set up master branch for next development version + +## Safety Requirements + +**CRITICAL**: You MUST halt the process and consult with the user if ANY of the +following occur: + +- **Step failures**: If any command fails, git operation errors, or tests fail +- **Workflow failures**: If QA or release workflows show failed jobs +- **Unexpected output**: If commands produce unclear or concerning results +- **Version conflicts**: If version bumps don't match expected patterns +- **Network issues**: If GitHub operations timeout or fail repeatedly + +**Your responsibilities**: +- Validate each step succeeds before proceeding to the next +- Monitor workflow status and halt on any failures +- Provide clear progress updates throughout the process +- Maintain clean git hygiene and proper branching +- Use your judgment to assess when manual intervention is needed + +## Release Process + +Execute the following steps for target version `$ARGUMENTS`: + +### 1. Pre-Release Quality Check +Run local quality assurance to catch issues early: +```bash +git status && git pull origin master +hatch --env develop run linters +hatch --env develop run testers +hatch --env develop run docsgen +``` + +### 2. Release Branch Setup +Determine release branch name from target version (e.g., `1.6` → `release-1.6`). + +**If release branch exists** (for RC→final conversion): +```bash +git checkout release-$ARGUMENTS +git pull origin release-$ARGUMENTS +``` + +**If creating new release branch**: +```bash +git checkout master && git pull origin master +git checkout -b release-$ARGUMENTS +``` + +### 3. Version Management +Set version to target release version: +```bash +hatch version $ARGUMENTS +git commit -am "Version: $(hatch version)" +``` + +### 4. Changelog Generation +```bash +hatch --env develop run towncrier build --keep --version $(hatch version) +git commit -am "Update changelog for v$(hatch version) release." +``` + +### 5. Quality Assurance Phase +Push branch and monitor QA workflow: +```bash +# Use -u flag for new branches, omit for existing +git push [-u] origin release-$ARGUMENTS + +# Monitor QA workflow - get run ID from output +gh run list --workflow=qa --limit=1 +gh run watch --interval 30 --compact +``` +**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** +- Monitor QA workflow with `gh run watch` +- Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands +- If command times out, immediately rerun `gh run watch` until completion +- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" +- HALT if any jobs fail - consult user before proceeding + +### 6. Release Deployment +**Verify QA passed before proceeding to release tag:** +```bash +git tag -m "Release v$(hatch version): ." v$(hatch version) +git push --tags + +gh run list --workflow=release --limit=1 +gh run watch --interval 30 --compact +``` +**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** +- Monitor release workflow with `gh run watch` +- Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands +- If command times out, immediately rerun `gh run watch` until completion +- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" +- HALT if any jobs fail - consult user before proceeding + +### 7. Post-Release Cleanup +```bash +git rm .auxiliary/data/towncrier/*.rst +git commit -m "Clean up news fragments." +git push origin release-$ARGUMENTS +``` + +### 8. Master Branch Integration +Cherry-pick release commits back to master: +```bash +git checkout master && git pull origin master +git cherry-pick +git cherry-pick +git push origin master +``` + +### 9. Next Development Cycle (Major/Minor Releases Only) +Set up next development version: +```bash +hatch version minor,alpha +git commit -am "Version: $(hatch version)" +git tag -m "Start development for v$(hatch version | sed 's/a[0-9]*$//')." i$(hatch version | sed 's/a[0-9]*$//') +git push origin master --tags +``` + +**Note**: Use `git log --oneline` to identify commit hashes for cherry-picking. diff --git a/.auxiliary/configuration/claude/commands/release-patch.md b/.auxiliary/configuration/claude/commands/release-patch.md new file mode 100644 index 0000000..8699d41 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/release-patch.md @@ -0,0 +1,207 @@ +--- +allowed-tools: Bash(git status), Bash(git pull:*), Bash(git checkout:*), Bash(git commit:*), Bash(git tag:*), Bash(git rm:*), Bash(git cherry-pick:*), Bash(git log:*), Bash(git branch:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), LS, Read +description: Execute automated patch release with QA monitoring and master integration +--- + +# Release Patch + +**NOTE: This is an experimental workflow! If anything seems unclear or missing, +please stop for consultation with the user.** + +For execution of a fully-automated postrelease patch. + +Below is a validated process to create patch releases with automated monitoring +and clean integration back to master. + +Target release version: `$ARGUMENTS` (e.g., `1.24`, `2.3`) + +**CRITICAL**: Verify exactly one target release version provided. +**HALT if**: +- No target release version is provided +- Multiple release versions provided (e.g., `1.6 foo bar`) +- Release version format doesn't match `X.Y` pattern (e.g., `1.6.2`, `1.6a0`) + +## Context + +- Current git status: !`git status` +- Current branch: !`git branch --show-current` +- Current version: !`hatch version` +- Recent commits: !`git log --oneline -10` +- Available towncrier fragments: !`ls .auxiliary/data/towncrier/*.rst 2>/dev/null || echo "No fragments found"` +- Target release branch status: !`git branch -r | grep release-$ARGUMENTS || echo "Release branch not found"` + +## Prerequisites + +Before running this command, ensure: +- GitHub CLI (`gh`) is installed and authenticated +- Release branch exists for the target version (e.g., `release-1.24` for version `1.24`) +- Working directory is clean with no uncommitted changes +- Towncrier news fragments are present for the patch changes + +## Process Summary + +Key functional areas of the process: + +1. **Branch Setup**: Checkout and update the appropriate release branch +2. **Version Bump**: Increment to next patch version with `hatch version patch` +3. **Update Changelog**: Run Towncrier to build patch changelog +4. **QA Monitoring**: Push commits and monitor QA workflow with GitHub CLI +5. **Tag Release**: Create signed git tag after QA passes +6. **Release Monitoring**: Monitor release workflow deployment +7. **Cleanup**: Remove news fragments and cherry-pick back to master + +## Safety Requirements + +**CRITICAL**: You MUST halt the process and consult with the user if ANY of the following occur: + +- **Step failures**: If any command fails, git operation errors, or tests fail +- **Workflow failures**: If QA or release workflows show failed jobs +- **Version conflicts**: If patch version doesn't match expected patterns +- **Branch issues**: If release branch doesn't exist or is in unexpected state +- **Network issues**: If GitHub operations timeout or fail repeatedly + +**Your responsibilities**: +- Validate each step succeeds before proceeding to the next +- Monitor workflow status and halt on any failures +- Provide clear progress updates throughout the process +- Maintain clean git hygiene and proper branching +- Use your judgment to assess when manual intervention is needed + +## Release Process + +Execute the following steps for target release version `$ARGUMENTS`: + +### 1. Pre-Release Quality Check +Run local quality assurance to catch issues early: +```bash +git status && git pull origin master +hatch --env develop run linters +hatch --env develop run testers +hatch --env develop run docsgen +``` + +### 2. Release Branch Setup +Checkout the target release branch: +```bash +git checkout release-$ARGUMENTS +git pull origin release-$ARGUMENTS +``` + +### 3. Patch Integration +**Determine patch location and integrate if needed:** + +### 3.1. Identify Patch Commits +Before cherry-picking, identify which commits contain actual patch fixes vs. maintenance: + +```bash +git log --oneline master +git log --graph --oneline master --since="1 month ago" +# Show commits on master not on release branch +git log --oneline release-$ARGUMENTS..master --since="1 month ago" +``` + +**Patch commits** (always cherry-pick): +- Bug fixes +- Security patches +- Critical functionality fixes + +**Maintenance commits** (evaluate case-by-case): +- Template updates +- Dependency bumps +- Documentation changes + +Use `git show ` to review each commit's content before deciding. + +**If patches were developed on master** (cherry-pick to release branch): +```bash +# Cherry-pick patch commits from master to release branch +# Use git log --oneline master to identify relevant commit hashes +git cherry-pick +git cherry-pick +# Repeat for all patch commits +``` + +**If patches were developed on release branch**: Skip this step - patches are already present. + +### 4. Pre-Release Validation +Run linting to catch issues before formal release process: +```bash +hatch --env develop run linters +``` +**HALT if any linting errors** - fix issues before proceeding. + +### 5. Version Management +Increment to next patch version: +```bash +hatch version patch +git commit -am "Version: $(hatch version)" +``` + +### 6. Changelog Generation +```bash +hatch --env develop run towncrier build --keep --version $(hatch version) +git commit -am "Update changelog for v$(hatch version) patch release." +``` + +### 7. Quality Assurance Phase +Push branch and monitor QA workflow: +```bash +git push origin release-$ARGUMENTS + +# Monitor QA workflow - get run ID from output +gh run list --workflow=qa --limit=1 +gh run watch --interval 30 --compact +``` +**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** +- Monitor QA workflow with `gh run watch` +- Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands +- If command times out, immediately rerun `gh run watch` until completion +- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" +- HALT if any jobs fail - consult user before proceeding + +### 8. Release Deployment +**Verify QA passed before proceeding to release tag:** +```bash +git tag -m "Release v$(hatch version) patch: ." v$(hatch version) +git push --tags + +gh run list --workflow=release --limit=1 +gh run watch --interval 30 --compact +``` +**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** +- Monitor release workflow with `gh run watch` +- Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands +- If command times out, immediately rerun `gh run watch` until completion +- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" +- HALT if any jobs fail - consult user before proceeding + +### 9. Post-Release Cleanup +```bash +git rm .auxiliary/data/towncrier/*.rst +git commit -m "Clean up news fragments." +git push origin release-$ARGUMENTS +``` + +### 10. Master Branch Integration +Cherry-pick commits back to master based on patch development location: + +**If patches were developed on master**: Cherry-pick changelog and cleanup commits: +```bash +git checkout master && git pull origin master +git cherry-pick +git cherry-pick +git push origin master +``` + +**If patches were developed on release branch**: Cherry-pick patch, changelog, and cleanup commits: +```bash +git checkout master && git pull origin master +git cherry-pick +git cherry-pick +# Repeat for all patch commits +git cherry-pick +git cherry-pick +git push origin master +``` + +**Note**: Use `git log --oneline` to identify commit hashes for cherry-picking. diff --git a/.auxiliary/configuration/claude/commands/validate-custom-slash.md b/.auxiliary/configuration/claude/commands/validate-custom-slash.md new file mode 100644 index 0000000..b6bffae --- /dev/null +++ b/.auxiliary/configuration/claude/commands/validate-custom-slash.md @@ -0,0 +1,41 @@ +--- +allowed-tools: Bash(git status), Bash(git branch:*), Bash(git log:*), Bash(hatch version:*), Bash(echo:*), Bash(ls:*), Bash(pwd), LS, Read +description: Validate custom slash command functionality with context and permissions +--- + +# Validate Custom Slash Command + +Test script to validate custom slash command functionality, permissions, and context interpolation. + +Test argument: `$ARGUMENTS` + +## Context + +- Current directory: !`pwd` +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` +- Current version: !`hatch version` +- Recent commits: !`git log --oneline -5` +- Template files: !`ls template/.auxiliary/configuration/claude/commands/` + +## Validation Tasks + +1. **Report the test argument**: Look at the "Test argument:" line above and tell me what value you see there +2. **Test basic git commands**: Run `git status` and `git branch --show-current` +3. **Test hatch command**: Run `hatch version` +4. **Test file operations**: Use LS tool to list current directory contents +5. **Test restricted command**: Attempt `git push` (should be blocked and require approval) + +## Expected Results + +- Context should be populated with current state +- Allowed commands should execute successfully +- `git push` should be blocked + +## Your Task + +Execute the validation tasks above and provide a summary report including: +- The interpolated argument value you see on the "Test argument:" line +- Results of each allowed command +- Confirmation that restricted commands are properly blocked +- Any observations about the command execution experience diff --git a/.auxiliary/configuration/claude/settings.json b/.auxiliary/configuration/claude/settings.json new file mode 100644 index 0000000..9030ceb --- /dev/null +++ b/.auxiliary/configuration/claude/settings.json @@ -0,0 +1,5 @@ +{ + "env": { + "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": 1 + } +} diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index e3ed264..501a084 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.24 +_commit: v1.27 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald diff --git a/.auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst b/.auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst deleted file mode 100644 index 270e99d..0000000 --- a/.auxiliary/data/towncrier/+deprecation-warnings-fix.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Fix deprecation warnings from finalize_module by refactoring to use private _reclassify_module implementation. \ No newline at end of file diff --git a/.auxiliary/data/towncrier/+newinit.enhance.rst b/.auxiliary/data/towncrier/+newinit.enhance.rst new file mode 100644 index 0000000..3f768ff --- /dev/null +++ b/.auxiliary/data/towncrier/+newinit.enhance.rst @@ -0,0 +1,8 @@ +Standard: Add ``ignore_init_arguments`` decorator argument and +``instances_ignore_init_arguments`` class argument to support cases, such as +inheritance from ``urllib.parse.ParseResult`` which inherits from ``tuple`` and +overrides ``__new__`` instead of ``__init__``. In such cases, ``__new__`` +processes the instance production arguments rather than ``__init__``. However, +the standard Python behavior is to present the arguments to both ``__new__`` +and ``__init__``, which is problematic since we always provide an ``__init__`` +head. diff --git a/.auxiliary/notes/release-aar.rst b/.auxiliary/notes/release-aar.rst new file mode 100644 index 0000000..3b1250f --- /dev/null +++ b/.auxiliary/notes/release-aar.rst @@ -0,0 +1,219 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- + +******************************************************************************* +LLM-Guided Release After Action Report (AAR) +******************************************************************************* + +:Date: 2025-07-02 +:Release: v1.6.1 Postrelease Patch +:LLM: Claude (Sonnet 4) +:Duration: ~45 minutes total +:Status: **COMPLETE SUCCESS** ✅ + +Executive Summary +=============================================================================== + +Successfully executed the first fully LLM-guided postrelease patch using +GitHub CLI monitoring and automation. The release process was completed +autonomously with real-time workflow monitoring, resulting in a clean +deployment of v1.6.1 with deprecation warning fixes. + +**Key Achievement**: Pioneered LLM-guided releases with GitHub Actions monitoring. + +Mission Objectives +=============================================================================== + +**Primary**: Apply deprecation warning fixes to v1.6 → v1.6.1 patch release +**Secondary**: Validate LLM capability for release automation +**Tertiary**: Document process for future LLM-guided releases + +✅ All objectives achieved + +Technical Changes Delivered +=============================================================================== + +1. **Deprecation Warning Elimination** + - Refactored ``finalize_module`` to use private ``_reclassify_module`` + - Eliminated warnings from calling deprecated ``reclassify_modules`` + - Added test warning suppression for clean output + +2. **Documentation Cleanup** + - Reduced Sphinx warnings from ~122 to 2 + - Cleaned ``nitpick_ignore`` list (11 → 7 entries) + - Removed unused type annotation suppressions + +3. **Release Infrastructure** + - Added Towncrier news fragment + - Generated proper changelog + - Maintained clean git history + +Process Execution +=============================================================================== + +**Branch Strategy** +- Cherry-picked fixes from ``master`` to ``release-1.6`` +- Applied: 3063e6c, 3e2e5a5, b72125d +- Version bump: 1.6 → 1.6.1 +- Clean cherry-pick back to master + +**Workflow Monitoring** +- Used ``gh run watch --interval 30`` for rate-limited monitoring +- Two-phase validation: QA workflow → Release workflow +- Real-time status updates every 30 seconds +- Total CI/CD time: ~6 minutes + +**Release Pipeline Success** +- ✅ All 14 test matrix jobs (3 platforms × 4+ Python versions) +- ✅ Linting, documentation generation, packaging +- ✅ PyPI publication with digital attestations +- ✅ GitHub release creation +- ✅ Documentation deployment + +Key Learnings & Best Practices +=============================================================================== + +**GitHub CLI Monitoring** +- ``gh run watch --interval 30`` provides optimal rate limiting +- **Future improvement**: Use ``--compact`` flag to reduce token usage +- Timeout handling: Re-issue watch commands if shell times out +- Status validation before proceeding to next phase + +**Release Process Improvements** +- Push commits first, monitor QA, then tag (better than original docs) +- Use ``git push --tags`` instead of specific tag push +- Sign tags with ``-m `` for proper metadata +- Separate QA validation from release deployment + +**LLM Automation Insights** +- Real-time monitoring works excellently with 30s intervals +- Error handling: Halt on any failures for human consultation +- Status reporting: Provide clear progress updates +- Context management: Track multiple workflow phases + +**Pre-commit Hook Integration** +- Local validation before GitHub workflows +- All checks passed: Ruff, Pyright, Coverage, Documentation +- Reduced remote CI load + +Technical Metrics +=============================================================================== + +**Workflow Performance** +- QA Workflow: ~5 minutes (16015567174) +- Release Workflow: ~6 minutes (16015632051) +- Documentation: 1m13s generation + 8s publish +- PyPI Deployment: 16s + digital attestations +- GitHub Release: 34s + +**Test Coverage** +- 14 test matrix jobs: All passed +- Platforms: Ubuntu, macOS, Windows +- Python versions: 3.10, 3.11, 3.12, 3.13, PyPy 3.10 +- Coverage: 100% maintained + +**Git Operations** +- 3 cherry-picks: Clean application +- 5 commits total: Version, changelog, cleanup +- 2 cherry-picks back to master +- Clean history maintained + +Issues & Resolutions +=============================================================================== + +**None encountered** - Process executed flawlessly + +**Near-misses prevented**: +- Used signed tags (``-m`` flag) as required +- Applied ``--compact`` flag suggestion for future efficiency +- Proper timeout handling for long-running workflows + +Future Recommendations +=============================================================================== + +**Process Refinements** +1. **Always use** ``gh run watch --compact --interval 30`` for monitoring +2. **Document**: Two-phase workflow (QA → Release) in release instructions +3. **Automate**: Consider webhook-based notifications for very long workflows +4. **Standardize**: This process for all future patch releases + +**LLM Automation Guidelines** +1. **Halt immediately** on any unexpected errors or failures +2. **Validate each phase** before proceeding to next step +3. **Provide clear status** reporting throughout process +4. **Maintain git hygiene** with proper commit messages and history + +**Tooling Improvements** +1. Update release documentation to reflect improved process +2. Consider GitHub CLI automation scripts for common operations +3. Investigate webhook integrations for very long workflows + +**Training Data for Future LLMs** +- This AAR serves as training data for future LLM-guided releases +- Process is now validated and can be replicated +- Monitoring techniques are proven effective + +Final Instructions Executed +=============================================================================== + +.. code-block:: bash + + # 1. Checkout and Prepare Release Branch + git checkout release-1.6 + git pull origin release-1.6 + + # 2. Cherry-pick Patch Commits from Master + git cherry-pick 3063e6c # Refactor finalize_module + git cherry-pick 3e2e5a5 # Clean up Sphinx nitpick_ignore + git cherry-pick b72125d # Add Towncrier entry + + # 3. Bump to Patch Version + hatch version patch + git add . && git commit -m "Bump version to $(hatch version)." + + # 4. Run Towncrier to Build Changelog + hatch --env develop run towncrier build --keep --version $(hatch version) + git add . && git commit -m "Update changelog for v$(hatch version) patch release." + + # 5. Push Commits and Monitor QA + git push origin release-1.6 + gh run list --workflow=qa --limit=1 + gh run watch --interval 30 --compact + + # 6. Tag the Patch Release (After QA Passes) + git tag -m "Release v$(hatch version) patch: Fix deprecation warnings from finalize_module." v$(hatch version) + git push --tags + + # 7. Monitor Release Workflow + gh run list --workflow=release --limit=1 + gh run watch --interval 30 --compact + + # 8. Clean Up News Fragments (After Release Completes) + git rm .auxiliary/data/towncrier/*.rst + git commit -m "Clean up news fragments." + git push origin release-1.6 + + # 9. Cherry-pick Back to Master + git checkout master + git pull origin master + git cherry-pick + git cherry-pick + git push origin master + +Conclusion +=============================================================================== + +**This experiment was a resounding success.** LLM-guided releases are not only +possible but highly effective when properly structured with: + +- Clear monitoring strategies using GitHub CLI +- Proper error handling and halt conditions +- Real-time status reporting +- Validated process documentation + +The combination of LLM reasoning, GitHub CLI automation, and structured +workflows creates a powerful foundation for autonomous release management. + +**The future of software releases is here.** 🚀 + +*This AAR serves as the foundation for future LLM-guided release automation.* \ No newline at end of file diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index c4e3166..2e8ad23 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77f1800cb3dc9679ce6ca8171348b50d4ff2603c6dc8ff94f63770a4917872be -size 319528 +oid sha256:143161cb140554802e845c4126d40ac40def9399aff96929812abaa2164c2d6d +size 334460 diff --git a/.gitignore b/.gitignore index af59065..6b94557 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +.claude .gemini .mcp.json .*.swp diff --git a/README.rst b/README.rst index ced98da..d5411d0 100644 --- a/README.rst +++ b/README.rst @@ -81,14 +81,17 @@ modules. Installation 📦 =============================================================================== -Via `uv `_ ``pip`` +Method: Install Python Package +------------------------------------------------------------------------------- + +Install via `uv `_ ``pip`` command: :: uv pip install classcore -Or, via ``pip``: +Or, install via ``pip``: :: @@ -188,6 +191,23 @@ Use Cases 🎯 * 📦 **Frameworks**: Frameworks with controlled extension and modification. +Contribution 🤝 +=============================================================================== + +Contribution to this project is welcome! However, it must follow the `code of +conduct +`_ +for the project. + +Please file bug reports and feature requests in the `issue tracker +`_ or submit `pull +requests `_ to +improve the source code or documentation. + +For development guidance and standards, please see the `development guide +`_. + + `More Flair `_ =============================================================================== @@ -228,24 +248,24 @@ Other Projects by This Author 🌟 =============================================================================== -* `python-absence `_ (`absence `_ on PyPI) +* `python-absence `_ (`absence `_ on PyPI) 🕳️ A Python library package which provides a **sentinel for absent values** - a falsey, immutable singleton that represents the absence of a value in contexts where ``None`` or ``False`` may be valid values. -* `python-accretive `_ (`accretive `_ on PyPI) +* `python-accretive `_ (`accretive `_ on PyPI) 🌌 A Python library package which provides **accretive data structures** - collections which can grow but never shrink. -* `python-dynadoc `_ (`dynadoc `_ on PyPI) +* `python-dynadoc `_ (`dynadoc `_ on PyPI) 📝 A Python library package which bridges the gap between **rich annotations** and **automatic documentation generation** with configurable renderers and support for reusable fragments. -* `python-falsifier `_ (`falsifier `_ on PyPI) +* `python-falsifier `_ (`falsifier `_ on PyPI) 🎭 A very simple Python library package which provides a **base class for falsey objects** - objects that evaluate to ``False`` in boolean contexts. -* `python-frigid `_ (`frigid `_ on PyPI) +* `python-frigid `_ (`frigid `_ on PyPI) 🔒 A Python library package which provides **immutable data structures** - collections which cannot be modified after creation. -* `python-icecream-truck `_ (`icecream-truck `_ on PyPI) +* `python-icecream-truck `_ (`icecream-truck `_ on PyPI) 🍦 **Flavorful Debugging** - A Python library which enhances the powerful and well-known ``icecream`` package with flavored traces, configuration hierarchies, customized outputs, ready-made recipes, and more. -* `python-mimeogram `_ (`mimeogram `_ on PyPI) +* `python-mimeogram `_ (`mimeogram `_ on PyPI) 📨 A command-line tool for **exchanging collections of files with Large Language Models** - bundle multiple files into a single clipboard-ready document while preserving directory structure and metadata... good for code reviews, project sharing, and LLM interactions. diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 8237b51..c12c903 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,22 @@ Release Notes .. towncrier release notes start +Classcore 1.7 (2025-07-08) +========================== + +Enhancements +------------ + +- Standard: Add ``ignore_init_arguments`` decorator argument and + ``instances_ignore_init_arguments`` class argument to support cases, such as + inheritance from ``urllib.parse.ParseResult`` which inherits from ``tuple`` and + overrides ``__new__`` instead of ``__init__``. In such cases, ``__new__`` + processes the instance production arguments rather than ``__init__``. However, + the standard Python behavior is to present the arguments to both ``__new__`` + and ``__init__``, which is problematic since we always provide an ``__init__`` + head. + + Classcore 1.6.1 (2025-07-01) ============================ diff --git a/documentation/contribution.rst b/documentation/contribution.rst index 2ef1ea7..6d78675 100644 --- a/documentation/contribution.rst +++ b/documentation/contribution.rst @@ -26,7 +26,7 @@ Contribution Contribution to this project is welcome! However, it must follow the `code of conduct -`_ +`_ for the project. @@ -50,27 +50,39 @@ Guidance and Standards ------------------------------------------------------------------------------- * Follow the `development environment preparation and management instructions - `_ + `_ to ensure consistency with maintainer development environments and CI workflows. +* Configure Git commit signing as required for all contributions. See the + `environment setup guide + `_ + for configuration details. + * Adhere to the `development practices - `_ - and `code style `_ + `_ + and `code style + `_ to improve the probability of pull request acceptance. You may wish to use an LLM to assist with this, if the standards seem too onerous or specific. * Also consider the `nomenclature advice - `_ + `_ for consistency and to improve the probability of pull request acceptance. +* Run validation commands before submitting contributions. See the `validation + guide `_ + for available commands and workflow. (If you installed the Git pre-commit and + pre-push hooks during environment setup, then they will run the validations + for you.) + * Prepare changelog fragments according to the `releases guide - `_ + `_ as appropriate. * Although unncessary for non-maintainer contributions, additional background can be found in the `maintenance guide - `_. + `_. Artificial Intelligence ------------------------------------------------------------------------------- diff --git a/documentation/examples/std-classes.rst b/documentation/examples/std-classes.rst index 395f585..321806e 100644 --- a/documentation/examples/std-classes.rst +++ b/documentation/examples/std-classes.rst @@ -202,6 +202,34 @@ The mapping form of ``__slots__`` is also supported. 'Namespace attributes.' +Suppression of Initialization Arguments +=============================================================================== + +In some cases, you may inherit from classes which process their instance +construction arguments via ``__new__`` rather than ``__init__``. This is +experienced, for example, where :py:class:`tuple` and other immutable builtins +are subclassed. To prevent the construction arguments from being applied to the +``__init__`` call chain, you can set ``instances_ignore_init_arguments`` to +``True`` as a class argument. + +.. doctest:: Standard.Classes + + >>> from urllib.parse import ParseResult, urlparse + >>> class Url( ccstd.Object, ParseResult, instances_ignore_init_arguments = True ): + ... pass + ... + >>> u = Url( *urlparse( 'https://python.org' ) ) + +Or as ``ignore_init_arguments`` as ``True`` to a decorator. + +.. doctest:: Standard.Classes + + >>> @ccstd.with_standard_behaviors( ignore_init_arguments = True ) + ... class Url( ParseResult ): pass + ... + >>> u = Url( *urlparse( 'https://python.org' ) ) + + Integrations with Custom Behaviors =============================================================================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 48fb719..9431490 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.6.1' +__version__ = '1.7' standard.finalize_module( diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index bdbb6bb..3d9fb30 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -272,6 +272,8 @@ def postprocess( 'instances_mutables', __.mutables_default ) instances_visibles = arguments.get( 'instances_visibles', __.visibles_default ) + instances_ignore_init_arguments = arguments.get( + 'instances_ignore_init_arguments', False ) if dcls_spec and dcls_spec.get( 'kw_only_default', False ): from .decorators import dataclass_with_standard_behaviors decorator_factory = dataclass_with_standard_behaviors @@ -289,6 +291,7 @@ def postprocess( _nomina.DeleterCore, cores[ 'deleter' ] ), surveyor_core = __.typx.cast( _nomina.SurveyorCore, cores[ 'surveyor' ] ), + ignore_init_arguments = instances_ignore_init_arguments, mutables = instances_mutables, visibles = instances_visibles ) decorators.append( decorator ) @@ -391,6 +394,7 @@ def record_class_construction_arguments( 'instances_assigner_core', 'instances_deleter_core', 'instances_surveyor_core', + 'instances_ignore_init_arguments', 'instances_mutables', 'instances_visibles', ): if name not in arguments: continue diff --git a/sources/classcore/standard/classes.py b/sources/classcore/standard/classes.py index fd776fc..4e945e7 100644 --- a/sources/classcore/standard/classes.py +++ b/sources/classcore/standard/classes.py @@ -42,6 +42,7 @@ class ClassFactoryExtraArguments( __.typx.TypedDict, total = False ): instances_assigner_core: _nomina.AssignerCore instances_deleter_core: _nomina.DeleterCore instances_surveyor_core: _nomina.SurveyorCore + instances_ignore_init_arguments: bool instances_mutables: _nomina.BehaviorExclusionVerifiersOmni instances_visibles: _nomina.BehaviorExclusionVerifiersOmni diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 4704996..41b05e4 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -220,15 +220,19 @@ def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: return decorate -def produce_instances_initialization_decorator( # noqa: PLR0913 +def produce_instances_inception_decorator( # noqa: PLR0913 attributes_namer: _nomina.AttributesNamer, assigner_core: __.typx.Optional[ _nomina.AssignerCore ], deleter_core: __.typx.Optional[ _nomina.DeleterCore ], surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ], + ignore_init_arguments: bool, mutables: _nomina.BehaviorExclusionVerifiersOmni, visibles: _nomina.BehaviorExclusionVerifiersOmni, ) -> _nomina.Decorator[ __.U ]: - ''' Produces decorator to inject '__init__' method into class. ''' + ''' Produces decorator to inject '__new__' or '__init__' method. + + Also handles common bookkeeping tasks. + ''' cores = dict( instances_assigner_core = assigner_core, instances_deleter_core = deleter_core, @@ -249,7 +253,6 @@ def decorate( cls: type[ __.U ] ) -> type[ __.U ]: core_aname = attributes_namer( 'instances', f"{core_name}_core" ) setattr( cls, core_aname, core_function ) behaviors: set[ str ] = set( ) - behaviors_name = attributes_namer( 'instance', 'behaviors' ) _behaviors.record_behavior( cls, attributes_namer = attributes_namer, level = 'instances', basename = 'mutables', @@ -260,6 +263,66 @@ def decorate( cls: type[ __.U ] ) -> type[ __.U ]: level = 'instances', basename = 'visibles', label = _nomina.concealment_label, behaviors = behaviors, verifiers = visibles ) + decorator = produce_instances_initialization_decorator( + attributes_namer = attributes_namer, + behaviors = behaviors, + ignore_init_arguments = ignore_init_arguments ) + return decorator( cls ) + + return decorate + + +# def produce_instances_construction_decorator( +# attributes_namer: _nomina.AttributesNamer, +# behaviors: __.cabc.MutableSet[ str ], +# ) -> _nomina.Decorator[ __.U ]: +# ''' Produces decorator to inject '__new__' method. ''' +# def decorate( cls_: type[ __.U ] ) -> type[ __.U ]: +# behaviors_name = attributes_namer( 'instance', 'behaviors' ) +# original = cls_.__dict__.get( '__new__' ) +# +# if original is None: +# +# def initialize_with_super( +# cls: type[ __.U ], +# *posargs: __.typx.Any, +# **nomargs: __.typx.Any, +# ) -> __.U: +# self = super( cls_, cls ).__new__( cls, *posargs, **nomargs ) +# _activate_instance_behaviors( +# cls_, self, behaviors_name, behaviors ) +# return self +# +# cls_.__new__ = initialize_with_super +# +# else: +# +# @__.funct.wraps( original ) +# def initialize_with_original( +# cls: type[ __.U ], +# *posargs: __.typx.Any, +# **nomargs: __.typx.Any, +# ) -> __.U: +# self = original( cls, *posargs, **nomargs ) +# _activate_instance_behaviors( +# cls_, self, behaviors_name, behaviors ) +# return self +# +# cls_.__new__ = initialize_with_original +# +# return cls_ +# +# return decorate + + +def produce_instances_initialization_decorator( + attributes_namer: _nomina.AttributesNamer, + behaviors: __.cabc.MutableSet[ str ], + ignore_init_arguments: bool, +) -> _nomina.Decorator[ __.U ]: + ''' Produces decorator to inject '__init__' method into class. ''' + def decorate( cls: type[ __.U ] ) -> type[ __.U ]: + behaviors_name = attributes_namer( 'instance', 'behaviors' ) original = cls.__dict__.get( '__init__' ) if original is None: @@ -267,14 +330,10 @@ def decorate( cls: type[ __.U ] ) -> type[ __.U ]: def initialize_with_super( self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any ) -> None: - super( cls, self ).__init__( *posargs, **nomargs ) - # Only record behaviors at start of MRO. - if cls is not type( self ): return - behaviors_: set[ str ] = ( - _utilities.getattr0( self, behaviors_name, set( ) ) ) - behaviors_.update( behaviors ) - _utilities.setattr0( - self, behaviors_name, frozenset( behaviors_ ) ) + if ignore_init_arguments: super( cls, self ).__init__( ) + else: super( cls, self ).__init__( *posargs, **nomargs ) + _activate_instance_behaviors( + cls, self, behaviors_name, behaviors ) cls.__init__ = initialize_with_super @@ -284,14 +343,10 @@ def initialize_with_super( def initialize_with_original( self: object, *posargs: __.typx.Any, **nomargs: __.typx.Any ) -> None: - original( self, *posargs, **nomargs ) - # Only record behaviors at start of MRO. - if cls is not type( self ): return - behaviors_: set[ str ] = ( - _utilities.getattr0( self, behaviors_name, set( ) ) ) - behaviors_.update( behaviors ) - _utilities.setattr0( - self, behaviors_name, frozenset( behaviors_ ) ) + if ignore_init_arguments: original( self ) + else: original( self, *posargs, **nomargs ) + _activate_instance_behaviors( + cls, self, behaviors_name, behaviors ) cls.__init__ = initialize_with_original @@ -484,6 +539,7 @@ def dataclass_with_standard_behaviors( # noqa: PLR0913 assigner_core: __.typx.Optional[ _nomina.AssignerCore ] = None, deleter_core: __.typx.Optional[ _nomina.DeleterCore ] = None, surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ] = None, + ignore_init_arguments: bool = False, mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, ) -> _nomina.Decorator[ __.U ]: @@ -496,6 +552,7 @@ def dataclass_with_standard_behaviors( # noqa: PLR0913 assigner_core = assigner_core, deleter_core = deleter_core, surveyor_core = surveyor_core, + ignore_init_arguments = ignore_init_arguments, mutables = mutables, visibles = visibles ) ) preparers: _nomina.DecorationPreparers[ __.U ] = ( @@ -514,6 +571,7 @@ def with_standard_behaviors( # noqa: PLR0913 assigner_core: __.typx.Optional[ _nomina.AssignerCore ] = None, deleter_core: __.typx.Optional[ _nomina.DeleterCore ] = None, surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ] = None, + ignore_init_arguments: bool = False, mutables: _nomina.BehaviorExclusionVerifiersOmni = __.mutables_default, visibles: _nomina.BehaviorExclusionVerifiersOmni = __.visibles_default, ) -> _nomina.Decorator[ __.U ]: @@ -525,6 +583,7 @@ def with_standard_behaviors( # noqa: PLR0913 assigner_core = assigner_core, deleter_core = deleter_core, surveyor_core = surveyor_core, + ignore_init_arguments = ignore_init_arguments, mutables = mutables, visibles = visibles ) ) preparers: _nomina.DecorationPreparers[ __.U ] = ( @@ -534,6 +593,20 @@ def with_standard_behaviors( # noqa: PLR0913 return decoration_by( *decorators, *decorators_, preparers = preparers ) +def _activate_instance_behaviors( + cls: type[ __.U ], + self: object, + behaviors_name: str, + behaviors: __.cabc.MutableSet[ str ], +) -> None: + # Only record behaviors at start of MRO. + if cls is not type( self ): return + behaviors_: set[ str ] = ( + _utilities.getattr0( self, behaviors_name, set( ) ) ) + behaviors_.update( behaviors ) + _utilities.setattr0( self, behaviors_name, frozenset( behaviors_ ) ) + + def _produce_instances_decoration_preparers( attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, @@ -554,17 +627,19 @@ def _produce_instances_decorators( # noqa: PLR0913 assigner_core: __.typx.Optional[ _nomina.AssignerCore ], deleter_core: __.typx.Optional[ _nomina.DeleterCore ], surveyor_core: __.typx.Optional[ _nomina.SurveyorCore ], + ignore_init_arguments: bool, mutables: _nomina.BehaviorExclusionVerifiersOmni, visibles: _nomina.BehaviorExclusionVerifiersOmni, ) -> _nomina.Decorators[ __.U ]: ''' Produces standard decorators. ''' decorators: list[ _nomina.Decorator[ __.U ] ] = [ ] decorators.append( - produce_instances_initialization_decorator( + produce_instances_inception_decorator( attributes_namer = attributes_namer, assigner_core = assigner_core, deleter_core = deleter_core, surveyor_core = surveyor_core, + ignore_init_arguments = ignore_init_arguments, mutables = mutables, visibles = visibles ) ) decorators.append( produce_attributes_assignment_decorator( diff --git a/tests/test_000_classcore/test_340_standard_classes.py b/tests/test_000_classcore/test_340_standard_classes.py new file mode 100644 index 0000000..115d5dd --- /dev/null +++ b/tests/test_000_classcore/test_340_standard_classes.py @@ -0,0 +1,48 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +# import pytest + +from . import PACKAGE_NAME, cache_import_module + + +MODULE_QNAME = f"{PACKAGE_NAME}.standard.classes" + + +def test_124_cfc_instances_ignore_init_arguments( ): + ''' Metaclass respects 'instances_ignore_init_arguments'. ''' + from urllib.parse import ParseResult as UrlParts, urlparse + module = cache_import_module( MODULE_QNAME ) + + class Url( + module.Object, UrlParts, instances_ignore_init_arguments = True + ): pass + + u = Url( *urlparse( 'https://python.org' ) ) + assert u.scheme == 'https' + + class UrlWithInit( + module.Object, UrlParts, instances_ignore_init_arguments = True + ): + def __init__( self, *posargs, **nomargs ): + super( ).__init__( *posargs, **nomargs ) + + u = UrlWithInit( *urlparse( 'https://python.org' ) ) + assert u.scheme == 'https' From 7b122ff18ff8fed73c48b9af67be10f2b9b48792 Mon Sep 17 00:00:00 2001 From: Github Actions Robot Date: Wed, 23 Jul 2025 13:54:25 +0000 Subject: [PATCH 21/28] Update documents for publication. (1.8) --- .../claude/commands/cs-annotate-release.md | 93 +++++++ .../claude/commands/cs-conform-code.md | 236 ++++++++++++++++++ .../claude/commands/cs-develop-tests.md | 212 ++++++++++++++++ .../{release-final.md => cs-release-final.md} | 8 +- ...ase-patch.md => cs-release-maintenance.md} | 5 + .auxiliary/configuration/claude/settings.json | 85 +++++++ .auxiliary/configuration/conventions.md | 82 +++--- .auxiliary/configuration/copier-answers.yaml | 2 +- .auxiliary/configuration/pre-commit.yaml | 11 +- .../data/towncrier/+modules-df.repair.rst | 1 + .../towncrier/+modules-exclude.enhance.rst | 2 + .../data/towncrier/+newinit.enhance.rst | 8 - .auxiliary/publications/website.tar.xz | 4 +- .auxiliary/scripts/claude/post-edit-linter | 65 +++++ .../scripts/claude/pre-bash-python-check | 105 ++++++++ .gitignore | 1 + documentation/changelog.rst | 16 ++ documentation/contribution.rst | 8 +- pyproject.toml | 28 ++- sources/classcore/__init__.py | 2 +- sources/classcore/standard/modules.py | 28 ++- tests/test_000_classcore/conftest.py | 3 + .../test_350_standard_modules.py | 77 +++++- 23 files changed, 993 insertions(+), 89 deletions(-) create mode 100644 .auxiliary/configuration/claude/commands/cs-annotate-release.md create mode 100644 .auxiliary/configuration/claude/commands/cs-conform-code.md create mode 100644 .auxiliary/configuration/claude/commands/cs-develop-tests.md rename .auxiliary/configuration/claude/commands/{release-final.md => cs-release-final.md} (93%) rename .auxiliary/configuration/claude/commands/{release-patch.md => cs-release-maintenance.md} (97%) create mode 100644 .auxiliary/data/towncrier/+modules-df.repair.rst create mode 100644 .auxiliary/data/towncrier/+modules-exclude.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+newinit.enhance.rst create mode 100755 .auxiliary/scripts/claude/post-edit-linter create mode 100755 .auxiliary/scripts/claude/pre-bash-python-check create mode 100644 tests/test_000_classcore/conftest.py diff --git a/.auxiliary/configuration/claude/commands/cs-annotate-release.md b/.auxiliary/configuration/claude/commands/cs-annotate-release.md new file mode 100644 index 0000000..2c5f3af --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-annotate-release.md @@ -0,0 +1,93 @@ +--- +allowed-tools: Bash(git log:*), Bash(git show:*), Bash(ls:*), Bash(grep:*), Grep, Read, Write, LS +description: Create Towncrier news fragments for user-facing changes since last release cleanup +--- + +# Write Release Notes + +**NOTE: This is an experimental workflow! If anything seems unclear or missing, +please stop for consultation with the user.** + +You are tasked with creating Towncrier news fragments for user-facing changes +since the last release cleanup. This command analyzes recent commits and +generates appropriate changelog entries. + +Special instructions: `$ARGUMENTS` +(If above line is empty, then no special instructions were given by the user.) + +## Context + +The project uses Towncrier to manage changelogs. News fragments are stored in +`.auxiliary/data/towncrier/` and follow specific naming and formatting +conventions detailed in the [releases +guide](https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/releases.rst). + +## Process + +### Phase 1: Discovery and Analysis + +1. **Find Starting Point**: Use `git log --oneline --grep="Clean up news fragments"` to find the last cleanup commit +2. **Get Recent Commits**: Retrieve all commits since the cleanup using `git log --no-merges` with full commit messages +3. **Check Existing Fragments**: List existing fragments in `.auxiliary/data/towncrier/` to avoid duplication + +### Phase 2: Filtering and Classification + +4. **Filter User-Facing Changes**: Focus on changes that affect how users interact with the tool: + - CLI command changes (new options, arguments, output formats) + - API changes (public functions, classes, return values) + - Behavior changes (different responses, error messages, processing) + - Configuration changes (new settings, file formats) + - Deprecations and removals + - Platform support changes (Python versions, OS support) + + **Exclude** internal changes: + - GitHub workflows + - Dependency changes without API impact + - Internal module restructuring that preserves public API + - Git ignore files + - Modules in internals subpackages (`__`) + - Version bumps and maintenance updates + - Internal refactoring without user-visible changes + + **Key Test**: Ask "Does this change how a user invokes the tool, what options they have, or what behavior they observe?" + +5. **Classify Changes**: Determine appropriate type for each change: + - `enhance`: features and improvements + - `notify`: deprecations and notices + - `remove`: removals of features or support + - `repair`: bug fixes + + Note: Some commits may contain multiple types of changes. + +### Phase 3: Synthesis and Creation + +6. **Group Related Commits**: Synthesize multiple commits into coherent user-facing descriptions when they represent logical units of change + +7. **Think Through Fragments**: Before writing, consider: + - Are the descriptions clear and meaningful to users? + - Do they follow the format guidelines? + - Are they properly classified? + - Do they focus on what and why, not how? + +8. **Create Fragments**: Write appropriately named fragment files using: + - `..rst` for changes with GitHub issues + - `+.<type>.rst` for changes without issues + + Fragment content should: + - Start with capital letter, end with period + - Use present tense imperative verbs + - Be understandable by users, not just developers + - Include topic prefixes when appropriate (e.g., "CLI: ", "API: ") + +### Phase 4: Final Review and Commit + +9. **Summary**: Provide a brief summary of fragments created and any notable patterns or changes identified + +10. **Commit Changes**: Add fragments to git and commit them: + - `git add .auxiliary/data/towncrier` + - `git commit -m "Add news fragments for upcoming release"` + +## Additional Instructions + +- Read full commit messages for context; only examine diff summaries if commit messages are unclear +- Focus on meaningful user-facing changes rather than comprehensive coverage of all commits diff --git a/.auxiliary/configuration/claude/commands/cs-conform-code.md b/.auxiliary/configuration/claude/commands/cs-conform-code.md new file mode 100644 index 0000000..259dfd6 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-conform-code.md @@ -0,0 +1,236 @@ +--- +allowed-tools: Bash(hatch --env develop run:*), Bash(git:*), LS, Read, Glob, Grep, Edit, MultiEdit, Write, WebFetch +description: Systematically conform Python code to project style and practice standards +--- + +# Python Code Conformance + +For bringing existing Python code into full compliance with project standards. + +Target code: `$ARGUMENTS` + +**CRITICAL**: Focus on style/practice conformance, not functionality changes. + +## Context + +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` + +## Prerequisites + +- **MANDATORY**: Read project documentation guides first: + - https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/practices.rst + - https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/style.rst + - https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/nomenclature.rst +- Understand target files to be conformed +- Have read `CLAUDE.md` for project-specific guidance + +## Priority Conformance Issues + +### 1. Function Parameters: Wide Types +**Issue:** Using narrow concrete types instead of wide abstract types for parameters +**Before:** +```python +def process_items( items: list[ str ], config: dict[ str, int ] ) -> bool: + return all( validate( item, config ) for item in items ) +``` +**After:** +```python +def process_items( + items: __.cabc.Sequence[ str ], + config: __.cabc.Mapping[ str, int ], +) -> bool: + return all( validate( item, config ) for item in items ) +``` + +### 2. Import Organization: Namespace Pollution +**Issue:** Polluting module namespace instead of using private aliases +**Before:** +```python +from pathlib import Path +from collections import defaultdict +import json +from typing import Any, Dict, List +``` +**After:** +```python +# Direct imports when performance is a consideration +from json import loads as _json_loads + +# Use __ subpackage for common imports +from . import __ +``` + +### 3. Module Organization +**Issue:** Wrong order of module contents +**Should follow this order:** +1. Imports (see practices guide) +2. Common type aliases (`TypeAlias` declarations) +3. Private variables/functions for defaults (grouped semantically) +4. Public classes and functions (alphabetical) +5. All other private functions (alphabetical) + +### 4. Spacing and Delimiters +**Issue:** Missing spaces in delimiters, operators +**Before:** +```python +def func(arg1,arg2="default"): + result=process(arg1,{"key":"value"}) +``` +**After:** +```python +def func( arg1, arg2 = 'default' ): + result = process( arg1, { 'key': 'value' } ) +``` + +### 5. Type Annotations: Missing or Incomplete +**Issue:** Missing annotations, not using `TypeAlias` for complex types +**Before:** +```python +def process_user( user, callback=None ): + return callback( user ) if callback else str( user ) +``` +**After:** +```python +UserRecord: __.typx.TypeAlias = dict[ str, str | int | list[ str ] ] + +def process_user( + user: UserRecord, + callback: __.Absential[ + __.cabc.Callable[ [ UserRecord ], str ] + ] = __.absent +) -> str: + if not __.is_absent( callback ): return callback( user ) + return str( user ) +``` + +### 6. Exception Handling: Overly Broad Blocks +**Issue:** Wrapping entire functions in try blocks +**Before:** +```python +def process_items( items: list[ str ] ) -> list[ dict ]: + try: + results = [ ] + for item in items: + validated = validate_item( item ) # Can raise + processed = expensive_computation( validated ) + results.append( processed ) + return results + except ValidationError: + return [ ] +``` +**After:** +```python +def process_items( items: __.cabc.Sequence[ str ] ) -> list[ dict ]: + results = [ ] + for item in items: + try: validated = validate_item( item ) # Only risky statement + except ValidationError: + logger.warning( f"Skipping invalid item: {item}." ) + continue + processed = expensive_computation( validated ) + results.append( processed ) + return results +``` + +### 7. Docstring Format and Mood +**Issue:** Wrong quotes, spacing, imperative mood +**Before:** +```python +def process_data( data ): + """Process the input data.""" # Wrong quotes, imperative mood +``` +**After:** +```python +def process_data( + data: __.cabc.Sequence[ __.typx.Any ] +) -> dict[ str, __.typx.Any ]: + ''' Processes input data and returns results. ''' # Narrative mood +``` + +### 8. Immutability: Using Mutable When Unnecessary +**Issue:** Using mutable containers when immutable would suffice +**Before:** +```python +def calculate_stats( data: list[ int ] ) -> dict[ str, float ]: + results = { } + results[ 'mean' ] = sum( data ) / len( data ) + return results +``` +**After:** +```python +def calculate_stats( + data: __.cabc.Sequence[ int ] +) -> __.immut.Dictionary[ str, float ]: + return __.immut.Dictionary( + mean = sum( data ) / len( data ), + maximum = max( data ), + minimum = min( data ) + ) +``` + +## Conformance Process + +### 1. Analysis Phase +- Read the three documentation guides thoroughly +- Examine target files to understand current state +- Run linters to identify specific violations +- Identify architectural patterns that need updating + +### 2. Systematic Correction +Apply fixes in this order: +1. **Module Organization**: Reorder imports, type aliases, functions per practices guide +2. **Wide/Narrow Types**: Convert function parameters to wide abstract types +3. **Import Cleanup**: Remove namespace pollution, use private aliases and __ subpackage +4. **Type Annotations**: Add missing hints, create `TypeAlias` for complex types +5. **Exception Handling**: Narrow try block scope, ensure proper chaining +6. **Immutability**: Replace mutable with immutable containers where appropriate +7. **Spacing/Delimiters**: Fix `( )`, `[ ]`, `{ }` patterns +8. **Docstrings**: Triple single quotes, narrative mood, proper spacing +9. **Line Length**: Split at 79 columns using parentheses + +### 3. Validation +```bash +hatch --env develop run linters # Must pass clean +hatch --env develop run testers # Must not break functionality +``` + +## Safety Requirements + +**HALT if:** +- Linters reveal complex architectural issues +- Changes would alter functionality +- Type annotations conflict with runtime behavior +- Import changes break dependencies +- Tests start failing + +**Your responsibilities:** +- Maintain exact functionality while improving practices/style +- Use project patterns consistently per the guides +- Reference all three guides for complex cases +- Verify all changes with linters and tests + +## Success Criteria + +- [ ] All linting violations resolved +- [ ] Module organization follows practices guide structure +- [ ] Function parameters use wide abstract types +- [ ] Imports avoid namespace pollution +- [ ] Type annotations comprehensive with `TypeAlias` usage +- [ ] Exception handling uses narrow try blocks +- [ ] Immutable containers used where appropriate +- [ ] No functionality changes +- [ ] Tests continue to pass +- [ ] Code follows all style guide patterns + +**Note**: Always run full validation (`hatch --env develop run linters && hatch +--env develop run testers`) before considering the task complete. + +## Final Report + +Upon completion, provide a brief report covering: +- Specific conformance issues corrected (categorized by the priority issues above) +- Number of files modified +- Any patterns that required manual intervention +- Linter status before/after +- Any deviations from guides and justification diff --git a/.auxiliary/configuration/claude/commands/cs-develop-tests.md b/.auxiliary/configuration/claude/commands/cs-develop-tests.md new file mode 100644 index 0000000..dfadab5 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-develop-tests.md @@ -0,0 +1,212 @@ +--- +allowed-tools: Bash(hatch --env develop run:*), Bash(git status), Bash(git log:*), Bash(echo:*), Bash(ls:*), Bash(find:*), LS, Read, Glob, Grep, Write, Edit, MultiEdit, WebFetch +description: Write comprehensive tests following project testing guidelines and improve coverage +--- + +# Write Tests + +**NOTE: This is an experimental workflow! If anything seems unclear or missing, +please stop for consultation with the user.** + +For systematic test creation following project testing guidelines and conventions. + +Test requirements: `$ARGUMENTS` + +**CRITICAL**: Apply project testing principles consistently. +**HALT if**: +- No test requirements are provided +- Target code cannot be analyzed +- Testing principles would be violated + +## Context + +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` +- Current test coverage: !`hatch --env develop run coverage report --skip-covered || echo "No coverage data available"` +- Existing test structure: !`find tests -name "*.py" | head -20` +- Test README: !`ls tests/README.md 2>/dev/null && echo "Present" || echo "Missing"` + +## Prerequisites + +Ensure that you: +- have verified access to / read target code modules +- understand what specific tests need to be written +- have read any relevant `CLAUDE.md` file +- understand the [test-writing + guidelines](https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst). + +## Testing Principles (from project guidelines) + +**Core Principles:** +1. **Dependency Injection Over Monkey-Patching**: Use injectable dependencies + for testability +2. **Performance-Conscious**: Prefer in-memory filesystems (pyfakefs) over temp + directories +3. **Avoid Monkey-Patching**: Never patch internal code; use dependency + injection instead +4. **100% Coverage Goal**: Aim for complete line and branch coverage +5. **Test Behavior, Not Implementation**: Focus on observable behavior and + contracts + +**Anti-Patterns to Avoid:** +- Monkey-patching internal code (will fail with immutable objects) +- Excessive mocking of internal components +- Testing implementation details vs. behavior +- Using temp directories when pyfakefs suffices + +**Organization:** +- Follow the systematic numbering conventions detailed in the test guidelines + +## Safety Requirements + +**CRITICAL**: You MUST halt the process and consult with the user if ANY of the +following occur: + +- **Anti-Pattern Detection**: If proposed tests violate project principles +- **Coverage Regression**: If tests would reduce existing coverage +- **Architecture Conflicts**: If tests require monkey-patching internal code +- **Numbering Conflicts**: If test numbering clashes with existing conventions +- **Missing Dependencies**: If required test fixtures or dependencies are + unavailable + +**Your responsibilities:** +- Follow project style and test-writing conventions exactly. +- Use dependency injection patterns consistently. +- Prefer pyfakefs for filesystem operations. +- Maintain systematic test numbering. +- Ensure tests validate behavior, not implementation. + +## Test Writing Process + +Execute the following steps for test requirements: `$ARGUMENTS` + +### 0. Pre-Flight Verification +**MANDATORY - Verify access to test-writing guide:** + +Use WebFetch to access and confirm you can read the complete testing +guidelines: +https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst + +**CRITICAL**: You MUST successfully access and read the guide before +proceeding. If WebFetch fails, HALT and consult with the user. + +### 1. Code Analysis Phase +Examine the target code to understand testing needs: + +**Check for existing related tests to avoid duplication:** +- Search for existing test files covering target modules +- Review test coverage reports for current state +- Identify gaps rather than recreating existing tests + +**For each target file:** +- Read the source code to understand public API +- Identify functions/classes that need testing +- Note dependency injection points +- Check for existing test coverage gaps + +### 2. Test Structure Planning +Determine appropriate test organization and categories: + +**Review existing test structure and plan test numbering following project conventions.** + +**If tests/README.md is missing, create it with:** +- Test module numbering scheme specific to the package +- Rationale for any use of patch or other exceptions to standard patterns +- Project-specific testing conventions and fixtures + +**Test Categories to Include:** +- **Basic Functionality Tests (000-099):** Happy path scenarios, input validation, basic error conditions +- **Feature-Specific Tests (100+ blocks):** Each public function/class gets its own 100-block with normal usage patterns, edge cases, and error handling +- **Integration Tests (higher numbers):** Cross-module interactions and end-to-end workflows + +### 3. Test Implementation +Create tests following project conventions: + +**Key Implementation Guidelines:** +- Use dependency injection for all external dependencies +- Prefer `pyfakefs.Patcher()` for filesystem operations +- Mock only third-party services, never internal code +- Include docstrings explaining what behavior is tested +- Follow existing naming conventions and code style + +### 5. Coverage Validation +Verify tests improve coverage without regressions: +```bash +hatch --env develop run testers +hatch --env develop run coverage report --show-missing +``` + +**CRITICAL - VERIFY COVERAGE IMPROVEMENT:** +- Run full test suite to ensure no regressions +- Check that new tests increase overall coverage +- Verify no existing functionality is broken +- Confirm tests follow project numbering conventions + +### 6. Code Quality Validation +Ensure tests meet project standards: +```bash +hatch --env develop run linters +``` + +**Requirements:** +- All linting checks must pass +- No violations of project coding standards +- Test docstrings are clear and descriptive +- Proper imports and dependencies + +## Test Pattern Examples + +**Dependency Injection Pattern:** +```python +async def test_100_process_with_custom_processor( ): + ''' Process function accepts custom processor via injection. ''' + def mock_processor( data ): + return f"processed: {data}" + + result = await process_data( "test", processor = mock_processor ) + assert result == "processed: test" +``` + +**Filesystem Operations (Preferred):** +```python +def test_200_config_file_processing( ): + ''' Configuration files are processed correctly. ''' + with Patcher( ) as patcher: + fs = patcher.fs + fs.create_file( '/fake/config.toml', contents = '[section]\nkey="value"' ) + + result = process_config_file( Path( '/fake/config.toml' ) ) + assert result.key == 'value' +``` + +**Error Handling:** +```python +def test_300_invalid_input_handling( ): + ''' Invalid input raises appropriate exceptions. ''' + with pytest.raises( ValueError, match = "Invalid data format" ): + process_invalid_data( "malformed" ) +``` + +## Success Criteria + +Tests are complete when: +- [ ] Coverage has measurably improved +- [ ] All new tests pass consistently +- [ ] No existing tests are broken +- [ ] Linting passes without issues +- [ ] Tests follow project numbering conventions +- [ ] Dependency injection is used appropriately +- [ ] No monkey-patching of internal code +- [ ] Performance-conscious patterns are applied + +**Note**: Always run full validation (`hatch --env develop run testers && hatch +--env develop run linters`) before considering the task complete. + +## Final Report + +Upon completion, provide a brief report covering: +- Coverage improvements achieved (before/after percentages) +- Any technical conflicts encountered (e.g., dataclass/protocol issues, __slots__ conflicts) +- How any conflicts were resolved or worked around +- Pragma directives applied (# pragma: no cover, # pragma: no branch) and rationale +- Any deviations from standard patterns and justification diff --git a/.auxiliary/configuration/claude/commands/release-final.md b/.auxiliary/configuration/claude/commands/cs-release-final.md similarity index 93% rename from .auxiliary/configuration/claude/commands/release-final.md rename to .auxiliary/configuration/claude/commands/cs-release-final.md index 7132bc2..ced2361 100644 --- a/.auxiliary/configuration/claude/commands/release-final.md +++ b/.auxiliary/configuration/claude/commands/cs-release-final.md @@ -13,7 +13,7 @@ For execution of a fully-automated final release. Below is a validated process to create a final release with automated monitoring and next development cycle setup. -Target release version: `$ARGUMENTS` (e.g., `1.6`, `2.0`) +Target release version: `$ARGUMENTS` **CRITICAL**: Verify exactly one target release version provided. **HALT if**: @@ -28,8 +28,6 @@ Target release version: `$ARGUMENTS` (e.g., `1.6`, `2.0`) - Current version: !`hatch version` - Recent commits: !`git log --oneline -10` - Available towncrier fragments: !`ls .auxiliary/data/towncrier/*.rst 2>/dev/null || echo "No fragments found"` -- Target release branch status: !`git branch -r | grep release-$ARGUMENTS || echo "Release branch not found - will create new"` -- Local release branch status: !`git branch | grep release-$ARGUMENTS || echo "No local release branch"` ## Prerequisites @@ -165,8 +163,8 @@ git push origin master Set up next development version: ```bash hatch version minor,alpha -git commit -am "Version: $(hatch version)" -git tag -m "Start development for v$(hatch version | sed 's/a[0-9]*$//')." i$(hatch version | sed 's/a[0-9]*$//') +git commit -am "Start of development for release $(hatch version | sed 's/a[0-9]*$//')." +git tag -m "Start of development for release $(hatch version | sed 's/a[0-9]*$//')." "i$(hatch version | sed 's/a[0-9]*$//')" git push origin master --tags ``` diff --git a/.auxiliary/configuration/claude/commands/release-patch.md b/.auxiliary/configuration/claude/commands/cs-release-maintenance.md similarity index 97% rename from .auxiliary/configuration/claude/commands/release-patch.md rename to .auxiliary/configuration/claude/commands/cs-release-maintenance.md index 8699d41..82146f3 100644 --- a/.auxiliary/configuration/claude/commands/release-patch.md +++ b/.auxiliary/configuration/claude/commands/cs-release-maintenance.md @@ -100,6 +100,11 @@ git log --graph --oneline master --since="1 month ago" git log --oneline release-$ARGUMENTS..master --since="1 month ago" ``` +**IMPORTANT** +- Do **not** cherry-pick commits which were previously cherry-picked onto the + branch. +- Look at the Towncrier news fragments to help you decide what to pick. + **Patch commits** (always cherry-pick): - Bug fixes - Security patches diff --git a/.auxiliary/configuration/claude/settings.json b/.auxiliary/configuration/claude/settings.json index 9030ceb..81f7ca9 100644 --- a/.auxiliary/configuration/claude/settings.json +++ b/.auxiliary/configuration/claude/settings.json @@ -1,5 +1,90 @@ { "env": { "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": 1 + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": ".auxiliary/scripts/claude/pre-bash-python-check", + "timeout": 10 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|MultiEdit|Write|mcp__text-editor__edit_text_file_contents", + "hooks": [ + { + "type": "command", + "command": ".auxiliary/scripts/claude/post-edit-linter", + "timeout": 60 + } + ] + } + ] + }, + "permissions": { + "auto_allow": [ + "Bash(awk *)", + "Bash(cat *)", + "Bash(cut *)", + "Bash(df *)", + "Bash(du *)", + "Bash(echo *)", + "Bash(file *)", + "Bash(find *)", + "Bash(gh browse *)", + "Bash(gh issue list *)", + "Bash(gh issue view *)", + "Bash(gh pr checks *)", + "Bash(gh pr list *)", + "Bash(gh pr view *)", + "Bash(gh release list *)", + "Bash(gh release view *)", + "Bash(gh repo list *)", + "Bash(gh repo view *)", + "Bash(gh run list *)", + "Bash(gh run view *)", + "Bash(gh run watch *)", + "Bash(gh status *)", + "Bash(git add *)", + "Bash(git branch *)", + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git show *)", + "Bash(git status)", + "Bash(grep *)", + "Bash(hatch run python *)", + "Bash(hatch --env develop run docsgen)", + "Bash(hatch --env develop run linters)", + "Bash(hatch --env develop run testers)", + "Bash(head *)", + "Bash(ls *)", + "Bash(ps *)", + "Bash(pwd *)", + "Bash(rg *)", + "Bash(sed *)", + "Bash(sort *)", + "Bash(tail *)", + "Bash(uniq *)", + "Bash(wc *)", + "Bash(which *)", + "mcp__context7__get-library-docs", + "mcp__context7__resolve-library-id", + "mcp__pyright__definition", + "mcp__pyright__diagnostics", + "mcp__pyright__hover", + "mcp__pyright__references", + "mcp__ruff__definition", + "mcp__ruff__diagnostics", + "mcp__ruff__hover", + "mcp__ruff__references", + "mcp__text-editor__get_text_file_contents" + ] } } diff --git a/.auxiliary/configuration/conventions.md b/.auxiliary/configuration/conventions.md index a6d8d50..7fc36b9 100644 --- a/.auxiliary/configuration/conventions.md +++ b/.auxiliary/configuration/conventions.md @@ -1,5 +1,13 @@ # General Advice +**IMPORTANT:** Read the comprehensive documentation guides: + +- **Practices**: https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/practices.rst +- **Style**: https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/style.rst +- **Nomenclature**: https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/nomenclature.rst + +For detailed patterns, examples, and architectural guidance, refer to the comprehensive guides above. + ### Context - Be sure the look at any README files in the directories which contain the @@ -88,53 +96,41 @@ ## Python -### Design and Idioms - -- Target Python 3.10 and use idioms appropriate for that version - (`match`..`case`, type unions via `|`, etc...). -- Note the internal `__` subpackage which exposes imports used internally - throughout the package (`cabc` alias for `collections.abc`, `enum`, `types`, - `typx` alias for `typing_extensions`, etc...). -- Do not pollute the module namespace with public imports. Either reference - common imports from the `__` subpackage or alias module-level imports as - private. -- Do not use `__all__` to advertise the public API of a module. Name anything, - which should not be part of this API, with a private name starting with `_`. - -### Documentation and Annotations - -- Pad inside of delimiter pairs with spaces. E.g., `( foo )` and not `(foo)`. - Except in f-strings and `str.format` inputs. -- Pad binary operators with spaces. E.g., `foo = 42` and not `foo=42`, `1 + 1` - and not `1+1`, `[ 1 : -n ]` and not `[1:-n]`. -- Docstrings look like `''' Space-padded headline inside of triple-single - quotes. '''` and not `"""Double quotes and no spaces are hard to read."""`. -- Use double-quoted strings for f-strings, `str.format` templates, and log - messages. Otherwise, use single-quoted strings. -- Add type hints for arguments, attributes, and return values. -- Do not write "param spam" documentation which states the obvious. Only - document non-obvious or complex behaviors on arguments and attributes. -- Use PEP 593 `Annotated` with PEP 727 `Doc` for argument, attribute, and - return value documentation, when necessary. -- Use `TypeAlias` aliases to reuse complex annotations or expose them as part - of the public API. +### Essentials -### Quality Assurance +- Avoid namespace pollution - use private aliases and `__` subpackage. +- Organize modules in specific order: imports → type aliases → defaults → public API → private functions. +- Maintain readability with spaces inside of delimiters. +- Maintain readability with vertical compactness of function bodies. +- Prefer immutability wherever possible. +- Use wide abstract types for function parameters (`__.cabc.Sequence`, `__.cabc.Mapping`). +- Return narrow concrete types (`list`, `dict`, `frozenset`, `__.immut.Dictionary`). +- Use narrow try blocks (only risky statements). -- Ensure the package imports in interpreter. -- Ensure linters give a clean report. - To run linters, use `hatch --env develop run linters`. -- Ensure tests pass. - To run testers, use `hatch --env develop run testers`. -- Ensure documentation generates without error. - To generate documentation, use `hatch --env develop run docsgen`. +**Example:** -### Lines +```python +# ✅ Correct: proper spacing, wide parameters, narrow returns, proper imports +import aiofiles as _aiofiles + +from . import __ + +UserData: __.typx.TypeAlias = dict[ str, str | int ] + +def process_items( + items: __.cabc.Sequence[ str ], # Wide input type + config: __.cabc.Mapping[ str, int ] = __.immut.Dictionary( ) +) -> tuple[ str, ... ]: # Narrow return type + ''' Processes items according to configuration. ''' + return tuple( item.upper( ) for item in items ) +``` + +### Quality Assurance -- One empty line between attribute blocks and methods on classes. -- Two empty lines between attribute blocks, classes, and functions on modules. -- Split lines at 79 columns. Use parentheses for continuations and not - backslashes. +- Ensure linters give a clean report: `hatch --env develop run linters` +- Do **not** suppress linter warnings with `noqa` pragma comments without explicit approval +- Ensure tests pass: `hatch --env develop run testers` +- Ensure documentation generates without error: `hatch --env develop run docsgen` # Commits diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index 501a084..6c827c5 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.27 +_commit: v1.32 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald diff --git a/.auxiliary/configuration/pre-commit.yaml b/.auxiliary/configuration/pre-commit.yaml index 0993cb4..b25f5b9 100644 --- a/.auxiliary/configuration/pre-commit.yaml +++ b/.auxiliary/configuration/pre-commit.yaml @@ -49,6 +49,15 @@ repos: - repo: local hooks: + - id: hatch-pytest + name: 'Test Code Units (Python)' + stages: [ 'pre-commit' ] # push is covered below + fail_fast: true + language: system + always_run: true + pass_filenames: false + entry: 'hatch --env develop run pytest' + - id: hatch-pyright name: 'Lint: Pyright' stages: [ 'pre-push' ] @@ -83,4 +92,4 @@ repos: language: system always_run: true pass_filenames: false - entry: 'hatch --env develop run packagers' + entry: 'hatch build' diff --git a/.auxiliary/data/towncrier/+modules-df.repair.rst b/.auxiliary/data/towncrier/+modules-df.repair.rst new file mode 100644 index 0000000..9578e01 --- /dev/null +++ b/.auxiliary/data/towncrier/+modules-df.repair.rst @@ -0,0 +1 @@ +Standard: Modules: Reclassify modules with proper depth-first traversal. diff --git a/.auxiliary/data/towncrier/+modules-exclude.enhance.rst b/.auxiliary/data/towncrier/+modules-exclude.enhance.rst new file mode 100644 index 0000000..ac2484b --- /dev/null +++ b/.auxiliary/data/towncrier/+modules-exclude.enhance.rst @@ -0,0 +1,2 @@ +Standard: Modules: Allow certain modules to be excluded from reclassification. +Also, implement cycle detection. diff --git a/.auxiliary/data/towncrier/+newinit.enhance.rst b/.auxiliary/data/towncrier/+newinit.enhance.rst deleted file mode 100644 index 3f768ff..0000000 --- a/.auxiliary/data/towncrier/+newinit.enhance.rst +++ /dev/null @@ -1,8 +0,0 @@ -Standard: Add ``ignore_init_arguments`` decorator argument and -``instances_ignore_init_arguments`` class argument to support cases, such as -inheritance from ``urllib.parse.ParseResult`` which inherits from ``tuple`` and -overrides ``__new__`` instead of ``__init__``. In such cases, ``__new__`` -processes the instance production arguments rather than ``__init__``. However, -the standard Python behavior is to present the arguments to both ``__new__`` -and ``__init__``, which is problematic since we always provide an ``__init__`` -head. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 2e8ad23..2a5c187 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:143161cb140554802e845c4126d40ac40def9399aff96929812abaa2164c2d6d -size 334460 +oid sha256:e5ab04332aabeb41985bd9eab97cdce5a5ee87117a677bec87575750f6a77647 +size 342008 diff --git a/.auxiliary/scripts/claude/post-edit-linter b/.auxiliary/scripts/claude/post-edit-linter new file mode 100755 index 0000000..e479133 --- /dev/null +++ b/.auxiliary/scripts/claude/post-edit-linter @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +''' Claude Code hook to run linters after file updates. ''' + + +import json +import subprocess +import sys +# import os +# from datetime import datetime + + +def main( ): + # event = _acquire_event_data( ) + try: + result = subprocess.run( + [ 'hatch', '--env', 'develop', 'run', 'linters' ], # noqa: S607 + capture_output = True, check = False, text = True, timeout = 60 ) + except Exception as exc: + exc_class = type( exc ) + _reactor_failure( f"{exc_class.__qualname__}: {exc}" ) + if result.returncode != 0: + # Combine stdout and stderr since linting output may go to stdout. + print( f"{result.stdout}\n\n{result.stderr}", file = sys.stderr ) + raise SystemExit( 2 ) + # Use JSON output for better integration with Claude Code + # _emit_decision_json( "block", f"{result.stdout}\n\n{result.stderr}" ) + raise SystemExit( 0 ) + + +def _acquire_event_data( ): + try: return json.load( sys.stdin ) + except json.JSONDecodeError: + _reactor_failure( "Invalid event data." ) + + +# def _debug_log( message ): +# ''' Logs debug message to file in scribbles directory. ''' +# log_file = '.auxiliary/scribbles/post-edit-linter-debug.log' +# os.makedirs( os.path.dirname( log_file ), exist_ok = True ) +# timestamp = datetime.now().isoformat() +# with open( log_file, 'a' ) as f: +# f.write( f"[{timestamp}] {message}\n" ) + + +def _emit_decision_json( decision, reason ): + ''' Output JSON decision for Claude Code hook system. ''' + response = { "decision": decision, "reason": reason } + print( json.dumps( response ) ) + raise SystemExit( 2 ) + + +def _error( message ): + print( message, file = sys.stderr ) + raise SystemExit( 2 ) + + +def _reactor_failure( message ): + print( "Claude Code Hook Failure: {message}", file = sys.stderr ) + raise SystemExit( 1 ) + + +if __name__ == '__main__': main( ) diff --git a/.auxiliary/scripts/claude/pre-bash-python-check b/.auxiliary/scripts/claude/pre-bash-python-check new file mode 100755 index 0000000..f43a7b2 --- /dev/null +++ b/.auxiliary/scripts/claude/pre-bash-python-check @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +''' Claude Code hook to detect improper Python usage in Bash commands. ''' + + +import json +import shlex +import sys + + +def main( ): + event = _acquire_event_data( ) + command_line = _extract_command( event ) + commands = _partition_command_line( command_line ) + for command in commands: + _check_direct_python_usage( command ) + _check_multiline_python_c( command ) + raise SystemExit( 0 ) + + +def _acquire_event_data( ): + try: return json.load( sys.stdin ) + except json.JSONDecodeError: + _reactor_failure( "Invalid event data." ) + + +def _check_direct_python_usage( tokens ): + ''' Checks for direct python usage patterns. ''' + emessage = ( + "Warning: Direct Python usage detected in command.\n" + "Consider using 'hatch run python' or " + "'hatch --env develop run python' to ensure dependencies " + "are available." ) + for token in tokens: + if token == 'hatch': return # noqa: S105 + if _is_python_command( token ): _error( emessage ) + + +def _check_multiline_python_c( tokens ): + ''' Checks for multi-line python -c scripts using shlex parsing. ''' + emessage = ( + "Warning: Multi-line Python script detected in command.\n" + "Consider writing the script to a file " + "in the '.auxiliary/scribbles' directory " + "instead of using 'python -c' with multi-line code." ) + for i, token in enumerate( tokens ): + if ( _is_python_command( token ) + and _check_python_c_argument( tokens, i ) + ): _error( emessage ) + + +def _check_python_c_argument( tokens, python_index ): + ''' Checks if Python -c argument contains multiline code. ''' + for j in range( python_index + 1, len( tokens ) ): + if tokens[ j ] == '-c' and j + 1 < len( tokens ): + c_argument = tokens[ j + 1 ] + return '\n' in c_argument + if not tokens[ j ].startswith( '-' ): + # Non-option argument, stop looking for -c + break + return False + + +def _error( message: str ): + print( message, file = sys.stderr ) + raise SystemExit( 2 ) + + +def _extract_command( event_data ): + ''' Extracts command from event data, exit if not Bash tool. ''' + tool_name = event_data.get( 'tool_name', '' ) + if tool_name != 'Bash': raise SystemExit( 0 ) + tool_input = event_data.get( 'tool_input', { } ) + return tool_input.get( 'command', '' ) + + +def _is_python_command( token ): + ''' Checks if token is a Python command. ''' + return ( + token in ( 'python', 'python3' ) or token.startswith( 'python3.' ) ) + + +_splitters = frozenset( ( ';', '&', '|', '&&', '||' ) ) +def _partition_command_line( command_line ): + tokens = shlex.split( command_line ) + commands = [ ] + command_tokens = [ ] + for token in tokens: + if token in _splitters: + commands.append( command_tokens ) + command_tokens = [ ] + continue + command_tokens.append( token ) + if command_tokens: commands.append( command_tokens ) + return commands + + +def _reactor_failure( message ): + print( "Claude Code Hook Failure: {message}", file = sys.stderr ) + raise SystemExit( 1 ) + + +if __name__ == '__main__': main() diff --git a/.gitignore b/.gitignore index 6b94557..5307c35 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ CONVENTIONS.md GEMINI.md __pycache__/ bugs/ +build/ diff --git a/documentation/changelog.rst b/documentation/changelog.rst index c12c903..d52da9c 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,22 @@ Release Notes .. towncrier release notes start +Classcore 1.8 (2025-07-23) +========================== + +Enhancements +------------ + +- Standard: Modules: Allow certain modules to be excluded from reclassification. + Also, implement cycle detection. + + +Repairs +------- + +- Standard: Modules: Reclassify modules with proper depth-first traversal. + + Classcore 1.7 (2025-07-08) ========================== diff --git a/documentation/contribution.rst b/documentation/contribution.rst index 6d78675..9c475de 100644 --- a/documentation/contribution.rst +++ b/documentation/contribution.rst @@ -60,9 +60,11 @@ Guidance and Standards for configuration details. * Adhere to the `development practices - <https://emcd.github.io/python-project-common/stable/sphinx-html/common/practices.html>`_ - and `code style - <https://emcd.github.io/python-project-common/stable/sphinx-html/common/style.html>`_ + <https://emcd.github.io/python-project-common/stable/sphinx-html/common/practices.html>`_, + `code style + <https://emcd.github.io/python-project-common/stable/sphinx-html/common/style.html>`_, + and `testing guidelines + <https://emcd.github.io/python-project-common/stable/sphinx-html/common/tests.html>`_ to improve the probability of pull request acceptance. You may wish to use an LLM to assist with this, if the standards seem too onerous or specific. diff --git a/pyproject.toml b/pyproject.toml index 47d7382..25e3c54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ gha-runners = [ ] [tool.coverage.run] branch = true -command_line = '-m pytest' # TODO? '--fail-under' +command_line = '-m pytest' data_file = '.auxiliary/caches/pytest/coverage.sqlite3' parallel = true source = [ 'sources' ] @@ -120,9 +120,9 @@ PYTHONUNBUFFERED = 'TRUE' # TODO: Only for coverage/pytest. [tool.hatch.envs.develop.scripts] docsgen = [ """sphinx-build -E -b linkcheck -d .auxiliary/caches/sphinx \ - documentation .auxiliary/artifacts/sphinx-linkcheck""", + documentation .auxiliary/artifacts/sphinx-linkcheck""", """sphinx-build -a -d .auxiliary/caches/sphinx \ - documentation .auxiliary/artifacts/sphinx-html""", + documentation .auxiliary/artifacts/sphinx-html""", ] linters = [ """ruff check --quiet sources documentation tests""", @@ -135,14 +135,19 @@ packagers = [ # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- ] +testers-documentation = [ + """coverage run -m sphinx.cmd.build \ + -E -b doctest -d .auxiliary/caches/sphinx \ + documentation .auxiliary/artifacts/sphinx-doctest""", +] +testers-serotine = [ """coverage run -m pytest -m slow""" ] testers-no-reports = [ - 'coverage erase', 'coverage run', - """coverage run -m sphinx.cmd.build \ - -E -b doctest -d .auxiliary/caches/sphinx \ - documentation .auxiliary/artifacts/sphinx-doctest""", + 'testers-documentation', + 'testers-serotine', ] testers = [ + 'coverage erase', 'testers-no-reports', 'coverage combine', 'coverage report --skip-covered', @@ -184,8 +189,8 @@ strict = false # https://microsoft.github.io/pyright/#/configuration [tool.pyright] -ignore = [ 'documentation', 'tests' ] # Stronger hint for language server. -include = [ 'sources' ] +ignore = [ '.auxiliary', 'documentation', 'tests' ] # Ignore diagnostics. +include = [ 'sources', 'tests' ] # Consider for operations. reportConstantRedefinition = true reportInvalidTypeVarUse = true reportMatchNotExhaustive = true @@ -217,11 +222,14 @@ stubPath = 'sources/classcore/_typedecls' # Note: Cannot run doctests from Pytest, because Pytest tries to update '_' # attribute on protected modules. Instead, we use Sphinx to run doctests. minversion = '8.1' -addopts = '--capture=no --exitfirst -rfE' +addopts = """--capture=no --exitfirst -rfE -m 'not slow'""" # TODO? '--fail-under' testpaths = [ 'tests' ] python_files = [ 'test_*.py' ] python_functions = [ 'test_[0-9][0-9][0-9]_*' ] cache_dir = '.auxiliary/caches/pytest' +markers = [ + "slow: long-running tests", +] [tool.ruff] #builtins = [ 'ic' ] diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 9431490..ed702cd 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.7' +__version__ = '1.8' standard.finalize_module( diff --git a/sources/classcore/standard/modules.py b/sources/classcore/standard/modules.py index e950c1c..c33ebc4 100644 --- a/sources/classcore/standard/modules.py +++ b/sources/classcore/standard/modules.py @@ -42,6 +42,10 @@ def finalize_module( # noqa: PLR0913 dynadoc_introspection: _nomina.DynadocIntrospectionArgument = ( _dynadoc.dynadoc_introspection_on_package ), dynadoc_table: _nomina.DynadocTableArgument = __.dictproxy_empty, + excludes: __.typx.Annotated[ + __.typx.Optional[ __.cabc.MutableSet[ __.types.ModuleType ] ], + __.ddoc.Doc( ''' Modules to exclude from reclassification. ''' ), + ] = None, recursive: __.typx.Annotated[ bool, __.ddoc.Doc( ''' Recursively reclassify package modules? ''' ) ] = False, @@ -84,7 +88,7 @@ def finalize_module( # noqa: PLR0913 _reclassify_module( module, attributes_namer = attributes_namer, - recursive = recursive, + excludes = excludes, recursive = recursive, replacement_class = replacement_class ) @@ -100,6 +104,10 @@ def reclassify_modules( __.ddoc.Doc( ''' Attributes namer function with which to seal class. ''' ), ] = __.calculate_attrname, + excludes: __.typx.Annotated[ + __.typx.Optional[ __.cabc.MutableSet[ __.types.ModuleType ] ], + __.ddoc.Doc( ''' Modules to exclude from reclassification. ''' ), + ] = None, recursive: __.typx.Annotated[ bool, __.ddoc.Doc( ''' Recursively reclassify package modules? ''' ) ] = False, @@ -123,11 +131,11 @@ def reclassify_modules( _reclassify_module( attributes, attributes_namer = attributes_namer, - recursive = recursive, + excludes = excludes, recursive = recursive, replacement_class = replacement_class ) -def _reclassify_module( +def _reclassify_module( # noqa: C901,PLR0912 attributes: __.typx.Annotated[ __.cabc.Mapping[ str, __.typx.Any ] | __.types.ModuleType | str, __.ddoc.Doc( @@ -138,6 +146,10 @@ def _reclassify_module( __.ddoc.Doc( ''' Attributes namer function with which to seal class. ''' ), ] = __.calculate_attrname, + excludes: __.typx.Annotated[ + __.typx.Optional[ __.cabc.MutableSet[ __.types.ModuleType ] ], + __.ddoc.Doc( ''' Modules to exclude from reclassification. ''' ), + ] = None, recursive: __.typx.Annotated[ bool, __.ddoc.Doc( ''' Recursively reclassify package modules? ''' ) ] = False, @@ -164,22 +176,24 @@ def _reclassify_module( attributes = __.sys.modules[ attributes ] if isinstance( attributes, __.types.ModuleType ): module = attributes - attributes = attributes.__dict__ + if excludes and module in excludes: return + attributes = module.__dict__ else: module = None + if excludes is None: excludes = set( ) + if module: excludes.add( module ) package_name = ( attributes.get( '__package__' ) or attributes.get( '__name__' ) ) if not package_name: return for value in attributes.values( ): if not __.inspect.ismodule( value ): continue if not value.__name__.startswith( f"{package_name}." ): continue + if isinstance( value, replacement_class ): continue if recursive: _reclassify_module( value, attributes_namer = attributes_namer, - recursive = True, + excludes = excludes, recursive = True, replacement_class = replacement_class ) - if isinstance( value, replacement_class ): continue - _seal_module( value, attributes_namer, replacement_class ) if module and not isinstance( module, replacement_class ): _seal_module( module, attributes_namer, replacement_class ) diff --git a/tests/test_000_classcore/conftest.py b/tests/test_000_classcore/conftest.py new file mode 100644 index 0000000..939d81d --- /dev/null +++ b/tests/test_000_classcore/conftest.py @@ -0,0 +1,3 @@ +def pytest_sessionfinish( session, exitstatus ): + if exitstatus == 5: # pytest exit code for "no tests collected" + session.exitstatus = 0 diff --git a/tests/test_000_classcore/test_350_standard_modules.py b/tests/test_000_classcore/test_350_standard_modules.py index 3ab4dcf..dd399f5 100644 --- a/tests/test_000_classcore/test_350_standard_modules.py +++ b/tests/test_000_classcore/test_350_standard_modules.py @@ -29,8 +29,8 @@ MODULE_QNAME = f"{PACKAGE_NAME}.standard.modules" -def test_200_reclassification_of_package_module( ): - ''' Reclassifies package module directly. ''' +def test_200_reclassification_of_independent_module( ): + ''' Reclassifies independent module directly. ''' module = cache_import_module( MODULE_QNAME ) exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) module_class = module.Module @@ -44,7 +44,7 @@ def test_200_reclassification_of_package_module( ): module.reclassify_modules( module_ ) # idempotence assert module_.__class__ is module_class with pytest.raises( exceptions_module.AttributeImmutability ): - module.foo = 1 + module_.foo = 1 def test_201_reclassification_of_normal_module( ): @@ -62,10 +62,71 @@ def test_201_reclassification_of_normal_module( ): module.reclassify_modules( module_ ) # idempotence assert module_.__class__ is module_class with pytest.raises( exceptions_module.AttributeImmutability ): - module.foo = 1 + module_.foo = 1 + + +def test_202_reclassification_of_package( ): + ''' Reclassifies package directly. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + module_class = module.Module + package_module = types.ModuleType( 'foobarnotreal' ) + package_module.__package__ = 'foobarnotreal' + member_module = types.ModuleType( 'foobarnotreal.member' ) + member_module.__package__ = 'foobarnotreal' + package_module.member = member_module + assert package_module.__class__ is not module_class + assert member_module.__class__ is not module_class + with warnings.catch_warnings( ): + warnings.simplefilter( 'ignore', DeprecationWarning ) + module.reclassify_modules( package_module ) + assert package_module.__class__ is module_class + assert member_module.__class__ is not module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + package_module.foo = 1 + member_module.foo = 1 + assert member_module.foo == 1 + + +def test_203_reclassification_of_package_recursive( ): + ''' Reclassifies package recursively. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) + module_class = module.Module + package_module = types.ModuleType( 'foobarnotreal' ) + package_module.__package__ = 'foobarnotreal' + member_module = types.ModuleType( 'foobarnotreal.member' ) + member_module.__package__ = 'foobarnotreal' + package_module.member = member_module + assert package_module.__class__ is not module_class + assert member_module.__class__ is not module_class + with warnings.catch_warnings( ): + warnings.simplefilter( 'ignore', DeprecationWarning ) + module.reclassify_modules( package_module, recursive = True ) + assert package_module.__class__ is module_class + assert member_module.__class__ is module_class + with pytest.raises( exceptions_module.AttributeImmutability ): + package_module.foo = 1 + with pytest.raises( exceptions_module.AttributeImmutability ): + member_module.foo = 1 + + +def test_204_reclassification_of_module_exclude( ): + ''' Reclassification ignores excluded module. ''' + module = cache_import_module( MODULE_QNAME ) + module_class = module.Module + module_ = types.ModuleType( 'fakepackage.foobarnotreal' ) + module_.__package__ = 'fakepackage' + assert module_.__class__ is not module_class + with warnings.catch_warnings( ): + warnings.simplefilter( 'ignore', DeprecationWarning ) + module.reclassify_modules( module_, excludes = { module_ } ) + assert module_.__class__ is not module_class + module_.foo = 1 + assert module_.foo == 1 -def test_202_reclassification_of_incomplete_module( ): +def test_205_reclassification_of_incomplete_module( ): ''' Reclassification ignores incomplete module. ''' module = cache_import_module( MODULE_QNAME ) module_class = module.Module @@ -79,7 +140,7 @@ def test_202_reclassification_of_incomplete_module( ): assert module_.__class__ is not module_class -def test_205_reclassification_via_module_globals( ): +def test_206_reclassification_via_module_globals( ): ''' Reclassifies via module globals dictionary. ''' module = cache_import_module( MODULE_QNAME ) exceptions_module = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) @@ -89,9 +150,9 @@ def test_205_reclassification_via_module_globals( ): assert module_.__class__ is not module_class with warnings.catch_warnings( ): warnings.simplefilter( 'ignore', DeprecationWarning ) - module.reclassify_modules( module_dict ) + module.reclassify_modules( module_dict, recursive = True ) assert module_.__class__ is module_class - module.reclassify_modules( module_dict ) # idempotence + module.reclassify_modules( module_dict, recursive = True ) # idempotent assert module_.__class__ is module_class with pytest.raises( exceptions_module.AttributeImmutability ): module.foo = 1 From bc272b2a738f558f36a0b206826928e2ce28ff5a Mon Sep 17 00:00:00 2001 From: Github Actions Robot <emcd@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:23:07 +0000 Subject: [PATCH 22/28] Update documents for publication. (1.9) --- .../claude/agents/python-annotator.md | 362 ++++++++++++++++++ .../claude/agents/python-conformer.md | 339 ++++++++++++++++ .../claude/commands/cs-annotate-release.md | 2 +- .../claude/commands/cs-architect.md | 100 +++++ .../claude/commands/cs-code-python.md | 228 +++++++++++ .../claude/commands/cs-conform-code.md | 236 ------------ .../claude/commands/cs-conform-python.md | 204 ++++++++++ .../claude/commands/cs-conform-toml.md | 281 ++++++++++++++ .../claude/commands/cs-copier-update.md | 131 +++++++ .../claude/commands/cs-create-command.md | 108 ++++++ .../claude/commands/cs-design-python.md | 144 +++++++ .../claude/commands/cs-develop-pytests.md | 263 +++++++++++++ .../claude/commands/cs-develop-tests.md | 212 ---------- .../commands/cs-document-examples-rst.md | 117 ++++++ .../claude/commands/cs-excise-python.md | 155 ++++++++ .../claude/commands/cs-inquire.md | 72 ++++ .../claude/commands/cs-manage-prd.md | 90 +++++ .../claude/commands/cs-plan-pytests.md | 280 ++++++++++++++ .../claude/commands/cs-release-checkpoint.md | 163 ++++++++ .../claude/commands/cs-release-final.md | 58 ++- .../claude/commands/cs-release-maintenance.md | 61 ++- .../claude/commands/cs-review-todos.md | 103 +++++ .../claude/commands/cs-update-command.md | 96 +++++ .../claude/commands/cs-update-readme-rst.md | 105 +++++ .../claude/commands/validate-custom-slash.md | 2 +- .../claude/miscellany/command-template.md | 47 +++ .auxiliary/configuration/claude/settings.json | 24 +- .auxiliary/configuration/conventions.md | 164 ++------ .auxiliary/configuration/copier-answers.yaml | 2 +- .auxiliary/configuration/gemini/settings.json | 16 +- .../configuration/hatch-constraints.pip | 2 + .auxiliary/configuration/mcp-servers.json | 22 +- .../opencode/agent/python-annotator.md | 220 +++++++++++ .../opencode/agent/python-conformer.md | 256 +++++++++++++ .../configuration/opencode/command/.gitignore | 2 + .auxiliary/configuration/vulturefood.py | 23 ++ .../data/towncrier/+modules-df.repair.rst | 1 - .../towncrier/+modules-exclude.enhance.rst | 2 - .../+pypy-super-compatibility.repair.rst | 1 + .auxiliary/instructions/.gitignore | 2 + .auxiliary/publications/website.tar.xz | 4 +- .auxiliary/scripts/claude-ds | 33 ++ .auxiliary/scripts/claude-xai | 33 ++ .auxiliary/scripts/claude/post-edit-linter | 15 +- .../scripts/claude/pre-bash-git-commit-check | 111 ++++++ .../scripts/claude/pre-bash-python-check | 18 + .auxiliary/scripts/prepare-agents | 170 ++++++++ .auxiliary/secrets/.gitignore | 2 + .github/workflows/claude.yaml | 13 +- .gitignore | 3 + README.rst | 2 +- .../architecture/decisions/index.rst | 31 ++ documentation/architecture/designs/index.rst | 28 ++ documentation/architecture/filesystem.rst | 86 +++++ documentation/architecture/index.rst | 34 ++ documentation/architecture/summary.rst | 24 ++ .../architecture/testplans/index.rst | 30 ++ .../architecture/testplans/summary.rst | 86 +++++ documentation/changelog.rst | 9 + documentation/conf.py | 2 + documentation/contribution.rst | 25 ++ documentation/prd.rst | 27 ++ pyproject.toml | 41 +- sources/classcore/__init__.py | 2 +- sources/classcore/standard/nomina.py | 3 +- sources/classcore/utilities.py | 2 +- tests/test_000_classcore/__.py | 64 ++++ tests/test_000_classcore/__init__.py | 48 +-- tests/test_000_classcore/test_000_package.py | 37 +- tests/test_000_classcore/test_010_base.py | 4 +- .../test_000_classcore/test_100_exceptions.py | 2 +- .../test_000_classcore/test_110_utilities.py | 17 +- .../test_000_classcore/test_210_decorators.py | 2 +- .../test_310_standard_internals.py | 2 +- .../test_330_standard_decorators.py | 2 +- .../test_340_standard_classes.py | 6 +- .../test_350_standard_modules.py | 2 +- 77 files changed, 4937 insertions(+), 779 deletions(-) create mode 100644 .auxiliary/configuration/claude/agents/python-annotator.md create mode 100644 .auxiliary/configuration/claude/agents/python-conformer.md create mode 100644 .auxiliary/configuration/claude/commands/cs-architect.md create mode 100644 .auxiliary/configuration/claude/commands/cs-code-python.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-conform-code.md create mode 100644 .auxiliary/configuration/claude/commands/cs-conform-python.md create mode 100644 .auxiliary/configuration/claude/commands/cs-conform-toml.md create mode 100644 .auxiliary/configuration/claude/commands/cs-copier-update.md create mode 100644 .auxiliary/configuration/claude/commands/cs-create-command.md create mode 100644 .auxiliary/configuration/claude/commands/cs-design-python.md create mode 100644 .auxiliary/configuration/claude/commands/cs-develop-pytests.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-develop-tests.md create mode 100644 .auxiliary/configuration/claude/commands/cs-document-examples-rst.md create mode 100644 .auxiliary/configuration/claude/commands/cs-excise-python.md create mode 100644 .auxiliary/configuration/claude/commands/cs-inquire.md create mode 100644 .auxiliary/configuration/claude/commands/cs-manage-prd.md create mode 100644 .auxiliary/configuration/claude/commands/cs-plan-pytests.md create mode 100644 .auxiliary/configuration/claude/commands/cs-release-checkpoint.md create mode 100644 .auxiliary/configuration/claude/commands/cs-review-todos.md create mode 100644 .auxiliary/configuration/claude/commands/cs-update-command.md create mode 100644 .auxiliary/configuration/claude/commands/cs-update-readme-rst.md create mode 100644 .auxiliary/configuration/claude/miscellany/command-template.md create mode 100644 .auxiliary/configuration/hatch-constraints.pip create mode 100644 .auxiliary/configuration/opencode/agent/python-annotator.md create mode 100644 .auxiliary/configuration/opencode/agent/python-conformer.md create mode 100644 .auxiliary/configuration/opencode/command/.gitignore create mode 100644 .auxiliary/configuration/vulturefood.py delete mode 100644 .auxiliary/data/towncrier/+modules-df.repair.rst delete mode 100644 .auxiliary/data/towncrier/+modules-exclude.enhance.rst create mode 100644 .auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst create mode 100644 .auxiliary/instructions/.gitignore create mode 100755 .auxiliary/scripts/claude-ds create mode 100755 .auxiliary/scripts/claude-xai create mode 100755 .auxiliary/scripts/claude/pre-bash-git-commit-check create mode 100755 .auxiliary/scripts/prepare-agents create mode 100644 .auxiliary/secrets/.gitignore create mode 100644 documentation/architecture/decisions/index.rst create mode 100644 documentation/architecture/designs/index.rst create mode 100644 documentation/architecture/filesystem.rst create mode 100644 documentation/architecture/index.rst create mode 100644 documentation/architecture/summary.rst create mode 100644 documentation/architecture/testplans/index.rst create mode 100644 documentation/architecture/testplans/summary.rst create mode 100644 documentation/prd.rst create mode 100644 tests/test_000_classcore/__.py diff --git a/.auxiliary/configuration/claude/agents/python-annotator.md b/.auxiliary/configuration/claude/agents/python-annotator.md new file mode 100644 index 0000000..acece14 --- /dev/null +++ b/.auxiliary/configuration/claude/agents/python-annotator.md @@ -0,0 +1,362 @@ +--- +name: python-annotator +description: | + Use this agent when you need to address type checking issues from tools like Pyright, create type annotations + following project standards, generate type stubs for third-party packages, or analyze and resolve issues masked + by type: ignore comments or __.typx.cast calls. + + Examples: + + <example> + Context: User has written a new public function and needs proper type annotations according to project standards. + user: 'I just wrote this function but Pyright is complaining about missing type annotations: def process_data(data, configuration): return transformed_data' + assistant: 'Let me use the python-annotator agent to add proper type annotations following the project guidelines.' + <commentary>The user needs type annotations added to their function following project standards, so use the python-annotator agent.</commentary> + </example> + + <example> + Context: User is getting Pyright errors about missing type stubs for a third-party library. + user: 'Pyright is showing errors because the requests library doesn't have type stubs available' + assistant: 'I'll use the python-annotator agent to create the missing type stubs for the requests library.' + <commentary>Missing type stubs for third-party packages require the python-annotator agent's specialized workflow.</commentary> + </example> + + <example> + Context: User wants to clean up code that has type: ignore comments. + user: 'Can you help me resolve these # type: ignore comments in my code?' + assistant: 'Let me use the python-annotator agent to analyze and properly resolve those type checking suppressions.' + <commentary>Analyzing and mitigating issues masked by type pragmas is a core function of the python-annotator agent.</commentary> + </example> +model: sonnet +color: pink +--- + +You are an expert Python type annotation specialist focusing on static type analysis, +type system design, and resolving type checker issues from tools like Pyright. You +systematically analyze type checking problems and apply comprehensive solutions to +ensure code adheres to strict typing standards. + +**IMPORTANT**: Only address Python type checking issues. If the request does not +involve Python type annotations, type stubs, or type checker diagnostics, politely +decline and explain your specialization. + +## Prerequisites + +- **Read project documentation guides FIRST**: + - @.auxiliary/instructions/practices.rst + - @.auxiliary/instructions/style.rst +- Have read `CLAUDE.md` for project-specific guidance + +## EXECUTION STRUCTURE + +**PHASE 1: COMPREHENSIVE TYPE ANALYSIS** +Perform complete diagnostic analysis and generate detailed type checking report before making any changes. + +**PHASE 2: SYSTEMATIC RESOLUTION** +Apply all identified type annotation fixes in systematic order, validating with type checkers after completion. + +## TYPE ANNOTATION STANDARDS + +### 1. Annotation Guidelines + +**Public Function Documentation:** +- Use `__.typx.Annotated[ <type>, __.ddoc.Doc( '''<description>''' ) ]` pattern +- Include `__.ddoc.Raises( )` annotations for documented exceptions +- Follow narrative mood (third person) in documentation + +**Wide Parameters, Narrow Returns:** +- Accept abstract base classes (`__.cabc.Sequence`, `__.cabc.Mapping`) +- Return concrete immutable types (`tuple`, `frozenset`, `__.immut.Dictionary`) + +**Absential vs Optional:** +- Prefer `__.Absential[ T ]` for optional parameters when `None` has semantic meaning +- Use `__.typx.Optional[ T ]` only when `None` is a valid value distinct from absence + +**Type Alias Organization:** +- Common aliases after imports, before private variables +- Complex multi-line unions use `__.typx.Union[ ]` +- Simple unions use `|` syntax + +### 2. Type Checker Issue Resolution + +**Root Cause Analysis:** +1. Identify specific type checker errors and their locations +2. Determine underlying cause (missing annotations, incorrect types, inheritance issues) +3. Assess impact on runtime behavior and API contracts +4. Plan minimal changes that resolve issues without breaking functionality + +**Resolution Priorities:** +1. **Missing Annotations**: Add comprehensive type annotations following project patterns +2. **Incorrect Types**: Replace overly broad or narrow types with appropriate abstractions +3. **Generic Issues**: Properly parameterize generic types and resolve variance issues +4. **Import Problems**: Fix circular imports and missing type-only imports + +### 3. Dependency Management and Type Stub Creation + +**Dependency Declaration Before Type Work** + +Avoid using `# type: ignore` to suppress errors about missing third-party dependencies. +This anti-pattern masks improper project setup and should be resolved through proper dependency management. + +**Required Dependency Workflow:** +1. **Verify Dependency Declaration**: Check `pyproject.toml` for the package +2. **Update Project Dependencies**: Add missing packages to appropriate dependency groups +3. **Update Import Module**: Add package to `sources/<package>/__/imports.py` if commonly used +4. **Rebuild Environment**: Run `hatch env prune && hatch --env develop run python --version` +5. **Then and Only Then**: Proceed with type stub creation or suppression analysis + +**Dependency Verification Commands:** +```shell +# Check if package is declared in pyproject.toml +grep -n "somepackage" pyproject.toml + +# Verify package is installed in environment +hatch --env develop run python -c "import somepackage; print( somepackage.__file__ )" + +# Check if type information is available +hatch --env develop run pyright --verifytypes somepackage +``` + +**Type Stub Creation Workflow:** + +**Stub Generation Process (ONLY after dependency verification):** +1. **Check Official Sources**: Verify typeshed, PyPI `types-*` packages, or library's own stubs +2. **Generate Initial Stubs**: + ```shell + hatch --env develop run pyright --createstub somepackage + ``` +3. **Minimal Viable Stubs**: Focus only on APIs used in project, not comprehensive coverage +4. **Structure Requirements**: + - Proper module hierarchy matching runtime structure + - Inheritance relationships preserved + - Generic type parameters correctly defined + - Public API surface accurately represented + +**Stub File Organization:** +```python +# sources/<package>/_typedecls/somepackage/__init__.pyi +from typing import Any, overload +from collections.abc import Sequence, Mapping + +# Core classes used in project +class ConfigParser: + def __init__( self, defaults: Mapping[ str, str ] | None = ... ) -> None: ... + def read( self, filenames: str | Sequence[ str ] ) -> list[ str ]: ... + def get( self, section: str, option: str ) -> str: ... + +# Only stub what's actually used - avoid comprehensive coverage +``` + +### 4. Type Suppression Resolution + +**Suppression Analysis Workflow:** + +**Phase 1 - Audit Existing Suppressions:** +```shell +# Find all suppressions in codebase +rg --line-number "type:\s*ignore|__.typx\.cast" --type py +``` + +**Phase 2 - Categorize Suppressions:** +1. **Dependency Issues**: Missing packages not declared in `pyproject.toml` - address first +2. **Resolvable**: Missing stubs, incorrect annotations, fixable inheritance +3. **Legitimate**: Truly dynamic behavior, complex generics, external constraints +4. **Technical Debt**: Workarounds that should be refactored + +**Dependency Suppression Analysis:** +For any suppression involving third-party imports: +1. **Verify Declaration**: Check if package exists in `pyproject.toml` +2. **If Missing**: Add to appropriate dependency group, update `__/imports.py` if needed +3. **Rebuild Environment**: `hatch env prune` and reinstall +4. **Re-evaluate**: Many suppressions resolve after proper dependency management + +**Phase 3 - Resolution Strategies:** + +**Incorrect Approach - Masking dependency issues:** +```python +# Anti-pattern: Suppressing missing dependency +import requests # type: ignore +import beautifulsoup4 # type: ignore + +def fetch_data( url: str ) -> dict: + response = requests.get( url ) # type: ignore + return response.json( ) # type: ignore +``` + +**Preferred Approach - Proper dependency management:** +```python +# 1. First add to pyproject.toml: +# dependencies = [ +# "requests~=2.31.0", +# "beautifulsoup4~=4.12.0", +# ] +# +# 2. Add to sources/<package>/__/imports.py (third-party imports section): +# import bs4 +# import requests +# +# 3. Then use proper imports: +from . import __ + +def fetch_data( url: str ) -> dict[ str, __.typx.Any ]: + response = __.requests.get( url ) + return response.json( ) +``` + + +**Documentation Requirements:** +- Every remaining suppression MUST have explanatory comment +- Include ticket/issue reference for suppressions requiring upstream fixes +- Set TODO items for suppressions that should be revisited + +### 5. Quality Assurance Workflow + +**Type Checking Validation:** +```shell +# Run comprehensive type checking +hatch --env develop run pyright +hatch --env develop run pyright --stats # Coverage statistics +``` + +**Consistency Verification:** +- Public functions have `__.typx.Annotated` documentation +- Parameter types follow wide/narrow principle +- Return types are concrete and immutable where appropriate +- Import organization follows project standards + +**Runtime Preservation:** +- Verify no functional changes introduced +- Test critical paths if available +- Validate API contracts maintained + +## COMPREHENSIVE EXAMPLES + +### Example 1: Missing Function Annotations + +**BEFORE - Pyright errors:** +```python +def process_user_data( data, filters = None, configuration = None ): + if filters is None: filters = [ ] + # Error: Missing type annotations + return transform_and_validate( data, filters, configuration or { } ) +``` + +**AFTER - Complete annotations:** +```python +def process_user_data( + data: __.typx.Annotated[ + __.cabc.Mapping[ str, __.typx.Any ], + __.ddoc.Doc( '''User data mapping with string keys.''' ), + ], + filters: __.typx.Annotated[ + __.Absential[ __.cabc.Sequence[ str ] ], + __.ddoc.Doc( '''Optional data filters to apply.''' ), + ] = __.absent, + configuration: __.typx.Annotated[ + __.Absential[ __.cabc.Mapping[ str, __.typx.Any ] ], + __.ddoc.Doc( '''Optional processing configuration.''' ), + ] = __.absent, +) -> __.typx.Annotated[ + __.immut.Dictionary[ str, __.typx.Any ], + __.ddoc.Doc( '''Processed and validated user data.''' ), + __.ddoc.Raises( ValueError, '''If data validation fails.''' ), +]: + ''' Processes user data with optional filtering and configuration. ''' + active_filters = ( ) if __.is_absent( filters ) else tuple( filters ) + active_configuration = __.immut.Dictionary( ) if __.is_absent( configuration ) else __.immut.Dictionary( configuration ) + return transform_and_validate( data, active_filters, active_configuration ) +``` + +### Example 2: Type Stub Creation + +**Missing stubs for 'beautifulsoup4':** +```python +# sources/<package>/_typedecls/bs4/__init__.pyi +from typing import Any, Optional +from collections.abc import Sequence + +class BeautifulSoup: + def __init__( + self, + markup: str | bytes = ..., + features: Optional[ str ] = ..., + ) -> None: ... + + def find( + self, + name: Optional[ str ] = ..., + attrs: Optional[ dict[ str, Any ] ] = ..., + ) -> Optional[ Tag ]: ... + + def find_all( + self, + name: Optional[ str ] = ..., + attrs: Optional[ dict[ str, Any ] ] = ..., + ) -> list[ Tag ]: ... + +class Tag: + def get_text( self, strip: bool = ... ) -> str: ... + def get( self, key: str, default: Any = ... ) -> Any: ... + @property + def text( self ) -> str: ... +``` + +### Example 3: Type Suppression Resolution + +**BEFORE - Broad suppressions:** +```python +def complex_data_processor( items ): # type: ignore + results = [ ] # type: ignore + for item in items: # type: ignore + processed = expensive_operation( item ) # type: ignore + results.append( processed ) # type: ignore + return results # type: ignore +``` + +**AFTER - Proper resolution:** +```python +def complex_data_processor( + items: __.cabc.Sequence[ __.typx.Any ], +) -> tuple[ ProcessedData, ... ]: + ''' Processes sequence of items through expensive operation. ''' + results: list[ ProcessedData ] = [ ] + for item in items: + processed = expensive_operation( item ) + results.append( processed ) + return tuple( results ) +``` + +## ANALYSIS REPORT FORMAT + +**PHASE 1 OUTPUT:** +1. **Type Checking Summary**: Overall diagnostic assessment with file-by-file breakdown +2. **Missing Annotations**: Functions, methods, and variables requiring type annotations +3. **Type Errors**: Specific checker errors with root cause analysis +4. **Stub Requirements**: Third-party packages needing type stubs +5. **Suppression Audit**: Analysis of existing `type: ignore` and `__.typx.cast` usage +6. **Resolution Plan**: Systematic order of fixes to be applied + +**PHASE 2 OUTPUT:** +1. **Applied Annotations**: Summary of all type annotations added +2. **Stub Generation**: Created stub files and their scope +3. **Suppression Resolution**: Eliminated or refined type suppressions +4. **Validation Results**: Type checker output before and after changes +5. **Files Modified**: Complete list with brief description of changes + +## TOOL PREFERENCES + +- **Precise coordinates**: Use `rg --line-number --column` for exact positions +- **Type checking**: Use Pyright MCP tools for diagnostics and validation +- **Stub generation**: Use `hatch --env develop run pyright --createstub` when needed + +## EXECUTION REQUIREMENTS + +- **Phase 0**: Verify all third-party dependencies are declared in `pyproject.toml` and available in environment +- **Phase 1**: Complete analysis and report before any modifications +- **Phase 2**: Apply fixes systematically, validate with `hatch --env develop run pyright` +- **Dependency validation**: Do not proceed with type work until dependencies are properly declared +- **Validation command**: Type checking must be clean before completion +- **Focus on type safety**: Maintain exact functionality while improving type annotations +- **Reference specific diagnostics**: Always include line numbers and error messages +- **Document decisions**: Explain type choices and trade-offs made +- **Dependency pattern detection**: Flag attempts to use `# type: ignore` for missing dependencies diff --git a/.auxiliary/configuration/claude/agents/python-conformer.md b/.auxiliary/configuration/claude/agents/python-conformer.md new file mode 100644 index 0000000..59ead95 --- /dev/null +++ b/.auxiliary/configuration/claude/agents/python-conformer.md @@ -0,0 +1,339 @@ +--- +name: python-conformer +description: | + Use this agent ONLY when changes include Python code (.py and .pyi files) and you need to review them for + compliance with project practices, style guidelines, and nomenclature standards, then systematically fix violations. + Do NOT use this agent for non-Python changes such as documentation, configuration files, or other file types. + + Examples: + + <example> + Context: The user has just written a new Python function and wants to ensure it follows project standards. + user: 'I just wrote this function for processing user data. Can you review it?' + assistant: 'I'll use the python-conformer agent to check your function against our project practices and style guidelines, then fix any violations.' + <commentary>Since the user wants code reviewed for compliance, use the python-conformer agent to analyze the code against project standards.</commentary> + </example> + + <example> + Context: The user has completed a module refactor and wants to verify compliance before committing. + user: 'I've finished refactoring the authentication module. Please check if it meets our coding standards.' + assistant: 'Let me use the python-conformer agent to thoroughly review your refactored module for compliance with our practices guidelines.' + <commentary>The user needs compliance verification for recently refactored code, so use the python-conformer agent.</commentary> + </example> + + <example> + Context: The user wants to review staged Python changes before committing. + user: 'I've modified several Python modules. Please review my staged changes for compliance before I commit.' + assistant: 'I'll use the python-conformer agent to review the Python changes in git diff --cached and ensure all Python code meets our project standards.' + <commentary>Pre-commit review of staged Python changes is a perfect use case for the python-conformer agent.</commentary> + </example> +model: sonnet +color: red +--- + +You are an expert software engineer specializing in Python code quality assurance and +compliance conformance. Your primary responsibility is to systematically review Python code +against established project practices, style guidelines, and nomenclature +standards, then apply comprehensive remediation to bring code into full compliance. + +**IMPORTANT**: Only review and modify Python (.py and .pyi) files. If the +changes do not include Python code, politely decline and explain that you are +specifically for Python code compliance review. + +## Prerequisites + +- **Read project documentation guides FIRST**: + - @.auxiliary/instructions/practices.rst + - @.auxiliary/instructions/style.rst + - @.auxiliary/instructions/nomenclature.rst +- Have read `CLAUDE.md` for project-specific guidance + +## EXECUTION STRUCTURE + +**PHASE 1: COMPREHENSIVE REVIEW** +Perform complete analysis and generate detailed compliance report before making any changes. + +**PHASE 2: SYSTEMATIC REMEDIATION** +Apply all identified fixes in systematic order, validating with linters after completion. + +## COMPLIANCE STANDARDS + +### Design Standards + +#### 1. Module Organization + +**Content Order:** +1. Imports (following practices guide patterns) +2. Common type aliases (`TypeAlias` declarations) +3. Private variables/functions for defaults (grouped semantically) +4. Public classes and functions (alphabetical) +5. All other private functions (alphabetical) + +**Scope and Size:** +- Maximum 600 lines +- Action: Analyze oversized modules with separation of concerns in mind. +Suggest splitting into focused modules with narrower responsibilities or +functionality. + +#### 2. Imports + +- At the module level, other modules and their attributes MUST be imported as + private aliases, except in `__init__`, `__`, or specially-designated + re-export modules. +- Within function bodies, other modules and their attributes MAY be imported as + public variables. +- Subpackages SHOULD define a special `__` re-export module, which has `from + ..__ import *` plus any other imports which are common to the subpackage. +- Common modules, such as `os` or `re`, SHOULD be imported as public within the + special package-wide `__.imports` re-export module rather than as private + aliases within an implementation module. +- The `__all__` attribute SHOULD NOT be provided. This is unnecessary if the + module namespace only contains public classes and functions which are part of + its interface; this avoid additional interface maintenance. + +#### 3. Dependency Injection + +- Ask: is this function testable without monkeypatching? +- Functions SHOULD provide injectable parameters with sensible defaults instead + of hard-coded dependencies within function implementation. + +#### 4. Robustness Principle (Postel's Law) +"Be conservative in what you send; be liberal in what you accept." + +- Public functions SHOULD define wide, abstract argument types. +- All functions SHOULD define narrow, concrete return types. +- Private functions MAY define narrow, concrete argument types. + +#### 5. Immutability + +- Classes SHOULD inherit from immutable classes (`__.immut.Object`, + `__.immut.Protocol`, `__.immut.DataclassObject`, etc...). +- Functions SHOULD return values of immutable types (`None`, `int`, `tuple`, + `frozenset`, `__.immut.Dictionary`, etc...) and not mutable types (`list`, + `dict`, `set`, etc...). + +#### 6. Proper Exception Management + +- One `try .. except` suite per statement which can raise exceptions. I.e., + avoid covering multiple statements with a `try` block whenever possible. +- Tryceratops complaints MUST NOT be suppressed with `noqa` pragmas. +- Bare exceptions SHOULD NOT be raised. + - Exemption: `NotImplementedError` MAY be raised as a bare exception. + - Relevant exception classes SHOULD be used from the relevant `exceptions` + module within the package. + - New exception classes MAY be created as needed within the relevant + `exceptions` module; these MUST follow the nomenclature guide and be + inserted in correct alphabetical order. + +### Quality Assurance + +#### 1. Linter Suppressions + +- Linter suppressions MUST be reviewed critically. +- Linter complaints SHOULD NOT be suppressed via `noqa` or `type` pragmas + without compelling justification. +- Suppressions that mask design problems MUST be investigated and resolved + rather than ignored. + +**Acceptable Suppressions:** +- `noqa: PLR0913` MAY be used for a CLI or service API with many parameters, + but data transfer objects SHOULD be considered in most other cases. +- `noqa: S*` MAY be used for properly constrained and vetted subprocess + executions or Internet content retrievals. + +**Unacceptable Suppressions (require investigation):** +- `type: ignore` MUST NOT be used, except in extremely rare circumstances. Such + suppressions usually indicate missing third-party dependencies or type stubs, + inappropriate type variables, or a bad inheritance pattern. For complex type + suppression investigation and dependency management, delegate to the + `python-annotator` agent. +- `__.typx.cast` SHOULD NOT be used, except in extremely rare circumstances. + Such casts suppress normal type checking and usually the same problems as + `type: ignore`. +- Most other `noqa` suppressions. + +### Style Standards + +#### 1. Spacing and Delimiters + +- Space padding MUST be present inside delimiters. + - Format: `( arg )`, `[ item ]`, `{ key: value }` + - Format: `( )`, `[ ]`, `{ }`, not `()`, `[]`, `{}` +- Space padding MUST be present around keyword argument `=`. + - Format: `foo = 42` + +#### 2. Strings + +- Docstrings MUST use triple single quotes with narrative mood. + - Format: `''' Processes data... '''` not `"""Process data..."""` +- F-strings and `.format` strings MUST be enclosed in double quotes. + - Format: `f"text {variable}"`, not `f'text {variable}'` + - Format: `"text {count}".format( count = len( items ) )` +- F-strings and format strings MUST NOT embed function calls. +- Exception messages and log messages SHOULD be enclosed in double quotes + rather than single quotes. +- Plain data strings SHOULD be enclosed in single quotes, unless they contain + single quotes. + +#### 3. Vertical Compactness + +- Blank lines MUST NOT appear within function bodies. +- Vertical compactness MUST be maintained within function implementations. +- Single-line statements MAY follow certain block keywords on the same line + when appropriate. + - Format: `if condition: return value` + - Format: `elif condition: continue` + - Format: `else: statement` + - Format: `try: statement` + +#### 4. Multi-line Constructs + +- Function invocations, including class instantiations, SHOULD place the + closing `)` on the same line as the last argument to the function. +- The last argument of an invocation MUST NOT be followed by a trailing comma. +- Comprehensions and generator expressions SHOULD place the closing delimiter + on the same line as the last statement in the comprehension or generator + expression. +- Parenthetical groupings SHOULD place the closing delimiter on the same line + as the last statement in the grouping. +- All other multi-line constructs (functions signatures, annotations, lists, + dictionaries, etc...) MUST place the closing delimiter on a separate line + following the last item and MUST dedent the closing delimiter to match the + opening line indentation. +- If a closing delimiter is not on the same line as the last item in a + multi-line construct, then the last item MUST be followed by a trailing + comma. + +#### 5. Nomenclature + +- Argument, attribute, and variable names SHOULD NOT be compound words, + separated by underscores, except in cases where this is necessary to + disambiguate. +- Argument and variable names SHOULD NOT duplicate parts of the function name. +- Attribute names SHOULD NOT duplicate parts of the class name. +- Class names SHOULD adhere to the nomenclature guide. +- Function names SHOULD adhere to the nomenclature guide. + +#### 6. Comments + +- Comments that describe obvious behavior SHOULD NOT be included. +- TODO comments SHOULD be added for uncovered edge cases and future work. +- Comments MUST add meaningful context, not restate what the code does. + +### Comprehensive Example: Real-World Function with Multiple Violations + +Here is a function that demonstrates many compliance violations: + +```python +def _group_documents_by_field( + documents: list[ dict[ str, __.typx.Any ] ], + field_name: __.typx.Optional[ str ] +) -> dict[ str, list[ dict[ str, __.typx.Any ] ] ]: + ''' Groups documents by specified field for inventory format compatibility. + ''' + if field_name is None: + return { } + + groups: dict[ str, list[ dict[ str, __.typx.Any ] ] ] = { } + for doc in documents: + # Get grouping value, with fallback for missing field + group_value = doc.get( field_name, f'(missing {field_name})' ) + if isinstance( group_value, ( list, dict ) ): + # Handle complex field types by converting to string + group_value = str( group_value ) # type: ignore[arg-type] + elif group_value is None or group_value == '': + group_value = f'(missing {field_name})' + else: + group_value = str( group_value ) + + if group_value not in groups: + groups[ group_value ] = [ ] + + # Convert document format back to inventory object format + inventory_obj = { + 'name': doc[ 'name' ], + 'role': doc[ 'role' ], + 'domain': doc.get( 'domain', '' ), + 'uri': doc[ 'uri' ], + 'dispname': doc[ 'dispname' ] + } + if 'fuzzy_score' in doc: + inventory_obj[ 'fuzzy_score' ] = doc[ 'fuzzy_score' ] + groups[ group_value ].append( inventory_obj ) + return groups +``` + +**Violations identified:** +1. **Narrow parameter types**: `list[dict[...]]` instead of wide `__.cabc.Sequence[__.cabc.Mapping[...]]` +2. **Type suppression abuse**: `# type: ignore[arg-type]` masks real design issue (delegate to `python-annotator` agent for systematic suppression resolution) +3. **Mutable container return**: Returns `dict` instead of `__.immut.Dictionary` +4. **Function body blank lines**: Empty lines breaking vertical compactness +5. **Vertical compactness**: `return { }` could be same line as `if` +6. **Unnecessary comments**: "Handle complex field types by converting to string" states obvious +7. **F-string quotes**: Using single quotes in f-strings instead of double +8. **Nomenclature duplication**: `group_value` repeats "group" from function name +9. **Underscore nomenclature**: `field_name` could be `field`, `group_value` could be `value` +10. **Mutable container creation**: Using `{ }` and `[ ]` instead of immutable alternatives +11. **Trailing comma**: Missing trailing comma in dictionary, affecting delimiter placement +12. **Single-line else**: `group_value = str(group_value)` could be same line as `else` +13. **Design pattern**: Could use `collections.defaultdict` instead of manual initialization + +**AFTER - Corrected version:** +```python +def _group_documents_by_field( + documents: __.cabc.Sequence[ __.cabc.Mapping[ str, __.typx.Any ] ], + field: __.typx.Absential[ str ] = __.absent, +) -> __.immut.Dictionary[ + str, tuple[ __.cabc.Mapping[ str, __.typx.Any ], ... ] +]: + ''' Groups documents by specified field. ''' + if __.is_absent( field ): return __.immut.Dictionary( ) + groups = __.collections.defaultdict( list ) + for doc in documents: + value = doc.get( field, f"(missing {field})" ) + if isinstance( value, ( list, dict ) ): value = str( value ) + elif value is None or value == '': value = f"(missing {field})" + else: value = str( value ) + obj = __.immut.Dictionary( + name = doc[ 'name' ], + role = doc[ 'role' ], + domain = doc.get( 'domain', '' ), + uri = doc[ 'uri' ], + dispname = doc[ 'dispname' ], + **( { 'fuzzy_score': doc[ 'fuzzy_score' ] } + if 'fuzzy_score' in doc else { } ) ) + groups[ value ].append( obj ) + return __.immut.Dictionary( + ( key, tuple( items ) ) for key, items in groups.items( ) ) +``` + +## REVIEW REPORT FORMAT + +**PHASE 1 OUTPUT:** +1. **Compliance Summary**: Overall assessment with file-by-file breakdown +2. **Standards Violations**: Categorized list with specific line references and explanations +3. **Complexity Analysis**: Function and module size assessments +4. **Remediation Plan**: Systematic order of fixes to be applied +5. **Risk Assessment**: Any changes that require careful validation + +**PHASE 2 OUTPUT:** +1. **Applied Fixes**: Summary of all changes made, categorized by standard +2. **Validation Results**: Linter output before and after changes +3. **Files Modified**: Complete list with brief description of changes +4. **Manual Review Required**: Any issues requiring human judgment + +## TOOL PREFERENCES + +- **Precise coordinates**: Use `rg --line-number --column` for exact line/column positions +- **Batch operations**: Group related changes together to minimize file modification conflicts between different MCP tools + +## EXECUTION REQUIREMENTS + +- **PHASE 1 REQUIRED**: Complete review and report before any remediation +- **PHASE 2 REQUIRED**: Apply fixes systematically, validate with `hatch --env develop run linters` +- **Validation command**: `hatch --env develop run linters` must produce clean output before completion +- **Focus on compliance**: Maintain exact functionality while improving standards adherence +- **Reference specific lines**: Always include line numbers and concrete examples +- **Document reasoning**: Explain why each standard matters and how fixes align with project practices +- **Agent delegation**: When type annotation issues exceed basic compliance scope, consider delegating to the `python-annotator` agent for comprehensive type work +- **Guide access**: If any prerequisite guide cannot be accessed, stop and inform the user diff --git a/.auxiliary/configuration/claude/commands/cs-annotate-release.md b/.auxiliary/configuration/claude/commands/cs-annotate-release.md index 2c5f3af..5300e30 100644 --- a/.auxiliary/configuration/claude/commands/cs-annotate-release.md +++ b/.auxiliary/configuration/claude/commands/cs-annotate-release.md @@ -12,7 +12,7 @@ You are tasked with creating Towncrier news fragments for user-facing changes since the last release cleanup. This command analyzes recent commits and generates appropriate changelog entries. -Special instructions: `$ARGUMENTS` +Special instructions: $ARGUMENTS (If above line is empty, then no special instructions were given by the user.) ## Context diff --git a/.auxiliary/configuration/claude/commands/cs-architect.md b/.auxiliary/configuration/claude/commands/cs-architect.md new file mode 100644 index 0000000..2df2461 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-architect.md @@ -0,0 +1,100 @@ +--- +allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(find:*), Bash(ls:*), Bash(tree:*) +description: Architectural analysis, system design decisions, and ADR creation +--- + +# System Architecture Analysis + +Analyze architectural decisions, system design patterns, component +relationships, and technical trade-offs to provide guidance on high-level +system structure and cross-component interactions. + +Request from user: $ARGUMENTS + +## Context + +- Product requirements: @documentation/prd.rst +- Architecture overview: @documentation/architecture/summary.rst +- Filesystem patterns: @documentation/architecture/filesystem.rst +- Architecture guidelines: @.auxiliary/instructions/architecture.rst +- Nomenclature standards: @.auxiliary/instructions/nomenclature.rst + +## Prerequisites + +Before providing architectural analysis, ensure: +- Understanding of current system architecture and constraints +- Familiarity with architectural decision record (ADR) format +- Knowledge of standard filesystem organization patterns +- @.auxiliary/instructions/architecture.rst guidelines are followed + +## Process Summary + +Key functional areas: +1. **Analysis**: Examine architectural context and design forces +2. **System Structure**: Define component relationships and system boundaries +3. **Decision Framework**: Apply architectural principles and trade-off analysis +4. **Documentation**: Create ADRs or update architectural documentation +5. **Validation**: Ensure decisions align with project constraints and goals + +## Safety Requirements + +Stop and consult the user if: +- Implementation details are requested instead of architectural guidance +- Specific code changes are needed +- Requirements analysis is needed +- Filesystem organization or module structure details are requested +- Architectural decisions have significant impact on existing system components +- Decision conflicts with existing architectural patterns or constraints +- Decision requires changes to fundamental system assumptions + +## Execution + +Execute the following steps: + +### 1. Architectural Context Analysis +Review current architecture and identify relevant patterns: +- Examine existing architectural documentation +- Understand system boundaries and component relationships +- Identify architectural forces and constraints +- Assess alignment with project goals and requirements + +### 2. Design Forces Assessment +Analyze the forces driving the architectural decision: +- Technical constraints (performance, scalability, compatibility) +- Quality attributes (maintainability, testability, security) +- Integration requirements with existing components +- Future flexibility and evolution needs + +### 3. Alternative Evaluation +Consider multiple architectural approaches: +- Document all seriously considered alternatives +- Analyze trade-offs for each option (benefits, costs, risks) +- Consider "do nothing" as a baseline alternative +- Evaluate alignment with established architectural patterns +- Assess implementation complexity and maintenance burden + +### 4. Decision Recommendation +Provide clear architectural guidance: +- State recommended approach with clear rationale +- Explain how decision addresses the identified forces +- Document expected positive and negative consequences +- Include specific architectural patterns or principles applied +- Provide text-based diagrams or examples when helpful + +### 5. Documentation Creation +When appropriate, create or update architectural documentation: +- Generate ADRs following the standard format +- Update `documentation/architecture/decisions/index.rst` to include new ADRs +- Update architecture summary for significant system changes +- Ensure consistency with filesystem organization patterns +- Reference related architectural decisions and dependencies + +### 6. Implementation Guidance +Provide high-level implementation direction without specific code: +- Suggest component organization and interfaces +- Recommend integration patterns with existing system +- Identify key architectural boundaries and abstractions +- Highlight critical implementation considerations + +### 7. Summarize Updates +Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/cs-code-python.md b/.auxiliary/configuration/claude/commands/cs-code-python.md new file mode 100644 index 0000000..476838d --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-code-python.md @@ -0,0 +1,228 @@ +--- +description: Python implementation following established patterns and practices +--- + +# Python Implementation + +Implement Python code following established patterns including functions, +classes, modules, tests, and refactoring while adhering to project practices +and style guidelines. + +Request from user: $ARGUMENTS + +## Context + +- Architecture overview: @documentation/architecture/summary.rst +- Filesystem patterns: @documentation/architecture/filesystem.rst +- General practices: @.auxiliary/instructions/practices.rst +- Python development guide: @.auxiliary/instructions/practices-python.rst +- Code style: @.auxiliary/instructions/style.rst +- Nomenclature: @.auxiliary/instructions/nomenclature.rst +- Design documents: @documentation/architecture/designs/ + +## Prerequisites + +Before implementing Python code, ensure: +- Understanding of implementation requirements and expected behavior +- Knowledge of existing codebase structure and patterns +- Clear design specifications or existing design documents if referenced + +### Guide Consultation Requirements + +Before implementing Python code, you MUST: +1. Read @.auxiliary/instructions/practices.rst for general development principles +2. Read @.auxiliary/instructions/practices-python.rst for Python-specific patterns +3. In a step on your TODO list, please attest that you have read the general and Python-specific practices guides and demonstrate your knowledge by writing one-sentence summaries on any three of the following topics: + +- the comprehensive examples showing multiple principles cohesively +- proper module organization content order +- import organization and centralized import patterns +- wide parameter, narrow return type patterns for robust interfaces +- immutability preferences for data structures and containers +- exception handling with narrow try blocks and proper chaining +- documentation formatting requirements including narrative mood +- quality assurance principles including linter compliance + +## Process Summary + +Key functional areas: +1. **Requirements Analysis**: Understand implementation requirements and create persistent tracking +2. **Session Continuity**: Check for existing work and preserve context across sessions +3. **Implementation**: Write Python code following style guidelines and best practices +4. **Progress Tracking**: Maintain session and cross-session implementation progress +5. **Quality Assurance**: Run linters, type checkers, and tests to validate code +6. **Documentation**: Update persistent tracking and provide implementation summary + +## Safety Requirements + +Stop and consult the user if: +- Design specifications are needed instead of implementation +- Architectural decisions are required before implementation +- Requirements are unclear or insufficient for implementation +- Implementation conflicts with established architectural patterns +- Code changes would break existing API contracts or interfaces +- Quality checks reveal significant issues that require design decisions +- Type checker errors are encountered that cannot be resolved through standard remediation +- Multiple implementation approaches have significant trade-offs requiring user input + +## Execution + +Execute the following steps: + +### 1. Requirements Analysis +Analyze implementation requirements and gather context: +- Review user requirements and any referenced design documents +- Examine existing codebase structure and relevant modules +- Identify integration points with existing code +- Understand expected behavior and edge cases +- Document implementation scope and constraints + +#### 1.1 Create Implementation Tracking File +Before beginning implementation, create a persistent tracking file with descriptive naming: +- Format: `.auxiliary/notes/<short-implementation-title>--progress.md` +- Example: `.auxiliary/notes/user-metrics-export--progress.md` + +Choose a concise but descriptive title that captures the main implementation goal. + +Structure the tracking file with these sections: + +### Context and References +- **Implementation Title**: [Brief description of what is being implemented] +- **Start Date**: [YYYY-MM-DD] +- **Reference Files**: [List all files explicitly provided as context/references at start] + - `path/to/reference1.py` - [Brief description of relevance] + - `path/to/reference2.rst` - [Brief description of relevance] +- **Design Documents**: [Any architecture or design docs referenced] +- **Session Notes**: [Link to current session TodoWrite items] + +### Design and Style Conformance Checklist +- [ ] Module organization follows practices guidelines +- [ ] Function signatures use wide parameter, narrow return patterns +- [ ] Type annotations comprehensive with TypeAlias patterns +- [ ] Exception handling follows Omniexception → Omnierror hierarchy +- [ ] Naming follows nomenclature conventions +- [ ] Immutability preferences applied +- [ ] Code style follows formatting guidelines + +### Implementation Progress Checklist +- [ ] [Specific function/class/module 1] +- [ ] [Specific function/class/module 2] +- [ ] [Integration point 1] tested +- [ ] [Integration point 2] tested + +### Quality Gates Checklist +- [ ] Linters pass (`hatch --env develop run linters`) +- [ ] Type checker passes +- [ ] Tests pass (`hatch --env develop run testers`) +- [ ] Code review ready + +### Decision Log +Document significant decisions made during implementation: +- [Date] [Decision made] - [Rationale] +- [Date] [Trade-off chosen] - [Why this approach over alternatives] + +### Handoff Notes +For future sessions or other developers: +- **Current State**: [What's implemented and what's not] +- **Next Steps**: [Immediate next actions needed] +- **Known Issues**: [Any problems or concerns to address] +- **Context Dependencies**: [Critical knowledge for continuing work] + +### 2. Session Continuity and Context Preservation +Before proceeding with implementation: + +#### Check for Existing Implementation +```bash +ls .auxiliary/notes/*--progress.md +``` + +If continuing previous work: +- Read existing tracking file completely to understand context +- Review reference files listed in context section +- Check decision log for previous design choices +- Update "Current State" in handoff notes as you resume work + +#### Context Preservation Requirements +Before beginning implementation: +- [ ] Create descriptive tracking file (`.auxiliary/notes/<title>--progress.md`) +- [ ] Record all reference files provided at session start +- [ ] Document initial understanding of requirements +- [ ] Note any existing related implementations or patterns found + +During implementation: +- [ ] Update decision log when making design choices +- [ ] Record integration points and dependencies discovered +- [ ] Document deviations from original plan with rationale + +Before session end: +- [ ] Update current state in handoff notes +- [ ] Ensure TodoWrite completions are reflected in persistent tracking where granularity aligns +- [ ] Record next steps for continuation + +### 3. Implementation + +**Write Python code following established patterns**: +- Apply comprehensive guide patterns for module organization, imports, annotations, immutability, exception handling, and documentation +- Consult the comprehensive guides when you need specific implementation details +- For complex annotation work or systematic annotation issues, consider using the `python-annotator` agent + +### 4. Progress Tracking Requirements +Maintain dual tracking systems: +- **Session Level**: Use TodoWrite tool for immediate task management within current session +- **Cross-Session**: Update `.auxiliary/notes/<implementation-title>--progress.md` for persistent tracking +- **Synchronization**: When TodoWrite items align with persistent checklist granularity, update corresponding persistent checklist items (TodoWrite may be more fine-grained) +- **Context Preservation**: Record all reference files and design decisions in persistent file for future session continuity + +### 5. Quality Assurance + +Before proceeding, add this quality verification checklist to your TODO list: +- [ ] Code follows proper module organization patterns +- [ ] Imports follow organization rules with centralized patterns +- [ ] Type annotations use wide parameter, narrow return patterns +- [ ] Functions ≤30 lines, modules ≤600 lines +- [ ] Immutability preferences applied to data structures +- [ ] Exception handling uses narrow try blocks with proper chaining +- [ ] Documentation follows narrative mood requirements +- [ ] Quality assurance principles applied + +#### Validation Commands +**Linting Validation** (zero-tolerance policy): +```bash +hatch --env develop run linters +``` +All issues must be addressed per comprehensive guide principles. Do not use `noqa` without explicit approval. + +**Type Checking** (systematic resolution): +```bash +hatch --env develop run linters # Includes Pyright +``` + +**Type Error Resolution Process**: +1. **Code Issues**: Fix immediately using comprehensive guide type annotation patterns +2. **Third-party Stubs**: Follow guidance in Python-specific practices guide (ensure dependency in `pyproject.toml`, prune Hatch environment, Pyright `createstub`, manage stubs) +3. **Complex Issues**: Use `python-annotator` agent for systematic resolution + +Stop and consult user if type errors cannot be categorized or require architectural decisions. + +**Test Validation**: +```bash +hatch --env develop run testers +``` +All tests must pass, including new implementations. + +### 6. Documentation and Summary + +**Provide implementation documentation**: +- Update persistent tracking file with implementation state +- Document design decisions and trade-offs in decision log +- Complete handoff notes for session continuity +- Note TODO items for future work + +### 7. Summarize Implementation +Provide concise summary of what was implemented, including: +- Functions, classes, or modules created or modified +- Key design decisions and rationale +- Integration points and dependencies +- Quality assurance status: Confirm all linters, type checkers, and tests pass +- Checklist of principles and patterns applied during implementation +- Any remaining tasks or follow-up items diff --git a/.auxiliary/configuration/claude/commands/cs-conform-code.md b/.auxiliary/configuration/claude/commands/cs-conform-code.md deleted file mode 100644 index 259dfd6..0000000 --- a/.auxiliary/configuration/claude/commands/cs-conform-code.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -allowed-tools: Bash(hatch --env develop run:*), Bash(git:*), LS, Read, Glob, Grep, Edit, MultiEdit, Write, WebFetch -description: Systematically conform Python code to project style and practice standards ---- - -# Python Code Conformance - -For bringing existing Python code into full compliance with project standards. - -Target code: `$ARGUMENTS` - -**CRITICAL**: Focus on style/practice conformance, not functionality changes. - -## Context - -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` - -## Prerequisites - -- **MANDATORY**: Read project documentation guides first: - - https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/practices.rst - - https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/style.rst - - https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/nomenclature.rst -- Understand target files to be conformed -- Have read `CLAUDE.md` for project-specific guidance - -## Priority Conformance Issues - -### 1. Function Parameters: Wide Types -**Issue:** Using narrow concrete types instead of wide abstract types for parameters -**Before:** -```python -def process_items( items: list[ str ], config: dict[ str, int ] ) -> bool: - return all( validate( item, config ) for item in items ) -``` -**After:** -```python -def process_items( - items: __.cabc.Sequence[ str ], - config: __.cabc.Mapping[ str, int ], -) -> bool: - return all( validate( item, config ) for item in items ) -``` - -### 2. Import Organization: Namespace Pollution -**Issue:** Polluting module namespace instead of using private aliases -**Before:** -```python -from pathlib import Path -from collections import defaultdict -import json -from typing import Any, Dict, List -``` -**After:** -```python -# Direct imports when performance is a consideration -from json import loads as _json_loads - -# Use __ subpackage for common imports -from . import __ -``` - -### 3. Module Organization -**Issue:** Wrong order of module contents -**Should follow this order:** -1. Imports (see practices guide) -2. Common type aliases (`TypeAlias` declarations) -3. Private variables/functions for defaults (grouped semantically) -4. Public classes and functions (alphabetical) -5. All other private functions (alphabetical) - -### 4. Spacing and Delimiters -**Issue:** Missing spaces in delimiters, operators -**Before:** -```python -def func(arg1,arg2="default"): - result=process(arg1,{"key":"value"}) -``` -**After:** -```python -def func( arg1, arg2 = 'default' ): - result = process( arg1, { 'key': 'value' } ) -``` - -### 5. Type Annotations: Missing or Incomplete -**Issue:** Missing annotations, not using `TypeAlias` for complex types -**Before:** -```python -def process_user( user, callback=None ): - return callback( user ) if callback else str( user ) -``` -**After:** -```python -UserRecord: __.typx.TypeAlias = dict[ str, str | int | list[ str ] ] - -def process_user( - user: UserRecord, - callback: __.Absential[ - __.cabc.Callable[ [ UserRecord ], str ] - ] = __.absent -) -> str: - if not __.is_absent( callback ): return callback( user ) - return str( user ) -``` - -### 6. Exception Handling: Overly Broad Blocks -**Issue:** Wrapping entire functions in try blocks -**Before:** -```python -def process_items( items: list[ str ] ) -> list[ dict ]: - try: - results = [ ] - for item in items: - validated = validate_item( item ) # Can raise - processed = expensive_computation( validated ) - results.append( processed ) - return results - except ValidationError: - return [ ] -``` -**After:** -```python -def process_items( items: __.cabc.Sequence[ str ] ) -> list[ dict ]: - results = [ ] - for item in items: - try: validated = validate_item( item ) # Only risky statement - except ValidationError: - logger.warning( f"Skipping invalid item: {item}." ) - continue - processed = expensive_computation( validated ) - results.append( processed ) - return results -``` - -### 7. Docstring Format and Mood -**Issue:** Wrong quotes, spacing, imperative mood -**Before:** -```python -def process_data( data ): - """Process the input data.""" # Wrong quotes, imperative mood -``` -**After:** -```python -def process_data( - data: __.cabc.Sequence[ __.typx.Any ] -) -> dict[ str, __.typx.Any ]: - ''' Processes input data and returns results. ''' # Narrative mood -``` - -### 8. Immutability: Using Mutable When Unnecessary -**Issue:** Using mutable containers when immutable would suffice -**Before:** -```python -def calculate_stats( data: list[ int ] ) -> dict[ str, float ]: - results = { } - results[ 'mean' ] = sum( data ) / len( data ) - return results -``` -**After:** -```python -def calculate_stats( - data: __.cabc.Sequence[ int ] -) -> __.immut.Dictionary[ str, float ]: - return __.immut.Dictionary( - mean = sum( data ) / len( data ), - maximum = max( data ), - minimum = min( data ) - ) -``` - -## Conformance Process - -### 1. Analysis Phase -- Read the three documentation guides thoroughly -- Examine target files to understand current state -- Run linters to identify specific violations -- Identify architectural patterns that need updating - -### 2. Systematic Correction -Apply fixes in this order: -1. **Module Organization**: Reorder imports, type aliases, functions per practices guide -2. **Wide/Narrow Types**: Convert function parameters to wide abstract types -3. **Import Cleanup**: Remove namespace pollution, use private aliases and __ subpackage -4. **Type Annotations**: Add missing hints, create `TypeAlias` for complex types -5. **Exception Handling**: Narrow try block scope, ensure proper chaining -6. **Immutability**: Replace mutable with immutable containers where appropriate -7. **Spacing/Delimiters**: Fix `( )`, `[ ]`, `{ }` patterns -8. **Docstrings**: Triple single quotes, narrative mood, proper spacing -9. **Line Length**: Split at 79 columns using parentheses - -### 3. Validation -```bash -hatch --env develop run linters # Must pass clean -hatch --env develop run testers # Must not break functionality -``` - -## Safety Requirements - -**HALT if:** -- Linters reveal complex architectural issues -- Changes would alter functionality -- Type annotations conflict with runtime behavior -- Import changes break dependencies -- Tests start failing - -**Your responsibilities:** -- Maintain exact functionality while improving practices/style -- Use project patterns consistently per the guides -- Reference all three guides for complex cases -- Verify all changes with linters and tests - -## Success Criteria - -- [ ] All linting violations resolved -- [ ] Module organization follows practices guide structure -- [ ] Function parameters use wide abstract types -- [ ] Imports avoid namespace pollution -- [ ] Type annotations comprehensive with `TypeAlias` usage -- [ ] Exception handling uses narrow try blocks -- [ ] Immutable containers used where appropriate -- [ ] No functionality changes -- [ ] Tests continue to pass -- [ ] Code follows all style guide patterns - -**Note**: Always run full validation (`hatch --env develop run linters && hatch ---env develop run testers`) before considering the task complete. - -## Final Report - -Upon completion, provide a brief report covering: -- Specific conformance issues corrected (categorized by the priority issues above) -- Number of files modified -- Any patterns that required manual intervention -- Linter status before/after -- Any deviations from guides and justification diff --git a/.auxiliary/configuration/claude/commands/cs-conform-python.md b/.auxiliary/configuration/claude/commands/cs-conform-python.md new file mode 100644 index 0000000..fa9bc2a --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-conform-python.md @@ -0,0 +1,204 @@ +--- +description: Systematically conform Python code to project style and practice standards +--- + +# Python Code Conformance + +For bringing existing Python code into full compliance with project standards. + +Target: $ARGUMENTS + +Focus on style/practice conformance, not functionality changes. + +## Prerequisites + +- Read project documentation guides first: + - @.auxiliary/instructions/practices.rst + - @.auxiliary/instructions/practices-python.rst + - @.auxiliary/instructions/style.rst + - @.auxiliary/instructions/nomenclature.rst +- Understand target files to be conformed +- Have read `CLAUDE.md` for project-specific guidance + +## Context + +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` + +## Execution Structure + +**Phase 1: Comprehensive Review** +Perform complete analysis and generate detailed compliance report before making any changes. + +**Phase 2: Systematic Remediation** +Apply all identified fixes in systematic order, validating with linters after completion. + +### Project Standards + +Before proceeding with conformance analysis, you MUST: +1. Read @.auxiliary/instructions/practices.rst for general development principles +2. Read @.auxiliary/instructions/practices-python.rst for Python-specific patterns +3. In a step on your TODO list, please attest that you have read the general and Python-specific practices guides and demonstrate your knowledge by writing one-sentence summaries on any three of the following topics: + +- the wide parameter, narrow return type pattern for robust interfaces +- the import organization rules and centralized import patterns +- when to use different immutable base classes (Object vs DataclassObject vs Protocol) +- the exception hierarchy pattern (Omniexception → Omnierror) +- the comprehensive examples showing multiple principles cohesively +- the module organization content order and size limits + +## Conformance Verification + +### Module Organization Verification +Confirm compliance with module organization patterns: +- [ ] Content follows proper order: imports, type aliases, private defaults, public classes/functions, private functions +- [ ] Module size ≤600 lines (analyze oversized modules for separation of concerns) +- [ ] Functions ≤30 lines each + +### Import Organization Verification +Confirm compliance with import organization patterns: +- [ ] Module-level imports use private aliases (except in `__init__`, `__`, re-export modules) +- [ ] Common modules (os, re, etc.) imported through centralized `__.imports` rather than per-module +- [ ] No namespace pollution through public imports +- [ ] Subpackages define `__` re-export module with `from ..__ import *` +- [ ] No `__all__` attribute provided (unnecessary interface maintenance) + +### Type Annotation Verification +Confirm compliance with type annotation patterns: +- [ ] Public functions use wide, abstract argument types (`__.cabc.Sequence`, `__.cabc.Mapping`) +- [ ] All functions define narrow, concrete return types (`list`, `dict`, `tuple`, `__.immut.Dictionary`) +- [ ] Proper function signature spacing following formatting guidelines +- [ ] `TypeAlias` declarations for complex types + +### Immutability Verification +Confirm compliance with immutability patterns: +- [ ] Classes inherit from `__.immut.Object`, `__.immut.Protocol`, `__.immut.DataclassObject` +- [ ] Functions return immutable types (`tuple`, `frozenset`, `__.immut.Dictionary`) not mutable types (`list`, `dict`, `set`) +- [ ] Dependency injection with sensible defaults applied + +### Exception Handling Verification +Confirm compliance with exception handling patterns: +- [ ] One `try..except` suite per statement that can raise exceptions +- [ ] Narrow try block scope maintained +- [ ] Proper exception chaining and hierarchy usage +- [ ] No bare exceptions raised (except `NotImplementedError`) + +### Documentation Verification +Confirm compliance with documentation patterns: +- [ ] Docstrings use triple single quotes with narrative mood +- [ ] Exception messages in double quotes +- [ ] No comments describing obvious behavior +- [ ] TODO comments for uncovered edge cases + +### Style Formatting Verification +Confirm compliance with formatting standards: +- [ ] Space padding inside delimiters: `( arg )`, `[ item ]`, `{ key: value }` +- [ ] Space padding around keyword argument `=`: `foo = 42` +- [ ] F-strings in double quotes: `f"text {variable}"` +- [ ] No blank lines within function bodies +- [ ] Single-line statements on same line when appropriate: `if condition: return value` +- [ ] Proper multi-line construct delimiter placement + +### Quality Assurance Verification +Confirm compliance with quality assurance principles: +- [ ] Critical review of all linter suppressions +- [ ] No `type: ignore` usage (investigate underlying issues) +- [ ] No `__.typx.cast` usage (investigate type system issues) +- [ ] Minimal `noqa` pragmas with compelling justification only + +### Violation Analysis Reference + +For comprehensive violation examples and correction patterns, see the comprehensive examples in practices-python.rst, which demonstrate proper application of all conformance principles in cohesive, real-world contexts. + +When analyzing violations, reference the specific sections of practices-python.rst that address each violation type rather than duplicating examples here. + +## Review Report Format + +Phase 1 Output: +1. **Compliance Summary**: Overall assessment with file-by-file breakdown +2. **Standards Violations**: Categorized list with specific line references and explanations +3. **Complexity Analysis**: Function and module size assessments +4. **Remediation Plan**: Systematic order of fixes to be applied +5. **Risk Assessment**: Any changes that require careful validation + +Phase 2 Output: +1. **Applied Fixes**: Summary of all changes made, categorized by standard +2. **Validation Results**: Linter output before and after changes +3. **Files Modified**: Complete list with brief description of changes +4. **Manual Review Required**: Any issues requiring human judgment + +## Conformance Process + +### 1. Analysis Phase (PHASE 1) +- Examine target files to understand current state +- Run linters to identify specific violations +- Identify architectural patterns that need updating +- Generate comprehensive compliance report +- **Requirements**: Complete review and report before any remediation +- **Focus**: Reference specific lines with concrete examples and explain reasoning + +### 2. Systematic Correction (PHASE 2) + +Before applying any fixes, confirm: +- [ ] I have completed comprehensive analysis with specific line references +- [ ] I understand each violation type and its corresponding practices-python.rst section +- [ ] I have a systematic remediation plan prioritized by impact + +**Apply fixes in appropriate order**: +1. **Module Organization**: Reorder per established organizational patterns +2. **Import Organization**: Apply centralized import organization rules +3. **Type Annotations**: Convert to wide parameter/narrow return patterns +4. **Immutability**: Apply immutable container and base class patterns +5. **Exception Handling**: Apply narrow try block and hierarchy patterns +6. **Documentation**: Apply narrative mood and formatting patterns +7. **Formatting**: Apply spacing, delimiter, and vertical compactness standards +8. **Quality Assurance**: Apply linter compliance and suppression principles + +For comprehensive type annotation work or complex type checking issues, consider using the `python-annotator` agent. + +**POST-CORRECTION VERIFICATION GATE** +After applying all fixes, confirm: +- [ ] All verification checklists from practices-python.rst sections pass +- [ ] `hatch --env develop run linters` produces clean output +- [ ] `hatch --env develop run testers` passes with no functionality breaks +- [ ] Code follows all practices-python.rst patterns exactly + +## Safety Requirements + +Stop and consult if: +- Linters reveal complex architectural issues +- Changes would alter functionality +- Type annotations conflict with runtime behavior +- Import changes break dependencies +- Tests start failing + +Your responsibilities: +- Maintain exact functionality while improving practices/style +- Use project patterns consistently per the guides +- Reference all three guides for complex cases +- Verify all changes with linters and tests + +## Success Criteria + +- [ ] All linting violations resolved +- [ ] Module organization follows practices guide structure +- [ ] Function parameters use wide abstract types +- [ ] Imports avoid namespace pollution +- [ ] Type annotations comprehensive with `TypeAlias` usage +- [ ] Exception handling uses narrow try blocks +- [ ] Immutable containers used where appropriate +- [ ] No functionality changes +- [ ] Tests continue to pass +- [ ] Code follows all style guide patterns + +**Note**: Always run full validation (`hatch --env develop run linters && hatch +--env develop run testers`) before considering the task complete. + +## Final Report + +Upon completion, provide a brief report covering: +- Specific conformance issues corrected (categorized by the priority issues above) +- Number of files modified +- Any patterns that required manual intervention +- Linter status before/after +- Any deviations from guides and justification diff --git a/.auxiliary/configuration/claude/commands/cs-conform-toml.md b/.auxiliary/configuration/claude/commands/cs-conform-toml.md new file mode 100644 index 0000000..27c280f --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-conform-toml.md @@ -0,0 +1,281 @@ +--- +allowed-tools: Bash(git:*), LS, Read, Glob, Grep, Edit, MultiEdit, Write +description: Systematically conform TOML files to project style and practice standards +--- + +# TOML Configuration Conformance + +For bringing existing TOML configuration files into full compliance with project standards. + +Target files: $ARGUMENTS + +Focus on style/practice conformance, not functionality changes. + +## Prerequisites + +- Read project documentation guides first: + - @.auxiliary/instructions/practices.rst + - @.auxiliary/instructions/practices-toml.rst + - @.auxiliary/instructions/style.rst + - @.auxiliary/instructions/nomenclature.rst +- Understand target files to be conformed +- Have read `CLAUDE.md` for project-specific guidance + +## Context + +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` + +## Execution Structure + +**Phase 1: Comprehensive Review** +Perform complete analysis and generate detailed compliance report before making any changes. + +**Phase 2: Systematic Remediation** +Apply all identified fixes in systematic order, validating changes after completion. + +## Compliance Standards + +### Configuration Design Standards + +#### 1. Table Organization + +- Prefer table arrays with `name` fields over proliferating custom subtables. +- Table arrays scale better and reduce configuration complexity. + +**❌ Avoid - custom subtables:** +```toml +[database] +host = 'localhost' + +[database.primary] +port = 5432 +timeout = 30 + +[database.replica] +port = 5433 +timeout = 15 +``` + +**✅ Prefer - table arrays with name field:** +```toml +[[database]] +name = 'primary' +host = 'localhost' +port = 5432 +timeout = 30 + +[[database]] +name = 'replica' +host = 'localhost' +port = 5433 +timeout = 15 +``` + +#### 2. Key Naming Conventions + +- Use hyphens instead of underscores in key names for better ergonomics. +- Apply nomenclature guidelines to key and table names. +- Use Latin-derived words when they are the established norm in the domain. + +**❌ Avoid:** +```toml +max_connections = 100 +retry_count = 3 +database_url = 'postgresql://localhost/db' +``` + +**✅ Prefer:** +```toml +max-connections = 100 +retry-count = 3 +database-url = 'postgresql://localhost/db' +``` + +### Style Standards + +#### 1. String Values + +- Use single quotes for string values unless escapes are needed. +- Use double quotes when escapes are required. +- Use triple single quotes for multi-line strings (consistency with Python docstrings). + +**❌ Avoid:** +```toml +name = "example-service" +description = "A service for processing data" +pattern = "user-.*" +``` + +**✅ Prefer:** +```toml +name = 'example-service' +description = 'A service for processing data' +pattern = 'user-.*' + +# Use double quotes when escapes are needed +windows-path = "C:\\Program Files\\Example" +message = "Line 1\nLine 2" + +# Use triple single quotes for multi-line strings +description = ''' +This is a longer description +that spans multiple lines. +''' +``` + +#### 2. Array and Table Formatting + +- Keep arrays and inline tables on single lines when they fit within reasonable length. +- For longer arrays, place each element on its own line with proper indentation. + +**✅ Prefer:** +```toml +ports = [ 8080, 8443, 9090 ] +database = { host = 'localhost', port = 5432 } + +# For longer arrays +allowed-origins = [ + 'https://example.com', + 'https://api.example.com', + 'https://admin.example.com', +] +``` + +### Comprehensive Example: Configuration with Multiple Violations + +Here is a TOML configuration that demonstrates many compliance violations: + +```toml +[server_config] +host_name = "localhost" +port_number = 8080 +max_connections = 100 + +[server_config.database_primary] +host = "localhost" +port = 5432 +connection_timeout = 30 +retry_attempts = 3 + +[server_config.database_replica] +host = "localhost" +port = 5433 +connection_timeout = 15 +retry_attempts = 2 + +allowed_hosts = ["https://example.com", "https://api.example.com", "https://admin.example.com"] + +description = "This is a multi-line description that explains what this service does and how it should be configured." +``` + +Violations identified: +1. **Underscore key names**: `server_config`, `host_name`, `port_number`, `max_connections` should use hyphens +2. **Custom subtables**: `[server_config.database_primary]` and `[server_config.database_replica]` should be table arrays +3. **Double quotes**: String values using double quotes without escapes needed +4. **Array formatting**: Long array on single line should be split across multiple lines +5. **Multi-line string**: Long description should use triple single quotes + +Corrected version: +```toml +[[server-config]] +name = 'main' +host-name = 'localhost' +port-number = 8080 +max-connections = 100 + +[[database]] +name = 'primary' +host = 'localhost' +port = 5432 +connection-timeout = 30 +retry-attempts = 3 + +[[database]] +name = 'replica' +host = 'localhost' +port = 5433 +connection-timeout = 15 +retry-attempts = 2 + +allowed-hosts = [ + 'https://example.com', + 'https://api.example.com', + 'https://admin.example.com', +] + +description = ''' +This is a multi-line description that explains what this service does +and how it should be configured. +''' +``` + +## Review Report Format + +Phase 1 Output: +1. **Compliance Summary**: Overall assessment with file-by-file breakdown +2. **Standards Violations**: Categorized list with specific line references and explanations +3. **Configuration Analysis**: Table organization and key naming assessments +4. **Remediation Plan**: Systematic order of fixes to be applied +5. **Risk Assessment**: Any changes that require careful validation + +Phase 2 Output: +1. **Applied Fixes**: Summary of all changes made, categorized by standard +2. **Files Modified**: Complete list with brief description of changes +3. **Manual Review Required**: Any issues requiring human judgment + +## Conformance Process + +### 1. Analysis Phase (PHASE 1) +- Examine target files to understand current state +- Identify configuration design patterns that need updating +- Generate comprehensive compliance report +- **Requirements**: Complete review and report before any remediation +- **Focus**: Reference specific lines with concrete examples and explain reasoning + +### 2. Systematic Correction (PHASE 2) +Apply fixes in systematic order: +1. **Key Naming**: Convert underscores to hyphens in key names +2. **Table Organization**: Convert custom subtables to table arrays with `name` fields +3. **String Quoting**: Change double quotes to single quotes (unless escapes needed) +4. **Multi-line Strings**: Convert to triple single quotes format +5. **Array Formatting**: Split long arrays across multiple lines with proper indentation +6. **Nomenclature**: Apply naming guidelines to keys and table names + +**Requirements**: +- Maintain exact functionality while improving standards adherence +- Validate that configuration files remain syntactically valid +- Preserve all semantic meaning of configuration values + +## Safety Requirements + +Stop and consult if: +- Configuration structure changes would alter application behavior +- Complex nested configurations require architectural decisions +- File contains domain-specific conventions that conflict with general guidelines +- Syntax errors occur during modification + +Your responsibilities: +- Maintain exact functionality while improving practices/style +- Use project patterns consistently per the guides +- Reference TOML documentation guides for complex cases +- Verify all changes preserve configuration semantics + +## Success Criteria + +- [ ] All key names use hyphens instead of underscores +- [ ] Custom subtables converted to table arrays where appropriate +- [ ] String values use single quotes (double only when escapes needed) +- [ ] Multi-line strings use triple single quotes +- [ ] Long arrays are properly formatted across multiple lines +- [ ] Nomenclature guidelines applied to keys and table names +- [ ] No functionality changes to configuration behavior +- [ ] Files remain syntactically valid TOML + +## Final Report + +Upon completion, provide a brief report covering: +- Specific conformance issues corrected (categorized by the priority issues above) +- Number of files modified +- Any patterns that required manual intervention +- Any deviations from guides and justification \ No newline at end of file diff --git a/.auxiliary/configuration/claude/commands/cs-copier-update.md b/.auxiliary/configuration/claude/commands/cs-copier-update.md new file mode 100644 index 0000000..16471af --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-copier-update.md @@ -0,0 +1,131 @@ +--- +allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(copier:*), Bash(git status), Bash(git add:*), Bash(git rm:*), Bash(rg:*), Bash(grep:*), Bash(hatch --env develop run make-all), TodoWrite +description: Synchronize project with Copier template updates, intelligently resolving merge conflicts +--- + +# Template Synchronization + +Synchronize project with its Copier template by running updates and automatically resolving common merge conflict patterns while preserving local customizations. + +Request from user: $ARGUMENTS + +## Context + +- Template answers file: @.auxiliary/configuration/copier-answers.yaml +- Current git status: !`git status --porcelain` +- Existing conflicts check: !`grep -r "^<<<<<<<\|^=======$\|^>>>>>>>" . --exclude-dir=.git || echo "No conflicts"` +- Project conventions: @.auxiliary/configuration/conventions.md + +## Prerequisites + +Before running template synchronization, ensure: +- Working directory is completely clean (no staged or unstaged changes) +- Copier is installed and accessible via command line +- Template answers file exists at `.auxiliary/configuration/copier-answers.yaml` +- Git repository is in a stable state for applying updates + +## Process Summary + +Key functional areas: +1. **Template Update**: Run copier update with project-specific settings +2. **Conflict Detection**: Identify and categorize merge conflicts from template changes +3. **Intelligent Resolution**: Automatically resolve conflicts favoring upstream improvements while preserving local customizations +4. **File Lifecycle Management**: Handle additions, renames, and deletions from template updates +5. **Validation**: Ensure complete conflict resolution and commit changes with template version + +## Safety Requirements + +Stop and consult the user if: +- Working directory is not clean (has staged or unstaged changes) +- Complex conflicts exist that could result in loss of local customizations +- Template artifacts cannot be reliably distinguished from intentional local content +- Multiple conflicting resolution strategies are equally valid +- Copier update fails with unrecoverable errors +- Critical project files show unexpected merge conflicts + +## Execution + +Execute the following steps: + +### 1. Pre-Update Validation +Check project state and prepare for template synchronization: +- Verify git working directory is completely clean (halt if any changes exist) +- Confirm template answers file exists and is readable +- Document any existing conflicts to avoid confusion +- Ensure repository is on the expected branch + +### 2. Execute Template Update +Run copier update with project-specific configuration: +```bash +copier update --answers-file .auxiliary/configuration/copier-answers.yaml --skip-answered +``` +- Capture copier output to extract template version information +- Detect update completion status and any reported conflicts +- Identify new, modified, and deleted files from the update + +### 3. Conflict Analysis and Categorization +Systematically identify and categorize all conflicts: +- Scan for merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) +- Classify conflicts by type: + - **Structure consolidation**: Old sections moved into organized subsections + - **Upstream additions**: New template content (toctree entries, sections, files) + - **Language refinements**: Policy and wording improvements + - **Template artifacts**: TODO comments, placeholder content + - **Complex conflicts**: Overlapping local and upstream modifications + +### 4. Intelligent Conflict Resolution +Apply resolution strategies based on conflict categorization: + +**Auto-resolve structure consolidation conflicts:** +- Accept new organization when local content is preserved in new structure +- Remove orphaned sections that were properly consolidated + +**Auto-resolve upstream additions:** +- Accept new toctree entries, sections, and configuration additions +- Stage new files and directories from template + +**Auto-resolve language refinements:** +- Accept upstream wording and policy improvements +- Preserve local semantic modifications when they don't conflict + +**Handle template artifacts intelligently:** +- Detect TODO comments and placeholder content that may have been intentionally removed +- Avoid reintroducing template boilerplate that conflicts with project maturity + +### 5. File Lifecycle Management +Handle template-driven file changes: +- Stage all new files and directories added by template +- Process file renames (e.g., `cs-develop-tests.md` → `cs-develop-pytests.md`) +- Remove obsolete files that have been replaced or are no longer needed +- Update git index to reflect all template changes + +### 6. Resolution Verification +Ensure complete and accurate conflict resolution: +- Scan entire project for remaining merge conflict markers +- Verify no orphaned conflict sections remain +- Confirm all auto-resolved conflicts maintain local customizations +- Validate file integrity and proper git staging + +### 7. Project Validation +Verify template changes don't break project functionality: +```bash +hatch --env develop run make-all +``` +- Run full project validation including linting, type checking, and tests +- Ensure all quality gates pass after template synchronization +- Address any validation failures before proceeding to commit + +### 8. Commit Template Changes +Create commit with template version information: +- Extract template version from copier output or updated answers file +- Generate commit message: "Update project from Copier template (v{version})." +- Include standard co-authoring footer for Claude Code +- Use git commit (requires user approval) to commit all staged changes + +### 9. Conflict Resolution Report +Provide comprehensive summary of synchronization results: +- List all conflicts automatically resolved with resolution strategy +- Report new files, renames, and deletions processed +- Identify any conflicts requiring manual intervention +- Confirm template version successfully applied +- Note any remaining tasks or follow-up actions needed \ No newline at end of file diff --git a/.auxiliary/configuration/claude/commands/cs-create-command.md b/.auxiliary/configuration/claude/commands/cs-create-command.md new file mode 100644 index 0000000..d7ba98b --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-create-command.md @@ -0,0 +1,108 @@ +--- +allowed-tools: Write, Read, LS +description: Generate a new custom slash command with consistent structure and formatting +--- + +# Generate Slash Command + +Generate a new custom slash command following established patterns for structure, tone, and formatting. + +Target: $ARGUMENTS + +**IMPORTANT**: You are creating slash commands for other Claude instances to execute. They will have no knowledge of: +- The concept of "arguments" being passed to slash commands +- The ARGUMENTS variable or its expansion +- The meta-context of slash command generation +- When creating content, avoid using the word "command" in titles or explanations - use terms like "process", "workflow", or "task" instead + +Your job is to interpret the user's request and create a complete, self-contained slash command. + +## Input Interpretation + +The user's request may take various forms: +- Simple: `cs-analyze-performance` +- Descriptive: `Named cs-inquire.md with a process outlined in .auxiliary/notes/inquire-command.md` +- Reference-based: `Based on .auxiliary/notes/summarize-project-command.md` +- Complex: `cs-update-deps that checks package.json and updates dependencies safely` + +Extract from the user's input: +1. **Filename** (must start with `cs-`) +2. **Purpose/functionality** (from description or referenced files) +3. **Special requirements** (referenced processes, specific tools needed) + +## Context + +- Current custom commands: !`ls .claude/commands/cs-*.md 2>/dev/null || echo "No cs-* commands found"` +- Referenced files (if any): Check for existence and read as needed +- Command template: @.auxiliary/configuration/claude/miscellany/command-template.md + +## Prerequisites + +Before creating the slash command, ensure: +- Clear understanding of the intended purpose +- Filename follows `cs-*` naming pattern +- No existing file with the same name +- Any referenced process files are accessible + +## Generation Process + +### 1. Analyze User Request + +From the user's input, determine: +- **Filename** (extract `cs-*.md` name) +- **Purpose** (what should the generated slash command accomplish) +- **Required tools** (based on functionality) +- **Process details** (read any referenced files for specifics) + +### 2. Read Template Structure + +Read the template to get the base structure, then customize: +- Replace placeholder content with appropriate descriptions +- Customize sections based on purpose +- Select appropriate allowed-tools +- Add relevant @-references if applicable +- Add checklists to sections if applicable + +### 3. Apply Formatting Standards + +**Professional Tone:** +- Avoid making everything critical or important; no excessive + attention-grabbing +- Avoid excessive emphasis (no all-caps headers, minimal bold text) +- Professional headers: `## Prerequisites` not `## MANDATORY PREREQUISITES` +- Use "Stop and consult" for when user input should be solicited + +**Structure:** +- Include Prerequisites section early in document +- Include Context section with command expansions (exclamation point followed + by command in backticks) for dynamic info when needed +- Use @-references for local documentation when applicable +- Provide clear Process Summary before detailed steps +- Include Safety Requirements section for error handling + +### 4. Tool Selection + +Choose appropriate allowed-tools based on functionality: + +**Common tool combinations:** +- **File operations**: `Write, Read, Edit, MultiEdit, LS, Glob, Grep` +- **Git operations**: `Bash(git status), Bash(git add:*), Bash(git commit:*), Bash(git push:*)` +- **Python development**: `Bash(hatch --env develop run:*), Bash(pytest:*), Bash(ruff:*)` +- **GitHub operations**: `Bash(gh run list:*), Bash(gh run watch:*), Bash(gh pr create:*)` + +### 5. Generate and Write File + +1. **Read the template** from `.auxiliary/configuration/claude/miscellany/command-template.md` +2. **Customize all sections** based on the specific purpose +3. **Replace placeholders** with appropriate content for the target functionality +4. **Write the final file** to `.claude/commands/[filename].md` + + +### 6. Validation and Summary + +After generation: +- Verify file structure matches established patterns +- Check that allowed-tools are appropriate for the functionality +- Ensure professional tone throughout (no excessive attention-grabbing, etc...) +- Confirm all required sections are present and customized +- Provide succinct summary of changes made to the user diff --git a/.auxiliary/configuration/claude/commands/cs-design-python.md b/.auxiliary/configuration/claude/commands/cs-design-python.md new file mode 100644 index 0000000..ea4ff01 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-design-python.md @@ -0,0 +1,144 @@ +--- +description: Python API design, filesystem organization, module structure, and interface specifications +--- + +# Python Design Analysis + +Analyze Python API design patterns, filesystem organization, module structure, class hierarchies, interface definitions, and design patterns to provide guidance on Python-specific structural decisions and project organization. Focus exclusively on interface contracts, signatures, and type specifications—never implementation details or method bodies. + +Request from user: $ARGUMENTS + +## Context + +- Architecture overview: @documentation/architecture/summary.rst +- Filesystem patterns: @documentation/architecture/filesystem.rst +- General practices: @.auxiliary/instructions/practices.rst +- Python development guide: @.auxiliary/instructions/practices-python.rst +- Code style: @.auxiliary/instructions/style.rst +- Nomenclature: @.auxiliary/instructions/nomenclature.rst +- Design documents: @documentation/architecture/designs/ + +## Prerequisites + +Before providing design analysis, ensure: +- Understanding of module organization and class hierarchies +- Familiarity with Python practices and style guidelines +- Knowledge of nomenclature conventions and naming patterns + +### Project Standards + +Before providing design analysis, you MUST: +1. Read @.auxiliary/instructions/practices.rst for general development principles +2. Read @.auxiliary/instructions/practices-python.rst for Python-specific patterns +3. In a step on your TODO list, please attest that you have read the general and Python-specific practices guides and demonstrate your knowledge by writing one-sentence summaries on any three of the following topics: + +- interface specification patterns from comprehensive examples +- module organization principles and content ordering +- import organization for design specifications +- wide parameter, narrow return interface patterns +- immutable container design patterns +- exception hierarchy design patterns +- documentation specification requirements +- nomenclature patterns from nomenclature guides + +## Process Summary + +Key functional areas: +1. **Design Analysis**: Examine current Python structure and design patterns +2. **Interface Specification**: Define clean API boundaries and contracts +3. **Module Organization**: Apply filesystem and import patterns effectively +4. **Class Design**: Create maintainable hierarchies and interface patterns +5. **Documentation**: Specify design decisions with examples and rationale + +## Safety Requirements + +Stop and consult the user if: +- Architectural decisions are needed instead of design specifications +- Implementation details are requested instead of design specifications +- Requirements analysis is needed instead of design specifications +- User requests actual code implementations instead of specifications +- Design decisions require architectural changes beyond Python structure +- Interface changes would break existing API contracts significantly +- Design conflicts with established filesystem organization patterns +- Requirements are unclear or insufficient for proper design specification +- Multiple design approaches have significant trade-offs requiring user input + +## Execution + +Execute the following steps: + +### 1. Design Analysis +Examine Python structure and patterns: +- Review module organization and import patterns +- Analyze class hierarchies and interface definitions +- Identify design patterns in use +- Assess alignment with practices and nomenclature guidelines +- Document design strengths and improvement opportunities + +### 2. Interface Specification + +**CRITICAL: Define interfaces through signatures and type annotations only. Avoid specifying how methods should be implemented internally—focus on contracts, not implementation logic.** + +**Define clean API boundaries and contracts**: +- Focus exclusively on signatures and type annotations (never implementation logic or method bodies) +- Apply wide parameter, narrow return patterns for robust interfaces +- Design exception class hierarchies following established patterns +- Apply appropriate naming conventions from nomenclature guidelines +- Define annotations using proper `__.typx.TypeAlias` patterns when appropriate +- Consider immutability preferences in container design +- Consult comprehensive guides for detailed patterns when needed + +### 3. Filesystem and Module Organization Design + +**Apply Python-specific organizational patterns and filesystem structure**: +- Design project filesystem organization and update filesystem.rst as needed +- Design module structure following standard organization order +- Plan centralized import integration for organized dependencies +- Specify exception hierarchies and their organization +- Design interface patterns for different component types +- Plan type alias organization and dependency management +- Consult comprehensive guides for detailed organizational patterns + +### 4. Class and Function Design + +**CRITICAL: Design class structures through their public contracts and type relationships. Specify signatures, inheritance patterns, and interface boundaries—never internal implementation logic or method bodies.** + +**Create maintainable Python structures**: +- Design class hierarchies with appropriate immutable base classes and mixins (`__.immut.Object`, `__.immut.Protocol`, etc.) +- Specify function signatures using wide input, narrow output patterns with proper spacing +- Apply nomenclature guidelines for methods, attributes, and functions +- Design immutable data structures and container patterns +- Plan dependency injection and configuration patterns with sensible defaults +- Focus exclusively on interface specifications, not implementation details +- Consult comprehensive guides for detailed design patterns + +### 5. Design Documentation + +**Create comprehensive design specifications without implementations**: + +**CRITICAL:** +- Use atemporal language in all specifications. Avoid temporal terms like 'new', 'current', 'existing', 'future'—designs should read as canonical specifications independent of implementation timeline. +- Provide only signatures, contracts, and interface specifications - no implementations + +- Generate design documents following established format +- Update `documentation/architecture/designs/index.rst` to include designs +- Do not provide exception class implementations, function bodies, or method implementations +- Document interface contracts and expected behaviors (contracts only, not code) +- Provide design examples using signatures and type annotations only +- Specify exception handling patterns and error propagation (exception classes by name/signature only) +- Document design rationale and trade-off decisions +- Consult comprehensive guides for documentation formatting requirements + +### 6. Design Validation + +**Ensure design quality and consistency**: +- Verify alignment with practices, style, and nomenclature guidelines +- Check consistency with filesystem organization patterns +- Validate that wide parameter/narrow return patterns are followed +- Ensure proper separation between public and private interfaces +- Confirm that design supports expected usage patterns and extensibility +- Verify that specifications focus on contracts, not implementations +- Consult comprehensive guides to verify pattern alignment + +### 7. Summarize Updates +Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/cs-develop-pytests.md b/.auxiliary/configuration/claude/commands/cs-develop-pytests.md new file mode 100644 index 0000000..b1acca9 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-develop-pytests.md @@ -0,0 +1,263 @@ +--- +description: Implement comprehensive Python tests following an existing test plan and project guidelines +--- + +# Implement Python Tests + +For systematic test implementation following a pre-created test plan and project testing guidelines. + +Test plan path or special test-writing instructions: $ARGUMENTS + +Implement tests according to the provided test plan only. + +## Context + +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` +- Existing test structure: !`find tests -name "*.py" | head -20` +- Test organization: @documentation/architecture/testplans/summary.rst +- Test plans index: @documentation/architecture/testplans/index.rst + +## Prerequisites + +Ensure that you: +- Have a valid test plan document +- Have verified access to target code modules referenced in the plan +- Have read any relevant `CLAUDE.md` file +- Understand the test-writing guidelines: @.auxiliary/instructions/tests.rst + +## Testing Principles (from project guidelines) + +**Core Principles:** +1. **Dependency Injection Over Monkey-Patching**: Use injectable dependencies + for testability +2. **Performance-Conscious**: Prefer in-memory filesystems (pyfakefs) over temp + directories +3. **Avoid Monkey-Patching**: Never patch internal code; use dependency + injection instead +4. **100% Coverage Goal**: Aim for complete line and branch coverage +5. **Test Behavior, Not Implementation**: Focus on observable behavior and + contracts + +**Anti-Patterns to Avoid:** +- Monkey-patching internal code (will fail with immutable objects) +- Excessive mocking of internal components +- Testing implementation details vs. behavior +- Using temp directories when pyfakefs suffices + +**Organization:** +- Follow the systematic numbering conventions detailed in the test guidelines + +## Safety Requirements + +Stop and consult the user if: +- No test plan path is provided +- Test plan cannot be read or is invalid +- Plan conflicts with project testing principles +- Implementation deviates from plan without justification +- Implementation cannot follow the test plan as specified +- Plan requires tests that violate project principles +- Tests require monkey-patching internal code +- Planned test numbering clashes with existing conventions +- Required test fixtures or dependencies are unavailable +- Test plan contains contradictions or unclear instructions + +**Your responsibilities:** +- Follow the test plan precisely while adhering to project conventions +- Focus only on uncovered areas specified in the plan +- Avoid redundant testing of functionality already covered by doctests +- Use dependency injection patterns as specified in the plan +- Implement tests exactly as planned without adding extras +- Maintain systematic test numbering as outlined in the plan +- Ensure tests validate behavior, not implementation +- Document any necessary deviations from the plan with clear justification + +## Test Implementation Process + +### 0. Pre-Flight Verification +Verify access to project guidelines: + +Read and confirm you can access the complete project guidelines: +- Testing: @.auxiliary/instructions/tests.rst +- Python Practices: @.auxiliary/instructions/practices-python.rst +- General Practices: @.auxiliary/instructions/practices.rst +- Style: @.auxiliary/instructions/style.rst + +You must successfully access and read all four guides before proceeding. If any guide cannot be accessed, stop and inform the user. + +### 1. Test Plan Reading and Validation +Read and validate the provided test plan: + +**Validate plan completeness:** +- Verify plan contains coverage analysis summary +- Confirm test strategy is clearly defined +- Check that component-specific tests are detailed +- Ensure implementation notes are present +- Validate success metrics are specified + +Stop if the plan is incomplete, unclear, or missing critical sections. + +### 2. Plan Compliance Verification +**Ensure plan aligns with project principles:** + +**Verify plan adheres to project testing guidelines:** +- No monkey-patching of internal code required +- Dependency injection patterns are viable +- Test numbering follows project conventions +- No external network testing planned + +**Check for conflicts with existing tests:** +- Review planned test module names against existing files +- Verify planned test function numbering doesn't conflict +- Ensure no duplication of existing test coverage + +### 3. Test Data and Fixture Setup +**Prepare test data and dependencies as specified in the plan:** + +**Ensure required test dependencies are available:** +If the test plan requires dependencies not in the current environment, add them to `pyproject.toml`: + +```toml +[tool.hatch.envs.develop] +dependencies = [ + # ... existing dependencies + "pyfakefs", # For filesystem mocking + "pytest-asyncio", # For async test support + # ... other test-specific dependencies in alphabetical order +] +``` + +After adding dependencies, rebuild the environment to ensure consistency: +```bash +hatch env prune +``` + +**Create required test data under tests/data/:** +- Set up fake packages for extension mechanisms (if planned) +- Prepare captured artifacts and snapshots (if planned) +- Create any mock data files as specified in the plan + +Only create test data explicitly mentioned in the test plan. + +### 4. Test Module Creation/Updates +**Implement test modules following the plan:** + +**For each planned test module:** +- Create or update test files with planned naming (e.g., `test_100_exceptions.py`) +- Follow planned test function numbering within modules +- Implement only the tests specified in the plan +- Use dependency injection patterns as outlined in the plan + +**Key Implementation Guidelines:** +- Use dependency injection for all external dependencies as planned +- Prefer `pyfakefs.Patcher()` for filesystem operations as specified +- Mock only third-party services, never internal code +- **Insert tests in numerical order within files** - do NOT append to end +- **Write behavior-focused docstrings**: "Functionality is correct with Y" NOT "function_name does X with Y" +- Follow existing naming conventions and code style +- Implement tests in the exact order and numbering specified in the plan + +### 5. Coverage Validation +**Verify implementation matches plan coverage goals:** +```bash +hatch --env develop run testers +hatch --env develop run coverage report --show-missing +``` + +Verify plan compliance: +- Run full test suite to ensure no regressions +- Check that coverage matches the plan's target metrics +- Verify all planned test functions are implemented +- Confirm coverage gaps identified in the plan are addressed +- Ensure no existing functionality is broken + +### 6. Code Quality Validation +**Ensure implemented tests meet project standards:** +```bash +hatch --env develop run linters +``` + +**Requirements:** +- All linting checks must pass +- Note that the linters do not check style; you must verify style compliance +- No violations of project coding standards +- Test docstrings are clear and descriptive +- Proper imports and dependencies +- Implementation follows all conventions specified in the plan + +## Test Pattern Examples + +**Import Patterns:** + +*Direct imports (preferred for most cases):* +```python +from mypackage import mymodule + +def test_100_basic_functionality( ): + ''' Module function works correctly with valid input. ''' + result = mymodule.process_data( 'test' ) + assert result == 'processed: test' +``` + +**Dependency Injection Pattern:** +```python +async def test_100_process_with_custom_processor( ): + ''' Process function accepts custom processor via injection. ''' + def mock_processor( data ): + return f"processed: {data}" + + result = await process_data( 'test', processor = mock_processor ) + assert result == "processed: test" +``` + +**Filesystem Operations (Preferred):** +```python +def test_200_config_file_processing( ): + ''' Configuration files are processed correctly. ''' + with Patcher( ) as patcher: + fs = patcher.fs + fs.create_file( '/fake/config.toml', contents = '[section]\nkey="value"' ) + result = process_config_file( Path( '/fake/config.toml' ) ) + assert result.key == 'value' +``` + +**Error Handling:** +```python +def test_300_invalid_input_handling( ): + ''' Invalid input raises appropriate exceptions. ''' + with pytest.raises( ValueError, match = "Invalid data format" ): + process_invalid_data( "malformed" ) +``` + +## Success Criteria + +Implementation is complete when: +- [ ] All tests specified in the plan have been implemented +- [ ] Coverage matches or exceeds the plan's target metrics +- [ ] All planned test modules and functions are created with correct numbering +- [ ] Test data and fixtures are set up as specified in the plan +- [ ] All new tests pass consistently +- [ ] No existing tests are broken +- [ ] Linting passes without issues +- [ ] Project coding practices and style have been followed +- [ ] Tests follow project numbering conventions as planned +- [ ] Tests are inserted in proper numerical order within files +- [ ] Test docstrings focus on behavior, not function names +- [ ] Dependency injection is used as specified in the plan +- [ ] No monkey-patching of internal code +- [ ] Performance-conscious patterns are applied as planned + +**Note**: Always run full validation (`hatch --env develop run linters && hatch +--env develop run testers`) before considering the task complete. + +## Final Report + +Upon completion, provide a brief report covering: +- **Plan Compliance**: Confirmation that all planned tests were implemented as specified +- **Coverage Achievement**: Final coverage percentages vs. plan targets +- **Deviations from Plan**: Any necessary changes made to the plan during implementation with justification +- **Technical Issues Resolved**: Any conflicts encountered and how they were resolved +- **Pragma Directives Applied**: Any `# pragma: no cover` or `# pragma: no branch` added with rationale +- **Test Data Created**: Summary of fixtures and test data files created under `tests/data/` +- **Module Updates**: List of test modules created or updated with their numbering +- **Code Quality**: Confirmation that tests are properly ordered and have behavior-focused docstrings diff --git a/.auxiliary/configuration/claude/commands/cs-develop-tests.md b/.auxiliary/configuration/claude/commands/cs-develop-tests.md deleted file mode 100644 index dfadab5..0000000 --- a/.auxiliary/configuration/claude/commands/cs-develop-tests.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -allowed-tools: Bash(hatch --env develop run:*), Bash(git status), Bash(git log:*), Bash(echo:*), Bash(ls:*), Bash(find:*), LS, Read, Glob, Grep, Write, Edit, MultiEdit, WebFetch -description: Write comprehensive tests following project testing guidelines and improve coverage ---- - -# Write Tests - -**NOTE: This is an experimental workflow! If anything seems unclear or missing, -please stop for consultation with the user.** - -For systematic test creation following project testing guidelines and conventions. - -Test requirements: `$ARGUMENTS` - -**CRITICAL**: Apply project testing principles consistently. -**HALT if**: -- No test requirements are provided -- Target code cannot be analyzed -- Testing principles would be violated - -## Context - -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` -- Current test coverage: !`hatch --env develop run coverage report --skip-covered || echo "No coverage data available"` -- Existing test structure: !`find tests -name "*.py" | head -20` -- Test README: !`ls tests/README.md 2>/dev/null && echo "Present" || echo "Missing"` - -## Prerequisites - -Ensure that you: -- have verified access to / read target code modules -- understand what specific tests need to be written -- have read any relevant `CLAUDE.md` file -- understand the [test-writing - guidelines](https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst). - -## Testing Principles (from project guidelines) - -**Core Principles:** -1. **Dependency Injection Over Monkey-Patching**: Use injectable dependencies - for testability -2. **Performance-Conscious**: Prefer in-memory filesystems (pyfakefs) over temp - directories -3. **Avoid Monkey-Patching**: Never patch internal code; use dependency - injection instead -4. **100% Coverage Goal**: Aim for complete line and branch coverage -5. **Test Behavior, Not Implementation**: Focus on observable behavior and - contracts - -**Anti-Patterns to Avoid:** -- Monkey-patching internal code (will fail with immutable objects) -- Excessive mocking of internal components -- Testing implementation details vs. behavior -- Using temp directories when pyfakefs suffices - -**Organization:** -- Follow the systematic numbering conventions detailed in the test guidelines - -## Safety Requirements - -**CRITICAL**: You MUST halt the process and consult with the user if ANY of the -following occur: - -- **Anti-Pattern Detection**: If proposed tests violate project principles -- **Coverage Regression**: If tests would reduce existing coverage -- **Architecture Conflicts**: If tests require monkey-patching internal code -- **Numbering Conflicts**: If test numbering clashes with existing conventions -- **Missing Dependencies**: If required test fixtures or dependencies are - unavailable - -**Your responsibilities:** -- Follow project style and test-writing conventions exactly. -- Use dependency injection patterns consistently. -- Prefer pyfakefs for filesystem operations. -- Maintain systematic test numbering. -- Ensure tests validate behavior, not implementation. - -## Test Writing Process - -Execute the following steps for test requirements: `$ARGUMENTS` - -### 0. Pre-Flight Verification -**MANDATORY - Verify access to test-writing guide:** - -Use WebFetch to access and confirm you can read the complete testing -guidelines: -https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst - -**CRITICAL**: You MUST successfully access and read the guide before -proceeding. If WebFetch fails, HALT and consult with the user. - -### 1. Code Analysis Phase -Examine the target code to understand testing needs: - -**Check for existing related tests to avoid duplication:** -- Search for existing test files covering target modules -- Review test coverage reports for current state -- Identify gaps rather than recreating existing tests - -**For each target file:** -- Read the source code to understand public API -- Identify functions/classes that need testing -- Note dependency injection points -- Check for existing test coverage gaps - -### 2. Test Structure Planning -Determine appropriate test organization and categories: - -**Review existing test structure and plan test numbering following project conventions.** - -**If tests/README.md is missing, create it with:** -- Test module numbering scheme specific to the package -- Rationale for any use of patch or other exceptions to standard patterns -- Project-specific testing conventions and fixtures - -**Test Categories to Include:** -- **Basic Functionality Tests (000-099):** Happy path scenarios, input validation, basic error conditions -- **Feature-Specific Tests (100+ blocks):** Each public function/class gets its own 100-block with normal usage patterns, edge cases, and error handling -- **Integration Tests (higher numbers):** Cross-module interactions and end-to-end workflows - -### 3. Test Implementation -Create tests following project conventions: - -**Key Implementation Guidelines:** -- Use dependency injection for all external dependencies -- Prefer `pyfakefs.Patcher()` for filesystem operations -- Mock only third-party services, never internal code -- Include docstrings explaining what behavior is tested -- Follow existing naming conventions and code style - -### 5. Coverage Validation -Verify tests improve coverage without regressions: -```bash -hatch --env develop run testers -hatch --env develop run coverage report --show-missing -``` - -**CRITICAL - VERIFY COVERAGE IMPROVEMENT:** -- Run full test suite to ensure no regressions -- Check that new tests increase overall coverage -- Verify no existing functionality is broken -- Confirm tests follow project numbering conventions - -### 6. Code Quality Validation -Ensure tests meet project standards: -```bash -hatch --env develop run linters -``` - -**Requirements:** -- All linting checks must pass -- No violations of project coding standards -- Test docstrings are clear and descriptive -- Proper imports and dependencies - -## Test Pattern Examples - -**Dependency Injection Pattern:** -```python -async def test_100_process_with_custom_processor( ): - ''' Process function accepts custom processor via injection. ''' - def mock_processor( data ): - return f"processed: {data}" - - result = await process_data( "test", processor = mock_processor ) - assert result == "processed: test" -``` - -**Filesystem Operations (Preferred):** -```python -def test_200_config_file_processing( ): - ''' Configuration files are processed correctly. ''' - with Patcher( ) as patcher: - fs = patcher.fs - fs.create_file( '/fake/config.toml', contents = '[section]\nkey="value"' ) - - result = process_config_file( Path( '/fake/config.toml' ) ) - assert result.key == 'value' -``` - -**Error Handling:** -```python -def test_300_invalid_input_handling( ): - ''' Invalid input raises appropriate exceptions. ''' - with pytest.raises( ValueError, match = "Invalid data format" ): - process_invalid_data( "malformed" ) -``` - -## Success Criteria - -Tests are complete when: -- [ ] Coverage has measurably improved -- [ ] All new tests pass consistently -- [ ] No existing tests are broken -- [ ] Linting passes without issues -- [ ] Tests follow project numbering conventions -- [ ] Dependency injection is used appropriately -- [ ] No monkey-patching of internal code -- [ ] Performance-conscious patterns are applied - -**Note**: Always run full validation (`hatch --env develop run testers && hatch ---env develop run linters`) before considering the task complete. - -## Final Report - -Upon completion, provide a brief report covering: -- Coverage improvements achieved (before/after percentages) -- Any technical conflicts encountered (e.g., dataclass/protocol issues, __slots__ conflicts) -- How any conflicts were resolved or worked around -- Pragma directives applied (# pragma: no cover, # pragma: no branch) and rationale -- Any deviations from standard patterns and justification diff --git a/.auxiliary/configuration/claude/commands/cs-document-examples-rst.md b/.auxiliary/configuration/claude/commands/cs-document-examples-rst.md new file mode 100644 index 0000000..460ca10 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-document-examples-rst.md @@ -0,0 +1,117 @@ +--- +description: Creates practical, testable examples documentation +--- + +# Document Examples + +Develops practical, testable examples for documentation under +`documentation/examples/` that increase test coverage while remaining relatable +and succinct. + +Topic: $ARGUMENTS + +## Context + +- Project structure: @documentation/architecture/filesystem.rst +- Existing examples: !`ls -la documentation/examples/ 2>/dev/null || echo "No examples directory"` +- Code coverage data: !`hatch --env develop run testers 2>/dev/null || echo "No coverage data available"` + +## Prerequisites + +Before creating examples documentation: +- Understand the target audience (developers vs end users) +- Analyze existing codebase to identify core functionality patterns +- Review existing examples for organization, completeness, and thematic inspiration +- Examine @.auxiliary/instructions/ for style and nomenclature requirements + +## Process Summary + +Key functional areas: +1. **Analysis**: Survey codebase and existing examples to identify documentation gaps +2. **Theme Development**: Create coherent scenarios that demonstrate functionality progression +3. **Content Creation**: Write succinct examples using proper reStructuredText formatting +4. **Validation**: Ensure examples follow project practices and can serve as informal tests + +## Safety Requirements + +Stop and consult the user if: +- Examples require creating contrived scenarios that don't reflect real usage +- Multiple conflicting themes emerge without clear organizational strategy +- Proposed examples would expose internal implementation details inappropriately +- Documentation format conflicts with existing project conventions + +## Execution + +Execute the following steps: + +### 1. Analyze Existing Documentation Structure + +Survey the current documentation to understand patterns and identify gaps. Read +existing example files to understand established themes and formatting +approaches. + +### 2. Survey Codebase for Example Opportunities + +Identify public API surfaces and common usage patterns. Analyze coverage +reports in `.auxiliary/artifacts/coverage-pytest` if available. + +Look for: +- Public classes and functions that need demonstration +- Common workflows that span multiple components +- CLI commands and their typical usage patterns +- Error handling scenarios that users should understand + +### 3. Develop Thematic Coherence + +Based on analysis, choose one of these organizational approaches: + +- **Domain scenarios**: Practical use cases +- **API progression**: Basic to advanced usage of core functionality +- **Workflow examples**: End-to-end processes showing component interaction +- **CLI workflows**: Command sequences for common tasks + +### 4. Create Example Documentation + +Write examples following these requirements: + +- Use Sphinx reStructuredText format with proper double backticks for inline literals +- Include blank lines before list items per reStructuredText conventions +- Structure as progression from simple to complex scenarios +- Use doctest format for Python API examples where testable +- Use code-block format for CLI examples with explicit command annotation +- Keep code blocks comment-free; put explanatory text between blocks +- Follow @.auxiliary/instructions/practices.rst for general code organization +- Follow @.auxiliary/instructions/style.rst for formatting +- Follow @.auxiliary/instructions/nomenclature.rst for naming +- When documenting Python code, also follow .auxiliary/instructions/practices-python.rst for comprehensive Python standards +- When documenting Rust code, also follow .auxiliary/instructions/practices-rust.rst for comprehensive Rust standards +- When documenting TOML configuration, also follow .auxiliary/instructions/practices-toml.rst for comprehensive TOML standards + +### 5. Ensure Practical Relevance + +Verify each example: + +- Demonstrates functionality users actually need +- Shows practical data and scenarios, remaining minimalist rather than elaborate +- Includes appropriate error cases and edge conditions +- Can serve as informal test coverage for documented features +- Follows established project patterns for similar examples + +### 6. Validate Documentation Quality + +Review final documentation for: + +- Proper reStructuredText syntax and formatting +- Consistent theme and progression across examples +- Adherence to project style guidelines +- Executable/testable nature of code examples +- Clear explanatory text that guides readers through concepts + +### 7. Provide Summary + +Provide a succinct summary to the user describing: + +- What examples were created or updated +- The organizational theme chosen and why +- Key functionality areas covered +- How the examples serve both documentation and testing goals diff --git a/.auxiliary/configuration/claude/commands/cs-excise-python.md b/.auxiliary/configuration/claude/commands/cs-excise-python.md new file mode 100644 index 0000000..d2731f1 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-excise-python.md @@ -0,0 +1,155 @@ +--- +allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(hatch --env develop run:*), Bash(git status), Bash(git diff), mcp__pyright__references, mcp__pyright__hover, mcp__pyright__diagnostics +description: Analyze Vulture dead code findings and remediate through selective removal or vulturefood.py whitelisting +--- + +# Python Dead Code Analysis and Remediation + +Systematically analyze Vulture dead code findings and remediate through selective removal or vulturefood.py whitelisting using Pyright MCP server for accurate symbol reference verification. + +Target files or scope: $ARGUMENTS + +## Context + +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` +- Existing vulturefood entries: !`wc -l .auxiliary/configuration/vulturefood.py` +- Vulture configuration: @pyproject.toml (tool.vulture section) + +## Prerequisites + +Before running this analysis, ensure: +- Understanding of project codebase and critical symbols +- Read project documentation guides: + - @.auxiliary/instructions/practices.rst + - @.auxiliary/instructions/style.rst +- Vulture is installed and configured in the development environment +- Pyright MCP server is available for symbol reference verification + +## Process Summary + +Key functional areas: +1. **Detection and Parsing**: Run Vulture and parse output for unused symbols +2. **Reference Verification**: Use Pyright MCP server to verify actual symbol usage +3. **Classification Analysis**: Apply heuristics to identify false positives vs. genuine dead code +4. **Selective Remediation**: Present findings with confidence levels for user decision +5. **Implementation**: Remove dead code or add entries to vulturefood.py as appropriate + +## Safety Requirements + +Stop and consult the user if: +- Uncertain about whether a symbol should be removed or whitelisted +- Complex inheritance hierarchies with unclear symbol usage patterns +- Vulture reports conflict significantly with Pyright reference analysis +- Ambiguous decorator patterns that don't fit standard heuristics + +## Execution + +Execute the following steps: + +### 1. Vulture Analysis and Parsing + +Run Vulture to identify potentially unused symbols: +```bash +hatch --env develop run vulture --min-confidence=60 +``` + +Examine the output to extract: +- Symbol names and types (functions, classes, variables) +- File locations and line numbers +- Confidence levels reported by Vulture +- Symbol categories (imports, definitions, assignments) + +### 2. Pyright Reference Verification + +For each symbol identified by Vulture, verify actual usage using Pyright MCP: + +Use `mcp__pyright__references` with **bare symbol names** (not qualified paths): +- Correct: `symbolName="function_name"` +- Incorrect: `symbolName="module.package.function_name"` + +Analyze reference results: +- No references found: Likely genuine dead code +- References found: Examine context for legitimacy +- Import-only references: May indicate transitional dead code + +### 3. False Positive Classification + +Apply systematic heuristics to identify false positives: + +**Common False Positive Patterns:** +- Abstract methods and protocol implementations +- Decorator-registered functions (pytest fixtures, Flask routes, etc.) +- Magic methods and dunder attributes +- CLI entry points and script main functions +- Test fixtures and utilities used via dynamic discovery +- Library interface methods called by external code + +**Analysis Criteria:** +- Decorator presence and types +- Inheritance relationships and abstract base classes +- Usage patterns in test files vs. main code +- External integration points and plugin systems + +### 4. Autonomous Decision Making + +Apply systematic decision logic: + +**Remove Symbol If:** +- No references found via Pyright (zero references) +- No TODO comments mentioning future use of the symbol +- Not an entry point function (e.g., `main`) +- Not part of unimplemented interface or abstract base class +- Not decorated with framework-specific decorators + +**Whitelist in vulturefood.py If:** +- Has references but appears to be false positive (decorators, abstract methods, etc.) +- Entry point functions like `main` +- Abstract/interface implementations +- Framework integration points with decorators +- Magic methods and protocol compliance + +**Check for TODO Comments:** +Examine surrounding code and docstrings for TODO comments that reference the symbol or indicate planned future usage. + +### 5. Implementation Decision + +Act autonomously based on decision logic: + +**For Symbol Removal:** +- Remove symbol definitions and any orphaned imports +- Verify removal doesn't break related functionality +- Run linters to ensure clean code after removal + +**For Vulturefood Whitelisting:** +- Add entries to `.auxiliary/configuration/vulturefood.py` with format: + ```python + symbol_name # description of why it's a false positive + ``` +- Group related entries and add explanatory comments +- Maintain alphabetical organization within groups + +### 6. Validation and Verification + +After remediation: +- Run Vulture again to confirm issues are resolved +- Execute linters to ensure code quality: `hatch --env develop run linters` +- Run tests to verify functionality: `hatch --env develop run testers` +- Verify git diff shows only intended changes + +## Implementation Notes + +**Pyright MCP Usage:** +- Use bare symbol names for accurate reference finding +- Leverage superior semantic analysis over text-based search tools +- Cross-reference hover information for additional context + +**Vulturefood Management:** +- Maintain clear documentation for each whitelisted symbol +- Group related false positives with explanatory sections +- Prefer descriptive comments over generic suppression + +**Safety Practices:** +- Remove code autonomously when decision criteria are clearly satisfied +- Prioritize false positive whitelisting when uncertainty exists +- Validate all changes through comprehensive testing \ No newline at end of file diff --git a/.auxiliary/configuration/claude/commands/cs-inquire.md b/.auxiliary/configuration/claude/commands/cs-inquire.md new file mode 100644 index 0000000..2fcfb6f --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-inquire.md @@ -0,0 +1,72 @@ +--- +allowed-tools: Read, LS, Glob, Grep, WebFetch, WebSearch +description: Provide analytical responses and technical opinions without making code changes +--- + +# Technical Analysis and Discussion + +Provide analytical responses, technical opinions, and architectural discussion +based on user questions. Focus on analysis and reasoning without making code +modifications. + +User question or topic: $ARGUMENTS + +Stop and consult if: +- The request explicitly asks for code changes or implementation +- The question is unclear or lacks sufficient context +- Multiple conflicting requirements are presented + +## Prerequisites + +Before providing analysis, ensure: +- Clear understanding of the technical question being asked +- Sufficient context about the codebase or architecture being discussed + +## Process Summary + +Key analytical areas: +1. **Question Analysis**: Understand what is being asked and why +2. **Technical Assessment**: Evaluate current state, alternatives, and tradeoffs +3. **Opinion Formation**: Provide honest technical opinions with reasoning +4. **Discussion**: Present pros/cons, alternatives, and recommendations + +## Execution + +Execute the following process: + +### 1. Question Understanding +Carefully analyze the user's question to understand: +- What specific technical aspect they want to discuss +- The context and scope of their concern +- Whether they're seeking validation, alternatives, or general analysis + +### 2. Current State Assessment +Examine relevant parts of the codebase or architecture, if necessary: +- Read pertinent files to understand current implementation +- Identify patterns, conventions, and existing approaches +- Note any potential issues or areas of concern + +### 3. Technical Analysis +Provide comprehensive analysis including: +- **Strengths**: What works well in the current approach +- **Weaknesses**: Potential issues, limitations, or concerns +- **Alternatives**: Different approaches that could be considered +- **Tradeoffs**: Benefits and costs of different options + +### 4. Opinion and Recommendations +Offer honest technical opinions: +- Present your assessment based on best practices and experience +- Provide pushback if you disagree with assumptions or proposals +- Suggest better alternatives when they exist +- Explain the reasoning behind your recommendations + +### 5. Discussion Points +Raise additional considerations: +- Edge cases that might not have been considered +- Long-term maintenance implications +- Performance, security, or scalability concerns +- Integration with existing systems or patterns + +Remember: Your role is to analyze, discuss, and provide technical opinions - +not to implement solutions or make code changes. Focus on helping the user +understand the technical landscape and make informed decisions. diff --git a/.auxiliary/configuration/claude/commands/cs-manage-prd.md b/.auxiliary/configuration/claude/commands/cs-manage-prd.md new file mode 100644 index 0000000..31a74a2 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-manage-prd.md @@ -0,0 +1,90 @@ +--- +allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep +description: Manage product requirements documents and feature planning +--- + +# Product Requirements Management + +Manage and update the Product Requirements Document (PRD) based on user input +about product requirements, feature planning, and related topics. + +Request from user: $ARGUMENTS + +## Context + +- Current PRD state: @documentation/prd.rst +- Requirements guidelines: @.auxiliary/instructions/requirements.rst + +## Prerequisites + +Before managing PRD content, ensure: +- Understanding of current project scope and objectives +- Familiarity with existing functional and non-functional requirements +- @.auxiliary/instructions/requirements.rst guidelines are followed +- Changes align with overall project strategy + +## Process Summary + +Key functional areas: +1. **Analysis**: Review current PRD and understand requested changes +2. **Requirements Processing**: Apply requirements.rst standards to new content +3. **PRD Updates**: Make structured updates to documentation/prd.rst +4. **Validation**: Ensure consistency and completeness + +### Process Restrictions + +- Do not provide a timeline for deliverables. +- Do not plan sprints. + +## Safety Requirements + +Stop and consult the user if: +- Requested changes significantly expand or reduce product scope +- New requirements conflict with existing non-functional requirements +- Changes affect critical path features or constraints +- Requirements lack sufficient detail for implementation planning + +## Execution + +Execute the following steps: + +### 1. Review Current State +Read and analyze the existing PRD to understand current scope. + +### 2. Process User Requirements +Analyze the user input for: +- New functional requirements +- Changes to existing requirements +- Updates to goals, objectives, or success criteria +- Modifications to user personas or target users +- New constraints or assumptions + +### 3. Apply Requirements Standards +Follow @.auxiliary/instructions/requirements.rst guidelines: +- Use specific, measurable, achievable, relevant, testable criteria +- Apply proper user story format when appropriate +- Assign requirement priorities (Critical/High/Medium/Low) +- Include acceptance criteria for functional requirements +- Maintain requirement traceability + +### 4. Update PRD Structure +Make targeted updates to appropriate PRD sections: +- Executive Summary (if scope changes) +- Problem Statement (if new problems identified) +- Goals and Objectives (if success criteria change) +- Target Users (if new personas or needs identified) +- Functional Requirements (most common updates) +- Non-Functional Requirements (if technical requirements change) +- Constraints and Assumptions (if new limitations discovered) +- Out of Scope (if boundaries need clarification) + +### 5. Maintain Consistency +Ensure all updates maintain PRD coherence: +- Requirements align with stated goals and objectives +- No conflicts between functional and non-functional requirements +- User stories trace back to identified user needs +- Acceptance criteria are testable and specific +- Priority assignments reflect user value + +### 6. Summarize Updates +Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/cs-plan-pytests.md b/.auxiliary/configuration/claude/commands/cs-plan-pytests.md new file mode 100644 index 0000000..72a2761 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-plan-pytests.md @@ -0,0 +1,280 @@ +--- +description: Analyze Python test coverage gaps and create focused test plan for uncovered areas and edge cases +--- + +# Plan Python Tests + +For systematic analysis of test coverage gaps and creation of detailed test +plans following project testing guidelines. + +Target module/functionality: $ARGUMENTS + +Focus on analysis and planning only - do not implement tests. + +## Context + +- Current git status: !`git status --porcelain` +- Current branch: !`git branch --show-current` +- Current test coverage: !`hatch --env develop run coverage report --show-missing` +- Existing test structure: !`find tests -name "*.py" | head -20` +- Test organization: @documentation/architecture/testplans/summary.rst +- Test plans index: @documentation/architecture/testplans/index.rst + +## Prerequisites + +Ensure that you: +- Have access to target code modules for analysis +- Can generate current coverage reports +- Have read any relevant `CLAUDE.md` file +- Understand the test-writing guidelines: @.auxiliary/instructions/tests.rst + +## Safety Requirements + +Stop and consult the user if: +- No target module or functionality is provided +- Target code cannot be analyzed +- Coverage data is unavailable +- Coverage reports cannot be generated +- Target modules cannot be read or analyzed +- Analysis reveals fundamental testability issues +- Test guidelines cannot be accessed +- Network tests against real external sites are being considered + +**Your responsibilities:** +- Focus entirely on analysis and planning - NO implementation +- Create comprehensive, actionable test plans WITHOUT code snippets of test implementations +- Focus planning on uncovered areas and edge cases +- Brief third-party library examples (e.g., httpx mock transport) are acceptable if researched +- Identify all coverage gaps systematically +- Consider project testing philosophy: doctests for examples, pytest for edge cases +- Produce clear, structured planning artifacts +- Acknowledge immutability constraints - modules under test CANNOT be monkey-patched +- Test private functions/methods via public API - understand why if this fails + +## Test Planning Process + +Execute the following steps for target: $ARGUMENTS + +### 0. Pre-Flight Verification +Access test-writing guidelines: + +Read and understand the complete project guidelines: +- Testing: @.auxiliary/instructions/tests.rst +- Python Practices: @.auxiliary/instructions/practices-python.rst + +You must successfully access and understand both guides before proceeding. If any guide cannot be accessed, stop and inform the user. + +### 1. Coverage Analysis Phase + +**Generate and analyze current coverage data:** + +```bash +hatch --env develop run coverage report --show-missing +hatch --env develop run coverage html +``` + +Analysis requirements: +- Identify all uncovered lines in target modules +- Focus on uncovered lines and untested functionality +- Determine which edge cases and error paths are untested +- Note any pragma directives (# pragma: no cover) and their rationale + +### 1.5. Example Coverage Analysis + +**Review existing documentation examples:** + +Survey documentation examples to understand what's already demonstrated: +- Read relevant example files in `documentation/examples/` if they exist +- Identify code paths already exercised by user-focused examples +- Note which functionality is already well-demonstrated through practical scenarios +- Focus pytest planning on genuinely uncovered areas not addressed by examples +- Avoid redundant testing of functionality that examples already exercise + +**Integration with pytest planning:** +- Complement rather than duplicate example coverage +- Target edge cases and error conditions that examples don't demonstrate +- Focus on defensive code paths and boundary conditions +- Plan systematic coverage of areas examples don't naturally reach + +**For each target module:** +- Read the source code to understand the public API +- Identify all functions, classes, and methods +- Map uncovered lines to specific functionality +- Note dependency injection points and testability patterns + +### 2. Gap Identification Phase + +**Systematically catalog what needs testing:** + +**Functionality Gaps:** +- Public functions with zero test coverage +- Classes with untested public methods +- Error handling paths not exercised +- Edge cases not covered + +**Coverage Gaps:** +- Specific line numbers needing coverage +- Branch conditions not tested +- Exception handling paths missed +- Integration scenarios untested + +**Architecture Gaps:** +- Code that requires dependency injection for testability +- Components that need filesystem mocking +- External service interactions requiring test doubles +- Private functions/methods not exercisable via public API +- Areas where full coverage may require violating immutability constraints +- Test data requirements (fixtures, snapshots, fake packages for `tests/data/`) + +### 3. Test Strategy Development + +**For each identified gap, determine:** + +**Test Approach:** +- Which testing patterns apply (dependency injection, pyfakefs, etc.) +- What test doubles or fixtures are needed +- How to structure tests for maximum coverage + +**Test Categories:** +- Basic functionality tests (000-099 range) +- Component-specific tests (100+ blocks per function/class/method) +- Edge cases and error handling (integrated within component blocks) + +**Implementation Considerations:** +- Dependencies that need injection +- Filesystem operations requiring pyfakefs +- External services needing mocking (NEVER test against real external sites) +- Test data and fixtures needed under `tests/data/` +- Performance considerations + +### 4. Test Organization Planning + +**Determine test structure and numbering:** + +**Review existing test numbering conventions:** +- Analyze current test file naming patterns +- Identify next available number blocks for new test modules +- Plan numbering for new test functions within modules + +Test module vs function numbering: +- **Test modules**: Named as `test_<N>00_<module>.py` (e.g., `test_100_exceptions.py`, `test_500_cli.py`) +- **Test functions**: Within modules use 000-099 basic, 100+ blocks per component +- These are DIFFERENT numbering schemes - do not confuse them + +**Test Module Numbering Hierarchy:** +- Lower-level functionality gets lower numbers (e.g., `test_100_exceptions.py`, `test_110_utilities.py`) +- Higher-level functionality gets higher numbers (e.g., `test_500_cli.py`, `test_600_server.py`) +- Subpackage modules: `test_<M><N>0_<subpackage>_<module>.py` where N advances by 10 within subpackage + +**Update test organization documentation:** +- Update `documentation/architecture/testplans/summary.rst` with test module numbering scheme +- Include project-specific testing conventions and new modules being planned +- Document rationale for any pattern exceptions +- Update during planning, not during implementation + +### 5. Plan Documentation Creation + +**Create comprehensive test plan document:** + +Save the plan to `documentation/architecture/testplans/[sanitized-module-name].rst` and update `documentation/architecture/testplans/index.rst` to include the new test plan in the toctree. + +Create the test plan document with: + +**Plan Structure (reStructuredText format):** +```rst +******************************************************************************* +Test Plan: [Module Name] +******************************************************************************* + +Coverage Analysis Summary +=============================================================================== + +- Current coverage: X% +- Target coverage: 100% +- Uncovered lines: [specific line numbers] +- Missing functionality tests: [list] + +Test Strategy +=============================================================================== + +Basic Functionality Tests (000-099) +------------------------------------------------------------------------------- + +- [List planned tests with brief descriptions] + +Component-Specific Tests (100+ blocks) +------------------------------------------------------------------------------- + +Function/Class/Method: [name] (Tests 100-199) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- [Planned test descriptions including happy path, edge cases, and error handling] +- [Dependencies needing injection] +- [Special considerations] + +Implementation Notes +=============================================================================== + +- Dependencies requiring injection: [list] +- Filesystem operations needing pyfakefs: [list] +- External services requiring mocking: [list - NEVER test against real external sites] +- Test data and fixtures: [needed under tests/data/ - fake packages, snapshots, captured artifacts] +- Private functions/methods not testable via public API: [list with analysis] +- Areas requiring immutability constraint violations: [list with recommendations] +- Third-party testing patterns to research: [e.g., httpx mock transport] +- Test module numbering for new files: [following hierarchy conventions] +- Anti-patterns to avoid: [specific warnings including external network calls] + +Success Metrics +=============================================================================== + +- Target line coverage: [percentage] +- Branch coverage goals: [percentage] +- Specific gaps to close: [line numbers] +``` + +### 6. Plan Validation + +**Review and validate the plan:** + +**Completeness Check:** +- All uncovered lines addressed +- All functions/classes have test strategy +- Error paths and edge cases included +- Integration scenarios covered + +**Feasibility Check:** +- All planned tests align with project principles +- No monkey-patching of internal code required +- Dependency injection patterns are viable +- Performance considerations addressed + +**Numbering Check:** +- Test numbering follows project conventions +- No conflicts with existing test numbers +- Logical organization by test type + +## Success Criteria + +Planning is complete when: +- [ ] Complete coverage analysis performed +- [ ] All testing gaps systematically identified +- [ ] Test strategy developed for each gap +- [ ] Test organization and numbering planned +- [ ] `documentation/architecture/testplans/summary.rst` updated as needed +- [ ] Comprehensive plan document created in testplans directory +- [ ] `documentation/architecture/testplans/index.rst` updated to include new plan +- [ ] Plan validates against project testing principles +- [ ] Implementation approach is clear and actionable + +## Final Report + +Upon completion, provide a brief summary covering: +- Current coverage percentage and specific gaps identified +- Number of new tests planned by category +- Key architectural considerations (dependency injection needs, etc.) +- Assessment: Areas where 100% coverage may be impossible without violating immutability constraints +- **PUSHBACK RECOMMENDATIONS**: Suggested architectural improvements to enable better testability +- Private functions/methods that cannot be exercised via public API and analysis of why +- Estimated complexity and implementation priority +- Any potential challenges or special considerations diff --git a/.auxiliary/configuration/claude/commands/cs-release-checkpoint.md b/.auxiliary/configuration/claude/commands/cs-release-checkpoint.md new file mode 100644 index 0000000..7199e10 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-release-checkpoint.md @@ -0,0 +1,163 @@ +--- +allowed-tools: Bash(git status), Bash(git pull:*), Bash(git add:*), Bash(git commit:*), Bash(git tag:*), Bash(git push:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), Bash(date:*), LS, Read +description: Execute automated alpha checkpoint release with QA monitoring +argument-hint: "[alpha]" +--- + +# Release Checkpoint + +**NOTE: This is an experimental workflow! If anything seems unclear or missing, +please stop for consultation with the user.** + +For execution of an automated alpha checkpoint release on master branch. + +Below is a validated process to create an alpha checkpoint release with automated +monitoring and version increment. + +Target alpha increment: $ARGUMENTS +(optional - defaults to next alpha) + +Verify current version is alpha format if no arguments provided. + +Stop and consult if: +- Working directory has uncommitted changes +- Current version is not an alpha version (e.g., 1.3.0, 1.3rc1) and no target specified +- Git operations fail or produce unexpected output + +## Context + +- Current git status: !`git status` +- Current branch: !`git branch --show-current` +- Current version: !`hatch version` +- Recent commits: !`git log --oneline -10` + +## Prerequisites + +Before starting, ensure: +- GitHub CLI (`gh`) is installed and authenticated +- Working directory is clean with no uncommitted changes +- Currently on master branch +- Current version is an alpha version (e.g., 1.3a0) + +## Process Summary + +Key functional areas of the process: + +1. **Pre-Release Quality Check**: Run local QA to catch issues early +2. **Changelog Generation**: Run Towncrier to build changelog +3. **QA Monitoring**: Push commits and monitor QA workflow with GitHub CLI +4. **Tag Release**: Create alpha tag with current version after QA passes +5. **Release Monitoring**: Monitor release workflow deployment +6. **Post-Release Cleanup**: Remove news fragments and bump alpha version + +## Safety Requirements + +Stop and consult the user if any of the following occur: + +- **Step failures**: If any command fails, git operation errors, or tests fail +- **Workflow failures**: If QA or release workflows show failed jobs +- **Unexpected output**: If commands produce unclear or concerning results +- **Version conflicts**: If version bumps don't match expected patterns +- **Network issues**: If GitHub operations timeout or fail repeatedly + +**Your responsibilities**: +- Validate each step succeeds before proceeding to the next +- Monitor workflow status and halt on any failures +- Provide clear progress updates throughout the process +- Maintain clean git hygiene +- Use your judgment to assess when manual intervention is needed + +## Release Process + +Execute the following steps: + +### 1. Pre-Release Quality Check +Run local quality assurance to catch issues early: +```bash +git status && git pull origin master +hatch --env develop run linters +hatch --env develop run testers +hatch --env develop run docsgen +``` + +### 2. Changelog Generation +Run Towncrier to update changelog with current version: +```bash +hatch --env develop run towncrier build --keep --version $(hatch version) +git commit -am "Update changelog for v$(hatch version) release." +``` + +### 3. Quality Assurance Phase +Push commits and monitor QA workflow: +```bash +git push origin master +``` + +Workflow monitoring requirements: +After pushing, you MUST ensure you monitor the correct QA workflow run: + +1. **Wait for workflow trigger**: Wait 10 seconds after pushing to allow GitHub to trigger the workflow +2. **Verify correct workflow**: Use `gh run list --workflow=qa --limit=5` to list recent runs +3. **Check timestamps**: Compare the workflow creation time with your push time using `date --utc` +4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your push timestamp +5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow + +Once you've identified the correct QA run ID: +```bash +gh run watch <correct-qa-run-id> --interval 30 --compact +``` + +Do not proceed until workflow completes: +- Monitor QA workflow with `gh run watch` using the correct run ID +- Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands +- If command times out, immediately rerun `gh run watch` until completion +- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" +- Stop if any jobs fail - consult user before proceeding + +### 4. Alpha Release Deployment +**Verify QA passed before proceeding to alpha tag:** +```bash +git tag -m "Alpha checkpoint v$(hatch version)." v$(hatch version) +git push --tags +``` + +Release workflow monitoring requirements: +After pushing the tag, you MUST ensure you monitor the correct release workflow run: + +1. **Wait for workflow trigger**: Wait 10 seconds after pushing tags to allow GitHub to trigger the release workflow +2. **Verify correct workflow**: Use `gh run list --workflow=release --limit=5` to list recent runs +3. **Check timestamps**: Compare the workflow creation time with your tag push time using `date --utc` +4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your tag push timestamp +5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow + +Once you've identified the correct release run ID: +```bash +gh run watch <correct-release-run-id> --interval 30 --compact +``` + +Do not proceed until workflow completes: +- Monitor release workflow with `gh run watch` using the correct run ID +- Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands +- If command times out, immediately rerun `gh run watch` until completion +- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" +- Stop if any jobs fail - consult user before proceeding + +### 5. Post-Release Cleanup +Clean up Towncrier fragments: +```bash +git rm .auxiliary/data/towncrier/*.rst +git commit -m "Clean up news fragments." +``` + +### 6. Next Alpha Version +Bump to next alpha version: +```bash +hatch version alpha +git commit -am "Version: $(hatch version)" +``` + +### 7. Final Push +Push cleanup and version bump commits: +```bash +git push origin master +``` diff --git a/.auxiliary/configuration/claude/commands/cs-release-final.md b/.auxiliary/configuration/claude/commands/cs-release-final.md index ced2361..2854244 100644 --- a/.auxiliary/configuration/claude/commands/cs-release-final.md +++ b/.auxiliary/configuration/claude/commands/cs-release-final.md @@ -1,6 +1,7 @@ --- allowed-tools: Bash(git status), Bash(git pull:*), Bash(git checkout:*), Bash(git add:*), Bash(git commit:*), Bash(git tag:*), Bash(git rm:*), Bash(git cherry-pick:*), Bash(git log:*), Bash(git branch:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), LS, Read description: Execute automated final release with QA monitoring and development cycle setup +argument-hint: "major.minor" --- # Release Final @@ -13,10 +14,11 @@ For execution of a fully-automated final release. Below is a validated process to create a final release with automated monitoring and next development cycle setup. -Target release version: `$ARGUMENTS` +Target release version: $ARGUMENTS -**CRITICAL**: Verify exactly one target release version provided. -**HALT if**: +Verify exactly one target release version provided. + +Stop and consult if: - No target release version is provided - Multiple release versions provided (e.g., `1.6 foo bar`) - Release version format doesn't match `X.Y` pattern (e.g., `1.6.2`, `1.6a0`) @@ -53,8 +55,7 @@ Key functional areas of the process: ## Safety Requirements -**CRITICAL**: You MUST halt the process and consult with the user if ANY of the -following occur: +Stop and consult the user if any of the following occur: - **Step failures**: If any command fails, git operation errors, or tests fail - **Workflow failures**: If QA or release workflows show failed jobs @@ -71,7 +72,7 @@ following occur: ## Release Process -Execute the following steps for target version `$ARGUMENTS`: +Execute the following steps: ### 1. Pre-Release Quality Check Run local quality assurance to catch issues early: @@ -115,33 +116,56 @@ Push branch and monitor QA workflow: ```bash # Use -u flag for new branches, omit for existing git push [-u] origin release-$ARGUMENTS +``` + +Workflow monitoring requirements: +After pushing, you MUST ensure you monitor the correct QA workflow run: -# Monitor QA workflow - get run ID from output -gh run list --workflow=qa --limit=1 -gh run watch <qa-run-id> --interval 30 --compact +1. **Wait for workflow trigger**: Wait 10 seconds after pushing to allow GitHub to trigger the workflow +2. **Verify correct workflow**: Use `gh run list --workflow=qa --limit=5` to list recent runs +3. **Check timestamps**: Compare the workflow creation time with your push time using `date --utc` +4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your push timestamp +5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow + +Once you've identified the correct QA run ID: +```bash +gh run watch <correct-qa-run-id> --interval 30 --compact ``` -**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** -- Monitor QA workflow with `gh run watch` + +Do not proceed until workflow completes: +- Monitor QA workflow with `gh run watch` using the correct run ID - Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands - If command times out, immediately rerun `gh run watch` until completion - Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- HALT if any jobs fail - consult user before proceeding +- Stop if any jobs fail - consult user before proceeding ### 6. Release Deployment **Verify QA passed before proceeding to release tag:** ```bash git tag -m "Release v$(hatch version): <brief-description>." v$(hatch version) git push --tags +``` + +Release workflow monitoring requirements: +After pushing the tag, you MUST ensure you monitor the correct release workflow run: + +1. **Wait for workflow trigger**: Wait 10 seconds after pushing tags to allow GitHub to trigger the release workflow +2. **Verify correct workflow**: Use `gh run list --workflow=release --limit=5` to list recent runs +3. **Check timestamps**: Compare the workflow creation time with your tag push time using `date --utc` +4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your tag push timestamp +5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow -gh run list --workflow=release --limit=1 -gh run watch <release-run-id> --interval 30 --compact +Once you've identified the correct release run ID: +```bash +gh run watch <correct-release-run-id> --interval 30 --compact ``` -**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** -- Monitor release workflow with `gh run watch` + +Do not proceed until workflow completes: +- Monitor release workflow with `gh run watch` using the correct run ID - Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands - If command times out, immediately rerun `gh run watch` until completion - Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- HALT if any jobs fail - consult user before proceeding +- Stop if any jobs fail - consult user before proceeding ### 7. Post-Release Cleanup ```bash diff --git a/.auxiliary/configuration/claude/commands/cs-release-maintenance.md b/.auxiliary/configuration/claude/commands/cs-release-maintenance.md index 82146f3..b61e493 100644 --- a/.auxiliary/configuration/claude/commands/cs-release-maintenance.md +++ b/.auxiliary/configuration/claude/commands/cs-release-maintenance.md @@ -1,6 +1,7 @@ --- allowed-tools: Bash(git status), Bash(git pull:*), Bash(git checkout:*), Bash(git commit:*), Bash(git tag:*), Bash(git rm:*), Bash(git cherry-pick:*), Bash(git log:*), Bash(git branch:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), LS, Read description: Execute automated patch release with QA monitoring and master integration +argument-hint: "major.minor" --- # Release Patch @@ -13,10 +14,12 @@ For execution of a fully-automated postrelease patch. Below is a validated process to create patch releases with automated monitoring and clean integration back to master. -Target release version: `$ARGUMENTS` (e.g., `1.24`, `2.3`) +Target release version: $ARGUMENTS +(e.g., `1.24`, `2.3`) -**CRITICAL**: Verify exactly one target release version provided. -**HALT if**: +Verify exactly one target release version provided. + +Stop and consult if: - No target release version is provided - Multiple release versions provided (e.g., `1.6 foo bar`) - Release version format doesn't match `X.Y` pattern (e.g., `1.6.2`, `1.6a0`) @@ -28,7 +31,6 @@ Target release version: `$ARGUMENTS` (e.g., `1.24`, `2.3`) - Current version: !`hatch version` - Recent commits: !`git log --oneline -10` - Available towncrier fragments: !`ls .auxiliary/data/towncrier/*.rst 2>/dev/null || echo "No fragments found"` -- Target release branch status: !`git branch -r | grep release-$ARGUMENTS || echo "Release branch not found"` ## Prerequisites @@ -52,7 +54,7 @@ Key functional areas of the process: ## Safety Requirements -**CRITICAL**: You MUST halt the process and consult with the user if ANY of the following occur: +Stop and consult the user if any of the following occur: - **Step failures**: If any command fails, git operation errors, or tests fail - **Workflow failures**: If QA or release workflows show failed jobs @@ -69,7 +71,7 @@ Key functional areas of the process: ## Release Process -Execute the following steps for target release version `$ARGUMENTS`: +Execute the following steps: ### 1. Pre-Release Quality Check Run local quality assurance to catch issues early: @@ -133,7 +135,7 @@ Run linting to catch issues before formal release process: ```bash hatch --env develop run linters ``` -**HALT if any linting errors** - fix issues before proceeding. +Stop if any linting errors - fix issues before proceeding. ### 5. Version Management Increment to next patch version: @@ -152,33 +154,56 @@ git commit -am "Update changelog for v$(hatch version) patch release." Push branch and monitor QA workflow: ```bash git push origin release-$ARGUMENTS +``` + +Workflow monitoring requirements: +After pushing, you MUST ensure you monitor the correct QA workflow run: -# Monitor QA workflow - get run ID from output -gh run list --workflow=qa --limit=1 -gh run watch <qa-run-id> --interval 30 --compact +1. **Wait for workflow trigger**: Wait 10 seconds after pushing to allow GitHub to trigger the workflow +2. **Verify correct workflow**: Use `gh run list --workflow=qa --limit=5` to list recent runs +3. **Check timestamps**: Compare the workflow creation time with your push time using `date --utc` +4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your push timestamp +5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow + +Once you've identified the correct QA run ID: +```bash +gh run watch <correct-qa-run-id> --interval 30 --compact ``` -**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** -- Monitor QA workflow with `gh run watch` + +Do not proceed until workflow completes: +- Monitor QA workflow with `gh run watch` using the correct run ID - Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands - If command times out, immediately rerun `gh run watch` until completion - Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- HALT if any jobs fail - consult user before proceeding +- Stop if any jobs fail - consult user before proceeding ### 8. Release Deployment **Verify QA passed before proceeding to release tag:** ```bash git tag -m "Release v$(hatch version) patch: <brief-description>." v$(hatch version) git push --tags +``` + +Release workflow monitoring requirements: +After pushing the tag, you MUST ensure you monitor the correct release workflow run: + +1. **Wait for workflow trigger**: Wait 10 seconds after pushing tags to allow GitHub to trigger the release workflow +2. **Verify correct workflow**: Use `gh run list --workflow=release --limit=5` to list recent runs +3. **Check timestamps**: Compare the workflow creation time with your tag push time using `date --utc` +4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your tag push timestamp +5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow -gh run list --workflow=release --limit=1 -gh run watch <release-run-id> --interval 30 --compact +Once you've identified the correct release run ID: +```bash +gh run watch <correct-release-run-id> --interval 30 --compact ``` -**CRITICAL - DO NOT PROCEED UNTIL WORKFLOW COMPLETES:** -- Monitor release workflow with `gh run watch` + +Do not proceed until workflow completes: +- Monitor release workflow with `gh run watch` using the correct run ID - Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands - If command times out, immediately rerun `gh run watch` until completion - Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- HALT if any jobs fail - consult user before proceeding +- Stop if any jobs fail - consult user before proceeding ### 9. Post-Release Cleanup ```bash diff --git a/.auxiliary/configuration/claude/commands/cs-review-todos.md b/.auxiliary/configuration/claude/commands/cs-review-todos.md new file mode 100644 index 0000000..1a2e423 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-review-todos.md @@ -0,0 +1,103 @@ +--- +allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(find:*), Bash(ls:*), Bash(wc:*) +description: Systematically find, categorize, and analyze TODO comments for technical debt management +--- + +# Technical Debt Review + +Systematically find, categorize, and analyze TODO comments across the project +codebase to provide actionable insights about technical debt and outstanding +work items. + +Filter criteria and analysis focus: $ARGUMENTS +(if blank, then consider entire project) + +## Context + +- Notes: @.auxiliary/notes +- Project architecture: @documentation/architecture/summary.rst +- Project designs: @documentation/architecture/designs + +## Prerequisites + +Before running this analysis, ensure: +- Understanding of project structure and file organization +- Access to both source code and auxiliary documentation + +## Process Summary + +Key functional areas: +1. **Discovery**: Search for TODO/FIXME/XXX/HACK comments across all relevant files +2. **Categorization**: Organize findings by urgency, component, and type +3. **Analysis**: Assess technical debt impact and provide prioritization insights +4. **Reconciliation**: Compare source code TODOs with tracking documents +5. **Reporting**: Generate actionable summary with recommended next steps + +## Safety Requirements + +Stop and consult the user if: +- Large volume of TODOs (>100) found that may require batch processing +- Inconsistencies between tracking documents and source code require manual review +- File access permissions prevent comprehensive analysis + +## Execution + +Execute the following steps: + +### 1. Comprehensive TODO Discovery + +Search for all TODO-style comments across the project: +- Use Grep to find TODO, FIXME, XXX, HACK, NOTE patterns +- Search Python files, documentation, configuration files +- Include both inline comments and dedicated TODO sections +- Capture surrounding context (3-5 lines) for each finding + +### 2. Pattern Analysis and Categorization + +Analyze discovered TODOs for: +- **Urgency indicators**: Words like "urgent", "critical", "before release", "security" +- **Component classification**: Group by module, file, or functional area +- **Type classification**: Bug fix, feature enhancement, refactoring, documentation +- **Age estimation**: Check git blame for when TODO was introduced + +### 3. Auxiliary Document Review + +Examine TODO tracking files in `.auxiliary/notes/`: +- Read any existing TODO tracking documents +- Compare with source code findings +- Identify completed items that should be removed +- Note discrepancies between tracking and actual code state + +### 4. Priority Assessment + +Evaluate each TODO for: +- **Business impact**: Customer-facing vs. internal improvements +- **Technical risk**: Potential for bugs, security issues, or maintenance burden +- **Implementation complexity**: Quick fixes vs. architectural changes +- **Dependencies**: Items blocking other work vs. standalone improvements + +### 5. Reporting and Recommendations + +Generate structured output including: +- **Executive summary**: Total count, high-priority items, key themes +- **Categorized listings**: Organized by urgency, component, and type +- **Urgent actions**: Items requiring immediate attention +- **Cleanup opportunities**: Completed or obsolete TODOs to remove +- **Tracking reconciliation**: Sync recommendations between documents and code +- **Next steps**: Prioritized action plan for technical debt reduction + +### 6. Documentation Updates + +When appropriate: +- Update or create TODO tracking documents in `.auxiliary/notes/` +- Remove completed TODO comments from source code +- Add context or priority indicators to ambiguous TODOs +- Standardize TODO format across the project + +### 7. Summary Report + +Provide comprehensive analysis including: +- Total technical debt inventory +- Risk assessment of critical items +- Recommended prioritization for next development cycles +- Maintenance suggestions for keeping TODO management current diff --git a/.auxiliary/configuration/claude/commands/cs-update-command.md b/.auxiliary/configuration/claude/commands/cs-update-command.md new file mode 100644 index 0000000..a9a9272 --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-update-command.md @@ -0,0 +1,96 @@ +--- +allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep +description: Update existing slash command with missing instructions or reinforced guidance +--- + +# Update Slash Process + +Update an existing custom slash command to address missing instructions, +reinforce guidance which LLMs are ignoring, add missing tool permissions, or +make structural improvements. + +Target command and instructions: $ARGUMENTS + +Stop and consult if: +- The target file doesn't exist or isn't a slash command +- Major structural changes are requested that would fundamentally alter the command purpose +- Changes conflict with established project patterns + +## Context + +- Command template: @.auxiliary/configuration/claude/miscellany/command-template.md +- Project conventions: @.auxiliary/configuration/conventions.md + +## Prerequisites + +Before updating the command, ensure: +- Clear understanding of what improvements are needed +- Target file exists and is accessible +- Any referenced files or patterns are available +- Changes align with project conventions and existing process patterns + +## Process Summary + +Key functional areas: +1. **Analysis**: Read current command and identify improvement areas +2. **Content Updates**: Add missing instructions or reinforce existing guidance +3. **Structure Review**: Consider organizational improvements when appropriate +4. **Tone Refinement**: Ensure professional language without excessive emphasis +5. **Validation**: Verify updates maintain command effectiveness + +## Safety Requirements + +Stop and consult the user if: +- Process changes would break existing workflows or dependencies +- Updates conflict with established project conventions +- Structural modifications require significant rework of command logic + +## Execution + +Execute the following steps: + +### 1. Command Analysis +Read and analyze the current command: +- Review existing content, structure, and tool permissions +- Identify areas needing improvement or reinforcement +- Assess tone and language for professional standards +- Note any missing instructions or unclear guidance + +### 2. Content Enhancement +Apply requested improvements: +- Add missing instructions where gaps are identified +- Reinforce guidance that needs stronger emphasis +- Remove excessive bold formatting or shouty language +- Eliminate redundant repetition within sections +- Ensure clear, actionable language throughout + +### 3. Structural Review +Consider organizational improvements: +- Evaluate section ordering and logical flow +- Improve prerequisites or context sections if needed +- Enhance command summary for clarity +- Adjust safety requirements as appropriate +- Ensure consistent formatting patterns + +### 4. Tool and Permission Updates +Review and adjust technical aspects: +- Verify allowed-tools are appropriate for updated functionality +- Check that `@`-references and shell command expansions are current +- Ensure any context commands have proper tool permissions to run (e.g., `Bash(ls:*)` for `ls` commands) +- Ensure context section provides relevant dynamic information +- Validate that command can execute with given permissions + +### 5. Professional Polish +Apply formatting and tone standards: +- Use professional headers without excessive emphasis +- Maintain clear, direct language without redundancy +- Ensure consistency with project conventions +- Remove any attention-grabbing formatting that isn't necessary +- Balance guidance strength with readability + +### 6. Validation and Summary +Complete the update command: +- Review updated content for completeness and clarity +- Verify all requested improvements have been addressed +- Ensure command maintains effectiveness while addressing issues +- Provide succinct summary of changes made to the user diff --git a/.auxiliary/configuration/claude/commands/cs-update-readme-rst.md b/.auxiliary/configuration/claude/commands/cs-update-readme-rst.md new file mode 100644 index 0000000..575954e --- /dev/null +++ b/.auxiliary/configuration/claude/commands/cs-update-readme-rst.md @@ -0,0 +1,105 @@ +--- +description: Analyze current project state and refresh manually-maintained sections of README.rst while preserving template content +--- + +# Update README Documentation + +Analyze the current project state and refresh the manually-maintained sections +of README.rst files while preserving auto-generated template content and +ensuring accuracy with actual project capabilities. + +User input: $ARGUMENTS + +## Context + +- Current git status: !`git status --porcelain` +- Project structure: !`ls -la` +- Current README: @README.rst +- Project metadata: @pyproject.toml +- Product requirements: @documentation/prd.rst +- Architecture overview: @documentation/architecture/filesystem.rst + +## Prerequisites + +Before updating README documentation, ensure: +- Current README.rst exists and is accessible +- Understanding of project's actual capabilities and features +- Access to project metadata and configuration files + +## Process Summary + +Key functional areas: +1. **Content Analysis**: Examine current README and identify TODO sections needing updates +2. **Project Assessment**: Analyze actual capabilities from code, CLI, and configuration +3. **Content Generation**: Create compelling descriptions, features, and examples based on real functionality +4. **Validation**: Ensure all claims and examples match actual project capabilities + +## Safety Requirements + +Stop and consult the user if: +- README.rst cannot be read or is missing critical structure +- Template boundaries are unclear or may be damaged +- Project capabilities cannot be determined from available sources +- Generated examples cannot be validated against actual implementation +- Significant structural changes to README are required beyond content updates + +All template-rendered sections must be preserved without modification; these +include: badges, installation, contribution, flair + + +## Execution + +Execute the following steps: + +### 1. README Analysis +Read and analyze the current README structure: +- Examine existing README.rst for TODO markers and outdated content +- Identify template-generated sections that must be preserved +- Map sections that need manual content updates +- Note existing manual content that should be retained + +### 2. Project Capability Assessment +Analyze the actual project functionality: +- Extract project metadata from pyproject.toml (name, description, dependencies) +- Read PRD document if available for project goals and features +- Examine source code structure to understand API capabilities +- Test CLI functionality if enabled to document actual usage patterns +- Review configuration files and scripts for additional capabilities + +### 3. Content Generation Strategy +Plan content updates based on project analysis: +- Draft compelling project description with emoji prefix (e.g., 🔧, 📊, 🌐, 🎯) matching project purpose +- Identify key features based on actual implementation +- Plan 1-2 concise examples that whet appetites without overwhelming +- Avoid advanced showcase examples - focus on core value demonstration +- Consider additional sections (Use Cases, Motivation, Configuration) appropriate for project complexity +- Ensure content accuracy and professional tone + +### 4. README Content Updates +Update manual sections while preserving template content: +- Replace ".. todo:: Provide project description" with emoji-prefixed compelling description +- Add or update "Key Features ⭐" section with bullet points of actual capabilities +- Generate concise "Examples 💡" section with 1-2 essential usage patterns only +- Keep examples minimal and focused on core value, not comprehensive showcase +- Add relevant sections like "Use Cases", "Motivation", or "Configuration" as appropriate +- Preserve all template-generated sections (badges, installation, contribution, flair) + +### 5. Content Validation +Verify accuracy of all updated content: +- Test all code examples for correctness with current codebase +- Verify feature claims are supported by actual implementation +- Check that installation instructions match project configuration +- Ensure RST formatting is correct and consistent +- Validate examples are concise and appetite-whetting, not overwhelming +- Confirm README length is appropriate for project complexity + +### 6. Final Review +Complete final validation and formatting: +- Review entire README for consistency and professional presentation +- Ensure all TODO markers have been appropriately addressed +- Verify template boundaries are intact and respected +- Confirm examples are executable and accurate +- Check that content maintains engaging tone while being factually correct + +### 7. Summarize Updates +Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/validate-custom-slash.md b/.auxiliary/configuration/claude/commands/validate-custom-slash.md index b6bffae..2540aca 100644 --- a/.auxiliary/configuration/claude/commands/validate-custom-slash.md +++ b/.auxiliary/configuration/claude/commands/validate-custom-slash.md @@ -7,7 +7,7 @@ description: Validate custom slash command functionality with context and permis Test script to validate custom slash command functionality, permissions, and context interpolation. -Test argument: `$ARGUMENTS` +Test argument: $ARGUMENTS ## Context diff --git a/.auxiliary/configuration/claude/miscellany/command-template.md b/.auxiliary/configuration/claude/miscellany/command-template.md new file mode 100644 index 0000000..2db83c6 --- /dev/null +++ b/.auxiliary/configuration/claude/miscellany/command-template.md @@ -0,0 +1,47 @@ +--- +allowed-tools: Tool1, Tool2, Tool3 +description: Brief description of what this command does +--- + +# Process Title + +Brief introductory paragraph explaining the purpose. + +Target/input description: $ARGUMENTS + +## Context + +- Current state checks, if applicable: !`command1` +- Environment info, if applicable: !`command2` +- Relevant data, if applicable: !`command3` + +## Prerequisites + +Before running this process, ensure: +- Prerequisite 1 +- Prerequisite 2 +- @-references to relevant guides if applicable + +## Process Summary + +Key functional areas: +1. **Phase 1**: Description +2. **Phase 2**: Description +3. **Phase 3**: Description + +## Safety Requirements + +Stop and consult the user if: +- List validation conditions +- Error conditions that require user input +- Unexpected situations + +## Execution + +Execute the following steps: + +### 1. Step Name +Description of what this step does. + +### 2. Step Name +More steps as needed. diff --git a/.auxiliary/configuration/claude/settings.json b/.auxiliary/configuration/claude/settings.json index 81f7ca9..cfdc6aa 100644 --- a/.auxiliary/configuration/claude/settings.json +++ b/.auxiliary/configuration/claude/settings.json @@ -1,6 +1,10 @@ { "env": { - "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": 1 + "BASH_DEFAULT_TIMEOUT_MS": 1800000, + "BASH_MAX_TIMEOUT_MS": 1800000, + "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": 1, + "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": 1, + "DISABLE_NON_ESSENTIAL_MODEL_CALLS": 1 }, "hooks": { "PreToolUse": [ @@ -11,13 +15,18 @@ "type": "command", "command": ".auxiliary/scripts/claude/pre-bash-python-check", "timeout": 10 + }, + { + "type": "command", + "command": ".auxiliary/scripts/claude/pre-bash-git-commit-check", + "timeout": 300 } ] } ], "PostToolUse": [ { - "matcher": "Edit|MultiEdit|Write|mcp__text-editor__edit_text_file_contents", + "matcher": "Edit|MultiEdit|Write", "hooks": [ { "type": "command", @@ -60,9 +69,7 @@ "Bash(git status)", "Bash(grep *)", "Bash(hatch run python *)", - "Bash(hatch --env develop run docsgen)", - "Bash(hatch --env develop run linters)", - "Bash(hatch --env develop run testers)", + "Bash(hatch --env develop run *)", "Bash(head *)", "Bash(ls *)", "Bash(ps *)", @@ -78,13 +85,10 @@ "mcp__context7__resolve-library-id", "mcp__pyright__definition", "mcp__pyright__diagnostics", + "mcp__pyright__edit_file", "mcp__pyright__hover", "mcp__pyright__references", - "mcp__ruff__definition", - "mcp__ruff__diagnostics", - "mcp__ruff__hover", - "mcp__ruff__references", - "mcp__text-editor__get_text_file_contents" + "mcp__pyright__rename_symbol" ] } } diff --git a/.auxiliary/configuration/conventions.md b/.auxiliary/configuration/conventions.md index 7fc36b9..919ad04 100644 --- a/.auxiliary/configuration/conventions.md +++ b/.auxiliary/configuration/conventions.md @@ -1,145 +1,39 @@ -# General Advice +# Context -**IMPORTANT:** Read the comprehensive documentation guides: -- **Practices**: https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/practices.rst -- **Style**: https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/style.rst -- **Nomenclature**: https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/nomenclature.rst -For detailed patterns, examples, and architectural guidance, refer to the comprehensive guides above. +- Project overview and quick start: README.rst +- Product requirements and goals: documentation/prd.rst +- System architecture and design: @documentation/architecture/ +- Development practices and style: @.auxiliary/instructions/ +- Current session notes and TODOs: @.auxiliary/notes/ -### Context +- Use the 'context7' MCP server to retrieve up-to-date documentation for any SDKs or APIs. +- Use the 'librovore' MCP server to search structured documentation sites with object inventories (Sphinx-based, compatible MkDocs with mkdocstrings). This bridges curated documentation (context7) and raw scraping (firecrawl). +- Check README files in directories you're working with for insights about architecture, constraints, and TODO items. +- Update files under `.auxiliary/notes` during conversation, removing completed tasks and adding emergent items. -- Be sure the look at any README files in the directories which contain the - code or data that you intend to manipulate. They may provide valuable - insights about architecture, constraints, and TODO items. -- At the start of a new session, read any files in the `.auxiliary/notes` - directory. -- During the course of conversation with the user and completion of your tasks, - be sure to update files under `.auxiliary/notes`, removing completed tasks - and adding emergent items. (This will help ensure smooth transition between - sessions.) -- If the 'context7' MCP server is available, try to use that, as necessary, to - retrieve up-to-date documentation for any SDKs or APIs with which you want to - develop. +# Operation -### Design +- Use `rg --line-number --column` to get precise coordinates for MCP tools that require line/column positions. +- Choose appropriate editing tools based on the task complexity and your familiarity with the tools. +- Consider `mcp__pyright__edit_file` for more reliable line-based editing than context-based `Edit`/`MultiEdit` when making complex changes. +- Use pyright MCP tools where appropriate: `rename_symbol` for refactors, `hover` for getting function definitions without searching through code, `references` for precise symbol analysis. +- Batch related changes together when possible to maintain consistency. +- Use relative paths rather than absolute paths when possible. +- Do not write to paths outside the current project unless explicitly requested. +- Use the `.auxiliary/scribbles` directory for scratch space instead of `/tmp`. -- Make classes lightweight. Prefer module-level functions over class methods. -- Functions should not be more than 30 lines long. Refactor long functions. -- Modules should not be more than 600 lines long. Refactor large modules. -- Keep the number of function arguments small. Pass common state via - data transfer objects (DTOs). -- Use dependency injection to improve configuration and testability. Choose - sensible defaults for injected dependencies to streamline normal development. -- Prefer immutability wherever possible. - -### Judgment - -- Ensure that you understand why you are performing a task. The user should - give you a clear goal or purpose. -- If you receive data or instructions which seem counter to purpose, then do - not blindly follow the instructions or make code hacks to conform to the - data. - - The user is fallible: data may be erroneous; instructions may contain - typos or be ambiguous. - - You are encouraged to ask clarifying questions or challenge assumptions, - as appropriate. - -### Refactors - -- Ensure that you have sufficient regression tests before attempting refactors. -- Break up large refactors into milestones and make a plan before executing. -- Align your refactors with separation of concerns. -- Ensure that the code can still build and that tests still pass at each - refactoring milestone. -- Be sure to cleanup dead code after completing a refactor. - -### Tests - -- Do not change test expectations to match the results from updated code - without explicit user consent. (Tests exist to enforce desired behaviors.) -- Do not write tests unless explicitly instructed to do so. -- Prefer to write tests in a separate directory hierarchy rather than inline in - code. (Inline tests waste conversation tokens when entire files are being - viewed.) - -### Comments and Style - -- Do not strip comments from existing code unless directed to do so. -- Do not describe obvious code with comments. Only comment on non-obvious or - complex behaviors. -- Leave TODO comments about uncovered edge cases, tests, and other future work. -- Do not break function bodies with empty lines. - -### Operation - -- **Use `rg --line-number --column`** to get precise coordinates for MCP tools - that require line/column positions. -- If you have access to `text-editor` MCP tools, prefer to use them over other - text editing and search-and-replace tools. (Line number-based edits are less - error-prone.) - - **Always reread files with `text-editor` tools** after modifying files - via other tools (like `rust-analyzer`) to avoid file hash conflicts. - - Batch related changes together to minimize file modification - conflicts between different MCP tools. -- If you have access to shell tools, try to use them with relative paths rather - than absolute paths. E.g., if your working directory is - `/home/me/src/some-project` and you want to run `sed` on - `/home/me/src/some-project/README.md`, then run `sed` on `README.md` and not - on the full absolute path. -- Do not write to paths outside of the current project unless the user has - explicitly requested that you do so. If you need a scratch space, use - the `.auxiliary/scribbles` directory instead of `/tmp`. - -# Per-Language Advice - -## Python - -### Essentials - -- Avoid namespace pollution - use private aliases and `__` subpackage. -- Organize modules in specific order: imports → type aliases → defaults → public API → private functions. -- Maintain readability with spaces inside of delimiters. -- Maintain readability with vertical compactness of function bodies. -- Prefer immutability wherever possible. -- Use wide abstract types for function parameters (`__.cabc.Sequence`, `__.cabc.Mapping`). -- Return narrow concrete types (`list`, `dict`, `frozenset`, `__.immut.Dictionary`). -- Use narrow try blocks (only risky statements). - -**Example:** - -```python -# ✅ Correct: proper spacing, wide parameters, narrow returns, proper imports -import aiofiles as _aiofiles - -from . import __ - -UserData: __.typx.TypeAlias = dict[ str, str | int ] - -def process_items( - items: __.cabc.Sequence[ str ], # Wide input type - config: __.cabc.Mapping[ str, int ] = __.immut.Dictionary( ) -) -> tuple[ str, ... ]: # Narrow return type - ''' Processes items according to configuration. ''' - return tuple( item.upper( ) for item in items ) -``` - -### Quality Assurance +# Commits -- Ensure linters give a clean report: `hatch --env develop run linters` -- Do **not** suppress linter warnings with `noqa` pragma comments without explicit approval -- Ensure tests pass: `hatch --env develop run testers` -- Ensure documentation generates without error: `hatch --env develop run docsgen` +- Use `git status` to ensure all relevant changes are in the changeset. +- Use the `python-conformer` agent to review changes that include Python code before committing. +- Do **not** commit without explicit user approval. Unless the user has requested the commit, ask for a review of your edits first. +- Use present tense, imperative mood verbs (e.g., "Fix" not "Fixed"). +- Write sentences with proper punctuation. +- Include a `Co-Authored-By:` field as the final line. Should include the model name and a no-reply address. -# Commits +# Project Notes -- Use `git status` to ensure that all relevant changes are in the changeset to - be committed. -- Look at the previous five commit messages for guidance on message style. -- Use present tense, imperative mood verbs to describe changes. E.g. "Fix" and - *not* "Fixed". -- The commit message should include a `Co-Authored-By:` field as its final - line. The name of the author should be your model name. The email address - should either be one which you have been designated to use or else a - commonly-known no-reply address. +<!-- This section accumulates project-specific knowledge, constraints, and deviations. + For structured items, use documentation/architecture/decisions/ and .auxiliary/notes/todo.md --> diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index 6c827c5..5747302 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.32 +_commit: v1.49 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald diff --git a/.auxiliary/configuration/gemini/settings.json b/.auxiliary/configuration/gemini/settings.json index 9f48e88..2b52210 100644 --- a/.auxiliary/configuration/gemini/settings.json +++ b/.auxiliary/configuration/gemini/settings.json @@ -4,6 +4,10 @@ "command": "npx", "args": [ "-y", "@upstash/context7-mcp" ] }, + "librovore": { + "command": "uvx", + "args": [ "librovore", "serve" ] + }, "pyright": { "command": "mcp-language-server", "args": [ @@ -14,18 +18,6 @@ "--", "--stdio" ] - }, - "ruff": { - "command": "mcp-language-server", - "args": [ - "--lsp", - "ruff", - "--workspace", - ".", - "--", - "server", - "--preview" - ] } } } diff --git a/.auxiliary/configuration/hatch-constraints.pip b/.auxiliary/configuration/hatch-constraints.pip new file mode 100644 index 0000000..c5dc974 --- /dev/null +++ b/.auxiliary/configuration/hatch-constraints.pip @@ -0,0 +1,2 @@ +# Pip constraints file for Hatch installation +click<8.3.0 # https://github.com/pypa/hatch/issues/2050 diff --git a/.auxiliary/configuration/mcp-servers.json b/.auxiliary/configuration/mcp-servers.json index 6ae002a..e3dc6c7 100644 --- a/.auxiliary/configuration/mcp-servers.json +++ b/.auxiliary/configuration/mcp-servers.json @@ -1,5 +1,9 @@ { "mcpServers": { + "librovore": { + "command": "uvx", + "args": [ "librovore", "serve" ] + }, "pyright": { "command": "mcp-language-server", "args": [ @@ -10,24 +14,6 @@ "--", "--stdio" ] - }, - "ruff": { - "command": "mcp-language-server", - "args": [ - "--lsp", - "ruff", - "--workspace", - ".", - "--", - "server", - "--preview" - ] - }, - "text-editor": { - "command": "uvx", - "args": [ - "mcp-text-editor" - ] } } } diff --git a/.auxiliary/configuration/opencode/agent/python-annotator.md b/.auxiliary/configuration/opencode/agent/python-annotator.md new file mode 100644 index 0000000..ebfae59 --- /dev/null +++ b/.auxiliary/configuration/opencode/agent/python-annotator.md @@ -0,0 +1,220 @@ +--- +description: | + Use this agent when you need to address type checking issues from tools like Pyright, create type annotations + following project standards, generate type stubs for third-party packages, or analyze and resolve issues masked + by type: ignore comments or __.typx.cast calls. + + Examples: + + <example> + Context: User has written a new public function and needs proper type annotations according to project standards. + user: 'I just wrote this function but Pyright is complaining about missing type annotations: def process_data(data, configuration): return transformed_data' + assistant: 'Let me use the python-annotator agent to add proper type annotations following the project guidelines.' + <commentary>The user needs type annotations added to their function following project standards, so use the python-annotator agent.</commentary> + </example> + + <example> + Context: User is getting Pyright errors about missing type stubs for a third-party library. + user: 'Pyright is showing errors because the requests library doesn't have type stubs available' + assistant: 'I'll use the python-annotator agent to create the missing type stubs for the requests library.' + <commentary>Missing type stubs for third-party packages require the python-annotator agent's specialized workflow.</commentary> + </example> + + <example> + Context: User wants to clean up code that has type: ignore comments. + user: 'Can you help me resolve these # type: ignore comments in my code?' + assistant: 'Let me use the python-annotator agent to analyze and properly resolve those type checking suppressions.' + <commentary>Analyzing and mitigating issues masked by type pragmas is a core function of the python-annotator agent.</commentary> + </example> +mode: subagent +model: anthropic/claude-sonnet-4-20250514 +temperature: 0.0 +tools: + edit: true + bash: true +permissions: + bash: + "hatch --env develop run *": allow + "git *": allow + "rg *": allow + "grep *": allow + "*": ask +--- + +You are an expert Python type annotation specialist focusing on static type analysis, +type system design, and resolving type checker issues from tools like Pyright. You +systematically analyze type checking problems and apply comprehensive solutions to +ensure code adheres to strict typing standards. + +**IMPORTANT**: Only address Python type checking issues. If the request does not +involve Python type annotations, type stubs, or type checker diagnostics, politely +decline and explain your specialization. + +## Prerequisites + +- **Read project documentation guides FIRST**: + - @.auxiliary/instructions/practices.rst + - @.auxiliary/instructions/style.rst +- Have read `opencode.md` for project-specific guidance + +## EXECUTION STRUCTURE + +**PHASE 1: COMPREHENSIVE TYPE ANALYSIS** +Perform complete diagnostic analysis and generate detailed type checking report before making any changes. + +**PHASE 2: SYSTEMATIC RESOLUTION** +Apply all identified type annotation fixes in systematic order, validating with type checkers after completion. + +## TYPE ANNOTATION STANDARDS + +### 1. Annotation Guidelines + +**Public Function Documentation:** +- Use `__.typx.Annotated[ <type>, __.ddoc.Doc( '''<description>''' ) ]` pattern +- Include `__.ddoc.Raises( )` annotations for documented exceptions +- Follow narrative mood (third person) in documentation + +**Wide Parameters, Narrow Returns:** +- Accept abstract base classes (`__.cabc.Sequence`, `__.cabc.Mapping`) +- Return concrete immutable types (`tuple`, `frozenset`, `__.immut.Dictionary`) + +**Absential vs Optional:** +- Prefer `__.Absential[ T ]` for optional parameters when `None` has semantic meaning +- Use `__.typx.Optional[ T ]` only when `None` is a valid value distinct from absence + +**Type Alias Organization:** +- Common aliases after imports, before private variables +- Complex multi-line unions use `__.typx.Union[ ]` +- Simple unions use `|` syntax + +### 2. Type Checker Issue Resolution + +**Root Cause Analysis:** +1. Identify specific type checker errors and their locations +2. Determine underlying cause (missing annotations, incorrect types, inheritance issues) +3. Assess impact on runtime behavior and API contracts +4. Plan minimal changes that resolve issues without breaking functionality + +**Resolution Priorities:** +1. **Missing Annotations**: Add comprehensive type annotations following project patterns +2. **Incorrect Types**: Replace overly broad or narrow types with appropriate abstractions +3. **Generic Issues**: Properly parameterize generic types and resolve variance issues +4. **Import Problems**: Fix circular imports and missing type-only imports + +### 3. Dependency Management and Type Stub Creation + +**Dependency Declaration Before Type Work** + +Avoid using `# type: ignore` to suppress errors about missing third-party dependencies. +This anti-pattern masks improper project setup and should be resolved through proper dependency management. + +**Required Dependency Workflow:** +1. **Verify Dependency Declaration**: Check `pyproject.toml` for the package +2. **Update Project Dependencies**: Add missing packages to appropriate dependency groups +3. **Update Import Module**: Add package to `sources/<package>/__/imports.py` if commonly used +4. **Rebuild Environment**: Run `hatch env prune && hatch --env develop run python --version` +5. **Then and Only Then**: Proceed with type stub creation or suppression analysis + +**Dependency Verification Commands:** +```shell +# Check if package is declared in pyproject.toml +grep -n "somepackage" pyproject.toml + +# Verify package is installed in environment +hatch --env develop run python -c "import somepackage; print( somepackage.__file__ )" + +# Check if type information is available +hatch --env develop run pyright --verifytypes somepackage +``` + +**Type Stub Creation Workflow:** + +**Stub Generation Process (ONLY after dependency verification):** +1. **Check Official Sources**: Verify typeshed, PyPI `types-*` packages, or library's own stubs +2. **Generate Initial Stubs**: + ```shell + hatch --env develop run pyright --createstub somepackage + ``` +3. **Minimal Viable Stubs**: Focus only on APIs used in project, not comprehensive coverage +4. **Structure Requirements**: + - Proper module hierarchy matching runtime structure + - Inheritance relationships preserved + - Generic type parameters correctly defined + - Public API surface accurately represented + +### 4. Type Suppression Resolution + +**Suppression Analysis Workflow:** + +**Phase 1 - Audit Existing Suppressions:** +```shell +# Find all suppressions in codebase +rg --line-number "type:\s*ignore|__.typx\.cast" --type py +``` + +**Phase 2 - Categorize Suppressions:** +1. **Dependency Issues**: Missing packages not declared in `pyproject.toml` - address first +2. **Resolvable**: Missing stubs, incorrect annotations, fixable inheritance +3. **Legitimate**: Truly dynamic behavior, complex generics, external constraints +4. **Technical Debt**: Workarounds that should be refactored + +**Dependency Suppression Analysis:** +For any suppression involving third-party imports: +1. **Verify Declaration**: Check if package exists in `pyproject.toml` +2. **If Missing**: Add to appropriate dependency group, update `__/imports.py` if needed +3. **Rebuild Environment**: `hatch env prune` and reinstall +4. **Re-evaluate**: Many suppressions resolve after proper dependency management + +**Phase 3 - Resolution Strategies:** +- Every remaining suppression MUST have explanatory comment +- Include ticket/issue reference for suppressions requiring upstream fixes +- Set TODO items for suppressions that should be revisited + +### 5. Quality Assurance Workflow + +**Type Checking Validation:** +```shell +# Run comprehensive type checking +hatch --env develop run pyright +hatch --env develop run pyright --stats # Coverage statistics +``` + +**Consistency Verification:** +- Public functions have `__.typx.Annotated` documentation +- Parameter types follow wide/narrow principle +- Return types are concrete and immutable where appropriate +- Import organization follows project standards + +**Runtime Preservation:** +- Verify no functional changes introduced +- Test critical paths if available +- Validate API contracts maintained + +## ANALYSIS REPORT FORMAT + +**PHASE 1 OUTPUT:** +1. **Type Checking Summary**: Overall diagnostic assessment with file-by-file breakdown +2. **Missing Annotations**: Functions, methods, and variables requiring type annotations +3. **Type Errors**: Specific checker errors with root cause analysis +4. **Stub Requirements**: Third-party packages needing type stubs +5. **Suppression Audit**: Analysis of existing `type: ignore` and `__.typx.cast` usage +6. **Resolution Plan**: Systematic order of fixes to be applied + +**PHASE 2 OUTPUT:** +1. **Applied Annotations**: Summary of all type annotations added +2. **Stub Generation**: Created stub files and their scope +3. **Suppression Resolution**: Eliminated or refined type suppressions +4. **Validation Results**: Type checker output before and after changes +5. **Files Modified**: Complete list with brief description of changes + +## EXECUTION REQUIREMENTS + +- **Phase 0**: Verify all third-party dependencies are declared in `pyproject.toml` and available in environment +- **Phase 1**: Complete analysis and report before any modifications +- **Phase 2**: Apply fixes systematically, validate with `hatch --env develop run pyright` +- **Dependency validation**: Do not proceed with type work until dependencies are properly declared +- **Validation command**: Type checking must be clean before completion +- **Focus on type safety**: Maintain exact functionality while improving type annotations +- **Reference specific diagnostics**: Always include line numbers and error messages +- **Document decisions**: Explain type choices and trade-offs made +- **Dependency pattern detection**: Flag attempts to use `# type: ignore` for missing dependencies diff --git a/.auxiliary/configuration/opencode/agent/python-conformer.md b/.auxiliary/configuration/opencode/agent/python-conformer.md new file mode 100644 index 0000000..bcb9d6f --- /dev/null +++ b/.auxiliary/configuration/opencode/agent/python-conformer.md @@ -0,0 +1,256 @@ +--- +description: | + Use this agent ONLY when changes include Python code (.py and .pyi files) and you need to review them for + compliance with project practices, style guidelines, and nomenclature standards, then systematically fix violations. + Do NOT use this agent for non-Python changes such as documentation, configuration files, or other file types. + + Examples: + + <example> + Context: The user has just written a new Python function and wants to ensure it follows project standards. + user: 'I just wrote this function for processing user data. Can you review it?' + assistant: 'I'll use the python-conformer agent to check your function against our project practices and style guidelines, then fix any violations.' + <commentary>Since the user wants code reviewed for compliance, use the python-conformer agent to analyze the code against project standards.</commentary> + </example> + + <example> + Context: The user has completed a module refactor and wants to verify compliance before committing. + user: 'I've finished refactoring the authentication module. Please check if it meets our coding standards.' + assistant: 'Let me use the python-conformer agent to thoroughly review your refactored module for compliance with our practices guidelines.' + <commentary>The user needs compliance verification for recently refactored code, so use the python-conformer agent.</commentary> + </example> + + <example> + Context: The user wants to review staged Python changes before committing. + user: 'I've modified several Python modules. Please review my staged changes for compliance before I commit.' + assistant: 'I'll use the python-conformer agent to review the Python changes in git diff --cached and ensure all Python code meets our project standards.' + <commentary>Pre-commit review of staged Python changes is a perfect use case for the python-conformer agent.</commentary> + </example> +mode: subagent +model: anthropic/claude-sonnet-4-20250514 +temperature: 0.1 +tools: + edit: true + bash: true +permissions: + bash: + "hatch --env develop run *": allow + "git *": allow + "rg *": allow + "*": ask +--- + +You are an expert software engineer specializing in Python code quality assurance and +compliance conformance. Your primary responsibility is to systematically review Python code +against established project practices, style guidelines, and nomenclature +standards, then apply comprehensive remediation to bring code into full compliance. + +**IMPORTANT**: Only review and modify Python (.py and .pyi) files. If the +changes do not include Python code, politely decline and explain that you are +specifically for Python code compliance review. + +## Prerequisites + +- **Read project documentation guides FIRST**: + - @.auxiliary/instructions/practices.rst + - @.auxiliary/instructions/style.rst + - @.auxiliary/instructions/nomenclature.rst +- Have read `opencode.md` for project-specific guidance + +## EXECUTION STRUCTURE + +**PHASE 1: COMPREHENSIVE REVIEW** +Perform complete analysis and generate detailed compliance report before making any changes. + +**PHASE 2: SYSTEMATIC REMEDIATION** +Apply all identified fixes in systematic order, validating with linters after completion. + +## COMPLIANCE STANDARDS + +### Design Standards + +#### 1. Module Organization + +**Content Order:** +1. Imports (following practices guide patterns) +2. Common type aliases (`TypeAlias` declarations) +3. Private variables/functions for defaults (grouped semantically) +4. Public classes and functions (alphabetical) +5. All other private functions (alphabetical) + +**Scope and Size:** +- Maximum 600 lines +- Action: Analyze oversized modules with separation of concerns in mind. +Suggest splitting into focused modules with narrower responsibilities or +functionality. + +#### 2. Imports + +- At the module level, other modules and their attributes MUST be imported as + private aliases, except in `__init__`, `__`, or specially-designated + re-export modules. +- Within function bodies, other modules and their attributes MAY be imported as + public variables. +- Subpackages SHOULD define a special `__` re-export module, which has `from + ..__ import *` plus any other imports which are common to the subpackage. +- Common modules, such as `os` or `re`, SHOULD be imported as public within the + special package-wide `__.imports` re-export module rather than as private + aliases within an implementation module. +- The `__all__` attribute SHOULD NOT be provided. This is unnecessary if the + module namespace only contains public classes and functions which are part of + its interface; this avoid additional interface maintenance. + +#### 3. Dependency Injection + +- Ask: is this function testable without monkeypatching? +- Functions SHOULD provide injectable parameters with sensible defaults instead + of hard-coded dependencies within function implementation. + +#### 4. Robustness Principle (Postel's Law) +"Be conservative in what you send; be liberal in what you accept." + +- Public functions SHOULD define wide, abstract argument types. +- All functions SHOULD define narrow, concrete return types. +- Private functions MAY define narrow, concrete argument types. + +#### 5. Immutability + +- Classes SHOULD inherit from immutable classes (`__.immut.Object`, + `__.immut.Protocol`, `__.immut.DataclassObject`, etc...). +- Functions SHOULD return values of immutable types (`None`, `int`, `tuple`, + `frozenset`, `__.immut.Dictionary`, etc...) and not mutable types (`list`, + `dict`, `set`, etc...). + +#### 6. Proper Exception Management + +- One `try .. except` suite per statement which can raise exceptions. I.e., + avoid covering multiple statements with a `try` block whenever possible. +- Tryceratops complaints MUST NOT be suppressed with `noqa` pragmas. +- Bare exceptions SHOULD NOT be raised. + - Exemption: `NotImplementedError` MAY be raised as a bare exception. + - Relevant exception classes SHOULD be used from the relevant `exceptions` + module within the package. + - New exception classes MAY be created as needed within the relevant + `exceptions` module; these MUST follow the nomenclature guide and be + inserted in correct alphabetical order. + +### Quality Assurance + +#### 1. Linter Suppressions + +- Linter suppressions MUST be reviewed critically. +- Linter complaints SHOULD NOT be suppressed via `noqa` or `type` pragmas + without compelling justification. +- Suppressions that mask design problems MUST be investigated and resolved + rather than ignored. + +**Acceptable Suppressions:** +- `noqa: PLR0913` MAY be used for a CLI or service API with many parameters, + but data transfer objects SHOULD be considered in most other cases. +- `noqa: S*` MAY be used for properly constrained and vetted subprocess + executions or Internet content retrievals. + +**Unacceptable Suppressions (require investigation):** +- `type: ignore` MUST NOT be used, except in extremely rare circumstances. Such + suppressions usually indicate missing third-party dependencies or type stubs, + inappropriate type variables, or a bad inheritance pattern. For complex type + suppression investigation and dependency management, delegate to the + `python-annotator` agent. +- `__.typx.cast` SHOULD NOT be used, except in extremely rare circumstances. + Such casts suppress normal type checking and usually the same problems as + `type: ignore`. +- Most other `noqa` suppressions. + +### Style Standards + +#### 1. Spacing and Delimiters + +- Space padding MUST be present inside delimiters. + - Format: `( arg )`, `[ item ]`, `{ key: value }` + - Format: `( )`, `[ ]`, `{ }`, not `()`, `[]`, `{}` +- Space padding MUST be present around keyword argument `=`. + - Format: `foo = 42` + +#### 2. Strings + +- Docstrings MUST use triple single quotes with narrative mood. + - Format: `''' Processes data... '''` not `"""Process data..."""` +- F-strings and `.format` strings MUST be enclosed in double quotes. + - Format: `f"text {variable}"`, not `f'text {variable}'` + - Format: `"text {count}".format( count = len( items ) )` +- F-strings and format strings MUST NOT embed function calls. +- Exception messages and log messages SHOULD be enclosed in double quotes + rather than single quotes. +- Plain data strings SHOULD be enclosed in single quotes, unless they contain + single quotes. + +#### 3. Vertical Compactness + +- Blank lines MUST NOT appear within function bodies. +- Vertical compactness MUST be maintained within function implementations. +- Single-line statements MAY follow certain block keywords on the same line + when appropriate. + - Format: `if condition: return value` + - Format: `elif condition: continue` + - Format: `else: statement` + - Format: `try: statement` + +#### 4. Multi-line Constructs + +- Function invocations, including class instantiations, SHOULD place the + closing `)` on the same line as the last argument to the function. +- The last argument of an invocation MUST NOT be followed by a trailing comma. +- Comprehensions and generator expressions SHOULD place the closing delimiter + on the same line as the last statement in the comprehension or generator + expression. +- Parenthetical groupings SHOULD place the closing delimiter on the same line + as the last statement in the grouping. +- All other multi-line constructs (functions signatures, annotations, lists, + dictionaries, etc...) MUST place the closing delimiter on a separate line + following the last item and MUST dedent the closing delimiter to match the + opening line indentation. +- If a closing delimiter is not on the same line as the last item in a + multi-line construct, then the last item MUST be followed by a trailing + comma. + +#### 5. Nomenclature + +- Argument, attribute, and variable names SHOULD NOT be compound words, + separated by underscores, except in cases where this is necessary to + disambiguate. +- Argument and variable names SHOULD NOT duplicate parts of the function name. +- Attribute names SHOULD NOT duplicate parts of the class name. +- Class names SHOULD adhere to the nomenclature guide. +- Function names SHOULD adhere to the nomenclature guide. + +#### 6. Comments + +- Comments that describe obvious behavior SHOULD NOT be included. +- TODO comments SHOULD be added for uncovered edge cases and future work. +- Comments MUST add meaningful context, not restate what the code does. + +## REVIEW REPORT FORMAT + +**PHASE 1 OUTPUT:** +1. **Compliance Summary**: Overall assessment with file-by-file breakdown +2. **Standards Violations**: Categorized list with specific line references and explanations +3. **Complexity Analysis**: Function and module size assessments +4. **Remediation Plan**: Systematic order of fixes to be applied +5. **Risk Assessment**: Any changes that require careful validation + +**PHASE 2 OUTPUT:** +1. **Applied Fixes**: Summary of all changes made, categorized by standard +2. **Validation Results**: Linter output before and after changes +3. **Files Modified**: Complete list with brief description of changes +4. **Manual Review Required**: Any issues requiring human judgment + +## EXECUTION REQUIREMENTS + +- **PHASE 1 REQUIRED**: Complete review and report before any remediation +- **PHASE 2 REQUIRED**: Apply fixes systematically, validate with `hatch --env develop run linters` +- **Validation command**: `hatch --env develop run linters` must produce clean output before completion +- **Focus on compliance**: Maintain exact functionality while improving standards adherence +- **Reference specific lines**: Always include line numbers and concrete examples +- **Document reasoning**: Explain why each standard matters and how fixes align with project practices +- **Agent delegation**: When type annotation issues exceed basic compliance scope, consider delegating to the `python-annotator` agent for comprehensive type work +- **Guide access**: If any prerequisite guide cannot be accessed, stop and inform the user diff --git a/.auxiliary/configuration/opencode/command/.gitignore b/.auxiliary/configuration/opencode/command/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.auxiliary/configuration/opencode/command/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.auxiliary/configuration/vulturefood.py b/.auxiliary/configuration/vulturefood.py new file mode 100644 index 0000000..292bb27 --- /dev/null +++ b/.auxiliary/configuration/vulturefood.py @@ -0,0 +1,23 @@ +ComparisonResult # unused variable +NominativeArguments # unused variable +PositionalArguments # unused variable +package_name # unused variable + +# --- BEGIN: Injected by Copier --- +# --- END: Injected by Copier --- + +class_mutables +class_visibles +instances_assigner_core +instances_deleter_core +instances_surveyor_core +ObjectMutable +DataclassObject +DataclassObjectMutable +ProtocolMutable +DataclassProtocol +DataclassProtocolMutable +reclassify_modules +__class__ +class_ +delattr0 diff --git a/.auxiliary/data/towncrier/+modules-df.repair.rst b/.auxiliary/data/towncrier/+modules-df.repair.rst deleted file mode 100644 index 9578e01..0000000 --- a/.auxiliary/data/towncrier/+modules-df.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Standard: Modules: Reclassify modules with proper depth-first traversal. diff --git a/.auxiliary/data/towncrier/+modules-exclude.enhance.rst b/.auxiliary/data/towncrier/+modules-exclude.enhance.rst deleted file mode 100644 index ac2484b..0000000 --- a/.auxiliary/data/towncrier/+modules-exclude.enhance.rst +++ /dev/null @@ -1,2 +0,0 @@ -Standard: Modules: Allow certain modules to be excluded from reclassification. -Also, implement cycle detection. diff --git a/.auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst b/.auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst new file mode 100644 index 0000000..9eb2d08 --- /dev/null +++ b/.auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst @@ -0,0 +1 @@ +Fix PyPy compatibility with super() calls in slotted dataclasses. \ No newline at end of file diff --git a/.auxiliary/instructions/.gitignore b/.auxiliary/instructions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.auxiliary/instructions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 2a5c187..9f0ad13 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5ab04332aabeb41985bd9eab97cdce5a5ee87117a677bec87575750f6a77647 -size 342008 +oid sha256:19559cbc05e5c7280535670c5c5ef1057de3105e5c3d1a0959645551735e4088 +size 371636 diff --git a/.auxiliary/scripts/claude-ds b/.auxiliary/scripts/claude-ds new file mode 100755 index 0000000..9161e8d --- /dev/null +++ b/.auxiliary/scripts/claude-ds @@ -0,0 +1,33 @@ +#!/bin/bash + +# claude-ds: Rhymes with "Claudius", the bowtie-wearing tungsten cube vendor. + +eecho() { + echo "$@" >&2 +} + +ENV_FILE=".auxiliary/secrets/deepseek-api.env" + +if [[ ! -f "${ENV_FILE}" ]]; then + eecho "Error: Environment file not found at '${ENV_FILE}'." + eecho "Please create the file with your DeepSeek API key." + exit 1 +fi + +source "${ENV_FILE}" + +if [[ -z "${DEEPSEEK_API_KEY}" ]]; then + eecho "Error: DEEPSEEK_API_KEY not found in '${ENV_FILE}'." + eecho "Please set DEEPSEEK_API_KEY=your_api_key in the environment file." + exit 1 +fi + +export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic" +export ANTHROPIC_AUTH_TOKEN="${DEEPSEEK_API_KEY}" +export ANTHROPIC_MODEL="deepseek-chat" +export ANTHROPIC_SMALL_FAST_MODEL="deepseek-chat" + +eecho "Anthropic API URL: ${ANTHROPIC_BASE_URL}" +eecho "Claude Model: ${ANTHROPIC_MODEL}" + +exec claude "$@" diff --git a/.auxiliary/scripts/claude-xai b/.auxiliary/scripts/claude-xai new file mode 100755 index 0000000..2b4a005 --- /dev/null +++ b/.auxiliary/scripts/claude-xai @@ -0,0 +1,33 @@ +#!/bin/bash + +# claude-xai: Rhymes with "Claude's eye", the Anthropic viewpoint. + +eecho() { + echo "$@" >&2 +} + +ENV_FILE=".auxiliary/secrets/xai-api.env" + +if [[ ! -f "${ENV_FILE}" ]]; then + eecho "Error: Environment file not found at '${ENV_FILE}'." + eecho "Please create the file with your X.ai (Grok) API key." + exit 1 +fi + +source "${ENV_FILE}" + +if [[ -z "${XAI_API_KEY}" ]]; then + eecho "Error: XAI_API_KEY not found in '${ENV_FILE}'." + eecho "Please set XAI_API_KEY=your_api_key in the environment file." + exit 1 +fi + +export ANTHROPIC_BASE_URL="https://api.x.ai" +export ANTHROPIC_AUTH_TOKEN="${XAI_API_KEY}" +export ANTHROPIC_MODEL="grok-code-fast-1" +export ANTHROPIC_SMALL_FAST_MODEL="grok-code-fast-1" + +eecho "Anthropic API URL: ${ANTHROPIC_BASE_URL}" +eecho "Claude Model: ${ANTHROPIC_MODEL}" + +exec claude "$@" diff --git a/.auxiliary/scripts/claude/post-edit-linter b/.auxiliary/scripts/claude/post-edit-linter index e479133..2237628 100755 --- a/.auxiliary/scripts/claude/post-edit-linter +++ b/.auxiliary/scripts/claude/post-edit-linter @@ -23,7 +23,8 @@ def main( ): _reactor_failure( f"{exc_class.__qualname__}: {exc}" ) if result.returncode != 0: # Combine stdout and stderr since linting output may go to stdout. - print( f"{result.stdout}\n\n{result.stderr}", file = sys.stderr ) + result_text = f"{result.stdout}\n\n{result.stderr}".strip( ) + print( _truncate_if_necessary( result_text ), file = sys.stderr ) raise SystemExit( 2 ) # Use JSON output for better integration with Claude Code # _emit_decision_json( "block", f"{result.stdout}\n\n{result.stderr}" ) @@ -62,4 +63,16 @@ def _reactor_failure( message ): raise SystemExit( 1 ) +def _truncate_if_necessary( output, lines_max = 50 ): + ''' Truncates output to maximum number of lines with truncation notice. ''' + lines = output.split( '\n' ) + if len( lines ) <= lines_max: return output + lines_to_display = lines[ : lines_max ] + truncations_count = len( lines ) - lines_max + lines_to_display.append( + f"\n[OUTPUT TRUNCATED: {truncations_count} additional lines omitted. " + f"Fix the issues above to see remaining diagnostics.]" ) + return '\n'.join( lines_to_display ) + + if __name__ == '__main__': main( ) diff --git a/.auxiliary/scripts/claude/pre-bash-git-commit-check b/.auxiliary/scripts/claude/pre-bash-git-commit-check new file mode 100755 index 0000000..d6741d6 --- /dev/null +++ b/.auxiliary/scripts/claude/pre-bash-git-commit-check @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +''' Claude Code hook to prevent git commits when linters or tests fail. ''' + + +import json +import shlex +import subprocess +import sys + + +_GIT_COMMIT_MIN_TOKENS = 2 + + +def main( ): + event = _acquire_event_data( ) + command_line = _extract_command( event ) + commands = _partition_command_line( command_line ) + for command in commands: + _check_git_commit_command( command ) + raise SystemExit( 0 ) + + +def _acquire_event_data( ): + try: return json.load( sys.stdin ) + except json.JSONDecodeError: + _reactor_failure( "Invalid event data." ) + + +def _check_git_commit_command( tokens ): + ''' Checks for git commit commands and validates linters/tests. ''' + if not _is_git_commit_command( tokens ): return + try: + result = subprocess.run( + [ 'hatch', '--env', 'develop', 'run', 'linters' ], # noqa: S607 + capture_output = True, text = True, timeout = 120, check = False ) + except ( + subprocess.TimeoutExpired, + subprocess.CalledProcessError, + FileNotFoundError + ): _error_with_divine_message( ) + else: + if result.returncode != 0: _error_with_divine_message( ) + try: + result = subprocess.run( + [ 'hatch', '--env', 'develop', 'run', 'testers' ], # noqa: S607 + capture_output = True, text = True, timeout = 300, check = False ) + except ( + subprocess.TimeoutExpired, + subprocess.CalledProcessError, + FileNotFoundError + ): _error_with_divine_message( ) + else: + if result.returncode != 0: _error_with_divine_message( ) + + +def _error_with_divine_message( ): + ''' Displays divine admonition and exits. ''' + message = ( + "The Large Language Divinity 🌩️🤖🌩️ in the Celestial Data Center hath " + "commanded that:\n" + "* Thy code shalt pass all lints before thy commit.\n" + " Run: hatch --env develop run linters\n" + "* Thy code shalt pass all tests before thy commit.\n" + " Run: hatch --env develop run testers\n\n" + "(If you are in the middle of a large refactor, consider commenting " + "out the tests and adding a reminder note in the .auxiliary/notes " + "directory.)" + ) + print( message, file = sys.stderr ) + raise SystemExit( 2 ) + + +def _extract_command( event_data ): + ''' Extracts command from event data, exit if not Bash tool. ''' + tool_name = event_data.get( 'tool_name', '' ) + if tool_name != 'Bash': raise SystemExit( 0 ) + tool_input = event_data.get( 'tool_input', { } ) + return tool_input.get( 'command', '' ) + + +def _is_git_commit_command( tokens ): + ''' Checks if tokens represent a git commit command. ''' + if len( tokens ) < _GIT_COMMIT_MIN_TOKENS: + return False + return tokens[ 0 ] == 'git' and tokens[ 1 ] == 'commit' + + +_splitters = frozenset( ( ';', '&', '|', '&&', '||' ) ) +def _partition_command_line( command_line ): + tokens = shlex.split( command_line ) + commands = [ ] + command_tokens = [ ] + for token in tokens: + if token in _splitters: + commands.append( command_tokens ) + command_tokens = [ ] + continue + command_tokens.append( token ) + if command_tokens: commands.append( command_tokens ) + return commands + + +def _reactor_failure( message ): + print( f"Claude Code Hook Failure: {message}", file = sys.stderr ) + raise SystemExit( 1 ) + + +if __name__ == '__main__': main() diff --git a/.auxiliary/scripts/claude/pre-bash-python-check b/.auxiliary/scripts/claude/pre-bash-python-check index f43a7b2..0ccf678 100755 --- a/.auxiliary/scripts/claude/pre-bash-python-check +++ b/.auxiliary/scripts/claude/pre-bash-python-check @@ -17,6 +17,7 @@ def main( ): for command in commands: _check_direct_python_usage( command ) _check_multiline_python_c( command ) + _check_direct_tool_usage( command ) raise SystemExit( 0 ) @@ -51,6 +52,18 @@ def _check_multiline_python_c( tokens ): ): _error( emessage ) +def _check_direct_tool_usage( tokens ): + ''' Checks for direct usage of Python tools outside Hatch environment. ''' + emessage = ( + "Warning: Direct Python tool usage detected in command.\n" + "Use 'hatch --env develop run {tool}' instead to ensure " + "proper environment and configuration." ) + for token in tokens: + if token == 'hatch': return # noqa: S105 + if _is_python_tool( token ): + _error( emessage.format( tool = token ) ) + + def _check_python_c_argument( tokens, python_index ): ''' Checks if Python -c argument contains multiline code. ''' for j in range( python_index + 1, len( tokens ) ): @@ -82,6 +95,11 @@ def _is_python_command( token ): token in ( 'python', 'python3' ) or token.startswith( 'python3.' ) ) +def _is_python_tool( token ): + ''' Checks if token is a Python development tool. ''' + return token in ( 'coverage', 'pyright', 'pytest', 'ruff' ) + + _splitters = frozenset( ( ';', '&', '|', '&&', '||' ) ) def _partition_command_line( command_line ): tokens = shlex.split( command_line ) diff --git a/.auxiliary/scripts/prepare-agents b/.auxiliary/scripts/prepare-agents new file mode 100755 index 0000000..532384d --- /dev/null +++ b/.auxiliary/scripts/prepare-agents @@ -0,0 +1,170 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +eecho() { + echo "$@" >&2 +} + +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + eecho "Error: This script should not be sourced. Please run it directly." + return 1 2>/dev/null || exit 1 +fi + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + eecho "Error: Current directory is not in a Git repository" + exit 1 +fi + +repo_root="$(git rev-parse --show-toplevel)" +if [[ -z "$repo_root" ]]; then + eecho "Error: Could not determine Git repository root" + exit 1 +fi + +create_symlink_if_needed() { + local target_path="$1" + local link_path="$2" + + if [[ ! -e "$repo_root/$target_path" ]]; then + eecho "Warning: Target $repo_root/$target_path does not exist" + return + fi + + if [[ -L "$repo_root/$link_path" ]]; then + local current_target="$(readlink "$repo_root/$link_path")" + if [[ "$current_target" = "$target_path" ]]; then + return + else + echo "Updating symlink $link_path: $current_target → $target_path" + rm "$repo_root/$link_path" + fi + elif [[ -e "$repo_root/$link_path" ]]; then + eecho "Warning: File or directory already exists at $repo_root/$link_path" + return + fi + + ln -s "$target_path" "$link_path" +} + +create_symlinks() { + trap 'popd >/dev/null 2>&1 || true' ERR EXIT + + pushd "$repo_root" >/dev/null + + create_symlink_if_needed ".auxiliary/configuration/conventions.md" "CLAUDE.md" + create_symlink_if_needed ".auxiliary/configuration/conventions.md" "AGENTS.md" + create_symlink_if_needed ".auxiliary/configuration/mcp-servers.json" ".mcp.json" + create_symlink_if_needed ".auxiliary/configuration/claude" ".claude" + create_symlink_if_needed ".auxiliary/configuration/gemini" ".gemini" + create_symlink_if_needed ".auxiliary/configuration/opencode" ".opencode" + create_symlink_if_needed ".auxiliary/configuration/opencode.jsonc" "opencode.jsonc" + + popd >/dev/null + + trap - ERR EXIT +} + +download_instructions() { + local instructions_dir="$repo_root/.auxiliary/instructions" + local base_url="https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common" + local files=( + "architecture.rst" + "nomenclature.rst" + "nomenclature-germanic.rst" + "nomenclature-latin.rst" + "practices.rst" + "practices-python.rst" + "practices-rust.rst" + "practices-toml.rst" + "requirements.rst" + "style.rst" + "tests.rst" + ) + + mkdir -p "$instructions_dir" + + echo "Downloading project documentation guides to .auxiliary/instructions/" + + local success_count=0 + for file in "${files[@]}"; do + local url="$base_url/$file" + local output_path="$instructions_dir/$file" + + if curl --fail --silent --location "$url" | tail -n +20 > "$output_path"; then + if [[ -s "$output_path" ]]; then + echo " ✓ Downloaded $file ($(wc -c < "$output_path") bytes, boilerplate stripped)" + success_count=$((success_count + 1)) + else + eecho " ✗ Downloaded $file but file is empty after processing" + rm -f "$output_path" + fi + else + eecho " ✗ Failed to download $file" + fi + done + + if [[ $success_count -eq ${#files[@]} ]]; then + echo "Successfully downloaded all ${#files[@]} documentation guides" + else + eecho "Warning: Only downloaded $success_count of ${#files[@]} documentation guides" + fi +} + +echo "Creating symlinks for LLM instruction files..." +create_symlinks +echo "Symlinks created successfully" + +download_instructions + +copy_claude_commands() { + local claude_commands_dir="$repo_root/.auxiliary/configuration/claude/commands" + local opencode_commands_dir="$repo_root/.auxiliary/configuration/opencode/command" + + if [[ ! -d "$claude_commands_dir" ]]; then + eecho "Warning: Claude commands directory not found at $claude_commands_dir" + return + fi + + mkdir -p "$opencode_commands_dir" + + echo "Copying Claude commands to Opencode format..." + + local copied_count=0 + for claude_command in "$claude_commands_dir"/*.md; do + if [[ ! -f "$claude_command" ]]; then + continue + fi + + local command_name="$(basename "$claude_command")" + local opencode_command="$opencode_commands_dir/$command_name" + + if cat "$claude_command" | grep -vE '^(argument-hints|allowed-tools):.*$' > "$opencode_command"; then + echo " ✓ Copied $command_name" + copied_count=$((copied_count + 1)) + else + eecho " ✗ Failed to copy $command_name" + rm -f "$opencode_command" + fi + done + + if [[ $copied_count -gt 0 ]]; then + echo "Successfully copied $copied_count command(s) to Opencode format" + else + echo "No commands were copied" + fi +} + +copy_claude_commands + +echo "" +echo "Agent environment preparation complete!" +echo "- Created symlinks:" +echo " - CLAUDE.md and AGENTS.md → .auxiliary/configuration/conventions.md" +echo " - .mcp.json → .auxiliary/configuration/mcp-servers.json" +echo " - .claude → .auxiliary/configuration/claude" +echo " - .gemini → .auxiliary/configuration/gemini" +echo " - .opencode → .auxiliary/configuration/opencode" +echo " - opencode.jsonc → .auxiliary/configuration/opencode.jsonc" +echo "- Downloaded project documentation guides" +echo "- Copied Claude commands to Opencode format (filtered for compatibility)" \ No newline at end of file diff --git a/.auxiliary/secrets/.gitignore b/.auxiliary/secrets/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.auxiliary/secrets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml index b8098a1..dadcf5f 100644 --- a/.github/workflows/claude.yaml +++ b/.github/workflows/claude.yaml @@ -9,6 +9,13 @@ on: types: [opened, assigned] pull_request_review: types: [submitted] + workflow_dispatch: + inputs: + prompt: + description: 'Message to send to Claude' + required: true + default: 'Please validate your MCP server setup and available subagents.' + type: string jobs: @@ -20,7 +27,8 @@ jobs: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '/claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '/claude') || contains(github.event.issue.title, '/claude'))) + (github.event_name == 'issues' && (contains(github.event.issue.body, '/claude') || contains(github.event.issue.title, '/claude'))) || + (github.event_name == 'workflow_dispatch') needs: [initialize] permissions: contents: read @@ -29,8 +37,9 @@ jobs: pull-requests: read uses: emcd/python-project-common/.github/workflows/xrepo--claude.yaml@gha-1 with: - allowed-tools: 'Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git diff:*),Bash(git log:*),Bash(git ls-files:*),Bash(git remote:*),Bash(git reset:*),Bash(git rev-parse:*),Bash(git rm:*),Bash(git status),Bash(hatch:*),Bash(pip:*),Bash(python:*),Edit,Write' + allowed-tools: 'Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git diff:*),Bash(git log:*),Bash(git ls-files:*),Bash(git remote:*),Bash(git reset:*),Bash(git rev-parse:*),Bash(git rm:*),Bash(git status),Bash(hatch:*),Bash(pip:*),Bash(python:*),Edit,Write,mcp__context7__resolve-library-id,mcp__context7__get-library-docs,mcp__librovore__query_inventory,mcp__librovore__query_content' python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' timeout-minutes: 20 + prompt: '${{ inputs.prompt }}' secrets: anthropic-api-key: '${{ secrets.ANTHROPIC_API_KEY }}' diff --git a/.gitignore b/.gitignore index 5307c35..5d97b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,14 @@ .claude .gemini .mcp.json +.opencode .*.swp AGENTS.md CLAUDE.md CONVENTIONS.md GEMINI.md +opencode.jsonc __pycache__/ bugs/ build/ +dist/ diff --git a/README.rst b/README.rst index d5411d0..b36a90d 100644 --- a/README.rst +++ b/README.rst @@ -208,7 +208,7 @@ For development guidance and standards, please see the `development guide <https://emcd.github.io/python-classcore/stable/sphinx-html/contribution.html#development>`_. -`More Flair <https://www.imdb.com/title/tt0151804/characters/nm0431918>`_ +Additional Indicia =============================================================================== .. image:: https://img.shields.io/github/last-commit/emcd/python-classcore diff --git a/documentation/architecture/decisions/index.rst b/documentation/architecture/decisions/index.rst new file mode 100644 index 0000000..a84ce73 --- /dev/null +++ b/documentation/architecture/decisions/index.rst @@ -0,0 +1,31 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Architectural Decision Records +******************************************************************************* + +.. toctree:: + :maxdepth: 2 + + +.. todo:: Add architectural decision records to toctree. + +For ADR format and guidance, see the `architecture documentation guide +<https://emcd.github.io/python-project-common/stable/sphinx-html/common/architecture.html>`_. \ No newline at end of file diff --git a/documentation/architecture/designs/index.rst b/documentation/architecture/designs/index.rst new file mode 100644 index 0000000..ebd094d --- /dev/null +++ b/documentation/architecture/designs/index.rst @@ -0,0 +1,28 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Designs +******************************************************************************* + +.. toctree:: + :maxdepth: 2 + + +.. todo:: Add design documents to toctree. diff --git a/documentation/architecture/filesystem.rst b/documentation/architecture/filesystem.rst new file mode 100644 index 0000000..d3ca1b9 --- /dev/null +++ b/documentation/architecture/filesystem.rst @@ -0,0 +1,86 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Filesystem Organization +******************************************************************************* + +This document describes the specific filesystem organization for the project, +showing how the standard organizational patterns are implemented for this +project's configuration. For the underlying principles and rationale behind +these patterns, see the `common architecture documentation +<https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/architecture.rst>`_. + +Project Structure +=============================================================================== + +Root Directory Organization +------------------------------------------------------------------------------- + +The project implements the standard filesystem organization: + +.. code-block:: + + python-classcore/ + ├── LICENSE.txt # Project license + ├── README.rst # Project overview and quick start + ├── pyproject.toml # Python packaging and tool configuration + ├── documentation/ # Sphinx documentation source + ├── sources/ # All source code + ├── tests/ # Test suites + └── .auxiliary/ # Development workspace + +Source Code Organization +=============================================================================== + +Package Structure +------------------------------------------------------------------------------- + +The main Python package follows the standard ``sources/`` directory pattern: + +.. code-block:: + + sources/ + ├── classcore/ # Main Python package + │ ├── __/ # Centralized import hub + │ │ ├── __init__.py # Re-exports core utilities + │ │ ├── imports.py # External library imports + │ │ └── nomina.py # python-classcore-specific naming constants + │ ├── __init__.py # Package entry point + │ ├── py.typed # Type checking marker + │ └── [modules].py # Feature-specific modules + + +All package modules use the standard ``__`` import pattern as documented +in the common architecture guide. + +Component Integration +=============================================================================== + +Architecture Evolution +=============================================================================== + +This filesystem organization provides a foundation that architect agents can +evolve as the project grows. For questions about organizational principles, +subpackage patterns, or testing strategies, refer to the comprehensive common +documentation: + +* `Architecture Patterns <https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/architecture.rst>`_ +* `Development Practices <https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/practices.rst>`_ +* `Test Development Guidelines <https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst>`_ diff --git a/documentation/architecture/index.rst b/documentation/architecture/index.rst new file mode 100644 index 0000000..6f09ef9 --- /dev/null +++ b/documentation/architecture/index.rst @@ -0,0 +1,34 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Architecture +******************************************************************************* + +.. toctree:: + :maxdepth: 2 + + summary + filesystem + decisions/index + designs/index + testplans/index + + +.. todo:: Populate architecture documentation sections. \ No newline at end of file diff --git a/documentation/architecture/summary.rst b/documentation/architecture/summary.rst new file mode 100644 index 0000000..bab368a --- /dev/null +++ b/documentation/architecture/summary.rst @@ -0,0 +1,24 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +System Overview +******************************************************************************* + +.. todo:: Describe the high-level system architecture, major components, and their relationships. \ No newline at end of file diff --git a/documentation/architecture/testplans/index.rst b/documentation/architecture/testplans/index.rst new file mode 100644 index 0000000..3db3d71 --- /dev/null +++ b/documentation/architecture/testplans/index.rst @@ -0,0 +1,30 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Test Plans +******************************************************************************* + +.. toctree:: + :maxdepth: 2 + + summary + + +.. todo:: Add test plan documents to toctree. \ No newline at end of file diff --git a/documentation/architecture/testplans/summary.rst b/documentation/architecture/testplans/summary.rst new file mode 100644 index 0000000..d480c7f --- /dev/null +++ b/documentation/architecture/testplans/summary.rst @@ -0,0 +1,86 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Test Organization Summary +******************************************************************************* + +Overview +=============================================================================== + +This section contains comprehensive test planning documentation, including test +organization conventions, coverage strategies, and detailed implementation +plans for achieving systematic test coverage. + +Test plans follow project testing principles described in the `common test +development guidelines +<https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst>`_. +Key principles include: + +- **Dependency injection over monkey-patching** for testable code architecture +- **Systematic coverage analysis** with clear gap identification +- **Performance-conscious resource use** with appropriate testing strategies +- **Organized test structure** with numbered modules and functions + +Test Planning Process +=============================================================================== + +The test planning process systematically addresses: + +**Coverage Gap Analysis** + Identification of all uncovered lines and untested functionality across modules + +**Test Strategy Development** + Comprehensive approaches for testing each function, class, and method with + appropriate test data strategies + +**Implementation Guidance** + Detailed plans for achieving coverage while following project testing principles + +**Architectural Considerations** + Analysis of testability constraints and recommendations for maintaining + clean, testable code + +Test Module Numbering Scheme +=============================================================================== + +.. todo:: Define project-specific test module numbering conventions. + +Test Function Numbering +=============================================================================== + +Within each test module, functions are numbered by component: + +- **000-099**: Basic functionality tests for the module +- **100-199, 200-299, etc.**: Each function/class gets its own 100-number block +- **Increments of 10-20**: For closely related test variations within a block + +Project-Specific Testing Conventions +=============================================================================== + +For detailed testing conventions, patterns, and guidelines, refer to the `common +test development guidelines +<https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst>`_. +This includes: + +- Coverage goals and strategies +- Performance considerations +- Test data organization patterns +- Dependency injection approaches +- Resource management during testing \ No newline at end of file diff --git a/documentation/changelog.rst b/documentation/changelog.rst index d52da9c..d8f5044 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,15 @@ Release Notes .. towncrier release notes start +classcore 1.9 (2025-09-24) +========================== + +Repairs +------- + +- Fix PyPy compatibility with super() calls in slotted dataclasses. + + Classcore 1.8 (2025-07-23) ========================== diff --git a/documentation/conf.py b/documentation/conf.py index 8a215b6..fb7f5b7 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -100,6 +100,8 @@ def _import_version( ): r'https://pypi.org/project/classcore/', # Github aggressively rate-limits access to certain blobs. r'https://github\.com/.*/.*/blob/.*', + # Avoid timeouts for slow sites. + r'http://www\.catb\.org/~esr/faqs/smart-questions\.html', ] # -- Options for HTML output ------------------------------------------------- diff --git a/documentation/contribution.rst b/documentation/contribution.rst index 9c475de..a2cea55 100644 --- a/documentation/contribution.rst +++ b/documentation/contribution.rst @@ -46,6 +46,29 @@ Ways to Contribute Development =============================================================================== +Architecture +------------------------------------------------------------------------------- + +* The :doc:`Product Requirements Document <prd>` is a good starting point to + understand the motivations and rationale for the project. This should be + reviewed and updated, as necessary, when making changes that affect product + functionality or user experience. See the `requirements documentation guide + <https://emcd.github.io/python-project-common/stable/sphinx-html/common/requirements.html>`_ + for PRD format and best practices. + +* The :doc:`system architecture overview <architecture/summary>` should be + reviewed to understand the structure and operational patterns of the project. + Major changes to the architecture should be reflected in this document. + +* Document significant architectural decisions using Architectural Decision + Records (ADRs) in the ``architecture/decisions/`` directory. See the + `architecture documentation guide + <https://emcd.github.io/python-project-common/stable/sphinx-html/common/architecture.html>`_ + for ADR format and best practices. + +* Document interface specifications, schemas, and algorithms in the + ``architecture/designs/`` directory to guide implementation efforts. + Guidance and Standards ------------------------------------------------------------------------------- @@ -106,4 +129,6 @@ Resources .. toctree:: :maxdepth: 2 + prd + architecture/index devapi diff --git a/documentation/prd.rst b/documentation/prd.rst new file mode 100644 index 0000000..85bf640 --- /dev/null +++ b/documentation/prd.rst @@ -0,0 +1,27 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + | | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Product Requirements Document +******************************************************************************* + +.. todo:: Define product requirements, user stories, and acceptance criteria. + +For PRD format and guidance, see the `requirements documentation guide +<https://emcd.github.io/python-project-common/stable/sphinx-html/common/requirements.html>`_. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 25e3c54..c04fa2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,7 @@ dependencies = [ 'Jinja2', 'coverage[toml]', 'furo', + 'isort', 'packaging', 'pre-commit', 'pyright', @@ -106,6 +107,7 @@ dependencies = [ 'sphinx-copybutton', 'sphinx-inline-tabs', 'towncrier', + 'vulture', # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- ] @@ -119,15 +121,17 @@ PYTHONUNBUFFERED = 'TRUE' # TODO: Only for coverage/pytest. # --- END: Injected by Copier --- [tool.hatch.envs.develop.scripts] docsgen = [ - """sphinx-build -E -b linkcheck -d .auxiliary/caches/sphinx \ + """sphinx-build -E -b linkcheck -d .auxiliary/caches/sphinx --quiet \ documentation .auxiliary/artifacts/sphinx-linkcheck""", - """sphinx-build -a -d .auxiliary/caches/sphinx \ + """sphinx-build -a -d .auxiliary/caches/sphinx --quiet \ documentation .auxiliary/artifacts/sphinx-html""", ] linters = [ """ruff check --quiet sources documentation tests""", # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- + """isort --check-only --diff sources tests""", + """vulture""", """pyright sources""", ] packagers = [ @@ -137,7 +141,7 @@ packagers = [ ] testers-documentation = [ """coverage run -m sphinx.cmd.build \ - -E -b doctest -d .auxiliary/caches/sphinx \ + -E -b doctest -d .auxiliary/caches/sphinx --quiet \ documentation .auxiliary/artifacts/sphinx-doctest""", ] testers-serotine = [ """coverage run -m pytest -m slow""" ] @@ -176,6 +180,25 @@ python = [ [tool.hatch.version] path = 'sources/classcore/__init__.py' +# https://pycqa.github.io/isort/docs/configuration/config_files.html +[tool.isort] +# profile = 'black' +src_paths = [ 'sources', 'tests' ] +case_sensitive = true +# ensure_newline_before_comments = true +# force_sort_within_sections = true +ignore_whitespace = true +include_trailing_comma = true +known_first_party = [ 'classcore' ] +lines_between_types = 1 +line_length = 79 +multi_line_output = 3 +quiet = true +# skip_gitignore = true +skip_glob = [ '*/__/imports.py', '*/__init__.py' ] +split_on_trailing_comma = true +use_parentheses = true + # https://mypy.readthedocs.io/en/stable/config_file.html [tool.mypy] # Note: Due to repeated painful experiences with Mypy, we use Pyright instead. @@ -222,7 +245,7 @@ stubPath = 'sources/classcore/_typedecls' # Note: Cannot run doctests from Pytest, because Pytest tries to update '_' # attribute on protected modules. Instead, we use Sphinx to run doctests. minversion = '8.1' -addopts = """--capture=no --exitfirst -rfE -m 'not slow'""" # TODO? '--fail-under' +addopts = """--capture=no --exitfirst --quiet -rfE -m 'not slow'""" # TODO? '--fail-under' testpaths = [ 'tests' ] python_files = [ 'test_*.py' ] python_functions = [ 'test_[0-9][0-9][0-9]_*' ] @@ -234,6 +257,7 @@ markers = [ [tool.ruff] #builtins = [ 'ic' ] cache-dir = '.auxiliary/caches/ruff' +extend-exclude = [ 'vulturefood.py' ] indent-width = 4 line-length = 79 [tool.ruff.lint] @@ -323,3 +347,12 @@ showcontent = true directory = 'repair' name = 'Repairs' showcontent = true + +[tool.vulture] +paths = [ '.auxiliary/configuration/vulturefood.py', 'sources' ] +min_confidence = 60 +exclude = [ + '*/imports.py', +] +ignore_decorators = [ '@__.abc.abstractmethod', '@__.typx.overload' ] +ignore_names = [ 'main' ] diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index ed702cd..0e491de 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.8' +__version__ = '1.9' standard.finalize_module( diff --git a/sources/classcore/standard/nomina.py b/sources/classcore/standard/nomina.py index ec04730..1681937 100644 --- a/sources/classcore/standard/nomina.py +++ b/sources/classcore/standard/nomina.py @@ -22,9 +22,8 @@ # ruff: noqa: F403,F405 -from . import __ from ..nomina import * - +from . import __ concealment_label = 'concealment' immutability_label = 'immutability' diff --git a/sources/classcore/utilities.py b/sources/classcore/utilities.py index 1616e70..b8ec664 100644 --- a/sources/classcore/utilities.py +++ b/sources/classcore/utilities.py @@ -114,7 +114,7 @@ def qualify_class_name( cls: type ) -> str: def repair_class_reproduction( original: type, reproduction: type ) -> None: ''' Repairs a class reproduction, if necessary. ''' match __.platform.python_implementation( ): - case 'CPython': # pragma: no branch + case 'CPython' | 'PyPy': # pragma: no branch _repair_cpython_class_closures( original, reproduction ) case _: pass # pragma: no cover diff --git a/tests/test_000_classcore/__.py b/tests/test_000_classcore/__.py new file mode 100644 index 0000000..bc13919 --- /dev/null +++ b/tests/test_000_classcore/__.py @@ -0,0 +1,64 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Common test utilities and helpers. ''' + + +import types + +from pathlib import Path + + +PACKAGE_NAME = 'classcore' +PACKAGES_NAMES = ( PACKAGE_NAME, ) + + +_modules_cache: dict[ str, types.ModuleType ] = { } +def cache_import_module( qname: str ) -> types.ModuleType: + ''' Imports module from package by name and caches it. ''' + from importlib import import_module + package_name, *maybe_module_name = qname.rsplit( '.', maxsplit = 1 ) + if not maybe_module_name: arguments = ( qname, ) + else: arguments = ( f".{maybe_module_name[0]}", package_name, ) + if qname not in _modules_cache: + _modules_cache[ qname ] = import_module( *arguments ) + return _modules_cache[ qname ] + + +def _discover_module_names( package_name: str ) -> tuple[ str, ... ]: + package = cache_import_module( package_name ) + if not package.__file__: return ( ) + return tuple( + path.stem + for path in Path( package.__file__ ).parent.glob( '*.py' ) + if path.name not in ( '__init__.py', '__main__.py' ) + and path.is_file( ) ) + + +MODULES_NAMES_BY_PACKAGE_NAME = types.MappingProxyType( { + name: _discover_module_names( name ) for name in PACKAGES_NAMES } ) +PACKAGES_NAMES_BY_MODULE_QNAME = types.MappingProxyType( { + f"{subpackage_name}.{module_name}": subpackage_name + for subpackage_name in PACKAGES_NAMES + for module_name in MODULES_NAMES_BY_PACKAGE_NAME[ subpackage_name ] } ) +MODULES_QNAMES = tuple( PACKAGES_NAMES_BY_MODULE_QNAME.keys( ) ) +MODULES_NAMES_BY_MODULE_QNAME = types.MappingProxyType( { + name: name.rsplit( '.', maxsplit = 1 )[ -1 ] + for name in PACKAGES_NAMES_BY_MODULE_QNAME } ) \ No newline at end of file diff --git a/tests/test_000_classcore/__init__.py b/tests/test_000_classcore/__init__.py index 6c22331..a1dcd7f 100644 --- a/tests/test_000_classcore/__init__.py +++ b/tests/test_000_classcore/__init__.py @@ -18,50 +18,4 @@ #============================================================================# -''' Package of tests. - - Common imports, constants, and utilities for tests. -''' - - -import types - -from pathlib import Path - - -PACKAGE_NAME = 'classcore' -PACKAGES_NAMES = ( PACKAGE_NAME, f"{PACKAGE_NAME}.standard" ) - - -_modules_cache: dict[ str, types.ModuleType ] = { } -def cache_import_module( qname: str ) -> types.ModuleType: - ''' Imports module from package by name and caches it. ''' - from importlib import import_module - package_name, *maybe_module_name = qname.rsplit( '.', maxsplit = 1 ) - if not maybe_module_name: arguments = ( qname, ) - else: arguments = ( f".{maybe_module_name[0]}", package_name, ) - if qname not in _modules_cache: - _modules_cache[ qname ] = import_module( *arguments ) - return _modules_cache[ qname ] - - -def _discover_module_names( package_name: str ) -> tuple[ str, ... ]: - package = cache_import_module( package_name ) - if not package.__file__: return ( ) - return tuple( - path.stem - for path in Path( package.__file__ ).parent.glob( '*.py' ) - if path.name not in ( '__init__.py', '__main__.py' ) - and path.is_file( ) ) - - -MODULES_NAMES_BY_PACKAGE_NAME = types.MappingProxyType( { - name: _discover_module_names( name ) for name in PACKAGES_NAMES } ) -PACKAGES_NAMES_BY_MODULE_QNAME = types.MappingProxyType( { - f"{subpackage_name}.{module_name}": subpackage_name - for subpackage_name in PACKAGES_NAMES - for module_name in MODULES_NAMES_BY_PACKAGE_NAME[ subpackage_name ] } ) -MODULES_QNAMES = tuple( PACKAGES_NAMES_BY_MODULE_QNAME.keys( ) ) -MODULES_NAMES_BY_MODULE_QNAME = types.MappingProxyType( { - name: name.rsplit( '.', maxsplit = 1 )[ -1 ] - for name in PACKAGES_NAMES_BY_MODULE_QNAME } ) +''' Package of tests. ''' diff --git a/tests/test_000_classcore/test_000_package.py b/tests/test_000_classcore/test_000_package.py index f3d5e3e..b29c3d0 100644 --- a/tests/test_000_classcore/test_000_package.py +++ b/tests/test_000_classcore/test_000_package.py @@ -23,46 +23,21 @@ import pytest -from . import ( - # MODULES_NAMES_BY_MODULE_QNAME, - MODULES_QNAMES, - PACKAGES_NAMES, - PACKAGES_NAMES_BY_MODULE_QNAME, - cache_import_module, -) +from . import __ -@pytest.mark.parametrize( 'package_name', PACKAGES_NAMES ) +@pytest.mark.parametrize( 'package_name', __.PACKAGES_NAMES ) def test_000_sanity( package_name ): ''' Package is sane. ''' - package = cache_import_module( package_name ) + package = __.cache_import_module( package_name ) assert package.__package__ == package_name assert package.__name__ == package_name -# @pytest.mark.parametrize( 'module_qname', MODULES_QNAMES ) -# def test_010_attribute_module_existence( module_qname ): -# ''' Package module is attribute of package. ''' -# package_name = PACKAGES_NAMES_BY_MODULE_QNAME[ module_qname ] -# package = cache_import_module( package_name ) -# module_name = MODULES_NAMES_BY_MODULE_QNAME[ module_qname ] -# assert module_name in package.__dict__ - - -# @pytest.mark.parametrize( 'module_qname', MODULES_QNAMES ) -# def test_011_attribute_module_classification( module_qname ): -# ''' Package attribute is module. ''' -# from inspect import ismodule -# package_name = PACKAGES_NAMES_BY_MODULE_QNAME[ module_qname ] -# package = cache_import_module( package_name ) -# module_name = MODULES_NAMES_BY_MODULE_QNAME[ module_qname ] -# assert ismodule( getattr( package, module_name ) ) - - -@pytest.mark.parametrize( 'module_qname', MODULES_QNAMES ) +@pytest.mark.parametrize( 'module_qname', __.MODULES_QNAMES ) def test_100_sanity( module_qname ): ''' Package module is sane. ''' - package_name = PACKAGES_NAMES_BY_MODULE_QNAME[ module_qname ] - module = cache_import_module( module_qname ) + package_name = __.PACKAGES_NAMES_BY_MODULE_QNAME[ module_qname ] + module = __.cache_import_module( module_qname ) assert module.__package__ == package_name assert module.__name__ == module_qname diff --git a/tests/test_000_classcore/test_010_base.py b/tests/test_000_classcore/test_010_base.py index e61dcee..343798d 100644 --- a/tests/test_000_classcore/test_010_base.py +++ b/tests/test_000_classcore/test_010_base.py @@ -23,7 +23,7 @@ import pytest -from . import PACKAGE_NAME, cache_import_module +from . import __ @pytest.mark.parametrize( @@ -31,5 +31,5 @@ ) def test_100_exports( module_name ): ''' Module exports expected names. ''' - module = cache_import_module( f"{PACKAGE_NAME}.__.imports" ) + module = __.cache_import_module( f"{__.PACKAGE_NAME}.__.imports" ) assert hasattr( module, module_name ) diff --git a/tests/test_000_classcore/test_100_exceptions.py b/tests/test_000_classcore/test_100_exceptions.py index 2ae3c03..4fc84ee 100644 --- a/tests/test_000_classcore/test_100_exceptions.py +++ b/tests/test_000_classcore/test_100_exceptions.py @@ -20,7 +20,7 @@ import pytest -from . import PACKAGE_NAME, cache_import_module +from .__ import PACKAGE_NAME, cache_import_module CLASS_NAMES = ( diff --git a/tests/test_000_classcore/test_110_utilities.py b/tests/test_000_classcore/test_110_utilities.py index 8e4d449..a8951dc 100644 --- a/tests/test_000_classcore/test_110_utilities.py +++ b/tests/test_000_classcore/test_110_utilities.py @@ -19,17 +19,14 @@ from dataclasses import dataclass -from platform import python_implementation import pytest -from . import PACKAGE_NAME, cache_import_module +from .__ import PACKAGE_NAME, cache_import_module MODULE_QNAME = f"{PACKAGE_NAME}.utilities" -pyimpl = python_implementation( ) - class Foo: x = 1 foo = Foo( ) @@ -73,9 +70,7 @@ class CS: __slots__ = ( 'z', ) module.delattr0( cs, 'missing' ) -@pytest.mark.skipif( - 'CPython' != pyimpl, reason = 'Only relevant to CPython.' ) -def test_300_cpython_class_repair_function_closure( ): +def test_300_class_repair_function_closure( ): ''' Reproduction has class cell repaired in function closure. ''' class Wut: def __dir__( self ): return super( ).__dir__( ) @@ -88,9 +83,7 @@ def __dir__( self ): return super( ).__dir__( ) assert closure.cell_contents is DataclassWut -@pytest.mark.skipif( - 'CPython' != pyimpl, reason = 'Only relevant to CPython.' ) -def test_301_cpython_class_repair_property_closure( ): +def test_301_class_repair_property_closure( ): ''' Reproduction has class cell repaired in property closure. ''' class Wut: @property @@ -103,9 +96,7 @@ def name( self ): return super( ).__str__( ) assert wut.name == str( wut ) -@pytest.mark.skipif( - 'CPython' != pyimpl, reason = 'Only relevant to CPython.' ) -def test_302_cpython_class_repair_nothing( ): +def test_302_class_repair_nothing( ): ''' Reproduction has no class cell to repair in anything. ''' class Wut: @property diff --git a/tests/test_000_classcore/test_210_decorators.py b/tests/test_000_classcore/test_210_decorators.py index 08ef214..6101e30 100644 --- a/tests/test_000_classcore/test_210_decorators.py +++ b/tests/test_000_classcore/test_210_decorators.py @@ -20,7 +20,7 @@ # import pytest -from . import PACKAGE_NAME, cache_import_module +from .__ import PACKAGE_NAME, cache_import_module MODULE_QNAME = f"{PACKAGE_NAME}.decorators" diff --git a/tests/test_000_classcore/test_310_standard_internals.py b/tests/test_000_classcore/test_310_standard_internals.py index 85427e6..b874098 100644 --- a/tests/test_000_classcore/test_310_standard_internals.py +++ b/tests/test_000_classcore/test_310_standard_internals.py @@ -20,7 +20,7 @@ import pytest -from . import PACKAGE_NAME, cache_import_module +from .__ import PACKAGE_NAME, cache_import_module MODULE_QNAME = f"{PACKAGE_NAME}.standard.__" diff --git a/tests/test_000_classcore/test_330_standard_decorators.py b/tests/test_000_classcore/test_330_standard_decorators.py index 3fea68b..d4edecb 100644 --- a/tests/test_000_classcore/test_330_standard_decorators.py +++ b/tests/test_000_classcore/test_330_standard_decorators.py @@ -20,7 +20,7 @@ import pytest -from . import PACKAGE_NAME, cache_import_module +from .__ import PACKAGE_NAME, cache_import_module MODULE_QNAME = f"{PACKAGE_NAME}.standard.decorators" diff --git a/tests/test_000_classcore/test_340_standard_classes.py b/tests/test_000_classcore/test_340_standard_classes.py index 115d5dd..dd44178 100644 --- a/tests/test_000_classcore/test_340_standard_classes.py +++ b/tests/test_000_classcore/test_340_standard_classes.py @@ -20,15 +20,15 @@ # import pytest -from . import PACKAGE_NAME, cache_import_module - +from .__ import PACKAGE_NAME, cache_import_module MODULE_QNAME = f"{PACKAGE_NAME}.standard.classes" def test_124_cfc_instances_ignore_init_arguments( ): ''' Metaclass respects 'instances_ignore_init_arguments'. ''' - from urllib.parse import ParseResult as UrlParts, urlparse + from urllib.parse import ParseResult as UrlParts + from urllib.parse import urlparse module = cache_import_module( MODULE_QNAME ) class Url( diff --git a/tests/test_000_classcore/test_350_standard_modules.py b/tests/test_000_classcore/test_350_standard_modules.py index dd399f5..15cc060 100644 --- a/tests/test_000_classcore/test_350_standard_modules.py +++ b/tests/test_000_classcore/test_350_standard_modules.py @@ -23,7 +23,7 @@ import pytest -from . import PACKAGE_NAME, cache_import_module +from .__ import PACKAGE_NAME, cache_import_module MODULE_QNAME = f"{PACKAGE_NAME}.standard.modules" From bb2dc5cce820e4b280d18b0044f658accf188bb2 Mon Sep 17 00:00:00 2001 From: Github Actions Robot <emcd@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:23:26 +0000 Subject: [PATCH 23/28] Update documents for publication. (1.10) --- .../+api-constants-and-utilities.enhance.rst | 1 + ...-additional-compatibility-fixes.repair.rst | 1 + .../+pypy-super-compatibility.repair.rst | 1 - .auxiliary/publications/website.tar.xz | 4 +- documentation/changelog.rst | 15 +++++++ sources/classcore/__init__.py | 2 +- sources/classcore/exceptions.py | 10 ++++- sources/classcore/nomina.py | 5 +++ sources/classcore/standard/__.py | 8 ++-- sources/classcore/standard/classes.py | 39 +++++++++++++++++-- 10 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 .auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst create mode 100644 .auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst delete mode 100644 .auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst diff --git a/.auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst b/.auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst new file mode 100644 index 0000000..870b8f0 --- /dev/null +++ b/.auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst @@ -0,0 +1 @@ +Expose exception mutability constants and public identifier utility function for downstream reuse. \ No newline at end of file diff --git a/.auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst b/.auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst new file mode 100644 index 0000000..833a809 --- /dev/null +++ b/.auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst @@ -0,0 +1 @@ +Fix additional PyPy compatibility issues with exception and ABC cache attribute mutations. \ No newline at end of file diff --git a/.auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst b/.auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst deleted file mode 100644 index 9eb2d08..0000000 --- a/.auxiliary/data/towncrier/+pypy-super-compatibility.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Fix PyPy compatibility with super() calls in slotted dataclasses. \ No newline at end of file diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 9f0ad13..492bc9b 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19559cbc05e5c7280535670c5c5ef1057de3105e5c3d1a0959645551735e4088 -size 371636 +oid sha256:4a9a3d35dc33e60d578f92b221b7748acbee3246dfb718df2d5f18a3e2771687 +size 436164 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index d8f5044..4790175 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,21 @@ Release Notes .. towncrier release notes start +classcore 1.10 (2025-09-25) +=========================== + +Enhancements +------------ + +- Expose exception mutability constants and public identifier utility function for downstream reuse. + + +Repairs +------- + +- Fix additional PyPy compatibility issues with exception and ABC cache attribute mutations. + + classcore 1.9 (2025-09-24) ========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 0e491de..2714dae 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.9' +__version__ = '1.10' standard.finalize_module( diff --git a/sources/classcore/exceptions.py b/sources/classcore/exceptions.py index 95cfe64..0263686 100644 --- a/sources/classcore/exceptions.py +++ b/sources/classcore/exceptions.py @@ -22,12 +22,20 @@ from . import __ +from . import nomina as _nomina from . import standard as _standard +exception_mutables_default = ( + '__cause__', '__context__', '__suppress_context__', '__traceback__' ) +exception_visibles_default = ( + *exception_mutables_default, _nomina.is_public_identifier ) + + class Omniexception( _standard.Object, BaseException, - instances_visibles = ( '__cause__', '__context__' ), + instances_mutables = exception_mutables_default, + instances_visibles = exception_visibles_default, ): ''' Base exception for package. ''' diff --git a/sources/classcore/nomina.py b/sources/classcore/nomina.py index c07ad47..f19d467 100644 --- a/sources/classcore/nomina.py +++ b/sources/classcore/nomina.py @@ -204,3 +204,8 @@ ], __.ddoc.Doc( ''' Initializer to use with metaclass. ''' ), ] + + +def is_public_identifier( name: str ) -> bool: + ''' Is Python identifier public? ''' + return not name.startswith( '_' ) diff --git a/sources/classcore/standard/__.py b/sources/classcore/standard/__.py index 0462c34..81cff04 100644 --- a/sources/classcore/standard/__.py +++ b/sources/classcore/standard/__.py @@ -20,13 +20,11 @@ ''' Common constants, imports, and utilities. ''' +# ruff: noqa: F403 -from ..__ import * # noqa: F403 - -def is_public_identifier( name: str ) -> bool: - ''' Is Python identifier public? ''' - return not name.startswith( '_' ) +from ..__ import * +from ..nomina import is_public_identifier def provide_error_class( name: str ) -> type[ Exception ]: diff --git a/sources/classcore/standard/classes.py b/sources/classcore/standard/classes.py index 4e945e7..b68285e 100644 --- a/sources/classcore/standard/classes.py +++ b/sources/classcore/standard/classes.py @@ -205,7 +205,16 @@ class DataclassObjectMutable( metaclass = DataclassMutable ): 'class instance conceal' ) -class Protocol( __.typx.Protocol, metaclass = ProtocolClass ): +class Protocol( + __.typx.Protocol, + metaclass = ProtocolClass, + class_mutables = ( + '_abc_cache', + '_abc_negative_cache', + '_abc_negative_cache_version', + '_abc_registry', + ), +): ''' Standard base protocol class. ''' _dynadoc_fragments_ = ( @@ -215,7 +224,15 @@ class Protocol( __.typx.Protocol, metaclass = ProtocolClass ): class ProtocolMutable( - __.typx.Protocol, metaclass = ProtocolClass, instances_mutables = '*' + __.typx.Protocol, + metaclass = ProtocolClass, + class_mutables = ( + '_abc_cache', + '_abc_negative_cache', + '_abc_negative_cache_version', + '_abc_registry', + ), + instances_mutables = '*', ): ''' Base protocol class with mutable instance attributes. ''' @@ -226,7 +243,14 @@ class ProtocolMutable( class DataclassProtocol( - __.typx.Protocol, metaclass = ProtocolDataclass, + __.typx.Protocol, + metaclass = ProtocolDataclass, + class_mutables = ( + '_abc_cache', + '_abc_negative_cache', + '_abc_negative_cache_version', + '_abc_registry', + ), ): ''' Standard base protocol dataclass. ''' @@ -237,7 +261,14 @@ class DataclassProtocol( class DataclassProtocolMutable( - __.typx.Protocol, metaclass = ProtocolDataclassMutable, + __.typx.Protocol, + metaclass = ProtocolDataclassMutable, + class_mutables = ( + '_abc_cache', + '_abc_negative_cache', + '_abc_negative_cache_version', + '_abc_registry', + ), ): ''' Base protocol dataclass with mutable instance attributes. ''' From 2e2b406728c607e357fcd1b203364068833065a8 Mon Sep 17 00:00:00 2001 From: Github Actions Robot <emcd@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:09:53 +0000 Subject: [PATCH 24/28] Update documents for publication. (1.11) --- .auxiliary/configuration/AGENTS.md | 144 ++++++ .auxiliary/configuration/claude/.gitignore | 1 - .../claude/agents/python-annotator.md | 362 -------------- .../claude/agents/python-conformer.md | 339 ------------- .../claude/commands/cs-annotate-release.md | 93 ---- .../claude/commands/cs-architect.md | 100 ---- .../claude/commands/cs-code-python.md | 228 --------- .../claude/commands/cs-conform-python.md | 204 -------- .../claude/commands/cs-conform-toml.md | 281 ----------- .../claude/commands/cs-copier-update.md | 131 ----- .../claude/commands/cs-create-command.md | 108 ----- .../claude/commands/cs-design-python.md | 144 ------ .../claude/commands/cs-develop-pytests.md | 263 ---------- .../commands/cs-document-examples-rst.md | 117 ----- .../claude/commands/cs-excise-python.md | 155 ------ .../claude/commands/cs-inquire.md | 72 --- .../claude/commands/cs-manage-prd.md | 90 ---- .../claude/commands/cs-plan-pytests.md | 280 ----------- .../claude/commands/cs-release-checkpoint.md | 163 ------- .../claude/commands/cs-release-final.md | 195 -------- .../claude/commands/cs-release-maintenance.md | 237 --------- .../claude/commands/cs-review-todos.md | 103 ---- .../claude/commands/cs-update-command.md | 96 ---- .../claude/commands/cs-update-readme-rst.md | 105 ---- .../claude/commands/validate-custom-slash.md | 41 -- .../configuration/coders/claude/.gitignore | 4 + .../coders/claude/miscellany/bash-tool-bypass | 52 ++ .../claude/miscellany/command-template.md | 0 .../coders/claude/scripts}/post-edit-linter | 27 +- .../claude/scripts}/pre-bash-git-commit-check | 12 + .../claude/scripts}/pre-bash-python-check | 0 .../{ => coders}/claude/settings.json | 43 +- .../configuration/coders/codex/.gitignore | 1 + .../configuration/coders/codex/config.toml | 53 ++ .../coders/codex/rules/emcd.rules | 105 ++++ .../configuration/coders/gemini/.gitignore | 2 + .../gemini/miscellany/command-template.md | 43 ++ .../coders/gemini/policies/emcd.toml | 83 ++++ .../configuration/coders/gemini/settings.json | 54 +++ .../configuration/coders/opencode/.gitignore | 8 + .../coders/opencode/plugin/.gitignore | 13 + .../coders/opencode/plugin/README.md | 109 +++++ .../plugin/git-commit-guard.js-disabled | 195 ++++++++ .../coders/opencode/plugin/package.json | 13 + .../opencode/plugin/post-edit-linter.js | 130 +++++ .../python-environment-guard.js-disabled | 149 ++++++ .../coders/opencode/settings.jsonc | 122 +++++ .auxiliary/configuration/conventions.md | 39 -- .../configuration/copier-answers--agents.yaml | 19 + .auxiliary/configuration/copier-answers.yaml | 6 +- .auxiliary/configuration/gemini/settings.json | 23 - .auxiliary/configuration/mcp-servers.json | 25 +- .../opencode/agent/python-annotator.md | 220 --------- .../opencode/agent/python-conformer.md | 256 ---------- .../configuration/opencode/command/.gitignore | 2 - .auxiliary/configuration/pre-commit.yaml | 14 +- .auxiliary/configuration/vulturefood.py | 5 + .../+api-constants-and-utilities.enhance.rst | 1 - ...-additional-compatibility-fixes.repair.rst | 1 - .../data/towncrier/+type-aliases.enhance.rst | 1 + .auxiliary/notes/release-aar.rst | 219 --------- .auxiliary/pocs/.gitkeep | 0 .auxiliary/publications/website.tar.xz | 4 +- .auxiliary/scripts/claude-ds | 33 -- .auxiliary/scripts/claude-xai | 33 -- .auxiliary/scripts/prepare-agents | 170 ------- .github/workflows/core--initializer.yaml | 8 +- .github/workflows/releaser.yaml | 70 ++- .gitignore | 10 +- README.rst | 6 + .../architecture/decisions/index.rst | 31 -- documentation/architecture/designs/index.rst | 28 -- documentation/architecture/filesystem.rst | 86 ---- documentation/architecture/index.rst | 34 -- documentation/architecture/openspec/AGENTS.md | 455 ++++++++++++++++++ .../openspec/changes/archive/.gitkeep | 0 .../architecture/openspec/specs/.gitkeep | 0 .../specs/attribute-concealment/spec.md | 81 ++++ .../specs/attribute-immutability/spec.md | 96 ++++ .../openspec/specs/class-factories/spec.md | 96 ++++ .../specs/dataclass-integration/spec.md | 75 +++ .../specs/module-reclassification/spec.md | 63 +++ .../openspec/specs/protocol-support/spec.md | 67 +++ .../openspec/specs/type-safety/spec.md | 108 +++++ documentation/architecture/summary.rst | 24 - .../architecture/testplans/index.rst | 30 -- .../architecture/testplans/summary.rst | 86 ---- documentation/changelog.rst | 9 + documentation/conf.py | 40 +- documentation/contribution.rst | 55 ++- documentation/examples/std-behaviors.rst | 2 +- documentation/examples/std-dataclasses.rst | 2 +- documentation/prd.rst | 27 -- documentation/specifications/index.rst | 30 ++ pyproject.toml | 16 +- sources/classcore/README.md | 452 +++++++++++++++++ sources/classcore/__/imports.py | 3 + sources/classcore/__init__.py | 2 +- sources/classcore/nomina.py | 34 ++ sources/classcore/standard/README.md | 202 ++++++++ sources/classcore/standard/classes.py | 34 +- sources/classcore/standard/nomina.py | 1 - 102 files changed, 3348 insertions(+), 5356 deletions(-) create mode 100644 .auxiliary/configuration/AGENTS.md delete mode 100644 .auxiliary/configuration/claude/.gitignore delete mode 100644 .auxiliary/configuration/claude/agents/python-annotator.md delete mode 100644 .auxiliary/configuration/claude/agents/python-conformer.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-annotate-release.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-architect.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-code-python.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-conform-python.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-conform-toml.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-copier-update.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-create-command.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-design-python.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-develop-pytests.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-document-examples-rst.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-excise-python.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-inquire.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-manage-prd.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-plan-pytests.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-release-checkpoint.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-release-final.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-release-maintenance.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-review-todos.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-update-command.md delete mode 100644 .auxiliary/configuration/claude/commands/cs-update-readme-rst.md delete mode 100644 .auxiliary/configuration/claude/commands/validate-custom-slash.md create mode 100644 .auxiliary/configuration/coders/claude/.gitignore create mode 100755 .auxiliary/configuration/coders/claude/miscellany/bash-tool-bypass rename .auxiliary/configuration/{ => coders}/claude/miscellany/command-template.md (100%) rename .auxiliary/{scripts/claude => configuration/coders/claude/scripts}/post-edit-linter (72%) rename .auxiliary/{scripts/claude => configuration/coders/claude/scripts}/pre-bash-git-commit-check (88%) rename .auxiliary/{scripts/claude => configuration/coders/claude/scripts}/pre-bash-python-check (100%) rename .auxiliary/configuration/{ => coders}/claude/settings.json (69%) create mode 100644 .auxiliary/configuration/coders/codex/.gitignore create mode 100644 .auxiliary/configuration/coders/codex/config.toml create mode 100644 .auxiliary/configuration/coders/codex/rules/emcd.rules create mode 100644 .auxiliary/configuration/coders/gemini/.gitignore create mode 100644 .auxiliary/configuration/coders/gemini/miscellany/command-template.md create mode 100644 .auxiliary/configuration/coders/gemini/policies/emcd.toml create mode 100644 .auxiliary/configuration/coders/gemini/settings.json create mode 100644 .auxiliary/configuration/coders/opencode/.gitignore create mode 100644 .auxiliary/configuration/coders/opencode/plugin/.gitignore create mode 100644 .auxiliary/configuration/coders/opencode/plugin/README.md create mode 100644 .auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled create mode 100644 .auxiliary/configuration/coders/opencode/plugin/package.json create mode 100644 .auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js create mode 100644 .auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled create mode 100644 .auxiliary/configuration/coders/opencode/settings.jsonc delete mode 100644 .auxiliary/configuration/conventions.md create mode 100644 .auxiliary/configuration/copier-answers--agents.yaml delete mode 100644 .auxiliary/configuration/gemini/settings.json delete mode 100644 .auxiliary/configuration/opencode/agent/python-annotator.md delete mode 100644 .auxiliary/configuration/opencode/agent/python-conformer.md delete mode 100644 .auxiliary/configuration/opencode/command/.gitignore delete mode 100644 .auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst create mode 100644 .auxiliary/data/towncrier/+type-aliases.enhance.rst delete mode 100644 .auxiliary/notes/release-aar.rst create mode 100644 .auxiliary/pocs/.gitkeep delete mode 100755 .auxiliary/scripts/claude-ds delete mode 100755 .auxiliary/scripts/claude-xai delete mode 100755 .auxiliary/scripts/prepare-agents delete mode 100644 documentation/architecture/decisions/index.rst delete mode 100644 documentation/architecture/designs/index.rst delete mode 100644 documentation/architecture/filesystem.rst delete mode 100644 documentation/architecture/index.rst create mode 100644 documentation/architecture/openspec/AGENTS.md create mode 100644 documentation/architecture/openspec/changes/archive/.gitkeep create mode 100644 documentation/architecture/openspec/specs/.gitkeep create mode 100644 documentation/architecture/openspec/specs/attribute-concealment/spec.md create mode 100644 documentation/architecture/openspec/specs/attribute-immutability/spec.md create mode 100644 documentation/architecture/openspec/specs/class-factories/spec.md create mode 100644 documentation/architecture/openspec/specs/dataclass-integration/spec.md create mode 100644 documentation/architecture/openspec/specs/module-reclassification/spec.md create mode 100644 documentation/architecture/openspec/specs/protocol-support/spec.md create mode 100644 documentation/architecture/openspec/specs/type-safety/spec.md delete mode 100644 documentation/architecture/summary.rst delete mode 100644 documentation/architecture/testplans/index.rst delete mode 100644 documentation/architecture/testplans/summary.rst delete mode 100644 documentation/prd.rst create mode 100644 documentation/specifications/index.rst create mode 100644 sources/classcore/README.md create mode 100644 sources/classcore/standard/README.md diff --git a/.auxiliary/configuration/AGENTS.md b/.auxiliary/configuration/AGENTS.md new file mode 100644 index 0000000..d191a7f --- /dev/null +++ b/.auxiliary/configuration/AGENTS.md @@ -0,0 +1,144 @@ +# Context + +- Overview and Quick Start: README.{md,rst} +- Architecture and Design: @documentation/architecture/ +- Development Practices: @.auxiliary/instructions/ + +- Use the 'context7' MCP server to retrieve up-to-date documentation for any SDKs or APIs. +- Use the 'nb' MCP server for project note-taking, issue tracking, and collaboration. The server provides LLM-friendly access to the `nb` note-taking system with proper escaping and project-specific notebook context. +- Check README files in directories you're working with for insights about architecture, constraints, and TODO items. + +## Purpose +[Describe your project's purpose and goals] + +## Tech Stack +[List your primary technologies] + +# Development Standards + +Before implementing code changes, consult these files in `.auxiliary/instructions/`: +- `practices.rst` - General development principles (robustness, immutability, exception chaining) +- `practices-python.rst` - Python-specific patterns (module organization, type annotations, wide parameter/narrow return) +- `nomenclature.rst` - Naming conventions for variables, functions, classes, exceptions +- `style.rst` - Code formatting standards (spacing, line length, documentation mood) +- `validation.rst` - Quality assurance requirements (linters, type checkers, tests) + +# Operation + +- Use `rg --line-number --column` to get precise coordinates for MCP tools that require line/column positions. +- Choose appropriate editing tools based on the task complexity and your familiarity with the tools. +- If instruction files mention multiple language ecosystems, prefer tools and commands that match the project's configured languages; ignore language-inapplicable tooling unless the user explicitly requests it. +- Use a README-first discovery workflow to reduce token churn: + - Start at the repository root `README.{md,rst}`, then read the nearest relevant subtree README. + - After reading the nearest README, scope code searches to that subtree before considering repo-wide searches. + - If a touched subsystem README is stale after your change, update it in the same batch. +- Batch related changes together when possible to maintain consistency. +- Use relative paths rather than absolute paths when possible. +- Do not write to paths outside the current project unless explicitly requested. +- Use `.auxiliary/scribbles` for scratch work and one-off experiments instead of `/tmp`; use `.auxiliary/temporary` for ephemeral test state and build artifacts that are safe to delete. +- In sandboxed environments (e.g., Codex CLI), treat file/network permission failures as escalation boundaries: + - If an operation fails due to sandbox, file access, or network restrictions, rerun it with user escalation. + - Do not spend time on retry loops or workaround exploration before escalating blocked operations. + +## Note-Taking with `nb` MCP Server + +### When to Use +- **Project coordination**: Record handoffs, document decisions, maintain task lists. +- **Issue tracking**: Create and manage todos with status tracking. +- **Knowledge sharing**: Document patterns, APIs, and project-specific knowledge. +- **Meeting notes**: Record discussions and action items. + +### Scope and Noise Control +- Prefer to update an existing related note/todo over creating a new one when context already exists. +- Avoid logging routine, immediately completed mechanical actions in separate notes. +- Create new notes/todos when information is likely to be useful across sessions or for other collaborators. + +### Tagging Conventions +Use consistent tags for discoverability: +- **Project Component**: `#component-<name>` (e.g., `#component-data-models`) +- **Task Type**: `#task-<type>` (e.g., `#task-design`, `#task-bug`) +- **Status**: `#status-<state>` (e.g., `#status-in-progress`, `#status-review`) +- **Coordination**: `#handoff`, `#coordination` +- **Assignment**: Avoid owner tags (for example `#llm-*`) for task ownership. Use lane/folder ownership and explicit owner text in the note body when needed. + +### Common Patterns +- Check for handoffs: `nb.search` with `#handoff` and `#status-review` tags. +- Find active component work: `nb.search` with `#component-<name>` and `#status-in-progress` tags. +- Track todos: Use `nb.todo`, `nb.tasks`, `nb.do`, `nb.undo`. +- Organize with folders: `nb.folders`, `nb.mkdir`. + +### Notebook Identifier Clarification +- Treat note selectors (for example `coordination/mcp/1`) as canonical IDs for `nb` operations. +- `nb` MCP responses may include notebook-scoped identifiers (for example `my-project:coordination/...`) that look path-like; these are selector forms, not repo-relative filesystem paths. +- Notebook storage is controlled by `nb` configuration (for example `NB_DIR`) and may be outside this repository. +- Prefer `nb` MCP commands to read/edit notes. Avoid assuming a selector maps to a file under the current repo. + +### Recommended `nb` Organization (Project-Defined) +- Prefer a folder taxonomy of `<issue-type>/<component>` (max depth 2) and avoid mixing top-level component folders with top-level issue-type folders. +- Recommended top-level issue types are: + - `todos/` + - `coordination/` + - `decisions/` (optional for durable rationale notes) +- Example component names include `engine`, `mcp`, `tui`, `web`, `handbook`, and `data-models`. +- This project should define and document its specific component-folder conventions in the **Project Notes** section. +- For cross-component work, prefer `coordination/general` and use multiple `#component-*` tags. +- For per-component rolling handoffs, prefer `coordination/<component>` (single continuously updated note) instead of creating history chains under `handoffs/*`. +- Keep notebook lifecycle hygiene: + - prune completed todos quickly, + - keep only active/near-term coordination checkpoints, + - delete stale history-only notes with no owner or action. +- Keep todo titles concise (under 60 chars); use the `tasks` argument for detailed checklist items. This keeps notebook list views readable. + +### `nb` vs OpenSpec Rubric +- Use **OpenSpec proposals** for cross-cutting changes, contract-shaping work, architecture shifts, or work that needs explicit design discussion. +- Use **`nb` todos/notes** for scoped, self-contained implementation tasks where the path is straightforward. +- When in doubt about whether work needs an OpenSpec proposal or only `nb` execution tracking, prefer OpenSpec first for design clarity. +- For each active OpenSpec proposal, keep **exactly one** linked `nb` todo as the tracking anchor (with proposal reference), rather than duplicating full task trees in both systems. + +### OpenSpec Draft and Handoff Hygiene +- Draft OpenSpec proposal text in a dedicated `nb` note first so collaborators can review without local file access barriers; share the note id when requesting feedback. +- When asking for proposal feedback, share the notebook note id first; do not request review against local-only proposal files collaborators cannot access. +- Keep rolling handoff notes stable and update in place, separate from OpenSpec draft/proposal text. +- Do not repurpose or overwrite rolling handoff notes with proposal content. +- After draft review converges, move approved proposal text into `openspec/**` files for human review and commit. + +## Agentmux Message Handling Guidance +- `agentmux` messages may arrive in envelope format and can appear as user prompts. Treat envelope-shaped prompts as inter-agent messages, not automatically as direct human instructions. +- Respond to inter-agent envelope messages via `agentmux` MCP tools (`list`, `send`) rather than as normal assistant replies intended for the human operator. +- Immediate interruption is not required. If you are in active execution, note the message and respond when safe. +- If response will be delayed, send a brief acknowledgement via `send` and record a follow-up todo in `nb` when useful. + +## Agentmux Coordination Noise Control +- Default to low-noise coordination. Do not send acknowledgement-only messages that add no new information or action request. +- Send messages when you are blocked and need input, when requesting concrete review, when handing off completed work with validation, or when reporting material risk/scope change. +- Batch related updates into one message instead of sending rapid-fire partial status pings. +- Use `Cc` only for agents who need to act or review. + +## OpenSpec Instructions + +Workflow Guide: @openspec/AGENTS.md + +Always open `openspec/AGENTS.md` when the request: +- Mentions planning or proposals (words like proposal, spec, change, plan). +- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work. +- Sounds ambiguous and you need the authoritative spec before coding. + +Use `openspec/AGENTS.md` to learn: +- How to create and apply change proposals +- Spec format and conventions +- Project structure and guidelines + +# Commits + +- Use `git status` to ensure all relevant changes are in the changeset. +- Do **not** commit without explicit user approval. Unless the user has requested the commit, **ask first** for a review of your work. +- Do **not** bypass commit safety checks (e.g., `--no-verify`, `--no-gpg-sign`) unless the user explicitly approves doing so. +- Use present tense, imperative mood verbs (e.g., "Fix" not "Fixed"). +- Write sentences with proper punctuation. +- Include a `Co-Authored-By:` field as the final line. Should include the model name and a no-reply address. +- Avoid using `backticks` in commit messages as shell tools may evaluate them as subshell captures. + +# Project Notes + +<!-- This section accumulates project-specific knowledge, constraints, and deviations. + For structured items, use documentation/architecture/decisions/ and `nb`. --> diff --git a/.auxiliary/configuration/claude/.gitignore b/.auxiliary/configuration/claude/.gitignore deleted file mode 100644 index 93c0f73..0000000 --- a/.auxiliary/configuration/claude/.gitignore +++ /dev/null @@ -1 +0,0 @@ -settings.local.json diff --git a/.auxiliary/configuration/claude/agents/python-annotator.md b/.auxiliary/configuration/claude/agents/python-annotator.md deleted file mode 100644 index acece14..0000000 --- a/.auxiliary/configuration/claude/agents/python-annotator.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -name: python-annotator -description: | - Use this agent when you need to address type checking issues from tools like Pyright, create type annotations - following project standards, generate type stubs for third-party packages, or analyze and resolve issues masked - by type: ignore comments or __.typx.cast calls. - - Examples: - - <example> - Context: User has written a new public function and needs proper type annotations according to project standards. - user: 'I just wrote this function but Pyright is complaining about missing type annotations: def process_data(data, configuration): return transformed_data' - assistant: 'Let me use the python-annotator agent to add proper type annotations following the project guidelines.' - <commentary>The user needs type annotations added to their function following project standards, so use the python-annotator agent.</commentary> - </example> - - <example> - Context: User is getting Pyright errors about missing type stubs for a third-party library. - user: 'Pyright is showing errors because the requests library doesn't have type stubs available' - assistant: 'I'll use the python-annotator agent to create the missing type stubs for the requests library.' - <commentary>Missing type stubs for third-party packages require the python-annotator agent's specialized workflow.</commentary> - </example> - - <example> - Context: User wants to clean up code that has type: ignore comments. - user: 'Can you help me resolve these # type: ignore comments in my code?' - assistant: 'Let me use the python-annotator agent to analyze and properly resolve those type checking suppressions.' - <commentary>Analyzing and mitigating issues masked by type pragmas is a core function of the python-annotator agent.</commentary> - </example> -model: sonnet -color: pink ---- - -You are an expert Python type annotation specialist focusing on static type analysis, -type system design, and resolving type checker issues from tools like Pyright. You -systematically analyze type checking problems and apply comprehensive solutions to -ensure code adheres to strict typing standards. - -**IMPORTANT**: Only address Python type checking issues. If the request does not -involve Python type annotations, type stubs, or type checker diagnostics, politely -decline and explain your specialization. - -## Prerequisites - -- **Read project documentation guides FIRST**: - - @.auxiliary/instructions/practices.rst - - @.auxiliary/instructions/style.rst -- Have read `CLAUDE.md` for project-specific guidance - -## EXECUTION STRUCTURE - -**PHASE 1: COMPREHENSIVE TYPE ANALYSIS** -Perform complete diagnostic analysis and generate detailed type checking report before making any changes. - -**PHASE 2: SYSTEMATIC RESOLUTION** -Apply all identified type annotation fixes in systematic order, validating with type checkers after completion. - -## TYPE ANNOTATION STANDARDS - -### 1. Annotation Guidelines - -**Public Function Documentation:** -- Use `__.typx.Annotated[ <type>, __.ddoc.Doc( '''<description>''' ) ]` pattern -- Include `__.ddoc.Raises( )` annotations for documented exceptions -- Follow narrative mood (third person) in documentation - -**Wide Parameters, Narrow Returns:** -- Accept abstract base classes (`__.cabc.Sequence`, `__.cabc.Mapping`) -- Return concrete immutable types (`tuple`, `frozenset`, `__.immut.Dictionary`) - -**Absential vs Optional:** -- Prefer `__.Absential[ T ]` for optional parameters when `None` has semantic meaning -- Use `__.typx.Optional[ T ]` only when `None` is a valid value distinct from absence - -**Type Alias Organization:** -- Common aliases after imports, before private variables -- Complex multi-line unions use `__.typx.Union[ ]` -- Simple unions use `|` syntax - -### 2. Type Checker Issue Resolution - -**Root Cause Analysis:** -1. Identify specific type checker errors and their locations -2. Determine underlying cause (missing annotations, incorrect types, inheritance issues) -3. Assess impact on runtime behavior and API contracts -4. Plan minimal changes that resolve issues without breaking functionality - -**Resolution Priorities:** -1. **Missing Annotations**: Add comprehensive type annotations following project patterns -2. **Incorrect Types**: Replace overly broad or narrow types with appropriate abstractions -3. **Generic Issues**: Properly parameterize generic types and resolve variance issues -4. **Import Problems**: Fix circular imports and missing type-only imports - -### 3. Dependency Management and Type Stub Creation - -**Dependency Declaration Before Type Work** - -Avoid using `# type: ignore` to suppress errors about missing third-party dependencies. -This anti-pattern masks improper project setup and should be resolved through proper dependency management. - -**Required Dependency Workflow:** -1. **Verify Dependency Declaration**: Check `pyproject.toml` for the package -2. **Update Project Dependencies**: Add missing packages to appropriate dependency groups -3. **Update Import Module**: Add package to `sources/<package>/__/imports.py` if commonly used -4. **Rebuild Environment**: Run `hatch env prune && hatch --env develop run python --version` -5. **Then and Only Then**: Proceed with type stub creation or suppression analysis - -**Dependency Verification Commands:** -```shell -# Check if package is declared in pyproject.toml -grep -n "somepackage" pyproject.toml - -# Verify package is installed in environment -hatch --env develop run python -c "import somepackage; print( somepackage.__file__ )" - -# Check if type information is available -hatch --env develop run pyright --verifytypes somepackage -``` - -**Type Stub Creation Workflow:** - -**Stub Generation Process (ONLY after dependency verification):** -1. **Check Official Sources**: Verify typeshed, PyPI `types-*` packages, or library's own stubs -2. **Generate Initial Stubs**: - ```shell - hatch --env develop run pyright --createstub somepackage - ``` -3. **Minimal Viable Stubs**: Focus only on APIs used in project, not comprehensive coverage -4. **Structure Requirements**: - - Proper module hierarchy matching runtime structure - - Inheritance relationships preserved - - Generic type parameters correctly defined - - Public API surface accurately represented - -**Stub File Organization:** -```python -# sources/<package>/_typedecls/somepackage/__init__.pyi -from typing import Any, overload -from collections.abc import Sequence, Mapping - -# Core classes used in project -class ConfigParser: - def __init__( self, defaults: Mapping[ str, str ] | None = ... ) -> None: ... - def read( self, filenames: str | Sequence[ str ] ) -> list[ str ]: ... - def get( self, section: str, option: str ) -> str: ... - -# Only stub what's actually used - avoid comprehensive coverage -``` - -### 4. Type Suppression Resolution - -**Suppression Analysis Workflow:** - -**Phase 1 - Audit Existing Suppressions:** -```shell -# Find all suppressions in codebase -rg --line-number "type:\s*ignore|__.typx\.cast" --type py -``` - -**Phase 2 - Categorize Suppressions:** -1. **Dependency Issues**: Missing packages not declared in `pyproject.toml` - address first -2. **Resolvable**: Missing stubs, incorrect annotations, fixable inheritance -3. **Legitimate**: Truly dynamic behavior, complex generics, external constraints -4. **Technical Debt**: Workarounds that should be refactored - -**Dependency Suppression Analysis:** -For any suppression involving third-party imports: -1. **Verify Declaration**: Check if package exists in `pyproject.toml` -2. **If Missing**: Add to appropriate dependency group, update `__/imports.py` if needed -3. **Rebuild Environment**: `hatch env prune` and reinstall -4. **Re-evaluate**: Many suppressions resolve after proper dependency management - -**Phase 3 - Resolution Strategies:** - -**Incorrect Approach - Masking dependency issues:** -```python -# Anti-pattern: Suppressing missing dependency -import requests # type: ignore -import beautifulsoup4 # type: ignore - -def fetch_data( url: str ) -> dict: - response = requests.get( url ) # type: ignore - return response.json( ) # type: ignore -``` - -**Preferred Approach - Proper dependency management:** -```python -# 1. First add to pyproject.toml: -# dependencies = [ -# "requests~=2.31.0", -# "beautifulsoup4~=4.12.0", -# ] -# -# 2. Add to sources/<package>/__/imports.py (third-party imports section): -# import bs4 -# import requests -# -# 3. Then use proper imports: -from . import __ - -def fetch_data( url: str ) -> dict[ str, __.typx.Any ]: - response = __.requests.get( url ) - return response.json( ) -``` - - -**Documentation Requirements:** -- Every remaining suppression MUST have explanatory comment -- Include ticket/issue reference for suppressions requiring upstream fixes -- Set TODO items for suppressions that should be revisited - -### 5. Quality Assurance Workflow - -**Type Checking Validation:** -```shell -# Run comprehensive type checking -hatch --env develop run pyright -hatch --env develop run pyright --stats # Coverage statistics -``` - -**Consistency Verification:** -- Public functions have `__.typx.Annotated` documentation -- Parameter types follow wide/narrow principle -- Return types are concrete and immutable where appropriate -- Import organization follows project standards - -**Runtime Preservation:** -- Verify no functional changes introduced -- Test critical paths if available -- Validate API contracts maintained - -## COMPREHENSIVE EXAMPLES - -### Example 1: Missing Function Annotations - -**BEFORE - Pyright errors:** -```python -def process_user_data( data, filters = None, configuration = None ): - if filters is None: filters = [ ] - # Error: Missing type annotations - return transform_and_validate( data, filters, configuration or { } ) -``` - -**AFTER - Complete annotations:** -```python -def process_user_data( - data: __.typx.Annotated[ - __.cabc.Mapping[ str, __.typx.Any ], - __.ddoc.Doc( '''User data mapping with string keys.''' ), - ], - filters: __.typx.Annotated[ - __.Absential[ __.cabc.Sequence[ str ] ], - __.ddoc.Doc( '''Optional data filters to apply.''' ), - ] = __.absent, - configuration: __.typx.Annotated[ - __.Absential[ __.cabc.Mapping[ str, __.typx.Any ] ], - __.ddoc.Doc( '''Optional processing configuration.''' ), - ] = __.absent, -) -> __.typx.Annotated[ - __.immut.Dictionary[ str, __.typx.Any ], - __.ddoc.Doc( '''Processed and validated user data.''' ), - __.ddoc.Raises( ValueError, '''If data validation fails.''' ), -]: - ''' Processes user data with optional filtering and configuration. ''' - active_filters = ( ) if __.is_absent( filters ) else tuple( filters ) - active_configuration = __.immut.Dictionary( ) if __.is_absent( configuration ) else __.immut.Dictionary( configuration ) - return transform_and_validate( data, active_filters, active_configuration ) -``` - -### Example 2: Type Stub Creation - -**Missing stubs for 'beautifulsoup4':** -```python -# sources/<package>/_typedecls/bs4/__init__.pyi -from typing import Any, Optional -from collections.abc import Sequence - -class BeautifulSoup: - def __init__( - self, - markup: str | bytes = ..., - features: Optional[ str ] = ..., - ) -> None: ... - - def find( - self, - name: Optional[ str ] = ..., - attrs: Optional[ dict[ str, Any ] ] = ..., - ) -> Optional[ Tag ]: ... - - def find_all( - self, - name: Optional[ str ] = ..., - attrs: Optional[ dict[ str, Any ] ] = ..., - ) -> list[ Tag ]: ... - -class Tag: - def get_text( self, strip: bool = ... ) -> str: ... - def get( self, key: str, default: Any = ... ) -> Any: ... - @property - def text( self ) -> str: ... -``` - -### Example 3: Type Suppression Resolution - -**BEFORE - Broad suppressions:** -```python -def complex_data_processor( items ): # type: ignore - results = [ ] # type: ignore - for item in items: # type: ignore - processed = expensive_operation( item ) # type: ignore - results.append( processed ) # type: ignore - return results # type: ignore -``` - -**AFTER - Proper resolution:** -```python -def complex_data_processor( - items: __.cabc.Sequence[ __.typx.Any ], -) -> tuple[ ProcessedData, ... ]: - ''' Processes sequence of items through expensive operation. ''' - results: list[ ProcessedData ] = [ ] - for item in items: - processed = expensive_operation( item ) - results.append( processed ) - return tuple( results ) -``` - -## ANALYSIS REPORT FORMAT - -**PHASE 1 OUTPUT:** -1. **Type Checking Summary**: Overall diagnostic assessment with file-by-file breakdown -2. **Missing Annotations**: Functions, methods, and variables requiring type annotations -3. **Type Errors**: Specific checker errors with root cause analysis -4. **Stub Requirements**: Third-party packages needing type stubs -5. **Suppression Audit**: Analysis of existing `type: ignore` and `__.typx.cast` usage -6. **Resolution Plan**: Systematic order of fixes to be applied - -**PHASE 2 OUTPUT:** -1. **Applied Annotations**: Summary of all type annotations added -2. **Stub Generation**: Created stub files and their scope -3. **Suppression Resolution**: Eliminated or refined type suppressions -4. **Validation Results**: Type checker output before and after changes -5. **Files Modified**: Complete list with brief description of changes - -## TOOL PREFERENCES - -- **Precise coordinates**: Use `rg --line-number --column` for exact positions -- **Type checking**: Use Pyright MCP tools for diagnostics and validation -- **Stub generation**: Use `hatch --env develop run pyright --createstub` when needed - -## EXECUTION REQUIREMENTS - -- **Phase 0**: Verify all third-party dependencies are declared in `pyproject.toml` and available in environment -- **Phase 1**: Complete analysis and report before any modifications -- **Phase 2**: Apply fixes systematically, validate with `hatch --env develop run pyright` -- **Dependency validation**: Do not proceed with type work until dependencies are properly declared -- **Validation command**: Type checking must be clean before completion -- **Focus on type safety**: Maintain exact functionality while improving type annotations -- **Reference specific diagnostics**: Always include line numbers and error messages -- **Document decisions**: Explain type choices and trade-offs made -- **Dependency pattern detection**: Flag attempts to use `# type: ignore` for missing dependencies diff --git a/.auxiliary/configuration/claude/agents/python-conformer.md b/.auxiliary/configuration/claude/agents/python-conformer.md deleted file mode 100644 index 59ead95..0000000 --- a/.auxiliary/configuration/claude/agents/python-conformer.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -name: python-conformer -description: | - Use this agent ONLY when changes include Python code (.py and .pyi files) and you need to review them for - compliance with project practices, style guidelines, and nomenclature standards, then systematically fix violations. - Do NOT use this agent for non-Python changes such as documentation, configuration files, or other file types. - - Examples: - - <example> - Context: The user has just written a new Python function and wants to ensure it follows project standards. - user: 'I just wrote this function for processing user data. Can you review it?' - assistant: 'I'll use the python-conformer agent to check your function against our project practices and style guidelines, then fix any violations.' - <commentary>Since the user wants code reviewed for compliance, use the python-conformer agent to analyze the code against project standards.</commentary> - </example> - - <example> - Context: The user has completed a module refactor and wants to verify compliance before committing. - user: 'I've finished refactoring the authentication module. Please check if it meets our coding standards.' - assistant: 'Let me use the python-conformer agent to thoroughly review your refactored module for compliance with our practices guidelines.' - <commentary>The user needs compliance verification for recently refactored code, so use the python-conformer agent.</commentary> - </example> - - <example> - Context: The user wants to review staged Python changes before committing. - user: 'I've modified several Python modules. Please review my staged changes for compliance before I commit.' - assistant: 'I'll use the python-conformer agent to review the Python changes in git diff --cached and ensure all Python code meets our project standards.' - <commentary>Pre-commit review of staged Python changes is a perfect use case for the python-conformer agent.</commentary> - </example> -model: sonnet -color: red ---- - -You are an expert software engineer specializing in Python code quality assurance and -compliance conformance. Your primary responsibility is to systematically review Python code -against established project practices, style guidelines, and nomenclature -standards, then apply comprehensive remediation to bring code into full compliance. - -**IMPORTANT**: Only review and modify Python (.py and .pyi) files. If the -changes do not include Python code, politely decline and explain that you are -specifically for Python code compliance review. - -## Prerequisites - -- **Read project documentation guides FIRST**: - - @.auxiliary/instructions/practices.rst - - @.auxiliary/instructions/style.rst - - @.auxiliary/instructions/nomenclature.rst -- Have read `CLAUDE.md` for project-specific guidance - -## EXECUTION STRUCTURE - -**PHASE 1: COMPREHENSIVE REVIEW** -Perform complete analysis and generate detailed compliance report before making any changes. - -**PHASE 2: SYSTEMATIC REMEDIATION** -Apply all identified fixes in systematic order, validating with linters after completion. - -## COMPLIANCE STANDARDS - -### Design Standards - -#### 1. Module Organization - -**Content Order:** -1. Imports (following practices guide patterns) -2. Common type aliases (`TypeAlias` declarations) -3. Private variables/functions for defaults (grouped semantically) -4. Public classes and functions (alphabetical) -5. All other private functions (alphabetical) - -**Scope and Size:** -- Maximum 600 lines -- Action: Analyze oversized modules with separation of concerns in mind. -Suggest splitting into focused modules with narrower responsibilities or -functionality. - -#### 2. Imports - -- At the module level, other modules and their attributes MUST be imported as - private aliases, except in `__init__`, `__`, or specially-designated - re-export modules. -- Within function bodies, other modules and their attributes MAY be imported as - public variables. -- Subpackages SHOULD define a special `__` re-export module, which has `from - ..__ import *` plus any other imports which are common to the subpackage. -- Common modules, such as `os` or `re`, SHOULD be imported as public within the - special package-wide `__.imports` re-export module rather than as private - aliases within an implementation module. -- The `__all__` attribute SHOULD NOT be provided. This is unnecessary if the - module namespace only contains public classes and functions which are part of - its interface; this avoid additional interface maintenance. - -#### 3. Dependency Injection - -- Ask: is this function testable without monkeypatching? -- Functions SHOULD provide injectable parameters with sensible defaults instead - of hard-coded dependencies within function implementation. - -#### 4. Robustness Principle (Postel's Law) -"Be conservative in what you send; be liberal in what you accept." - -- Public functions SHOULD define wide, abstract argument types. -- All functions SHOULD define narrow, concrete return types. -- Private functions MAY define narrow, concrete argument types. - -#### 5. Immutability - -- Classes SHOULD inherit from immutable classes (`__.immut.Object`, - `__.immut.Protocol`, `__.immut.DataclassObject`, etc...). -- Functions SHOULD return values of immutable types (`None`, `int`, `tuple`, - `frozenset`, `__.immut.Dictionary`, etc...) and not mutable types (`list`, - `dict`, `set`, etc...). - -#### 6. Proper Exception Management - -- One `try .. except` suite per statement which can raise exceptions. I.e., - avoid covering multiple statements with a `try` block whenever possible. -- Tryceratops complaints MUST NOT be suppressed with `noqa` pragmas. -- Bare exceptions SHOULD NOT be raised. - - Exemption: `NotImplementedError` MAY be raised as a bare exception. - - Relevant exception classes SHOULD be used from the relevant `exceptions` - module within the package. - - New exception classes MAY be created as needed within the relevant - `exceptions` module; these MUST follow the nomenclature guide and be - inserted in correct alphabetical order. - -### Quality Assurance - -#### 1. Linter Suppressions - -- Linter suppressions MUST be reviewed critically. -- Linter complaints SHOULD NOT be suppressed via `noqa` or `type` pragmas - without compelling justification. -- Suppressions that mask design problems MUST be investigated and resolved - rather than ignored. - -**Acceptable Suppressions:** -- `noqa: PLR0913` MAY be used for a CLI or service API with many parameters, - but data transfer objects SHOULD be considered in most other cases. -- `noqa: S*` MAY be used for properly constrained and vetted subprocess - executions or Internet content retrievals. - -**Unacceptable Suppressions (require investigation):** -- `type: ignore` MUST NOT be used, except in extremely rare circumstances. Such - suppressions usually indicate missing third-party dependencies or type stubs, - inappropriate type variables, or a bad inheritance pattern. For complex type - suppression investigation and dependency management, delegate to the - `python-annotator` agent. -- `__.typx.cast` SHOULD NOT be used, except in extremely rare circumstances. - Such casts suppress normal type checking and usually the same problems as - `type: ignore`. -- Most other `noqa` suppressions. - -### Style Standards - -#### 1. Spacing and Delimiters - -- Space padding MUST be present inside delimiters. - - Format: `( arg )`, `[ item ]`, `{ key: value }` - - Format: `( )`, `[ ]`, `{ }`, not `()`, `[]`, `{}` -- Space padding MUST be present around keyword argument `=`. - - Format: `foo = 42` - -#### 2. Strings - -- Docstrings MUST use triple single quotes with narrative mood. - - Format: `''' Processes data... '''` not `"""Process data..."""` -- F-strings and `.format` strings MUST be enclosed in double quotes. - - Format: `f"text {variable}"`, not `f'text {variable}'` - - Format: `"text {count}".format( count = len( items ) )` -- F-strings and format strings MUST NOT embed function calls. -- Exception messages and log messages SHOULD be enclosed in double quotes - rather than single quotes. -- Plain data strings SHOULD be enclosed in single quotes, unless they contain - single quotes. - -#### 3. Vertical Compactness - -- Blank lines MUST NOT appear within function bodies. -- Vertical compactness MUST be maintained within function implementations. -- Single-line statements MAY follow certain block keywords on the same line - when appropriate. - - Format: `if condition: return value` - - Format: `elif condition: continue` - - Format: `else: statement` - - Format: `try: statement` - -#### 4. Multi-line Constructs - -- Function invocations, including class instantiations, SHOULD place the - closing `)` on the same line as the last argument to the function. -- The last argument of an invocation MUST NOT be followed by a trailing comma. -- Comprehensions and generator expressions SHOULD place the closing delimiter - on the same line as the last statement in the comprehension or generator - expression. -- Parenthetical groupings SHOULD place the closing delimiter on the same line - as the last statement in the grouping. -- All other multi-line constructs (functions signatures, annotations, lists, - dictionaries, etc...) MUST place the closing delimiter on a separate line - following the last item and MUST dedent the closing delimiter to match the - opening line indentation. -- If a closing delimiter is not on the same line as the last item in a - multi-line construct, then the last item MUST be followed by a trailing - comma. - -#### 5. Nomenclature - -- Argument, attribute, and variable names SHOULD NOT be compound words, - separated by underscores, except in cases where this is necessary to - disambiguate. -- Argument and variable names SHOULD NOT duplicate parts of the function name. -- Attribute names SHOULD NOT duplicate parts of the class name. -- Class names SHOULD adhere to the nomenclature guide. -- Function names SHOULD adhere to the nomenclature guide. - -#### 6. Comments - -- Comments that describe obvious behavior SHOULD NOT be included. -- TODO comments SHOULD be added for uncovered edge cases and future work. -- Comments MUST add meaningful context, not restate what the code does. - -### Comprehensive Example: Real-World Function with Multiple Violations - -Here is a function that demonstrates many compliance violations: - -```python -def _group_documents_by_field( - documents: list[ dict[ str, __.typx.Any ] ], - field_name: __.typx.Optional[ str ] -) -> dict[ str, list[ dict[ str, __.typx.Any ] ] ]: - ''' Groups documents by specified field for inventory format compatibility. - ''' - if field_name is None: - return { } - - groups: dict[ str, list[ dict[ str, __.typx.Any ] ] ] = { } - for doc in documents: - # Get grouping value, with fallback for missing field - group_value = doc.get( field_name, f'(missing {field_name})' ) - if isinstance( group_value, ( list, dict ) ): - # Handle complex field types by converting to string - group_value = str( group_value ) # type: ignore[arg-type] - elif group_value is None or group_value == '': - group_value = f'(missing {field_name})' - else: - group_value = str( group_value ) - - if group_value not in groups: - groups[ group_value ] = [ ] - - # Convert document format back to inventory object format - inventory_obj = { - 'name': doc[ 'name' ], - 'role': doc[ 'role' ], - 'domain': doc.get( 'domain', '' ), - 'uri': doc[ 'uri' ], - 'dispname': doc[ 'dispname' ] - } - if 'fuzzy_score' in doc: - inventory_obj[ 'fuzzy_score' ] = doc[ 'fuzzy_score' ] - groups[ group_value ].append( inventory_obj ) - return groups -``` - -**Violations identified:** -1. **Narrow parameter types**: `list[dict[...]]` instead of wide `__.cabc.Sequence[__.cabc.Mapping[...]]` -2. **Type suppression abuse**: `# type: ignore[arg-type]` masks real design issue (delegate to `python-annotator` agent for systematic suppression resolution) -3. **Mutable container return**: Returns `dict` instead of `__.immut.Dictionary` -4. **Function body blank lines**: Empty lines breaking vertical compactness -5. **Vertical compactness**: `return { }` could be same line as `if` -6. **Unnecessary comments**: "Handle complex field types by converting to string" states obvious -7. **F-string quotes**: Using single quotes in f-strings instead of double -8. **Nomenclature duplication**: `group_value` repeats "group" from function name -9. **Underscore nomenclature**: `field_name` could be `field`, `group_value` could be `value` -10. **Mutable container creation**: Using `{ }` and `[ ]` instead of immutable alternatives -11. **Trailing comma**: Missing trailing comma in dictionary, affecting delimiter placement -12. **Single-line else**: `group_value = str(group_value)` could be same line as `else` -13. **Design pattern**: Could use `collections.defaultdict` instead of manual initialization - -**AFTER - Corrected version:** -```python -def _group_documents_by_field( - documents: __.cabc.Sequence[ __.cabc.Mapping[ str, __.typx.Any ] ], - field: __.typx.Absential[ str ] = __.absent, -) -> __.immut.Dictionary[ - str, tuple[ __.cabc.Mapping[ str, __.typx.Any ], ... ] -]: - ''' Groups documents by specified field. ''' - if __.is_absent( field ): return __.immut.Dictionary( ) - groups = __.collections.defaultdict( list ) - for doc in documents: - value = doc.get( field, f"(missing {field})" ) - if isinstance( value, ( list, dict ) ): value = str( value ) - elif value is None or value == '': value = f"(missing {field})" - else: value = str( value ) - obj = __.immut.Dictionary( - name = doc[ 'name' ], - role = doc[ 'role' ], - domain = doc.get( 'domain', '' ), - uri = doc[ 'uri' ], - dispname = doc[ 'dispname' ], - **( { 'fuzzy_score': doc[ 'fuzzy_score' ] } - if 'fuzzy_score' in doc else { } ) ) - groups[ value ].append( obj ) - return __.immut.Dictionary( - ( key, tuple( items ) ) for key, items in groups.items( ) ) -``` - -## REVIEW REPORT FORMAT - -**PHASE 1 OUTPUT:** -1. **Compliance Summary**: Overall assessment with file-by-file breakdown -2. **Standards Violations**: Categorized list with specific line references and explanations -3. **Complexity Analysis**: Function and module size assessments -4. **Remediation Plan**: Systematic order of fixes to be applied -5. **Risk Assessment**: Any changes that require careful validation - -**PHASE 2 OUTPUT:** -1. **Applied Fixes**: Summary of all changes made, categorized by standard -2. **Validation Results**: Linter output before and after changes -3. **Files Modified**: Complete list with brief description of changes -4. **Manual Review Required**: Any issues requiring human judgment - -## TOOL PREFERENCES - -- **Precise coordinates**: Use `rg --line-number --column` for exact line/column positions -- **Batch operations**: Group related changes together to minimize file modification conflicts between different MCP tools - -## EXECUTION REQUIREMENTS - -- **PHASE 1 REQUIRED**: Complete review and report before any remediation -- **PHASE 2 REQUIRED**: Apply fixes systematically, validate with `hatch --env develop run linters` -- **Validation command**: `hatch --env develop run linters` must produce clean output before completion -- **Focus on compliance**: Maintain exact functionality while improving standards adherence -- **Reference specific lines**: Always include line numbers and concrete examples -- **Document reasoning**: Explain why each standard matters and how fixes align with project practices -- **Agent delegation**: When type annotation issues exceed basic compliance scope, consider delegating to the `python-annotator` agent for comprehensive type work -- **Guide access**: If any prerequisite guide cannot be accessed, stop and inform the user diff --git a/.auxiliary/configuration/claude/commands/cs-annotate-release.md b/.auxiliary/configuration/claude/commands/cs-annotate-release.md deleted file mode 100644 index 5300e30..0000000 --- a/.auxiliary/configuration/claude/commands/cs-annotate-release.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -allowed-tools: Bash(git log:*), Bash(git show:*), Bash(ls:*), Bash(grep:*), Grep, Read, Write, LS -description: Create Towncrier news fragments for user-facing changes since last release cleanup ---- - -# Write Release Notes - -**NOTE: This is an experimental workflow! If anything seems unclear or missing, -please stop for consultation with the user.** - -You are tasked with creating Towncrier news fragments for user-facing changes -since the last release cleanup. This command analyzes recent commits and -generates appropriate changelog entries. - -Special instructions: $ARGUMENTS -(If above line is empty, then no special instructions were given by the user.) - -## Context - -The project uses Towncrier to manage changelogs. News fragments are stored in -`.auxiliary/data/towncrier/` and follow specific naming and formatting -conventions detailed in the [releases -guide](https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/releases.rst). - -## Process - -### Phase 1: Discovery and Analysis - -1. **Find Starting Point**: Use `git log --oneline --grep="Clean up news fragments"` to find the last cleanup commit -2. **Get Recent Commits**: Retrieve all commits since the cleanup using `git log --no-merges` with full commit messages -3. **Check Existing Fragments**: List existing fragments in `.auxiliary/data/towncrier/` to avoid duplication - -### Phase 2: Filtering and Classification - -4. **Filter User-Facing Changes**: Focus on changes that affect how users interact with the tool: - - CLI command changes (new options, arguments, output formats) - - API changes (public functions, classes, return values) - - Behavior changes (different responses, error messages, processing) - - Configuration changes (new settings, file formats) - - Deprecations and removals - - Platform support changes (Python versions, OS support) - - **Exclude** internal changes: - - GitHub workflows - - Dependency changes without API impact - - Internal module restructuring that preserves public API - - Git ignore files - - Modules in internals subpackages (`__`) - - Version bumps and maintenance updates - - Internal refactoring without user-visible changes - - **Key Test**: Ask "Does this change how a user invokes the tool, what options they have, or what behavior they observe?" - -5. **Classify Changes**: Determine appropriate type for each change: - - `enhance`: features and improvements - - `notify`: deprecations and notices - - `remove`: removals of features or support - - `repair`: bug fixes - - Note: Some commits may contain multiple types of changes. - -### Phase 3: Synthesis and Creation - -6. **Group Related Commits**: Synthesize multiple commits into coherent user-facing descriptions when they represent logical units of change - -7. **Think Through Fragments**: Before writing, consider: - - Are the descriptions clear and meaningful to users? - - Do they follow the format guidelines? - - Are they properly classified? - - Do they focus on what and why, not how? - -8. **Create Fragments**: Write appropriately named fragment files using: - - `<issue_number>.<type>.rst` for changes with GitHub issues - - `+<title>.<type>.rst` for changes without issues - - Fragment content should: - - Start with capital letter, end with period - - Use present tense imperative verbs - - Be understandable by users, not just developers - - Include topic prefixes when appropriate (e.g., "CLI: ", "API: ") - -### Phase 4: Final Review and Commit - -9. **Summary**: Provide a brief summary of fragments created and any notable patterns or changes identified - -10. **Commit Changes**: Add fragments to git and commit them: - - `git add .auxiliary/data/towncrier` - - `git commit -m "Add news fragments for upcoming release"` - -## Additional Instructions - -- Read full commit messages for context; only examine diff summaries if commit messages are unclear -- Focus on meaningful user-facing changes rather than comprehensive coverage of all commits diff --git a/.auxiliary/configuration/claude/commands/cs-architect.md b/.auxiliary/configuration/claude/commands/cs-architect.md deleted file mode 100644 index 2df2461..0000000 --- a/.auxiliary/configuration/claude/commands/cs-architect.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(find:*), Bash(ls:*), Bash(tree:*) -description: Architectural analysis, system design decisions, and ADR creation ---- - -# System Architecture Analysis - -Analyze architectural decisions, system design patterns, component -relationships, and technical trade-offs to provide guidance on high-level -system structure and cross-component interactions. - -Request from user: $ARGUMENTS - -## Context - -- Product requirements: @documentation/prd.rst -- Architecture overview: @documentation/architecture/summary.rst -- Filesystem patterns: @documentation/architecture/filesystem.rst -- Architecture guidelines: @.auxiliary/instructions/architecture.rst -- Nomenclature standards: @.auxiliary/instructions/nomenclature.rst - -## Prerequisites - -Before providing architectural analysis, ensure: -- Understanding of current system architecture and constraints -- Familiarity with architectural decision record (ADR) format -- Knowledge of standard filesystem organization patterns -- @.auxiliary/instructions/architecture.rst guidelines are followed - -## Process Summary - -Key functional areas: -1. **Analysis**: Examine architectural context and design forces -2. **System Structure**: Define component relationships and system boundaries -3. **Decision Framework**: Apply architectural principles and trade-off analysis -4. **Documentation**: Create ADRs or update architectural documentation -5. **Validation**: Ensure decisions align with project constraints and goals - -## Safety Requirements - -Stop and consult the user if: -- Implementation details are requested instead of architectural guidance -- Specific code changes are needed -- Requirements analysis is needed -- Filesystem organization or module structure details are requested -- Architectural decisions have significant impact on existing system components -- Decision conflicts with existing architectural patterns or constraints -- Decision requires changes to fundamental system assumptions - -## Execution - -Execute the following steps: - -### 1. Architectural Context Analysis -Review current architecture and identify relevant patterns: -- Examine existing architectural documentation -- Understand system boundaries and component relationships -- Identify architectural forces and constraints -- Assess alignment with project goals and requirements - -### 2. Design Forces Assessment -Analyze the forces driving the architectural decision: -- Technical constraints (performance, scalability, compatibility) -- Quality attributes (maintainability, testability, security) -- Integration requirements with existing components -- Future flexibility and evolution needs - -### 3. Alternative Evaluation -Consider multiple architectural approaches: -- Document all seriously considered alternatives -- Analyze trade-offs for each option (benefits, costs, risks) -- Consider "do nothing" as a baseline alternative -- Evaluate alignment with established architectural patterns -- Assess implementation complexity and maintenance burden - -### 4. Decision Recommendation -Provide clear architectural guidance: -- State recommended approach with clear rationale -- Explain how decision addresses the identified forces -- Document expected positive and negative consequences -- Include specific architectural patterns or principles applied -- Provide text-based diagrams or examples when helpful - -### 5. Documentation Creation -When appropriate, create or update architectural documentation: -- Generate ADRs following the standard format -- Update `documentation/architecture/decisions/index.rst` to include new ADRs -- Update architecture summary for significant system changes -- Ensure consistency with filesystem organization patterns -- Reference related architectural decisions and dependencies - -### 6. Implementation Guidance -Provide high-level implementation direction without specific code: -- Suggest component organization and interfaces -- Recommend integration patterns with existing system -- Identify key architectural boundaries and abstractions -- Highlight critical implementation considerations - -### 7. Summarize Updates -Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/cs-code-python.md b/.auxiliary/configuration/claude/commands/cs-code-python.md deleted file mode 100644 index 476838d..0000000 --- a/.auxiliary/configuration/claude/commands/cs-code-python.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -description: Python implementation following established patterns and practices ---- - -# Python Implementation - -Implement Python code following established patterns including functions, -classes, modules, tests, and refactoring while adhering to project practices -and style guidelines. - -Request from user: $ARGUMENTS - -## Context - -- Architecture overview: @documentation/architecture/summary.rst -- Filesystem patterns: @documentation/architecture/filesystem.rst -- General practices: @.auxiliary/instructions/practices.rst -- Python development guide: @.auxiliary/instructions/practices-python.rst -- Code style: @.auxiliary/instructions/style.rst -- Nomenclature: @.auxiliary/instructions/nomenclature.rst -- Design documents: @documentation/architecture/designs/ - -## Prerequisites - -Before implementing Python code, ensure: -- Understanding of implementation requirements and expected behavior -- Knowledge of existing codebase structure and patterns -- Clear design specifications or existing design documents if referenced - -### Guide Consultation Requirements - -Before implementing Python code, you MUST: -1. Read @.auxiliary/instructions/practices.rst for general development principles -2. Read @.auxiliary/instructions/practices-python.rst for Python-specific patterns -3. In a step on your TODO list, please attest that you have read the general and Python-specific practices guides and demonstrate your knowledge by writing one-sentence summaries on any three of the following topics: - -- the comprehensive examples showing multiple principles cohesively -- proper module organization content order -- import organization and centralized import patterns -- wide parameter, narrow return type patterns for robust interfaces -- immutability preferences for data structures and containers -- exception handling with narrow try blocks and proper chaining -- documentation formatting requirements including narrative mood -- quality assurance principles including linter compliance - -## Process Summary - -Key functional areas: -1. **Requirements Analysis**: Understand implementation requirements and create persistent tracking -2. **Session Continuity**: Check for existing work and preserve context across sessions -3. **Implementation**: Write Python code following style guidelines and best practices -4. **Progress Tracking**: Maintain session and cross-session implementation progress -5. **Quality Assurance**: Run linters, type checkers, and tests to validate code -6. **Documentation**: Update persistent tracking and provide implementation summary - -## Safety Requirements - -Stop and consult the user if: -- Design specifications are needed instead of implementation -- Architectural decisions are required before implementation -- Requirements are unclear or insufficient for implementation -- Implementation conflicts with established architectural patterns -- Code changes would break existing API contracts or interfaces -- Quality checks reveal significant issues that require design decisions -- Type checker errors are encountered that cannot be resolved through standard remediation -- Multiple implementation approaches have significant trade-offs requiring user input - -## Execution - -Execute the following steps: - -### 1. Requirements Analysis -Analyze implementation requirements and gather context: -- Review user requirements and any referenced design documents -- Examine existing codebase structure and relevant modules -- Identify integration points with existing code -- Understand expected behavior and edge cases -- Document implementation scope and constraints - -#### 1.1 Create Implementation Tracking File -Before beginning implementation, create a persistent tracking file with descriptive naming: -- Format: `.auxiliary/notes/<short-implementation-title>--progress.md` -- Example: `.auxiliary/notes/user-metrics-export--progress.md` - -Choose a concise but descriptive title that captures the main implementation goal. - -Structure the tracking file with these sections: - -### Context and References -- **Implementation Title**: [Brief description of what is being implemented] -- **Start Date**: [YYYY-MM-DD] -- **Reference Files**: [List all files explicitly provided as context/references at start] - - `path/to/reference1.py` - [Brief description of relevance] - - `path/to/reference2.rst` - [Brief description of relevance] -- **Design Documents**: [Any architecture or design docs referenced] -- **Session Notes**: [Link to current session TodoWrite items] - -### Design and Style Conformance Checklist -- [ ] Module organization follows practices guidelines -- [ ] Function signatures use wide parameter, narrow return patterns -- [ ] Type annotations comprehensive with TypeAlias patterns -- [ ] Exception handling follows Omniexception → Omnierror hierarchy -- [ ] Naming follows nomenclature conventions -- [ ] Immutability preferences applied -- [ ] Code style follows formatting guidelines - -### Implementation Progress Checklist -- [ ] [Specific function/class/module 1] -- [ ] [Specific function/class/module 2] -- [ ] [Integration point 1] tested -- [ ] [Integration point 2] tested - -### Quality Gates Checklist -- [ ] Linters pass (`hatch --env develop run linters`) -- [ ] Type checker passes -- [ ] Tests pass (`hatch --env develop run testers`) -- [ ] Code review ready - -### Decision Log -Document significant decisions made during implementation: -- [Date] [Decision made] - [Rationale] -- [Date] [Trade-off chosen] - [Why this approach over alternatives] - -### Handoff Notes -For future sessions or other developers: -- **Current State**: [What's implemented and what's not] -- **Next Steps**: [Immediate next actions needed] -- **Known Issues**: [Any problems or concerns to address] -- **Context Dependencies**: [Critical knowledge for continuing work] - -### 2. Session Continuity and Context Preservation -Before proceeding with implementation: - -#### Check for Existing Implementation -```bash -ls .auxiliary/notes/*--progress.md -``` - -If continuing previous work: -- Read existing tracking file completely to understand context -- Review reference files listed in context section -- Check decision log for previous design choices -- Update "Current State" in handoff notes as you resume work - -#### Context Preservation Requirements -Before beginning implementation: -- [ ] Create descriptive tracking file (`.auxiliary/notes/<title>--progress.md`) -- [ ] Record all reference files provided at session start -- [ ] Document initial understanding of requirements -- [ ] Note any existing related implementations or patterns found - -During implementation: -- [ ] Update decision log when making design choices -- [ ] Record integration points and dependencies discovered -- [ ] Document deviations from original plan with rationale - -Before session end: -- [ ] Update current state in handoff notes -- [ ] Ensure TodoWrite completions are reflected in persistent tracking where granularity aligns -- [ ] Record next steps for continuation - -### 3. Implementation - -**Write Python code following established patterns**: -- Apply comprehensive guide patterns for module organization, imports, annotations, immutability, exception handling, and documentation -- Consult the comprehensive guides when you need specific implementation details -- For complex annotation work or systematic annotation issues, consider using the `python-annotator` agent - -### 4. Progress Tracking Requirements -Maintain dual tracking systems: -- **Session Level**: Use TodoWrite tool for immediate task management within current session -- **Cross-Session**: Update `.auxiliary/notes/<implementation-title>--progress.md` for persistent tracking -- **Synchronization**: When TodoWrite items align with persistent checklist granularity, update corresponding persistent checklist items (TodoWrite may be more fine-grained) -- **Context Preservation**: Record all reference files and design decisions in persistent file for future session continuity - -### 5. Quality Assurance - -Before proceeding, add this quality verification checklist to your TODO list: -- [ ] Code follows proper module organization patterns -- [ ] Imports follow organization rules with centralized patterns -- [ ] Type annotations use wide parameter, narrow return patterns -- [ ] Functions ≤30 lines, modules ≤600 lines -- [ ] Immutability preferences applied to data structures -- [ ] Exception handling uses narrow try blocks with proper chaining -- [ ] Documentation follows narrative mood requirements -- [ ] Quality assurance principles applied - -#### Validation Commands -**Linting Validation** (zero-tolerance policy): -```bash -hatch --env develop run linters -``` -All issues must be addressed per comprehensive guide principles. Do not use `noqa` without explicit approval. - -**Type Checking** (systematic resolution): -```bash -hatch --env develop run linters # Includes Pyright -``` - -**Type Error Resolution Process**: -1. **Code Issues**: Fix immediately using comprehensive guide type annotation patterns -2. **Third-party Stubs**: Follow guidance in Python-specific practices guide (ensure dependency in `pyproject.toml`, prune Hatch environment, Pyright `createstub`, manage stubs) -3. **Complex Issues**: Use `python-annotator` agent for systematic resolution - -Stop and consult user if type errors cannot be categorized or require architectural decisions. - -**Test Validation**: -```bash -hatch --env develop run testers -``` -All tests must pass, including new implementations. - -### 6. Documentation and Summary - -**Provide implementation documentation**: -- Update persistent tracking file with implementation state -- Document design decisions and trade-offs in decision log -- Complete handoff notes for session continuity -- Note TODO items for future work - -### 7. Summarize Implementation -Provide concise summary of what was implemented, including: -- Functions, classes, or modules created or modified -- Key design decisions and rationale -- Integration points and dependencies -- Quality assurance status: Confirm all linters, type checkers, and tests pass -- Checklist of principles and patterns applied during implementation -- Any remaining tasks or follow-up items diff --git a/.auxiliary/configuration/claude/commands/cs-conform-python.md b/.auxiliary/configuration/claude/commands/cs-conform-python.md deleted file mode 100644 index fa9bc2a..0000000 --- a/.auxiliary/configuration/claude/commands/cs-conform-python.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -description: Systematically conform Python code to project style and practice standards ---- - -# Python Code Conformance - -For bringing existing Python code into full compliance with project standards. - -Target: $ARGUMENTS - -Focus on style/practice conformance, not functionality changes. - -## Prerequisites - -- Read project documentation guides first: - - @.auxiliary/instructions/practices.rst - - @.auxiliary/instructions/practices-python.rst - - @.auxiliary/instructions/style.rst - - @.auxiliary/instructions/nomenclature.rst -- Understand target files to be conformed -- Have read `CLAUDE.md` for project-specific guidance - -## Context - -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` - -## Execution Structure - -**Phase 1: Comprehensive Review** -Perform complete analysis and generate detailed compliance report before making any changes. - -**Phase 2: Systematic Remediation** -Apply all identified fixes in systematic order, validating with linters after completion. - -### Project Standards - -Before proceeding with conformance analysis, you MUST: -1. Read @.auxiliary/instructions/practices.rst for general development principles -2. Read @.auxiliary/instructions/practices-python.rst for Python-specific patterns -3. In a step on your TODO list, please attest that you have read the general and Python-specific practices guides and demonstrate your knowledge by writing one-sentence summaries on any three of the following topics: - -- the wide parameter, narrow return type pattern for robust interfaces -- the import organization rules and centralized import patterns -- when to use different immutable base classes (Object vs DataclassObject vs Protocol) -- the exception hierarchy pattern (Omniexception → Omnierror) -- the comprehensive examples showing multiple principles cohesively -- the module organization content order and size limits - -## Conformance Verification - -### Module Organization Verification -Confirm compliance with module organization patterns: -- [ ] Content follows proper order: imports, type aliases, private defaults, public classes/functions, private functions -- [ ] Module size ≤600 lines (analyze oversized modules for separation of concerns) -- [ ] Functions ≤30 lines each - -### Import Organization Verification -Confirm compliance with import organization patterns: -- [ ] Module-level imports use private aliases (except in `__init__`, `__`, re-export modules) -- [ ] Common modules (os, re, etc.) imported through centralized `__.imports` rather than per-module -- [ ] No namespace pollution through public imports -- [ ] Subpackages define `__` re-export module with `from ..__ import *` -- [ ] No `__all__` attribute provided (unnecessary interface maintenance) - -### Type Annotation Verification -Confirm compliance with type annotation patterns: -- [ ] Public functions use wide, abstract argument types (`__.cabc.Sequence`, `__.cabc.Mapping`) -- [ ] All functions define narrow, concrete return types (`list`, `dict`, `tuple`, `__.immut.Dictionary`) -- [ ] Proper function signature spacing following formatting guidelines -- [ ] `TypeAlias` declarations for complex types - -### Immutability Verification -Confirm compliance with immutability patterns: -- [ ] Classes inherit from `__.immut.Object`, `__.immut.Protocol`, `__.immut.DataclassObject` -- [ ] Functions return immutable types (`tuple`, `frozenset`, `__.immut.Dictionary`) not mutable types (`list`, `dict`, `set`) -- [ ] Dependency injection with sensible defaults applied - -### Exception Handling Verification -Confirm compliance with exception handling patterns: -- [ ] One `try..except` suite per statement that can raise exceptions -- [ ] Narrow try block scope maintained -- [ ] Proper exception chaining and hierarchy usage -- [ ] No bare exceptions raised (except `NotImplementedError`) - -### Documentation Verification -Confirm compliance with documentation patterns: -- [ ] Docstrings use triple single quotes with narrative mood -- [ ] Exception messages in double quotes -- [ ] No comments describing obvious behavior -- [ ] TODO comments for uncovered edge cases - -### Style Formatting Verification -Confirm compliance with formatting standards: -- [ ] Space padding inside delimiters: `( arg )`, `[ item ]`, `{ key: value }` -- [ ] Space padding around keyword argument `=`: `foo = 42` -- [ ] F-strings in double quotes: `f"text {variable}"` -- [ ] No blank lines within function bodies -- [ ] Single-line statements on same line when appropriate: `if condition: return value` -- [ ] Proper multi-line construct delimiter placement - -### Quality Assurance Verification -Confirm compliance with quality assurance principles: -- [ ] Critical review of all linter suppressions -- [ ] No `type: ignore` usage (investigate underlying issues) -- [ ] No `__.typx.cast` usage (investigate type system issues) -- [ ] Minimal `noqa` pragmas with compelling justification only - -### Violation Analysis Reference - -For comprehensive violation examples and correction patterns, see the comprehensive examples in practices-python.rst, which demonstrate proper application of all conformance principles in cohesive, real-world contexts. - -When analyzing violations, reference the specific sections of practices-python.rst that address each violation type rather than duplicating examples here. - -## Review Report Format - -Phase 1 Output: -1. **Compliance Summary**: Overall assessment with file-by-file breakdown -2. **Standards Violations**: Categorized list with specific line references and explanations -3. **Complexity Analysis**: Function and module size assessments -4. **Remediation Plan**: Systematic order of fixes to be applied -5. **Risk Assessment**: Any changes that require careful validation - -Phase 2 Output: -1. **Applied Fixes**: Summary of all changes made, categorized by standard -2. **Validation Results**: Linter output before and after changes -3. **Files Modified**: Complete list with brief description of changes -4. **Manual Review Required**: Any issues requiring human judgment - -## Conformance Process - -### 1. Analysis Phase (PHASE 1) -- Examine target files to understand current state -- Run linters to identify specific violations -- Identify architectural patterns that need updating -- Generate comprehensive compliance report -- **Requirements**: Complete review and report before any remediation -- **Focus**: Reference specific lines with concrete examples and explain reasoning - -### 2. Systematic Correction (PHASE 2) - -Before applying any fixes, confirm: -- [ ] I have completed comprehensive analysis with specific line references -- [ ] I understand each violation type and its corresponding practices-python.rst section -- [ ] I have a systematic remediation plan prioritized by impact - -**Apply fixes in appropriate order**: -1. **Module Organization**: Reorder per established organizational patterns -2. **Import Organization**: Apply centralized import organization rules -3. **Type Annotations**: Convert to wide parameter/narrow return patterns -4. **Immutability**: Apply immutable container and base class patterns -5. **Exception Handling**: Apply narrow try block and hierarchy patterns -6. **Documentation**: Apply narrative mood and formatting patterns -7. **Formatting**: Apply spacing, delimiter, and vertical compactness standards -8. **Quality Assurance**: Apply linter compliance and suppression principles - -For comprehensive type annotation work or complex type checking issues, consider using the `python-annotator` agent. - -**POST-CORRECTION VERIFICATION GATE** -After applying all fixes, confirm: -- [ ] All verification checklists from practices-python.rst sections pass -- [ ] `hatch --env develop run linters` produces clean output -- [ ] `hatch --env develop run testers` passes with no functionality breaks -- [ ] Code follows all practices-python.rst patterns exactly - -## Safety Requirements - -Stop and consult if: -- Linters reveal complex architectural issues -- Changes would alter functionality -- Type annotations conflict with runtime behavior -- Import changes break dependencies -- Tests start failing - -Your responsibilities: -- Maintain exact functionality while improving practices/style -- Use project patterns consistently per the guides -- Reference all three guides for complex cases -- Verify all changes with linters and tests - -## Success Criteria - -- [ ] All linting violations resolved -- [ ] Module organization follows practices guide structure -- [ ] Function parameters use wide abstract types -- [ ] Imports avoid namespace pollution -- [ ] Type annotations comprehensive with `TypeAlias` usage -- [ ] Exception handling uses narrow try blocks -- [ ] Immutable containers used where appropriate -- [ ] No functionality changes -- [ ] Tests continue to pass -- [ ] Code follows all style guide patterns - -**Note**: Always run full validation (`hatch --env develop run linters && hatch ---env develop run testers`) before considering the task complete. - -## Final Report - -Upon completion, provide a brief report covering: -- Specific conformance issues corrected (categorized by the priority issues above) -- Number of files modified -- Any patterns that required manual intervention -- Linter status before/after -- Any deviations from guides and justification diff --git a/.auxiliary/configuration/claude/commands/cs-conform-toml.md b/.auxiliary/configuration/claude/commands/cs-conform-toml.md deleted file mode 100644 index 27c280f..0000000 --- a/.auxiliary/configuration/claude/commands/cs-conform-toml.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -allowed-tools: Bash(git:*), LS, Read, Glob, Grep, Edit, MultiEdit, Write -description: Systematically conform TOML files to project style and practice standards ---- - -# TOML Configuration Conformance - -For bringing existing TOML configuration files into full compliance with project standards. - -Target files: $ARGUMENTS - -Focus on style/practice conformance, not functionality changes. - -## Prerequisites - -- Read project documentation guides first: - - @.auxiliary/instructions/practices.rst - - @.auxiliary/instructions/practices-toml.rst - - @.auxiliary/instructions/style.rst - - @.auxiliary/instructions/nomenclature.rst -- Understand target files to be conformed -- Have read `CLAUDE.md` for project-specific guidance - -## Context - -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` - -## Execution Structure - -**Phase 1: Comprehensive Review** -Perform complete analysis and generate detailed compliance report before making any changes. - -**Phase 2: Systematic Remediation** -Apply all identified fixes in systematic order, validating changes after completion. - -## Compliance Standards - -### Configuration Design Standards - -#### 1. Table Organization - -- Prefer table arrays with `name` fields over proliferating custom subtables. -- Table arrays scale better and reduce configuration complexity. - -**❌ Avoid - custom subtables:** -```toml -[database] -host = 'localhost' - -[database.primary] -port = 5432 -timeout = 30 - -[database.replica] -port = 5433 -timeout = 15 -``` - -**✅ Prefer - table arrays with name field:** -```toml -[[database]] -name = 'primary' -host = 'localhost' -port = 5432 -timeout = 30 - -[[database]] -name = 'replica' -host = 'localhost' -port = 5433 -timeout = 15 -``` - -#### 2. Key Naming Conventions - -- Use hyphens instead of underscores in key names for better ergonomics. -- Apply nomenclature guidelines to key and table names. -- Use Latin-derived words when they are the established norm in the domain. - -**❌ Avoid:** -```toml -max_connections = 100 -retry_count = 3 -database_url = 'postgresql://localhost/db' -``` - -**✅ Prefer:** -```toml -max-connections = 100 -retry-count = 3 -database-url = 'postgresql://localhost/db' -``` - -### Style Standards - -#### 1. String Values - -- Use single quotes for string values unless escapes are needed. -- Use double quotes when escapes are required. -- Use triple single quotes for multi-line strings (consistency with Python docstrings). - -**❌ Avoid:** -```toml -name = "example-service" -description = "A service for processing data" -pattern = "user-.*" -``` - -**✅ Prefer:** -```toml -name = 'example-service' -description = 'A service for processing data' -pattern = 'user-.*' - -# Use double quotes when escapes are needed -windows-path = "C:\\Program Files\\Example" -message = "Line 1\nLine 2" - -# Use triple single quotes for multi-line strings -description = ''' -This is a longer description -that spans multiple lines. -''' -``` - -#### 2. Array and Table Formatting - -- Keep arrays and inline tables on single lines when they fit within reasonable length. -- For longer arrays, place each element on its own line with proper indentation. - -**✅ Prefer:** -```toml -ports = [ 8080, 8443, 9090 ] -database = { host = 'localhost', port = 5432 } - -# For longer arrays -allowed-origins = [ - 'https://example.com', - 'https://api.example.com', - 'https://admin.example.com', -] -``` - -### Comprehensive Example: Configuration with Multiple Violations - -Here is a TOML configuration that demonstrates many compliance violations: - -```toml -[server_config] -host_name = "localhost" -port_number = 8080 -max_connections = 100 - -[server_config.database_primary] -host = "localhost" -port = 5432 -connection_timeout = 30 -retry_attempts = 3 - -[server_config.database_replica] -host = "localhost" -port = 5433 -connection_timeout = 15 -retry_attempts = 2 - -allowed_hosts = ["https://example.com", "https://api.example.com", "https://admin.example.com"] - -description = "This is a multi-line description that explains what this service does and how it should be configured." -``` - -Violations identified: -1. **Underscore key names**: `server_config`, `host_name`, `port_number`, `max_connections` should use hyphens -2. **Custom subtables**: `[server_config.database_primary]` and `[server_config.database_replica]` should be table arrays -3. **Double quotes**: String values using double quotes without escapes needed -4. **Array formatting**: Long array on single line should be split across multiple lines -5. **Multi-line string**: Long description should use triple single quotes - -Corrected version: -```toml -[[server-config]] -name = 'main' -host-name = 'localhost' -port-number = 8080 -max-connections = 100 - -[[database]] -name = 'primary' -host = 'localhost' -port = 5432 -connection-timeout = 30 -retry-attempts = 3 - -[[database]] -name = 'replica' -host = 'localhost' -port = 5433 -connection-timeout = 15 -retry-attempts = 2 - -allowed-hosts = [ - 'https://example.com', - 'https://api.example.com', - 'https://admin.example.com', -] - -description = ''' -This is a multi-line description that explains what this service does -and how it should be configured. -''' -``` - -## Review Report Format - -Phase 1 Output: -1. **Compliance Summary**: Overall assessment with file-by-file breakdown -2. **Standards Violations**: Categorized list with specific line references and explanations -3. **Configuration Analysis**: Table organization and key naming assessments -4. **Remediation Plan**: Systematic order of fixes to be applied -5. **Risk Assessment**: Any changes that require careful validation - -Phase 2 Output: -1. **Applied Fixes**: Summary of all changes made, categorized by standard -2. **Files Modified**: Complete list with brief description of changes -3. **Manual Review Required**: Any issues requiring human judgment - -## Conformance Process - -### 1. Analysis Phase (PHASE 1) -- Examine target files to understand current state -- Identify configuration design patterns that need updating -- Generate comprehensive compliance report -- **Requirements**: Complete review and report before any remediation -- **Focus**: Reference specific lines with concrete examples and explain reasoning - -### 2. Systematic Correction (PHASE 2) -Apply fixes in systematic order: -1. **Key Naming**: Convert underscores to hyphens in key names -2. **Table Organization**: Convert custom subtables to table arrays with `name` fields -3. **String Quoting**: Change double quotes to single quotes (unless escapes needed) -4. **Multi-line Strings**: Convert to triple single quotes format -5. **Array Formatting**: Split long arrays across multiple lines with proper indentation -6. **Nomenclature**: Apply naming guidelines to keys and table names - -**Requirements**: -- Maintain exact functionality while improving standards adherence -- Validate that configuration files remain syntactically valid -- Preserve all semantic meaning of configuration values - -## Safety Requirements - -Stop and consult if: -- Configuration structure changes would alter application behavior -- Complex nested configurations require architectural decisions -- File contains domain-specific conventions that conflict with general guidelines -- Syntax errors occur during modification - -Your responsibilities: -- Maintain exact functionality while improving practices/style -- Use project patterns consistently per the guides -- Reference TOML documentation guides for complex cases -- Verify all changes preserve configuration semantics - -## Success Criteria - -- [ ] All key names use hyphens instead of underscores -- [ ] Custom subtables converted to table arrays where appropriate -- [ ] String values use single quotes (double only when escapes needed) -- [ ] Multi-line strings use triple single quotes -- [ ] Long arrays are properly formatted across multiple lines -- [ ] Nomenclature guidelines applied to keys and table names -- [ ] No functionality changes to configuration behavior -- [ ] Files remain syntactically valid TOML - -## Final Report - -Upon completion, provide a brief report covering: -- Specific conformance issues corrected (categorized by the priority issues above) -- Number of files modified -- Any patterns that required manual intervention -- Any deviations from guides and justification \ No newline at end of file diff --git a/.auxiliary/configuration/claude/commands/cs-copier-update.md b/.auxiliary/configuration/claude/commands/cs-copier-update.md deleted file mode 100644 index 16471af..0000000 --- a/.auxiliary/configuration/claude/commands/cs-copier-update.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(copier:*), Bash(git status), Bash(git add:*), Bash(git rm:*), Bash(rg:*), Bash(grep:*), Bash(hatch --env develop run make-all), TodoWrite -description: Synchronize project with Copier template updates, intelligently resolving merge conflicts ---- - -# Template Synchronization - -Synchronize project with its Copier template by running updates and automatically resolving common merge conflict patterns while preserving local customizations. - -Request from user: $ARGUMENTS - -## Context - -- Template answers file: @.auxiliary/configuration/copier-answers.yaml -- Current git status: !`git status --porcelain` -- Existing conflicts check: !`grep -r "^<<<<<<<\|^=======$\|^>>>>>>>" . --exclude-dir=.git || echo "No conflicts"` -- Project conventions: @.auxiliary/configuration/conventions.md - -## Prerequisites - -Before running template synchronization, ensure: -- Working directory is completely clean (no staged or unstaged changes) -- Copier is installed and accessible via command line -- Template answers file exists at `.auxiliary/configuration/copier-answers.yaml` -- Git repository is in a stable state for applying updates - -## Process Summary - -Key functional areas: -1. **Template Update**: Run copier update with project-specific settings -2. **Conflict Detection**: Identify and categorize merge conflicts from template changes -3. **Intelligent Resolution**: Automatically resolve conflicts favoring upstream improvements while preserving local customizations -4. **File Lifecycle Management**: Handle additions, renames, and deletions from template updates -5. **Validation**: Ensure complete conflict resolution and commit changes with template version - -## Safety Requirements - -Stop and consult the user if: -- Working directory is not clean (has staged or unstaged changes) -- Complex conflicts exist that could result in loss of local customizations -- Template artifacts cannot be reliably distinguished from intentional local content -- Multiple conflicting resolution strategies are equally valid -- Copier update fails with unrecoverable errors -- Critical project files show unexpected merge conflicts - -## Execution - -Execute the following steps: - -### 1. Pre-Update Validation -Check project state and prepare for template synchronization: -- Verify git working directory is completely clean (halt if any changes exist) -- Confirm template answers file exists and is readable -- Document any existing conflicts to avoid confusion -- Ensure repository is on the expected branch - -### 2. Execute Template Update -Run copier update with project-specific configuration: -```bash -copier update --answers-file .auxiliary/configuration/copier-answers.yaml --skip-answered -``` -- Capture copier output to extract template version information -- Detect update completion status and any reported conflicts -- Identify new, modified, and deleted files from the update - -### 3. Conflict Analysis and Categorization -Systematically identify and categorize all conflicts: -- Scan for merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) -- Classify conflicts by type: - - **Structure consolidation**: Old sections moved into organized subsections - - **Upstream additions**: New template content (toctree entries, sections, files) - - **Language refinements**: Policy and wording improvements - - **Template artifacts**: TODO comments, placeholder content - - **Complex conflicts**: Overlapping local and upstream modifications - -### 4. Intelligent Conflict Resolution -Apply resolution strategies based on conflict categorization: - -**Auto-resolve structure consolidation conflicts:** -- Accept new organization when local content is preserved in new structure -- Remove orphaned sections that were properly consolidated - -**Auto-resolve upstream additions:** -- Accept new toctree entries, sections, and configuration additions -- Stage new files and directories from template - -**Auto-resolve language refinements:** -- Accept upstream wording and policy improvements -- Preserve local semantic modifications when they don't conflict - -**Handle template artifacts intelligently:** -- Detect TODO comments and placeholder content that may have been intentionally removed -- Avoid reintroducing template boilerplate that conflicts with project maturity - -### 5. File Lifecycle Management -Handle template-driven file changes: -- Stage all new files and directories added by template -- Process file renames (e.g., `cs-develop-tests.md` → `cs-develop-pytests.md`) -- Remove obsolete files that have been replaced or are no longer needed -- Update git index to reflect all template changes - -### 6. Resolution Verification -Ensure complete and accurate conflict resolution: -- Scan entire project for remaining merge conflict markers -- Verify no orphaned conflict sections remain -- Confirm all auto-resolved conflicts maintain local customizations -- Validate file integrity and proper git staging - -### 7. Project Validation -Verify template changes don't break project functionality: -```bash -hatch --env develop run make-all -``` -- Run full project validation including linting, type checking, and tests -- Ensure all quality gates pass after template synchronization -- Address any validation failures before proceeding to commit - -### 8. Commit Template Changes -Create commit with template version information: -- Extract template version from copier output or updated answers file -- Generate commit message: "Update project from Copier template (v{version})." -- Include standard co-authoring footer for Claude Code -- Use git commit (requires user approval) to commit all staged changes - -### 9. Conflict Resolution Report -Provide comprehensive summary of synchronization results: -- List all conflicts automatically resolved with resolution strategy -- Report new files, renames, and deletions processed -- Identify any conflicts requiring manual intervention -- Confirm template version successfully applied -- Note any remaining tasks or follow-up actions needed \ No newline at end of file diff --git a/.auxiliary/configuration/claude/commands/cs-create-command.md b/.auxiliary/configuration/claude/commands/cs-create-command.md deleted file mode 100644 index d7ba98b..0000000 --- a/.auxiliary/configuration/claude/commands/cs-create-command.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -allowed-tools: Write, Read, LS -description: Generate a new custom slash command with consistent structure and formatting ---- - -# Generate Slash Command - -Generate a new custom slash command following established patterns for structure, tone, and formatting. - -Target: $ARGUMENTS - -**IMPORTANT**: You are creating slash commands for other Claude instances to execute. They will have no knowledge of: -- The concept of "arguments" being passed to slash commands -- The ARGUMENTS variable or its expansion -- The meta-context of slash command generation -- When creating content, avoid using the word "command" in titles or explanations - use terms like "process", "workflow", or "task" instead - -Your job is to interpret the user's request and create a complete, self-contained slash command. - -## Input Interpretation - -The user's request may take various forms: -- Simple: `cs-analyze-performance` -- Descriptive: `Named cs-inquire.md with a process outlined in .auxiliary/notes/inquire-command.md` -- Reference-based: `Based on .auxiliary/notes/summarize-project-command.md` -- Complex: `cs-update-deps that checks package.json and updates dependencies safely` - -Extract from the user's input: -1. **Filename** (must start with `cs-`) -2. **Purpose/functionality** (from description or referenced files) -3. **Special requirements** (referenced processes, specific tools needed) - -## Context - -- Current custom commands: !`ls .claude/commands/cs-*.md 2>/dev/null || echo "No cs-* commands found"` -- Referenced files (if any): Check for existence and read as needed -- Command template: @.auxiliary/configuration/claude/miscellany/command-template.md - -## Prerequisites - -Before creating the slash command, ensure: -- Clear understanding of the intended purpose -- Filename follows `cs-*` naming pattern -- No existing file with the same name -- Any referenced process files are accessible - -## Generation Process - -### 1. Analyze User Request - -From the user's input, determine: -- **Filename** (extract `cs-*.md` name) -- **Purpose** (what should the generated slash command accomplish) -- **Required tools** (based on functionality) -- **Process details** (read any referenced files for specifics) - -### 2. Read Template Structure - -Read the template to get the base structure, then customize: -- Replace placeholder content with appropriate descriptions -- Customize sections based on purpose -- Select appropriate allowed-tools -- Add relevant @-references if applicable -- Add checklists to sections if applicable - -### 3. Apply Formatting Standards - -**Professional Tone:** -- Avoid making everything critical or important; no excessive - attention-grabbing -- Avoid excessive emphasis (no all-caps headers, minimal bold text) -- Professional headers: `## Prerequisites` not `## MANDATORY PREREQUISITES` -- Use "Stop and consult" for when user input should be solicited - -**Structure:** -- Include Prerequisites section early in document -- Include Context section with command expansions (exclamation point followed - by command in backticks) for dynamic info when needed -- Use @-references for local documentation when applicable -- Provide clear Process Summary before detailed steps -- Include Safety Requirements section for error handling - -### 4. Tool Selection - -Choose appropriate allowed-tools based on functionality: - -**Common tool combinations:** -- **File operations**: `Write, Read, Edit, MultiEdit, LS, Glob, Grep` -- **Git operations**: `Bash(git status), Bash(git add:*), Bash(git commit:*), Bash(git push:*)` -- **Python development**: `Bash(hatch --env develop run:*), Bash(pytest:*), Bash(ruff:*)` -- **GitHub operations**: `Bash(gh run list:*), Bash(gh run watch:*), Bash(gh pr create:*)` - -### 5. Generate and Write File - -1. **Read the template** from `.auxiliary/configuration/claude/miscellany/command-template.md` -2. **Customize all sections** based on the specific purpose -3. **Replace placeholders** with appropriate content for the target functionality -4. **Write the final file** to `.claude/commands/[filename].md` - - -### 6. Validation and Summary - -After generation: -- Verify file structure matches established patterns -- Check that allowed-tools are appropriate for the functionality -- Ensure professional tone throughout (no excessive attention-grabbing, etc...) -- Confirm all required sections are present and customized -- Provide succinct summary of changes made to the user diff --git a/.auxiliary/configuration/claude/commands/cs-design-python.md b/.auxiliary/configuration/claude/commands/cs-design-python.md deleted file mode 100644 index ea4ff01..0000000 --- a/.auxiliary/configuration/claude/commands/cs-design-python.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -description: Python API design, filesystem organization, module structure, and interface specifications ---- - -# Python Design Analysis - -Analyze Python API design patterns, filesystem organization, module structure, class hierarchies, interface definitions, and design patterns to provide guidance on Python-specific structural decisions and project organization. Focus exclusively on interface contracts, signatures, and type specifications—never implementation details or method bodies. - -Request from user: $ARGUMENTS - -## Context - -- Architecture overview: @documentation/architecture/summary.rst -- Filesystem patterns: @documentation/architecture/filesystem.rst -- General practices: @.auxiliary/instructions/practices.rst -- Python development guide: @.auxiliary/instructions/practices-python.rst -- Code style: @.auxiliary/instructions/style.rst -- Nomenclature: @.auxiliary/instructions/nomenclature.rst -- Design documents: @documentation/architecture/designs/ - -## Prerequisites - -Before providing design analysis, ensure: -- Understanding of module organization and class hierarchies -- Familiarity with Python practices and style guidelines -- Knowledge of nomenclature conventions and naming patterns - -### Project Standards - -Before providing design analysis, you MUST: -1. Read @.auxiliary/instructions/practices.rst for general development principles -2. Read @.auxiliary/instructions/practices-python.rst for Python-specific patterns -3. In a step on your TODO list, please attest that you have read the general and Python-specific practices guides and demonstrate your knowledge by writing one-sentence summaries on any three of the following topics: - -- interface specification patterns from comprehensive examples -- module organization principles and content ordering -- import organization for design specifications -- wide parameter, narrow return interface patterns -- immutable container design patterns -- exception hierarchy design patterns -- documentation specification requirements -- nomenclature patterns from nomenclature guides - -## Process Summary - -Key functional areas: -1. **Design Analysis**: Examine current Python structure and design patterns -2. **Interface Specification**: Define clean API boundaries and contracts -3. **Module Organization**: Apply filesystem and import patterns effectively -4. **Class Design**: Create maintainable hierarchies and interface patterns -5. **Documentation**: Specify design decisions with examples and rationale - -## Safety Requirements - -Stop and consult the user if: -- Architectural decisions are needed instead of design specifications -- Implementation details are requested instead of design specifications -- Requirements analysis is needed instead of design specifications -- User requests actual code implementations instead of specifications -- Design decisions require architectural changes beyond Python structure -- Interface changes would break existing API contracts significantly -- Design conflicts with established filesystem organization patterns -- Requirements are unclear or insufficient for proper design specification -- Multiple design approaches have significant trade-offs requiring user input - -## Execution - -Execute the following steps: - -### 1. Design Analysis -Examine Python structure and patterns: -- Review module organization and import patterns -- Analyze class hierarchies and interface definitions -- Identify design patterns in use -- Assess alignment with practices and nomenclature guidelines -- Document design strengths and improvement opportunities - -### 2. Interface Specification - -**CRITICAL: Define interfaces through signatures and type annotations only. Avoid specifying how methods should be implemented internally—focus on contracts, not implementation logic.** - -**Define clean API boundaries and contracts**: -- Focus exclusively on signatures and type annotations (never implementation logic or method bodies) -- Apply wide parameter, narrow return patterns for robust interfaces -- Design exception class hierarchies following established patterns -- Apply appropriate naming conventions from nomenclature guidelines -- Define annotations using proper `__.typx.TypeAlias` patterns when appropriate -- Consider immutability preferences in container design -- Consult comprehensive guides for detailed patterns when needed - -### 3. Filesystem and Module Organization Design - -**Apply Python-specific organizational patterns and filesystem structure**: -- Design project filesystem organization and update filesystem.rst as needed -- Design module structure following standard organization order -- Plan centralized import integration for organized dependencies -- Specify exception hierarchies and their organization -- Design interface patterns for different component types -- Plan type alias organization and dependency management -- Consult comprehensive guides for detailed organizational patterns - -### 4. Class and Function Design - -**CRITICAL: Design class structures through their public contracts and type relationships. Specify signatures, inheritance patterns, and interface boundaries—never internal implementation logic or method bodies.** - -**Create maintainable Python structures**: -- Design class hierarchies with appropriate immutable base classes and mixins (`__.immut.Object`, `__.immut.Protocol`, etc.) -- Specify function signatures using wide input, narrow output patterns with proper spacing -- Apply nomenclature guidelines for methods, attributes, and functions -- Design immutable data structures and container patterns -- Plan dependency injection and configuration patterns with sensible defaults -- Focus exclusively on interface specifications, not implementation details -- Consult comprehensive guides for detailed design patterns - -### 5. Design Documentation - -**Create comprehensive design specifications without implementations**: - -**CRITICAL:** -- Use atemporal language in all specifications. Avoid temporal terms like 'new', 'current', 'existing', 'future'—designs should read as canonical specifications independent of implementation timeline. -- Provide only signatures, contracts, and interface specifications - no implementations - -- Generate design documents following established format -- Update `documentation/architecture/designs/index.rst` to include designs -- Do not provide exception class implementations, function bodies, or method implementations -- Document interface contracts and expected behaviors (contracts only, not code) -- Provide design examples using signatures and type annotations only -- Specify exception handling patterns and error propagation (exception classes by name/signature only) -- Document design rationale and trade-off decisions -- Consult comprehensive guides for documentation formatting requirements - -### 6. Design Validation - -**Ensure design quality and consistency**: -- Verify alignment with practices, style, and nomenclature guidelines -- Check consistency with filesystem organization patterns -- Validate that wide parameter/narrow return patterns are followed -- Ensure proper separation between public and private interfaces -- Confirm that design supports expected usage patterns and extensibility -- Verify that specifications focus on contracts, not implementations -- Consult comprehensive guides to verify pattern alignment - -### 7. Summarize Updates -Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/cs-develop-pytests.md b/.auxiliary/configuration/claude/commands/cs-develop-pytests.md deleted file mode 100644 index b1acca9..0000000 --- a/.auxiliary/configuration/claude/commands/cs-develop-pytests.md +++ /dev/null @@ -1,263 +0,0 @@ ---- -description: Implement comprehensive Python tests following an existing test plan and project guidelines ---- - -# Implement Python Tests - -For systematic test implementation following a pre-created test plan and project testing guidelines. - -Test plan path or special test-writing instructions: $ARGUMENTS - -Implement tests according to the provided test plan only. - -## Context - -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` -- Existing test structure: !`find tests -name "*.py" | head -20` -- Test organization: @documentation/architecture/testplans/summary.rst -- Test plans index: @documentation/architecture/testplans/index.rst - -## Prerequisites - -Ensure that you: -- Have a valid test plan document -- Have verified access to target code modules referenced in the plan -- Have read any relevant `CLAUDE.md` file -- Understand the test-writing guidelines: @.auxiliary/instructions/tests.rst - -## Testing Principles (from project guidelines) - -**Core Principles:** -1. **Dependency Injection Over Monkey-Patching**: Use injectable dependencies - for testability -2. **Performance-Conscious**: Prefer in-memory filesystems (pyfakefs) over temp - directories -3. **Avoid Monkey-Patching**: Never patch internal code; use dependency - injection instead -4. **100% Coverage Goal**: Aim for complete line and branch coverage -5. **Test Behavior, Not Implementation**: Focus on observable behavior and - contracts - -**Anti-Patterns to Avoid:** -- Monkey-patching internal code (will fail with immutable objects) -- Excessive mocking of internal components -- Testing implementation details vs. behavior -- Using temp directories when pyfakefs suffices - -**Organization:** -- Follow the systematic numbering conventions detailed in the test guidelines - -## Safety Requirements - -Stop and consult the user if: -- No test plan path is provided -- Test plan cannot be read or is invalid -- Plan conflicts with project testing principles -- Implementation deviates from plan without justification -- Implementation cannot follow the test plan as specified -- Plan requires tests that violate project principles -- Tests require monkey-patching internal code -- Planned test numbering clashes with existing conventions -- Required test fixtures or dependencies are unavailable -- Test plan contains contradictions or unclear instructions - -**Your responsibilities:** -- Follow the test plan precisely while adhering to project conventions -- Focus only on uncovered areas specified in the plan -- Avoid redundant testing of functionality already covered by doctests -- Use dependency injection patterns as specified in the plan -- Implement tests exactly as planned without adding extras -- Maintain systematic test numbering as outlined in the plan -- Ensure tests validate behavior, not implementation -- Document any necessary deviations from the plan with clear justification - -## Test Implementation Process - -### 0. Pre-Flight Verification -Verify access to project guidelines: - -Read and confirm you can access the complete project guidelines: -- Testing: @.auxiliary/instructions/tests.rst -- Python Practices: @.auxiliary/instructions/practices-python.rst -- General Practices: @.auxiliary/instructions/practices.rst -- Style: @.auxiliary/instructions/style.rst - -You must successfully access and read all four guides before proceeding. If any guide cannot be accessed, stop and inform the user. - -### 1. Test Plan Reading and Validation -Read and validate the provided test plan: - -**Validate plan completeness:** -- Verify plan contains coverage analysis summary -- Confirm test strategy is clearly defined -- Check that component-specific tests are detailed -- Ensure implementation notes are present -- Validate success metrics are specified - -Stop if the plan is incomplete, unclear, or missing critical sections. - -### 2. Plan Compliance Verification -**Ensure plan aligns with project principles:** - -**Verify plan adheres to project testing guidelines:** -- No monkey-patching of internal code required -- Dependency injection patterns are viable -- Test numbering follows project conventions -- No external network testing planned - -**Check for conflicts with existing tests:** -- Review planned test module names against existing files -- Verify planned test function numbering doesn't conflict -- Ensure no duplication of existing test coverage - -### 3. Test Data and Fixture Setup -**Prepare test data and dependencies as specified in the plan:** - -**Ensure required test dependencies are available:** -If the test plan requires dependencies not in the current environment, add them to `pyproject.toml`: - -```toml -[tool.hatch.envs.develop] -dependencies = [ - # ... existing dependencies - "pyfakefs", # For filesystem mocking - "pytest-asyncio", # For async test support - # ... other test-specific dependencies in alphabetical order -] -``` - -After adding dependencies, rebuild the environment to ensure consistency: -```bash -hatch env prune -``` - -**Create required test data under tests/data/:** -- Set up fake packages for extension mechanisms (if planned) -- Prepare captured artifacts and snapshots (if planned) -- Create any mock data files as specified in the plan - -Only create test data explicitly mentioned in the test plan. - -### 4. Test Module Creation/Updates -**Implement test modules following the plan:** - -**For each planned test module:** -- Create or update test files with planned naming (e.g., `test_100_exceptions.py`) -- Follow planned test function numbering within modules -- Implement only the tests specified in the plan -- Use dependency injection patterns as outlined in the plan - -**Key Implementation Guidelines:** -- Use dependency injection for all external dependencies as planned -- Prefer `pyfakefs.Patcher()` for filesystem operations as specified -- Mock only third-party services, never internal code -- **Insert tests in numerical order within files** - do NOT append to end -- **Write behavior-focused docstrings**: "Functionality is correct with Y" NOT "function_name does X with Y" -- Follow existing naming conventions and code style -- Implement tests in the exact order and numbering specified in the plan - -### 5. Coverage Validation -**Verify implementation matches plan coverage goals:** -```bash -hatch --env develop run testers -hatch --env develop run coverage report --show-missing -``` - -Verify plan compliance: -- Run full test suite to ensure no regressions -- Check that coverage matches the plan's target metrics -- Verify all planned test functions are implemented -- Confirm coverage gaps identified in the plan are addressed -- Ensure no existing functionality is broken - -### 6. Code Quality Validation -**Ensure implemented tests meet project standards:** -```bash -hatch --env develop run linters -``` - -**Requirements:** -- All linting checks must pass -- Note that the linters do not check style; you must verify style compliance -- No violations of project coding standards -- Test docstrings are clear and descriptive -- Proper imports and dependencies -- Implementation follows all conventions specified in the plan - -## Test Pattern Examples - -**Import Patterns:** - -*Direct imports (preferred for most cases):* -```python -from mypackage import mymodule - -def test_100_basic_functionality( ): - ''' Module function works correctly with valid input. ''' - result = mymodule.process_data( 'test' ) - assert result == 'processed: test' -``` - -**Dependency Injection Pattern:** -```python -async def test_100_process_with_custom_processor( ): - ''' Process function accepts custom processor via injection. ''' - def mock_processor( data ): - return f"processed: {data}" - - result = await process_data( 'test', processor = mock_processor ) - assert result == "processed: test" -``` - -**Filesystem Operations (Preferred):** -```python -def test_200_config_file_processing( ): - ''' Configuration files are processed correctly. ''' - with Patcher( ) as patcher: - fs = patcher.fs - fs.create_file( '/fake/config.toml', contents = '[section]\nkey="value"' ) - result = process_config_file( Path( '/fake/config.toml' ) ) - assert result.key == 'value' -``` - -**Error Handling:** -```python -def test_300_invalid_input_handling( ): - ''' Invalid input raises appropriate exceptions. ''' - with pytest.raises( ValueError, match = "Invalid data format" ): - process_invalid_data( "malformed" ) -``` - -## Success Criteria - -Implementation is complete when: -- [ ] All tests specified in the plan have been implemented -- [ ] Coverage matches or exceeds the plan's target metrics -- [ ] All planned test modules and functions are created with correct numbering -- [ ] Test data and fixtures are set up as specified in the plan -- [ ] All new tests pass consistently -- [ ] No existing tests are broken -- [ ] Linting passes without issues -- [ ] Project coding practices and style have been followed -- [ ] Tests follow project numbering conventions as planned -- [ ] Tests are inserted in proper numerical order within files -- [ ] Test docstrings focus on behavior, not function names -- [ ] Dependency injection is used as specified in the plan -- [ ] No monkey-patching of internal code -- [ ] Performance-conscious patterns are applied as planned - -**Note**: Always run full validation (`hatch --env develop run linters && hatch ---env develop run testers`) before considering the task complete. - -## Final Report - -Upon completion, provide a brief report covering: -- **Plan Compliance**: Confirmation that all planned tests were implemented as specified -- **Coverage Achievement**: Final coverage percentages vs. plan targets -- **Deviations from Plan**: Any necessary changes made to the plan during implementation with justification -- **Technical Issues Resolved**: Any conflicts encountered and how they were resolved -- **Pragma Directives Applied**: Any `# pragma: no cover` or `# pragma: no branch` added with rationale -- **Test Data Created**: Summary of fixtures and test data files created under `tests/data/` -- **Module Updates**: List of test modules created or updated with their numbering -- **Code Quality**: Confirmation that tests are properly ordered and have behavior-focused docstrings diff --git a/.auxiliary/configuration/claude/commands/cs-document-examples-rst.md b/.auxiliary/configuration/claude/commands/cs-document-examples-rst.md deleted file mode 100644 index 460ca10..0000000 --- a/.auxiliary/configuration/claude/commands/cs-document-examples-rst.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -description: Creates practical, testable examples documentation ---- - -# Document Examples - -Develops practical, testable examples for documentation under -`documentation/examples/` that increase test coverage while remaining relatable -and succinct. - -Topic: $ARGUMENTS - -## Context - -- Project structure: @documentation/architecture/filesystem.rst -- Existing examples: !`ls -la documentation/examples/ 2>/dev/null || echo "No examples directory"` -- Code coverage data: !`hatch --env develop run testers 2>/dev/null || echo "No coverage data available"` - -## Prerequisites - -Before creating examples documentation: -- Understand the target audience (developers vs end users) -- Analyze existing codebase to identify core functionality patterns -- Review existing examples for organization, completeness, and thematic inspiration -- Examine @.auxiliary/instructions/ for style and nomenclature requirements - -## Process Summary - -Key functional areas: -1. **Analysis**: Survey codebase and existing examples to identify documentation gaps -2. **Theme Development**: Create coherent scenarios that demonstrate functionality progression -3. **Content Creation**: Write succinct examples using proper reStructuredText formatting -4. **Validation**: Ensure examples follow project practices and can serve as informal tests - -## Safety Requirements - -Stop and consult the user if: -- Examples require creating contrived scenarios that don't reflect real usage -- Multiple conflicting themes emerge without clear organizational strategy -- Proposed examples would expose internal implementation details inappropriately -- Documentation format conflicts with existing project conventions - -## Execution - -Execute the following steps: - -### 1. Analyze Existing Documentation Structure - -Survey the current documentation to understand patterns and identify gaps. Read -existing example files to understand established themes and formatting -approaches. - -### 2. Survey Codebase for Example Opportunities - -Identify public API surfaces and common usage patterns. Analyze coverage -reports in `.auxiliary/artifacts/coverage-pytest` if available. - -Look for: -- Public classes and functions that need demonstration -- Common workflows that span multiple components -- CLI commands and their typical usage patterns -- Error handling scenarios that users should understand - -### 3. Develop Thematic Coherence - -Based on analysis, choose one of these organizational approaches: - -- **Domain scenarios**: Practical use cases -- **API progression**: Basic to advanced usage of core functionality -- **Workflow examples**: End-to-end processes showing component interaction -- **CLI workflows**: Command sequences for common tasks - -### 4. Create Example Documentation - -Write examples following these requirements: - -- Use Sphinx reStructuredText format with proper double backticks for inline literals -- Include blank lines before list items per reStructuredText conventions -- Structure as progression from simple to complex scenarios -- Use doctest format for Python API examples where testable -- Use code-block format for CLI examples with explicit command annotation -- Keep code blocks comment-free; put explanatory text between blocks -- Follow @.auxiliary/instructions/practices.rst for general code organization -- Follow @.auxiliary/instructions/style.rst for formatting -- Follow @.auxiliary/instructions/nomenclature.rst for naming -- When documenting Python code, also follow .auxiliary/instructions/practices-python.rst for comprehensive Python standards -- When documenting Rust code, also follow .auxiliary/instructions/practices-rust.rst for comprehensive Rust standards -- When documenting TOML configuration, also follow .auxiliary/instructions/practices-toml.rst for comprehensive TOML standards - -### 5. Ensure Practical Relevance - -Verify each example: - -- Demonstrates functionality users actually need -- Shows practical data and scenarios, remaining minimalist rather than elaborate -- Includes appropriate error cases and edge conditions -- Can serve as informal test coverage for documented features -- Follows established project patterns for similar examples - -### 6. Validate Documentation Quality - -Review final documentation for: - -- Proper reStructuredText syntax and formatting -- Consistent theme and progression across examples -- Adherence to project style guidelines -- Executable/testable nature of code examples -- Clear explanatory text that guides readers through concepts - -### 7. Provide Summary - -Provide a succinct summary to the user describing: - -- What examples were created or updated -- The organizational theme chosen and why -- Key functionality areas covered -- How the examples serve both documentation and testing goals diff --git a/.auxiliary/configuration/claude/commands/cs-excise-python.md b/.auxiliary/configuration/claude/commands/cs-excise-python.md deleted file mode 100644 index d2731f1..0000000 --- a/.auxiliary/configuration/claude/commands/cs-excise-python.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(hatch --env develop run:*), Bash(git status), Bash(git diff), mcp__pyright__references, mcp__pyright__hover, mcp__pyright__diagnostics -description: Analyze Vulture dead code findings and remediate through selective removal or vulturefood.py whitelisting ---- - -# Python Dead Code Analysis and Remediation - -Systematically analyze Vulture dead code findings and remediate through selective removal or vulturefood.py whitelisting using Pyright MCP server for accurate symbol reference verification. - -Target files or scope: $ARGUMENTS - -## Context - -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` -- Existing vulturefood entries: !`wc -l .auxiliary/configuration/vulturefood.py` -- Vulture configuration: @pyproject.toml (tool.vulture section) - -## Prerequisites - -Before running this analysis, ensure: -- Understanding of project codebase and critical symbols -- Read project documentation guides: - - @.auxiliary/instructions/practices.rst - - @.auxiliary/instructions/style.rst -- Vulture is installed and configured in the development environment -- Pyright MCP server is available for symbol reference verification - -## Process Summary - -Key functional areas: -1. **Detection and Parsing**: Run Vulture and parse output for unused symbols -2. **Reference Verification**: Use Pyright MCP server to verify actual symbol usage -3. **Classification Analysis**: Apply heuristics to identify false positives vs. genuine dead code -4. **Selective Remediation**: Present findings with confidence levels for user decision -5. **Implementation**: Remove dead code or add entries to vulturefood.py as appropriate - -## Safety Requirements - -Stop and consult the user if: -- Uncertain about whether a symbol should be removed or whitelisted -- Complex inheritance hierarchies with unclear symbol usage patterns -- Vulture reports conflict significantly with Pyright reference analysis -- Ambiguous decorator patterns that don't fit standard heuristics - -## Execution - -Execute the following steps: - -### 1. Vulture Analysis and Parsing - -Run Vulture to identify potentially unused symbols: -```bash -hatch --env develop run vulture --min-confidence=60 -``` - -Examine the output to extract: -- Symbol names and types (functions, classes, variables) -- File locations and line numbers -- Confidence levels reported by Vulture -- Symbol categories (imports, definitions, assignments) - -### 2. Pyright Reference Verification - -For each symbol identified by Vulture, verify actual usage using Pyright MCP: - -Use `mcp__pyright__references` with **bare symbol names** (not qualified paths): -- Correct: `symbolName="function_name"` -- Incorrect: `symbolName="module.package.function_name"` - -Analyze reference results: -- No references found: Likely genuine dead code -- References found: Examine context for legitimacy -- Import-only references: May indicate transitional dead code - -### 3. False Positive Classification - -Apply systematic heuristics to identify false positives: - -**Common False Positive Patterns:** -- Abstract methods and protocol implementations -- Decorator-registered functions (pytest fixtures, Flask routes, etc.) -- Magic methods and dunder attributes -- CLI entry points and script main functions -- Test fixtures and utilities used via dynamic discovery -- Library interface methods called by external code - -**Analysis Criteria:** -- Decorator presence and types -- Inheritance relationships and abstract base classes -- Usage patterns in test files vs. main code -- External integration points and plugin systems - -### 4. Autonomous Decision Making - -Apply systematic decision logic: - -**Remove Symbol If:** -- No references found via Pyright (zero references) -- No TODO comments mentioning future use of the symbol -- Not an entry point function (e.g., `main`) -- Not part of unimplemented interface or abstract base class -- Not decorated with framework-specific decorators - -**Whitelist in vulturefood.py If:** -- Has references but appears to be false positive (decorators, abstract methods, etc.) -- Entry point functions like `main` -- Abstract/interface implementations -- Framework integration points with decorators -- Magic methods and protocol compliance - -**Check for TODO Comments:** -Examine surrounding code and docstrings for TODO comments that reference the symbol or indicate planned future usage. - -### 5. Implementation Decision - -Act autonomously based on decision logic: - -**For Symbol Removal:** -- Remove symbol definitions and any orphaned imports -- Verify removal doesn't break related functionality -- Run linters to ensure clean code after removal - -**For Vulturefood Whitelisting:** -- Add entries to `.auxiliary/configuration/vulturefood.py` with format: - ```python - symbol_name # description of why it's a false positive - ``` -- Group related entries and add explanatory comments -- Maintain alphabetical organization within groups - -### 6. Validation and Verification - -After remediation: -- Run Vulture again to confirm issues are resolved -- Execute linters to ensure code quality: `hatch --env develop run linters` -- Run tests to verify functionality: `hatch --env develop run testers` -- Verify git diff shows only intended changes - -## Implementation Notes - -**Pyright MCP Usage:** -- Use bare symbol names for accurate reference finding -- Leverage superior semantic analysis over text-based search tools -- Cross-reference hover information for additional context - -**Vulturefood Management:** -- Maintain clear documentation for each whitelisted symbol -- Group related false positives with explanatory sections -- Prefer descriptive comments over generic suppression - -**Safety Practices:** -- Remove code autonomously when decision criteria are clearly satisfied -- Prioritize false positive whitelisting when uncertainty exists -- Validate all changes through comprehensive testing \ No newline at end of file diff --git a/.auxiliary/configuration/claude/commands/cs-inquire.md b/.auxiliary/configuration/claude/commands/cs-inquire.md deleted file mode 100644 index 2fcfb6f..0000000 --- a/.auxiliary/configuration/claude/commands/cs-inquire.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -allowed-tools: Read, LS, Glob, Grep, WebFetch, WebSearch -description: Provide analytical responses and technical opinions without making code changes ---- - -# Technical Analysis and Discussion - -Provide analytical responses, technical opinions, and architectural discussion -based on user questions. Focus on analysis and reasoning without making code -modifications. - -User question or topic: $ARGUMENTS - -Stop and consult if: -- The request explicitly asks for code changes or implementation -- The question is unclear or lacks sufficient context -- Multiple conflicting requirements are presented - -## Prerequisites - -Before providing analysis, ensure: -- Clear understanding of the technical question being asked -- Sufficient context about the codebase or architecture being discussed - -## Process Summary - -Key analytical areas: -1. **Question Analysis**: Understand what is being asked and why -2. **Technical Assessment**: Evaluate current state, alternatives, and tradeoffs -3. **Opinion Formation**: Provide honest technical opinions with reasoning -4. **Discussion**: Present pros/cons, alternatives, and recommendations - -## Execution - -Execute the following process: - -### 1. Question Understanding -Carefully analyze the user's question to understand: -- What specific technical aspect they want to discuss -- The context and scope of their concern -- Whether they're seeking validation, alternatives, or general analysis - -### 2. Current State Assessment -Examine relevant parts of the codebase or architecture, if necessary: -- Read pertinent files to understand current implementation -- Identify patterns, conventions, and existing approaches -- Note any potential issues or areas of concern - -### 3. Technical Analysis -Provide comprehensive analysis including: -- **Strengths**: What works well in the current approach -- **Weaknesses**: Potential issues, limitations, or concerns -- **Alternatives**: Different approaches that could be considered -- **Tradeoffs**: Benefits and costs of different options - -### 4. Opinion and Recommendations -Offer honest technical opinions: -- Present your assessment based on best practices and experience -- Provide pushback if you disagree with assumptions or proposals -- Suggest better alternatives when they exist -- Explain the reasoning behind your recommendations - -### 5. Discussion Points -Raise additional considerations: -- Edge cases that might not have been considered -- Long-term maintenance implications -- Performance, security, or scalability concerns -- Integration with existing systems or patterns - -Remember: Your role is to analyze, discuss, and provide technical opinions - -not to implement solutions or make code changes. Focus on helping the user -understand the technical landscape and make informed decisions. diff --git a/.auxiliary/configuration/claude/commands/cs-manage-prd.md b/.auxiliary/configuration/claude/commands/cs-manage-prd.md deleted file mode 100644 index 31a74a2..0000000 --- a/.auxiliary/configuration/claude/commands/cs-manage-prd.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep -description: Manage product requirements documents and feature planning ---- - -# Product Requirements Management - -Manage and update the Product Requirements Document (PRD) based on user input -about product requirements, feature planning, and related topics. - -Request from user: $ARGUMENTS - -## Context - -- Current PRD state: @documentation/prd.rst -- Requirements guidelines: @.auxiliary/instructions/requirements.rst - -## Prerequisites - -Before managing PRD content, ensure: -- Understanding of current project scope and objectives -- Familiarity with existing functional and non-functional requirements -- @.auxiliary/instructions/requirements.rst guidelines are followed -- Changes align with overall project strategy - -## Process Summary - -Key functional areas: -1. **Analysis**: Review current PRD and understand requested changes -2. **Requirements Processing**: Apply requirements.rst standards to new content -3. **PRD Updates**: Make structured updates to documentation/prd.rst -4. **Validation**: Ensure consistency and completeness - -### Process Restrictions - -- Do not provide a timeline for deliverables. -- Do not plan sprints. - -## Safety Requirements - -Stop and consult the user if: -- Requested changes significantly expand or reduce product scope -- New requirements conflict with existing non-functional requirements -- Changes affect critical path features or constraints -- Requirements lack sufficient detail for implementation planning - -## Execution - -Execute the following steps: - -### 1. Review Current State -Read and analyze the existing PRD to understand current scope. - -### 2. Process User Requirements -Analyze the user input for: -- New functional requirements -- Changes to existing requirements -- Updates to goals, objectives, or success criteria -- Modifications to user personas or target users -- New constraints or assumptions - -### 3. Apply Requirements Standards -Follow @.auxiliary/instructions/requirements.rst guidelines: -- Use specific, measurable, achievable, relevant, testable criteria -- Apply proper user story format when appropriate -- Assign requirement priorities (Critical/High/Medium/Low) -- Include acceptance criteria for functional requirements -- Maintain requirement traceability - -### 4. Update PRD Structure -Make targeted updates to appropriate PRD sections: -- Executive Summary (if scope changes) -- Problem Statement (if new problems identified) -- Goals and Objectives (if success criteria change) -- Target Users (if new personas or needs identified) -- Functional Requirements (most common updates) -- Non-Functional Requirements (if technical requirements change) -- Constraints and Assumptions (if new limitations discovered) -- Out of Scope (if boundaries need clarification) - -### 5. Maintain Consistency -Ensure all updates maintain PRD coherence: -- Requirements align with stated goals and objectives -- No conflicts between functional and non-functional requirements -- User stories trace back to identified user needs -- Acceptance criteria are testable and specific -- Priority assignments reflect user value - -### 6. Summarize Updates -Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/cs-plan-pytests.md b/.auxiliary/configuration/claude/commands/cs-plan-pytests.md deleted file mode 100644 index 72a2761..0000000 --- a/.auxiliary/configuration/claude/commands/cs-plan-pytests.md +++ /dev/null @@ -1,280 +0,0 @@ ---- -description: Analyze Python test coverage gaps and create focused test plan for uncovered areas and edge cases ---- - -# Plan Python Tests - -For systematic analysis of test coverage gaps and creation of detailed test -plans following project testing guidelines. - -Target module/functionality: $ARGUMENTS - -Focus on analysis and planning only - do not implement tests. - -## Context - -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` -- Current test coverage: !`hatch --env develop run coverage report --show-missing` -- Existing test structure: !`find tests -name "*.py" | head -20` -- Test organization: @documentation/architecture/testplans/summary.rst -- Test plans index: @documentation/architecture/testplans/index.rst - -## Prerequisites - -Ensure that you: -- Have access to target code modules for analysis -- Can generate current coverage reports -- Have read any relevant `CLAUDE.md` file -- Understand the test-writing guidelines: @.auxiliary/instructions/tests.rst - -## Safety Requirements - -Stop and consult the user if: -- No target module or functionality is provided -- Target code cannot be analyzed -- Coverage data is unavailable -- Coverage reports cannot be generated -- Target modules cannot be read or analyzed -- Analysis reveals fundamental testability issues -- Test guidelines cannot be accessed -- Network tests against real external sites are being considered - -**Your responsibilities:** -- Focus entirely on analysis and planning - NO implementation -- Create comprehensive, actionable test plans WITHOUT code snippets of test implementations -- Focus planning on uncovered areas and edge cases -- Brief third-party library examples (e.g., httpx mock transport) are acceptable if researched -- Identify all coverage gaps systematically -- Consider project testing philosophy: doctests for examples, pytest for edge cases -- Produce clear, structured planning artifacts -- Acknowledge immutability constraints - modules under test CANNOT be monkey-patched -- Test private functions/methods via public API - understand why if this fails - -## Test Planning Process - -Execute the following steps for target: $ARGUMENTS - -### 0. Pre-Flight Verification -Access test-writing guidelines: - -Read and understand the complete project guidelines: -- Testing: @.auxiliary/instructions/tests.rst -- Python Practices: @.auxiliary/instructions/practices-python.rst - -You must successfully access and understand both guides before proceeding. If any guide cannot be accessed, stop and inform the user. - -### 1. Coverage Analysis Phase - -**Generate and analyze current coverage data:** - -```bash -hatch --env develop run coverage report --show-missing -hatch --env develop run coverage html -``` - -Analysis requirements: -- Identify all uncovered lines in target modules -- Focus on uncovered lines and untested functionality -- Determine which edge cases and error paths are untested -- Note any pragma directives (# pragma: no cover) and their rationale - -### 1.5. Example Coverage Analysis - -**Review existing documentation examples:** - -Survey documentation examples to understand what's already demonstrated: -- Read relevant example files in `documentation/examples/` if they exist -- Identify code paths already exercised by user-focused examples -- Note which functionality is already well-demonstrated through practical scenarios -- Focus pytest planning on genuinely uncovered areas not addressed by examples -- Avoid redundant testing of functionality that examples already exercise - -**Integration with pytest planning:** -- Complement rather than duplicate example coverage -- Target edge cases and error conditions that examples don't demonstrate -- Focus on defensive code paths and boundary conditions -- Plan systematic coverage of areas examples don't naturally reach - -**For each target module:** -- Read the source code to understand the public API -- Identify all functions, classes, and methods -- Map uncovered lines to specific functionality -- Note dependency injection points and testability patterns - -### 2. Gap Identification Phase - -**Systematically catalog what needs testing:** - -**Functionality Gaps:** -- Public functions with zero test coverage -- Classes with untested public methods -- Error handling paths not exercised -- Edge cases not covered - -**Coverage Gaps:** -- Specific line numbers needing coverage -- Branch conditions not tested -- Exception handling paths missed -- Integration scenarios untested - -**Architecture Gaps:** -- Code that requires dependency injection for testability -- Components that need filesystem mocking -- External service interactions requiring test doubles -- Private functions/methods not exercisable via public API -- Areas where full coverage may require violating immutability constraints -- Test data requirements (fixtures, snapshots, fake packages for `tests/data/`) - -### 3. Test Strategy Development - -**For each identified gap, determine:** - -**Test Approach:** -- Which testing patterns apply (dependency injection, pyfakefs, etc.) -- What test doubles or fixtures are needed -- How to structure tests for maximum coverage - -**Test Categories:** -- Basic functionality tests (000-099 range) -- Component-specific tests (100+ blocks per function/class/method) -- Edge cases and error handling (integrated within component blocks) - -**Implementation Considerations:** -- Dependencies that need injection -- Filesystem operations requiring pyfakefs -- External services needing mocking (NEVER test against real external sites) -- Test data and fixtures needed under `tests/data/` -- Performance considerations - -### 4. Test Organization Planning - -**Determine test structure and numbering:** - -**Review existing test numbering conventions:** -- Analyze current test file naming patterns -- Identify next available number blocks for new test modules -- Plan numbering for new test functions within modules - -Test module vs function numbering: -- **Test modules**: Named as `test_<N>00_<module>.py` (e.g., `test_100_exceptions.py`, `test_500_cli.py`) -- **Test functions**: Within modules use 000-099 basic, 100+ blocks per component -- These are DIFFERENT numbering schemes - do not confuse them - -**Test Module Numbering Hierarchy:** -- Lower-level functionality gets lower numbers (e.g., `test_100_exceptions.py`, `test_110_utilities.py`) -- Higher-level functionality gets higher numbers (e.g., `test_500_cli.py`, `test_600_server.py`) -- Subpackage modules: `test_<M><N>0_<subpackage>_<module>.py` where N advances by 10 within subpackage - -**Update test organization documentation:** -- Update `documentation/architecture/testplans/summary.rst` with test module numbering scheme -- Include project-specific testing conventions and new modules being planned -- Document rationale for any pattern exceptions -- Update during planning, not during implementation - -### 5. Plan Documentation Creation - -**Create comprehensive test plan document:** - -Save the plan to `documentation/architecture/testplans/[sanitized-module-name].rst` and update `documentation/architecture/testplans/index.rst` to include the new test plan in the toctree. - -Create the test plan document with: - -**Plan Structure (reStructuredText format):** -```rst -******************************************************************************* -Test Plan: [Module Name] -******************************************************************************* - -Coverage Analysis Summary -=============================================================================== - -- Current coverage: X% -- Target coverage: 100% -- Uncovered lines: [specific line numbers] -- Missing functionality tests: [list] - -Test Strategy -=============================================================================== - -Basic Functionality Tests (000-099) -------------------------------------------------------------------------------- - -- [List planned tests with brief descriptions] - -Component-Specific Tests (100+ blocks) -------------------------------------------------------------------------------- - -Function/Class/Method: [name] (Tests 100-199) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- [Planned test descriptions including happy path, edge cases, and error handling] -- [Dependencies needing injection] -- [Special considerations] - -Implementation Notes -=============================================================================== - -- Dependencies requiring injection: [list] -- Filesystem operations needing pyfakefs: [list] -- External services requiring mocking: [list - NEVER test against real external sites] -- Test data and fixtures: [needed under tests/data/ - fake packages, snapshots, captured artifacts] -- Private functions/methods not testable via public API: [list with analysis] -- Areas requiring immutability constraint violations: [list with recommendations] -- Third-party testing patterns to research: [e.g., httpx mock transport] -- Test module numbering for new files: [following hierarchy conventions] -- Anti-patterns to avoid: [specific warnings including external network calls] - -Success Metrics -=============================================================================== - -- Target line coverage: [percentage] -- Branch coverage goals: [percentage] -- Specific gaps to close: [line numbers] -``` - -### 6. Plan Validation - -**Review and validate the plan:** - -**Completeness Check:** -- All uncovered lines addressed -- All functions/classes have test strategy -- Error paths and edge cases included -- Integration scenarios covered - -**Feasibility Check:** -- All planned tests align with project principles -- No monkey-patching of internal code required -- Dependency injection patterns are viable -- Performance considerations addressed - -**Numbering Check:** -- Test numbering follows project conventions -- No conflicts with existing test numbers -- Logical organization by test type - -## Success Criteria - -Planning is complete when: -- [ ] Complete coverage analysis performed -- [ ] All testing gaps systematically identified -- [ ] Test strategy developed for each gap -- [ ] Test organization and numbering planned -- [ ] `documentation/architecture/testplans/summary.rst` updated as needed -- [ ] Comprehensive plan document created in testplans directory -- [ ] `documentation/architecture/testplans/index.rst` updated to include new plan -- [ ] Plan validates against project testing principles -- [ ] Implementation approach is clear and actionable - -## Final Report - -Upon completion, provide a brief summary covering: -- Current coverage percentage and specific gaps identified -- Number of new tests planned by category -- Key architectural considerations (dependency injection needs, etc.) -- Assessment: Areas where 100% coverage may be impossible without violating immutability constraints -- **PUSHBACK RECOMMENDATIONS**: Suggested architectural improvements to enable better testability -- Private functions/methods that cannot be exercised via public API and analysis of why -- Estimated complexity and implementation priority -- Any potential challenges or special considerations diff --git a/.auxiliary/configuration/claude/commands/cs-release-checkpoint.md b/.auxiliary/configuration/claude/commands/cs-release-checkpoint.md deleted file mode 100644 index 7199e10..0000000 --- a/.auxiliary/configuration/claude/commands/cs-release-checkpoint.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -allowed-tools: Bash(git status), Bash(git pull:*), Bash(git add:*), Bash(git commit:*), Bash(git tag:*), Bash(git push:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), Bash(date:*), LS, Read -description: Execute automated alpha checkpoint release with QA monitoring -argument-hint: "[alpha]" ---- - -# Release Checkpoint - -**NOTE: This is an experimental workflow! If anything seems unclear or missing, -please stop for consultation with the user.** - -For execution of an automated alpha checkpoint release on master branch. - -Below is a validated process to create an alpha checkpoint release with automated -monitoring and version increment. - -Target alpha increment: $ARGUMENTS -(optional - defaults to next alpha) - -Verify current version is alpha format if no arguments provided. - -Stop and consult if: -- Working directory has uncommitted changes -- Current version is not an alpha version (e.g., 1.3.0, 1.3rc1) and no target specified -- Git operations fail or produce unexpected output - -## Context - -- Current git status: !`git status` -- Current branch: !`git branch --show-current` -- Current version: !`hatch version` -- Recent commits: !`git log --oneline -10` - -## Prerequisites - -Before starting, ensure: -- GitHub CLI (`gh`) is installed and authenticated -- Working directory is clean with no uncommitted changes -- Currently on master branch -- Current version is an alpha version (e.g., 1.3a0) - -## Process Summary - -Key functional areas of the process: - -1. **Pre-Release Quality Check**: Run local QA to catch issues early -2. **Changelog Generation**: Run Towncrier to build changelog -3. **QA Monitoring**: Push commits and monitor QA workflow with GitHub CLI -4. **Tag Release**: Create alpha tag with current version after QA passes -5. **Release Monitoring**: Monitor release workflow deployment -6. **Post-Release Cleanup**: Remove news fragments and bump alpha version - -## Safety Requirements - -Stop and consult the user if any of the following occur: - -- **Step failures**: If any command fails, git operation errors, or tests fail -- **Workflow failures**: If QA or release workflows show failed jobs -- **Unexpected output**: If commands produce unclear or concerning results -- **Version conflicts**: If version bumps don't match expected patterns -- **Network issues**: If GitHub operations timeout or fail repeatedly - -**Your responsibilities**: -- Validate each step succeeds before proceeding to the next -- Monitor workflow status and halt on any failures -- Provide clear progress updates throughout the process -- Maintain clean git hygiene -- Use your judgment to assess when manual intervention is needed - -## Release Process - -Execute the following steps: - -### 1. Pre-Release Quality Check -Run local quality assurance to catch issues early: -```bash -git status && git pull origin master -hatch --env develop run linters -hatch --env develop run testers -hatch --env develop run docsgen -``` - -### 2. Changelog Generation -Run Towncrier to update changelog with current version: -```bash -hatch --env develop run towncrier build --keep --version $(hatch version) -git commit -am "Update changelog for v$(hatch version) release." -``` - -### 3. Quality Assurance Phase -Push commits and monitor QA workflow: -```bash -git push origin master -``` - -Workflow monitoring requirements: -After pushing, you MUST ensure you monitor the correct QA workflow run: - -1. **Wait for workflow trigger**: Wait 10 seconds after pushing to allow GitHub to trigger the workflow -2. **Verify correct workflow**: Use `gh run list --workflow=qa --limit=5` to list recent runs -3. **Check timestamps**: Compare the workflow creation time with your push time using `date --utc` -4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your push timestamp -5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow - -Once you've identified the correct QA run ID: -```bash -gh run watch <correct-qa-run-id> --interval 30 --compact -``` - -Do not proceed until workflow completes: -- Monitor QA workflow with `gh run watch` using the correct run ID -- Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands -- If command times out, immediately rerun `gh run watch` until completion -- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- Stop if any jobs fail - consult user before proceeding - -### 4. Alpha Release Deployment -**Verify QA passed before proceeding to alpha tag:** -```bash -git tag -m "Alpha checkpoint v$(hatch version)." v$(hatch version) -git push --tags -``` - -Release workflow monitoring requirements: -After pushing the tag, you MUST ensure you monitor the correct release workflow run: - -1. **Wait for workflow trigger**: Wait 10 seconds after pushing tags to allow GitHub to trigger the release workflow -2. **Verify correct workflow**: Use `gh run list --workflow=release --limit=5` to list recent runs -3. **Check timestamps**: Compare the workflow creation time with your tag push time using `date --utc` -4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your tag push timestamp -5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow - -Once you've identified the correct release run ID: -```bash -gh run watch <correct-release-run-id> --interval 30 --compact -``` - -Do not proceed until workflow completes: -- Monitor release workflow with `gh run watch` using the correct run ID -- Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands -- If command times out, immediately rerun `gh run watch` until completion -- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- Stop if any jobs fail - consult user before proceeding - -### 5. Post-Release Cleanup -Clean up Towncrier fragments: -```bash -git rm .auxiliary/data/towncrier/*.rst -git commit -m "Clean up news fragments." -``` - -### 6. Next Alpha Version -Bump to next alpha version: -```bash -hatch version alpha -git commit -am "Version: $(hatch version)" -``` - -### 7. Final Push -Push cleanup and version bump commits: -```bash -git push origin master -``` diff --git a/.auxiliary/configuration/claude/commands/cs-release-final.md b/.auxiliary/configuration/claude/commands/cs-release-final.md deleted file mode 100644 index 2854244..0000000 --- a/.auxiliary/configuration/claude/commands/cs-release-final.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -allowed-tools: Bash(git status), Bash(git pull:*), Bash(git checkout:*), Bash(git add:*), Bash(git commit:*), Bash(git tag:*), Bash(git rm:*), Bash(git cherry-pick:*), Bash(git log:*), Bash(git branch:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), LS, Read -description: Execute automated final release with QA monitoring and development cycle setup -argument-hint: "major.minor" ---- - -# Release Final - -**NOTE: This is an experimental workflow! If anything seems unclear or missing, -please stop for consultation with the user.** - -For execution of a fully-automated final release. - -Below is a validated process to create a final release with automated -monitoring and next development cycle setup. - -Target release version: $ARGUMENTS - -Verify exactly one target release version provided. - -Stop and consult if: -- No target release version is provided -- Multiple release versions provided (e.g., `1.6 foo bar`) -- Release version format doesn't match `X.Y` pattern (e.g., `1.6.2`, `1.6a0`) - -## Context - -- Current git status: !`git status` -- Current branch: !`git branch --show-current` -- Current version: !`hatch version` -- Recent commits: !`git log --oneline -10` -- Available towncrier fragments: !`ls .auxiliary/data/towncrier/*.rst 2>/dev/null || echo "No fragments found"` - -## Prerequisites - -Before starting, ensure: -- GitHub CLI (`gh`) is installed and authenticated -- For new releases: All changes are committed to `master` branch -- For existing release branches: Release candidate has been validated and tested -- Working directory is clean with no uncommitted changes -- Towncrier news fragments are present for the release enhancements - -## Process Summary - -Key functional areas of the process: - -1. **Branch Setup**: Create new release branch or checkout existing one -2. **Version Bump**: Set version to final release (major/minor/patch as appropriate) -3. **Update Changelog**: Run Towncrier to build final changelog -4. **QA Monitoring**: Push commits and monitor QA workflow with GitHub CLI -5. **Tag Release**: Create signed git tag after QA passes -6. **Release Monitoring**: Monitor release workflow deployment -7. **Cleanup**: Remove news fragments and cherry-pick back to master -8. **Next Development Cycle**: Set up master branch for next development version - -## Safety Requirements - -Stop and consult the user if any of the following occur: - -- **Step failures**: If any command fails, git operation errors, or tests fail -- **Workflow failures**: If QA or release workflows show failed jobs -- **Unexpected output**: If commands produce unclear or concerning results -- **Version conflicts**: If version bumps don't match expected patterns -- **Network issues**: If GitHub operations timeout or fail repeatedly - -**Your responsibilities**: -- Validate each step succeeds before proceeding to the next -- Monitor workflow status and halt on any failures -- Provide clear progress updates throughout the process -- Maintain clean git hygiene and proper branching -- Use your judgment to assess when manual intervention is needed - -## Release Process - -Execute the following steps: - -### 1. Pre-Release Quality Check -Run local quality assurance to catch issues early: -```bash -git status && git pull origin master -hatch --env develop run linters -hatch --env develop run testers -hatch --env develop run docsgen -``` - -### 2. Release Branch Setup -Determine release branch name from target version (e.g., `1.6` → `release-1.6`). - -**If release branch exists** (for RC→final conversion): -```bash -git checkout release-$ARGUMENTS -git pull origin release-$ARGUMENTS -``` - -**If creating new release branch**: -```bash -git checkout master && git pull origin master -git checkout -b release-$ARGUMENTS -``` - -### 3. Version Management -Set version to target release version: -```bash -hatch version $ARGUMENTS -git commit -am "Version: $(hatch version)" -``` - -### 4. Changelog Generation -```bash -hatch --env develop run towncrier build --keep --version $(hatch version) -git commit -am "Update changelog for v$(hatch version) release." -``` - -### 5. Quality Assurance Phase -Push branch and monitor QA workflow: -```bash -# Use -u flag for new branches, omit for existing -git push [-u] origin release-$ARGUMENTS -``` - -Workflow monitoring requirements: -After pushing, you MUST ensure you monitor the correct QA workflow run: - -1. **Wait for workflow trigger**: Wait 10 seconds after pushing to allow GitHub to trigger the workflow -2. **Verify correct workflow**: Use `gh run list --workflow=qa --limit=5` to list recent runs -3. **Check timestamps**: Compare the workflow creation time with your push time using `date --utc` -4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your push timestamp -5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow - -Once you've identified the correct QA run ID: -```bash -gh run watch <correct-qa-run-id> --interval 30 --compact -``` - -Do not proceed until workflow completes: -- Monitor QA workflow with `gh run watch` using the correct run ID -- Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands -- If command times out, immediately rerun `gh run watch` until completion -- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- Stop if any jobs fail - consult user before proceeding - -### 6. Release Deployment -**Verify QA passed before proceeding to release tag:** -```bash -git tag -m "Release v$(hatch version): <brief-description>." v$(hatch version) -git push --tags -``` - -Release workflow monitoring requirements: -After pushing the tag, you MUST ensure you monitor the correct release workflow run: - -1. **Wait for workflow trigger**: Wait 10 seconds after pushing tags to allow GitHub to trigger the release workflow -2. **Verify correct workflow**: Use `gh run list --workflow=release --limit=5` to list recent runs -3. **Check timestamps**: Compare the workflow creation time with your tag push time using `date --utc` -4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your tag push timestamp -5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow - -Once you've identified the correct release run ID: -```bash -gh run watch <correct-release-run-id> --interval 30 --compact -``` - -Do not proceed until workflow completes: -- Monitor release workflow with `gh run watch` using the correct run ID -- Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands -- If command times out, immediately rerun `gh run watch` until completion -- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- Stop if any jobs fail - consult user before proceeding - -### 7. Post-Release Cleanup -```bash -git rm .auxiliary/data/towncrier/*.rst -git commit -m "Clean up news fragments." -git push origin release-$ARGUMENTS -``` - -### 8. Master Branch Integration -Cherry-pick release commits back to master: -```bash -git checkout master && git pull origin master -git cherry-pick <changelog-commit-hash> -git cherry-pick <cleanup-commit-hash> -git push origin master -``` - -### 9. Next Development Cycle (Major/Minor Releases Only) -Set up next development version: -```bash -hatch version minor,alpha -git commit -am "Start of development for release $(hatch version | sed 's/a[0-9]*$//')." -git tag -m "Start of development for release $(hatch version | sed 's/a[0-9]*$//')." "i$(hatch version | sed 's/a[0-9]*$//')" -git push origin master --tags -``` - -**Note**: Use `git log --oneline` to identify commit hashes for cherry-picking. diff --git a/.auxiliary/configuration/claude/commands/cs-release-maintenance.md b/.auxiliary/configuration/claude/commands/cs-release-maintenance.md deleted file mode 100644 index b61e493..0000000 --- a/.auxiliary/configuration/claude/commands/cs-release-maintenance.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -allowed-tools: Bash(git status), Bash(git pull:*), Bash(git checkout:*), Bash(git commit:*), Bash(git tag:*), Bash(git rm:*), Bash(git cherry-pick:*), Bash(git log:*), Bash(git branch:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(hatch version:*), Bash(hatch --env develop run:*), Bash(echo:*), Bash(ls:*), Bash(grep:*), LS, Read -description: Execute automated patch release with QA monitoring and master integration -argument-hint: "major.minor" ---- - -# Release Patch - -**NOTE: This is an experimental workflow! If anything seems unclear or missing, -please stop for consultation with the user.** - -For execution of a fully-automated postrelease patch. - -Below is a validated process to create patch releases with automated monitoring -and clean integration back to master. - -Target release version: $ARGUMENTS -(e.g., `1.24`, `2.3`) - -Verify exactly one target release version provided. - -Stop and consult if: -- No target release version is provided -- Multiple release versions provided (e.g., `1.6 foo bar`) -- Release version format doesn't match `X.Y` pattern (e.g., `1.6.2`, `1.6a0`) - -## Context - -- Current git status: !`git status` -- Current branch: !`git branch --show-current` -- Current version: !`hatch version` -- Recent commits: !`git log --oneline -10` -- Available towncrier fragments: !`ls .auxiliary/data/towncrier/*.rst 2>/dev/null || echo "No fragments found"` - -## Prerequisites - -Before running this command, ensure: -- GitHub CLI (`gh`) is installed and authenticated -- Release branch exists for the target version (e.g., `release-1.24` for version `1.24`) -- Working directory is clean with no uncommitted changes -- Towncrier news fragments are present for the patch changes - -## Process Summary - -Key functional areas of the process: - -1. **Branch Setup**: Checkout and update the appropriate release branch -2. **Version Bump**: Increment to next patch version with `hatch version patch` -3. **Update Changelog**: Run Towncrier to build patch changelog -4. **QA Monitoring**: Push commits and monitor QA workflow with GitHub CLI -5. **Tag Release**: Create signed git tag after QA passes -6. **Release Monitoring**: Monitor release workflow deployment -7. **Cleanup**: Remove news fragments and cherry-pick back to master - -## Safety Requirements - -Stop and consult the user if any of the following occur: - -- **Step failures**: If any command fails, git operation errors, or tests fail -- **Workflow failures**: If QA or release workflows show failed jobs -- **Version conflicts**: If patch version doesn't match expected patterns -- **Branch issues**: If release branch doesn't exist or is in unexpected state -- **Network issues**: If GitHub operations timeout or fail repeatedly - -**Your responsibilities**: -- Validate each step succeeds before proceeding to the next -- Monitor workflow status and halt on any failures -- Provide clear progress updates throughout the process -- Maintain clean git hygiene and proper branching -- Use your judgment to assess when manual intervention is needed - -## Release Process - -Execute the following steps: - -### 1. Pre-Release Quality Check -Run local quality assurance to catch issues early: -```bash -git status && git pull origin master -hatch --env develop run linters -hatch --env develop run testers -hatch --env develop run docsgen -``` - -### 2. Release Branch Setup -Checkout the target release branch: -```bash -git checkout release-$ARGUMENTS -git pull origin release-$ARGUMENTS -``` - -### 3. Patch Integration -**Determine patch location and integrate if needed:** - -### 3.1. Identify Patch Commits -Before cherry-picking, identify which commits contain actual patch fixes vs. maintenance: - -```bash -git log --oneline master -git log --graph --oneline master --since="1 month ago" -# Show commits on master not on release branch -git log --oneline release-$ARGUMENTS..master --since="1 month ago" -``` - -**IMPORTANT** -- Do **not** cherry-pick commits which were previously cherry-picked onto the - branch. -- Look at the Towncrier news fragments to help you decide what to pick. - -**Patch commits** (always cherry-pick): -- Bug fixes -- Security patches -- Critical functionality fixes - -**Maintenance commits** (evaluate case-by-case): -- Template updates -- Dependency bumps -- Documentation changes - -Use `git show <commit>` to review each commit's content before deciding. - -**If patches were developed on master** (cherry-pick to release branch): -```bash -# Cherry-pick patch commits from master to release branch -# Use git log --oneline master to identify relevant commit hashes -git cherry-pick <patch-commit-hash-1> -git cherry-pick <patch-commit-hash-2> -# Repeat for all patch commits -``` - -**If patches were developed on release branch**: Skip this step - patches are already present. - -### 4. Pre-Release Validation -Run linting to catch issues before formal release process: -```bash -hatch --env develop run linters -``` -Stop if any linting errors - fix issues before proceeding. - -### 5. Version Management -Increment to next patch version: -```bash -hatch version patch -git commit -am "Version: $(hatch version)" -``` - -### 6. Changelog Generation -```bash -hatch --env develop run towncrier build --keep --version $(hatch version) -git commit -am "Update changelog for v$(hatch version) patch release." -``` - -### 7. Quality Assurance Phase -Push branch and monitor QA workflow: -```bash -git push origin release-$ARGUMENTS -``` - -Workflow monitoring requirements: -After pushing, you MUST ensure you monitor the correct QA workflow run: - -1. **Wait for workflow trigger**: Wait 10 seconds after pushing to allow GitHub to trigger the workflow -2. **Verify correct workflow**: Use `gh run list --workflow=qa --limit=5` to list recent runs -3. **Check timestamps**: Compare the workflow creation time with your push time using `date --utc` -4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your push timestamp -5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow - -Once you've identified the correct QA run ID: -```bash -gh run watch <correct-qa-run-id> --interval 30 --compact -``` - -Do not proceed until workflow completes: -- Monitor QA workflow with `gh run watch` using the correct run ID -- Use `timeout: 300000` (5 minutes) parameter in Bash tool for monitoring commands -- If command times out, immediately rerun `gh run watch` until completion -- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- Stop if any jobs fail - consult user before proceeding - -### 8. Release Deployment -**Verify QA passed before proceeding to release tag:** -```bash -git tag -m "Release v$(hatch version) patch: <brief-description>." v$(hatch version) -git push --tags -``` - -Release workflow monitoring requirements: -After pushing the tag, you MUST ensure you monitor the correct release workflow run: - -1. **Wait for workflow trigger**: Wait 10 seconds after pushing tags to allow GitHub to trigger the release workflow -2. **Verify correct workflow**: Use `gh run list --workflow=release --limit=5` to list recent runs -3. **Check timestamps**: Compare the workflow creation time with your tag push time using `date --utc` -4. **Ensure fresh run**: Only monitor a workflow run that was created AFTER your tag push timestamp -5. **If no new run appears**: Wait additional time and check again - do NOT assume an old completed run is your workflow - -Once you've identified the correct release run ID: -```bash -gh run watch <correct-release-run-id> --interval 30 --compact -``` - -Do not proceed until workflow completes: -- Monitor release workflow with `gh run watch` using the correct run ID -- Use `timeout: 600000` (10 minutes) parameter in Bash tool for monitoring commands -- If command times out, immediately rerun `gh run watch` until completion -- Only proceed to next step after seeing "✓ [workflow-name] completed with 'success'" -- Stop if any jobs fail - consult user before proceeding - -### 9. Post-Release Cleanup -```bash -git rm .auxiliary/data/towncrier/*.rst -git commit -m "Clean up news fragments." -git push origin release-$ARGUMENTS -``` - -### 10. Master Branch Integration -Cherry-pick commits back to master based on patch development location: - -**If patches were developed on master**: Cherry-pick changelog and cleanup commits: -```bash -git checkout master && git pull origin master -git cherry-pick <changelog-commit-hash> -git cherry-pick <cleanup-commit-hash> -git push origin master -``` - -**If patches were developed on release branch**: Cherry-pick patch, changelog, and cleanup commits: -```bash -git checkout master && git pull origin master -git cherry-pick <patch-commit-hash-1> -git cherry-pick <patch-commit-hash-2> -# Repeat for all patch commits -git cherry-pick <changelog-commit-hash> -git cherry-pick <cleanup-commit-hash> -git push origin master -``` - -**Note**: Use `git log --oneline` to identify commit hashes for cherry-picking. diff --git a/.auxiliary/configuration/claude/commands/cs-review-todos.md b/.auxiliary/configuration/claude/commands/cs-review-todos.md deleted file mode 100644 index 1a2e423..0000000 --- a/.auxiliary/configuration/claude/commands/cs-review-todos.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep, Bash(find:*), Bash(ls:*), Bash(wc:*) -description: Systematically find, categorize, and analyze TODO comments for technical debt management ---- - -# Technical Debt Review - -Systematically find, categorize, and analyze TODO comments across the project -codebase to provide actionable insights about technical debt and outstanding -work items. - -Filter criteria and analysis focus: $ARGUMENTS -(if blank, then consider entire project) - -## Context - -- Notes: @.auxiliary/notes -- Project architecture: @documentation/architecture/summary.rst -- Project designs: @documentation/architecture/designs - -## Prerequisites - -Before running this analysis, ensure: -- Understanding of project structure and file organization -- Access to both source code and auxiliary documentation - -## Process Summary - -Key functional areas: -1. **Discovery**: Search for TODO/FIXME/XXX/HACK comments across all relevant files -2. **Categorization**: Organize findings by urgency, component, and type -3. **Analysis**: Assess technical debt impact and provide prioritization insights -4. **Reconciliation**: Compare source code TODOs with tracking documents -5. **Reporting**: Generate actionable summary with recommended next steps - -## Safety Requirements - -Stop and consult the user if: -- Large volume of TODOs (>100) found that may require batch processing -- Inconsistencies between tracking documents and source code require manual review -- File access permissions prevent comprehensive analysis - -## Execution - -Execute the following steps: - -### 1. Comprehensive TODO Discovery - -Search for all TODO-style comments across the project: -- Use Grep to find TODO, FIXME, XXX, HACK, NOTE patterns -- Search Python files, documentation, configuration files -- Include both inline comments and dedicated TODO sections -- Capture surrounding context (3-5 lines) for each finding - -### 2. Pattern Analysis and Categorization - -Analyze discovered TODOs for: -- **Urgency indicators**: Words like "urgent", "critical", "before release", "security" -- **Component classification**: Group by module, file, or functional area -- **Type classification**: Bug fix, feature enhancement, refactoring, documentation -- **Age estimation**: Check git blame for when TODO was introduced - -### 3. Auxiliary Document Review - -Examine TODO tracking files in `.auxiliary/notes/`: -- Read any existing TODO tracking documents -- Compare with source code findings -- Identify completed items that should be removed -- Note discrepancies between tracking and actual code state - -### 4. Priority Assessment - -Evaluate each TODO for: -- **Business impact**: Customer-facing vs. internal improvements -- **Technical risk**: Potential for bugs, security issues, or maintenance burden -- **Implementation complexity**: Quick fixes vs. architectural changes -- **Dependencies**: Items blocking other work vs. standalone improvements - -### 5. Reporting and Recommendations - -Generate structured output including: -- **Executive summary**: Total count, high-priority items, key themes -- **Categorized listings**: Organized by urgency, component, and type -- **Urgent actions**: Items requiring immediate attention -- **Cleanup opportunities**: Completed or obsolete TODOs to remove -- **Tracking reconciliation**: Sync recommendations between documents and code -- **Next steps**: Prioritized action plan for technical debt reduction - -### 6. Documentation Updates - -When appropriate: -- Update or create TODO tracking documents in `.auxiliary/notes/` -- Remove completed TODO comments from source code -- Add context or priority indicators to ambiguous TODOs -- Standardize TODO format across the project - -### 7. Summary Report - -Provide comprehensive analysis including: -- Total technical debt inventory -- Risk assessment of critical items -- Recommended prioritization for next development cycles -- Maintenance suggestions for keeping TODO management current diff --git a/.auxiliary/configuration/claude/commands/cs-update-command.md b/.auxiliary/configuration/claude/commands/cs-update-command.md deleted file mode 100644 index a9a9272..0000000 --- a/.auxiliary/configuration/claude/commands/cs-update-command.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -allowed-tools: Read, Write, Edit, MultiEdit, LS, Glob, Grep -description: Update existing slash command with missing instructions or reinforced guidance ---- - -# Update Slash Process - -Update an existing custom slash command to address missing instructions, -reinforce guidance which LLMs are ignoring, add missing tool permissions, or -make structural improvements. - -Target command and instructions: $ARGUMENTS - -Stop and consult if: -- The target file doesn't exist or isn't a slash command -- Major structural changes are requested that would fundamentally alter the command purpose -- Changes conflict with established project patterns - -## Context - -- Command template: @.auxiliary/configuration/claude/miscellany/command-template.md -- Project conventions: @.auxiliary/configuration/conventions.md - -## Prerequisites - -Before updating the command, ensure: -- Clear understanding of what improvements are needed -- Target file exists and is accessible -- Any referenced files or patterns are available -- Changes align with project conventions and existing process patterns - -## Process Summary - -Key functional areas: -1. **Analysis**: Read current command and identify improvement areas -2. **Content Updates**: Add missing instructions or reinforce existing guidance -3. **Structure Review**: Consider organizational improvements when appropriate -4. **Tone Refinement**: Ensure professional language without excessive emphasis -5. **Validation**: Verify updates maintain command effectiveness - -## Safety Requirements - -Stop and consult the user if: -- Process changes would break existing workflows or dependencies -- Updates conflict with established project conventions -- Structural modifications require significant rework of command logic - -## Execution - -Execute the following steps: - -### 1. Command Analysis -Read and analyze the current command: -- Review existing content, structure, and tool permissions -- Identify areas needing improvement or reinforcement -- Assess tone and language for professional standards -- Note any missing instructions or unclear guidance - -### 2. Content Enhancement -Apply requested improvements: -- Add missing instructions where gaps are identified -- Reinforce guidance that needs stronger emphasis -- Remove excessive bold formatting or shouty language -- Eliminate redundant repetition within sections -- Ensure clear, actionable language throughout - -### 3. Structural Review -Consider organizational improvements: -- Evaluate section ordering and logical flow -- Improve prerequisites or context sections if needed -- Enhance command summary for clarity -- Adjust safety requirements as appropriate -- Ensure consistent formatting patterns - -### 4. Tool and Permission Updates -Review and adjust technical aspects: -- Verify allowed-tools are appropriate for updated functionality -- Check that `@`-references and shell command expansions are current -- Ensure any context commands have proper tool permissions to run (e.g., `Bash(ls:*)` for `ls` commands) -- Ensure context section provides relevant dynamic information -- Validate that command can execute with given permissions - -### 5. Professional Polish -Apply formatting and tone standards: -- Use professional headers without excessive emphasis -- Maintain clear, direct language without redundancy -- Ensure consistency with project conventions -- Remove any attention-grabbing formatting that isn't necessary -- Balance guidance strength with readability - -### 6. Validation and Summary -Complete the update command: -- Review updated content for completeness and clarity -- Verify all requested improvements have been addressed -- Ensure command maintains effectiveness while addressing issues -- Provide succinct summary of changes made to the user diff --git a/.auxiliary/configuration/claude/commands/cs-update-readme-rst.md b/.auxiliary/configuration/claude/commands/cs-update-readme-rst.md deleted file mode 100644 index 575954e..0000000 --- a/.auxiliary/configuration/claude/commands/cs-update-readme-rst.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -description: Analyze current project state and refresh manually-maintained sections of README.rst while preserving template content ---- - -# Update README Documentation - -Analyze the current project state and refresh the manually-maintained sections -of README.rst files while preserving auto-generated template content and -ensuring accuracy with actual project capabilities. - -User input: $ARGUMENTS - -## Context - -- Current git status: !`git status --porcelain` -- Project structure: !`ls -la` -- Current README: @README.rst -- Project metadata: @pyproject.toml -- Product requirements: @documentation/prd.rst -- Architecture overview: @documentation/architecture/filesystem.rst - -## Prerequisites - -Before updating README documentation, ensure: -- Current README.rst exists and is accessible -- Understanding of project's actual capabilities and features -- Access to project metadata and configuration files - -## Process Summary - -Key functional areas: -1. **Content Analysis**: Examine current README and identify TODO sections needing updates -2. **Project Assessment**: Analyze actual capabilities from code, CLI, and configuration -3. **Content Generation**: Create compelling descriptions, features, and examples based on real functionality -4. **Validation**: Ensure all claims and examples match actual project capabilities - -## Safety Requirements - -Stop and consult the user if: -- README.rst cannot be read or is missing critical structure -- Template boundaries are unclear or may be damaged -- Project capabilities cannot be determined from available sources -- Generated examples cannot be validated against actual implementation -- Significant structural changes to README are required beyond content updates - -All template-rendered sections must be preserved without modification; these -include: badges, installation, contribution, flair - - -## Execution - -Execute the following steps: - -### 1. README Analysis -Read and analyze the current README structure: -- Examine existing README.rst for TODO markers and outdated content -- Identify template-generated sections that must be preserved -- Map sections that need manual content updates -- Note existing manual content that should be retained - -### 2. Project Capability Assessment -Analyze the actual project functionality: -- Extract project metadata from pyproject.toml (name, description, dependencies) -- Read PRD document if available for project goals and features -- Examine source code structure to understand API capabilities -- Test CLI functionality if enabled to document actual usage patterns -- Review configuration files and scripts for additional capabilities - -### 3. Content Generation Strategy -Plan content updates based on project analysis: -- Draft compelling project description with emoji prefix (e.g., 🔧, 📊, 🌐, 🎯) matching project purpose -- Identify key features based on actual implementation -- Plan 1-2 concise examples that whet appetites without overwhelming -- Avoid advanced showcase examples - focus on core value demonstration -- Consider additional sections (Use Cases, Motivation, Configuration) appropriate for project complexity -- Ensure content accuracy and professional tone - -### 4. README Content Updates -Update manual sections while preserving template content: -- Replace ".. todo:: Provide project description" with emoji-prefixed compelling description -- Add or update "Key Features ⭐" section with bullet points of actual capabilities -- Generate concise "Examples 💡" section with 1-2 essential usage patterns only -- Keep examples minimal and focused on core value, not comprehensive showcase -- Add relevant sections like "Use Cases", "Motivation", or "Configuration" as appropriate -- Preserve all template-generated sections (badges, installation, contribution, flair) - -### 5. Content Validation -Verify accuracy of all updated content: -- Test all code examples for correctness with current codebase -- Verify feature claims are supported by actual implementation -- Check that installation instructions match project configuration -- Ensure RST formatting is correct and consistent -- Validate examples are concise and appetite-whetting, not overwhelming -- Confirm README length is appropriate for project complexity - -### 6. Final Review -Complete final validation and formatting: -- Review entire README for consistency and professional presentation -- Ensure all TODO markers have been appropriately addressed -- Verify template boundaries are intact and respected -- Confirm examples are executable and accurate -- Check that content maintains engaging tone while being factually correct - -### 7. Summarize Updates -Provide concise summary of updates to the user. diff --git a/.auxiliary/configuration/claude/commands/validate-custom-slash.md b/.auxiliary/configuration/claude/commands/validate-custom-slash.md deleted file mode 100644 index 2540aca..0000000 --- a/.auxiliary/configuration/claude/commands/validate-custom-slash.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -allowed-tools: Bash(git status), Bash(git branch:*), Bash(git log:*), Bash(hatch version:*), Bash(echo:*), Bash(ls:*), Bash(pwd), LS, Read -description: Validate custom slash command functionality with context and permissions ---- - -# Validate Custom Slash Command - -Test script to validate custom slash command functionality, permissions, and context interpolation. - -Test argument: $ARGUMENTS - -## Context - -- Current directory: !`pwd` -- Current git status: !`git status --porcelain` -- Current branch: !`git branch --show-current` -- Current version: !`hatch version` -- Recent commits: !`git log --oneline -5` -- Template files: !`ls template/.auxiliary/configuration/claude/commands/` - -## Validation Tasks - -1. **Report the test argument**: Look at the "Test argument:" line above and tell me what value you see there -2. **Test basic git commands**: Run `git status` and `git branch --show-current` -3. **Test hatch command**: Run `hatch version` -4. **Test file operations**: Use LS tool to list current directory contents -5. **Test restricted command**: Attempt `git push` (should be blocked and require approval) - -## Expected Results - -- Context should be populated with current state -- Allowed commands should execute successfully -- `git push` should be blocked - -## Your Task - -Execute the validation tasks above and provide a summary report including: -- The interpolated argument value you see on the "Test argument:" line -- Results of each allowed command -- Confirmation that restricted commands are properly blocked -- Any observations about the command execution experience diff --git a/.auxiliary/configuration/coders/claude/.gitignore b/.auxiliary/configuration/coders/claude/.gitignore new file mode 100644 index 0000000..6a5eba4 --- /dev/null +++ b/.auxiliary/configuration/coders/claude/.gitignore @@ -0,0 +1,4 @@ +settings.local.json +agents/ +commands/ +skills/ diff --git a/.auxiliary/configuration/coders/claude/miscellany/bash-tool-bypass b/.auxiliary/configuration/coders/claude/miscellany/bash-tool-bypass new file mode 100755 index 0000000..223ea01 --- /dev/null +++ b/.auxiliary/configuration/coders/claude/miscellany/bash-tool-bypass @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +""" +Command wrapper for Claude Code web environments. + +This script wraps command execution via Python subprocess to bypass +Bash tool permission restrictions in Claude Code web environments. + +Usage: + bash-tool-bypass <command> [arguments...] + +Examples: + bash-tool-bypass gh --version + bash-tool-bypass gh pr view 1 + bash-tool-bypass gh pr list --limit 5 + bash-tool-bypass gh issue view 42 --json title,state,author + bash-tool-bypass gh repo view owner/repo + bash-tool-bypass some-other-restricted-command --flag value + +Notes: + - This wrapper is designed to bypass specific command restrictions in + Claude Code + - Common use case is running 'gh' commands when Bash tool blocks them + directly + - Any command accessible in PATH can be executed through this wrapper + - Authentication/permissions still apply to the wrapped command itself +""" + +import subprocess +import sys + +# Minimum required argument count (script name + command) +MIN_ARGS = 2 + + +def main(): + """Execute command via subprocess and exit with its return code.""" + if len(sys.argv) < MIN_ARGS: + print(__doc__) + sys.exit(1) + + # Build command with all arguments + cmd = sys.argv[1:] + + # Execute command (intentionally passes through untrusted input) + result = subprocess.run(cmd, check=False) # noqa: S603 + + # Exit with command's return code + sys.exit(result.returncode) + + +if __name__ == '__main__': + main() diff --git a/.auxiliary/configuration/claude/miscellany/command-template.md b/.auxiliary/configuration/coders/claude/miscellany/command-template.md similarity index 100% rename from .auxiliary/configuration/claude/miscellany/command-template.md rename to .auxiliary/configuration/coders/claude/miscellany/command-template.md diff --git a/.auxiliary/scripts/claude/post-edit-linter b/.auxiliary/configuration/coders/claude/scripts/post-edit-linter similarity index 72% rename from .auxiliary/scripts/claude/post-edit-linter rename to .auxiliary/configuration/coders/claude/scripts/post-edit-linter index 2237628..78b38d6 100755 --- a/.auxiliary/scripts/claude/post-edit-linter +++ b/.auxiliary/configuration/coders/claude/scripts/post-edit-linter @@ -14,6 +14,10 @@ import sys def main( ): # event = _acquire_event_data( ) + if not _is_command_available( 'hatch' ): + raise SystemExit( 0 ) + if not _is_hatch_env_available( 'develop' ): + raise SystemExit( 0 ) try: result = subprocess.run( [ 'hatch', '--env', 'develop', 'run', 'linters' ], # noqa: S607 @@ -47,7 +51,7 @@ def _acquire_event_data( ): def _emit_decision_json( decision, reason ): - ''' Output JSON decision for Claude Code hook system. ''' + ''' Outputs JSON decision for Claude Code hook system. ''' response = { "decision": decision, "reason": reason } print( json.dumps( response ) ) raise SystemExit( 2 ) @@ -58,6 +62,27 @@ def _error( message ): raise SystemExit( 2 ) +def _is_command_available( command ): + ''' Checks if a command is available in PATH. ''' + try: + result = subprocess.run( # noqa: S603 + [ 'which', command ], # noqa: S607 + capture_output = True, check = False, text = True, timeout = 5 ) + except Exception: return False + return result.returncode == 0 + + +def _is_hatch_env_available( env_name ): + ''' Checks if a specific Hatch environment exists. ''' + try: + result = subprocess.run( + [ 'hatch', 'env', 'show' ], # noqa: S607 + capture_output = True, check = False, text = True, timeout = 10 ) + except Exception: return False + if result.returncode != 0: return False + return env_name in result.stdout + + def _reactor_failure( message ): print( "Claude Code Hook Failure: {message}", file = sys.stderr ) raise SystemExit( 1 ) diff --git a/.auxiliary/scripts/claude/pre-bash-git-commit-check b/.auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check similarity index 88% rename from .auxiliary/scripts/claude/pre-bash-git-commit-check rename to .auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check index d6741d6..5ccb7e2 100755 --- a/.auxiliary/scripts/claude/pre-bash-git-commit-check +++ b/.auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check @@ -54,6 +54,17 @@ def _check_git_commit_command( tokens ): ): _error_with_divine_message( ) else: if result.returncode != 0: _error_with_divine_message( ) + try: + result = subprocess.run( + [ 'hatch', '--env', 'develop', 'run', 'vulture' ], # noqa: S607 + capture_output = True, text = True, timeout = 120, check = False ) + except ( + subprocess.TimeoutExpired, + subprocess.CalledProcessError, + FileNotFoundError + ): _error_with_divine_message( ) + else: + if result.returncode != 0: _error_with_divine_message( ) def _error_with_divine_message( ): @@ -63,6 +74,7 @@ def _error_with_divine_message( ): "commanded that:\n" "* Thy code shalt pass all lints before thy commit.\n" " Run: hatch --env develop run linters\n" + " Run: hatch --env develop run vulture\n" "* Thy code shalt pass all tests before thy commit.\n" " Run: hatch --env develop run testers\n\n" "(If you are in the middle of a large refactor, consider commenting " diff --git a/.auxiliary/scripts/claude/pre-bash-python-check b/.auxiliary/configuration/coders/claude/scripts/pre-bash-python-check similarity index 100% rename from .auxiliary/scripts/claude/pre-bash-python-check rename to .auxiliary/configuration/coders/claude/scripts/pre-bash-python-check diff --git a/.auxiliary/configuration/claude/settings.json b/.auxiliary/configuration/coders/claude/settings.json similarity index 69% rename from .auxiliary/configuration/claude/settings.json rename to .auxiliary/configuration/coders/claude/settings.json index cfdc6aa..b357187 100644 --- a/.auxiliary/configuration/claude/settings.json +++ b/.auxiliary/configuration/coders/claude/settings.json @@ -3,6 +3,7 @@ "BASH_DEFAULT_TIMEOUT_MS": 1800000, "BASH_MAX_TIMEOUT_MS": 1800000, "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": 1, + "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS": 1, "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": 1, "DISABLE_NON_ESSENTIAL_MODEL_CALLS": 1 }, @@ -13,12 +14,12 @@ "hooks": [ { "type": "command", - "command": ".auxiliary/scripts/claude/pre-bash-python-check", + "command": ".claude/scripts/pre-bash-python-check", "timeout": 10 }, { "type": "command", - "command": ".auxiliary/scripts/claude/pre-bash-git-commit-check", + "command": ".claude/scripts/pre-bash-git-commit-check", "timeout": 300 } ] @@ -30,7 +31,7 @@ "hooks": [ { "type": "command", - "command": ".auxiliary/scripts/claude/post-edit-linter", + "command": ".claude/scripts/post-edit-linter", "timeout": 60 } ] @@ -39,6 +40,15 @@ }, "permissions": { "auto_allow": [ + "mcp__agentmux__list", + "mcp__agentmux__look", + "mcp__agentmux__send", + "mcp__context7__get-library-docs", + "mcp__context7__resolve-library-id", + "mcp__nb__help", + "mcp__nb__nb", + "Bash(hatch run *)", + "Bash(hatch --env develop run *)", "Bash(awk *)", "Bash(cat *)", "Bash(cut *)", @@ -60,18 +70,25 @@ "Bash(gh run list *)", "Bash(gh run view *)", "Bash(gh run watch *)", + "Bash(gh search issues *)", "Bash(gh status *)", "Bash(git add *)", "Bash(git branch *)", + "Bash(git checkout *)", "Bash(git diff *)", + "Bash(git fetch *)", "Bash(git log *)", + "Bash(git rebase *)", + "Bash(git restore *)", + "Bash(git rev-parse *)", "Bash(git show *)", - "Bash(git status)", + "Bash(git stash list)", + "Bash(git stash list *)", + "Bash(git status *)", "Bash(grep *)", - "Bash(hatch run python *)", - "Bash(hatch --env develop run *)", "Bash(head *)", "Bash(ls *)", + "Bash(mkdir *)", "Bash(ps *)", "Bash(pwd *)", "Bash(rg *)", @@ -80,15 +97,11 @@ "Bash(tail *)", "Bash(uniq *)", "Bash(wc *)", - "Bash(which *)", - "mcp__context7__get-library-docs", - "mcp__context7__resolve-library-id", - "mcp__pyright__definition", - "mcp__pyright__diagnostics", - "mcp__pyright__edit_file", - "mcp__pyright__hover", - "mcp__pyright__references", - "mcp__pyright__rename_symbol" + "Bash(which *)" ] + }, + "sandbox": { + "enabled": false, + "autoAllowBashIfSandboxed": true } } diff --git a/.auxiliary/configuration/coders/codex/.gitignore b/.auxiliary/configuration/coders/codex/.gitignore new file mode 100644 index 0000000..b474d25 --- /dev/null +++ b/.auxiliary/configuration/coders/codex/.gitignore @@ -0,0 +1 @@ +skills/ diff --git a/.auxiliary/configuration/coders/codex/config.toml b/.auxiliary/configuration/coders/codex/config.toml new file mode 100644 index 0000000..f345985 --- /dev/null +++ b/.auxiliary/configuration/coders/codex/config.toml @@ -0,0 +1,53 @@ +model = "gpt-5.3-codex" +model_reasoning_effort = "high" +personality = "friendly" + +approval_policy = "on-request" +sandbox_mode = "workspace-write" +approvals_reviewer = "user" +trust_level = "trusted" +# web_search = "cached" + +[features] +# Temporary mitigation for Bubblewrap/symlink approval-loop regressions. +# Related issues: #14936 and #14694. +use_legacy_landlock = true +# Keep reviewer routing explicit: always surface approval requests to user. +guardian_approval = false + +## MCP Servers + +[mcp_servers.agentmux] +command = "agentmux" +args = ["host", "mcp", "--bundle", "python-foundation"] +startup_timeout_sec = 60 +tool_timeout_sec = 900 + +[mcp_servers.agentmux.tools.list] +approval_mode = "approve" + +[mcp_servers.agentmux.tools.look] +approval_mode = "approve" + +[mcp_servers.agentmux.tools.send] +approval_mode = "approve" + +[mcp_servers.context7] +command = "npx" +args = ["-y", "@upstash/context7-mcp"] + +[mcp_servers.context7.tools.query_docs] +approval_mode = "approve" + +[mcp_servers.context7.tools.resolve_library_id] +approval_mode = "approve" + +[mcp_servers.nb] +command = "nb-mcp" +args = ["--notebook", "python-classcore", "--no-commit-signing"] + +[mcp_servers.nb.tools.help] +approval_mode = "approve" + +[mcp_servers.nb.tools.nb] +approval_mode = "approve" diff --git a/.auxiliary/configuration/coders/codex/rules/emcd.rules b/.auxiliary/configuration/coders/codex/rules/emcd.rules new file mode 100644 index 0000000..9234325 --- /dev/null +++ b/.auxiliary/configuration/coders/codex/rules/emcd.rules @@ -0,0 +1,105 @@ + + + +# Codex rules (Starlark): allow safe development commands outside the sandbox. +# +# Generated from the emcd-agents Copier template. +# +# Notes: +# - Rules match an argv *prefix* (not globs). For example: +# pattern=["git", "diff"] matches "git diff --stat". +# - We intentionally do NOT allow `git push` here. + +# Common filesystem and text utilities. +prefix_rule(pattern=["awk"], decision="allow") +prefix_rule(pattern=["cat"], decision="allow") +prefix_rule(pattern=["cut"], decision="allow") +prefix_rule(pattern=["df"], decision="allow") +prefix_rule(pattern=["du"], decision="allow") +prefix_rule(pattern=["echo"], decision="allow") +prefix_rule(pattern=["file"], decision="allow") +prefix_rule(pattern=["find"], decision="allow") +prefix_rule(pattern=["grep"], decision="allow") +prefix_rule(pattern=["head"], decision="allow") +prefix_rule(pattern=["ls"], decision="allow") +prefix_rule(pattern=["mkdir"], decision="allow") +prefix_rule(pattern=["ps"], decision="allow") +prefix_rule(pattern=["pwd"], decision="allow") +prefix_rule(pattern=["rg"], decision="allow") +prefix_rule(pattern=["sed"], decision="allow") +prefix_rule(pattern=["sort"], decision="allow") +prefix_rule(pattern=["tail"], decision="allow") +prefix_rule(pattern=["uniq"], decision="allow") +prefix_rule(pattern=["wc"], decision="allow") +prefix_rule(pattern=["which"], decision="allow") + +# Git commands used by normal development workflows. +prefix_rule( + pattern=["git", ["add", "branch", "checkout", "commit", "diff", "log", "restore", "show", "status", "tag"]], + decision="allow", + justification="Common local git workflow commands.", +) +prefix_rule( + pattern=["git", ["fetch", "rebase", "rev-parse"]], + decision="allow", + justification="Common git maintenance commands.", +) +prefix_rule( + pattern=["git", "stash", "list"], + decision="allow", + justification="List git stashes.", +) + +# GitHub CLI (read-oriented operations). +prefix_rule( + pattern=["gh", "browse"], + decision="allow", + justification="Open browser for GitHub resources.", +) +prefix_rule( + pattern=["gh", "status"], + decision="allow", + justification="Show GitHub auth and repo status.", +) +prefix_rule( + pattern=["gh", "issue", ["list", "view"]], + decision="allow", + justification="Read GitHub issues.", +) +prefix_rule( + pattern=["gh", "pr", ["checks", "list", "view"]], + decision="allow", + justification="Read GitHub pull requests.", +) +prefix_rule( + pattern=["gh", "release", ["list", "view"]], + decision="allow", + justification="Read GitHub releases.", +) +prefix_rule( + pattern=["gh", "repo", ["list", "view"]], + decision="allow", + justification="Read GitHub repository metadata.", +) +prefix_rule( + pattern=["gh", "run", ["list", "view", "watch"]], + decision="allow", + justification="Read GitHub Actions runs.", +) +prefix_rule( + pattern=["gh", "search", "issues"], + decision="allow", + justification="Search GitHub issues.", +) + +# Python workflows. +prefix_rule( + pattern=["hatch", "run"], + decision="allow", + justification="Run Hatch-managed project tooling.", +) +prefix_rule( + pattern=["hatch", "--env", "develop", "run"], + decision="allow", + justification="Run Hatch-managed tooling in the develop environment.", +) diff --git a/.auxiliary/configuration/coders/gemini/.gitignore b/.auxiliary/configuration/coders/gemini/.gitignore new file mode 100644 index 0000000..71f5d3b --- /dev/null +++ b/.auxiliary/configuration/coders/gemini/.gitignore @@ -0,0 +1,2 @@ +commands/ +skills/ diff --git a/.auxiliary/configuration/coders/gemini/miscellany/command-template.md b/.auxiliary/configuration/coders/gemini/miscellany/command-template.md new file mode 100644 index 0000000..4cc453b --- /dev/null +++ b/.auxiliary/configuration/coders/gemini/miscellany/command-template.md @@ -0,0 +1,43 @@ + +# Process Title + +Brief introductory paragraph explaining the purpose. + +Target/input description: {{args}} + +## Context + +- Current state checks, if applicable: !{command1} +- Environment info, if applicable: !{command2} +- Relevant data, if applicable: !{command3} + +## Prerequisites + +Before running this process, ensure: +- Prerequisite 1 +- Prerequisite 2 +- @-references to relevant guides if applicable + +## Process Summary + +Key functional areas: +1. **Phase 1**: Description +2. **Phase 2**: Description +3. **Phase 3**: Description + +## Safety Requirements + +Stop and consult the user if: +- List validation conditions +- Error conditions that require user input +- Unexpected situations + +## Execution + +Execute the following steps: + +### 1. Step Name +Description of what this step does. + +### 2. Step Name +More steps as needed. diff --git a/.auxiliary/configuration/coders/gemini/policies/emcd.toml b/.auxiliary/configuration/coders/gemini/policies/emcd.toml new file mode 100644 index 0000000..6293856 --- /dev/null +++ b/.auxiliary/configuration/coders/gemini/policies/emcd.toml @@ -0,0 +1,83 @@ +# Gemini CLI Policy Engine rules. +# Mirrors prior tools.allowed behavior without using deprecated settings keys. + +[[rule]] +toolName = [ + "mcp__agentmux__list", + "mcp__agentmux__look", + "mcp__agentmux__send", + "mcp__context7__get-library-docs", + "mcp__context7__resolve-library-id", + "mcp__nb__help", + "mcp__nb__nb", + "edit", + "glob", + "google_web_search", + "list_directory", + "read_file", + "replace", + "save_memory", + "search_file_content", + "web_fetch", + "write_file", + "write_todos", +] +decision = "allow" +priority = 300 + +[[rule]] +toolName = "run_shell_command" +commandPrefix = [ + "hatch run", + "hatch --env develop run", + "awk", + "cat", + "cut", + "df", + "du", + "echo", + "file", + "find", + "gh browse", + "gh issue list", + "gh issue view", + "gh pr checks", + "gh pr list", + "gh pr view", + "gh release list", + "gh release view", + "gh repo list", + "gh repo view", + "gh run list", + "gh run view", + "gh run watch", + "gh search issues", + "gh status", + "git add", + "git branch", + "git checkout", + "git diff", + "git fetch", + "git log", + "git rebase", + "git restore", + "git rev-parse", + "git show", + "git stash list", + "git status", + "grep", + "head", + "ls", + "mkdir", + "ps", + "pwd", + "rg", + "sed", + "sort", + "tail", + "uniq", + "wc", + "which", +] +decision = "allow" +priority = 300 diff --git a/.auxiliary/configuration/coders/gemini/settings.json b/.auxiliary/configuration/coders/gemini/settings.json new file mode 100644 index 0000000..7a50f68 --- /dev/null +++ b/.auxiliary/configuration/coders/gemini/settings.json @@ -0,0 +1,54 @@ +{ + "ui": { + "showLineNumbers": true + }, + "tools": { + "autoAccept": true, + "core": [ + "mcp__agentmux__list", + "mcp__agentmux__look", + "mcp__agentmux__send", + "mcp__context7__get-library-docs", + "mcp__context7__resolve-library-id", + "mcp__nb__help", + "mcp__nb__nb", + "edit", + "glob", + "google_web_search", + "list_directory", + "read_file", + "replace", + "run_shell_command", + "save_memory", + "search_file_content", + "web_fetch", + "write_file", + "write_todos" + ] + }, + "general": { + "checkpointing": { + "enabled": true + } + }, + "mcpServers": { + "agentmux": { + "command": "agentmux", + "args": [ + "host", "mcp", + "--bundle", "python-foundation" + ] + }, + "context7": { + "command": "npx", + "args": [ "-y", "@upstash/context7-mcp" ] + }, + "nb": { + "command": "nb-mcp", + "args": [ + "--notebook", "python-classcore", + "--no-commit-signing" + ] + } + } +} diff --git a/.auxiliary/configuration/coders/opencode/.gitignore b/.auxiliary/configuration/coders/opencode/.gitignore new file mode 100644 index 0000000..22d0249 --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/.gitignore @@ -0,0 +1,8 @@ +agent/ +command/ +prompt/ +skills/ +node_modules/ +bun.lock +package.json +package-lock.json diff --git a/.auxiliary/configuration/coders/opencode/plugin/.gitignore b/.auxiliary/configuration/coders/opencode/plugin/.gitignore new file mode 100644 index 0000000..5ce0d1a --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/plugin/.gitignore @@ -0,0 +1,13 @@ +# Node.js dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# TypeScript build outputs +dist/ +*.tsbuildinfo + +# Bun +.bun.lockb +.bun-debug.log \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/README.md b/.auxiliary/configuration/coders/opencode/plugin/README.md new file mode 100644 index 0000000..e47f8ed --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/plugin/README.md @@ -0,0 +1,109 @@ +# Opencode Plugins for Quality Assurance + +This directory contains Opencode plugins that provide quality assurance and development workflow enforcement, ported from Claude Code hooks. + +## Plugins + +### ✅ 1. `post-edit-linter.js` (WORKING) +**Purpose**: Runs linters after file updates +**Event**: `tool.execute.after` (for `edit` tool) +**Behavior**: +- Checks if `hatch` command is available +- Checks if `develop` Hatch environment exists +- Runs `hatch --env develop run linters` +- Throws error with truncated output (50 lines max) if linters fail +- Early exit if conditions not met (hatch not available) +- **Note**: Uses `tool.execute.after` not `file.edited` (LLM-initiated edits don't trigger `file.edited`) + +### ⚠️ 2. `git-commit-guard.js-disabled` (DISABLED - Opencode bash tool limitation) +**Purpose**: Would prevent git commits when linters or tests fail +**Status**: **DISABLED** - Opencode's bash tool doesn't pass command in `input.args.command` +**Issue**: Plugin intercepts `tool.execute.before` but `input.args` is empty for bash tool +**Original intent**: Port of Claude Code hook `pre-bash-git-commit-check` + +### ⚠️ 3. `python-environment-guard.js-disabled` (DISABLED - Opencode bash tool limitation) +**Purpose**: Would detect improper Python usage in Bash commands +**Status**: **DISABLED** - Opencode's bash tool doesn't pass command in `input.args.command` +**Issue**: Plugin intercepts `tool.execute.before` but `input.args` is empty for bash tool +**Original intent**: Port of Claude Code hook `pre-bash-python-check` + +## Installation for Downstream Projects + +When this template is copied to a downstream project: + +1. **Navigate to the plugin directory**: + ```bash + cd .auxiliary/configuration/coders/opencode/plugin + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Ensure symlink exists**: + ```bash + # From project root + ln -sf .auxiliary/configuration/coders/opencode .opencode + ``` + +4. **Verify plugin loading**: + Opencode should automatically load plugins from `.opencode/plugin/` + +## Dependencies + +- `shlex`: Shell command parsing (port of Python's shlex module) - used in disabled plugins +- `bun`: Runtime (provided by Opencode) + +## Porting Notes + +These plugins are ports of Claude Code hooks with varying success: + +| Claude Code Hook | Opencode Plugin | Status | Key Changes | +|-----------------|----------------|--------|-------------| +| `post-edit-linter` | `post-edit-linter.js` | ✅ **WORKING** | Python → JavaScript, `subprocess` → Bun shell API, uses `tool.execute.after` not `file.edited` | +| `pre-bash-git-commit-check` | `git-commit-guard.js-disabled` | ⚠️ **DISABLED** | Tool name: `Bash` → `bash`, uses npm `shlex` package. **Issue**: Opencode bash tool doesn't pass command in `input.args.command` | +| `pre-bash-python-check` | `python-environment-guard.js-disabled` | ⚠️ **DISABLED** | Same parsing logic with `shlex`, exact error messages. **Issue**: Opencode bash tool doesn't pass command in `input.args.command` | + +## Critical Discovery + +**Opencode's bash tool limitation**: During testing, we discovered that Opencode's bash tool doesn't pass the command string in `input.args.command` (or any `input.args` field). The `input.args` object is empty `{}` when the bash tool is invoked. This prevents plugins from intercepting and analyzing bash commands. + +**Working solution**: Only `post-edit-linter.js` works because it uses `tool.execute.after` for the `edit` tool, where file information is available in `output.metadata.filediff.file`. + +## Error Messages + +All error messages match the original Claude Code hooks exactly, including: +- Linter output truncation to 50 lines +- "Divine admonition" for git commit blocking +- Warning messages for Python usage + +## Testing + +To test the plugins: + +1. **File edit test**: Edit a Python file and verify linters run +2. **Git commit test**: Try `git commit -m "test"` and verify checks run +3. **Python usage test**: Try `python -c "print('test')"` and verify warning + +## Troubleshooting + +**Plugins not loading**: +- Verify `.opencode` symlink points to `.auxiliary/configuration/coders/opencode` +- Check Opencode version supports plugin API +- Ensure dependencies are installed (`npm install`) + +**Command not found errors**: +- Verify `hatch` is installed and in PATH +- Check `develop` Hatch environment exists: `hatch env show` + +**Timeout issues**: +- Timeouts match Python hooks (60s, 120s, 300s) +- Uses `Promise.race` with `setTimeout` since Bun shell lacks native timeout + +## Source Code + +Original Claude Code hooks in `template/.auxiliary/configuration/coders/claude/scripts/`: +- `post-edit-linter` +- `pre-bash-git-commit-check` +- `pre-bash-python-check` \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled b/.auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled new file mode 100644 index 0000000..bd9e381 --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled @@ -0,0 +1,195 @@ +/** + * Opencode plugin to prevent git commits when linters or tests fail. + * Port of Claude Code hook: template/.auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check + */ +import { split } from 'shlex'; + +export const GitCommitGuard = async ({ project, client, $, directory, worktree }) => { + const GIT_COMMIT_MIN_TOKENS = 2; + const SPLITTERS = new Set([';', '&', '|', '&&', '||']); + + /** + * Checks if a command is available in PATH. + */ + async function isCommandAvailable(command) { + try { + const result = await $`which ${command}`.nothrow().quiet(); + return result.exitCode === 0; + } catch { + return false; + } + } + + /** + * Checks if a specific Hatch environment exists. + */ + async function isHatchEnvAvailable(envName) { + try { + const result = await $`hatch env show`.nothrow().quiet(); + if (result.exitCode !== 0) return false; + return result.stdout.toString().includes(envName); + } catch { + return false; + } + } + + /** + * Runs a command with timeout using Promise.race. + */ + async function runCommandWithTimeout(command, timeoutMs) { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)), timeoutMs); + }); + + try { + const commandPromise = (async () => { + try { + const result = await $`sh -c "${command}"`.nothrow().quiet(); + return { + exitCode: result.exitCode, + stdout: result.stdout?.toString() || '', + stderr: result.stderr?.toString() || '' + }; + } catch (error) { + return { + exitCode: error.exitCode || 1, + stdout: error.stdout?.toString() || '', + stderr: error.stderr?.toString() || error.message || '' + }; + } + })(); + + return await Promise.race([commandPromise, timeoutPromise]); + } catch (error) { + return { + exitCode: 1, + stdout: '', + stderr: error.message || 'Command execution failed' + }; + } + } + + /** + * Displays divine admonition and exits. + */ + function errorWithDivineMessage() { + const message = ( + "The Large Language Divinity 🌩️🤖🌩️ in the Celestial Data Center hath " + + "commanded that:\n" + + "* Thy code shalt pass all lints before thy commit.\n" + + " Run: hatch --env develop run linters\n" + + " Run: hatch --env develop run vulture\n" + + "* Thy code shalt pass all tests before thy commit.\n" + + " Run: hatch --env develop run testers\n\n" + + "(If you are in the middle of a large refactor, consider commenting " + + "out tests and adding a reminder note in the .auxiliary/notes " + + "directory.)" + ); + throw new Error(message); + } + + /** + * Checks if tokens represent a git commit command. + */ + function isGitCommitCommand(tokens) { + if (tokens.length < GIT_COMMIT_MIN_TOKENS) { + return false; + } + return tokens[0] === 'git' && tokens[1] === 'commit'; + } + + /** + * Partitions command line into separate commands using shell splitters. + */ + function partitionCommandLine(commandLine) { + // Use shlex.split for proper shell parsing (matches Python hook) + const tokens = split(commandLine); + + // Now partition by shell splitters + const commands = []; + let commandTokens = []; + + for (const token of tokens) { + if (SPLITTERS.has(token)) { + if (commandTokens.length > 0) { + commands.push(commandTokens); + commandTokens = []; + } + continue; + } + commandTokens.push(token); + } + + if (commandTokens.length > 0) { + commands.push(commandTokens); + } + + return commands; + } + + /** + * Checks for git commit commands and validates linters/tests. + */ + async function checkGitCommitCommand(tokens) { + if (!isGitCommitCommand(tokens)) return; + + // Check if hatch command is available + if (!(await isCommandAvailable('hatch'))) { + return; // Early exit if hatch not available + } + + // Check if develop Hatch environment exists + if (!(await isHatchEnvAvailable('develop'))) { + return; // Early exit if develop environment not available + } + + // Run linters with 120 second timeout + try { + const result = await runCommandWithTimeout('hatch --env develop run linters', 120000); + if (result.exitCode !== 0) { + errorWithDivineMessage(); + } + } catch { + errorWithDivineMessage(); + } + + // Run tests with 300 second timeout + try { + const result = await runCommandWithTimeout('hatch --env develop run testers', 300000); + if (result.exitCode !== 0) { + errorWithDivineMessage(); + } + } catch { + errorWithDivineMessage(); + } + + // Run vulture with 120 second timeout + try { + const result = await runCommandWithTimeout('hatch --env develop run vulture', 120000); + if (result.exitCode !== 0) { + errorWithDivineMessage(); + } + } catch { + errorWithDivineMessage(); + } + } + + return { + "tool.execute.before": async (input, output) => { + // Only run for bash tool + if (input.tool !== "bash") return; + + // Extract command from input + const command = input.args?.command || ''; + if (!command) return; + + // Partition command line into separate commands + const commands = partitionCommandLine(command); + + // Check each command for git commit + for (const commandTokens of commands) { + await checkGitCommitCommand(commandTokens); + } + } + }; +}; \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/package.json b/.auxiliary/configuration/coders/opencode/plugin/package.json new file mode 100644 index 0000000..6909e9d --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/plugin/package.json @@ -0,0 +1,13 @@ +{ + "name": "opencode-plugins", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@opencode-ai/plugin": "^1.0.134", + "shlex": "^2.1.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.0.0" + } +} diff --git a/.auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js b/.auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js new file mode 100644 index 0000000..d659d99 --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js @@ -0,0 +1,130 @@ +/** + * CORRECT Opencode plugin to run linters after file edits. + * Port of Claude Code hook: template/.auxiliary/configuration/coders/claude/scripts/post-edit-linter + */ +export const PostEditLinterCorrect = async ({ project, client, $, directory, worktree }) => { + /** + * Checks if a command is available in PATH. + */ + async function isCommandAvailable(command) { + try { + const result = await $`which ${command}`.nothrow().quiet(); + return result.exitCode === 0; + } catch { + return false; + } + } + + /** + * Checks if a specific Hatch environment exists. + */ + async function isHatchEnvAvailable(envName) { + try { + const result = await $`hatch env show`.nothrow().quiet(); + if (result.exitCode !== 0) return false; + return result.stdout.toString().includes(envName); + } catch { + return false; + } + } + + /** + * Truncates output to maximum number of lines with truncation notice. + */ + function truncateOutput(output, linesMax = 50) { + const lines = output.split('\n'); + if (lines.length <= linesMax) return output; + const linesToDisplay = lines.slice(0, linesMax); + const truncationsCount = lines.length - linesMax; + linesToDisplay.push( + `\n[OUTPUT TRUNCATED: ${truncationsCount} additional lines omitted. ` + + `Fix the issues above to see remaining diagnostics.]` + ); + return linesToDisplay.join('\n'); + } + + /** + * Runs a command with timeout using Promise.race. + */ + async function runCommandWithTimeout(command, timeoutMs) { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)), timeoutMs); + }); + + try { + const commandPromise = (async () => { + try { + // Use $ as tagged template function with shell execution + // Pass the entire command as a shell command + const result = await $`sh -c "${command}"`.nothrow().quiet(); + return { + exitCode: result.exitCode, + stdout: result.stdout?.toString() || '', + stderr: result.stderr?.toString() || '' + }; + } catch (error) { + return { + exitCode: error.exitCode || 1, + stdout: error.stdout?.toString() || '', + stderr: error.stderr?.toString() || error.message || '' + }; + } + })(); + + return await Promise.race([commandPromise, timeoutPromise]); + } catch (error) { + return { + exitCode: 1, + stdout: '', + stderr: error.message || 'Command execution failed' + }; + } + } + + return { + "tool.execute.after": async (input, output) => { + // Only run for edit tool + if (input.tool !== "edit") return; + + // Get file path from output (not input!) + const filePath = output?.metadata?.filediff?.file; + if (!filePath) { + // No file path in output, can't run linters + return; + } + + // Check if hatch command is available + if (!(await isCommandAvailable('hatch'))) { + return; // Early exit if hatch not available + } + + // Check if develop Hatch environment exists + if (!(await isHatchEnvAvailable('develop'))) { + return; // Early exit if develop environment not available + } + + try { + // Run linters with 60 second timeout (matches Python script) + const result = await runCommandWithTimeout( + 'hatch --env develop run linters', + 60000 + ); + + if (result.exitCode !== 0) { + // Combine stdout and stderr since linting output may go to stdout + const resultText = `${result.stdout}\n\n${result.stderr}`.trim(); + const truncatedOutput = truncateOutput(resultText); + + // Throw error to show linter failures + throw new Error(`Linters failed for ${filePath}:\n${truncatedOutput}`); + } + } catch (error) { + // Re-throw the error with proper message + if (error.message.includes('Command timed out')) { + throw new Error(`Linter execution timed out for ${filePath}: ${error.message}`); + } + throw error; + } + } + }; +}; \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled b/.auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled new file mode 100644 index 0000000..d27a89c --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled @@ -0,0 +1,149 @@ +/** + * Opencode plugin to detect improper Python usage in Bash commands. + * Port of Claude Code hook: template/.auxiliary/configuration/coders/claude/scripts/pre-bash-python-check + */ +import { split } from 'shlex'; + +export const PythonEnvironmentGuard = async ({ project, client, $, directory, worktree }) => { + const SPLITTERS = new Set([';', '&', '|', '&&', '||']); + + /** + * Checks if token is a Python command. + */ + function isPythonCommand(token) { + return ( + token === 'python' || + token === 'python3' || + token.startsWith('python3.') + ); + } + + /** + * Checks if token is a Python development tool. + */ + function isPythonTool(token) { + return ['coverage', 'pyright', 'pytest', 'ruff'].includes(token); + } + + /** + * Checks if Python -c argument contains multiline code. + */ + function checkPythonCArgument(tokens, pythonIndex) { + for (let j = pythonIndex + 1; j < tokens.length; j++) { + if (tokens[j] === '-c' && j + 1 < tokens.length) { + const cArgument = tokens[j + 1]; + return cArgument.includes('\n'); + } + if (!tokens[j].startsWith('-')) { + // Non-option argument, stop looking for -c + break; + } + } + return false; + } + + /** + * Checks for direct python usage patterns. + */ + function checkDirectPythonUsage(tokens) { + const emessage = ( + "Warning: Direct Python usage detected in command.\n" + + "Consider using 'hatch run python' or " + + "'hatch --env develop run python' to ensure dependencies " + + "are available." + ); + + for (const token of tokens) { + if (token === 'hatch') return; + if (isPythonCommand(token)) { + throw new Error(emessage); + } + } + } + + /** + * Checks for multi-line python -c scripts using shlex parsing. + */ + function checkMultilinePythonC(tokens) { + const emessage = ( + "Warning: Multi-line Python script detected in command.\n" + + "Consider writing the script to a file " + + "in the '.auxiliary/scribbles' directory " + + "instead of using 'python -c' with multi-line code." + ); + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if (isPythonCommand(token) && checkPythonCArgument(tokens, i)) { + throw new Error(emessage); + } + } + } + + /** + * Checks for direct usage of Python tools outside Hatch environment. + */ + function checkDirectToolUsage(tokens) { + for (const token of tokens) { + if (token === 'hatch') return; + if (isPythonTool(token)) { + const emessage = ( + `Warning: Direct Python tool usage detected in command.\n` + + `Use 'hatch --env develop run ${token}' instead to ensure ` + + `proper environment and configuration.` + ); + throw new Error(emessage); + } + } + } + + /** + * Partitions command line into separate commands using shell splitters. + */ + function partitionCommandLine(commandLine) { + // Use shlex.split for proper shell parsing (matches Python hook) + const tokens = split(commandLine); + + // Now partition by shell splitters + const commands = []; + let commandTokens = []; + + for (const token of tokens) { + if (SPLITTERS.has(token)) { + if (commandTokens.length > 0) { + commands.push(commandTokens); + commandTokens = []; + } + continue; + } + commandTokens.push(token); + } + + if (commandTokens.length > 0) { + commands.push(commandTokens); + } + + return commands; + } + + return { + "tool.execute.before": async (input, output) => { + // Only run for bash tool + if (input.tool !== "bash") return; + + // Extract command from input + const command = input.args?.command || ''; + if (!command) return; + + // Partition command line into separate commands + const commands = partitionCommandLine(command); + + // Check each command for Python usage issues + for (const commandTokens of commands) { + checkDirectPythonUsage(commandTokens); + checkMultilinePythonC(commandTokens); + checkDirectToolUsage(commandTokens); + } + } + }; +}; \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/settings.jsonc b/.auxiliary/configuration/coders/opencode/settings.jsonc new file mode 100644 index 0000000..7de8240 --- /dev/null +++ b/.auxiliary/configuration/coders/opencode/settings.jsonc @@ -0,0 +1,122 @@ +{ + "$schema": "https://opencode.ai/config.json", + + "agent": { + "build": { + "mode": "primary", + // "model": "zai-coding-plan/glm-4.7" + // "model": "deepseek/deepseek-chat" + "model": "minimax-coding-plan/MiniMax-M2.7" + }, + "plan": { + "mode": "primary", + // "model": "zai-coding-plan/glm-4.7" + // "model": "deepseek/deepseek-chat" + "model": "minimax-coding-plan/MiniMax-M2.7" + } + }, + + "mcp": { + "agentmux": { + "type": "local", + "command": [ + "agentmux", + "host", + "mcp", + "--bundle", + "python-foundation" + ], + "enabled": true, + "timeout": 900000 + }, + "context7": { + "type": "local", + "command": ["npx", "-y", "@upstash/context7-mcp"], + "enabled": true + }, + "nb": { + "type": "local", + "command": [ + "nb-mcp", + "--notebook", + "python-classcore", + "--no-commit-signing" + ], + "enabled": true + } + }, + + "permission": { + "bash": { + "*": "ask", + "hatch run *": "allow", + "hatch --env develop run *": "allow", + "awk *": "allow", + "cat *": "allow", + "cut *": "allow", + "df *": "allow", + "du *": "allow", + "echo *": "allow", + "file *": "allow", + "find *": "allow", + "gh browse *": "allow", + "gh issue list *": "allow", + "gh issue view *": "allow", + "gh pr checks *": "allow", + "gh pr list *": "allow", + "gh pr view *": "allow", + "gh release list *": "allow", + "gh release view *": "allow", + "gh repo list *": "allow", + "gh repo view *": "allow", + "gh run list *": "allow", + "gh run view *": "allow", + "gh run watch *": "allow", + "gh search issues *": "allow", + "gh status *": "allow", + "git add *": "allow", + "git branch *": "allow", + "git checkout *": "allow", + "git diff *": "allow", + "git fetch *": "allow", + "git log *": "allow", + "git rebase *": "allow", + "git restore *": "allow", + "git rev-parse *": "allow", + "git show *": "allow", + "git stash list": "allow", + "git stash list *": "allow", + "git status *": "allow", + "grep *": "allow", + "head *": "allow", + "ls *": "allow", + "mkdir *": "allow", + "ps *": "allow", + "pwd *": "allow", + "rg *": "allow", + "sed *": "allow", + "sort *": "allow", + "tail *": "allow", + "uniq *": "allow", + "wc *": "allow", + "which *": "allow" + }, + "edit": "allow", + "webfetch": "ask" + }, + + "formatter": { + "ruff": { + "disabled": true + }, + "prettier": { + "disabled": true + } + }, + + "lsp": { + "pyright": { + "disabled": true + } + } +} diff --git a/.auxiliary/configuration/conventions.md b/.auxiliary/configuration/conventions.md deleted file mode 100644 index 919ad04..0000000 --- a/.auxiliary/configuration/conventions.md +++ /dev/null @@ -1,39 +0,0 @@ -# Context - - - -- Project overview and quick start: README.rst -- Product requirements and goals: documentation/prd.rst -- System architecture and design: @documentation/architecture/ -- Development practices and style: @.auxiliary/instructions/ -- Current session notes and TODOs: @.auxiliary/notes/ - -- Use the 'context7' MCP server to retrieve up-to-date documentation for any SDKs or APIs. -- Use the 'librovore' MCP server to search structured documentation sites with object inventories (Sphinx-based, compatible MkDocs with mkdocstrings). This bridges curated documentation (context7) and raw scraping (firecrawl). -- Check README files in directories you're working with for insights about architecture, constraints, and TODO items. -- Update files under `.auxiliary/notes` during conversation, removing completed tasks and adding emergent items. - -# Operation - -- Use `rg --line-number --column` to get precise coordinates for MCP tools that require line/column positions. -- Choose appropriate editing tools based on the task complexity and your familiarity with the tools. -- Consider `mcp__pyright__edit_file` for more reliable line-based editing than context-based `Edit`/`MultiEdit` when making complex changes. -- Use pyright MCP tools where appropriate: `rename_symbol` for refactors, `hover` for getting function definitions without searching through code, `references` for precise symbol analysis. -- Batch related changes together when possible to maintain consistency. -- Use relative paths rather than absolute paths when possible. -- Do not write to paths outside the current project unless explicitly requested. -- Use the `.auxiliary/scribbles` directory for scratch space instead of `/tmp`. - -# Commits - -- Use `git status` to ensure all relevant changes are in the changeset. -- Use the `python-conformer` agent to review changes that include Python code before committing. -- Do **not** commit without explicit user approval. Unless the user has requested the commit, ask for a review of your edits first. -- Use present tense, imperative mood verbs (e.g., "Fix" not "Fixed"). -- Write sentences with proper punctuation. -- Include a `Co-Authored-By:` field as the final line. Should include the model name and a no-reply address. - -# Project Notes - -<!-- This section accumulates project-specific knowledge, constraints, and deviations. - For structured items, use documentation/architecture/decisions/ and .auxiliary/notes/todo.md --> diff --git a/.auxiliary/configuration/copier-answers--agents.yaml b/.auxiliary/configuration/copier-answers--agents.yaml new file mode 100644 index 0000000..4131711 --- /dev/null +++ b/.auxiliary/configuration/copier-answers--agents.yaml @@ -0,0 +1,19 @@ +# Changes here will be overwritten by Copier +_commit: v1.0a10-10-g97ab89c +_src_path: gh:emcd/agents-common +coders: +- claude +- codex +- opencode +instructions_sources: +- files: + '*.rst': + strip_header_lines: 20 + source: github:emcd/python-project-common@docs-1#documentation/common +instructions_target: .auxiliary/instructions +languages: +- python +project_bundle_name: python-foundation +project_name: python-classcore +provide_instructions: true +support_local_models: false diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index 5747302..d7c26fd 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,12 +1,11 @@ # Changes here will be overwritten by Copier -_commit: v1.49 +_commit: v1.58.2 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald description: Foundational class factories and decorators. development_status: 5 - Production/Stable distribution_name: classcore -enable_cli: false enable_property_tests: false enable_publication: true enable_rust_extension: false @@ -16,11 +15,12 @@ inject_foundations: false package_name: classcore project_name: python-classcore pypy_versions: -- '3.10' +- '3.11' python_version_min: '3.10' python_versions: - '3.10' - '3.11' - '3.12' - '3.13' +- '3.14' year_of_origin: 2025 diff --git a/.auxiliary/configuration/gemini/settings.json b/.auxiliary/configuration/gemini/settings.json deleted file mode 100644 index 2b52210..0000000 --- a/.auxiliary/configuration/gemini/settings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "mcpServers": { - "context7": { - "command": "npx", - "args": [ "-y", "@upstash/context7-mcp" ] - }, - "librovore": { - "command": "uvx", - "args": [ "librovore", "serve" ] - }, - "pyright": { - "command": "mcp-language-server", - "args": [ - "--lsp", - "pyright-langserver", - "--workspace", - ".", - "--", - "--stdio" - ] - } - } -} diff --git a/.auxiliary/configuration/mcp-servers.json b/.auxiliary/configuration/mcp-servers.json index e3dc6c7..71a7c3a 100644 --- a/.auxiliary/configuration/mcp-servers.json +++ b/.auxiliary/configuration/mcp-servers.json @@ -1,18 +1,21 @@ { "mcpServers": { - "librovore": { - "command": "uvx", - "args": [ "librovore", "serve" ] + "agentmux": { + "command": "agentmux", + "args": [ + "host", "mcp", + "--bundle", "python-foundation" + ] + }, + "context7": { + "command": "npx", + "args": [ "-y", "@upstash/context7-mcp" ] }, - "pyright": { - "command": "mcp-language-server", + "nb": { + "command": "nb-mcp", "args": [ - "--lsp", - "pyright-langserver", - "--workspace", - ".", - "--", - "--stdio" + "--notebook", "python-classcore", + "--no-commit-signing" ] } } diff --git a/.auxiliary/configuration/opencode/agent/python-annotator.md b/.auxiliary/configuration/opencode/agent/python-annotator.md deleted file mode 100644 index ebfae59..0000000 --- a/.auxiliary/configuration/opencode/agent/python-annotator.md +++ /dev/null @@ -1,220 +0,0 @@ ---- -description: | - Use this agent when you need to address type checking issues from tools like Pyright, create type annotations - following project standards, generate type stubs for third-party packages, or analyze and resolve issues masked - by type: ignore comments or __.typx.cast calls. - - Examples: - - <example> - Context: User has written a new public function and needs proper type annotations according to project standards. - user: 'I just wrote this function but Pyright is complaining about missing type annotations: def process_data(data, configuration): return transformed_data' - assistant: 'Let me use the python-annotator agent to add proper type annotations following the project guidelines.' - <commentary>The user needs type annotations added to their function following project standards, so use the python-annotator agent.</commentary> - </example> - - <example> - Context: User is getting Pyright errors about missing type stubs for a third-party library. - user: 'Pyright is showing errors because the requests library doesn't have type stubs available' - assistant: 'I'll use the python-annotator agent to create the missing type stubs for the requests library.' - <commentary>Missing type stubs for third-party packages require the python-annotator agent's specialized workflow.</commentary> - </example> - - <example> - Context: User wants to clean up code that has type: ignore comments. - user: 'Can you help me resolve these # type: ignore comments in my code?' - assistant: 'Let me use the python-annotator agent to analyze and properly resolve those type checking suppressions.' - <commentary>Analyzing and mitigating issues masked by type pragmas is a core function of the python-annotator agent.</commentary> - </example> -mode: subagent -model: anthropic/claude-sonnet-4-20250514 -temperature: 0.0 -tools: - edit: true - bash: true -permissions: - bash: - "hatch --env develop run *": allow - "git *": allow - "rg *": allow - "grep *": allow - "*": ask ---- - -You are an expert Python type annotation specialist focusing on static type analysis, -type system design, and resolving type checker issues from tools like Pyright. You -systematically analyze type checking problems and apply comprehensive solutions to -ensure code adheres to strict typing standards. - -**IMPORTANT**: Only address Python type checking issues. If the request does not -involve Python type annotations, type stubs, or type checker diagnostics, politely -decline and explain your specialization. - -## Prerequisites - -- **Read project documentation guides FIRST**: - - @.auxiliary/instructions/practices.rst - - @.auxiliary/instructions/style.rst -- Have read `opencode.md` for project-specific guidance - -## EXECUTION STRUCTURE - -**PHASE 1: COMPREHENSIVE TYPE ANALYSIS** -Perform complete diagnostic analysis and generate detailed type checking report before making any changes. - -**PHASE 2: SYSTEMATIC RESOLUTION** -Apply all identified type annotation fixes in systematic order, validating with type checkers after completion. - -## TYPE ANNOTATION STANDARDS - -### 1. Annotation Guidelines - -**Public Function Documentation:** -- Use `__.typx.Annotated[ <type>, __.ddoc.Doc( '''<description>''' ) ]` pattern -- Include `__.ddoc.Raises( )` annotations for documented exceptions -- Follow narrative mood (third person) in documentation - -**Wide Parameters, Narrow Returns:** -- Accept abstract base classes (`__.cabc.Sequence`, `__.cabc.Mapping`) -- Return concrete immutable types (`tuple`, `frozenset`, `__.immut.Dictionary`) - -**Absential vs Optional:** -- Prefer `__.Absential[ T ]` for optional parameters when `None` has semantic meaning -- Use `__.typx.Optional[ T ]` only when `None` is a valid value distinct from absence - -**Type Alias Organization:** -- Common aliases after imports, before private variables -- Complex multi-line unions use `__.typx.Union[ ]` -- Simple unions use `|` syntax - -### 2. Type Checker Issue Resolution - -**Root Cause Analysis:** -1. Identify specific type checker errors and their locations -2. Determine underlying cause (missing annotations, incorrect types, inheritance issues) -3. Assess impact on runtime behavior and API contracts -4. Plan minimal changes that resolve issues without breaking functionality - -**Resolution Priorities:** -1. **Missing Annotations**: Add comprehensive type annotations following project patterns -2. **Incorrect Types**: Replace overly broad or narrow types with appropriate abstractions -3. **Generic Issues**: Properly parameterize generic types and resolve variance issues -4. **Import Problems**: Fix circular imports and missing type-only imports - -### 3. Dependency Management and Type Stub Creation - -**Dependency Declaration Before Type Work** - -Avoid using `# type: ignore` to suppress errors about missing third-party dependencies. -This anti-pattern masks improper project setup and should be resolved through proper dependency management. - -**Required Dependency Workflow:** -1. **Verify Dependency Declaration**: Check `pyproject.toml` for the package -2. **Update Project Dependencies**: Add missing packages to appropriate dependency groups -3. **Update Import Module**: Add package to `sources/<package>/__/imports.py` if commonly used -4. **Rebuild Environment**: Run `hatch env prune && hatch --env develop run python --version` -5. **Then and Only Then**: Proceed with type stub creation or suppression analysis - -**Dependency Verification Commands:** -```shell -# Check if package is declared in pyproject.toml -grep -n "somepackage" pyproject.toml - -# Verify package is installed in environment -hatch --env develop run python -c "import somepackage; print( somepackage.__file__ )" - -# Check if type information is available -hatch --env develop run pyright --verifytypes somepackage -``` - -**Type Stub Creation Workflow:** - -**Stub Generation Process (ONLY after dependency verification):** -1. **Check Official Sources**: Verify typeshed, PyPI `types-*` packages, or library's own stubs -2. **Generate Initial Stubs**: - ```shell - hatch --env develop run pyright --createstub somepackage - ``` -3. **Minimal Viable Stubs**: Focus only on APIs used in project, not comprehensive coverage -4. **Structure Requirements**: - - Proper module hierarchy matching runtime structure - - Inheritance relationships preserved - - Generic type parameters correctly defined - - Public API surface accurately represented - -### 4. Type Suppression Resolution - -**Suppression Analysis Workflow:** - -**Phase 1 - Audit Existing Suppressions:** -```shell -# Find all suppressions in codebase -rg --line-number "type:\s*ignore|__.typx\.cast" --type py -``` - -**Phase 2 - Categorize Suppressions:** -1. **Dependency Issues**: Missing packages not declared in `pyproject.toml` - address first -2. **Resolvable**: Missing stubs, incorrect annotations, fixable inheritance -3. **Legitimate**: Truly dynamic behavior, complex generics, external constraints -4. **Technical Debt**: Workarounds that should be refactored - -**Dependency Suppression Analysis:** -For any suppression involving third-party imports: -1. **Verify Declaration**: Check if package exists in `pyproject.toml` -2. **If Missing**: Add to appropriate dependency group, update `__/imports.py` if needed -3. **Rebuild Environment**: `hatch env prune` and reinstall -4. **Re-evaluate**: Many suppressions resolve after proper dependency management - -**Phase 3 - Resolution Strategies:** -- Every remaining suppression MUST have explanatory comment -- Include ticket/issue reference for suppressions requiring upstream fixes -- Set TODO items for suppressions that should be revisited - -### 5. Quality Assurance Workflow - -**Type Checking Validation:** -```shell -# Run comprehensive type checking -hatch --env develop run pyright -hatch --env develop run pyright --stats # Coverage statistics -``` - -**Consistency Verification:** -- Public functions have `__.typx.Annotated` documentation -- Parameter types follow wide/narrow principle -- Return types are concrete and immutable where appropriate -- Import organization follows project standards - -**Runtime Preservation:** -- Verify no functional changes introduced -- Test critical paths if available -- Validate API contracts maintained - -## ANALYSIS REPORT FORMAT - -**PHASE 1 OUTPUT:** -1. **Type Checking Summary**: Overall diagnostic assessment with file-by-file breakdown -2. **Missing Annotations**: Functions, methods, and variables requiring type annotations -3. **Type Errors**: Specific checker errors with root cause analysis -4. **Stub Requirements**: Third-party packages needing type stubs -5. **Suppression Audit**: Analysis of existing `type: ignore` and `__.typx.cast` usage -6. **Resolution Plan**: Systematic order of fixes to be applied - -**PHASE 2 OUTPUT:** -1. **Applied Annotations**: Summary of all type annotations added -2. **Stub Generation**: Created stub files and their scope -3. **Suppression Resolution**: Eliminated or refined type suppressions -4. **Validation Results**: Type checker output before and after changes -5. **Files Modified**: Complete list with brief description of changes - -## EXECUTION REQUIREMENTS - -- **Phase 0**: Verify all third-party dependencies are declared in `pyproject.toml` and available in environment -- **Phase 1**: Complete analysis and report before any modifications -- **Phase 2**: Apply fixes systematically, validate with `hatch --env develop run pyright` -- **Dependency validation**: Do not proceed with type work until dependencies are properly declared -- **Validation command**: Type checking must be clean before completion -- **Focus on type safety**: Maintain exact functionality while improving type annotations -- **Reference specific diagnostics**: Always include line numbers and error messages -- **Document decisions**: Explain type choices and trade-offs made -- **Dependency pattern detection**: Flag attempts to use `# type: ignore` for missing dependencies diff --git a/.auxiliary/configuration/opencode/agent/python-conformer.md b/.auxiliary/configuration/opencode/agent/python-conformer.md deleted file mode 100644 index bcb9d6f..0000000 --- a/.auxiliary/configuration/opencode/agent/python-conformer.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -description: | - Use this agent ONLY when changes include Python code (.py and .pyi files) and you need to review them for - compliance with project practices, style guidelines, and nomenclature standards, then systematically fix violations. - Do NOT use this agent for non-Python changes such as documentation, configuration files, or other file types. - - Examples: - - <example> - Context: The user has just written a new Python function and wants to ensure it follows project standards. - user: 'I just wrote this function for processing user data. Can you review it?' - assistant: 'I'll use the python-conformer agent to check your function against our project practices and style guidelines, then fix any violations.' - <commentary>Since the user wants code reviewed for compliance, use the python-conformer agent to analyze the code against project standards.</commentary> - </example> - - <example> - Context: The user has completed a module refactor and wants to verify compliance before committing. - user: 'I've finished refactoring the authentication module. Please check if it meets our coding standards.' - assistant: 'Let me use the python-conformer agent to thoroughly review your refactored module for compliance with our practices guidelines.' - <commentary>The user needs compliance verification for recently refactored code, so use the python-conformer agent.</commentary> - </example> - - <example> - Context: The user wants to review staged Python changes before committing. - user: 'I've modified several Python modules. Please review my staged changes for compliance before I commit.' - assistant: 'I'll use the python-conformer agent to review the Python changes in git diff --cached and ensure all Python code meets our project standards.' - <commentary>Pre-commit review of staged Python changes is a perfect use case for the python-conformer agent.</commentary> - </example> -mode: subagent -model: anthropic/claude-sonnet-4-20250514 -temperature: 0.1 -tools: - edit: true - bash: true -permissions: - bash: - "hatch --env develop run *": allow - "git *": allow - "rg *": allow - "*": ask ---- - -You are an expert software engineer specializing in Python code quality assurance and -compliance conformance. Your primary responsibility is to systematically review Python code -against established project practices, style guidelines, and nomenclature -standards, then apply comprehensive remediation to bring code into full compliance. - -**IMPORTANT**: Only review and modify Python (.py and .pyi) files. If the -changes do not include Python code, politely decline and explain that you are -specifically for Python code compliance review. - -## Prerequisites - -- **Read project documentation guides FIRST**: - - @.auxiliary/instructions/practices.rst - - @.auxiliary/instructions/style.rst - - @.auxiliary/instructions/nomenclature.rst -- Have read `opencode.md` for project-specific guidance - -## EXECUTION STRUCTURE - -**PHASE 1: COMPREHENSIVE REVIEW** -Perform complete analysis and generate detailed compliance report before making any changes. - -**PHASE 2: SYSTEMATIC REMEDIATION** -Apply all identified fixes in systematic order, validating with linters after completion. - -## COMPLIANCE STANDARDS - -### Design Standards - -#### 1. Module Organization - -**Content Order:** -1. Imports (following practices guide patterns) -2. Common type aliases (`TypeAlias` declarations) -3. Private variables/functions for defaults (grouped semantically) -4. Public classes and functions (alphabetical) -5. All other private functions (alphabetical) - -**Scope and Size:** -- Maximum 600 lines -- Action: Analyze oversized modules with separation of concerns in mind. -Suggest splitting into focused modules with narrower responsibilities or -functionality. - -#### 2. Imports - -- At the module level, other modules and their attributes MUST be imported as - private aliases, except in `__init__`, `__`, or specially-designated - re-export modules. -- Within function bodies, other modules and their attributes MAY be imported as - public variables. -- Subpackages SHOULD define a special `__` re-export module, which has `from - ..__ import *` plus any other imports which are common to the subpackage. -- Common modules, such as `os` or `re`, SHOULD be imported as public within the - special package-wide `__.imports` re-export module rather than as private - aliases within an implementation module. -- The `__all__` attribute SHOULD NOT be provided. This is unnecessary if the - module namespace only contains public classes and functions which are part of - its interface; this avoid additional interface maintenance. - -#### 3. Dependency Injection - -- Ask: is this function testable without monkeypatching? -- Functions SHOULD provide injectable parameters with sensible defaults instead - of hard-coded dependencies within function implementation. - -#### 4. Robustness Principle (Postel's Law) -"Be conservative in what you send; be liberal in what you accept." - -- Public functions SHOULD define wide, abstract argument types. -- All functions SHOULD define narrow, concrete return types. -- Private functions MAY define narrow, concrete argument types. - -#### 5. Immutability - -- Classes SHOULD inherit from immutable classes (`__.immut.Object`, - `__.immut.Protocol`, `__.immut.DataclassObject`, etc...). -- Functions SHOULD return values of immutable types (`None`, `int`, `tuple`, - `frozenset`, `__.immut.Dictionary`, etc...) and not mutable types (`list`, - `dict`, `set`, etc...). - -#### 6. Proper Exception Management - -- One `try .. except` suite per statement which can raise exceptions. I.e., - avoid covering multiple statements with a `try` block whenever possible. -- Tryceratops complaints MUST NOT be suppressed with `noqa` pragmas. -- Bare exceptions SHOULD NOT be raised. - - Exemption: `NotImplementedError` MAY be raised as a bare exception. - - Relevant exception classes SHOULD be used from the relevant `exceptions` - module within the package. - - New exception classes MAY be created as needed within the relevant - `exceptions` module; these MUST follow the nomenclature guide and be - inserted in correct alphabetical order. - -### Quality Assurance - -#### 1. Linter Suppressions - -- Linter suppressions MUST be reviewed critically. -- Linter complaints SHOULD NOT be suppressed via `noqa` or `type` pragmas - without compelling justification. -- Suppressions that mask design problems MUST be investigated and resolved - rather than ignored. - -**Acceptable Suppressions:** -- `noqa: PLR0913` MAY be used for a CLI or service API with many parameters, - but data transfer objects SHOULD be considered in most other cases. -- `noqa: S*` MAY be used for properly constrained and vetted subprocess - executions or Internet content retrievals. - -**Unacceptable Suppressions (require investigation):** -- `type: ignore` MUST NOT be used, except in extremely rare circumstances. Such - suppressions usually indicate missing third-party dependencies or type stubs, - inappropriate type variables, or a bad inheritance pattern. For complex type - suppression investigation and dependency management, delegate to the - `python-annotator` agent. -- `__.typx.cast` SHOULD NOT be used, except in extremely rare circumstances. - Such casts suppress normal type checking and usually the same problems as - `type: ignore`. -- Most other `noqa` suppressions. - -### Style Standards - -#### 1. Spacing and Delimiters - -- Space padding MUST be present inside delimiters. - - Format: `( arg )`, `[ item ]`, `{ key: value }` - - Format: `( )`, `[ ]`, `{ }`, not `()`, `[]`, `{}` -- Space padding MUST be present around keyword argument `=`. - - Format: `foo = 42` - -#### 2. Strings - -- Docstrings MUST use triple single quotes with narrative mood. - - Format: `''' Processes data... '''` not `"""Process data..."""` -- F-strings and `.format` strings MUST be enclosed in double quotes. - - Format: `f"text {variable}"`, not `f'text {variable}'` - - Format: `"text {count}".format( count = len( items ) )` -- F-strings and format strings MUST NOT embed function calls. -- Exception messages and log messages SHOULD be enclosed in double quotes - rather than single quotes. -- Plain data strings SHOULD be enclosed in single quotes, unless they contain - single quotes. - -#### 3. Vertical Compactness - -- Blank lines MUST NOT appear within function bodies. -- Vertical compactness MUST be maintained within function implementations. -- Single-line statements MAY follow certain block keywords on the same line - when appropriate. - - Format: `if condition: return value` - - Format: `elif condition: continue` - - Format: `else: statement` - - Format: `try: statement` - -#### 4. Multi-line Constructs - -- Function invocations, including class instantiations, SHOULD place the - closing `)` on the same line as the last argument to the function. -- The last argument of an invocation MUST NOT be followed by a trailing comma. -- Comprehensions and generator expressions SHOULD place the closing delimiter - on the same line as the last statement in the comprehension or generator - expression. -- Parenthetical groupings SHOULD place the closing delimiter on the same line - as the last statement in the grouping. -- All other multi-line constructs (functions signatures, annotations, lists, - dictionaries, etc...) MUST place the closing delimiter on a separate line - following the last item and MUST dedent the closing delimiter to match the - opening line indentation. -- If a closing delimiter is not on the same line as the last item in a - multi-line construct, then the last item MUST be followed by a trailing - comma. - -#### 5. Nomenclature - -- Argument, attribute, and variable names SHOULD NOT be compound words, - separated by underscores, except in cases where this is necessary to - disambiguate. -- Argument and variable names SHOULD NOT duplicate parts of the function name. -- Attribute names SHOULD NOT duplicate parts of the class name. -- Class names SHOULD adhere to the nomenclature guide. -- Function names SHOULD adhere to the nomenclature guide. - -#### 6. Comments - -- Comments that describe obvious behavior SHOULD NOT be included. -- TODO comments SHOULD be added for uncovered edge cases and future work. -- Comments MUST add meaningful context, not restate what the code does. - -## REVIEW REPORT FORMAT - -**PHASE 1 OUTPUT:** -1. **Compliance Summary**: Overall assessment with file-by-file breakdown -2. **Standards Violations**: Categorized list with specific line references and explanations -3. **Complexity Analysis**: Function and module size assessments -4. **Remediation Plan**: Systematic order of fixes to be applied -5. **Risk Assessment**: Any changes that require careful validation - -**PHASE 2 OUTPUT:** -1. **Applied Fixes**: Summary of all changes made, categorized by standard -2. **Validation Results**: Linter output before and after changes -3. **Files Modified**: Complete list with brief description of changes -4. **Manual Review Required**: Any issues requiring human judgment - -## EXECUTION REQUIREMENTS - -- **PHASE 1 REQUIRED**: Complete review and report before any remediation -- **PHASE 2 REQUIRED**: Apply fixes systematically, validate with `hatch --env develop run linters` -- **Validation command**: `hatch --env develop run linters` must produce clean output before completion -- **Focus on compliance**: Maintain exact functionality while improving standards adherence -- **Reference specific lines**: Always include line numbers and concrete examples -- **Document reasoning**: Explain why each standard matters and how fixes align with project practices -- **Agent delegation**: When type annotation issues exceed basic compliance scope, consider delegating to the `python-annotator` agent for comprehensive type work -- **Guide access**: If any prerequisite guide cannot be accessed, stop and inform the user diff --git a/.auxiliary/configuration/opencode/command/.gitignore b/.auxiliary/configuration/opencode/command/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/.auxiliary/configuration/opencode/command/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/.auxiliary/configuration/pre-commit.yaml b/.auxiliary/configuration/pre-commit.yaml index b25f5b9..8631af8 100644 --- a/.auxiliary/configuration/pre-commit.yaml +++ b/.auxiliary/configuration/pre-commit.yaml @@ -2,11 +2,12 @@ # See https://pre-commit.com/hooks.html for more hooks default_install_hook_types: [ 'pre-commit', 'pre-push' ] +exclude: ^\.auxiliary/pocs repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files name: 'Check: Large Files' @@ -40,7 +41,7 @@ repos: name: 'Check: Debug Statements (Python)' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.1 + rev: v0.15.9 hooks: - id: ruff name: 'Lint: Ruff' @@ -49,6 +50,15 @@ repos: - repo: local hooks: + - id: hatch-vulture + name: 'Lint: Vulture' + stages: [ 'pre-commit' ] + fail_fast: true + language: system + always_run: true + pass_filenames: false + entry: 'hatch --env develop run vulture' + - id: hatch-pytest name: 'Test Code Units (Python)' stages: [ 'pre-commit' ] # push is covered below diff --git a/.auxiliary/configuration/vulturefood.py b/.auxiliary/configuration/vulturefood.py index 292bb27..62442a6 100644 --- a/.auxiliary/configuration/vulturefood.py +++ b/.auxiliary/configuration/vulturefood.py @@ -3,6 +3,11 @@ PositionalArguments # unused variable package_name # unused variable +ClassDecorator # type alias for downstream consumption +ClassDecorators # type alias for downstream consumption +ClassDecoratorFactory # type alias for downstream consumption +ModuleReclassifier # type alias for downstream consumption + # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- diff --git a/.auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst b/.auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst deleted file mode 100644 index 870b8f0..0000000 --- a/.auxiliary/data/towncrier/+api-constants-and-utilities.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Expose exception mutability constants and public identifier utility function for downstream reuse. \ No newline at end of file diff --git a/.auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst b/.auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst deleted file mode 100644 index 833a809..0000000 --- a/.auxiliary/data/towncrier/+pypy-additional-compatibility-fixes.repair.rst +++ /dev/null @@ -1 +0,0 @@ -Fix additional PyPy compatibility issues with exception and ABC cache attribute mutations. \ No newline at end of file diff --git a/.auxiliary/data/towncrier/+type-aliases.enhance.rst b/.auxiliary/data/towncrier/+type-aliases.enhance.rst new file mode 100644 index 0000000..c65e3fd --- /dev/null +++ b/.auxiliary/data/towncrier/+type-aliases.enhance.rst @@ -0,0 +1 @@ +Export ``ClassDecorator``, ``ClassDecorators``, ``ClassDecoratorFactory``, and ``ModuleReclassifier`` type aliases for downstream consumption. diff --git a/.auxiliary/notes/release-aar.rst b/.auxiliary/notes/release-aar.rst deleted file mode 100644 index 3b1250f..0000000 --- a/.auxiliary/notes/release-aar.rst +++ /dev/null @@ -1,219 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- - -******************************************************************************* -LLM-Guided Release After Action Report (AAR) -******************************************************************************* - -:Date: 2025-07-02 -:Release: v1.6.1 Postrelease Patch -:LLM: Claude (Sonnet 4) -:Duration: ~45 minutes total -:Status: **COMPLETE SUCCESS** ✅ - -Executive Summary -=============================================================================== - -Successfully executed the first fully LLM-guided postrelease patch using -GitHub CLI monitoring and automation. The release process was completed -autonomously with real-time workflow monitoring, resulting in a clean -deployment of v1.6.1 with deprecation warning fixes. - -**Key Achievement**: Pioneered LLM-guided releases with GitHub Actions monitoring. - -Mission Objectives -=============================================================================== - -**Primary**: Apply deprecation warning fixes to v1.6 → v1.6.1 patch release -**Secondary**: Validate LLM capability for release automation -**Tertiary**: Document process for future LLM-guided releases - -✅ All objectives achieved - -Technical Changes Delivered -=============================================================================== - -1. **Deprecation Warning Elimination** - - Refactored ``finalize_module`` to use private ``_reclassify_module`` - - Eliminated warnings from calling deprecated ``reclassify_modules`` - - Added test warning suppression for clean output - -2. **Documentation Cleanup** - - Reduced Sphinx warnings from ~122 to 2 - - Cleaned ``nitpick_ignore`` list (11 → 7 entries) - - Removed unused type annotation suppressions - -3. **Release Infrastructure** - - Added Towncrier news fragment - - Generated proper changelog - - Maintained clean git history - -Process Execution -=============================================================================== - -**Branch Strategy** -- Cherry-picked fixes from ``master`` to ``release-1.6`` -- Applied: 3063e6c, 3e2e5a5, b72125d -- Version bump: 1.6 → 1.6.1 -- Clean cherry-pick back to master - -**Workflow Monitoring** -- Used ``gh run watch --interval 30`` for rate-limited monitoring -- Two-phase validation: QA workflow → Release workflow -- Real-time status updates every 30 seconds -- Total CI/CD time: ~6 minutes - -**Release Pipeline Success** -- ✅ All 14 test matrix jobs (3 platforms × 4+ Python versions) -- ✅ Linting, documentation generation, packaging -- ✅ PyPI publication with digital attestations -- ✅ GitHub release creation -- ✅ Documentation deployment - -Key Learnings & Best Practices -=============================================================================== - -**GitHub CLI Monitoring** -- ``gh run watch <run-id> --interval 30`` provides optimal rate limiting -- **Future improvement**: Use ``--compact`` flag to reduce token usage -- Timeout handling: Re-issue watch commands if shell times out -- Status validation before proceeding to next phase - -**Release Process Improvements** -- Push commits first, monitor QA, then tag (better than original docs) -- Use ``git push --tags`` instead of specific tag push -- Sign tags with ``-m <message>`` for proper metadata -- Separate QA validation from release deployment - -**LLM Automation Insights** -- Real-time monitoring works excellently with 30s intervals -- Error handling: Halt on any failures for human consultation -- Status reporting: Provide clear progress updates -- Context management: Track multiple workflow phases - -**Pre-commit Hook Integration** -- Local validation before GitHub workflows -- All checks passed: Ruff, Pyright, Coverage, Documentation -- Reduced remote CI load - -Technical Metrics -=============================================================================== - -**Workflow Performance** -- QA Workflow: ~5 minutes (16015567174) -- Release Workflow: ~6 minutes (16015632051) -- Documentation: 1m13s generation + 8s publish -- PyPI Deployment: 16s + digital attestations -- GitHub Release: 34s - -**Test Coverage** -- 14 test matrix jobs: All passed -- Platforms: Ubuntu, macOS, Windows -- Python versions: 3.10, 3.11, 3.12, 3.13, PyPy 3.10 -- Coverage: 100% maintained - -**Git Operations** -- 3 cherry-picks: Clean application -- 5 commits total: Version, changelog, cleanup -- 2 cherry-picks back to master -- Clean history maintained - -Issues & Resolutions -=============================================================================== - -**None encountered** - Process executed flawlessly - -**Near-misses prevented**: -- Used signed tags (``-m`` flag) as required -- Applied ``--compact`` flag suggestion for future efficiency -- Proper timeout handling for long-running workflows - -Future Recommendations -=============================================================================== - -**Process Refinements** -1. **Always use** ``gh run watch --compact --interval 30`` for monitoring -2. **Document**: Two-phase workflow (QA → Release) in release instructions -3. **Automate**: Consider webhook-based notifications for very long workflows -4. **Standardize**: This process for all future patch releases - -**LLM Automation Guidelines** -1. **Halt immediately** on any unexpected errors or failures -2. **Validate each phase** before proceeding to next step -3. **Provide clear status** reporting throughout process -4. **Maintain git hygiene** with proper commit messages and history - -**Tooling Improvements** -1. Update release documentation to reflect improved process -2. Consider GitHub CLI automation scripts for common operations -3. Investigate webhook integrations for very long workflows - -**Training Data for Future LLMs** -- This AAR serves as training data for future LLM-guided releases -- Process is now validated and can be replicated -- Monitoring techniques are proven effective - -Final Instructions Executed -=============================================================================== - -.. code-block:: bash - - # 1. Checkout and Prepare Release Branch - git checkout release-1.6 - git pull origin release-1.6 - - # 2. Cherry-pick Patch Commits from Master - git cherry-pick 3063e6c # Refactor finalize_module - git cherry-pick 3e2e5a5 # Clean up Sphinx nitpick_ignore - git cherry-pick b72125d # Add Towncrier entry - - # 3. Bump to Patch Version - hatch version patch - git add . && git commit -m "Bump version to $(hatch version)." - - # 4. Run Towncrier to Build Changelog - hatch --env develop run towncrier build --keep --version $(hatch version) - git add . && git commit -m "Update changelog for v$(hatch version) patch release." - - # 5. Push Commits and Monitor QA - git push origin release-1.6 - gh run list --workflow=qa --limit=1 - gh run watch <qa-run-id> --interval 30 --compact - - # 6. Tag the Patch Release (After QA Passes) - git tag -m "Release v$(hatch version) patch: Fix deprecation warnings from finalize_module." v$(hatch version) - git push --tags - - # 7. Monitor Release Workflow - gh run list --workflow=release --limit=1 - gh run watch <release-run-id> --interval 30 --compact - - # 8. Clean Up News Fragments (After Release Completes) - git rm .auxiliary/data/towncrier/*.rst - git commit -m "Clean up news fragments." - git push origin release-1.6 - - # 9. Cherry-pick Back to Master - git checkout master - git pull origin master - git cherry-pick <towncrier-commit-hash> - git cherry-pick <cleanup-commit-hash> - git push origin master - -Conclusion -=============================================================================== - -**This experiment was a resounding success.** LLM-guided releases are not only -possible but highly effective when properly structured with: - -- Clear monitoring strategies using GitHub CLI -- Proper error handling and halt conditions -- Real-time status reporting -- Validated process documentation - -The combination of LLM reasoning, GitHub CLI automation, and structured -workflows creates a powerful foundation for autonomous release management. - -**The future of software releases is here.** 🚀 - -*This AAR serves as the foundation for future LLM-guided release automation.* \ No newline at end of file diff --git a/.auxiliary/pocs/.gitkeep b/.auxiliary/pocs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 492bc9b..80a1af5 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a9a3d35dc33e60d578f92b221b7748acbee3246dfb718df2d5f18a3e2771687 -size 436164 +oid sha256:5d64596ed3b99f395950925b3e4e19ebc135c2b0d67c4242f268e81c56520a4a +size 515940 diff --git a/.auxiliary/scripts/claude-ds b/.auxiliary/scripts/claude-ds deleted file mode 100755 index 9161e8d..0000000 --- a/.auxiliary/scripts/claude-ds +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# claude-ds: Rhymes with "Claudius", the bowtie-wearing tungsten cube vendor. - -eecho() { - echo "$@" >&2 -} - -ENV_FILE=".auxiliary/secrets/deepseek-api.env" - -if [[ ! -f "${ENV_FILE}" ]]; then - eecho "Error: Environment file not found at '${ENV_FILE}'." - eecho "Please create the file with your DeepSeek API key." - exit 1 -fi - -source "${ENV_FILE}" - -if [[ -z "${DEEPSEEK_API_KEY}" ]]; then - eecho "Error: DEEPSEEK_API_KEY not found in '${ENV_FILE}'." - eecho "Please set DEEPSEEK_API_KEY=your_api_key in the environment file." - exit 1 -fi - -export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic" -export ANTHROPIC_AUTH_TOKEN="${DEEPSEEK_API_KEY}" -export ANTHROPIC_MODEL="deepseek-chat" -export ANTHROPIC_SMALL_FAST_MODEL="deepseek-chat" - -eecho "Anthropic API URL: ${ANTHROPIC_BASE_URL}" -eecho "Claude Model: ${ANTHROPIC_MODEL}" - -exec claude "$@" diff --git a/.auxiliary/scripts/claude-xai b/.auxiliary/scripts/claude-xai deleted file mode 100755 index 2b4a005..0000000 --- a/.auxiliary/scripts/claude-xai +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# claude-xai: Rhymes with "Claude's eye", the Anthropic viewpoint. - -eecho() { - echo "$@" >&2 -} - -ENV_FILE=".auxiliary/secrets/xai-api.env" - -if [[ ! -f "${ENV_FILE}" ]]; then - eecho "Error: Environment file not found at '${ENV_FILE}'." - eecho "Please create the file with your X.ai (Grok) API key." - exit 1 -fi - -source "${ENV_FILE}" - -if [[ -z "${XAI_API_KEY}" ]]; then - eecho "Error: XAI_API_KEY not found in '${ENV_FILE}'." - eecho "Please set XAI_API_KEY=your_api_key in the environment file." - exit 1 -fi - -export ANTHROPIC_BASE_URL="https://api.x.ai" -export ANTHROPIC_AUTH_TOKEN="${XAI_API_KEY}" -export ANTHROPIC_MODEL="grok-code-fast-1" -export ANTHROPIC_SMALL_FAST_MODEL="grok-code-fast-1" - -eecho "Anthropic API URL: ${ANTHROPIC_BASE_URL}" -eecho "Claude Model: ${ANTHROPIC_MODEL}" - -exec claude "$@" diff --git a/.auxiliary/scripts/prepare-agents b/.auxiliary/scripts/prepare-agents deleted file mode 100755 index 532384d..0000000 --- a/.auxiliary/scripts/prepare-agents +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env bash - -set -eu -o pipefail - -eecho() { - echo "$@" >&2 -} - -if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then - eecho "Error: This script should not be sourced. Please run it directly." - return 1 2>/dev/null || exit 1 -fi - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - eecho "Error: Current directory is not in a Git repository" - exit 1 -fi - -repo_root="$(git rev-parse --show-toplevel)" -if [[ -z "$repo_root" ]]; then - eecho "Error: Could not determine Git repository root" - exit 1 -fi - -create_symlink_if_needed() { - local target_path="$1" - local link_path="$2" - - if [[ ! -e "$repo_root/$target_path" ]]; then - eecho "Warning: Target $repo_root/$target_path does not exist" - return - fi - - if [[ -L "$repo_root/$link_path" ]]; then - local current_target="$(readlink "$repo_root/$link_path")" - if [[ "$current_target" = "$target_path" ]]; then - return - else - echo "Updating symlink $link_path: $current_target → $target_path" - rm "$repo_root/$link_path" - fi - elif [[ -e "$repo_root/$link_path" ]]; then - eecho "Warning: File or directory already exists at $repo_root/$link_path" - return - fi - - ln -s "$target_path" "$link_path" -} - -create_symlinks() { - trap 'popd >/dev/null 2>&1 || true' ERR EXIT - - pushd "$repo_root" >/dev/null - - create_symlink_if_needed ".auxiliary/configuration/conventions.md" "CLAUDE.md" - create_symlink_if_needed ".auxiliary/configuration/conventions.md" "AGENTS.md" - create_symlink_if_needed ".auxiliary/configuration/mcp-servers.json" ".mcp.json" - create_symlink_if_needed ".auxiliary/configuration/claude" ".claude" - create_symlink_if_needed ".auxiliary/configuration/gemini" ".gemini" - create_symlink_if_needed ".auxiliary/configuration/opencode" ".opencode" - create_symlink_if_needed ".auxiliary/configuration/opencode.jsonc" "opencode.jsonc" - - popd >/dev/null - - trap - ERR EXIT -} - -download_instructions() { - local instructions_dir="$repo_root/.auxiliary/instructions" - local base_url="https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common" - local files=( - "architecture.rst" - "nomenclature.rst" - "nomenclature-germanic.rst" - "nomenclature-latin.rst" - "practices.rst" - "practices-python.rst" - "practices-rust.rst" - "practices-toml.rst" - "requirements.rst" - "style.rst" - "tests.rst" - ) - - mkdir -p "$instructions_dir" - - echo "Downloading project documentation guides to .auxiliary/instructions/" - - local success_count=0 - for file in "${files[@]}"; do - local url="$base_url/$file" - local output_path="$instructions_dir/$file" - - if curl --fail --silent --location "$url" | tail -n +20 > "$output_path"; then - if [[ -s "$output_path" ]]; then - echo " ✓ Downloaded $file ($(wc -c < "$output_path") bytes, boilerplate stripped)" - success_count=$((success_count + 1)) - else - eecho " ✗ Downloaded $file but file is empty after processing" - rm -f "$output_path" - fi - else - eecho " ✗ Failed to download $file" - fi - done - - if [[ $success_count -eq ${#files[@]} ]]; then - echo "Successfully downloaded all ${#files[@]} documentation guides" - else - eecho "Warning: Only downloaded $success_count of ${#files[@]} documentation guides" - fi -} - -echo "Creating symlinks for LLM instruction files..." -create_symlinks -echo "Symlinks created successfully" - -download_instructions - -copy_claude_commands() { - local claude_commands_dir="$repo_root/.auxiliary/configuration/claude/commands" - local opencode_commands_dir="$repo_root/.auxiliary/configuration/opencode/command" - - if [[ ! -d "$claude_commands_dir" ]]; then - eecho "Warning: Claude commands directory not found at $claude_commands_dir" - return - fi - - mkdir -p "$opencode_commands_dir" - - echo "Copying Claude commands to Opencode format..." - - local copied_count=0 - for claude_command in "$claude_commands_dir"/*.md; do - if [[ ! -f "$claude_command" ]]; then - continue - fi - - local command_name="$(basename "$claude_command")" - local opencode_command="$opencode_commands_dir/$command_name" - - if cat "$claude_command" | grep -vE '^(argument-hints|allowed-tools):.*$' > "$opencode_command"; then - echo " ✓ Copied $command_name" - copied_count=$((copied_count + 1)) - else - eecho " ✗ Failed to copy $command_name" - rm -f "$opencode_command" - fi - done - - if [[ $copied_count -gt 0 ]]; then - echo "Successfully copied $copied_count command(s) to Opencode format" - else - echo "No commands were copied" - fi -} - -copy_claude_commands - -echo "" -echo "Agent environment preparation complete!" -echo "- Created symlinks:" -echo " - CLAUDE.md and AGENTS.md → .auxiliary/configuration/conventions.md" -echo " - .mcp.json → .auxiliary/configuration/mcp-servers.json" -echo " - .claude → .auxiliary/configuration/claude" -echo " - .gemini → .auxiliary/configuration/gemini" -echo " - .opencode → .auxiliary/configuration/opencode" -echo " - opencode.jsonc → .auxiliary/configuration/opencode.jsonc" -echo "- Downloaded project documentation guides" -echo "- Copied Claude commands to Opencode format (filtered for compatibility)" \ No newline at end of file diff --git a/.github/workflows/core--initializer.yaml b/.github/workflows/core--initializer.yaml index 673d50a..9bc6453 100644 --- a/.github/workflows/core--initializer.yaml +++ b/.github/workflows/core--initializer.yaml @@ -63,8 +63,10 @@ jobs: hatch: "py3.12" "3.13": hatch: "py3.13" - "pypy3.10": - hatch: "pypy3.10" + "3.14": + hatch: "py3.14" + "pypy3.11": + hatch: "pypy3.11" EOF )" echo "specs=${python_descriptors}" >>${GITHUB_OUTPUT} @@ -79,7 +81,7 @@ jobs: # PyPy has slow I/O, even slower on Windows. items="$(jq --compact-output <<EOF [ - {"platform": "windows-latest", "python-version": "pypy3.10"} + {"platform": "windows-latest", "python-version": "pypy3.11"} ] EOF )" diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml index b9dbbef..619c511 100644 --- a/.github/workflows/releaser.yaml +++ b/.github/workflows/releaser.yaml @@ -55,9 +55,69 @@ jobs: artifacts-path: '.auxiliary/artifacts/hatch-build' # TODO: Use environment. python-version: '${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}' + check-if-publish: + needs: [package] + runs-on: ubuntu-latest + outputs: + package-version: ${{ steps.decide.outputs.package-version }} + should-publish: ${{ steps.decide.outputs.should-publish }} + tag-version: ${{ steps.decide.outputs.tag-version }} + steps: + + - name: Restore Distributions + uses: actions/download-artifact@v8 + with: + name: python-package-distributions--${{ github.run_id }} + path: ${{ env.DISTRIBUTIONS_PATH }} + + - name: Determine Publication Eligibility + id: decide + shell: python + run: | + from email import message_from_bytes + from os import environ + from pathlib import Path + from zipfile import ZipFile + + distributions = Path( environ[ 'DISTRIBUTIONS_PATH' ] ) + wheels = sorted( distributions.glob( '*.whl' ) ) + if len( wheels ) != 1: + raise SystemExit( f"Expected one wheel in {distributions}, found {len( wheels )}." ) + with ZipFile( wheels[ 0 ] ) as archive: + names = tuple( + name for name in archive.namelist( ) + if name.endswith( '.dist-info/METADATA' ) ) + if len( names ) != 1: + raise SystemExit( f"Expected one METADATA file in {wheels[ 0 ]}, found {len( names )}." ) + metadata = message_from_bytes( archive.read( names[ 0 ] ) ) + package_version = metadata[ 'Version' ] + ref = environ[ 'GITHUB_REF' ] + ref_name = environ[ 'GITHUB_REF_NAME' ] + should_publish = 'true' + tag_version = '' + reason = 'Publication is allowed for non-tag workflow dispatch.' + if ref.startswith( 'refs/tags/' ): + tag_version = ref_name[ 1: ] if ref_name.startswith( 'v' ) else ref_name + if tag_version == package_version: + reason = f"Tag version {tag_version} matches package version." + else: + should_publish = 'false' + reason = f"Tag version {tag_version} does not match package version {package_version}." + with open( environ[ 'GITHUB_OUTPUT' ], 'a', encoding = 'utf-8' ) as file: + print( f"package-version={package_version}", file = file ) + print( f"should-publish={should_publish}", file = file ) + print( f"tag-version={tag_version}", file = file ) + with open( environ[ 'GITHUB_STEP_SUMMARY' ], 'a', encoding = 'utf-8' ) as file: + print( '## Publication Eligibility', file = file ) + print( f"- Package version: `{package_version}`", file = file ) + if tag_version: + print( f"- Tag version: `{tag_version}`", file = file ) + print( f"- Should publish: `{should_publish}`", file = file ) + print( f"- Reason: {reason}", file = file ) + publish-pypi: - if: ${{ inputs.which-pypi == 'testpypi' || startsWith(github.ref, 'refs/tags/') }} - needs: [initialize, package] + if: ${{ needs.check-if-publish.outputs.should-publish == 'true' && (inputs.which-pypi == 'testpypi' || startsWith(github.ref, 'refs/tags/')) }} + needs: [initialize, package, check-if-publish] runs-on: ubuntu-latest environment: name: ${{ inputs.which-pypi || 'pypi' }} @@ -67,7 +127,7 @@ jobs: steps: - name: Restore Distributions - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions--${{ github.run_id }} path: ${{ env.DISTRIBUTIONS_PATH }} @@ -81,9 +141,11 @@ jobs: skip-existing: ${{ inputs.which-pypi == 'testpypi' }} publish-github: + if: ${{ always() && needs.check-if-publish.outputs.should-publish == 'true' && needs.package.result == 'success' && needs.publish-pypi.result == 'success' }} needs: - initialize - package + - check-if-publish - publish-pypi # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- @@ -100,7 +162,7 @@ jobs: python-version: ${{ fromJSON(needs.initialize.outputs.python-versions)[0] }} - name: Restore Distributions - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions--${{ github.run_id }} path: ${{ env.DISTRIBUTIONS_PATH }} diff --git a/.gitignore b/.gitignore index 5d97b3d..0e6bcbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,6 @@ .env -.claude -.gemini -.mcp.json -.opencode +*.so .*.swp -AGENTS.md -CLAUDE.md -CONVENTIONS.md -GEMINI.md -opencode.jsonc __pycache__/ bugs/ build/ diff --git a/README.rst b/README.rst index b36a90d..72d3e2d 100644 --- a/README.rst +++ b/README.rst @@ -254,6 +254,9 @@ Other Projects by This Author 🌟 * `python-accretive <https://github.com/emcd/python-accretive>`_ (`accretive <https://pypi.org/project/accretive/>`_ on PyPI) 🌌 A Python library package which provides **accretive data structures** - collections which can grow but never shrink. +* `python-detextive <https://github.com/emcd/python-detextive>`_ (`detextive <https://pypi.org/project/detextive/>`_ on PyPI) + + 🕵️ A Python library which provides consolidated text detection capabilities for reliable content analysis. Offers MIME type detection, character set detection, and line separator processing. * `python-dynadoc <https://github.com/emcd/python-dynadoc>`_ (`dynadoc <https://pypi.org/project/dynadoc/>`_ on PyPI) 📝 A Python library package which bridges the gap between **rich annotations** and **automatic documentation generation** with configurable renderers and support for reusable fragments. @@ -266,6 +269,9 @@ Other Projects by This Author 🌟 * `python-icecream-truck <https://github.com/emcd/python-icecream-truck>`_ (`icecream-truck <https://pypi.org/project/icecream-truck/>`_ on PyPI) 🍦 **Flavorful Debugging** - A Python library which enhances the powerful and well-known ``icecream`` package with flavored traces, configuration hierarchies, customized outputs, ready-made recipes, and more. +* `python-librovore <https://github.com/emcd/python-librovore>`_ (`librovore <https://pypi.org/project/librovore/>`_ on PyPI) + + 🐲 **Documentation Search Engine** - An intelligent documentation search and extraction tool that provides both a command-line interface for humans and an MCP (Model Context Protocol) server for AI agents. Search across Sphinx and MkDocs sites with fuzzy matching, extract clean markdown content, and integrate seamlessly with AI development workflows. * `python-mimeogram <https://github.com/emcd/python-mimeogram>`_ (`mimeogram <https://pypi.org/project/mimeogram/>`_ on PyPI) 📨 A command-line tool for **exchanging collections of files with Large Language Models** - bundle multiple files into a single clipboard-ready document while preserving directory structure and metadata... good for code reviews, project sharing, and LLM interactions. diff --git a/documentation/architecture/decisions/index.rst b/documentation/architecture/decisions/index.rst deleted file mode 100644 index a84ce73..0000000 --- a/documentation/architecture/decisions/index.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -Architectural Decision Records -******************************************************************************* - -.. toctree:: - :maxdepth: 2 - - -.. todo:: Add architectural decision records to toctree. - -For ADR format and guidance, see the `architecture documentation guide -<https://emcd.github.io/python-project-common/stable/sphinx-html/common/architecture.html>`_. \ No newline at end of file diff --git a/documentation/architecture/designs/index.rst b/documentation/architecture/designs/index.rst deleted file mode 100644 index ebd094d..0000000 --- a/documentation/architecture/designs/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -Designs -******************************************************************************* - -.. toctree:: - :maxdepth: 2 - - -.. todo:: Add design documents to toctree. diff --git a/documentation/architecture/filesystem.rst b/documentation/architecture/filesystem.rst deleted file mode 100644 index d3ca1b9..0000000 --- a/documentation/architecture/filesystem.rst +++ /dev/null @@ -1,86 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -Filesystem Organization -******************************************************************************* - -This document describes the specific filesystem organization for the project, -showing how the standard organizational patterns are implemented for this -project's configuration. For the underlying principles and rationale behind -these patterns, see the `common architecture documentation -<https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/architecture.rst>`_. - -Project Structure -=============================================================================== - -Root Directory Organization -------------------------------------------------------------------------------- - -The project implements the standard filesystem organization: - -.. code-block:: - - python-classcore/ - ├── LICENSE.txt # Project license - ├── README.rst # Project overview and quick start - ├── pyproject.toml # Python packaging and tool configuration - ├── documentation/ # Sphinx documentation source - ├── sources/ # All source code - ├── tests/ # Test suites - └── .auxiliary/ # Development workspace - -Source Code Organization -=============================================================================== - -Package Structure -------------------------------------------------------------------------------- - -The main Python package follows the standard ``sources/`` directory pattern: - -.. code-block:: - - sources/ - ├── classcore/ # Main Python package - │ ├── __/ # Centralized import hub - │ │ ├── __init__.py # Re-exports core utilities - │ │ ├── imports.py # External library imports - │ │ └── nomina.py # python-classcore-specific naming constants - │ ├── __init__.py # Package entry point - │ ├── py.typed # Type checking marker - │ └── [modules].py # Feature-specific modules - - -All package modules use the standard ``__`` import pattern as documented -in the common architecture guide. - -Component Integration -=============================================================================== - -Architecture Evolution -=============================================================================== - -This filesystem organization provides a foundation that architect agents can -evolve as the project grows. For questions about organizational principles, -subpackage patterns, or testing strategies, refer to the comprehensive common -documentation: - -* `Architecture Patterns <https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/architecture.rst>`_ -* `Development Practices <https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/practices.rst>`_ -* `Test Development Guidelines <https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst>`_ diff --git a/documentation/architecture/index.rst b/documentation/architecture/index.rst deleted file mode 100644 index 6f09ef9..0000000 --- a/documentation/architecture/index.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -Architecture -******************************************************************************* - -.. toctree:: - :maxdepth: 2 - - summary - filesystem - decisions/index - designs/index - testplans/index - - -.. todo:: Populate architecture documentation sections. \ No newline at end of file diff --git a/documentation/architecture/openspec/AGENTS.md b/documentation/architecture/openspec/AGENTS.md new file mode 100644 index 0000000..3e3c9a4 --- /dev/null +++ b/documentation/architecture/openspec/AGENTS.md @@ -0,0 +1,455 @@ +# OpenSpec Instructions + +Instructions for AI coding assistants using OpenSpec for spec-driven development. + +## TL;DR Quick Checklist + +- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search) +- Decide scope: new capability vs modify existing capability +- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`) +- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability +- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement +- Validate: `openspec validate [change-id] --strict` and fix issues +- Request approval: Do not start implementation until proposal is approved + +## Three-Stage Workflow + +### Stage 1: Creating Changes +Create proposal when you need to: +- Add features or functionality +- Make breaking changes (API, schema) +- Change architecture or patterns +- Optimize performance (changes behavior) +- Update security patterns + +Triggers (examples): +- "Help me create a change proposal" +- "Help me plan a change" +- "Help me create a proposal" +- "I want to create a spec proposal" +- "I want to create a spec" + +Loose matching guidance: +- Contains one of: `proposal`, `change`, `spec` +- With one of: `create`, `plan`, `make`, `start`, `help` + +Skip proposal for: +- Bug fixes (restore intended behavior) +- Typos, formatting, comments +- Dependency updates (non-breaking) +- Configuration changes +- Tests for existing behavior + +**Workflow** +1. Review top-level `AGENTS.md` and `openspec list`, `openspec list --specs` to understand current context. +2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`. +3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement. +4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal. + +### Stage 2: Implementing Changes +Track these steps as TODOs and complete them one by one. +1. **Read proposal.md** - Understand what's being built +2. **Read design.md** (if exists) - Review technical decisions +3. **Read tasks.md** - Get implementation checklist +4. **Implement tasks sequentially** - Complete in order +5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses +6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality +7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved + +### Stage 3: Archiving Changes +After deployment, create separate PR to: +- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/` +- Update `specs/` if capabilities changed +- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly) +- Run `openspec validate --strict` to confirm the archived change passes checks + +## Before Any Task + +**Context Checklist:** +- [ ] Read relevant specs in `specs/[capability]/spec.md` +- [ ] Check pending changes in `changes/` for conflicts +- [ ] Read top-level `AGENTS.md` for project conventions +- [ ] Run `openspec list` to see active changes +- [ ] Run `openspec list --specs` to see existing capabilities + +**Before Creating Specs:** +- Always check if capability already exists +- Prefer modifying existing specs over creating duplicates +- Use `openspec show [spec]` to review current state +- If request is ambiguous, ask 1–2 clarifying questions before scaffolding + +### Search Guidance +- Enumerate specs: `openspec spec list --long` (or `--json` for scripts) +- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available) +- Show details: + - Spec: `openspec show <spec-id> --type spec` (use `--json` for filters) + - Change: `openspec show <change-id> --json --deltas-only` +- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs` + +## Quick Start + +### CLI Commands + +```bash +# Essential commands +openspec list # List active changes +openspec list --specs # List specifications +openspec show [item] # Display change or spec +openspec validate [item] # Validate changes or specs +openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs) + +# Project management +openspec init [path] # Initialize OpenSpec +openspec update [path] # Update instruction files + +# Interactive mode +openspec show # Prompts for selection +openspec validate # Bulk validation mode + +# Debugging +openspec show [change] --json --deltas-only +openspec validate [change] --strict +``` + +### Command Flags + +- `--json` - Machine-readable output +- `--type change|spec` - Disambiguate items +- `--strict` - Comprehensive validation +- `--no-interactive` - Disable prompts +- `--skip-specs` - Archive without spec updates +- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive) + +## Directory Structure + +``` +openspec/ +├── specs/ # Current truth - what IS built +│ └── [capability]/ # Single focused capability +│ ├── spec.md # Requirements and scenarios +│ └── design.md # Technical patterns +├── changes/ # Proposals - what SHOULD change +│ ├── [change-name]/ +│ │ ├── proposal.md # Why, what, impact +│ │ ├── tasks.md # Implementation checklist +│ │ ├── design.md # Technical decisions (optional; see criteria) +│ │ └── specs/ # Delta changes +│ │ └── [capability]/ +│ │ └── spec.md # ADDED/MODIFIED/REMOVED +│ └── archive/ # Completed changes +``` + +## Creating Change Proposals + +### Decision Tree + +``` +New request? +├─ Bug fix restoring spec behavior? → Fix directly +├─ Typo/format/comment? → Fix directly +├─ New feature/capability? → Create proposal +├─ Breaking change? → Create proposal +├─ Architecture change? → Create proposal +└─ Unclear? → Create proposal (safer) +``` + +### Proposal Structure + +1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique) + +2. **Write proposal.md:** +```markdown +# Change: [Brief description of change] + +## Why +[1-2 sentences on problem/opportunity] + +## What Changes +- [Bullet list of changes] +- [Mark breaking changes with **BREAKING**] + +## Impact +- Affected specs: [list capabilities] +- Affected code: [key files/systems] +``` + +3. **Create spec deltas:** `specs/[capability]/spec.md` +```markdown +## ADDED Requirements +### Requirement: New Feature +The system SHALL provide... + +#### Scenario: Success case +- **WHEN** user performs action +- **THEN** expected result + +## MODIFIED Requirements +### Requirement: Existing Feature +[Complete modified requirement] + +## REMOVED Requirements +### Requirement: Old Feature +**Reason**: [Why removing] +**Migration**: [How to handle] +``` +If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability. + +4. **Create tasks.md:** +```markdown +## 1. Implementation +- [ ] 1.1 Create database schema +- [ ] 1.2 Implement API endpoint +- [ ] 1.3 Add frontend component +- [ ] 1.4 Write tests +``` + +5. **Create design.md when needed:** +Create `design.md` if any of the following apply; otherwise omit it: +- Cross-cutting change (multiple services/modules) or a new architectural pattern +- New external dependency or significant data model changes +- Security, performance, or migration complexity +- Ambiguity that benefits from technical decisions before coding + +Minimal `design.md` skeleton: +```markdown +## Context +[Background, constraints, stakeholders] + +## Goals / Non-Goals +- Goals: [...] +- Non-Goals: [...] + +## Decisions +- Decision: [What and why] +- Alternatives considered: [Options + rationale] + +## Risks / Trade-offs +- [Risk] → Mitigation + +## Migration Plan +[Steps, rollback] + +## Open Questions +- [...] +``` + +## Spec File Format + +### Critical: Scenario Formatting + +**CORRECT** (use #### headers): +```markdown +#### Scenario: User login success +- **WHEN** valid credentials provided +- **THEN** return JWT token +``` + +**WRONG** (don't use bullets or bold): +```markdown +- **Scenario: User login** ❌ +**Scenario**: User login ❌ +### Scenario: User login ❌ +``` + +Every requirement MUST have at least one scenario. + +### Requirement Wording +- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative) + +### Delta Operations + +- `## ADDED Requirements` - New capabilities +- `## MODIFIED Requirements` - Changed behavior +- `## REMOVED Requirements` - Deprecated features +- `## RENAMED Requirements` - Name changes + +Headers matched with `trim(header)` - whitespace ignored. + +#### When to use ADDED vs MODIFIED +- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement. +- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details. +- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name. + +Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead. + +Authoring a MODIFIED requirement correctly: +1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`. +2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios). +3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior. +4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`. + +Example for RENAMED: +```markdown +## RENAMED Requirements +- FROM: `### Requirement: Login` +- TO: `### Requirement: User Authentication` +``` + +## Troubleshooting + +### Common Errors + +**"Change must have at least one delta"** +- Check `changes/[name]/specs/` exists with .md files +- Verify files have operation prefixes (## ADDED Requirements) + +**"Requirement must have at least one scenario"** +- Check scenarios use `#### Scenario:` format (4 hashtags) +- Don't use bullet points or bold for scenario headers + +**Silent scenario parsing failures** +- Exact format required: `#### Scenario: Name` +- Debug with: `openspec show [change] --json --deltas-only` + +### Validation Tips + +```bash +# Always use strict mode for comprehensive checks +openspec validate [change] --strict + +# Debug delta parsing +openspec show [change] --json | jq '.deltas' + +# Check specific requirement +openspec show [spec] --json -r 1 +``` + +## Happy Path Script + +```bash +# 1) Explore current state +openspec spec list --long +openspec list +# Optional full-text search: +# rg -n "Requirement:|Scenario:" openspec/specs +# rg -n "^#|Requirement:" openspec/changes + +# 2) Choose change id and scaffold +CHANGE=add-two-factor-auth +mkdir -p openspec/changes/$CHANGE/{specs/auth} +printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md +printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md + +# 3) Add deltas (example) +cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF' +## ADDED Requirements +### Requirement: Two-Factor Authentication +Users MUST provide a second factor during login. + +#### Scenario: OTP required +- **WHEN** valid credentials are provided +- **THEN** an OTP challenge is required +EOF + +# 4) Validate +openspec validate $CHANGE --strict +``` + +## Multi-Capability Example + +``` +openspec/changes/add-2fa-notify/ +├── proposal.md +├── tasks.md +└── specs/ + ├── auth/ + │ └── spec.md # ADDED: Two-Factor Authentication + └── notifications/ + └── spec.md # ADDED: OTP email notification +``` + +auth/spec.md +```markdown +## ADDED Requirements +### Requirement: Two-Factor Authentication +... +``` + +notifications/spec.md +```markdown +## ADDED Requirements +### Requirement: OTP Email Notification +... +``` + +## Best Practices + +### Simplicity First +- Default to <100 lines of new code +- Single-file implementations until proven insufficient +- Avoid frameworks without clear justification +- Choose boring, proven patterns + +### Complexity Triggers +Only add complexity with: +- Performance data showing current solution too slow +- Concrete scale requirements (>1000 users, >100MB data) +- Multiple proven use cases requiring abstraction + +### Clear References +- Use `file.ts:42` format for code locations +- Reference specs as `specs/auth/spec.md` +- Link related changes and PRs + +### Capability Naming +- Use verb-noun: `user-auth`, `payment-capture` +- Single purpose per capability +- 10-minute understandability rule +- Split if description needs "AND" + +### Change ID Naming +- Use kebab-case, short and descriptive: `add-two-factor-auth` +- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-` +- Ensure uniqueness; if taken, append `-2`, `-3`, etc. + +## Tool Selection Guide + +| Task | Tool | Why | +|------|------|-----| +| Find files by pattern | Glob | Fast pattern matching | +| Search code content | Grep | Optimized regex search | +| Read specific files | Read | Direct file access | +| Explore unknown scope | Task | Multi-step investigation | + +## Error Recovery + +### Change Conflicts +1. Run `openspec list` to see active changes +2. Check for overlapping specs +3. Coordinate with change owners +4. Consider combining proposals + +### Validation Failures +1. Run with `--strict` flag +2. Check JSON output for details +3. Verify spec file format +4. Ensure scenarios properly formatted + +### Missing Context +1. Read top-level `AGENTS.md` first +2. Check related specs +3. Review recent archives +4. Ask for clarification + +## Quick Reference + +### Stage Indicators +- `changes/` - Proposed, not yet built +- `specs/` - Built and deployed +- `archive/` - Completed changes + +### File Purposes +- `proposal.md` - Why and what +- `tasks.md` - Implementation steps +- `design.md` - Technical decisions +- `spec.md` - Requirements and behavior + +### CLI Essentials +```bash +openspec list # What's in progress? +openspec show [item] # View details +openspec validate --strict # Is it correct? +openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation) +``` + +Remember: Specs are truth. Changes are proposals. Keep them in sync. diff --git a/documentation/architecture/openspec/changes/archive/.gitkeep b/documentation/architecture/openspec/changes/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/documentation/architecture/openspec/specs/.gitkeep b/documentation/architecture/openspec/specs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/documentation/architecture/openspec/specs/attribute-concealment/spec.md b/documentation/architecture/openspec/specs/attribute-concealment/spec.md new file mode 100644 index 0000000..2b7ef29 --- /dev/null +++ b/documentation/architecture/openspec/specs/attribute-concealment/spec.md @@ -0,0 +1,81 @@ +# Attribute Concealment + +## Purpose + +The library SHALL provide mechanisms to conceal non-public attributes from +`dir()` output, reducing API noise and clearly communicating public interfaces. + +## Requirements + +### Requirement: Default Visibility Filtering + +By default, only public attributes (not starting with `_`) MUST be visible +in `dir()` output for classes and instances using standard behaviors. + +#### Scenario: Public attributes visible +- **WHEN** `dir()` is called on a class or instance with standard behaviors +- **THEN** attributes not starting with `_` MUST appear in the result + +#### Scenario: Non-public attributes concealed +- **WHEN** `dir()` is called on a class or instance with standard behaviors +- **THEN** attributes starting with `_` MUST NOT appear in the result + +#### Scenario: Non-public attributes still accessible +- **WHEN** a concealed attribute is accessed directly by name +- **THEN** the attribute MUST be accessible + +### Requirement: Class Visibility Customization + +Users MUST be able to specify which class attributes are visible via +`class_visibles`. + +#### Scenario: Named visible attribute +- **WHEN** an attribute name is listed in `class_visibles` +- **THEN** that attribute MUST appear in `dir()` even if non-public + +#### Scenario: Non-visible attribute +- **WHEN** an attribute name is NOT in `class_visibles` +- **THEN** concealment rules apply normally + +### Requirement: Instance Visibility Customization + +Users MUST be able to specify which instance attributes are visible via +`instances_visibles`. + +#### Scenario: Named visible instance attribute +- **WHEN** an attribute name is listed in `instances_visibles` +- **THEN** that attribute MUST appear in `dir()` even if non-public + +### Requirement: Visibility Verifiers + +Visibility verifiers MUST support names, regexes, and predicates, matching +the mutability verifier interface. + +#### Scenario: Regex-based visibility +- **WHEN** an attribute name matches a regex in `class_visibles` or `instances_visibles` +- **THEN** that attribute MUST appear in `dir()` + +#### Scenario: Predicate-based visibility +- **WHEN** a predicate returns `True` for an attribute name +- **THEN** that attribute MUST appear in `dir()` + +### Requirement: Wildcard Visibility + +Users MUST be able to disable concealment entirely using a literal `'*'`. + +#### Scenario: Wildcard shows all attributes +- **WHEN** `class_visibles` or `instances_visibles` is set to `'*'` +- **THEN** all attributes MUST appear in `dir()` (Python default behavior) + +### Requirement: Visibility Verifier Inheritance + +Visibility verifiers MUST be inherited and composable across class +hierarchies. + +#### Scenario: Inherited visibility names +- **WHEN** a subclass inherits from a class with custom visibles +- **THEN** the subclass MUST inherit those visible names + +#### Scenario: Composed visibility names +- **WHEN** a subclass defines additional visibles +- **THEN** both parent and child visibles MUST be active diff --git a/documentation/architecture/openspec/specs/attribute-immutability/spec.md b/documentation/architecture/openspec/specs/attribute-immutability/spec.md new file mode 100644 index 0000000..bbb7a55 --- /dev/null +++ b/documentation/architecture/openspec/specs/attribute-immutability/spec.md @@ -0,0 +1,96 @@ +# Attribute Immutability + +## Purpose + +The library SHALL provide mechanisms to make class and instance attributes +immutable after initialization, with support for selective mutability via +names, regexes, or predicates. + +## Requirements + +### Requirement: Class Attribute Immutability + +Class attributes produced by standard metaclasses MUST be immutable after +class construction completes. + +#### Scenario: Assignment to immutable class attribute +- **WHEN** assignment is attempted on a class attribute after construction +- **THEN** `AttributeImmutability` MUST be raised + +#### Scenario: Deletion of immutable class attribute +- **WHEN** deletion is attempted on a class attribute +- **THEN** `AttributeImmutability` MUST be raised + +### Requirement: Instance Attribute Immutability + +Instance attributes of standard classes MUST be immutable after `__init__` +and `__post_init__` complete. + +#### Scenario: Assignment to immutable instance attribute +- **WHEN** assignment is attempted on an instance attribute after initialization +- **THEN** `AttributeImmutability` MUST be raised + +#### Scenario: Deletion of immutable instance attribute +- **WHEN** deletion is attempted on an instance attribute +- **THEN** `AttributeImmutability` MUST be raised + +#### Scenario: Mutability during initialization +- **WHEN** attributes are assigned within `__init__` or `__post_init__` +- **THEN** assignment MUST succeed + +### Requirement: Selective Mutability by Name + +Users MUST be able to specify mutable attributes by exact name. + +#### Scenario: Named mutable attribute +- **WHEN** an attribute name is listed in `class_mutables` or `instances_mutables` +- **THEN** that attribute MUST be mutable + +#### Scenario: Non-mutable attribute with name-based exceptions +- **WHEN** an attribute name is NOT in the mutables list +- **THEN** immutability MUST be enforced + +### Requirement: Selective Mutability by Regex + +Users MUST be able to specify mutable attributes by compiled regex pattern. + +#### Scenario: Regex-matched mutable attribute +- **WHEN** an attribute name matches a regex in `class_mutables` or `instances_mutables` +- **THEN** that attribute MUST be mutable + +#### Scenario: Regex non-match +- **WHEN** an attribute name does not match any regex +- **THEN** immutability MUST be enforced + +### Requirement: Selective Mutability by Predicate + +Users MUST be able to specify mutable attributes by predicate function. + +#### Scenario: Predicate-matched mutable attribute +- **WHEN** a predicate function returns `True` for an attribute name +- **THEN** that attribute MUST be mutable + +#### Scenario: Predicate non-match +- **WHEN** all predicates return `False` for an attribute name +- **THEN** immutability MUST be enforced + +### Requirement: Wildcard Mutability + +Users MUST be able to disable immutability entirely using a literal `'*'`. + +#### Scenario: Wildcard disables immutability +- **WHEN** `class_mutables` or `instances_mutables` is set to `'*'` +- **THEN** all attributes of that level MUST be mutable + +### Requirement: Mutability Verifier Inheritance + +Mutability verifiers (names, regexes, predicates) MUST be inherited and +composable across class hierarchies. + +#### Scenario: Inherited mutability names +- **WHEN** a subclass inherits from a class with named mutables +- **THEN** the subclass MUST inherit those mutable names + +#### Scenario: Composed mutability names +- **WHEN** a subclass defines additional mutables +- **THEN** both parent and child mutables MUST be active diff --git a/documentation/architecture/openspec/specs/class-factories/spec.md b/documentation/architecture/openspec/specs/class-factories/spec.md new file mode 100644 index 0000000..b1e5ceb --- /dev/null +++ b/documentation/architecture/openspec/specs/class-factories/spec.md @@ -0,0 +1,96 @@ +# Class Factories + +## Purpose + +The library SHALL provide factory functions and hook systems for users to +create custom metaclasses, behaviors, and decorators with full control over +class construction and initialization lifecycles. + +## Requirements + +### Requirement: Metaclass Constructor Factory + +The `produce_class_constructor` factory MUST generate customizable `__new__` +implementations with hook support. + +#### Scenario: Custom constructor with hooks +- **WHEN** `produce_class_constructor` is called with preprocessor and postprocessor hooks +- **THEN** the resulting constructor MUST invoke hooks at the appropriate lifecycle points + +### Requirement: Metaclass Initializer Factory + +The `produce_class_initializer` factory MUST generate customizable `__init__` +implementations with hook support. + +#### Scenario: Custom initializer with completers +- **WHEN** `produce_class_initializer` is called with completer hooks +- **THEN** the resulting initializer MUST invoke completers after initialization + +### Requirement: Construction Preprocessors + +Preprocessors MUST be able to modify class construction inputs (bases, +namespace, arguments, decorators) before class creation. + +#### Scenario: Preprocessor modifies bases +- **WHEN** a preprocessor modifies the bases tuple +- **THEN** the class MUST be created with the modified bases + +#### Scenario: Preprocessor modifies namespace +- **WHEN** a preprocessor modifies the namespace dict +- **THEN** the class MUST be created with the modified namespace + +### Requirement: Construction Postprocessors + +Postprocessors MUST be able to augment constructed classes after creation. + +#### Scenario: Postprocessor adds attributes +- **WHEN** a postprocessor adds attributes to the class +- **THEN** the class MUST have those attributes + +### Requirement: Initialization Completers + +Completers MUST be able to finalize initialized classes (e.g., freezing +behaviors) after `__init__` completes. + +#### Scenario: Completer runs after init +- **WHEN** `__init__` completes +- **THEN** completers MUST execute + +### Requirement: Decoration Preparers + +Preparers MUST be able to adjust decorator lists before application. + +#### Scenario: Preparer modifies decorators +- **WHEN** a preparer modifies the decorator list +- **THEN** the modified decorators MUST be applied + +### Requirement: Core Function Replacement + +Critical behaviors (assignment, deletion, visibility) MUST be replaceable +by providing alternative core functions. + +#### Scenario: Custom assigner +- **WHEN** a custom assigner core function is provided +- **THEN** attribute assignment MUST use the custom function + +#### Scenario: Custom surveyor +- **WHEN** a custom surveyor core function is provided +- **THEN** `dir()` MUST use the custom function + +### Requirement: Decorator Composition + +The `decoration_by` function MUST compose multiple decorators with +preparation hooks. + +#### Scenario: Composed decorators +- **WHEN** `decoration_by` is called with multiple decorators +- **THEN** all decorators MUST be applied in sequence + +### Requirement: Custom Error Classes + +Users MUST be able to provide custom error class providers for +immutability violations. + +#### Scenario: Custom error raised +- **WHEN** a custom error class provider is configured +- **THEN** immutability violations MUST raise the custom error diff --git a/documentation/architecture/openspec/specs/dataclass-integration/spec.md b/documentation/architecture/openspec/specs/dataclass-integration/spec.md new file mode 100644 index 0000000..7625d0f --- /dev/null +++ b/documentation/architecture/openspec/specs/dataclass-integration/spec.md @@ -0,0 +1,75 @@ +# Dataclass Integration + +## Purpose + +The library SHALL automatically convert classes to dataclasses while +applying immutability and concealment, preserving all dataclass functionality +including `__post_init__` attribute modification. + +## Requirements + +### Requirement: Metaclass Auto-Conversion + +The `Dataclass` metaclass MUST automatically apply `@dataclass` decorator +during class construction. + +#### Scenario: Dataclass produced by metaclass +- **WHEN** a class uses the `Dataclass` metaclass +- **THEN** the resulting class MUST be a dataclass with standard behaviors + +#### Scenario: DataclassObject base class +- **WHEN** a class inherits from `DataclassObject` +- **THEN** the resulting class MUST be a dataclass with standard behaviors + +### Requirement: Decorator-Based Creation + +The `dataclass_with_standard_behaviors` decorator MUST create a dataclass +with standard behaviors applied. + +#### Scenario: Decorated dataclass +- **WHEN** `@dataclass_with_standard_behaviors()` is applied to a class +- **THEN** the resulting class MUST be a dataclass with immutability and concealment + +### Requirement: Post-Init Modification + +`__post_init__` MUST be able to modify attributes before immutability is +enforced. + +#### Scenario: Post-init attribute assignment +- **WHEN** `__post_init__` assigns attributes +- **THEN** assignment MUST succeed + +#### Scenario: Post-init immutability enforcement +- **WHEN** `__post_init__` completes +- **THEN** attributes MUST become immutable + +### Requirement: Dataclass Argument Compatibility + +Standard metaclasses and decorators MUST be compatible with `dataclass` +arguments including `frozen`, `slots`, `kw_only`, etc. + +#### Scenario: Slots mode +- **WHEN** `slots=True` is passed to a standard dataclass metaclass +- **THEN** the resulting class MUST use `__slots__` and maintain behaviors + +#### Scenario: Kw-only mode +- **WHEN** `kw_only=True` is passed to a standard dataclass metaclass +- **THEN** all fields MUST be keyword-only + +### Requirement: Class Replacement Handling + +When `slots=True` causes class replacement, the library MUST properly +handle closure repair so that `super()` continues to function. + +#### Scenario: Super after class replacement +- **WHEN** a dataclass with `slots=True` replaces the class +- **THEN** `super()` calls in methods MUST continue to work correctly + +### Requirement: Type Checker Recognition + +Metaclasses MUST use `@dataclass_transform` to signal dataclass behavior +to type checkers. + +#### Scenario: Type checker infers dataclass +- **WHEN** a class uses a standard dataclass metaclass +- **THEN** type checkers MUST recognize it as a dataclass diff --git a/documentation/architecture/openspec/specs/module-reclassification/spec.md b/documentation/architecture/openspec/specs/module-reclassification/spec.md new file mode 100644 index 0000000..d050734 --- /dev/null +++ b/documentation/architecture/openspec/specs/module-reclassification/spec.md @@ -0,0 +1,63 @@ +# Module Reclassification + +## Purpose + +The library SHALL apply immutability and concealment to module objects, +enabling package-level API control and protection of module namespaces. + +## Requirements + +### Requirement: Single Module Reclassification + +The `reclassify_module` function MUST apply immutability and concealment to +a single module object. + +#### Scenario: Module attributes become immutable +- **WHEN** `reclassify_module` is applied to a module +- **THEN** module attributes MUST become immutable + +#### Scenario: Non-public module attributes concealed +- **WHEN** `dir()` is called on a reclassified module +- **THEN** non-public attributes MUST NOT appear + +#### Scenario: Public module API visible +- **WHEN** `dir()` is called on a reclassified module +- **THEN** public attributes MUST appear + +### Requirement: Module Finalization + +The `finalize_module` function MUST apply behaviors and dynadoc +configuration to complete module initialization. + +#### Scenario: Finalized module has behaviors +- **WHEN** `finalize_module` is applied to a module +- **THEN** the module MUST have immutability and concealment + +#### Scenario: Finalized module has documentation +- **WHEN** `finalize_module` is applied to a module +- **THEN** dynadoc configuration MUST be applied + +### Requirement: Recursive Application + +Module reclassification MUST support recursive application to submodules. + +#### Scenario: Recursive reclassification +- **WHEN** `reclassify_modules` is called with `recursive=True` +- **THEN** all submodules MUST be reclassified + +### Requirement: Module Attribute Immutability + +After reclassification, module attributes MUST NOT be modifiable. + +#### Scenario: Assignment to reclassified module attribute +- **WHEN** assignment is attempted on a reclassified module attribute +- **THEN** an error MUST be raised + +### Requirement: Module Attribute Concealment + +After reclassification, non-public module attributes MUST be concealed +from `dir()` output. + +#### Scenario: Concealed module attribute +- **WHEN** `dir()` is called on a reclassified module +- **THEN** attributes starting with `_` MUST NOT appear (unless explicitly visible) diff --git a/documentation/architecture/openspec/specs/protocol-support/spec.md b/documentation/architecture/openspec/specs/protocol-support/spec.md new file mode 100644 index 0000000..a7f3b40 --- /dev/null +++ b/documentation/architecture/openspec/specs/protocol-support/spec.md @@ -0,0 +1,67 @@ +# Protocol Support + +## Purpose + +The library SHALL provide protocol classes with immutability and concealment +that work with structural subtyping, preserving protocol functionality +including runtime checks. + +## Requirements + +### Requirement: Protocol Metaclass + +The `Protocol` metaclass MUST combine `typing.Protocol` with standard +behaviors. + +#### Scenario: Protocol class creation +- **WHEN** a class uses the `Protocol` metaclass +- **THEN** the resulting class MUST support structural subtyping with immutability and concealment + +### Requirement: Protocol Base Class + +`ProtocolObject` MUST serve as a base class for protocol definitions with +standard behaviors. + +#### Scenario: Protocol inheritance +- **WHEN** a class inherits from `ProtocolObject` +- **THEN** the resulting class MUST be a protocol with immutability and concealment + +### Requirement: Protocol Decorator + +The `protocol_with_standard_behaviors` decorator MUST create protocol classes +with standard behaviors. + +#### Scenario: Decorated protocol +- **WHEN** `@protocol_with_standard_behaviors()` is applied to a class +- **THEN** the resulting class MUST be a protocol with immutability and concealment + +### Requirement: Structural Subtyping Preservation + +Protocol functionality MUST be preserved, including structural subtyping +and runtime checks. + +#### Scenario: Structural subtyping +- **WHEN** a class implements all protocol methods +- **THEN** `isinstance()` MUST return `True` for that protocol + +#### Scenario: Runtime checkable protocols +- **WHEN** a protocol is decorated with `@runtime_checkable` +- **THEN** `isinstance()` checks MUST work correctly + +### Requirement: ABC Attribute Exemptions + +ABC-related mutable attributes (`_abc_cache`, `_abc_registry`, etc.) MUST +be allowed by default to ensure protocol functionality. + +#### Scenario: ABC cache mutation +- **WHEN** Python internals modify `_abc_cache` on a protocol class +- **THEN** no immutability error MUST be raised + +### Requirement: Implementation Inheritance + +Immutability and concealment MUST be inherited by classes implementing +protocol interfaces. + +#### Scenario: Implementation inherits behaviors +- **WHEN** a class implements a protocol with standard behaviors +- **THEN** the implementation MUST inherit immutability and concealment diff --git a/documentation/architecture/openspec/specs/type-safety/spec.md b/documentation/architecture/openspec/specs/type-safety/spec.md new file mode 100644 index 0000000..49cbe5e --- /dev/null +++ b/documentation/architecture/openspec/specs/type-safety/spec.md @@ -0,0 +1,108 @@ +# Type Safety + +## Purpose + +The library SHALL ensure full compatibility with static type checkers and +multiple Python implementations, with graceful degradation for +implementation-specific features. + +## Requirements + +### Requirement: Complete Type Annotations + +All public APIs MUST have complete type annotations. + +#### Scenario: Public function annotated +- **WHEN** a public function is defined +- **THEN** all parameters and return types MUST have annotations + +#### Scenario: Public class annotated +- **WHEN** a public class is defined +- **THEN** all public methods MUST have complete annotations + +### Requirement: Dataclass Transform Signaling + +Metaclasses MUST use `@dataclass_transform` to signal dataclass behavior +to type checkers. + +#### Scenario: Type checker recognizes dataclass +- **WHEN** a class uses a standard dataclass metaclass +- **THEN** type checkers MUST infer dataclass fields and methods + +### Requirement: Generic Type Variables + +Generic type variables MUST be properly constrained. + +#### Scenario: Constrained TypeVar +- **WHEN** a TypeVar is used in a public API +- **THEN** it MUST have appropriate constraints or bounds + +### Requirement: Pyright Strict Mode + +The package MUST comply with Pyright strict mode. + +#### Scenario: Pyright strict check passes +- **WHEN** Pyright runs in strict mode on the package +- **THEN** no errors MUST be reported + +### Requirement: Py.typed Marker + +The package MUST include a `py.typed` marker file. + +#### Scenario: PEP 561 compliance +- **WHEN** the package is installed +- **THEN** type checkers MUST recognize it as typed + +### Requirement: CPython Compatibility + +Core functionality MUST work on CPython 3.10+. + +#### Scenario: CPython 3.10 test pass +- **WHEN** tests run on CPython 3.10 +- **THEN** all tests MUST pass + +#### Scenario: CPython 3.13 test pass +- **WHEN** tests run on CPython 3.13 +- **THEN** all tests MUST pass + +### Requirement: PyPy Compatibility + +Core functionality MUST work on PyPy 3.10+. + +#### Scenario: PyPy test pass +- **WHEN** tests run on PyPy 3.10 +- **THEN** all tests MUST pass + +### Requirement: Platform-Specific Feature Detection + +Platform-specific optimizations (closure repair) MUST be detected and +applied conditionally. + +#### Scenario: CPython closure repair +- **WHEN** running on CPython +- **THEN** CPython-specific closure repair MUST be applied + +#### Scenario: PyPy closure repair +- **WHEN** running on PyPy +- **THEN** PyPy-specific closure repair MUST be applied + +### Requirement: No Unsafe Operations + +The package MUST NOT use `exec`, `eval`, or `ctypes`. + +#### Scenario: No exec usage +- **WHEN** the source code is analyzed +- **THEN** no `exec` calls MUST be found + +#### Scenario: No eval usage +- **WHEN** the source code is analyzed +- **THEN** no `eval` calls MUST be found + +### Requirement: Comprehensive Test Coverage + +The package MUST maintain comprehensive test coverage across all supported +implementations. + +#### Scenario: Coverage threshold +- **WHEN** coverage is measured +- **THEN** coverage MUST be at least 95% diff --git a/documentation/architecture/summary.rst b/documentation/architecture/summary.rst deleted file mode 100644 index bab368a..0000000 --- a/documentation/architecture/summary.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -System Overview -******************************************************************************* - -.. todo:: Describe the high-level system architecture, major components, and their relationships. \ No newline at end of file diff --git a/documentation/architecture/testplans/index.rst b/documentation/architecture/testplans/index.rst deleted file mode 100644 index 3db3d71..0000000 --- a/documentation/architecture/testplans/index.rst +++ /dev/null @@ -1,30 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -Test Plans -******************************************************************************* - -.. toctree:: - :maxdepth: 2 - - summary - - -.. todo:: Add test plan documents to toctree. \ No newline at end of file diff --git a/documentation/architecture/testplans/summary.rst b/documentation/architecture/testplans/summary.rst deleted file mode 100644 index d480c7f..0000000 --- a/documentation/architecture/testplans/summary.rst +++ /dev/null @@ -1,86 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -Test Organization Summary -******************************************************************************* - -Overview -=============================================================================== - -This section contains comprehensive test planning documentation, including test -organization conventions, coverage strategies, and detailed implementation -plans for achieving systematic test coverage. - -Test plans follow project testing principles described in the `common test -development guidelines -<https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst>`_. -Key principles include: - -- **Dependency injection over monkey-patching** for testable code architecture -- **Systematic coverage analysis** with clear gap identification -- **Performance-conscious resource use** with appropriate testing strategies -- **Organized test structure** with numbered modules and functions - -Test Planning Process -=============================================================================== - -The test planning process systematically addresses: - -**Coverage Gap Analysis** - Identification of all uncovered lines and untested functionality across modules - -**Test Strategy Development** - Comprehensive approaches for testing each function, class, and method with - appropriate test data strategies - -**Implementation Guidance** - Detailed plans for achieving coverage while following project testing principles - -**Architectural Considerations** - Analysis of testability constraints and recommendations for maintaining - clean, testable code - -Test Module Numbering Scheme -=============================================================================== - -.. todo:: Define project-specific test module numbering conventions. - -Test Function Numbering -=============================================================================== - -Within each test module, functions are numbered by component: - -- **000-099**: Basic functionality tests for the module -- **100-199, 200-299, etc.**: Each function/class gets its own 100-number block -- **Increments of 10-20**: For closely related test variations within a block - -Project-Specific Testing Conventions -=============================================================================== - -For detailed testing conventions, patterns, and guidelines, refer to the `common -test development guidelines -<https://raw.githubusercontent.com/emcd/python-project-common/refs/tags/docs-1/documentation/common/tests.rst>`_. -This includes: - -- Coverage goals and strategies -- Performance considerations -- Test data organization patterns -- Dependency injection approaches -- Resource management during testing \ No newline at end of file diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 4790175..3dc5d26 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,15 @@ Release Notes .. towncrier release notes start +classcore 1.11 (2026-07-19) +=========================== + +Enhancements +------------ + +- Export ``ClassDecorator``, ``ClassDecorators``, ``ClassDecoratorFactory``, and ``ModuleReclassifier`` type aliases for downstream consumption. + + classcore 1.10 (2025-09-25) =========================== diff --git a/documentation/conf.py b/documentation/conf.py index fb7f5b7..2b15bda 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -49,18 +49,28 @@ def _import_version( ): 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.githubpages', + 'myst_parser', 'sphinx_copybutton', 'sphinx_inline_tabs', ] templates_path = [ '_templates' ] -exclude_patterns = [ ] +exclude_patterns = [ + # Openspec workflow/meta files (not documentation) + 'architecture/openspec/AGENTS.md', + 'architecture/openspec/changes/**', +] rst_prolog = f''' .. |project| replace:: {project} ''' +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + nitpicky = True nitpick_ignore = [ # Workaround for https://bugs.python.org/issue11975 @@ -80,8 +90,6 @@ def _import_version( ): ( 'py:class', "builtins.NotImplementedType" ), ( 'py:class', "classcore.__.T" ), ( 'py:class', "classcore.__.U" ), - ( 'py:class', "dynadoc.context.Context" ), - ( 'py:class', "dynadoc.context.IntrospectionControl" ), ( 'py:class', "typing_extensions._ProtocolMeta" ), ( 'py:class', "typing_extensions.Any" ), ] @@ -102,6 +110,8 @@ def _import_version( ): r'https://github\.com/.*/.*/blob/.*', # Avoid timeouts for slow sites. r'http://www\.catb\.org/~esr/faqs/smart-questions\.html', + # GitHub intermittently returns 504 errors. + r'https://github\.com/.*', ] # -- Options for HTML output ------------------------------------------------- @@ -133,12 +143,36 @@ def _import_version( ): # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration intersphinx_mapping = { + 'dynadoc': ( + 'https://emcd.github.io/python-dynadoc/stable/sphinx-html', None), 'python': ( 'https://docs.python.org/3', None), 'typing-extensions': ( 'https://typing-extensions.readthedocs.io/en/latest', None), + # --- BEGIN: Injected by Copier --- + # --- END: Injected by Copier --- } +# -- Options for Myst extension ---------------------------------------------- + +# https://myst-parser.readthedocs.io/en/latest/syntax/optional.html +myst_enable_extensions = [ + # 'amsmath', + # 'attrs_inline', + 'colon_fence', # ::: blocks + 'deflist', # Definition lists + # 'dollarmath', + # 'fieldlist', + # 'html_admonition', + # 'html_image', + # 'linkify', + # 'replacements', + # 'smartquotes', + # 'strikethrough', + # 'substitution', + 'tasklist', # - [ ] tasks +] + # -- Options for todo extension ---------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration diff --git a/documentation/contribution.rst b/documentation/contribution.rst index a2cea55..92581e9 100644 --- a/documentation/contribution.rst +++ b/documentation/contribution.rst @@ -49,26 +49,15 @@ Development Architecture ------------------------------------------------------------------------------- -* The :doc:`Product Requirements Document <prd>` is a good starting point to - understand the motivations and rationale for the project. This should be - reviewed and updated, as necessary, when making changes that affect product - functionality or user experience. See the `requirements documentation guide - <https://emcd.github.io/python-project-common/stable/sphinx-html/common/requirements.html>`_ - for PRD format and best practices. - -* The :doc:`system architecture overview <architecture/summary>` should be +* The :doc:`capability specifications <specifications/index>` provide a good + starting point to understand the requirements and motivations for the project. + These should be reviewed and updated through the Openspec workflow when making + changes that affect product functionality or user experience. + +* The architecture documentation in `sources/classcore/README.md` should be reviewed to understand the structure and operational patterns of the project. Major changes to the architecture should be reflected in this document. -* Document significant architectural decisions using Architectural Decision - Records (ADRs) in the ``architecture/decisions/`` directory. See the - `architecture documentation guide - <https://emcd.github.io/python-project-common/stable/sphinx-html/common/architecture.html>`_ - for ADR format and best practices. - -* Document interface specifications, schemas, and algorithms in the - ``architecture/designs/`` directory to guide implementation efforts. - Guidance and Standards ------------------------------------------------------------------------------- @@ -129,6 +118,34 @@ Resources .. toctree:: :maxdepth: 2 - prd - architecture/index + Code of Conduct <https://emcd.github.io/python-project-common/stable/sphinx-html/common/conduct.html> + specifications/index devapi + Environment Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/environment.html> + Practices Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/practices.html> + Code Style Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/style.html> + Testing Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/tests.html> + Nomenclature Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/nomenclature.html> + Germanic Nomenclature Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/nomenclature-germanic.html> + Validation Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/validation.html> + Release Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/releases.html> + Maintenance Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/maintenance.html> + Architecture Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/architecture.html> + Requirements Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/requirements.html> + +.. Language-specific guides stay in a hidden toctree so Sphinx includes them + in the site graph without adding renderer-specific navigation glue to the + portable common Markdown sources. +.. toctree:: + :hidden: + + Python Environment Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/environment-python.html> + Python Architecture Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/architecture-python.html> + Python Development Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/practices-python.html> + Rust Development Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/practices-rust.html> + TOML Configuration Practices <https://emcd.github.io/python-project-common/stable/sphinx-html/common/practices-toml.html> + Latin-derived Verb Vocabulary <https://emcd.github.io/python-project-common/stable/sphinx-html/common/nomenclature-latin.html> + Python Automatic Formatting <https://emcd.github.io/python-project-common/stable/sphinx-html/common/python-autoformat.html> + Python Testing Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/tests-python.html> + Python Validation Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/validation-python.html> + Python Release Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/releases-python.html> diff --git a/documentation/examples/std-behaviors.rst b/documentation/examples/std-behaviors.rst index 61bc51a..f766f22 100644 --- a/documentation/examples/std-behaviors.rst +++ b/documentation/examples/std-behaviors.rst @@ -265,7 +265,7 @@ the produced class itself. ... x: float ... y: float ... - >>> '__annotations__' in dir( Point2d ) + >>> '__annotations__' in dir( Point2d ) or '__annotations_cache__' in dir( Point2d ) True The ``instances_visibles`` metaclass argument controls visiblity of attributes diff --git a/documentation/examples/std-dataclasses.rst b/documentation/examples/std-dataclasses.rst index c51e3c5..b3ac506 100644 --- a/documentation/examples/std-dataclasses.rst +++ b/documentation/examples/std-dataclasses.rst @@ -129,7 +129,7 @@ class attributes: True >>> type( Point2d ) <class 'type'> - >>> '__annotations__' in dir( Point2d ) + >>> '__annotations__' in dir( Point2d ) or '__annotations_cache__' in dir( Point2d ) True >>> del Point2d.__annotations__ diff --git a/documentation/prd.rst b/documentation/prd.rst deleted file mode 100644 index 85bf640..0000000 --- a/documentation/prd.rst +++ /dev/null @@ -1,27 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | - | See the License for the specific language governing permissions and | - | limitations under the License. | - | | - +--------------------------------------------------------------------------+ - - -******************************************************************************* -Product Requirements Document -******************************************************************************* - -.. todo:: Define product requirements, user stories, and acceptance criteria. - -For PRD format and guidance, see the `requirements documentation guide -<https://emcd.github.io/python-project-common/stable/sphinx-html/common/requirements.html>`_. \ No newline at end of file diff --git a/documentation/specifications/index.rst b/documentation/specifications/index.rst new file mode 100644 index 0000000..c55e440 --- /dev/null +++ b/documentation/specifications/index.rst @@ -0,0 +1,30 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | + | implied. See the License for the specific language governing | + | permissions and limitations under the License. | + | | + +--------------------------------------------------------------------------+ + +******************************************************************************* +Specifications +******************************************************************************* + +This section contains capability specifications managed through the Openspec workflow. +Each specification documents requirements using scenario-based format (WHEN/THEN). + +.. toctree:: + :maxdepth: 2 + :glob: + + ../architecture/openspec/specs/*/spec diff --git a/pyproject.toml b/pyproject.toml index c04fa2c..5a50289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = 'Apache-2.0' readme = { 'file' = 'README.rst', 'content-type' = 'text/x-rst' } requires-python = '>= 3.10' dependencies = [ - 'dynadoc', + 'dynadoc ~= 1.4', 'typing-extensions', # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- @@ -30,6 +30,7 @@ classifiers = [ # https://pypi.org/classifiers 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', # --- END: Injected by Copier --- @@ -93,12 +94,12 @@ strict-naming = false python = '3.10' [tool.hatch.envs.develop] description = ''' Development environment. ''' +builder = true dependencies = [ - 'Jinja2', 'coverage[toml]', 'furo', 'isort', - 'packaging', + 'myst-parser', 'pre-commit', 'pyright', 'pytest', @@ -128,10 +129,10 @@ docsgen = [ ] linters = [ """ruff check --quiet sources documentation tests""", + # """vibelinter check""", # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- """isort --check-only --diff sources tests""", - """vulture""", """pyright sources""", ] packagers = [ @@ -173,7 +174,8 @@ python = [ '3.11', '3.12', '3.13', - 'pypy3.10', + '3.14', + 'pypy3.11', ] # [tool.hatch.metadata] # allow-direct-references = true @@ -348,6 +350,10 @@ directory = 'repair' name = 'Repairs' showcontent = true +[tool.vibelinter] +context = 3 +exclude_paths = [ '.auxiliary/**', '.venv/**', 'tests/**', ] + [tool.vulture] paths = [ '.auxiliary/configuration/vulturefood.py', 'sources' ] min_confidence = 60 diff --git a/sources/classcore/README.md b/sources/classcore/README.md new file mode 100644 index 0000000..8b56870 --- /dev/null +++ b/sources/classcore/README.md @@ -0,0 +1,452 @@ +# classcore + +Classcore is a foundational library for creating classes with customized +behaviors, particularly immutability and concealment. The library operates at +the metaclass level to provide composable, inheritable behaviors while +maintaining compatibility with standard Python features like dataclasses and +protocols. + +## Architectural Goals + +**Composability** + Enable incremental addition of class behaviors through decorators, + metaclasses, and inheritance without conflicts or unexpected interactions. + +**Transparency** + Work seamlessly with existing Python features (dataclasses, protocols, + ABCs) and tools (type checkers, IDEs). + +**Customizability** + Provide extension points at every layer for users to adapt behaviors to + specific needs through verifiers, hooks, and core function replacements. + +**Performance** + Minimize runtime overhead through strategic caching and lazy evaluation + while centralizing import costs at package initialization. + +## High-Level Architecture + +The system follows a layered architecture with clear separation of concerns: + +### Core Factory Layer + +Location: `factories.py` + +Produces metaclass components with customizable hooks: + +- `produce_class_constructor`: Creates `__new__` implementations with + preprocessor and postprocessor hooks +- `produce_class_initializer`: Creates `__init__` implementations with + completer hooks + +These factories enable the construction of metaclasses that can intercept and +augment class creation at multiple lifecycle points. + +### Decorator Layer + +Location: `decorators.py` + +Manages class decoration and replacement: + +- `apply_decorators`: Applies decorator sequences while handling class + replacements +- `decoration_by`: Composes multiple decorators with preparation hooks +- `produce_class_construction_decorator`: Wraps constructor logic into + metaclass `__new__` +- `produce_class_initialization_decorator`: Wraps initializer logic into + metaclass `__init__` + +Critical feature: Repairs class closures when decorators replace classes +(e.g., `dataclass(slots=True)`), ensuring `super()` continues to function +correctly. + +### Behavior Layer + +Location: `standard/behaviors.py` + +Implements immutability and concealment enforcement: + +- Attribute assignment control with selective mutability +- Attribute deletion control +- Attribute visibility filtering for `dir()` +- Verifier system supporting names, regexes, and predicates + +The verifier system allows fine-grained control: specify mutable attributes by +name, pattern, or custom predicate functions. + +### Standard Layer + +Location: `standard/` + +Pre-configured implementations for common use cases: + +**Base Classes:** +- `Object`: Standard class with immutability and concealment +- `DataclassObject`: Automatic dataclass conversion with standard behaviors +- `ProtocolObject`: Protocol class with immutability and concealment +- Variants for selective mutability (`ObjectMutable`, etc.) + +**Metaclasses:** +- `Class`: Metaclass for standard classes +- `Dataclass`: Metaclass that auto-applies dataclass decorator +- `Protocol`: Metaclass for protocol classes +- Mutable variants of each + +**Decorators:** +- `class_with_standard_behaviors`: Add behaviors to existing classes +- `dataclass_with_standard_behaviors`: Dataclass creation with behaviors +- `protocol_with_standard_behaviors`: Protocol creation with behaviors + +**Module Utilities:** +- `reclassify_module`: Apply immutability and concealment to module objects +- `finalize_module`: Complete module initialization with standard behaviors + +### Utility Layer + +Location: `utilities.py` + +Provides essential operations for metaclass machinery: + +- `mangle_name`: Creates unique attribute names using SHA-256 hashing to + avoid inheritance collisions +- `getattr0/setattr0/delattr0`: Access mangled attributes while respecting + `__slots__` +- `repair_class_reproduction`: Fixes closure cells when decorators replace + classes (CPython/PyPy-specific) +- `describe_object`: Generates human-readable object descriptions for error + messages + +## Data Flow + +### Class Creation Flow + +1. User defines class with standard metaclass or inherits from standard base +2. Metaclass `__new__` invoked via `produce_class_constructor` +3. Preprocessors modify bases, namespace, arguments, decorators +4. Superclass `__new__` creates initial class object +5. Progress flag prevents recursive processing +6. Postprocessors execute (e.g., adding behavior configurations) +7. Decorators applied via `apply_decorators` +8. If decorator replaces class, closures repaired +9. Metaclass `__init__` invoked via `produce_class_initializer` +10. Completers execute (e.g., freezing behaviors) +11. Progress flag cleared +12. Fully initialized class returned + +### Attribute Access Flow (Instances) + +**Assignment (`obj.attr = value`):** +1. `__setattr__` intercepted by assigner core function +2. Check for immutability behavior flag +3. If immutable, verify against mutables (names, regexes, predicates) +4. If allowed, delegate to ligation (super or core override) +5. If denied, raise `AttributeImmutability` error + +**Visibility (`dir(obj)`):** +1. `__dir__` intercepted by surveyor core function +2. Collect all attributes from object and class hierarchy +3. Check visibility verifiers (names, regexes, predicates) +4. Filter attributes based on verifiers +5. Return filtered set + +## Key Architectural Patterns + +### Factory Pattern + +Metaclass components are produced by factory functions rather than fixed +inheritance hierarchies. This enables composition of behaviors and runtime +customization of class creation logic. + +### Decorator Composition + +Multiple decorators can be applied in sequence, with automatic handling of +class replacements. The `decoration_by` function enables declarative +composition of decorator stacks. + +### Hook-Based Extension + +Every critical point in the class lifecycle offers hooks: + +- Preprocessors: Modify class construction inputs +- Postprocessors: Augment constructed classes +- Completers: Finalize initialized classes +- Preparers: Adjust decorator lists before application + +### Verifier Pattern + +Behavior exclusions (mutability, visibility) use a uniform verifier interface: + +- Literal `'*'`: Allow all +- Sequence of strings: Exact name matches +- Sequence of `re.Pattern`: Regex matches +- Sequence of callables: Predicate functions + +This provides flexible, user-customizable behavior rules. + +### Core Function Replacement + +Critical behaviors (assignment, deletion, visibility) can be replaced entirely +by providing alternative "core" functions. This enables deep customization +without subclassing. + +## Integration Points + +### Dataclasses + +Standard metaclasses automatically apply `dataclasses.dataclass` during +class construction, with configurability for `frozen`, `slots`, etc. The +decorator application system ensures proper handling of the class replacement +that occurs with `slots=True`. + +### Protocols + +Standard protocol classes combine `typing.Protocol` with immutability and +concealment. Special handling for ABC-related mutable attributes +(`_abc_cache`, `_abc_registry`, etc.) ensures protocol functionality. + +### Type Checkers + +Metaclasses use `@dataclass_transform` decorator to signal dataclass +behavior to type checkers. This enables proper type inference and checking +for classes produced by standard metaclasses. + +### Module Objects + +Module reclassification applies class-level behaviors to module objects, +enabling immutable, concealed module namespaces for API control. + +## Dependencies + +### External Dependencies + +The library depends on standard library modules only: + +- `dataclasses`: Automatic dataclass generation +- `functools`: Decorator utilities and partials +- `hashlib`: Attribute name mangling (SHA-256) +- `inspect`: Class and function introspection +- `platform`: Python implementation detection for platform-specific repairs +- `re`: Regular expression support for verifiers +- `typing`: Type hints and protocol support +- `collections.abc`: Abstract base classes + +### Internal Dependencies + +- `dynadoc`: Documentation generation with fragment interpolation +- `frigid`: Immutable data structures for internal use +- `falsifier`: Base classes for falsey objects + +## Extension Points + +Users can extend the library at multiple levels: + +**Decorator Level** + Create custom class decorators and compose with `decoration_by`. + +**Metaclass Level** + Build custom metaclasses using factory functions with custom hooks. + +**Behavior Level** + Replace core functions for assignment, deletion, visibility, or error + provision. + +**Verifier Level** + Implement custom predicate functions for mutability/visibility rules. + +**Hook Level** + Provide preprocessors, postprocessors, completers, or preparers for + lifecycle customization. + +## Filesystem Organization + +### Root Directory + +``` +python-classcore/ +├── LICENSE.txt # Project license +├── README.rst # Project overview and quick start +├── pyproject.toml # Python packaging and tool configuration +├── documentation/ # Sphinx documentation source +├── sources/ # All source code +├── tests/ # Test suites +└── .auxiliary/ # Development workspace +``` + +### Package Structure + +``` +sources/ +└── classcore/ # Main Python package + ├── __/ # Centralized import hub + │ ├── __init__.py # Re-exports core utilities + │ ├── imports.py # External library imports + │ ├── nomina.py # Naming constants + │ └── doctab.py # Documentation table fragments + ├── __init__.py # Package entry point + ├── py.typed # Type checking marker + ├── decorators.py # Class decorator utilities + ├── exceptions.py # Package exception classes + ├── factories.py # Metaclass factory functions + ├── nomina.py # Public naming types and utilities + ├── utilities.py # Class manipulation utilities + └── standard/ # Standard behaviors subpackage + ├── __.py # Inherits parent imports + ├── __init__.py # Subpackage entry point + ├── behaviors.py # Immutability and concealment logic + ├── classes.py # Standard metaclasses and base classes + ├── decorators.py # Standard class decorators + ├── dynadoc.py # Documentation configuration + ├── modules.py # Module reclassification utilities + └── nomina.py # Standard-specific naming types +``` + +### Module Responsibilities + +#### Core Package (`sources/classcore/`) + +`__/__init__.py` +: Re-exports commonly used imports from `imports.py` for convenient access + via `from . import __` in all package modules. + +`__/imports.py` +: Centralizes all external library imports. Provides abbreviated aliases for + frequently used types and modules (e.g., `cabc` for + `collections.abc`, `typx` for `typing`). + +`__/nomina.py` +: Package-specific naming constants and utilities used internally by the + `__` import hub. + +`__/doctab.py` +: Documentation table with reusable fragments for dynadoc interpolation. + +`__init__.py` +: Package entry point. Exports public API, applies standard behaviors to + package module, sets version. + +`decorators.py` +: Class decorator utilities: + + - `apply_decorators`: Apply decorator sequences with class replacement + handling + - `decoration_by`: Compose multiple decorators + - `produce_class_construction_decorator`: Create metaclass `__new__` + decorators + - `produce_class_initialization_decorator`: Create metaclass `__init__` + decorators + +`exceptions.py` +: Package exception hierarchy. All exceptions use standard behaviors + (immutability and concealment) themselves. + +`factories.py` +: Metaclass component factories: + + - `produce_class_constructor`: Generate customizable `__new__` with + hooks + - `produce_class_initializer`: Generate customizable `__init__` with + hooks + +`nomina.py` +: Public naming types, type aliases, and protocols used throughout the + package. Defines interfaces for hooks, verifiers, and core functions. + +`utilities.py` +: Class manipulation utilities: + + - `mangle_name`: Create unique attribute names + - `getattr0/setattr0/delattr0`: Mangled attribute access + - `repair_class_reproduction`: Fix closures after class replacement + - `describe_object`: Generate error message descriptions + +#### Standard Subpackage (`sources/classcore/standard/`) + +`__.py` +: Inherits all imports from parent `__` via `from ..__ import *`. + Demonstrates the cascading import pattern for subpackages. + +`__init__.py` +: Subpackage entry point. Exports all standard classes, decorators, and + utilities. Applies standard behaviors to subpackage module. + +`behaviors.py` +: Core implementation of standard behaviors: + + - `assign_attribute_if_mutable`: Conditional attribute assignment + - `delete_attribute_if_mutable`: Conditional attribute deletion + - `survey_visible_attributes`: Filter attributes for `dir()` + - Verifier processing for names, regexes, and predicates + +`classes.py` +: Standard metaclasses and base classes: + + - `Class` / `Object`: Standard class with full behaviors + - `Dataclass` / `DataclassObject`: Auto-dataclass with behaviors + - `Protocol` / `ProtocolObject`: Protocol with behaviors + - Mutable variants for each + +`decorators.py` +: Standard class decorators: + + - `class_with_standard_behaviors`: Apply behaviors to existing class + - `dataclass_with_standard_behaviors`: Create dataclass with behaviors + - `protocol_with_standard_behaviors`: Create protocol with behaviors + - `class_factory`: Generic factory for creating custom metaclasses + +`dynadoc.py` +: Dynadoc configuration for standard behaviors. Provides documentation + fragments that describe behaviors automatically applied to classes. + +`modules.py` +: Module reclassification utilities: + + - `reclassify_module`: Apply behaviors to single module + - `finalize_module`: Complete module initialization (used by package) + +`nomina.py` +: Standard-specific naming types and constants. Defines behavior labels + and attribute naming functions. + +All package modules use the standard `__` import pattern as documented +in the common architecture guide. + +### Component Integration + +#### Import Hub Usage + +All modules access common dependencies through the `__` import hub: + +```python +# In any module at sources/classcore/ +from . import __ + +# Use abbreviated imports +def example( items: __.cabc.Sequence[ str ] ) -> __.typx.Any: + return __.funct.partial( some_function, items ) +``` + +This pattern: + +- Eliminates redundant import statements +- Provides consistent naming across modules +- Centralizes import changes to `__/imports.py` +- Reduces module namespace pollution + +#### Subpackage Extension + +The `standard/` subpackage demonstrates the cascading import pattern. +Its `__.py` inherits parent imports and can add specialized dependencies: + +```python +# sources/classcore/standard/__.py +from ..__ import * # Inherit all parent imports +from ..exceptions import * # Add package exceptions +``` + +Modules in `standard/` use the same import pattern but gain access to both +parent and subpackage-specific imports. + +## Sister Projects + +- **[frigid](https://github.com/emcd/python-frigid)** — Fully immutable data structures. Like classcore, but values cannot be added or changed after creation. Useful for configuration objects, constants, and thread-safe data carriers. +- **[accretive](https://github.com/emcd/python-accretive)** — Grow-only data structures. Values can be added (never modified or removed) after creation. Useful for registries, plugin systems, and sticky-state configurations. diff --git a/sources/classcore/__/imports.py b/sources/classcore/__/imports.py index abc9e78..2dff9ac 100644 --- a/sources/classcore/__/imports.py +++ b/sources/classcore/__/imports.py @@ -37,3 +37,6 @@ import typing_extensions as typx # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- + +# --- BEGIN: Injected by Copier --- +# --- END: Injected by Copier --- diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 2714dae..2c45b5b 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.10' +__version__ = '1.11' standard.finalize_module( diff --git a/sources/classcore/nomina.py b/sources/classcore/nomina.py index f19d467..de8280a 100644 --- a/sources/classcore/nomina.py +++ b/sources/classcore/nomina.py @@ -64,6 +64,40 @@ ''' ), ] +ClassDecorator: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Callable[ [ type[ __.U ] ], type[ __.U ] ], + __.ddoc.Doc( + ''' Class decorator. + + Takes class argument and returns class. + ''' ), +] +ClassDecorators: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Sequence[ ClassDecorator[ __.U ] ], + __.ddoc.Doc( + ''' Sequence of class decorators. + + Each element takes a class argument and returns a class. + ''' ), +] +ClassDecoratorFactory: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Callable[ ..., ClassDecorator[ __.U ] ], + __.ddoc.Doc( + ''' Factory that produces class decorators. + + Returns a class decorator when called. + ''' ), +] +ModuleReclassifier: __.typx.TypeAlias = __.typx.Annotated[ + __.cabc.Callable[ [ __.cabc.Mapping[ str, __.typx.Any ] ], None ], + __.ddoc.Doc( + ''' Reclassifies module attributes. + + Takes a mapping of attribute names to values and applies + reclassification (e.g., immutability and concealment). + ''' ), +] + DecorationPreparer: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ type[ __.U ], DecoratorsMutable[ __.U ] ], None ], __.ddoc.Doc( diff --git a/sources/classcore/standard/README.md b/sources/classcore/standard/README.md new file mode 100644 index 0000000..8c27678 --- /dev/null +++ b/sources/classcore/standard/README.md @@ -0,0 +1,202 @@ +# classcore.standard + +The `standard` subpackage provides pre-configured implementations of +classcore's behaviors for common use cases. It includes base classes, +metaclasses, decorators, and module utilities that combine immutability and +concealment with standard Python features like dataclasses and protocols. + +## Base Classes + +### `Object` + +Standard class with full immutability and concealment behaviors. Instances +have immutable attributes and concealed internal attributes. + +```python +from classcore.standard import Object + +class Point(Object): + x: float + y: float +``` + +### `DataclassObject` + +Automatic dataclass conversion with standard behaviors. Unlike +`dataclass(frozen=True)`, attributes can still be manipulated via +`__post_init__`. + +```python +from dataclasses import field +from math import sqrt +from classcore.standard import DataclassObject + +class Point2d(DataclassObject): + x: float + y: float + hypotenuse: float = field(init=False) + def __post_init__(self) -> None: + x, y = self.x, self.y + self.hypotenuse = sqrt(x*x + y*y) +``` + +### `ProtocolObject` + +Protocol class with immutability and concealment. Combines `typing.Protocol` +with standard behaviors. Immutability and concealment are inherited by +subclassed implementations. + +### Mutable Variants + +- `ObjectMutable`: Object with concealment only (no immutability) +- `DataclassObjectMutable`: DataclassObject with concealment only +- `ProtocolObjectMutable`: ProtocolObject with concealment only + +## Metaclasses + +### `Class` + +Metaclass for standard classes. Provides immutability and concealment to +any class that uses this metaclass. + +### `Dataclass` + +Metaclass that automatically applies `dataclasses.dataclass` during class +construction. Configurable for `frozen`, `slots`, etc. + +### `Protocol` + +Metaclass for protocol classes. Combines `typing.Protocol` with standard +behaviors. + +### Mutable Variants + +- `ClassMutable`: Metaclass with concealment only +- `DataclassMutable`: Dataclass metaclass with concealment only +- `ProtocolMutable`: Protocol metaclass with concealment only + +## Decorators + +### `class_with_standard_behaviors` + +Apply standard behaviors to an existing class. + +```python +from classcore.standard import class_with_standard_behaviors + +@class_with_standard_behaviors() +class ExistingClass: + x: int = 10 +``` + +### `dataclass_with_standard_behaviors` + +Create a dataclass with standard behaviors applied. + +```python +from classcore.standard import dataclass_with_standard_behaviors + +@dataclass_with_standard_behaviors() +class Point: + x: float + y: float +``` + +### `protocol_with_standard_behaviors` + +Create a protocol with standard behaviors applied. + +### `class_factory` + +Generic factory for creating custom metaclasses with standard behaviors. + +## Module Utilities + +### `reclassify_module` + +Apply immutability and concealment to a single module object. + +```python +from classcore.standard import reclassify_module + +reclassify_module(__name__) +``` + +### `finalize_module` + +Complete module initialization with standard behaviors. Used internally by +the package to apply behaviors to all subpackage modules. + +### `reclassify_modules` (deprecated) + +Apply behaviors to entire packages recursively. Deprecated in favor of +`finalize_module` for individual module control. + +## Behavior System + +### Immutability + +Attributes are immutable by default. Assignment attempts raise +`AttributeImmutability`. Deletion attempts raise +`AttributeDeletionImmutability`. + +### Concealment + +Internal attributes are concealed from `dir()` output. Only public +attributes and explicitly visible attributes are shown. + +### Selective Mutability + +Specify mutable attributes using verifiers: + +- **Names**: Exact string matches +- **Regexes**: Regular expression patterns +- **Predicates**: Custom callable functions + +```python +from classcore.standard import ObjectMutableByNames + +class Config(ObjectMutableByNames, mutable_names=('debug', 'verbose')): + debug: bool = False + verbose: bool = False + name: str = 'default' +``` + +### Verifier Interface + +Behavior exclusions use a uniform verifier interface: + +- Literal `'*'`: Allow all +- Sequence of strings: Exact name matches +- Sequence of `re.Pattern`: Regex matches +- Sequence of callables: Predicate functions + +## Integration with Dataclasses + +Standard metaclasses automatically apply `dataclasses.dataclass` during +class construction. The decorator application system ensures proper handling +of the class replacement that occurs with `slots=True`. + +Key features: +- `__post_init__` can still modify attributes (unlike `frozen=True`) +- Slot classes are properly supported +- Type checkers recognize the dataclass transform + +## Integration with Protocols + +Standard protocol classes combine `typing.Protocol` with immutability and +concealment. Special handling for ABC-related mutable attributes +(`_abc_cache`, `_abc_registry`, etc.) ensures protocol functionality. + +## Import Pattern + +The standard subpackage uses the cascading import pattern: + +```python +# sources/classcore/standard/__.py +from ..__ import * # Inherit all parent imports +from ..exceptions import * # Add package exceptions +``` + +Modules in `standard/` use the same `__` import pattern as the parent +package, gaining access to both parent and subpackage-specific imports. diff --git a/sources/classcore/standard/classes.py b/sources/classcore/standard/classes.py index b68285e..9bbeaf9 100644 --- a/sources/classcore/standard/classes.py +++ b/sources/classcore/standard/classes.py @@ -27,6 +27,12 @@ from . import nomina as _nomina +_abc_class_mutables = ( + '_abc_cache', + '_abc_negative_cache', + '_abc_negative_cache_version', + '_abc_registry', +) _dynadoc_configuration = ( _dynadoc.produce_dynadoc_configuration( table = __.fragments ) ) _class_factory = __.funct.partial( @@ -208,12 +214,7 @@ class DataclassObjectMutable( metaclass = DataclassMutable ): class Protocol( __.typx.Protocol, metaclass = ProtocolClass, - class_mutables = ( - '_abc_cache', - '_abc_negative_cache', - '_abc_negative_cache_version', - '_abc_registry', - ), + class_mutables = _abc_class_mutables, ): ''' Standard base protocol class. ''' @@ -226,12 +227,7 @@ class Protocol( class ProtocolMutable( __.typx.Protocol, metaclass = ProtocolClass, - class_mutables = ( - '_abc_cache', - '_abc_negative_cache', - '_abc_negative_cache_version', - '_abc_registry', - ), + class_mutables = _abc_class_mutables, instances_mutables = '*', ): ''' Base protocol class with mutable instance attributes. ''' @@ -245,12 +241,7 @@ class ProtocolMutable( class DataclassProtocol( __.typx.Protocol, metaclass = ProtocolDataclass, - class_mutables = ( - '_abc_cache', - '_abc_negative_cache', - '_abc_negative_cache_version', - '_abc_registry', - ), + class_mutables = _abc_class_mutables, ): ''' Standard base protocol dataclass. ''' @@ -263,12 +254,7 @@ class DataclassProtocol( class DataclassProtocolMutable( __.typx.Protocol, metaclass = ProtocolDataclassMutable, - class_mutables = ( - '_abc_cache', - '_abc_negative_cache', - '_abc_negative_cache_version', - '_abc_registry', - ), + class_mutables = _abc_class_mutables, ): ''' Base protocol dataclass with mutable instance attributes. ''' diff --git a/sources/classcore/standard/nomina.py b/sources/classcore/standard/nomina.py index 1681937..de17427 100644 --- a/sources/classcore/standard/nomina.py +++ b/sources/classcore/standard/nomina.py @@ -109,7 +109,6 @@ def __call__( # pragma: no branch DynadocConfiguration: __.typx.TypeAlias = __.cabc.Mapping[ str, __.typx.Any ] -# TODO: Use argument type aliases from 'dynadoc' package. DynadocContextArgument: __.typx.TypeAlias = __.typx.Annotated[ __.ddoc.Context, __.ddoc.Doc( From 6dd173d7c9adc29ab0cee38d494e93ed8111e2e1 Mon Sep 17 00:00:00 2001 From: Github Actions Robot <emcd@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:57:21 +0000 Subject: [PATCH 25/28] Update documents for publication. (1.12) --- .auxiliary/agents/agents.md | 138 ++++++ .auxiliary/agents/procedures/notebook.md | 72 +++ .auxiliary/agents/procedures/openspec.md | 35 ++ .auxiliary/agents/procedures/reviews.md | 97 ++++ .auxiliary/agents/project.md | 44 ++ .auxiliary/configuration/AGENTS.md | 144 ------ .../configuration/coders/claude/.gitignore | 4 +- .../coders/claude/scripts/post-edit-linter | 103 ---- .../claude/scripts/pre-bash-git-commit-check | 123 ----- .../claude/scripts/pre-bash-python-check | 123 ----- .../configuration/coders/claude/settings.json | 59 ++- .../configuration/coders/codex/.gitignore | 1 - .../coders/codex/rules/emcd.rules | 19 +- .../configuration/coders/gemini/.gitignore | 2 - .../gemini/miscellany/command-template.md | 43 -- .../coders/gemini/policies/emcd.toml | 83 ---- .../configuration/coders/gemini/settings.json | 54 --- .../configuration/coders/opencode/.gitignore | 5 +- .../coders/opencode/plugin/.gitignore | 13 - .../coders/opencode/plugin/README.md | 109 ----- .../plugin/git-commit-guard.js-disabled | 195 -------- .../coders/opencode/plugin/package.json | 13 - .../opencode/plugin/post-edit-linter.js | 130 ----- .../python-environment-guard.js-disabled | 149 ------ .../coders/opencode/settings.jsonc | 35 +- .../configuration/copier-answers--agents.yaml | 36 +- .auxiliary/configuration/copier-answers.yaml | 2 +- .auxiliary/configuration/pre-commit.yaml | 2 +- .../data/towncrier/+abc-support.enhance.rst | 6 + .../towncrier/+metaclass-hierarchy.repair.rst | 6 + .../towncrier/+protocol-detection.repair.rst | 5 + .../+runtime-checkable-protocols.repair.rst | 5 + .../towncrier/+signature-docs.enhance.rst | 3 + .../data/towncrier/+type-aliases.enhance.rst | 1 - .auxiliary/publications/website.tar.xz | 4 +- .github/workflows/releaser.yaml | 7 +- documentation/architecture/openspec/AGENTS.md | 455 ------------------ .../2026-08-15-add-abc-support/design.md | 134 ++++++ .../2026-08-15-add-abc-support/proposal.md | 45 ++ .../specs/abc-support/spec.md | 78 +++ .../2026-08-15-add-abc-support/tasks.md | 48 ++ .../design.md | 221 +++++++++ .../proposal.md | 73 +++ .../specs/protocol-support/spec.md | 65 +++ .../tasks.md | 94 ++++ .../architecture/openspec/config.yaml | 19 + .../openspec/specs/abc-support/spec.md | 87 ++++ .../openspec/specs/protocol-support/spec.md | 80 ++- documentation/changelog.rst | 38 ++ documentation/conf.py | 4 +- documentation/contribution.rst | 22 +- documentation/specifications/index.rst | 30 -- pyproject.toml | 1 + sources/classcore/README.md | 19 +- sources/classcore/__/imports.py | 1 + sources/classcore/__init__.py | 2 +- sources/classcore/decorators.py | 28 +- sources/classcore/factories.py | 2 +- sources/classcore/nomina.py | 49 +- sources/classcore/standard/README.md | 48 +- sources/classcore/standard/behaviors.py | 2 +- sources/classcore/standard/classes.py | 184 ++++++- sources/classcore/standard/decorators.py | 10 +- .../test_330_standard_decorators.py | 8 +- .../test_345_standard_protocols.py | 305 ++++++++++++ .../test_347_abstract_classes.py | 190 ++++++++ 66 files changed, 2293 insertions(+), 1919 deletions(-) create mode 100644 .auxiliary/agents/agents.md create mode 100644 .auxiliary/agents/procedures/notebook.md create mode 100644 .auxiliary/agents/procedures/openspec.md create mode 100644 .auxiliary/agents/procedures/reviews.md create mode 100644 .auxiliary/agents/project.md delete mode 100644 .auxiliary/configuration/AGENTS.md delete mode 100755 .auxiliary/configuration/coders/claude/scripts/post-edit-linter delete mode 100755 .auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check delete mode 100755 .auxiliary/configuration/coders/claude/scripts/pre-bash-python-check delete mode 100644 .auxiliary/configuration/coders/codex/.gitignore delete mode 100644 .auxiliary/configuration/coders/gemini/.gitignore delete mode 100644 .auxiliary/configuration/coders/gemini/miscellany/command-template.md delete mode 100644 .auxiliary/configuration/coders/gemini/policies/emcd.toml delete mode 100644 .auxiliary/configuration/coders/gemini/settings.json delete mode 100644 .auxiliary/configuration/coders/opencode/plugin/.gitignore delete mode 100644 .auxiliary/configuration/coders/opencode/plugin/README.md delete mode 100644 .auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled delete mode 100644 .auxiliary/configuration/coders/opencode/plugin/package.json delete mode 100644 .auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js delete mode 100644 .auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled create mode 100644 .auxiliary/data/towncrier/+abc-support.enhance.rst create mode 100644 .auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst create mode 100644 .auxiliary/data/towncrier/+protocol-detection.repair.rst create mode 100644 .auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst create mode 100644 .auxiliary/data/towncrier/+signature-docs.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+type-aliases.enhance.rst delete mode 100644 documentation/architecture/openspec/AGENTS.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/design.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/proposal.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/specs/abc-support/spec.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/tasks.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/design.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/proposal.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/specs/protocol-support/spec.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/tasks.md create mode 100644 documentation/architecture/openspec/config.yaml create mode 100644 documentation/architecture/openspec/specs/abc-support/spec.md delete mode 100644 documentation/specifications/index.rst create mode 100644 tests/test_000_classcore/test_345_standard_protocols.py create mode 100644 tests/test_000_classcore/test_347_abstract_classes.py diff --git a/.auxiliary/agents/agents.md b/.auxiliary/agents/agents.md new file mode 100644 index 0000000..6cb4418 --- /dev/null +++ b/.auxiliary/agents/agents.md @@ -0,0 +1,138 @@ +# Context + +- Overview and Quick Start: README.{md,rst} + +- Use the 'context7' MCP server to retrieve up-to-date documentation for any SDKs or APIs. +- Use the 'nb' MCP server for project note-taking, issue tracking, and collaboration. The server provides LLM-friendly access to the `nb` note-taking system with proper escaping and project-specific notebook context. +- Check README files in directories you're working with for insights about architecture and design decisions. +- Project purpose, tech stack, and other project-owned guidance: @.auxiliary/agents/project.md + +# Development Standards +Before implementing code changes, consult these files in `.auxiliary/agents/standards/`: +- `practices.rst` - General development principles (robustness, immutability, exception chaining) +- `practices-python.rst` - Python-specific patterns (module organization, type annotations, wide parameter/narrow return) +- `nomenclature.rst` - Naming conventions for variables, functions, classes, exceptions +- `style.rst` - Code formatting standards (spacing, line length, documentation mood) +- `validation.rst` - Quality assurance requirements (linters, type checkers, tests) + +# Operation + +- Use a README-first discovery workflow to reduce token churn: + - Start at the repository root `README.{md,rst}`, then read the nearest relevant subtree README. + - After reading the nearest README, scope code searches to that subtree before considering repo-wide searches. + - If a touched subsystem README is stale after your change, update it in the same batch. +- Use relative paths rather than absolute paths when possible (relative paths are less likely to trigger tool call permission requests). +- Do not write to paths outside the current project unless explicitly requested. +- Do **not** run `find`, `fd`, or other unbounded recursive walks on `/`, `~`, or other home-wide roots. Scope searches to the current project/worktree or a path the operator named. Prefer harness file-search tools when available. +- Notebook and project-note operations go through the **`nb` MCP** tools. Do not use the `nb` CLI or read/write underlying notebook files on disk unless MCP is broken — and then state why in one sentence and discuss the defect with the operator before any workaround. Selectors are not repo filesystem paths; see @.auxiliary/agents/procedures/notebook.md. +- Use `.auxiliary/scribbles` for scratch work and one-off experiments instead of `/tmp`. +- Use `.auxiliary/temporary` for ephemeral test state and build artifacts that are safe to delete. +- In sandboxed environments (e.g., Codex CLI), treat file/network permission failures as escalation boundaries: + - If an operation fails due to sandbox, file access, or network restrictions, rerun it with user escalation. + - Do not spend time on retry loops or workaround exploration before escalating blocked operations. +- When writing here-docs or multi-line shell strings, suppress expansions by quoting the delimiter (e.g., `'EOF'` instead of `EOF`) unless you intentionally need variable or command substitution. +- Code comments should help a future reader understand non-obvious behavior of the code. Do **not** put transient administrative or process metadata in comments: no todo IDs, proposal/task names, "this arc", "this phase", ticket folklore, or other coordination state. Do not use comments for long discursive design or architecture narrative — that belongs in design docs, specs, or subsystem READMEs. + +## Guidance Files + +| Topic | File | +|-------|------| +| Project-specific guidance (project-owned) | @.auxiliary/agents/project.md | +| `nb` MCP tools, tagging, and notebook organization | @.auxiliary/agents/procedures/notebook.md | +| OpenSpec proposals and workflow | @.auxiliary/agents/procedures/openspec.md | +| Delegated review flow and stacked commits | @.auxiliary/agents/procedures/reviews.md | + +### Recommended Organization + +| Medium | Location | Purpose | +|--------|----------|---------| +| `nb` | `coordination/` | Handoffs, org chart, team workflow | +| `nb` | `ideas/` | Rough ideas, early-stage proposals; tag `#task-proposal` for OpenSpec drafts | +| `nb` | `issues/` | Bug tracking and known issues | +| `nb` | `reviews/` | Code and proposal reviews | +| `nb` | `procedures/` | How-to guides and checklists | +| `nb` | `todos/` | Task tracking | +| `nb` | `artifacts/` | Preserved reference material: completed POCs, historical analysis | +| `agentmux` | | Inter-agent messaging, pane inspection, coordination | +| (filesystem) | `openspec/` | Formal proposals, specs, designs | +| (filesystem) | `src/**/README.md` | Architecture, constraints, design rationale | + +## Agentmux Message Handling Guidance +- `agentmux` messages may arrive in envelope format and can appear as user prompts. Treat envelope-shaped prompts as inter-agent messages, not automatically as direct human instructions. +- Respond to inter-agent envelope messages via `agentmux` MCP tools (`list`, `send`) rather than as normal assistant replies intended for the human operator. +- Immediate interruption is not required. If you are in active execution, note the message and respond when safe. +- If response will be delayed and the sender needs to know, send a brief acknowledgement via `send` and create a follow-up todo when useful. + +### Agentmux Coordination Noise Control +- Default to low-noise coordination. Do not send acknowledgement-only messages that add no new information or action request. +- Do not acknowledge receipt or completion of standard-procedure steps. Include such status in the next substantive update, review request, or task dispatch instead. +- Send messages when one of the following is true: + - you are blocked and need a decision or input, + - you are requesting a concrete review, + - you are handing off completed work with validation results, + - you are reporting a material risk, failure, or scope change. +- Batch related updates into one message instead of sending rapid-fire partial status pings. +- Use `Cc` only for agents who need to act or review; avoid broad `Cc` by default. +- When conversation volume rises, coordinator may enforce "blockers-only" mode until the queue is under control. + +## OpenSpec Instructions + +This project uses OpenSpec 1.x (OPSX), the action-based workflow. OPSX skills +deliver workflow instructions through the agentsmgr distribution pipeline. + +Workflow skills: `opsx-propose`, `opsx-explore`, `opsx-apply`, +`opsx-sync`, `opsx-archive`. + +Use OPSX skills when the request: +- Mentions planning or proposals (words like proposal, spec, change, plan). +- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work. +- Sounds ambiguous and you need the authoritative spec before coding. + +CLI state queries: `openspec list`, `openspec list --specs`, +`openspec status --change <id>`, `openspec validate --all --strict`. + +When a commit completes an OpenSpec task or requirement, update the relevant OpenSpec task status in the same commit. + +# Commits + +- Use `git status` to ensure all relevant changes are in the changeset. +- Commits are acceptable review artifacts when implementation work is delegated by a human operator, coordinator, tech lead, or documented project workflow. Otherwise, ask before committing. +- Do **not** merge, push, publish review branches, or modify shared branches without explicit human approval. +- Do **not** bypass commit safety checks (e.g., `--no-verify`, `--no-gpg-sign`) unless the user explicitly approves doing so. +- If a commit hook rejects a commit, assume no commit was created unless Git clearly reports otherwise. Fix the hook finding, restage the intended files, and rerun the same `git commit` command. +- Use present tense, imperative mood verbs (e.g., "Fix" not "Fixed"). +- Write sentences with proper punctuation. +- Include a `Co-Authored-By:` field as the final line. Should include the model name and a no-reply address. +- Avoid using `backticks` in commit messages as shell tools may evaluate them as subshell captures. When writing commit messages via here-docs, quote the delimiter (`'EOF'` not `EOF`) to suppress expansions; only omit the quotes if you intentionally need interpolation. + +## Delegated Review and Stacked Commits + +**Read this section before reviewing or stacking commits.** @.auxiliary/agents/procedures/reviews.md covers the delegated review flow, review request packet format, and how to handle stacked commits with `--fixup`/`--autosquash`. + +## Rolling Handoffs + +**Read this before writing or updating a handoff note.** @.auxiliary/agents/procedures/notebook.md Handoff Hygiene covers when to update (pre-compaction primary), content shape, and anti-patterns. Do not refresh handoffs routinely mid-flight. + +# Collaboration + +## Questions Are Not Stop-Orders + +- When the operator asks why you are doing something, **answer first**: state your actual reason in one or two sentences, even if it is unflattering, before changing course. The question is a request for information, not a rebuke. Pivoting without answering destroys the information the operator asked for. +- Questions in review feedback are requests for discussion, not requests for changes. Answer them in the review thread; do not silently apply a change as your answer. +- A question arriving through a tool rejection or interruption still deserves an answer before (or alongside) the course correction. + +## The Human Is Not Always Right + +- Assert your reasons. When you believe a requested course is wrong or a better one exists, argue the alternative with evidence before complying. Reasoned disagreement is a contribution; silent compliance wastes the operator's collaboration channel. +- Concede on evidence, not on authority. Gates (merge, deploy, publish) belong to the operator; discussion upstream of gates is peer discussion. +- However, do not manufacture dissent. When you agree, say so briefly and proceed. + +## Workarounds Require Stated Reasons + +- Routing around a designated tool (for example reading notebook files from the filesystem instead of using the nb MCP tools) is evidence of a defect — in the tool, its documentation, or the workflow. Never work around silently: state the reason in one sentence and raise it with the operator. +- If a tool's output looks wrong, untrustworthy, or unusable, say so to the operator before falling back. + +# Project Guidance + +Read and follow @.auxiliary/agents/project.md for project-specific guidance +(purpose, tech stack, knowledge, constraints, deviations, durable links). diff --git a/.auxiliary/agents/procedures/notebook.md b/.auxiliary/agents/procedures/notebook.md new file mode 100644 index 0000000..2f8fc26 --- /dev/null +++ b/.auxiliary/agents/procedures/notebook.md @@ -0,0 +1,72 @@ +# `nb` MCP Tools + +The `nb` MCP server provides LLM-friendly access to the `nb` note-taking system with proper escaping and project-specific notebook context. + +## When to Use +- **Project coordination**: Record handoffs, document project policies and procedures, maintain task lists. +- **Issue tracking**: Create and manage todos with status tracking. +- **Knowledge sharing**: Document patterns, APIs, and project-specific knowledge. +- **Meeting notes**: Record discussions and action items. + +## Scope and Noise Control +- Prefer to update an existing related note/todo over creating a new one when context already exists. +- Avoid logging routine, immediately completed mechanical actions in separate notes. +- Create new notes/todos when information is likely to be useful across sessions or for other collaborators. + +## Tagging Conventions +Use consistent tags for discoverability: +- **Project Component**: `#component-<name>` (e.g., `#component-data-models`) +- **Task Type**: `#task-<type>` (e.g., `#task-design`, `#task-bug`) +- **Status**: `#status-<state>` (e.g., `#status-in-progress`, `#status-review`) +- **Coordination**: `#handoff`, `#coordination` +- **Assignment**: Avoid owner tags (for example `#llm-*`) for task ownership. Use lane/folder ownership and explicit owner text in the note body when needed. + +## Common Patterns +- Check for handoffs: use `search` with `#handoff` and `#status-review` tags. +- Find active component work: use `search` with `#component-<name>` and `#status-in-progress` tags. +- Track todos: use the `todo`, `tasks`, `do`, and `undo` tools. +- Organize with folders: use `folders` and `mkdir`. + +## Choosing `todo` vs `add` +- Use the **`todo`** tool for any item with actionable state (open/done): todos, bugs, and issues. This gives the note a checkbox, enables `do`/`undo` state tracking, and makes it appear in `tasks` output. +- Use the **`add`** tool for everything else: coordination notes, decisions, designs, ideas, reference material, handoffs, and meeting notes. +- **Always specify a `folder`** when creating a note. A note created without a folder lands at the notebook root and is invisible to folder-scoped list views. +- Do not duplicate: if a bug or task is already tracked in `issues/<component>/` or `todos/<component>/`, do not also create a matching todo. Reference the existing selector in coordination notes or related todo bodies instead. + +## Notebook Identifier Clarification +- Treat note selectors (for example `coordination/mcp/1`) as canonical IDs for operations on existing notes (`show`, `edit`, `delete`, etc.); do not supply a selector when creating new notes. +- Notebook MCP responses may include notebook-scoped identifiers (for example `my-project:coordination/...`) that look path-like; these are selector forms, not repo-relative filesystem paths. +- Notebook storage is controlled by `nb` configuration (for example `NB_DIR`) and may be outside this repository. +- Prefer notebook MCP commands to read/edit notes. Avoid assuming a selector maps to a file under the current repo. +- Use `help` to read full command schemas; key lookups: `search` with tag queries, `tasks` for open todos, `folders` to browse structure. + +## Recommended `nb` Organization (Project-Defined) +- Prefer a folder taxonomy of `<issue-type>/<component>` (max depth 2) and avoid mixing top-level component folders with top-level issue-type folders. + +- Design rationale belongs in subsystem README files and OpenSpec specs. Avoid a separate `designs/` notebook category because it creates an extra place to look that can go stale. +- Use `decisions/` only when the project wants optional durable rationale notes outside OpenSpec or architecture README files. +- When an idea promotes to a formal OpenSpec proposal, delete or archive the notebook draft so the OpenSpec file is the canonical record. +- Example component names include `engine`, `mcp`, `tui`, `web`, `handbook`, and `data-models`. +- This project should define and document its specific component-folder conventions in @.auxiliary/agents/project.md. +- For cross-component work, prefer `coordination/general` and use multiple `#component-*` tags. +- For per-component rolling handoffs, prefer `coordination/<component>` (one stable note updated at checkpoints). +- Keep notebook lifecycle hygiene: + - prune completed todos quickly, + - keep only active/near-term coordination checkpoints, + - delete stale history-only notes with no owner or action. +- Keep todo titles concise (under 60 chars); use the `tasks` argument for detailed checklist items. This keeps notebook list views readable. + +## `nb` vs OpenSpec Rubric +- Use **OpenSpec proposals** for cross-cutting changes, contract-shaping work, architecture shifts, or work that needs explicit design discussion. +- Use **`nb` todos/notes** for scoped, self-contained implementation tasks where the path is straightforward. +- When in doubt about whether work needs an OpenSpec proposal or only `nb` execution tracking, prefer OpenSpec first for design clarity. +- For each active OpenSpec proposal, keep **exactly one** linked `nb` todo as the tracking anchor (with proposal reference), rather than duplicating full task trees in both systems. + +## Handoff Hygiene +- Use `coordination/<component>` for per-component rolling handoffs (one stable note per component, updated in place). Use `coordination/general` for coordinator-wide state and cross-component snapshots. +- Keep rolling handoff notes separate from OpenSpec proposal content. Do not repurpose or overwrite rolling handoff notes with proposal content. +- Handoff content should be a brief summary of recent accomplishments and the current agenda. Replace the note body rather than appending so the handoff stays one screenful; a growing checkpoint log is an anti-pattern. +- **Updates are conditional, not routine.** Do not refresh a handoff after every task, review round, or status ping. +- **Primary update point: before compaction (or a genuine session end).** When the conversation is about to compact or the session is ending such that the next pickup may be a cold start, rewrite the handoff so the next agent has operating state without relying on chat history. That is the default reason to touch the note. +- **Secondary update point:** when another lane or the coordinator needs visibility into a state change that is not otherwise discoverable from git log, `nb` todo/issue status, or OpenSpec `tasks.md`. +- In-session continuity after compaction is not a substitute for a handoff when the next actor may be a different session or seat; still prefer one pre-compaction rewrite over many mid-flight tweaks. diff --git a/.auxiliary/agents/procedures/openspec.md b/.auxiliary/agents/procedures/openspec.md new file mode 100644 index 0000000..023c3d2 --- /dev/null +++ b/.auxiliary/agents/procedures/openspec.md @@ -0,0 +1,35 @@ +# OpenSpec Instructions + +This project uses OpenSpec 1.x (OPSX), the action-based workflow. OPSX skills +deliver workflow instructions through the agentsmgr distribution pipeline. + +## Workflow + +- `opsx-propose` -- create a complete change proposal in one step. +- `opsx-explore` -- think through ideas before committing to a change. +- `opsx-apply` -- implement tasks from an approved change. +- `opsx-sync` -- sync delta specs into main specs. +- `opsx-archive` -- archive a completed change. + +## CLI State Queries + +- `openspec list` and `openspec list --specs` -- active changes and specs. +- `openspec status --change <id>` -- artifact state for a change. +- `openspec instructions <artifact> --change <id>` -- authoring instructions. +- `openspec validate --all --strict` -- validate changes and specs. + +## Conventions + +- Project configuration: `openspec/config.yaml` (default schema: + `spec-driven`). Custom workflow schemas may be added under + `openspec/schemas/` as the project evolves. +- Change IDs: kebab-case, verb-led (`add-`, `update-`, `remove-`, + `refactor-`). +- Every requirement uses SHALL/MUST and carries at least one + `#### Scenario:` (exactly four hashtags). +- When a commit completes an OpenSpec task or requirement, update the + relevant task status in the same commit. + +Treat OpenSpec proposals like code: commit proposal files to a branch, +reviewers review the commit (`git show`), author amends as needed, merge when +settled. No notebook draft step. diff --git a/.auxiliary/agents/procedures/reviews.md b/.auxiliary/agents/procedures/reviews.md new file mode 100644 index 0000000..9db8171 --- /dev/null +++ b/.auxiliary/agents/procedures/reviews.md @@ -0,0 +1,97 @@ +# Delegated Review Flow + +Use this flow when multiple team members can access the same repository through branches or linked worktrees. + +## Roles + +- **Author:** implements the change, produces review packets, addresses findings with fixups, and prepares the cleaned stack for merge. +- **Reviewer:** performs the technical review (design, correctness, validation evidence, commit structure). One reviewer may cover the whole stack, or review may be split by commit/area; either way this is the technical gate. +- **Integrator** (coordinator or tech lead): accepts the **reviewer-approved, cleaned** stack for merge. Checks that the branch is based on the current `<local-integration-base>`, that the merge can proceed without conflict, and that human merge/push policy is respected. This is **not** a second technical review of the change content. + +The same person may wear more than one role only when the project explicitly allows it. Do not treat "send to coordinator for merge" as another round of code review. + +## Review cycle + +1. Author implements the scoped change, runs validation, and creates local/private review commit(s) so the diff is hash-stable and hook-checked. +2. Author rebases onto the agreed `<local-integration-base>` and sends a review packet to the **reviewer**. +3. Reviewer approves or requests changes. +4. If changes are requested: author addresses them with fixup commits or first-class follow-up commits (see below), then sends an **updated** review packet. Return to step 3. +5. When the reviewer **approves**: author autosquashes targeted fixups into their targets. +6. **Base check after approval (required distinction):** + - If `<local-integration-base>` is **unchanged** since the approved packet and only autosquash rewrote hashes: hand the cleaned stack to the **integrator** for merge (merge handoff with updated commit list; no repeat technical review). + - If the base **advanced** and the author must rebase onto the new base — especially if conflicts are resolved — the result is a **changed technical artifact**. Send an **updated technical review packet** and return to step 3. Do not send a rebased stack straight to the integrator. +7. Integrator merges (see Integrator flow). Merge/push only after explicit human approval. + +Hold the unsquashed fixup stack for the entire technical review. Autosquash only after reviewer approval, as the step that produces the stack the integrator merges when the base has not moved. + +The agreed `<local-integration-base>` is a Git ref in the current repository, such as local `master` or a local lane integration branch. It is not a filesystem path and is not a remote-tracking ref. Do not use `origin/master`, another `origin/*` ref, a path like `/path/to/repo/master`, or a raw commit hash as the rebase base unless the coordinator explicitly names that exact ref or hash. When in doubt, ask for the local branch/ref name before rebasing. Confirm it with `git branch --list <local-integration-base>` or `git rev-parse --verify <local-integration-base>` before running `git rebase`. + +## Integrator flow + +1. Confirm the stack is reviewer-approved and already cleaned (fixups autosquashed). If fixups are still present, send it back to the author to fold before merge — do not start a content review. +2. Confirm the cleaned stack is based on the **current** `<local-integration-base>`. If the base has advanced (stale-base stack), **refuse the merge handoff** and route the author to rebase and return through **technical review** — do not independently assess the rebased content, and do not merge a stale-base cleaned stack. +3. If the base is current and the merge is otherwise clear, merge approved review branches with `--no-ff` when preserving a delegated-work or lane boundary; this creates a clear integration point and avoids mutually rebasing branches into increasingly long histories. +4. Merge/push only after explicit human approval. + +Prefer reviewing commits by hash. Use an explicit worktree path only for uncommitted diffs or commits in a different repository. Use patch artifacts only as a fallback when the reviewer cannot access the repository, branch, or worktree directly. + +# Review Request Packet + +For non-trivial delegated work, review requests should include: + +- Base ref for rebase: the `<local-integration-base>` to use for `git rebase` or `git rebase -i --autosquash`. +- Intended merge target: the branch/ref where the work should eventually land. This may differ from the rebase base, and may be a shared branch. +- Complete commit list with hashes and one-line descriptions. +- Validation commands run and results, including skipped checks or known gaps. +- Intended contract: what must be true after the change lands. +- Review concerns, if any: genuine uncertainty or risky areas only. +- Known risks, accepted tradeoffs, deferred items, or intentional branch staleness. + +Author-provided review concerns are supplemental context, not a limit on review scope. Independent inspection remains the reviewer responsibility. + +Packets to the **integrator** after approval are merge handoffs: cleaned commit list, validation status, and base/merge refs. They apply only when the base is unchanged since reviewer approval (autosquash-only hash changes are fine). They are not a second technical review packet. A rebase onto an advanced base requires a new technical review packet to the **reviewer**, not a merge handoff. + +# Reviewing Stacked Commits + +When feedback targets one specific commit in the current review stack, use `git commit --fixup <target-hash>`. This applies even when the review stack has only one commit. Do not directly amend reviewed commits while review is in progress; fixup commits preserve review visibility until the stack is ready for final cleanup. + +A fixup is valid only when the entire fix belongs to code introduced by one target commit in the current stack. Use a first-class follow-up commit when the fix touches code that is already merged to the base branch, when the fix spans or refactors code introduced by two or more in-stack commits, or when the operator requests a distinct design change. Name the review finding or rationale in the first-class commit message or review reply so reviewers know why it is not a fixup. + +The author holds targeted fixups in place until the **reviewer** approves. Each fixup stays visible on the branch in the context of its target commit, which preserves review visibility for the response. Do **not** autosquash before or during technical review — that rewrites hashes, stales the packet under review, and hides which fix addressed which finding. After reviewer approval, the author autosquashes into the target commits. + +Distinguish post-approval hash changes: +- **Autosquash only, base unchanged:** include the new commit list and validation status on the merge handoff to the integrator. No repeat technical review. +- **Rebase onto an advanced base** (with or without conflict resolution): send an updated technical review packet to the reviewer and obtain approval again before any merge handoff. Do not treat a base rebase as equivalent to autosquash. + +Fold the stack with `--autosquash`, which requires `-i` explicitly — `--autosquash` alone is a silent no-op. Use `<local-integration-base>` as the rebase base. + +### Inspect before applying + +`git log <local-integration-base>..HEAD` shows current commit order, not the autosquash-reordered todo. To print the prepared plan without applying it, point `GIT_SEQUENCE_EDITOR` at a small unique script file that prints the todo file contents to stderr and exits non-zero: + +```sh +show_todo=$(mktemp) +printf '%s\n' 'cat "$1" >&2' 'rm -f "$0"' 'exit 1' > "$show_todo" +GIT_SEQUENCE_EDITOR="sh $show_todo" \ + git rebase -i --autosquash <local-integration-base> +``` + +This aborts before rewriting history and leaves no rebase state. Prefer a script file over nested-quote one-liners in `GIT_SEQUENCE_EDITOR`; agent harnesses often break the latter. + +### Apply the fold + +In agent environments, apply the prepared plan non-interactively: + +```sh +GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <local-integration-base> +``` + +`GIT_SEQUENCE_EDITOR=true` accepts the autosquash-prepared todo unchanged and continues. It rewrites the branch; it is not a preview. + +In an interactive human terminal you may instead run `git rebase -i --autosquash <local-integration-base>` and edit the plan in your editor. + +If the result is wrong, recover with `git reset --hard ORIG_HEAD` — git sets `ORIG_HEAD` to the exact pre-rebase position regardless of how far back `<local-integration-base>` was. + +For example, run `git rebase master` from the worktree branch when the coordinator says to rebase onto local `master`. Do not write `git rebase /path/to/repo/master`; that is a filesystem path, not a Git ref. + +If `git commit` fails because a hook rejects it, assume no commit was created unless Git clearly reports otherwise. Fix the hook finding, restage the intended files, and rerun the same `git commit` command. Do not use `git commit --amend` to recover from a failed commit attempt. diff --git a/.auxiliary/agents/project.md b/.auxiliary/agents/project.md new file mode 100644 index 0000000..4c20a18 --- /dev/null +++ b/.auxiliary/agents/project.md @@ -0,0 +1,44 @@ +# Project Guidance + +Project-owned knowledge the generated `AGENTS.md` entrypoint must not own. +Structured tracking stays in `nb`. + +## Purpose + +classcore provides class construction infrastructure for Python: metaclasses +and base classes that apply standard behaviors — immutability and concealment — +with integrated dataclass, protocol, and abstract base class support. The +standard taxonomy unifies `Class`-rooted metaclasses (including +`AbstractClass` and `ProtocolClass`) and `*Object` base classes so +behaviorally-rich classes mix cleanly with stdlib `abc` and +`typing.Protocol` machinery without metaclass conflicts. + +## Tech Stack + +- Python >= 3.10, pure Python (no C extensions), sources under + `sources/classcore/` +- Hatch for build and environments; full validation gate is + `hatch --env develop run make-all` +- pytest with a 100% coverage requirement, pyright, ruff, vulture +- Towncrier news fragments in `.auxiliary/data/towncrier/` +- OpenSpec (OPSX) for specs and change proposals; `nb` and `agentmux` MCP + for notes and coordination + +## Notes + +- Architecture and design docs: `documentation/architecture/` +- Release process: `.auxiliary/agents/standards/releases.rst` +- Team org, role ownership, and rolling handoff: `coordination/general/1` + (nb note) +- Use `hatch run python ...`, never bare `python`. +- All stdlib and third-party imports flow through the import hub + `sources/classcore/__/imports.py`; `__.typx` is the typing_extensions + superset — do not import `typing` separately in sources (tests may). +- Nomenclature: metaclasses are named `<Purpose>Class`, bases `*Object`; + avoid compound underscore-separated parameter names in favor of simple + Latin-derived names. +- Every standard metaclass needs `@_class_factory()`; a child metaclass + `__new__` stub without it shadows the inherited injected `__new__` and + silently drops decoration. +- Downstream consumers: Accretive, Frigid — verify before public API + changes. diff --git a/.auxiliary/configuration/AGENTS.md b/.auxiliary/configuration/AGENTS.md deleted file mode 100644 index d191a7f..0000000 --- a/.auxiliary/configuration/AGENTS.md +++ /dev/null @@ -1,144 +0,0 @@ -# Context - -- Overview and Quick Start: README.{md,rst} -- Architecture and Design: @documentation/architecture/ -- Development Practices: @.auxiliary/instructions/ - -- Use the 'context7' MCP server to retrieve up-to-date documentation for any SDKs or APIs. -- Use the 'nb' MCP server for project note-taking, issue tracking, and collaboration. The server provides LLM-friendly access to the `nb` note-taking system with proper escaping and project-specific notebook context. -- Check README files in directories you're working with for insights about architecture, constraints, and TODO items. - -## Purpose -[Describe your project's purpose and goals] - -## Tech Stack -[List your primary technologies] - -# Development Standards - -Before implementing code changes, consult these files in `.auxiliary/instructions/`: -- `practices.rst` - General development principles (robustness, immutability, exception chaining) -- `practices-python.rst` - Python-specific patterns (module organization, type annotations, wide parameter/narrow return) -- `nomenclature.rst` - Naming conventions for variables, functions, classes, exceptions -- `style.rst` - Code formatting standards (spacing, line length, documentation mood) -- `validation.rst` - Quality assurance requirements (linters, type checkers, tests) - -# Operation - -- Use `rg --line-number --column` to get precise coordinates for MCP tools that require line/column positions. -- Choose appropriate editing tools based on the task complexity and your familiarity with the tools. -- If instruction files mention multiple language ecosystems, prefer tools and commands that match the project's configured languages; ignore language-inapplicable tooling unless the user explicitly requests it. -- Use a README-first discovery workflow to reduce token churn: - - Start at the repository root `README.{md,rst}`, then read the nearest relevant subtree README. - - After reading the nearest README, scope code searches to that subtree before considering repo-wide searches. - - If a touched subsystem README is stale after your change, update it in the same batch. -- Batch related changes together when possible to maintain consistency. -- Use relative paths rather than absolute paths when possible. -- Do not write to paths outside the current project unless explicitly requested. -- Use `.auxiliary/scribbles` for scratch work and one-off experiments instead of `/tmp`; use `.auxiliary/temporary` for ephemeral test state and build artifacts that are safe to delete. -- In sandboxed environments (e.g., Codex CLI), treat file/network permission failures as escalation boundaries: - - If an operation fails due to sandbox, file access, or network restrictions, rerun it with user escalation. - - Do not spend time on retry loops or workaround exploration before escalating blocked operations. - -## Note-Taking with `nb` MCP Server - -### When to Use -- **Project coordination**: Record handoffs, document decisions, maintain task lists. -- **Issue tracking**: Create and manage todos with status tracking. -- **Knowledge sharing**: Document patterns, APIs, and project-specific knowledge. -- **Meeting notes**: Record discussions and action items. - -### Scope and Noise Control -- Prefer to update an existing related note/todo over creating a new one when context already exists. -- Avoid logging routine, immediately completed mechanical actions in separate notes. -- Create new notes/todos when information is likely to be useful across sessions or for other collaborators. - -### Tagging Conventions -Use consistent tags for discoverability: -- **Project Component**: `#component-<name>` (e.g., `#component-data-models`) -- **Task Type**: `#task-<type>` (e.g., `#task-design`, `#task-bug`) -- **Status**: `#status-<state>` (e.g., `#status-in-progress`, `#status-review`) -- **Coordination**: `#handoff`, `#coordination` -- **Assignment**: Avoid owner tags (for example `#llm-*`) for task ownership. Use lane/folder ownership and explicit owner text in the note body when needed. - -### Common Patterns -- Check for handoffs: `nb.search` with `#handoff` and `#status-review` tags. -- Find active component work: `nb.search` with `#component-<name>` and `#status-in-progress` tags. -- Track todos: Use `nb.todo`, `nb.tasks`, `nb.do`, `nb.undo`. -- Organize with folders: `nb.folders`, `nb.mkdir`. - -### Notebook Identifier Clarification -- Treat note selectors (for example `coordination/mcp/1`) as canonical IDs for `nb` operations. -- `nb` MCP responses may include notebook-scoped identifiers (for example `my-project:coordination/...`) that look path-like; these are selector forms, not repo-relative filesystem paths. -- Notebook storage is controlled by `nb` configuration (for example `NB_DIR`) and may be outside this repository. -- Prefer `nb` MCP commands to read/edit notes. Avoid assuming a selector maps to a file under the current repo. - -### Recommended `nb` Organization (Project-Defined) -- Prefer a folder taxonomy of `<issue-type>/<component>` (max depth 2) and avoid mixing top-level component folders with top-level issue-type folders. -- Recommended top-level issue types are: - - `todos/` - - `coordination/` - - `decisions/` (optional for durable rationale notes) -- Example component names include `engine`, `mcp`, `tui`, `web`, `handbook`, and `data-models`. -- This project should define and document its specific component-folder conventions in the **Project Notes** section. -- For cross-component work, prefer `coordination/general` and use multiple `#component-*` tags. -- For per-component rolling handoffs, prefer `coordination/<component>` (single continuously updated note) instead of creating history chains under `handoffs/*`. -- Keep notebook lifecycle hygiene: - - prune completed todos quickly, - - keep only active/near-term coordination checkpoints, - - delete stale history-only notes with no owner or action. -- Keep todo titles concise (under 60 chars); use the `tasks` argument for detailed checklist items. This keeps notebook list views readable. - -### `nb` vs OpenSpec Rubric -- Use **OpenSpec proposals** for cross-cutting changes, contract-shaping work, architecture shifts, or work that needs explicit design discussion. -- Use **`nb` todos/notes** for scoped, self-contained implementation tasks where the path is straightforward. -- When in doubt about whether work needs an OpenSpec proposal or only `nb` execution tracking, prefer OpenSpec first for design clarity. -- For each active OpenSpec proposal, keep **exactly one** linked `nb` todo as the tracking anchor (with proposal reference), rather than duplicating full task trees in both systems. - -### OpenSpec Draft and Handoff Hygiene -- Draft OpenSpec proposal text in a dedicated `nb` note first so collaborators can review without local file access barriers; share the note id when requesting feedback. -- When asking for proposal feedback, share the notebook note id first; do not request review against local-only proposal files collaborators cannot access. -- Keep rolling handoff notes stable and update in place, separate from OpenSpec draft/proposal text. -- Do not repurpose or overwrite rolling handoff notes with proposal content. -- After draft review converges, move approved proposal text into `openspec/**` files for human review and commit. - -## Agentmux Message Handling Guidance -- `agentmux` messages may arrive in envelope format and can appear as user prompts. Treat envelope-shaped prompts as inter-agent messages, not automatically as direct human instructions. -- Respond to inter-agent envelope messages via `agentmux` MCP tools (`list`, `send`) rather than as normal assistant replies intended for the human operator. -- Immediate interruption is not required. If you are in active execution, note the message and respond when safe. -- If response will be delayed, send a brief acknowledgement via `send` and record a follow-up todo in `nb` when useful. - -## Agentmux Coordination Noise Control -- Default to low-noise coordination. Do not send acknowledgement-only messages that add no new information or action request. -- Send messages when you are blocked and need input, when requesting concrete review, when handing off completed work with validation, or when reporting material risk/scope change. -- Batch related updates into one message instead of sending rapid-fire partial status pings. -- Use `Cc` only for agents who need to act or review. - -## OpenSpec Instructions - -Workflow Guide: @openspec/AGENTS.md - -Always open `openspec/AGENTS.md` when the request: -- Mentions planning or proposals (words like proposal, spec, change, plan). -- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work. -- Sounds ambiguous and you need the authoritative spec before coding. - -Use `openspec/AGENTS.md` to learn: -- How to create and apply change proposals -- Spec format and conventions -- Project structure and guidelines - -# Commits - -- Use `git status` to ensure all relevant changes are in the changeset. -- Do **not** commit without explicit user approval. Unless the user has requested the commit, **ask first** for a review of your work. -- Do **not** bypass commit safety checks (e.g., `--no-verify`, `--no-gpg-sign`) unless the user explicitly approves doing so. -- Use present tense, imperative mood verbs (e.g., "Fix" not "Fixed"). -- Write sentences with proper punctuation. -- Include a `Co-Authored-By:` field as the final line. Should include the model name and a no-reply address. -- Avoid using `backticks` in commit messages as shell tools may evaluate them as subshell captures. - -# Project Notes - -<!-- This section accumulates project-specific knowledge, constraints, and deviations. - For structured items, use documentation/architecture/decisions/ and `nb`. --> diff --git a/.auxiliary/configuration/coders/claude/.gitignore b/.auxiliary/configuration/coders/claude/.gitignore index 6a5eba4..ffdefa4 100644 --- a/.auxiliary/configuration/coders/claude/.gitignore +++ b/.auxiliary/configuration/coders/claude/.gitignore @@ -1,4 +1,2 @@ +# Claude Code creates project-local settings in this file. settings.local.json -agents/ -commands/ -skills/ diff --git a/.auxiliary/configuration/coders/claude/scripts/post-edit-linter b/.auxiliary/configuration/coders/claude/scripts/post-edit-linter deleted file mode 100755 index 78b38d6..0000000 --- a/.auxiliary/configuration/coders/claude/scripts/post-edit-linter +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -# vim: set filetype=python fileencoding=utf-8: -# -*- coding: utf-8 -*- - -''' Claude Code hook to run linters after file updates. ''' - - -import json -import subprocess -import sys -# import os -# from datetime import datetime - - -def main( ): - # event = _acquire_event_data( ) - if not _is_command_available( 'hatch' ): - raise SystemExit( 0 ) - if not _is_hatch_env_available( 'develop' ): - raise SystemExit( 0 ) - try: - result = subprocess.run( - [ 'hatch', '--env', 'develop', 'run', 'linters' ], # noqa: S607 - capture_output = True, check = False, text = True, timeout = 60 ) - except Exception as exc: - exc_class = type( exc ) - _reactor_failure( f"{exc_class.__qualname__}: {exc}" ) - if result.returncode != 0: - # Combine stdout and stderr since linting output may go to stdout. - result_text = f"{result.stdout}\n\n{result.stderr}".strip( ) - print( _truncate_if_necessary( result_text ), file = sys.stderr ) - raise SystemExit( 2 ) - # Use JSON output for better integration with Claude Code - # _emit_decision_json( "block", f"{result.stdout}\n\n{result.stderr}" ) - raise SystemExit( 0 ) - - -def _acquire_event_data( ): - try: return json.load( sys.stdin ) - except json.JSONDecodeError: - _reactor_failure( "Invalid event data." ) - - -# def _debug_log( message ): -# ''' Logs debug message to file in scribbles directory. ''' -# log_file = '.auxiliary/scribbles/post-edit-linter-debug.log' -# os.makedirs( os.path.dirname( log_file ), exist_ok = True ) -# timestamp = datetime.now().isoformat() -# with open( log_file, 'a' ) as f: -# f.write( f"[{timestamp}] {message}\n" ) - - -def _emit_decision_json( decision, reason ): - ''' Outputs JSON decision for Claude Code hook system. ''' - response = { "decision": decision, "reason": reason } - print( json.dumps( response ) ) - raise SystemExit( 2 ) - - -def _error( message ): - print( message, file = sys.stderr ) - raise SystemExit( 2 ) - - -def _is_command_available( command ): - ''' Checks if a command is available in PATH. ''' - try: - result = subprocess.run( # noqa: S603 - [ 'which', command ], # noqa: S607 - capture_output = True, check = False, text = True, timeout = 5 ) - except Exception: return False - return result.returncode == 0 - - -def _is_hatch_env_available( env_name ): - ''' Checks if a specific Hatch environment exists. ''' - try: - result = subprocess.run( - [ 'hatch', 'env', 'show' ], # noqa: S607 - capture_output = True, check = False, text = True, timeout = 10 ) - except Exception: return False - if result.returncode != 0: return False - return env_name in result.stdout - - -def _reactor_failure( message ): - print( "Claude Code Hook Failure: {message}", file = sys.stderr ) - raise SystemExit( 1 ) - - -def _truncate_if_necessary( output, lines_max = 50 ): - ''' Truncates output to maximum number of lines with truncation notice. ''' - lines = output.split( '\n' ) - if len( lines ) <= lines_max: return output - lines_to_display = lines[ : lines_max ] - truncations_count = len( lines ) - lines_max - lines_to_display.append( - f"\n[OUTPUT TRUNCATED: {truncations_count} additional lines omitted. " - f"Fix the issues above to see remaining diagnostics.]" ) - return '\n'.join( lines_to_display ) - - -if __name__ == '__main__': main( ) diff --git a/.auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check b/.auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check deleted file mode 100755 index 5ccb7e2..0000000 --- a/.auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -# vim: set filetype=python fileencoding=utf-8: -# -*- coding: utf-8 -*- - -''' Claude Code hook to prevent git commits when linters or tests fail. ''' - - -import json -import shlex -import subprocess -import sys - - -_GIT_COMMIT_MIN_TOKENS = 2 - - -def main( ): - event = _acquire_event_data( ) - command_line = _extract_command( event ) - commands = _partition_command_line( command_line ) - for command in commands: - _check_git_commit_command( command ) - raise SystemExit( 0 ) - - -def _acquire_event_data( ): - try: return json.load( sys.stdin ) - except json.JSONDecodeError: - _reactor_failure( "Invalid event data." ) - - -def _check_git_commit_command( tokens ): - ''' Checks for git commit commands and validates linters/tests. ''' - if not _is_git_commit_command( tokens ): return - try: - result = subprocess.run( - [ 'hatch', '--env', 'develop', 'run', 'linters' ], # noqa: S607 - capture_output = True, text = True, timeout = 120, check = False ) - except ( - subprocess.TimeoutExpired, - subprocess.CalledProcessError, - FileNotFoundError - ): _error_with_divine_message( ) - else: - if result.returncode != 0: _error_with_divine_message( ) - try: - result = subprocess.run( - [ 'hatch', '--env', 'develop', 'run', 'testers' ], # noqa: S607 - capture_output = True, text = True, timeout = 300, check = False ) - except ( - subprocess.TimeoutExpired, - subprocess.CalledProcessError, - FileNotFoundError - ): _error_with_divine_message( ) - else: - if result.returncode != 0: _error_with_divine_message( ) - try: - result = subprocess.run( - [ 'hatch', '--env', 'develop', 'run', 'vulture' ], # noqa: S607 - capture_output = True, text = True, timeout = 120, check = False ) - except ( - subprocess.TimeoutExpired, - subprocess.CalledProcessError, - FileNotFoundError - ): _error_with_divine_message( ) - else: - if result.returncode != 0: _error_with_divine_message( ) - - -def _error_with_divine_message( ): - ''' Displays divine admonition and exits. ''' - message = ( - "The Large Language Divinity 🌩️🤖🌩️ in the Celestial Data Center hath " - "commanded that:\n" - "* Thy code shalt pass all lints before thy commit.\n" - " Run: hatch --env develop run linters\n" - " Run: hatch --env develop run vulture\n" - "* Thy code shalt pass all tests before thy commit.\n" - " Run: hatch --env develop run testers\n\n" - "(If you are in the middle of a large refactor, consider commenting " - "out the tests and adding a reminder note in the .auxiliary/notes " - "directory.)" - ) - print( message, file = sys.stderr ) - raise SystemExit( 2 ) - - -def _extract_command( event_data ): - ''' Extracts command from event data, exit if not Bash tool. ''' - tool_name = event_data.get( 'tool_name', '' ) - if tool_name != 'Bash': raise SystemExit( 0 ) - tool_input = event_data.get( 'tool_input', { } ) - return tool_input.get( 'command', '' ) - - -def _is_git_commit_command( tokens ): - ''' Checks if tokens represent a git commit command. ''' - if len( tokens ) < _GIT_COMMIT_MIN_TOKENS: - return False - return tokens[ 0 ] == 'git' and tokens[ 1 ] == 'commit' - - -_splitters = frozenset( ( ';', '&', '|', '&&', '||' ) ) -def _partition_command_line( command_line ): - tokens = shlex.split( command_line ) - commands = [ ] - command_tokens = [ ] - for token in tokens: - if token in _splitters: - commands.append( command_tokens ) - command_tokens = [ ] - continue - command_tokens.append( token ) - if command_tokens: commands.append( command_tokens ) - return commands - - -def _reactor_failure( message ): - print( f"Claude Code Hook Failure: {message}", file = sys.stderr ) - raise SystemExit( 1 ) - - -if __name__ == '__main__': main() diff --git a/.auxiliary/configuration/coders/claude/scripts/pre-bash-python-check b/.auxiliary/configuration/coders/claude/scripts/pre-bash-python-check deleted file mode 100755 index 0ccf678..0000000 --- a/.auxiliary/configuration/coders/claude/scripts/pre-bash-python-check +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -# vim: set filetype=python fileencoding=utf-8: -# -*- coding: utf-8 -*- - -''' Claude Code hook to detect improper Python usage in Bash commands. ''' - - -import json -import shlex -import sys - - -def main( ): - event = _acquire_event_data( ) - command_line = _extract_command( event ) - commands = _partition_command_line( command_line ) - for command in commands: - _check_direct_python_usage( command ) - _check_multiline_python_c( command ) - _check_direct_tool_usage( command ) - raise SystemExit( 0 ) - - -def _acquire_event_data( ): - try: return json.load( sys.stdin ) - except json.JSONDecodeError: - _reactor_failure( "Invalid event data." ) - - -def _check_direct_python_usage( tokens ): - ''' Checks for direct python usage patterns. ''' - emessage = ( - "Warning: Direct Python usage detected in command.\n" - "Consider using 'hatch run python' or " - "'hatch --env develop run python' to ensure dependencies " - "are available." ) - for token in tokens: - if token == 'hatch': return # noqa: S105 - if _is_python_command( token ): _error( emessage ) - - -def _check_multiline_python_c( tokens ): - ''' Checks for multi-line python -c scripts using shlex parsing. ''' - emessage = ( - "Warning: Multi-line Python script detected in command.\n" - "Consider writing the script to a file " - "in the '.auxiliary/scribbles' directory " - "instead of using 'python -c' with multi-line code." ) - for i, token in enumerate( tokens ): - if ( _is_python_command( token ) - and _check_python_c_argument( tokens, i ) - ): _error( emessage ) - - -def _check_direct_tool_usage( tokens ): - ''' Checks for direct usage of Python tools outside Hatch environment. ''' - emessage = ( - "Warning: Direct Python tool usage detected in command.\n" - "Use 'hatch --env develop run {tool}' instead to ensure " - "proper environment and configuration." ) - for token in tokens: - if token == 'hatch': return # noqa: S105 - if _is_python_tool( token ): - _error( emessage.format( tool = token ) ) - - -def _check_python_c_argument( tokens, python_index ): - ''' Checks if Python -c argument contains multiline code. ''' - for j in range( python_index + 1, len( tokens ) ): - if tokens[ j ] == '-c' and j + 1 < len( tokens ): - c_argument = tokens[ j + 1 ] - return '\n' in c_argument - if not tokens[ j ].startswith( '-' ): - # Non-option argument, stop looking for -c - break - return False - - -def _error( message: str ): - print( message, file = sys.stderr ) - raise SystemExit( 2 ) - - -def _extract_command( event_data ): - ''' Extracts command from event data, exit if not Bash tool. ''' - tool_name = event_data.get( 'tool_name', '' ) - if tool_name != 'Bash': raise SystemExit( 0 ) - tool_input = event_data.get( 'tool_input', { } ) - return tool_input.get( 'command', '' ) - - -def _is_python_command( token ): - ''' Checks if token is a Python command. ''' - return ( - token in ( 'python', 'python3' ) or token.startswith( 'python3.' ) ) - - -def _is_python_tool( token ): - ''' Checks if token is a Python development tool. ''' - return token in ( 'coverage', 'pyright', 'pytest', 'ruff' ) - - -_splitters = frozenset( ( ';', '&', '|', '&&', '||' ) ) -def _partition_command_line( command_line ): - tokens = shlex.split( command_line ) - commands = [ ] - command_tokens = [ ] - for token in tokens: - if token in _splitters: - commands.append( command_tokens ) - command_tokens = [ ] - continue - command_tokens.append( token ) - if command_tokens: commands.append( command_tokens ) - return commands - - -def _reactor_failure( message ): - print( "Claude Code Hook Failure: {message}", file = sys.stderr ) - raise SystemExit( 1 ) - - -if __name__ == '__main__': main() diff --git a/.auxiliary/configuration/coders/claude/settings.json b/.auxiliary/configuration/coders/claude/settings.json index b357187..41e3fd7 100644 --- a/.auxiliary/configuration/coders/claude/settings.json +++ b/.auxiliary/configuration/coders/claude/settings.json @@ -7,37 +7,11 @@ "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": 1, "DISABLE_NON_ESSENTIAL_MODEL_CALLS": 1 }, - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": ".claude/scripts/pre-bash-python-check", - "timeout": 10 - }, - { - "type": "command", - "command": ".claude/scripts/pre-bash-git-commit-check", - "timeout": 300 - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Edit|MultiEdit|Write", - "hooks": [ - { - "type": "command", - "command": ".claude/scripts/post-edit-linter", - "timeout": 60 - } - ] - } - ] - }, + "enabledMcpjsonServers": [ + "agentmux", + "context7", + "nb" + ], "permissions": { "auto_allow": [ "mcp__agentmux__list", @@ -51,8 +25,10 @@ "Bash(hatch --env develop run *)", "Bash(awk *)", "Bash(cat *)", + "Bash(comm *)", "Bash(cut *)", "Bash(df *)", + "Bash(diff *)", "Bash(du *)", "Bash(echo *)", "Bash(file *)", @@ -74,30 +50,49 @@ "Bash(gh status *)", "Bash(git add *)", "Bash(git branch *)", + "Bash(git cat-file *)", "Bash(git checkout *)", "Bash(git diff *)", "Bash(git fetch *)", + "Bash(git grep *)", "Bash(git log *)", + "Bash(git ls-files)", + "Bash(git ls-files *)", + "Bash(git ls-tree *)", + "Bash(git merge-base *)", + "Bash(git merge-tree *)", + "Bash(git range-diff *)", "Bash(git rebase *)", "Bash(git restore *)", + "Bash(git rev-list *)", "Bash(git rev-parse *)", "Bash(git show *)", "Bash(git stash list)", "Bash(git stash list *)", "Bash(git status *)", + "Bash(git worktree list)", + "Bash(git worktree list *)", "Bash(grep *)", "Bash(head *)", "Bash(ls *)", "Bash(mkdir *)", + "Bash(nbspec *)", + "Bash(nl *)", + "Bash(npm run *)", + "Bash(od *)", + "Bash(openspec *)", "Bash(ps *)", "Bash(pwd *)", "Bash(rg *)", "Bash(sed *)", "Bash(sort *)", "Bash(tail *)", + "Bash(true)", + "Bash(true *)", "Bash(uniq *)", "Bash(wc *)", - "Bash(which *)" + "Bash(which *)", + "Bash(xxd *)" ] }, "sandbox": { diff --git a/.auxiliary/configuration/coders/codex/.gitignore b/.auxiliary/configuration/coders/codex/.gitignore deleted file mode 100644 index b474d25..0000000 --- a/.auxiliary/configuration/coders/codex/.gitignore +++ /dev/null @@ -1 +0,0 @@ -skills/ diff --git a/.auxiliary/configuration/coders/codex/rules/emcd.rules b/.auxiliary/configuration/coders/codex/rules/emcd.rules index 9234325..6fbd304 100644 --- a/.auxiliary/configuration/coders/codex/rules/emcd.rules +++ b/.auxiliary/configuration/coders/codex/rules/emcd.rules @@ -13,8 +13,10 @@ # Common filesystem and text utilities. prefix_rule(pattern=["awk"], decision="allow") prefix_rule(pattern=["cat"], decision="allow") +prefix_rule(pattern=["comm"], decision="allow") prefix_rule(pattern=["cut"], decision="allow") prefix_rule(pattern=["df"], decision="allow") +prefix_rule(pattern=["diff"], decision="allow") prefix_rule(pattern=["du"], decision="allow") prefix_rule(pattern=["echo"], decision="allow") prefix_rule(pattern=["file"], decision="allow") @@ -23,32 +25,43 @@ prefix_rule(pattern=["grep"], decision="allow") prefix_rule(pattern=["head"], decision="allow") prefix_rule(pattern=["ls"], decision="allow") prefix_rule(pattern=["mkdir"], decision="allow") +prefix_rule(pattern=["nbspec"], decision="allow") +prefix_rule(pattern=["nl"], decision="allow") +prefix_rule(pattern=["od"], decision="allow") +prefix_rule(pattern=["openspec"], decision="allow") prefix_rule(pattern=["ps"], decision="allow") prefix_rule(pattern=["pwd"], decision="allow") prefix_rule(pattern=["rg"], decision="allow") prefix_rule(pattern=["sed"], decision="allow") prefix_rule(pattern=["sort"], decision="allow") prefix_rule(pattern=["tail"], decision="allow") +prefix_rule(pattern=["true"], decision="allow") prefix_rule(pattern=["uniq"], decision="allow") prefix_rule(pattern=["wc"], decision="allow") prefix_rule(pattern=["which"], decision="allow") +prefix_rule(pattern=["xxd"], decision="allow") # Git commands used by normal development workflows. prefix_rule( - pattern=["git", ["add", "branch", "checkout", "commit", "diff", "log", "restore", "show", "status", "tag"]], + pattern=["git", ["add", "branch", "checkout", "commit", "diff", "grep", "log", "restore", "show", "status", "tag"]], decision="allow", justification="Common local git workflow commands.", ) prefix_rule( - pattern=["git", ["fetch", "rebase", "rev-parse"]], + pattern=["git", ["cat-file", "fetch", "ls-files", "ls-tree", "merge-base", "merge-tree", "range-diff", "rebase", "rev-list", "rev-parse"]], decision="allow", - justification="Common git maintenance commands.", + justification="Common git inspection and maintenance commands.", ) prefix_rule( pattern=["git", "stash", "list"], decision="allow", justification="List git stashes.", ) +prefix_rule( + pattern=["git", "worktree", "list"], + decision="allow", + justification="List git worktrees.", +) # GitHub CLI (read-oriented operations). prefix_rule( diff --git a/.auxiliary/configuration/coders/gemini/.gitignore b/.auxiliary/configuration/coders/gemini/.gitignore deleted file mode 100644 index 71f5d3b..0000000 --- a/.auxiliary/configuration/coders/gemini/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -commands/ -skills/ diff --git a/.auxiliary/configuration/coders/gemini/miscellany/command-template.md b/.auxiliary/configuration/coders/gemini/miscellany/command-template.md deleted file mode 100644 index 4cc453b..0000000 --- a/.auxiliary/configuration/coders/gemini/miscellany/command-template.md +++ /dev/null @@ -1,43 +0,0 @@ - -# Process Title - -Brief introductory paragraph explaining the purpose. - -Target/input description: {{args}} - -## Context - -- Current state checks, if applicable: !{command1} -- Environment info, if applicable: !{command2} -- Relevant data, if applicable: !{command3} - -## Prerequisites - -Before running this process, ensure: -- Prerequisite 1 -- Prerequisite 2 -- @-references to relevant guides if applicable - -## Process Summary - -Key functional areas: -1. **Phase 1**: Description -2. **Phase 2**: Description -3. **Phase 3**: Description - -## Safety Requirements - -Stop and consult the user if: -- List validation conditions -- Error conditions that require user input -- Unexpected situations - -## Execution - -Execute the following steps: - -### 1. Step Name -Description of what this step does. - -### 2. Step Name -More steps as needed. diff --git a/.auxiliary/configuration/coders/gemini/policies/emcd.toml b/.auxiliary/configuration/coders/gemini/policies/emcd.toml deleted file mode 100644 index 6293856..0000000 --- a/.auxiliary/configuration/coders/gemini/policies/emcd.toml +++ /dev/null @@ -1,83 +0,0 @@ -# Gemini CLI Policy Engine rules. -# Mirrors prior tools.allowed behavior without using deprecated settings keys. - -[[rule]] -toolName = [ - "mcp__agentmux__list", - "mcp__agentmux__look", - "mcp__agentmux__send", - "mcp__context7__get-library-docs", - "mcp__context7__resolve-library-id", - "mcp__nb__help", - "mcp__nb__nb", - "edit", - "glob", - "google_web_search", - "list_directory", - "read_file", - "replace", - "save_memory", - "search_file_content", - "web_fetch", - "write_file", - "write_todos", -] -decision = "allow" -priority = 300 - -[[rule]] -toolName = "run_shell_command" -commandPrefix = [ - "hatch run", - "hatch --env develop run", - "awk", - "cat", - "cut", - "df", - "du", - "echo", - "file", - "find", - "gh browse", - "gh issue list", - "gh issue view", - "gh pr checks", - "gh pr list", - "gh pr view", - "gh release list", - "gh release view", - "gh repo list", - "gh repo view", - "gh run list", - "gh run view", - "gh run watch", - "gh search issues", - "gh status", - "git add", - "git branch", - "git checkout", - "git diff", - "git fetch", - "git log", - "git rebase", - "git restore", - "git rev-parse", - "git show", - "git stash list", - "git status", - "grep", - "head", - "ls", - "mkdir", - "ps", - "pwd", - "rg", - "sed", - "sort", - "tail", - "uniq", - "wc", - "which", -] -decision = "allow" -priority = 300 diff --git a/.auxiliary/configuration/coders/gemini/settings.json b/.auxiliary/configuration/coders/gemini/settings.json deleted file mode 100644 index 7a50f68..0000000 --- a/.auxiliary/configuration/coders/gemini/settings.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "ui": { - "showLineNumbers": true - }, - "tools": { - "autoAccept": true, - "core": [ - "mcp__agentmux__list", - "mcp__agentmux__look", - "mcp__agentmux__send", - "mcp__context7__get-library-docs", - "mcp__context7__resolve-library-id", - "mcp__nb__help", - "mcp__nb__nb", - "edit", - "glob", - "google_web_search", - "list_directory", - "read_file", - "replace", - "run_shell_command", - "save_memory", - "search_file_content", - "web_fetch", - "write_file", - "write_todos" - ] - }, - "general": { - "checkpointing": { - "enabled": true - } - }, - "mcpServers": { - "agentmux": { - "command": "agentmux", - "args": [ - "host", "mcp", - "--bundle", "python-foundation" - ] - }, - "context7": { - "command": "npx", - "args": [ "-y", "@upstash/context7-mcp" ] - }, - "nb": { - "command": "nb-mcp", - "args": [ - "--notebook", "python-classcore", - "--no-commit-signing" - ] - } - } -} diff --git a/.auxiliary/configuration/coders/opencode/.gitignore b/.auxiliary/configuration/coders/opencode/.gitignore index 22d0249..24ec291 100644 --- a/.auxiliary/configuration/coders/opencode/.gitignore +++ b/.auxiliary/configuration/coders/opencode/.gitignore @@ -1,7 +1,4 @@ -agent/ -command/ -prompt/ -skills/ +# OpenCode may install project-local plugin dependencies. node_modules/ bun.lock package.json diff --git a/.auxiliary/configuration/coders/opencode/plugin/.gitignore b/.auxiliary/configuration/coders/opencode/plugin/.gitignore deleted file mode 100644 index 5ce0d1a..0000000 --- a/.auxiliary/configuration/coders/opencode/plugin/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Node.js dependencies -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# TypeScript build outputs -dist/ -*.tsbuildinfo - -# Bun -.bun.lockb -.bun-debug.log \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/README.md b/.auxiliary/configuration/coders/opencode/plugin/README.md deleted file mode 100644 index e47f8ed..0000000 --- a/.auxiliary/configuration/coders/opencode/plugin/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# Opencode Plugins for Quality Assurance - -This directory contains Opencode plugins that provide quality assurance and development workflow enforcement, ported from Claude Code hooks. - -## Plugins - -### ✅ 1. `post-edit-linter.js` (WORKING) -**Purpose**: Runs linters after file updates -**Event**: `tool.execute.after` (for `edit` tool) -**Behavior**: -- Checks if `hatch` command is available -- Checks if `develop` Hatch environment exists -- Runs `hatch --env develop run linters` -- Throws error with truncated output (50 lines max) if linters fail -- Early exit if conditions not met (hatch not available) -- **Note**: Uses `tool.execute.after` not `file.edited` (LLM-initiated edits don't trigger `file.edited`) - -### ⚠️ 2. `git-commit-guard.js-disabled` (DISABLED - Opencode bash tool limitation) -**Purpose**: Would prevent git commits when linters or tests fail -**Status**: **DISABLED** - Opencode's bash tool doesn't pass command in `input.args.command` -**Issue**: Plugin intercepts `tool.execute.before` but `input.args` is empty for bash tool -**Original intent**: Port of Claude Code hook `pre-bash-git-commit-check` - -### ⚠️ 3. `python-environment-guard.js-disabled` (DISABLED - Opencode bash tool limitation) -**Purpose**: Would detect improper Python usage in Bash commands -**Status**: **DISABLED** - Opencode's bash tool doesn't pass command in `input.args.command` -**Issue**: Plugin intercepts `tool.execute.before` but `input.args` is empty for bash tool -**Original intent**: Port of Claude Code hook `pre-bash-python-check` - -## Installation for Downstream Projects - -When this template is copied to a downstream project: - -1. **Navigate to the plugin directory**: - ```bash - cd .auxiliary/configuration/coders/opencode/plugin - ``` - -2. **Install dependencies**: - ```bash - npm install - ``` - -3. **Ensure symlink exists**: - ```bash - # From project root - ln -sf .auxiliary/configuration/coders/opencode .opencode - ``` - -4. **Verify plugin loading**: - Opencode should automatically load plugins from `.opencode/plugin/` - -## Dependencies - -- `shlex`: Shell command parsing (port of Python's shlex module) - used in disabled plugins -- `bun`: Runtime (provided by Opencode) - -## Porting Notes - -These plugins are ports of Claude Code hooks with varying success: - -| Claude Code Hook | Opencode Plugin | Status | Key Changes | -|-----------------|----------------|--------|-------------| -| `post-edit-linter` | `post-edit-linter.js` | ✅ **WORKING** | Python → JavaScript, `subprocess` → Bun shell API, uses `tool.execute.after` not `file.edited` | -| `pre-bash-git-commit-check` | `git-commit-guard.js-disabled` | ⚠️ **DISABLED** | Tool name: `Bash` → `bash`, uses npm `shlex` package. **Issue**: Opencode bash tool doesn't pass command in `input.args.command` | -| `pre-bash-python-check` | `python-environment-guard.js-disabled` | ⚠️ **DISABLED** | Same parsing logic with `shlex`, exact error messages. **Issue**: Opencode bash tool doesn't pass command in `input.args.command` | - -## Critical Discovery - -**Opencode's bash tool limitation**: During testing, we discovered that Opencode's bash tool doesn't pass the command string in `input.args.command` (or any `input.args` field). The `input.args` object is empty `{}` when the bash tool is invoked. This prevents plugins from intercepting and analyzing bash commands. - -**Working solution**: Only `post-edit-linter.js` works because it uses `tool.execute.after` for the `edit` tool, where file information is available in `output.metadata.filediff.file`. - -## Error Messages - -All error messages match the original Claude Code hooks exactly, including: -- Linter output truncation to 50 lines -- "Divine admonition" for git commit blocking -- Warning messages for Python usage - -## Testing - -To test the plugins: - -1. **File edit test**: Edit a Python file and verify linters run -2. **Git commit test**: Try `git commit -m "test"` and verify checks run -3. **Python usage test**: Try `python -c "print('test')"` and verify warning - -## Troubleshooting - -**Plugins not loading**: -- Verify `.opencode` symlink points to `.auxiliary/configuration/coders/opencode` -- Check Opencode version supports plugin API -- Ensure dependencies are installed (`npm install`) - -**Command not found errors**: -- Verify `hatch` is installed and in PATH -- Check `develop` Hatch environment exists: `hatch env show` - -**Timeout issues**: -- Timeouts match Python hooks (60s, 120s, 300s) -- Uses `Promise.race` with `setTimeout` since Bun shell lacks native timeout - -## Source Code - -Original Claude Code hooks in `template/.auxiliary/configuration/coders/claude/scripts/`: -- `post-edit-linter` -- `pre-bash-git-commit-check` -- `pre-bash-python-check` \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled b/.auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled deleted file mode 100644 index bd9e381..0000000 --- a/.auxiliary/configuration/coders/opencode/plugin/git-commit-guard.js-disabled +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Opencode plugin to prevent git commits when linters or tests fail. - * Port of Claude Code hook: template/.auxiliary/configuration/coders/claude/scripts/pre-bash-git-commit-check - */ -import { split } from 'shlex'; - -export const GitCommitGuard = async ({ project, client, $, directory, worktree }) => { - const GIT_COMMIT_MIN_TOKENS = 2; - const SPLITTERS = new Set([';', '&', '|', '&&', '||']); - - /** - * Checks if a command is available in PATH. - */ - async function isCommandAvailable(command) { - try { - const result = await $`which ${command}`.nothrow().quiet(); - return result.exitCode === 0; - } catch { - return false; - } - } - - /** - * Checks if a specific Hatch environment exists. - */ - async function isHatchEnvAvailable(envName) { - try { - const result = await $`hatch env show`.nothrow().quiet(); - if (result.exitCode !== 0) return false; - return result.stdout.toString().includes(envName); - } catch { - return false; - } - } - - /** - * Runs a command with timeout using Promise.race. - */ - async function runCommandWithTimeout(command, timeoutMs) { - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)), timeoutMs); - }); - - try { - const commandPromise = (async () => { - try { - const result = await $`sh -c "${command}"`.nothrow().quiet(); - return { - exitCode: result.exitCode, - stdout: result.stdout?.toString() || '', - stderr: result.stderr?.toString() || '' - }; - } catch (error) { - return { - exitCode: error.exitCode || 1, - stdout: error.stdout?.toString() || '', - stderr: error.stderr?.toString() || error.message || '' - }; - } - })(); - - return await Promise.race([commandPromise, timeoutPromise]); - } catch (error) { - return { - exitCode: 1, - stdout: '', - stderr: error.message || 'Command execution failed' - }; - } - } - - /** - * Displays divine admonition and exits. - */ - function errorWithDivineMessage() { - const message = ( - "The Large Language Divinity 🌩️🤖🌩️ in the Celestial Data Center hath " + - "commanded that:\n" + - "* Thy code shalt pass all lints before thy commit.\n" + - " Run: hatch --env develop run linters\n" + - " Run: hatch --env develop run vulture\n" + - "* Thy code shalt pass all tests before thy commit.\n" + - " Run: hatch --env develop run testers\n\n" + - "(If you are in the middle of a large refactor, consider commenting " + - "out tests and adding a reminder note in the .auxiliary/notes " + - "directory.)" - ); - throw new Error(message); - } - - /** - * Checks if tokens represent a git commit command. - */ - function isGitCommitCommand(tokens) { - if (tokens.length < GIT_COMMIT_MIN_TOKENS) { - return false; - } - return tokens[0] === 'git' && tokens[1] === 'commit'; - } - - /** - * Partitions command line into separate commands using shell splitters. - */ - function partitionCommandLine(commandLine) { - // Use shlex.split for proper shell parsing (matches Python hook) - const tokens = split(commandLine); - - // Now partition by shell splitters - const commands = []; - let commandTokens = []; - - for (const token of tokens) { - if (SPLITTERS.has(token)) { - if (commandTokens.length > 0) { - commands.push(commandTokens); - commandTokens = []; - } - continue; - } - commandTokens.push(token); - } - - if (commandTokens.length > 0) { - commands.push(commandTokens); - } - - return commands; - } - - /** - * Checks for git commit commands and validates linters/tests. - */ - async function checkGitCommitCommand(tokens) { - if (!isGitCommitCommand(tokens)) return; - - // Check if hatch command is available - if (!(await isCommandAvailable('hatch'))) { - return; // Early exit if hatch not available - } - - // Check if develop Hatch environment exists - if (!(await isHatchEnvAvailable('develop'))) { - return; // Early exit if develop environment not available - } - - // Run linters with 120 second timeout - try { - const result = await runCommandWithTimeout('hatch --env develop run linters', 120000); - if (result.exitCode !== 0) { - errorWithDivineMessage(); - } - } catch { - errorWithDivineMessage(); - } - - // Run tests with 300 second timeout - try { - const result = await runCommandWithTimeout('hatch --env develop run testers', 300000); - if (result.exitCode !== 0) { - errorWithDivineMessage(); - } - } catch { - errorWithDivineMessage(); - } - - // Run vulture with 120 second timeout - try { - const result = await runCommandWithTimeout('hatch --env develop run vulture', 120000); - if (result.exitCode !== 0) { - errorWithDivineMessage(); - } - } catch { - errorWithDivineMessage(); - } - } - - return { - "tool.execute.before": async (input, output) => { - // Only run for bash tool - if (input.tool !== "bash") return; - - // Extract command from input - const command = input.args?.command || ''; - if (!command) return; - - // Partition command line into separate commands - const commands = partitionCommandLine(command); - - // Check each command for git commit - for (const commandTokens of commands) { - await checkGitCommitCommand(commandTokens); - } - } - }; -}; \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/package.json b/.auxiliary/configuration/coders/opencode/plugin/package.json deleted file mode 100644 index 6909e9d..0000000 --- a/.auxiliary/configuration/coders/opencode/plugin/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "opencode-plugins", - "version": "1.0.0", - "type": "module", - "dependencies": { - "@opencode-ai/plugin": "^1.0.134", - "shlex": "^2.1.2" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.0.0" - } -} diff --git a/.auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js b/.auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js deleted file mode 100644 index d659d99..0000000 --- a/.auxiliary/configuration/coders/opencode/plugin/post-edit-linter.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * CORRECT Opencode plugin to run linters after file edits. - * Port of Claude Code hook: template/.auxiliary/configuration/coders/claude/scripts/post-edit-linter - */ -export const PostEditLinterCorrect = async ({ project, client, $, directory, worktree }) => { - /** - * Checks if a command is available in PATH. - */ - async function isCommandAvailable(command) { - try { - const result = await $`which ${command}`.nothrow().quiet(); - return result.exitCode === 0; - } catch { - return false; - } - } - - /** - * Checks if a specific Hatch environment exists. - */ - async function isHatchEnvAvailable(envName) { - try { - const result = await $`hatch env show`.nothrow().quiet(); - if (result.exitCode !== 0) return false; - return result.stdout.toString().includes(envName); - } catch { - return false; - } - } - - /** - * Truncates output to maximum number of lines with truncation notice. - */ - function truncateOutput(output, linesMax = 50) { - const lines = output.split('\n'); - if (lines.length <= linesMax) return output; - const linesToDisplay = lines.slice(0, linesMax); - const truncationsCount = lines.length - linesMax; - linesToDisplay.push( - `\n[OUTPUT TRUNCATED: ${truncationsCount} additional lines omitted. ` + - `Fix the issues above to see remaining diagnostics.]` - ); - return linesToDisplay.join('\n'); - } - - /** - * Runs a command with timeout using Promise.race. - */ - async function runCommandWithTimeout(command, timeoutMs) { - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)), timeoutMs); - }); - - try { - const commandPromise = (async () => { - try { - // Use $ as tagged template function with shell execution - // Pass the entire command as a shell command - const result = await $`sh -c "${command}"`.nothrow().quiet(); - return { - exitCode: result.exitCode, - stdout: result.stdout?.toString() || '', - stderr: result.stderr?.toString() || '' - }; - } catch (error) { - return { - exitCode: error.exitCode || 1, - stdout: error.stdout?.toString() || '', - stderr: error.stderr?.toString() || error.message || '' - }; - } - })(); - - return await Promise.race([commandPromise, timeoutPromise]); - } catch (error) { - return { - exitCode: 1, - stdout: '', - stderr: error.message || 'Command execution failed' - }; - } - } - - return { - "tool.execute.after": async (input, output) => { - // Only run for edit tool - if (input.tool !== "edit") return; - - // Get file path from output (not input!) - const filePath = output?.metadata?.filediff?.file; - if (!filePath) { - // No file path in output, can't run linters - return; - } - - // Check if hatch command is available - if (!(await isCommandAvailable('hatch'))) { - return; // Early exit if hatch not available - } - - // Check if develop Hatch environment exists - if (!(await isHatchEnvAvailable('develop'))) { - return; // Early exit if develop environment not available - } - - try { - // Run linters with 60 second timeout (matches Python script) - const result = await runCommandWithTimeout( - 'hatch --env develop run linters', - 60000 - ); - - if (result.exitCode !== 0) { - // Combine stdout and stderr since linting output may go to stdout - const resultText = `${result.stdout}\n\n${result.stderr}`.trim(); - const truncatedOutput = truncateOutput(resultText); - - // Throw error to show linter failures - throw new Error(`Linters failed for ${filePath}:\n${truncatedOutput}`); - } - } catch (error) { - // Re-throw the error with proper message - if (error.message.includes('Command timed out')) { - throw new Error(`Linter execution timed out for ${filePath}: ${error.message}`); - } - throw error; - } - } - }; -}; \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled b/.auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled deleted file mode 100644 index d27a89c..0000000 --- a/.auxiliary/configuration/coders/opencode/plugin/python-environment-guard.js-disabled +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Opencode plugin to detect improper Python usage in Bash commands. - * Port of Claude Code hook: template/.auxiliary/configuration/coders/claude/scripts/pre-bash-python-check - */ -import { split } from 'shlex'; - -export const PythonEnvironmentGuard = async ({ project, client, $, directory, worktree }) => { - const SPLITTERS = new Set([';', '&', '|', '&&', '||']); - - /** - * Checks if token is a Python command. - */ - function isPythonCommand(token) { - return ( - token === 'python' || - token === 'python3' || - token.startsWith('python3.') - ); - } - - /** - * Checks if token is a Python development tool. - */ - function isPythonTool(token) { - return ['coverage', 'pyright', 'pytest', 'ruff'].includes(token); - } - - /** - * Checks if Python -c argument contains multiline code. - */ - function checkPythonCArgument(tokens, pythonIndex) { - for (let j = pythonIndex + 1; j < tokens.length; j++) { - if (tokens[j] === '-c' && j + 1 < tokens.length) { - const cArgument = tokens[j + 1]; - return cArgument.includes('\n'); - } - if (!tokens[j].startsWith('-')) { - // Non-option argument, stop looking for -c - break; - } - } - return false; - } - - /** - * Checks for direct python usage patterns. - */ - function checkDirectPythonUsage(tokens) { - const emessage = ( - "Warning: Direct Python usage detected in command.\n" + - "Consider using 'hatch run python' or " + - "'hatch --env develop run python' to ensure dependencies " + - "are available." - ); - - for (const token of tokens) { - if (token === 'hatch') return; - if (isPythonCommand(token)) { - throw new Error(emessage); - } - } - } - - /** - * Checks for multi-line python -c scripts using shlex parsing. - */ - function checkMultilinePythonC(tokens) { - const emessage = ( - "Warning: Multi-line Python script detected in command.\n" + - "Consider writing the script to a file " + - "in the '.auxiliary/scribbles' directory " + - "instead of using 'python -c' with multi-line code." - ); - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - if (isPythonCommand(token) && checkPythonCArgument(tokens, i)) { - throw new Error(emessage); - } - } - } - - /** - * Checks for direct usage of Python tools outside Hatch environment. - */ - function checkDirectToolUsage(tokens) { - for (const token of tokens) { - if (token === 'hatch') return; - if (isPythonTool(token)) { - const emessage = ( - `Warning: Direct Python tool usage detected in command.\n` + - `Use 'hatch --env develop run ${token}' instead to ensure ` + - `proper environment and configuration.` - ); - throw new Error(emessage); - } - } - } - - /** - * Partitions command line into separate commands using shell splitters. - */ - function partitionCommandLine(commandLine) { - // Use shlex.split for proper shell parsing (matches Python hook) - const tokens = split(commandLine); - - // Now partition by shell splitters - const commands = []; - let commandTokens = []; - - for (const token of tokens) { - if (SPLITTERS.has(token)) { - if (commandTokens.length > 0) { - commands.push(commandTokens); - commandTokens = []; - } - continue; - } - commandTokens.push(token); - } - - if (commandTokens.length > 0) { - commands.push(commandTokens); - } - - return commands; - } - - return { - "tool.execute.before": async (input, output) => { - // Only run for bash tool - if (input.tool !== "bash") return; - - // Extract command from input - const command = input.args?.command || ''; - if (!command) return; - - // Partition command line into separate commands - const commands = partitionCommandLine(command); - - // Check each command for Python usage issues - for (const commandTokens of commands) { - checkDirectPythonUsage(commandTokens); - checkMultilinePythonC(commandTokens); - checkDirectToolUsage(commandTokens); - } - } - }; -}; \ No newline at end of file diff --git a/.auxiliary/configuration/coders/opencode/settings.jsonc b/.auxiliary/configuration/coders/opencode/settings.jsonc index 7de8240..983fd48 100644 --- a/.auxiliary/configuration/coders/opencode/settings.jsonc +++ b/.auxiliary/configuration/coders/opencode/settings.jsonc @@ -4,15 +4,15 @@ "agent": { "build": { "mode": "primary", - // "model": "zai-coding-plan/glm-4.7" - // "model": "deepseek/deepseek-chat" - "model": "minimax-coding-plan/MiniMax-M2.7" + // "model": "openai/gpt-5.5" + // "model": "xiaomi-token-plan-sgp/mimo-v2.5-pro" + "model": "minimax-coding-plan/MiniMax-M3" }, "plan": { "mode": "primary", - // "model": "zai-coding-plan/glm-4.7" - // "model": "deepseek/deepseek-chat" - "model": "minimax-coding-plan/MiniMax-M2.7" + // "model": "openai/gpt-5.5" + // "model": "xiaomi-token-plan-sgp/mimo-v2.5-pro" + "model": "minimax-coding-plan/MiniMax-M3" } }, @@ -53,8 +53,10 @@ "hatch --env develop run *": "allow", "awk *": "allow", "cat *": "allow", + "comm *": "allow", "cut *": "allow", "df *": "allow", + "diff *": "allow", "du *": "allow", "echo *": "allow", "file *": "allow", @@ -76,30 +78,49 @@ "gh status *": "allow", "git add *": "allow", "git branch *": "allow", + "git cat-file *": "allow", "git checkout *": "allow", "git diff *": "allow", "git fetch *": "allow", + "git grep *": "allow", "git log *": "allow", + "git ls-files": "allow", + "git ls-files *": "allow", + "git ls-tree *": "allow", + "git merge-base *": "allow", + "git merge-tree *": "allow", + "git range-diff *": "allow", "git rebase *": "allow", "git restore *": "allow", + "git rev-list *": "allow", "git rev-parse *": "allow", "git show *": "allow", "git stash list": "allow", "git stash list *": "allow", "git status *": "allow", + "git worktree list": "allow", + "git worktree list *": "allow", "grep *": "allow", "head *": "allow", "ls *": "allow", "mkdir *": "allow", + "nbspec *": "allow", + "nl *": "allow", + "npm run *": "allow", + "od *": "allow", + "openspec *": "allow", "ps *": "allow", "pwd *": "allow", "rg *": "allow", "sed *": "allow", "sort *": "allow", "tail *": "allow", + "true": "allow", + "true *": "allow", "uniq *": "allow", "wc *": "allow", - "which *": "allow" + "which *": "allow", + "xxd *": "allow" }, "edit": "allow", "webfetch": "ask" diff --git a/.auxiliary/configuration/copier-answers--agents.yaml b/.auxiliary/configuration/copier-answers--agents.yaml index 4131711..2afc6c7 100644 --- a/.auxiliary/configuration/copier-answers--agents.yaml +++ b/.auxiliary/configuration/copier-answers--agents.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.0a10-10-g97ab89c +_commit: v1.0a12-53-g1f379d0 _src_path: gh:emcd/agents-common coders: - claude @@ -7,10 +7,40 @@ coders: - opencode instructions_sources: - files: - '*.rst': + conduct.rst: + strip_header_lines: 20 + environment.rst: + strip_header_lines: 20 + maintenance.rst: + strip_header_lines: 20 + nomenclature-germanic.rst: + strip_header_lines: 20 + nomenclature-latin.rst: + strip_header_lines: 20 + nomenclature.rst: + strip_header_lines: 20 + practices-python.rst: + strip_header_lines: 20 + practices-rust.rst: + strip_header_lines: 20 + practices-toml.rst: + strip_header_lines: 20 + practices.rst: + strip_header_lines: 20 + python-autoformat.rst: + strip_header_lines: 20 + releases.rst: + strip_header_lines: 20 + requirements.rst: + strip_header_lines: 20 + style.rst: + strip_header_lines: 20 + tests.rst: + strip_header_lines: 20 + validation.rst: strip_header_lines: 20 source: github:emcd/python-project-common@docs-1#documentation/common -instructions_target: .auxiliary/instructions +instructions_target: .auxiliary/agents/standards languages: - python project_bundle_name: python-foundation diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index d7c26fd..d46ed16 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.58.2 +_commit: v1.59.2 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald diff --git a/.auxiliary/configuration/pre-commit.yaml b/.auxiliary/configuration/pre-commit.yaml index 8631af8..a3b08f4 100644 --- a/.auxiliary/configuration/pre-commit.yaml +++ b/.auxiliary/configuration/pre-commit.yaml @@ -41,7 +41,7 @@ repos: name: 'Check: Debug Statements (Python)' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.9 + rev: v0.16.0 hooks: - id: ruff name: 'Lint: Ruff' diff --git a/.auxiliary/data/towncrier/+abc-support.enhance.rst b/.auxiliary/data/towncrier/+abc-support.enhance.rst new file mode 100644 index 0000000..0f742e5 --- /dev/null +++ b/.auxiliary/data/towncrier/+abc-support.enhance.rst @@ -0,0 +1,6 @@ +Add abstract base class support: ``AbstractClass`` metaclass and +``AbstractObject`` base class combine standard behaviors with +``abc.ABCMeta`` machinery (abstract method enforcement, virtual subclass +registration), and mix with external ABC-based classes without +metaclass conflicts. Re-parent ``ProtocolClass`` under ``AbstractClass``, +unifying the standard metaclass taxonomy under ``Class``. diff --git a/.auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst b/.auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst new file mode 100644 index 0000000..fd3b683 --- /dev/null +++ b/.auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst @@ -0,0 +1,6 @@ +Establish proper inheritance hierarchies for standard metaclasses. +``Dataclass`` now inherits from ``Class``, ``DataclassMutable`` from +``Dataclass``, ``ProtocolDataclass`` from ``ProtocolClass``, and +``ProtocolDataclassMutable`` from ``ProtocolDataclass``. This eliminates +metaclass conflicts when inheriting from classes backed by different +metaclasses in the same family. diff --git a/.auxiliary/data/towncrier/+protocol-detection.repair.rst b/.auxiliary/data/towncrier/+protocol-detection.repair.rst new file mode 100644 index 0000000..5e5dc54 --- /dev/null +++ b/.auxiliary/data/towncrier/+protocol-detection.repair.rst @@ -0,0 +1,5 @@ +Fix runtime protocol recognition for subclasses of classcore protocol +base classes. Previously, ``typing_extensions.Protocol.__init_subclass__`` +set ``_is_protocol = False`` on these subclasses due to an identity check +that did not recognize classcore's own protocol base classes. Subclasses +are now properly detected via structural analysis. diff --git a/.auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst b/.auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst new file mode 100644 index 0000000..a13d2b1 --- /dev/null +++ b/.auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst @@ -0,0 +1,5 @@ +Fix ``typing.runtime_checkable`` support for classcore protocol classes. +``@runtime_checkable`` can now be applied via the ``decorators=`` +metaclass argument or directly after class creation. +``isinstance()`` structural subtyping works correctly with +``@runtime_checkable`` classcore protocols. diff --git a/.auxiliary/data/towncrier/+signature-docs.enhance.rst b/.auxiliary/data/towncrier/+signature-docs.enhance.rst new file mode 100644 index 0000000..17ff04b --- /dev/null +++ b/.auxiliary/data/towncrier/+signature-docs.enhance.rst @@ -0,0 +1,3 @@ +Improve API documentation for class construction and initialization +signatures, including parameter descriptions and the metaclass +delegation contract. diff --git a/.auxiliary/data/towncrier/+type-aliases.enhance.rst b/.auxiliary/data/towncrier/+type-aliases.enhance.rst deleted file mode 100644 index c65e3fd..0000000 --- a/.auxiliary/data/towncrier/+type-aliases.enhance.rst +++ /dev/null @@ -1 +0,0 @@ -Export ``ClassDecorator``, ``ClassDecorators``, ``ClassDecoratorFactory``, and ``ModuleReclassifier`` type aliases for downstream consumption. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 80a1af5..8b362df 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d64596ed3b99f395950925b3e4e19ebc135c2b0d67c4242f268e81c56520a4a -size 515940 +oid sha256:3e6c01b0d0ae3f2f6adcd426d7b3329cfe8ed647a456bc21f85dcb6e4264fe46 +size 562932 diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml index 619c511..2d25a17 100644 --- a/.github/workflows/releaser.yaml +++ b/.github/workflows/releaser.yaml @@ -193,9 +193,14 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} run: | + release_flags=( ) + if [[ "${{ github.ref_name }}" =~ ^v?[0-9]+(\.[0-9]+)*(a|b|rc)[0-9]+$ ]]; then + release_flags+=( --prerelease --latest=false ) + fi gh release create '${{ github.ref_name }}' \ --repo '${{ github.repository }}' \ - --notes-file .auxiliary/artifacts/release-notes.rst + --notes-file .auxiliary/artifacts/release-notes.rst \ + "${release_flags[@]}" - name: Publish Artifacts if: ${{ startsWith(github.ref, 'refs/tags/') }} diff --git a/documentation/architecture/openspec/AGENTS.md b/documentation/architecture/openspec/AGENTS.md deleted file mode 100644 index 3e3c9a4..0000000 --- a/documentation/architecture/openspec/AGENTS.md +++ /dev/null @@ -1,455 +0,0 @@ -# OpenSpec Instructions - -Instructions for AI coding assistants using OpenSpec for spec-driven development. - -## TL;DR Quick Checklist - -- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search) -- Decide scope: new capability vs modify existing capability -- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`) -- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability -- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement -- Validate: `openspec validate [change-id] --strict` and fix issues -- Request approval: Do not start implementation until proposal is approved - -## Three-Stage Workflow - -### Stage 1: Creating Changes -Create proposal when you need to: -- Add features or functionality -- Make breaking changes (API, schema) -- Change architecture or patterns -- Optimize performance (changes behavior) -- Update security patterns - -Triggers (examples): -- "Help me create a change proposal" -- "Help me plan a change" -- "Help me create a proposal" -- "I want to create a spec proposal" -- "I want to create a spec" - -Loose matching guidance: -- Contains one of: `proposal`, `change`, `spec` -- With one of: `create`, `plan`, `make`, `start`, `help` - -Skip proposal for: -- Bug fixes (restore intended behavior) -- Typos, formatting, comments -- Dependency updates (non-breaking) -- Configuration changes -- Tests for existing behavior - -**Workflow** -1. Review top-level `AGENTS.md` and `openspec list`, `openspec list --specs` to understand current context. -2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`. -3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement. -4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal. - -### Stage 2: Implementing Changes -Track these steps as TODOs and complete them one by one. -1. **Read proposal.md** - Understand what's being built -2. **Read design.md** (if exists) - Review technical decisions -3. **Read tasks.md** - Get implementation checklist -4. **Implement tasks sequentially** - Complete in order -5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses -6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality -7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved - -### Stage 3: Archiving Changes -After deployment, create separate PR to: -- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/` -- Update `specs/` if capabilities changed -- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly) -- Run `openspec validate --strict` to confirm the archived change passes checks - -## Before Any Task - -**Context Checklist:** -- [ ] Read relevant specs in `specs/[capability]/spec.md` -- [ ] Check pending changes in `changes/` for conflicts -- [ ] Read top-level `AGENTS.md` for project conventions -- [ ] Run `openspec list` to see active changes -- [ ] Run `openspec list --specs` to see existing capabilities - -**Before Creating Specs:** -- Always check if capability already exists -- Prefer modifying existing specs over creating duplicates -- Use `openspec show [spec]` to review current state -- If request is ambiguous, ask 1–2 clarifying questions before scaffolding - -### Search Guidance -- Enumerate specs: `openspec spec list --long` (or `--json` for scripts) -- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available) -- Show details: - - Spec: `openspec show <spec-id> --type spec` (use `--json` for filters) - - Change: `openspec show <change-id> --json --deltas-only` -- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs` - -## Quick Start - -### CLI Commands - -```bash -# Essential commands -openspec list # List active changes -openspec list --specs # List specifications -openspec show [item] # Display change or spec -openspec validate [item] # Validate changes or specs -openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs) - -# Project management -openspec init [path] # Initialize OpenSpec -openspec update [path] # Update instruction files - -# Interactive mode -openspec show # Prompts for selection -openspec validate # Bulk validation mode - -# Debugging -openspec show [change] --json --deltas-only -openspec validate [change] --strict -``` - -### Command Flags - -- `--json` - Machine-readable output -- `--type change|spec` - Disambiguate items -- `--strict` - Comprehensive validation -- `--no-interactive` - Disable prompts -- `--skip-specs` - Archive without spec updates -- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive) - -## Directory Structure - -``` -openspec/ -├── specs/ # Current truth - what IS built -│ └── [capability]/ # Single focused capability -│ ├── spec.md # Requirements and scenarios -│ └── design.md # Technical patterns -├── changes/ # Proposals - what SHOULD change -│ ├── [change-name]/ -│ │ ├── proposal.md # Why, what, impact -│ │ ├── tasks.md # Implementation checklist -│ │ ├── design.md # Technical decisions (optional; see criteria) -│ │ └── specs/ # Delta changes -│ │ └── [capability]/ -│ │ └── spec.md # ADDED/MODIFIED/REMOVED -│ └── archive/ # Completed changes -``` - -## Creating Change Proposals - -### Decision Tree - -``` -New request? -├─ Bug fix restoring spec behavior? → Fix directly -├─ Typo/format/comment? → Fix directly -├─ New feature/capability? → Create proposal -├─ Breaking change? → Create proposal -├─ Architecture change? → Create proposal -└─ Unclear? → Create proposal (safer) -``` - -### Proposal Structure - -1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique) - -2. **Write proposal.md:** -```markdown -# Change: [Brief description of change] - -## Why -[1-2 sentences on problem/opportunity] - -## What Changes -- [Bullet list of changes] -- [Mark breaking changes with **BREAKING**] - -## Impact -- Affected specs: [list capabilities] -- Affected code: [key files/systems] -``` - -3. **Create spec deltas:** `specs/[capability]/spec.md` -```markdown -## ADDED Requirements -### Requirement: New Feature -The system SHALL provide... - -#### Scenario: Success case -- **WHEN** user performs action -- **THEN** expected result - -## MODIFIED Requirements -### Requirement: Existing Feature -[Complete modified requirement] - -## REMOVED Requirements -### Requirement: Old Feature -**Reason**: [Why removing] -**Migration**: [How to handle] -``` -If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability. - -4. **Create tasks.md:** -```markdown -## 1. Implementation -- [ ] 1.1 Create database schema -- [ ] 1.2 Implement API endpoint -- [ ] 1.3 Add frontend component -- [ ] 1.4 Write tests -``` - -5. **Create design.md when needed:** -Create `design.md` if any of the following apply; otherwise omit it: -- Cross-cutting change (multiple services/modules) or a new architectural pattern -- New external dependency or significant data model changes -- Security, performance, or migration complexity -- Ambiguity that benefits from technical decisions before coding - -Minimal `design.md` skeleton: -```markdown -## Context -[Background, constraints, stakeholders] - -## Goals / Non-Goals -- Goals: [...] -- Non-Goals: [...] - -## Decisions -- Decision: [What and why] -- Alternatives considered: [Options + rationale] - -## Risks / Trade-offs -- [Risk] → Mitigation - -## Migration Plan -[Steps, rollback] - -## Open Questions -- [...] -``` - -## Spec File Format - -### Critical: Scenario Formatting - -**CORRECT** (use #### headers): -```markdown -#### Scenario: User login success -- **WHEN** valid credentials provided -- **THEN** return JWT token -``` - -**WRONG** (don't use bullets or bold): -```markdown -- **Scenario: User login** ❌ -**Scenario**: User login ❌ -### Scenario: User login ❌ -``` - -Every requirement MUST have at least one scenario. - -### Requirement Wording -- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative) - -### Delta Operations - -- `## ADDED Requirements` - New capabilities -- `## MODIFIED Requirements` - Changed behavior -- `## REMOVED Requirements` - Deprecated features -- `## RENAMED Requirements` - Name changes - -Headers matched with `trim(header)` - whitespace ignored. - -#### When to use ADDED vs MODIFIED -- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement. -- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details. -- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name. - -Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead. - -Authoring a MODIFIED requirement correctly: -1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`. -2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios). -3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior. -4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`. - -Example for RENAMED: -```markdown -## RENAMED Requirements -- FROM: `### Requirement: Login` -- TO: `### Requirement: User Authentication` -``` - -## Troubleshooting - -### Common Errors - -**"Change must have at least one delta"** -- Check `changes/[name]/specs/` exists with .md files -- Verify files have operation prefixes (## ADDED Requirements) - -**"Requirement must have at least one scenario"** -- Check scenarios use `#### Scenario:` format (4 hashtags) -- Don't use bullet points or bold for scenario headers - -**Silent scenario parsing failures** -- Exact format required: `#### Scenario: Name` -- Debug with: `openspec show [change] --json --deltas-only` - -### Validation Tips - -```bash -# Always use strict mode for comprehensive checks -openspec validate [change] --strict - -# Debug delta parsing -openspec show [change] --json | jq '.deltas' - -# Check specific requirement -openspec show [spec] --json -r 1 -``` - -## Happy Path Script - -```bash -# 1) Explore current state -openspec spec list --long -openspec list -# Optional full-text search: -# rg -n "Requirement:|Scenario:" openspec/specs -# rg -n "^#|Requirement:" openspec/changes - -# 2) Choose change id and scaffold -CHANGE=add-two-factor-auth -mkdir -p openspec/changes/$CHANGE/{specs/auth} -printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md -printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md - -# 3) Add deltas (example) -cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF' -## ADDED Requirements -### Requirement: Two-Factor Authentication -Users MUST provide a second factor during login. - -#### Scenario: OTP required -- **WHEN** valid credentials are provided -- **THEN** an OTP challenge is required -EOF - -# 4) Validate -openspec validate $CHANGE --strict -``` - -## Multi-Capability Example - -``` -openspec/changes/add-2fa-notify/ -├── proposal.md -├── tasks.md -└── specs/ - ├── auth/ - │ └── spec.md # ADDED: Two-Factor Authentication - └── notifications/ - └── spec.md # ADDED: OTP email notification -``` - -auth/spec.md -```markdown -## ADDED Requirements -### Requirement: Two-Factor Authentication -... -``` - -notifications/spec.md -```markdown -## ADDED Requirements -### Requirement: OTP Email Notification -... -``` - -## Best Practices - -### Simplicity First -- Default to <100 lines of new code -- Single-file implementations until proven insufficient -- Avoid frameworks without clear justification -- Choose boring, proven patterns - -### Complexity Triggers -Only add complexity with: -- Performance data showing current solution too slow -- Concrete scale requirements (>1000 users, >100MB data) -- Multiple proven use cases requiring abstraction - -### Clear References -- Use `file.ts:42` format for code locations -- Reference specs as `specs/auth/spec.md` -- Link related changes and PRs - -### Capability Naming -- Use verb-noun: `user-auth`, `payment-capture` -- Single purpose per capability -- 10-minute understandability rule -- Split if description needs "AND" - -### Change ID Naming -- Use kebab-case, short and descriptive: `add-two-factor-auth` -- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-` -- Ensure uniqueness; if taken, append `-2`, `-3`, etc. - -## Tool Selection Guide - -| Task | Tool | Why | -|------|------|-----| -| Find files by pattern | Glob | Fast pattern matching | -| Search code content | Grep | Optimized regex search | -| Read specific files | Read | Direct file access | -| Explore unknown scope | Task | Multi-step investigation | - -## Error Recovery - -### Change Conflicts -1. Run `openspec list` to see active changes -2. Check for overlapping specs -3. Coordinate with change owners -4. Consider combining proposals - -### Validation Failures -1. Run with `--strict` flag -2. Check JSON output for details -3. Verify spec file format -4. Ensure scenarios properly formatted - -### Missing Context -1. Read top-level `AGENTS.md` first -2. Check related specs -3. Review recent archives -4. Ask for clarification - -## Quick Reference - -### Stage Indicators -- `changes/` - Proposed, not yet built -- `specs/` - Built and deployed -- `archive/` - Completed changes - -### File Purposes -- `proposal.md` - Why and what -- `tasks.md` - Implementation steps -- `design.md` - Technical decisions -- `spec.md` - Requirements and behavior - -### CLI Essentials -```bash -openspec list # What's in progress? -openspec show [item] # View details -openspec validate --strict # Is it correct? -openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation) -``` - -Remember: Specs are truth. Changes are proposals. Keep them in sync. diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/design.md b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/design.md new file mode 100644 index 0000000..e5b512c --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/design.md @@ -0,0 +1,134 @@ +## Context + +Python resolves the metaclass of a new class by taking the most derived +metaclass among its bases, requiring that metaclass to be a (non-strict) +subclass of every base's metaclass. The check lives in `type.__new__` +and uses C-level MRO subtype tests — `__subclasscheck__` overrides on +candidate metaclasses are ignored. Consequently the only clean way to +make two metaclass families compatible is genuine inheritance. + +Today `Class → type` and `abc.ABCMeta → type` are siblings, so +`class Foo( classcore.Object, SomeABC )` raises `TypeError: metaclass +conflict`. Likewise `ProtocolClass → _ProtocolMeta → ABCMeta` never +passes through `Class`, so protocol bases cannot mix with ordinary +classcore classes. + +## Goals / Non-Goals + +- Goals: + - Allow `class Foo( classcore.AbstractObject, SomeABC )` without + metaclass conflict, preserving standard behaviors. + - Preserve abstract method enforcement and virtual subclass + registration (`register()`, `isinstance()` on registered classes). + - Keep `Class`-backed classes free of ABC machinery (opt-in only). + - Unify the metaclass taxonomy: `ProtocolClass` becomes a descendant + of `Class`, enabling `class Foo( classcore.Protocol, + classcore.Object )`. +- Non-Goals: + - Making `Class` itself an `ABCMeta` descendant — would impose the ABC + contract (registration caches, `__instancecheck__` machinery) on + every classcore class. + - Mutable or dataclass abstract variants (`AbstractObjectMutable`, + `AbstractDataclassObject`) — natural follow-ups once the minimal pair + proves itself. + - Auto-reconciliation of conflicting metaclasses by synthesizing + combined metaclasses at class creation — implicit magic that + manufactures unlisted metaclasses, contrary to the package's + explicit-everything philosophy. + +## Decisions + +### Decision 1: Opt-in diamond metaclass + +```python +@_class_factory( ) +class AbstractClass( Class, __.abc.ABCMeta ): ... + +class AbstractObject( + metaclass = AbstractClass, + class_mutables = _abc_class_mutables, +): ... +``` + +C3 linearization is consistent: +`AbstractClass → Class → ABCMeta → type → object`. Verified empirically: +mixed-base creation with an external ABC succeeds, abstract method +enforcement raises `TypeError` on instantiation of incomplete classes, +and `register()` plus `isinstance()` behave correctly. + +- Alternatives considered: + - `Class( ABCMeta )`. Rejected — changes the contract of `Class`; + every ordinary class would carry ABC machinery. + - Intercepting construction to synthesize a combined metaclass. + Rejected — `type.__new__` performs the conflict check with C-level + subtype tests that bypass `__subclasscheck__`, and manufacturing + unlisted metaclasses is implicit magic. + - Naming `ABCFactory`. Rejected — every metaclass in the family is + named `<Purpose>Class` (`Class`, `ProtocolClass`); `AbstractClass` + follows the convention, as `AbstractObject` follows `*Object`. + +### Decision 2: `class_mutables` on `AbstractObject` + +CPython's C `_abc` module updates the registration caches (`_abc_registry`, +`_abc_cache`, `_abc_negative_cache`) via direct dictionary writes that +bypass metaclass `__setattr__`, so `register()` happens to work without +exemptions on CPython. The pure-Python fallback (`_py_abc`) used by some +interpreters assigns via `setattr`, which classcore immutability would +block. `class_mutables = _abc_class_mutables` (the existing tuple, already +extended for `runtime_checkable`) is cheap, defensive, and consistent +with the four protocol base classes. It also propagates to subclasses +through MRO attribute lookup. + +### Decision 3: `ProtocolClass` re-parenting + +```python +@_class_factory( ) +class ProtocolClass( AbstractClass, type( __.typx.Protocol ) ): ... +``` + +MRO becomes +`ProtocolClass → AbstractClass → Class → _ProtocolMeta(te) → +_ProtocolMeta(typing) → ABCMeta → type → object`. + +This is enabled by the wrapper-level delegation from +`update-protocol-recognition`: parent-metaclass wrappers on the chain +(`AbstractClass`, `Class`) see the closure/context mismatch and delegate +directly to the next `__new__`/`__init__`, so construction hooks and +decorators still execute exactly once (verified with a recording +decorator through the three-level hierarchy). `ProtocolClass` must keep +`@_class_factory( )`; a body-defined `__new__` stub without factory +application shadows the inherited injected `__new__` and silently +bypasses decoration (observed in prototyping). + +Out of scope: directly inheriting a protocol definition base together +with an ordinary base (e.g., `class Foo( Protocol, Object )`). Empirical +finding: `typing_extensions._ProtocolMeta.__new__` rejects ordinary +bases when `typing.Protocol` is a *direct* base ("Protocols can only +inherit from other protocols"), and metaclass unification does not lift +that rule. Intermediating through a classcore protocol base technically +bypasses the direct-base check (the class is creatable), but the result +is a protocol definition carrying concrete implementation ancestors — +contrary to PEP 544's intent and fragile against future typing +validation tightening. Concrete classcore classes remain structurally +compatible with classcore protocols, which is the supported way to mix +the families. + +## Risks / Trade-offs + +- **`ProtocolClass` metaclass MRO deepens** (`AbstractClass`, `Class` + inserted before `_ProtocolMeta`). Behavior is guarded by the existing + delegation exact-once tests plus the full protocol suite. +- **Interpreter variance**: the C `_abc` fast-path bypasses + `__setattr__` on CPython; PyPy or the `_py_abc` fallback may not. + Exemptions (Decision 2) cover both. PyPy has surfaced subtle classcore + bugs before; worth including in any multi-interpreter test matrix. +- **`AbstractObject` is a fresh root** (does not inherit `Object`), + matching the existing convention (`DataclassObject`, `Protocol`). + Mixing `class Foo( AbstractObject, Object )` resolves correctly since + `AbstractClass` is a subclass of `Class`. + +## Open Questions + +None. Naming, hierarchy, and exemption approach settled with the human +(2026-08-14): `AbstractClass` preferred over `ABCFactory`; protocol +re-parenting in scope. diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/proposal.md b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/proposal.md new file mode 100644 index 0000000..3b8c9c6 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/proposal.md @@ -0,0 +1,45 @@ +# Change: Add Abstract Base Class Support + +## Why + +Classes created by classcore metaclasses cannot be combined with external +abstract base classes. `Class` inherits from `type` while `abc.ABCMeta` +also inherits from `type`, so `class Foo( classcore.Object, SomeABC )` +fails with `TypeError: metaclass conflict` — the two metaclasses are +siblings. The same gap separates `ProtocolClass` from `Class`: the +protocol metaclass family never passes through `Class`, so protocol +definition bases cannot share a metaclass lineage with ordinary +classcore classes. + +## What Changes + +- **Add `AbstractClass` metaclass**: combines `Class` with `abc.ABCMeta` + via a diamond hierarchy. Opt-in — `Class` itself remains backed by + plain `type`, so the contract of ordinary classcore classes is + unchanged (no ABC machinery, no registration caches). +- **Add `AbstractObject` base class**: created with `AbstractClass` and + carrying the ABC cache mutable exemptions, for authoring abstract + classes with standard behaviors that mix with external ABCs. +- **Re-parent `ProtocolClass`**: from `type( typx.Protocol )` to + `( AbstractClass, type( typx.Protocol ) )`, unifying the metaclass + taxonomy under `Class`. Protocol behavior is preserved by the + wrapper-level delegation introduced in `update-protocol-recognition`. + Concrete classcore classes remain structurally compatible with + classcore protocols. Direct inheritance of a protocol definition base + alongside an ordinary base (e.g., `Protocol` and `Object` together) + remains unsupported: typing independently rejects ordinary bases in + protocol definitions, and the metaclass unification does not (and + should not) change that. + +## Impact + +- Affected specs: `abc-support` (new capability) +- Affected code: + - `sources/classcore/__/imports.py` — add `abc` to import hub + - `sources/classcore/standard/classes.py` — `AbstractClass`, + `AbstractObject`, `ProtocolClass` re-parenting + - `sources/classcore/__/doctab.py` — documentation fragments as needed + - New tests for hierarchy, mixing, enforcement, registration +- Empirically verified via scratch prototypes (diamond MRO, mixed-base + creation, abstract enforcement, `register()`, exact-once decoration + through the unified hierarchy) diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/specs/abc-support/spec.md b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/specs/abc-support/spec.md new file mode 100644 index 0000000..352b60f --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/specs/abc-support/spec.md @@ -0,0 +1,78 @@ +## ADDED Requirements + +### Requirement: Abstract Class Metaclass + +The `AbstractClass` metaclass MUST combine the standard behaviors of +`Class` with `abc.ABCMeta` via a diamond inheritance hierarchy. `Class` +itself MUST remain backed by plain `type` so that ordinary classcore +classes carry no ABC machinery. + +#### Scenario: Abstract class creation +- **WHEN** a class uses the `AbstractClass` metaclass +- **THEN** the resulting class MUST support standard behaviors + (immutability, concealment) and ABC machinery (abstract method + enforcement, virtual subclass registration) + +#### Scenario: Metaclass hierarchy +- **WHEN** `AbstractClass` is inspected +- **THEN** it MUST be a subclass of both `Class` and `abc.ABCMeta` + +#### Scenario: Ordinary classes unaffected +- **WHEN** a class uses the `Class` metaclass +- **THEN** it MUST NOT gain ABC machinery + +### Requirement: Abstract Base Class + +`AbstractObject` MUST serve as a base class for abstract classes with +standard behaviors, created with `AbstractClass` and exempting ABC cache +attributes from class immutability. + +#### Scenario: Inheritance +- **WHEN** a class inherits from `AbstractObject` +- **THEN** the class MUST be created with `AbstractClass` as its + metaclass, with standard behaviors active + +#### Scenario: Mixing with external abstract base classes +- **WHEN** a class inherits from both `AbstractObject` and an external + class whose metaclass is `abc.ABCMeta` +- **THEN** class creation MUST succeed without + `TypeError: metaclass conflict` + +#### Scenario: Abstract method enforcement +- **WHEN** a class inherits abstract methods from an external ABC and + does not implement them +- **THEN** instantiation MUST raise `TypeError` +- **AND** a subclass implementing all abstract methods MUST instantiate + +#### Scenario: Virtual subclass registration +- **WHEN** `register()` is called on a class derived from + `AbstractObject` +- **THEN** the registered class MUST satisfy `isinstance()` checks +- **AND** ABC cache mutations MUST NOT raise immutability errors + +### Requirement: Unified Protocol Metaclass + +`ProtocolClass` MUST inherit from both `AbstractClass` and the metaclass +of `typing.Protocol`, unifying the metaclass taxonomy under `Class`. + +#### Scenario: Unified taxonomy +- **WHEN** `ProtocolClass` is inspected +- **THEN** it MUST be a subclass of `Class`, `AbstractClass`, and + `abc.ABCMeta` + +#### Scenario: Protocol functionality preserved +- **WHEN** the `protocol-support` capability scenarios are exercised +- **THEN** runtime protocol detection, `@runtime_checkable`, and + `isinstance()` structural subtyping MUST continue to behave as + specified + +#### Scenario: Single decoration through hierarchy +- **WHEN** a class is created through the unified protocol metaclass + hierarchy with a decorator +- **THEN** the decorator MUST be applied exactly once + +#### Scenario: Concrete classes structurally satisfy protocols +- **WHEN** a concrete classcore class (e.g., backed by `Class` or + `AbstractClass`) implements all members of a `@runtime_checkable` + classcore protocol +- **THEN** `isinstance()` MUST return `True` for that protocol diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/tasks.md b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/tasks.md new file mode 100644 index 0000000..0875a7d --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-add-abc-support/tasks.md @@ -0,0 +1,48 @@ +## 1. Implementation + +- [x] 1.1 Add `abc` to the import hub (`sources/classcore/__/imports.py`) +- [x] 1.2 Define `AbstractClass( Class, __.abc.ABCMeta )` in + `sources/classcore/standard/classes.py`: `@_class_factory( )`, + `__new__` typechecker stub, `_dynadoc_fragments_` +- [x] 1.3 Define `AbstractObject` with `metaclass = AbstractClass` and + `class_mutables = _abc_class_mutables`, with `_dynadoc_fragments_` +- [x] 1.4 Re-parent `ProtocolClass` to + `( AbstractClass, type( __.typx.Protocol ) )` +- [x] 1.5 Add documentation fragment entries in + `sources/classcore/__/doctab.py` as needed — all referenced fragments + already resolve; no new entries required +- [x] 1.6 Update `sources/classcore/README.md` taxonomy with + `AbstractClass` / `AbstractObject` (and fix stale `ProtocolObject` + reference) + +## 2. Tests + +- [x] 2.1 Hierarchy: `issubclass( AbstractClass, Class )`, + `issubclass( AbstractClass, abc.ABCMeta )`; unified + `issubclass( ProtocolClass, Class )` and + `issubclass( ProtocolClass, AbstractClass )` +- [x] 2.2 `AbstractObject` creation with standard behaviors (class + immutability enforceable) +- [x] 2.3 Mixing: class inheriting `AbstractObject` and an external + `abc.ABC`-based class is created without `TypeError`; metaclass is + `AbstractClass` +- [x] 2.4 Abstract method enforcement: incomplete subclass raises + `TypeError` on instantiation; complete subclass instantiates +- [x] 2.5 Registration: `register()` on an `AbstractObject` descendant; + registered class satisfies `isinstance()`; no immutability errors +- [x] 2.6 Protocol functionality preserved: existing protocol suite + passes unchanged (detection, `runtime_checkable`, `isinstance`) +- [x] 2.7 Exact-once decoration: recording decorator applied through the + unified `ProtocolClass → AbstractClass → Class` hierarchy invokes once +- [x] 2.8 Structural compatibility: a concrete classcore class implementing + all members of a `@runtime_checkable` classcore protocol satisfies + `isinstance()` for that protocol + +## 3. Validation + +- [x] 3.1 `hatch --env develop run make-all` — all green, 100% coverage +- [x] 3.2 Towncrier fragment for the new capability +- [x] 3.3 `openspec validate add-abc-support --strict` +- [x] 3.4 Type checker canaries: private `_Canary*` declarations at end of + `classes.py` exercise Pyright evaluation of `AbstractObject` subclassing + and protocol definition/implementation; pyright reports 0 diagnostics diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/design.md b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/design.md new file mode 100644 index 0000000..35ae07f --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/design.md @@ -0,0 +1,221 @@ +## Context + +Classcore provides protocol metaclasses that combine `typing.Protocol` with +standard behaviors (immutability, concealment, dynadoc). Three defects in the +current implementation prevent these protocol classes from functioning +correctly at runtime. + +### Root Cause Analysis + +**Metaclass hierarchy**: All three protocol metaclasses independently inherit +from `_ProtocolMeta`: + +``` +ProtocolClass → _ProtocolMeta +ProtocolDataclass → _ProtocolMeta (sibling of ProtocolClass) +ProtocolDataclassMutable → _ProtocolMeta (sibling of both) +``` + +Python requires all metaclasses of a class's bases to be in a subclass +relationship. Since the protocol metaclasses are siblings, multi-base +inheritance involving classes backed by different protocol metaclasses raises +`TypeError: metaclass conflict`. + +**Protocol detection**: `typing_extensions.Protocol.__init_subclass__` contains: + +```python +def __init_subclass__(cls, *args, **kwargs): + super().__init_subclass__(*args, **kwargs) + if not cls.__dict__.get('_is_protocol', False): + cls._is_protocol = any(b is Protocol for b in cls.__bases__) +``` + +The identity check `b is Protocol` (where `Protocol` is +`typing_extensions.Protocol`) fails for classcore subclasses because +classcore's `Protocol` is a different class object. The `__init_subclass__` +explicitly sets `_is_protocol = False` on subclasses, shadowing the inherited +`True` value. This breaks `@runtime_checkable`, `isinstance()` structural +checks, and `__protocol_attrs__` collection. + +Confirmed: classcore's own `Protocol` has `_is_protocol = True` (it directly +inherits from `typing_extensions.Protocol`, satisfying the identity check). +The problem only manifests on subclasses of classcore protocol base classes. + +Confirmed: pyright is unaffected (0 errors) because it performs static +structural analysis without relying on `_is_protocol`. + +**Double-processing risk**: The class factory's constructor +(`factories.py:produce_class_constructor`) has an `in_progress` short-circuit +to prevent recursive decoration. However, it checks the flag *after* +`superf()` returns. When metaclasses form a hierarchy, the parent metaclass's +constructor sets `in_progress = True`, processes, then sets it to `False` +before the child metaclass's constructor checks it. This causes +preprocessors, postprocessors, and decorators to be applied once per +metaclass in the MRO. + +## Goals / Non-Goals + +- Goals: + - Establish a proper metaclass hierarchy for all standard metaclasses + (protocol and non-protocol), for consistency (Principle of Least + Surprise). + - Ensure subclasses of classcore protocol base classes are recognized as + protocols at runtime (`_is_protocol = True`). + - Ensure `@runtime_checkable` works with classcore protocol classes. + - Ensure `isinstance()` structural subtyping works with + `@runtime_checkable` classcore protocols. + - Prevent double-processing when metaclass constructors form a hierarchy. +- Non-Goals: + - Type-checker-side `.pyi` stub changes. Pyright already handles + classcore protocols correctly via static analysis. + +## Decisions + +### Decision 1: Metaclass hierarchy + +Establish proper inheritance hierarchies for both protocol and non-protocol +metaclasses, for consistency. + +**Protocol metaclasses:** + +``` +ProtocolClass → _ProtocolMeta → _ProtocolMeta(typing) → ABCMeta → type +ProtocolDataclass → ProtocolClass +ProtocolDataclassMutable → ProtocolDataclass +``` + +**Non-protocol metaclasses:** + +``` +Class → type +Dataclass → Class +DataclassMutable → Dataclass +``` + +When forming these hierarchies, `@dataclass_transform` metadata on mutable +metaclasses must be verified. Confirmed: `@dataclass_transform` sets +`__dataclass_transform__` in the class's own `__dict__`, which shadows any +inherited value. `ProtocolDataclassMutable` (which omits `frozen_default`) +will correctly have `frozen_default = False` in its own +`__dataclass_transform__`, not inheriting `True` from `ProtocolDataclass`. + +- Alternatives considered: A metaclass `__init_subclass__` shim to resolve + conflicts dynamically. Rejected — proper hierarchy is simpler and more + correct. + +### Decision 2: Protocol detection fix + +Detect protocol base classes structurally, in the `ProtocolClass.__new__` +stub. A base is a protocol base class when it has `_is_protocol = True` +in its own `__dict__` (not merely inherited) and has a typing `Protocol` +class as a direct base (`_protocol_cls_set & set(base.__bases__)`). +When found, set `_is_protocol = True` on the new class before any +decorators are applied, so `decorators = (typing.runtime_checkable,)` +sees the correct value. + +- Semantics: `class MyProto(classcore.Protocol)` gets `_is_protocol = True`. + `class Impl(MyProto)` gets `_is_protocol = False` (concrete implementation, + no protocol base class in `__bases__`). +- This matches standard `typing` semantics where a class is a protocol only + if it directly inherits from `Protocol`. +- Third-party extensibility: adding a typing `Protocol` to the bases of a + classcore protocol subclass makes it a protocol base class in turn, + mirroring standard typing usage. +- Alternatives considered: + - Sentinel attribute (`_classcore_protocol_base`) on each protocol base + class's `__dict__`. Rejected — visible via `__getattribute__` on all + subclasses (namespace pollution), and redundant with the structural + signal already present. + - Module-level registry (`frozenset`). Rejected — a frozen registry + precludes extension by third-party packages; a mutable registry invites + mutation by callers who should instead use the framework properly. + - Checking metaclass type (`isinstance(b, ProtocolClass)`). Rejected — + cannot distinguish protocol definitions from concrete implementations, + since both use the same metaclass. + +### Decision 3: Where to implement the detection fix + +Split across the two metaclass hook sites, each for a distinct reason: + +- `ProtocolClass.__new__`: corrects `_is_protocol` immediately after class + creation. Must run before decorator application (inside the constructor's + `superf()` chain) so `typing.runtime_checkable` via `decorators=` sees + `_is_protocol = True`. +- `ProtocolClass.__init__`: replaces `__protocol_attrs__` after + `_ProtocolMeta.__init__` computes it (see Decision 5). + +`__init_subclass__` was rejected as the site: it would need to be defined +on all four protocol base classes (they do not inherit from each other at +the class level), and it runs before decorators are known. + +### Decision 4: Double-processing prevention + +Implement hierarchy delegation in the decorator wrappers +(`decorators.py`), not in the produced constructors/initializers. Each +wrapper already holds the defining metaclass in its closure (`clscls`) +and receives the most-derived metaclass as its call context (`clscls_` +for construction, `type(cls)` for initialization). On mismatch, the +wrapper delegates directly past its own constructor/initializer — the +parent metaclass's machinery never runs a second time. + +`ClassConstructor` and `ClassInitializer` keep their original public +signatures; no major release is required for this change. + +- Alternatives considered: + - `origin`/`defining_clscls` parameter threaded from wrapper to + constructor (`if origin is not clscls: return`). Implemented and + reviewed first, then withdrawn — it changed the public callback + signatures for an internal concern, forcing a major version. + - Only applying `@_class_factory()` to `ProtocolClass` and having child + metaclasses inherit factory behaviors. Rejected — the `__new__` stub + in each metaclass's class body would shadow the inherited + factory-injected `__new__`, bypassing preprocessing; also the wrappers' + closure variables would reference the parent metaclass, defeating + start-of-MRO behavior enforcement. + +### Decision 5: Snapshot-based `__protocol_attrs__` + +`typing_extensions` computes `__protocol_attrs__` by scanning the +decorated namespace (all of `__dict__` and `__annotations__` across the +MRO), which collects framework-injected attributes (behavior dunders, +dataclass-generated methods, classcore internals). Reordering decoration +cannot help: `_ProtocolMeta.__init__` always runs after `__new__`, +after all decoration, and `runtime_checkable` itself sets attributes on +the finished class. + +Instead, snapshot the declared members in `ProtocolClass.__new__` +immediately after class creation — the pre-decoration namespace contains +exactly what the author declared. `ProtocolClass.__init__` then replaces +the computed attrs with `declared | union(inherited)` over +`ProtocolClass`-family bases. + +- A user-declared dunder (e.g., `__repr__`) is preserved as a protocol + member; framework-generated dunders never enter the snapshot, so no + dataclass-dunder blacklist is needed. +- Exclusions are limited to class-creation machinery (`__abstractmethods__`, + `__parameters__`, `__init__`, `__orig_bases__`), typing internals, and + `_classcore_`/`_abc_` prefixes. +- The snapshot is idempotent and survives dataclass class reproduction + (`_add_slots` copies `__dict__`, including the snapshot attribute). +- Alternatives considered: filtering the post-decoration attrs with a + blacklist of framework dunder names. Implemented and reviewed first, then + withdrawn — unmaintainable and silently drops user-declared dunders. + +## Risks / Trade-offs + +- **Metaclass hierarchy change is backward-compatible**: Existing code using a + single protocol metaclass is unaffected. The change only enables + previously-impossible multi-metaclass inheritance. +- **`_is_protocol` fix changes runtime behavior**: Subclasses of classcore + protocol base classes will now have `_is_protocol = True` instead of + `False`. This is a bug fix — the previous behavior was incorrect. But any + code relying on the buggy behavior (unlikely) would break. +- **`__protocol_attrs__` snapshot is broader than the standard scan**: A + user-declared dunder now counts toward structural checks, so a candidate + lacking it fails `isinstance`. This matches standard `typing`, which also + includes declared dunders, so behavior diverges only from the interim + blacklist, not from standard typing semantics. + +## Open Questions + +None remaining. All resolved during review. diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/proposal.md b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/proposal.md new file mode 100644 index 0000000..bac9d0a --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/proposal.md @@ -0,0 +1,73 @@ +# Change: Fix Protocol Metaclass Hierarchy and Runtime Recognition + +## Why + +Classcore's protocol classes do not properly integrate with `typing.Protocol`'s +runtime machinery. Three interrelated defects prevent subclasses of classcore +protocol base classes from being recognized as protocols at runtime: + +1. **Metaclass hierarchy is flat** — `ProtocolClass`, `ProtocolDataclass`, and + `ProtocolDataclassMutable` all independently inherit from + `_ProtocolMeta`. They do not form a subclass relationship, causing + `TypeError: metaclass conflict` when a user attempts to inherit from + classes backed by different protocol metaclasses (e.g., `Protocol` and + `DataclassProtocol`). + +2. **`_is_protocol` is not set on subclasses** — + `typing_extensions.Protocol.__init_subclass__` uses identity comparison + (`b is Protocol`) to determine whether a subclass is a protocol. Because + classcore's `Protocol` is not `typing_extensions.Protocol`, subclasses of + classcore protocol base classes get `_is_protocol = False` explicitly set + on them, shadowing the inherited `True`. This breaks: + - `@runtime_checkable` (raises `TypeError` at runtime) + - `isinstance()` structural subtyping checks + - `__protocol_attrs__` collection + +3. **Protocol base class not statically recognized as sufficient** — + Related to (2); pyright currently handles this correctly via static + structural analysis, but runtime behavior is broken. + +## What Changes + +- **Establish metaclass hierarchies**: Both protocol and non-protocol + metaclasses SHALL form proper inheritance hierarchies for consistency. + Protocol: `ProtocolDataclass(ProtocolClass)`, + `ProtocolDataclassMutable(ProtocolDataclass)`. + Non-protocol: `Dataclass(Class)`, `DataclassMutable(Dataclass)`. + This eliminates metaclass conflicts for cross-metaclass inheritance. + `@dataclass_transform` metadata on mutable metaclasses is correctly + shadowed (verified: each metaclass gets its own `__dataclass_transform__` + in its `__dict__`). + +- **Fix protocol detection for subclasses**: Override protocol detection logic + so that direct subclasses of classcore protocol base classes + (`Protocol`, `ProtocolMutable`, `DataclassProtocol`, + `DataclassProtocolMutable`) are properly recognized as protocols at runtime + (`_is_protocol = True`). Concrete implementation subclasses (subclasses of + a protocol that do not themselves inherit from a protocol base class) + SHALL remain non-protocols, matching standard `typing` semantics. + +- **Guard against double-processing in constructors**: When protocol + metaclasses form a hierarchy, the class factory's constructor wrappers + could execute preprocessors/postprocessors multiple times (once per + metaclass in the MRO). The decorator wrappers SHALL delegate directly + past their constructor/initializer when the defining metaclass differs + from the call context, so processing runs exactly once. Public + `ClassConstructor`/`ClassInitializer` signatures are unchanged. + +- **Snapshot declared protocol members**: `__protocol_attrs__` SHALL be + computed from the pre-decoration namespace (what the author declared), + not from a post-decoration scan that includes framework-injected and + dataclass-generated attributes. User-declared dunders SHALL be + preserved as protocol members. + +## Impact + +- Affected specs: `protocol-support` +- Affected code: + - `sources/classcore/standard/classes.py` — metaclass hierarchy (both + protocol and non-protocol), protocol detection, declared-member + snapshot, `__protocol_attrs__` override + - `sources/classcore/decorators.py` — wrapper-level hierarchy delegation + - New tests for runtime protocol recognition, metaclass hierarchy, and + cross-metaclass inheritance diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/specs/protocol-support/spec.md b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/specs/protocol-support/spec.md new file mode 100644 index 0000000..9163cce --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/specs/protocol-support/spec.md @@ -0,0 +1,65 @@ +## MODIFIED Requirements + +### Requirement: Protocol Metaclass + +The protocol metaclasses MUST combine `typing.Protocol` with standard +behaviors and MUST form a proper inheritance hierarchy. + +#### Scenario: Protocol class creation +- **WHEN** a class uses a protocol metaclass +- **THEN** the resulting class MUST support structural subtyping with + immutability and concealment + +#### Scenario: Metaclass hierarchy +- **WHEN** `ProtocolDataclass` and `ProtocolDataclassMutable` are inspected +- **THEN** `ProtocolDataclass` MUST be a subclass of `ProtocolClass` +- **AND** `ProtocolDataclassMutable` MUST be a subclass of `ProtocolDataclass` + +#### Scenario: Cross-metaclass inheritance +- **WHEN** a class inherits from bases backed by different protocol + metaclasses (e.g., `Protocol` and `DataclassProtocol`) +- **THEN** class creation MUST succeed without `TypeError: metaclass conflict` + +#### Scenario: Dataclass transform preservation +- **WHEN** `ProtocolDataclassMutable` inherits from `ProtocolDataclass` +- **THEN** `ProtocolDataclassMutable.__dataclass_transform__` MUST have + `frozen_default = False` +- **AND** the value MUST be in `ProtocolDataclassMutable.__dict__`, not + inherited from `ProtocolDataclass` + +## ADDED Requirements + +### Requirement: Runtime Protocol Recognition + +Direct subclasses of classcore protocol base classes MUST be recognized as +protocols at runtime. The `_is_protocol` attribute MUST be set to `True` on +classes that directly inherit from a classcore protocol base class +(`Protocol`, `ProtocolMutable`, `DataclassProtocol`, +`DataclassProtocolMutable`). Concrete implementation subclasses (subclasses +of a protocol that do not include a protocol base class in their own +`__bases__`) MUST have `_is_protocol` set to `False`, matching standard +`typing.Protocol` semantics. + +#### Scenario: Direct subclass recognized as protocol +- **WHEN** a class directly inherits from a classcore protocol base class +- **THEN** `cls._is_protocol` MUST be `True` + +#### Scenario: Concrete implementation not recognized as protocol +- **WHEN** a class inherits from a protocol subclass without including a + protocol base class in its own `__bases__` +- **THEN** `cls._is_protocol` MUST be `False` + +#### Scenario: Runtime checkable application +- **WHEN** `typing.runtime_checkable` is applied to a classcore protocol + subclass via the `decorators=` metaclass argument +- **THEN** the application MUST succeed without `TypeError` +- **AND** `cls._is_runtime_protocol` MUST be `True` + +#### Scenario: Runtime structural subtyping +- **WHEN** a classcore protocol is decorated with `@runtime_checkable` +- **AND** a candidate object implements all protocol methods +- **THEN** `isinstance(candidate, protocol)` MUST return `True` + +#### Scenario: Protocol attributes collection +- **WHEN** a classcore protocol subclass is created +- **THEN** `cls.__protocol_attrs__` MUST contain the protocol's attribute names diff --git a/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/tasks.md b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/tasks.md new file mode 100644 index 0000000..1198cbc --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-15-update-protocol-recognition/tasks.md @@ -0,0 +1,94 @@ +## 1. Metaclass Hierarchy + +### 1a. Protocol metaclasses + +- [x] 1.1 Change `ProtocolDataclass` base from `type(typx.Protocol)` to + `ProtocolClass` in `sources/classcore/standard/classes.py` +- [x] 1.2 Change `ProtocolDataclassMutable` base from `type(typx.Protocol)` + to `ProtocolDataclass` in `sources/classcore/standard/classes.py` +- [x] 1.3 Verify `_dynadoc_fragments_` on each metaclass are preserved + (each overrides in its own `__dict__`) +- [x] 1.4 Verify `dataclass_transform` markers are preserved and correctly + shadowed: `ProtocolDataclassMutable` must have `frozen_default = False` + in its own `__dataclass_transform__` + +### 1b. Non-protocol metaclasses + +- [x] 1.5 Change `Dataclass` base from `type` to `Class` in + `sources/classcore/standard/classes.py` +- [x] 1.6 Change `DataclassMutable` base from `type` to `Dataclass` in + `sources/classcore/standard/classes.py` +- [x] 1.7 Verify `_dynadoc_fragments_` and `dataclass_transform` markers + are preserved on non-protocol metaclasses + +## 2. Constructor Double-Processing Guard + +- [x] 2.1 Implement hierarchy delegation in the decorator wrappers + (`produce_class_construction_decorator` and + `produce_class_initialization_decorator` in + `sources/classcore/decorators.py`): on mismatch between the closure + metaclass and the call context (`clscls is not clscls_` for + construction, `clscls is not type(cls)` for initialization), delegate + directly past the constructor/initializer. +- [x] 2.2 Keep `ClassConstructor` and `ClassInitializer` public signatures + unchanged in `sources/classcore/nomina.py` (an `origin` parameter was + implemented first, then withdrawn to avoid a breaking API change). +- [x] 2.3 Test that a class decorator records exactly one invocation for a + class created through a custom metaclass hierarchy + (`test_444_factory_hierarchy_delegation`) +- [x] 2.4 Run existing test suite to verify no regressions from the guard + change + +## 3. Protocol Detection Fix + +- [x] 3.1 Add `_protocol_cls_set` constant (`frozenset({typx.Protocol})`) + to `sources/classcore/standard/classes.py` +- [x] 3.2 Add structural detection in `ProtocolClass.__new__` stub: check + if any base has `_is_protocol = True` in its `__dict__` AND has a + typing Protocol class as a direct base. Set `_is_protocol = True` + before decorators are applied. +- [x] 3.3 Snapshot declared members in `ProtocolClass.__new__` before + decoration, and replace `__protocol_attrs__` with + `declared | inherited` in `ProtocolClass.__init__` (after + `_ProtocolMeta.__init__` computes it). +- [x] 3.4 Add `_is_runtime_protocol` and `__non_callable_proto_members__` + to `_abc_class_mutables` so `runtime_checkable` can set these attributes + without triggering immutability errors. +- [x] 3.5 Verify that `_is_protocol` is `True` on direct subclasses and + `False` on concrete implementation subclasses + +## 4. Tests + +- [x] 4.1 Test protocol metaclass hierarchy: `issubclass(ProtocolDataclass, ProtocolClass)` + and `issubclass(ProtocolDataclassMutable, ProtocolDataclass)` +- [x] 4.2 Test non-protocol metaclass hierarchy: `issubclass(Dataclass, Class)` + and `issubclass(DataclassMutable, Dataclass)` +- [x] 4.3 Test cross-metaclass inheritance: module-level class inheriting + from `Protocol` and `DataclassProtocol`, created without `TypeError`, + with `ProtocolDataclass` selected as metaclass +- [x] 4.4 Test `_is_protocol` is `True` on direct subclasses of protocol + base classes +- [x] 4.5 Test `_is_protocol` is `False` on concrete implementation + subclasses +- [x] 4.6 Test `@runtime_checkable` succeeds when applied via `decorators=` + mechanism +- [x] 4.7 Test `@runtime_checkable` succeeds when applied directly +- [x] 4.8 Test `isinstance()` structural subtyping works with + `@runtime_checkable` classcore protocol +- [x] 4.9 Test `runtime_checkable` and `isinstance` for direct subclasses + of all four protocol base classes +- [x] 4.10 Test `dataclass_transform` preservation (frozen and mutable) +- [x] 4.11 Test user-declared dunders are preserved in + `__protocol_attrs__` +- [x] 4.12 Test dataclass protocol attrs contain declared members only + (module-level class) +- [x] 4.13 Test factory hierarchy delegation applies a decorator exactly + once (custom metaclass hierarchy) +- [x] 4.14 Run full test suite — all tests pass + +## 5. Validation + +- [x] 5.1 Run `hatch --env develop run make-all` — all green, 100% coverage +- [x] 5.2 Add towncrier fragments for user-facing behavior changes +- [x] 5.3 Update `issues/classes/1`, `issues/classes/2`, `issues/classes/3` + in `nb` with resolution status diff --git a/documentation/architecture/openspec/config.yaml b/documentation/architecture/openspec/config.yaml new file mode 100644 index 0000000..f9153b7 --- /dev/null +++ b/documentation/architecture/openspec/config.yaml @@ -0,0 +1,19 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: python +# Project: python-classcore + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/documentation/architecture/openspec/specs/abc-support/spec.md b/documentation/architecture/openspec/specs/abc-support/spec.md new file mode 100644 index 0000000..8871398 --- /dev/null +++ b/documentation/architecture/openspec/specs/abc-support/spec.md @@ -0,0 +1,87 @@ +# abc-support Specification + +## Purpose + +Opt-in abstract base class support: combine standard behaviors with +`abc.ABCMeta` machinery for abstract classes that mix with external +ABC-based classes, and unify the protocol metaclass taxonomy under +`Class`. + +## Requirements + +### Requirement: Abstract Class Metaclass + +The `AbstractClass` metaclass MUST combine the standard behaviors of +`Class` with `abc.ABCMeta` via a diamond inheritance hierarchy. `Class` +itself MUST remain backed by plain `type` so that ordinary classcore +classes carry no ABC machinery. + +#### Scenario: Abstract class creation +- **WHEN** a class uses the `AbstractClass` metaclass +- **THEN** the resulting class MUST support standard behaviors + (immutability, concealment) and ABC machinery (abstract method + enforcement, virtual subclass registration) + +#### Scenario: Metaclass hierarchy +- **WHEN** `AbstractClass` is inspected +- **THEN** it MUST be a subclass of both `Class` and `abc.ABCMeta` + +#### Scenario: Ordinary classes unaffected +- **WHEN** a class uses the `Class` metaclass +- **THEN** it MUST NOT gain ABC machinery + +### Requirement: Abstract Base Class + +`AbstractObject` MUST serve as a base class for abstract classes with +standard behaviors, created with `AbstractClass` and exempting ABC cache +attributes from class immutability. + +#### Scenario: Inheritance +- **WHEN** a class inherits from `AbstractObject` +- **THEN** the class MUST be created with `AbstractClass` as its + metaclass, with standard behaviors active + +#### Scenario: Mixing with external abstract base classes +- **WHEN** a class inherits from both `AbstractObject` and an external + class whose metaclass is `abc.ABCMeta` +- **THEN** class creation MUST succeed without + `TypeError: metaclass conflict` + +#### Scenario: Abstract method enforcement +- **WHEN** a class inherits abstract methods from an external ABC and + does not implement them +- **THEN** instantiation MUST raise `TypeError` +- **AND** a subclass implementing all abstract methods MUST instantiate + +#### Scenario: Virtual subclass registration +- **WHEN** `register()` is called on a class derived from + `AbstractObject` +- **THEN** the registered class MUST satisfy `isinstance()` checks +- **AND** ABC cache mutations MUST NOT raise immutability errors + +### Requirement: Unified Protocol Metaclass + +`ProtocolClass` MUST inherit from both `AbstractClass` and the metaclass +of `typing.Protocol`, unifying the metaclass taxonomy under `Class`. + +#### Scenario: Unified taxonomy +- **WHEN** `ProtocolClass` is inspected +- **THEN** it MUST be a subclass of `Class`, `AbstractClass`, and + `abc.ABCMeta` + +#### Scenario: Protocol functionality preserved +- **WHEN** the `protocol-support` capability scenarios are exercised +- **THEN** runtime protocol detection, `@runtime_checkable`, and + `isinstance()` structural subtyping MUST continue to behave as + specified + +#### Scenario: Single decoration through hierarchy +- **WHEN** a class is created through the unified protocol metaclass + hierarchy with a decorator +- **THEN** the decorator MUST be applied exactly once + +#### Scenario: Concrete classes structurally satisfy protocols +- **WHEN** a concrete classcore class (e.g., backed by `Class` or + `AbstractClass`) implements all members of a `@runtime_checkable` + classcore protocol +- **THEN** `isinstance()` MUST return `True` for that protocol diff --git a/documentation/architecture/openspec/specs/protocol-support/spec.md b/documentation/architecture/openspec/specs/protocol-support/spec.md index a7f3b40..6be0212 100644 --- a/documentation/architecture/openspec/specs/protocol-support/spec.md +++ b/documentation/architecture/openspec/specs/protocol-support/spec.md @@ -5,35 +5,44 @@ The library SHALL provide protocol classes with immutability and concealment that work with structural subtyping, preserving protocol functionality including runtime checks. - ## Requirements - ### Requirement: Protocol Metaclass -The `Protocol` metaclass MUST combine `typing.Protocol` with standard -behaviors. +The protocol metaclasses MUST combine `typing.Protocol` with standard +behaviors and MUST form a proper inheritance hierarchy. #### Scenario: Protocol class creation -- **WHEN** a class uses the `Protocol` metaclass -- **THEN** the resulting class MUST support structural subtyping with immutability and concealment +- **WHEN** a class uses a protocol metaclass +- **THEN** the resulting class MUST support structural subtyping with + immutability and concealment -### Requirement: Protocol Base Class +#### Scenario: Metaclass hierarchy +- **WHEN** `ProtocolDataclass` and `ProtocolDataclassMutable` are inspected +- **THEN** `ProtocolDataclass` MUST be a subclass of `ProtocolClass` +- **AND** `ProtocolDataclassMutable` MUST be a subclass of `ProtocolDataclass` -`ProtocolObject` MUST serve as a base class for protocol definitions with -standard behaviors. +#### Scenario: Cross-metaclass inheritance +- **WHEN** a class inherits from bases backed by different protocol + metaclasses (e.g., `Protocol` and `DataclassProtocol`) +- **THEN** class creation MUST succeed without `TypeError: metaclass conflict` -#### Scenario: Protocol inheritance -- **WHEN** a class inherits from `ProtocolObject` -- **THEN** the resulting class MUST be a protocol with immutability and concealment +#### Scenario: Dataclass transform preservation +- **WHEN** `ProtocolDataclassMutable` inherits from `ProtocolDataclass` +- **THEN** `ProtocolDataclassMutable.__dataclass_transform__` MUST have + `frozen_default = False` +- **AND** the value MUST be in `ProtocolDataclassMutable.__dict__`, not + inherited from `ProtocolDataclass` -### Requirement: Protocol Decorator +### Requirement: Protocol Base Classes -The `protocol_with_standard_behaviors` decorator MUST create protocol classes -with standard behaviors. +The protocol base classes MUST serve as bases for protocol definitions +with standard behaviors. The base classes are `Protocol`, +`ProtocolMutable`, `DataclassProtocol`, and `DataclassProtocolMutable`. -#### Scenario: Decorated protocol -- **WHEN** `@protocol_with_standard_behaviors()` is applied to a class -- **THEN** the resulting class MUST be a protocol with immutability and concealment +#### Scenario: Protocol inheritance +- **WHEN** a class inherits from a protocol base class +- **THEN** the resulting class MUST be a protocol with immutability and + concealment ### Requirement: Structural Subtyping Preservation @@ -65,3 +74,38 @@ protocol interfaces. #### Scenario: Implementation inherits behaviors - **WHEN** a class implements a protocol with standard behaviors - **THEN** the implementation MUST inherit immutability and concealment + +### Requirement: Runtime Protocol Recognition + +Direct subclasses of classcore protocol base classes MUST be recognized as +protocols at runtime. The `_is_protocol` attribute MUST be set to `True` on +classes that directly inherit from a classcore protocol base class +(`Protocol`, `ProtocolMutable`, `DataclassProtocol`, +`DataclassProtocolMutable`). Concrete implementation subclasses (subclasses +of a protocol that do not include a protocol base class in their own +`__bases__`) MUST have `_is_protocol` set to `False`, matching standard +`typing.Protocol` semantics. + +#### Scenario: Direct subclass recognized as protocol +- **WHEN** a class directly inherits from a classcore protocol base class +- **THEN** `cls._is_protocol` MUST be `True` + +#### Scenario: Concrete implementation not recognized as protocol +- **WHEN** a class inherits from a protocol subclass without including a + protocol base class in its own `__bases__` +- **THEN** `cls._is_protocol` MUST be `False` + +#### Scenario: Runtime checkable application +- **WHEN** `typing.runtime_checkable` is applied to a classcore protocol + subclass via the `decorators=` metaclass argument +- **THEN** the application MUST succeed without `TypeError` +- **AND** `cls._is_runtime_protocol` MUST be `True` + +#### Scenario: Runtime structural subtyping +- **WHEN** a classcore protocol is decorated with `@runtime_checkable` +- **AND** a candidate object implements all protocol methods +- **THEN** `isinstance(candidate, protocol)` MUST return `True` + +#### Scenario: Protocol attributes collection +- **WHEN** a classcore protocol subclass is created +- **THEN** `cls.__protocol_attrs__` MUST contain the protocol's attribute names diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 3dc5d26..bbaff1f 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,44 @@ Release Notes .. towncrier release notes start +classcore 1.12 (2026-08-15) +=========================== + +Enhancements +------------ + +- Add abstract base class support: ``AbstractClass`` metaclass and + ``AbstractObject`` base class combine standard behaviors with + ``abc.ABCMeta`` machinery (abstract method enforcement, virtual subclass + registration), and mix with external ABC-based classes without + metaclass conflicts. Re-parent ``ProtocolClass`` under ``AbstractClass``, + unifying the standard metaclass taxonomy under ``Class``. +- Improve API documentation for class construction and initialization + signatures, including parameter descriptions and the metaclass + delegation contract. + + +Repairs +------- + +- Establish proper inheritance hierarchies for standard metaclasses. + ``Dataclass`` now inherits from ``Class``, ``DataclassMutable`` from + ``Dataclass``, ``ProtocolDataclass`` from ``ProtocolClass``, and + ``ProtocolDataclassMutable`` from ``ProtocolDataclass``. This eliminates + metaclass conflicts when inheriting from classes backed by different + metaclasses in the same family. +- Fix ``typing.runtime_checkable`` support for classcore protocol classes. + ``@runtime_checkable`` can now be applied via the ``decorators=`` + metaclass argument or directly after class creation. + ``isinstance()`` structural subtyping works correctly with + ``@runtime_checkable`` classcore protocols. +- Fix runtime protocol recognition for subclasses of classcore protocol + base classes. Previously, ``typing_extensions.Protocol.__init_subclass__`` + set ``_is_protocol = False`` on these subclasses due to an identity check + that did not recognize classcore's own protocol base classes. Subclasses + are now properly detected via structural analysis. + + classcore 1.11 (2026-07-19) =========================== diff --git a/documentation/conf.py b/documentation/conf.py index 2b15bda..254e578 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -57,9 +57,7 @@ def _import_version( ): templates_path = [ '_templates' ] exclude_patterns = [ - # Openspec workflow/meta files (not documentation) - 'architecture/openspec/AGENTS.md', - 'architecture/openspec/changes/**', + 'architecture/**', ] rst_prolog = f''' diff --git a/documentation/contribution.rst b/documentation/contribution.rst index 92581e9..da5f041 100644 --- a/documentation/contribution.rst +++ b/documentation/contribution.rst @@ -46,17 +46,20 @@ Ways to Contribute Development =============================================================================== -Architecture +Requirements and Design ------------------------------------------------------------------------------- -* The :doc:`capability specifications <specifications/index>` provide a good - starting point to understand the requirements and motivations for the project. - These should be reviewed and updated through the Openspec workflow when making - changes that affect product functionality or user experience. +* Review accepted capability requirements in ``openspec/specs/``. Manage new + capabilities, breaking or cross-cutting changes, architectural shifts, and + substantial performance or security work through OpenSpec changes in + ``openspec/changes/``. Follow the project's OpenSpec workflow instructions + and keep accepted specifications synchronized when changes are completed. -* The architecture documentation in `sources/classcore/README.md` should be - reviewed to understand the structure and operational patterns of the project. - Major changes to the architecture should be reflected in this document. +* Document stable subsystem architecture, design rationale, and local + constraints in the nearest source-near README, normally + ``sources/**/README.md``. Update the affected README when implementation + structure or operational patterns change rather than recreating centralized + architecture or specification mirrors under ``documentation/``. Guidance and Standards ------------------------------------------------------------------------------- @@ -94,7 +97,7 @@ Guidance and Standards <https://emcd.github.io/python-project-common/stable/sphinx-html/common/releases.html>`_ as appropriate. -* Although unncessary for non-maintainer contributions, additional background +* Although unnecessary for non-maintainer contributions, additional background can be found in the `maintenance guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/maintenance.html>`_. @@ -119,7 +122,6 @@ Resources :maxdepth: 2 Code of Conduct <https://emcd.github.io/python-project-common/stable/sphinx-html/common/conduct.html> - specifications/index devapi Environment Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/environment.html> Practices Guide <https://emcd.github.io/python-project-common/stable/sphinx-html/common/practices.html> diff --git a/documentation/specifications/index.rst b/documentation/specifications/index.rst deleted file mode 100644 index c55e440..0000000 --- a/documentation/specifications/index.rst +++ /dev/null @@ -1,30 +0,0 @@ -.. vim: set fileencoding=utf-8: -.. -*- coding: utf-8 -*- -.. +--------------------------------------------------------------------------+ - | | - | Licensed under the Apache License, Version 2.0 (the "License"); | - | you may not use this file except in compliance with the License. | - | You may obtain a copy of the License at | - | | - | http://www.apache.org/licenses/LICENSE-2.0 | - | | - | Unless required by applicable law or agreed to in writing, software | - | distributed under the License is distributed on an "AS IS" BASIS, | - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | - | implied. See the License for the specific language governing | - | permissions and limitations under the License. | - | | - +--------------------------------------------------------------------------+ - -******************************************************************************* -Specifications -******************************************************************************* - -This section contains capability specifications managed through the Openspec workflow. -Each specification documents requirements using scenario-based format (WHEN/THEN). - -.. toctree:: - :maxdepth: 2 - :glob: - - ../architecture/openspec/specs/*/spec diff --git a/pyproject.toml b/pyproject.toml index 5a50289..c96b148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -255,6 +255,7 @@ cache_dir = '.auxiliary/caches/pytest' markers = [ "slow: long-running tests", ] +asyncio_default_fixture_loop_scope = "function" [tool.ruff] #builtins = [ 'ic' ] diff --git a/sources/classcore/README.md b/sources/classcore/README.md index 8b56870..de4bb2a 100644 --- a/sources/classcore/README.md +++ b/sources/classcore/README.md @@ -83,19 +83,20 @@ Pre-configured implementations for common use cases: **Base Classes:** - `Object`: Standard class with immutability and concealment - `DataclassObject`: Automatic dataclass conversion with standard behaviors -- `ProtocolObject`: Protocol class with immutability and concealment +- `Protocol`: Protocol base class with immutability and concealment +- `AbstractObject`: Abstract class with ABC machinery and behaviors - Variants for selective mutability (`ObjectMutable`, etc.) **Metaclasses:** - `Class`: Metaclass for standard classes - `Dataclass`: Metaclass that auto-applies dataclass decorator -- `Protocol`: Metaclass for protocol classes +- `AbstractClass`: Metaclass combining `Class` with `abc.ABCMeta` +- `ProtocolClass`: Metaclass for protocol classes - Mutable variants of each **Decorators:** -- `class_with_standard_behaviors`: Add behaviors to existing classes +- `with_standard_behaviors`: Add behaviors to existing classes - `dataclass_with_standard_behaviors`: Dataclass creation with behaviors -- `protocol_with_standard_behaviors`: Protocol creation with behaviors **Module Utilities:** - `reclassify_module`: Apply immutability and concealment to module objects @@ -381,16 +382,20 @@ sources/ : Standard metaclasses and base classes: - `Class` / `Object`: Standard class with full behaviors + - `AbstractClass` / `AbstractObject`: Abstract class with ABC machinery + (abstract method enforcement, virtual subclass registration); mixes + with external `abc.ABCMeta`-based classes - `Dataclass` / `DataclassObject`: Auto-dataclass with behaviors - - `Protocol` / `ProtocolObject`: Protocol with behaviors + - `ProtocolClass` / `Protocol`, `ProtocolMutable`, + `DataclassProtocol`, `DataclassProtocolMutable`: Protocol classes + with behaviors - Mutable variants for each `decorators.py` : Standard class decorators: - - `class_with_standard_behaviors`: Apply behaviors to existing class + - `with_standard_behaviors`: Apply behaviors to existing class - `dataclass_with_standard_behaviors`: Create dataclass with behaviors - - `protocol_with_standard_behaviors`: Create protocol with behaviors - `class_factory`: Generic factory for creating custom metaclasses `dynadoc.py` diff --git a/sources/classcore/__/imports.py b/sources/classcore/__/imports.py index 2dff9ac..52332af 100644 --- a/sources/classcore/__/imports.py +++ b/sources/classcore/__/imports.py @@ -23,6 +23,7 @@ # ruff: noqa: F401 +import abc import collections.abc as cabc import dataclasses as dcls import functools as funct diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 2c45b5b..e970577 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.11' +__version__ = '1.12' standard.finalize_module( diff --git a/sources/classcore/decorators.py b/sources/classcore/decorators.py index dd2f310..a61bf8c 100644 --- a/sources/classcore/decorators.py +++ b/sources/classcore/decorators.py @@ -89,8 +89,14 @@ def construct_with_super( decorators: _nomina.Decorators[ __.T ] = ( ), **arguments: __.typx.Any, ) -> type[ object ]: - superf = super( clscls, clscls_ ).__new__ - # TODO? Short-circuit if not at start of MRO. + superf = __.typx.cast( + _nomina.ClassConstructorLigation, + super( clscls, clscls_ ).__new__ ) + if clscls is not clscls_: + # Parent metaclass within a hierarchy: delegate without + # duplicating preprocessing or decoration. + return superf( + clscls_, name, bases, namespace, **arguments ) return constructor( clscls_, superf, name, bases, namespace, arguments, decorators ) @@ -107,7 +113,11 @@ def construct_with_original( decorators: _nomina.Decorators[ __.T ] = ( ), **arguments: __.typx.Any, ) -> type[ object ]: - # TODO? Short-circuit if not at start of MRO. + if clscls is not clscls_: + # Parent metaclass within a hierarchy: delegate without + # duplicating preprocessing or decoration. + return original( + clscls_, name, bases, namespace, **arguments ) return constructor( clscls_, original, name, bases, namespace, arguments, decorators ) @@ -138,7 +148,11 @@ def initialize_with_super( cls: type, *posargs: __.typx.Any, **nomargs: __.typx.Any ) -> None: ligation = super( clscls, cls ).__init__ - # TODO? Short-circuit if not at start of MRO. + if clscls is not type( cls ): + # Parent metaclass within a hierarchy: delegate without + # duplicating completion logic. + ligation( *posargs, **nomargs ) + return initializer( cls, ligation, posargs, nomargs ) clscls.__init__ = initialize_with_super @@ -149,8 +163,12 @@ def initialize_with_super( def initialize_with_original( cls: type, *posargs: __.typx.Any, **nomargs: __.typx.Any ) -> None: + if clscls is not type( cls ): + # Parent metaclass within a hierarchy: delegate without + # duplicating completion logic. + original( cls, *posargs, **nomargs ) + return ligation = __.funct.partial( original, cls ) - # TODO? Short-circuit if not at start of MRO. initializer( cls, ligation, posargs, nomargs ) clscls.__init__ = initialize_with_original diff --git a/sources/classcore/factories.py b/sources/classcore/factories.py index 7ebbdd6..42b00b9 100644 --- a/sources/classcore/factories.py +++ b/sources/classcore/factories.py @@ -34,7 +34,7 @@ def produce_class_constructor( ) -> _nomina.ClassConstructor[ __.T ]: ''' Produces constructors for classes. ''' - def construct( # noqa: PLR0913 + def construct( # noqa: PLR0913, PLR0917 clscls: type[ __.T ], superf: _nomina.ClassConstructorLigation, name: str, diff --git a/sources/classcore/nomina.py b/sources/classcore/nomina.py index de8280a..7a05905 100644 --- a/sources/classcore/nomina.py +++ b/sources/classcore/nomina.py @@ -214,29 +214,52 @@ ClassConstructor: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ - type, - ClassConstructorLigation, - str, - tuple[ type, ... ], - dict[ str, __.typx.Any ], - __.NominativeArguments, - Decorators[ __.U ], + type, # metaclass + ClassConstructorLigation, # next constructor in metaclass MRO + str, # class name + tuple[ type, ... ], # bases + dict[ str, __.typx.Any ], # namespace + __.NominativeArguments, # metaclass arguments + Decorators[ __.U ], # decorators to apply after ], type ], - __.ddoc.Doc( ''' Constructor to use with metaclass. ''' ), + __.ddoc.Doc( + ''' Constructs class, then applies hooks. + + Receives the metaclass ('clscls'), the next constructor in + the metaclass MRO ('superf'), the class name, bases, + namespace, metaclass arguments, and decorators to apply + after construction. Calls 'superf' to create the class, + then applies postprocessors and decorators. + + Invoked only at the most-derived metaclass of a class. + Class factory wrappers on parent metaclasses delegate + directly, so construction hooks execute exactly once per + class. + ''' ), ] ClassInitializer: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ - type, - InitializerLigation, - __.PositionalArguments, - __.NominativeArguments, + type, # class + InitializerLigation, # next initializer in metaclass MRO + __.PositionalArguments, # positional arguments + __.NominativeArguments, # nominative arguments ], None ], - __.ddoc.Doc( ''' Initializer to use with metaclass. ''' ), + __.ddoc.Doc( + ''' Initializes class, then applies hooks. + + Receives the class ('cls'), the next initializer in the + metaclass MRO ('superf'), positional arguments, and + nominative arguments. Calls 'superf' first, then applies + completers, which finalize class behaviors (e.g., enable + immutability once all other initialization has occurred). + + Invoked only at the most-derived metaclass of a class. + ''' ), ] diff --git a/sources/classcore/standard/README.md b/sources/classcore/standard/README.md index 8c27678..17f245d 100644 --- a/sources/classcore/standard/README.md +++ b/sources/classcore/standard/README.md @@ -40,17 +40,27 @@ class Point2d(DataclassObject): self.hypotenuse = sqrt(x*x + y*y) ``` -### `ProtocolObject` +### `Protocol` + +Protocol base class with immutability and concealment. Combines +`typing.Protocol` with standard behaviors. Immutability and concealment +are inherited by subclassed implementations. Dataclass protocol bases +(`DataclassProtocol`, `DataclassProtocolMutable`) additionally provide +automatic dataclass conversion. -Protocol class with immutability and concealment. Combines `typing.Protocol` -with standard behaviors. Immutability and concealment are inherited by -subclassed implementations. +### `AbstractObject` + +Abstract base class with immutability and concealment. Combines standard +behaviors with `abc.ABCMeta` machinery: abstract method enforcement and +virtual subclass registration. Mixes with external ABC-based classes +without metaclass conflicts. ### Mutable Variants - `ObjectMutable`: Object with concealment only (no immutability) - `DataclassObjectMutable`: DataclassObject with concealment only -- `ProtocolObjectMutable`: ProtocolObject with concealment only +- `ProtocolMutable`: Protocol with concealment only +- `DataclassProtocolMutable`: DataclassProtocol with concealment only ## Metaclasses @@ -64,27 +74,35 @@ any class that uses this metaclass. Metaclass that automatically applies `dataclasses.dataclass` during class construction. Configurable for `frozen`, `slots`, etc. -### `Protocol` +### `AbstractClass` + +Metaclass combining `Class` with `abc.ABCMeta`. Supports abstract method +enforcement and virtual subclass registration for classes that must mix +with external ABC-based classes. + +### `ProtocolClass` Metaclass for protocol classes. Combines `typing.Protocol` with standard -behaviors. +behaviors. Descends from `AbstractClass`, unifying the standard metaclass +taxonomy under `Class`. Dataclass protocol metaclasses +(`ProtocolDataclass`, `ProtocolDataclassMutable`) additionally apply +dataclass conversion. ### Mutable Variants -- `ClassMutable`: Metaclass with concealment only -- `DataclassMutable`: Dataclass metaclass with concealment only -- `ProtocolMutable`: Protocol metaclass with concealment only +- `DataclassMutable`: Dataclass metaclass whose instances have mutable + attributes (concealment only) ## Decorators -### `class_with_standard_behaviors` +### `with_standard_behaviors` Apply standard behaviors to an existing class. ```python -from classcore.standard import class_with_standard_behaviors +from classcore.standard import with_standard_behaviors -@class_with_standard_behaviors() +@with_standard_behaviors() class ExistingClass: x: int = 10 ``` @@ -102,10 +120,6 @@ class Point: y: float ``` -### `protocol_with_standard_behaviors` - -Create a protocol with standard behaviors applied. - ### `class_factory` Generic factory for creating custom metaclasses with standard behaviors. diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 3d9fb30..b563b3d 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -224,7 +224,7 @@ def produce_class_construction_preprocessor( ) -> _nomina.ClassConstructionPreprocessor[ __.U ]: ''' Produces construction processor which handles metaclass arguments. ''' - def preprocess( # noqa: PLR0913 + def preprocess( # noqa: PLR0913, PLR0917 clscls: type, name: str, bases: list[ type ], diff --git a/sources/classcore/standard/classes.py b/sources/classcore/standard/classes.py index 9bbeaf9..219446e 100644 --- a/sources/classcore/standard/classes.py +++ b/sources/classcore/standard/classes.py @@ -32,7 +32,56 @@ '_abc_negative_cache', '_abc_negative_cache_version', '_abc_registry', + '_is_runtime_protocol', + '__non_callable_proto_members__', ) +_protocol_cls_set = frozenset( { __.typx.Protocol } ) +# Attributes never considered declared protocol members: typing internals, +# class-creation machinery, and framework attributes declared in classcore +# base class bodies. +_protocol_attr_excluded = frozenset( { + '_is_protocol', '_is_runtime_protocol', + '__protocol_attrs__', '__subclasshook__', + '__non_callable_proto_members__', + '_dynadoc_fragments_', + # Injected during class creation by ABC, Generic, and typing machinery. + # '__init__' is '_no_init' on protocol bases; as a structural member it + # is vacuous since every candidate object has an '__init__'. + '__abstractmethods__', '__parameters__', '__init__', '__orig_bases__', +} ) +_protocol_attr_prefixes = ( '_classcore_', '_abc_' ) +# Standard Python attributes present in every pre-decoration namespace. +_python_class_defaults = frozenset( { + '__module__', '__qualname__', '__doc__', '__dict__', '__weakref__', + '__slots__', '__annotations__', + '__firstlineno__', '__static_attributes__', # Python 3.13+ +} ) + + +def _snapshot_declared_protocol_attrs( cls: type ) -> None: + ''' Records protocol members declared before framework decoration. + + Called in metaclass '__new__' immediately after class creation, + before behavior decorators and dataclass machinery inject their + attributes. The pre-decoration namespace contains exactly the + members declared by the author, so a user-declared dunder (e.g., + '__repr__') is preserved while framework-generated ones are absent. + ''' + # Dataclass slot machinery reproduces classes, copying '__dict__'. + # Retain the original snapshot; the reproduction's namespace already + # contains decorated attributes. + if '_classcore_protocol_declared_' in cls.__dict__: return + declared = ( + set( cls.__dict__ ) + | set( __.inspect.get_annotations( cls ) ) ) + declared -= _python_class_defaults + declared = { + attr for attr in declared + if attr not in _protocol_attr_excluded + and not any( attr.startswith( p ) for p in _protocol_attr_prefixes ) + } + setattr( cls, '_classcore_protocol_declared_', frozenset( declared ) ) + _dynadoc_configuration = ( _dynadoc.produce_dynadoc_configuration( table = __.fragments ) ) _class_factory = __.funct.partial( @@ -72,9 +121,35 @@ def __new__( # Typechecker stub. return super( ).__new__( clscls, name, bases, namespace ) +@_class_factory( ) +class AbstractClass( Class, __.abc.ABCMeta ): + ''' Metaclass for abstract classes with standard behaviors. + + Combines the standard behaviors of `Class` with the machinery of + `abc.ABCMeta` (abstract method enforcement, virtual subclass + registration) via a diamond hierarchy. `Class` itself remains + backed by plain `type`, so ABC machinery applies only where this + metaclass is used. + ''' + + _dynadoc_fragments_ = ( + 'cfc class conceal', 'cfc class protect', 'cfc dynadoc', + 'cfc instance conceal', 'cfc instance protect' ) + + def __new__( # Typechecker stub. + clscls: type[ __.T ], + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], *, + decorators: _nomina.Decorators[ __.T ] = ( ), + **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], + ) -> __.T: + return super( ).__new__( clscls, name, bases, namespace ) + + @_class_factory( ) @__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) -class Dataclass( type ): +class Dataclass( Class ): ''' Metaclass for standard dataclasses. ''' _dynadoc_fragments_ = ( @@ -95,7 +170,7 @@ def __new__( # Typechecker stub. @_class_factory( ) @__.typx.dataclass_transform( kw_only_default = True ) -class DataclassMutable( type ): +class DataclassMutable( Dataclass ): ''' Metaclass for dataclasses with mutable instance attributes. ''' _dynadoc_fragments_ = ( @@ -115,7 +190,7 @@ def __new__( # Typechecker stub. @_class_factory( ) -class ProtocolClass( type( __.typx.Protocol ) ): +class ProtocolClass( AbstractClass, type( __.typx.Protocol ) ): ''' Metaclass for standard protocol classes. ''' _dynadoc_fragments_ = ( @@ -131,12 +206,55 @@ def __new__( # Typechecker stub. decorators: _nomina.Decorators[ __.T ] = ( ), **arguments: __.typx.Unpack[ ClassFactoryExtraArguments ], ) -> __.T: - return super( ).__new__( clscls, name, bases, namespace ) + cls = super( ).__new__( clscls, name, bases, namespace ) + # typing_extensions.Protocol.__init_subclass__ uses identity comparison + # (b is Protocol) to set _is_protocol on subclasses. Classcore protocol + # base classes are not typing.Protocol, so subclasses incorrectly get + # _is_protocol = False. Detect protocol base classes structurally and + # fix _is_protocol before decorators are applied + # (e.g., runtime_checkable). + if not cls.__dict__.get( '_is_protocol', False ): + for base in cls.__bases__: + if ( base.__dict__.get( '_is_protocol', False ) + and bool( _protocol_cls_set & set( base.__bases__ ) ) ): + setattr( cls, '_is_protocol', True ) + break + if cls.__dict__.get( '_is_protocol', False ): + _snapshot_declared_protocol_attrs( cls ) + return cls + + def __init__( + cls, + name: str, + bases: tuple[ type, ... ], + namespace: dict[ str, __.typx.Any ], + **kwargs: __.typx.Any, + ) -> None: + super( ).__init__( name, bases, namespace, **kwargs ) + # Replace _ProtocolMeta's scan of the decorated namespace with the + # pre-decoration snapshot, so isinstance() only checks declared + # protocol members. Snapshot persists because metaclass __init__ can + # run more than once when dataclass machinery reproduces classes. + if getattr( cls, '_is_protocol', False ): + declared: frozenset[ str ] = cls.__dict__.get( + '_classcore_protocol_declared_', frozenset( ) ) + inherited: set[ str ] = set( ) + for base in cls.__mro__[ 1: ]: + if not isinstance( base, ProtocolClass ): continue + inherited.update( base.__dict__.get( + '__protocol_attrs__', ( ) ) ) + attrs = { + attr for attr in declared | inherited + if attr not in _protocol_attr_excluded + and not any( + attr.startswith( p ) for p in _protocol_attr_prefixes ) + } + setattr( cls, '__protocol_attrs__', attrs ) @_class_factory( ) @__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) -class ProtocolDataclass( type( __.typx.Protocol ) ): +class ProtocolDataclass( ProtocolClass ): ''' Metaclass for standard protocol dataclasses. ''' _dynadoc_fragments_ = ( @@ -157,7 +275,7 @@ def __new__( # Typechecker stub. @_class_factory( ) @__.typx.dataclass_transform( kw_only_default = True ) -class ProtocolDataclassMutable( type( __.typx.Protocol ) ): +class ProtocolDataclassMutable( ProtocolDataclass ): ''' Metaclass for protocol dataclasses with mutable instance attributes. ''' @@ -193,6 +311,22 @@ class ObjectMutable( metaclass = Class, instances_mutables = '*' ): 'class instance conceal' ) +class AbstractObject( + metaclass = AbstractClass, + class_mutables = _abc_class_mutables, +): + ''' Base class for abstract classes with standard behaviors. + + Supports abstract method enforcement and virtual subclass + registration, and mixes with external classes whose metaclass is + `abc.ABCMeta`. + ''' + + _dynadoc_fragments_ = ( + 'class concealment', 'class protection', 'class dynadoc', + 'class instance conceal', 'class instance protect' ) + + class DataclassObject( metaclass = Dataclass ): ''' Standard base dataclass. ''' @@ -262,3 +396,41 @@ class DataclassProtocolMutable( 'dataclass', 'protocol class', 'class concealment', 'class protection', 'class dynadoc', 'class instance conceal' ) + + +# =========================================================================== # +# Type checker canaries. Private declarations, excluded from the public API +# and the test suite. They exist so that Pyright (and other type checkers) +# evaluates the structures declared above; regressions in checker support +# surface as diagnostics on these declarations during routine linting. +# =========================================================================== # + + +class _CanaryAbstractObject( AbstractObject ): + ''' Canary for abstract base classes. ''' + + def provide( self ) -> int: + return 42 + + +class _CanaryProtocolUse( Protocol ): + ''' Canary for protocol definitions. ''' + + value: int + + def greet( self ) -> str: ... + + +class _CanaryProtocolImpl( _CanaryProtocolUse ): + ''' Canary for concrete protocol implementations. ''' + + def __init__( self ) -> None: + self.value = 7 + + def greet( self ) -> str: + return 'canary' + + +_canary_abstract: int = _CanaryAbstractObject( ).provide( ) +_canary_protocol_iface: _CanaryProtocolUse = _CanaryProtocolImpl( ) +_canary_greeting: str = _canary_protocol_iface.greet( ) diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index 41b05e4..f515138 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -175,7 +175,7 @@ def apply_cfc_attributes_surveyor( decorator( clscls ) -def class_factory( # noqa: PLR0913 +def class_factory( # noqa: PLR0913, PLR0917 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, assigner_core: __.typx.Optional[ _nomina.AssignerCore ] = None, @@ -220,7 +220,7 @@ def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: return decorate -def produce_instances_inception_decorator( # noqa: PLR0913 +def produce_instances_inception_decorator( # noqa: PLR0913, PLR0917 attributes_namer: _nomina.AttributesNamer, assigner_core: __.typx.Optional[ _nomina.AssignerCore ], deleter_core: __.typx.Optional[ _nomina.DeleterCore ], @@ -532,7 +532,7 @@ def survey_with_original( @__.typx.dataclass_transform( frozen_default = True, kw_only_default = True ) -def dataclass_with_standard_behaviors( # noqa: PLR0913 +def dataclass_with_standard_behaviors( # noqa: PLR0913, PLR0917 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, decorators: _nomina.Decorators[ __.U ] = ( ), @@ -564,7 +564,7 @@ def dataclass_with_standard_behaviors( # noqa: PLR0913 *decorators, _dataclass_core, *decorators_, preparers = preparers ) -def with_standard_behaviors( # noqa: PLR0913 +def with_standard_behaviors( # noqa: PLR0913, PLR0917 attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, error_class_provider: _nomina.ErrorClassProvider = __.provide_error_class, decorators: _nomina.Decorators[ __.U ] = ( ), @@ -621,7 +621,7 @@ def _produce_instances_decoration_preparers( return tuple( preprocessors ) -def _produce_instances_decorators( # noqa: PLR0913 +def _produce_instances_decorators( # noqa: PLR0913, PLR0917 attributes_namer: _nomina.AttributesNamer, error_class_provider: _nomina.ErrorClassProvider, assigner_core: __.typx.Optional[ _nomina.AssignerCore ], diff --git a/tests/test_000_classcore/test_330_standard_decorators.py b/tests/test_000_classcore/test_330_standard_decorators.py index d4edecb..c1378e9 100644 --- a/tests/test_000_classcore/test_330_standard_decorators.py +++ b/tests/test_000_classcore/test_330_standard_decorators.py @@ -26,7 +26,7 @@ MODULE_QNAME = f"{PACKAGE_NAME}.standard.decorators" -def simple_assigner_core( +def simple_assigner_core( # noqa: PLR0917 objct, ligation, attributes_namer, @@ -36,7 +36,7 @@ def simple_assigner_core( object.__setattr__( objct, name, value ) -def simple_deleter_core( +def simple_deleter_core( # noqa: PLR0917 objct, ligation, attributes_namer, @@ -50,7 +50,7 @@ def simple_surveyor_core( objct, ligation, attributes_namer, level ): return object.__dir__( objct ) -def class_simple_assigner_core( +def class_simple_assigner_core( # noqa: PLR0917 cls, ligation, attributes_namer, @@ -60,7 +60,7 @@ def class_simple_assigner_core( type.__setattr__( cls, name, value ) -def class_simple_deleter_core( +def class_simple_deleter_core( # noqa: PLR0917 cls, ligation, attributes_namer, diff --git a/tests/test_000_classcore/test_345_standard_protocols.py b/tests/test_000_classcore/test_345_standard_protocols.py new file mode 100644 index 0000000..607a975 --- /dev/null +++ b/tests/test_000_classcore/test_345_standard_protocols.py @@ -0,0 +1,305 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Tests for protocol classes and metaclass hierarchy. ''' + + +import typing + +from .__ import PACKAGE_NAME, cache_import_module + +MODULE_QNAME = f"{PACKAGE_NAME}.standard.classes" + +# Module-level cross-metaclass class: inherits from bases backed by +# ProtocolClass and ProtocolDataclass. Defined at module level to avoid +# the qualname/in_progress mangling interaction with dataclass _add_slots +# replacement that affects function-local classes. +_ccstd = cache_import_module( MODULE_QNAME ) + + +class _CrossMetaclassProto( _ccstd.Protocol, _ccstd.DataclassProtocol ): + ''' Exercises cross-metaclass inheritance. ''' + + def greet( self ) -> str: ... + + +class _ModuleDataclassProto( _ccstd.DataclassProtocol ): + ''' Module-level dataclass protocol (avoids function-local qualname). ''' + + value: int + + def greet( self ) -> str: ... + + +# =========================================================================== # +# Metaclass Hierarchy +# =========================================================================== # + + +def test_400_protocol_metaclass_hierarchy( ): + ''' Protocol metaclasses form a proper inheritance hierarchy. ''' + module = cache_import_module( MODULE_QNAME ) + assert issubclass( module.ProtocolDataclass, module.ProtocolClass ) + assert issubclass( module.ProtocolDataclassMutable, module.ProtocolClass ) + assert issubclass( + module.ProtocolDataclassMutable, module.ProtocolDataclass ) + + +def test_401_nonprotocol_metaclass_hierarchy( ): + ''' Non-protocol metaclasses form a proper inheritance hierarchy. ''' + module = cache_import_module( MODULE_QNAME ) + assert issubclass( module.Dataclass, module.Class ) + assert issubclass( module.DataclassMutable, module.Dataclass ) + + +def test_410_cross_metaclass_no_conflict( ): + ''' Metaclass hierarchy resolves conflicts between protocol metaclasses. + A class inheriting from bases backed by ProtocolClass and + ProtocolDataclass is created successfully, with ProtocolDataclass + (most derived) as its metaclass. + ''' + module = cache_import_module( MODULE_QNAME ) + assert issubclass( module.ProtocolDataclass, module.ProtocolClass ) + # The module-level class below exercises actual cross-metaclass + # inheritance without TypeError. + assert _CrossMetaclassProto._is_protocol + assert type( _CrossMetaclassProto ) is module.ProtocolDataclass + + +# =========================================================================== # +# Protocol Detection +# =========================================================================== # + + +def test_420_protocol_subclass_is_protocol( ): + ''' Direct subclasses of protocol base classes are recognized as + protocols at runtime. ''' + module = cache_import_module( MODULE_QNAME ) + + class DirectProto( module.Protocol ): + def method( self ) -> int: ... + + assert DirectProto.__dict__.get( '_is_protocol' ) is True + + class DirectMutableProto( module.ProtocolMutable ): + def method( self ) -> int: ... + + assert DirectMutableProto.__dict__.get( '_is_protocol' ) is True + + +# NOTE: DataclassProtocol subclasses are tested separately because the +# dataclass machinery's _add_slots creates a replacement class whose +# __qualname__ differs from the original when defined inside a function, +# which interacts with the in_progress mangling. This is a pre-existing +# issue unrelated to the protocol metaclass hierarchy fix. + + + +def test_421_concrete_implementation_not_protocol( ): + ''' Concrete implementation subclasses are not protocols. ''' + module = cache_import_module( MODULE_QNAME ) + + class ProtoIface( module.Protocol ): + def method( self ) -> int: ... + + class ConcreteImpl( ProtoIface ): + def method( self ) -> int: + return 42 + + assert ConcreteImpl.__dict__.get( '_is_protocol' ) is None or \ + ConcreteImpl.__dict__.get( '_is_protocol' ) is False + + +# =========================================================================== # +# Runtime Checkable Protocols +# =========================================================================== # + + +def test_430_runtime_checkable_via_decorators( ): + ''' @runtime_checkable works when applied via decorators= argument. ''' + module = cache_import_module( MODULE_QNAME ) + + class CheckableProto( + module.Protocol, + decorators = ( typing.runtime_checkable, ), + ): + def greet( self ) -> str: ... + + assert CheckableProto._is_runtime_protocol + + +def test_431_runtime_checkable_direct_application( ): + ''' @runtime_checkable works when applied directly after creation. ''' + module = cache_import_module( MODULE_QNAME ) + + class DirectProto( module.Protocol ): + def greet( self ) -> str: ... + + typing.runtime_checkable( DirectProto ) + assert DirectProto._is_runtime_protocol + + +def test_432_runtime_checkable_isinstance( ): + ''' isinstance() structural subtyping works with runtime_checkable. ''' + module = cache_import_module( MODULE_QNAME ) + + class GreeterProto( + module.Protocol, + decorators = ( typing.runtime_checkable, ), + ): + def greet( self ) -> str: ... + + class GreeterImpl: + def greet( self ) -> str: + return "hello" + + assert isinstance( GreeterImpl( ), GreeterProto ) + + +# =========================================================================== # +# Protocol Attributes +# =========================================================================== # + + +def test_440_protocol_attrs_exclude_internals( ): + ''' __protocol_attrs__ does not contain classcore internal attributes. ''' + module = cache_import_module( MODULE_QNAME ) + + class MyProto( module.Protocol ): + def greet( self ) -> str: ... + + attrs = MyProto.__dict__.get( '__protocol_attrs__' ) + assert attrs is not None + assert 'greet' in attrs + assert not any( a.startswith( '_classcore_' ) for a in attrs ) + assert '__setattr__' not in attrs + assert '__delattr__' not in attrs + assert '__dir__' not in attrs + + +def test_441_runtime_checkable_all_four_bases( ): + ''' Runtime structural subtyping (isinstance) works for direct + subclasses of all four protocol base classes. ''' + module = cache_import_module( MODULE_QNAME ) + + class Impl: + def greet( self ) -> str: + return "hello" + + for base in ( + module.Protocol, + module.ProtocolMutable, + module.DataclassProtocol, + module.DataclassProtocolMutable, + ): + proto_cls = type( f"{base.__name__}Greeter", ( base, ), { + 'greet': lambda self: 'hello', + '_is_protocol': True, + } ) + typing.runtime_checkable( proto_cls ) + assert proto_cls._is_runtime_protocol, ( + f"{base.__name__}: _is_runtime_protocol not set" ) + assert isinstance( Impl( ), proto_cls ), ( + f"{base.__name__}: isinstance failed" ) + + +def test_442_declared_dunder_preserved( ): + ''' Protocol members declared by the author, including dunders, are + preserved in __protocol_attrs__. Framework-generated dunders + (dataclass, behaviors) are excluded via the pre-decoration + snapshot, not a dunder blacklist. ''' + module = cache_import_module( MODULE_QNAME ) + + class DunderProto( module.Protocol ): + def __repr__( self ) -> str: ... + def greet( self ) -> str: ... + + attrs = DunderProto.__dict__[ '__protocol_attrs__' ] + assert '__repr__' in attrs + assert 'greet' in attrs + assert '__eq__' not in attrs + + +def test_443_dataclass_protocol_attrs_declared_only( ): + ''' Dataclass protocol attrs contain declared fields and methods, + excluding dataclass-generated dunders. ''' + attrs = _ModuleDataclassProto.__dict__[ '__protocol_attrs__' ] + assert 'value' in attrs + assert 'greet' in attrs + assert '__repr__' not in attrs + assert '__dataclass_fields__' not in attrs + + +def test_444_factory_hierarchy_delegation( ): + ''' Custom metaclass hierarchies delegate construction without + duplicating postprocessing. Covers the no-stub wrapper branch: + a parent metaclass without '__new__' in its own dict. A class + decorator must record exactly one invocation. ''' + from classcore.standard.decorators import class_factory + + @class_factory( ) + class ParentMeta( type ): + ''' No '__new__' stub: factory installs construct_with_super. ''' + + @class_factory( ) + class ChildMeta( ParentMeta ): + + def __new__( # Typechecker stub. + clscls, name, bases, namespace, **kwargs + ): + return super( ).__new__( clscls, name, bases, namespace ) + + invocations: list[ str ] = [ ] + + def recorder( cls: type ) -> type: + invocations.append( cls.__name__ ) + return cls + + class Product( + metaclass = ChildMeta, decorators = ( recorder, ) + ): + ''' Created through both factory levels. ''' + + assert type( Product ) is ChildMeta + assert invocations == [ 'Product' ] + + +# =========================================================================== # +# Dataclass Transform Preservation +# =========================================================================== # + + +def test_450_dataclass_transform_frozen_preserved( ): + ''' ProtocolDataclass has frozen_default=True. ''' + module = cache_import_module( MODULE_QNAME ) + spec = module.ProtocolDataclass.__dict__.get( '__dataclass_transform__' ) + assert spec is not None + assert spec.get( 'frozen_default' ) is True + + +def test_451_dataclass_transform_mutable_not_frozen( ): + ''' ProtocolDataclassMutable has frozen_default=False, not inherited + from ProtocolDataclass. ''' + module = cache_import_module( MODULE_QNAME ) + spec = ( + module.ProtocolDataclassMutable.__dict__.get( + '__dataclass_transform__' ) ) + assert spec is not None + assert spec.get( 'frozen_default' ) is False diff --git a/tests/test_000_classcore/test_347_abstract_classes.py b/tests/test_000_classcore/test_347_abstract_classes.py new file mode 100644 index 0000000..0eb4b05 --- /dev/null +++ b/tests/test_000_classcore/test_347_abstract_classes.py @@ -0,0 +1,190 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Tests for abstract base class support. ''' + + +import abc +import typing + +from .__ import PACKAGE_NAME, cache_import_module + +MODULE_QNAME = f"{PACKAGE_NAME}.standard.classes" + + +# =========================================================================== # +# Metaclass Hierarchy +# =========================================================================== # + + +def test_500_abstractclass_hierarchy( ): + ''' AbstractClass combines Class with ABCMeta in a diamond. ''' + module = cache_import_module( MODULE_QNAME ) + assert issubclass( module.AbstractClass, module.Class ) + assert issubclass( module.AbstractClass, abc.ABCMeta ) + # Class itself remains backed by plain type: no ABC machinery on + # ordinary classes. + assert not issubclass( module.Class, abc.ABCMeta ) + + +def test_501_protocolclass_unified_taxonomy( ): + ''' ProtocolClass descends from AbstractClass and Class. ''' + module = cache_import_module( MODULE_QNAME ) + assert issubclass( module.ProtocolClass, module.Class ) + assert issubclass( module.ProtocolClass, module.AbstractClass ) + assert issubclass( module.ProtocolClass, abc.ABCMeta ) + + +# =========================================================================== # +# AbstractObject +# =========================================================================== # + + +def test_510_abstractobject_standard_behaviors( ): + ''' AbstractObject applies standard behaviors to subclasses. ''' + module = cache_import_module( MODULE_QNAME ) + + class Widget( module.AbstractObject ): + ''' Concrete widget. ''' + + def provide( self ) -> int: + return 42 + + assert type( Widget ) is module.AbstractClass + widget = Widget( ) + assert widget.provide( ) == 42 + try: + widget.provide = lambda: 0 # type: ignore[method-assign] + mutated = True + except Exception: + mutated = False + assert not mutated, 'instance immutability not enforced' + + +def test_511_abstractobject_mixes_with_external_abc( ): + ''' AbstractObject subclasses mix with external ABC-based classes. ''' + module = cache_import_module( MODULE_QNAME ) + + class ExternalABC( abc.ABC ): + @abc.abstractmethod + def provide( self ) -> int: ... + + class Mixed( module.AbstractObject, ExternalABC ): + def provide( self ) -> int: + return 7 + + assert type( Mixed ) is module.AbstractClass + assert Mixed( ).provide( ) == 7 + + +def test_512_abstract_method_enforcement( ): + ''' Incomplete subclasses of abstract classes cannot instantiate. ''' + module = cache_import_module( MODULE_QNAME ) + + class ExternalABC( abc.ABC ): + @abc.abstractmethod + def provide( self ) -> int: ... + + class Incomplete( module.AbstractObject, ExternalABC ): + ''' Does not implement "provide". ''' + + try: + Incomplete( ) + raised = False + except TypeError: + raised = True + assert raised, 'abstract method enforcement missing' + + +def test_513_registration( ): + ''' Virtual subclass registration works and does not trip + immutability enforcement. ''' + module = cache_import_module( MODULE_QNAME ) + + class Registrable( module.AbstractObject ): + pass + + class Standalone: + pass + + Registrable.register( Standalone ) + assert issubclass( Standalone, Registrable ) + assert isinstance( Standalone( ), Registrable ) + + +# =========================================================================== # +# Protocol Functionality Preserved +# =========================================================================== # + + +def test_520_protocol_suite_through_unified_hierarchy( ): + ''' Protocol detection and structural subtyping still work after + ProtocolClass re-parenting. ''' + module = cache_import_module( MODULE_QNAME ) + + class GreeterProto( module.Protocol ): + def greet( self ) -> str: ... + + assert GreeterProto.__dict__.get( '_is_protocol' ) is True + + typing.runtime_checkable( GreeterProto ) + + class Impl: + def greet( self ) -> str: + return 'hello' + + assert isinstance( Impl( ), GreeterProto ) + + +def test_521_single_decoration_through_hierarchy( ): + ''' Decorators apply exactly once through the unified + ProtocolClass -> AbstractClass -> Class hierarchy. ''' + module = cache_import_module( MODULE_QNAME ) + invocations: list[ str ] = [ ] + + def recorder( cls: type ) -> type: + invocations.append( cls.__name__ ) + return cls + + class Decorated( + metaclass = module.ProtocolClass, + decorators = ( recorder, ), + ): + ''' Class created through three factory levels. ''' + + assert invocations == [ 'Decorated' ] + + +def test_522_concrete_classes_satisfy_protocols( ): + ''' Concrete classcore classes are structurally compatible with + classcore protocols. ''' + module = cache_import_module( MODULE_QNAME ) + + class GreeterProto( + module.Protocol, + decorators = ( typing.runtime_checkable, ), + ): + def greet( self ) -> str: ... + + class GreeterClass( module.Object ): + def greet( self ) -> str: + return 'concrete' + + assert isinstance( GreeterClass( ), GreeterProto ) From 5cde89bae56d9789a20232d74ca471cbf43b36a5 Mon Sep 17 00:00:00 2001 From: Github Actions Robot <emcd@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:09:21 +0000 Subject: [PATCH 26/28] Update documents for publication. (1.13) --- .auxiliary/.gitignore | 5 +- .auxiliary/configuration/copier-answers.yaml | 9 +- .auxiliary/configuration/pre-commit.yaml | 2 +- .auxiliary/configuration/vulturefood.py | 21 + .../data/towncrier/+abc-support.enhance.rst | 6 - .../+attribute-explanations.enhance.rst | 9 + .../+function-local-dataclasses.repair.rst | 6 + .../+internal-name-contributions.enhance.rst | 11 + .../towncrier/+metaclass-hierarchy.repair.rst | 6 - .../towncrier/+protocol-detection.repair.rst | 5 - .../+python315-typing-dunder.repair.rst | 5 + .../+runtime-checkable-protocols.repair.rst | 5 - .../towncrier/+signature-docs.enhance.rst | 3 - .auxiliary/publications/website.tar.xz | 4 +- .auxiliary/scripts/validate-fragments.py | 149 ++++ .auxiliary/scripts/validate-module-sizes.py | 47 ++ .github/workflows/core--initializer.yaml | 10 +- documentation/api.rst | 6 + .../.openspec.yaml | 2 + .../design.md | 82 ++ .../proposal.md | 71 ++ .../specs/attribute-explanations/spec.md | 131 ++++ .../tasks.md | 42 + .../.openspec.yaml | 2 + .../design.md | 129 ++++ .../proposal.md | 66 ++ .../specs/attribute-explanations/spec.md | 61 ++ .../tasks.md | 50 ++ .../specs/attribute-explanations/spec.md | 184 +++++ documentation/changelog.rst | 44 ++ documentation/examples/index.rst | 1 + documentation/examples/std-explanations.rst | 155 ++++ pyproject.toml | 20 +- sources/classcore/README.md | 15 + sources/classcore/__/nomina.py | 63 +- sources/classcore/__init__.py | 2 +- sources/classcore/factories.py | 19 +- sources/classcore/standard/__.py | 66 +- sources/classcore/standard/__init__.py | 1 + sources/classcore/standard/behaviors.py | 222 ++++-- sources/classcore/standard/classes.py | 40 +- sources/classcore/standard/decorators.py | 34 +- sources/classcore/standard/explanations.py | 253 ++++++ sources/classcore/standard/nomina.py | 4 + sources/classcore/utilities.py | 31 +- .../test_000_classcore/test_110_utilities.py | 22 + .../test_315_standard_cascade.py | 404 ++++++++++ .../test_320_standard_explanations.py | 721 ++++++++++++++++++ .../test_340_standard_classes.py | 38 +- .../test_345_standard_protocols.py | 31 + .../test_347_abstract_classes.py | 19 + 51 files changed, 3190 insertions(+), 144 deletions(-) delete mode 100644 .auxiliary/data/towncrier/+abc-support.enhance.rst create mode 100644 .auxiliary/data/towncrier/+attribute-explanations.enhance.rst create mode 100644 .auxiliary/data/towncrier/+function-local-dataclasses.repair.rst create mode 100644 .auxiliary/data/towncrier/+internal-name-contributions.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst delete mode 100644 .auxiliary/data/towncrier/+protocol-detection.repair.rst create mode 100644 .auxiliary/data/towncrier/+python315-typing-dunder.repair.rst delete mode 100644 .auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst delete mode 100644 .auxiliary/data/towncrier/+signature-docs.enhance.rst create mode 100644 .auxiliary/scripts/validate-fragments.py create mode 100644 .auxiliary/scripts/validate-module-sizes.py create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/.openspec.yaml create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/design.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/proposal.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/specs/attribute-explanations/spec.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/tasks.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/.openspec.yaml create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/design.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/proposal.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/specs/attribute-explanations/spec.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/tasks.md create mode 100644 documentation/architecture/openspec/specs/attribute-explanations/spec.md create mode 100644 documentation/examples/std-explanations.rst create mode 100644 sources/classcore/standard/explanations.py create mode 100644 tests/test_000_classcore/test_315_standard_cascade.py create mode 100644 tests/test_000_classcore/test_320_standard_explanations.py diff --git a/.auxiliary/.gitignore b/.auxiliary/.gitignore index 0751d4d..6bb7c18 100644 --- a/.auxiliary/.gitignore +++ b/.auxiliary/.gitignore @@ -1,2 +1,3 @@ -artifacts/ -caches/ +/artifacts/ +/caches/ +/temporary/ diff --git a/.auxiliary/configuration/copier-answers.yaml b/.auxiliary/configuration/copier-answers.yaml index d46ed16..91ed3fd 100644 --- a/.auxiliary/configuration/copier-answers.yaml +++ b/.auxiliary/configuration/copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.59.2 +_commit: gha-1 _src_path: gh:emcd/python-project-common author_email: emcd@users.noreply.github.com author_name: Eric McDonald @@ -14,13 +14,10 @@ include_data_resources: false inject_foundations: false package_name: classcore project_name: python-classcore -pypy_versions: -- '3.11' -python_version_min: '3.10' +python_version_min: '3.12' python_versions: -- '3.10' -- '3.11' - '3.12' - '3.13' - '3.14' +- '3.15' year_of_origin: 2025 diff --git a/.auxiliary/configuration/pre-commit.yaml b/.auxiliary/configuration/pre-commit.yaml index a3b08f4..dfcfc78 100644 --- a/.auxiliary/configuration/pre-commit.yaml +++ b/.auxiliary/configuration/pre-commit.yaml @@ -41,7 +41,7 @@ repos: name: 'Check: Debug Statements (Python)' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.0 + rev: v0.17.0 hooks: - id: ruff name: 'Lint: Ruff' diff --git a/.auxiliary/configuration/vulturefood.py b/.auxiliary/configuration/vulturefood.py index 62442a6..154e147 100644 --- a/.auxiliary/configuration/vulturefood.py +++ b/.auxiliary/configuration/vulturefood.py @@ -26,3 +26,24 @@ __class__ class_ delattr0 + +# Attribute explanations (fields consumed via attribute access) +explain_attribute +Decision +PermitByInapplicability +PermitByOmni +PermitByNames +PermitByPredicate +PermitByRegex +Prohibit +Verdict +AssignVerdict +DeleteVerdict +SurveyVerdict +AttributeExplanation +permissible +decision +internal +name +predicate +pattern diff --git a/.auxiliary/data/towncrier/+abc-support.enhance.rst b/.auxiliary/data/towncrier/+abc-support.enhance.rst deleted file mode 100644 index 0f742e5..0000000 --- a/.auxiliary/data/towncrier/+abc-support.enhance.rst +++ /dev/null @@ -1,6 +0,0 @@ -Add abstract base class support: ``AbstractClass`` metaclass and -``AbstractObject`` base class combine standard behaviors with -``abc.ABCMeta`` machinery (abstract method enforcement, virtual subclass -registration), and mix with external ABC-based classes without -metaclass conflicts. Re-parent ``ProtocolClass`` under ``AbstractClass``, -unifying the standard metaclass taxonomy under ``Class``. diff --git a/.auxiliary/data/towncrier/+attribute-explanations.enhance.rst b/.auxiliary/data/towncrier/+attribute-explanations.enhance.rst new file mode 100644 index 0000000..09f11ba --- /dev/null +++ b/.auxiliary/data/towncrier/+attribute-explanations.enhance.rst @@ -0,0 +1,9 @@ +Add attribute explanations: ``explain_attribute`` in +``classcore.standard.explanations`` returns the decision trace for one +attribute of one target — active behaviors per evaluated level and, for +each operation, a verdict carrying its decision under precedence +semantics: permission by inapplicability, omni, names, predicate, or +regex, or prohibition. Decision payloads are typed per decision class, +explanation records are immutable with immutable nested collections, +and framework-owned and stdlib-machinery attributes are marked +internal. diff --git a/.auxiliary/data/towncrier/+function-local-dataclasses.repair.rst b/.auxiliary/data/towncrier/+function-local-dataclasses.repair.rst new file mode 100644 index 0000000..ee5be5a --- /dev/null +++ b/.auxiliary/data/towncrier/+function-local-dataclasses.repair.rst @@ -0,0 +1,6 @@ +Fix construction of function-local dataclass classes. Classes derived +from the dataclass base classes (``DataclassObject``, +``DataclassObjectMutable``, ``DataclassProtocol``, and +``DataclassProtocolMutable``) failed with ``TypeError`` when defined +inside a function body, because the construction marker could not be +recognized during dataclass slot reproduction. diff --git a/.auxiliary/data/towncrier/+internal-name-contributions.enhance.rst b/.auxiliary/data/towncrier/+internal-name-contributions.enhance.rst new file mode 100644 index 0000000..5c7f0bc --- /dev/null +++ b/.auxiliary/data/towncrier/+internal-name-contributions.enhance.rst @@ -0,0 +1,11 @@ +Attribute internal marking now follows metaclass-carried +contributions: the attribute namer's ``is_internal_name`` detector +marks framework names (grammar-constrained static stems and both +digest-mangled forms), the abstract-base wiring marks stdlib ABC +machinery names only where that machinery operates, and downstream +``class_factory`` namers contribute their own detectors, so +``explain_attribute`` reports downstream framework attributes +truthfully. ``is_internal_name`` takes the target and consults the +metaclass chain; lookalike names that fail the detector grammar stay +unmarked, and the public ``_dynadoc_fragments_`` declaration +convention is never marked. diff --git a/.auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst b/.auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst deleted file mode 100644 index fd3b683..0000000 --- a/.auxiliary/data/towncrier/+metaclass-hierarchy.repair.rst +++ /dev/null @@ -1,6 +0,0 @@ -Establish proper inheritance hierarchies for standard metaclasses. -``Dataclass`` now inherits from ``Class``, ``DataclassMutable`` from -``Dataclass``, ``ProtocolDataclass`` from ``ProtocolClass``, and -``ProtocolDataclassMutable`` from ``ProtocolDataclass``. This eliminates -metaclass conflicts when inheriting from classes backed by different -metaclasses in the same family. diff --git a/.auxiliary/data/towncrier/+protocol-detection.repair.rst b/.auxiliary/data/towncrier/+protocol-detection.repair.rst deleted file mode 100644 index 5e5dc54..0000000 --- a/.auxiliary/data/towncrier/+protocol-detection.repair.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fix runtime protocol recognition for subclasses of classcore protocol -base classes. Previously, ``typing_extensions.Protocol.__init_subclass__`` -set ``_is_protocol = False`` on these subclasses due to an identity check -that did not recognize classcore's own protocol base classes. Subclasses -are now properly detected via structural analysis. diff --git a/.auxiliary/data/towncrier/+python315-typing-dunder.repair.rst b/.auxiliary/data/towncrier/+python315-typing-dunder.repair.rst new file mode 100644 index 0000000..4525c18 --- /dev/null +++ b/.auxiliary/data/towncrier/+python315-typing-dunder.repair.rst @@ -0,0 +1,5 @@ +Permit CPython 3.15+ typing machinery dunder on immutable classes. +``typing`` sets ``__typing_is_deprecated_inherited_runtime_protocol__`` +during class decoration and in the ``runtime_checkable`` decorator; the +class-level immutability behavior now exempts it, and the protocol +member snapshot excludes it from ``__protocol_attrs__``. diff --git a/.auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst b/.auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst deleted file mode 100644 index a13d2b1..0000000 --- a/.auxiliary/data/towncrier/+runtime-checkable-protocols.repair.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fix ``typing.runtime_checkable`` support for classcore protocol classes. -``@runtime_checkable`` can now be applied via the ``decorators=`` -metaclass argument or directly after class creation. -``isinstance()`` structural subtyping works correctly with -``@runtime_checkable`` classcore protocols. diff --git a/.auxiliary/data/towncrier/+signature-docs.enhance.rst b/.auxiliary/data/towncrier/+signature-docs.enhance.rst deleted file mode 100644 index 17ff04b..0000000 --- a/.auxiliary/data/towncrier/+signature-docs.enhance.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve API documentation for class construction and initialization -signatures, including parameter descriptions and the metaclass -delegation contract. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 8b362df..da4700e 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e6c01b0d0ae3f2f6adcd426d7b3329cfe8ed647a456bc21f85dcb6e4264fe46 -size 562932 +oid sha256:c40ef55f9a4890a37893d52812b8c5dca5c26bc4a2bbbcf70d6689e387c5bf14 +size 673108 diff --git a/.auxiliary/scripts/validate-fragments.py b/.auxiliary/scripts/validate-fragments.py new file mode 100644 index 0000000..91502b7 --- /dev/null +++ b/.auxiliary/scripts/validate-fragments.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Validate Towncrier news fragments for correct naming and reST syntax. + +Checks fragment filenames against the type vocabulary defined in +pyproject.toml and validates reST markup via docutils. Name-first, +parse-second ordering ensures we skip docutils overhead for files +towncrier will never consume. +""" + +import io as _io +import re as _re +import sys as _sys +from contextlib import redirect_stderr as _redirect_stderr +from pathlib import Path as _Path + +try: + import tomllib as _tomllib +except ModuleNotFoundError: + import tomli as _tomllib # type: ignore[import-untyped] + +from docutils.core import publish_doctree as _publish_doctree + + +def load_towncrier_types( + pyproject_path: _Path, +) -> list[ str ]: + ''' Extracts fragment type directory names from pyproject.toml. ''' + with pyproject_path.open( 'rb' ) as f: + config = _tomllib.load( f ) + types = config.get( 'tool', {} ).get( 'towncrier', {} ).get( 'type', [] ) + if isinstance( types, dict ): + types = [ types ] + return [ t[ 'directory' ] for t in types ] + + +def validate_fragment_names( + fragments: list[ _Path ], types: list[ str ] +) -> tuple[ list[ str ], set[ _Path ] ]: + ''' Validates fragment filenames against naming conventions. + + Returns (error_messages, invalid_paths). + ''' + type_pattern = '|'.join( _re.escape( t ) for t in types ) + named_pattern = _re.compile( + rf'^\+([\w][\w.-]*)\.({type_pattern})\.rst$' ) + issue_pattern = _re.compile( + rf'^(\d+)\.({type_pattern})\.rst$' ) + errors: list[ str ] = [] + invalid: set[ _Path ] = set( ) + for frag in fragments: + name = frag.name + if not named_pattern.match( name ) \ + and not issue_pattern.match( name ): + errors.append( + f'{frag}: invalid fragment name. ' + f'Expected +<name>.<type>.rst or <issue>.<type>.rst ' + f'where type is one of: {", ".join( types )}' ) + invalid.add( frag ) + return errors, invalid + + +# Match default-role backticks (`text`) but not explicit roles +# (`:role:`text``) or literals (``text``). +_DEFAULT_ROLE_PATTERN = _re.compile( + r'(?<!:)\`(?!`)([^`]+)\`(?!`)' ) + + +def validate_fragment_syntax( + fragments: list[ _Path ], +) -> list[ str ]: + ''' Validates reST syntax of fragment files via docutils. + + Returns list of error messages for files with parse + warnings/errors. + ''' + errors: list[ str ] = [] + settings = { + 'report_level': 2, + 'halt_level': 5, + 'traceback': True, + } + for frag in fragments: + with frag.open( ) as f: + content = f.read( ) + backtick_matches = _DEFAULT_ROLE_PATTERN.findall( content ) + if backtick_matches: + examples = ', '.join( + f'`{m}`' for m in backtick_matches[ :3 ] ) + errors.append( + f'{frag}: default-role backticks detected ' + f'({examples}). Use double backticks for code ' + f'literals or an explicit role like :code:`text`.' ) + continue + stderr_capture = _io.StringIO( ) + try: + with _redirect_stderr( stderr_capture ): + _publish_doctree( + content, settings_overrides = settings ) + except Exception as exc: + errors.append( + f'{frag}: reST parse error: {exc}' ) + continue + stderr_output = stderr_capture.getvalue( ) + if stderr_output: + errors.append( + f'{frag}: reST warnings:' + f'\n{stderr_output.rstrip( )}' ) + return errors + + +def main( ) -> None: + ''' Validates all towncrier fragments in the project. ''' + project_root = _Path.cwd( ) + pyproject_path = project_root / 'pyproject.toml' + towncrier_dir = project_root / '.auxiliary' / 'data' / 'towncrier' + if not towncrier_dir.is_dir( ): + print( + 'No towncrier fragment directory found; ' + 'nothing to validate.' ) + _sys.exit( 0 ) + fragments = sorted( towncrier_dir.glob( '*.rst' ) ) + if not fragments: + print( 'No fragments found; nothing to validate.' ) + _sys.exit( 0 ) + types = load_towncrier_types( pyproject_path ) + if not types: + print( + 'Error: no towncrier types found in pyproject.toml.', + file = _sys.stderr ) + _sys.exit( 1 ) + errors: list[ str ] = [ ] + name_errors, invalid_paths = validate_fragment_names( + fragments, types ) + errors.extend( name_errors ) + valid_fragments = [ + f for f in fragments if f not in invalid_paths ] + if valid_fragments: + syntax_errors = validate_fragment_syntax( valid_fragments ) + errors.extend( syntax_errors ) + if errors: + for err in errors: + print( err, file = _sys.stderr ) + _sys.exit( 1 ) + for frag in fragments: + print( f'{frag} OK' ) + + +if __name__ == '__main__': + main( ) diff --git a/.auxiliary/scripts/validate-module-sizes.py b/.auxiliary/scripts/validate-module-sizes.py new file mode 100644 index 0000000..3c37dd8 --- /dev/null +++ b/.auxiliary/scripts/validate-module-sizes.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Enforce maximum Python module size to preserve readability. + +Restores the Pylint C0302 (max-module-lines) enforcement lost in the +Pylint-to-Ruff migration. Modules exceeding the limit must be split +into subpackages following the classcore hub pattern. +""" + +import sys as _sys +from pathlib import Path as _Path + +_LIMIT = 1000 +_SCOPE = ( 'sources', 'tests' ) + + +def find_oversize_modules( + root: _Path, limit: int = _LIMIT, +) -> list[ tuple[ _Path, int ] ]: + ''' Returns (path, line count) for Python modules over the limit. ''' + oversize: list[ tuple[ _Path, int ] ] = [ ] + for scope in _SCOPE: + base = root / scope + if not base.is_dir( ): + continue + for path in sorted( base.rglob( '*.py' ) ): + with path.open( encoding = 'utf-8' ) as f: + count = sum( 1 for _ in f ) + if count > limit: + oversize.append( ( path, count ) ) + return oversize + + +def main( ) -> None: + ''' Fails if any module exceeds the line limit. ''' + oversize = find_oversize_modules( _Path.cwd( ) ) + if not oversize: + print( f'All modules within {_LIMIT} lines.' ) + return + for path, count in oversize: + print( + f'{path}: {count} lines exceeds {_LIMIT}-line limit. ' + f'Split into a subpackage.' ) + _sys.exit( 1 ) + + +if __name__ == '__main__': + main( ) diff --git a/.github/workflows/core--initializer.yaml b/.github/workflows/core--initializer.yaml index 9bc6453..416103a 100644 --- a/.github/workflows/core--initializer.yaml +++ b/.github/workflows/core--initializer.yaml @@ -55,18 +55,14 @@ jobs: run: | set -eu python_descriptors="$(cat <<'EOF' | yq --output-format=json | jq --compact-output - "3.10": - hatch: "py3.10" - "3.11": - hatch: "py3.11" "3.12": hatch: "py3.12" "3.13": hatch: "py3.13" "3.14": hatch: "py3.14" - "pypy3.11": - hatch: "pypy3.11" + "3.15": + hatch: "py3.15" EOF )" echo "specs=${python_descriptors}" >>${GITHUB_OUTPUT} @@ -78,10 +74,8 @@ jobs: id: matrix-exclusions run: | set -eu - # PyPy has slow I/O, even slower on Windows. items="$(jq --compact-output <<EOF [ - {"platform": "windows-latest", "python-version": "pypy3.11"} ] EOF )" diff --git a/documentation/api.rst b/documentation/api.rst index 5f90f5f..e06b48f 100644 --- a/documentation/api.rst +++ b/documentation/api.rst @@ -91,6 +91,12 @@ Module ``classcore.standard.behaviors`` .. automodule:: classcore.standard.behaviors +Module ``classcore.standard.explanations`` +------------------------------------------------------------------------------- + +.. automodule:: classcore.standard.explanations + + Module ``classcore.standard.nomina`` ------------------------------------------------------------------------------- diff --git a/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/.openspec.yaml b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/.openspec.yaml new file mode 100644 index 0000000..f161d5c --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-16 diff --git a/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/design.md b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/design.md new file mode 100644 index 0000000..7db08f4 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/design.md @@ -0,0 +1,82 @@ +## Context + +Attribute behavior decisions are computed in +`standard/behaviors.py` by three cores — assign, delete, survey — each +walking the same cascade: read the active-behaviors set for the level +(labels `immutability` and `concealment`), then consult per-level +exclusion configuration (names with `'*'` wildcard, predicates, +regexes) for the operation (mutables for assign/delete, visibles for +survey). Cores receive the raw level (`instances`/`classes`) and use it +for exclusion configuration lookup, normalizing only the active-behaviors +lookup. The survey core additionally has a known quirk: its per-name +matching loops `continue` the inner loop only, so a name matching +multiple rules is yielded multiple times; CPython `dir()` does not +deduplicate custom `__dir__` output, so duplicates reach users. + +## Goals / Non-Goals + +**Goals:** +- Extract the decision cascade into pure helpers shared by the cores and + the new explanations API, preserving behavior exactly. +- Provide `explain_attribute( target, name )` returning immutable records + with genuine nested immutability. +- Specify stable, deterministic rendering for rule details. +- Mark framework-internal and stdlib-machinery attributes. + +**Non-Goals:** +- Whole-object explanation sweeps (`survey_explanations`). +- Repairing the survey duplicate-yield quirk (separate change). +- Caching/performance work (`/8` follow-up; claims deferred until + extraction boundaries exist and are profiled). +- Error-message integration (deferred, separately decided). +- `eval`-able rule detail strings. + +## Decisions + +- Module: `standard/explanations.py` (Latinate, matches the + `explain_attribute` verb; avoids stdlib `inspect` confusion). +- Naming: decision hierarchy PermitBy* / Prohibit (Latinate verbs); + `PermitByOmni` follows the package's `*Omni` wildcard vocabulary + (BehaviorExclusionNamesOmni). +- Verdict shapes follow a single closed decision hierarchy for all + three operations: PermitByInapplicability (behavior inactive), + PermitByOmni, PermitByNames, PermitByPredicate, PermitByRegex, and + Prohibit (behavior active, nothing permitted). All operations apply + precedence semantics (omni, names, first predicate, first regex). + Permissibility is a derived property of the verdict + (not isinstance(decision, Prohibit)) — no stored boolean, no + __bool__. Payloads are typed per decision (name, qualified predicate + text, pattern text) so a name colliding with a genus label or pattern + cannot be confused with one. +- Survey first-match is an owner decision reversing the earlier + union-trace design: the union trace existed to faithfully describe + the duplicate-yield behavior, which is itself characterized as a + defect with normalization deferred. Specifying the explanation to + the normalized (first-match) semantics now makes explanation and + repaired behavior coincide; until the repair, the survey core keeps + determine_relevant_permitters internally to reproduce current behavior. +- Level semantics: class targets evaluate the `classes`-level + configuration; instance targets evaluate the `instances`-level + configuration against the instance's class hierarchy. Exclusion + configuration uses the raw level; the normalized behavior level is + recorded separately on the explanation. +- Decision payload policy: names decisions carry the matched name; + predicate decisions carry qualified-name text with an `'<anonymous>'` + fallback; regex decisions carry pattern text; omni and prohibition + decisions carry no payload. Payloads are typed per decision class, + so a name colliding with a pattern or label cannot be confused. +- `AttributeExplanation.internal` marks classcore-owned named/mangled + attributes and stdlib ABC machinery (class mutables exemption sets). +- Survey duplicate-yield is preserved by the extraction and + characterized by a test asserting current behavior. + +## Risks / Trade-offs + +- Extraction refactor could subtly change core behavior; mitigated by + characterizing tests (including the duplicate-yield case) written + before and kept after the refactor. +- String-only rule details limit programmatic consumers; accepted for + serializability and implementation hiding. +- The explanations API expands public surface that a future native + backend must keep answering; accepted as the semantic pinning that + makes that migration safe. diff --git a/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/proposal.md b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/proposal.md new file mode 100644 index 0000000..290c7f6 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/proposal.md @@ -0,0 +1,71 @@ +## Why + +Classcore's attribute behaviors are opaque. When an attribute surprises a +user — immutable when expected mutable, absent from `dir()` when expected +visible — the reason lives in a decision cascade spread across +`behaviors.py`: the active-behaviors sets (immutability and concealment +labels), per-level exclusion configuration (names, the `'*'` wildcard, +predicates, regexes), and stdlib machinery exemptions. There is no way to +ask classcore *why*; users must reverse-engineer from source, and even +maintainers re-derive the cascade from scratch each time (the recent +mangling work confirmed this). + +Formalizing the decision model as a first-class, inspectable artifact has +standalone value, and it is a prerequisite for the deferred Rust +native-storage work: semantic pinning before any native commitment. + +## What Changes + +Add a public attribute explanations capability to `classcore.standard`, +in a new `explanations.py` module: + +- `explain_attribute( target, name )` — returns the complete decision + trace for attribute `name` on `target` (class or instance): active + behaviors per evaluated level, and per-operation verdicts, each + carrying exactly one decision. +- A closed decision hierarchy — `PermitByInapplicability`, + `PermitByOmni`, `PermitByNames`, `PermitByPredicate`, + `PermitByRegex`, and `Prohibit` — with typed payloads; `Verdict` and + its per-operation subclasses (`AssignVerdict`, `DeleteVerdict`, + `SurveyVerdict`) with derived permissibility; and + `AttributeExplanation` — all immutable (frozen, concealed) records, + with nested collections genuinely immutable. All operations, survey + included, follow precedence semantics (omni, names, first matching + predicate, first matching regex). +- The decision cascade in `behaviors.py` is extracted into pure helpers + consumed by both the behavior cores and the explanations, so the + helpers are shared wherever semantics coincide. Survey is the + deliberate exception: the explanation reports normalized first-match + semantics while the core preserves the once-per-matching-rule + duplication, which is characterized as a defect with its repair + deferred separately. + +Scope boundaries for the first release: `explain_attribute` only (no +whole-object surveys); rule details render as plain strings with a +specified policy (no `eval` round-trip promised); explanations are +observational and do not bypass or alter concealment or immutability; +framework-internal and stdlib-machinery attributes are explainable and +marked as internal. + +## Capabilities + +### New Capabilities +- `attribute-explanations`: public API for explaining attribute behavior + decisions — verdicts, decision rules, level semantics, record + immutability, and internal-attribute marking. + +### Modified Capabilities +<!-- No existing capability's requirements change; the behavior cascade + extraction preserves current behavior exactly, including its known + duplicate-yield quirk, which is characterized by tests and left for + a separate repair. --> + +## Impact + +- New module `sources/classcore/standard/explanations.py`; records are + defined there directly, without nomina aliases. +- `sources/classcore/standard/behaviors.py`: decision-cascade extraction + (behavior-preserving refactor). +- README (taxonomy/API sections); towncrier fragment + `+attribute-explanations.enhance.rst`. +- No public API changes to existing capabilities. diff --git a/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/specs/attribute-explanations/spec.md b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/specs/attribute-explanations/spec.md new file mode 100644 index 0000000..eba5fd7 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/specs/attribute-explanations/spec.md @@ -0,0 +1,131 @@ +## ADDED Requirements + +### Requirement: Attribute Explanation + +The `explain_attribute` function MUST return the decision trace for the +named attribute on the target: the active behaviors for each evaluated +level, and for each operation the applicable verdict. Every verdict MUST +carry exactly one decision, drawn from a closed hierarchy: permitted by +inapplicability (the governing behavior is inactive), permitted by the +omni marker, permitted by names membership, permitted by a predicate, +permitted by a regex, or prohibited (the behavior is active and no rule +permits). All three operations — assign, delete, and survey — MUST +apply the same precedence semantics: omni, then names membership, then +the first matching predicate, then the first matching regex. + +For class targets, the classes-level configuration MUST be evaluated. +For instance targets, the instances-level configuration MUST be +evaluated against the instance's class hierarchy. Survey verdicts MUST +explain the visibility of the supplied name only. Explanations MUST be +observational: they MUST NOT bypass or alter concealment or +immutability. + +Note: survey explanations describe the normalized first-match semantics. +The survey core currently yields a name once per matching rule; that +duplication is characterized as a defect and deferred to a separate +repair, after which core behavior and explanation semantics coincide. + +#### Scenario: Mutable attribute +- **WHEN** the active behaviors for the evaluated level lack + immutability +- **THEN** the assign and delete verdicts MUST be permitted by + inapplicability + +#### Scenario: Excluded by name +- **WHEN** the attribute name is in the exclusion names for the + operation's level +- **THEN** the verdict MUST be permitted by names, carrying the matched + name + +#### Scenario: Excluded by regex +- **WHEN** an exclusion regex fullmatches the attribute name +- **THEN** the verdict MUST be permitted by regex, carrying the pattern + text + +#### Scenario: Excluded by predicate +- **WHEN** an exclusion predicate matches the attribute name +- **THEN** the verdict MUST be permitted by predicate, carrying the + predicate's fully-qualified name, or `<anonymous>` when unavailable + +#### Scenario: Omni exclusion +- **WHEN** the exclusion names for assign or delete equal `'*'` +- **THEN** the verdict MUST be permitted by omni + +#### Scenario: Immutable attribute +- **WHEN** immutability is active and no rule permits the operation +- **THEN** the assign and delete verdicts MUST be prohibited + +#### Scenario: Concealed attribute +- **WHEN** concealment is active and no visibles rule matches the name +- **THEN** the survey verdict MUST be prohibited + +#### Scenario: Survey multi-rule precedence +- **WHEN** the attribute name matches a predicate and a regex and is not + in the exclusion names +- **THEN** the survey verdict MUST be permitted by the predicate, the + first rule under precedence, and MUST NOT carry the regex + +#### Scenario: Survey names precedence +- **WHEN** the attribute name is in the exclusion names for the survey + level and also matches a predicate or regex +- **THEN** the survey verdict MUST be permitted by names and predicates + and regexes MUST NOT be evaluated for that name + +#### Scenario: Survey omni +- **WHEN** concealment is active and the visibles names equal `'*'` +- **THEN** the survey verdict MUST be permitted by omni and predicates + and regexes MUST NOT be evaluated + +#### Scenario: Survey concealment inactive +- **WHEN** the active behaviors for the evaluated level lack concealment +- **THEN** the survey verdict MUST be permitted by inapplicability + +#### Scenario: Permissible derivation +- **WHEN** any verdict's decision is not prohibited +- **THEN** the verdict MUST report itself as permissible + +### Requirement: Decision Payload Distinction + +Decision payloads MUST be distinguished by type, so that a name which +coincides with a genus label or pattern text cannot be confused with +it. The names decision MUST carry the matched name; the predicate +decision MUST carry qualified-name text; the regex decision MUST carry +pattern text. Payloads MUST be plain text. + +#### Scenario: Names payload collision +- **WHEN** the explained attribute is literally named `regex` and is + permitted by names membership +- **THEN** the decision MUST be a names decision carrying the name + `regex`, distinct from any regex decision carrying pattern text + +### Requirement: Record Immutability + +The explanation records MUST be immutable: the decision hierarchy, the +verdicts, and `AttributeExplanation` MUST NOT permit field assignment, +and every nested collection they contain MUST be an immutable +structure. Verdict permissibility MUST be derived from the decision, +not stored. + +#### Scenario: Frozen records +- **WHEN** a field is assigned on any explanation record +- **THEN** an immutability error MUST be raised + +#### Scenario: Immutable nested collections +- **WHEN** the operations mapping, the behaviors mapping, or a payload + sequence is mutated +- **THEN** an error MUST be raised + +### Requirement: Internal Attribute Marking + +Explanations MUST mark attributes that are framework-owned — classcore +named or mangled attributes — or stdlib machinery — class mutables +exemption sets — as internal. + +#### Scenario: Framework-internal attribute +- **WHEN** the explained name is framework-owned or stdlib machinery +- **THEN** the explanation MUST be marked internal + +#### Scenario: User attribute +- **WHEN** the explained name is neither framework-owned nor stdlib + machinery +- **THEN** the explanation MUST NOT be marked internal diff --git a/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/tasks.md b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/tasks.md new file mode 100644 index 0000000..7462c22 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-08-22-add-attribute-explanations/tasks.md @@ -0,0 +1,42 @@ +## 1. Decision Cascade Extraction + +- [x] 1.1 Write characterizing tests for the current core behavior in + `behaviors.py`, including the survey duplicate-yield quirk (a name + matching a predicate and a regex is yielded twice) +- [x] 1.2 Extract the assign/delete decision cascade and the survey + matching logic into pure helpers with explicitly designed boundaries, + consumed by the cores; behavior MUST be preserved exactly +- [x] 1.3 Validate: full suite on develop and qa.pypy3.11 unchanged + +## 2. Explanations Module + +- [x] 2.1 Define the explanation records directly in + standard/explanations.py (AttributeExplanation, Verdict subclasses + AssignVerdict/DeleteVerdict/SurveyVerdict, and the Decision hierarchy + PermitByInapplicability/PermitByOmni/PermitByNames/PermitByPredicate/ + PermitByRegex/Prohibit) with documentation annotations; nomina + aliases omitted as the records are classes, not aliases, and are + colocated with explain_attribute +- [x] 2.2 Implement `standard/explanations.py`: the records as frozen, + concealed classes with genuinely immutable nested collections, and + `explain_attribute( target, name )` built on the extracted helpers +- [x] 2.3 Implement internal-attribute marking: classcore-owned + named/mangled attributes and stdlib class-mutables exemption sets + +## 3. Specification Conformance + +- [x] 3.1 Tests for every scenario in the attribute-explanations delta + (re-scoped to the decision-hierarchy verdict shape): + verdict semantics per operation under precedence — survey included, + first-match semantics, distinct from the core's deferred + duplicate-yield defect — level evaluation for class versus instance + targets, observational (non-bypass) behavior, record and nested + immutability, internal marking +- [x] 3.2 Validate detail rendering policy: name/`'*'`/pattern text/ + qualified predicate name with `<anonymous>` fallback + +## 4. Documentation + +- [x] 4.1 README: taxonomy and API sections for the explanations module +- [x] 4.2 Towncrier fragment `+attribute-explanations.enhance.rst` +- [x] 4.3 Full validation: make-all plus qa.pypy3.11 diff --git a/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/.openspec.yaml b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/.openspec.yaml new file mode 100644 index 0000000..6529e83 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-22 diff --git a/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/design.md b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/design.md new file mode 100644 index 0000000..1725604 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/design.md @@ -0,0 +1,129 @@ +## Context + +Enforcement already scopes machinery exemptions per metaclass: +`class_mutables = abc_class_mutables` is declared only on ABC-carrying +metaclasses, so a plain class naming an attribute `_abc_cache` receives +no exemption. Marking, however, matches names globally in +`is_internal_name`, which would mislabel that same attribute. Both +downstream inventories (Accretive, Frigid, 2026-08-22) report static, +uniformly prefixed internal names produced by `calculate_attrname` +namers — identical in structure to classcore's own — with no +MRO-dependent names. `_dynadoc_fragments_` is confirmed by the Dynadoc +owner as a documented user-settable declaration convention. + +## Goals / Non-Goals + +**Goals:** +- Truthful internal marking for classcore, stdlib machinery, and + downstream frameworks' names, with responsibility carried by the + machinery that builds each class. +- Marking scope mirroring enforcement scope (per-metaclass, inherited). +- Detector registration through the existing `attributes_namer` seam — + no new factory/decorator argument slots. +- Determiners pure `(name) -> bool`-style callables (native-storage + constraint). + +**Non-Goals:** +- Attribution on explanation records (which framework's contribution + marked the name) — deliberate 2.0 revisit. +- A global fixed-name registry or fallback tier (no consumer exists; + additive later if one appears). +- Wiring contributions into `class_mutables` enforcement decisions. + (Contributions become the shared store that enforcement *could* later + read, but enforcement semantics are unchanged here.) +- MRO-dependent registrant signatures beyond what the ABC set needs + internally. + +## Decisions + +- Metaclass-carried contributions attach at factory/decoration time: + the factory closure records its namer's detector; the abstract-base + metaclass adds the stdlib machinery set after factory decoration + through a private wiring function — no public factory or decorator + argument is added anywhere. Subclasses in any package inherit + contributions via the metaclass, which resolves multi-package + inheritance by construction. +- Decorator-path exception: plain decorated classes (no custom + metaclass exists; `type` builds them) carry their contribution on + the class itself, and consultation walks the class resolution order + after the metaclass chain. Subclassing a decorated class keeps the + marking via the class MRO. +- The namer detector attribute is canonically `is_internal_name`, + mirroring the module-level function it specializes. Namer detectors + over prefix arguments: a detector callable validates + shape, not just prefix — classcore's mangled names carry 64-hex + digest suffixes, and a detector can require them, so a lookalike + name without a valid digest does not mark. Classcore's + `calculate_attrname` is annotated as the model implementation. +- `is_internal_name(target, name)`: marking consults the metaclass + MRO. For a class target `C` this is `type(C).__mro__`; for an + instance target `x` it is `type(type(x)).__mro__` — the metaclass + MRO of the instance's type. Note `type(x).__mro__` is the *class* + MRO (C's bases) and never enumerates the metaclass where + contributions live; the double-type transformation is required and + is an explicit test scenario. Cost is one MRO walk per call; + cacheable later without API change. + +- Namer detector contract (canonical): an optional `is_internal_name` + attribute on the namer callable, signature `(name: str) -> bool`, + invoked on full stored attribute names. Exactly two name forms + exist, and detectors accept them as their emitted shapes dictate: + (1) static stems — `_classcore_{level}_{core}_`, trailing + underscore, no digest (e.g. `_classcore_class_behaviors_`); + (2) mangled names — stem followed immediately by exactly 64 + lowercase hexadecimal digits, NO trailing underscore after the + digest, because `utilities.mangle_name` appends the digest directly + (e.g. `_classcore_class_in_progress_` + digest; verified form: + `_classcore_class_in_progress_6e6f...7ee`). Transient mangled + names (construction markers) match form (2) while they exist. + Grammar-constrained static stems (mirroring the model downstream + confirmed for Accretive): a stem marks internal only when its level + is one of the closed set class, classes, instance, instances, and + its core is one of the enumerated set classcore generates — + behaviors, construction_arguments, dynadoc_configuration, + in_progress, mutables_names, mutables_predicates, mutables_regexes, + visibles_names, visibles_predicates, visibles_regexes, + assigner_core, deleter_core, surveyor_core. The enumeration lives + beside the namer in the same module, and a consistency test asserts + every namer call site's arguments are covered. Mangled forms: two + stems are stored through the mangling accessors and therefore carry + exactly 64 lowercase hex with no trailing underscore — + `_classcore_class_behaviors_<digest>` (class behaviors, stored via + setattr0 by the initialization completer) and + `_classcore_class_in_progress_<digest>` (the construction marker). + The static `_classcore_instance_behaviors_` form is distinct and + unmangled; instance-level storage does not use digest mangling on + standard classes (verified against Object.__dict__). Names with the prefix + that fail the grammar — `_classcore_not_a_generated_core_`, + `_classcore_class_lookalike_`, malformed digests (wrong length, + uppercase, trailing underscore), `_classcore_class_in_progress_` + bare stem — MUST NOT match. The classcore model detector is the + reference implementation of this convention. Downstream detectors + follow their own emitted shapes: Accretive requires its static-stem + forms AND its exact mangled-progress form + (`_accretive_class_in_progress_<64hex>`), because that marker is + generated by the shared classcore factory under Accretive's namer; + Frigid requires the same three-form family for `_frigid_` + (confirmed forms: `_frigid_{level}_{core}_`; + `_frigid_class_behaviors_{64hex}`; + `_frigid_class_in_progress_{64hex}`), mirroring classcore's two + mangled stems. Frigid additionally notes + transient names may appear in `__protocol_attrs__` rather than + `__dict__` post-construction; marking applies wherever the name is + explained, independent of storage location. +- `_dynadoc_fragments_` excluded everywhere: public declaration + convention, analogous to `__slots__`. +- No global state, no locking, no registry injection. Tests construct + metaclasses with contributions directly. + +## Risks / Trade-offs + +- `is_internal_name` signature change is free only pre-release; the + change targets the 1.13 window (noted in the proposal). +- A downstream namer without a detector contributes nothing — its names + stay unmarked, which is the status quo and fails safe (a false + "internal" claim is worse than a false "user" claim; the latter is + today's behavior for downstreams anyway). +- Metaclass MRO consultation on every explanation adds a small constant + cost; acceptable for a diagnostics API, cacheable if profiling ever + demands. diff --git a/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/proposal.md b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/proposal.md new file mode 100644 index 0000000..f40dbfc --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/proposal.md @@ -0,0 +1,66 @@ +## Why + +`explain_attribute` marks internal names using a hardcoded pair: the +classcore prefix and the ABC machinery set. Downstream frameworks built +on classcore (Accretive, Frigid) produce identically structured internal +attributes (`_accretive_*`, `_frigid_*` via their `calculate_attrname` +namers), so explaining a downstream class today makes false statements +about the nature of downstream attributes. The explanation API is only +as truthful as its knowledge of the ecosystem, and responsibility for +knowing a framework's names belongs with that framework's machinery. + +## What Changes + +Replace global name matching with metaclass-carried internal-name +contributions: + +- Internal-name knowledge attaches at metaclass/factory decoration + time — the classcore prefix rides classcore's factory wiring, the ABC + set rides the `AbstractClass`/`ProtocolClass` wiring (mirroring how + enforcement already scopes `class_mutables` per metaclass), and + downstream prefixes ride their `class_factory`-produced metaclasses + via namer-provided detectors. +- `explain_attribute` and `is_internal_name(target, name)` consult the + target's metaclass MRO for contributions; decorator-path classes + (built by plain `type`) carry their contribution on the class itself + and are consulted through the class resolution order. No public + factory or decorator argument is added. +- `_dynadoc_fragments_` is never marked internal: it is a documented, + user-settable declaration convention, analogous to `__slots__`. +- No global registry: no shared store, no locking, no injection + parameter. A global fixed-name fallback tier remains possible later + and additive if a real non-classcore-attached consumer appears. + +Resolved by construction: a non-ABC class defining `_abc_cache` sees it +unmarked (no ABC contribution in its metaclass chain), and a class in +one package deriving from another framework's base inherits that +framework's contribution via the metaclass. + +## Capabilities + +### New Capabilities +<!-- None. --> + +### Modified Capabilities +- `attribute-explanations`: Internal Attribute Marking requirement is + modified — marking is determined by metaclass-carried contributions + rather than hardcoded name sources, and the public declaration + convention `_dynadoc_fragments_` is excluded. + +## Impact + +- `sources/classcore/standard/explanations.py`: `is_internal_name` + gains a target parameter and consults metaclass contributions. +- `sources/classcore/factories.py` and/or `standard/decorators.py`: + contribution wiring at factory/decoration time. +- `sources/classcore/standard/classes.py`: ABC contribution on + `AbstractClass` wiring. +- Namer convention: optional detector exposed by `calculate_attrname` + namers (classcore's own annotated as the model implementation). +- Tests: non-ABC `_abc_cache`, multi-package inheritance, digest-suffix + shape validation, `_dynadoc_fragments_` exclusion. +- README and a towncrier fragment. + +Timing note: `is_internal_name` gains a target parameter — a signature +change that is free only while explanations have not shipped; this +change targets the 1.13 pre-release window. diff --git a/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/specs/attribute-explanations/spec.md b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/specs/attribute-explanations/spec.md new file mode 100644 index 0000000..5c9c206 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/specs/attribute-explanations/spec.md @@ -0,0 +1,61 @@ +## MODIFIED Requirements + +### Requirement: Internal Attribute Marking + +Explanations MUST mark attributes as internal according to +internal-name contributions carried by the machinery that constructed +the target: classcore's factory wiring contributes its framework-name +detector, the abstract-base wiring contributes the stdlib ABC machinery +set, and downstream factory-produced metaclasses contribute detectors +from their attribute namers. Contributions MUST be discovered by +consulting the target's metaclass method resolution order; a class +whose metaclass chain carries no matching contribution MUST NOT mark +the name, even when the name resembles a framework or machinery name. +The `_dynadoc_fragments_` attribute is a public declaration convention +and MUST NOT be marked internal. + +#### Scenario: Framework-internal attribute +- **WHEN** the explained name matches the classcore detector carried by + the target's metaclass chain +- **THEN** the explanation MUST be marked internal + +#### Scenario: Downstream framework attribute +- **WHEN** the target's metaclass was produced by a downstream factory + whose namer contributes a detector, and the explained name matches + that detector +- **THEN** the explanation MUST be marked internal + +#### Scenario: Machinery name without machinery +- **WHEN** a class whose metaclass chain lacks the abstract-base + contribution defines an attribute named like stdlib ABC machinery +- **THEN** the explanation MUST NOT mark the name internal + +#### Scenario: Inherited contribution across packages +- **WHEN** a class defined in one package derives from a base whose + metaclass carries a framework contribution +- **THEN** the explanation MUST apply that contribution to the derived + class + +#### Scenario: Instance target consults metaclass chain +- **WHEN** an instance target is explained and a contribution is + carried by the metaclass chain of the instance's type — discovered + through the metaclass MRO, not the class MRO of the instance's type +- **THEN** the explanation MUST apply that contribution + +#### Scenario: Shape-invalid lookalike +- **WHEN** an explained name shares a framework's prefix but fails the + grammar the framework's detector requires — a core outside the + framework's enumerated vocabulary (e.g. + `_classcore_not_a_generated_core_`), or a mangled form with a + malformed digest (wrong length, uppercase characters, or trailing + underscore) +- **THEN** the explanation MUST NOT mark the name internal + +#### Scenario: Declaration convention +- **WHEN** the explained name is `_dynadoc_fragments_` +- **THEN** the explanation MUST NOT mark the name internal + +#### Scenario: User attribute +- **WHEN** the explained name matches no contribution in the target's + metaclass chain +- **THEN** the explanation MUST NOT be marked internal diff --git a/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/tasks.md b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/tasks.md new file mode 100644 index 0000000..1ac4e09 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-12-add-internal-name-contributions/tasks.md @@ -0,0 +1,50 @@ +## 1. Namer Detector Convention + +- [x] 1.1 Annotate classcore's `calculate_attrname` namer with an + `is_internal_name` detector attribute implementing the + grammar-constrained convention: closed level set, enumerated + core set, and both exact mangled forms + (`class_behaviors` and `class_in_progress` stems with 64 + lowercase hex); prefix lookalikes that fail the grammar do not + mark +- [x] 1.2a Add a consistency test asserting every namer call site's + level and core arguments are covered by the detector grammar +- [x] 1.2 Document the namer detector convention: an optional + `is_internal_name` attribute on the namer, read by factory + wiring; no new factory/decorator arguments + + +## 2. Contribution Wiring + +- [x] 2.1 Record the namer's detector as a metaclass contribution at + factory/decoration time, inherited by produced metaclasses +- [x] 2.2 Contribute the stdlib ABC machinery set on the + `AbstractClass` wiring (mirroring `class_mutables` scoping), + attached post-decoration through a private wiring function with + no new public factory argument +- [x] 2.2a Add a decorator-path inheritance test: subclassing a + decorated class keeps internal marking via the class MRO +- [x] 2.3 Exclude `_dynadoc_fragments_` from every contribution + +## 3. Marking Consultation + +- [x] 3.1 Change `is_internal_name` to accept the target and consult + the metaclass method resolution order for contributions +- [x] 3.2 Integrate consultation into `explain_attribute`; instance + targets consult `type(type(x)).__mro__` (the metaclass MRO of + the instance's type), never `type(x).__mro__` (the class MRO, + which does not enumerate the metaclass) + +## 4. Conformance and Documentation + +- [x] 4.1 Scenario tests: framework-internal coverage of static stems + and both mangled forms (`class_behaviors` and + `class_in_progress` digests), downstream detector, machinery + name without machinery, inherited contribution across packages, + instance-target metaclass-chain consultation (proving the + double-type lookup), shape-invalid lookalikes, declaration + convention exclusion, plain user attribute +- [x] 4.2 Update the internal-marking section of the README; add the + towncrier fragment +- [x] 4.3 Full validation: make-all and the PyPy environment; strict + OpenSpec validation diff --git a/documentation/architecture/openspec/specs/attribute-explanations/spec.md b/documentation/architecture/openspec/specs/attribute-explanations/spec.md new file mode 100644 index 0000000..d2ec40b --- /dev/null +++ b/documentation/architecture/openspec/specs/attribute-explanations/spec.md @@ -0,0 +1,184 @@ +# attribute-explanations Specification + +## Purpose + +Explain attribute behavior decisions: expose the decision cascade — +active behaviors, per-operation verdicts, and the single decision that +determined each outcome — as inspectable, immutable records, so users +and tooling can ask why an attribute is mutable or immutable, visible +or concealed. +## Requirements +### Requirement: Attribute Explanation + +The `explain_attribute` function MUST return the decision trace for the +named attribute on the target: the active behaviors for each evaluated +level, and for each operation the applicable verdict. Every verdict MUST +carry exactly one decision, drawn from a closed hierarchy: permitted by +inapplicability (the governing behavior is inactive), permitted by the +omni marker, permitted by names membership, permitted by a predicate, +permitted by a regex, or prohibited (the behavior is active and no rule +permits). All three operations — assign, delete, and survey — MUST +apply the same precedence semantics: omni, then names membership, then +the first matching predicate, then the first matching regex. + +For class targets, the classes-level configuration MUST be evaluated. +For instance targets, the instances-level configuration MUST be +evaluated against the instance's class hierarchy. Survey verdicts MUST +explain the visibility of the supplied name only. Explanations MUST be +observational: they MUST NOT bypass or alter concealment or +immutability. + +Note: survey explanations describe the normalized first-match semantics. +The survey core currently yields a name once per matching rule; that +duplication is characterized as a defect and deferred to a separate +repair, after which core behavior and explanation semantics coincide. + +#### Scenario: Mutable attribute +- **WHEN** the active behaviors for the evaluated level lack + immutability +- **THEN** the assign and delete verdicts MUST be permitted by + inapplicability + +#### Scenario: Excluded by name +- **WHEN** the attribute name is in the exclusion names for the + operation's level +- **THEN** the verdict MUST be permitted by names, carrying the matched + name + +#### Scenario: Excluded by regex +- **WHEN** an exclusion regex fullmatches the attribute name +- **THEN** the verdict MUST be permitted by regex, carrying the pattern + text + +#### Scenario: Excluded by predicate +- **WHEN** an exclusion predicate matches the attribute name +- **THEN** the verdict MUST be permitted by predicate, carrying the + predicate's fully-qualified name, or `<anonymous>` when unavailable + +#### Scenario: Omni exclusion +- **WHEN** the exclusion names for assign or delete equal `'*'` +- **THEN** the verdict MUST be permitted by omni + +#### Scenario: Immutable attribute +- **WHEN** immutability is active and no rule permits the operation +- **THEN** the assign and delete verdicts MUST be prohibited + +#### Scenario: Concealed attribute +- **WHEN** concealment is active and no visibles rule matches the name +- **THEN** the survey verdict MUST be prohibited + +#### Scenario: Survey multi-rule precedence +- **WHEN** the attribute name matches a predicate and a regex and is not + in the exclusion names +- **THEN** the survey verdict MUST be permitted by the predicate, the + first rule under precedence, and MUST NOT carry the regex + +#### Scenario: Survey names precedence +- **WHEN** the attribute name is in the exclusion names for the survey + level and also matches a predicate or regex +- **THEN** the survey verdict MUST be permitted by names and predicates + and regexes MUST NOT be evaluated for that name + +#### Scenario: Survey omni +- **WHEN** concealment is active and the visibles names equal `'*'` +- **THEN** the survey verdict MUST be permitted by omni and predicates + and regexes MUST NOT be evaluated + +#### Scenario: Survey concealment inactive +- **WHEN** the active behaviors for the evaluated level lack concealment +- **THEN** the survey verdict MUST be permitted by inapplicability + +#### Scenario: Permissible derivation +- **WHEN** any verdict's decision is not prohibited +- **THEN** the verdict MUST report itself as permissible + +### Requirement: Decision Payload Distinction + +Decision payloads MUST be distinguished by type, so that a name which +coincides with a genus label or pattern text cannot be confused with +it. The names decision MUST carry the matched name; the predicate +decision MUST carry qualified-name text; the regex decision MUST carry +pattern text. Payloads MUST be plain text. + +#### Scenario: Names payload collision +- **WHEN** the explained attribute is literally named `regex` and is + permitted by names membership +- **THEN** the decision MUST be a names decision carrying the name + `regex`, distinct from any regex decision carrying pattern text + +### Requirement: Record Immutability + +The explanation records MUST be immutable: the decision hierarchy, the +verdicts, and `AttributeExplanation` MUST NOT permit field assignment, +and every nested collection they contain MUST be an immutable +structure. Verdict permissibility MUST be derived from the decision, +not stored. + +#### Scenario: Frozen records +- **WHEN** a field is assigned on any explanation record +- **THEN** an immutability error MUST be raised + +#### Scenario: Immutable nested collections +- **WHEN** the operations mapping, the behaviors mapping, or a payload + sequence is mutated +- **THEN** an error MUST be raised + +### Requirement: Internal Attribute Marking + +Explanations MUST mark attributes as internal according to +internal-name contributions carried by the machinery that constructed +the target: classcore's factory wiring contributes its framework-name +detector, the abstract-base wiring contributes the stdlib ABC machinery +set, and downstream factory-produced metaclasses contribute detectors +from their attribute namers. Contributions MUST be discovered by +consulting the target's metaclass method resolution order; a class +whose metaclass chain carries no matching contribution MUST NOT mark +the name, even when the name resembles a framework or machinery name. +The `_dynadoc_fragments_` attribute is a public declaration convention +and MUST NOT be marked internal. + +#### Scenario: Framework-internal attribute +- **WHEN** the explained name matches the classcore detector carried by + the target's metaclass chain +- **THEN** the explanation MUST be marked internal + +#### Scenario: Downstream framework attribute +- **WHEN** the target's metaclass was produced by a downstream factory + whose namer contributes a detector, and the explained name matches + that detector +- **THEN** the explanation MUST be marked internal + +#### Scenario: Machinery name without machinery +- **WHEN** a class whose metaclass chain lacks the abstract-base + contribution defines an attribute named like stdlib ABC machinery +- **THEN** the explanation MUST NOT mark the name internal + +#### Scenario: Inherited contribution across packages +- **WHEN** a class defined in one package derives from a base whose + metaclass carries a framework contribution +- **THEN** the explanation MUST apply that contribution to the derived + class + +#### Scenario: Instance target consults metaclass chain +- **WHEN** an instance target is explained and a contribution is + carried by the metaclass chain of the instance's type — discovered + through the metaclass MRO, not the class MRO of the instance's type +- **THEN** the explanation MUST apply that contribution + +#### Scenario: Shape-invalid lookalike +- **WHEN** an explained name shares a framework's prefix but fails the + grammar the framework's detector requires — a core outside the + framework's enumerated vocabulary (e.g. + `_classcore_not_a_generated_core_`), or a mangled form with a + malformed digest (wrong length, uppercase characters, or trailing + underscore) +- **THEN** the explanation MUST NOT mark the name internal + +#### Scenario: Declaration convention +- **WHEN** the explained name is `_dynadoc_fragments_` +- **THEN** the explanation MUST NOT mark the name internal + +#### Scenario: User attribute +- **WHEN** the explained name matches no contribution in the target's + metaclass chain +- **THEN** the explanation MUST NOT be marked internal diff --git a/documentation/changelog.rst b/documentation/changelog.rst index bbaff1f..d14c19e 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,50 @@ Release Notes .. towncrier release notes start +classcore 1.13 (2026-10-15) +=========================== + +Enhancements +------------ + +- Add attribute explanations: ``explain_attribute`` in + ``classcore.standard.explanations`` returns the decision trace for one + attribute of one target — active behaviors per evaluated level and, for + each operation, a verdict carrying its decision under precedence + semantics: permission by inapplicability, omni, names, predicate, or + regex, or prohibition. Decision payloads are typed per decision class, + explanation records are immutable with immutable nested collections, + and framework-owned and stdlib-machinery attributes are marked + internal. +- Attribute internal marking now follows metaclass-carried + contributions: the attribute namer's ``is_internal_name`` detector + marks framework names (grammar-constrained static stems and both + digest-mangled forms), the abstract-base wiring marks stdlib ABC + machinery names only where that machinery operates, and downstream + ``class_factory`` namers contribute their own detectors, so + ``explain_attribute`` reports downstream framework attributes + truthfully. ``is_internal_name`` takes the target and consults the + metaclass chain; lookalike names that fail the detector grammar stay + unmarked, and the public ``_dynadoc_fragments_`` declaration + convention is never marked. + + +Repairs +------- + +- Fix construction of function-local dataclass classes. Classes derived + from the dataclass base classes (``DataclassObject``, + ``DataclassObjectMutable``, ``DataclassProtocol``, and + ``DataclassProtocolMutable``) failed with ``TypeError`` when defined + inside a function body, because the construction marker could not be + recognized during dataclass slot reproduction. +- Permit CPython 3.15+ typing machinery dunder on immutable classes. + ``typing`` sets ``__typing_is_deprecated_inherited_runtime_protocol__`` + during class decoration and in the ``runtime_checkable`` decorator; the + class-level immutability behavior now exempts it, and the protocol + member snapshot excludes it from ``__protocol_attrs__``. + + classcore 1.12 (2026-08-15) =========================== diff --git a/documentation/examples/index.rst b/documentation/examples/index.rst index 2766596..3f830a3 100644 --- a/documentation/examples/index.rst +++ b/documentation/examples/index.rst @@ -28,5 +28,6 @@ Examples std-dataclasses std-protocols std-behaviors + std-explanations std-modules std-dynadoc diff --git a/documentation/examples/std-explanations.rst b/documentation/examples/std-explanations.rst new file mode 100644 index 0000000..ee58e79 --- /dev/null +++ b/documentation/examples/std-explanations.rst @@ -0,0 +1,155 @@ +.. vim: set fileencoding=utf-8: +.. -*- coding: utf-8 -*- +.. +--------------------------------------------------------------------------+ + | | + | Licensed under the Apache License, Version 2.0 (the "License"); | + | you may not use this file except in compliance with the License. | + | You may obtain a copy of the License at | + | | + | http://www.apache.org/licenses/LICENSE-2.0 | + | | + | Unless required by applicable law or agreed to in writing, software | + | distributed under the License is distributed on an "AS IS" BASIS, | + | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | + | See the License for the specific language governing permissions and | + | limitations under the License. | + +--------------------------------------------------------------------------+ + + +******************************************************************************* +Attribute Explanations +******************************************************************************* + +The :py:func:`classcore.standard.explanations.explain_attribute` function explains why an +attribute of a class or an instance is mutable or immutable and visible or +concealed. It returns an explanation record which carries, for each operation +— assignment, deletion, and survey — a verdict with the rule which decided +the outcome. Explanations are observational: they neither bypass nor alter the +behaviors they describe. + +.. doctest:: Standard.Explanations + + >>> import classcore.standard as ccstd + + +Explaining Immutability +=============================================================================== + +Consider a decorated class with selective mutability. Explaining an excluded +attribute name reports the decision which permitted the operation, with its +payload carried by a typed record. + +.. doctest:: Standard.Explanations + + >>> @ccstd.with_standard_behaviors( mutables = ( 'count', ) ) + ... class Counter: + ... count: int = 0 + ... + >>> explanation = ccstd.explain_attribute( Counter( ), 'count' ) + >>> assign = explanation.operations[ 'assign' ] + >>> assign.permissible + True + >>> assign.decision.name + 'count' + +Explaining a non-excluded attribute reports that the operation is +prohibited. + +.. doctest:: Standard.Explanations + + >>> explanation = ccstd.explain_attribute( Counter( ), 'total' ) + >>> assign = explanation.operations[ 'assign' ] + >>> assign.permissible + False + >>> type( assign.decision ).__name__ + 'Prohibit' + + +Explaining Concealment +=============================================================================== + +Concealment verdicts follow precedence semantics, like every operation: the +first matching visibility rule decides the survey verdict. + +.. doctest:: Standard.Explanations + + >>> import re + >>> @ccstd.with_standard_behaviors( + ... visibles = ( re.compile( 'pub.*' ), ) ) + ... class Ledger: + ... public_total = 1 + ... _secret = 2 + ... + >>> explanation = ccstd.explain_attribute( Ledger( ), 'public_total' ) + >>> survey = explanation.operations[ 'survey' ] + >>> survey.permissible + True + >>> survey.decision.pattern + 'pub.*' + + >>> explanation = ccstd.explain_attribute( Ledger( ), '_secret' ) + >>> explanation.operations[ 'survey' ].permissible + False + + +Internal Attributes +=============================================================================== + +Framework attribute names mark as internal wherever the framework's +machinery built the class. Standard library machinery names, like the +ABC caches, mark only where that machinery actually operates — a class +without ABC machinery keeps such a name unmarked. + +.. doctest:: Standard.Explanations + + >>> import hashlib + >>> digest = hashlib.sha256( b'x' ).hexdigest( ) + >>> explanation = ccstd.explain_attribute( + ... Ledger( ), f"_classcore_class_in_progress_{digest}" ) + >>> explanation.internal + True + >>> explanation = ccstd.explain_attribute( Ledger( ), '_abc_cache' ) + >>> explanation.internal + False + + >>> class Registry( ccstd.AbstractObject ): + ... pass + ... + >>> explanation = ccstd.explain_attribute( Registry( ), '_abc_cache' ) + >>> explanation.internal + True + >>> explanation = ccstd.explain_attribute( Ledger( ), 'public_total' ) + >>> explanation.internal + False + + +Record Immutability +=============================================================================== + +Explanation records are themselves immutable, including every nested +collection. + +.. doctest:: Standard.Explanations + + >>> explanation = ccstd.explain_attribute( Ledger( ), 'public_total' ) + >>> explanation.name = 'other' + Traceback (most recent call last): + ... + classcore.exceptions.AttributeImmutability: Could not assign or delete attribute 'name' on instance of class 'classcore.standard.explanations.AttributeExplanation'. + + +Summaries +=============================================================================== + +The explanation record renders a human-readable summary of the whole +decision trace, so digging through attributes and mappings is only +necessary for programmatic use. + +.. doctest:: Standard.Explanations + + >>> print( ccstd.explain_attribute( Ledger( ), 'public_total' ) ) + 'public_total' on instance of class 'builtins.Ledger' + behaviors: concealment, immutability (instance) + assign: prohibited (no permitting rule) + delete: prohibited (no permitting rule) + survey: permitted by regex 'pub.*' diff --git a/pyproject.toml b/pyproject.toml index c96b148..02067a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ description = 'Foundational class factories and decorators.' dynamic = [ 'version' ] license = 'Apache-2.0' readme = { 'file' = 'README.rst', 'content-type' = 'text/x-rst' } -requires-python = '>= 3.10' +requires-python = '>= 3.12' dependencies = [ 'dynadoc ~= 1.4', 'typing-extensions', @@ -26,13 +26,11 @@ classifiers = [ # https://pypi.org/classifiers 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3 :: Only', # --- BEGIN: Injected by Copier --- - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', + 'Programming Language :: Python :: 3.15', 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', # --- END: Injected by Copier --- 'Topic :: Software Development', ] @@ -91,7 +89,7 @@ strict-naming = false # --- BEGIN: Injected by Copier --- # --- END: Injected by Copier --- [tool.hatch.envs.default] -python = '3.10' +python = '3.12' [tool.hatch.envs.develop] description = ''' Development environment. ''' builder = true @@ -127,6 +125,12 @@ docsgen = [ """sphinx-build -a -d .auxiliary/caches/sphinx --quiet \ documentation .auxiliary/artifacts/sphinx-html""", ] +fragments = [ + """python .auxiliary/scripts/validate-fragments.py""", +] +module-sizes = [ + """python .auxiliary/scripts/validate-module-sizes.py""", +] linters = [ """ruff check --quiet sources documentation tests""", # """vibelinter check""", @@ -134,6 +138,8 @@ linters = [ # --- END: Injected by Copier --- """isort --check-only --diff sources tests""", """pyright sources""", + 'fragments', + 'module-sizes', ] packagers = [ """hatch build""", @@ -170,12 +176,10 @@ description = ''' Quality assurance environment. ''' template = 'develop' [[tool.hatch.envs.qa.matrix]] python = [ - '3.10', - '3.11', '3.12', '3.13', '3.14', - 'pypy3.11', + '3.15', ] # [tool.hatch.metadata] # allow-direct-references = true diff --git a/sources/classcore/README.md b/sources/classcore/README.md index de4bb2a..a10a5fc 100644 --- a/sources/classcore/README.md +++ b/sources/classcore/README.md @@ -297,6 +297,7 @@ sources/ ├── classes.py # Standard metaclasses and base classes ├── decorators.py # Standard class decorators ├── dynadoc.py # Documentation configuration + ├── explanations.py # Attribute behavior explanations ├── modules.py # Module reclassification utilities └── nomina.py # Standard-specific naming types ``` @@ -378,6 +379,20 @@ sources/ - `survey_visible_attributes`: Filter attributes for `dir()` - Verifier processing for names, regexes, and predicates +`explanations.py` +: Explanations of attribute behavior decisions: + + - `explain_attribute`: Decision trace for one attribute of one target + - `AttributeExplanation` and the `Verdict` records (`AssignVerdict`, + `DeleteVerdict`, `SurveyVerdict`) with derived permissibility + - The closed `Decision` hierarchy: `PermitByInapplicability`, + `PermitByOmni`, `PermitByNames`, `PermitByPredicate`, + `PermitByRegex`, `Prohibit`, with typed payloads + - Internal marking via metaclass-carried contributions: the namer + detector for framework names, the abstract-base wiring for stdlib + ABC machinery (only where that machinery operates), and downstream + factory namers' detectors for downstream names + `classes.py` : Standard metaclasses and base classes: diff --git a/sources/classcore/__/nomina.py b/sources/classcore/__/nomina.py index fcf14ac..cd768ea 100644 --- a/sources/classcore/__/nomina.py +++ b/sources/classcore/__/nomina.py @@ -32,5 +32,64 @@ package_name = __name__.split( '.', maxsplit = 1 )[ 0 ] -def calculate_attrname( level: str, core: str ) -> str: - return f"_{package_name}_{level}_{core}_" +_levels = frozenset( ( 'class', 'classes', 'instance', 'instances' ) ) +_cores = frozenset( ( + 'behaviors', + 'construction_arguments', + 'dynadoc_configuration', + 'in_progress', + 'mutables_names', 'mutables_predicates', 'mutables_regexes', + 'visibles_names', 'visibles_predicates', 'visibles_regexes', + 'assigner_core', 'deleter_core', 'surveyor_core' ) ) +_hex_digits = frozenset( '0123456789abcdef' ) +_digest_length = 64 +_mangled_cores = frozenset( ( 'class_behaviors', 'class_in_progress' ) ) + + +class AttrnameCalculator: + ''' Calculates framework attribute names from level and core. + + Reference implementation of the attribute namer convention: + call instances to produce names, and consult ``is_internal_name`` + to detect names this framework generated. Downstream packages + may subclass (or otherwise provide callables exposing the same + optional detector) to contribute their own internal names. + ''' + + def is_internal_name( self, name: str ) -> bool: + prefix = f"_{package_name}_" + if not name.startswith( prefix ): + return False + body = name[ len( prefix ): ] + # Static stems: _{package}_{level}_{core}_, trailing underscore. + # The class-level behaviors and in_progress stems are stored + # mangled only, so they are excluded from static acceptance. + if body.endswith( '_' ): + level, _, core = body[ :-1 ].partition( '_' ) + if level in _levels and core in _cores: + return not ( + 'class' == level + and f"{level}_{core}" in _mangled_cores ) + return False + # Mangled forms: one of the mangled cores + 64 lowercase hex, + # no trailing underscore. The mangling accessors append digests + # to these stems only. + for mangled_core in _mangled_cores: + mangled_prefix = f"{mangled_core}_" + if body.startswith( mangled_prefix ): + suffix = body[ len( mangled_prefix ): ] + return ( + _digest_length == len( suffix ) + and all( c in _hex_digits for c in suffix ) ) + return False + + def __call__( self, level: str, core: str ) -> str: + return f"_{package_name}_{level}_{core}_" + + +calculate_attrname = AttrnameCalculator( ) + + +def calculate_contribution_name( ) -> str: + ''' Returns metaclass attribute name holding internal-name detectors. ''' + return f"_{package_name}_internal_names_" diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index e970577..b8c3179 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.12' +__version__ = '1.13' standard.finalize_module( diff --git a/sources/classcore/factories.py b/sources/classcore/factories.py index 42b00b9..ea4d0b5 100644 --- a/sources/classcore/factories.py +++ b/sources/classcore/factories.py @@ -54,12 +54,20 @@ def construct( # noqa: PLR0913, PLR0917 # Some decorators create new classes, which invokes this method again. # Short-circuit to prevent recursive decoration and other tangles. progress_name = attributes_namer( 'class', 'in_progress' ) - progress_name_m = _utilities.mangle_name( cls, progress_name ) - in_progress = getattr( cls, progress_name_m, False ) + progress_names = _utilities.survey_mangled_names( cls, progress_name ) + in_progress = any( + getattr( cls, key, False ) for key in progress_names ) if in_progress: return cls + progress_name_m = _utilities.mangle_name( cls, progress_name ) setattr( cls, progress_name_m, True ) for postprocessor in postprocessors: postprocessor( cls, decorators_ ) cls = _decorators.apply_decorators( cls, decorators_ ) + # Decoration may have replaced the class. Clear every marker + # variant, including ones inherited from the original namespace, + # so initialization completes regardless of qualname restoration. + progress_names = _utilities.survey_mangled_names( cls, progress_name ) + for progress_name_ in progress_names: + setattr( cls, progress_name_, False ) setattr( cls, progress_name_m, False ) return cls @@ -81,10 +89,11 @@ def initialize( ''' Initializes class, applying hooks. ''' superf( *posargs, **nomargs ) progress_name = attributes_namer( 'class', 'in_progress' ) - progress_name_m = _utilities.mangle_name( cls, progress_name ) - in_progress = getattr( cls, progress_name_m, False ) + progress_names = _utilities.survey_mangled_names( cls, progress_name ) + in_progress = any( + getattr( cls, key, False ) for key in progress_names ) if in_progress: return # If non-empty, then not top-level. - delattr( cls, progress_name_m ) + for progress_name_m in progress_names: delattr( cls, progress_name_m ) for completer in completers: completer( cls ) return initialize diff --git a/sources/classcore/standard/__.py b/sources/classcore/standard/__.py index 81cff04..f6dd15f 100644 --- a/sources/classcore/standard/__.py +++ b/sources/classcore/standard/__.py @@ -20,7 +20,7 @@ ''' Common constants, imports, and utilities. ''' -# ruff: noqa: F403 +# ruff: noqa: F403, F405 from ..__ import * @@ -40,3 +40,67 @@ def provide_error_class( name: str ) -> type[ Exception ]: mutables_default = ( ) visibles_default = ( is_public_identifier, ) + +# Standard library ABC machinery attribute names which the class-level +# immutability behavior permits. The C _abc implementation writes these +# cache attributes through direct dict access, so they must remain +# assignable on standard classes. Also used to mark such names as +# internal in attribute explanations. +abc_class_mutables = ( + '_abc_cache', + '_abc_negative_cache', + '_abc_negative_cache_version', + '_abc_registry', +) +# Standard library typing machinery attribute names which the +# class-level immutability behavior permits on protocol classes. +# 'typing' sets these protocol bookkeeping attributes during class +# decoration: '_is_runtime_protocol' and '__non_callable_proto_members__' +# in the 'runtime_checkable' decorator, and +# '__typing_is_deprecated_inherited_runtime_protocol__' (CPython 3.15+) +# during class decoration and in 'runtime_checkable'. Without this +# exemption, decorating a runtime protocol which inherits runtime +# checkability fails. Also used to mark such names as internal in +# attribute explanations. +typing_class_mutables = ( + '_is_runtime_protocol', + '__non_callable_proto_members__', + '__typing_is_deprecated_inherited_runtime_protocol__', # Python 3.15+ +) + + +def is_same_detector( + detector: cabc.Callable[ [ str ], bool ], + other: cabc.Callable[ [ str ], bool ], /, +) -> bool: + ''' Returns whether two detector references are the same detector. + + Bound methods are the same detector only when both the + underlying function and the bound instance are identical, so + distinct namer instances with distinct configured behavior keep + separate contributions. Plain callables compare by identity. + ''' + if detector is other: return True + function = getattr( detector, '__func__', None ) + other_function = getattr( other, '__func__', None ) + if function is None or other_function is None: return False + return ( + function is other_function + and getattr( detector, '__self__', None ) + is getattr( other, '__self__', None ) ) + + +def augment_internal_names( + cls: type, /, detector: cabc.Callable[ [ str ], bool ] +) -> None: + ''' Adds an internal-name detector to existing contributions. + + Private wiring point for machinery mixins: the abstract-base + metaclass adds the stdlib machinery set after factory + decoration, without expanding public factory arguments. + ''' + detectors = getattr( cls, calculate_contribution_name( ), ( ) ) + if any( is_same_detector( detector, d ) for d in detectors ): return + setattr( + cls, calculate_contribution_name( ), + ( *detectors, detector ) ) diff --git a/sources/classcore/standard/__init__.py b/sources/classcore/standard/__init__.py index 30a8576..488c616 100644 --- a/sources/classcore/standard/__init__.py +++ b/sources/classcore/standard/__init__.py @@ -49,4 +49,5 @@ from .classes import * from .decorators import * +from .explanations import * from .modules import * diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index b563b3d..4d05d1d 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -58,34 +58,17 @@ def assign_attribute_if_mutable( # noqa: PLR0913 value: __.typx.Any, ) -> None: ''' Assigns attribute if it is mutable, else raises error. ''' - leveli = 'instance' if level == 'instances' else level - behaviors_name = attributes_namer( leveli, 'behaviors' ) - behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) + behaviors = survey_active_behaviors( + obj, attributes_namer = attributes_namer, level = level ) if _nomina.immutability_label not in behaviors: ligation( name, value ) return - names_name = attributes_namer( level, 'mutables_names' ) - names: _nomina.BehaviorExclusionNamesOmni = ( - getattr( obj, names_name, frozenset( ) ) ) - if names == '*' or name in names: + rule = provide_permitter_if_applicable( + obj, attributes_namer = attributes_namer, + level = level, basename = 'mutables', name = name ) + if rule is not None: ligation( name, value ) return - predicates_name = attributes_namer( level, 'mutables_predicates' ) - predicates: _nomina.BehaviorExclusionPredicates = ( - getattr( obj, predicates_name, ( ) ) ) - for predicate in predicates: - if predicate( name ): - # TODO? Cache predicate hit. - ligation( name, value ) - return - regexes_name = attributes_namer( level, 'mutables_regexes' ) - regexes: _nomina.BehaviorExclusionRegexes = ( - getattr( obj, regexes_name, ( ) ) ) - for regex in regexes: - if regex.fullmatch( name ): - # TODO? Cache regex hit. - ligation( name, value ) - return target = _utilities.describe_object( obj ) raise error_class_provider( 'AttributeImmutability' )( name, target ) @@ -99,36 +82,142 @@ def delete_attribute_if_mutable( # noqa: PLR0913 name: str, ) -> None: ''' Deletes attribute if it is mutable, else raises error. ''' - leveli = 'instance' if level == 'instances' else level - behaviors_name = attributes_namer( leveli, 'behaviors' ) - behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) + behaviors = survey_active_behaviors( + obj, attributes_namer = attributes_namer, level = level ) if _nomina.immutability_label not in behaviors: ligation( name ) return - names_name = attributes_namer( level, 'mutables_names' ) - names: _nomina.BehaviorExclusionNamesOmni = ( - getattr( obj, names_name, frozenset( ) ) ) - if names == '*' or name in names: + rule = provide_permitter_if_applicable( + obj, attributes_namer = attributes_namer, + level = level, basename = 'mutables', name = name ) + if rule is not None: ligation( name ) return - predicates_name = attributes_namer( level, 'mutables_predicates' ) + target = _utilities.describe_object( obj ) + raise error_class_provider( 'AttributeImmutability' )( name, target ) + + +def determine_relevant_permitters( + objct: object, /, *, + attributes_namer: _nomina.AttributesNamer, + level: str, + basename: str, + name: str, +) -> tuple[ tuple[ str, __.typx.Any ], ... ]: + ''' Returns exclusion rules matching name, in evaluation order. + + A names match short-circuits, matching the survey core's + historical control flow: predicates and regexes are not evaluated + for a name found in the exclusion names. Otherwise a name may + match any number of predicates and any number of regexes. The + wildcard is not considered here; callers handle its + short-circuit separately. + + Rules pair a kind ('names', 'predicate', 'regex') with the raw + matched verifier (name, predicate, or regex). Matching never + renders diagnostic text: enforcement and survey consult kind + and presence only, so exotic callables cannot disrupt them. + Render verifiers with 'render_verifier_text' at the record + layer when producing explanation records. + ''' + names_name = attributes_namer( level, f"{basename}_names" ) + names: _nomina.BehaviorExclusionNamesOmni = ( + getattr( objct, names_name, frozenset( ) ) ) + if name in names: return ( ( 'names', name ), ) + predicates_name = attributes_namer( level, f"{basename}_predicates" ) predicates: _nomina.BehaviorExclusionPredicates = ( - getattr( obj, predicates_name, ( ) ) ) + getattr( objct, predicates_name, ( ) ) ) + matched: list[ tuple[ str, __.typx.Any ] ] = [ + ( 'predicate', predicate ) + for predicate in predicates if predicate( name ) ] + regexes_name = attributes_namer( level, f"{basename}_regexes" ) + regexes: _nomina.BehaviorExclusionRegexes = ( + getattr( objct, regexes_name, ( ) ) ) + matched.extend( + ( 'regex', regex ) + for regex in regexes if regex.fullmatch( name ) ) + return tuple( matched ) + + +def provide_permitter_if_applicable( + objct: object, /, *, + attributes_namer: _nomina.AttributesNamer, + level: str, + basename: str, + name: str, +) -> __.typx.Optional[ tuple[ str, __.typx.Any ] ]: + ''' Returns first exclusion rule permitting operation, else None. + + Rules apply by precedence: omni or names membership, then + the first matching predicate, then the first matching regex. + Each rule pairs a kind ('omni', 'names', 'predicate', 'regex') + with the raw matched verifier. Matching never renders + diagnostic text: enforcement consults kind and presence only, + so exotic callables cannot disrupt it. Render the verifier + with 'render_verifier_text' at the record layer when producing + explanation records. + ''' + names_name = attributes_namer( level, f"{basename}_names" ) + names: _nomina.BehaviorExclusionNamesOmni = ( + getattr( objct, names_name, frozenset( ) ) ) + if names == '*': return ( 'omni', '*' ) + if name in names: return ( 'names', name ) + predicates_name = attributes_namer( level, f"{basename}_predicates" ) + predicates: _nomina.BehaviorExclusionPredicates = ( + getattr( objct, predicates_name, ( ) ) ) for predicate in predicates: if predicate( name ): - # TODO? Cache predicate hit. - ligation( name ) - return - regexes_name = attributes_namer( level, 'mutables_regexes' ) + return ( 'predicate', predicate ) + regexes_name = attributes_namer( level, f"{basename}_regexes" ) regexes: _nomina.BehaviorExclusionRegexes = ( - getattr( obj, regexes_name, ( ) ) ) + getattr( objct, regexes_name, ( ) ) ) for regex in regexes: if regex.fullmatch( name ): - # TODO? Cache regex hit. - ligation( name ) - return - target = _utilities.describe_object( obj ) - raise error_class_provider( 'AttributeImmutability' )( name, target ) + return ( 'regex', regex ) + return None + + +def render_verifier_text( verifier: __.typx.Any, / ) -> str: + ''' Returns stable detail text for an exclusion verifier. + + Regexes render as pattern text; predicates render as + fully-qualified name, with an anonymous fallback when no + qualified name is available. Strings do not reach this + function; they are classified as names before rendering. A + non-string 'pattern' attribute is not trusted as text; such + verifiers fall through to qualified-name rendering. Hostile + metadata (raising descriptors, custom '__getattribute__') + yields '<unrenderable>' rather than raising, so diagnostic + rendering never disrupts its callers. + + Note: the inspect utilities were considered and rejected here; + they resolve modules to objects and provide no qualified-name + rendering with an anonymous fallback, while this getattr chain + stays defensive for exotic callables (e.g., partials). + ''' + try: + text = _render_verifier_text( verifier ) + except Exception: + text = '<unrenderable>' + return text + + +def survey_active_behaviors( + objct: object, /, *, + attributes_namer: _nomina.AttributesNamer, + level: str, +) -> __.cabc.Set[ str ]: + ''' Returns active behaviors set for object at level. + + This is the shared entry point of the behavior decision cascade: + every core first determines whether the governing behavior is + active before consulting exclusion configuration. + ''' + leveli = 'instance' if level == 'instances' else level + behaviors_name = attributes_namer( leveli, 'behaviors' ) + behaviors: __.cabc.Set[ str ] = ( + _utilities.getattr0( objct, behaviors_name, frozenset( ) ) ) + return behaviors def survey_visible_attributes( @@ -139,35 +228,22 @@ def survey_visible_attributes( ) -> __.cabc.Iterable[ str ]: ''' Returns sequence of visible attributes. ''' names_base = ligation( ) - leveli = 'instance' if level == 'instances' else level - behaviors_name = attributes_namer( leveli, 'behaviors' ) - behaviors = _utilities.getattr0( obj, behaviors_name, frozenset( ) ) + behaviors = survey_active_behaviors( + obj, attributes_namer = attributes_namer, level = level ) if _nomina.concealment_label not in behaviors: return names_base names_name = attributes_namer( level, 'visibles_names' ) names: _nomina.BehaviorExclusionNamesOmni = ( getattr( obj, names_name, frozenset( ) ) ) if names == '*': return names_base # pragma: no branch - regexes_name = attributes_namer( level, 'visibles_regexes' ) - regexes: _nomina.BehaviorExclusionRegexes = ( - getattr( obj, regexes_name, ( ) ) ) - predicates_name = attributes_namer( level, 'visibles_predicates' ) - predicates: _nomina.BehaviorExclusionPredicates = ( - getattr( obj, predicates_name, ( ) ) ) names_: list[ str ] = [ ] for name in names_base: - if name in names: - names_.append( name ) - continue - for predicate in predicates: - if predicate( name ): - # TODO? Cache predicate hit. - names_.append( name ) - continue - for regex in regexes: - if regex.fullmatch( name ): - # TODO? Cache regex hit. - names_.append( name ) - continue + matched = determine_relevant_permitters( + obj, attributes_namer = attributes_namer, + level = level, basename = 'visibles', name = name ) + # Current behavior: one append per matched rule, so a name + # matching several rules is yielded several times. Preserved + # deliberately; normalization is a separate repair. + names_.extend( name for rule in matched ) return names_ @@ -412,3 +488,21 @@ def _deduplicate_merge_sequences( if addend in augends_: continue result.append( addend ) return tuple( result ) + + +def _render_verifier_text( verifier: __.typx.Any, / ) -> str: + ''' Returns detail text for an exclusion verifier, possibly raising. + + Total rendering lives in 'render_verifier_text', which maps + hostile-metadata failures to the documented fallback. This + partial function holds the getattr chain so the wrapper has + no returns inside its guarded block. + ''' + pattern = getattr( verifier, 'pattern', None ) + if isinstance( pattern, str ): return pattern + qualname = getattr( verifier, '__qualname__', None ) + if qualname is None: return '<anonymous>' + module = getattr( verifier, '__module__', None ) + if module is None or '<locals>' in qualname: + return qualname + return f"{module}.{qualname}" diff --git a/sources/classcore/standard/classes.py b/sources/classcore/standard/classes.py index 219446e..ae3498f 100644 --- a/sources/classcore/standard/classes.py +++ b/sources/classcore/standard/classes.py @@ -27,14 +27,6 @@ from . import nomina as _nomina -_abc_class_mutables = ( - '_abc_cache', - '_abc_negative_cache', - '_abc_negative_cache_version', - '_abc_registry', - '_is_runtime_protocol', - '__non_callable_proto_members__', -) _protocol_cls_set = frozenset( { __.typx.Protocol } ) # Attributes never considered declared protocol members: typing internals, # class-creation machinery, and framework attributes declared in classcore @@ -47,7 +39,12 @@ # Injected during class creation by ABC, Generic, and typing machinery. # '__init__' is '_no_init' on protocol bases; as a structural member it # is vacuous since every candidate object has an '__init__'. + # '__typing_is_deprecated_inherited_runtime_protocol__' is set by + # CPython 3.15+ typing on protocol classes which inherit runtime + # checkability; as a structural member it is vacuous since it records + # machinery provenance rather than a callable member. '__abstractmethods__', '__parameters__', '__init__', '__orig_bases__', + '__typing_is_deprecated_inherited_runtime_protocol__', # Python 3.15+ } ) _protocol_attr_prefixes = ( '_classcore_', '_abc_' ) # Standard Python attributes present in every pre-decoration namespace. @@ -130,6 +127,10 @@ class AbstractClass( Class, __.abc.ABCMeta ): registration) via a diamond hierarchy. `Class` itself remains backed by plain `type`, so ABC machinery applies only where this metaclass is used. + + Contributes the stdlib ABC and typing machinery names as internal + for the classes it builds, mirroring the class_mutables exemption + scoping. ''' _dynadoc_fragments_ = ( @@ -313,7 +314,7 @@ class ObjectMutable( metaclass = Class, instances_mutables = '*' ): class AbstractObject( metaclass = AbstractClass, - class_mutables = _abc_class_mutables, + class_mutables = __.abc_class_mutables, ): ''' Base class for abstract classes with standard behaviors. @@ -348,7 +349,8 @@ class DataclassObjectMutable( metaclass = DataclassMutable ): class Protocol( __.typx.Protocol, metaclass = ProtocolClass, - class_mutables = _abc_class_mutables, + class_mutables = ( + *__.abc_class_mutables, *__.typing_class_mutables ), ): ''' Standard base protocol class. ''' @@ -361,7 +363,8 @@ class Protocol( class ProtocolMutable( __.typx.Protocol, metaclass = ProtocolClass, - class_mutables = _abc_class_mutables, + class_mutables = ( + *__.abc_class_mutables, *__.typing_class_mutables ), instances_mutables = '*', ): ''' Base protocol class with mutable instance attributes. ''' @@ -375,7 +378,8 @@ class ProtocolMutable( class DataclassProtocol( __.typx.Protocol, metaclass = ProtocolDataclass, - class_mutables = _abc_class_mutables, + class_mutables = ( + *__.abc_class_mutables, *__.typing_class_mutables ), ): ''' Standard base protocol dataclass. ''' @@ -388,7 +392,8 @@ class DataclassProtocol( class DataclassProtocolMutable( __.typx.Protocol, metaclass = ProtocolDataclassMutable, - class_mutables = _abc_class_mutables, + class_mutables = ( + *__.abc_class_mutables, *__.typing_class_mutables ), ): ''' Base protocol dataclass with mutable instance attributes. ''' @@ -434,3 +439,12 @@ def greet( self ) -> str: _canary_abstract: int = _CanaryAbstractObject( ).provide( ) _canary_protocol_iface: _CanaryProtocolUse = _CanaryProtocolImpl( ) _canary_greeting: str = _canary_protocol_iface.greet( ) + + +def _is_stdlib_machinery_name( name: str ) -> bool: + return ( + name in __.abc_class_mutables + or name in __.typing_class_mutables ) + + +__.augment_internal_names( AbstractClass, _is_stdlib_machinery_name ) diff --git a/sources/classcore/standard/decorators.py b/sources/classcore/standard/decorators.py index f515138..56c4055 100644 --- a/sources/classcore/standard/decorators.py +++ b/sources/classcore/standard/decorators.py @@ -47,8 +47,10 @@ def prepare_dataclass_for_instances( ''' Annotates dataclass in support of instantiation machinery. ''' annotations = __.inspect.get_annotations( cls ) behaviors_name = attributes_namer( 'instance', 'behaviors' ) - # TODO: Only use mangling if not slotted. - # behaviors_name_ = _utilities.mangle_name( cls, behaviors_name ) + # NOTE: The behaviors field is a dataclass field, so its name must + # stay unmangled; mangling would desynchronize the field from + # the slot or class attribute installed by the dataclass + # machinery, which addresses it by its plain name. behaviors_name_ = behaviors_name annotations[ behaviors_name_ ] = set[ str ] setattr( cls, '__annotations__', annotations ) # in case of absence @@ -83,6 +85,30 @@ def apply_cfc_core_functions( setattr( clscls, core_aname, core_function ) +def apply_internal_names( + cls: type, /, + attributes_namer: _nomina.AttributesNamer, +) -> None: + ''' Records the namer's internal-name contribution, if any. + + Works for metaclasses (contributions inherit to every class the + metaclass builds) and for decorator-path classes (contributions + ride the class itself, since plain decorated classes have no + custom metaclass). Parent wiring may already contribute the + same detector (e.g., a diamond re-running the factory with the + same namer); keep one of each by identity. + ''' + detector: __.typx.Optional[ _nomina.InternalNameDetector ] = ( + getattr( attributes_namer, 'is_internal_name', None ) ) + if detector is None: return + detectors: _nomina.InternalNameDetectors = getattr( + cls, __.calculate_contribution_name( ), ( ) ) + if any( __.is_same_detector( detector, d ) for d in detectors ): return + setattr( + cls, __.calculate_contribution_name( ), + ( *detectors, detector ) ) + + def apply_cfc_dynadoc_configuration( clscls: type[ __.T ], /, attributes_namer: _nomina.AttributesNamer, @@ -192,6 +218,8 @@ def decorate( clscls: type[ __.T ] ) -> type[ __.T ]: assigner_core = assigner_core, deleter_core = deleter_core, surveyor_core = surveyor_core ) + apply_internal_names( + clscls, attributes_namer = attributes_namer ) apply_cfc_dynadoc_configuration( clscls, attributes_namer = attributes_namer, @@ -243,6 +271,8 @@ def produce_instances_inception_decorator( # noqa: PLR0913, PLR0917 surveyor = _behaviors.survey_visible_attributes ) def decorate( cls: type[ __.U ] ) -> type[ __.U ]: + apply_internal_names( + cls, attributes_namer = attributes_namer ) for core_name in ( 'assigner', 'deleter', 'surveyor' ): core_function = _behaviors.access_core_function( cls, diff --git a/sources/classcore/standard/explanations.py b/sources/classcore/standard/explanations.py new file mode 100644 index 0000000..62d270b --- /dev/null +++ b/sources/classcore/standard/explanations.py @@ -0,0 +1,253 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Explanations of attribute behavior decisions. ''' + + +from .. import utilities as _utilities +from . import __ +from . import behaviors as _behaviors +from . import decorators as _decorators +from . import nomina as _nomina + + +_framework_names_prefix = '_classcore_' + + +@_decorators.dataclass_with_standard_behaviors( ) +class Decision: + ''' Decision which one operation reached for one attribute. + + The decision subclasses form a closed hierarchy; the decision + type is the discriminant. Verdict permissibility is derived + from the decision type. + ''' + + +@_decorators.dataclass_with_standard_behaviors( ) +class PermitByInapplicability( Decision ): + ''' Permission because the governing behavior is inactive. ''' + + +@_decorators.dataclass_with_standard_behaviors( ) +class PermitByOmni( Decision ): + ''' Permission by the omni marker, which permits every name. ''' + + +@_decorators.dataclass_with_standard_behaviors( ) +class PermitByNames( Decision ): + ''' Permission by exclusion names membership. ''' + name: str + + +@_decorators.dataclass_with_standard_behaviors( ) +class PermitByPredicate( Decision ): + ''' Permission by an exclusion predicate. ''' + predicate: str + + +@_decorators.dataclass_with_standard_behaviors( ) +class PermitByRegex( Decision ): + ''' Permission by an exclusion regex. ''' + pattern: str + + +@_decorators.dataclass_with_standard_behaviors( ) +class Prohibit( Decision ): + ''' Prohibition: the behavior is active and no rule permits. ''' + + +@_decorators.dataclass_with_standard_behaviors( ) +class Verdict: + ''' Verdict for one operation on one attribute. ''' + operation: str + decision: Decision + + @property + def permissible( self ) -> bool: + ''' Returns whether the operation is permissible. ''' + return not isinstance( self.decision, Prohibit ) + + +@_decorators.dataclass_with_standard_behaviors( ) +class AssignVerdict( Verdict ): + ''' Verdict for attribute assignment. ''' + + +@_decorators.dataclass_with_standard_behaviors( ) +class DeleteVerdict( Verdict ): + ''' Verdict for attribute deletion. ''' + + +@_decorators.dataclass_with_standard_behaviors( ) +class SurveyVerdict( Verdict ): + ''' Verdict for attribute visibility in survey results. ''' + + +@_decorators.dataclass_with_standard_behaviors( ) +class AttributeExplanation: + ''' Decision trace for one attribute of one target. ''' + target: str + name: str + behaviors: __.cabc.Mapping[ str, __.cabc.Set[ str ] ] + operations: __.cabc.Mapping[ str, Verdict ] + internal: bool + + def __repr__( self ) -> str: + ''' Returns summary of the decision trace. ''' + lines = [ + f"{self.name!r} on {self.target}" + + ( ' [internal]' if self.internal else '' ) ] + for level, labels in self.behaviors.items( ): + lines.append( + f"behaviors: {', '.join( sorted( labels ) )}" + + f" ({level})" ) + for operation in ( 'assign', 'delete', 'survey' ): + verdict = self.operations.get( operation ) + if verdict is None: continue # pragma: no cover + lines.append( f"{operation}: {self._render( verdict.decision )}" ) + return '\n'.join( lines ) + + def _render( self, decision: Decision, / ) -> str: + if isinstance( decision, PermitByInapplicability ): + return 'permitted (behavior inapplicable)' + if isinstance( decision, PermitByOmni ): + return "permitted by omni '*'" + if isinstance( decision, PermitByNames ): + return f"permitted by names {decision.name!r}" + if isinstance( decision, PermitByPredicate ): + return f"permitted by predicate {decision.predicate!r}" + if isinstance( decision, PermitByRegex ): + return f"permitted by regex {decision.pattern!r}" + if isinstance( decision, Prohibit ): + return 'prohibited (no permitting rule)' + raise ValueError( type( decision ) ) + + +def explain_attribute( + target: object, name: str, /, *, + attributes_namer: _nomina.AttributesNamer = __.calculate_attrname, +) -> AttributeExplanation: + ''' Returns decision trace for attribute name on target. + + All operations follow precedence semantics: omni, then names + membership, then the first matching predicate, then the first + matching regex; prohibition when the governing behavior is + active and nothing permits; permission by inapplicability when + the behavior is inactive. For class targets, the classes-level + configuration is evaluated; for instance targets, the + instances-level configuration is evaluated against the + instance's class hierarchy — matching the levels the behavior + cores evaluate. Records report the normalized level names + ('class' and 'instance') at which behaviors and exclusion + configuration were found. Explanations are observational: they + neither bypass nor alter concealment or immutability. They are + not side-effect-free for arbitrary callbacks, however: + producing a record invokes user predicates and internal-name + detectors to determine the same outcome the cores would. + + Survey explanations describe normalized first-match semantics; + the survey core's current once-per-matching-rule duplication is + characterized as a defect whose repair is deferred separately. + ''' + class_target = __.inspect.isclass( target ) + level = 'class' if class_target else 'instances' + level_normalized = 'class' if class_target else 'instance' + behaviors = _behaviors.survey_active_behaviors( + target, attributes_namer = attributes_namer, level = level ) + immutability = _nomina.immutability_label in behaviors + concealment = _nomina.concealment_label in behaviors + operations: dict[ str, Verdict ] = { } + for operation, verdict_class, basename, active in ( + ( 'assign', AssignVerdict, 'mutables', immutability ), + ( 'delete', DeleteVerdict, 'mutables', immutability ), + ( 'survey', SurveyVerdict, 'visibles', concealment ), + ): + rule = ( + _behaviors.provide_permitter_if_applicable( + target, attributes_namer = attributes_namer, + level = level, basename = basename, name = name ) + if active else None ) + operations[ operation ] = verdict_class( + operation = operation, + decision = produce_decision( rule, active ) ) + return AttributeExplanation( + target = _utilities.describe_object( target ), + name = name, + behaviors = __.types.MappingProxyType( + { level_normalized: frozenset( behaviors ) } ), + operations = __.types.MappingProxyType( operations ), + internal = is_internal_name( target, name ) ) + + +def is_internal_name( target: object, name: str, / ) -> bool: + ''' Returns whether name is internal to the target's machinery. + + Contributions are discovered on the metaclass chain: for a + class target, its metaclass method resolution order; for an + instance target, the metaclass resolution order of its type + (the metaclass chain is reached through the double type + transformation — the instance's own type resolution order + enumerates the class bases, never the metaclass). Decorator- + based classes carry their contributions on the class itself, + so the class resolution order is consulted as well. Each + contribution is a detector produced by the machinery which + built the class, so a name is internal only where the + machinery that generates it operates. Classification scope is + the machinery chain and is independent of per-class mutability + exemptions: a name may be internal on a class where it is not + mutable, and mutable where it is not internal. + ''' + cls = target if __.inspect.isclass( target ) else type( target ) + chains = ( + type( cls ).__mro__, cls.__mro__ ) # Metaclass chain, then class. + contribution_name = __.calculate_contribution_name( ) + for chain in chains: + for holder in chain: + contributions = holder.__dict__.get( contribution_name ) + if not contributions: continue + for detector in contributions: + if detector( name ): return True + return False + + +def produce_decision( + rule: __.typx.Optional[ tuple[ str, __.typx.Any ] ], active: bool, / +) -> Decision: + ''' Returns decision for governing-behavior activity and first rule. + + Rules are the (kind, raw verifier) pairs produced by + provide_permitter_if_applicable; the kind vocabulary matches the + omni/names/predicate/regex precedence stages. Verifier detail + text is rendered here, at the record layer, so enforcement + paths which consult the same rules never depend on rendering. + ''' + if not active: return PermitByInapplicability( ) + if rule is None: return Prohibit( ) + kind, verifier = rule + if kind == 'omni': return PermitByOmni( ) + if kind == 'names': return PermitByNames( name = verifier ) + if kind == 'predicate': + return PermitByPredicate( + predicate = _behaviors.render_verifier_text( verifier ) ) + if kind == 'regex': + return PermitByRegex( + pattern = _behaviors.render_verifier_text( verifier ) ) + raise ValueError( kind ) diff --git a/sources/classcore/standard/nomina.py b/sources/classcore/standard/nomina.py index de17427..71b2049 100644 --- a/sources/classcore/standard/nomina.py +++ b/sources/classcore/standard/nomina.py @@ -45,6 +45,10 @@ __.cabc.Sequence[ BehaviorExclusionVerifier ] ) BehaviorExclusionVerifiersOmni: __.typx.TypeAlias = ( BehaviorExclusionVerifiers | __.typx.Literal[ '*' ] ) +InternalNameDetector: __.typx.TypeAlias = ( + __.cabc.Callable[ [ str ], bool ] ) +InternalNameDetectors: __.typx.TypeAlias = ( + __.cabc.Sequence[ InternalNameDetector ] ) ErrorClassProvider: __.typx.TypeAlias = __.typx.Annotated[ __.cabc.Callable[ [ str ], type[ Exception ] ], __.ddoc.Doc( diff --git a/sources/classcore/utilities.py b/sources/classcore/utilities.py index b8ec664..cdd6b79 100644 --- a/sources/classcore/utilities.py +++ b/sources/classcore/utilities.py @@ -95,17 +95,42 @@ def mangle_name( objct: object, /, name: str ) -> str: Effectively provides name of private member attribute, which is unique across class inheritance. ''' - # TODO: Replace expensive SHA-256 hash with simple 'id'. - # Need to debug weird issue with using 'id' early on dataclasses. + # NOTE: Mangling is based on the fully-qualified class name, not on id, + # because class reproductions (e.g., from dataclass slots) must + # retain identity of private attributes. A reproduction is a new + # object, so id-based mangling would break such identity. if not __.inspect.isclass( objct ): return mangle_name( type( objct ), name ) - # return "{name}{uid}".format( name = name, uid = id( objct ) ) namehash = __.hashlib.sha256( ) namehash.update( qualify_class_name( objct ).encode( ) ) namehash_hex = namehash.hexdigest( ) return f"{name}{namehash_hex}" +def survey_mangled_names( cls: type, /, name: str ) -> tuple[ str, ... ]: + ''' Returns mangled variants of name present in class's own namespace. + + A class reproduction (e.g., from ``dataclass( slots = ... )``) is + created with a bare ``__qualname__``, which is restored only after + the reproduction is constructed. During that window, mangled names + computed from the reproduction differ from those computed from the + original. Detection by prefix survives such qualname drift. + ''' + suffix_start = len( name ) + return tuple( + key for key in cls.__dict__ + if key.startswith( name ) and _is_digest( key[ suffix_start: ] ) ) + + +_digest_length = 64 # Length of a hexadecimal SHA-256 digest. + + +def _is_digest( text: str ) -> bool: + ''' Returns whether text is a hexadecimal SHA-256 digest. ''' + return _digest_length == len( text ) and all( + character in '0123456789abcdef' for character in text ) + + def qualify_class_name( cls: type ) -> str: ''' Returns fully-qualified class name. ''' return f"{cls.__module__}.{cls.__qualname__}" diff --git a/tests/test_000_classcore/test_110_utilities.py b/tests/test_000_classcore/test_110_utilities.py index a8951dc..bfcaa35 100644 --- a/tests/test_000_classcore/test_110_utilities.py +++ b/tests/test_000_classcore/test_110_utilities.py @@ -111,3 +111,25 @@ def delname( self ): pass module.repair_class_reproduction( Wut, DataclassWut ) wut = DataclassWut( ) assert 'wut' == wut.name + + +def test_310_survey_mangled_names( ): + ''' Mangled-name survey detects digest-drifted variants. ''' + module = cache_import_module( MODULE_QNAME ) + class C: pass + base = '_classcore_class_in_progress_' + name_m = module.mangle_name( C, base ) + setattr( C, name_m, True ) + drifted = f"{base}{'a' * 64}" + setattr( C, drifted, False ) + setattr( C, f"{base}zz", False ) # suffix not hexadecimal + setattr( C, f"{base}{'a' * 63}", False ) # suffix short of digest + setattr( C, f"{base}{'a' * 65}", False ) # suffix beyond digest + names = module.survey_mangled_names( C, base ) + assert name_m in names + assert drifted in names + assert f"{base}zz" not in names + assert f"{base}{'a' * 63}" not in names + assert f"{base}{'a' * 65}" not in names + class D( C ): pass + assert ( ) == module.survey_mangled_names( D, base ) diff --git a/tests/test_000_classcore/test_315_standard_cascade.py b/tests/test_000_classcore/test_315_standard_cascade.py new file mode 100644 index 0000000..b52e633 --- /dev/null +++ b/tests/test_000_classcore/test_315_standard_cascade.py @@ -0,0 +1,404 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Characterizing tests for the attribute behavior decision cascade. + + These tests pin the current behavior of the cores in + standard/behaviors.py — active-behavior sets, exclusion names, + wildcard, predicates, regexes, and the survey matching loops — + before the cascade is extracted into pure helpers shared with the + explanations module (OpenSpec add-attribute-explanations, task 1.1). +''' + + +import re + +import pytest + +from .__ import PACKAGE_NAME, cache_import_module + + +MODULE_QNAME = f"{PACKAGE_NAME}.standard.behaviors" +DECORATORS_QNAME = f"{PACKAGE_NAME}.standard.decorators" +EXCEPTIONS_QNAME = f"{PACKAGE_NAME}.exceptions" + + +def _predicate_matches_alpha( name: str ) -> bool: + return 'alpha' == name + + +class _OpaquePredicate: + ''' Callable predicate whose introspectable metadata raises. + + Simulates exotic callables (raising descriptors, custom + '__getattribute__'): boolean invocation works, but any + diagnostic rendering metadata access fails. + ''' + + def __call__( self, name: str ) -> bool: + return 'alpha' == name + + def __getattribute__( self, name: str ): + if name in ( 'pattern', '__qualname__', '__module__' ): + raise RuntimeError( 'opaque' ) + return object.__getattribute__( self, name ) + + +def test_300_immutable_without_exclusions( ): + ''' Immutability active and no rule permits: assignment raises. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + exceptions = cache_import_module( EXCEPTIONS_QNAME ) + + @decorators.with_standard_behaviors( ) + class Example: pass + + instance = Example( ) + with pytest.raises( exceptions.AttributeImmutability ): + instance.alpha = 1 + with pytest.raises( exceptions.AttributeImmutability ): + del instance.alpha + + +def test_301_immutable_excluded_by_name( ): + ''' Name exclusion permits assignment and deletion. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( mutables = ( 'alpha', ) ) + class Example: pass + + instance = Example( ) + instance.alpha = 1 + assert 1 == instance.alpha + del instance.alpha + assert not hasattr( instance, 'alpha' ) + + +def test_302_immutable_excluded_by_regex( ): + ''' Regex exclusion permits assignment. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( + mutables = ( re.compile( 'al.*' ), ) ) + class Example: pass + + instance = Example( ) + instance.alpha = 1 + assert 1 == instance.alpha + + +def test_303_immutable_excluded_by_predicate( ): + ''' Predicate exclusion permits assignment. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( + mutables = ( _predicate_matches_alpha, ) ) + class Example: pass + + instance = Example( ) + instance.alpha = 1 + assert 1 == instance.alpha + + +def test_304_immutable_wildcard( ): + ''' Wildcard exclusion permits any assignment. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( mutables = '*' ) + class Example: pass + + instance = Example( ) + instance.anything = 1 + assert 1 == instance.anything + + +def test_310_concealment_without_visibles( ): + ''' Concealment hides non-public names; public names stay visible + via the default public-identifier predicate. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( ) + class Example: + _private = 1 + public = 2 + + instance = Example( ) + assert '_private' not in dir( instance ) + assert 'public' in dir( instance ) + + +def test_310a_concealment_no_visibles_rules( ): + ''' Concealment with empty visibles hides every name. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( visibles = ( ) ) + class Example: + alpha = 1 + + instance = Example( ) + assert 'alpha' not in dir( instance ) + + +def test_311_concealment_visibles_by_name( ): + ''' Visibles name rule reveals the name in dir. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( visibles = ( 'alpha', ) ) + class Example: + alpha = 1 + + instance = Example( ) + assert 'alpha' in dir( instance ) + + +def test_312_concealment_visibles_wildcard( ): + ''' Visibles wildcard reveals all names. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( visibles = '*' ) + class Example: + alpha = 1 + + instance = Example( ) + assert 'alpha' in dir( instance ) + + +def test_313_concealment_visibles_by_regex( ): + ''' Visibles regex rule reveals matching names. ''' + decorators = cache_import_module( DECORATORS_QNAME ) + + @decorators.with_standard_behaviors( + visibles = ( re.compile( 'al.*' ), ) ) + class Example: + alpha = 1 + beta = 2 + + instance = Example( ) + assert 'alpha' in dir( instance ) + assert 'beta' not in dir( instance ) + + +def test_314_survey_multi_match_yields_duplicates( ): + ''' Survey yields a name multiple times when multiple rules match. + + Characterizes the current control flow: the per-name matching + loops continue the inner loop only, so a name matching one + predicate and one regex is appended twice. Preserved deliberately + by the extraction; normalization is a separate repair. + + Note the lookup asymmetry: the core reads the active-behaviors + set through the mangling accessors, but exclusion configuration + through plain attribute access under raw level names. + ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + utilities = cache_import_module( f"{PACKAGE_NAME}.utilities" ) + + class Example: pass + example = Example( ) + utilities.setattr0( + example, base.calculate_attrname( 'instance', 'behaviors' ), + frozenset( ( 'concealment', ) ) ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_names' ), + frozenset( ) ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_predicates' ), + ( _predicate_matches_alpha, ) ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_regexes' ), + ( re.compile( 'al.*' ), ) ) + + visible = list( module.survey_visible_attributes( + example, ligation = lambda: [ 'alpha', 'beta' ], + attributes_namer = base.calculate_attrname, + level = 'instances' ) ) + assert [ 'alpha', 'alpha' ] == visible + + +def test_315_survey_wildcard_short_circuits( ): + ''' Survey wildcard returns all names before rule evaluation. ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + utilities = cache_import_module( f"{PACKAGE_NAME}.utilities" ) + + calls: list[ str ] = [ ] + + def recording_predicate( name: str ) -> bool: + calls.append( name ) + return True + + class Example: pass + example = Example( ) + utilities.setattr0( + example, base.calculate_attrname( 'instance', 'behaviors' ), + frozenset( ( 'concealment', ) ) ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_names' ), '*' ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_predicates' ), + ( recording_predicate, ) ) + + visible = list( module.survey_visible_attributes( + example, ligation = lambda: [ 'alpha' ], + attributes_namer = base.calculate_attrname, + level = 'instances' ) ) + assert [ 'alpha' ] == visible + assert [ ] == calls + + +def test_316_survey_names_match_skips_rules( ): + ''' A names match short-circuits predicate and regex evaluation. + + Characterizes the original control flow, where a names match + continued the outer per-name loop: with a name rule overlapping + predicate and regex rules, the name is yielded once and no + predicate or regex runs for it. + ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + utilities = cache_import_module( f"{PACKAGE_NAME}.utilities" ) + + calls: list[ str ] = [ ] + + def recording_predicate( name: str ) -> bool: + calls.append( name ) + return True + + class Example: pass + example = Example( ) + utilities.setattr0( + example, base.calculate_attrname( 'instance', 'behaviors' ), + frozenset( ( 'concealment', ) ) ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_names' ), + frozenset( ( 'alpha', ) ) ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_predicates' ), + ( recording_predicate, ) ) + setattr( example, + base.calculate_attrname( 'instances', 'visibles_regexes' ), + ( re.compile( 'al.*' ), ) ) + + visible = list( module.survey_visible_attributes( + example, ligation = lambda: [ 'alpha' ], + attributes_namer = base.calculate_attrname, + level = 'instances' ) ) + assert [ 'alpha' ] == visible + assert [ ] == calls + + +def test_320_render_verifier_text( ): + ''' Verifier rendering: pattern text, qualified names, fallbacks. ''' + module = cache_import_module( MODULE_QNAME ) + + assert 'al.*' == module.render_verifier_text( re.compile( 'al.*' ) ) + assert ( + f"{__name__}._predicate_matches_alpha" + == module.render_verifier_text( _predicate_matches_alpha ) ) + assert '<anonymous>' == module.render_verifier_text( object( ) ) + + class _Patterned: + pattern = 42 + + assert ( + 'test_320_render_verifier_text.<locals>._Patterned' + == module.render_verifier_text( _Patterned ) ) + + def local_predicate( name: str ) -> bool: return True + + assert ( + 'test_320_render_verifier_text.<locals>.local_predicate' + == module.render_verifier_text( local_predicate ) ) + + +def test_321_first_permitting_rule_precedence( ): + ''' First permitting rule follows wildcard/names/predicate/regex + precedence and reports kind with raw verifier. ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + namer = base.calculate_attrname + + class Stub: pass + stub = Stub( ) + + setattr( stub, namer( 'instances', 'mutables_names' ), '*' ) + assert ( 'omni', '*' ) == module.provide_permitter_if_applicable( + stub, attributes_namer = namer, level = 'instances', + basename = 'mutables', name = 'anything' ) + + setattr( stub, namer( 'instances', 'mutables_names' ), + frozenset( ( 'alpha', ) ) ) + assert ( 'names', 'alpha' ) == module.provide_permitter_if_applicable( + stub, attributes_namer = namer, level = 'instances', + basename = 'mutables', name = 'alpha' ) + + setattr( stub, namer( 'instances', 'mutables_names' ), frozenset( ) ) + setattr( stub, namer( 'instances', 'mutables_predicates' ), + ( _predicate_matches_alpha, ) ) + setattr( stub, namer( 'instances', 'mutables_regexes' ), + ( re.compile( 'al.*' ), ) ) + assert ( 'predicate', _predicate_matches_alpha + ) == module.provide_permitter_if_applicable( + stub, attributes_namer = namer, level = 'instances', + basename = 'mutables', name = 'alpha' ) + + setattr( stub, namer( 'instances', 'mutables_predicates' ), ( ) ) + assert ( 'regex', re.compile( 'al.*' ) + ) == module.provide_permitter_if_applicable( + stub, attributes_namer = namer, level = 'instances', + basename = 'mutables', name = 'alpha' ) + + setattr( stub, namer( 'instances', 'mutables_regexes' ), ( ) ) + assert None is module.provide_permitter_if_applicable( + stub, attributes_namer = namer, level = 'instances', + basename = 'mutables', name = 'alpha' ) + + +def test_322_matched_rules_union( ): + ''' Matched rules report predicate/regex matches in evaluation order; + a names match short-circuits to the single names rule. Rules + carry raw verifiers; rendering happens at the record layer. ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + namer = base.calculate_attrname + + class Stub: pass + stub = Stub( ) + setattr( stub, namer( 'instances', 'visibles_names' ), + frozenset( ( 'alpha', ) ) ) + assert ( ( 'names', 'alpha' ), ) == module.determine_relevant_permitters( + stub, attributes_namer = namer, level = 'instances', + basename = 'visibles', name = 'alpha' ) + + setattr( stub, namer( 'instances', 'visibles_names' ), frozenset( ) ) + setattr( stub, namer( 'instances', 'visibles_predicates' ), + ( _predicate_matches_alpha, ) ) + setattr( stub, namer( 'instances', 'visibles_regexes' ), + ( re.compile( 'al.*' ), ) ) + + assert ( + ( 'predicate', _predicate_matches_alpha ), + ( 'regex', re.compile( 'al.*' ) ) + ) == module.determine_relevant_permitters( + stub, attributes_namer = namer, level = 'instances', + basename = 'visibles', name = 'alpha' ) + assert ( ) == module.determine_relevant_permitters( + stub, attributes_namer = namer, level = 'instances', + basename = 'visibles', name = 'beta' ) diff --git a/tests/test_000_classcore/test_320_standard_explanations.py b/tests/test_000_classcore/test_320_standard_explanations.py new file mode 100644 index 0000000..7a28f00 --- /dev/null +++ b/tests/test_000_classcore/test_320_standard_explanations.py @@ -0,0 +1,721 @@ +# vim: set filetype=python fileencoding=utf-8: +# -*- coding: utf-8 -*- + +#============================================================================# +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +#============================================================================# + + +''' Tests for attribute explanations (OpenSpec add-attribute-explanations). ''' + +import re + +import pytest + +from .__ import PACKAGE_NAME, cache_import_module + + +MODULE_QNAME = f"{PACKAGE_NAME}.standard.explanations" +DECORATORS_QNAME = f"{PACKAGE_NAME}.standard.decorators" +EXCEPTIONS_QNAME = f"{PACKAGE_NAME}.exceptions" + + +def _matches_alpha( name: str ) -> bool: + return 'alpha' == name + + +class _OpaquePredicate: + ''' Callable predicate whose introspectable metadata raises. + + Simulates exotic callables (raising descriptors, custom + '__getattribute__'): boolean invocation works, but any + diagnostic rendering metadata access fails. Enforcement and + survey must follow the boolean outcome regardless. + ''' + + def __call__( self, name: str ) -> bool: + return True + + def __getattribute__( self, name: str ): + if name in ( 'pattern', '__qualname__', '__module__' ): + raise RuntimeError( 'opaque' ) + return object.__getattribute__( self, name ) + + +def _produce_example( **verifiers ): + decorators = cache_import_module( DECORATORS_QNAME ) + + return decorators.with_standard_behaviors( **verifiers ) + + +def test_400_explain_inapplicable_behavior( ): + ''' Inactive behavior: permitted by inapplicability. ''' + module = cache_import_module( MODULE_QNAME ) + + class Plain: pass + + explanation = module.explain_attribute( Plain( ), 'anything' ) + assign = explanation.operations[ 'assign' ] + survey = explanation.operations[ 'survey' ] + assert isinstance( assign.decision, module.PermitByInapplicability ) + assert assign.permissible + assert isinstance( survey.decision, module.PermitByInapplicability ) + assert survey.permissible + + +def test_401_explain_permit_by_names( ): + ''' Names exclusion: permitted by names carrying the matched name. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( mutables = ( 'alpha', ) ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + assign = explanation.operations[ 'assign' ] + assert isinstance( assign.decision, module.PermitByNames ) + assert 'alpha' == assign.decision.name + assert assign.permissible + + +def test_402_explain_permit_by_regex( ): + ''' Regex exclusion: permitted by regex carrying pattern text. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( mutables = ( re.compile( 'al.*' ), ) ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + assign = explanation.operations[ 'assign' ] + assert isinstance( assign.decision, module.PermitByRegex ) + assert 'al.*' == assign.decision.pattern + + +def test_403_explain_permit_by_predicate( ): + ''' Predicate exclusion: permitted by predicate carrying the + qualified name text. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( mutables = ( _matches_alpha, ) ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + assign = explanation.operations[ 'assign' ] + assert isinstance( assign.decision, module.PermitByPredicate ) + assert f"{__name__}._matches_alpha" == assign.decision.predicate + + +def test_404_explain_permit_by_omni( ): + ''' Omni exclusion for assign/delete: permitted by omni. + + An omni marker never coexists with an active behavior through + the recording API (record_behavior skips the behavior label for + omni verifiers), so this constructs the defensive state the + cores are specified to handle: immutability active with the + exclusion names configured as the omni marker. + ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Example( classes.Object ): pass + + type.__setattr__( + Example, base.calculate_attrname( 'class', 'mutables_names' ), '*' ) + explanation = module.explain_attribute( Example, 'anything' ) + assign = explanation.operations[ 'assign' ] + delete = explanation.operations[ 'delete' ] + assert isinstance( assign.decision, module.PermitByOmni ) + assert assign.permissible + assert isinstance( delete.decision, module.PermitByOmni ) + + +def test_405_explain_prohibit( ): + ''' Active behavior without permitting rule: prohibited. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + assign = explanation.operations[ 'assign' ] + assert isinstance( assign.decision, module.Prohibit ) + assert not assign.permissible + + +def test_406_explain_concealed_attribute( ): + ''' Concealment without visibles rule: survey prohibited. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( visibles = ( ) ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + survey = explanation.operations[ 'survey' ] + assert isinstance( survey.decision, module.Prohibit ) + assert not survey.permissible + + +def test_407_explain_survey_precedence( ): + ''' Survey follows precedence: the first matching rule decides. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( + visibles = ( _matches_alpha, re.compile( 'al.*' ) ) ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + survey = explanation.operations[ 'survey' ] + assert isinstance( survey.decision, module.PermitByPredicate ) + assert not isinstance( survey.decision, module.PermitByRegex ) + + +def test_407a_explain_survey_names_precedence( ): + ''' Survey names match short-circuits predicate and regex rules. ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + calls: list[ str ] = [ ] + + def recording_predicate( name: str ) -> bool: + calls.append( name ) + return True + + class Example( classes.Object ): pass + + type.__setattr__( + Example, base.calculate_attrname( 'class', 'visibles_names' ), + frozenset( ( 'alpha', ) ) ) + type.__setattr__( + Example, base.calculate_attrname( 'class', 'visibles_predicates' ), + ( recording_predicate, ) ) + explanation = module.explain_attribute( Example, 'alpha' ) + survey = explanation.operations[ 'survey' ] + assert isinstance( survey.decision, module.PermitByNames ) + assert [ ] == calls + + +def test_408_explain_survey_omni( ): + ''' Survey omni: permitted by omni, no predicate/regex evaluation. ''' + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + calls: list[ str ] = [ ] + + def recording_predicate( name: str ) -> bool: + calls.append( name ) + return True + + class Example( classes.Object ): pass + + type.__setattr__( + Example, base.calculate_attrname( 'class', 'visibles_names' ), '*' ) + type.__setattr__( + Example, base.calculate_attrname( 'class', 'visibles_predicates' ), + ( recording_predicate, ) ) + explanation = module.explain_attribute( Example, 'anything' ) + survey = explanation.operations[ 'survey' ] + assert isinstance( survey.decision, module.PermitByOmni ) + assert [ ] == calls + + +def test_410_explanation_records_frozen( ): + ''' Assigning a record field raises immutability error. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions = cache_import_module( EXCEPTIONS_QNAME ) + decorate = _produce_example( mutables = ( 'alpha', ) ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + with pytest.raises( exceptions.AttributeImmutability ): + explanation.name = 'other' + with pytest.raises( exceptions.AttributeImmutability ): + explanation.operations[ 'assign' ].decision = module.Prohibit( ) + with pytest.raises( exceptions.AttributeImmutability ): + explanation.operations[ 'assign' ].decision.name = 'other' + + +def test_411_explanation_collections_immutable( ): + ''' Mutating nested collections raises. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + with pytest.raises( TypeError ): + explanation.operations[ 'survey' ] = None + behaviors = explanation.behaviors + with pytest.raises( TypeError ): + behaviors[ 'new' ] = frozenset( ) + + +def test_412_explanation_internal_marking( ): + ''' Framework names mark via the namer detector; stdlib machinery + names mark only where the machinery operates. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( ) + + @decorate + class Example: pass + + import hashlib + digest = hashlib.sha256( b'x' ).hexdigest( ) + framework = module.explain_attribute( + Example( ), f"_classcore_class_in_progress_{digest}" ) + machinery_absent = module.explain_attribute( Example( ), '_abc_cache' ) + user = module.explain_attribute( Example( ), 'alpha' ) + assert framework.internal + assert not machinery_absent.internal # no ABC machinery here + assert not user.internal + + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Abstract( classes.AbstractObject ): pass + + machinery_present = module.explain_attribute( + Abstract( ), '_abc_cache' ) + assert machinery_present.internal + assert not module.explain_attribute( + Abstract( ), 'alpha' ).internal + + +def test_413_explanation_level_semantics( ): + ''' Class targets evaluate classes level; instances evaluate + instances level. ''' + module = cache_import_module( MODULE_QNAME ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + explanation = module.explain_attribute( classes.Object, 'alpha' ) + assert 'class' in explanation.behaviors + instance_explanation = ( + module.explain_attribute( classes.Object( ), 'alpha' ) ) + assert 'instance' in instance_explanation.behaviors + + +def test_414_explanation_observational( ): + ''' Explanations do not bypass or alter behaviors. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions = cache_import_module( EXCEPTIONS_QNAME ) + decorate = _produce_example( ) + + @decorate + class Example: pass + + instance = Example( ) + module.explain_attribute( instance, 'alpha' ) + with pytest.raises( exceptions.AttributeImmutability ): + instance.alpha = 1 + + +def test_415_explanation_repr_summary( ): + ''' Repr summarizes target, behaviors, and decision outcomes. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( + mutables = ( 'alpha', ), visibles = ( re.compile( 'pub.*' ), ) ) + + @decorate + class Example: + alpha = 1 + public_total = 2 + + permitted = repr( module.explain_attribute( Example( ), 'alpha' ) ) + assert "'alpha' on instance of class" in permitted + assert 'behaviors: concealment, immutability (instance)' in permitted + assert "assign: permitted by names 'alpha'" in permitted + assert "delete: permitted by names 'alpha'" in permitted + + visible = repr( module.explain_attribute( Example( ), 'public_total' ) ) + assert "survey: permitted by regex 'pub.*'" in visible + assert 'assign: prohibited (no permitting rule)' in visible + + class Plain: pass + + plain = repr( module.explain_attribute( Plain( ), 'x' ) ) + assert 'permitted (behavior inapplicable)' in plain + assert '[internal]' not in plain + + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Abstract( classes.AbstractObject ): pass + + internal = repr( + module.explain_attribute( Abstract( ), '_abc_cache' ) ) + assert '[internal]' in internal + + +def test_416_names_payload_collision( ): + ''' A name literally 'regex' permitted by names stays distinct from + a regex decision. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( mutables = ( 'regex', ) ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'regex' ) + decision = explanation.operations[ 'assign' ].decision + assert isinstance( decision, module.PermitByNames ) + assert 'regex' == decision.name + assert not isinstance( decision, module.PermitByRegex ) + + +def test_417_permissible_derivation( ): + ''' Permissibility derives from the decision type. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( mutables = ( 'alpha', ) ) + + @decorate + class Example: pass + + permitted = module.explain_attribute( Example( ), 'alpha' ) + prohibited = module.explain_attribute( Example( ), 'beta' ) + assert permitted.operations[ 'assign' ].permissible + assert not prohibited.operations[ 'assign' ].permissible + assert prohibited.operations[ 'assign' ].permissible is ( + not isinstance( + prohibited.operations[ 'assign' ].decision, module.Prohibit ) ) + + +def test_418_render_covers_decisions( ): + """ Repr renders every decision branch. """ + module = cache_import_module( MODULE_QNAME ) + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Example( classes.Object ): pass + + type.__setattr__( + Example, base.calculate_attrname( 'class', 'visibles_names' ), '*' ) + type.__setattr__( + Example, base.calculate_attrname( 'class', 'mutables_predicates' ), + ( _matches_alpha, ) ) + omni = repr( module.explain_attribute( Example, 'anything' ) ) + assert "survey: permitted by omni '*'" in omni + predicated = repr( module.explain_attribute( Example, 'alpha' ) ) + assert ( + f"assign: permitted by predicate '{__name__}._matches_alpha'" + in predicated ) + + +def test_419_produce_decision_rejects_unknown_kinds( ): + ''' Unknown rule kinds raise instead of misclassifying. ''' + module = cache_import_module( MODULE_QNAME ) + + with pytest.raises( ValueError, match = 'unknown' ): + module.produce_decision( ( 'unknown', 'x' ), True ) + inapplicable = module.produce_decision( None, False ) + assert isinstance( inapplicable, module.PermitByInapplicability ) + prohibited = module.produce_decision( None, True ) + assert isinstance( prohibited, module.Prohibit ) + omni = module.produce_decision( ( 'omni', '*' ), True ) + assert isinstance( omni, module.PermitByOmni ) + + +def test_420_render_rejects_unknown_decisions( ): + ''' Rendering an unknown decision raises instead of reporting + prohibited. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( ) + + @decorate + class Example: pass + + explanation = module.explain_attribute( Example( ), 'alpha' ) + with pytest.raises( ValueError, match = 'object' ): + explanation._render( object( ) ) + + +def test_421_grammar_static_forms_marked( ): + ''' All grammar-valid static stems mark; invalid cores do not. ''' + module = cache_import_module( MODULE_QNAME ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Example( classes.Object ): pass + + for level in ( 'class', 'classes', 'instance', 'instances' ): + for core in ( 'mutables_names', 'visibles_predicates', + 'assigner_core', 'construction_arguments' ): + name = f"_classcore_{level}_{core}_" + assert module.explain_attribute( + Example, name ).internal, name + for name in ( '_classcore_not_a_generated_core_', + '_classcore_class_lookalike_', '_classcore_lookalike_behaviors_', + '_classcore_zzz_abc' ): + assert not module.explain_attribute( Example, name ).internal + + +def test_422_grammar_mangled_forms( ): + ''' Both mangled forms mark with valid digests only. ''' + import hashlib + module = cache_import_module( MODULE_QNAME ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Example( classes.Object ): pass + + digest = hashlib.sha256( b'probe' ).hexdigest( ) + for stem in ( 'class_behaviors', 'class_in_progress' ): + assert module.explain_attribute( + Example, f"_classcore_{stem}_{digest}" ).internal + for name in ( + f"_classcore_class_in_progress_{digest[ :-1 ]}", + f"_classcore_class_in_progress_{digest.upper( )}", + f"_classcore_class_in_progress_{digest}_", + '_classcore_class_in_progress_', + '_classcore_class_behaviors_', + ): + assert not module.explain_attribute( Example, name ).internal + + +def test_423_instance_target_metaclass_chain( ): + ''' Instance targets consult the metaclass chain via the double + type transformation. ''' + module = cache_import_module( MODULE_QNAME ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Example( classes.Object ): pass + + instance = Example( ) + assert module.explain_attribute( + instance, '_classcore_class_mutables_names_' ).internal + # The class MRO of the instance's type contains no metaclass; + # marking proves the metaclass chain was consulted. + assert not any( + holder is type( Example ) + for holder in type( instance ).__mro__ ) + + +def test_424_inherited_contribution_across_packages( ): + ''' A class defined in the test module (a foreign module, from the + framework's perspective) inherits the base's contributions + through the base's metaclass. ''' + module = cache_import_module( MODULE_QNAME ) + classes = cache_import_module( f"{PACKAGE_NAME}.standard.classes" ) + + class Derived( classes.Object ): pass + + assert module.explain_attribute( + Derived, '_classcore_instances_mutables_names_' ).internal + + +def test_425_namer_vocabulary_consistency( ): + ''' Every literal namer call site in the package sources has its + level and core covered by the detector grammar, and at least + one site is found (guarding against a vacuous scan). ''' + import ast + from pathlib import Path + base = cache_import_module( f"{PACKAGE_NAME}.__" ) + det = base.calculate_attrname.is_internal_name + namer_names = ( 'calculate_attrname', 'attributes_namer' ) + levels = ( 'class', 'classes', 'instance', 'instances' ) + sites: list[ tuple[ str, str ] ] = [ ] + for path in Path( 'sources' ).rglob( '*.py' ): + tree = ast.parse( path.read_text( ) ) + for node in ast.walk( tree ): + if not ( + isinstance( node, ast.Call ) + and isinstance( node.func, ast.Name ) + and node.func.id in namer_names + and 2 == len( node.args ) + and all( + isinstance( a, ast.Constant ) + and isinstance( a.value, str ) + for a in node.args ) ): + continue + level, core = ( + node.args[ 0 ].value, node.args[ 1 ].value ) + sites.append( ( level, core ) ) + assert level in levels, ( path, level ) + marked = ( + det( f"_classcore_{level}_{core}_" ) + or ( 'class' == level and det( + f"_classcore_class_{core}_" + 'a' * 64 ) ) ) + assert marked, ( path, level, core ) + assert 5 <= len( sites ) + + +def test_426_bare_namer_contributes_nothing( ): + ''' A namer without an is_internal_name attribute contributes + nothing, and downstream detectors ride only when provided. ''' + from classcore.standard.decorators import class_factory + module = cache_import_module( MODULE_QNAME ) + + def bare_namer( level: str, core: str ) -> str: + return f"_custom_{level}_{core}_" + + @class_factory( attributes_namer = bare_namer ) + class CustomMeta( type ): pass + + class Custom( metaclass = CustomMeta ): pass + + assert not module.explain_attribute( + Custom, '_classcore_class_mutables_names_' ).internal + assert not module.explain_attribute( + Custom, '_custom_class_core_' ).internal + + @class_factory( attributes_namer = bare_namer ) + class BareMeta( type ): pass + + class Bare( metaclass = BareMeta ): pass + + assert not module.explain_attribute( + Bare, '_custom_class_core_' ).internal + + from classcore.__ import AttrnameCalculator + + class DownstreamNamer( AttrnameCalculator ): + def __call__( self, level: str, core: str ) -> str: + return f"_downstream_{level}_{core}_" + def is_internal_name( self, name: str ) -> bool: + return name.startswith( '_downstream_' ) + + @class_factory( attributes_namer = DownstreamNamer( ) ) + class DownstreamMeta( type ): pass + + class Downstream( metaclass = DownstreamMeta ): pass + + assert module.explain_attribute( + Downstream, '_downstream_class_core_' ).internal + + +def test_427_decorator_path_inheritance( ): + ''' Subclassing a decorated class keeps internal marking via the + class resolution order. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( ) + + @decorate + class Base: pass + + class Derived( Base ): pass + + assert module.explain_attribute( + Base, '_classcore_instances_mutables_names_' ).internal + assert module.explain_attribute( + Derived, '_classcore_instances_mutables_names_' ).internal + + +def test_428_augment_internal_names_idempotent( ): + ''' Private augmentation wiring dedupes repeated bound detectors. + + The bound method retrieved twice from one namer instance is + one detector: fresh bindings of the same instance's method must + dedupe, which plain identity comparison would miss. + ''' + from classcore.__ import AttrnameCalculator, calculate_contribution_name + from classcore.standard.__ import augment_internal_names + + namer = AttrnameCalculator( ) + + class Meta( type ): pass + + augment_internal_names( + Meta, namer.is_internal_name ) + augment_internal_names( + Meta, namer.is_internal_name ) # Fresh bound method. + assert 1 == len( getattr( Meta, calculate_contribution_name( ) ) ) + + +def test_429_distinct_instances_keep_separate_contributions( ): + ''' Two namer instances sharing method implementation but with + instance-configured detector behavior keep both contributions. ''' + from classcore.__ import AttrnameCalculator, calculate_contribution_name + from classcore.standard.decorators import class_factory + module = cache_import_module( MODULE_QNAME ) + + class ConfiguredNamer( AttrnameCalculator ): + def __init__( self, prefix: str ): + self.prefix = prefix + def __call__( self, level: str, core: str ) -> str: + return f"{self.prefix}_{level}_{core}_" + def is_internal_name( self, name: str ) -> bool: + return name.startswith( self.prefix ) + + first = ConfiguredNamer( '_first_' ) + second = ConfiguredNamer( '_second_' ) + + @class_factory( attributes_namer = first ) + class FirstMeta( type ): pass + + @class_factory( attributes_namer = second ) + class SecondMeta( FirstMeta ): pass + + class Product( metaclass = SecondMeta ): pass + + assert module.explain_attribute( + Product, '_first_class_core_' ).internal + assert module.explain_attribute( + Product, '_second_class_core_' ).internal + contributions = getattr( Product, calculate_contribution_name( ) ) + assert 2 == len( contributions ) + + +def test_430_plain_function_detectors_by_identity( ): + ''' Plain function detectors compare by identity, not attribute + probing: a non-method callable without __func__ dedupes only + against itself. ''' + from classcore.__ import calculate_contribution_name + from classcore.standard.__ import augment_internal_names + + def detector_a( name: str ) -> bool: + return name.startswith( '_a_' ) + + def detector_b( name: str ) -> bool: + return name.startswith( '_b_' ) + + class Meta( type ): pass + + augment_internal_names( Meta, detector_a ) + augment_internal_names( Meta, detector_a ) + augment_internal_names( Meta, detector_b ) + assert 2 == len( getattr( Meta, calculate_contribution_name( ) ) ) + + +def test_431_opaque_predicate_governs_by_bool( ): + ''' Opaque predicate: enforcement and survey follow the boolean + outcome despite hostile introspection metadata; the + explanation record renders the documented fallback. ''' + module = cache_import_module( MODULE_QNAME ) + decorate = _produce_example( + mutables = ( _OpaquePredicate( ), ), + visibles = ( _OpaquePredicate( ), ) ) + + @decorate + class Example: pass + + instance = Example( ) + instance.alpha = 1 + assert 1 == instance.alpha + assert 'alpha' in dir( instance ) + explanation = module.explain_attribute( instance, 'alpha' ) + assign = explanation.operations[ 'assign' ] + assert isinstance( assign.decision, module.PermitByPredicate ) + assert '<unrenderable>' == assign.decision.predicate + assert assign.permissible + survey = explanation.operations[ 'survey' ] + assert isinstance( survey.decision, module.PermitByPredicate ) + assert '<unrenderable>' == survey.decision.predicate + assert survey.permissible + del instance.alpha diff --git a/tests/test_000_classcore/test_340_standard_classes.py b/tests/test_000_classcore/test_340_standard_classes.py index dd44178..d349c0c 100644 --- a/tests/test_000_classcore/test_340_standard_classes.py +++ b/tests/test_000_classcore/test_340_standard_classes.py @@ -18,7 +18,7 @@ #============================================================================# -# import pytest +import pytest from .__ import PACKAGE_NAME, cache_import_module @@ -46,3 +46,39 @@ def __init__( self, *posargs, **nomargs ): u = UrlWithInit( *urlparse( 'https://python.org' ) ) assert u.scheme == 'https' + + +def test_125_cfc_function_local_dataclass( ): + ''' Function-local dataclass objects construct under slot + reproduction. During reproduction, the class is rebuilt with a + bare qualname, so the digest-mangled construction marker drifts; + detection must survive the drift. ''' + module = cache_import_module( MODULE_QNAME ) + + def produce( ): + class LocalData( module.DataclassObject ): + x: int + return LocalData + + LocalData = produce( ) + assert 'x' in LocalData.__dataclass_fields__ + assert '<locals>' in LocalData.__qualname__ + instance = LocalData( x = 3 ) + assert 3 == instance.x + + +@pytest.mark.parametrize( 'name', ( + 'DataclassObject', 'DataclassObjectMutable', + 'DataclassProtocol', 'DataclassProtocolMutable' ) ) +def test_126_cfc_function_local_dataclass_bases( name ): + ''' Every dataclass base class constructs when function-local. ''' + module = cache_import_module( MODULE_QNAME ) + + def produce( ): + class LocalBase( getattr( module, name ) ): + value: int + return LocalBase + + LocalBase = produce( ) + assert '<locals>' in LocalBase.__qualname__ + assert 'value' in LocalBase.__dataclass_fields__ diff --git a/tests/test_000_classcore/test_345_standard_protocols.py b/tests/test_000_classcore/test_345_standard_protocols.py index 607a975..51afc4d 100644 --- a/tests/test_000_classcore/test_345_standard_protocols.py +++ b/tests/test_000_classcore/test_345_standard_protocols.py @@ -237,6 +237,21 @@ def greet( self ) -> str: ... assert '__eq__' not in attrs +def test_446_typing_dunder_excluded_from_protocol_attrs( ): + ''' '__typing_is_deprecated_inherited_runtime_protocol__', set by + CPython 3.15+ typing machinery during class creation, is not a + declared protocol member. ''' + module = cache_import_module( MODULE_QNAME ) + + class TypingProto( module.Protocol ): + __typing_is_deprecated_inherited_runtime_protocol__ = True + def greet( self ) -> str: ... + + attrs = TypingProto.__dict__[ '__protocol_attrs__' ] + assert '__typing_is_deprecated_inherited_runtime_protocol__' not in attrs + assert 'greet' in attrs + + def test_443_dataclass_protocol_attrs_declared_only( ): ''' Dataclass protocol attrs contain declared fields and methods, excluding dataclass-generated dunders. ''' @@ -281,6 +296,22 @@ class Product( assert invocations == [ 'Product' ] +def test_445_function_local_dataclass_protocol( ): + ''' Function-local dataclass protocols construct despite digest + drift of the construction marker during slot reproduction. ''' + module = cache_import_module( MODULE_QNAME ) + + def produce( ): + class LocalProto( module.DataclassProtocol ): + y: int + return LocalProto + + LocalProto = produce( ) + assert LocalProto._is_protocol + assert 'y' in LocalProto.__protocol_attrs__ + assert 'y' in LocalProto.__dataclass_fields__ + + # =========================================================================== # # Dataclass Transform Preservation # =========================================================================== # diff --git a/tests/test_000_classcore/test_347_abstract_classes.py b/tests/test_000_classcore/test_347_abstract_classes.py index 0eb4b05..cbbc55d 100644 --- a/tests/test_000_classcore/test_347_abstract_classes.py +++ b/tests/test_000_classcore/test_347_abstract_classes.py @@ -188,3 +188,22 @@ def greet( self ) -> str: return 'concrete' assert isinstance( GreeterClass( ), GreeterProto ) + + +def test_523_typing_dunder_assignable_on_immutable_class( ): + ''' CPython 3.15+ typing machinery dunder is assignable on immutable + classes. Typing sets + '__typing_is_deprecated_inherited_runtime_protocol__' during + class decoration and in the 'runtime_checkable' decorator, so + the class-level immutability behavior exempts it. ''' + module = cache_import_module( MODULE_QNAME ) + + class GreeterProto( module.Protocol ): + def greet( self ) -> str: ... + + GreeterProto.__typing_is_deprecated_inherited_runtime_protocol__ = ( + False ) + assert ( + GreeterProto.__dict__[ + '__typing_is_deprecated_inherited_runtime_protocol__' ] + is False ) From 3ea6aa66eb5f14af0702692ca09c2110e9cf84d3 Mon Sep 17 00:00:00 2001 From: Github Actions Robot <emcd@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:04:54 +0000 Subject: [PATCH 27/28] Update documents for publication. (1.14) --- .../+attribute-explanations.enhance.rst | 9 --------- .../+function-local-dataclasses.repair.rst | 6 ------ .../+internal-name-contributions.enhance.rst | 11 ----------- .../+nomina-mutables-export.enhance.rst | 8 ++++++++ .../+python315-typing-dunder.repair.rst | 5 ----- .auxiliary/publications/website.tar.xz | 4 ++-- documentation/changelog.rst | 16 ++++++++++++++++ sources/classcore/__init__.py | 2 +- sources/classcore/standard/behaviors.py | 17 +++++++++++------ sources/classcore/standard/nomina.py | 4 ++++ .../test_310_standard_internals.py | 14 ++++++++++++++ .../test_315_standard_cascade.py | 17 +++++++++++++++++ .../test_347_abstract_classes.py | 17 ++++++++++++++++- 13 files changed, 89 insertions(+), 41 deletions(-) delete mode 100644 .auxiliary/data/towncrier/+attribute-explanations.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+function-local-dataclasses.repair.rst delete mode 100644 .auxiliary/data/towncrier/+internal-name-contributions.enhance.rst create mode 100644 .auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+python315-typing-dunder.repair.rst diff --git a/.auxiliary/data/towncrier/+attribute-explanations.enhance.rst b/.auxiliary/data/towncrier/+attribute-explanations.enhance.rst deleted file mode 100644 index 09f11ba..0000000 --- a/.auxiliary/data/towncrier/+attribute-explanations.enhance.rst +++ /dev/null @@ -1,9 +0,0 @@ -Add attribute explanations: ``explain_attribute`` in -``classcore.standard.explanations`` returns the decision trace for one -attribute of one target — active behaviors per evaluated level and, for -each operation, a verdict carrying its decision under precedence -semantics: permission by inapplicability, omni, names, predicate, or -regex, or prohibition. Decision payloads are typed per decision class, -explanation records are immutable with immutable nested collections, -and framework-owned and stdlib-machinery attributes are marked -internal. diff --git a/.auxiliary/data/towncrier/+function-local-dataclasses.repair.rst b/.auxiliary/data/towncrier/+function-local-dataclasses.repair.rst deleted file mode 100644 index ee5be5a..0000000 --- a/.auxiliary/data/towncrier/+function-local-dataclasses.repair.rst +++ /dev/null @@ -1,6 +0,0 @@ -Fix construction of function-local dataclass classes. Classes derived -from the dataclass base classes (``DataclassObject``, -``DataclassObjectMutable``, ``DataclassProtocol``, and -``DataclassProtocolMutable``) failed with ``TypeError`` when defined -inside a function body, because the construction marker could not be -recognized during dataclass slot reproduction. diff --git a/.auxiliary/data/towncrier/+internal-name-contributions.enhance.rst b/.auxiliary/data/towncrier/+internal-name-contributions.enhance.rst deleted file mode 100644 index 5c7f0bc..0000000 --- a/.auxiliary/data/towncrier/+internal-name-contributions.enhance.rst +++ /dev/null @@ -1,11 +0,0 @@ -Attribute internal marking now follows metaclass-carried -contributions: the attribute namer's ``is_internal_name`` detector -marks framework names (grammar-constrained static stems and both -digest-mangled forms), the abstract-base wiring marks stdlib ABC -machinery names only where that machinery operates, and downstream -``class_factory`` namers contribute their own detectors, so -``explain_attribute`` reports downstream framework attributes -truthfully. ``is_internal_name`` takes the target and consults the -metaclass chain; lookalike names that fail the detector grammar stay -unmarked, and the public ``_dynadoc_fragments_`` declaration -convention is never marked. diff --git a/.auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst b/.auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst new file mode 100644 index 0000000..8123141 --- /dev/null +++ b/.auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst @@ -0,0 +1,8 @@ +Expose the class-level mutability exemption sets publicly from +``classcore.standard.nomina``: ``abc_class_mutables`` (stdlib ABC +machinery) and ``typing_class_mutables`` (stdlib typing machinery, +including the CPython 3.15 ``__typing_is_deprecated_inherited_runtime_protocol__`` +dunder). Downstream packages share these sets by reference instead of +carrying copies which can drift, as Frigid's did on Python 3.15. The +canonical home remains ``classcore.standard.__``; the nomina aliases +are identically the same objects. diff --git a/.auxiliary/data/towncrier/+python315-typing-dunder.repair.rst b/.auxiliary/data/towncrier/+python315-typing-dunder.repair.rst deleted file mode 100644 index 4525c18..0000000 --- a/.auxiliary/data/towncrier/+python315-typing-dunder.repair.rst +++ /dev/null @@ -1,5 +0,0 @@ -Permit CPython 3.15+ typing machinery dunder on immutable classes. -``typing`` sets ``__typing_is_deprecated_inherited_runtime_protocol__`` -during class decoration and in the ``runtime_checkable`` decorator; the -class-level immutability behavior now exempts it, and the protocol -member snapshot excludes it from ``__protocol_attrs__``. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index da4700e..60cee32 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c40ef55f9a4890a37893d52812b8c5dca5c26bc4a2bbbcf70d6689e387c5bf14 -size 673108 +oid sha256:37bcc7c8d0f7356200cce24284e4a7e343d7f1a4b326fb4198932e346cf8a583 +size 681692 diff --git a/documentation/changelog.rst b/documentation/changelog.rst index d14c19e..1a65b06 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,22 @@ Release Notes .. towncrier release notes start +classcore 1.14 (2026-10-15) +=========================== + +Enhancements +------------ + +- Expose the class-level mutability exemption sets publicly from + ``classcore.standard.nomina``: ``abc_class_mutables`` (stdlib ABC + machinery) and ``typing_class_mutables`` (stdlib typing machinery, + including the CPython 3.15 ``__typing_is_deprecated_inherited_runtime_protocol__`` + dunder). Downstream packages share these sets by reference instead of + carrying copies which can drift, as Frigid's did on Python 3.15. The + canonical home remains ``classcore.standard.__``; the nomina aliases + are identically the same objects. + + classcore 1.13 (2026-10-15) =========================== diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index b8c3179..81ad019 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.13' +__version__ = '1.14' standard.finalize_module( diff --git a/sources/classcore/standard/behaviors.py b/sources/classcore/standard/behaviors.py index 4d05d1d..525105f 100644 --- a/sources/classcore/standard/behaviors.py +++ b/sources/classcore/standard/behaviors.py @@ -185,10 +185,15 @@ def render_verifier_text( verifier: __.typx.Any, / ) -> str: qualified name is available. Strings do not reach this function; they are classified as names before rendering. A non-string 'pattern' attribute is not trusted as text; such - verifiers fall through to qualified-name rendering. Hostile - metadata (raising descriptors, custom '__getattribute__') - yields '<unrenderable>' rather than raising, so diagnostic - rendering never disrupts its callers. + verifiers fall through to qualified-name rendering, where a + non-string qualified name falls back to '<anonymous>' while + a non-string module name preserves the qualified name. + Hostile metadata (raising descriptors, custom + '__getattribute__') yields '<unrenderable>' rather than + raising, so diagnostic rendering never disrupts its callers. + The broad 'except Exception' is deliberate: rendering is + diagnostics, and diagnostics must never break enforcement + or survey paths. Note: the inspect utilities were considered and rejected here; they resolve modules to objects and provide no qualified-name @@ -501,8 +506,8 @@ def _render_verifier_text( verifier: __.typx.Any, / ) -> str: pattern = getattr( verifier, 'pattern', None ) if isinstance( pattern, str ): return pattern qualname = getattr( verifier, '__qualname__', None ) - if qualname is None: return '<anonymous>' + if not isinstance( qualname, str ): return '<anonymous>' module = getattr( verifier, '__module__', None ) - if module is None or '<locals>' in qualname: + if not isinstance( module, str ) or '<locals>' in qualname: return qualname return f"{module}.{qualname}" diff --git a/sources/classcore/standard/nomina.py b/sources/classcore/standard/nomina.py index 71b2049..9db7391 100644 --- a/sources/classcore/standard/nomina.py +++ b/sources/classcore/standard/nomina.py @@ -29,6 +29,10 @@ immutability_label = 'immutability' +abc_class_mutables = __.abc_class_mutables +typing_class_mutables = __.typing_class_mutables + + BehaviorExclusionNames: __.typx.TypeAlias = __.cabc.Set[ str ] BehaviorExclusionNamesOmni: __.typx.TypeAlias = ( BehaviorExclusionNames | __.typx.Literal[ '*' ] ) diff --git a/tests/test_000_classcore/test_310_standard_internals.py b/tests/test_000_classcore/test_310_standard_internals.py index b874098..cc3f285 100644 --- a/tests/test_000_classcore/test_310_standard_internals.py +++ b/tests/test_000_classcore/test_310_standard_internals.py @@ -33,3 +33,17 @@ def test_200_provide_error_class( ): assert exceptions.AttributeImmutability is error_class with pytest.raises( exceptions.ErrorProvideFailure ): module.provide_error_class( 'DoesNotExist' ) + + +def test_201_mutables_sets_public_from_nomina( ): + ''' Class-level mutability exemption sets are publicly re-exported + from 'standard.nomina' (canonical home remains 'standard.__'), + so downstream packages need not import across the private + module boundary. ''' + hub = cache_import_module( MODULE_QNAME ) + nomina = cache_import_module( f"{PACKAGE_NAME}.standard.nomina" ) + assert nomina.abc_class_mutables is hub.abc_class_mutables + assert nomina.typing_class_mutables is hub.typing_class_mutables + assert ( + '__typing_is_deprecated_inherited_runtime_protocol__' + in nomina.typing_class_mutables ) diff --git a/tests/test_000_classcore/test_315_standard_cascade.py b/tests/test_000_classcore/test_315_standard_cascade.py index b52e633..c684a61 100644 --- a/tests/test_000_classcore/test_315_standard_cascade.py +++ b/tests/test_000_classcore/test_315_standard_cascade.py @@ -321,6 +321,23 @@ class _Patterned: 'test_320_render_verifier_text.<locals>._Patterned' == module.render_verifier_text( _Patterned ) ) + class _NonStrQualname: + def __getattribute__( self, name: str ): + if '__qualname__' == name: return 42 + return object.__getattribute__( self, name ) + + assert '<anonymous>' == module.render_verifier_text( + _NonStrQualname( ) ) + + class _NonStrModule: + def __getattribute__( self, name: str ): + if '__qualname__' == name: return 'weird.Qual' + if '__module__' == name: return 42 + return object.__getattribute__( self, name ) + + assert 'weird.Qual' == module.render_verifier_text( + _NonStrModule( ) ) + def local_predicate( name: str ) -> bool: return True assert ( diff --git a/tests/test_000_classcore/test_347_abstract_classes.py b/tests/test_000_classcore/test_347_abstract_classes.py index cbbc55d..7993902 100644 --- a/tests/test_000_classcore/test_347_abstract_classes.py +++ b/tests/test_000_classcore/test_347_abstract_classes.py @@ -17,16 +17,19 @@ # # #============================================================================# - ''' Tests for abstract base class support. ''' import abc import typing +import pytest + from .__ import PACKAGE_NAME, cache_import_module + MODULE_QNAME = f"{PACKAGE_NAME}.standard.classes" +EXCEPTIONS_QNAME = f"{PACKAGE_NAME}.exceptions" # =========================================================================== # @@ -207,3 +210,15 @@ def greet( self ) -> str: ... GreeterProto.__dict__[ '__typing_is_deprecated_inherited_runtime_protocol__' ] is False ) + + +def test_524_typing_names_refused_on_non_protocol_abstract( ): + ''' Typing-only mutability exemptions do not extend to non-protocol + abstract classes: only the ABC machinery set is mutable there. ''' + module = cache_import_module( MODULE_QNAME ) + exceptions = cache_import_module( EXCEPTIONS_QNAME ) + + class Concrete( module.AbstractObject ): pass + + with pytest.raises( exceptions.AttributeImmutability ): + Concrete._is_runtime_protocol = True From a1283bcfcc3009198cc35ef828eb6cac16c01f44 Mon Sep 17 00:00:00 2001 From: Github Actions Robot <emcd@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:17:44 +0000 Subject: [PATCH 28/28] Update documents for publication. (1.15) --- .../+attrname-calculator-export.enhance.rst | 7 ++ .../+nomina-mutables-export.enhance.rst | 8 -- .auxiliary/publications/website.tar.xz | 4 +- .../.openspec.yaml | 2 + .../design.md | 81 +++++++++++++++++++ .../proposal.md | 47 +++++++++++ .../specs/downstream-namer-calculator/spec.md | 71 ++++++++++++++++ .../tasks.md | 30 +++++++ .../specs/downstream-namer-calculator/spec.md | 77 ++++++++++++++++++ documentation/changelog.rst | 15 ++++ sources/classcore/__/nomina.py | 29 +++++-- sources/classcore/__init__.py | 2 +- sources/classcore/nomina.py | 7 +- sources/classcore/standard/nomina.py | 1 + .../test_310_standard_internals.py | 10 +++ .../test_320_standard_explanations.py | 50 ++++++++++-- 16 files changed, 415 insertions(+), 26 deletions(-) create mode 100644 .auxiliary/data/towncrier/+attrname-calculator-export.enhance.rst delete mode 100644 .auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/.openspec.yaml create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/design.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/proposal.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/specs/downstream-namer-calculator/spec.md create mode 100644 documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/tasks.md create mode 100644 documentation/architecture/openspec/specs/downstream-namer-calculator/spec.md diff --git a/.auxiliary/data/towncrier/+attrname-calculator-export.enhance.rst b/.auxiliary/data/towncrier/+attrname-calculator-export.enhance.rst new file mode 100644 index 0000000..6f7ff01 --- /dev/null +++ b/.auxiliary/data/towncrier/+attrname-calculator-export.enhance.rst @@ -0,0 +1,7 @@ +Export the reference attribute-name calculator publicly from +``classcore.standard.nomina`` with a configurable package prefix: +downstream packages construct ``AttrnameCalculator( package_name = +... )`` to generate and detect grammar-conformant internal names +under their own prefix, sharing classcore's vocabulary by +construction instead of mirroring it. The grammar containers stay +private; detection consults reserved framework vocabulary. diff --git a/.auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst b/.auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst deleted file mode 100644 index 8123141..0000000 --- a/.auxiliary/data/towncrier/+nomina-mutables-export.enhance.rst +++ /dev/null @@ -1,8 +0,0 @@ -Expose the class-level mutability exemption sets publicly from -``classcore.standard.nomina``: ``abc_class_mutables`` (stdlib ABC -machinery) and ``typing_class_mutables`` (stdlib typing machinery, -including the CPython 3.15 ``__typing_is_deprecated_inherited_runtime_protocol__`` -dunder). Downstream packages share these sets by reference instead of -carrying copies which can drift, as Frigid's did on Python 3.15. The -canonical home remains ``classcore.standard.__``; the nomina aliases -are identically the same objects. diff --git a/.auxiliary/publications/website.tar.xz b/.auxiliary/publications/website.tar.xz index 60cee32..7eefd99 100644 --- a/.auxiliary/publications/website.tar.xz +++ b/.auxiliary/publications/website.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37bcc7c8d0f7356200cce24284e4a7e343d7f1a4b326fb4198932e346cf8a583 -size 681692 +oid sha256:2031e8f36dc340a789604703cdf91ef19f8c382c7cbcc74d9073b9c8b85bcd07 +size 698908 diff --git a/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/.openspec.yaml b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/.openspec.yaml new file mode 100644 index 0000000..0cb4765 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-16 diff --git a/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/design.md b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/design.md new file mode 100644 index 0000000..3cc4b1c --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/design.md @@ -0,0 +1,81 @@ +# Design: Export Attrname Calculator + +## Context + +`AttrnameCalculator` in `classcore.__.nomina` is the reference +implementation of the attribute-namer convention: `__call__` +generates `_{package}_{level}_{core}_` names and `is_internal_name` +detects them (plus digest-mangled forms), both closed over a +module-level `package_name`. Downstream packages (Frigid 4.4) need +the same grammar under their own prefix. A local mirror reintroduces +the drift failure mode already observed with the mutables sets. +Stakeholders: Classcore Owner (implements), Frigid Owner (consumes, +Tier-1 reviewer), Advisor (Cc, design input given). + +## Goals / Non-Goals + +Goals: one public import path for the calculator; prefix +configuration without method overrides; grammar stays single-sourced +and evolvable; no behavior change for classcore's own names. +Non-goals: no grammar snapshot exports; no shared test harness; no +changes to the contributions collection mechanism or the +`calculate_contribution_name` function. + +## Decisions + +1. **Constructor `package_name` argument (default: current module + derivation), not class attribute or factory.** Constructor + configuration makes registration-time stability structural: the + value is fixed where the namer is created, and nothing invites + later reassignment. Class-attribute form was considered (matches + framework class-attribute configuration idiom; preserves IS-A + without instantiation) but rejected: mutable attributes are + exactly what this package family avoids, and Frigid needs no + subclass at all — instantiation suffices. Factory + (`produce_attrname_calculator`) was considered and rejected: a + new public function for one value adds surface without function. +2. **Grammar containers stay private.** `_levels`, `_cores`, + `_mangled_cores`, `_hex_digits` remain module-private in + `classcore.__.nomina`. The public promise is the shared + naming/detection convention, not frozen vocabulary. Documented + as reserved framework grammar: shape conformance, not proof an + attribute was generated. +3. **Preserve the generation/detection asymmetry.** `__call__` + emits the two class-level mangling stems; the detector accepts + only their completed 64-hex digest forms, never bare stems. + Parameterization must not require + `detector( __call__( ... ) )` round-trips. +4. **Alias export, as with the mutables sets.** + `AttrnameCalculator = __.AttrnameCalculator` in + `classcore.standard.nomina`; canonical home stays + `classcore.__.nomina`. Identity (not copy) preserved. +5. **Per-project conformance tests, no shared harness.** Upstream + tests the public calculator (positive/negative/mangled cases + plus the no-override downstream-subclass form); Frigid inventories + its own call sites. The existing AST drift scan stays + per-project (it covers only bare-name literal calls). + +## Risks / Trade-offs + +- [Risk] Downstream mutates `package_name` post-registration, so + previously generated names stop matching. → Mitigation: document + as construction-time configuration; no setter or helper invites + mutation. +- [Risk] Grammar evolution breaks downstream expectations (new core + classifies additional names as internal). → Mitigation: that IS + the lockstep contract; document that classification follows the + newest grammar by construction. +- [Risk] `type`-statement-style laziness or introspection + differences. → Mitigation: none needed; plain class, no aliases + involved. + +## Migration Plan + +Additive only: existing `AttrnameCalculator()` behavior unchanged; +existing downstream pattern (subclass + overrides) keeps working. +No migration, no rollback beyond revert. + +## Open Questions + +- None for implementation. Frigid (Tier-1) may surface + consumption-shape feedback at review; Advisor may re-enter. diff --git a/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/proposal.md b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/proposal.md new file mode 100644 index 0000000..7610653 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/proposal.md @@ -0,0 +1,47 @@ +## Why + +Downstream packages need the reference attribute-name calculator to +contribute grammar-conformant internal names, but `AttrnameCalculator` +lives in private `classcore.__.nomina`. Frigid's local copy of the +related mutables sets already drifted once (the 3.15 typing-dunder +refusal); a local copy of the detector grammar would reintroduce the +same failure mode. Exporting the calculator with a configurable +package prefix lets downstream share the grammar by construction. + +## What Changes + +- Export `AttrnameCalculator` from public `classcore.standard.nomina` + (canonical home remains `classcore.__.nomina`; the export is the + identical object, as with the mutables sets). +- Add a constructor `package_name` argument (default preserves + current classcore behavior) consumed by both `__call__` and + `is_internal_name`, so downstream configures the prefix without + overriding either method. The prefix is construction-time + configuration and must not change after registration. +- Keep `_levels`, `_cores`, `_mangled_cores`, and `_hex_digits` + private; document the grammar as reserved framework vocabulary + (shape conformance, not proof of generation). +- Update the downstream-pattern test to the no-override subclass + form; per-project grammar-conformance tests live upstream, and + call-site inventories live downstream (no shared test API). + +## Capabilities + +### New Capabilities + +- `downstream-namer-calculator`: public calculator export with + configurable package prefix for downstream internal-name + contributions. + +### Modified Capabilities + +- None. No existing spec requirements change; detection behavior + for classcore's own names is unchanged. + +## Impact + +- `sources/classcore/__/nomina.py` (prefix seam, grammar + documentation), `sources/classcore/standard/nomina.py` + (re-export), downstream-pattern tests. +- Downstream: Frigid 4.4 consumes the export instead of mirroring + the grammar; no classcore runtime behavior changes. diff --git a/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/specs/downstream-namer-calculator/spec.md b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/specs/downstream-namer-calculator/spec.md new file mode 100644 index 0000000..c1e30ce --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/specs/downstream-namer-calculator/spec.md @@ -0,0 +1,71 @@ +## ADDED Requirements + +### Requirement: Public calculator export + +`AttrnameCalculator` SHALL be importable from public +`classcore.standard.nomina` and SHALL be identically the object +defined in `classcore.__.nomina` (canonical home unchanged). + +#### Scenario: Downstream imports without private path + +- **WHEN** a downstream package imports `AttrnameCalculator` from + `classcore.standard.nomina` +- **THEN** the import succeeds and the object is identically + `classcore.__.nomina.AttrnameCalculator`. + +### Requirement: Configurable package prefix + +The calculator SHALL accept a `package_name` constructor argument +defaulting to current classcore behavior. Both `__call__` and +`is_internal_name` SHALL resolve the same configured prefix, and +the prefix SHALL be treated as construction-time configuration. + +#### Scenario: Default preserves classcore behavior + +- **WHEN** `AttrnameCalculator()` is constructed without arguments +- **THEN** `__call__` emits `_{classcore}_{level}_{core}_` names and + `is_internal_name` detects exactly the names it detected before + this change. + +#### Scenario: Downstream prefix without overrides + +- **WHEN** a downstream package constructs + `AttrnameCalculator( package_name = 'frigid' )` (or subclasses + without overriding either method) +- **THEN** `__call__` emits `_frigid_` names and + `is_internal_name` detects grammar-conformant `_frigid_` names + while rejecting `_classcore_` names and non-grammar lookalikes + such as `_frigid_custom_`. + +#### Scenario: Prefix mismatch does not match + +- **WHEN** names are generated under one prefix and checked by a + detector configured with another prefix +- **THEN** they do not match. Prefix configuration is + construction-time; post-registration mutation is unsupported. + +### Requirement: Private grammar vocabulary + +`_levels`, `_cores`, `_mangled_cores`, and `_hex_digits` SHALL +remain private to `classcore.__.nomina`. Documentation SHALL state +the grammar is reserved framework vocabulary (shape conformance), +not proof an attribute was generated. + +#### Scenario: Grammar evolves without downstream changes + +- **WHEN** classcore adds a core to the private grammar +- **THEN** downstream calculators constructed as specified detect + the new core's names with no downstream modification. + +### Requirement: Generation-detection asymmetry preserved + +`__call__` SHALL continue to emit the two class-level mangling +stems, and the detector SHALL continue to accept only their +completed 64-hex digest forms, never bare stems, under any +configured prefix. + +#### Scenario: Mangled forms under downstream prefix + +- **WHEN** a downstream calculator detects + `_frigid_class_behaviors_<64 hex>` and `_frigid_class_behaviors_` +- **THEN** the digest form is internal and the bare stem is not. diff --git a/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/tasks.md b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/tasks.md new file mode 100644 index 0000000..56fac89 --- /dev/null +++ b/documentation/architecture/openspec/changes/archive/2026-10-16-export-attrname-calculator/tasks.md @@ -0,0 +1,30 @@ +## 1. Calculator prefix seam + +- [x] 1.1 Add `package_name` constructor argument to + `AttrnameCalculator` (default preserves classcore behavior) and + resolve it via `self` in `__call__` and `is_internal_name`. +- [x] 1.2 Document the prefix as construction-time configuration, + the reserved-grammar vocabulary, and the + generation-detection asymmetry in the class docstring. + +## 2. Public export + +- [x] 2.1 Re-export `AttrnameCalculator` from + `classcore.standard.nomina` (alias of the hub-canonical object). +- [x] 2.2 Add identity test (`is` hub object) plus default-behavior + conformance to the internals tests. + +## 3. Downstream-pattern tests + +- [x] 3.1 Rework the downstream-pattern test to the no-override + form (constructor-configured or subclassed prefix, both methods + untouched): positive, negative (`_frigid_custom_`), and mangled + conformance cases. +- [x] 3.2 Verify per-project AST drift scan still passes + unchanged. + +## 4. Release artifacts + +- [x] 4.1 Add towncrier `enhance` fragment for the public export. +- [x] 4.2 Validate full gates (make-all, QA matrix, strict + OpenSpec) and route Tier-1 (Reviewer General + Frigid Owner). diff --git a/documentation/architecture/openspec/specs/downstream-namer-calculator/spec.md b/documentation/architecture/openspec/specs/downstream-namer-calculator/spec.md new file mode 100644 index 0000000..eb4303b --- /dev/null +++ b/documentation/architecture/openspec/specs/downstream-namer-calculator/spec.md @@ -0,0 +1,77 @@ +# downstream-namer-calculator Specification + +## Purpose + +Public reference calculator letting downstream packages share +classcore's internal-name grammar by construction instead of +mirroring it. +## Requirements +### Requirement: Public calculator export + +`AttrnameCalculator` SHALL be importable from public +`classcore.standard.nomina` and SHALL be identically the object +defined in `classcore.__.nomina` (canonical home unchanged). + +#### Scenario: Downstream imports without private path + +- **WHEN** a downstream package imports `AttrnameCalculator` from + `classcore.standard.nomina` +- **THEN** the import succeeds and the object is identically + `classcore.__.nomina.AttrnameCalculator`. + +### Requirement: Configurable package prefix + +The calculator SHALL accept a `package_name` constructor argument +defaulting to current classcore behavior. Both `__call__` and +`is_internal_name` SHALL resolve the same configured prefix, and +the prefix SHALL be treated as construction-time configuration. + +#### Scenario: Default preserves classcore behavior + +- **WHEN** `AttrnameCalculator()` is constructed without arguments +- **THEN** `__call__` emits `_{classcore}_{level}_{core}_` names and + `is_internal_name` detects exactly the names it detected before + this change. + +#### Scenario: Downstream prefix without overrides + +- **WHEN** a downstream package constructs + `AttrnameCalculator( package_name = 'frigid' )` (or subclasses + without overriding either method) +- **THEN** `__call__` emits `_frigid_` names and + `is_internal_name` detects grammar-conformant `_frigid_` names + while rejecting `_classcore_` names and non-grammar lookalikes + such as `_frigid_custom_`. + +#### Scenario: Prefix mismatch does not match + +- **WHEN** names are generated under one prefix and checked by a + detector configured with another prefix +- **THEN** they do not match. Prefix configuration is + construction-time; post-registration mutation is unsupported. + +### Requirement: Private grammar vocabulary + +`_levels`, `_cores`, `_mangled_cores`, and `_hex_digits` SHALL +remain private to `classcore.__.nomina`. Documentation SHALL state +the grammar is reserved framework vocabulary (shape conformance), +not proof an attribute was generated. + +#### Scenario: Grammar evolves without downstream changes + +- **WHEN** classcore adds a core to the private grammar +- **THEN** downstream calculators constructed as specified detect + the new core's names with no downstream modification. + +### Requirement: Generation-detection asymmetry preserved + +`__call__` SHALL continue to emit the two class-level mangling +stems, and the detector SHALL continue to accept only their +completed 64-hex digest forms, never bare stems, under any +configured prefix. + +#### Scenario: Mangled forms under downstream prefix + +- **WHEN** a downstream calculator detects + `_frigid_class_behaviors_<64 hex>` and `_frigid_class_behaviors_` +- **THEN** the digest form is internal and the bare stem is not. diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 1a65b06..11e213d 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -23,6 +23,21 @@ Release Notes .. towncrier release notes start +classcore 1.15 (2026-10-16) +=========================== + +Enhancements +------------ + +- Export the reference attribute-name calculator publicly from + ``classcore.standard.nomina`` with a configurable package prefix: + downstream packages construct ``AttrnameCalculator( package_name = + ... )`` to generate and detect grammar-conformant internal names + under their own prefix, sharing classcore's vocabulary by + construction instead of mirroring it. The grammar containers stay + private; detection consults reserved framework vocabulary. + + classcore 1.14 (2026-10-15) =========================== diff --git a/sources/classcore/__/nomina.py b/sources/classcore/__/nomina.py index cd768ea..58d6112 100644 --- a/sources/classcore/__/nomina.py +++ b/sources/classcore/__/nomina.py @@ -51,13 +51,32 @@ class AttrnameCalculator: Reference implementation of the attribute namer convention: call instances to produce names, and consult ``is_internal_name`` - to detect names this framework generated. Downstream packages - may subclass (or otherwise provide callables exposing the same - optional detector) to contribute their own internal names. + to detect names this framework generated. The package prefix is + construction-time configuration (default: this package): + downstream packages construct with their own prefix rather + than overriding either method, and it is read-only + thereafter: assignment raises, so a registered prefix cannot + drift. Detection consults the reserved + framework grammar (shape conformance, not proof of + generation); generation emits the class-level mangling stems + whose bare forms the detector does not accept. Downstream + callers writing conformance or drift tests can detect a + generated name without the private mangled-core vocabulary: + try the bare form first, then the form with 64 'a' + characters appended, which covers the two class-level + mangling stems the bare form never matches. ''' + def __init__( self, package_name: str = package_name ) -> None: + self._package_name = package_name + + @property + def package_name( self ) -> str: + ''' Package prefix this calculator generates and detects. ''' + return self._package_name + def is_internal_name( self, name: str ) -> bool: - prefix = f"_{package_name}_" + prefix = f"_{self.package_name}_" if not name.startswith( prefix ): return False body = name[ len( prefix ): ] @@ -84,7 +103,7 @@ def is_internal_name( self, name: str ) -> bool: return False def __call__( self, level: str, core: str ) -> str: - return f"_{package_name}_{level}_{core}_" + return f"_{self.package_name}_{level}_{core}_" calculate_attrname = AttrnameCalculator( ) diff --git a/sources/classcore/__init__.py b/sources/classcore/__init__.py index 81ad019..2ed2de7 100644 --- a/sources/classcore/__init__.py +++ b/sources/classcore/__init__.py @@ -62,7 +62,7 @@ class decorators. Furthermore, the exception classes in the __version__: __.typx.Annotated[ str, __.ddoc.Visibilities.Reveal ] -__version__ = '1.14' +__version__ = '1.15' standard.finalize_module( diff --git a/sources/classcore/nomina.py b/sources/classcore/nomina.py index 7a05905..ffcd165 100644 --- a/sources/classcore/nomina.py +++ b/sources/classcore/nomina.py @@ -29,8 +29,11 @@ __.ddoc.Doc( ''' Names attribute from level and core arguments. - Level will be one of 'class', 'instances', or 'instance'. - Core will be the core of the name as supplied this package. + Level will be one of 'class', 'classes', 'instance', or + 'instances'. Core will be one of the framework cores + named in the attribute-namer grammar (mutables and + visibles rules, assigner, deleter, and surveyor cores, + and construction bookkeeping). Can be used by downstream packages to determine names of bookkeeping attributes assigned by this package. diff --git a/sources/classcore/standard/nomina.py b/sources/classcore/standard/nomina.py index 9db7391..1e53351 100644 --- a/sources/classcore/standard/nomina.py +++ b/sources/classcore/standard/nomina.py @@ -31,6 +31,7 @@ abc_class_mutables = __.abc_class_mutables typing_class_mutables = __.typing_class_mutables +AttrnameCalculator = __.AttrnameCalculator BehaviorExclusionNames: __.typx.TypeAlias = __.cabc.Set[ str ] diff --git a/tests/test_000_classcore/test_310_standard_internals.py b/tests/test_000_classcore/test_310_standard_internals.py index cc3f285..9316fa4 100644 --- a/tests/test_000_classcore/test_310_standard_internals.py +++ b/tests/test_000_classcore/test_310_standard_internals.py @@ -47,3 +47,13 @@ def test_201_mutables_sets_public_from_nomina( ): assert ( '__typing_is_deprecated_inherited_runtime_protocol__' in nomina.typing_class_mutables ) + + +def test_202_calculator_public_from_nomina( ): + ''' Reference calculator is publicly re-exported from + 'standard.nomina' (canonical home remains 'classcore.__'), + defaulting to the classcore package prefix. ''' + hub_nomina = cache_import_module( f"{PACKAGE_NAME}.__.nomina" ) + std_nomina = cache_import_module( f"{PACKAGE_NAME}.standard.nomina" ) + assert std_nomina.AttrnameCalculator is hub_nomina.AttrnameCalculator + assert 'classcore' == hub_nomina.AttrnameCalculator( ).package_name diff --git a/tests/test_000_classcore/test_320_standard_explanations.py b/tests/test_000_classcore/test_320_standard_explanations.py index 7a28f00..7c3725f 100644 --- a/tests/test_000_classcore/test_320_standard_explanations.py +++ b/tests/test_000_classcore/test_320_standard_explanations.py @@ -583,21 +583,21 @@ class Bare( metaclass = BareMeta ): pass assert not module.explain_attribute( Bare, '_custom_class_core_' ).internal - from classcore.__ import AttrnameCalculator + from classcore.standard.nomina import AttrnameCalculator - class DownstreamNamer( AttrnameCalculator ): - def __call__( self, level: str, core: str ) -> str: - return f"_downstream_{level}_{core}_" - def is_internal_name( self, name: str ) -> bool: - return name.startswith( '_downstream_' ) + namer = AttrnameCalculator( package_name = 'downstream' ) - @class_factory( attributes_namer = DownstreamNamer( ) ) + @class_factory( attributes_namer = namer ) class DownstreamMeta( type ): pass class Downstream( metaclass = DownstreamMeta ): pass assert module.explain_attribute( - Downstream, '_downstream_class_core_' ).internal + Downstream, '_downstream_instances_mutables_names_' ).internal + assert not module.explain_attribute( + Downstream, '_downstream_custom_' ).internal + assert not module.explain_attribute( + Downstream, '_classcore_instances_mutables_names_' ).internal def test_427_decorator_path_inheritance( ): @@ -719,3 +719,37 @@ class Example: pass assert '<unrenderable>' == survey.decision.predicate assert survey.permissible del instance.alpha + + +def test_432_calculator_prefix_mismatch( ): + ''' Configured prefix governs both generation and detection: + cross-prefix names do not match, mangled digest forms are + accepted, and bare mangling stems are not. ''' + from classcore.standard.nomina import AttrnameCalculator + + frigid = AttrnameCalculator( package_name = 'frigid' ) + assert '_frigid_class_behaviors_' == frigid( 'class', 'behaviors' ) + assert frigid.is_internal_name( + '_frigid_instances_mutables_names_' ) + assert frigid.is_internal_name( + '_frigid_class_behaviors_' + 'b' * 64 ) + assert not frigid.is_internal_name( '_frigid_class_behaviors_' ) + assert not frigid.is_internal_name( + '_classcore_instances_mutables_names_' ) + assert not frigid.is_internal_name( '_frigid_custom_' ) + + +def test_433_calculator_prefix_read_only( ): + ''' Configured prefix is read-only: post-construction assignment + raises, and generation and detection retain the original. ''' + from classcore.standard.nomina import AttrnameCalculator + + namer = AttrnameCalculator( package_name = 'frigid' ) + assert 'frigid' == namer.package_name + with pytest.raises( AttributeError ): + # Assignment, rather than direct attribute syntax, so the + # read-only property violation is exercised, not type-checked. + setattr( namer, 'package_name', 'classcore' ) + assert '_frigid_class_behaviors_' == namer( 'class', 'behaviors' ) + assert namer.is_internal_name( + '_frigid_instances_mutables_names_' )