nameparser 1.3.0__tar.gz → 1.3.1__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.
Files changed (48) hide show
  1. {nameparser-1.3.0/nameparser.egg-info → nameparser-1.3.1}/PKG-INFO +37 -23
  2. {nameparser-1.3.0 → nameparser-1.3.1}/README.rst +36 -22
  3. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/_version.py +1 -1
  4. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/regexes.py +7 -0
  5. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/parser.py +20 -3
  6. {nameparser-1.3.0 → nameparser-1.3.1/nameparser.egg-info}/PKG-INFO +37 -23
  7. {nameparser-1.3.0 → nameparser-1.3.1}/pyproject.toml +1 -0
  8. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_output_format.py +39 -0
  9. {nameparser-1.3.0 → nameparser-1.3.1}/AUTHORS +0 -0
  10. {nameparser-1.3.0 → nameparser-1.3.1}/LICENSE +0 -0
  11. {nameparser-1.3.0 → nameparser-1.3.1}/MANIFEST.in +0 -0
  12. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/__init__.py +0 -0
  13. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/__main__.py +0 -0
  14. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/__init__.py +0 -0
  15. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/bound_first_names.py +0 -0
  16. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/capitalization.py +0 -0
  17. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/conjunctions.py +0 -0
  18. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/prefixes.py +0 -0
  19. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/suffixes.py +0 -0
  20. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/config/titles.py +0 -0
  21. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/py.typed +0 -0
  22. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser/util.py +0 -0
  23. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser.egg-info/SOURCES.txt +0 -0
  24. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser.egg-info/dependency_links.txt +0 -0
  25. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser.egg-info/requires.txt +0 -0
  26. {nameparser-1.3.0 → nameparser-1.3.1}/nameparser.egg-info/top_level.txt +0 -0
  27. {nameparser-1.3.0 → nameparser-1.3.1}/setup.cfg +0 -0
  28. {nameparser-1.3.0 → nameparser-1.3.1}/tests/__init__.py +0 -0
  29. {nameparser-1.3.0 → nameparser-1.3.1}/tests/base.py +0 -0
  30. {nameparser-1.3.0 → nameparser-1.3.1}/tests/conftest.py +0 -0
  31. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_bound_first_names.py +0 -0
  32. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_brute_force.py +0 -0
  33. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_capitalization.py +0 -0
  34. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_config_attribute_docstrings.py +0 -0
  35. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_conjunctions.py +0 -0
  36. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_constants.py +0 -0
  37. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_east_slavic_patronymic_order.py +0 -0
  38. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_first_name.py +0 -0
  39. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_initials.py +0 -0
  40. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_middle_name_as_last.py +0 -0
  41. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_nicknames.py +0 -0
  42. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_parser_util.py +0 -0
  43. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_prefixes.py +0 -0
  44. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_python_api.py +0 -0
  45. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_suffixes.py +0 -0
  46. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_titles.py +0 -0
  47. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_turkic_patronymic_order.py +0 -0
  48. {nameparser-1.3.0 → nameparser-1.3.1}/tests/test_variations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nameparser
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: A simple Python module for parsing human names into their individual components.
5
5
  Author-email: Derek Gulbranson <derek73@gmail.com>
6
6
  License: LGPL
@@ -57,19 +57,35 @@ are optional. Comma-separated format like "Last, First" is also supported.
57
57
  2. Lastname [Suffix], Title Firstname (Nickname) Middle Middle[,] Suffix [, Suffix]
58
58
  3. Title Firstname M Lastname [Suffix], Suffix [Suffix] [, Suffix]
59
59
 
