From f8f0a2077d93be2ec7e21f28a973c91464391cb0 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 27 Nov 2025 19:49:39 +0100 Subject: [PATCH 01/11] ref: Extract determining default taskrc from TConf class To start streamlining the TConf class a little, by removing some internal logic which may be better residing in the Options dict, we extract determining the correct taskrc file. --- topen.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/topen.py b/topen.py index 1da9452..a35f494 100755 --- a/topen.py +++ b/topen.py @@ -122,13 +122,25 @@ class Opt: help_text: str = "" +def _real_path(p: Path | str) -> Path: + return Path(os.path.expandvars(p)).expanduser() + + +def _determine_default_task_rc() -> Path: + if _real_path("~/.taskrc").exists(): + return _real_path("~/.taskrc") + if _real_path("$XDG_CONFIG_HOME/task/taskrc").exists(): + return _real_path("$XDG_CONFIG_HOME/task/taskrc") + return _real_path("~/.config/task/taskrc") + + OPTIONS: dict[str, Opt] = { "task_id": Opt(None, None, None, default=None), "task_rc": Opt( ("--task-rc",), "TASKRC", None, # taskrc has no key for this - default=Path("~/.taskrc"), + default=_determine_default_task_rc(), metavar="FILE", cast=Path, help_text="Location of taskwarrior config file", @@ -214,8 +226,6 @@ class TConf: """If set topen will give no feedback on note editing.""" def __post_init__(self): - if self.task_rc == NON_EXISTENT_PATH: - self.task_rc = self._default_task_rc() self.task_rc = _real_path(self.task_rc) self.task_data = _real_path(self.task_data) if self.notes_dir == NON_EXISTENT_PATH: @@ -225,14 +235,6 @@ class TConf: def __or__(self, other: Any, /) -> Self: return self.__class__(**asdict(self) | asdict(other)) - def _default_task_rc(self) -> Path: - if Path("~/.taskrc").exists(): - return Path("~/.taskrc") - elif Path("$XDG_CONFIG_HOME/task/taskrc").exists(): - return Path("$XDG_CONFIG_HOME/task/taskrc") - else: - return Path("~/.config/task/taskrc") - def _default_notes_dir(self) -> Path: return self.task_data.joinpath("notes") @@ -250,7 +252,7 @@ def build_config() -> TConf: cli = parse_cli() env = parse_env() rc_path = Path( - cli.get("task_rc") or env.get("task_rc") or TConf(0).task_rc + cli.get("task_rc") or env.get("task_rc") or OPTIONS["task_rc"].default ).expanduser() rc = parse_rc(rc_path) if rc_path.exists() else {} @@ -334,9 +336,5 @@ def whisper(text: str) -> None: print(text) -def _real_path(p: Path | str) -> Path: - return Path(os.path.expandvars(p)).expanduser() - - if __name__ == "__main__": main() From d103a632d01d77297c5e87e1af2c14f396fe6d1d Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 15:36:01 +0100 Subject: [PATCH 02/11] ref: Use OPTION dict defaults for TConf --- topen.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/topen.py b/topen.py index a35f494..91958bc 100755 --- a/topen.py +++ b/topen.py @@ -207,22 +207,22 @@ class TConf: task_id: int """The id (or uuid) of the task to edit a note for.""" - task_rc: Path = NON_EXISTENT_PATH + task_rc: Path = OPTIONS["task_rc"].default """The path to the taskwarrior taskrc file. Can be absolute or relative to cwd.""" - task_data: Path = Path("~/.task") + task_data: Path = OPTIONS["task_data"].default """The path to the taskwarrior data directory. Can be absolute or relative to cwd.""" notes_dir: Path = NON_EXISTENT_PATH """The path to the notes directory.""" - notes_ext: str = "md" + notes_ext: str = OPTIONS["notes_ext"].default """The extension of note files.""" - notes_annot: str = "Note" + notes_annot: str = OPTIONS["notes_annot"].default """The annotation to add to taskwarrior tasks with notes.""" - notes_editor: str = os.getenv("EDITOR") or os.getenv("VISUAL") or "nano" + notes_editor: str = OPTIONS["notes_editor"].default """The editor to open note files with.""" - notes_quiet: bool = False + notes_quiet: bool = OPTIONS["notes_quiet"].default """If set topen will give no feedback on note editing.""" def __post_init__(self): From ce8ffa3ae86df22c34e56578b8f916eed8dcafd7 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 15:55:46 +0100 Subject: [PATCH 03/11] ref: Extract stdio operations into adapter --- topen.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/topen.py b/topen.py index 91958bc..341a8fb 100755 --- a/topen.py +++ b/topen.py @@ -30,7 +30,7 @@ from tasklib import Task, TaskWarrior NON_EXISTENT_PATH = Path("%%%%I_DONT_EXIST_%%%%") -def main(cfg: "TConf | None" = None): +def main(cfg: "TConf | None" = None, io: "_IO | None" = None): """Runs the cli interface. First sets up the correct options, with overrides in the following order: @@ -45,29 +45,31 @@ def main(cfg: "TConf | None" = None): """ if not cfg: cfg = build_config() + if not io: + io = _IO(quiet=cfg.notes_quiet) if not cfg.task_id: - _ = sys.stderr.write("Please provide task ID as argument.\n") - if cfg.notes_quiet: - global IS_QUIET - IS_QUIET = True + _ = io.err("Please provide task ID as argument.\n") task = get_task(id=cfg.task_id, data_location=cfg.task_data) uuid = task["uuid"] if not uuid: - _ = sys.stderr.write(f"Could not find task for ID: {cfg.task_id}.") + _ = io.err(f"Could not find task for ID: {cfg.task_id}.") sys.exit(1) fpath = get_notes_file(uuid, notes_dir=cfg.notes_dir, notes_ext=cfg.notes_ext) if not fpath.parent.exists(): fpath.parent.mkdir(parents=True, exist_ok=True) + + io.out(f"Editing note: {fpath}") open_editor(fpath, editor=cfg.notes_editor) if fpath.exists(): add_annotation_if_missing(task, annotation_content=cfg.notes_annot) + io.out(f"Added annotation: {cfg.notes_annot}") return - whisper("No note file, doing nothing.") + io.out("No note file, doing nothing.") def get_task(id: str | int, data_location: Path) -> Task: @@ -91,7 +93,6 @@ def get_notes_file(uuid: str, notes_dir: Path, notes_ext: str) -> Path: def open_editor(file: Path, editor: str) -> None: """Opens a file with the chosen editor.""" - whisper(f"Editing note: {file}") _ = subprocess.run(f"{editor} {file}", shell=True) @@ -106,7 +107,6 @@ def add_annotation_if_missing(task: Task, annotation_content: str) -> None: if annot["description"] == annotation_content: return task.add_annotation(annotation_content) - _ = whisper(f"Added annotation: {annotation_content}") @dataclass() @@ -328,12 +328,16 @@ def parse_rc(rc_path: Path) -> dict: return out -IS_QUIET = False +class _IO: + def __init__(self, quiet: bool = False) -> None: + self.quiet = quiet + def out(self, text: str) -> None: + if not self.quiet: + print(text) -def whisper(text: str) -> None: - if not IS_QUIET: - print(text) + def err(self, text: str) -> None: + sys.stderr.write(text) if __name__ == "__main__": From db11128beb43018f8045554840f1989de5dc3fb5 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 19:13:15 +0100 Subject: [PATCH 04/11] ref: Extract sys module use from main func Main function instead returns the given error code and only when invoked as a cli script will we use the error code to exit with the corresponding code to the shell. --- topen.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/topen.py b/topen.py index 341a8fb..b829745 100755 --- a/topen.py +++ b/topen.py @@ -23,14 +23,14 @@ import subprocess import sys from dataclasses import asdict, dataclass from pathlib import Path -from typing import Any, Self +from typing import Any, Self, cast from tasklib import Task, TaskWarrior NON_EXISTENT_PATH = Path("%%%%I_DONT_EXIST_%%%%") -def main(cfg: "TConf | None" = None, io: "_IO | None" = None): +def main(cfg: "TConf | None" = None, io: "_IO | None" = None) -> int: """Runs the cli interface. First sets up the correct options, with overrides in the following order: @@ -42,6 +42,8 @@ def main(cfg: "TConf | None" = None, io: "_IO | None" = None): pointing to the file. If the task does not yet have a note annotation it also adds it automatically. + + Returns the status code as int, 0 for success, 1 for error. """ if not cfg: cfg = build_config() @@ -50,12 +52,14 @@ def main(cfg: "TConf | None" = None, io: "_IO | None" = None): if not cfg.task_id: _ = io.err("Please provide task ID as argument.\n") + return 1 - task = get_task(id=cfg.task_id, data_location=cfg.task_data) - uuid = task["uuid"] - if not uuid: - _ = io.err(f"Could not find task for ID: {cfg.task_id}.") - sys.exit(1) + try: + task = get_task(id=cfg.task_id, data_location=cfg.task_data) + uuid = cast(str, task["uuid"]) + except Task.DoesNotExist: + _ = io.err(f"Could not find task for ID: {cfg.task_id}.\n") + return 1 fpath = get_notes_file(uuid, notes_dir=cfg.notes_dir, notes_ext=cfg.notes_ext) @@ -68,8 +72,9 @@ def main(cfg: "TConf | None" = None, io: "_IO | None" = None): if fpath.exists(): add_annotation_if_missing(task, annotation_content=cfg.notes_annot) io.out(f"Added annotation: {cfg.notes_annot}") - return + return 0 io.out("No note file, doing nothing.") + return 0 def get_task(id: str | int, data_location: Path) -> Task: @@ -341,4 +346,5 @@ class _IO: if __name__ == "__main__": - main() + exit = main() + sys.exit(exit) From bd05dadf566ed89520372187cc27fd542d203e54 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 19:23:49 +0100 Subject: [PATCH 05/11] test: Add simple pytest harness --- pyproject.toml | 1 + test/__init__.py | 0 test/test_cli.py | 7 +++ uv.lock | 116 +++++++++++++++++++++++++++++++++++------------ 4 files changed, 94 insertions(+), 30 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/test_cli.py diff --git a/pyproject.toml b/pyproject.toml index 2fac9be..3240253 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,4 +25,5 @@ typeCheckingMode = "standard" [dependency-groups] dev = [ "pdoc>=15.0.1", + "pytest>=9.0.1", ] diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 0000000..cae322a --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,7 @@ +from topen import parse_cli + + +class TestCli: + def test_cli_minimum_id(self, monkeypatch): + monkeypatch.setattr("sys.argv", ["topen", "42"]) + assert parse_cli() == {"task_id": "42"} diff --git a/uv.lock b/uv.lock index c03569d..dd53c89 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,25 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.13" +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -9,37 +27,46 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] @@ -51,25 +78,50 @@ dependencies = [ { name = "markupsafe" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/16/1b542af6f18a27de059f722c487a596681127897b6d31f78e46d6e5bf2fe/pdoc-15.0.1.tar.gz", hash = "sha256:3b08382c9d312243ee6c2a1813d0ff517a6ab84d596fa2c6c6b5255b17c3d666", size = 154174 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/16/1b542af6f18a27de059f722c487a596681127897b6d31f78e46d6e5bf2fe/pdoc-15.0.1.tar.gz", hash = "sha256:3b08382c9d312243ee6c2a1813d0ff517a6ab84d596fa2c6c6b5255b17c3d666", size = 154174, upload-time = "2024-12-12T15:39:18.498Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/4d/60d856a1b12fbf6ac1539efccfa138e57c6b88675c9867d84bbb46455cc1/pdoc-15.0.1-py3-none-any.whl", hash = "sha256:fd437ab8eb55f9b942226af7865a3801e2fb731665199b74fd9a44737dbe20f9", size = 144186 }, + { url = "https://files.pythonhosted.org/packages/2f/4d/60d856a1b12fbf6ac1539efccfa138e57c6b88675c9867d84bbb46455cc1/pdoc-15.0.1-py3-none-any.whl", hash = "sha256:fd437ab8eb55f9b942226af7865a3801e2fb731665199b74fd9a44737dbe20f9", size = 144186, upload-time = "2024-12-12T15:39:17.107Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] [[package]] name = "tasklib" version = "2.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/50/3e876f39e31bad8783fd3fe117577cbf1dde836e161f8446631bde71aeb4/tasklib-2.5.1.tar.gz", hash = "sha256:5ccd731b52636dd10457a8b8d858cb0d026ffaab1e3e751baf791bf803e37d7b", size = 23805 } +sdist = { url = "https://files.pythonhosted.org/packages/3e/50/3e876f39e31bad8783fd3fe117577cbf1dde836e161f8446631bde71aeb4/tasklib-2.5.1.tar.gz", hash = "sha256:5ccd731b52636dd10457a8b8d858cb0d026ffaab1e3e751baf791bf803e37d7b", size = 23805, upload-time = "2022-11-17T02:48:09.446Z" } [[package]] name = "topen" @@ -82,10 +134,14 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pdoc" }, + { name = "pytest" }, ] [package.metadata] requires-dist = [{ name = "tasklib", specifier = ">=2.5.1" }] [package.metadata.requires-dev] -dev = [{ name = "pdoc", specifier = ">=15.0.1" }] +dev = [ + { name = "pdoc", specifier = ">=15.0.1" }, + { name = "pytest", specifier = ">=9.0.1" }, +] From ee8fef930ac4b923ccd59f3f05dee069d88bb666 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 22:09:13 +0100 Subject: [PATCH 06/11] test: Fix correct cli option value casting Since we can set the type of the relevant option in the OPTIONS dict, we should also let the cli correctly cast them on parsing, just as the env options do. --- test/test_configuration.py | 32 ++++++++++++++++++++++++++++++++ topen.py | 1 + 2 files changed, 33 insertions(+) create mode 100644 test/test_configuration.py diff --git a/test/test_configuration.py b/test/test_configuration.py new file mode 100644 index 0000000..b40223f --- /dev/null +++ b/test/test_configuration.py @@ -0,0 +1,32 @@ +from pathlib import Path + +import pytest + +from topen import OPTIONS, parse_cli, parse_env, parse_rc + + +class TestCli: + def test_cli_minimum_id(self, monkeypatch): + monkeypatch.setattr("sys.argv", ["topen", "42"]) + assert parse_cli() == {"task_id": "42"} + + def test_cli_options(self, monkeypatch): + monkeypatch.setattr( + "sys.argv", + [ + "topen", + "123", + "--extension", + "txt", + "--editor", + "vim", + "--quiet", + "True", + ], + ) + assert parse_cli() == { + "task_id": "123", + "notes_ext": "txt", + "notes_editor": "vim", + "notes_quiet": True, + } diff --git a/topen.py b/topen.py index b829745..90f99e6 100755 --- a/topen.py +++ b/topen.py @@ -295,6 +295,7 @@ you view the task. dest=key, metavar=opt.metavar, help=opt.help_text, + type=opt.cast or str, default=None, ) args = parser.parse_args() From 97478d62d1dfcb6122527343fe2dadd4c3b36a6f Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 22:09:13 +0100 Subject: [PATCH 07/11] test: Add tests for environment option setting --- test/test_cli.py | 7 ------- test/test_configuration.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) delete mode 100644 test/test_cli.py diff --git a/test/test_cli.py b/test/test_cli.py deleted file mode 100644 index cae322a..0000000 --- a/test/test_cli.py +++ /dev/null @@ -1,7 +0,0 @@ -from topen import parse_cli - - -class TestCli: - def test_cli_minimum_id(self, monkeypatch): - monkeypatch.setattr("sys.argv", ["topen", "42"]) - assert parse_cli() == {"task_id": "42"} diff --git a/test/test_configuration.py b/test/test_configuration.py index b40223f..07ded09 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -30,3 +30,33 @@ class TestCli: "notes_editor": "vim", "notes_quiet": True, } + + +@pytest.fixture +def isolate_env(monkeypatch): + # delete all existing env vars that could interfere + for opt in OPTIONS.values(): + if opt.env: + monkeypatch.delenv(opt.env, raising=False) + + +class TestEnv: + def test_env_notes_ext(self, isolate_env, monkeypatch): + monkeypatch.setenv("TOPEN_NOTES_DIR", "/blablubb") + monkeypatch.setenv("TOPEN_NOTES_EXT", "rst") + monkeypatch.setenv("TOPEN_NOTES_ANNOT", "qmd") + monkeypatch.setenv("TOPEN_NOTES_EDITOR", "vim") + monkeypatch.setenv("TOPEN_NOTES_QUIET", "true") + env = parse_env() + assert env["notes_dir"] == Path("/blablubb") + assert env["notes_ext"] == "rst" + assert env["notes_annot"] == "qmd" + assert env["notes_editor"] == "vim" + assert env["notes_quiet"] is True + + def test_env_task_rc(self, isolate_env, monkeypatch): + monkeypatch.setenv("TASKRC", "/a/dir/that/dont/exist/file") + monkeypatch.setenv("TASKDATA", "~/somewhere/tasks") + env = parse_env() + assert env["task_rc"] == Path("/a/dir/that/dont/exist/file") + assert env["task_data"] == Path("~/somewhere/tasks") From cb5e38b50384c2913c0e34092469fff811bed915 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 22:15:44 +0100 Subject: [PATCH 08/11] test: Add rc config file test --- test/test_configuration.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/test_configuration.py b/test/test_configuration.py index 07ded09..ecbaeaf 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -60,3 +60,29 @@ class TestEnv: env = parse_env() assert env["task_rc"] == Path("/a/dir/that/dont/exist/file") assert env["task_data"] == Path("~/somewhere/tasks") + + +@pytest.fixture +def fake_rc(tmp_path: Path, monkeypatch): + rc = tmp_path / "test.taskrc" + monkeypatch.setattr(OPTIONS["task_rc"], "default", rc) + return rc + + +class TestRcFile: + def test_taskrc_parsing(self, fake_rc): + fake_rc.write_text(""" + data.location=~/.taskies + notes.dir=/there + notes.ext=yaml + notes.annot=Boo! + notes.editor=micro + notes.quiet=true + """) + rc_cfg = parse_rc(fake_rc) + assert rc_cfg["task_data"] == Path("~/.taskies") + assert rc_cfg["notes_dir"] == Path("/there") + assert rc_cfg["notes_ext"] == "yaml" + assert rc_cfg["notes_annot"] == "Boo!" + assert rc_cfg["notes_editor"] == "micro" + assert rc_cfg["notes_quiet"] is True From 620a7bc4015234285c2335a596295c26d4aeae07 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 22:16:10 +0100 Subject: [PATCH 09/11] test: Add TConf class tests --- test/test_configuration.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/test_configuration.py b/test/test_configuration.py index ecbaeaf..ad50caf 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from topen import OPTIONS, parse_cli, parse_env, parse_rc +from topen import OPTIONS, TConf, parse_cli, parse_env, parse_rc class TestCli: @@ -35,6 +35,8 @@ class TestCli: @pytest.fixture def isolate_env(monkeypatch): # delete all existing env vars that could interfere + monkeypatch.delenv("EDITOR", raising=False) + monkeypatch.delenv("VISUAL", raising=False) for opt in OPTIONS.values(): if opt.env: monkeypatch.delenv(opt.env, raising=False) @@ -86,3 +88,22 @@ class TestRcFile: assert rc_cfg["notes_annot"] == "Boo!" assert rc_cfg["notes_editor"] == "micro" assert rc_cfg["notes_quiet"] is True + + +class TestTConf: + def test_paths_are_expanded(self): + cfg = TConf.from_dict( + { + "task_data": "~/somewhere/tasks", + "task_id": 0, + "task_rc": "$HOME/taskrc", + "notes_dir": "$HOME/notes", + } + ) + assert cfg.task_data == Path("~/somewhere/tasks").expanduser() + assert cfg.task_rc == Path("~/taskrc").expanduser() + assert cfg.notes_dir == Path("~/notes").expanduser() + + def test_default_notes_sub_dir(self): + cfg = TConf.from_dict({"task_data": "~/my/tasks", "task_id": 0}) + assert cfg.notes_dir == Path("~/my/tasks/notes").expanduser() From 46135f93250764fb085aa8cd7b713b62b03a99e6 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 22:42:31 +0100 Subject: [PATCH 10/11] test: Correctly fall back to EDITOR or VISUAL env vars --- test/test_configuration.py | 13 +++++++++++++ topen.py | 10 ++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/test/test_configuration.py b/test/test_configuration.py index ad50caf..8eb6296 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -107,3 +107,16 @@ class TestTConf: def test_default_notes_sub_dir(self): cfg = TConf.from_dict({"task_data": "~/my/tasks", "task_id": 0}) assert cfg.notes_dir == Path("~/my/tasks/notes").expanduser() + + @pytest.mark.parametrize( + "env,expected", + [ + ({"EDITOR": "vim"}, "vim"), + ({"VISUAL": "emacs", "EDITOR": ""}, "emacs"), + ({"VISUAL": "nvim", "EDITOR": "notepad"}, "notepad"), + ], + ) + def test_editor_env_resolution(isolate_env, monkeypatch, env, expected): + for k, v in env.items(): + monkeypatch.setenv(k, v) + assert TConf(0).notes_editor == expected diff --git a/topen.py b/topen.py index 90f99e6..a290e98 100755 --- a/topen.py +++ b/topen.py @@ -188,7 +188,7 @@ OPTIONS: dict[str, Opt] = { ("--editor",), "TOPEN_NOTES_EDITOR", "notes.editor", - default=os.getenv("EDITOR") or os.getenv("VISUAL") or "nano", + default="nano", metavar="CMD", help_text="Program to open note files with", ), @@ -225,7 +225,7 @@ class TConf: """The extension of note files.""" notes_annot: str = OPTIONS["notes_annot"].default """The annotation to add to taskwarrior tasks with notes.""" - notes_editor: str = OPTIONS["notes_editor"].default + notes_editor: str = "" # added in post-init """The editor to open note files with.""" notes_quiet: bool = OPTIONS["notes_quiet"].default """If set topen will give no feedback on note editing.""" @@ -236,6 +236,12 @@ class TConf: if self.notes_dir == NON_EXISTENT_PATH: self.notes_dir = self._default_notes_dir() self.notes_dir = _real_path(self.notes_dir) + if not self.notes_editor: + self.notes_editor = ( + os.getenv("EDITOR") + or os.getenv("VISUAL") + or OPTIONS["notes_editor"].default + ) def __or__(self, other: Any, /) -> Self: return self.__class__(**asdict(self) | asdict(other)) From 9b2dc37279c69d48440e6bdbb695070ceb9ad1b8 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 23:00:09 +0100 Subject: [PATCH 11/11] fix: Do not print annotation added if it already exists Turned annotation adding into a separate (pure) checking function to see if we have to add an annotation, and a (side-effect) function which actually adds it. This way we can decouple checking and send an info to io only if it is necessary while keeping the two tasks of checking and adding separate. --- topen.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/topen.py b/topen.py index a290e98..611d771 100755 --- a/topen.py +++ b/topen.py @@ -70,8 +70,9 @@ def main(cfg: "TConf | None" = None, io: "_IO | None" = None) -> int: open_editor(fpath, editor=cfg.notes_editor) if fpath.exists(): - add_annotation_if_missing(task, annotation_content=cfg.notes_annot) - io.out(f"Added annotation: {cfg.notes_annot}") + if is_annotation_missing(task, annotation_content=cfg.notes_annot): + add_annotation(task, annotation_content=cfg.notes_annot) + io.out(f"Added annotation: {cfg.notes_annot}") return 0 io.out("No note file, doing nothing.") return 0 @@ -101,16 +102,22 @@ def open_editor(file: Path, editor: str) -> None: _ = subprocess.run(f"{editor} {file}", shell=True) -def add_annotation_if_missing(task: Task, annotation_content: str) -> None: - """Conditionally adds an annotation to a task. +def is_annotation_missing(task: Task, annotation_content: str) -> bool: + """Checks if the task is missing the annotation. - Only adds the annotation if the task does not yet have an - annotation with exactly that content (i.e. avoids - duplication). + Only succeeds if the _complete_ annatotation is found, + and not just as a substring. + + Returns True if annotation was added, otherwise False. """ for annot in task["annotations"] or []: if annot["description"] == annotation_content: - return + return False + return True + + +def add_annotation(task: Task, annotation_content: str) -> None: + """Adds an annotation to a task.""" task.add_annotation(annotation_content)