Skip to content

Commit dce8204

Browse files
derek73claude
andcommitted
Add 2.0 deprecation warnings for the 1.4 second-wave sweep (#263)
One themed PR, mirroring how the 1.3.0 sweep shipped (#253), so every decided 2.0 removal in the second wave warns ahead of time: - empty_attribute_default assignment (#255): once None support goes, the only legal value left is the default, so assignment now warns via a new descriptor; reads stay silent. - TupleManager/RegexTupleManager unknown-key attribute access (#256): a misspelled or omitted key currently degrades silently (None/ EMPTY_REGEX); now warns naming the miss and the known keys. Excludes single-underscore introspection probes (IPython's _repr_html_, etc.) alongside the existing dunder guard. - HumanName slice __getitem__ and all of __setitem__ (#258): field access by position has no real use case, and item assignment duplicates plain attribute assignment. - SetManager.add_with_encoding() (#245): the method itself now warns on every call regardless of argument type, naming add() as the replacement -- previously only the bytes-decode path warned. - Constants.__setstate__'s legacy-pickle migration shim (#279): warns once per call (not once per skipped key) when it loads a pre-1.3.0 blob, telling users to re-pickle. Same standards as #253: TDD throughout, suite verified noise-free including under -W error::DeprecationWarning, mypy/ruff clean, Sphinx doctest + html builds clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4aaedb8 commit dce8204

12 files changed

Lines changed: 374 additions & 43 deletions

docs/release_log.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Release Log
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)
7+
- Deprecate assigning ``Constants.empty_attribute_default`` for removal in 2.0 (#255): once ``None`` support goes, the only legal value left is the default ``''``, so a dial with one position isn't configuration. Emits ``DeprecationWarning``; reading the attribute is unaffected
8+
- Deprecate unknown-key attribute access on ``TupleManager``/``RegexTupleManager`` (``CONSTANTS.regexes.typo``, ``CONSTANTS.capitalization_exceptions.typo``, etc.) for removal in 2.0 (#256): a misspelled or omitted key currently degrades silently (``None``/``EMPTY_REGEX``) with no traceback pointing at the typo. Emits ``DeprecationWarning`` naming the miss and the known keys; will raise ``AttributeError`` in 2.0. ``.get()`` remains available for intentional soft access
9+
- Deprecate ``HumanName`` slice access (``name[1:-3]``) and item assignment (``name['first'] = value``) for removal in 2.0 (#258): field access by position has no real use case, and item assignment duplicates plain attribute assignment. Both emit ``DeprecationWarning``; string-key access (``name['first']``) is unaffected
10+
- Deprecate ``SetManager.add_with_encoding()`` itself for removal in 2.0 (#245), regardless of argument type: use ``add()`` instead (decoding bytes first). Previously only the ``bytes`` path warned; the ``str`` path was silent even though the whole method goes away
11+
- Deprecate loading a legacy-format ``Constants`` pickle (written by nameparser <= 1.2.x, before the 1.3.0 pickle fix) for removal in 2.0 (#279): ``__setstate__``'s migration shim currently skips the stale computed-property key silently. Emits ``DeprecationWarning`` once per call telling users to re-pickle; will raise ``ValueError`` in 2.0
712
- Fix the ``"Lastname, Firstname"`` comma format not being recognized when the input uses the Arabic comma ``،`` (U+060C, the standard comma in Arabic/Persian/Urdu text) or the fullwidth CJK comma ```` (U+FF0C) instead of the ASCII comma: both variants now also split the format and no longer leak into the parsed output (closes #265)
813

914
* 1.3.1 - July 11, 2026

docs/usage.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,17 @@ Requires Python 3.10+.
7373
5
7474
>>> list(name)
7575
['Dr.', 'Juan', 'Q. Xavier', 'de la Vega', 'III']
76-
>>> name[1:-3]
77-
['Juan', 'Q. Xavier', 'de la Vega']
76+
>>> name['first']
77+
'Juan'
7878

7979
``name == other`` and ``hash(name)`` are deprecated and will be removed in
8080
2.0; use ``matches()`` for comparison and ``comparison_key()`` for sets,
8181
dicts, and dedup (see `issue #223
82-
<https://github.com/derek73/python-nameparser/issues/223>`_).
82+
<https://github.com/derek73/python-nameparser/issues/223>`_). Slicing a name
83+
by position (``name[1:-3]``) and item assignment (``name['first'] = ...``)
84+
are likewise deprecated and will be removed in 2.0; use the named attributes
85+
instead (see `issue #258
86+
<https://github.com/derek73/python-nameparser/issues/258>`_).
8387

8488
Empty or unparsable input does not raise an error; it produces a name whose
8589
attributes are all empty. Check ``len(name) == 0`` (or ``str(name) == ''``)

nameparser/config/__init__.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
above; it will raise ``TypeError`` in 2.0 (issue #260).
4141
"""
4242
import copy
43+
import inspect
4344
import re
4445
import sys
4546
import warnings
@@ -256,7 +257,18 @@ def add_with_encoding(self, s: str | bytes, encoding: str | None = None) -> None
256257
.. deprecated:: 1.3.0
257258
``bytes`` arguments will raise ``TypeError`` in 2.0 (see issue
258259
#245); decode before adding.
260+
261+
.. deprecated:: 1.4.0
262+
The method itself is removed in 2.0 (see issue #245); use
263+
:py:func:`add` instead, decoding bytes first.
259264
"""
265+
warnings.warn(
266+
"SetManager.add_with_encoding() is deprecated and will be "
267+
"removed in 2.0; use add() instead (decode bytes first). See "
268+
"https://github.com/derek73/python-nameparser/issues/245",
269+
DeprecationWarning,
270+
stacklevel=2,
271+
)
260272
self._add_normalized(s, encoding, stacklevel=3)
261273

262274
def add(self, *strings: str) -> Self:
@@ -397,10 +409,28 @@ def __init__(
397409
arg = checked
398410
super().__init__(arg, **kwargs)
399411

412+
def _warn_unknown_key(self, attr: str) -> None:
413+
# Deprecated 1.4.0, raises AttributeError in 2.0 (#256): a misspelled
414+
# key otherwise degrades silently with no traceback pointing at the
415+
# typo.
416+
warnings.warn(
417+
f"{attr!r} is not a known key ({', '.join(sorted(self))}); "
418+
"unknown-key attribute access is deprecated and will raise "
419+
"AttributeError in 2.0. Use .get() for intentional soft access. "
420+
"See https://github.com/derek73/python-nameparser/issues/256",
421+
DeprecationWarning,
422+
stacklevel=3,
423+
)
424+
400425
def __getattr__(self, attr: str) -> T | None:
401426
# Otherwise the dict default (None) is mistaken for a real protocol hook.
402427
if _is_dunder(attr):
403428
raise AttributeError(attr)
429+
# Single-underscore introspection probes (IPython/Jupyter's
430+
# _repr_html_, _ipython_canary_method_should_not_exist_, etc.) are
431+
# never config keys either -- no real config key starts with '_'.
432+
if attr not in self and not attr.startswith('_'):
433+
self._warn_unknown_key(attr)
404434
return self.get(attr)
405435

406436
def __setattr__(self, attr: str, value: T) -> None:
@@ -444,6 +474,8 @@ def __getattr__(self, attr: str) -> re.Pattern[str]:
444474
# then tries to call the returned re.Pattern and raises TypeError.
445475
if _is_dunder(attr):
446476
raise AttributeError(attr)
477+
if attr not in self and not attr.startswith('_'):
478+
self._warn_unknown_key(attr)
447479
return self.get(attr, EMPTY_REGEX)
448480

449481

@@ -509,6 +541,38 @@ def __set__(self, obj: 'Constants', value: SetManager) -> None:
509541
setattr(obj, self._attr, value)
510542

511543

544+
class _EmptyAttributeDefaultAttribute:
545+
"""Descriptor backing ``Constants.empty_attribute_default``.
546+
547+
.. deprecated:: 1.4.0
548+
Assignment is deprecated (see issue #255): the only legal value
549+
left once ``None`` support goes in 2.0 is the default ``''``, so a
550+
dial with one position isn't configuration.
551+
"""
552+
553+
_attr = '_empty_attribute_default'
554+
555+
def __get__(self, obj: 'Constants | None', objtype: type | None = None) -> str:
556+
# Annotated `str`, not `str | None`, to match the pre-descriptor
557+
# plain-attribute inference: None is documented/supported (see the
558+
# class docstring), but typing it honestly cascades `| None`
559+
# through ~8 public str-typed properties (title, first, ... ).
560+
if obj is None:
561+
return ''
562+
return getattr(obj, self._attr, '')
563+
564+
def __set__(self, obj: 'Constants', value: str | None) -> None:
565+
warnings.warn(
566+
"Assigning Constants.empty_attribute_default is deprecated and "
567+
"will raise TypeError in 2.0; empty attributes will always "
568+
"return ''. See "
569+
"https://github.com/derek73/python-nameparser/issues/255",
570+
DeprecationWarning,
571+
stacklevel=2,
572+
)
573+
setattr(obj, self._attr, value)
574+
575+
512576
class Constants:
513577
"""
514578
An instance of this class hold all of the configuration constants for the parser.
@@ -615,20 +679,29 @@ class Constants:
615679
does not get mistaken for a suffix split.
616680
"""
617681

618-
empty_attribute_default = ''
682+
empty_attribute_default = _EmptyAttributeDefaultAttribute()
619683
"""
620684
Default return value for empty attributes.
621685
686+
.. deprecated:: 1.4.0
687+
Assignment emits ``DeprecationWarning``; the option is removed in
688+
2.0 (see issue #255) and empty attributes will always return ``''``.
689+
622690
.. doctest::
623691
692+
>>> import warnings
624693
>>> from nameparser.config import CONSTANTS
625-
>>> CONSTANTS.empty_attribute_default = None
694+
>>> with warnings.catch_warnings():
695+
... warnings.simplefilter('ignore', DeprecationWarning)
696+
... CONSTANTS.empty_attribute_default = None
626697
>>> name = HumanName("John Doe")
627698
>>> print(name.title)
628699
None
629700
>>> name.first
630701
'John'
631-
>>> CONSTANTS.empty_attribute_default = ''
702+
>>> with warnings.catch_warnings():
703+
... warnings.simplefilter('ignore', DeprecationWarning)
704+
... CONSTANTS.empty_attribute_default = ''
632705
633706
"""
634707

@@ -838,7 +911,11 @@ def __setstate__(self, state: Mapping[str, Any]) -> None:
838911
# which silently reverted every collection to its module default on
839912
# unpickling.
840913
self._pst = None
914+
legacy_format = False
841915
for name, value in state.items():
916+
# inspect.getattr_static, not getattr, so descriptors are
917+
# inspected directly rather than triggering their __get__.
918+
descriptor = inspect.getattr_static(type(self), name, None)
842919
# Migration shim: pickles written before this fix (1.3.0 and earlier,
843920
# including 1.2.1) used a dir() sweep for __getstate__, so their state
844921
# carries the read-only ``suffixes_prefixes_titles`` property. Skip any
@@ -847,9 +924,29 @@ def __setstate__(self, state: Mapping[str, Any]) -> None:
847924
# don't promise to read pre-fix blobs forever — this only smooths
848925
# migration for anyone persisting them, and can be dropped a release
849926
# or two after 1.3.0 once they've re-pickled.
850-
if isinstance(getattr(type(self), name, None), property):
927+
if isinstance(descriptor, property):
928+
legacy_format = True
929+
continue
930+
if isinstance(descriptor, _EmptyAttributeDefaultAttribute):
931+
# Bypass the descriptor's setter: restoring saved state isn't
932+
# a user assignment, so it shouldn't emit #255's deprecation
933+
# warning on every unpickle/copy() of a customized instance.
934+
setattr(self, descriptor._attr, value)
851935
continue
852936
setattr(self, name, value)
937+
if legacy_format:
938+
# Once per __setstate__ call, not once per skipped key (see
939+
# issue #279): the 2.0 removal turns this into a ValueError
940+
# naming the same fix.
941+
warnings.warn(
942+
"Loading a legacy-format Constants pickle (written by "
943+
"nameparser <= 1.2.x, before the 1.3.0 pickle fix) is "
944+
"deprecated and will raise ValueError in 2.0; re-pickle "
945+
"under 1.3/1.4 to migrate. See "
946+
"https://github.com/derek73/python-nameparser/issues/279",
947+
DeprecationWarning,
948+
stacklevel=2,
949+
)
853950
# Verify each descriptor-backed attr was restored. Without this, a missing
854951
# key surfaces later as AttributeError: 'Constants' object has no attribute
855952
# '_prefixes' — the private mangled name, not the public one, making it
@@ -874,7 +971,8 @@ def __getstate__(self) -> Mapping[str, Any]:
874971
for name, val in self.__dict__.items():
875972
if name.startswith('_'):
876973
public = name[1:]
877-
if isinstance(getattr(type(self), public, None), _SetManagerAttribute):
974+
descriptor = inspect.getattr_static(type(self), public, None)
975+
if isinstance(descriptor, (_SetManagerAttribute, _EmptyAttributeDefaultAttribute)):
878976
state[public] = val
879977
else:
880978
state[name] = val

nameparser/parser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,38 @@ def __getitem__(self, key: slice) -> list[str]: ...
254254
@overload
255255
def __getitem__(self, key: str) -> str: ...
256256
def __getitem__(self, key: slice | str) -> str | list[str]:
257+
"""
258+
.. deprecated:: 1.4.0
259+
Slice access (``name[1:-3]``) is removed in 2.0 (see issue
260+
#258); field access by position has no real use case.
261+
String-key access (``name['first']``) is unaffected.
262+
"""
257263
if isinstance(key, slice):
264+
warnings.warn(
265+
"Slicing a HumanName by position is deprecated and will be "
266+
"removed in 2.0; access the named attributes instead. See "
267+
"https://github.com/derek73/python-nameparser/issues/258",
268+
DeprecationWarning,
269+
stacklevel=2,
270+
)
258271
return [getattr(self, x) for x in self._members[key]]
259272
else:
260273
return getattr(self, key)
261274

262275
def __setitem__(self, key: str, value: str | list[str] | None) -> None:
276+
"""
277+
.. deprecated:: 1.4.0
278+
Removed in 2.0 (see issue #258); it duplicates plain attribute
279+
assignment. Use ``name.first = value`` instead.
280+
"""
281+
warnings.warn(
282+
"HumanName item assignment is deprecated and will be removed "
283+
"in 2.0; it duplicates plain attribute assignment, use "
284+
"name.first = value instead. See "
285+
"https://github.com/derek73/python-nameparser/issues/258",
286+
DeprecationWarning,
287+
stacklevel=2,
288+
)
263289
if key in self._members:
264290
self._set_list(key, value)
265291
else:

tests/conftest.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import warnings
23
from collections.abc import Iterator
34

45
import pytest
@@ -60,9 +61,19 @@ def empty_attribute_default(request: pytest.FixtureRequest) -> Iterator[str | No
6061
attr: copy.deepcopy(getattr(CONSTANTS, attr))
6162
for attr in _COLLECTION_CONFIG_ATTRS
6263
}
63-
CONSTANTS.empty_attribute_default = request.param
64+
# empty_attribute_default assignment is deprecated (#255); this fixture's
65+
# own systematic exercise of both settings across the whole suite is
66+
# infrastructure, not a test of the deprecation itself, so it's silenced
67+
# here (narrowly, around just the assignment) rather than at every one of
68+
# its ~1500 call sites. Must not wrap `yield` -- that would suppress
69+
# DeprecationWarning for the test body itself, hiding real ones.
70+
with warnings.catch_warnings():
71+
warnings.simplefilter("ignore", DeprecationWarning)
72+
CONSTANTS.empty_attribute_default = request.param
6473
yield request.param
65-
for attr, value in scalar_snapshot.items():
66-
setattr(CONSTANTS, attr, value)
74+
with warnings.catch_warnings():
75+
warnings.simplefilter("ignore", DeprecationWarning)
76+
for attr, value in scalar_snapshot.items():
77+
setattr(CONSTANTS, attr, value)
6778
for attr, value in collection_snapshot.items():
6879
setattr(CONSTANTS, attr, value)

tests/test_comma_variants.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
from nameparser import HumanName
24
from nameparser.config import Constants
35
from nameparser.config.regexes import REGEXES
@@ -40,8 +42,12 @@ def test_custom_regexes_without_commas_key_does_not_shatter_name(self) -> None:
4042
# other no-comma input (word tokenizing drops the punctuation),
4143
# yielding a plain first/last pair -- not the inverted "Last, First"
4244
# reading, and definitely not single-character pieces.
45+
# Unknown-key access (here, the deliberately-omitted 'commas') is
46+
# deprecated for removal in 2.0 (#256); this fallback pattern will
47+
# AttributeError-crash the parser instead of degrading silently.
4348
custom = {k: v for k, v in REGEXES.items() if k != 'commas'}
4449
c = Constants(regexes=custom)
45-
hn = HumanName("Smith, John", constants=c)
50+
with pytest.deprecated_call():
51+
hn = HumanName("Smith, John", constants=c)
4652
self.m(hn.first, "Smith", hn)
4753
self.m(hn.last, "John", hn)

0 commit comments

Comments
 (0)