60
- Instantiating the `HumanName` class with a string splits on commas and then spaces,
61
- classifying name parts based on placement in the string and matches against known name
62
- pieces like titles and suffixes.
63
-
64
- It correctly handles some common conjunctions and special prefixes to last names
65
- like "del". Titles and conjunctions can be chained together to handle complex
66
- titles like "Asst Secretary of State". It can also try to correct capitalization
67
- of names that are all upper- or lowercase names.
68
-
69
- It attempts the best guess that can be made with a simple, rule-based approach.
70
- Its main use case is English and it is not likely to be useful for languages
71
- that do not conform to the supported name structure. It's not perfect, but it
72
- gets you pretty far.
60
+ How It Works
61
+ ~~~~~~~~~~~~
62
+
63
+ The parser works in two layers.
64
+
65
+ A **vocabulary layer** recognizes name pieces by what they are, using
66
+ configurable sets of known words: titles ("Dr."), suffixes ("III", "PhD"),
67
+ last-name prefixes ("de la"), conjunctions ("y", "&"), and delimited
68
+ nicknames ("Doc"). Titles and conjunctions chain together to handle complex
69
+ titles like "Asst Secretary of State"; prefixes join forward so "de la Vega"
70
+ stays one last name. This layer doesn't care where in the string a word
71
+ appears and it's the layer you customize, by adding or removing entries
72
+ in the sets to fit your dataset.
73
+
74
+ A **positional layer** then assigns everything the vocabulary layer didn't
75
+ claim, based purely on where it sits: the first unclaimed word is the first
76
+ name, the last one is the last name, and anything between them is a middle
77
+ name. There is no semantic understanding — "Dr" is a title when it comes
78
+ before a name and a suffix when it comes after ("pre-nominal" and
79
+ "post-nominal" would probably be better names) — and no attempt to correct
80
+ mistakes in the input.
81
+
82
+ It attempts the best guess that can be made with a simple, deterministic,
83
+ rule-based approach — no statistical models or machine learning; the same
84
+ input always parses the same way. The positional layer assumes Western name
85
+ order (given name first), so the main use case is English and other
86
+ languages that share that structure. It can also try to correct the
87
+ capitalization of names that are all upper- or lowercase. It's not perfect,
88
+ but it gets you pretty far.
73
89
 
74
90
  Installation
75
91
  ------------
@@ -116,11 +132,8 @@ Quick Start Example
116
132
  'Juan de la Vega'
117
133
 
118
134
 
119
- The parser does not attempt to correct mistakes in the input. It mostly just splits on white
120
- space and puts things in buckets based on their position in the string. This also means
121
- the difference between 'title' and 'suffix' is positional, not semantic. "Dr" is a title
122
- when it comes before the name and a suffix when it comes after. ("Pre-nominal"
123
- and "post-nominal" would probably be better names.)
135
+ Because the positional layer has no semantic understanding, position is
136
+ everything:
124
137
 
125
138
  ::
126
139
 
@@ -139,10 +152,11 @@ and "post-nominal" would probably be better names.)
139
152
  Customization
140
153
  -------------
141
154
 
142
- Your project may need some adjustment for your dataset. You can
143
- do this in your own pre- or post-processing, by `customizing the configured pre-defined
144
- sets`_ of titles, prefixes, etc., or by subclassing the `HumanName` class. See the
145
- `full documentation`_ for more information.
155
+ Your project may need some adjustment for your dataset. Most customization
156
+ is vocabulary `customizing the configured pre-defined sets`_ of titles,
157
+ prefixes, etc. that the vocabulary layer matches against. You can also do
158
+ your own pre- or post-processing, or subclass the `HumanName` class for
159
+ deeper changes. See the `full documentation`_ for more information.
146
160
 
147
161
 
148
162
  `Full documentation`_
@@ -29,19 +29,35 @@ are optional. Comma-separated format like "Last, First" is also supported.
29
29
  2. Lastname [Suffix], Title Firstname (Nickname) Middle Middle[,] Suffix [, Suffix]
30
30
  3. Title Firstname M Lastname [Suffix], Suffix [Suffix] [, Suffix]
31
31
 
