diff --git a/flashcards/constants.py b/flashcards/constants.py index 8b0a0dd3f5..43e3c49e6f 100644 --- a/flashcards/constants.py +++ b/flashcards/constants.py @@ -2,11 +2,10 @@ import json import os import re +import typing import deck -import enforcer import field -import type_enforced import utils @@ -22,14 +21,12 @@ DICT_WIDTH = "1000px" -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def crum( deck_name: str, deck_id: int, dialect_cols: list[str], force_front: bool = True, ) -> deck.deck: - @type_enforced.Enforcer(enabled=enforcer.ENABLED) def roots_col( col_name: str, line_br: bool = False, @@ -66,7 +63,6 @@ def root_appendix( # This replaces all Coptic words, regardless of whether they # represent plain text. Coptic text that occurs inside a tag (for example # as a tag property) would still get wrapped inside this tag. - @type_enforced.Enforcer(enabled=enforcer.ENABLED) def cdo(text: str) -> str: return COPTIC_WORD_RE.sub( r'\1', @@ -77,19 +73,17 @@ def cdo(text: str) -> str: # This replaces all Greek words, regardless of whether they # represent plain text. Greek text that occurs inside a tag (for example # as a tag property) would still acquire this tag. - @type_enforced.Enforcer(enabled=enforcer.ENABLED) def greek(text: str) -> str: return GREEK_WORD_RE.sub( r'\1', text, ) - @type_enforced.Enforcer(enabled=enforcer.ENABLED) - def create_front() -> field.Field: + def create_front() -> field.field: if len(dialect_cols) == 1: return roots_col(dialect_cols[0], line_br=True, force=False) - def dialect(col: str) -> field.Field: + def dialect(col: str) -> field.field: return field.aon( '', "(", @@ -392,9 +386,7 @@ def _explanatory_alt(path: str) -> str: ) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def copticsite_com(deck_name: str, deck_id: int) -> deck.deck: - @type_enforced.Enforcer(enabled=enforcer.ENABLED) def col( col_name: str, line_br: bool = False, @@ -442,9 +434,7 @@ def col( ) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def kellia(deck_name: str, deck_id: int, basename: str) -> deck.deck: - @type_enforced.Enforcer(enabled=enforcer.ENABLED) def col( col_name: str, line_br: bool = False, @@ -489,7 +479,6 @@ def col( ) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def _dedup(arr: list[int], at_most_once: bool = False) -> list[int]: """ Args: @@ -503,7 +492,7 @@ def _dedup(arr: list[int], at_most_once: bool = False) -> list[int]: """ if at_most_once: return list(dict.fromkeys(arr)) - out = [] + out: list[int] = [] for x in arr: if out and out[-1] == x: continue @@ -511,7 +500,6 @@ def _dedup(arr: list[int], at_most_once: bool = False) -> list[int]: return out -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def _page_numbers(page_ranges: str) -> list[int]: """page_ranges is a comma-separated list of integers or integer ranges, just like what you type when you're using your printer. @@ -519,7 +507,6 @@ def _page_numbers(page_ranges: str) -> list[int]: For example, "1,3-5,8-9" means [1, 3, 4, 5, 8, 9]. """ - @type_enforced.Enforcer(enabled=enforcer.ENABLED) def parse(page_number: str) -> int: page_number = page_number.strip() if page_number[-1] in ["a", "b"]: @@ -543,10 +530,9 @@ def parse(page_number: str) -> int: return out -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class _dir_lister: - def __init__(self, dir: str, get_key: enforcer.Callable) -> None: - self.cache = {} + def __init__(self, dir: str, get_key: typing.Callable) -> None: + self.cache: dict[str, list[str]] = {} if not os.path.exists(dir): return for file in os.listdir(dir): @@ -560,7 +546,6 @@ def get(self, key: str) -> list[str]: return utils.sort_semver(self.cache.get(key, [])) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class _sensor: def __init__(self, keys: list[str], sense_jsons: list[str]) -> None: self.decoder = json.JSONDecoder( @@ -634,7 +619,6 @@ def get_caption(self, path: str) -> str: KELLIA_GREEK = "KELLIA::Greek" -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def file_name(deck_name: str) -> str: """Given a deck name, return a string that is valid as a file name. @@ -645,7 +629,7 @@ def file_name(deck_name: str) -> str: ) -LAMBDAS: dict[str, enforcer.Callable] = { +LAMBDAS: dict[str, typing.Callable] = { CRUM_ALL: lambda deck_name: crum( deck_name, 1284010387, diff --git a/flashcards/deck.py b/flashcards/deck.py index 81c151524c..41d30d2d55 100644 --- a/flashcards/deck.py +++ b/flashcards/deck.py @@ -2,10 +2,8 @@ import pathlib import shutil -import enforcer import field -import genanki -import type_enforced +import genanki # type: ignore[import-untyped] import utils @@ -36,7 +34,6 @@ ANKI_JS_FMT = """(() => {{ {javascript} }})();""" -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class stats: def __init__(self) -> None: self._exported_notes = 0 @@ -61,7 +58,6 @@ def print(self) -> None: self.problematic(self._duplicate_key, "notes have duplicate keys.") -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class Note(genanki.Note): @property def guid(self): @@ -70,7 +66,6 @@ def guid(self): return genanki.guid_for(self.fields[2]) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class deck: def __init__( self, @@ -79,10 +74,10 @@ def __init__( deck_description: str, css: str, javascript: str, - key: field.Field, - front: field.Field, - back: field.Field, - title: field.Field, + key: field.field, + front: field.field, + back: field.field, + title: field.field, force_key: bool = True, force_no_duplicate_keys: bool = True, force_front: bool = True, @@ -185,6 +180,10 @@ def __init__( else: continue + assert isinstance(k, str) + assert isinstance(f, str) + assert isinstance(b, str) + assert isinstance(t, str) self.keys.append(f"{deck_name} - {k}") self.fronts.append(f) self.backs.append(b) @@ -229,8 +228,8 @@ def write_web(self, dir: str) -> None: ) with open(os.path.join(dir, CSS_BASENAME), "w") as f: f.write(self.css) - for f in self.media: - shutil.copy(f, dir) + for path in self.media: + shutil.copy(path, dir) utils.wrote(dir) def html_to_anki(self, html: str) -> str: diff --git a/flashcards/enforcer.py b/flashcards/enforcer.py deleted file mode 100644 index 7b9ad16b27..0000000000 --- a/flashcards/enforcer.py +++ /dev/null @@ -1,8 +0,0 @@ -import typing - -import type_enforced - -ENABLED = False - -Callable = typing.Callable | type_enforced.enforcer.FunctionMethodEnforcer -OptionalCallable = typing.Optional[Callable] diff --git a/flashcards/field.py b/flashcards/field.py index 747c4d85fc..5acc6c83d6 100644 --- a/flashcards/field.py +++ b/flashcards/field.py @@ -3,8 +3,7 @@ import shutil import typing -import enforcer -import type_enforced +import pandas as pd import utils @@ -17,18 +16,16 @@ _work_dir = "" _initialized = False -_in_work_dir = {} -_tsv = {} +_in_work_dir: dict[str, str] = {} +_tsv: dict[str, pd.DataFrame] = {} -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def init(work_dir: str) -> None: global _work_dir, _initialized _work_dir = work_dir _initialized = True -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def _add_to_work_dir(path: str) -> str: assert _initialized if path in _in_work_dir: @@ -40,19 +37,17 @@ def _add_to_work_dir(path: str) -> str: return new_path -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class field: def next(self) -> str | list[str]: - raise ValueError("Unimplemented!") + raise NotImplementedError() def length(self) -> int: - raise ValueError("Unimplemented!") + raise NotImplementedError() def media_files(self) -> list[str]: - raise ValueError("Unimplemented!") + raise NotImplementedError() -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class _primitive_field(field): def length(self) -> int: return NO_LENGTH @@ -61,7 +56,6 @@ def media_files(self) -> list[str]: return [] -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class txt(_primitive_field): """A constant text field.""" @@ -84,7 +78,6 @@ def str(self) -> str: return self._text -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class seq(_primitive_field): """A numerical sequence field.""" @@ -95,11 +88,10 @@ def next(self) -> str: return str(next(self._counter)) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class _content_field(field): def __init__( self, - content: list[list[str]] | list[str], + content: list[str], media_files: list[str], force: bool = True, ) -> None: @@ -112,7 +104,7 @@ def __init__( def media_files(self) -> list[str]: return self._media_files - def next(self) -> str | list[str]: + def next(self) -> str: val = self._content[self._counter] self._counter += 1 return val @@ -121,7 +113,6 @@ def length(self) -> int: return len(self._content) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class tsv(_content_field): """A TSV column field.""" @@ -143,7 +134,6 @@ def __init__( super().__init__(content, [], force=force) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class tsvs(_content_field): """A TSVS column field.""" @@ -165,52 +155,6 @@ def __init__( super().__init__(content, [], force=force) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -class grp(_content_field): # dead: disable - """ - Group entries in a TSV column using another column. - See this example: - keys: [1, 2, 3] - groupby: [1, 2, 1, 2, 3, 3] - selected: ["a", "b", "c", "d", "e", "f"] - - The first step is to zip `groupby` and `selected` to obtain the - following: - [(1, "a"), - (2, "b"), - (1, "c"), - (2, "d"), - (3, "e"), - (e, f")] - And then, for each call to next(), we return the selected entries with - a corresponding gropuby that matches the key. - - This type is complicated and currently unused. Maybe we should just - delete it! - """ - - def __init__( - self, - keys, - group_by, - selected, - force: bool = True, - unique: bool = False, - ) -> None: - keys = [keys.next() for _ in range(keys.length())] - key_to_selected = {k: [] for k in keys} - for _ in range(num_entries(group_by, selected)): - key_to_selected[group_by.next()].append(selected.next()) - if unique: - assert all(len(value) == 1 for value in key_to_selected.values()) - key_to_selected = { - key: value[0] for key, value in key_to_selected.items() - } - content = [key_to_selected[k] for k in keys] - super().__init__(content, [], force) - - -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class media(_content_field): def __init__( self, @@ -218,10 +162,10 @@ def __init__( html_fmt: str, keys, # Map key to list of paths. - get_paths: enforcer.Callable, + get_paths: typing.Callable, # Map path to `format` arguments. # Your final HTML will be `html_fmt.format(fmt_args(path))`. - fmt_args: enforcer.OptionalCallable = None, + fmt_args: typing.Optional[typing.Callable] = None, force: bool = True, ) -> None: """The final path to a media file must be a basename. Directories are @@ -238,7 +182,7 @@ def __init__( """ content = [] - media_files = set() + media_files: set[str] = set() for _ in range(keys.length()): key = keys.next() if force: @@ -263,15 +207,13 @@ def __init__( media_files.add(new_path) content.append(cur) - media_files = list(media_files) - super().__init__(content, media_files, force=force) + super().__init__(content, list(media_files), force=force) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def img( keys, - get_paths: enforcer.Callable, - fmt_args: enforcer.OptionalCallable = None, + get_paths: typing.Callable, + fmt_args: typing.Optional[typing.Callable] = None, force: bool = True, ) -> media: """ @@ -302,10 +244,9 @@ def img( ) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def snd( keys, - get_paths: enforcer.Callable, + get_paths: typing.Callable, force: bool = True, ) -> media: return media( @@ -316,11 +257,10 @@ def snd( ) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) class apl(field): """Apply a lambda to a field.""" - def __init__(self, lam: enforcer.Callable, *fields) -> None: + def __init__(self, lam: typing.Callable, *fields) -> None: self._lambda = lam self._fields = _convert_strings(*fields) @@ -334,15 +274,9 @@ def length(self) -> int: return num_entries(*self._fields) -# NOTE: This must follow the last field subclass definition. -Field = type_enforced.utils.WithSubclasses(field) -FieldOrStr = Field + [str] - - -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def fmt( fmt: str, - key_to_field: dict[str, Field], + key_to_field: dict[str, field], force: bool = True, aon: typing.Optional[bool] = None, ) -> apl: @@ -365,29 +299,23 @@ def format(*nexts: str) -> str: return apl(format, *[key_to_field[k] for k in keys]) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -def aon(*fields: FieldOrStr) -> apl: +def aon(*fields: field | str) -> apl: """Construct an all-or-nothing field.""" - @type_enforced.Enforcer(enabled=enforcer.ENABLED) def all_or_nothing(*nexts: str) -> str: return "".join(nexts) if all(nexts) else "" return apl(all_or_nothing, *fields) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -def cat(*fields: FieldOrStr) -> apl: - @type_enforced.Enforcer(enabled=enforcer.ENABLED) +def cat(*fields: field | str) -> apl: def concatenate(*nexts: str) -> str: return "".join(nexts) return apl(concatenate, *fields) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -def xor(*fields: FieldOrStr) -> apl: - @type_enforced.Enforcer(enabled=enforcer.ENABLED) +def xor(*fields: field | str) -> apl: def first_match(*nexts: str) -> str: for n in nexts: if n: @@ -397,27 +325,21 @@ def first_match(*nexts: str) -> str: return apl(first_match, *fields) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -def jne(sep: str, *fields: FieldOrStr) -> apl: - @type_enforced.Enforcer(enabled=enforcer.ENABLED) +def jne(sep: str, *fields: field | str) -> apl: def join_non_empty(*nexts: str) -> str: return sep.join(filter(None, nexts)) return apl(join_non_empty, *fields) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -def _convert_strings( - *fields: FieldOrStr, -) -> list[*Field]: +def _convert_strings(*fields: field | str) -> list[field]: return [ txt(f, line_br=False, force=False) if isinstance(f, str) else f for f in fields ] -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -def num_entries(*fields: Field) -> int: +def num_entries(*fields: field) -> int: cur = NO_LENGTH for f in fields: length = f.length() @@ -430,8 +352,7 @@ def num_entries(*fields: Field) -> int: return cur -@type_enforced.Enforcer(enabled=enforcer.ENABLED) -def merge_media_files(*fields: Field) -> list[str]: +def merge_media_files(*fields: field) -> list[str]: m = sum([f.media_files() for f in fields], []) # Eliminate duplicates. This significantly reduces the package size. # While this is handled by Anki, it's not supported in genanki. diff --git a/flashcards/main.py b/flashcards/main.py index eac0120db3..21977503b2 100644 --- a/flashcards/main.py +++ b/flashcards/main.py @@ -5,10 +5,8 @@ import constants import deck -import enforcer import field -import genanki -import type_enforced +import genanki # type: ignore[import-untyped] import utils @@ -53,7 +51,6 @@ ) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def verify_unique_object_keys(decks: list[genanki.Deck]) -> None: utils.verify_unique([d.deck_id for d in decks], "Deck ids") utils.verify_unique([d.name for d in decks], "Deck names") @@ -71,20 +68,20 @@ def verify_unique_object_keys(decks: list[genanki.Deck]) -> None: ) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def write_anki(decks: list[deck.deck], path: str) -> None: - media_files = set() + media_files_set: set[str] = set() anki_decks = [] for d in decks: anki_deck, anki_media = d.anki() anki_decks.append(anki_deck) - media_files.update(anki_media) + media_files_set.update(anki_media) # Sorting the media files increases the chances that we will get an # identical Anki package in the output. - media_files = sorted(list(media_files)) + media_files: list[str] = sorted(list(media_files_set)) + del media_files_set verify_unique_object_keys(anki_decks) package = genanki.Package(anki_decks, media_files=media_files) @@ -93,7 +90,6 @@ def write_anki(decks: list[deck.deck], path: str) -> None: utils.wrote(path) -@type_enforced.Enforcer(enabled=enforcer.ENABLED) def main() -> None: args = argparser.parse_args() diff --git a/flashcards/test/test_enforcer.py b/flashcards/test/test_enforcer.py deleted file mode 100644 index 6354a1ca36..0000000000 --- a/flashcards/test/test_enforcer.py +++ /dev/null @@ -1,12 +0,0 @@ -import unittest - -import enforcer - - -class Test(unittest.TestCase): - def test(self): - pass - - -if __name__ == "__main__": - unittest.main() diff --git a/pre-commit/mypy.sh b/pre-commit/mypy.sh index 9558824acc..921c65e4b8 100755 --- a/pre-commit/mypy.sh +++ b/pre-commit/mypy.sh @@ -35,7 +35,7 @@ _mypy "bible" "${@}" _mypy "dictionary/copticsite.com" "${@}" # _mypy "dictionary/kellia.uni-goettingen.de" "${@}" # _mypy "dictionary/marcion.sourceforge.net" "${@}" -# _mypy "flashcards" "${@}" +_mypy "flashcards" "${@}" _mypy "morphology" "${@}" _mypy "site" "${@}"