Skip to content

nameparser 2.0 implementation - #288

Merged
derek73 merged 319 commits into
masterfrom
v2/core-foundation
Jul 28, 2026
Merged

nameparser 2.0 implementation#288
derek73 merged 319 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 (a StrEnum since rc2), Token, Ambiguity/AmbiguityKind, ParsedName; Lexicon, Policy/PolicyPatch (applied via Policy.patched()), Locale. Frozen, slotted, hashable, picklable dataclasses with eager fail-loud validation.
  • 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 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 diffs
  • 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
  • Documentation rewrite — new-API-first docs (getting started / concepts / customize / locales / migrate + regrouped API reference) and a README that doubles as the PyPI page; every doc code block runs as a Sphinx doctest in CI
  • v2.0.0rc1 released to PyPI (2026-07-23) — pip install --pre nameparser; feedback on nameparser 2.0: a new core API, with full compatibility through 2.x — direction and feedback #284
  • Post-rc1 API polish (Post-rc1 API polish: StrEnum Role, Parser conveniences, dead-vocabulary repair #290) — Role as a StrEnum, tokens_for() coercion, STABLE_TAGS exported, Policy.patched(), Parser.matches()/capitalized()/revise(), eight dead multi-word vocabulary entries repaired, a UserWarning for newly-stored multi-word entries, legacy-pickle guard. The release log's "Changed since 2.0.0rc1" section lists the rc1-visible deltas
  • v2.0.0rc2 released to PyPI (2026-07-26)
  • Post-rc2 hardening — unexpected warnings are now test failures (Make unexpected warnings test failures #292); facade warns on given/family assignment (the 2.0 spellings it never had)

Notes on what's here so far

🤖 Generated with Claude Code

@derek73 derek73 self-assigned this Jul 12, 2026
@derek73 derek73 added this to the v2.0 milestone Jul 12, 2026
@codecov-commenter

codecov-commenter commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.20759% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.19%. Comparing base (7c4f9ac) to head (ac59a5f).
⚠️ Report is 320 commits behind head on master.

Files with missing lines Patch % Lines
nameparser/__main__.py 0.00% 28 Missing ⚠️
nameparser/_config_shim.py 97.89% 9 Missing ⚠️
nameparser/_facade.py 99.43% 2 Missing ⚠️
nameparser/_lexicon.py 98.92% 2 Missing ⚠️
nameparser/_parser.py 98.82% 1 Missing ⚠️
nameparser/_pipeline/_tokenize.py 98.24% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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>
derek73 and others added 12 commits July 12, 2026 21:35
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>
Comment thread nameparser/_pipeline/_tokenize.py Fixed
derek73 and others added 11 commits July 13, 2026 02:28
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>
derek73 and others added 20 commits July 26, 2026 02:43
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>
derek73 and others added 6 commits July 26, 2026 14:03
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
@derek73
derek73 marked this pull request as ready for review July 28, 2026 05:44
@derek73
derek73 merged commit 6dc5261 into master Jul 28, 2026
7 checks passed
@derek73
derek73 deleted the v2/core-foundation branch July 28, 2026 05:50
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