Skip to content

Commit c941430

Browse files
committed
Add type hints
This also required some refactoring: * I removed some code that seemed dead. * I created a subclass of TupleManager for regexes with a fallback. When accessing regexes previously, it was possible to get None. * I had to move all setter next to their getters due to a bug in mypy. See python/mypy#1465
1 parent b7f64d3 commit c941430

6 files changed

Lines changed: 566 additions & 521 deletions

File tree

nameparser/config/__init__.py

Lines changed: 66 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@
2727
``hn.C`` will be a reference to the module config, possibly yielding
2828
unexpected results. See `Customizing the Parser <customize.html>`_.
2929
"""
30+
import re
3031
import sys
31-
from collections.abc import Set
32+
from collections.abc import Iterable, Iterator, Mapping, Set
33+
from typing import Any, TypeVar
34+
35+
from typing_extensions import Self
3236

3337
from nameparser.util import lc
3438
from nameparser.config.prefixes import PREFIXES
@@ -38,7 +42,7 @@
3842
from nameparser.config.suffixes import SUFFIX_NOT_ACRONYMS
3943
from nameparser.config.titles import TITLES
4044
from nameparser.config.titles import FIRST_NAME_TITLES
41-
from nameparser.config.regexes import REGEXES
45+
from nameparser.config.regexes import EMPTY_REGEX, REGEXES
4246

4347
DEFAULT_ENCODING = 'UTF-8'
4448

@@ -55,37 +59,25 @@ class SetManager(Set):
5559
5660
'''
5761

58-
def __init__(self, elements):
62+
def __init__(self, elements: Iterable[str]) -> None:
5963
self.elements = set(elements)
6064

61-
def __call__(self):
65+
def __call__(self) -> Set[str]:
6266
return self.elements
6367

64-
def __repr__(self):
68+
def __repr__(self) -> str:
6569
return "SetManager({})".format(self.elements) # used for docs
6670

67-
def __iter__(self):
71+
def __iter__(self) -> Iterator[str]:
6872
return iter(self.elements)
6973

70-
def __contains__(self, value):
74+
def __contains__(self, value: object) -> bool:
7175
return value in self.elements
7276

73-
def __len__(self):
77+
def __len__(self) -> int:
7478
return len(self.elements)
7579

76-
def next(self):
77-
return self.__next__()
78-
79-
def __next__(self):
80-
if self.count >= len(self.elements):
81-
self.count = 0
82-
raise StopIteration
83-
else:
84-
c = self.count
85-
self.count = c + 1
86-
return getattr(self, self.elements[c]) or next(self)
87-
88-
def add_with_encoding(self, s, encoding=None):
80+
def add_with_encoding(self, s: str, encoding: str | None = None) -> None:
8981
"""
9082
Add the lower case and no-period version of the string to the set. Pass an
9183
explicit `encoding` parameter to specify the encoding of binary strings that
@@ -99,44 +91,58 @@ def add_with_encoding(self, s, encoding=None):
9991
s = s.decode(encoding)
10092
self.elements.add(lc(s))
10193

102-
def add(self, *strings):
94+
def add(self, *strings: str) -> Self:
10395
"""
10496
Add the lower case and no-period version of the string arguments to the set.
10597
Can pass a list of strings. Returns ``self`` for chaining.
10698
"""
107-
[self.add_with_encoding(s) for s in strings]
99+
for s in strings:
100+
self.add_with_encoding(s)
101+
108102
return self
109103

110-
def remove(self, *strings):
104+
def remove(self, *strings: str) -> Self:
111105
"""
112106
Remove the lower case and no-period version of the string arguments from the set.
113107
Returns ``self`` for chaining.
114108
"""
115-
[self.elements.remove(lc(s)) for s in strings if lc(s) in self.elements]
109+
for s in strings:
110+
if (lower := lc(s)) in self.elements:
111+
self.elements.remove(lower)
112+
116113
return self
117114

118115

119-
class TupleManager(dict):
116+
T = TypeVar('T')
117+
118+
119+
class TupleManager(dict[str, T]):
120120
'''
121121
A dictionary with dot.notation access. Subclass of ``dict``. Makes the tuple constants
122122
more friendly.
123123
'''
124124

125-
def __getattr__(self, attr):
125+
def __getattr__(self, attr: str) -> T | None:
126126
return self.get(attr)
127+
127128
__setattr__ = dict.__setitem__
128129
__delattr__ = dict.__delitem__
129130

130-
def __getstate__(self):
131+
def __getstate__(self) -> Mapping[str, T]:
131132
return dict(self)
132133

133-
def __setstate__(self, state):
134-
self.__init__(state)
134+
def __setstate__(self, state: Mapping[str, T]) -> None:
135+
self.update(state)
135136

