Skip to content

Commit 5331b5a

Browse files
authored
Merge pull request #354 from derek73/claude/issue-293-vocabulary-rename
Rename the vocabulary data modules to the 2.0 terminology, with a 2.x bridge (#293)
2 parents 18b0e49 + f653b69 commit 5331b5a

32 files changed

Lines changed: 1346 additions & 326 deletions

AGENTS.md

Lines changed: 13 additions & 11 deletions
Large diffs are not rendered by default.

docs/customize.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ accepts a plain set of lowercase words, keyword by field name (``titles``
2828
above; ``particles``, ``suffix_words``, and the rest work the same
2929
way) — see :doc:`modules` for the full field list.
3030

31+
The default word lists themselves — ``TITLES``, ``PARTICLES`` and the
32+
rest of ``nameparser.config`` — are frozen, so a runtime addition
33+
belongs on a :class:`~nameparser.Lexicon` as above, or on a private
34+
``Constants`` if you are still parsing through ``HumanName``. Those
35+
constants were renamed in 2.2 to match the field names used here; the
36+
1.x names still import, with a ``DeprecationWarning``, until 3.0 — see
37+
:doc:`migrate` for the mapping.
38+
3139
Vocabulary entries are matched one word at a time (``given_name_titles``
3240
excepted), so a multi-word entry like ``titles={"grand moff"}`` can
3341
never match; the constructor warns when it sees one

docs/migrate.rst

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,134 @@ fields:
222222
- Pair-valued; set it via ``dataclasses.replace(lexicon,
223223
capitalization_exceptions={...})``, not ``add()``/``remove()``
224224

225-
And behavior/render scalars map onto :class:`~nameparser.Policy` (or a
225+
The vocabulary that feeds both columns lives in ``nameparser.config``,
226+
and in 2.2 its module and constant names moved to the vocabulary the
227+
``Lexicon`` column speaks — particles, bound given names, given-name
228+
titles, suffix words. Terminology only; one of the four kept 1.x's
229+
*meaning* while its ``Lexicon`` counterpart marks the opposite set, so
230+
read the caveat under the table before pairing them up. If you import
231+
the default word lists directly — to read one, extend one, or copy one
232+
into your own configuration — four vocabularies moved:
233+
234+
.. list-table::
235+
:header-rows: 1
236+
:widths: 50 50
237+
238+
* - 1.x name
239+
- 2.2 name
240+
* - ``nameparser.config.prefixes``
241+
- :mod:`nameparser.config.particles`
242+
* - ``prefixes.PREFIXES``
243+
- ``particles.PARTICLES``
244+
* - ``prefixes.NON_FIRST_NAME_PREFIXES``
245+
- ``particles.NON_GIVEN_NAME_PARTICLES``
246+
* - ``nameparser.config.bound_first_names``
247+
- :mod:`nameparser.config.bound_given_names`
248+
* - ``bound_first_names.BOUND_FIRST_NAMES``
249+
- ``bound_given_names.BOUND_GIVEN_NAMES``
250+
* - ``titles.FIRST_NAME_TITLES``
251+
- ``titles.GIVEN_NAME_TITLES``
252+
* - ``suffixes.SUFFIX_NOT_ACRONYMS``
253+
- ``suffixes.SUFFIX_WORDS``
254+
255+
The caveat is on the third row. ``NON_GIVEN_NAME_PARTICLES`` is
256+
``NON_FIRST_NAME_PREFIXES`` renamed and nothing else — same members,
257+
same *never a given name* meaning. It is **not** the constant behind
258+
``Lexicon.particles_ambiguous``, which is that field's complement, even
259+
though the two now sound as though they belong together. Pairing this
260+
table's third row with the field-mapping table above and concluding
261+
that ``NON_GIVEN_NAME_PARTICLES`` is what ``particles_ambiguous``
262+
holds is exactly the inversion the flip warning below exists to
263+
prevent.
264+
265+
Every row still resolves, and the old names are removed in 3.0. The two
266+
module rows are import paths and nothing more: importing
267+
``nameparser.config.prefixes`` or ``nameparser.config.bound_first_names``
268+
still works and says nothing, because the modules are now empty shims.
269+
It is reading a *constant* that reports — by attribute access, by
270+
``from ... import``, and by ``from ... import *`` alike. The read emits
271+
a ``DeprecationWarning`` naming the module and constant to move to,
272+
then returns the constant from its new home.
273+
274+
The warning fires once per line that reads a retired name, not once per
275+
process, so a repeated read of the same import stays quiet while a
276+
second import somewhere else in your code reports for itself. To find
277+
your own uses, raise ``DeprecationWarning`` — which Python hides by
278+
default outside ``__main__``, so an untouched run of a library that
279+
reads these names on import shows nothing::
280+
281+
python -W error::DeprecationWarning -c "import yourapp"
282+
283+
That stops at the first one, with a traceback whose last frame outside
284+
nameparser is the line to edit. Swap ``error`` for ``default`` to print
285+
them all and keep going.
286+
287+
Only the data layer moved: the ``CONSTANTS`` attribute names in the
288+
field-mapping table above are v1 facade surface and are unaffected,
289+
so ``constants.prefixes``,
290+
``constants.non_first_name_prefixes``, ``constants.bound_first_names``,
291+
``constants.first_name_titles`` and ``constants.suffix_not_acronyms``
292+
keep their 1.x spelling for as long as the facade exists.
293+
294+
Every vocabulary *set* in ``nameparser.config`` is also a ``frozenset``
295+
as of 2.2 — the renamed ones and the rest. Every set, that is; the one
296+
mapping constant is untouched, and there is a note on it below. The
297+
freeze retires one 1.x idiom outright: ``TITLES.add("dean")`` — editing
298+
a default word list in place — now raises ``AttributeError`` at the
299+
line that writes it, rather than changing some parses and not others
300+
some distance away.
301+
302+
It was never a dependable way to change a default, because the two
303+
config layers read the module constants at different moments.
304+
``Lexicon.default()`` is cached and reads them exactly once, at its
305+
first call; a v1 ``Constants`` copies them at every construction; and
306+
the shared ``CONSTANTS`` singleton is one such copy, taken at import.
307+
An edit landing *after* the first parse therefore reached only a
308+
freshly built ``Constants`` — neither ``parse()``, whose lexicon was
309+
already built, nor the shared ``CONSTANTS``, which predated the edit.
310+
An edit landing *before* any parse reached ``Lexicon.default()``, and
311+
so ``parse()``, and a fresh ``Constants`` — but still never the shared
312+
``CONSTANTS``. Whether an edit reached a given parse thus depended on
313+
which config objects the program had already built, and one program
314+
could hold two disagreeing defaults with nothing to say so.
315+
316+
``CAPITALIZATION_EXCEPTIONS`` is the constant the freeze left out. It
317+
is a mapping rather than a set, and it is still a plain mutable
318+
``dict`` — ``CAPITALIZATION_EXCEPTIONS["phd"] = "PhD"`` runs on 2.2 and
319+
raises nothing. Everything just said about split defaults still applies
320+
to it, unchanged and measured on 2.2: an edit after the first parse
321+
reaches a freshly built ``Constants``, and neither
322+
``Lexicon.default()`` nor the shared ``CONSTANTS``. The advice below is
323+
the same advice — configure the object, with
324+
``constants.capitalization_exceptions["phd"] = "PhD"`` on a private
325+
``Constants``, or ``dataclasses.replace(lexicon,
326+
capitalization_exceptions={...})`` for the 2.0 API.
327+
328+
Configure the objects instead, which both APIs have always supported
329+
and neither the freeze nor the rename affects. For ``HumanName``, build
330+
a private ``Constants`` and pass it::
331+
332+
from nameparser import HumanName
333+
from nameparser.config import Constants
334+
335+
constants = Constants()
336+
constants.titles.add("dean")
337+
name = HumanName("Dean Smith", constants=constants)
338+
339+
For the 2.0 API, extend the default lexicon and hand it to a parser::
340+
341+
from nameparser import Lexicon, Parser
342+
343+
parser = Parser(lexicon=Lexicon.default().add(titles={"dean"}))
344+
name = parser.parse("Dean Smith")
345+
346+
Mutating the shared ``CONSTANTS`` singleton still works and still
347+
reaches every ``HumanName`` that reads it, but it warns: it is
348+
deprecated along with the rest of the v1 facade and goes away in 3.0.
349+
Prefer a private ``Constants`` in new code. See :doc:`customize` for
350+
the full set of knobs on each.
351+
352+
Behavior and render scalars map onto :class:`~nameparser.Policy` (or a
226353
rendering argument, where the 2.0 equivalent isn't config at all):
227354

228355
.. list-table::
@@ -279,7 +406,12 @@ handing the parser a regex.
279406
**complementary** sets, not the same set under a new name.
280407
``non_first_name_prefixes`` lists particles that are *never* read as
281408
a given name; ``particles_ambiguous`` lists the particles that
282-
*may* be read as one. Translating a customization means flipping
409+
*may* be read as one. The same holds for the config constant behind
410+
it: ``particles.NON_GIVEN_NAME_PARTICLES`` (1.x
411+
``prefixes.NON_FIRST_NAME_PREFIXES``) marks the never-given set, so
412+
it is the complement of ``particles_ambiguous`` too, however much
413+
the 2.2 names now suggest otherwise. Translating a customization
414+
means flipping
283415
the set: ``particles_ambiguous = lexicon.particles -
284416
constants.non_first_name_prefixes``. Copying
285417
``non_first_name_prefixes`` straight into ``particles_ambiguous``

docs/modules.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ HumanName.config Defaults
209209
:members:
210210
.. automodule:: nameparser.config.suffixes
211211
:members:
212-
.. automodule:: nameparser.config.prefixes
212+
.. automodule:: nameparser.config.particles
213213
:members:
214-
.. automodule:: nameparser.config.bound_first_names
214+
.. automodule:: nameparser.config.bound_given_names
215215
:members:
216216
.. automodule:: nameparser.config.conjunctions
217217
:members:

docs/release_log.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
Release Log
22
===========
3+
* 2.2.0 - Unreleased
4+
5+
nameparser 2.2 finishes the 2.0 rename at the layer it never
6+
reached. The word lists in ``nameparser.config`` were still named
7+
for v1's fields — prefixes, first names — while the
8+
``Lexicon`` they feed has spoken of particles and given names since
9+
2.0. They now agree. The lists are also frozen, which retires
10+
editing one in place as a way to change a default and replaces it
11+
with configuring a ``Lexicon`` or a private ``Constants``.
12+
13+
Nothing moved between vocabularies and no parse changes: over the
14+
751 names of the differential corpora, every one of the seven
15+
fields is identical to 2.1 through both the 2.0 and the 1.x API.
16+
What breaks is code that *writes* to a default word list, and code
17+
that imports one by its 1.x name has until 3.0.
18+
19+
**Breaking Changes**
20+
21+
- Change every vocabulary set in ``nameparser.config`` to a ``frozenset``: ``TITLES``, ``GIVEN_NAME_TITLES``, ``SUFFIX_WORDS``, ``SUFFIX_ACRONYMS``, ``SUFFIX_ACRONYMS_AMBIGUOUS``, ``GLUED_HONORIFICS``, ``PARTICLES``, ``NON_GIVEN_NAME_PARTICLES``, ``BOUND_GIVEN_NAMES``, ``CONJUNCTIONS`` and ``MAIDEN_MARKERS`` (``KOREAN_SURNAMES`` already was one). Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError: 'frozenset' object has no attribute 'add'`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. For the 2.0 API, build a lexicon and pass it to a parser (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Neither is affected by this change. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293)
22+
23+
**Deprecations**
24+
25+
- Rename the four vocabularies whose 1.x names described the fields they feed in v1's words, so the data layer matches the ``Lexicon``:
26+
27+
.. list-table::
28+
:header-rows: 1
29+
:widths: 50 50
30+
31+
* - 1.x name
32+
- 2.2 name
33+
* - ``nameparser.config.prefixes``
34+
- :mod:`nameparser.config.particles`
35+
* - ``prefixes.PREFIXES``
36+
- ``particles.PARTICLES``
37+
* - ``prefixes.NON_FIRST_NAME_PREFIXES``
38+
- ``particles.NON_GIVEN_NAME_PARTICLES``
39+
* - ``nameparser.config.bound_first_names``
40+
- :mod:`nameparser.config.bound_given_names`
41+
* - ``bound_first_names.BOUND_FIRST_NAMES``
42+
- ``bound_given_names.BOUND_GIVEN_NAMES``
43+
* - ``titles.FIRST_NAME_TITLES``
44+
- ``titles.GIVEN_NAME_TITLES``
45+
* - ``suffixes.SUFFIX_NOT_ACRONYMS``
46+
- ``suffixes.SUFFIX_WORDS``
47+
48+
Every row above still resolves and is removed in 3.0. The two module rows are import paths: importing them still works and says nothing, since both modules are now empty shims. Reading a *constant* -- by attribute access, by ``from ... import``, or by ``from ... import *`` -- emits a ``DeprecationWarning`` naming the module and constant to move to, once per line that reads it rather than once per process, so every place you have to edit is reported rather than only whichever one ran first. ``python -W error::DeprecationWarning -c "import yourapp"`` surfaces them; Python hides ``DeprecationWarning`` outside ``__main__``. Two of the four kept their module, so only the constant moved there. ``SUFFIX_NOT_ACRONYMS`` was also inaccurate as well as dated — ``esq`` is in ``SUFFIX_ACRONYMS`` too. The ``CONSTANTS`` attribute names (``prefixes``, ``non_first_name_prefixes``, ``bound_first_names``, ``first_name_titles``, ``suffix_not_acronyms``) are v1 facade surface and are unchanged. See :doc:`migrate` (#293)
49+
350
* 2.1.0 - August 7, 2026
451

552
nameparser 2.1 makes East Asian names work without configuration.

docs/usage.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ name to its full vocabulary set:
8787
- adjacent suffixes
8888
- ``suffix``
8989
- ``John Smith PhD MD`` → ``PhD, MD``
90-
* - :mod:`Bound given names <nameparser.config.bound_first_names>`
90+
* - :mod:`Bound given names <nameparser.config.bound_given_names>`
9191
- the following word
9292
- ``given``
9393
- ``abdul salam ahmed`` → ``abdul salam``
94-
* - :mod:`Particles <nameparser.config.prefixes>`
94+
* - :mod:`Particles <nameparser.config.particles>`
9595
- the following surname
9696
- ``family``
9797
- ``Juan de la Vega`` → ``de la Vega``

nameparser/_config_shim.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -664,28 +664,28 @@ def _raise_readonly(name: str) -> None:
664664
)
665665

666666

667-
def _default_vocab() -> dict[str, set[str]]:
667+
def _default_vocab() -> dict[str, frozenset[str]]:
668668
# v1 data modules stay the single vocabulary source through 2.x
669669
# (same rule as Lexicon.default()).
670-
from nameparser.config.bound_first_names import BOUND_FIRST_NAMES
670+
from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES
671671
from nameparser.config.conjunctions import CONJUNCTIONS
672-
from nameparser.config.prefixes import (
673-
NON_FIRST_NAME_PREFIXES, PREFIXES,
672+
from nameparser.config.particles import (
673+
NON_GIVEN_NAME_PARTICLES, PARTICLES,
674674
)
675675
from nameparser.config.suffixes import (
676-
SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_NOT_ACRONYMS,
676+
SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_WORDS,
677677
)
678-
from nameparser.config.titles import FIRST_NAME_TITLES, TITLES
678+
from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
679679
return {
680-
"prefixes": PREFIXES,
680+
"prefixes": PARTICLES,
681681
"suffix_acronyms": SUFFIX_ACRONYMS,
682-
"suffix_not_acronyms": SUFFIX_NOT_ACRONYMS,
682+
"suffix_not_acronyms": SUFFIX_WORDS,
683683
"suffix_acronyms_ambiguous": SUFFIX_ACRONYMS_AMBIGUOUS,
684684
"titles": TITLES,
685-
"first_name_titles": FIRST_NAME_TITLES,
685+
"first_name_titles": GIVEN_NAME_TITLES,
686686
"conjunctions": CONJUNCTIONS,
687-
"bound_first_names": BOUND_FIRST_NAMES,
688-
"non_first_name_prefixes": NON_FIRST_NAME_PREFIXES,
687+
"bound_first_names": BOUND_GIVEN_NAMES,
688+
"non_first_name_prefixes": NON_GIVEN_NAME_PARTICLES,
689689
}
690690

691691

@@ -1038,9 +1038,9 @@ def _build_snapshot(self) -> tuple[Lexicon, Policy, _RenderDefaults]:
10381038
particles=particles,
10391039
# complement translation: v1 marks the never-given subset;
10401040
# v2 marks the may-be-given subset. The trailing union keeps
1041-
# a config v1 accepted: prefixes.py asserts its own data has
1042-
# no word in both non_first_name_prefixes and
1043-
# bound_first_names, but nothing stops a caller adding one at
1041+
# a config v1 accepted: particles.py asserts its own data has
1042+
# no word in both NON_GIVEN_NAME_PARTICLES and
1043+
# BOUND_GIVEN_NAMES, but nothing stops a caller adding one at
10441044
# runtime, and v1 then lets the bound rule win (leading "dos
10451045
# Santos Silva" parses first="dos Santos"). Treating such a
10461046
# word as may-be-given reproduces that rather than raising.
@@ -1062,23 +1062,19 @@ def _build_snapshot(self) -> tuple[Lexicon, Policy, _RenderDefaults]:
10621062
bound_given_names=bound,
10631063
# v1 Constants has no manager for these (#274 is 2.0
10641064
# behavior); the data module is the only source
1065-
maiden_markers=frozenset(MAIDEN_MARKERS),
1065+
maiden_markers=MAIDEN_MARKERS,
10661066
# likewise no v1 manager: the unspaced-name segmentation
10671067
# vocabulary is 2.0 behavior (#271), so it rides in the
10681068
# snapshot only -- v1's Constants surface stays frozen.
1069-
# Unwrapped where maiden_markers above is wrapped: this
1070-
# module is born frozen (#293), so no wrap
10711069
surnames=KOREAN_SURNAMES,
10721070
# likewise no v1 manager: the glued-honorific tail set is
10731071
# 2.1 behavior (#308), so it rides in the snapshot only.
1074-
# Wrapped, unlike surnames above: suffixes.py is still a
1075-
# mutable v1 module, not born-frozen like surnames.py
1076-
# (#293). Intersect with the word set: Lexicon enforces
1077-
# tails <= suffix_words, and v1 semantics are that deleting
1078-
# a suffix word turns the behavior off -- a lingering tail
1079-
# simply stops mattering, the same rule ambiguous_acronyms
1080-
# gets against suffix_acronyms above.
1081-
honorific_tails=frozenset(GLUED_HONORIFICS) & suffix_words,
1072+
# Intersect with the word set: Lexicon enforces tails <=
1073+
# suffix_words, and v1 semantics are that deleting a suffix
1074+
# word turns the behavior off -- a lingering tail simply
1075+
# stops mattering, the same rule ambiguous_acronyms gets
1076+
# against suffix_acronyms above.
1077+
honorific_tails=GLUED_HONORIFICS & suffix_words,
10821078
# TupleManager is dict[str, object] (v1 parity: values were
10831079
# never statically str-typed); every real entry is a str,
10841080
# same assumption _DelimiterManager's sentinel lookup makes

nameparser/_facade.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def _split_last(self) -> tuple[list[str], list[str]]:
475475
# v1 parser.py _split_last, verbatim: vocabulary lookup at ACCESS
476476
# time (so assigned last names split too), with the all-particle
477477
# guard (a family name is assumed not to consist entirely of
478-
# particles, e.g. surname "Do" which also appears in PREFIXES)
478+
# particles, e.g. surname "Do" which also appears in PARTICLES)
479479
words = " ".join(self.last_list).split()
480480
i = 0
481481
while i < len(words) and self._is_particle(words[i]):

0 commit comments

Comments
 (0)