Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
3c35b85
Add v2 Span, Role, and Token with construction validation
derek73 Jul 12, 2026
2f70ac7
Raise the Python floor to 3.11 on the 2.0 branch (#257, partial)
derek73 Jul 12, 2026
0a416ed
Fail loud on malformed Token spans and non-string text
derek73 Jul 12, 2026
3f3d448
Add v2 AmbiguityKind StrEnum and Ambiguity type
derek73 Jul 12, 2026
de78ebf
Hoist v2 test imports to the top-of-file block
derek73 Jul 12, 2026
072e2da
Validate Ambiguity detail and token element types
derek73 Jul 12, 2026
ef6d2b8
Add v2 ParsedName with constructor-enforced span invariants
derek73 Jul 12, 2026
4a79080
Validate ParsedName element types and original
derek73 Jul 12, 2026
8e8160b
Add v2 ParsedName string properties, derived views, and as_dict
derek73 Jul 12, 2026
3190abd
Add v2 ParsedName.replace() with positional synthetic tokens
derek73 Jul 12, 2026
06a7e22
Fail loud on non-str replace values; canonical append order
derek73 Jul 12, 2026
aeefe51
Add v2 ParsedName.comparison_key()
derek73 Jul 12, 2026
9d781c7
Add v2 Lexicon with canonical storage and v1-sourced default
derek73 Jul 12, 2026
61362bd
Fail loud on malformed Lexicon input; dedupe exception keys
derek73 Jul 12, 2026
f216e5f
Add v2 Lexicon composition: add/remove and field-wise union
derek73 Jul 12, 2026
0be9e53
Add v2 Policy with order-spec validation and PatronymicRule
derek73 Jul 12, 2026
21c817b
Reject malformed Policy delimiter pairs and patronymic rules
derek73 Jul 12, 2026
719fee6
Coerce and validate all Policy collection fields
derek73 Jul 12, 2026
0a47671
Add v2 PolicyPatch with UNSET sentinel and declared composition
derek73 Jul 12, 2026
cd46673
Document the replace type-ignore; rename parity test
derek73 Jul 12, 2026
29f603f
Canonicalize PolicyPatch union fields; lock field types in parity test
derek73 Jul 12, 2026
2d3dd04
Add v2 Locale type
derek73 Jul 12, 2026
91845fd
Reject whitespace in Locale codes
derek73 Jul 12, 2026
b88d3ac
Drop exception keys that normalize to empty
derek73 Jul 12, 2026
0838e98
Add bounded deviation-only reprs to all v2 types
derek73 Jul 12, 2026
af6a01a
Export v2 core types; enforce layering and mypy strictness
derek73 Jul 12, 2026
30ef763
Make Lexicon picklable despite its mappingproxy slot
derek73 Jul 12, 2026
401535c
Drop Python 3.10 from the CI matrix (#257, partial)
derek73 Jul 12, 2026
f2b42c8
Drop the pre-3.11 typing_extensions shim (#257)
derek73 Jul 12, 2026
4c171fc
Annotate v2 test returns for ruff ANN compliance
derek73 Jul 12, 2026
353d10a
Tighten the layering test to exact-module matching
derek73 Jul 12, 2026
09cc85d
Pin the derived views' particle tag to STABLE_TAGS
derek73 Jul 12, 2026
473c0b9
Reject mappings passed to plain Lexicon vocab fields
derek73 Jul 12, 2026
ce7b36b
Diff Lexicon reprs against the nearer named baseline
derek73 Jul 12, 2026
334c617
Order Lexicon sections per conventions doc §4
derek73 Jul 12, 2026
5a5bf80
Adopt the stdlib exception taxonomy across v2 validation
derek73 Jul 12, 2026
974aec2
Source maiden markers from a config data module (#274)
derek73 Jul 12, 2026
5d78933
Add Scandinavian participle maiden markers (#274)
derek73 Jul 12, 2026
a16364e
Distill 2.0 implementation conventions into AGENTS.md
derek73 Jul 12, 2026
339a3ed
Guard Token.tags and coerce Token.role
derek73 Jul 12, 2026
9d7493c
Close the fail-loud gaps in Policy.__post_init__
derek73 Jul 12, 2026
cff23ff
Canonicalize PolicyPatch's scalar name_order to a tuple
derek73 Jul 12, 2026
d4cb1b2
Validate capitalization_exceptions entry shapes
derek73 Jul 12, 2026
af3aad0
Fail at unpickle time on Lexicon field-layout skew
derek73 Jul 12, 2026
c5090d1
Fix vacuous removal assertion in the add/remove test
derek73 Jul 12, 2026
2310fb1
Pin four stated-but-untested v2 contracts
derek73 Jul 12, 2026
cbd8cdf
Fix stale and inaccurate comments found in review
derek73 Jul 12, 2026
358ce80
Raise on entries that normalize to empty
derek73 Jul 12, 2026
22fe2ea
Block Span concatenation and bool span coordinates
derek73 Jul 12, 2026
f07082b
Pin Locale.code to the registry-key charset [a-z0-9_]+
derek73 Jul 12, 2026
22ea503
Extract _coerce_enum for the duplicated enum coercion blocks
derek73 Jul 12, 2026
eaba92f
Extract _normpairs as _normset's sibling
derek73 Jul 12, 2026
cac4231
Drop the whitespace check the charset pin subsumed
derek73 Jul 12, 2026
a2d88ea
Test the real matrix interpreter, and add 3.15 pre-release (#259)
derek73 Jul 12, 2026
73e816e
Add _render with the #254-normative collapse; extend layering for it
derek73 Jul 12, 2026
a6750dc
Add render(): spec formatting over role fields and derived views
derek73 Jul 12, 2026
bbca365
Add ParsedName.render() and __str__ via call-time _render delegation
derek73 Jul 12, 2026
445571d
Add initials() with call-site delimiter/separator arguments
derek73 Jul 12, 2026
da94eb5
Add capitalized(): lexicon-driven case fixing, same spans
derek73 Jul 12, 2026
936d347
True up module docstrings to the rendering delegation
derek73 Jul 12, 2026
27616dc
Guard all frozen types against pickle layout skew
derek73 Jul 12, 2026
35e1836
Validate rendering arguments eagerly
derek73 Jul 13, 2026
7e8b6fb
Reject bare-string name_order with the taxonomy TypeError
derek73 Jul 13, 2026
4db87b7
Pin two documented rendering contracts
derek73 Jul 13, 2026
96b9166
Fix three stale doc claims from the PR review
derek73 Jul 13, 2026
79277de
Simplify the rendering module
derek73 Jul 13, 2026
60802f1
Single-source the bare-string name_order guard
derek73 Jul 13, 2026
bc56bd2
Enforce the pickle guards mechanically; flatten test imports
derek73 Jul 13, 2026
bf68147
Add the pipeline package skeleton: ParseState and layering wiring
derek73 Jul 13, 2026
24c5191
Add extract_delimited: delimiter pairs to nickname/maiden spans
derek73 Jul 13, 2026
7e341a9
Add tokenize: char-classification tokenizer with exact spans
derek73 Jul 13, 2026
29ceb18
Add segment: comma-structure decision over token index runs
derek73 Jul 13, 2026
a91e465
Add classify: vocabulary tags from the lexicon
derek73 Jul 13, 2026
20e6bf6
Add group: piece boundaries by index; joins ported from v1
derek73 Jul 13, 2026
5e22e78
Add assign: positional roles per name_order over pieces
derek73 Jul 13, 2026
4f3a707
Add post_rules: firstname swap and opt-in patronymic rotations
derek73 Jul 13, 2026
bb80c8d
Add assemble: final ParseState to validated ParsedName
derek73 Jul 13, 2026
9bcd007
Add Parser and parse(): the 2.0 pipeline goes end to end
derek73 Jul 13, 2026
8ad0247
Add parser_for() with locale-identified errors and matches()
derek73 Jul 13, 2026
c7e3fb0
Fix the shared case-table format and seed it with the pinned corpus
derek73 Jul 13, 2026
9347848
Add property, contract, and benchmark test layers
derek73 Jul 13, 2026
bacb42b
Apply type-design review fixes to the pipeline state contract
derek73 Jul 13, 2026
f65bd7c
Exclude the ambiguous subset from the plain suffix-acronym test
derek73 Jul 13, 2026
c66be14
Heal the split Ph. D. credential across the pipeline (I2/I3)
derek73 Jul 13, 2026
ec9e3a2
Fold a leading never-given particle into the family name (I4)
derek73 Jul 13, 2026
481f27a
Wire the three dead Policy fields (I5)
derek73 Jul 13, 2026
e7ccd67
Restrict name_order to the exported orders; drop dangled ambiguities
derek73 Jul 13, 2026
ddd0416
Add the review's test-coverage batch (T1-T4, T6)
derek73 Jul 13, 2026
d64cb6a
Single-source the suffix-as-written rule and the comma set
derek73 Jul 16, 2026
222b079
Factor the shared piece machinery in group and assign
derek73 Jul 16, 2026
2d5d8c2
Trim constant-factor waste on the tokenize hot path
derek73 Jul 16, 2026
12298df
Add the shim SetManager (migration spec §3)
derek73 Jul 16, 2026
95de007
Add shim TupleManager, delimiter managers, and the read-only regexes …
derek73 Jul 16, 2026
e2979f6
Add the shim Constants with generation tracking and pickle acceptance
derek73 Jul 16, 2026
8b22f1d
Resolve shim Constants to (Lexicon, Policy, defaults) snapshots with …
derek73 Jul 16, 2026
273e373
Wire Policy.extra_suffix_delimiters through segment and group (v1 suf…
derek73 Jul 17, 2026
3db907e
Add the HumanName facade skeleton: construction, config binding, dirt…
derek73 Jul 17, 2026
0719ae0
Add facade field properties, list snapshots, and derived views
derek73 Jul 17, 2026
ea49778
Add facade dunders: v1 str/repr/iteration, #258 item semantics, ident…
derek73 Jul 17, 2026
bd2abc7
Port v1 initials to the facade and validate render-default assignment
derek73 Jul 17, 2026
0cdd787
Add facade capitalize/matches/comparison_key and v1-compatible pickling
derek73 Jul 17, 2026
75e3219
Swap parser.py and config to the 2.0 facade and shim (v1 suite reconc…
derek73 Jul 17, 2026
2d3fb93
M12 batch 1: drop the dead empty_attribute_default dual-run fixture (…
derek73 Jul 17, 2026
a689e1d
M12 batch 1: delete test_config_attribute_docstrings.py (bucket A)
derek73 Jul 17, 2026
b959112
Preserve empty comma segments' structural position (v1 parity)
derek73 Jul 17, 2026
2e64c48
Close four v1-compat gaps: Constants kwargs, parse_full_name, C sette…
derek73 Jul 17, 2026
81f2109
Reconcile test_python_api against the facade (buckets A, B, E)
derek73 Jul 17, 2026
7bf53f3
Port v1's parts[1]-only suffix-comma decision and period-joined vocab…
derek73 Jul 17, 2026
7e81187
Restore v1.3/1.4 manager hardening in the shim (#221/#238/#241/#242/#…
derek73 Jul 17, 2026
7da89ee
Reconcile test_constants against the shim (buckets A, B, C)
derek73 Jul 17, 2026
c1b5730
Add the two v1.4 bool constructor kwargs to the shim Constants
derek73 Jul 17, 2026
eb4b362
Lift test_constants from the skip lists (bool kwargs landed)
derek73 Jul 17, 2026
be4e106
Reconcile test_output_format against the facade (buckets B, C)
derek73 Jul 17, 2026
0368081
Reconcile test_initials against the facade (buckets B, C, E)
derek73 Jul 17, 2026
91dad40
Hold test_capitalization on a confirmed _cap_word conjunction/initial…
derek73 Jul 17, 2026
e8dd59f
Hold test_first_name on a family-segment trailing-suffix pipeline bug
derek73 Jul 17, 2026
dac3f22
Reconcile test_titles; hold on two comma/nickname parity bugs (bucket D)
derek73 Jul 17, 2026
2c45150
Fix four v1-parity bugs from the M12 batch-2 queue
derek73 Jul 17, 2026
8ea1b36
Lift capitalization, first_name, and titles from the skip list
derek73 Jul 17, 2026
8784070
Hold test_suffixes on six suspected pipeline gaps (bucket D, reported)
derek73 Jul 17, 2026
35f6280
Route suffix-shaped delimited content past nickname extraction (v1 pa…
derek73 Jul 17, 2026
21795cc
Reconcile test_prefixes (bucket C); hold on the B1 trailing-suffix gap
derek73 Jul 17, 2026
20c2829
Hold test_conjunctions on the join-stage conjunction/initial bug
derek73 Jul 17, 2026
2c17454
Reconcile test_nicknames (buckets A, B); hold on a bucket-precedence …
derek73 Jul 17, 2026
7e044dd
Reconcile test_comma_variants against the facade (bucket B)
derek73 Jul 17, 2026
c131dde
Fix the M12 batch-3 bug queue: walk order, chunk rules, tail entries
derek73 Jul 17, 2026
95b148a
Lift suffixes, prefixes, conjunctions, nicknames from the skip list
derek73 Jul 17, 2026
bf06045
Reconcile patronymic + bound-given files; hold two on parity bugs
derek73 Jul 17, 2026
d8e3042
Delete test_parser_util.py (bucket A)
derek73 Jul 17, 2026
11d9e37
Reconcile test_variations against the facade (buckets A, B)
derek73 Jul 17, 2026
fb524d8
Reconcile test_brute_force (no edits needed); drop the mypy exclude
derek73 Jul 17, 2026
06bab72
Fix the last two M12 bugs and lift the reconciliation skip list
derek73 Jul 17, 2026
785c3e5
Add the facade runner over the shared case table
derek73 Jul 17, 2026
1748493
Add the v1-vs-2.0 differential harness with a classified allowlist
derek73 Jul 17, 2026
cd94143
Reimplement the CLI over the new API with --json
derek73 Jul 17, 2026
fa95cd1
Record the migration layer in AGENTS.md
derek73 Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,22 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
# 3.15 is pre-release until 2026-10-01 (#259): surface breakage
# without blocking the branch; flip to blocking when it goes final.
continue-on-error: ${{ matrix.python-version == '3.15' }}
env:
# Without this, uv sync honors .python-version (3.11) over the
# matrix interpreter and every job silently tests 3.11.
UV_PYTHON: ${{ matrix.python-version }}

steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.10
3.11
18 changes: 17 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ Parse flow:

Each named attribute (`title`, `first`, etc.) is a `@property` that joins its corresponding `_list`. Setters call `_set_list()` which runs the value through `parse_pieces()`, so assigning `hn.last = "de la Vega"` correctly re-parses prefix tokens.

## 2.0 API modules (`nameparser/_*.py`, `tests/v2/`)

The 2.0 rewrite lands as underscore-private modules alongside the v1 code. These conventions apply to all new-API code and are stricter than the v1 sections above. The full design record (rationale, settled-decision logs, dated amendments) lives in untracked `docs/superpowers/specs/`; this section is the enforceable subset. **A commit that establishes or amends one of these conventions must update this section in the same commit** — grep-driven staleness sweeps miss paraphrased prose (see Workflow above), so write-time maintenance is the mechanism, audits are the backstop.

- **Module layout**: every new module is underscore-private (`_types.py`, `_lexicon.py`, `_policy.py`, `_locale.py`, `_render.py`, `_pipeline/`, `_parser.py`, plus the facade layer: `_facade.py`, `_config_shim.py`). The public import surface is exactly `nameparser` (and later `nameparser.locales`); `nameparser/__init__.py` holds re-exports and `__all__` only — no logic. Since the M11 swap, the old paths are import-path-preserving re-exports: `nameparser.parser` re-exports the `_facade` `HumanName`, `nameparser.config` re-exports the `_config_shim` names (`Constants`, `CONSTANTS`, `SetManager`, `TupleManager`, `RegexTupleManager`); the `config/` DATA modules stay the vocabulary source through 2.x. The whole facade layer is deleted in 3.0.
- **Facade layer** (`_facade.py`, `_config_shim.py`): the v1-compat `HumanName`/`Constants` over the core. Key mechanisms: `Constants._generation` dirty-tracking (every mutation bumps; facades resolve their `Parser` lazily via `_cached_parser(lexicon, policy)`); `Constants._snapshot()` mirrors `_lexicon._default_lexicon()` (equality-pinned); the facade pickles v1-SHAPED state (component lists, one `__setstate__` path for 1.4 and 2.x blobs; components rebuild via `replace()`, never a re-parse); `_V1_HOOKS` overrides warn once per subclass (#280). The compat contract is the migration spec's promise: warning-free 1.4 code behaves identically except release-log-classified fixes — `tools/differential/` (dev-only, not shipped) verifies this against 1.4-on-PyPI over a 486-name corpus.
- **Layering is enforced by `tests/v2/test_layering.py`** (exact-module matching; `if TYPE_CHECKING:` imports don't count): `_types` imports nothing internal at module level — its rendering delegates import `_render` at call time; `_lexicon` and `_policy` sit above `_types` independently (`_lexicon` may import `nameparser.config.*` DATA modules only — vocabulary is single-sourced from the v1 data modules through 2.x, e.g. `config/maiden_markers.py`); `_locale` sits on `_lexicon`+`_policy` (plus `_types` for the shared pickle mixin); `_render` imports `_types` and `_lexicon` (for `Lexicon.default()` and `_normalize`); `_pipeline/*` imports `_types`+`_lexicon`+`_policy` plus in-package `_pipeline` helpers; `_parser` sits on everything except `_render`; the facade layer (`_facade`, `_config_shim`, `parser`, `config/__init__`, `__main__`) may import anything public plus `_render`. Extend the test's `ALLOWED` table when adding a module.
- **Canonical field order** — the seven roles in `Role` enum declaration order, defined once and derived everywhere (properties, `as_dict`, reprs, `comparison_key`). Never restate the order literally.
- **Method organization**, fixed section order in every class: fields + `__post_init__` validation → alternative constructors → dunders (construction/equality → protocol → operators) → properties → public methods by concern (access → editing → comparison → rendering delegates) → private helpers last, except a helper serving exactly one section may sit at that section's head.
- **Validation is eager and fail-loud**: every `raise` states the offending value, the expected form, and the fix. Exception taxonomy: wrong type — including wrong element type inside a collection, bare `str` where an iterable of strings is expected, or a `Mapping` where a plain iterable is expected — raises `TypeError`; well-typed but unacceptable values raise `ValueError`; failed enum lookups stay `ValueError` for any input (stdlib `EnumType` precedent).
- **Reprs are bounded**: render which fields deviate from a named baseline and by how much, never contents (`Lexicon(default + titles: +2)`).
- **Typing/docs**: `from __future__ import annotations`; `frozen=True, slots=True` on every public dataclass; strict-profile mypy flags via per-module overrides in pyproject (`strict = true` itself is not valid per-module). Docstrings state contracts in prose with **no doctest blocks** — `--doctest-modules` makes every example a test; behavior examples go to unit tests per the lean-docs rule.
- **Pickling**: v2 types must round-trip (`Parser` will be picklable by construction, and it holds a `Lexicon`). Every frozen type assigns `_guarded_getstate`/`_guarded_setstate` (`_types.py`) in its class body (`@dataclass(slots=True)` would override inherited pickle methods) — unpickling fails at the LOAD site on field-layout skew, and values are deliberately NOT re-validated (pickle is not a security boundary; canonical state comes from a validated instance). `Lexicon` keeps its own copy of the guard (layering) plus the `mappingproxy` slot rebuild; a new unpicklable slot type needs the same treatment plus a round-trip test.
- **One sanctioned global**: the (future) cached default `Parser`. Lazily cached FROZEN singletons (`Lexicon.default()`'s `functools.cache`, the future default parser) are constants, not state; any second piece of module-level MUTABLE state requires amending the conventions doc, on purpose, in review. Sanctioned exceptions, facade layer only: `_config_shim.CONSTANTS` (the v1 shared singleton, mutable by design) and `_facade._WARNED_SUBCLASSES` (the once-per-subclass hook-warning dedup set) — both deleted with the layer in 3.0.
- **Tests**: all v2 tests live in the `tests/v2/` package (its `conftest.py` overrides the v1 dual-run fixture — v2 code never reads shared `CONSTANTS`), one test module per source module plus cross-cutting `test_reprs.py` and `test_layering.py`, names stating behavior. Never assert `Lexicon.default()` contents; the narrow sourcing spot-checks in `test_default_sources_v1_vocabulary` that pin the v1→v2 migration contract (e.g. the flipped `particles_ambiguous` model) are the sanctioned exception.

## Extension Patterns

**Adding a scalar `Constants` attribute + `HumanName` kwarg** (e.g. `initials_separator`, `suffix_delimiter`):
Expand All @@ -120,7 +136,7 @@ Each named attribute (`title`, `first`, etc.) is a `@property` that joins its co

Add a dedicated `copy.deepcopy()` round-trip test for it too (see `test_regexes_deepcopy_roundtrip`/`test_nickname_delimiters_deepcopy_roundtrip` in `tests/test_constants.py`), not just reliance on conftest's autouse snapshot/restore exercising it incidentally. `TupleManager`/`RegexTupleManager.__getattr__` answer *any* unknown attribute lookup — dunder probes like `__deepcopy__` raise `AttributeError` (never answered as config keys), everything else still returns `self.get(attr)`/`EMPTY_REGEX` — so a new manager subtype or a `__getattr__` tweak can silently break `copy.deepcopy` (this bit `RegexTupleManager` before the dunder-lookup guard was added). A direct test on the new attribute's own manager instance catches that where the conftest fixture, which never asserts on the copy, would not. As with the scalar-attribute pattern above, also check `AGENTS.md` itself for now-stale references when you touch this.

**Unknown-key attribute access on `TupleManager`/`RegexTupleManager` warns (1.4, #256)** — a key not currently in the dict emits `DeprecationWarning` naming the miss and the known keys (`_warn_unknown_key`), before falling back to the same `None`/`EMPTY_REGEX` default as before; `.get()` stays silent. Dunder probes (`__deepcopy__`) still raise `AttributeError` outright, and single-underscore probes (`_repr_html_`, IPython's `_ipython_canary_method_should_not_exist_`, etc.) are excluded from the warning too — no real config key starts with `_`, so both guards just keep protocol/introspection probes from misfiring as "typo" warnings. This means internal parser code that reads `self.C.regexes.<key>` unconditionally (e.g. `squash_bidi`'s `bidi`) now warns if a caller's custom `regexes` dict omits that key — a previously-silent partial-override pattern is on the same deprecation path as an actual typo.
**Unknown-key attribute access on `TupleManager`/`RegexTupleManager` warns (1.4, #256) — 2.0 note: the warning became a hard `AttributeError` naming the known keys (shim `TupleManager`); the paragraph below describes the deleted v1 machinery and applies only when reading v1 history.** — a key not currently in the dict emits `DeprecationWarning` naming the miss and the known keys (`_warn_unknown_key`), before falling back to the same `None`/`EMPTY_REGEX` default as before; `.get()` stays silent. Dunder probes (`__deepcopy__`) still raise `AttributeError` outright, and single-underscore probes (`_repr_html_`, IPython's `_ipython_canary_method_should_not_exist_`, etc.) are excluded from the warning too — no real config key starts with `_`, so both guards just keep protocol/introspection probes from misfiring as "typo" warnings. This means internal parser code that reads `self.C.regexes.<key>` unconditionally (e.g. `squash_bidi`'s `bidi`) now warns if a caller's custom `regexes` dict omits that key — a previously-silent partial-override pattern is on the same deprecation path as an actual typo.

**Adding a word to a config set** — first check the *other* sets for the same word (grep `nameparser/config/` or intersect the sets in a `python3 -c`). Real overlaps exist: `do`/`st`/`mc` ∈ `PREFIXES` ∩ `TITLES`/`SUFFIX_ACRONYMS`; `abd` = "ABD" ∈ `SUFFIX_ACRONYMS`; `abu` ∈ `PREFIXES` ∩ `bound_first_names` (position-dependent: leading token → first-name join, mid-name → last-name join). Usually position-dependent and harmless, but can force a guard or an exclusion (the `last_base` all-particles guard; dropping `abd` from `bound_first_names`).

Expand Down
49 changes: 21 additions & 28 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,17 @@ overriding e.g. ``CONSTANTS.regexes.parenthesis`` still works exactly as
before) and :py:obj:`~nameparser.config.Constants.maiden_delimiters` (empty
by default -- see "Routing to Maiden Name" below).

To recognize an *additional* delimiter, add a compiled pattern to
``nickname_delimiters`` under any key, then re-run
:py:meth:`~nameparser.parser.HumanName.parse_full_name` to pick it up:
In 2.0, custom delimiter patterns on ``Constants`` were removed; adding a
non-built-in key raises ``TypeError``. Custom delimiter *pairs* are a
first-class feature of the new API instead -- pass them to
:py:class:`~nameparser.Policy`:

.. doctest::

>>> import re
>>> from nameparser import HumanName
>>> from nameparser.config import Constants
>>> hn = HumanName("Benjamin {Ben} Franklin", constants=Constants())
>>> hn.nickname
''
>>> hn.C.nickname_delimiters['curly_braces'] = re.compile(r'\{(.*?)\}', re.U)
>>> hn.parse_full_name()
>>> hn.nickname
>>> from nameparser import Parser, Policy
>>> policy = Policy(
... nickname_delimiters=Policy().nickname_delimiters | {("{", "}")})
>>> Parser(policy=policy).parse("Benjamin {Ben} Franklin").nickname
'Ben'

Routing to Maiden Name
Expand Down Expand Up @@ -521,35 +517,32 @@ values with the behavior described above.
Don't Remove Emojis
~~~~~~~~~~~~~~~~~~~

By default, all emojis are removed from the input string before the name is parsed.
You can turn this off by setting the ``emoji`` regex to ``False``.
By default, all emojis are removed from the input string before the name is
parsed. In 2.0 the ``regexes.emoji`` override was removed (assignment raises
``TypeError``); the switch is the :py:class:`~nameparser.Policy` flag
``strip_emoji``:

.. doctest::

>>> from nameparser import HumanName
>>> from nameparser.config import Constants
>>> constants = Constants()
>>> constants.regexes.emoji = False
>>> hn = HumanName("Sam 😊 Smith", constants=constants)
>>> str(hn)
>>> from nameparser import Parser, Policy
>>> parser = Parser(policy=Policy(strip_emoji=False))
>>> str(parser.parse("Sam 😊 Smith"))
'Sam 😊 Smith'

Don't Remove Bidi Control Characters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, invisible bidirectional control characters (the left-to-right and
right-to-left marks and friends, common in copy-pasted right-to-left names) are
removed from the input string before the name is parsed. You can turn this off
by setting the ``bidi`` regex to ``False``.
removed from the input string before the name is parsed. In 2.0 the
``regexes.bidi`` override was removed (assignment raises ``TypeError``); the
switch is the :py:class:`~nameparser.Policy` flag ``strip_bidi``:

.. doctest::

>>> from nameparser import HumanName
>>> from nameparser.config import Constants
>>> constants = Constants()
>>> constants.regexes.bidi = False
>>> hn = HumanName("\u200fJohn\u200f Smith", constants=constants)
>>> hn.first == "\u200fJohn\u200f"
>>> from nameparser import Parser, Policy
>>> parser = Parser(policy=Policy(strip_bidi=False))
>>> parser.parse("\u200fJohn\u200f Smith").given == "\u200fJohn\u200f"
True

Config Changes May Need Parse Refresh
Expand Down
2 changes: 2 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ HumanName.config Defaults
:members:
.. automodule:: nameparser.config.conjunctions
:members:
.. automodule:: nameparser.config.maiden_markers
:members:
.. automodule:: nameparser.config.capitalization
:members:
.. automodule:: nameparser.config.regexes
Expand Down
24 changes: 24 additions & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
Release Log
===========
* 2.0.0 - unreleased

**Behavior Changes (draft)**

.. note::
This section is a **draft**, generated from the v1-vs-2.0
differential harness (``tools/differential/``) run against the
pre-M12 v1 test corpus (486 name strings). It will be replaced
by hand-written release notes before 2.0 ships; entries below
are grouped by the harness's classification and reference the
``tests/v2/cases.py`` row (if any) that pins the new behavior.

- Fix ``"Andrews, M.D."``-shaped input: the lone strict-suffix-or-title piece after a comma (e.g. ``"Smith, Dr."``, ``"Andrews, M.D."``) was routed to ``first`` in 1.x; 2.0 routes it to ``suffix``/``title`` instead, since the pre-comma piece is definitionally the family name (``tests/v2/cases.py`` rows ``family_comma_lone_suffix_piece``, ``family_comma_lone_title``; verified live against 1.4.0 -- 1/486 corpus names diff, all others parity)
- Fix a lone recognized trailing suffix (e.g. ``"Johnson PhD"``, ``"Mr. Johnson PhD"``) being routed to ``first``/``last`` in 1.x when no comma is present; 2.0 keeps a recognized suffix in ``suffix`` (``tests/v2/cases.py`` rows ``suffix_stays_suffix``, ``suffix_stays_suffix_title``; not present in the differential corpus -- no v1 test string exercises the bare two-token shape, so this is unverified against 1.4 live but pinned by the case table)
- Fix maiden-name markers (``née``/``nee``/``born``/``geb.``/``roz.``) being folded into ``middle``/``last`` in 1.x (e.g. ``"Jane Smith née Jones"`` → ``middle="Smith née"``); 2.0 recognizes the marker and routes the following name to the new ``maiden`` field (closes #274; ``tests/v2/cases.py`` row ``maiden_marker``; not present in the differential corpus)
- Data change: ``ma``/``do`` added to ``suffix_acronyms_ambiguous`` so a bare common surname (``"Jack Ma"``) is no longer misread as a suffix acronym, restoring v1's older (pre-regression) parity (``tests/v2/cases.py`` row ``ambiguous_surname_acronyms``). Side effect: parenthesized/quoted ``"(MA)"``/``"(DO)"`` (no periods) no longer escape to ``suffix`` the way 1.x did -- they now fall through to nickname parsing like any other ambiguous-acronym delimited content. Not present in the differential corpus
- Change suffix-delimiter rendering: with a custom ``Policy``/``Constants`` suffix delimiter configured (e.g. ``suffix_delimiter="/"``, ``"John Smith, RN/CRNA"``), 1.x split the token and rendered ``suffix="RN, CRNA"``; 2.0 keeps the no-space delimiter-core token whole (``suffix="RN/CRNA"``) -- role assignment is unchanged, only rendering differs (anti-#100, migration plan deviation 5; ``tests/v2/cases.py`` row ``suffix_delimiter_no_space_core``). Only fires with a non-default policy, so it does not appear in the (default-policy) differential corpus

Everything else in the 486-name differential corpus (built from the
v1 test banks as of commit ``2d5d8c2``, pre-dating the M12 test
reconciliation) parses identically between nameparser 1.4.0 and
this working tree; see ``tools/differential/README.md`` for how to
reproduce the comparison.

* 1.4.0 - July 12, 2026

- Add ``Constants.copy()``, a detached deep copy that preserves the source instance's current customizations (unlike ``Constants()``, which always starts from library defaults) -- useful as ``CONSTANTS.copy()`` for a private snapshot of the shared config (#260)
Expand Down
31 changes: 31 additions & 0 deletions nameparser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,34 @@
__author_email__ = 'derek73@gmail.com'
__license__ = "LGPL"
__url__ = "https://github.com/derek73/python-nameparser"

from nameparser._lexicon import Lexicon
from nameparser._locale import Locale
from nameparser._parser import Parser, parse, parser_for
from nameparser._policy import (
FAMILY_FIRST,
FAMILY_FIRST_GIVEN_LAST,
GIVEN_FIRST,
UNSET,
PatronymicRule,
Policy,
PolicyPatch,
)
from nameparser._types import (
Ambiguity,
AmbiguityKind,
ParsedName,
Role,
Span,
Token,
)

__all__ = [
# v1 (compatibility layer)
"HumanName",
# v2 core
"Span", "Role", "Token", "Ambiguity", "AmbiguityKind", "ParsedName",
"Lexicon", "Policy", "PolicyPatch", "PatronymicRule", "UNSET",
"GIVEN_FIRST", "FAMILY_FIRST", "FAMILY_FIRST_GIVEN_LAST", "Locale",
"Parser", "parse", "parser_for",
]
Loading
Loading