Skip to content

Commit b9cbff4

Browse files
authored
Merge pull request #1262 from PyThaiNLP/copilot/add-type-hints-to-submodules-another-one
Add type hints to foundation submodules (tools, cli, corpus)
2 parents 924a915 + bf0baa4 commit b9cbff4

15 files changed

Lines changed: 182 additions & 54 deletions

File tree

pyproject.toml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,81 @@ basepython = "python"
363363
deps = ["ruff"]
364364
commands = [["ruff", "check", "pythainlp"], ["ruff", "format", "--check", "pythainlp"]]
365365
skip_install = true
366+
367+
# Mypy configuration
368+
[tool.mypy]
369+
python_version = "3.9"
370+
warn_return_any = true
371+
warn_unused_configs = true
372+
disallow_untyped_defs = false
373+
disallow_incomplete_defs = false
374+
check_untyped_defs = true
375+
disallow_untyped_decorators = false
376+
no_implicit_optional = true
377+
warn_redundant_casts = true
378+
warn_unused_ignores = true
379+
warn_no_return = true
380+
warn_unreachable = true
381+
strict_equality = true
382+
show_error_codes = true
383+
show_column_numbers = true
384+
pretty = true
385+
386+
# Per-module options for stricter checking on foundation modules
387+
[[tool.mypy.overrides]]
388+
module = [
389+
"pythainlp.tools.*",
390+
"pythainlp.cli.*",
391+
]
392+
disallow_untyped_defs = true
393+
disallow_incomplete_defs = true
394+
395+
# Ignore missing imports for optional dependencies
396+
[[tool.mypy.overrides]]
397+
module = [
398+
"attacut.*",
399+
"bpemb.*",
400+
"budoux.*",
401+
"deepcut.*",
402+
"emoji.*",
403+
"epitran.*",
404+
"esupar.*",
405+
"fairseq.*",
406+
"fastai.*",
407+
"fastcoref.*",
408+
"gensim.*",
409+
"huggingface_hub.*",
410+
"icu.*",
411+
"khamyo.*",
412+
"khanaa.*",
413+
"multiel.*",
414+
"nlpo3.*",
415+
"nltk.*",
416+
"numpy.*",
417+
"onnxruntime.*",
418+
"oskut.*",
419+
"pandas.*",
420+
"panphon.*",
421+
"phunspell.*",
422+
"pycrfsuite.*",
423+
"pyicu.*",
424+
"sacremoses.*",
425+
"sefr_cut.*",
426+
"sentencepiece.*",
427+
"sentence_transformers.*",
428+
"spacy.*",
429+
"spacy_thai.*",
430+
"ssg.*",
431+
"symspellpy.*",
432+
"thai_nner.*",
433+
"tltk.*",
434+
"torch.*",
435+
"tqdm.*",
436+
"transformers.*",
437+
"ufal.chu_liu_edmonds.*",
438+
"word2word.*",
439+
"wtpsplit.*",
440+
"wunsen.*",
441+
"yaml.*",
442+
]
443+
ignore_missing_imports = true

pythainlp/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
CLI_NAME = "thainlp"
2323

2424

25-
def make_usage(command: str) -> dict:
25+
def make_usage(command: str) -> dict[str, str]:
2626
prog = f"{CLI_NAME} {command}"
2727

2828
return {"prog": prog, "usage": f"{prog} [options]"}

pythainlp/cli/benchmark.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@
33
# SPDX-FileType: SOURCE
44
# SPDX-License-Identifier: Apache-2.0
55

6+
from __future__ import annotations
7+
68
import argparse
79
import json
810
import os
11+
from typing import TYPE_CHECKING
912

1013
from pythainlp import cli
1114
from pythainlp.tools import safe_print
1215

16+
if TYPE_CHECKING:
17+
from collections.abc import Sequence
18+
1319

14-
def _read_file(path):
20+
def _read_file(path: str) -> list[str]:
1521
with open(path, encoding="utf-8") as f:
1622
lines = (r.strip() for r in f.readlines())
1723
return list(lines)
1824

1925

2026
class App:
21-
def __init__(self, argv):
27+
def __init__(self, argv: Sequence[str]) -> None:
2228
parser = argparse.ArgumentParser(
2329
prog="benchmark",
2430
description=(
@@ -45,8 +51,8 @@ def __init__(self, argv):
4551

4652

4753
class WordTokenizationBenchmark:
48-
def __init__(self, name, argv):
49-
parser = argparse.ArgumentParser(**cli.make_usage("benchmark " + name))
54+
def __init__(self, name: str, argv: Sequence[str]) -> None:
55+
parser = argparse.ArgumentParser(**cli.make_usage("benchmark " + name)) # type: ignore[arg-type]
5056

5157
parser.add_argument(
5258
"--input-file",

pythainlp/cli/data.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44
"""Command line for PyThaiNLP's dataset/corpus management.
55
"""
66

7+
from __future__ import annotations
8+
79
import argparse
10+
from typing import TYPE_CHECKING
811

912
from pythainlp import corpus
1013
from pythainlp.tools import get_pythainlp_data_path
1114

15+
if TYPE_CHECKING:
16+
from collections.abc import Sequence
17+
1218

1319
class App:
14-
def __init__(self, argv):
20+
def __init__(self, argv: Sequence[str]) -> None:
1521
parser = argparse.ArgumentParser(
1622
prog="data",
1723
description="Manage dataset/corpus.",
@@ -43,7 +49,7 @@ def __init__(self, argv):
4349
args = parser.parse_args(argv[2:3])
4450
getattr(self, args.subcommand)(argv)
4551

46-
def get(self, argv):
52+
def get(self, argv: Sequence[str]) -> None:
4753
parser = argparse.ArgumentParser(
4854
description="Download a dataset",
4955
usage="thainlp data get <dataset_name>",
@@ -59,7 +65,7 @@ def get(self, argv):
5965
else:
6066
print("Not found.")
6167

62-
def rm(self, argv):
68+
def rm(self, argv: Sequence[str]) -> None:
6369
parser = argparse.ArgumentParser(
6470
description="Remove a dataset",
6571
usage="thainlp data rm <dataset_name>",
@@ -75,7 +81,7 @@ def rm(self, argv):
7581
else:
7682
print("Not found.")
7783

78-
def info(self, argv):
84+
def info(self, argv: Sequence[str]) -> None:
7985
parser = argparse.ArgumentParser(
8086
description="Print information about a dataset",
8187
usage="thainlp data info <dataset_name>",
@@ -92,14 +98,14 @@ def info(self, argv):
9298
else:
9399
print("Not found.")
94100

95-
def catalog(self, argv):
101+
def catalog(self, argv: Sequence[str]) -> None:
96102
"""Print dataset/corpus available for download."""
97-
corpus_db = corpus.get_corpus_db(corpus.corpus_db_url())
98-
corpus_db = corpus_db.json()
99-
corpus_names = sorted(corpus_db.keys())
103+
corpus_db_response = corpus.get_corpus_db(corpus.corpus_db_url())
104+
corpus_db_dict: dict[str, dict[str, str]] = corpus_db_response.json() # type: ignore[union-attr]
105+
corpus_names = sorted(corpus_db_dict.keys())
100106
print("Dataset/corpus available for download:")
101107
for name in corpus_names:
102-
print(f"- {name} {corpus_db[name]['latest_version']}", end="")
108+
print(f"- {name} {corpus_db_dict[name]['latest_version']}", end="")
103109
corpus_info = corpus.get_corpus_db_detail(name)
104110
if corpus_info:
105111
print(f" (Local: {corpus_info['version']})")
@@ -111,6 +117,6 @@ def catalog(self, argv):
111117
"Example: thainlp data get crfcut\n"
112118
)
113119

114-
def path(self, argv):
120+
def path(self, argv: Sequence[str]) -> None:
115121
"""Print path of local dataset."""
116122
print(get_pythainlp_data_path())

pythainlp/cli/misspell.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
# SPDX-FileType: SOURCE
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from __future__ import annotations
6+
57
import argparse
68
import os
79
import random
10+
from typing import TYPE_CHECKING
811

912
from pythainlp.tools.misspell import misspell
1013

14+
if TYPE_CHECKING:
15+
from collections.abc import Sequence
16+
1117

1218
class App:
13-
def __init__(self, argv):
19+
def __init__(self, argv: Sequence[str]) -> None:
1420
parser = argparse.ArgumentParser(
1521
prog="misspell",
1622
description="Generate misspelled texts from a given file.",

pythainlp/cli/soundex.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66
It takes input text from the command line.
77
"""
88

9+
from __future__ import annotations
10+
911
import argparse
12+
from typing import TYPE_CHECKING
1013

1114
from pythainlp.soundex import DEFAULT_SOUNDEX_ENGINE, soundex
1215
from pythainlp.tools import safe_print
1316

17+
if TYPE_CHECKING:
18+
from collections.abc import Sequence
19+
1420

1521
class App:
16-
def __init__(self, argv):
22+
def __init__(self, argv: Sequence[str]) -> None:
1723
parser = argparse.ArgumentParser(
1824
prog="soundex",
1925
description="Convert a text to its sound-based index.",

pythainlp/cli/tag.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@
44
"""Command line for PyThaiNLP's taggers.
55
"""
66

7+
from __future__ import annotations
8+
79
import argparse
10+
from typing import TYPE_CHECKING
811

912
from pythainlp import cli
1013
from pythainlp.tag import pos_tag
1114
from pythainlp.tools import safe_print
1215

16+
if TYPE_CHECKING:
17+
from collections.abc import Callable, Sequence
18+
1319

1420
class SubAppBase:
15-
def __init__(self, name, argv):
16-
parser = argparse.ArgumentParser(**cli.make_usage("tag " + name))
21+
separator: str
22+
run: Callable[[list[str]], list[tuple[str, str]]]
23+
24+
def __init__(self, name: str, argv: Sequence[str]) -> None:
25+
parser = argparse.ArgumentParser(**cli.make_usage("tag " + name)) # type: ignore[arg-type]
1726
parser.add_argument(
1827
"text",
1928
type=str,
@@ -39,15 +48,15 @@ def __init__(self, name, argv):
3948

4049

4150
class POSTaggingApp(SubAppBase):
42-
def __init__(self, *args, **kwargs):
51+
def __init__(self, *args: str, **kwargs: str) -> None:
4352
self.separator = "|"
4453
self.run = pos_tag
4554

4655
super().__init__(*args, **kwargs)
4756

4857

4958
class App:
50-
def __init__(self, argv):
59+
def __init__(self, argv: Sequence[str]) -> None:
5160
parser = argparse.ArgumentParser(
5261
prog="tag",
5362
description="Annotate a text with linguistic information",
@@ -72,6 +81,6 @@ def __init__(self, argv):
7281
argv = argv[3:]
7382

7483
if tag_type == "pos":
75-
POSTaggingApp("Part-of-Speech tagging", argv)
84+
POSTaggingApp("Part-of-Speech tagging", argv) # type: ignore[arg-type]
7685
else:
7786
print(f"Tag type not available: {tag_type}")

pythainlp/cli/tokenize.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
# SPDX-License-Identifier: Apache-2.0
44
"""Command line for PyThaiNLP's tokenizers."""
55

6+
from __future__ import annotations
7+
68
import argparse
9+
from typing import TYPE_CHECKING
710

811
from pythainlp import cli
912
from pythainlp.tokenize import (
@@ -16,15 +19,22 @@
1619
)
1720
from pythainlp.tools import safe_print
1821

22+
if TYPE_CHECKING:
23+
from collections.abc import Callable, Sequence
24+
1925
DEFAULT_SENT_TOKEN_SEPARATOR = "@@" # noqa: S105
2026
DEFAULT_SUBWORD_TOKEN_SEPARATOR = "/" # noqa: S105
2127
DEFAULT_SYLLABLE_TOKEN_SEPARATOR = "~" # noqa: S105
2228
DEFAULT_WORD_TOKEN_SEPARATOR = "|" # noqa: S105
2329

2430

2531
class SubAppBase:
26-
def __init__(self, name, argv):
27-
parser = argparse.ArgumentParser(**cli.make_usage("tokenize " + name))
32+
separator: str
33+
algorithm: str
34+
run: Callable[..., list[str]]
35+
36+
def __init__(self, name: str, argv: Sequence[str]) -> None:
37+
parser = argparse.ArgumentParser(**cli.make_usage("tokenize " + name)) # type: ignore[arg-type]
2838
parser.add_argument(
2939
"text",
3040
type=str,
@@ -74,7 +84,7 @@ def __init__(self, name, argv):
7484

7585

7686
class WordTokenizationApp(SubAppBase):
77-
def __init__(self, *args, **kwargs):
87+
def __init__(self, *args: str, **kwargs: str) -> None:
7888
self.keep_whitespace = True
7989
self.algorithm = DEFAULT_WORD_TOKENIZE_ENGINE
8090
self.separator = DEFAULT_WORD_TOKEN_SEPARATOR
@@ -83,7 +93,7 @@ def __init__(self, *args, **kwargs):
8393

8494

8595
class SentenceTokenizationApp(SubAppBase):
86-
def __init__(self, *args, **kwargs):
96+
def __init__(self, *args: str, **kwargs: str) -> None:
8797
self.keep_whitespace = True
8898
self.algorithm = DEFAULT_SENT_TOKENIZE_ENGINE
8999
self.separator = DEFAULT_SENT_TOKEN_SEPARATOR
@@ -92,7 +102,7 @@ def __init__(self, *args, **kwargs):
92102

93103

94104
class SubwordTokenizationApp(SubAppBase):
95-
def __init__(self, *args, **kwargs):
105+
def __init__(self, *args: str, **kwargs: str) -> None:
96106
self.keep_whitespace = True
97107
self.algorithm = DEFAULT_SUBWORD_TOKENIZE_ENGINE
98108
self.separator = DEFAULT_SUBWORD_TOKEN_SEPARATOR
@@ -101,7 +111,7 @@ def __init__(self, *args, **kwargs):
101111

102112

103113
class App:
104-
def __init__(self, argv):
114+
def __init__(self, argv: Sequence[str]) -> None:
105115
parser = argparse.ArgumentParser(
106116
prog="tokenize",
107117
description="Break a text into small units (tokens).",
@@ -137,10 +147,10 @@ def __init__(self, argv):
137147

138148
argv = argv[3:]
139149
if token_type.startswith("w"):
140-
WordTokenizationApp("word", argv)
150+
WordTokenizationApp("word", argv) # type: ignore[arg-type]
141151
elif token_type.startswith("su"):
142-
SubwordTokenizationApp("subword", argv)
152+
SubwordTokenizationApp("subword", argv) # type: ignore[arg-type]
143153
elif token_type.startswith("se"):
144-
SentenceTokenizationApp("sent", argv)
154+
SentenceTokenizationApp("sent", argv) # type: ignore[arg-type]
145155
else:
146156
safe_print(f"Token type not available: {token_type}")

0 commit comments

Comments
 (0)