Skip to content

Commit 5a478b9

Browse files
authored
Add mypy (#128)
Check the code using mypy --strict Fix mypy warnings Fix some of the actual bugs Test on Python 3.13
1 parent 3565e07 commit 5a478b9

21 files changed

+226
-103
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
- "3.10"
1717
- "3.11"
1818
- "3.12"
19+
- "3.13"
1920

2021
steps:
2122
- uses: actions/checkout@v4
@@ -36,9 +37,15 @@ jobs:
3637
- name: Lint with ruff
3738
run: |
3839
poetry run ruff check . --statistics
40+
41+
- name: Check with mypy
42+
run: |
43+
poetry run mypy . --strict
44+
3945
- name: Test with pytest
4046
run: |
4147
poetry run pytest --cov=./
48+
4249
- name: Upload coverage data to coveralls.io
4350
run: |
4451
pip install coveralls

df_translation_toolkit/convert/objects_po_to_csv.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections import defaultdict
22
from collections.abc import Iterable, Iterator
33
from pathlib import Path
4-
from typing import TextIO
4+
from typing import IO, TextIO
55

66
import typer
77
from loguru import logger
@@ -21,10 +21,9 @@ def get_translations_from_tag_parts(
2121
) -> Iterator[tuple[str, str]]:
2222
tag_translations = defaultdict(list)
2323

24-
prev_original = None
25-
prev_translation = None
24+
prev_original = ""
25+
prev_translation = ""
2626
for original, translation in zip(original_parts, translation_parts, strict=False):
27-
original: str
2827
if all_caps(original) or original.isdecimal():
2928
if original == "STP" and translation != original and not all_caps(translation):
3029
tag_translations[prev_original + "s"].append(translation)
@@ -53,7 +52,7 @@ def get_translations_from_tag(original_tag: str, translation_tag: str) -> Iterat
5352
raise ValidationException(validation_problems) # pass warnings
5453

5554

56-
def prepare_dictionary(dictionary: Iterable[tuple[str, str]], errors_file: TextIO) -> Iterable[tuple[str, str]]:
55+
def prepare_dictionary(dictionary: Iterable[tuple[str, str]], errors_file: TextIO | None) -> Iterable[tuple[str, str]]:
5756
for original_string_tag, translation_tag in dictionary:
5857
if original_string_tag and translation_tag and translation_tag != original_string_tag:
5958
try:
@@ -66,7 +65,7 @@ def prepare_dictionary(dictionary: Iterable[tuple[str, str]], errors_file: TextI
6665
print(error_text, end="\n\n", file=errors_file)
6766

6867

69-
def convert(po_file: TextIO, csv_file: TextIO, error_file: TextIO | None = None) -> None:
68+
def convert(po_file: TextIO, csv_file: IO[str], error_file: TextIO | None = None) -> None:
7069
dictionary = simple_read_po(po_file)
7170
csv_writer = csv_utils.writer(csv_file)
7271

df_translation_toolkit/create_mod/from_template.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,22 @@ def create_info(info_file: Path, destination_encoding: str, language: str) -> No
9797

9898
def get_dictionaries(translation_path: Path, language: str) -> Dictionaries:
9999
po_files: dict[str, Path] = {}
100-
for po_file in ["objects", "text_set"]:
100+
for po_file_type in ["objects", "text_set"]:
101101
mtime = 0
102-
for file in translation_path.glob(f"*{po_file}*{language}.po"):
102+
for file in translation_path.glob(f"*{po_file_type}*{language}.po"):
103103
if file.is_file() and file.stat().st_mtime > mtime:
104-
po_files[po_file] = file
104+
po_files[po_file_type] = file
105105

106-
if po_file not in po_files:
107-
msg = f"Unable to find {po_file} po file for language {language}"
106+
if po_file_type not in po_files:
107+
msg = f"Unable to find {po_file_type} po file for language {language}"
108108
raise ValueError(msg)
109109

110-
with open(po_files["objects"], encoding="utf-8") as pofile:
110+
with open(po_files["objects"], encoding="utf-8") as po_file:
111111
dictionary_object: Mapping[tuple[str, str | None], str] = {
112-
(item.id, item.context): item.string for item in read_po(pofile)
112+
(str(item.id), str(item.context) if item.context else None): str(item.string) for item in read_po(po_file)
113113
}
114114
with open(po_files["text_set"], encoding="utf-8") as po_file:
115-
dictionary_textset: Mapping[str, str] = {item.id: item.string for item in read_po(po_file) if item.id}
115+
dictionary_textset: Mapping[str, str] = {str(item.id): str(item.string) for item in read_po(po_file) if item.id}
116116
return Dictionaries(language.lower(), dictionary_object, dictionary_textset)
117117

118118

df_translation_toolkit/create_pot/from_steam_text.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from collections import defaultdict
22
from collections.abc import Iterable
33
from pathlib import Path
4+
from typing import Any
45

56
import typer
67
from loguru import logger
78

89
from df_translation_toolkit.create_pot.from_raw_objects import extract_from_raw_file
910
from df_translation_toolkit.parse.parse_raws import split_tag, tokenize_raw_file
1011
from df_translation_toolkit.parse.parse_text_set import extract_from_vanilla_text
11-
from df_translation_toolkit.utils.po_utils import save_pot
12+
from df_translation_toolkit.utils.po_utils import TranslationItem, save_pot
1213

1314

1415
def traverse_vanilla_directories(vanilla_path: Path) -> Iterable[Path]:
@@ -19,7 +20,7 @@ def traverse_vanilla_directories(vanilla_path: Path) -> Iterable[Path]:
1920
yield objects
2021

2122

22-
def get_raw_object_type(file_name: Path, source_encoding: str) -> str:
23+
def get_raw_object_type(file_name: Path, source_encoding: str) -> str | None:
2324
with file_name.open(encoding=source_encoding) as file:
2425
for item in tokenize_raw_file(file):
2526
if item.is_tag:
@@ -33,7 +34,7 @@ def get_raw_object_type(file_name: Path, source_encoding: str) -> str:
3334
return None
3435

3536

36-
def get_translatable_strings(file_path: Path, source_encoding: str) -> tuple[str, Iterable[str]] | None:
37+
def get_translatable_strings(file_path: Path, source_encoding: str) -> tuple[str, Iterable[TranslationItem]] | None:
3738
object_type = get_raw_object_type(file_path, source_encoding)
3839
if object_type in dont_translate:
3940
return None
@@ -48,7 +49,7 @@ def get_translatable_strings(file_path: Path, source_encoding: str) -> tuple[str
4849
return key, data
4950

5051

51-
def iterable_is_empty(iterable: Iterable) -> bool:
52+
def iterable_is_empty(iterable: Iterable[Any]) -> bool:
5253
iterator = iter(iterable)
5354
try:
5455
next(iterator)
@@ -75,7 +76,7 @@ def main(vanilla_path: Path, destination_path: Path, source_encoding: str = "cp4
7576
msg = "Destination path doesn't exist"
7677
raise ValueError(msg)
7778

78-
results = defaultdict(list)
79+
results: dict[str, list[TranslationItem]] = defaultdict(list)
7980

8081
for directory in traverse_vanilla_directories(vanilla_path):
8182
logger.info(directory.relative_to(vanilla_path))
@@ -91,8 +92,8 @@ def main(vanilla_path: Path, destination_path: Path, source_encoding: str = "cp4
9192
for group, data in results.items():
9293
if data:
9394
pot_path = destination_path / (group.lower() + ".pot")
94-
with pot_path.open("wb") as file_path:
95-
save_pot(file_path, data)
95+
with pot_path.open("wb") as pot_file:
96+
save_pot(pot_file, data)
9697

9798

9899
if __name__ == "__main__":

df_translation_toolkit/parse/parse_raws.py

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ class FilePartInfo(NamedTuple):
6464
line_number: int
6565
translatable: bool
6666
context: str | None
67-
text: str | None = None
68-
tag: str | None = None
69-
tag_parts: Sequence[str] | None = None
67+
text: str = ""
68+
tag: str = ""
69+
tag_parts: Sequence[str] = ()
7070

7171

7272
def parse_raw_file(file: Iterable[str]) -> Iterator[FilePartInfo]:
@@ -75,47 +75,50 @@ def parse_raw_file(file: Iterable[str]) -> Iterator[FilePartInfo]:
7575
for token in tokenize_raw_file(file):
7676
if not token.is_tag:
7777
yield FilePartInfo(line_number=token.line_number, translatable=False, context=context, text=token.text)
78+
continue
79+
80+
tag = token.text
81+
tag_parts = split_tag(tag)
82+
83+
if tag_parts[0] == "OBJECT":
84+
object_name = tag_parts[1]
85+
yield FilePartInfo(line_number=token.line_number, translatable=False, context=context, tag=tag)
86+
elif object_name and (
87+
tag_parts[0] == object_name
88+
or (object_name in {"ITEM", "BUILDING"} and tag_parts[0].startswith(object_name))
89+
or object_name.endswith("_" + tag_parts[0])
90+
):
91+
context = ":".join(tag_parts)
92+
yield FilePartInfo(token.line_number, translatable=False, context=context, tag=tag)
7893
else:
79-
tag = token.text
80-
tag_parts = split_tag(tag)
81-
82-
if tag_parts[0] == "OBJECT":
83-
object_name = tag_parts[1]
84-
yield FilePartInfo(line_number=token.line_number, translatable=False, context=context, tag=tag)
85-
elif object_name and (
86-
tag_parts[0] == object_name
87-
or (object_name in {"ITEM", "BUILDING"} and tag_parts[0].startswith(object_name))
88-
or object_name.endswith("_" + tag_parts[0])
89-
):
90-
context = ":".join(tag_parts)
91-
yield FilePartInfo(token.line_number, translatable=False, context=context, tag=tag)
92-
else:
93-
yield FilePartInfo(token.line_number, translatable=True, context=context, tag=tag, tag_parts=tag_parts)
94+
yield FilePartInfo(token.line_number, translatable=True, context=context, tag=tag, tag_parts=tag_parts)
9495

9596

9697
def extract_translatables_from_raws(file: Iterable[str]) -> Iterator[TranslationItem]:
97-
translation_keys: set[tuple[str, tuple[str, ...]]] = set()
98+
translation_keys: set[tuple[str | None, tuple[str, ...]]] = set()
9899

99100
for item in parse_raw_file(file):
100-
if item.translatable:
101-
tag_parts = item.tag_parts
102-
if (
103-
"TILE" not in tag_parts[0]
104-
and any(is_translatable(s) for s in tag_parts[1:])
105-
and (item.context, tuple(tag_parts)) not in translation_keys # Don't add duplicate items to translate
106-
):
107-
if not is_translatable(tag_parts[-1]):
108-
last = last_suitable(tag_parts, is_translatable)
109-
tag_parts = tag_parts[:last]
110-
tag_parts.append("") # Add an empty element to the tag to mark the tag as not completed
111-
translation_keys.add((item.context, tuple(tag_parts)))
112-
yield TranslationItem(context=item.context, text=join_tag(tag_parts), line_number=item.line_number)
101+
if not item.translatable:
102+
continue
103+
104+
tag_parts = item.tag_parts
105+
if (
106+
"TILE" not in tag_parts[0]
107+
and any(is_translatable(s) for s in tag_parts[1:])
108+
and (item.context, tuple(tag_parts)) not in translation_keys # Don't add duplicate items to translate
109+
):
110+
if not is_translatable(tag_parts[-1]):
111+
last = last_suitable(tag_parts, is_translatable)
112+
tag_parts = list(tag_parts[:last])
113+
tag_parts.append("") # Add an empty element to the tag to mark the tag as not completed
114+
translation_keys.add((item.context, tuple(tag_parts)))
115+
yield TranslationItem(context=item.context, text=join_tag(tag_parts), line_number=item.line_number)
113116

114117

115118
def get_from_dict_with_context(
116119
dictionary: Mapping[tuple[str, str | None], str],
117120
key: str,
118-
context: str,
121+
context: str | None,
119122
) -> str | None:
120123
if (key, context) in dictionary:
121124
return dictionary[(key, context)]
@@ -128,7 +131,7 @@ def get_from_dict_with_context(
128131

129132
def get_tag_translation(dictionary: Mapping[tuple[str, str | None], str], item: FilePartInfo) -> str:
130133
tag = item.tag
131-
tag_parts = item.tag_parts
134+
tag_parts = list(item.tag_parts)
132135
context = item.context
133136
new_tag = get_from_dict_with_context(dictionary, tag, context)
134137

@@ -160,13 +163,13 @@ def translate_raw_file(file: Iterable[str], dictionary: Mapping[tuple[str, str |
160163
modified_line_parts = []
161164
prev_line_number = item.line_number
162165

163-
if item.text is not None:
166+
if item.text:
164167
modified_line_parts.append(item.text)
165-
elif not item.translatable or not any(is_translatable(s) for s in item.tag_parts[1:]):
166-
modified_line_parts.append(item.tag)
167-
else:
168+
elif item.translatable and any(is_translatable(s) for s in item.tag_parts[1:]):
168169
translation = get_tag_translation(dictionary, item)
169170
modified_line_parts.append(translation)
171+
else:
172+
modified_line_parts.append(item.tag)
170173

171174
if modified_line_parts:
172175
yield "".join(modified_line_parts)

df_translation_toolkit/translate/translate_plain_text.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def translate_plain_text_file(
1919
) -> Iterator[str]:
2020
with source_file_path.open() as source_file, destination_file_path.open("w", encoding=encoding) as destination_file:
2121
yield destination_file_path.name
22-
for text_block, _, _ in parse_plain_text_file(source_file, join_paragraphs):
22+
for text_block, _, _ in parse_plain_text_file(source_file, join_paragraphs=join_paragraphs):
2323
clean_text_block = text_block.rstrip("\n")
2424
if clean_text_block in dictionary:
2525
translation = dictionary[clean_text_block]
@@ -38,12 +38,18 @@ def translate_plain_text(
3838
join_paragraphs: bool = True,
3939
) -> Iterator[str]:
4040
with po_filename.open("r", encoding="utf-8") as po_file:
41-
dictionary = {item.id: item.string for item in read_po(po_file) if item.id}
41+
dictionary = {str(item.id): str(item.string) for item in read_po(po_file) if item.id}
4242

4343
for item in Path(path).rglob("*.txt"):
4444
if item.is_file():
4545
with backup(item) as backup_file:
46-
yield from translate_plain_text_file(backup_file, item, dictionary, encoding, join_paragraphs)
46+
yield from translate_plain_text_file(
47+
backup_file,
48+
item,
49+
dictionary,
50+
encoding,
51+
join_paragraphs=join_paragraphs,
52+
)
4753

4854

4955
def main(
@@ -53,7 +59,7 @@ def main(
5359
split: bool = False, # noqa: FBT001, FBT002
5460
) -> None:
5561
join_paragraphs = not split
56-
for filename in translate_plain_text(po_filename, path, encoding, join_paragraphs):
62+
for filename in translate_plain_text(po_filename, path, encoding, join_paragraphs=join_paragraphs):
5763
print(filename, file=sys.stderr)
5864

5965

df_translation_toolkit/translate/translate_raws.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@ def translate_single_raw_file(
3535

3636
def translate_raws(po_filename: Path, path: Path, encoding: str) -> Iterator[str]:
3737
with po_filename.open("r", encoding="utf-8") as pofile:
38-
dictionary = {(item.id, item.context): item.string for item in read_po(pofile)}
38+
dictionary = {
39+
(str(item.id), str(item.context) if item.context else None): str(item.string) for item in read_po(pofile)
40+
}
3941

4042
for file_path in path.glob("*.txt"):
4143
if file_path.is_file() and not file_path.name.startswith("language_"):
4244
with backup(file_path) as bak_name:
4345
yield from translate_single_raw_file(bak_name, file_path, dictionary, encoding)
4446

4547

46-
def main(po_filename: Path, path: Path, encoding: str) -> str:
48+
def main(po_filename: Path, path: Path, encoding: str) -> None:
4749
for filename in translate_raws(po_filename, path, encoding):
4850
print(filename, file=sys.stderr)
4951

df_translation_toolkit/utils/csv_utils.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import csv
2-
from collections.abc import Iterator
2+
from collections.abc import Iterable, Iterator
33
from pathlib import Path
4-
from typing import TextIO
4+
from typing import IO, Protocol, TextIO
55

66

7-
def writer(file: TextIO, **kwargs): # noqa: ANN201
7+
class CSVWriter(Protocol):
8+
def writerow(self, row: list[str]) -> None: ...
9+
def writerows(self, rows: Iterable[list[str]]) -> None: ...
10+
11+
12+
class CSVReader(Protocol):
13+
def __iter__(self) -> Iterator[list[str]]: ...
14+
15+
16+
def writer(file: IO[str], **kwargs) -> CSVWriter: # type: ignore[no-untyped-def]
817
return csv.writer(file, dialect="unix", lineterminator="\r\n", **kwargs)
918

1019

11-
def reader(file: TextIO, **kwargs): # noqa: ANN201
20+
def reader(file: TextIO, **kwargs) -> CSVReader: # type: ignore[no-untyped-def]
1221
return csv.reader(file, dialect="unix", lineterminator="\r\n", **kwargs)
1322

1423

df_translation_toolkit/utils/df_ignore_string_rules.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class IgnoringRuleRegistry:
1111
all_rules: dict[str, Callable[[str], bool]]
1212

13-
def __init__(self) -> str:
13+
def __init__(self) -> None:
1414
self.all_rules = {}
1515

1616
def register(self, function: Callable[[str], bool]) -> Callable[[str], bool]:
@@ -79,7 +79,11 @@ def ignore_paths(string: str) -> bool:
7979

8080
@rules.register
8181
def ignore_tags(string: str) -> bool:
82-
return len(string) >= MINMAL_TAG_LENGTH and string not in ignore_tags_exceptions and re.fullmatch("[A-Z_]+", string)
82+
return (
83+
len(string) >= MINMAL_TAG_LENGTH
84+
and string not in ignore_tags_exceptions
85+
and bool(re.fullmatch("[A-Z_]+", string))
86+
)
8387

8488

8589
@rules.register
@@ -458,7 +462,7 @@ def ignore_by_blacklisted_substrings(string: str) -> bool:
458462
return any(substring in string for substring in blacklisted_substrings)
459463

460464

461-
def all_ignore_rules(string: str) -> str:
465+
def all_ignore_rules(string: str) -> str | None:
462466
return rules.check_ignore(string)
463467

464468

0 commit comments

Comments
 (0)