Skip to content

Latest commit

 

History

History
383 lines (333 loc) · 14.4 KB

File metadata and controls

383 lines (333 loc) · 14.4 KB

Migrating from HumanName

HumanName and CONSTANTS keep working in 2.0 — same imports, same attributes, same mutation API (name.C.titles.add(...), name.first = "...", and so on). Upgrading to 2.0 and migrating your code to the new :class:`~nameparser.Parser`/:class:`~nameparser.Lexicon`/ :class:`~nameparser.Policy` API are two separate decisions — you can do the former today and the latter whenever it's convenient. The compatibility layer (HumanName and nameparser.config) is removed in 3.0; that release is not scheduled.

The full 1.x documentation remains the canonical reference for HumanName and stays online at the readthedocs stable build, currently the 1.4.0 release: https://nameparser.readthedocs.io/en/stable/

This page exists for the other direction: translating a v1 customization or a v1-shaped comparison into the 2.0 API, one row per old name.

Before you upgrade

What 2.0 removes is the batch of deprecations 1.3 and 1.4 announced. If your test suite runs clean on 1.4 under python -W error::DeprecationWarning, it will run on 2.0 — with four exceptions that 1.4 never warned about. The first three raise the first time you hit them; the fourth only warns, so read it carefully:

  • CONSTANTS.regexes.<name> = ... raises TypeError. This includes CONSTANTS.regexes.bidi = False, the opt-out 1.3.1 recommended for keeping bidirectional marks; the 2.0 spellings are Policy(strip_bidi=False) and Policy(strip_emoji=False)
  • assigning a *_list attribute (name.first_list = [...]) raises AttributeError — the lists are read-only snapshots in 2.0
  • positional Constants(...) construction raises TypeError; the constructor is keyword-only
  • a subclass overriding a v1 parsing hook (pre_process, parse_pieces, is_title, and the rest) gets a DeprecationWarning at construction naming the hooks it overrode, because 2.0 delegates parsing to the core parser and never calls them

One removal changes results without saying anything. HumanName no longer defines __eq__, so name == "John Smith" is now False where 1.x returned True. That one did warn on 1.4, but nothing will tell you on 2.0. If you compare names anywhere, grep for == before upgrading and move to matches() — see Comparison.

One step has to happen before you upgrade, because the fix is only available on the version you're leaving: a Constants pickle written by nameparser 1.2.x or earlier must be re-pickled under 1.3 or 1.4. 2.0 refuses to load one, and by then the code that could rewrite it is gone.

If your suite did not run clean, these are the replacements for the warned removals:

1.4 spelling 2.0 replacement
HumanName(b"..."), manager.add(b"...") Decode first: HumanName(raw.decode("utf-8"))
SetManager.add_with_encoding(b"...") add(), on already-decoded str
manager() (calling a set manager) set(manager), or iterate it directly
manager.remove(missing) (was tolerant) discard() to ignore missing; remove() now raises KeyError like set.remove
HumanName(..., constants=None) Constants() for library defaults, or CONSTANTS.copy() for a private snapshot of the current shared config
name['first'] = value name.first = value
CONSTANTS.regexes.typo (returned EMPTY_REGEX) .get("typo") for intentional soft access; attribute access now raises AttributeError. The same applies to capitalization_exceptions, whose error also lists the known keys
CONSTANTS.empty_attribute_default Gone; empty fields are always ''

Finally, if you want to see the difference on your own data rather than ours, tools/differential/ in the source repository diffs 1.4 and 2.0 parses over a corpus of names you supply. It is development tooling, not part of the installed package — see its README.

Attribute map

HumanName's seven fields and their aggregates map onto :class:`~nameparser.ParsedName` like this:

