Skip to content

Commit 46daca0

Browse files
committed
Respect registered classvar strategy leaves
1 parent 8d98b55 commit 46daca0

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

nominal_refactor_advisor/detectors/_helpers.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5313,16 +5313,82 @@ def _nominal_class_name_suffixes(class_name: str) -> tuple[str, ...]:
53135313
return tuple(("".join(tokens[index:]) for index in range(len(tokens) - 1)))
53145314

53155315

5316+
_REGISTERED_NOMINAL_AUTHORITY_ASSIGNMENTS = frozenset(
5317+
{
5318+
"__registry_key__",
5319+
"__enum_member_attr__",
5320+
"__enum_label_attr__",
5321+
}
5322+
)
5323+
5324+
5325+
def _module_class_nodes(module: ParsedModule) -> dict[str, ast.ClassDef]:
5326+
return {
5327+
node.name: node
5328+
for node in _walk_nodes(module.module)
5329+
if isinstance(node, ast.ClassDef)
5330+
}
5331+
5332+
5333+
def _class_family_nodes_in_module(
5334+
class_nodes: Mapping[str, ast.ClassDef],
5335+
node: ast.ClassDef,
5336+
) -> tuple[ast.ClassDef, ...]:
5337+
"""Return the class and same-module ancestors known to static analysis."""
5338+
family_nodes: list[ast.ClassDef] = []
5339+
seen: set[str] = set()
5340+
stack = [node]
5341+
while stack:
5342+
current = stack.pop()
5343+
if current.name in seen:
5344+
continue
5345+
seen.add(current.name)
5346+
family_nodes.append(current)
5347+
stack.extend(
5348+
(
5349+
class_nodes[base_name]
5350+
for base_name in CLASS_NODE_AUTHORITY.declared_base_names(current)
5351+
if base_name in class_nodes and base_name not in seen
5352+
)
5353+
)
5354+
return tuple(family_nodes)
5355+
5356+
5357+
def _is_registered_nominal_authority_node(node: ast.ClassDef) -> bool:
5358+
assignments = CLASS_NODE_AUTHORITY.direct_assignments(node)
5359+
return (
5360+
HELPER_SUPPORT_PROJECTION_AUTHORITY.declares_autoregister_meta(node)
5361+
or bool(_REGISTERED_NOMINAL_AUTHORITY_ASSIGNMENTS & set(assignments))
5362+
)
5363+
5364+
5365+
def _has_registered_nominal_authority_ancestor(
5366+
class_nodes: Mapping[str, ast.ClassDef],
5367+
node: ast.ClassDef,
5368+
) -> bool:
5369+
"""Return True when classvar leaves are backed by a real registry family."""
5370+
return any(
5371+
(
5372+
family_node is not node
5373+
and _is_registered_nominal_authority_node(family_node)
5374+
)
5375+
for family_node in _class_family_nodes_in_module(class_nodes, node)
5376+
)
5377+
5378+
53165379
def _metadata_only_class_family_candidates(
53175380
module: ParsedModule,
53185381
) -> tuple[MetadataOnlyClassFamilyCandidate, ...]:
5382+
class_nodes = _module_class_nodes(module)
53195383
grouped: dict[
53205384
str,
53215385
list[tuple[ast.ClassDef, tuple[str, ...], tuple[str, ...], int]],
53225386
] = defaultdict(list)
53235387
for node in _walk_nodes(module.module):
53245388
if not isinstance(node, ast.ClassDef) or node.decorator_list:
53255389
continue
5390+
if _has_registered_nominal_authority_ancestor(class_nodes, node):
5391+
continue
53265392
assigned_names = (
53275393
HELPER_SYNTAX_PROJECTION_AUTHORITY.metadata_only_class_assignment_names(
53285394
node
@@ -5602,11 +5668,14 @@ def _classvar_only_sibling_leaf_candidates_for_class(
56025668
def _classvar_only_sibling_leaf_candidates(
56035669
module: ParsedModule,
56045670
) -> tuple[DeclarativeFamilyLeafCandidate, ...]:
5671+
class_nodes = _module_class_nodes(module)
56055672
return CANDIDATE_COLLECTION_AUTHORITY.ast_node_candidates(
56065673
module,
56075674
module.module,
56085675
ast.ClassDef,
5609-
_classvar_only_sibling_leaf_candidates_for_class,
5676+
lambda parsed_module, node: ()
5677+
if _has_registered_nominal_authority_ancestor(class_nodes, node)
5678+
else _classvar_only_sibling_leaf_candidates_for_class(parsed_module, node),
56105679
)
56115680

56125681

tests/test_refactor_advisor.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7804,6 +7804,20 @@ def test_detects_classvar_only_sibling_leaf(tmp_path: Path) -> None:
78047804
assert "declarative family-definition table" in (finding.codemod_patch or "")
78057805

78067806

7807+
def test_ignores_registered_classvar_only_strategy_leaves(
7808+
tmp_path: Path,
7809+
) -> None:
7810+
_write_module(
7811+
tmp_path,
7812+
"pkg/mod.py",
7813+
'\nfrom abc import ABC, abstractmethod\nfrom enum import Enum\nfrom typing import ClassVar\n\nfrom metaclass_registry import AutoRegisterMeta\n\n\nclass Scheme(Enum):\n RGB = "RGB"\n CMYK = "CMYK"\n STACK = "Stack"\n\n\nclass EnumKeyedStrategyMixin:\n pass\n\n\nclass SchemeBindingStrategy(EnumKeyedStrategyMixin, ABC, metaclass=AutoRegisterMeta):\n __registry_key__ = "scheme_literal"\n __skip_if_no_key__ = True\n scheme_literal: ClassVar[str | None] = None\n __enum_member_attr__ = "scheme"\n __enum_label_attr__ = "scheme_literal"\n\n @abstractmethod\n def bind(self, module):\n raise NotImplementedError\n\n\nclass IndexedSchemeBindingStrategy(SchemeBindingStrategy):\n image_settings: ClassVar[tuple[str, ...]] = ()\n weight_settings: ClassVar[tuple[str, ...]] = ()\n\n def bind(self, module):\n return tuple(type(self).image_settings), tuple(type(self).weight_settings)\n\n\nclass RgbBindingStrategy(IndexedSchemeBindingStrategy):\n scheme = Scheme.RGB\n image_settings = ("red", "green", "blue")\n weight_settings = ("red_weight", "green_weight", "blue_weight")\n\n\nclass CmykBindingStrategy(IndexedSchemeBindingStrategy):\n scheme = Scheme.CMYK\n image_settings = ("cyan", "magenta", "yellow", "gray")\n weight_settings = ("cyan_weight", "magenta_weight", "yellow_weight", "gray_weight")\n\n\nclass StackBindingStrategy(SchemeBindingStrategy):\n scheme = Scheme.STACK\n',
7814+
)
7815+
findings = analyze_path(tmp_path)
7816+
detector_ids = {finding.detector_id for finding in findings}
7817+
assert "metadata_only_class_family" not in detector_ids
7818+
assert "classvar_only_sibling_leaf" not in detector_ids
7819+
7820+
78077821
def test_detects_metadata_only_class_family_with_varying_bases(
78087822
tmp_path: Path,
78097823
) -> None:

0 commit comments

Comments
 (0)