diff options
| -rwxr-xr-x | .ai/scripts/drill-to-anki.py | 38 | ||||
| -rw-r--r-- | .ai/scripts/tests/test_drill_to_anki.py | 44 | ||||
| -rwxr-xr-x | claude-templates/.ai/scripts/drill-to-anki.py | 38 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test_drill_to_anki.py | 44 |
4 files changed, 114 insertions, 50 deletions
diff --git a/.ai/scripts/drill-to-anki.py b/.ai/scripts/drill-to-anki.py index 543ccd8..1050021 100755 --- a/.ai/scripts/drill-to-anki.py +++ b/.ai/scripts/drill-to-anki.py @@ -13,15 +13,13 @@ Parses org-drill structure: text (sans :drill: tag). Back = entry body with newlines converted to <br>. -Deck name defaults to the org file's #+TITLE if present, otherwise the -input basename. Deck and model IDs are derived from the deck name via -stable hash so re-importing the same deck updates existing cards -instead of duplicating them. +Deck name defaults to the input basename, case preserved. Deck and model +IDs are derived from the deck name via stable hash so re-importing the +same deck updates existing cards instead of duplicating them. -Output defaults to ~/sync/org/drill/<input-basename>.apkg (matches -Craig's project convention where org source lives in the project and -symlinks into ~/sync/org/drill/, with the .apkg writing directly into -~/sync/org/drill/ as a build artifact). +Output defaults to ~/sync/phone/anki/<input-basename>.apkg. The .apkg is +a mobile-Anki artifact the phone picks up from its sync dir, so it lands +there rather than next to the org source. Usage: drill-to-anki.py <input.org> @@ -91,15 +89,6 @@ def escape_html(s: str) -> str: ) -def title_from_org(org_text: str) -> str | None: - """Return the #+TITLE: value if the org file declares one.""" - for line in org_text.splitlines()[:20]: - m = re.match(r"^#\+TITLE:\s*(.+?)\s*$", line, re.IGNORECASE) - if m: - return m.group(1).strip() - return None - - def strip_org_metadata(body_lines: list[str]) -> list[str]: """Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines. @@ -185,13 +174,13 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck: return deck -def default_deck_name(input_path: Path, org_text: str) -> str: - return title_from_org(org_text) or input_path.stem.replace("-", " ").title() +def default_deck_name(input_path: Path) -> str: + return input_path.stem def default_output_path(input_path: Path) -> Path: - drill_dir = Path.home() / "sync" / "org" / "drill" - return drill_dir / f"{input_path.stem}.apkg" + anki_dir = Path.home() / "sync" / "phone" / "anki" + return anki_dir / f"{input_path.stem}.apkg" def main() -> int: @@ -205,14 +194,13 @@ def main() -> int: ) parser.add_argument( "--deck", - help="Deck name. Defaults to the org file's #+TITLE, or the " - "input basename if no title is set.", + help="Deck name. Defaults to the input basename.", ) parser.add_argument( "--output", type=Path, help="Output .apkg path. Defaults to " - "~/sync/org/drill/<input-basename>.apkg.", + "~/sync/phone/anki/<input-basename>.apkg.", ) args = parser.parse_args() @@ -222,7 +210,7 @@ def main() -> int: return 1 org_text = input_path.read_text(encoding="utf-8") - deck_name = args.deck or default_deck_name(input_path, org_text) + deck_name = args.deck or default_deck_name(input_path) output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve() output_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/.ai/scripts/tests/test_drill_to_anki.py b/.ai/scripts/tests/test_drill_to_anki.py new file mode 100644 index 0000000..6490e58 --- /dev/null +++ b/.ai/scripts/tests/test_drill_to_anki.py @@ -0,0 +1,44 @@ +"""Tests for drill-to-anki.py default-path and deck-name helpers. + +The script is a PEP 723 uv-run script that imports genanki, which uv resolves +at runtime but isn't installed in the test environment. The fixture stubs +genanki in sys.modules so the module loads; the pure helpers under test never +call into it. +""" +from __future__ import annotations + +import importlib.util +import sys +import types +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).resolve().parents[1] / "drill-to-anki.py" + + +@pytest.fixture(scope="module") +def drill(): + # Only stub when genanki is genuinely absent, so a real install isn't shadowed. + sys.modules.setdefault("genanki", types.ModuleType("genanki")) + spec = importlib.util.spec_from_file_location("drill_to_anki", SCRIPT) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_default_output_path_targets_phone_anki_dir(drill): + """The .apkg is a phone artifact, so it defaults under sync/phone/anki/.""" + result = drill.default_output_path(Path("/home/x/projects/health/health-drill.org")) + assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg" + + +def test_default_deck_name_is_raw_basename(drill): + """Deck name is the input basename with case preserved; #+TITLE is ignored.""" + assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat" + + +def test_default_deck_name_keeps_hyphens(drill): + """A hyphenated basename is kept verbatim rather than title-cased.""" + assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill" diff --git a/claude-templates/.ai/scripts/drill-to-anki.py b/claude-templates/.ai/scripts/drill-to-anki.py index 543ccd8..1050021 100755 --- a/claude-templates/.ai/scripts/drill-to-anki.py +++ b/claude-templates/.ai/scripts/drill-to-anki.py @@ -13,15 +13,13 @@ Parses org-drill structure: text (sans :drill: tag). Back = entry body with newlines converted to <br>. -Deck name defaults to the org file's #+TITLE if present, otherwise the -input basename. Deck and model IDs are derived from the deck name via -stable hash so re-importing the same deck updates existing cards -instead of duplicating them. +Deck name defaults to the input basename, case preserved. Deck and model +IDs are derived from the deck name via stable hash so re-importing the +same deck updates existing cards instead of duplicating them. -Output defaults to ~/sync/org/drill/<input-basename>.apkg (matches -Craig's project convention where org source lives in the project and -symlinks into ~/sync/org/drill/, with the .apkg writing directly into -~/sync/org/drill/ as a build artifact). +Output defaults to ~/sync/phone/anki/<input-basename>.apkg. The .apkg is +a mobile-Anki artifact the phone picks up from its sync dir, so it lands +there rather than next to the org source. Usage: drill-to-anki.py <input.org> @@ -91,15 +89,6 @@ def escape_html(s: str) -> str: ) -def title_from_org(org_text: str) -> str | None: - """Return the #+TITLE: value if the org file declares one.""" - for line in org_text.splitlines()[:20]: - m = re.match(r"^#\+TITLE:\s*(.+?)\s*$", line, re.IGNORECASE) - if m: - return m.group(1).strip() - return None - - def strip_org_metadata(body_lines: list[str]) -> list[str]: """Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines. @@ -185,13 +174,13 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck: return deck -def default_deck_name(input_path: Path, org_text: str) -> str: - return title_from_org(org_text) or input_path.stem.replace("-", " ").title() +def default_deck_name(input_path: Path) -> str: + return input_path.stem def default_output_path(input_path: Path) -> Path: - drill_dir = Path.home() / "sync" / "org" / "drill" - return drill_dir / f"{input_path.stem}.apkg" + anki_dir = Path.home() / "sync" / "phone" / "anki" + return anki_dir / f"{input_path.stem}.apkg" def main() -> int: @@ -205,14 +194,13 @@ def main() -> int: ) parser.add_argument( "--deck", - help="Deck name. Defaults to the org file's #+TITLE, or the " - "input basename if no title is set.", + help="Deck name. Defaults to the input basename.", ) parser.add_argument( "--output", type=Path, help="Output .apkg path. Defaults to " - "~/sync/org/drill/<input-basename>.apkg.", + "~/sync/phone/anki/<input-basename>.apkg.", ) args = parser.parse_args() @@ -222,7 +210,7 @@ def main() -> int: return 1 org_text = input_path.read_text(encoding="utf-8") - deck_name = args.deck or default_deck_name(input_path, org_text) + deck_name = args.deck or default_deck_name(input_path) output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve() output_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/claude-templates/.ai/scripts/tests/test_drill_to_anki.py b/claude-templates/.ai/scripts/tests/test_drill_to_anki.py new file mode 100644 index 0000000..6490e58 --- /dev/null +++ b/claude-templates/.ai/scripts/tests/test_drill_to_anki.py @@ -0,0 +1,44 @@ +"""Tests for drill-to-anki.py default-path and deck-name helpers. + +The script is a PEP 723 uv-run script that imports genanki, which uv resolves +at runtime but isn't installed in the test environment. The fixture stubs +genanki in sys.modules so the module loads; the pure helpers under test never +call into it. +""" +from __future__ import annotations + +import importlib.util +import sys +import types +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).resolve().parents[1] / "drill-to-anki.py" + + +@pytest.fixture(scope="module") +def drill(): + # Only stub when genanki is genuinely absent, so a real install isn't shadowed. + sys.modules.setdefault("genanki", types.ModuleType("genanki")) + spec = importlib.util.spec_from_file_location("drill_to_anki", SCRIPT) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_default_output_path_targets_phone_anki_dir(drill): + """The .apkg is a phone artifact, so it defaults under sync/phone/anki/.""" + result = drill.default_output_path(Path("/home/x/projects/health/health-drill.org")) + assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg" + + +def test_default_deck_name_is_raw_basename(drill): + """Deck name is the input basename with case preserved; #+TITLE is ignored.""" + assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat" + + +def test_default_deck_name_keeps_hyphens(drill): + """A hyphenated basename is kept verbatim rather than title-cased.""" + assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill" |