HumanName ParsedName Note
title title  
first given  
middle middle  
last family  
suffix suffix  
nickname nickname  
maiden maiden New field (added 1.3)
title_list, first_list, middle_list, last_list, suffix_list, nickname_list, maiden_list tokens_for(Role.TITLE), tokens_for(Role.GIVEN), ... Returns the raw :class:`~nameparser.Token` tuple for that role; read .text off each token for the string a _list attribute gave you
given_names / given_names_list given_names Unchanged name; middle folded into first
surnames / surnames_list surnames Unchanged name; middle folded into last
last_base / last_base_list family_base The surname with leading particles split off
last_prefixes / last_prefixes_list family_particles The particles family_base was split from (e.g. "de la")
string_format render(spec) A per-call argument now, not stored config — see :doc:`customize`
initials_format, initials_delimiter, initials_separator initials(spec, delimiter, separator) Same three knobs, now call-site arguments to :meth:`~nameparser.ParsedName.initials`
suffix_delimiter Policy(extra_suffix_delimiters={...}) Moves from a HumanName/Constants scalar to a Policy set field, so more than one custom delimiter can be active at once. It is the set that moved, not just the name: passing the old scalar through (extra_suffix_delimiters=" - ") raises, rather than silently registering three one-character delimiters
capitalize(force=...) capitalized(force=...) :meth:`~nameparser.ParsedName.capitalized` returns a new value rather than mutating in place. Its optional first argument takes a :class:`~nameparser.Lexicon`, if you need custom capitalization exceptions

Side by side:

>>> from nameparser import HumanName, parse, Role
>>> hn = HumanName("Dr. Juan Q. Xavier de la Vega III")
>>> n = parse("Dr. Juan Q. Xavier de la Vega III")
>>> hn.title == n.title, hn.first == n.given, hn.last == n.family
(True, True, True)
>>> hn.first_list == [t.text for t in n.tokens_for(Role.GIVEN)]
True
>>> hn.given_names == n.given_names, hn.surnames == n.surnames
(True, True)
>>> hn.last_base == n.family_base, hn.last_prefixes == n.family_particles
(True, True)

Config map

CONSTANTS' vocabulary sets map onto :class:`~nameparser.Lexicon` fields:

CONSTANTS Lexicon Note
titles titles  
first_name_titles given_name_titles  
suffix_acronyms suffix_acronyms  
suffix_not_acronyms suffix_words  
suffix_acronyms_ambiguous suffix_acronyms_ambiguous  
prefixes particles  
non_first_name_prefixes particles_ambiguous Flipped — see the warning below
conjunctions conjunctions  
bound_first_names bound_given_names  
capitalization_exceptions capitalization_exceptions Pair-valued; set it via dataclasses.replace(lexicon, capitalization_exceptions={...}), not add()/remove()

