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
39 changes: 31 additions & 8 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ To recognize an *additional* delimiter, add a compiled pattern to

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


If you'd prefer new instances to have their own config values, one shortcut is to pass
``None`` as the second argument (or ``constants`` keyword argument) when
instantiating ``HumanName``. Each instance always has a ``C`` attribute, but if
you didn't pass ``None`` (or your own :py:class:`~nameparser.config.Constants`
instance) to the ``constants`` argument then it's a reference to the
module-level config values with the behavior described above.
If you'd prefer new instances to have their own config values, pass your own
:py:class:`~nameparser.config.Constants` instance as the ``constants``
argument when instantiating ``HumanName``. There are three spellings,
depending on which config you want the new instance to start from:

.. code-block::

HumanName(name) # shared CONSTANTS (unchanged)
HumanName(name, constants=Constants()) # private, fresh library defaults
HumanName(name, constants=CONSTANTS.copy()) # private, snapshot of the current shared config

The middle and last forms both give the instance an independent config that
further changes to ``CONSTANTS`` won't reach, but they answer different
questions: ``Constants()`` ignores any customization already made to
``CONSTANTS`` and starts clean, while ``CONSTANTS.copy()`` carries those
customizations over into the private copy. Each instance always has a ``C``
attribute, but if you didn't pass one of the private forms to the
``constants`` argument then it's a reference to the module-level config
values with the behavior described above.

.. doctest:: module config
:options: +ELLIPSIS, +NORMALIZE_WHITESPACE

>>> from nameparser import HumanName
>>> from nameparser.config import Constants
>>> instance = HumanName("Dean Robert Johns")
>>> instance.has_own_config
False
>>> instance.C.titles.add('dean')
SetManager({'10th', ..., 'zoologist'})
>>> other_instance = HumanName("Dean Robert Johns", None) # <-- pass None for per-instance config
>>> other_instance = HumanName("Dean Robert Johns", Constants()) # <-- fresh, private config
>>> other_instance
<HumanName : [
title: ''
Expand All @@ -485,6 +500,14 @@ module-level config values with the behavior described above.
>>> other_instance.has_own_config
True

.. deprecated:: 1.4.0
Passing ``None`` as the ``constants`` argument also builds a fresh
``Constants()``, but is deprecated: ``None`` conventionally means "use
the default," which here is the *shared* ``CONSTANTS`` -- the opposite of
what passing ``None`` actually does. It emits a ``DeprecationWarning``
and will raise ``TypeError`` in 2.0 (issue #260); use one of the two
explicit forms above instead.

Don't Remove Emojis
~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ Release Log
===========
* 1.4.0 - Unreleased

- 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)
- 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)
- 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)

* 1.3.1 - July 11, 2026
Expand Down
35 changes: 28 additions & 7 deletions nameparser/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,29 @@
>>> CONSTANTS.titles.remove('hon').add('chemistry','dean') # doctest: +SKIP

You can also adjust the configuration of individual instances by passing
``None`` as the second argument upon instantiation.
your own :py:class:`Constants` instance as the second argument upon
instantiation -- ``Constants()`` for fresh library defaults, or
``CONSTANTS.copy()`` for a private snapshot of the current module config.

::

>>> from nameparser import HumanName
>>> hn = HumanName("Dean Robert Johns", None)
>>> from nameparser.config import Constants
>>> hn = HumanName("Dean Robert Johns", Constants())
>>> hn.C.titles.add('dean') # doctest: +SKIP
>>> hn.parse_full_name() # need to run this again after config changes

**Potential Gotcha**: If you do not pass ``None`` (or your own
:py:class:`Constants` instance) as the second argument, ``hn.C`` will be a
reference to the module config, possibly yielding unexpected results. See
`Customizing the Parser <customize.html>`_.
**Potential Gotcha**: If you do not pass your own :py:class:`Constants`
instance as the second argument, ``hn.C`` will be a reference to the module
config, possibly yielding unexpected results. See `Customizing the Parser
<customize.html>`_.

