Skip to content

Commit 6f9159d

Browse files
authored
Merge pull request #477 from derek73/claude/v2-2-0-release-finalization-de07b4
Finish v2.2.0: trim the release notes, close #433 and #356, sweep the docs
2 parents 865e6bf + fe1bccb commit 6f9159d

11 files changed

Lines changed: 337 additions & 194 deletions

File tree

docs/concepts.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ else does.
6666
This is the whole parser in two sentences, and it explains its
6767
character. A word nameparser has never seen still gets a sensible role,
6868
because the positional layer does not need to recognize anything. The
69-
same word can play different parts in different places — ``Dr.`` is a
70-
title before a name and a suffix after it, which is why the field names
71-
``title`` and ``suffix`` are really "pre-nominal" and "post-nominal".
69+
same word can play different parts in different places — ``Lt.`` is a
70+
title before a name and a post-nominal after it, which is why the field
71+
names ``title`` and ``suffix`` are really "pre-nominal" and
72+
"post-nominal".
7273
And nothing is statistical: there is no model and no training data, so
7374
the same input always parses the same way, and a parse that is wrong is
7475
wrong reproducibly, which is what makes it fixable by configuration.

docs/customize.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ it matches and the word matches everywhere else. No warning is raised
6666
for a multi-word entry in either of these two fields, since there it is
6767
not a mistake.
6868

69+
The limit is on *storage*, not on the shape a name can have. Adjacent
70+
suffix words are reassembled after they match, so a multi-word
71+
credential is reachable as its component words even though the phrase
72+
itself cannot be stored:
73+
74+
.. doctest::
75+
76+
>>> from nameparser import parse
77+
>>> parse("John Smith, MD PhD").suffix
78+
'MD PhD'
79+
80+
That has held since 1.4.0. A credential whose words are not in the
81+
default vocabulary is reached by adding those words, not the phrase:
82+
83+
.. doctest::
84+
85+
>>> lex = Lexicon.default().add(suffix_acronyms={"leed", "ap"})
86+
>>> Parser(lexicon=lex).parse("John Smith, LEED AP").suffix
87+
'LEED AP'
88+
6989
Removing works the same way, and drops the word from recognition:
7090

7191
.. doctest::

docs/design/rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ P6. Rationale: a particle ending the name has nothing to link
500500

501501
## Suffixes: generational & credentials (S)
502502

503-
Background: what follows a name is one of two different things — generational suffixes (Jr., III), which attach to the name itself, and credentials (PhD, MD, MBA), which are earned attachments. The suffix sets match one written word at a time: a multi-word entry there can never match anything and is warned about at configuration. Only two sets are exempt from that rule -- given_name_titles and, since #434, maiden_markers -- and neither is a suffix set, so within the suffix vocabulary the one-word rule is absolute. The eight multi-word entries that shipped dead for years span the suffix sets and the titles alike and are the Excluded story in decisions.md. CLDR personNames keeps them as separate fields (`generation`, `credentials`) and formats them differently; this library currently reports both in one `suffix` field, a merge #326 examines. The vocabulary is largely split already: a generational word list and a credential acronym list, plus a short list of acronyms that are also ordinary names (MA, BA) and so are AMBIGUOUS as bare words.
503+
Background: what follows a name is one of two different things — generational suffixes (Jr., III), which attach to the name itself, and credentials (PhD, MD, MBA), which are earned attachments. The suffix sets match one written word at a time: a multi-word entry there can never match anything and is warned about at configuration. Only two sets are exempt from that rule -- given_name_titles and, since #434, maiden_markers -- and neither is a suffix set, so within the suffix vocabulary the one-word rule is absolute. That limit is on STORAGE, not on the shape a name may have: adjacent suffix tokens are reassembled after matching (`_vocab.is_wholly_suffix`), so a multi-word credential is reachable as its component words -- `John Smith, MD PhD` has read suffix `MD PhD` since 1.4.0 -- and a caller reaches an unshipped one by adding the words it is made of rather than the phrase (#433). The eight multi-word entries that shipped dead for years span the suffix sets and the titles alike and are the Excluded story in decisions.md. CLDR personNames keeps them as separate fields (`generation`, `credentials`) and formats them differently; this library currently reports both in one `suffix` field, a merge #326 examines. The vocabulary is largely split already: a generational word list and a credential acronym list, plus a short list of acronyms that are also ordinary names (MA, BA) and so are AMBIGUOUS as bare words.
504504

505505
S1. Rationale: brackets set off more than nicknames — credentials
506506
are routinely written parenthesized after a name, and a

docs/release_log.rst

Lines changed: 150 additions & 152 deletions
Large diffs are not rendered by default.

docs/usage.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,8 @@ discard the only one there is.
424424
So ``田中さん, V.`` and ``田中さん, Ph. D.`` give さん up exactly as
425425
``田中さん, PhD`` does. Where the credential itself lands is the
426426
comma's business rather than the peel's, and still differs by
427-
spelling: ``title`` for ``PhD``, ``given`` for ``V.``, and ``Ph. D.``
428-
in ``suffix`` beside さん.
427+
spelling: ``given`` for ``V.``, while ``PhD`` and ``Ph. D.`` join さん
428+
in ``suffix``.
429429

430430
Spacing, and where the name divides
431431
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

nameparser/_pipeline/_group.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
tokens are NEVER joined into strings: the anti-#100 invariant); maiden
88
tail tokens get role=MAIDEN; marker tokens land in dropped.
99
Reads: token tags (from classify), Lexicon.given_name_titles (the
10-
P5 licence, #369), and Policy.extra_suffix_delimiters, whose
11-
delimiter-core tokens tail segments drop (v1 suffix_delimiter parity)
12-
-- no other Policy field. The v1 "derived titles/prefixes"
10+
P5 licence, #369), Policy.extra_suffix_delimiters, whose
11+
delimiter-core tokens tail segments drop (v1 suffix_delimiter parity),
12+
and Policy.lenient_comma_suffixes, which segment_suffix_reading takes
13+
to pick the strict or lenient token test (#430/#432) -- no other Policy
14+
field. The v1 "derived titles/prefixes"
1315
registration becomes piece_tags entries -- per-parse state that
1416
dissolves with the state (v1 kept per-parse sets for the same reason).
1517

nameparser/config/_deprecated.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,15 @@ def __dir__() -> list[str]:
118118
# of tab completion and every getattr-free member scan --
119119
# autodoc's included. Pinned by test_config_aliases
120120
# ::test_dir_lists_the_live_names_as_well_as_the_retired_ones.
121-
return sorted(set(vars(sys.modules[module])) | set(aliases))
121+
#
122+
# .get, not [...]: a module dropped from sys.modules -- a test
123+
# that reloads the package, a plugin teardown -- would otherwise
124+
# make dir() raise KeyError, which is not among the things dir()
125+
# may do to a caller. The aliases are held in the closure and
126+
# are still nameable, so they are what is left to list (#356).
127+
live = sys.modules.get(module)
128+
names = set(vars(live)) if live is not None else set()
129+
return sorted(names | set(aliases))
122130

123131
# The table itself, reachable without tripping a warning. __all__ is
124132
# hand-written per module (it must stay in SOURCE order for autodoc,

nameparser/config/bound_given_names.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,9 @@
5252

5353

5454
assert_normalized("BOUND_GIVEN_NAMES", BOUND_GIVEN_NAMES)
55+
56+
# Star imports read __all__ and never the module __getattr__ -- see the
57+
# note in prefixes.py. Without it `assert_normalized`, imported only for
58+
# the invariant above, is bound by a star import as though it were
59+
# vocabulary (#356).
60+
__all__ = ["BOUND_GIVEN_NAMES"]

nameparser/config/particles.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,14 @@
262262
assert not (NON_GIVEN_NAME_PARTICLES & BOUND_GIVEN_NAMES), \
263263
"NON_GIVEN_NAME_PARTICLES must stay disjoint from BOUND_GIVEN_NAMES"
264264
assert_normalized("PARTICLES", PARTICLES)
265+
266+
# Star imports read __all__ and never the module __getattr__ -- see the
267+
# note in prefixes.py. This module has no retired name of its own to
268+
# serve; what __all__ buys here is the other half of that note's point,
269+
# which the shim modules got in #354 and the destinations did not:
270+
# without it a star import binds every module-level name, so
271+
# `assert_normalized` and BOUND_GIVEN_NAMES -- imported only for the
272+
# invariants below, and owned by another module -- landed in the
273+
# caller's namespace as though they were vocabulary (#356).
274+
# Source order, not alphabetical -- see the note in suffixes.py.
275+
__all__ = ["NON_GIVEN_NAME_PARTICLES", "PARTICLES"]

tests/test_capitalization.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,26 @@ def test_a_conjunction_inside_a_longer_token_is_a_name_word(self) -> None:
310310
uppered.capitalize()
311311
self.m(str(uppered), 'Juan E-F Smith', uppered)
312312

313+
# The same shape on a real name, which is what the release note
314+
# cites: Ortega y Gasset is routinely hyphenated in catalogues, and
315+
# `y` is conjunction vocabulary. Before #458 the two spellings
316+
# repaired to 'Jose Ortega-y-Gasset' and 'Jose Ortega-Y-Gasset'
317+
# (measured on the pre-#458 tree). The SPACED form is the contrast
318+
# and is untouched -- there `y` is a token of its own and IS the
319+
# conjunction, so it keeps the lowercase Spanish convention.
320+
def test_a_hyphenated_compound_surname_capitalizes_its_conjunction(
321+
self,
322+
) -> None:
323+
lowered = HumanName('jose ortega-y-gasset')
324+
lowered.capitalize(force=True)
325+
self.m(str(lowered), 'Jose Ortega-Y-Gasset', lowered)
326+
uppered = HumanName('JOSE ORTEGA-Y-GASSET')
327+
uppered.capitalize()
328+
self.m(str(uppered), 'Jose Ortega-Y-Gasset', uppered)
329+
spaced = HumanName('jose ortega y gasset')
330+
spaced.capitalize(force=True)
331+
self.m(str(spaced), 'Jose Ortega y Gasset', spaced)
332+
313333
# The third producer of never-classified text, and the one that is
314334
# not an assignment: __getstate__ pickles the *_list STRINGS and
315335
# nothing else (mechanisms.md#FACADE-CONTRACT -- components come

0 commit comments

Comments
 (0)