Skip to content

Commit 13edf06

Browse files
derek73claude
andcommitted
Address review feedback: harden tests, fix release log claim, document empty parse
- Correct release log: the unparsable guard broke in 2013 (v0.2.9), not in the earliest releases (2011-2013 releases had a working attribute) - Pin multiple empty comma segments ("Doe, John,, Jr.,, III") so a break-instead-of-continue regression can't pass the single-empty test - Pin hash interop with plain strings ("john smith" in {HumanName(...)}), which depends on hashing str(self).lower() specifically - Tighten the "final piece" comments in parse_full_name and document are_suffixes()'s vacuous-truth contract that the piece loops rely on - Document in usage.rst that empty/garbage input parses to an all-empty name, checked via len(name) == 0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 546331f commit 13edf06

5 files changed

Lines changed: 39 additions & 9 deletions

File tree

docs/release_log.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Release Log
22
===========
33
* 1.3.0 - Unreleased
4-
- Remove the vestigial ``unparsable`` attribute: the guard that was meant to set it was unreachable, so it has reported ``False`` for every parsed name since the earliest releases
4+
- Remove the vestigial ``unparsable`` attribute: the guard that was meant to set it has been unreachable since 2013 (v0.2.9), so it has reported ``False`` for every parsed name for over a decade; check ``len(name) == 0`` to detect an empty parse
55
- Fix ``__hash__`` to lowercase the name like ``__eq__`` does, so equal ``HumanName`` instances hash equal and behave correctly in sets and dicts
66
- Fix ``initials()`` emitting a stray empty initial (e.g. ``"J. . V."``) -- or raising ``TypeError`` when ``empty_attribute_default`` is ``None`` -- for name parts with no initialable words, e.g. a prefix-only middle name like ``"de la"``
77
- Fix a trailing suffix being silently dropped after an empty comma segment, e.g. ``"Doe, John,, Jr."`` losing the ``"Jr."``

docs/usage.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ Requires Python 3.10+.
7474
>>> name[1:-3]
7575
['Juan', 'Q. Xavier', 'de la Vega']
7676

77+
Empty or unparsable input does not raise an error; it produces a name whose
78+
attributes are all empty. Check ``len(name) == 0`` (or ``str(name) == ''``)
79+
to detect that nothing was parsed.
80+
81+
.. doctest::
82+
83+
>>> name = HumanName("")
84+
>>> len(name)
85+
0
86+
>>> str(name)
87+
''
88+
7789

7890
Capitalization Support
7991
----------------------

nameparser/parser.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,11 @@ def initials(self) -> str:
309309

310310
first_initials_list, middle_initials_list, last_initials_list = self._initials_lists()
311311

312-
# Empty parts must render as '' (not empty_attribute_default, which may be
313-
# None) so str.format does not interpolate the literal "None" into the
314-
# output. A fully-empty result falls back to empty_attribute_default,
315-
# matching the other attribute accessors (e.g. ``first``).
312+
# Empty name groups must render as '' (not empty_attribute_default,
313+
# which may be None) so str.format does not interpolate the literal
314+
# "None" into the output. A fully-empty result falls back to
315+
# empty_attribute_default, matching the other attribute accessors
316+
# (e.g. ``first``).
316317
initials_dict = {
317318
"first": (self.initials_delimiter + self.initials_separator).join(first_initials_list) + self.initials_delimiter
318319
if len(first_initials_list) else "",
@@ -650,7 +651,12 @@ def is_suffix(self, piece: str) -> bool:
650651
and not self.is_an_initial(piece)
651652

652653
def are_suffixes(self, pieces: Iterable[str]) -> bool:
653-
"""Return True if all pieces are suffixes."""
654+
"""Return True if all pieces are suffixes.
655+
656+
Vacuously True for an empty iterable — the piece loops in
657+
:py:func:`parse_full_name` rely on this to route the final piece
658+
to the last-name branch.
659+
"""
654660
for piece in pieces:
655661
if not self.is_suffix(piece):
656662
return False
@@ -1044,9 +1050,10 @@ def parse_full_name(self) -> None:
10441050
self.is_roman_numeral(nxt) and i == p_len - 2
10451051
and not self.is_an_initial(piece)
10461052
):
1047-
# the final piece always lands here: are_suffixes() is
1048-
# vacuously True for the empty tail, making this the
1049-
# last-name branch as well as the suffix branch
1053+
# any piece reaching this check as the final piece lands
1054+
# here: are_suffixes() is vacuously True for the empty
1055+
# tail, making this the last-name branch as well as the
1056+
# suffix branch
10501057
self.last_list.append(piece)
10511058
self.suffix_list += pieces[i+1:]
10521059
break

tests/test_python_api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ def test_hash_matches_case_insensitive_equality(self) -> None:
223223
self.assertEqual(hn1, hn2)
224224
self.assertEqual(hash(hn1), hash(hn2))
225225
self.assertEqual(len({hn1, hn2}), 1)
226+
# __eq__ also accepts plain strings, so hashing str(self).lower()
227+
# specifically (not e.g. an attribute tuple) is what lets strings and
228+
# HumanName instances interoperate in sets and dicts
229+
hn = HumanName("John Smith")
230+
self.assertEqual(hash(hn), hash("john smith"))
231+
self.assertIn("john smith", {hn})
226232

227233
def test_not_equal_operator(self) -> None:
228234
self.assertTrue(HumanName("John Smith") != HumanName("Jane Smith"))

tests/test_suffixes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,8 @@ def test_empty_comma_segment_does_not_drop_following_suffix(self) -> None:
431431
self.m(hn.first, "John", hn)
432432
self.m(hn.last, "Doe", hn)
433433
self.m(hn.suffix, "Jr.", hn)
434+
# each empty segment is skipped individually; segments after a later
435+
# empty must survive too (guards against a break-instead-of-continue
436+
# regression that the single-empty case above would not catch)
437+
hn = HumanName("Doe, John,, Jr.,, III")
438+
self.m(hn.suffix, "Jr., III", hn)

0 commit comments

Comments
 (0)