32
- Instantiating the `HumanName` class with a string splits on commas and then spaces,
33
- classifying name parts based on placement in the string and matches against known name
34
- pieces like titles and suffixes.
35
-
36
- It correctly handles some common conjunctions and special prefixes to last names
37
- like "del". Titles and conjunctions can be chained together to handle complex
38
- titles like "Asst Secretary of State". It can also try to correct capitalization
39
- of names that are all upper- or lowercase names.
40
-
41
- It attempts the best guess that can be made with a simple, rule-based approach.
42
- Its main use case is English and it is not likely to be useful for languages
43
- that do not conform to the supported name structure. It's not perfect, but it
44
- gets you pretty far.
32
+ How It Works
33
+ ~~~~~~~~~~~~
34
+
35
+ The parser works in two layers.
36
+
37
+ A **vocabulary layer** recognizes name pieces by what they are, using
38
+ configurable sets of known words: titles ("Dr."), suffixes ("III", "PhD"),
39
+ last-name prefixes ("de la"), conjunctions ("y", "&"), and delimited
40
+ nicknames ("Doc"). Titles and conjunctions chain together to handle complex
41
+ titles like "Asst Secretary of State"; prefixes join forward so "de la Vega"
42
+ stays one last name. This layer doesn't care where in the string a word
43
+ appears and it's the layer you customize, by adding or removing entries
44
+ in the sets to fit your dataset.
45
+
46
+ A **positional layer** then assigns everything the vocabulary layer didn't
47
+ claim, based purely on where it sits: the first unclaimed word is the first
48
+ name, the last one is the last name, and anything between them is a middle
49
+ name. There is no semantic understanding — "Dr" is a title when it comes
50
+ before a name and a suffix when it comes after ("pre-nominal" and
51
+ "post-nominal" would probably be better names) — and no attempt to correct
52
+ mistakes in the input.
53
+
54
+ It attempts the best guess that can be made with a simple, deterministic,
55
+ rule-based approach — no statistical models or machine learning; the same
56
+ input always parses the same way. The positional layer assumes Western name
57
+ order (given name first), so the main use case is English and other
58
+ languages that share that structure. It can also try to correct the
59
+ capitalization of names that are all upper- or lowercase. It's not perfect,
60
+ but it gets you pretty far.
45
61
 
46
62
  Installation
47
63
  ------------
@@ -88,11 +104,8 @@ Quick Start Example
88
104
  'Juan de la Vega'
89
105
 
90
106
 
91
- The parser does not attempt to correct mistakes in the input. It mostly just splits on white
92
- space and puts things in buckets based on their position in the string. This also means
93
- the difference between 'title' and 'suffix' is positional, not semantic. "Dr" is a title
94
- when it comes before the name and a suffix when it comes after. ("Pre-nominal"
95
- and "post-nominal" would probably be better names.)
107
+ Because the positional layer has no semantic understanding, position is
108
+ everything:
96
109
 
97
110
  ::
98
111
 
@@ -111,10 +124,11 @@ and "post-nominal" would probably be better names.)
111
124
  Customization
112
125
  -------------
113
126
 
114
- Your project may need some adjustment for your dataset. You can
115
- do this in your own pre- or post-processing, by `customizing the configured pre-defined
116
- sets`_ of titles, prefixes, etc., or by subclassing the `HumanName` class. See the
117
- `full documentation`_ for more information.
127
+ Your project may need some adjustment for your dataset. Most customization
128
+ is vocabulary `customizing the configured pre-defined sets`_ of titles,
129
+ prefixes, etc. that the vocabulary layer matches against. You can also do
130
+ your own pre- or post-processing, or subclass the `HumanName` class for
131
+ deeper changes. See the `full documentation`_ for more information.
118
132
 
119
133
 
120
134
  `Full documentation`_
@@ -1,2 +1,2 @@
1
- VERSION = (1, 3, 0)
1
+ VERSION = (1, 3, 1)
2
2
  __version__ = '.'.join(map(str, VERSION))
