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/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/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..27d8e5d 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -522,3 +522,51 @@ 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_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(""))