From 1a1d1c823c569861620c153403c21719095c0209 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sat, 4 Jul 2026 22:23:38 -0700 Subject: [PATCH 1/2] Give Constants a useful __repr__ The previous __repr__ ("") didn't identify which instance you were looking at or how it differed from the default, making it useless for debugging config state. Collections (some with hundreds of entries, e.g. titles/prefixes) are summarized as counts rather than dumped in full. Scalar config flags are only shown when they differ from the class default, so a plain Constants() reads as just the collection sizes. Formatted as a bracketed multi-line block to match HumanName's __repr__ style. Updates the two doctests in docs/customize.rst that asserted the old repr string, using ELLIPSIS so they don't break every time a config set's size changes. --- docs/customize.rst | 12 ++++++++---- nameparser/config/__init__.py | 23 ++++++++++++++++++++++- tests/test_constants.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/docs/customize.rst b/docs/customize.rst index af0ca60..b344eb0 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -17,8 +17,10 @@ The first is via ``from nameparser.config import CONSTANTS``. .. doctest:: >>> from nameparser.config import CONSTANTS - >>> CONSTANTS - + >>> CONSTANTS # doctest: +ELLIPSIS + The other is the ``C`` attribute of a ``HumanName`` instance, e.g. ``hn.C``. @@ -27,8 +29,10 @@ The other is the ``C`` attribute of a ``HumanName`` instance, e.g. >>> from nameparser import HumanName >>> hn = HumanName("Dean Robert Johns") - >>> hn.C - + >>> hn.C # doctest: +ELLIPSIS + Both places are usually a reference to the same shared module-level :py:class:`~nameparser.config.CONSTANTS` instance, depending on how you diff --git a/nameparser/config/__init__.py b/nameparser/config/__init__.py index a991f12..ed703a3 100644 --- a/nameparser/config/__init__.py +++ b/nameparser/config/__init__.py @@ -531,8 +531,29 @@ def suffixes_prefixes_titles(self) -> Set[str]: self._pst = self.prefixes | self.suffix_acronyms | self.suffix_not_acronyms | self.titles return self._pst + _repr_collection_attrs = ( + 'prefixes', 'suffix_acronyms', 'suffix_not_acronyms', 'titles', + 'first_name_titles', 'conjunctions', 'bound_first_names', + 'non_first_name_prefixes', 'suffix_acronyms_ambiguous', + ) + _repr_scalar_attrs = ( + 'string_format', 'initials_format', 'initials_delimiter', + 'initials_separator', 'suffix_delimiter', 'empty_attribute_default', + 'capitalize_name', 'force_mixed_case_capitalization', + 'patronymic_name_order', 'middle_name_as_last', + ) + def __repr__(self) -> str: - return "" + # Collections (some with hundreds of entries, e.g. titles/prefixes) + # are summarized as counts rather than dumped in full. Scalars are + # only shown when they differ from the class default, so a plain + # Constants() reads as just the collection sizes. + lines = [f" {name}: {len(getattr(self, name))}" for name in self._repr_collection_attrs] + lines += [ + f" {name}: {value!r}" for name in self._repr_scalar_attrs + if (value := getattr(self, name)) != getattr(type(self), name) + ] + return "" def __setstate__(self, state: Mapping[str, Any]) -> None: # Restore each saved attribute directly. The previous implementation diff --git a/tests/test_constants.py b/tests/test_constants.py index 459dd4b..70db1e2 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -522,3 +522,33 @@ def test_repeated_access_is_cached(self) -> None: f"{cached_per_call * 1e6:.1f} us/call vs an uncached build cost of " f"{uncached_per_call * 1e6:.1f} us/call. Was _pst caching removed?" ) + + +class ConstantsReprTests(HumanNameTestBase): + + def test_repr_reports_actual_collection_sizes(self) -> None: + c = Constants() + repr_str = repr(c) + for name in Constants._repr_collection_attrs: + self.assertIn(f"{name}: {len(getattr(c, name))}", repr_str) + + def test_repr_omits_scalars_at_default_value(self) -> None: + c = Constants() + repr_str = repr(c) + for name in Constants._repr_scalar_attrs: + self.assertNotIn(name, repr_str) + + def test_repr_shows_scalar_override(self) -> None: + c = Constants(middle_name_as_last=True) + self.assertIn("middle_name_as_last: True", repr(c)) + + def test_repr_reflects_mutated_collection_size(self) -> None: + c = Constants() + before = len(c.titles) + c.titles.add('a-brand-new-title-for-repr-test') + self.assertIn(f"titles: {before + 1}", repr(c)) + + def test_repr_is_bracketed_multiline(self) -> None: + repr_str = repr(Constants()) + self.assertTrue(repr_str.startswith("")) From 173e951ca334e5fdcc12a8948144d7462555ad8f Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sat, 4 Jul 2026 22:28:16 -0700 Subject: [PATCH 2/2] Address review feedback on Constants.__repr__ - Add a release_log.rst bullet for the __repr__ change (#221) - Fix the scalar-override test to exercise direct assignment, since 8 of the 10 tracked scalars aren't __init__ kwargs at all -- only patronymic_name_order and middle_name_as_last are - Add tests for multiple simultaneous scalar overrides and an empty collection --- docs/release_log.rst | 1 + tests/test_constants.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index 373e43e..d446a46 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -41,6 +41,7 @@ Release Log - Add international honorifics to ``TITLES`` (#187) - Add German/Austrian nobility and ecclesiastical titles to ``TITLES`` (closes #101) - Add German/Dutch last-name prefixes and title/degree suffixes; fix ``join_on_conjunctions()`` to register multi-word prefix chains (e.g. ``"von und zu"``) as prefixes, mirroring existing title handling (closes #18) + - Change ``Constants.__repr__`` to report collection sizes and non-default scalar config, replacing the uninformative ```` (#221) * 1.2.1 - June 19, 2026 - Fix ``initials()`` interpolating the literal ``None`` for empty name parts when ``empty_attribute_default = None`` (e.g. ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default`` - Add ``python -m nameparser "Name String"`` command-line helper that prints a parsed name diff --git a/tests/test_constants.py b/tests/test_constants.py index 70db1e2..27d8e5d 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -538,16 +538,34 @@ def test_repr_omits_scalars_at_default_value(self) -> None: for name in Constants._repr_scalar_attrs: self.assertNotIn(name, repr_str) - def test_repr_shows_scalar_override(self) -> None: + def test_repr_shows_scalar_override_via_constructor(self) -> None: c = Constants(middle_name_as_last=True) self.assertIn("middle_name_as_last: True", repr(c)) + def test_repr_shows_scalar_override_via_assignment(self) -> None: + # Most _repr_scalar_attrs (e.g. capitalize_name) aren't __init__ kwargs + # at all -- they're only ever overridden by direct assignment. + c = Constants() + c.capitalize_name = True + self.assertIn("capitalize_name: True", repr(c)) + + def test_repr_shows_multiple_simultaneous_scalar_overrides(self) -> None: + c = Constants(patronymic_name_order=True) + c.capitalize_name = True + repr_str = repr(c) + self.assertIn("patronymic_name_order: True", repr_str) + self.assertIn("capitalize_name: True", repr_str) + def test_repr_reflects_mutated_collection_size(self) -> None: c = Constants() before = len(c.titles) c.titles.add('a-brand-new-title-for-repr-test') self.assertIn(f"titles: {before + 1}", repr(c)) + def test_repr_reports_empty_collection(self) -> None: + c = Constants(titles=[]) + self.assertIn("titles: 0", repr(c)) + def test_repr_is_bracketed_multiline(self) -> None: repr_str = repr(Constants()) self.assertTrue(repr_str.startswith("