Skip to content

Commit ac37e57

Browse files
derek73claude
andauthored
Release v1.4.0 (#287)
Bump version to 1.4.0, date the release log entry, and sync CITATION.cff. Also fixes AGENTS.md drift caught during the release readiness check: TupleManager/RegexTupleManager.__getattr__'s unknown-key description was stale after #256 added a deprecation warning there, and the new _EmptyAttributeDefaultAttribute descriptor (#255) and its inspect.getattr_static requirement in __getstate__/__setstate__ were undocumented. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent b6c0679 commit ac37e57

4 files changed

Lines changed: 8 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ Most modules define a plain Python set of known name pieces; `capitalization.py`
8989

9090
**`_SetManagerAttribute`/`_CachedUnionMember` descriptors**: all nine `SetManager`-backed `Constants` attributes (`prefixes`, `suffix_acronyms`, `suffix_not_acronyms`, `titles`, `first_name_titles`, `conjunctions`, `bound_first_names`, `non_first_name_prefixes`, `suffix_acronyms_ambiguous`) are managed by a descriptor so a non-`SetManager` assignment (e.g. `c.conjunctions = 'and'`) raises `TypeError` immediately instead of silently degrading later `in` checks into substring tests (#241). The four PST-contributing attrs (`prefixes`, `suffix_acronyms`, `suffix_not_acronyms`, `titles`) additionally use `_CachedUnionMember`, a subclass that also wires the `_pst` cache-invalidation callback on assignment; the other five use the plain `_SetManagerAttribute` base with no cache involved. Both store their values under the *private* name (`_prefixes`, `_conjunctions`, etc.) in the instance `__dict__` so the descriptor's `__set__` owns every assignment. Any code that inspects `__dict__` directly (e.g. `__getstate__`) must map `_xxx` → `xxx` for descriptor-managed attrs rather than filtering on `not k.startswith('_')`.
9191

92+
**`_EmptyAttributeDefaultAttribute` descriptor** backs `empty_attribute_default` (1.4, #255): unlike `_SetManagerAttribute`, `__get__(obj=None, ...)` returns `''`, not `self` — deliberate, so `Constants.__repr__`'s `getattr(type(self), name)` default comparison still works and the pre-existing `str`-only mypy inference on the attribute (never widened to `str | None`, matching the descriptor-free days) is preserved. Because of that, `__getstate__`/`__setstate__` use `inspect.getattr_static(type(self), name)`, not plain `getattr`, to detect this descriptor (or the `property` used by the legacy-pickle migration shim below) without triggering `__get__` — a plain `getattr` there would silently miss both. `__setstate__` also bypasses the descriptor's own `__set__` (writes `_empty_attribute_default` directly) so restoring pickled/copied state doesn't itself emit the deprecation warning meant for user assignment.
93+
9294
### Parser (`nameparser/parser.py`)
9395

9496
`HumanName` is the single public class. Assigning to `full_name` (or instantiating with a string) triggers `parse_full_name()`.
@@ -116,7 +118,9 @@ Each named attribute (`title`, `first`, etc.) is a `@property` that joins its co
116118

117119
**Adding a new mutable/collection `Constants` attribute** (a `SetManager`/`TupleManager`-backed group, e.g. `nickname_delimiters`/`maiden_delimiters`): add it to `_COLLECTION_CONFIG_ATTRS` in `tests/conftest.py`, or tests that mutate the global `CONSTANTS` copy will leak state into later tests. Contents must be deep-copyable (the snapshot uses `copy.deepcopy`) — already true for the existing manager types. (The parse-no-mutation tests in `tests/test_constants.py` need no such registration — they discover collections structurally via `Constants.__getstate__()`; conftest's list is the only manual one.)
118120

119-
Add a dedicated `copy.deepcopy()` round-trip test for it too (see `test_regexes_deepcopy_roundtrip`/`test_nickname_delimiters_deepcopy_roundtrip` in `tests/test_constants.py`), not just reliance on conftest's autouse snapshot/restore exercising it incidentally. `TupleManager`/`RegexTupleManager.__getattr__` answer *any* unknown attribute lookup — including dunder probes like `__deepcopy__` — so a new manager subtype or a `__getattr__` tweak can silently break `copy.deepcopy` (this bit `RegexTupleManager` before the dunder-lookup guard was added). A direct test on the new attribute's own manager instance catches that where the conftest fixture, which never asserts on the copy, would not. As with the scalar-attribute pattern above, also check `AGENTS.md` itself for now-stale references when you touch this.
121+
Add a dedicated `copy.deepcopy()` round-trip test for it too (see `test_regexes_deepcopy_roundtrip`/`test_nickname_delimiters_deepcopy_roundtrip` in `tests/test_constants.py`), not just reliance on conftest's autouse snapshot/restore exercising it incidentally. `TupleManager`/`RegexTupleManager.__getattr__` answer *any* unknown attribute lookup — dunder probes like `__deepcopy__` raise `AttributeError` (never answered as config keys), everything else still returns `self.get(attr)`/`EMPTY_REGEX` — so a new manager subtype or a `__getattr__` tweak can silently break `copy.deepcopy` (this bit `RegexTupleManager` before the dunder-lookup guard was added). A direct test on the new attribute's own manager instance catches that where the conftest fixture, which never asserts on the copy, would not. As with the scalar-attribute pattern above, also check `AGENTS.md` itself for now-stale references when you touch this.
122+
123+
**Unknown-key attribute access on `TupleManager`/`RegexTupleManager` warns (1.4, #256)** — a key not currently in the dict emits `DeprecationWarning` naming the miss and the known keys (`_warn_unknown_key`), before falling back to the same `None`/`EMPTY_REGEX` default as before; `.get()` stays silent. Dunder probes (`__deepcopy__`) still raise `AttributeError` outright, and single-underscore probes (`_repr_html_`, IPython's `_ipython_canary_method_should_not_exist_`, etc.) are excluded from the warning too — no real config key starts with `_`, so both guards just keep protocol/introspection probes from misfiring as "typo" warnings. This means internal parser code that reads `self.C.regexes.<key>` unconditionally (e.g. `squash_bidi`'s `bidi`) now warns if a caller's custom `regexes` dict omits that key — a previously-silent partial-override pattern is on the same deprecation path as an actual typo.
120124

121125
**Adding a word to a config set** — first check the *other* sets for the same word (grep `nameparser/config/` or intersect the sets in a `python3 -c`). Real overlaps exist: `do`/`st`/`mc``PREFIXES``TITLES`/`SUFFIX_ACRONYMS`; `abd` = "ABD" ∈ `SUFFIX_ACRONYMS`; `abu``PREFIXES``bound_first_names` (position-dependent: leading token → first-name join, mid-name → last-name join). Usually position-dependent and harmless, but can force a guard or an exclusion (the `last_base` all-particles guard; dropping `abd` from `bound_first_names`).
122126

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ authors:
99
repository-code: "https://github.com/derek73/python-nameparser"
1010
url: "https://github.com/derek73/python-nameparser"
1111
license: LGPL-2.1-or-later
12-
version: "1.3.0"
12+
version: "1.4.0"

docs/release_log.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Release Log
22
===========
3-
* 1.4.0 - Unreleased
3+
* 1.4.0 - July 12, 2026
44

55
- Add ``Constants.copy()``, a detached deep copy that preserves the source instance's current customizations (unlike ``Constants()``, which always starts from library defaults) -- useful as ``CONSTANTS.copy()`` for a private snapshot of the shared config (#260)
66
- Deprecate passing ``constants=None`` to ``HumanName`` (or assigning ``hn.C = None``): it silently builds a fresh ``Constants()``, discarding any customizations the caller may have expected to carry over from the shared ``CONSTANTS``. Emits ``DeprecationWarning``; will raise ``TypeError`` in 2.0. Use ``constants=Constants()`` for fresh library defaults or ``constants=CONSTANTS.copy()`` for a private snapshot instead (closes #260)

nameparser/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
VERSION = (1, 3, 1)
1+
VERSION = (1, 4, 0)
22
__version__ = '.'.join(map(str, VERSION))

0 commit comments

Comments
 (0)