And behavior/render scalars map onto :class:`~nameparser.Policy` (or a rendering argument, where the 2.0 equivalent isn't config at all):

CONSTANTS 2.0 equivalent Note
patronymic_name_order Policy(patronymic_rules={PatronymicRule.EAST_SLAVIC, PatronymicRule.TURKIC}) v1's single flag enabled both detectors at once; pick one rule (or a locale pack, see :doc:`locales`) if you only want one tradition
middle_name_as_last Policy.middle_as_family  
nickname_delimiters Policy.nickname_delimiters Was a dict of named sentinels; now a plain frozenset of (open, close) pairs. Both APIs gained the #273 typographic defaults (smart quotes, guillemets, CJK brackets, ...) in 2.0
maiden_delimiters Policy.maiden_delimiters Same shape change as nickname_delimiters. Precedence differs: in the 2.0 API a pair listed here wins over nickname_delimiters; through the 1.x facade a pair in both buckets keeps parsing as nickname (v1 behavior)
regexes.bidi Policy.strip_bidi regexes.bidi = False becomes Policy(strip_bidi=False)
regexes.emoji Policy.strip_emoji regexes.emoji = False becomes Policy(strip_emoji=False)
force_mixed_case_capitalization capitalized(force=True) The stored default is gone; pass force at the call site. v1's capitalize(force=True) already worked that way — this constant only supplied the default for calls that omitted it
capitalize_name (no equivalent) 2.0 never capitalizes automatically during parse(); call .capitalized() explicitly on the result instead

Every other regexes.* entry (word, spaces, and the rest of the compiled-pattern proxy) has no 2.0 replacement — parsing behavior is configured entirely through named Policy fields now, not by handing the parser a regex.

Warning

non_first_name_prefixes and particles_ambiguous mark complementary sets, not the same set under a new name. non_first_name_prefixes lists particles that are never read as a given name; particles_ambiguous lists the particles that may be read as one. Translating a customization means flipping the set: particles_ambiguous = lexicon.particles - constants.non_first_name_prefixes. Copying non_first_name_prefixes straight into particles_ambiguous silently inverts which particles are allowed to double as a given name.

Comparison

HumanName.__eq__/__hash__ were deprecated in 1.3.0 and are gone in 2.0's core API; use matches() for "is this the same name?" and comparison_key() for dedup, dict keys, and sorting — both exist on HumanName and on :class:`~nameparser.ParsedName` with the same behavior:

>>> from nameparser import HumanName, parse
>>> hn = HumanName("de la Vega, Juan")
>>> n = parse("de la Vega, Juan")
>>> hn.matches("Juan de la Vega"), n.matches("Juan de la Vega")
(True, True)
>>> hn.comparison_key() == n.comparison_key()
True

One behavior changed underneath both methods: components now fold with str.casefold() instead of str.lower(), so more Unicode case-pairs compare equal than did under 1.4. The change is strictly more permissive — anything 1.4 matched still matches:

>>> parse("Anna STRASSE").matches("Anna Straße")
True

Behavior changes

Beyond the API surface mapped above, a handful of parse outputs differ between 1.4 and 2.0 for specific input shapes. The full list, with reasoning, is in the 2.0.0 section of :doc:`release_log`. These are the shapes worth grepping your own fixtures for, because a recognized suffix or title now stays in its own field instead of landing in first/last:

>>> HumanName("Andrews, M.D.").last, HumanName("Andrews, M.D.").suffix
('Andrews', 'M.D.')
>>> HumanName("Johnson PhD").first, HumanName("Johnson PhD").suffix
('Johnson', 'PhD')

Under 1.4 those read first="M.D."/last="Andrews" and first="Johnson"/last="PhD" respectively. Two more to check for: a maiden marker now fills the maiden field rather than being folded into middle/last ("Jane Smith née Jones"), and with a custom suffix delimiter configured, a no-space delimiter group renders whole ("RN/CRNA") where 1.x split it ("RN, CRNA") — the role assignment is identical, only the rendered string differs.

2.1 adds two more, and unlike most of the 2.0 API these do reach HumanName: a name written wholly in Han or Hangul is read family-first, and an unspaced Korean name is split into surname and given name.

>>> HumanName("毛 泽东").last
'毛'
>>> HumanName("김민준").last, HumanName("김민준").first
('김', '민준')

1.4 read the first as first="毛"/last="泽东", and left the second whole in first. The Korean one also changes what the name renders as — str(HumanName("김민준")) was "김민준" and is now "민준 김", because the split inserts a token boundary that the default format then writes given-name-first.

A third shape changes even though nothing splits it. 1.4 routed a lone token to first whatever it was, so an unspaced Chinese or Japanese name landed there entire; 2.1 reads it as native-script CJK and puts it in last instead. Nothing is segmented — Han splitting is opt-in through a locale pack and HumanName cannot apply one — but the field the whole string arrives in is different:

>>> HumanName("毛泽东").last, HumanName("毛泽东").first
('毛泽东', '')
>>> HumanName("山田太郎").last, HumanName("山田太郎").first
('山田太郎', '')

Both were first under 1.4. If you feed unspaced CJK through HumanName and read first, that is the change most likely to reach you, and it is silent — the string is intact, just in the other field.

Constants has no switch for any of this — the v1 configuration surface is frozen for 2.x — so the way out is the 2.0 API: Parser(policy=Policy(script_orders={}, segment_scripts=())) restores 1.4's reading of all three shapes.