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 diff --git a/tests/v2/test_differential.py b/tests/v2/test_differential.py index 786a06d60..187531387 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({"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: @@ -166,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"] == [ @@ -182,7 +192,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: