Skip to content

Commit 8a557df

Browse files
authored
Merge pull request #1354 from PyThaiNLP/copilot/narrow-type-hints-update
Fix runtime AttributeError in wordnet type casts, improve type safety and API contracts
2 parents 1b55f4f + d1dff74 commit 8a557df

56 files changed

Lines changed: 679 additions & 458 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pythainlp/ancient/currency.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from __future__ import annotations
55

66

7-
def convert_currency(value: float, from_unit: str) -> dict:
7+
def convert_currency(value: float, from_unit: str) -> dict[str, float]:
88
"""Convert ancient Thai currency to other units
99
1010
* เบี้ย (Bia)
@@ -23,7 +23,7 @@ def convert_currency(value: float, from_unit: str) -> dict:
2323
:param str from_unit: currency unit \
2424
('เบี้ย', 'อัฐ', 'ไพ', 'เฟื้อง', 'สลึง', 'บาท', 'ตำลึง', 'ชั่ง')
2525
:return: Thai currency
26-
:rtype: dict
26+
:rtype: dict[str, float]
2727
2828
:Example:
2929
::
@@ -63,8 +63,8 @@ def convert_currency(value: float, from_unit: str) -> dict:
6363
# start from 'อัฐ'
6464
value_in_att = value * conversion_factors_to_att[from_unit]
6565

66-
# Calculate values ​​in other units
67-
results = {}
66+
# Calculate values in other units
67+
results: dict[str, float] = {}
6868
for unit, factor in conversion_factors_to_att.items():
6969
results[unit] = value_in_att / factor
7070

pythainlp/augment/word2vec/bpemb_wv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# SPDX-License-Identifier: Apache-2.0
44
from __future__ import annotations
55

6-
from typing import TYPE_CHECKING
6+
from typing import TYPE_CHECKING, cast
77

88
from pythainlp.augment.word2vec.core import Word2VecAug
99

@@ -40,7 +40,7 @@ def tokenizer(self, text: str) -> list[str]:
4040
""":param str text: Thai text
4141
:rtype: List[str]
4242
"""
43-
return self.bpemb_temp.encode(text) # type: ignore[no-any-return]
43+
return cast(list[str], self.bpemb_temp.encode(text))
4444

4545
def load_w2v(self) -> None:
4646
"""Load BPEmb model"""

pythainlp/augment/word2vec/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ def __init__(
4242
_gensim_kv_logger.addFilter(_filter)
4343
try:
4444
if type == "file":
45-
self.model = word2vec.KeyedVectors.load_word2vec_format(model)
45+
self.model = word2vec.KeyedVectors.load_word2vec_format(
46+
model
47+
)
4648
else:
4749
self.model = word2vec.KeyedVectors.load_word2vec_format(
4850
model, binary=True, unicode_errors="ignore"

pythainlp/augment/wordnet.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,13 @@ class WordNetAug:
122122
"""Text Augment using wordnet"""
123123

124124
synonyms: list[str]
125-
list_synsets: list
125+
list_synsets: list[Synset]
126126
p2w_pos: Optional[str]
127127
synset: Synset
128128
syn: str
129129
synonyms_without_duplicates: list[str]
130130
list_words: list[str]
131-
list_synonym: list
131+
list_synonym: list[list[str]]
132132
p_all: int
133133
list_pos: list[tuple[str, str]]
134134
temp: list[str]
@@ -152,15 +152,15 @@ def find_synonyms(
152152
"""
153153
self.synonyms: list[str] = []
154154
if pos is None:
155-
self.list_synsets: list = wordnet.synsets(word)
155+
self.list_synsets: list[Synset] = wordnet.synsets(word)
156156
else:
157157
self.p2w_pos: Optional[str] = postype2wordnet(pos, postag_corpus)
158158
if self.p2w_pos != "":
159-
self.list_synsets: list = wordnet.synsets(
159+
self.list_synsets: list[Synset] = wordnet.synsets(
160160
word, pos=self.p2w_pos
161161
)
162162
else:
163-
self.list_synsets: list = wordnet.synsets(word)
163+
self.list_synsets: list[Synset] = wordnet.synsets(word)
164164

165165
for self.synset in wordnet.synsets(word):
166166
for self.syn in self.synset.lemma_names(lang="tha"):
@@ -206,7 +206,7 @@ def augment(
206206
"""
207207
new_sentences = []
208208
self.list_words: list[str] = tokenize(sentence)
209-
self.list_synonym: list = []
209+
self.list_synonym: list[list[str]] = []
210210
self.p_all: int = 1
211211
if postag:
212212
self.list_pos: list[tuple[str, str]] = pos_tag(

pythainlp/benchmarks/word_tokenization.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
if TYPE_CHECKING:
1111
import numpy as np
1212
import pandas as pd
13+
from numpy.typing import NDArray
1314

1415
SEPARATOR: str = "|"
1516

@@ -43,7 +44,7 @@ def _f1(precision: float, recall: float) -> float:
4344

4445

4546
def _flatten_result(
46-
my_dict: dict, sep: str = ":"
47+
my_dict: dict[str, dict[str, Union[int, str]]], sep: str = ":"
4748
) -> dict[str, Union[int, str]]:
4849
"""Flatten two-dimension dictionary.
4950
@@ -54,7 +55,8 @@ def _flatten_result(
5455
{ "a:b": 7 }
5556
5657
57-
:param dict my_dict: dictionary containing stats
58+
:param dict[str, dict[str, Union[int, str]]] my_dict: dictionary
59+
containing stats
5860
:param str sep: separator between the two keys (default: ":")
5961
6062
:return: a one-dimension dictionary with keys combined
@@ -91,7 +93,7 @@ def benchmark(ref_samples: list[str], samples: list[str]) -> "pd.DataFrame":
9193
flat_stats["expected"] = r
9294
flat_stats["actual"] = s
9395
results.append(flat_stats)
94-
except Exception:
96+
except Exception as exc:
9597
reason = """
9698
[Error]
9799
Reason: %s
@@ -107,7 +109,7 @@ def benchmark(ref_samples: list[str], samples: list[str]) -> "pd.DataFrame":
107109
r,
108110
s,
109111
)
110-
raise SystemExit(reason)
112+
raise SystemExit(reason) from exc
111113

112114
return pd.DataFrame(results)
113115

@@ -209,7 +211,9 @@ def compute_stats(
209211
}
210212

211213

212-
def _binary_representation(txt: str, verbose: bool = False) -> "np.ndarray":
214+
def _binary_representation(
215+
txt: str, verbose: bool = False
216+
) -> "NDArray[np.int8]":
213217
"""Transform text into {0, 1} sequence.
214218
215219
where (1) indicates that the corresponding character is the beginning of
@@ -219,7 +223,7 @@ def _binary_representation(txt: str, verbose: bool = False) -> "np.ndarray":
219223
:param bool verbose: for debugging purposes
220224
221225
:return: {0, 1} sequence
222-
:rtype: np.ndarray
226+
:rtype: numpy.typing.NDArray[numpy.int8]
223227
"""
224228
import numpy as np
225229

@@ -228,7 +232,7 @@ def _binary_representation(txt: str, verbose: bool = False) -> "np.ndarray":
228232
boundary = np.argwhere(chars == SEPARATOR).reshape(-1)
229233
boundary = boundary - np.array(range(boundary.shape[0]))
230234

231-
bin_rept: np.ndarray = np.zeros(len(txt) - boundary.shape[0])
235+
bin_rept = np.zeros(len(txt) - boundary.shape[0], dtype=np.int8)
232236
bin_rept[list(boundary) + [0]] = 1
233237

234238
sample_wo_seps = list(txt.replace(SEPARATOR, ""))
@@ -247,10 +251,13 @@ def _binary_representation(txt: str, verbose: bool = False) -> "np.ndarray":
247251
return bin_rept
248252

249253

250-
def _find_word_boundaries(bin_reps: "np.ndarray") -> list[tuple[int, int]]:
254+
def _find_word_boundaries(
255+
bin_reps: "NDArray[np.int8]",
256+
) -> list[tuple[int, int]]:
251257
"""Find the starting and ending location of each word.
252258
253-
:param numpy.ndarray bin_reps: binary representation of a text
259+
:param numpy.typing.NDArray[numpy.int8] bin_reps: binary representation
260+
of a text
254261
255262
:return: list of tuples (start, end)
256263
:rtype: list[tuple[int, int]]

pythainlp/chunk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
print(parser.parse(tokens_pos))
2626
# output: ['B-NP', 'B-VP', 'I-VP']
2727
"""
28+
2829
from __future__ import annotations
2930

3031
__all__: list[str] = [

pythainlp/chunk/crfchunk.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
# SPDX-FileType: SOURCE
33
# SPDX-License-Identifier: Apache-2.0
44
"""CRF-based Thai phrase structure (chunk) parser."""
5+
56
from __future__ import annotations
67

78
from importlib.resources import as_file, files
8-
from typing import TYPE_CHECKING, Any, Optional, Union
9+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
910

1011
if TYPE_CHECKING:
1112
import types
@@ -130,7 +131,7 @@ def parse(self, token_pos: list[tuple[str, str]]) -> list[str]:
130131
:rtype: list[str]
131132
"""
132133
self.xseq = _extract_features(token_pos)
133-
return self.tagger.tag(self.xseq) # type: ignore[no-any-return]
134+
return cast(list[str], self.tagger.tag(self.xseq))
134135

135136
def __enter__(self) -> CRFChunkParser:
136137
"""Context manager entry."""

pythainlp/coref/core.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@
33
# SPDX-License-Identifier: Apache-2.0
44
from __future__ import annotations
55

6-
from typing import Any, Optional, Union
6+
from typing import Any, Union, cast
77

8-
_MODEL: Optional[Any] = None
8+
_MODEL_CACHE: dict[tuple[str, str], Any] = {}
99

1010

1111
def coreference_resolution(
1212
texts: Union[str, list[str]],
1313
model_name: str = "han-coref-v1.0",
1414
device: str = "cpu",
15-
) -> list[dict]:
15+
) -> list[dict[str, Any]]:
1616
"""Coreference Resolution
1717
1818
:param Union[str, list[str]] texts: list of texts to apply coreference resolution to
1919
:param str model_name: coreference resolution model
2020
:param str device: device for running coreference resolution model on\
2121
("cpu", "cuda", and others)
2222
:return: List of texts with coreference resolution
23-
:rtype: list[dict]
23+
:rtype: list[dict[str, Any]]
2424
2525
:Options for model_name:
2626
* *han-coref-v1.0* - (default) Han-Coref: Thai coreference resolution\
@@ -43,17 +43,18 @@ def coreference_resolution(
4343
# 'clusters': [[(0, 10), (50, 52)]]}
4444
# ]
4545
"""
46-
global _MODEL
4746
if isinstance(texts, str):
4847
texts = [texts]
4948

50-
if _MODEL is None and model_name == "han-coref-v1.0":
49+
model_key = (model_name, device)
50+
if model_key not in _MODEL_CACHE and model_name == "han-coref-v1.0":
5151
from pythainlp.coref.han_coref import HanCoref
5252

53-
_MODEL = HanCoref(device=device)
53+
_MODEL_CACHE[model_key] = HanCoref(device=device)
5454

55-
if _MODEL:
56-
return _MODEL.predict(texts) # type: ignore[no-any-return]
55+
model = _MODEL_CACHE.get(model_key)
56+
if model is not None:
57+
return cast(list[dict[str, Any]], model.predict(texts))
5758

5859
return [
5960
{"text": text, "clusters_string": [], "clusters": []} for text in texts

pythainlp/corpus/common.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,12 @@ def thai_synonyms() -> dict[str, Union[list[str], list[list[str]]]]:
407407
pos = row.get("pos")
408408
synonym = row.get("synonym")
409409
if not (
410-
word and word.strip()
411-
and pos and pos.strip()
412-
and synonym and synonym.strip()
410+
word
411+
and word.strip()
412+
and pos
413+
and pos.strip()
414+
and synonym
415+
and synonym.strip()
413416
):
414417
warnings.warn(
415418
f"Skipping thai_synonyms entry with missing or empty field(s): {dict(row)!r}",
@@ -420,7 +423,11 @@ def thai_synonyms() -> dict[str, Union[list[str], list[list[str]]]]:
420423
words.append(word)
421424
pos_tags.append(pos)
422425
synonym_groups.append(synonym.split("|"))
423-
_THAI_SYNONYMS = {"word": words, "pos": pos_tags, "synonym": synonym_groups}
426+
_THAI_SYNONYMS = {
427+
"word": words,
428+
"pos": pos_tags,
429+
"synonym": synonym_groups,
430+
}
424431
return _THAI_SYNONYMS
425432

426433

pythainlp/corpus/core.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import zipfile
1414
from functools import lru_cache
1515
from importlib.resources import files
16-
from typing import TYPE_CHECKING
16+
from typing import TYPE_CHECKING, Any, Optional, cast
1717

1818
from pythainlp import __version__
1919
from pythainlp.corpus import corpus_db_path, corpus_db_url, corpus_path
@@ -26,7 +26,6 @@
2626

2727
if TYPE_CHECKING:
2828
from http.client import HTTPMessage, HTTPResponse
29-
from typing import Any, Optional
3029

3130
_USER_AGENT: str = (
3231
f"PyThaiNLP/{__version__} "
@@ -50,7 +49,9 @@ def __init__(self, response: HTTPResponse) -> None:
5049
def json(self) -> dict[str, Any]:
5150
"""Parse JSON content from response."""
5251
try:
53-
return json.loads(self._content.decode("utf-8")) # type: ignore[no-any-return]
52+
return cast(
53+
dict[str, Any], json.loads(self._content.decode("utf-8"))
54+
)
5455
except (json.JSONDecodeError, UnicodeDecodeError) as err:
5556
raise ValueError(f"Failed to parse JSON response: {err}") from err
5657

@@ -98,16 +99,15 @@ def get_corpus_db_detail(name: str, version: str = "") -> dict[str, Any]:
9899
if not version:
99100
for corpus in local_db["_default"].values():
100101
if corpus["name"] == name:
101-
return corpus # type: ignore[no-any-return]
102+
return cast(dict[str, Any], corpus)
102103
else:
103104
for corpus in local_db["_default"].values():
104105
if corpus["name"] == name and corpus["version"] == version:
105-
return corpus # type: ignore[no-any-return]
106+
return cast(dict[str, Any], corpus)
106107

107108
return {}
108109

109110

110-
111111
@lru_cache(maxsize=None)
112112
def get_corpus(filename: str, comments: bool = True) -> frozenset[str]:
113113
"""Read corpus data from file and return a frozenset.
@@ -221,7 +221,7 @@ def _load_default_db() -> dict[str, Any]:
221221
corpus_files = files("pythainlp.corpus")
222222
default_db_file = corpus_files.joinpath("default_db.json")
223223
text = default_db_file.read_text(encoding="utf-8-sig")
224-
return json.loads(text) # type: ignore[no-any-return]
224+
return cast(dict[str, Any], json.loads(text))
225225

226226

227227
def get_corpus_default_db(name: str, version: str = "") -> Optional[str]:
@@ -848,7 +848,6 @@ def remove(name: str) -> bool:
848848
return False
849849

850850

851-
852851
def make_safe_directory_name(name: str) -> str:
853852
"""Make safe directory name
854853
@@ -921,4 +920,4 @@ def get_hf_hub(repo_id: str, filename: str = "") -> str:
921920
output_path = snapshot_download(
922921
repo_id=repo_id, local_dir=root_project
923922
)
924-
return output_path # type: ignore[no-any-return]
923+
return str(output_path)

0 commit comments

Comments
 (0)