Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Release Log
* 1.3.1 - Unreleased

- Fix invisible Unicode bidirectional control characters (LRM/RLM/ALM, the embedding/override marks, and the isolates U+2066–U+2069) surviving parsing and sticking to ``first``/``last``/etc., so a copy-pasted right-to-left name silently failed equality and dedup. They are now stripped in preprocessing like emoji; disable via ``CONSTANTS.regexes.bidi = False`` (closes #266)
- Fix ``str()`` corrupting name text containing the substring ``"None"`` when ``empty_attribute_default`` is ``None`` (e.g. ``"Nonez Smith"`` rendered as ``"z Smith"``): empty attributes are now substituted as ``''`` before the format string is applied, instead of scrubbing the interpolated ``"None"`` from the output afterward (closes #254)

* 1.3.0 - July 5, 2026

Expand Down
9 changes: 7 additions & 2 deletions nameparser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,14 @@ def __setitem__(self, key: str, value: str | list[str] | None) -> None:
def __str__(self) -> str:
if self.string_format is not None:
# string_format = "{title} {first} {middle} {last} {suffix} ({nickname})"
_s = self.string_format.format(**self.as_dict()) # noqa: UP032
# Empty attributes must render as '' (not empty_attribute_default,
# which may be None) so str.format does not interpolate the
# literal "None" into the output, which cannot be scrubbed
# afterward without corrupting name text containing the same
# substring (#254).
_s = self.string_format.format(**{k: v or '' for k, v in self.as_dict().items()})
# remove trailing punctuation from missing nicknames
_s = _s.replace(str(self.C.empty_attribute_default), '').replace(" ()", "").replace(" ''", "").replace(' ""', "")
_s = _s.replace(" ()", "").replace(" ''", "").replace(' ""', "")
_s = self.C.regexes.space_before_comma.sub(',', _s)
return self.collapse_whitespace(_s).strip(', ')
return " ".join(self)
Expand Down
15 changes: 15 additions & 0 deletions tests/test_output_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ def test_formating_of_nicknames_in_middle(self) -> None:
hn.nickname = ''
self.assertEqual(str(hn), "Rev John A. Kenneth Doe III")

def test_name_containing_none_substring_with_none_empty_attribute_default(self) -> None:
# Regression for #254: with empty_attribute_default = None, __str__
# scrubbed the literal string 'None' from the formatted output,
# corrupting real name text containing that substring.
hn = HumanName("Nonez Smith", None)
hn.C.empty_attribute_default = None # type: ignore[assignment] # see test_constants.test_empty_attribute_default
self.assertEqual(str(hn), "Nonez Smith")

def test_name_none_as_literal_name_with_none_empty_attribute_default(self) -> None:
# Companion to the #254 regression: a name piece that is exactly
# 'None' must survive formatting in None-mode.
hn = HumanName("None Smith", None)
hn.C.empty_attribute_default = None # type: ignore[assignment] # see test_constants.test_empty_attribute_default
self.assertEqual(str(hn), "None Smith")

def test_empty_field_drops_surrounding_whitespace(self) -> None:
# issue #139: adjacent whitespace/punctuation should be dropped when a field is empty
hn = HumanName("John Smith")
Expand Down
Loading