python-iso639 2025.2.8__tar.gz → 2025.2.18__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 (23) hide show
  1. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/CHANGELOG.md +7 -1
  2. {python_iso639-2025.2.8/src/python_iso639.egg-info → python_iso639-2025.2.18}/PKG-INFO +20 -5
  3. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/README.md +19 -4
  4. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/pyproject.toml +1 -1
  5. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/language.py +72 -16
  6. {python_iso639-2025.2.8 → python_iso639-2025.2.18/src/python_iso639.egg-info}/PKG-INFO +20 -5
  7. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/tests/test_language.py +25 -9
  8. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/LICENSE.txt +0 -0
  9. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/MANIFEST.in +0 -0
  10. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/setup.cfg +0 -0
  11. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/__init__.py +0 -0
  12. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/_data/__init__.py +0 -0
  13. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/_data/iso-639-3-macrolanguages.tab +0 -0
  14. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/_data/iso-639-3.tab +0 -0
  15. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/_data/iso-639-3_Name_Index.tab +0 -0
  16. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/_data/iso-639-3_Retirements.tab +0 -0
  17. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/iso639/py.typed +0 -0
  18. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/python_iso639.egg-info/SOURCES.txt +0 -0
  19. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/python_iso639.egg-info/dependency_links.txt +0 -0
  20. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/python_iso639.egg-info/not-zip-safe +0 -0
  21. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/python_iso639.egg-info/requires.txt +0 -0
  22. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/src/python_iso639.egg-info/top_level.txt +0 -0
  23. {python_iso639-2025.2.8 → python_iso639-2025.2.18}/tests/test_version_number.py +0 -0
