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
12 changes: 8 additions & 4 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ The first is via ``from nameparser.config import CONSTANTS``.
.. doctest::

>>> from nameparser.config import CONSTANTS
>>> CONSTANTS
<Constants() instance>
>>> CONSTANTS # doctest: +ELLIPSIS
<Constants : [
prefixes: ...
]>

The other is the ``C`` attribute of a ``HumanName`` instance, e.g.
``hn.C``.
Expand All @@ -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
<Constants() instance>
>>> hn.C # doctest: +ELLIPSIS
<Constants : [
prefixes: ...
]>

Both places are usually a reference to the same shared module-level
:py:class:`~nameparser.config.CONSTANTS` instance, depending on how you
Expand Down
1 change: 1 addition & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<Constants() instance>`` (#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
Expand Down
23 changes: 22 additions & 1 deletion nameparser/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<Constants() instance>"
# 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 "<Constants : [\n" + "\n".join(lines) + "\n]>"

def __setstate__(self, state: Mapping[str, Any]) -> None:
# Restore each saved attribute directly. The previous implementation
Expand Down
48 changes: 48 additions & 0 deletions tests/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<Constants : [\n"))
self.assertTrue(repr_str.endswith("\n]>"))
Loading