.. deprecated:: 1.4.0
Passing ``None`` as the second argument also builds a fresh
``Constants()``, but is deprecated in favor of the explicit spellings
above; it will raise ``TypeError`` in 2.0 (issue #260).
"""
import copy
import re
import sys
import warnings
Expand Down Expand Up @@ -327,7 +336,7 @@ def _is_dunder(attr: str) -> bool:
# validate and normalize each one exactly once at import. Constants()
# copies these via _normalized_elements' SetManager fast path instead of
# re-checking ~1,400 elements per construction — a cost that otherwise
# repeats on the per-instance-config path, HumanName(constants=None).
# repeats on the per-instance-config path, HumanName(constants=Constants()).
#
# This snapshot is taken once, at import time: mutating a raw constant
# (e.g. `TITLES.add('x')`) after import is *not* picked up by Constants()
Expand Down Expand Up @@ -806,6 +815,18 @@ def __repr__(self) -> str:
]
return "<Constants : [\n" + "\n".join(lines) + "\n]>"

def copy(self) -> 'Constants':
"""
Return a detached deep copy of this ``Constants`` instance, preserving
its current customizations -- unlike :py:class:`Constants`'s own
constructor, which always starts from library defaults. Useful for
snapshotting the shared module-level ``CONSTANTS`` (including
whatever it's been customized with) into a private instance, e.g.
``CONSTANTS.copy()``. Relies on the same ``__getstate__``/``__setstate__``
pair pickling uses, so it's as cheap and correct as pickle round-tripping.
"""
return copy.deepcopy(self)

def __setstate__(self, state: Mapping[str, Any]) -> None:
# Restore each saved attribute directly. The previous implementation
# passed the whole state dict to __init__ as the ``prefixes`` argument,
Expand Down
41 changes: 34 additions & 7 deletions nameparser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,14 @@ class HumanName:
:param str full_name: The name string to be parsed.
:param constants:
a :py:class:`~nameparser.config.Constants` instance (subclasses are
honored). Pass ``None`` for `per-instance config <customize.html>`_.
Anything else raises ``TypeError``.
honored). Defaults to the shared module-level ``CONSTANTS``. For
`per-instance config <customize.html>`_, pass ``Constants()`` for
fresh library defaults, or ``CONSTANTS.copy()`` for a private
snapshot of the current shared config. Passing ``None`` also builds
a fresh ``Constants()``, but is deprecated (warns; raises
``TypeError`` in 2.0, see issue #260) since it silently discards any
customizations the caller may have expected to carry over. Anything
else raises ``TypeError``.
:param str encoding: string representing the encoding of your input
(deprecated with ``bytes`` input, removal in 2.0 — decode before
passing; see issue #245)
Expand Down Expand Up @@ -106,7 +112,10 @@ def __init__(
nickname: str | list[str] | None = None,
maiden: str | list[str] | None = None,
) -> None:
self.C = constants
# calls _validate_constants directly (not through the C setter) so
# the deprecation warning below attributes to this constructor's
# caller rather than to the setter, mirroring _apply_full_name below
self._C = self._validate_constants(constants, stacklevel=3)

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

@staticmethod
def _validate_constants(constants: 'Constants | None') -> 'Constants':
def _validate_constants(constants: 'Constants | None', *, stacklevel: int) -> 'Constants':
# Shared by the constructor and the C setter so both assignment paths
# give the same immediate TypeError instead of one bypassing the
# other and failing far from the cause (#239).
if constants is None:
# deprecated 1.4.0, raises TypeError in 2.0 (#260, removal #261):
# None means "build a fresh private Constants()", the opposite of
# what None conventionally means (the default is the *shared*
# CONSTANTS) -- an easy trap since customizing CONSTANTS then
# passing None elsewhere silently drops those customizations with
# no error. CONSTANTS.copy() is the explicit spelling for the
# other reading: a private snapshot of the current shared config.
warnings.warn(
"Passing constants=None is deprecated and will raise "
"TypeError in 2.0; use constants=Constants() for fresh "
"library defaults, or constants=CONSTANTS.copy() to snapshot "
"the current shared config. See "
"https://github.com/derek73/python-nameparser/issues/260",
DeprecationWarning,
stacklevel=stacklevel,
)
return Constants()
if not isinstance(constants, Constants):
# passing the class itself is the likeliest mistake, and
Expand All @@ -169,14 +194,16 @@ def C(self) -> 'Constants':
<customize.html>`_.

Assigning a non-``Constants`` value (besides ``None``, which builds a
fresh private ``Constants()``) raises the same ``TypeError`` as passing
an invalid ``constants`` argument to the constructor (#239).
fresh private ``Constants()`` and emits a ``DeprecationWarning`` --
see :py:meth:`~nameparser.parser.HumanName.__init__`) raises the same
``TypeError`` as passing an invalid ``constants`` argument to the
constructor (#239).
"""
return self._C

@C.setter
def C(self, constants: 'Constants | None') -> None:
self._C = self._validate_constants(constants)
self._C = self._validate_constants(constants, stacklevel=3)

def __getstate__(self) -> dict:
state = self.__dict__.copy()
Expand Down
Loading
Loading