diff --git a/docs/release_log.rst b/docs/release_log.rst index 1c5ccca..08792c0 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -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 diff --git a/nameparser/parser.py b/nameparser/parser.py index c649e7c..8059818 100644 --- a/nameparser/parser.py +++ b/nameparser/parser.py @@ -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) diff --git a/tests/test_output_format.py b/tests/test_output_format.py index 1336410..afb0d54 100644 --- a/tests/test_output_format.py +++ b/tests/test_output_format.py @@ -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")