136-
def __reduce__(self):
137+
def __reduce__(self) -> tuple[type, tuple[()], Mapping[str, T]]:
137138
return (TupleManager, (), self.__getstate__())
138139

139140

141+
class RegexTupleManager(TupleManager[re.Pattern[str]]):
142+
def __getattr__(self, attr: str) -> re.Pattern[str]:
143+
return self.get(attr, EMPTY_REGEX)
144+
145+
140146
class Constants:
141147
"""
142148
An instance of this class hold all of the configuration constants for the parser.
@@ -161,6 +167,17 @@ class Constants:
161167
:py:attr:`regexes` wrapped with :py:class:`TupleManager`.
162168
"""
163169

170+
prefixes: SetManager
171+
suffix_acronyms: SetManager
172+
suffix_not_acronyms: SetManager
173+
titles: SetManager
174+
first_name_titles: SetManager
175+
conjunctions: SetManager
176+
capitalization_exceptions: TupleManager[str]
177+
regexes: RegexTupleManager
178+
179+
_pst: Set[str] | None
180+
164181
string_format = "{title} {first} {middle} {last} {suffix} ({nickname})"
165182
"""
166183
The default string format use for all new `HumanName` instances.
@@ -180,17 +197,17 @@ class Constants:
180197
empty_attribute_default = ''
181198
"""
182199
Default return value for empty attributes.
183-
200+
184201
.. doctest::
185-
202+
186203
>>> from nameparser.config import CONSTANTS
187204
>>> CONSTANTS.empty_attribute_default = None
188205
>>> name = HumanName("John Doe")
189206
>>> name.title
190207
None
191208
>>>name.first
192209
'John'
193-
210+
194211
"""
195212

196213
capitalize_name = False
@@ -225,38 +242,38 @@ class Constants:
225242
"""
226243

227244
def __init__(self,
228-
prefixes=PREFIXES,
229-
suffix_acronyms=SUFFIX_ACRONYMS,
230-
suffix_not_acronyms=SUFFIX_NOT_ACRONYMS,
231-
titles=TITLES,
232-
first_name_titles=FIRST_NAME_TITLES,
233-
conjunctions=CONJUNCTIONS,
234-
capitalization_exceptions=CAPITALIZATION_EXCEPTIONS,
235-
regexes=REGEXES
236-
):
245+
prefixes: Iterable[str] = PREFIXES,
246+
suffix_acronyms: Iterable[str] = SUFFIX_ACRONYMS,
247+
suffix_not_acronyms: Iterable[str] = SUFFIX_NOT_ACRONYMS,
248+
titles: Iterable[str] = TITLES,
249+
first_name_titles: Iterable[str] = FIRST_NAME_TITLES,
250+
conjunctions: Iterable[str] = CONJUNCTIONS,
251+
capitalization_exceptions: TupleManager[str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS,
252+
regexes: RegexTupleManager | TupleManager[re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES
253+
) -> None:
237254
self.prefixes = SetManager(prefixes)
238255
self.suffix_acronyms = SetManager(suffix_acronyms)
239256
self.suffix_not_acronyms = SetManager(suffix_not_acronyms)
240257
self.titles = SetManager(titles)
241258
self.first_name_titles = SetManager(first_name_titles)
242259
self.conjunctions = SetManager(conjunctions)
243260
self.capitalization_exceptions = TupleManager(capitalization_exceptions)
244-
self.regexes = TupleManager(regexes)
261+
self.regexes = RegexTupleManager(regexes)
245262
self._pst = None
246263

247264
@property
248-
def suffixes_prefixes_titles(self):
265+
def suffixes_prefixes_titles(self) -> Set[str]:
249266
if not self._pst:
250267
self._pst = self.prefixes | self.suffix_acronyms | self.suffix_not_acronyms | self.titles
251268
return self._pst
252269

253-
def __repr__(self):
270+
def __repr__(self) -> str:
254271
return "<Constants() instance>"
255272

256-
def __setstate__(self, state):
257-
self.__init__(state)
273+
def __setstate__(self, state: Mapping[str, Any]) -> None:
274+
Constants.__init__(self, state)
258275

259-
def __getstate__(self):
276+
def __getstate__(self) -> Mapping[str, Any]:
260277
attrs = [x for x in dir(self) if not x.startswith('_')]
261278
return dict([(a, getattr(self, a)) for a in attrs])
262279

nameparser/config/regexes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
'[\u2600-\u26FF\u2700-\u27BF])+',
1717
re.UNICODE)
1818

19+
EMPTY_REGEX = re.compile('')
20+
1921
REGEXES = set([
2022
("spaces", re.compile(r"\s+", re.U)),
2123
("word", re.compile(r"(\w|\.)+", re.U)),

0 commit comments

Comments
 (0)