Skip to content

Commit 18c9b6a

Browse files
authored
Merge pull request #282 from derek73/feature/260-constants-copy-none-deprecation
Add Constants.copy(); deprecate constants=None
2 parents 59fdabb + a0933bc commit 18c9b6a

9 files changed

Lines changed: 209 additions & 48 deletions

File tree

docs/customize.rst

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ To recognize an *additional* delimiter, add a compiled pattern to
7979

8080
>>> import re
8181
>>> from nameparser import HumanName
82-
>>> hn = HumanName("Benjamin {Ben} Franklin", constants=None)
82+
>>> from nameparser.config import Constants
83+
>>> hn = HumanName("Benjamin {Ben} Franklin", constants=Constants())
8384
>>> hn.nickname
8485
''
8586
>>> hn.C.nickname_delimiters['curly_braces'] = re.compile(r'\{(.*?)\}', re.U)
@@ -455,23 +456,37 @@ e.g. parsing names on multiple threads.
455456
]>
456457

457458

458-
If you'd prefer new instances to have their own config values, one shortcut is to pass
459-
``None`` as the second argument (or ``constants`` keyword argument) when
460-
instantiating ``HumanName``. Each instance always has a ``C`` attribute, but if
461-
you didn't pass ``None`` (or your own :py:class:`~nameparser.config.Constants`
462-
instance) to the ``constants`` argument then it's a reference to the
463-
module-level config values with the behavior described above.
459+
If you'd prefer new instances to have their own config values, pass your own
460+
:py:class:`~nameparser.config.Constants` instance as the ``constants``
461+
argument when instantiating ``HumanName``. There are three spellings,
462+
depending on which config you want the new instance to start from:
463+
464+
.. code-block::
465+
466+
HumanName(name) # shared CONSTANTS (unchanged)
467+
HumanName(name, constants=Constants()) # private, fresh library defaults
468+
HumanName(name, constants=CONSTANTS.copy()) # private, snapshot of the current shared config
469+
470+
The middle and last forms both give the instance an independent config that
471+
further changes to ``CONSTANTS`` won't reach, but they answer different
472+
questions: ``Constants()`` ignores any customization already made to
473+
``CONSTANTS`` and starts clean, while ``CONSTANTS.copy()`` carries those
474+
customizations over into the private copy. Each instance always has a ``C``
475+
attribute, but if you didn't pass one of the private forms to the
476+
``constants`` argument then it's a reference to the module-level config
477+
values with the behavior described above.
464478

465479
.. doctest:: module config
466480
:options: +ELLIPSIS, +NORMALIZE_WHITESPACE
467481

468482
>>> from nameparser import HumanName
483+
>>> from nameparser.config import Constants
469484
>>> instance = HumanName("Dean Robert Johns")
470485
>>> instance.has_own_config
471486
False
472487
>>> instance.C.titles.add('dean')
473488
SetManager({'10th', ..., 'zoologist'})
474-
>>> other_instance = HumanName("Dean Robert Johns", None) # <-- pass None for per-instance config
489+
>>> other_instance = HumanName("Dean Robert Johns", Constants()) # <-- fresh, private config
475490
>>> other_instance
476491
<HumanName : [
477492
title: ''
@@ -485,6 +500,14 @@ module-level config values with the behavior described above.
485500
>>> other_instance.has_own_config
486501
True
487502

503+
.. deprecated:: 1.4.0
504+
Passing ``None`` as the ``constants`` argument also builds a fresh
505+
``Constants()``, but is deprecated: ``None`` conventionally means "use
506+
the default," which here is the *shared* ``CONSTANTS`` -- the opposite of
507+
what passing ``None`` actually does. It emits a ``DeprecationWarning``
508+
and will raise ``TypeError`` in 2.0 (issue #260); use one of the two
509+
explicit forms above instead.
510+
488511
Don't Remove Emojis
489512
~~~~~~~~~~~~~~~~~~~
490513

docs/release_log.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ Release Log
22
===========
33
* 1.4.0 - Unreleased
44

5+
- 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)
6+
- 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)
57
- 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)
68

