From 71d32994af2ee247d6a1aeac54ea9a3191c1742b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 10:12:13 -0400 Subject: [PATCH 1/3] Stop registering the witness-name mixins; require metaclass-registry 0.4 WitnessNameAliasMixin declared an AutoRegisterMeta family keyed by the witness_name descriptor. Nothing reads that registry. Every line witness that inherits ClassNameWitnessNameMixin or QualnameWitnessNameMixin inherited the mixin's key, so 19 classes claimed two keys between them and each displaced the last; a class listing the mixin first also never reached the LineWitnessCandidate registry. metaclass-registry 0.4 rejects the second claim at import. The mixin is now a plain ABC that supplies witness_name. Each witness registers once, in LineWitnessCandidate, under its own class-name key. Co-Authored-By: Claude Opus 5.5 --- nominal_refactor_advisor/detectors/_base.py | 5 +- pyproject.toml | 2 +- uv.lock | 156 +++++++++++++++----- 3 files changed, 121 insertions(+), 42 deletions(-) diff --git a/nominal_refactor_advisor/detectors/_base.py b/nominal_refactor_advisor/detectors/_base.py index 98bdc9e6..025e4b6e 100644 --- a/nominal_refactor_advisor/detectors/_base.py +++ b/nominal_refactor_advisor/detectors/_base.py @@ -6312,9 +6312,8 @@ def from_module(cls, module: ParsedModule) -> tuple[Self, ...]: raise NotImplementedError -class WitnessNameAliasMixin(ABC, metaclass=AutoRegisterMeta): - __registry_key__ = "witness_name" - __skip_if_no_key__ = True +class WitnessNameAliasMixin(ABC): + """Supplies ``witness_name`` to a line witness; the witness's own family registers it.""" @property @abstractmethod diff --git a/pyproject.toml b/pyproject.toml index 27046958..97a55bd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.11" authors = [{ name = "OpenHCSDev" }] dependencies = [ - "metaclass-registry>=0.1.4", + "metaclass-registry>=0.4.0", "tree-sitter>=0.25,<0.26", "tree-sitter-python>=0.25,<0.26", ] diff --git a/uv.lock b/uv.lock index c36d86b6..0bac07d9 100644 --- a/uv.lock +++ b/uv.lock @@ -307,6 +307,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -422,11 +431,11 @@ wheels = [ [[package]] name = "metaclass-registry" -version = "0.1.4" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/1b/13efa820cbf7b61a52ac0266023f5173156baa0648612fe132bdf1b32371/metaclass_registry-0.1.4.tar.gz", hash = "sha256:5ed7e42ded30a5fcd47b70ef81689aa8c64e3091fe43fca22dfb8d338312432a", size = 22710, upload-time = "2026-01-25T23:35:48.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a7/480cac5c73f0b5b50359ce2b5248d816b331b8d104e5ffb0741de669c020/metaclass_registry-0.4.0.tar.gz", hash = "sha256:b66fce0cbeb6460d7835103d4206dd11be4b607247db75608a843e2e57b60fd3", size = 45713, upload-time = "2026-10-12T12:11:39.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/fb/072fdcea75b9f361c238b327097e60e4b23ea61f009ba673ca5146681b9e/metaclass_registry-0.1.4-py3-none-any.whl", hash = "sha256:6f229e2eabe41eb0db64d0504dc882780345cfbf66f7a593088ebf1f2bbc23a4", size = 18371, upload-time = "2026-01-25T23:35:46.575Z" }, + { url = "https://files.pythonhosted.org/packages/40/e9/cc748c2779cea13bae6f89f7db2e0f5ba5da47c5a9380c7c0eb73adf7ce7/metaclass_registry-0.4.0-py3-none-any.whl", hash = "sha256:9b68043883edafd5da04d171f3aeeb14e5fa27094ffcb154d959832091a91dcd", size = 33389, upload-time = "2026-10-12T12:11:38.121Z" }, ] [[package]] @@ -444,6 +453,8 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "metaclass-registry" }, + { name = "tree-sitter" }, + { name = "tree-sitter-python" }, ] [package.optional-dependencies] @@ -453,6 +464,7 @@ dev = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "pytest-xdist" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] @@ -466,11 +478,14 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=26.3" }, { name = "build", marker = "extra == 'dev'", specifier = ">=1.2" }, { name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = ">=7" }, - { name = "metaclass-registry", specifier = ">=0.1.4" }, + { name = "metaclass-registry", specifier = ">=0.4.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.8" }, { name = "sphinx", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "sphinx", marker = "extra == 'docs'", specifier = ">=8.0" }, + { name = "tree-sitter", specifier = ">=0.25,<0.26" }, + { name = "tree-sitter-python", specifier = ">=0.25,<0.26" }, ] provides-extras = ["dev", "docs"] @@ -558,6 +573,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -633,23 +661,23 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.12'" }, - { name = "babel", marker = "python_full_version < '3.12'" }, - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.12'" }, - { name = "imagesize", marker = "python_full_version < '3.12'" }, - { name = "jinja2", marker = "python_full_version < '3.12'" }, - { name = "packaging", marker = "python_full_version < '3.12'" }, - { name = "pygments", marker = "python_full_version < '3.12'" }, - { name = "requests", marker = "python_full_version < '3.12'" }, - { name = "roman-numerals", marker = "python_full_version < '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -664,23 +692,23 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -795,6 +823,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tree-sitter" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, + { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" }, + { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" From 0b01b98881f67808cc69b00e024974f5c39efac9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 10:25:47 -0400 Subject: [PATCH 2/3] Report a twice-claimed requested key as a ValueError on metaclass-registry 0.4 require_requested_key_compatibility builds the requested members through the real AutoRegisterMeta and compares the registry with the request. Two different classes asked for under one key used to overwrite each other and fail that comparison; 0.4 refuses the second class itself, with an error that is not a ValueError and so escaped the callers' handling. Translate it to the same ValueError. Tests: each class of every NRA family holds one key; the witness-name mixins register nothing; the requested-key check refuses a key claimed twice. Co-Authored-By: Claude Opus 5.5 --- nominal_refactor_advisor/registry_identity.py | 6 +- tests/test_registry_keys_have_one_owner.py | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/test_registry_keys_have_one_owner.py diff --git a/nominal_refactor_advisor/registry_identity.py b/nominal_refactor_advisor/registry_identity.py index 60d9db5d..79edc3b6 100644 --- a/nominal_refactor_advisor/registry_identity.py +++ b/nominal_refactor_advisor/registry_identity.py @@ -16,7 +16,7 @@ TypeVar, ) -from metaclass_registry import AutoRegisterMeta +from metaclass_registry import AutoRegisterMeta, DuplicateRegistryKeyError from .assignment_projection import SingleAssignmentAndValueNameProjection from .ast_projection import ( @@ -123,6 +123,10 @@ def require_requested_key_compatibility( self.native_metaclass.declaration(name, (base,), {key_attribute: key}) for name, key in native_entries ) + except DuplicateRegistryKeyError as error: + raise ValueError( + "Requested native registration does not preserve source entries" + ) from error except ( ValueError, TypeError, diff --git a/tests/test_registry_keys_have_one_owner.py b/tests/test_registry_keys_have_one_owner.py new file mode 100644 index 00000000..1f72cd45 --- /dev/null +++ b/tests/test_registry_keys_have_one_owner.py @@ -0,0 +1,82 @@ +"""A registry key names one class, in NRA's own families and in requested ones.""" + +import ast +import importlib +import pkgutil + +import pytest +from metaclass_registry import AutoRegisterMeta + +import nominal_refactor_advisor +from nominal_refactor_advisor.detectors._base import ( + ClassLineWitnessCandidate, + LineWitnessCandidate, + QualnameLineWitnessCandidate, + WitnessNameAliasMixin, +) +from nominal_refactor_advisor.registry_identity import AutoRegisterClassAuthority + + +def _registered_families() -> dict[type, dict[object, type]]: + for module in pkgutil.walk_packages( + nominal_refactor_advisor.__path__, f"{nominal_refactor_advisor.__name__}." + ): + importlib.import_module(module.name) + families: dict[type, dict[object, type]] = {} + seen: set[type] = set() + pending: list[type] = [object] + while pending: + cls = pending.pop() + if cls in seen: + continue + seen.add(cls) + pending.extend(type.__subclasses__(cls)) + registry = vars(cls).get("__registry__") + if isinstance(cls, AutoRegisterMeta) and registry is not None: + families[cls] = dict(dict.items(registry)) + return families + + +def test_every_registered_class_holds_exactly_one_key_of_its_family(): + families = _registered_families() + assert families + for root, registry in families.items(): + members = tuple(registry.values()) + assert len(set(members)) == len(members), root + + +def test_witness_name_mixins_supply_a_name_and_register_nothing(): + assert not isinstance(WitnessNameAliasMixin, AutoRegisterMeta) + registry = LineWitnessCandidate.__registry__ + keys = { + witness: [key for key, member in registry.items() if member is witness] + for witness in (ClassLineWitnessCandidate, QualnameLineWitnessCandidate) + } + for witness, witness_keys in keys.items(): + assert issubclass(witness, WitnessNameAliasMixin) + assert len(witness_keys) == 1, witness + assert keys[ClassLineWitnessCandidate] != keys[QualnameLineWitnessCandidate] + + +@pytest.mark.parametrize( + "entries,accepted", + ( + ((("A", "x"), ("B", "y")), True), + ((("A", "x"), ("B", "x")), False), + ((("A", "x"), ("A", "x")), False), + ), +) +def test_requested_registration_refuses_a_key_claimed_twice(entries, accepted): + source = ( + "class Root(metaclass=AutoRegisterMeta):\n" + " __registry_key__ = 'kind'\n" + " __registry__ = {}\n" + ) + (node,) = ast.parse(source).body + assert isinstance(node, ast.ClassDef) + authority = AutoRegisterClassAuthority(node) + if accepted: + authority.require_requested_key_compatibility(entries) + return + with pytest.raises(ValueError, match="does not preserve source entries"): + authority.require_requested_key_compatibility(entries) From 42b83b2ee23848d9fcdc74418625d4c4bfbc53d4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 11:03:31 -0400 Subject: [PATCH 3/3] Tests follow metaclass-registry 0.4: an inherited key no longer overwrites The type-keyed descent test executed a fixture in which a subclass inherits its parent's key and takes it over. 0.4 refuses that class at its declaration, so the test now asserts the refusal and that the key keeps its owner; the codemod's static rejection is asserted as before. The one-key-per-class guard walks classes by identity, because other tests declare unhashable classes. Co-Authored-By: Claude Opus 5.5 --- tests/test_refactor_advisor.py | 9 +++++---- tests/test_registry_keys_have_one_owner.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/test_refactor_advisor.py b/tests/test_refactor_advisor.py index 45e57900..77a601da 100644 --- a/tests/test_refactor_advisor.py +++ b/tests/test_refactor_advisor.py @@ -17,6 +17,7 @@ from unittest.mock import Mock import pytest +from metaclass_registry import DuplicateRegistryKeyError from registry_test_sources import _type_keyed_behavior_projection_source from native_use_test_support import ( @@ -26886,11 +26887,11 @@ def test_type_keyed_descent_rejects_inherited_registry_key_overwrite( module_path = tmp_path / "pkg/mod.py" _write_module(tmp_path, "pkg/mod.py", source) namespace: dict[str, object] = {} - exec(compile(source, module_path.as_posix(), "exec"), namespace) - event = namespace["NamedEvent"]() - assert namespace["render_event"](event) == "replacement" + # The registry refuses the second class at its declaration; the key keeps its owner. + with pytest.raises(DuplicateRegistryKeyError, match="Replacement"): + exec(compile(source, module_path.as_posix(), "exec"), namespace) assert namespace["EventProjection"].__registry__[namespace["NamedEvent"]] is ( - namespace["Replacement"] + namespace["NamedEventProjection"] ) snapshot = CodemodSourceSnapshot.from_modules(parse_python_modules(tmp_path)) diff --git a/tests/test_registry_keys_have_one_owner.py b/tests/test_registry_keys_have_one_owner.py index 1f72cd45..8b0dae37 100644 --- a/tests/test_registry_keys_have_one_owner.py +++ b/tests/test_registry_keys_have_one_owner.py @@ -23,16 +23,21 @@ def _registered_families() -> dict[type, dict[object, type]]: ): importlib.import_module(module.name) families: dict[type, dict[object, type]] = {} - seen: set[type] = set() + seen: set[int] = set() pending: list[type] = [object] while pending: cls = pending.pop() - if cls in seen: + # Other tests declare unhashable classes; identity is enough here. + if id(cls) in seen: continue - seen.add(cls) + seen.add(id(cls)) pending.extend(type.__subclasses__(cls)) registry = vars(cls).get("__registry__") - if isinstance(cls, AutoRegisterMeta) and registry is not None: + if ( + isinstance(cls, AutoRegisterMeta) + and registry is not None + and cls.__module__.startswith(nominal_refactor_advisor.__name__) + ): families[cls] = dict(dict.items(registry)) return families