Skip to content

nameparser 2.0 implementation#288

Draft
derek73 wants to merge 176 commits into
masterfrom
v2/core-foundation
Draft

nameparser 2.0 implementation#288
derek73 wants to merge 176 commits into
masterfrom
v2/core-foundation

Conversation

@derek73

@derek73 derek73 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Implementation branch for the 2.0 design — see the RFC in #285 and the umbrella issue #284 for the design discussion and feedback questions. Implementation learnings that change the design will land as amendment commits on #285, per the plan described there.

⚠️ Merges only at 2.0.0. This branch raises the Python floor to >=3.11 (#257) and drops the typing_extensions conditional dependency (nameparser now has zero runtime dependencies). Master must remain able to cut 1.4.x patch releases for Python 3.10 users, so this PR stays a draft until the 2.0.0 release.

Progress

  • Core data & config modelSpan, Role, Token, Ambiguity/AmbiguityKind, ParsedName; Lexicon, Policy/PolicyPatch/apply_patch, Locale. Frozen, slotted, hashable, picklable dataclasses with eager fail-loud validation; ~110 dedicated tests under tests/v2/.
  • Rendering (render() / initials() / capitalized() / __str__)
  • Parse pipeline + Parser / parse() / parser_for() / matches() + shared case table
  • v1 HumanName facade + CONSTANTS shim (existing test corpus as regression harness) — full v1 suite (1,240+ tests) reconciled and passing against the facade; differential harness (tools/differential/) verifies 1.4-on-PyPI vs the facade over 486 corpus names with one classified diff
  • Locale packs (ru, tr_az) — shipped with the non-interference gate, Provide constants in non-Latin scripts (Cyrillic, Greek, Arabic, Hebrew) #269 default vocabulary, and CLI --locale

Notes on what's here so far

🤖 Generated with Claude Code

derek73 and others added 30 commits July 12, 2026 12:46
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v2 core design uses enum.StrEnum (3.11+). Full #257 (dropping
typing_extensions, CI matrix) remains tracked on the issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Validate that all replace() field values are str (user error if None)
- Append missing-role synthetics in canonical Role order, not kwargs order
- Unquote return type annotation (postponed annotations in effect)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Casefolded seven-component tuple in canonical Role order for dedup,
dict keys, and sorting. Semantic layer comparison; __eq__ remains strict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds tests/v2/test_layering.py to mechanically enforce the conventions
doc's import layering and the public v2 export surface. Appends the v2
core types (Span, Role, Token, Ambiguity, AmbiguityKind, ParsedName,
Lexicon, Policy, PolicyPatch, PatronymicRule, UNSET, GIVEN_FIRST,
FAMILY_FIRST, FAMILY_FIRST_GIVEN_LAST, Locale) to nameparser/__init__.py
alongside the existing v1 HumanName export.