79
* 1.3.1 - July 11, 2026

nameparser/config/__init__.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,29 @@
1212
>>> CONSTANTS.titles.remove('hon').add('chemistry','dean') # doctest: +SKIP
1313
1414
You can also adjust the configuration of individual instances by passing
15-
``None`` as the second argument upon instantiation.
15+
your own :py:class:`Constants` instance as the second argument upon
16+
instantiation -- ``Constants()`` for fresh library defaults, or
17+
``CONSTANTS.copy()`` for a private snapshot of the current module config.
1618
1719
::
1820
1921
>>> from nameparser import HumanName
20-
>>> hn = HumanName("Dean Robert Johns", None)
22+
>>> from nameparser.config import Constants
23+
>>> hn = HumanName("Dean Robert Johns", Constants())
2124
>>> hn.C.titles.add('dean') # doctest: +SKIP
2225
>>> hn.parse_full_name() # need to run this again after config changes
2326
24-
**Potential Gotcha**: If you do not pass ``None`` (or your own
25-
:py:class:`Constants` instance) as the second argument, ``hn.C`` will be a
26-
reference to the module config, possibly yielding unexpected results. See
27-
`Customizing the Parser <customize.html>`_.
27+
**Potential Gotcha**: If you do not pass your own :py:class:`Constants`
28+
instance as the second argument, ``hn.C`` will be a reference to the module
29+
config, possibly yielding unexpected results. See `Customizing the Parser
30+
<customize.html>`_.
31+
32+
.. deprecated:: 1.4.0
33+
Passing ``None`` as the second argument also builds a fresh
34+
``Constants()``, but is deprecated in favor of the explicit spellings
35+
above; it will raise ``TypeError`` in 2.0 (issue #260).
2836
"""
37+
import copy
2938
import re
3039
import sys
3140
import warnings
@@ -327,7 +336,7 @@ def _is_dunder(attr: str) -> bool:
327336
# validate and normalize each one exactly once at import. Constants()
328337
# copies these via _normalized_elements' SetManager fast path instead of
329338
# re-checking ~1,400 elements per construction — a cost that otherwise
330-
# repeats on the per-instance-config path, HumanName(constants=None).
339+
# repeats on the per-instance-config path, HumanName(constants=Constants()).
331340
#
332341
# This snapshot is taken once, at import time: mutating a raw constant
333342
# (e.g. `TITLES.add('x')`) after import is *not* picked up by Constants()
@@ -806,6 +815,18 @@ def __repr__(self) -> str:
806815
]
807816
return "<Constants : [\n" + "\n".join(lines) + "\n]>"
808817

818+
def copy(self) -> 'Constants':
819+
"""
820+
Return a detached deep copy of this ``Constants`` instance, preserving
821+
its current customizations -- unlike :py:class:`Constants`'s own
822+
constructor, which always starts from library defaults. Useful for
823+
snapshotting the shared module-level ``CONSTANTS`` (including
824+
whatever it's been customized with) into a private instance, e.g.
825+
``CONSTANTS.copy()``. Relies on the same ``__getstate__``/``__setstate__``
826+
pair pickling uses, so it's as cheap and correct as pickle round-tripping.
827+
"""
828+
return copy.deepcopy(self)
829+
809830
def __setstate__(self, state: Mapping[str, Any]) -> None:
810831
# Restore each saved attribute directly. The previous implementation
811832
# passed the whole state dict to __init__ as the ``prefixes`` argument,

nameparser/parser.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ class HumanName:
5151
:param str full_name: The name string to be parsed.
5252
:param constants:
5353
a :py:class:`~nameparser.config.Constants` instance (subclasses are
54-
honored). Pass ``None`` for `per-instance config <customize.html>`_.
55-
Anything else raises ``TypeError``.
54+
honored). Defaults to the shared module-level ``CONSTANTS``. For
55+
`per-instance config <customize.html>`_, pass ``Constants()`` for
56+
fresh library defaults, or ``CONSTANTS.copy()`` for a private
57+
snapshot of the current shared config. Passing ``None`` also builds
58+
a fresh ``Constants()``, but is deprecated (warns; raises
59+
``TypeError`` in 2.0, see issue #260) since it silently discards any
60+
customizations the caller may have expected to carry over. Anything
61+
else raises ``TypeError``.
5662
:param str encoding: string representing the encoding of your input
5763
(deprecated with ``bytes`` input, removal in 2.0 — decode before
5864
passing; see issue #245)
@@ -106,7 +112,10 @@ def __init__(
106112
nickname: str | list[str] | None = None,
107113
maiden: str | list[str] | None = None,
108114
) -> None:
109-
self.C = constants
115+
# calls _validate_constants directly (not through the C setter) so
116+
# the deprecation warning below attributes to this constructor's
117+
# caller rather than to the setter, mirroring _apply_full_name below
118+
self._C = self._validate_constants(constants, stacklevel=3)
110119

111120
# Lookup entries derived while parsing this instance (period-joined
112121
# titles/suffixes like "Lt.Gov.", conjunction-joined pieces like
@@ -143,11 +152,27 @@ def __init__(
143152
self._apply_full_name(full_name, stacklevel=3)
144153

145154
@staticmethod
146-
def _validate_constants(constants: 'Constants | None') -> 'Constants':
155+
def _validate_constants(constants: 'Constants | None', *, stacklevel: int) -> 'Constants':
147156
# Shared by the constructor and the C setter so both assignment paths
148157
# give the same immediate TypeError instead of one bypassing the
149158
# other and failing far from the cause (#239).
150159
if constants is None:
160+
# deprecated 1.4.0, raises TypeError in 2.0 (#260, removal #261):
161+
# None means "build a fresh private Constants()", the opposite of
162+
# what None conventionally means (the default is the *shared*
163+
# CONSTANTS) -- an easy trap since customizing CONSTANTS then
164+
# passing None elsewhere silently drops those customizations with
165+
# no error. CONSTANTS.copy() is the explicit spelling for the
166+
# other reading: a private snapshot of the current shared config.
167+
warnings.warn(
168+
"Passing constants=None is deprecated and will raise "
169+
"TypeError in 2.0; use constants=Constants() for fresh "
170+
"library defaults, or constants=CONSTANTS.copy() to snapshot "
171+
"the current shared config. See "
172+
"https://github.com/derek73/python-nameparser/issues/260",
173+
DeprecationWarning,
174+
stacklevel=stacklevel,
175+
)
151176
return Constants()
152177
if not isinstance(constants, Constants):
153178
# passing the class itself is the likeliest mistake, and
@@ -169,14 +194,16 @@ def C(self) -> 'Constants':
169194
<customize.html>`_.
170195
171196
Assigning a non-``Constants`` value (besides ``None``, which builds a
172-
fresh private ``Constants()``) raises the same ``TypeError`` as passing
173-
an invalid ``constants`` argument to the constructor (#239).
197+
fresh private ``Constants()`` and emits a ``DeprecationWarning`` --
198+
see :py:meth:`~nameparser.parser.HumanName.__init__`) raises the same
199+
``TypeError`` as passing an invalid ``constants`` argument to the
200+
constructor (#239).
174201
"""
175202
return self._C
176203

177204
@C.setter
178205
def C(self, constants: 'Constants | None') -> None:
179-
self._C = self._validate_constants(constants)
206+
self._C = self._validate_constants(constants, stacklevel=3)
180207

181208
def __getstate__(self) -> dict:
182209
state = self.__dict__.copy()

0 commit comments

Comments
 (0)