Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions nominal_refactor_advisor/detectors/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion nominal_refactor_advisor/registry_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
9 changes: 5 additions & 4 deletions tests/test_refactor_advisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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))
Expand Down
87 changes: 87 additions & 0 deletions tests/test_registry_keys_have_one_owner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""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[int] = set()
pending: list[type] = [object]
while pending:
cls = pending.pop()
# Other tests declare unhashable classes; identity is enough here.
if id(cls) in seen:
continue
seen.add(id(cls))
pending.extend(type.__subclasses__(cls))
registry = vars(cls).get("__registry__")
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


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)
Loading
Loading