nameparser 2.0 implementation - #288
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #288 +/- ##
==========================================
+ Coverage 97.67% 98.19% +0.51%
==========================================
Files 13 37 +24
Lines 947 2434 +1487
==========================================
+ Hits 925 2390 +1465
- Misses 22 44 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
derek73
added a commit
that referenced
this pull request
Jul 12, 2026
Every matrix job was silently testing CPython 3.11: uv sync honors .python-version (pinned 3.11 by the floor bump) over setup-python's interpreter, so 'build (3.14)' ran the suite on 3.11.15 (verified in the PR #288 run logs). UV_PYTHON makes the matrix real. With that fixed, add 3.15 (final 2026-10-01) as a non-blocking pre-release job so interpreter breakage surfaces while 2.0 is being built; flip it to blocking when 3.15 goes final. Locally green today on 3.15.0b3: pytest, mypy, ruff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the emoji/bidi character classes verbatim from config/regexes.py per the layering contract's duplication-by-design note; whitespace, emoji, bidi marks, and comma chars all act as separators without ever entering a token, so token spans always index original exactly. Wires tokenize into STAGES and registers the new module in test_layering's _MUST_EXIST. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds _vocab.py (is_initial/is_suffix_strict/is_suffix_lenient, shared across stages) and _segment.py (the family-comma/suffix-comma/no-comma decision). segment reads Lexicon suffix vocabulary directly rather than waiting for classify (recorded plan deviation #3: the suffix-comma vs family-comma call is definitionally vocabulary-dependent, mirroring v1's are_suffixes_after_comma). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports v1's join_on_conjunctions, prefix chains, and _join_bound_first_name as pure index operations over WorkToken runs (the anti-#100 invariant: no value-based lookup, no string joins). Adds two v2 additions from the plan: the "Ph. D."-split merge (plan deviation #1 -- lands in group since assign needs the merged piece to exist first) and the maiden-marker consuming rule (#274). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the seven-stage STAGES tuple. Patronymic regexes verified byte-for-byte against nameparser/config/regexes.py's east_slavic/turkic entries -- no mismatch found. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Not a pipeline stage but the runner's tail: converts the seven-stage fold's ParseState into a public ParsedName, dropping structural marker tokens (e.g. maiden markers) and materializing PendingAmbiguity indices into real Ambiguity objects over the final token tuple. Also adds _pipeline/_assemble.py to test_layering's _MUST_EXIST now that the file exists. 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/cases.py (Case dataclass + CASES tuple) and tests/v2/test_cases.py (the core runner), seeded with the v1 parity battery pinned in the plan header plus three carried-forward rows verified live against v1.4.0: - suffix_stays_suffix / suffix_stays_suffix_title: v1 routes a lone trailing recognized suffix (PhD) to the family/last field; v2 keeps it in suffix. Classified fix(suffix-routing). - family_comma_lone_title: v1 puts the pre-comma piece in first when it's followed only by a title; v2 treats pre-comma as definitionally family. Classified fix(comma-family). All 35 case rows pass against the pipeline as built through Task 11 with no corrections needed to the plan's predicted values; the East Slavic/Turkic policy rows were cross-checked against v1's equivalent patronymic_name_order opt-in and confirmed as true parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ParseState ownership map said roles belong to assign/post_rules exclusively, but group writes maiden roles -- an ownership contract wrong in one place teaches readers to distrust it everywhere. Fixed the map, noted post-group segments staleness and the sole-producer text invariant on WorkToken, documented PendingAmbiguity's dangling- index tolerance and Parser's None-default resolution, made assemble's role fallback say what it means, and pinned the whole ownership map mechanically: a new test folds the case corpus stage by stage and asserts each stage changes only the fields it owns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In the real data suffix_acronyms_ambiguous is a SUBSET of
suffix_acronyms (v1 shape), so the plain membership test in classify
and is_suffix_strict fired before the period gate could withhold
vocab:suffix -- the gate was dead code for the overlap members
({'ed', 'jd'}), and 'Smith, Ed' silently dropped the given name into
suffix. Both sites now exclude the ambiguous subset, Lexicon enforces
the subset invariant like particles_ambiguous, the classify/vocab
fixtures mirror the real subset shape instead of hiding the bug with
disjoint sets, and three v1-verified case rows pin the parity.
Found by cross-session PR review (C1/I1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The group-stage merge reunited the two tokens as a piece, but the suffix view joins SUFFIX tokens with ', ' -- 'John Ph. D.' rendered as suffix 'Ph., D.' -- and a mid-name credential never reached the trailing suffix peel, landing in MIDDLE. Continuation tokens of a suffix-merged piece now carry a new stable 'joined' tag (spec's provenance-tag sketch anticipated it) which the suffix view uses to attach with a space; assign peels group-flagged suffix pieces at ANY position, matching v1's fix_phd which extracted the credential before parsing. Contract-test trigger added for the new stable tag; two v1-verified case rows pin the behavior. Found by cross-session PR review (I2/I3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1's handle_non_first_name_prefix was unported: 'de la Vega' produced
given='de' with no ambiguity ('de' is not particles_ambiguous, so
PARTICLE_OR_GIVEN never fires -- a silent misassignment). Port it as
post_rules rule 1b keyed on the parsed given exactly like v1: a lone
given token tagged particle-but-not-ambiguous with middles or family
present folds into FAMILY; 'Jean de Mesnil' (non-leading) and bare
'de' (degenerate guard) stay untouched, and ambiguous 'van Gogh'
keeps the given reading. v1-verified case row added. Found by
cross-session PR review (I4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
middle_as_family was validated but consumed by no stage -- ported v1's handle_middle_name_as_last as post_rules rule 4 (runs after the patronymic rotations, comma or not, matching v1's post_process order; span order reproduces v1's prepend). lenient_comma_suffixes now gates segment's post-comma predicate (strict mode vetoes initial-shaped suffix words). extra_suffix_delimiters cannot be honored until token re-splitting lands with the migration work, so setting it warns instead of silently doing nothing. Found by cross-session PR review (I5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR review I6/I7/I8/I9: - assemble omits an ambiguity whose referent tokens were ALL dropped (born-empty ambiguities like UNBALANCED_DELIMITER are kept -- they are token-independent by design) - Policy.name_order now accepts only GIVEN_FIRST / FAMILY_FIRST / FAMILY_FIRST_GIVEN_LAST: the unnamed permutations had no implemented assignment semantics and would silently misassign. Non-Role elements get the taxonomy's TypeError. - Span.__add__ comment no longer cites a nonexistent join stage; the real rationale is that no covering-span operation exists (anti-#100) - the stage ownership test now also pins token-level ownership: texts and spans fixed at tokenize, classify changes only tags, the role-assigning stages only roles (group also tags, for 'joined') - docstring polish: ParseState names the three ambiguity-recording stages; _types names the matches() call-time import; _assign's PARTICLE_OR_GIVEN wording is order-neutral Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- T1: three-piece FAMILY_FIRST_GIVEN_LAST pins given-from-the-END
semantics (not a rotation of FAMILY_FIRST)
- T2: reverse-coverage property -- every input char lies in a token
span, a masked delimited span, or is individually ignorable; no
character silently vanishes
- T3: case row for post-comma non-suffix extras ('Smith, John, Extra,
Jr.' -> suffix 'Extra, Jr.' + COMMA_STRUCTURE; v1 parity pinned live)
- T4: multiple unbalanced delimiters are each reported; the scan does
not stop at the first unmatched opener
- T6: digits join the Hypothesis stress alphabet
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/simplify (all four reviewers converged on the first): the trickiest membership expression in the vocabulary layer -- unambiguous acronym OR suffix word, ambiguous acronym only with periods -- was spelled in full in both classify and is_suffix_strict, aligned only by comments. Drift would make the comma-structure decision disagree with piece classification for the same token. Now _vocab.suffix_as_written is the single source; classify and the strict/lenient predicates compose it (and normalize each token once instead of twice on the lenient path). The three-comma set was likewise defined in tokenize and extract under different names with no sync note; it moves to _state.COMMA_CHARS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/simplify: the piece predicates were typed for group's internal mutable lists, so every cross-stage call paid a triple list/set/tuple conversion (including O(n) token-tuple copies inside assign's peel loops and group's maiden scan). They now take Sequence/Set and the call-site churn is gone. Also: the leading-title peel was spelled twice (assign's main path and the FAMILY_COMMA branch, the latter with a dead 'not given_done' guard) -- extracted as _peel_leading_titles; the trailing all-suffix segment loops collapse into one loop over the structure's tail; the five hand-rolled pieces/ptags parallel-slice merges in _group_segment become one merge() closure that keeps the arrays in lockstep; post_rules' 'others' list was only ever tested for truthiness (any()); the patronymic comma gate is a named rotations_apply boolean instead of falsifying the policy value; and the module docstring records the GIVEN_FIRST positional assumption the rules inherit from v1 (#270). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/simplify efficiency findings: with the default strip flags every non-space character paid two failing regex calls in _ignorable; both strip classes are entirely non-ASCII, so an isascii() fast path skips them for virtually all real input. Span is a NamedTuple and already sorts as a tuple, so the tuple(...) sort keys in tokenize/extract were pure allocation. The case runner's seven-field list now derives from Role, whose declaration order the type declares canonical -- a new Role member fails the runner instead of being silently skipped. 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>
Every Lexicon vocabulary field except given_name_titles is matched one
word at a time, so a multi-word entry can never match -- the library
itself shipped eight such dead entries for years (repaired in the
previous commit). Per the inert-not-harmful convention a raise is
forbidden (see the given_name_titles Gotcha in AGENTS.md); warn
instead, from _normset (all per-word vocab fields) and _normpairs
(capitalization_exceptions keys), through every entry path --
constructor, add(), union, unpickle re-validation, and the v1 shim's
snapshot.
Also fixes tests/v2/test_properties.py's shared _VOCAB hypothesis pool,
which drew a multi-word phrase ("grand duke") into every Lexicon field
even though its own comment says it's "legal only in given_name_titles"
-- the fuzzer applied it everywhere, so it now tripped the new warning
in every per-word field. Split it into a separate _TITLE_VOCAB used
only for given_name_titles.
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>
… sweep Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ribution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ntness 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>
Post-rc1 API polish: StrEnum Role, Parser conveniences, dead-vocabulary repair
rc2 is rc1 plus the post-rc1 API-polish bundle (PR #290): Role as a StrEnum, the Parser conveniences (matches/capitalized/revise and Policy.patched), the dead-vocabulary repair, the multi-word-entry warning, and the legacy-pickle guard. The release log's "Changed since 2.0.0rc1" section lists what an rc1 tester would notice. Verified: __version__ == "2.0.0rc2" (packaging confirms is_prerelease), the wheel/sdist carry it, and twine check passes on both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make unexpected warnings test failures
The override neutralized the parent suite's dual-run fixture by name; the parent was renamed to _isolate_constants when the dual-run was dropped for #255, so the override has matched nothing since -- a dead autouse no-op under a docstring claiming the parent's behavior was skipped. Deleted rather than reconnected: the docstring's premise (v2 code never reads shared CONSTANTS) stopped being true when the facade/shim tests arrived -- test_config_shim.py mutates the shared singleton, so the parent's snapshot/restore is wanted protection here. Suite counts identical before and after (2393 passed), confirming the fixture changed nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setattr(hn, 'given', ...) has always succeeded silently while changing nothing about the parse -- the facade spells those attributes first/last, so the assignment just creates a stray instance attribute and the name silently forks. Role becoming a StrEnum added setattr(hn, Role.GIVEN, ...) as a second spelling of the same trap. Warn on exactly these two names and still perform the assignment: ad-hoc attribute stashing is a legal v1 pattern, the other five 2.0 field names are real properties whose setters work, and any code that worked keeps working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The maiden-marker bullet listed 'born', which is not in the shipped
vocabulary ('Jane Smith born Jones' does not route to maiden); the two
#270 bullets each closed a 'half' without saying the halves add up;
the #256 bullet claimed both mapping managers list known keys when
only capitalization_exceptions does (the regexes proxy names just the
miss).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # README.rst
derek73
marked this pull request as ready for review
July 28, 2026 05:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Progress
Span,Role(aStrEnumsince rc2),Token,Ambiguity/AmbiguityKind,ParsedName;Lexicon,Policy/PolicyPatch(applied viaPolicy.patched()),Locale. Frozen, slotted, hashable, picklable dataclasses with eager fail-loud validation.render()/initials()/capitalized()/__str__)Parser/parse()/parser_for()/matches()+ shared case tableHumanNamefacade +CONSTANTSshim (existing test corpus as regression harness) — full v1 suite reconciled and passing against the facade; the differential harness (tools/differential/) verifies 1.4-on-PyPI vs the facade over 652 corpus names, 0 unexplained diffsru,tr_az) — shipped with the non-interference gate, Provide constants in non-Latin scripts (Cyrillic, Greek, Arabic, Hebrew) #269 default vocabulary, and CLI--localepip install --pre nameparser; feedback on nameparser 2.0: a new core API, with full compatibility through 2.x — direction and feedback #284Roleas aStrEnum,tokens_for()coercion,STABLE_TAGSexported,Policy.patched(),Parser.matches()/capitalized()/revise(), eight dead multi-word vocabulary entries repaired, aUserWarningfor newly-stored multi-word entries, legacy-pickle guard. The release log's "Changed since 2.0.0rc1" section lists the rc1-visible deltasgiven/familyassignment (the 2.0 spellings it never had)Notes on what's here so far
TypeError, well-typed bad value →ValueError, enum lookups stayValueErrorper stdlib precedent.tests/v2/test_layering.py), and the seven-field canonical order derives fromRoledeclaration order everywhere — no ordering is ever stated twice.nameparser/configdata modules through 2.x, including the newmaiden_markers.py(Recognize maiden-name markers like "née" and "geb." (Jane Smith née Jones) #274 — marker words like "née"/"geb."; decisions recorded on the issue).filterwarnings = ["error"].🤖 Generated with Claude Code