Skip to content

Commit fbab289

Browse files
derek73claude
andcommitted
Allow adding nickname delimiter patterns at runtime (#112)
parse_nicknames() looped over a hardcoded tuple of three named regexes, so overriding an existing pattern in CONSTANTS.regexes and re-parsing worked, but adding a brand new delimiter had no effect since nothing iterated a variable set of patterns. Constants now exposes nickname_delimiters, a named TupleManager of delimiter patterns (seeded with the existing quoted_word/double_quotes/ parenthesis regexes) that parse_nicknames() iterates. Adding an entry and calling parse_full_name() again picks it up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent bd212e5 commit fbab289

4 files changed

Lines changed: 51 additions & 6 deletions

File tree

docs/customize.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,29 @@ Each set of constants comes with :py:func:`~nameparser.config.SetManager.add` an
5353
the constants for your project. These methods automatically lower case and
5454
remove punctuation to normalize them for comparison.
5555

56+
Adding Custom Nickname Delimiters
57+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
58+
59+
:py:obj:`~nameparser.config.Constants.nickname_delimiters` is a named,
60+
appendable group of the delimiter patterns that
61+
:py:meth:`~nameparser.parser.HumanName.parse_nicknames` loops through
62+
(``quoted_word``, ``double_quotes`` and ``parenthesis`` by default). Add
63+
your own pattern under a new key to recognize additional delimiters, then
64+
re-run :py:meth:`~nameparser.parser.HumanName.parse_full_name` to pick it
65+
up:
66+
67+
.. doctest::
68+
69+
>>> import re
70+
>>> from nameparser import HumanName
71+
>>> hn = HumanName("Benjamin {Ben} Franklin", constants=None)
72+
>>> hn.nickname
73+
''
74+
>>> hn.C.nickname_delimiters['curly_braces'] = re.compile(r'\{(.*?)\}', re.U)
75+
>>> hn.parse_full_name()
76+
>>> hn.nickname
77+
'Ben'
78+
5679
Other editable attributes
5780
~~~~~~~~~~~~~~~~~~~~~~~~~~
5881

nameparser/config/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ class Constants:
263263
suffix_acronyms_ambiguous: SetManager
264264
capitalization_exceptions: TupleManager[str]
265265
regexes: RegexTupleManager
266+
nickname_delimiters: TupleManager[re.Pattern[str]]
266267
_pst: Set[str] | None
267268

268269
string_format = "{title} {first} {middle} {last} {suffix} ({nickname})"
@@ -414,6 +415,15 @@ def __init__(self,
414415
self.suffix_acronyms_ambiguous = SetManager(suffix_acronyms_ambiguous)
415416
self.capitalization_exceptions = TupleManager(capitalization_exceptions)
416417
self.regexes = RegexTupleManager(regexes)
418+
# Named, appendable group of delimiter patterns that parse_nicknames()
419+
# iterates in order -- see nameparser.config.regexes for the defaults.
420+
# Add a pattern here (and re-parse) to recognize a new delimiter
421+
# without needing to override parse_nicknames() itself. See issue #112.
422+
self.nickname_delimiters = TupleManager({
423+
'quoted_word': self.regexes.quoted_word,
424+
'double_quotes': self.regexes.double_quotes,
425+
'parenthesis': self.regexes.parenthesis,
426+
})
417427
self.patronymic_name_order = patronymic_name_order
418428

419429
def _invalidate_pst(self) -> None:

nameparser/parser.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -788,10 +788,6 @@ def parse_nicknames(self) -> None:
788788
`quoted_word`, `double_quotes` and `parenthesis`.
789789
"""
790790

791-
re_quoted_word = self.C.regexes.quoted_word
792-
re_double_quotes = self.C.regexes.double_quotes
793-
re_parenthesis = self.C.regexes.parenthesis
794-
795791
def handle_match(m: 're.Match[str]') -> str:
796792
# Fall back to the whole match when the regex has no capturing
797793
# group (e.g. a custom override regex without one, like
@@ -826,10 +822,13 @@ def handle_match(m: 're.Match[str]') -> str:
826822
self.nickname_list.append(content)
827823
return ''
828824

829-
# Same handle_match for all three delimiters: suffix-shaped content
825+
# Same handle_match for every delimiter: suffix-shaped content
830826
# is rare in quotes but not impossible, and the logic is delimiter-
831827
# agnostic, so there's no reason to special-case parenthesis here.
832-
for _re in (re_quoted_word, re_double_quotes, re_parenthesis):
828+
# Iterating self.C.nickname_delimiters (rather than a hardcoded
829+
# tuple) lets callers add new delimiter patterns at runtime -- see
830+
# issue #112.
831+
for _re in self.C.nickname_delimiters.values():
833832
self._full_name = _re.sub(handle_match, self._full_name)
834833

835834
def squash_emoji(self) -> None:

tests/test_nicknames.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
import pytest
24

35
from nameparser import HumanName
@@ -14,6 +16,17 @@ def test_nickname_in_parenthesis(self) -> None:
1416
self.m(hn.last, "Franklin", hn)
1517
self.m(hn.nickname, "Ben", hn)
1618

19+
# https://github.com/derek73/python-nameparser/issues/112
20+
def test_add_custom_nickname_delimiter(self) -> None:
21+
hn = HumanName("Benjamin {Ben} Franklin", constants=None)
22+
# curly braces aren't a recognized delimiter by default
23+
self.m(hn.nickname, "", hn)
24+
hn.C.nickname_delimiters['curly_braces'] = re.compile(r'\{(.*?)\}', re.U)
25+
hn.parse_full_name()
26+
self.m(hn.first, "Benjamin", hn)
27+
self.m(hn.last, "Franklin", hn)
28+
self.m(hn.nickname, "Ben", hn)
29+
1730
def test_two_word_nickname_in_parenthesis(self) -> None:
1831
hn = HumanName("Benjamin (Big Ben) Franklin")
1932
self.m(hn.first, "Benjamin", hn)

0 commit comments

Comments
 (0)