@@ -6,6 +6,12 @@ re_emoji = re.compile('[' # lgtm[py/overly-large-range]
6
6
  '\U0001F680-\U0001F6FF'
7
7
  '\u2600-\u26FF\u2700-\u27BF]+')
8
8
 
9
+ # Invisible bidirectional formatting characters: ALM, LRM, RLM, the
10
+ # embedding/override marks (LRE/RLE/PDF/LRO/RLO) and the isolates
11
+ # (LRI/RLI/FSI/PDI). Copy-pasted RTL names often carry these; they render
12
+ # as nothing but stick to name parts and break equality/lookups.
13
+ re_bidi = re.compile('[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+')
14
+
9
15
  EMPTY_REGEX = re.compile('')
10
16
 
11
17
  REGEXES = {
@@ -20,6 +26,7 @@ REGEXES = {
20
26
  "no_vowels": re.compile(r'^[^aeyiuo]+$', re.I),
21
27
  "period_not_at_end": re.compile(r'.*\..+$', re.I),
22
28
  "emoji": re_emoji,
29
+ "bidi": re_bidi,
23
30
  "phd": re.compile(r'\s(ph\.?\s+d\.?)', re.I),
24
31
  "space_before_comma": re.compile(r'\s+,'),
25
32
  "east_slavic_patronymic": re.compile(
@@ -241,9 +241,14 @@ class HumanName:
241
241
  def __str__(self) -> str:
242
242
  if self.string_format is not None:
243
243
  # string_format = "{title} {first} {middle} {last} {suffix} ({nickname})"
244
- _s = self.string_format.format(**self.as_dict()) # noqa: UP032
244
+ # Empty attributes must render as '' (not empty_attribute_default,
245
+ # which may be None) so str.format does not interpolate the
246
+ # literal "None" into the output, which cannot be scrubbed
247
+ # afterward without corrupting name text containing the same
248
+ # substring (#254).
249
+ _s = self.string_format.format(**{k: v or '' for k, v in self.as_dict().items()})
245
250
  # remove trailing punctuation from missing nicknames
246
- _s = _s.replace(str(self.C.empty_attribute_default), '').replace(" ()", "").replace(" ''", "").replace(' ""', "")
251
+ _s = _s.replace(" ()", "").replace(" ''", "").replace(' ""', "")
247
252
  _s = self.C.regexes.space_before_comma.sub(',', _s)
248
253
  return self.collapse_whitespace(_s).strip(', ')
249
254
  return " ".join(self)
@@ -928,9 +933,11 @@ class HumanName:
928
933
  This method happens at the beginning of the :py:func:`parse_full_name`
929
934
  before any other processing of the string aside from unicode
930
935
  normalization, so it's a good place to do any custom handling in a
931
- subclass. Runs :py:func:`parse_nicknames` and :py:func:`squash_emoji`.
936
+ subclass. Runs :py:func:`squash_bidi`, :py:func:`parse_nicknames` and
937
+ :py:func:`squash_emoji`.
932
938
 
933
939
  """
940
+ self.squash_bidi()
934
941
  self.fix_phd()
935
942
  self.parse_nicknames()
936
943
  self.squash_emoji()
@@ -1133,6 +1140,16 @@ class HumanName:
1133
1140
  if re_emoji and re_emoji.search(self._full_name):
1134
1141
  self._full_name = re_emoji.sub('', self._full_name)
1135
1142
 
1143
+ def squash_bidi(self) -> None:
1144
+ """
1145
+ Remove invisible bidirectional control characters from the input
1146
+ string. They carry no name content but stick to the parts they
1147
+ surround, so parsed attributes stop comparing equal to the clean name.
1148
+ """
1149
+ re_bidi = self.C.regexes.bidi
1150
+ if re_bidi and re_bidi.search(self._full_name):
1151
+ self._full_name = re_bidi.sub('', self._full_name)
1152
+
1136
1153
  def handle_firstnames(self) -> None:
1137
1154
  """
1138
1155
  If there are only two parts and one is a title, assume it's a last name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nameparser
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: A simple Python module for parsing human names into their individual components.
5
5
  Author-email: Derek Gulbranson <derek73@gmail.com>
6
6
  License: LGPL
@@ -57,19 +57,35 @@ are optional. Comma-separated format like "Last, First" is also supported.
57
57
  2. Lastname [Suffix], Title Firstname (Nickname) Middle Middle[,] Suffix [, Suffix]
58
58
  3. Title Firstname M Lastname [Suffix], Suffix [Suffix] [, Suffix]
59
59
 
60
- Instantiating the `HumanName` class with a string splits on commas and then spaces,
61
- classifying name parts based on placement in the string and matches against known name
62
- pieces like titles and suffixes.
63
-
64
- It correctly handles some common conjunctions and special prefixes to last names
65
- like "del". Titles and conjunctions can be chained together to handle complex
66
- titles like "Asst Secretary of State". It can also try to correct capitalization
67
- of names that are all upper- or lowercase names.
68
-
69
- It attempts the best guess that can be made with a simple, rule-based approach.
70
- Its main use case is English and it is not likely to be useful for languages
71
- that do not conform to the supported name structure. It's not perfect, but it
72
- gets you pretty far.
60
+ How It Works
61
+ ~~~~~~~~~~~~
62
+
63
+ The parser works in two layers.
64
+
65
+ A **vocabulary layer** recognizes name pieces by what they are, using
66
+ configurable sets of known words: titles ("Dr."), suffixes ("III", "PhD"),
67
+ last-name prefixes ("de la"), conjunctions ("y", "&"), and delimited
68
+ nicknames ("Doc"). Titles and conjunctions chain together to handle complex
69
+ titles like "Asst Secretary of State"; prefixes join forward so "de la Vega"
70
+ stays one last name. This layer doesn't care where in the string a word
71
+ appears and it's the layer you customize, by adding or removing entries
72
+ in the sets to fit your dataset.
73
+
74
+ A **positional layer** then assigns everything the vocabulary layer didn't
75
+ claim, based purely on where it sits: the first unclaimed word is the first
76
+ name, the last one is the last name, and anything between them is a middle
77
+ name. There is no semantic understanding — "Dr" is a title when it comes
78
+ before a name and a suffix when it comes after ("pre-nominal" and
79
+ "post-nominal" would probably be better names) — and no attempt to correct
80
+ mistakes in the input.
81
+
82
+ It attempts the best guess that can be made with a simple, deterministic,
83
+ rule-based approach — no statistical models or machine learning; the same
84
+ input always parses the same way. The positional layer assumes Western name
85
+ order (given name first), so the main use case is English and other
86
+ languages that share that structure. It can also try to correct the
87
+ capitalization of names that are all upper- or lowercase. It's not perfect,
88
+ but it gets you pretty far.
73
89
 
74
90
  Installation
75
91
  ------------
@@ -116,11 +132,8 @@ Quick Start Example
116
132
  'Juan de la Vega'
117
133
 
118
134
 
119
- The parser does not attempt to correct mistakes in the input. It mostly just splits on white
120
- space and puts things in buckets based on their position in the string. This also means
121
- the difference between 'title' and 'suffix' is positional, not semantic. "Dr" is a title
122
- when it comes before the name and a suffix when it comes after. ("Pre-nominal"
123
- and "post-nominal" would probably be better names.)
135
+ Because the positional layer has no semantic understanding, position is
136
+ everything:
124
137
 
125
138
  ::
126
139
 
@@ -139,10 +152,11 @@ and "post-nominal" would probably be better names.)
139
152
  Customization
140
153
  -------------
141
154
 
142
- Your project may need some adjustment for your dataset. You can
143
- do this in your own pre- or post-processing, by `customizing the configured pre-defined
144
- sets`_ of titles, prefixes, etc., or by subclassing the `HumanName` class. See the
145
- `full documentation`_ for more information.
155
+ Your project may need some adjustment for your dataset. Most customization
156
+ is vocabulary `customizing the configured pre-defined sets`_ of titles,
157
+ prefixes, etc. that the vocabulary layer matches against. You can also do
158
+ your own pre- or post-processing, or subclass the `HumanName` class for
159
+ deeper changes. See the `full documentation`_ for more information.
146
160
 
147
161
 
148
162
  `Full documentation`_
@@ -42,6 +42,7 @@ dev = [
42
42
  "pytest (>=8)",
43
43
  "dill (>=0.2.5)",
44
44
  "sphinx (>=8)",
45
+ "furo",
45
46
  "mypy (>=2.1)",
46
47
  "ruff (>=0.15)",
47
48
  "pytest-timeout>=2.4.0",
@@ -99,6 +99,21 @@ class HumanNameOutputFormatTests(HumanNameTestBase):
99
99
  hn.nickname = ''
100
100
  self.assertEqual(str(hn), "Rev John A. Kenneth Doe III")
101
101
 
102
+ def test_name_containing_none_substring_with_none_empty_attribute_default(self) -> None:
103
+ # Regression for #254: with empty_attribute_default = None, __str__
104
+ # scrubbed the literal string 'None' from the formatted output,
105
+ # corrupting real name text containing that substring.
106
+ hn = HumanName("Nonez Smith", None)
107
+ hn.C.empty_attribute_default = None # type: ignore[assignment] # see test_constants.test_empty_attribute_default
108
+ self.assertEqual(str(hn), "Nonez Smith")
109
+
110
+ def test_name_none_as_literal_name_with_none_empty_attribute_default(self) -> None:
111
+ # Companion to the #254 regression: a name piece that is exactly
112
+ # 'None' must survive formatting in None-mode.
113
+ hn = HumanName("None Smith", None)
114
+ hn.C.empty_attribute_default = None # type: ignore[assignment] # see test_constants.test_empty_attribute_default
115
+ self.assertEqual(str(hn), "None Smith")
116
+
102
117
  def test_empty_field_drops_surrounding_whitespace(self) -> None:
103
118
  # issue #139: adjacent whitespace/punctuation should be dropped when a field is empty
104
119
  hn = HumanName("John Smith")
@@ -136,3 +151,27 @@ class HumanNameOutputFormatTests(HumanNameTestBase):
136
151
  self.m(hn.last, "Smith😊", hn)
137
152
  self.assertEqual(str(hn), "∫≜⩕ Smith😊")
138
153
  # test cleanup
154
+
155
+ def test_remove_bidi_control_chars(self) -> None:
156
+ # LRM/RLM and friends ride along with copy-pasted names and stick to
157
+ # the parts they surround. Covers every character in the bidi set.
158
+ for mark in ("\u200e", "\u200f", "\u061c", "\u202a", "\u202b",
159
+ "\u202c", "\u202d", "\u202e", "\u2066", "\u2067",
160
+ "\u2068", "\u2069"):
161
+ hn = HumanName(mark + "John" + mark + " Smith")
162
+ self.m(hn.first, "John", hn)
163
+ self.m(hn.last, "Smith", hn)
164
+
165
+ def test_bidi_stripped_name_compares_equal(self) -> None:
166
+ # The reported symptom: an invisible RLM around an RTL name makes the
167
+ # parsed part fail equality against the clean string (issue #266).
168
+ hn = HumanName("\u200fمحمد بن سلمان\u200f")
169
+ self.assertEqual(hn.first, "محمد")
170
+
171
+ def test_keep_bidi_control_chars(self) -> None:
172
+ from nameparser.config import Constants
173
+ constants = Constants()
174
+ constants.regexes.bidi = False # type: ignore[assignment]
175
+ hn = HumanName("\u200fJohn\u200f Smith", constants)
176
+ self.m(hn.first, "\u200fJohn\u200f", hn)
177
+ self.m(hn.last, "Smith", hn)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes