diff --git a/Dockerfile b/Dockerfile index 2c144c25..9187cc69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,20 @@ -# Use an official Python runtime as a parent image -FROM python:3.11-bookworm +FROM python:3.11-bullseye # Set the working directory to /app WORKDIR /app -# Install deps -COPY pyproject.toml ./ -RUN pip install .[dev] -RUN pip install .[tests] +# Dev experience +COPY makefile ./ + +COPY dev-requirements.txt ./ +RUN make install-dev -# Ensure pyright builds correctly. -# If run in make validate, it is run in parallel, which breaks its installation. -RUN pyright . +COPY requirements.txt ./ +RUN make install-deps + +COPY pyproject.toml ./ +RUN make type-check -# Install the entire app +# Install the app COPY . /app -RUN pip install -e . +RUN pip install . \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index 31ef99b8..f6db0530 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ cruft==2.15.0 pytest==7.1.3 pytest-xdist==3.3.1 -ruff==0.1.0 +ruff==0.1.2 pyright==1.1.328 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b74c1cd8..c3c33eb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ select = [ "SIM", "W", ] -ignore = ["ANN101", "ANN401", "E402", "E501", "F401", "F841", "UP006", "RET504"] +ignore = ["ANN101", "ANN401", "E402", "E501", "F401", "F841", "RET504", "COM812", "COM819", "Q000", "Q001", "Q002", "Q003", "W191"] # Allow autofix for all enabled rules (when `--fix`) is provided. unfixable = ["ERA"] # Exclude a variety of commonly ignored directories. diff --git a/src/personal_mnemonic_medium/card_pipeline.py b/src/personal_mnemonic_medium/card_pipeline.py index af01007b..6740c71a 100644 --- a/src/personal_mnemonic_medium/card_pipeline.py +++ b/src/personal_mnemonic_medium/card_pipeline.py @@ -1,6 +1,5 @@ from collections.abc import Sequence from pathlib import Path -from typing import List from personal_mnemonic_medium.exporters.anki.card_types.base import AnkiCard from personal_mnemonic_medium.exporters.base import CardExporter @@ -24,8 +23,8 @@ def __init__( def run( self, input_path: Path, - ) -> List[AnkiCard]: - notes: List[Document] = [] + ) -> list[AnkiCard]: + notes: list[Document] = [] if input_path.is_dir(): notes += list(self.document_factory.get_notes_from_dir(dir_path=input_path)) diff --git a/src/personal_mnemonic_medium/exporters/anki/card_types/base.py b/src/personal_mnemonic_medium/exporters/anki/card_types/base.py index 109eb1d2..1af1fa67 100644 --- a/src/personal_mnemonic_medium/exporters/anki/card_types/base.py +++ b/src/personal_mnemonic_medium/exporters/anki/card_types/base.py @@ -2,8 +2,9 @@ import os import re from abc import ABC, abstractmethod +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable, List, Optional, Tuple +from typing import Any, Optional import genanki from personal_mnemonic_medium.exporters.anki.globals import CONFIG @@ -22,9 +23,9 @@ class AnkiCard(ABC): def __init__( self, - fields: List[str], + fields: list[str], source_prompt: Prompt, - url_generator: Callable[[Path, Optional[int]], str] = get_obsidian_url, + url_generator: Callable[[Path, int | None], str] = get_obsidian_url, html_compiler: Callable[[str], str] = compile_field, ): self.markdown_fields = fields @@ -38,11 +39,11 @@ def source_markdown(self) -> str: return self.source_doc.content @property - def html_fields(self) -> List[str]: + def html_fields(self) -> list[str]: return list(map(self.html_compiler, self.markdown_fields)) @property - def tags(self) -> List[str]: + def tags(self) -> list[str]: return self.source_doc.tags @property @@ -132,9 +133,9 @@ def to_genanki_note(self) -> genanki.Note: tags=self.tags, ) - def make_ref_pair(self, filename: str) -> Tuple[Path, str]: + def make_ref_pair(self, filename: str) -> tuple[Path, str]: """Take a filename relative to the card, and make it absolute.""" - newname = "%".join(filename.split(os.sep)) + newname = "%".join(filename.split(os.sep)) # noqa: PTH206 if os.path.isabs(filename): # noqa abspath = Path(filename) @@ -163,8 +164,7 @@ def process_match(m) -> str: # noqa current_stage = re.sub(regex, process_match, current_stage) - for r in results: - yield r + yield from results # Anki seems to hate alt tags :( self.html_fields[i] = re.sub(r'alt="[^"]*?"', "", current_stage) diff --git a/src/personal_mnemonic_medium/exporters/anki/card_types/cloze.py b/src/personal_mnemonic_medium/exporters/anki/card_types/cloze.py index 3e5b854c..43216480 100644 --- a/src/personal_mnemonic_medium/exporters/anki/card_types/cloze.py +++ b/src/personal_mnemonic_medium/exporters/anki/card_types/cloze.py @@ -1,6 +1,6 @@ import re +from collections.abc import Callable from pathlib import Path -from typing import Callable, List, Optional import genanki from personal_mnemonic_medium.exporters.anki.card_types.base import AnkiCard @@ -18,9 +18,9 @@ class AnkiCloze(AnkiCard): def __init__( self, - fields: List[str], + fields: list[str], source_prompt: Prompt, - url_generator: Callable[[Path, Optional[int]], str] = get_obsidian_url, + url_generator: Callable[[Path, int | None], str] = get_obsidian_url, html_compiler: Callable[[str], str] = compile_field, ): super().__init__( diff --git a/src/personal_mnemonic_medium/exporters/anki/card_types/qa.py b/src/personal_mnemonic_medium/exporters/anki/card_types/qa.py index 98310ec5..67aa6093 100644 --- a/src/personal_mnemonic_medium/exporters/anki/card_types/qa.py +++ b/src/personal_mnemonic_medium/exporters/anki/card_types/qa.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from pathlib import Path -from typing import Callable, List, Optional import genanki from personal_mnemonic_medium.exporters.anki.card_types.base import AnkiCard @@ -17,9 +17,9 @@ class AnkiQA(AnkiCard): def __init__( self, - fields: List[str], + fields: list[str], source_prompt: Prompt, - url_generator: Callable[[Path, Optional[int]], str] = get_obsidian_url, + url_generator: Callable[[Path, int | None], str] = get_obsidian_url, html_compiler: Callable[[str], str] = compile_field, ): super().__init__( diff --git a/src/personal_mnemonic_medium/exporters/anki/globals.py b/src/personal_mnemonic_medium/exporters/anki/globals.py index 6f32482a..4323af3a 100644 --- a/src/personal_mnemonic_medium/exporters/anki/globals.py +++ b/src/personal_mnemonic_medium/exporters/anki/globals.py @@ -1,6 +1,6 @@ # Anki 2.1 has mathjax built in, but ankidroid and other clients don't. import textwrap -from typing import Any, Dict +from typing import Any from personal_mnemonic_medium.exporters.anki.anki_css import CARD_MODEL_CSS @@ -84,7 +84,7 @@ "card_model_template_cloze": CLOZE_MODEL_TEMPLATE, } -VERSION_LOG: Dict[Any, Any] = {} +VERSION_LOG: dict[Any, Any] = {} Q_TYPE_TAG = { "G": "med/type/1_GP", "A": "med/type/2_Acute_care", diff --git a/src/personal_mnemonic_medium/exporters/anki/package_generator.py b/src/personal_mnemonic_medium/exporters/anki/package_generator.py index 6fa31910..edeb9a29 100644 --- a/src/personal_mnemonic_medium/exporters/anki/package_generator.py +++ b/src/personal_mnemonic_medium/exporters/anki/package_generator.py @@ -8,10 +8,9 @@ from dataclasses import dataclass from pathlib import Path from shutil import copyfile -from typing import Any, List, Set +from typing import Any import genanki - from personal_mnemonic_medium.exporters.anki.card_types.base import AnkiCard from personal_mnemonic_medium.exporters.anki.card_types.cloze import AnkiCloze from personal_mnemonic_medium.exporters.anki.card_types.qa import AnkiQA @@ -43,7 +42,7 @@ def __getitem__(self, deckname: str) -> Any: @dataclass(frozen=True) class DeckBundle: deck: genanki.Deck - media: Set[str] + media: set[str] def get_package(self) -> genanki.Package: return genanki.Package(deck_or_decks=self.deck, media_files=list(self.media)) @@ -61,7 +60,7 @@ def __init__(self) -> None: pass @staticmethod - def cards_to_deck_bundle(cards: List[AnkiCard]) -> DeckBundle: + def cards_to_deck_bundle(cards: list[AnkiCard]) -> DeckBundle: """Take an iterable prompts, output an .apkg in a file called output_name. NOTE: We _must_ be in a temp directory. """ @@ -75,7 +74,7 @@ def cards_to_deck_bundle(cards: List[AnkiCard]) -> DeckBundle: @staticmethod def cards_to_deck( cards: Sequence[AnkiCard], - ) -> tuple[genanki.Deck, Set[str]]: + ) -> tuple[genanki.Deck, set[str]]: media = set() deck_name = cards[0].deckname @@ -103,7 +102,7 @@ def cards_to_deck( def prompts_to_cards( self, prompts: Sequence[Prompt], - ) -> List[AnkiCard]: + ) -> list[AnkiCard]: """Takes an iterable of prompts and turns them into AnkiCards""" cards: list[AnkiCard] = [] diff --git a/src/personal_mnemonic_medium/exporters/anki/sync.py b/src/personal_mnemonic_medium/exporters/anki/sync.py index 33cbc213..e76955af 100644 --- a/src/personal_mnemonic_medium/exporters/anki/sync.py +++ b/src/personal_mnemonic_medium/exporters/anki/sync.py @@ -3,19 +3,18 @@ import urllib.request from pathlib import Path from time import sleep -from typing import Any, Dict, List +from typing import Any from genanki import Model, Note -from wasabi import Printer - from personal_mnemonic_medium.exporters.anki.globals import ANKICONNECT_URL from personal_mnemonic_medium.exporters.anki.package_generator import DeckBundle +from wasabi import Printer msg = Printer(timestamp=True) # helper for creating anki connect requests -def request(action: Any, **params: Any) -> Dict[str, Any]: +def request(action: Any, **params: Any) -> dict[str, Any]: return {"action": action, "params": params, "version": 6} @@ -139,19 +138,19 @@ def sync_deck( def get_md_note_infos(deck_bundle: DeckBundle) -> set[str]: - md_notes: List[Note] = deck_bundle.deck.notes + md_notes: list[Note] = deck_bundle.deck.notes md_note_guids = {str(n.guid) for n in md_notes} return md_note_guids def get_anki_note_infos(deck_bundle: DeckBundle) -> tuple[dict[str, Any], set[str]]: - anki_card_ids: List[int] = invoke( + anki_card_ids: list[int] = invoke( "findCards", query=f'"deck:{deck_bundle.deck.name}"', ) # get a list of anki notes in the deck - anki_note_ids: List[int] = invoke("cardsToNotes", cards=anki_card_ids) + anki_note_ids: list[int] = invoke("cardsToNotes", cards=anki_card_ids) # get the note info for the notes in the deck anki_notes_info = invoke("notesInfo", notes=anki_note_ids) diff --git a/src/personal_mnemonic_medium/exporters/url_generators/obsidian_url.py b/src/personal_mnemonic_medium/exporters/url_generators/obsidian_url.py index 43f2f92c..96dd15a1 100644 --- a/src/personal_mnemonic_medium/exporters/url_generators/obsidian_url.py +++ b/src/personal_mnemonic_medium/exporters/url_generators/obsidian_url.py @@ -3,7 +3,7 @@ from typing import Optional -def get_obsidian_url(source_path: Path, line_nr: Optional[int] = None) -> str: +def get_obsidian_url(source_path: Path, line_nr: int | None = None) -> str: """Get the obsidian URI for the source document.""" vault: str = urllib.parse.quote(source_path.parent.name) # type: ignore file: str = urllib.parse.quote(source_path.name) # type: ignore diff --git a/src/personal_mnemonic_medium/note_factories/markdown.py b/src/personal_mnemonic_medium/note_factories/markdown.py index 8b548618..2676c673 100644 --- a/src/personal_mnemonic_medium/note_factories/markdown.py +++ b/src/personal_mnemonic_medium/note_factories/markdown.py @@ -4,10 +4,9 @@ from pathlib import Path from typing import Optional -from tqdm import tqdm - from personal_mnemonic_medium.note_factories.base import DocumentFactory from personal_mnemonic_medium.note_factories.note import Document +from tqdm import tqdm class MarkdownNoteFactory(DocumentFactory): @@ -31,7 +30,7 @@ def get_and_append_new_uuid(self, file_path: Path) -> str: def get_note_id(self, file_string: str) -> str: return re.findall(r"