Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
176 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
912e608
Harden shim notification/validation paths and facade failure ordering
derek73 Jul 18, 2026
c39ef49
Harden the differential comparator against silent false confidence
derek73 Jul 18, 2026
b0099a9
Type polish and coverage from the toolkit review
derek73 Jul 18, 2026
05329f8
Fix comment rot and citation precision from the toolkit review
derek73 Jul 18, 2026
43969ad
Document the ma/do ambiguous-acronym data change
derek73 Jul 18, 2026
1eca44b
Apply the /simplify findings: snapshot caching, shared collapse, one …
derek73 Jul 18, 2026
9673f00
Lift _vocab's regex constants to the module top
derek73 Jul 18, 2026
2776fa2
Record the facade layer's concern-grouped class organization
derek73 Jul 18, 2026
6ee4681
Anchor the emoji-range CodeQL suppression on the flagged line
derek73 Jul 18, 2026
697dbe3
Eliminate the py/overly-large-range false positive at the source
derek73 Jul 18, 2026
ac43dc9
Add the locales package with lazy loading and the RU pack
derek73 Jul 18, 2026
e68db58
Add the TR_AZ pack
derek73 Jul 18, 2026
be3f25e
Scan ru.DEVIATES per token (whole-string search under-declared)
derek73 Jul 18, 2026
6fc7031
Add pack case-table segments through parser_for
derek73 Jul 18, 2026
822170a
Prove the RU+TR_AZ composition example with the shipped packs
derek73 Jul 18, 2026
be28a9b
Add the non-interference gate over the default corpus
derek73 Jul 18, 2026
6028ab4
Add non-Latin default vocabulary (#269): Cyrillic, Greek, Arabic, Hebrew
derek73 Jul 18, 2026
a053e8a
Drop bare Greek κ and tighten the #269 allowlist rule
derek73 Jul 18, 2026
759b217
Add --locale to the CLI (closes the deferred migration deviation)
derek73 Jul 18, 2026
b2ce60a
Record the locales package in AGENTS.md and the release-log draft
derek73 Jul 18, 2026
b49239d
Make the SetManager doctest reprs addition-proof
derek73 Jul 18, 2026
3e6e08d
fix: normalize vocabulary with lower(), not casefold()
derek73 Jul 18, 2026
81f8fa3
test: give the non-interference gate teeth; close locale coverage gaps
derek73 Jul 18, 2026
affdd5b
fix: --locale '' errors instead of silently using the default parser
derek73 Jul 18, 2026
7d6071c
docs: comment accuracy pass from the locale review
derek73 Jul 18, 2026
cf46e3c
test: simplify locale test scaffolding (/simplify review)
derek73 Jul 18, 2026
d4aaafa
test: make the pack registry the gate contract (design note, option C)
derek73 Jul 18, 2026
0fd386c
fix: point patronymic_rules=True at the v1 migration path
derek73 Jul 18, 2026
17efbbe
docs+test: record the comparison_key casefold deviation from 1.4
derek73 Jul 18, 2026
38b8dcb
style: ParsedName repr indents with 4 spaces, matching HumanName's
derek73 Jul 18, 2026
bcd58b7
docs: add concepts page — the 2.0 model in four essays
derek73 Jul 18, 2026
684f854
docs: rewrite usage.rst as the 2.0 getting-started tour
derek73 Jul 18, 2026
e954825
docs: rewrite customize.rst principle-first for the 2.0 API
derek73 Jul 18, 2026
f817d22
docs: add locales page — using, creating, contributing packs
derek73 Jul 18, 2026
26e0dea
docs: add Migrating from HumanName page (#262 successor)
derek73 Jul 18, 2026
6b68fda
fix: migration hint names both rules v1's patronymic flag enabled
derek73 Jul 18, 2026
7306724
docs: correct two migrate-table facts from the table audit
derek73 Jul 18, 2026
b29861c
docs: regroup reference into 2.0 API + compat layer; new front page
derek73 Jul 18, 2026
432e39e
docs: rewrite README new-API-first (doubles as the PyPI page)
derek73 Jul 18, 2026
5063056
docs: drop the nonexistent _static path so -W builds run clean
derek73 Jul 18, 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
22 changes: 20 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ uv run mypy
# Lint
uv run ruff check nameparser/

# Debug how a specific name string is parsed (prints HumanName repr)
# Debug how a specific name string is parsed: 2.0 core parse() --
# prints the ParsedName repr raw and capitalized, then initials;
# --json emits the component dict
uv run python -m nameparser "Dr. Juan Q. Xavier de la Vega III"

# Build docs
Expand Down Expand Up @@ -107,6 +109,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 `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. `parser.py:NNNN` citations throughout the 2.0 code refer to the PRE-swap v1 file, deleted at the M11 swap; resolve them with `git show 2d5d8c2:nameparser/parser.py`.
- **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`; locale pack modules (`locales/*.py`) import `_locale`/`_lexicon`/`_policy` only, and the `locales/__init__` additionally lazy-imports its packs (PEP 562). 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. Sanctioned deviation, facade layer only: `HumanName` and the shim `Constants` organize by v1 concern groups (`# -- render defaults --`, `# -- config / parsing --`, `# -- fields --`, ..., dunders and pickle last) — the classes mirror v1's own surface and die in 3.0; the canonical order still binds every core type.
- **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 +138,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
173 changes: 35 additions & 138 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,9 @@ Name Parser

|Build Status| |PyPI| |PyPI version| |Documentation| |License| |Downloads| |Codecov|

📣 **nameparser 2.0 is in design** — a new immutable core API, with full
compatibility for existing code through 2.x. Read the `design RFC
<https://github.com/derek73/python-nameparser/pull/285>`__ and share
feedback on `the discussion issue
<https://github.com/derek73/python-nameparser/issues/284>`__.

A simple Python (3.10+) module for parsing human names into their
individual components.

* hn.title
* hn.first
* hn.middle
* hn.last
* hn.suffix
* hn.nickname
* hn.maiden
* hn.surnames *(middle + last)*
* hn.given_names *(first + middle)*
* hn.initials *(first initial of each name part)*
* hn.last_base *(last, minus any prefixes)*
* hn.last_prefixes *(leading last-name particles, e.g. "van der")*

Supported Name Structures
~~~~~~~~~~~~~~~~~~~~~~~~~

The supported name structure is generally "Title First Middle Last Suffix", where all pieces
are optional. Comma-separated format like "Last, First" is also supported.

1. Title Firstname "Nickname" Middle Middle Lastname Suffix
2. Lastname [Suffix], Title Firstname (Nickname) Middle Middle[,] Suffix [, Suffix]
3. Title Firstname M Lastname [Suffix], Suffix [Suffix] [, Suffix]

How It Works
~~~~~~~~~~~~

The parser works in two layers.

A **vocabulary layer** recognizes name pieces by what they are, using
configurable sets of known words: titles ("Dr."), suffixes ("III", "PhD"),
last-name prefixes ("de la"), conjunctions ("y", "&"), and delimited
nicknames ("Doc"). Titles and conjunctions chain together to handle complex
titles like "Asst Secretary of State"; prefixes join forward so "de la Vega"
stays one last name. This layer doesn't care where in the string a word
appears — and it's the layer you customize, by adding or removing entries
in the sets to fit your dataset.

A **positional layer** then assigns everything the vocabulary layer didn't
claim, based purely on where it sits: the first unclaimed word is the first
name, the last one is the last name, and anything between them is a middle
name. There is no semantic understanding — "Dr" is a title when it comes
before a name and a suffix when it comes after ("pre-nominal" and
"post-nominal" would probably be better names) — and no attempt to correct
mistakes in the input.

It attempts the best guess that can be made with a simple, deterministic,
rule-based approach — no statistical models or machine learning; the same
input always parses the same way. The positional layer assumes Western name
order (given name first), so the main use case is English and other
languages that share that structure. It can also try to correct the
capitalization of names that are all upper- or lowercase. It's not perfect,
but it gets you pretty far.
nameparser parses human names into seven fields — title, given, middle,
family, suffix, nickname, maiden. Results are immutable, configuration is
composable, and locale packs are opt-in.

Installation
------------
Expand All @@ -72,95 +14,50 @@ Installation

pip install nameparser

If you want to try out the latest code from GitHub you can
install with pip using the command below.

``pip install -e git+https://github.com/derek73/python-nameparser.git``

If you need to handle lists of names, check out
`namesparser <https://github.com/gwu-libraries/namesparser>`_, a
compliment to this module that handles multiple names in a string.

Requires Python 3.11+.

Quick Start Example
-------------------

::

>>> from nameparser import HumanName
>>> name = HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)")
>>> name
<HumanName : [
title: 'Dr.'
first: 'Juan'
middle: 'Q. Xavier'
last: 'de la Vega'
suffix: 'III'
nickname: 'Doc Vega'
maiden: ''
]>
>>> name.last
'de la Vega'
>>> name.as_dict()
{'title': 'Dr.', 'first': 'Juan', 'middle': 'Q. Xavier', 'last': 'de la Vega', 'suffix': 'III', 'nickname': 'Doc Vega', 'maiden': ''}
>>> str(name)
'Dr. Juan Q. Xavier de la Vega III (Doc Vega)'
>>> name.string_format = "{first} {last}"
>>> str(name)
'Juan de la Vega'


Because the positional layer has no semantic understanding, position is
everything:
--------------------

::

>>> name = HumanName("1 & 2, 3 4 5, Mr.")
>>> name
<HumanName : [
title: ''
first: '3'
middle: '4 5'
last: '1 & 2'
suffix: 'Mr.'
nickname: ''
maiden: ''
]>

Customization
-------------
.. code-block:: python

Your project may need some adjustment for your dataset. Most customization
is vocabulary — `customizing the configured pre-defined sets`_ of titles,
prefixes, etc. that the vocabulary layer matches against. You can also do
your own pre- or post-processing, or subclass the `HumanName` class for
deeper changes. See the `full documentation`_ for more information.
>>> from nameparser import parse
>>> name = parse("Dr. Juan Q. Xavier de la Vega III")
>>> name.given, name.family
('Juan', 'de la Vega')
>>> name.render("{family}, {given}")
'de la Vega, Juan'

Those seven fields are ``title``, ``given``, ``middle``, ``family``,
``suffix``, ``nickname``, and ``maiden`` — plus aggregate views like
``given_names``, ``surnames``, ``family_base``, and ``family_particles``
for combining or splitting them further.

`Full documentation`_
~~~~~~~~~~~~~~~~~~~~~
Learn more
----------

.. _customizing the configured pre-defined sets: http://nameparser.readthedocs.org/en/latest/customize.html
.. _Full documentation: http://nameparser.readthedocs.org/en/latest/


Contributing
------------
* `Getting started <https://nameparser.readthedocs.io/en/latest/usage.html>`__ — the full tour: parsing, aggregates, dicts, rendering, dedup
* `Customizing the parser <https://nameparser.readthedocs.io/en/latest/customize.html>`__ — vocabulary, behavior, and presentation
* `Locale packs <https://nameparser.readthedocs.io/en/latest/locales.html>`__ — opt-in bundles for East Slavic patronymics, Turkic markers, and more
* There's also a CLI: ``python -m nameparser --json "Doe, John"``

If you come across name piece that you think should be in the default config, you're
probably right. `Start a New Issue`_ and we can get them added.
Coming from 1.x
----------------

Please let me know if there are ways this library could be structured to make
it easier for you to use in your projects. Read CONTRIBUTING.md_ for more info
on running the tests and contributing to the project.
Nothing breaks. 2.0 keeps ``HumanName`` and ``CONSTANTS`` working exactly
as before — same imports, same attributes, same mutation API. See
`Migrating from HumanName <https://nameparser.readthedocs.io/en/latest/migrate.html>`__
for translating a v1 customization into the new API, whenever that's
convenient for you.

**GitHub Project**
See the `release log <https://nameparser.readthedocs.io/en/latest/release_log.html>`__
for the full list of changes in the 2.0 series.

https://github.com/derek73/python-nameparser
License
-------

.. _CONTRIBUTING.md: https://github.com/derek73/python-nameparser/tree/master/CONTRIBUTING.md
.. _Start a New Issue: https://github.com/derek73/python-nameparser/issues
.. _click here to propose changes to the titles: https://github.com/derek73/python-nameparser/edit/master/nameparser/config/titles.py
LGPL licensed. See `LICENSE <https://github.com/derek73/python-nameparser/blob/master/LICENSE>`__
for details.

.. |Build Status| image:: https://github.com/derek73/python-nameparser/actions/workflows/python-package.yml/badge.svg
:target: https://github.com/derek73/python-nameparser/actions/workflows/python-package.yml
Expand Down
Loading
Loading