Skip to content

Commit f1e2cca

Browse files
derek73claude
andcommitted
Pin the four hand-copied patterns the sync roster never listed
The M11 orphan sweep turned up not dead code but an unenforced promise. _render's _SPACES, _SPACE_BEFORE_COMMA, _MAC and _WORD are hand-copies of nameparser.config.regexes keys, and test_regex_sync.py -- whose docstring says "nothing previously enforced that promise" -- covered eleven such copies and not these four. They had not diverged; nothing would have reported it if they had. They went unnoticed because the config keys lost their only other reader when M11 deleted v1's parser.py (space_before_comma was parser.py:305), so the config side looks unused and the render side looks self-contained. Same shape as regexes.no_vowels (#268) and util.py's orphans, except here the pattern is still needed -- it just has two definitions and no link between them. Declared as a roster with a completeness check rather than one test per copy: the roster grew one test at a time, which is exactly how four copies were left out. A compiled pattern in those modules that is not declared -- with its config key, or None for the ones pinned by relationship (the _INITIAL pair) or sourced elsewhere (_COMMA_CHAR mirrors _state.COMMA_CHARS) -- now fails. Verified both directions: an undeclared new pattern fails the completeness check, and editing _SPACE_BEFORE_COMMA fails its sync assertion. Also pins _COMMA_CHAR against COMMA_CHARS, which had no test at all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f65ed13 commit f1e2cca

1 file changed

Lines changed: 78 additions & 0 deletions

File tree

tests/v2/test_regex_sync.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
CI signal. Tests may legally import both sides (test_layering.py's own
1010
convention), so this module is where the promise gets checked.
1111
"""
12+
import re
13+
14+
import pytest
15+
1216
from nameparser.config import regexes as _config
1317
from nameparser._pipeline import _assign, _post_rules, _tokenize, _vocab
1418
from nameparser import _render
@@ -71,3 +75,77 @@ def test_initial_copies_agree_with_each_other_and_config() -> None:
7175
reconstructed = trimmed[:-1] + "?" + trimmed[-1:]
7276
assert source.pattern == reconstructed
7377
assert source.flags == _vocab._INITIAL.flags
78+
79+
80+
# The roster above grew one test at a time, and four hand-copies were
81+
# never added to it -- _render's _SPACES, _SPACE_BEFORE_COMMA, _MAC and
82+
# _WORD. They had not diverged, but nothing would have said so, which
83+
# is the exact promise this module exists to keep. All four mirror a
84+
# config key whose only other reader was v1's parser.py, deleted at the
85+
# M11 swap, so nothing else touches them either.
86+
#
87+
# Declared as a roster rather than one test per copy, with a
88+
# completeness check below: adding a pattern without declaring its
89+
# source now fails here instead of being silently unpinned.
90+
_SOURCES: dict[tuple[str, str], str | None] = {
91+
("_assign", "_PERIOD_ABBREV"): "period_abbreviation",
92+
("_assign", "_ROMAN"): "roman_numeral",
93+
("_post_rules", "_EAST_SLAVIC"): "east_slavic_patronymic",
94+
("_post_rules", "_EAST_SLAVIC_CYR"): "east_slavic_patronymic_cyrillic",
95+
("_post_rules", "_TURKIC"): "turkic_patronymic_marker",
96+
("_post_rules", "_TURKIC_CYR"): "turkic_patronymic_marker_cyrillic",
97+
("_render", "_SPACES"): "spaces",
98+
("_render", "_SPACE_BEFORE_COMMA"): "space_before_comma",
99+
("_render", "_MAC"): "mac",
100+
("_render", "_WORD"): "word",
101+
("_vocab", "_PERIOD_NOT_AT_END"): "period_not_at_end",
102+
# Deliberately NOT a straight copy -- pinned by the dedicated tests
103+
# above, which assert the documented RELATIONSHIP instead:
104+
("_render", "_INITIAL"): None, # config's pattern minus one "?"
105+
("_vocab", "_INITIAL"): None, # same
106+
("_tokenize", "_BIDI"): None, # re_bidi, not a REGEXES key
107+
# Mirrors _pipeline._state.COMMA_CHARS, not nameparser.config
108+
("_render", "_COMMA_CHAR"): None,
109+
}
110+
111+
_MODULES = {"_assign": _assign, "_post_rules": _post_rules,
112+
"_render": _render, "_tokenize": _tokenize, "_vocab": _vocab}
113+
114+
115+
@pytest.mark.parametrize(
116+
"where,key", [(w, k) for w, k in _SOURCES.items() if k is not None],
117+
ids=lambda v: v if isinstance(v, str) else f"{v[0]}.{v[1]}")
118+
def test_declared_copy_matches_its_config_source(
119+
where: tuple[str, str], key: str) -> None:
120+
copy = getattr(_MODULES[where[0]], where[1])
121+
source = _config.REGEXES[key]
122+
assert copy.pattern == source.pattern, f"{where[1]} vs REGEXES[{key!r}]"
123+
assert copy.flags == source.flags, f"{where[1]} vs REGEXES[{key!r}]"
124+
125+
126+
def test_every_hand_copied_pattern_is_declared() -> None:
127+
"""The roster must cover every compiled pattern in these modules.
128+
129+
Without this, the roster is just another list that a new constant
130+
can be left out of -- which is how the four above went unpinned.
131+
"""
132+
undeclared = [
133+
(name, attr)
134+
for name, mod in _MODULES.items()
135+
for attr, value in vars(mod).items()
136+
if attr.startswith("_") and not attr.startswith("__")
137+
and isinstance(value, re.Pattern)
138+
and (name, attr) not in _SOURCES
139+
]
140+
assert not undeclared, (
141+
f"compiled patterns missing from _SOURCES: {undeclared}. Add each "
142+
f"with its nameparser.config.regexes key, or None if it has no "
143+
f"config counterpart.")
144+
145+
146+
def test_comma_char_matches_the_pipeline_comma_set() -> None:
147+
# _render splits on the same comma characters segment does; the set
148+
# lives in _state, so this one is pinned against that, not config.
149+
from nameparser._pipeline._state import COMMA_CHARS
150+
151+
assert set(_render._COMMA_CHAR.pattern.strip("[]")) == set(COMMA_CHARS)

0 commit comments

Comments
 (0)