Skip to content

Commit 4eaeee9

Browse files
derek73claude
andcommitted
Add 2.0 deprecation warnings for bytes input and SetManager legacy surface
1.3.0 is the bridge release, so every decided 2.0 removal warns in it: - bytes to HumanName/full_name or SetManager.add()/add_with_encoding() warns with a decode-first hint (#245); the encoding kwarg is deprecated with it - SetManager.__call__ warns: it returns the raw underlying set, and mutating that bypasses normalization and cache invalidation (#243) - SetManager.remove() of a missing member warns (raises KeyError in 2.0, matching set.remove); new discard() is the intentional ignore-missing spelling, wired to the same cache invalidation (#243) The option-A-only #243 removals (operators, the Set ABC) deliberately do not warn — that decision is still open for feedback on the issue. Present-member remove(), str input, and everything else stay silent; suite is warning-noise-free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 74bae19 commit 4eaeee9

6 files changed

Lines changed: 146 additions & 4 deletions

File tree

docs/customize.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Editable attributes of nameparser.config.CONSTANTS
5353
* :py:data:`~nameparser.config.CAPITALIZATION_EXCEPTIONS` - Dictionary of pieces that do not capitalize the first letter, e.g. "Ph.D".
5454
* :py:data:`~nameparser.config.REGEXES` - Regular expressions used to find words, initials, nicknames, etc.
5555

56-
Each set-valued constant comes with :py:func:`~nameparser.config.SetManager.add` and :py:func:`~nameparser.config.SetManager.remove` methods for tuning
56+
Each set-valued constant comes with :py:func:`~nameparser.config.SetManager.add`, :py:func:`~nameparser.config.SetManager.remove`, and :py:func:`~nameparser.config.SetManager.discard` methods for tuning
5757
the constants for your project. These methods automatically lower case and
5858
remove punctuation to normalize them for comparison. The two dict-valued
5959
constants (``CAPITALIZATION_EXCEPTIONS`` and ``REGEXES``) are edited with

docs/release_log.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Release Log
55
**Breaking Changes & Deprecations**
66

77
- Deprecate ``HumanName.__eq__`` and ``__hash__`` for removal in 2.0 (#223): the current design's three promises — case-insensitive equality, equality with plain strings, and hashability — are mutually inconsistent (equal objects can hash differently), equality depends on ``string_format``, and ``maiden`` is invisible to it. Both now emit ``DeprecationWarning`` naming the replacement; behavior is otherwise unchanged until 2.0 (closes #224)
8+
- Deprecate ``bytes`` input for removal in 2.0 (#245): passing ``bytes`` to ``HumanName``/``full_name`` or to ``SetManager.add()``/``add_with_encoding()`` now emits ``DeprecationWarning`` — decode first, e.g. ``value.decode('utf-8')``. The ``encoding`` constructor argument is deprecated with it
9+
- Deprecate ``SetManager.__call__`` for removal in 2.0 (#243): calling a manager returns the raw underlying set, so mutating the result bypasses normalization and cache invalidation; iterate the manager or copy with ``set(manager)`` instead
10+
- Add ``SetManager.discard()``, and deprecate ``remove()`` of a *missing* member (#243): it currently does nothing but will raise ``KeyError`` in 2.0, matching ``set.remove``; use ``discard()`` for intentional ignore-missing removal. Removing present members is unchanged and does not warn
811
- Fix ``HumanName`` acting as its own iterator with a stored cursor: breaking out of a loop, iterating in nested loops, or calling ``len(name)`` mid-loop corrupted subsequent iteration; ``iter(name)`` now returns a fresh independent iterator each time. ``next(name)`` on the instance itself (undocumented) now raises ``TypeError`` — call ``next(iter(name))`` instead (closes #225)
912
- Remove the vestigial ``unparsable`` attribute: the guard that was meant to set it has been unreachable since 2013 (v0.2.9), so it has reported ``False`` for every parsed name for over a decade; check ``len(name) == 0`` to detect an empty parse
1013
- Remove ``__ne__``; Python 3 derives ``!=`` from ``__eq__`` automatically

nameparser/config/__init__.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"""
2929
import re
3030
import sys
31+
import warnings
3132
from collections.abc import Callable, Iterable, Iterator, Mapping, Set
3233
from typing import Any, TypeVar, overload
3334

@@ -137,6 +138,20 @@ def __init__(self, elements: Iterable[str]) -> None:
137138
self._on_change = None
138139

139140
def __call__(self) -> Set[str]:
141+
"""
142+
.. deprecated:: 1.3.0
143+
Removed in 2.0 (see issue #243). Returns the raw underlying set,
144+
so mutating it bypasses normalization and cache invalidation;
145+
iterate the manager or copy with ``set(manager)`` instead.
146+
"""
147+
warnings.warn(
148+
"Calling a SetManager to get the raw underlying set is "
149+
"deprecated and will be removed in 2.0; iterate the manager or "
150+
"copy it with set(manager) instead. See "
151+
"https://github.com/derek73/python-nameparser/issues/243",
152+
DeprecationWarning,
153+
stacklevel=2,
154+
)
140155
return self.elements
141156

142157
def __repr__(self) -> str:
@@ -198,12 +213,24 @@ def add_with_encoding(self, s: str | bytes, encoding: str | None = None) -> None
198213
Add the lowercased, leading/trailing-periods-stripped version of the string to the set. Pass an
199214
explicit `encoding` parameter to specify the encoding of binary strings that
200215
are not DEFAULT_ENCODING (UTF-8).
216+
217+
.. deprecated:: 1.3.0
218+
``bytes`` arguments will raise ``TypeError`` in 2.0 (see issue
219+
#245); decode before adding.
201220
"""
202221
stdin_encoding = None
203222
if sys.stdin:
204223
stdin_encoding = sys.stdin.encoding
205224
encoding = encoding or stdin_encoding or DEFAULT_ENCODING
206225
if isinstance(s, bytes):
226+
warnings.warn(
227+
"Passing bytes to SetManager.add()/add_with_encoding() is "
228+
"deprecated and will raise TypeError in 2.0; decode it "
229+
"first, e.g. value.decode('utf-8'). See "
230+
"https://github.com/derek73/python-nameparser/issues/245",
231+
DeprecationWarning,
232+
stacklevel=2,
233+
)
207234
s = s.decode(encoding)
208235
normalized = lc(s)
209236
if normalized not in self.elements:
@@ -225,6 +252,35 @@ def remove(self, *strings: str) -> Self:
225252
"""
226253
Remove the lower case and no-period version of the string arguments from the set.
227254
Returns ``self`` for chaining.
255+
256+
.. deprecated:: 1.3.0
257+
Removing a *missing* member currently does nothing but will
258+
raise ``KeyError`` in 2.0, matching ``set.remove`` (see issue
259+
#243); use :py:func:`discard` to ignore missing members.
260+
"""
261+
changed = False
262+
for s in strings:
263+
if (lower := lc(s)) in self.elements:
264+
self.elements.remove(lower)
265+
changed = True
266+
else:
267+
warnings.warn(
268+
"SetManager.remove() of a missing member currently does "
269+
"nothing, but will raise KeyError in 2.0; use discard() "
270+
"to ignore missing members. See "
271+
"https://github.com/derek73/python-nameparser/issues/243",
272+
DeprecationWarning,
273+
stacklevel=2,
274+
)
275+
if changed and self._on_change:
276+
self._on_change()
277+
return self
278+
279+
def discard(self, *strings: str) -> Self:
280+
"""
281+
Remove the lower case and no-period version of the string arguments
282+
from the set if present; missing members are ignored, like
283+
``set.discard``. Returns ``self`` for chaining.
228284
"""
229285
changed = False
230286
for s in strings:

nameparser/parser.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class HumanName:
5454
honored). Pass ``None`` for `per-instance config <customize.html>`_.
5555
Anything else raises ``TypeError``.
5656
:param str encoding: string representing the encoding of your input
57+
(deprecated with ``bytes`` input, removal in 2.0 — decode before
58+
passing; see issue #245)
5759
:param str string_format: python string formatting
5860
:param str initials_format: python initials string formatting
5961
:param str initials_delimter: string delimiter for initials
@@ -889,6 +891,15 @@ def full_name(self, value: str | bytes) -> None:
889891
self.original = value
890892

891893
if isinstance(value, bytes):
894+
# deprecated 1.3.0, raises TypeError in 2.0 (#245)
895+
warnings.warn(
896+
"Passing bytes to HumanName is deprecated and will raise "
897+
"TypeError in 2.0; decode it first, e.g. "
898+
"value.decode('utf-8'). See "
899+
"https://github.com/derek73/python-nameparser/issues/245",
900+
DeprecationWarning,
901+
stacklevel=2,
902+
)
892903
self._full_name = value.decode(self.encoding)
893904
else:
894905
self._full_name = value

tests/test_constants.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pickle
33
import re
44
import timeit
5+
import warnings
56
from typing import Any
67

78
import pytest
@@ -332,10 +333,61 @@ def test_none_empty_attribute_string_formatting(self) -> None:
332333
self.assertEqual('', str(hn), hn)
333334

334335
def test_add_constant_with_explicit_encoding(self) -> None:
336+
# bytes input is deprecated (#245), still supported until 2.0
335337
c = Constants()
336-
c.titles.add_with_encoding(b'b\351ck', encoding='latin_1')
338+
with pytest.deprecated_call():
339+
c.titles.add_with_encoding(b'b\351ck', encoding='latin_1')
337340
self.assertIn('béck', c.titles)
338341

342+
def test_set_manager_add_bytes_emits_deprecation_warning(self) -> None:
343+
# bytes elements are removed in 2.0 (#245); the caller should decode
344+
sm = SetManager(['dr'])
345+
with pytest.deprecated_call(match="decode"):
346+
sm.add(b'esq') # type: ignore[arg-type] # deliberately deprecated input
347+
self.assertIn('esq', sm)
348+
349+
def test_set_manager_add_str_does_not_warn(self) -> None:
350+
sm = SetManager(['dr'])
351+
with warnings.catch_warnings():
352+
warnings.simplefilter("error")
353+
sm.add('esq')
354+
self.assertIn('esq', sm)
355+
356+
def test_set_manager_call_emits_deprecation_warning(self) -> None:
357+
# __call__ hands out the raw underlying set, bypassing normalization
358+
# and cache invalidation; removed in 2.0 (#243)
359+
sm = SetManager(['dr'])
360+
with pytest.deprecated_call(match="set"):
361+
elements = sm()
362+
self.assertEqual(elements, {'dr'})
363+
364+
def test_set_manager_discard_ignores_missing_without_warning(self) -> None:
365+
sm = SetManager(['dr', 'mr'])
366+
with warnings.catch_warnings():
367+
warnings.simplefilter("error")
368+
result = sm.discard('nope').discard('Dr.') # normalizes like remove()
369+
self.assertIs(result, sm)
370+
self.assertEqual(set(sm), {'mr'})
371+
372+
def test_set_manager_discard_invalidates_cached_union(self) -> None:
373+
c = Constants()
374+
self.assertIn('hon', c.suffixes_prefixes_titles) # prime the cache
375+
c.titles.discard('hon')
376+
self.assertNotIn('hon', c.suffixes_prefixes_titles)
377+
378+
def test_set_manager_remove_missing_member_emits_deprecation_warning(self) -> None:
379+
# ignore-missing remove() becomes KeyError in 2.0 (#243); discard()
380+
# is the intentional ignore-missing spelling
381+
sm = SetManager(['dr'])
382+
with pytest.deprecated_call(match="discard"):
383+
sm.remove('nope')
384+
self.assertEqual(set(sm), {'dr'})
385+
# removing a present member stays silent
386+
with warnings.catch_warnings():
387+
warnings.simplefilter("error")
388+
sm.remove('dr')
389+
self.assertEqual(len(sm), 0)
390+
339391
def test_pickle_roundtrip_preserves_customizations(self) -> None:
340392
"""A pickled Constants must restore its customized collections.
341393
@@ -622,7 +674,8 @@ def test_suffixes_prefixes_titles_reflects_add_with_encoding(self) -> None:
622674
"""add_with_encoding must invalidate the cache like add()/remove() do."""
623675
c = Constants()
624676
_ = c.suffixes_prefixes_titles # prime the cache
625-
c.titles.add_with_encoding(b'b\351ck', encoding='latin_1')
677+
with pytest.deprecated_call(): # bytes input deprecated (#245)
678+
c.titles.add_with_encoding(b'b\351ck', encoding='latin_1')
626679
self.assertIn('béck', c.suffixes_prefixes_titles)
627680

628681
def test_suffixes_prefixes_titles_reflects_replaced_manager(self) -> None:

tests/test_python_api.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,29 @@ def test_string_output(self) -> None:
2828
self.m(str(hn), "Jüan de la Véña", hn)
2929

3030
def test_escaped_utf8_bytes(self) -> None:
31-
hn = HumanName(b'B\xc3\xb6ck, Gerald')
31+
# bytes input is deprecated (#245), still supported until 2.0
32+
with pytest.deprecated_call():
33+
hn = HumanName(b'B\xc3\xb6ck, Gerald')
3234
self.m(hn.first, "Gerald", hn)
3335
self.m(hn.last, "Böck", hn)
3436

37+
def test_bytes_full_name_emits_deprecation_warning(self) -> None:
38+
# bytes input is removed in 2.0 (#245); the caller should decode
39+
with pytest.deprecated_call(match="decode"):
40+
hn = HumanName(b'John Smith')
41+
self.m(hn.first, "John", hn)
42+
hn2 = HumanName("Jane Doe")
43+
with pytest.deprecated_call(match="decode"):
44+
hn2.full_name = b'John Smith'
45+
self.m(hn2.first, "John", hn2)
46+
47+
def test_str_full_name_does_not_warn(self) -> None:
48+
with warnings.catch_warnings():
49+
warnings.simplefilter("error")
50+
hn = HumanName("John Smith")
51+
hn.full_name = "Jane Doe"
52+
self.m(hn.first, "Jane", hn)
53+
3554
def test_len(self) -> None:
3655
hn = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC")
3756
self.m(len(hn), 5, hn)

0 commit comments

Comments
 (0)