@@ -17,10 +17,16 @@ major/minor/micro version numbers like `05` (it'd have to be just `5`).
17
17
  ### Fixed
18
18
  ### Security
19
19
 
20
+ ## [2025.2.18]
21
+
22
+ ### Added
23
+ * `Language.match` now supports case-insensitive matching
24
+ and ignores leading/trailing whitespace by default.
25
+
20
26
  ## [2025.2.8]
21
27
 
22
28
  ### Added
23
- * Explcitly indicated that the library is type-annotated.
29
+ * Explicitly indicated that the library is type-annotated.
24
30
 
25
31
  ## [2025.1.28]
26
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python-iso639
3
- Version: 2025.2.8
3
+ Version: 2025.2.18
4
4
  Summary: ISO 639 language codes, names, and other associated information
5
5
  Author-email: "Jackson L. Lee" <jacksonlunlee@gmail.com>
6
6
  License: Apache 2.0
@@ -83,13 +83,13 @@ Create a `Language` instance by one of the class methods.
83
83
  Language(part3='fra', part2b='fre', part2t='fra', part1='fr', scope='I', type='L', name='French', comment=None, other_names=None, macrolanguage=None, retire_reason=None, retire_change_to=None, retire_remedy=None, retire_date=None)
84
84
  ```
85
85
 
86
- Fast object instantiation for retrieving language information (run on Python 3.13, macOS 15.2, Apple M1 Pro)
86
+ Fast object instantiation for retrieving language information (run on Python 3.13, macOS 15.3.1, Apple M1 Pro)
87
87
 
88
88
  ```python
89
89
  In [1]: import iso639
90
90
 
91
91
  In [2]: %timeit iso639.Language.from_part3("fra")
92
- 220 ns ± 0.658 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
92
+ 217 ns ± 0.139 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
93
93
  ```
94
94
 
95
95
  #### From Another ISO 639 Code Set or a Reference Name
@@ -154,11 +154,27 @@ Use the `match` classmethod:
154
154
  True
155
155
  ```
156
156
 
157
+ By default, the classmethod `match` supports case-insensitive matching
158
+ and ignores leading/trailing whitespace.
159
+ To enforce exact matching instead, pass in `exact=True`:
160
+
161
+ ```python
162
+ >>> lang5 = iso639.Language.match('FRA')
163
+ >>> lang6 = iso639.Language.match('fra ')
164
+ >>> lang7 = iso639.Language.match('french')
165
+ >>> lang4 == lang5 == lang6 == lang7
166
+ True
167
+ >>> iso639.Language.match("french", exact=True)
168
+ Traceback (most recent call last):
169
+ File "<stdin>", line 1, in <module>
170
+ LanguageNotFoundError: 'french' isn't an ISO language code or name
171
+ ```
172
+
157
173
  The classmethod `match` is particularly useful for consistently
158
174
  accessing a specific attribute from unknown inputs, e.g., the ISO 639-3 code.
159
175
 
160
176
  ```python
161
- >>> 'fra' == lang1.part3 == lang2.part3 == lang3.part3 == lang4.part3
177
+ >>> 'fra' == lang1.part3 == lang2.part3 == lang3.part3 == lang4.part3 == lang5.part3 == lang6.part3 == lang7.part3
162
178
  True
163
179
  ```
164
180
 
@@ -261,7 +277,6 @@ Beyond that, the precise order in matching is as follows:
261
277
  * ISO 639-3 alternative language names (the "print" ones)
262
278
  * ISO 639-3 alternative language names (the "inverted" ones)
263
279
 
264
- Only exact matching is done (there's no fuzzy string matching of any sort).
265
280
  As soon as a match is found, `Language.match` returns a `Language` instance.
266
281
  If there isn't a match, a `LanguageNotFoundError` is raised.
267
282
 
@@ -45,13 +45,13 @@ Create a `Language` instance by one of the class methods.
45
45
  Language(part3='fra', part2b='fre', part2t='fra', part1='fr', scope='I', type='L', name='French', comment=None, other_names=None, macrolanguage=None, retire_reason=None, retire_change_to=None, retire_remedy=None, retire_date=None)
46
46
  ```
47
47
 
48
- Fast object instantiation for retrieving language information (run on Python 3.13, macOS 15.2, Apple M1 Pro)
48
+ Fast object instantiation for retrieving language information (run on Python 3.13, macOS 15.3.1, Apple M1 Pro)
49
49
 
50
50
  ```python
51
51
  In [1]: import iso639
52
52
 
53
53
  In [2]: %timeit iso639.Language.from_part3("fra")
54
- 220 ns ± 0.658 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
54
+ 217 ns ± 0.139 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
55
55
  ```
56
56
 
57
57
  #### From Another ISO 639 Code Set or a Reference Name
@@ -116,11 +116,27 @@ Use the `match` classmethod:
116
116
  True
117
117
  ```
118
118
 
119
+ By default, the classmethod `match` supports case-insensitive matching
120
+ and ignores leading/trailing whitespace.
121
+ To enforce exact matching instead, pass in `exact=True`:
122
+
123
+ ```python
124
+ >>> lang5 = iso639.Language.match('FRA')
125
+ >>> lang6 = iso639.Language.match('fra ')
126
+ >>> lang7 = iso639.Language.match('french')
127
+ >>> lang4 == lang5 == lang6 == lang7
128
+ True
129
+ >>> iso639.Language.match("french", exact=True)
130
+ Traceback (most recent call last):
131
+ File "<stdin>", line 1, in <module>
132
+ LanguageNotFoundError: 'french' isn't an ISO language code or name
133
+ ```
134
+
119
135
  The classmethod `match` is particularly useful for consistently
120
136
  accessing a specific attribute from unknown inputs, e.g., the ISO 639-3 code.
121
137
 
122
138
  ```python
123
- >>> 'fra' == lang1.part3 == lang2.part3 == lang3.part3 == lang4.part3
139
+ >>> 'fra' == lang1.part3 == lang2.part3 == lang3.part3 == lang4.part3 == lang5.part3 == lang6.part3 == lang7.part3
124
140
  True
125
141
  ```
126
142
 
@@ -223,7 +239,6 @@ Beyond that, the precise order in matching is as follows:
223
239
  * ISO 639-3 alternative language names (the "print" ones)
224
240
  * ISO 639-3 alternative language names (the "inverted" ones)
225
241
 
226
- Only exact matching is done (there's no fuzzy string matching of any sort).
227
242
  As soon as a match is found, `Language.match` returns a `Language` instance.
228
243
  If there isn't a match, a `LanguageNotFoundError` is raised.
229
244
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-iso639"
7
- version = "2025.2.8"
7
+ version = "2025.2.18"
8
8
  description = "ISO 639 language codes, names, and other associated information"
9
9
  readme = "README.md"
10
10
  requires-python = ">= 3.9"
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import datetime
6
6
  from dataclasses import dataclass
7
7
 
8
- from typing import Dict, List, Union, Set
8
+ from typing import Dict, List, NoReturn, Optional, Union, Set
9
9
 
10
10
  from ._data import (
11
11
  _PART3_TO_CODES,
@@ -26,6 +26,12 @@ from ._data import (
26
26
  )
27
27
 
28
28
 
29
+ _STRING_CLEANING_FUNCS = [
30
+ lambda x: x.strip().lower(),
31
+ lambda x: x.strip().title(),
32
+ ]
33
+
34
+
29
35
  class LanguageNotFoundError(Exception):
30
36
  pass
31
37
 
@@ -100,13 +106,17 @@ class Language:
100
106
  return isinstance(other, Language) and self.part3 == other.part3
101
107
 
102
108
  @classmethod
103
- def match(cls, user_input: str, /) -> Language:
109
+ def match(cls, user_input: str, /, *, exact: bool = False) -> Language:
104
110
  """Return a ``Language`` instance by matching on the user input.
105
111
 
106
112
  Parameters
107
113
  ----------
108
114
  user_input : str
109
115
  A language code or name.
116
+ exact : bool, optional
117
+ Whether to enforce exact matching against the user input.
118
+ Defaults to `False`. If `False`, matching is case-insensitive
119
+ and ignores leading/trailing whitespace.
110
120
 
111
121
  Returns
112
122
  -------
@@ -140,29 +150,29 @@ class Language:
140
150
  _NameIndexColumn.PRINT_NAME,
141
151
  _NameIndexColumn.INVERTED_NAME,
142
152
  ]
143
- return _PART3_TO_LANGUAGES[_get_part3(user_input, query_order)]
153
+ return _PART3_TO_LANGUAGES[_get_part3(user_input, query_order, exact)]
144
154
 
145
155
  @classmethod
146
156
  def from_part3(cls, user_input: str, /) -> Language:
147
157
  """Return a ``Language`` instance from an ISO 639-3 code."""
148
158
  return _PART3_TO_LANGUAGES[
149
- _get_part3(user_input, [_CodesColumn.ID, _RetirementsColumn.ID])
159
+ _get_part3_exact(user_input, [_CodesColumn.ID, _RetirementsColumn.ID])
150
160
  ]
151
161
 
152
162
  @classmethod
153
163
  def from_part2b(cls, user_input: str, /) -> Language:
154
164
  """Return a ``Language`` instance from an ISO 639-2 (bibliographic) code."""
155
- return _PART3_TO_LANGUAGES[_get_part3(user_input, [_CodesColumn.PART2B])]
165
+ return _PART3_TO_LANGUAGES[_get_part3_exact(user_input, [_CodesColumn.PART2B])]
156
166
 
157
167
  @classmethod
158
168
  def from_part2t(cls, user_input: str, /) -> Language:
159
169
  """Return a ``Language`` instance from an ISO 639-2 (terminological) code."""
160
- return _PART3_TO_LANGUAGES[_get_part3(user_input, [_CodesColumn.PART2T])]
170
+ return _PART3_TO_LANGUAGES[_get_part3_exact(user_input, [_CodesColumn.PART2T])]
161
171
 
162
172
  @classmethod
163
173
  def from_part1(cls, user_input: str, /) -> Language:
164
174
  """Return a ``Language`` instance from an ISO 639-1 code."""
165
- return _PART3_TO_LANGUAGES[_get_part3(user_input, [_CodesColumn.PART1])]
175
+ return _PART3_TO_LANGUAGES[_get_part3_exact(user_input, [_CodesColumn.PART1])]
166
176
 
167
177
  @classmethod
168
178
  def from_name(cls, user_input: str, /) -> Language:
@@ -172,10 +182,57 @@ class Language:
172
182
  _NameIndexColumn.PRINT_NAME,
173
183
  _NameIndexColumn.INVERTED_NAME,
174
184
  ]
175
- return _PART3_TO_LANGUAGES[_get_part3(user_input, query_order)]
185
+ return _PART3_TO_LANGUAGES[_get_part3_exact(user_input, query_order)]
186
+
176
187
 
188
+ def _raise_language_not_found_error(user_input: str) -> NoReturn:
189
+ raise LanguageNotFoundError(f"{user_input!r} isn't an ISO language code or name")
190
+
191
+
192
+ def _get_part3(
193
+ user_input: str, query_order: List[_COLUMN_TYPE], exact: bool = True
194
+ ) -> str:
195
+ """Get the part 3 code of a language.
177
196
 
178
- def _get_part3(user_input: str, query_order: List[_COLUMN_TYPE]) -> str:
197
+ Parameters
198
+ ----------
199
+ user_input : str
200
+ The user-provided language code or name.
201
+ query_order : List[_COLUMN_TYPE]
202
+ A list of columns to specify query order.
203
+ exact : bool, optional
204
+ Whether to enforce exact matching against the user input. Defaults to `True`.
205
+ If `False`, basic string cleaning is applied to the user input.
206
+
207
+ Returns
208
+ -------
209
+ str
210
+
211
+ Raises
212
+ ------
213
+ LanguageNotFoundError
214
+ If `part3` isn't a language name or code
215
+ """
216
+ try:
217
+ return _get_part3_exact(user_input, query_order)
218
+ except LanguageNotFoundError as e:
219
+ if exact:
220
+ raise e
221
+ else:
222
+ for func in _STRING_CLEANING_FUNCS:
223
+ try:
224
+ return _get_part3_exact(func(user_input), query_order, user_input)
225
+ except LanguageNotFoundError:
226
+ continue
227
+ else:
228
+ _raise_language_not_found_error(user_input)
229
+
230
+
231
+ def _get_part3_exact(
232
+ user_input: str,
233
+ query_order: List[_COLUMN_TYPE],
234
+ original_user_input: Optional[str] = None,
235
+ ) -> str:
179
236
  """Get the part 3 code of a language.
180
237
 
181
238
  Parameters
@@ -184,6 +241,9 @@ def _get_part3(user_input: str, query_order: List[_COLUMN_TYPE]) -> str:
184
241
  The user-provided language code or name.
185
242
  query_order : List[_COLUMN_TYPE]
186
243
  A list of columns to specify query order.
244
+ original_user_input : str, optional
245
+ The original user input. Default is `None`.
246
+ This argument is used when the user input has been cleaned.
187
247
 
188
248
  Returns
189
249
  -------
@@ -198,8 +258,7 @@ def _get_part3(user_input: str, query_order: List[_COLUMN_TYPE]) -> str:
198
258
  for column in query_order:
199
259
  if column == _CodesColumn.ID:
200
260
  if user_input in _PART3_TO_CODES:
201
- part3 = user_input
202
- break
261
+ return user_input
203
262
  elif column == _CodesColumn.PART2B:
204
263
  part3 = _PART2B_TO_PART3.get(user_input)
205
264
  elif column == _CodesColumn.PART2T:
@@ -208,8 +267,7 @@ def _get_part3(user_input: str, query_order: List[_COLUMN_TYPE]) -> str:
208
267
  part3 = _PART1_TO_PART3.get(user_input)
209
268
  elif column == _RetirementsColumn.ID:
210
269
  if user_input in _PART3_TO_RETIREMENTS:
211
- part3 = user_input
212
- break
270
+ return user_input
213
271
  elif column == _CodesColumn.REF_NAME:
214
272
  part3 = _REF_NAME_TO_PART3.get(user_input)
215
273
  elif column == _NameIndexColumn.PRINT_NAME:
@@ -222,9 +280,7 @@ def _get_part3(user_input: str, query_order: List[_COLUMN_TYPE]) -> str:
222
280
  break
223
281
 
224
282
  if part3 is None:
225
- raise LanguageNotFoundError(
226
- f"{user_input!r} isn't an ISO language code or name"
227
- )
283
+ _raise_language_not_found_error(original_user_input or user_input)
228
284
 
229
285
  return part3
230
286
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python-iso639
3
- Version: 2025.2.8
3
+ Version: 2025.2.18
4
4
  Summary: ISO 639 language codes, names, and other associated information
5
5
  Author-email: "Jackson L. Lee" <jacksonlunlee@gmail.com>
6
6
  License: Apache 2.0
@@ -83,13 +83,13 @@ Create a `Language` instance by one of the class methods.
83
83
  Language(part3='fra', part2b='fre', part2t='fra', part1='fr', scope='I', type='L', name='French', comment=None, other_names=None, macrolanguage=None, retire_reason=None, retire_change_to=None, retire_remedy=None, retire_date=None)
84
84
  ```
85
85
 
86
- Fast object instantiation for retrieving language information (run on Python 3.13, macOS 15.2, Apple M1 Pro)
86
+ Fast object instantiation for retrieving language information (run on Python 3.13, macOS 15.3.1, Apple M1 Pro)
87
87
 
88
88
  ```python
89
89
  In [1]: import iso639
90
90
 
91
91
  In [2]: %timeit iso639.Language.from_part3("fra")
92
- 220 ns ± 0.658 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
92
+ 217 ns ± 0.139 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
93
93
  ```
94
94
 
95
95
  #### From Another ISO 639 Code Set or a Reference Name
@@ -154,11 +154,27 @@ Use the `match` classmethod:
154
154
  True
155
155
  ```
156
156
 
157
+ By default, the classmethod `match` supports case-insensitive matching
158
+ and ignores leading/trailing whitespace.
159
+ To enforce exact matching instead, pass in `exact=True`:
160
+
161
+ ```python
162
+ >>> lang5 = iso639.Language.match('FRA')
163
+ >>> lang6 = iso639.Language.match('fra ')
164
+ >>> lang7 = iso639.Language.match('french')
165
+ >>> lang4 == lang5 == lang6 == lang7
166
+ True
167
+ >>> iso639.Language.match("french", exact=True)
168
+ Traceback (most recent call last):
169
+ File "<stdin>", line 1, in <module>
170
+ LanguageNotFoundError: 'french' isn't an ISO language code or name
171
+ ```
172
+
157
173
  The classmethod `match` is particularly useful for consistently
158
174
  accessing a specific attribute from unknown inputs, e.g., the ISO 639-3 code.
159
175
 
160
176
  ```python
161
- >>> 'fra' == lang1.part3 == lang2.part3 == lang3.part3 == lang4.part3
177
+ >>> 'fra' == lang1.part3 == lang2.part3 == lang3.part3 == lang4.part3 == lang5.part3 == lang6.part3 == lang7.part3
162
178
  True
163
179
  ```
164
180
 
@@ -261,7 +277,6 @@ Beyond that, the precise order in matching is as follows:
261
277
  * ISO 639-3 alternative language names (the "print" ones)
262
278
  * ISO 639-3 alternative language names (the "inverted" ones)
263
279
 
264
- Only exact matching is done (there's no fuzzy string matching of any sort).
265
280
  As soon as a match is found, `Language.match` returns a `Language` instance.
266
281
  If there isn't a match, a `LanguageNotFoundError` is raised.
267
282
 
@@ -7,18 +7,34 @@ import pytest
7
7
 
8
8
 
9
9
  @pytest.mark.parametrize(
10
- "user_input, expected_part3",
10
+ "user_input, exact, expected_part3",
11
11
  [
12
- ("fra", "fra"),
13
- ("fre", "fra"),
14
- ("fr", "fra"),
15
- ("French", "fra"),
16
- ("Castilian", "spa"),
12
+ ("fra", True, "fra"),
13
+ ("fra", False, "fra"),
14
+ ("FRA", True, None),
15
+ ("FRA", False, "fra"),
16
+ (" FRA ", False, "fra"),
17
+ ("Fra", True, None),
18
+ ("Fra", False, "fra"),
19
+ (" Fra ", False, "fra"),
20
+ ("French", True, "fra"),
21
+ ("French", False, "fra"),
22
+ ("FRENCH", True, None),
23
+ ("FRENCH", False, "fra"),
24
+ (" FRENCH ", False, "fra"),
25
+ ("french", True, None),
26
+ ("french", False, "fra"),
27
+ (" french ", False, "fra"),
28
+ ("Castilian", True, "spa"),
17
29
  ],
18
30
  )
19
- def test_match(user_input, expected_part3):
20
- actual_part3 = Language.match(user_input).part3
21
- assert actual_part3 == expected_part3
31
+ def test_match(user_input, exact, expected_part3):
32
+ if expected_part3 is None:
33
+ with pytest.raises(LanguageNotFoundError):
34
+ Language.match(user_input, exact=exact)
35
+ else:
36
+ actual_part3 = Language.match(user_input, exact=exact).part3
37
+ assert actual_part3 == expected_part3
22
38
 
23
39
 
24
40
  def test_name():