nameparser 2.4.1__tar.gz → 2.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nameparser-2.4.1/nameparser.egg-info → nameparser-2.4.2}/PKG-INFO +1 -1
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_classify.py +13 -6
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_pieces.py +5 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_vocab.py +21 -2
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_version.py +1 -1
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/titles.py +0 -1
- {nameparser-2.4.1 → nameparser-2.4.2/nameparser.egg-info}/PKG-INFO +1 -1
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_cldr_agreement.py +70 -1
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_parser.py +70 -3
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_rules_doc.py +16 -6
- {nameparser-2.4.1 → nameparser-2.4.2}/AUTHORS +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/LICENSE +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/MANIFEST.in +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/README.rst +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/__main__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_config_shim.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_facade.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_lexicon.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_locale.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_parser.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_assemble.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_assign.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_comma.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_extract.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_group.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_post_rules.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_script_segment.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_segment.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_state.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_pipeline/_tokenize.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_policy.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_render.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/_types.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/_deprecated.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/_invariants.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/bound_first_names.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/bound_given_names.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/capitalization.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/conjunctions.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/maiden_markers.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/particles.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/prefixes.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/regexes.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/suffixes.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/config/surnames.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/locales/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/locales/ja.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/locales/ru.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/locales/tr_az.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/locales/zh.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/parser.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/py.typed +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser/util.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser.egg-info/SOURCES.txt +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser.egg-info/dependency_links.txt +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser.egg-info/requires.txt +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/nameparser.egg-info/top_level.txt +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/pyproject.toml +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/setup.cfg +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/base.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/conftest.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_bound_given_names.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_brute_force.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_capitalization.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_comma_variants.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_conjunctions.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_constants.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_east_slavic_patronymic_order.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_first_name.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_initials.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_middle_name_as_last.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_nicknames.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_output_format.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_particles.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_python_api.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_suffixes.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_titles.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_turkic_patronymic_order.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/test_variations.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/_differential_fixtures.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/cases.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/conftest.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_assemble.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_assign.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_classify.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_comma.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_extract.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_group.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_pieces.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_post_rules.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_script_segment.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_segment.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_state.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_tokenize.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/pipeline/test_vocab.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/rules_doc.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_benchmark.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_cases.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_cldr_corpus.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_cli.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_config_aliases.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_config_shim.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_contracts.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_corpora.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_differential.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_doc_citations.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_doc_spellings.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_facade.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_facade_cases.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_layering.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_lexicon.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_locale.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_locales.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_name_usage_tool.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_order_correspondence.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_policy.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_properties.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_regex_sync.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_render.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_reprs.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_rules_doc_grammar.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_snapshot.py +0 -0
- {nameparser-2.4.1 → nameparser-2.4.2}/tests/v2/test_types.py +0 -0
|
@@ -62,8 +62,8 @@ from nameparser._pipeline._state import (
|
|
|
62
62
|
)
|
|
63
63
|
from nameparser._types import AmbiguityKind, Role
|
|
64
64
|
from nameparser._pipeline._vocab import (
|
|
65
|
-
caps_shape_candidate, is_initial, is_one_case,
|
|
66
|
-
suffix_as_written, tag_marker_runs,
|
|
65
|
+
caps_shape_candidate, composed_len, is_initial, is_one_case,
|
|
66
|
+
period_joined_vocab, suffix_as_written, tag_marker_runs,
|
|
67
67
|
)
|
|
68
68
|
from nameparser._pipeline._pieces import own_words
|
|
69
69
|
|
|
@@ -134,9 +134,14 @@ def _tags_for(token: WorkToken, n: str, state: ParseState,
|
|
|
134
134
|
# written wholly in one case, where nothing says so — where the
|
|
135
135
|
# letter is one the vocabulary marks as reading both ways"
|
|
136
136
|
# (#383/#479; history: decisions.md#P3)
|
|
137
|
-
|
|
137
|
+
# one letter as the writer sees it: a decomposed 'й' is two code
|
|
138
|
+
# points (#665), so non-ASCII text is counted composed, after the
|
|
139
|
+
# C-level tests have declined everything else
|
|
140
|
+
single_letter_connective = (n in lex.conjunctions
|
|
138
141
|
and token.text.upper() != token.text.lower()
|
|
139
|
-
and
|
|
142
|
+
and (len(token.text) == 1
|
|
143
|
+
or not token.text.isascii()
|
|
144
|
+
and composed_len(token.text) == 1))
|
|
140
145
|
if single_letter_connective and one_case_own:
|
|
141
146
|
# No case evidence, so the vocabulary decides. No namespaced
|
|
142
147
|
# tag beside it: the emitted ambiguity IS the record of the
|
|
@@ -334,10 +339,12 @@ def classify(state: ParseState) -> ParseState:
|
|
|
334
339
|
# site (mechanisms.md#AMBIGUITY-AT-THE-DECISION-SITE), per
|
|
335
340
|
# token -- 'e and e' reports twice.
|
|
336
341
|
if (one_case and "initial" in token.tags
|
|
337
|
-
and len(token.text) == 1
|
|
338
342
|
and folded[i] in state.lexicon.conjunctions_ambiguous
|
|
339
343
|
and folded[i] in state.lexicon.conjunctions
|
|
340
|
-
and i < clause_at and token.role is None
|
|
344
|
+
and i < clause_at and token.role is None
|
|
345
|
+
and (len(token.text) == 1
|
|
346
|
+
or not token.text.isascii()
|
|
347
|
+
and composed_len(token.text) == 1)):
|
|
341
348
|
ambiguities.append(PendingAmbiguity(
|
|
342
349
|
AmbiguityKind.CONJUNCTION_OR_INITIAL,
|
|
343
350
|
f"{token.text!r} is both a connective and an initial; "
|
|
@@ -52,6 +52,7 @@ helper.
|
|
|
52
52
|
"""
|
|
53
53
|
from __future__ import annotations
|
|
54
54
|
|
|
55
|
+
import unicodedata
|
|
55
56
|
from collections.abc import (
|
|
56
57
|
Callable, Container, Iterable, Mapping, Sequence, Set)
|
|
57
58
|
from typing import NamedTuple
|
|
@@ -314,6 +315,10 @@ def join_connectives(pieces: list[list[int]], ptags: list[set[str]],
|
|
|
314
315
|
continue
|
|
315
316
|
if letter_stays and len(pieces[k]) == 1:
|
|
316
317
|
text = tokens[pieces[k][0]].text
|
|
318
|
+
if not text.isascii():
|
|
319
|
+
# a decomposed 'й' is one letter in two code points
|
|
320
|
+
# (#665); compose it, as the lookup does
|
|
321
|
+
text = unicodedata.normalize("NFC", text)
|
|
317
322
|
if len(text) == 1 and text.isalpha():
|
|
318
323
|
k += 1
|
|
319
324
|
continue
|
|
@@ -143,7 +143,18 @@ def _drop_kana_voicing_marks(text: str) -> str:
|
|
|
143
143
|
# vetoes the whole word -- 'Kim김.' is refused as a title along with
|
|
144
144
|
# '田中.' -- and that is deliberate: a word carrying a script with no
|
|
145
145
|
# abbreviations is not wearing an abbreviation's period.
|
|
146
|
-
|
|
146
|
+
_has_initialless_char = _script_matcher(*_NO_INITIALS, whole=False)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def in_initialless_script(text: str) -> bool:
|
|
150
|
+
"""Whether TEXT holds a character of a script that writes no
|
|
151
|
+
initials (#320), asked of the composed spelling: a decomposed
|
|
152
|
+
hangul syllable is conjoining jamo, which no _NO_INITIALS range
|
|
153
|
+
holds, so '씨.' typed decomposed escaped the veto every shape test
|
|
154
|
+
beside it reads composed (#665). ASCII answers without the
|
|
155
|
+
matcher's frame, as the callers' own `isascii()` gates do."""
|
|
156
|
+
return (not text.isascii()
|
|
157
|
+
and _has_initialless_char(unicodedata.normalize("NFC", text)))
|
|
147
158
|
|
|
148
159
|
|
|
149
160
|
# H2's own shape, text-level: an unlisted period-marked abbreviation,
|
|
@@ -224,7 +235,15 @@ def is_initial_shaped(text: str) -> bool:
|
|
|
224
235
|
name want is_initial (#320). The two answers differ only inside
|
|
225
236
|
_NO_INITIALS scripts, where '씨.' is initial-SHAPED but is not an
|
|
226
237
|
initial -- see assign's roman-numeral fork, the shape caller, for
|
|
227
|
-
what picking the wrong one costs.
|
|
238
|
+
what picking the wrong one costs.
|
|
239
|
+
|
|
240
|
+
Asked of the composed spelling, as `_render` asks it (#542): `\\w`
|
|
241
|
+
does not match a combining mark, so a decomposed 'É.' is three
|
|
242
|
+
characters to the pattern and no initial (#665). Composing rather
|
|
243
|
+
than widening keeps the pattern v1-verbatim, and both calls are
|
|
244
|
+
C-level, so the predicate costs no frame it did not before."""
|
|
245
|
+
if not text.isascii():
|
|
246
|
+
text = unicodedata.normalize("NFC", text)
|
|
228
247
|
return bool(_INITIAL.fullmatch(text))
|
|
229
248
|
|
|
230
249
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
#: cannot carry the dev marker, so `VERSION >= (2, 5, 0)` is already
|
|
8
8
|
#: true here while 2.5.0 is unreleased. Compare `__version__` instead
|
|
9
9
|
#: where that distinction matters.
|
|
10
|
-
VERSION = (2, 4,
|
|
10
|
+
VERSION = (2, 4, 2)
|
|
11
11
|
#: PEP 440 pre-release/dev segment appended to the numeric version, or
|
|
12
12
|
#: "" for a final release. Joined WITHOUT a dot ("2.0.0rc1", not
|
|
13
13
|
#: "2.0.0.rc1"); setuptools reads __version__ as the package version.
|
|
@@ -18,7 +18,7 @@ from types import ModuleType
|
|
|
18
18
|
|
|
19
19
|
import pytest
|
|
20
20
|
|
|
21
|
-
from nameparser import parse
|
|
21
|
+
from nameparser import locales, parse
|
|
22
22
|
|
|
23
23
|
_TOOLS = Path(__file__).parents[2] / "tools" / "differential"
|
|
24
24
|
|
|
@@ -206,3 +206,72 @@ def test_an_unspaced_role_is_expected_in_text_order() -> None:
|
|
|
206
206
|
{"given": "セシリア", "given2": "ローズ", "surname": "ブラウン"},
|
|
207
207
|
"ブラウン・ローズ・セシリア")
|
|
208
208
|
assert agreement.same(roles["given_names"], "ローズ セシリア", "ブラウン・ローズ・セシリア")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# -- the locale view (#672) --------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def test_every_locale_pack_is_measured_or_listed_as_having_no_cldr_file() -> None:
|
|
214
|
+
# A new pack must add its CLDR files to LOCALE_FILES or say it has
|
|
215
|
+
# none; otherwise it goes unmeasured by omission.
|
|
216
|
+
mapped = set(agreement.LOCALE_FILES.values())
|
|
217
|
+
assert not mapped & agreement.NO_CLDR
|
|
218
|
+
assert mapped | agreement.NO_CLDR == {code.upper() for code in locales.available()}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_every_mapped_cldr_file_has_renderings() -> None:
|
|
222
|
+
# A misspelled file in the table would measure nothing and pass.
|
|
223
|
+
files = {row["file"] for row in agreement.load_rows()}
|
|
224
|
+
assert set(agreement.LOCALE_FILES) <= files
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_the_locale_view_parses_with_the_pack() -> None:
|
|
228
|
+
# A view that fell back to the default parser would record the
|
|
229
|
+
# default's disagreements and still pass its own ratchet. 王小明 is
|
|
230
|
+
# one unspaced Han token: only ZH segments it.
|
|
231
|
+
row = {"file": "zh", "label": "nativeG", "order": "surnameFirst",
|
|
232
|
+
"text": "王小明", "shape": 4, "fields": {"surname": "王", "given": "小明"}}
|
|
233
|
+
assert agreement.disagreements([row], locale=True) == {}
|
|
234
|
+
assert agreement.disagreements([row]) != {}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_a_pack_parser_keeps_the_family_first_order_of_its_shape() -> None:
|
|
238
|
+
# Latin text: no script decides the order, so only the FAMILY_FIRST
|
|
239
|
+
# base the pack is applied over can read Ivanov as the family.
|
|
240
|
+
name = agreement.parse_rendering("Ivanov Ivan", 4, "RU")
|
|
241
|
+
assert (name.family, name.given) == ("Ivanov", "Ivan")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_a_recorded_entry_whose_file_left_the_table_is_reported_gone() -> None:
|
|
245
|
+
# No pack's check reads such an entry, so without this it would sit
|
|
246
|
+
# in the record unguarded -- and survive every re-record.
|
|
247
|
+
[message] = agreement.unmapped({"zh/nativeG/givenFirst": {}, "xx/nativeG/sorting": {}})
|
|
248
|
+
assert message.startswith("gone: xx/nativeG/sorting")
|
|
249
|
+
assert agreement.unmapped(agreement.read_record(agreement.LOCALE_RECORD)) == []
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_a_re_record_drops_entries_whose_file_left_the_table(
|
|
253
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
254
|
+
# The carry-over that keeps an unmeasured pack's entries must not
|
|
255
|
+
# keep an unmapped file's too.
|
|
256
|
+
record = tmp_path / "locales.json"
|
|
257
|
+
agreement.write_record({"xx/nativeG/sorting": {"text": "X"}}, record)
|
|
258
|
+
monkeypatch.setattr(agreement, "LOCALE_RECORD", record)
|
|
259
|
+
monkeypatch.setattr(agreement, "RECORD", tmp_path / "default.json")
|
|
260
|
+
assert agreement.main(["--record"]) == 0
|
|
261
|
+
assert "xx/nativeG/sorting" not in agreement.read_record(record)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@pytest.mark.parametrize("pack", [
|
|
265
|
+
pytest.param(pack, marks=pytest.mark.skipif(
|
|
266
|
+
not agreement.pack_available(pack),
|
|
267
|
+
reason=f"needs {agreement._EXTRAS.get(pack)}; CI's ja-extra job runs it"))
|
|
268
|
+
for pack in sorted(set(agreement.LOCALE_FILES.values()))])
|
|
269
|
+
def test_each_pack_still_agrees_with_cldr_where_it_did(pack: str) -> None:
|
|
270
|
+
problems = agreement.locale_changes(
|
|
271
|
+
agreement.load_rows(), pack, agreement.read_record(agreement.LOCALE_RECORD))
|
|
272
|
+
assert not problems, "\n".join([
|
|
273
|
+
*problems[:40],
|
|
274
|
+
f"{len(problems)} change(s) against "
|
|
275
|
+
f"tools/differential/cldr_agreement_locales.json for {pack}.",
|
|
276
|
+
f"If every one is intended, re-record with `{agreement.RECORD_COMMAND}` "
|
|
277
|
+
"and say why in the commit."])
|
|
@@ -1962,6 +1962,34 @@ _NFD_ROWS = [
|
|
|
1962
1962
|
"Janez Novak, Ž.Š.Č.",
|
|
1963
1963
|
"Janez Novak, Žž.Šš.",
|
|
1964
1964
|
"Smith, Ž.Š. Jones",
|
|
1965
|
+
# #665: a period-marked initial (`\w\.`) beside a connective, in
|
|
1966
|
+
# each slot and comma shape -- a lone one read alike before the fix
|
|
1967
|
+
"É. y É. Smith",
|
|
1968
|
+
"Й. John Smith",
|
|
1969
|
+
"Ana É. y Luis",
|
|
1970
|
+
"Smith, É. y É.",
|
|
1971
|
+
", É. y É.",
|
|
1972
|
+
"(Bob), É. y É.",
|
|
1973
|
+
# ... and the one-letter connective written in two code points:
|
|
1974
|
+
# three words (P3's carve-out keeps it a name word), four words
|
|
1975
|
+
# (it joins), one case, and after a comma
|
|
1976
|
+
"John й Smith",
|
|
1977
|
+
"JOHN Й SMITH",
|
|
1978
|
+
"john й smith",
|
|
1979
|
+
"Ivan й Petrov Sidorov",
|
|
1980
|
+
"Smith, John й Jones",
|
|
1981
|
+
# ... and the veto on scripts that write no initials (#320), asked
|
|
1982
|
+
# of the composed spelling too: decomposed hangul is conjoining
|
|
1983
|
+
# jamo, outside every _NO_INITIALS range, so composing the initial
|
|
1984
|
+
# shape alone made a decomposed '씨.' an initial (a title or a
|
|
1985
|
+
# given name). Spaced or after a comma, since an UNSPACED
|
|
1986
|
+
# decomposed hangul name is not split (docs/usage.rst) and so has
|
|
1987
|
+
# no composed twin to match
|
|
1988
|
+
"김민준, 씨.",
|
|
1989
|
+
"김 민준 씨.",
|
|
1990
|
+
"김 민준, 씨.",
|
|
1991
|
+
", 씨.",
|
|
1992
|
+
"김. 민준",
|
|
1965
1993
|
]
|
|
1966
1994
|
_NFD_POLICIES = [
|
|
1967
1995
|
Policy(),
|
|
@@ -1976,6 +2004,15 @@ def _composed_roles(parser: Parser, text: str) -> dict[str, str]:
|
|
|
1976
2004
|
for k, v in parser.parse(text).as_dict().items()}
|
|
1977
2005
|
|
|
1978
2006
|
|
|
2007
|
+
def _composed_reading(parser: Parser, text: str) -> tuple[object, ...]:
|
|
2008
|
+
"""The roles, the reports' kinds and initials(), composed: #665's
|
|
2009
|
+
`John й Smith` differed in its report as well as its fields."""
|
|
2010
|
+
name = parser.parse(text)
|
|
2011
|
+
return (_composed_roles(parser, text),
|
|
2012
|
+
[a.kind for a in name.ambiguities],
|
|
2013
|
+
unicodedata.normalize("NFC", name.initials()))
|
|
2014
|
+
|
|
2015
|
+
|
|
1979
2016
|
@pytest.mark.parametrize("policy", _NFD_POLICIES,
|
|
1980
2017
|
ids=["default", "family-first", "family-first-given-last",
|
|
1981
2018
|
"caps-everywhere"])
|
|
@@ -1996,13 +2033,43 @@ def test_a_decomposed_abbreviation_reads_as_its_composed_twin(
|
|
|
1996
2033
|
two-letter rows pass there; the two-letter rows exist for the caps
|
|
1997
2034
|
fix's own length count, which counted code points the first time
|
|
1998
2035
|
(`John Smith, MÁ` read as a pair composed and a credential
|
|
1999
|
-
decomposed, found in the review of 08c0dd60).
|
|
2036
|
+
decomposed, found in the review of 08c0dd60).
|
|
2037
|
+
|
|
2038
|
+
#665 added sixteen rows and compares kinds and initials() too.
|
|
2039
|
+
Recorded control, measured 2026-10-13 against f9b2b81c's
|
|
2040
|
+
nameparser/: 38 rows x 4 policies = 152 twin cases plus the four
|
|
2041
|
+
direct tests, of which 41 fail -- the six initial rows under all
|
|
2042
|
+
four policies (24; the same 24 with only `is_initial_shaped`'s
|
|
2043
|
+
compose reverted), the three-word `й` rows (12; `_pieces`'
|
|
2044
|
+
carve-out alone), `김. 민준` (4), and the one-case report test
|
|
2045
|
+
below (1; either `_classify` width site alone). The other four
|
|
2046
|
+
hangul rows pass there: they guard the fix itself, failing (with
|
|
2047
|
+
`김. 민준`, 20 in all) when `in_initialless_script`'s compose
|
|
2048
|
+
alone is reverted, since the composed initial shape without the
|
|
2049
|
+
composed veto made a decomposed '씨.' an initial. `Ivan й Petrov
|
|
2050
|
+
Sidorov` and `Smith, John й Jones` pass everywhere and pin parity
|
|
2051
|
+
only."""
|
|
2000
2052
|
composed = unicodedata.normalize("NFC", text)
|
|
2001
2053
|
decomposed = unicodedata.normalize("NFD", text)
|
|
2002
2054
|
assert decomposed != composed
|
|
2003
2055
|
parser = Parser(policy=policy)
|
|
2004
|
-
assert (
|
|
2005
|
-
==
|
|
2056
|
+
assert (_composed_reading(parser, decomposed)
|
|
2057
|
+
== _composed_reading(parser, composed))
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
def test_a_decomposed_conjunction_reports_as_its_composed_twin() -> None:
|
|
2061
|
+
"""#665: the one-case report on a letter that is both a connective
|
|
2062
|
+
and an initial (rules.md#P3) counted code points, so it is reached
|
|
2063
|
+
by `й` only once a caller marks it ambiguous -- no default entry is
|
|
2064
|
+
decomposable."""
|
|
2065
|
+
parser = Parser(lexicon=Lexicon.default().add(
|
|
2066
|
+
conjunctions_ambiguous={"й"}))
|
|
2067
|
+
text = "ivan й petrov"
|
|
2068
|
+
composed = parser.parse(text)
|
|
2069
|
+
assert [a.kind for a in composed.ambiguities] == [
|
|
2070
|
+
AmbiguityKind.CONJUNCTION_OR_INITIAL]
|
|
2071
|
+
assert (_composed_reading(parser, unicodedata.normalize("NFD", text))
|
|
2072
|
+
== _composed_reading(parser, text))
|
|
2006
2073
|
|
|
2007
2074
|
|
|
2008
2075
|
def test_a_decomposed_caps_credential_is_still_a_suffix() -> None:
|
|
@@ -145,12 +145,22 @@ def test_example(rule: Rule, example: Example) -> None:
|
|
|
145
145
|
assert actual == example.value
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
@pytest.mark.parametrize("decomposed", [
|
|
149
|
+
"inž.", # H2's abbreviation shape (#652): z + U+030C
|
|
150
|
+
"John й Smith", # P3's one-letter connective (#665)
|
|
151
|
+
"É. y É. Smith", # P3's initial beside a connective (#665)
|
|
152
|
+
"김. 민준", # H2's script veto on hangul typed as jamo (#665)
|
|
153
|
+
])
|
|
154
|
+
def test_the_nfd_examples_are_still_decomposed(decomposed: str) -> None:
|
|
155
|
+
"""The decomposed example lines in rules.md are the only executable
|
|
156
|
+
witnesses of its encoding clauses (H2, P3), and an editor or
|
|
157
|
+
formatter that NFC-normalizes the file turns each into the
|
|
158
|
+
composed case, which always worked and still passes. Each entry
|
|
159
|
+
here is typed decomposed, which the first assert checks so that
|
|
160
|
+
normalizing this file cannot hollow the test out either."""
|
|
161
|
+
import unicodedata
|
|
153
162
|
from pathlib import Path
|
|
163
|
+
assert not unicodedata.is_normalized("NFC", decomposed)
|
|
154
164
|
text = (Path(__file__).parents[2] / "docs" / "design" / "rules.md"
|
|
155
165
|
).read_text(encoding="utf-8")
|
|
156
|
-
assert
|
|
166
|
+
assert decomposed in text, f"the NFD example {decomposed!r} was recomposed"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|