From df083447f0fd7a7f9733618e599d5578773e9870 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 8 Nov 2026 10:40:47 -0800 Subject: [PATCH 1/3] Open the 3.1 cycle after 3.0.0 VERSION (3, 1, 0) and PRE_RELEASE "dev", so the tree reports 3.1.0dev, above 3.0.0 and below 3.1.0, with the version comments naming the new versions; and a 3.1.0 Unreleased entry in the release log. CITATION.cff stays at 3.0.0, the last released version. Co-Authored-By: Claude Opus 5.5 --- docs/release_log.rst | 4 ++++ nameparser/_version.py | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index 4cb2fa870..1edbb5155 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -1,5 +1,9 @@ Release Log =========== +* 3.1.0 - Unreleased + + nameparser 3.1 is under development. It refuses a ``Lexicon``, ``Parser`` or locale pack pickled by 2.x, which 3.0 loads with a ``DeprecationWarning``. + * 3.0.0 - November 8, 2026 **nameparser 3.0** removes ``HumanName``, ``Constants`` and the rest of the 1.x compatibility layer, which 2.8 deprecates, and rebuilds the vocabulary as readings of words, kept in files by language. A parser can be built for a choice of languages, the suffix splits by kind and the family name into two surnames, and every parsed word records the reading it took. :doc:`migrate` gives the 3.0 call for each 1.x and 2.x one. Highlights: diff --git a/nameparser/_version.py b/nameparser/_version.py index f1872e671..4f336cab6 100644 --- a/nameparser/_version.py +++ b/nameparser/_version.py @@ -4,16 +4,16 @@ #: #: Bumped when a cycle OPENS, not when it ships, so the tree says what #: it is building. Note the cost of the tuple being purely numeric: it -#: cannot carry the dev marker, so `VERSION >= (3, 0, 0)` is already -#: true here while 3.0.0 is unreleased. Compare `__version__` instead +#: cannot carry the dev marker, so `VERSION >= (3, 1, 0)` is already +#: true here while 3.1.0 is unreleased. Compare `__version__` instead #: where that distinction matters. -VERSION = (3, 0, 0) +VERSION = (3, 1, 0) #: PEP 440 pre-release/dev segment appended to the numeric version, or #: "" for a final release. Joined WITHOUT a dot ("2.0.0rc1", not #: "2.0.0.rc1"); setuptools reads __version__ as the package version. #: #: "dev" through the cycle, cleared at release. It normalizes to -#: 3.0.0.dev0, which sorts above 2.8.0 and BELOW 3.0.0, so an install +#: 3.1.0.dev0, which sorts above 3.0.0 and BELOW 3.1.0, so an install #: from master can never masquerade as the release it precedes. -PRE_RELEASE = "" +PRE_RELEASE = "dev" __version__ = ".".join(map(str, VERSION)) + PRE_RELEASE From 3ee047eced3f92d68aedfb1563eed284e6a3972a Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 8 Nov 2026 10:40:47 -0800 Subject: [PATCH 2/3] Compare 3.0's views against a baseline that has them compare.py's baseline worker recorded the seven fields, the reports, initials() and the case views, but not the generation, credentials, honorifics, patronymic and surname2 views snapshot.read() records since 3.0, so against 3.0.0 every name filling one read as moved: a bare run reported 669 of 7,107 entries in a tree that moved none. The worker now reads each view as the tree side does, says which views the wheel's ParsedName has, and unrecorded() leaves out the ones a 2.x baseline lacks, as it does the alternatives before 2.7 (--baseline 2.8.0: 1,669 entries with the views compared, 1,155 without). DEFAULT_BASELINE is 3.0.0, against which the tree now reads 0 moved. A test ties the worker's views to snapshot.RECORDED_LATER, so a view added to the snapshot and not the worker fails there. Co-Authored-By: Claude Opus 5.5 --- tests/v2/test_differential.py | 32 +++++++++++++++++++++++++++++++- tools/differential/README.md | 1 + tools/differential/compare.py | 31 ++++++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/tests/v2/test_differential.py b/tests/v2/test_differential.py index 786a06d60..85282f4e9 100644 --- a/tests/v2/test_differential.py +++ b/tests/v2/test_differential.py @@ -156,6 +156,14 @@ def _run_template(line: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]] # a report's alternative (#752 phase 2m), which the worker records # in its own copy of snapshot.alternatives() ("Jack", None, frozenset()), + # 3.0's views, one row per view the default parse fills (surname2 + # needs a policy the snapshot does not parse under): with none, the + # worker recorded no view and a bare compare.py against 3.0.0 + # reported 669 of 7,107 entries moved in a tree that moved none + # (2026-11-08) + ("John Smith Jr., MD", None, frozenset()), + ("田中さん", None, frozenset()), + ("Mahathir bin Mohamad", None, frozenset()), ]) def test_the_worker_reads_a_line_as_the_tree_does( name: str, order: str | None, views: frozenset[str]) -> None: @@ -182,7 +190,29 @@ def test_a_baseline_without_alternatives_is_not_compared_on_them() -> None: assert compare.unrecorded(tell) == () assert compare.unrecorded({**tell, "alternatives": False}) == ( "alternatives",) - assert compare.unrecorded({"__version__": "2.6.0"}) == ("alternatives",) + assert compare.unrecorded({"__version__": "2.6.0"}) == ( + "alternatives", "generation", "credentials", "honorifics", + "patronymic", "surname2") + + +def test_a_baseline_without_the_views_is_not_compared_on_them() -> None: + # A 2.x wheel's ParsedName has no generation, honorifics, patronymic + # or surname2, so every name the tree fills one for would read as + # moved; a 3.0 wheel has all four and is compared on each. + tell, _ = _run_template({"name": "John Smith Jr., MD", "order": None}) + assert tell["views"] == list(compare.VIEW_KEYS) + assert compare.unrecorded(tell) == () + assert compare.unrecorded({**tell, "views": ["generation"]}) == ( + "honorifics", "patronymic", "surname2") + + +def test_the_worker_views_are_every_view_a_snapshot_records() -> None: + # A view snapshot.row records that the worker leaves out reads as a + # move in every name holding it, the shape that hid until 3.0.0 + # became the baseline. + later = {k for keys, _ in snapshot.RECORDED_LATER for k in keys} + views = {k for keys in compare.VIEW_KEYS.values() for k in keys} + assert views == later - set(snapshot.CASE_VIEWS) - {"alternatives"} _WHEEL = "/Users/x/.cache/uv/environments-v2/w/lib/python3.11/" \ diff --git a/tools/differential/README.md b/tools/differential/README.md index c5bdd6d7b..2fa09a37e 100644 --- a/tools/differential/README.md +++ b/tools/differential/README.md @@ -77,6 +77,7 @@ both sides. A snapshot without the header records no alternatives, so says it has no `Ambiguity.alternative`: a name the tree reports one for is not a name that moved (867 of the 1,088 entries `--baseline 2.6.0` reported on 2026-10-26 were only that). +3.0's views are left out the same way against a release whose `ParsedName` lacks them, the worker naming the views it has: on 2026-11-08 `--baseline 2.8.0` reported 1,669 of 7,107 entries moved with them compared and 1,155 without, and until the worker read them at all, a bare run against 3.0.0 reported 669 entries moved in a tree that moved none. `--tree PATH` reads with another checkout's `nameparser` (the corpus is always this checkout's), and checks that the import really came from diff --git a/tools/differential/compare.py b/tools/differential/compare.py index 177069f9a..5a1640600 100644 --- a/tools/differential/compare.py +++ b/tools/differential/compare.py @@ -34,7 +34,7 @@ from typing import NamedTuple HERE = Path(__file__).resolve().parent -DEFAULT_BASELINE = "2.8.0" +DEFAULT_BASELINE = "3.0.0" REPO_ROOT = HERE.parents[1] @@ -124,10 +124,15 @@ def _surfaces_for(version: str) -> frozenset[str]: # whether this release reports the reading a fork did not take (2.7, # #752 phase 2m): compare() compares the alternatives only where it does _AMBIGUITY = getattr(nameparser, "Ambiguity", None) +# the views this release's ParsedName has (3.0's suffix, patronymic and +# second-surname views): compare() compares a view only where it does +_PARSED = getattr(nameparser, "ParsedName", None) print(json.dumps({"__version__": nameparser.__version__, "__file__": nameparser.__file__, "alternatives": "alternative" in getattr( - _AMBIGUITY, "__dataclass_fields__", {})}), flush=True) + _AMBIGUITY, "__dataclass_fields__", {}), + "views": [v for v in @@VIEWS@@ + if hasattr(_PARSED, v)]}), flush=True) if WANT_V2: from nameparser import Parser, Policy, parse @@ -163,6 +168,9 @@ def _v2_row(p): forced = str(p.capitalized(force=True)) row["_capitalized"] = plain if plain != rendered else "" row["_capitalized_force"] = forced if forced != plain else "" + # 3.0's views, as snapshot.read() records them: empty before 3.0 + for key in @@VIEW_ROW_KEYS@@: + row[f"_{key}"] = getattr(p, key, "") or "" return row @@ -201,6 +209,9 @@ def _worker_source(version: str, want_v2: bool) -> str: """ return (_WORKER_TEMPLATE .replace("@@VERSION@@", version) + .replace("@@VIEWS@@", repr(tuple(VIEW_KEYS))) + .replace("@@VIEW_ROW_KEYS@@", + repr(tuple(k for ks in VIEW_KEYS.values() for k in ks))) .replace("@@WANT_V2@@", "True" if want_v2 else "False")) @@ -329,11 +340,25 @@ class Result(NamedTuple): unrecorded: tuple[str, ...] = () # keys the baseline cannot read +#: The ParsedName views a row records (3.0), each with the row keys it +#: fills: `generation` fills `credentials` beside it (snapshot.row). +#: test_differential holds this to snapshot.RECORDED_LATER. +VIEW_KEYS = {"generation": ("generation", "credentials"), + "honorifics": ("honorifics",), + "patronymic": ("patronymic",), + "surname2": ("surname2",)} + + def unrecorded(tell: Mapping[str, object]) -> tuple[str, ...]: """The row keys the baseline worker's release cannot record, which compare() leaves out of both sides: the alternatives before 2.7, + and each view before the release whose ParsedName has it (3.0), since a tree reporting one is not a name that moved.""" - return () if tell.get("alternatives") else ("alternatives",) + has = tell.get("views") or () + assert isinstance(has, (list, tuple)) + return ((() if tell.get("alternatives") else ("alternatives",)) + + tuple(k for view, keys in VIEW_KEYS.items() + if view not in has for k in keys)) def compare(baseline: str, corpora: list[Path] | None = None) -> Result: From de127e508b9ad43ca75fcb7b756cb73c5b907fb8 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 8 Nov 2026 10:43:59 -0800 Subject: [PATCH 3/3] Hold each view row to the view it is there for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The case-view rows assert they still reach their arm; the three 3.0 view rows passed an empty set and checked nothing of the kind, so a vocabulary change that stopped 田中さん filling honorifics would leave the row passing while testing nothing. Each now names its views. Co-Authored-By: Claude Opus 5.5 --- tests/v2/test_differential.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/v2/test_differential.py b/tests/v2/test_differential.py index 85282f4e9..187531387 100644 --- a/tests/v2/test_differential.py +++ b/tests/v2/test_differential.py @@ -161,9 +161,9 @@ def _run_template(line: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]] # worker recorded no view and a bare compare.py against 3.0.0 # reported 669 of 7,107 entries moved in a tree that moved none # (2026-11-08) - ("John Smith Jr., MD", None, frozenset()), - ("田中さん", None, frozenset()), - ("Mahathir bin Mohamad", None, frozenset()), + ("John Smith Jr., MD", None, frozenset({"generation", "credentials"})), + ("田中さん", None, frozenset({"honorifics"})), + ("Mahathir bin Mohamad", None, frozenset({"patronymic"})), ]) def test_the_worker_reads_a_line_as_the_tree_does( name: str, order: str | None, views: frozenset[str]) -> None: @@ -174,7 +174,9 @@ def test_the_worker_reads_a_line_as_the_tree_does( assert set(raw) == {"v2"} tree = snapshot.read([(name, order)])[0] # the row still reaches the arm it is here for - assert set(snapshot.CASE_VIEWS) & set(tree) == views + every_view = {*snapshot.CASE_VIEWS, + *(k for keys in compare.VIEW_KEYS.values() for k in keys)} + assert every_view & set(tree) == views assert snapshot.row(name, order, raw["v2"]) == tree if name == "Jack": assert tree["alternatives"] == [