Turns on component-flag mypy strictness for the four v2 modules and
check_untyped_defs for tests/v2, then fixes the resulting test-only
type mismatches by using the precise constructed types (Span(...),
frozenset({...}), tuple-of-pairs) where the literal type didn't matter
to the test, and adding narrow # type: ignore[arg-type] comments only
where a test deliberately exercises runtime coercion/validation of an
intentionally mismatched static type (e.g. Token span/Ambiguity kind
coercion tests, PatronymicRule string coercion, dict aliasing test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pickle.dumps(Lexicon.default()) raised TypeError because the default
slots-dataclass pickle path serializes the _cap_map MappingProxyType.
Ship every other slot and rebuild the proxy from the canonical
capitalization_exceptions tuple on load. Parser (Plan 3) is picklable
by construction per the core spec, and a Parser holds a Lexicon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
requires-python is >=3.11 on this branch, so the 3.10 job can no
longer install the package (uv sync either fails or silently
substitutes a managed 3.11). The rest of #257 (typing_extensions,
classifiers) stays tracked on the issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With requires-python at >=3.11, Self imports directly from typing and
the conditional dependency can never activate; ruff (UP035/UP036)
flags the dead version block once the floor is raised.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v1 suite is fully annotated (since #250 put tests/ under mypy), so
pyproject carries no per-file ANN ignores; the new tests/v2 modules
must be annotated too or 'ruff check' fails in CI. Also drops an
unused import ruff flagged (F401).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
derek73 and others added 9 commits July 17, 2026 20:40
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds two composition tests over the real RU and TR_AZ packs (the
synthetic composition tests in test_parser.py stay as-is): the union
of both packs' patronymic rules parses one name from each pack's case
segment correctly, and a custom-lexicon base survives the parser_for
fold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spec §5.2: each pack (and both together) must parse the full default
corpus -- the shared case table's untargeted rows plus the 486-name
differential corpus at tools/differential/corpus.jsonl -- identically
to the default parser, or the deviation must be declared via the
pack's DEVIATES predicate. Ran clean: no undeclared deviations across
RU (1 declared), TR_AZ (1 declared), or the combined pack (2 declared)
-- the be3f25e predicate hardening already covers this corpus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prefix reasoning per new NON_FIRST_NAME_PREFIXES/PREFIXES entry
(nameparser/config/prefixes.py):

- بن ("bin"/"ibn", son of): added to NON_FIRST_NAME_PREFIXES. Latin
  'bin' stays ambiguous only to protect the unrelated Chinese given
  name "Bin"; that collision risk doesn't exist in Arabic script, and
  بن is never a bare given name on its own.
- بنت ("bint", daughter of): NON_FIRST_NAME_PREFIXES, mirroring the
  existing Latin 'bint' entry.
- ابن ("ibn", son of, alternate spelling): NON_FIRST_NAME_PREFIXES,
  mirroring the existing Latin 'ibn' entry.
- أبو / ابو ("abu", father of): left in plain PREFIXES (ambiguous),
  mirroring the existing Latin 'abu' entry -- "Abu Bakr" reads "Abu"
  as a given name.
- آل ("aal", family/clan of, e.g. "Al Saud"): NON_FIRST_NAME_PREFIXES.
  Distinct from the excluded definite article "ال" (#269 explicitly
  excludes standalone "ال"); a clan prefix, never a bare given name.
- בן ("ben", son of) / בת ("bat", daughter of): NON_FIRST_NAME_PREFIXES,
  same reasoning as بن -- no Latin collision risk, never a bare given
  name in Hebrew usage.

Verification notes:

- Every parametrized test row was verified live against a
  runtime-augmented Lexicon.default() before the data landed. Two
  rows came out differently than the task's first guess: a
  single-alphabetic-character conjunction (Cyrillic "и"/"і") hits v1's
  issue #11 initial carve-out and does not join a short 3-piece title
  chain, so the "и" conjunction row uses a longer, 5-piece chain
  instead; "الشيخ" was added to FIRST_NAME_TITLES (like its
  transliterated cousin "sheikh") so a lone following name reads as
  given, not family.
- Geresh/gershayim gate (#269 step 2): probed 'ד"ר'/"גב'" (ASCII
  quotes) against extract_delimited live. Both survive untouched --
  the quote sits mid-word and never satisfies _open_ok/_close_ok's
  boundary test -- so both the ASCII-quote and the typographic
  Unicode spellings ('ד״ר' U+05F4, 'גב׳' U+05F3) ship.
- Cyrillic мл/ст (junior/senior) deliberately NOT shipped, per the
  issue's own collision caveat; deferral comment left in
  nameparser/config/suffixes.py.
- Differential harness: adding native-script بن as a prefix changes
  one existing non-Latin corpus row ('‏محمد بن سلمان‏', RTL-marked) from
  v1's plain-middle-name reading to 2.0's prefix-chained family --
  exactly the new-recognition #269 exists to add, not a Latin-corpus
  regression. Classified in tools/differential/expected_changes.toml
  rather than reverting the entry; harness exits 0 with 2 classified
  diffs (the pre-existing comma-family diff plus this one), 0
  unexplained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up to 6028ab4:

- Bare 'κ' removed from TITLES: _normalize strips the edge period, so
  the abbreviated initial 'Κ.' matched the entry and 'Κ. Παπαδόπουλος'
  parsed as title='Κ.' with an EMPTY given -- degrading the very common
  initial+surname shape. The single-letter #11 carve-out guards
  conjunctions only, and Latin TITLES deliberately has no bare
  single-letter entries for the same reason. Deferral note left next
  to the Greek entry group; regression test pins the correct
  initial+surname reading; the bare-κ test row is replaced with
  κος/κα rows (verified live before pinning).
- expected_changes.toml: the #269 rule's name_regex tightened from
  "بن" to "\bبن\b" -- the bare form matches بن as a substring anywhere
  (including inside لبنان, 'Lebanon') and would silently absorb
  unrelated middle/last diffs. Verified live: re.search matches
  '‏محمد بن سلمان‏' and NOT 'لبنان'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four customize.rst doctests pinned the titles set's LAST element
('zoologist') through the ELLIPSIS form -- #269's non-Latin entries
sort after it and broke all four. The ellipsis now swallows to the
closing brace, so vocabulary additions cannot break the docs again
(the constant-content rule, applied to doctests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
derek73 and others added 4 commits July 17, 2026 21:57
casefold()'s caseless-matching folds mutated stored vocabulary: the
new Greek title 'κος' arrived in the lexicon as the misspelling 'κοσ'
(final sigma flattened) and v1's 'großfürst' as 'grossfürst' -- while
the v1 CONSTANTS path (lc() = lower()) kept both as authored, so the
two surfaces disagreed about the vocabulary's spelling. lower() follows
Unicode SpecialCasing contextually ('ΚΟΣ' -> 'κος'), matches v1's lc()
exactly, and _normalize is the single fold for storage and match-time
lookups, so matching stays symmetric. Pin the preserved spellings and
the default-lexicon fidelity of both entries. (Review finding,
2026-07-17.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared corpus exercised TR_AZ with a single name, leaving that
gate nearly vacuous. Add synthetic rotator names -- one per alternation
branch of each pack's marker regexes, both scripts -- prove each
deviates AND declares individually, and floor every gate's declared
count at its rotator count. Also: TR_AZ per-token DEVIATES regression
(mirror of the RU one), whole-token negative ('Ogluev'), rows for the
untested #269 entries (أبو/ابو kunya ambiguity split, آل leading and
chained, ابن, Ukrainian і), pack-policy-survives-custom-base and
parser_for-chaining composition tests, a facade-path #269 smoke test,
and monkeypatch-based module-cache handling in the laziness test.
(Review findings, 2026-07-17.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Truthiness swallowed an empty code (empty shell variable, typo) into
the default-parser branch; 'is not None' routes it to locales.get(),
which exits 2 listing the available codes like any unknown code.
(Review finding, 2026-07-17.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the invented 'protect the Chinese given name Bin' rationale from
prefixes.py -- no source in the repo history attests it; state only the
observable fact that Latin 'bin' stays outside NON_FIRST_NAME_PREFIXES.
Soften the мл/ст deferral comment to what #269 actually says (both need
within-script collision vetting; the 'ст' specificity was ours, not the
issue's). Cite issue #185 and its test bank in tr_az.py for parity with
ru.py's data-sources citation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
derek73 and others added 15 commits July 18, 2026 00:38
Drop ~13 function-scope re-imports that restated module-scope names
(hoisting Policy to the top); share one default-parser baseline
(functools.cache) and module-level packed parsers across the gate and
rotator tests, so the suite scales corpus + packs instead of corpus x
packs; factor the per-pack layering ALLOWED tuples into a shared
_LOCALE_PACK_ALLOWED constant mirroring _PIPELINE_STAGE_ALLOWED; and
replace the rotator lists' per-row branch comments as the only coverage
guarantee with a mechanical test that every alternation branch of every
marker regex is exercised by some rotator token. Document why the
corpus loader stays a local copy of compare.py's convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The non-interference gate, rotator lists, and branch-coverage test now
iterate the registered packs (_REGISTRY -> _PACKS/_PACKED/_ROTATORS
dicts) instead of hand-wiring each pack by name: the per-pack gate
tests collapse into one parametrized test, the combined test derives
its predicate by OR-ing every registered DEVIATES, and marker regexes
are discovered as the pack module's re.Pattern attributes (anchoring
read from the pattern's own shape). A contract meta-test requires every
registered pack to declare DEVIATES and ship a rotator list, so pack #3
fails structurally until it does -- accepted option C of the 2026-07-18
DEVIATES design note, keeping Locale pure data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1's patronymic_name_order was a bool flag, so True is the likeliest
wrong value a migrator passes to the 2.0 field; it failed loudly but
with the raw "'bool' object is not iterable". Probe with iter() --
NOT a try/except around tuple()/frozenset(), which would also rewrite
exceptions raised inside a caller's generator (pinned property) -- and
raise a message naming the v1 flag and both working replacements.
Same guard on PolicyPatch's union fields, with the v1 hint only where
it applies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1.4's comparison_key()/matches() lowercased; 2.0 casefolds (both the
new API and the facade). The deviation is strictly more permissive --
ß/SS and final-sigma case pairs now compare equal -- and comparison is
the one surface where casefold's aggressive folds are wanted: the key
is opaque, so the vocabulary spelling-mutation casefold caused in
storage (fixed to lower() in 3e6e08d) cannot happen here. Release-log
behavior-changes entry + a pin test on both API paths, so the next
casefold audit finds the intent recorded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The facade (and v1.4 before it) already indent the multi-line repr
body with 4 spaces; ParsedName was the lone tab hold-out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shim translates patronymic_name_order=True to {EAST_SLAVIC, TURKIC}
(v1's single bool governed both detectors); the True-guard's hint said
EAST_SLAVIC only, which would migrate users to narrower behavior than
the compat layer gives them. Caught by the migrate-page table audit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
patronymic_name_order=True maps to BOTH patronymic rules (what the
shim actually does); the maiden field landed in 1.3, not 1.4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants