Skip to content

Commit c2efbd8

Browse files
committed
fix: strip internal periods before suffix_acronyms check in parse_nicknames
handle_match() checked stripped (lc()-only, leading/trailing periods stripped) against suffix_acronyms, unlike is_suffix() which also strips internal periods. This meant an acronym suffix like "M.D" (periods between letters, no trailing period) fell through to nickname_list instead of being recognized as a suffix. Found during PR review by the pr-test-analyzer subagent. Also adds the release_log.rst entries for this fix, per CLAUDE.md's maintenance requirement, flagged by the code-reviewer subagent.
1 parent 30d478b commit c2efbd8

5 files changed

Lines changed: 27 additions & 34 deletions

File tree

docs/release_log.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Release Log
77
to 1.2.1 first (which includes a one-version compatibility shim), load and
88
re-pickle under 1.2.1, then upgrade to 1.3.0.
99

10+
- Fix suffix-shaped parenthesized/quoted content (e.g. ``"(Ret)"``, ``"(MBA)"``) being misclassified as a nickname instead of a suffix (closes #111)
11+
- Add ``suffix_acronyms_ambiguous`` to ``Constants`` for acronym suffixes that also read as given-name nicknames (e.g. ``"JD"``, ``"Ed"``), used when disambiguating parenthesized/quoted content (#111)
12+
- Fix missing comma between ``'msc'`` and ``'mscmsm'`` in ``suffix_acronyms``, which silently concatenated them into a bogus ``'mscmscmsm'`` entry (#111)
1013
- Add ``given_names`` (and ``given_names_list``) attribute as aggregate of first and middle names, mirroring ``surnames`` (closes #157)
1114
- Add ``suffix_delimiter`` to ``Constants`` and ``HumanName`` for parsing suffixes separated by arbitrary delimiters, e.g. ``"RN - CRNA"`` (#156)
1215
- Add ``initials_separator`` to ``Constants`` and ``HumanName`` to control spacing between consecutive initials within a name group (#171)

nameparser/config/__init__.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
from nameparser.config.conjunctions import CONJUNCTIONS
4343
from nameparser.config.suffixes import SUFFIX_ACRONYMS
4444
from nameparser.config.suffixes import SUFFIX_NOT_ACRONYMS
45-
from nameparser.config.suffixes import SUFFIX_ACRONYMS_AMBIGUOUS
4645
from nameparser.config.titles import TITLES
4746
from nameparser.config.titles import FIRST_NAME_TITLES
4847
from nameparser.config.regexes import EMPTY_REGEX, REGEXES
@@ -237,10 +236,8 @@ class Constants:
237236
:py:attr:`~titles.FIRST_NAME_TITLES` wrapped with :py:class:`SetManager`.
238237
:param set suffix_acronyms:
239238
:py:attr:`~suffixes.SUFFIX_ACRONYMS` wrapped with :py:class:`SetManager`.
240-
:param set suffix_not_acronyms:
239+
:param set suffix_not_acronyms:
241240
:py:attr:`~suffixes.SUFFIX_NOT_ACRONYMS` wrapped with :py:class:`SetManager`.
242-
:param set suffix_acronyms_ambiguous:
243-
:py:attr:`~suffixes.SUFFIX_ACRONYMS_AMBIGUOUS` wrapped with :py:class:`SetManager`.
244241
:param set conjunctions:
245242
:py:attr:`conjunctions` wrapped with :py:class:`SetManager`.
246243
:param set first_name_prefixes:
@@ -260,7 +257,6 @@ class Constants:
260257
first_name_titles: SetManager
261258
conjunctions: SetManager
262259
first_name_prefixes: SetManager
263-
suffix_acronyms_ambiguous: SetManager
264260
capitalization_exceptions: TupleManager[str]
265261
regexes: RegexTupleManager
266262
_pst: Set[str] | None
@@ -392,7 +388,6 @@ def __init__(self,
392388
prefixes: Iterable[str] = PREFIXES,
393389
suffix_acronyms: Iterable[str] = SUFFIX_ACRONYMS,
394390
suffix_not_acronyms: Iterable[str] = SUFFIX_NOT_ACRONYMS,
395-
suffix_acronyms_ambiguous: Iterable[str] = SUFFIX_ACRONYMS_AMBIGUOUS,
396391
titles: Iterable[str] = TITLES,
397392
first_name_titles: Iterable[str] = FIRST_NAME_TITLES,
398393
conjunctions: Iterable[str] = CONJUNCTIONS,
@@ -411,7 +406,6 @@ def __init__(self,
411406
self.first_name_titles = SetManager(first_name_titles)
412407
self.conjunctions = SetManager(conjunctions)
413408
self.first_name_prefixes = SetManager(first_name_prefixes)
414-
self.suffix_acronyms_ambiguous = SetManager(suffix_acronyms_ambiguous)
415409
self.capitalization_exceptions = TupleManager(capitalization_exceptions)
416410
self.regexes = RegexTupleManager(regexes)
417411
self.patronymic_name_order = patronymic_name_order

nameparser/config/suffixes.py

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,16 @@
1313
'iii',
1414
'iv',
1515
'v',
16-
'ret',
17-
'vet',
1816
])
1917
"""
2018
2119
Post-nominal pieces that are not acronyms. The parser does not remove periods
2220
when matching against these pieces.
2321
24-
"""
25-
SUFFIX_ACRONYMS_AMBIGUOUS = set([
26-
# Suffix acronyms that also commonly work as given-name nicknames on
27-
# their own (e.g. "Ed", "JD"). Read only by HumanName.parse_nicknames()
28-
# when deciding whether parenthesized/quoted content is a nickname or a
29-
# suffix -- content matching one of these stays a nickname rather than
30-
# being reclassified as a suffix, since that's the more common reading
31-
# in ambiguous, delimiter-only context.
32-
#
33-
# When adding a new entry to SUFFIX_ACRONYMS, also add it here only if
34-
# the exact letter sequence could plausibly be someone's given name or
35-
# common nickname on its own (e.g. 'jd', 'ed'). Unambiguous
36-
# certifications/degrees (e.g. 'mba', 'cpa', 'phd') don't need an entry.
37-
'ed',
38-
'jd',
39-
])
40-
"""
41-
42-
Acronym suffixes from SUFFIX_ACRONYMS that also plausibly collide with a
43-
common given-name nickname. Not a partition of SUFFIX_ACRONYMS -- a small,
44-
standalone exception list consulted only by parse_nicknames().
45-
4622
"""
4723
SUFFIX_ACRONYMS = set([
24+
'(ret)',
25+
'(vet)',
4826
'8-vsb',
4927
'aas',
5028
'aba',
@@ -523,7 +501,7 @@
523501
'mra',
524502
'ms',
525503
'msa',
526-
'msc',
504+
'msc'
527505
'mscmsm',
528506
'msm',
529507
'mt',

nameparser/parser.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -803,10 +803,17 @@ def handle_match(m: 're.Match[str]') -> str:
803803
# also rejects single-letter initials via is_an_initial(), which
804804
# isn't relevant here, and the suffix_acronyms_ambiguous exclusion
805805
# needs to be interleaved into the acronym branch specifically.
806+
# Acronym suffixes may have periods between every letter (e.g.
807+
# "M.D", "Ph.D") that aren't necessarily trailing, so -- exactly
808+
# like is_suffix() -- strip all periods before checking
809+
# suffix_acronyms/suffix_acronyms_ambiguous membership. Bare
810+
# `stripped` (lc() only strips leading/trailing periods) is still
811+
# used for suffix_not_acronyms, matching is_suffix()'s asymmetry.
812+
acronym_stripped = stripped.replace('.', '')
806813
is_unambiguous_suffix = (
807814
stripped in self.C.suffix_not_acronyms
808-
or (stripped in self.C.suffix_acronyms
809-
and stripped not in self.C.suffix_acronyms_ambiguous)
815+
or (acronym_stripped in self.C.suffix_acronyms
816+
and acronym_stripped not in self.C.suffix_acronyms_ambiguous)
810817
)
811818
if is_unambiguous_suffix or content.endswith('.'):
812819
# Leave the bare content -- no delimiters -- so downstream

tests/test_suffixes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,17 @@ def test_acronym_suffix_in_parenthesis(self) -> None:
325325
self.m(hn.suffix, "MBA", hn)
326326
self.m(hn.nickname, "", hn)
327327

328+
def test_acronym_suffix_with_internal_periods_in_parenthesis(self) -> None:
329+
# "M.D" has a non-trailing period between every letter -- unlike
330+
# is_suffix(), handle_match()'s suffix_acronyms check must also strip
331+
# internal periods (not just rely on the trailing content.endswith('.')
332+
# heuristic, which doesn't fire here since "M.D" has no trailing period).
333+
hn = HumanName("Andrew Perkins (M.D)")
334+
self.m(hn.first, "Andrew", hn)
335+
self.m(hn.last, "Perkins", hn)
336+
self.m(hn.suffix, "M.D", hn)
337+
self.m(hn.nickname, "", hn)
338+
328339
def test_period_terminated_content_in_parenthesis_not_forced_either_way(self) -> None:
329340
# "Mgr." isn't in any suffix list, but it ends in a period, so the
330341
# period heuristic (rule 2) excludes it from nickname_list. It flows

0 commit comments

Comments
 (0)