Compare commits

...

5 commits

Author SHA1 Message Date
e960f56b93
test: Add simple happypath annotation adding
Some checks are pending
website / build (push) Waiting to run
website / deploy (push) Blocked by required conditions
2025-11-29 18:52:44 +01:00
3ef552bbe5
ref: Print optional error message on editor process error
Editor function takes an optional io object which is used to print an
error output if the subprocess errors.
2025-11-29 18:52:44 +01:00
e84adc4392
fix: Correct whitespace separation on editor shell call
Switch to using 'sequence'-delineated arguments given to the subprocess
run call to correctly handle whitespace.
Also check the output, so we exit if we have an error.
Test accordingly.
2025-11-29 18:52:43 +01:00
84a16ee307
ref: Rename path expansion function
From `_real_path` to `_expand_path` to better express its use.
2025-11-29 18:52:43 +01:00
7251504dc4
ref: Extract note parent dir creation function 2025-11-29 18:52:42 +01:00
2 changed files with 39 additions and 15 deletions

17
test/test_cli.py Normal file
View file

@ -0,0 +1,17 @@
from pathlib import Path
from unittest.mock import Mock, patch
from topen import add_annotation, open_editor
def test_open_editor_escapes_shell():
"""Ensure filenames with spaces/metas do not allow shell injection."""
with patch("subprocess.run") as run_mock:
open_editor(Path("my note$1.txt"), "vim")
run_mock.assert_called_once_with(["vim", "my note$1.txt"], check=True)
def test_add_annotation_calls_tasklib():
task = Mock()
add_annotation(task, "hello")
task.add_annotation.assert_called_once_with("hello")

View file

@ -63,11 +63,9 @@ def main(cfg: "TConf | None" = None, io: "_IO | None" = None) -> int:
fpath = get_notes_file(uuid, notes_dir=cfg.notes_dir, notes_ext=cfg.notes_ext) fpath = get_notes_file(uuid, notes_dir=cfg.notes_dir, notes_ext=cfg.notes_ext)
if not fpath.parent.exists(): _ensure_parent_dir(fpath)
fpath.parent.mkdir(parents=True, exist_ok=True)
io.out(f"Editing note: {fpath}") io.out(f"Editing note: {fpath}")
open_editor(fpath, editor=cfg.notes_editor) open_editor(fpath, editor=cfg.notes_editor, io=io)
if fpath.exists(): if fpath.exists():
if is_annotation_missing(task, annotation_content=cfg.notes_annot): if is_annotation_missing(task, annotation_content=cfg.notes_annot):
@ -97,9 +95,13 @@ def get_notes_file(uuid: str, notes_dir: Path, notes_ext: str) -> Path:
return Path(notes_dir).joinpath(f"{uuid}.{notes_ext}") return Path(notes_dir).joinpath(f"{uuid}.{notes_ext}")
def open_editor(file: Path, editor: str) -> None: def open_editor(file: Path, editor: str, io: "_IO | None" = None) -> None:
"""Opens a file with the chosen editor.""" """Opens a file with the chosen editor."""
_ = subprocess.run(f"{editor} {file}", shell=True) try:
_ = subprocess.run([editor, str(file)], check=True)
except subprocess.CalledProcessError:
if io:
io.err("Editor exited with an error, aborting.\n")
def is_annotation_missing(task: Task, annotation_content: str) -> bool: def is_annotation_missing(task: Task, annotation_content: str) -> bool:
@ -135,16 +137,21 @@ class Opt:
is_flag: bool = False is_flag: bool = False
def _real_path(p: Path | str) -> Path: def _expand_path(p: Path | str) -> Path:
return Path(os.path.expandvars(p)).expanduser() return Path(os.path.expandvars(p)).expanduser()
def _ensure_parent_dir(file: Path) -> None:
if not file.parent.exists():
file.parent.mkdir(parents=True, exist_ok=True)
def _determine_default_task_rc() -> Path: def _determine_default_task_rc() -> Path:
if _real_path("~/.taskrc").exists(): if _expand_path("~/.taskrc").exists():
return _real_path("~/.taskrc") return _expand_path("~/.taskrc")
if _real_path("$XDG_CONFIG_HOME/task/taskrc").exists(): if _expand_path("$XDG_CONFIG_HOME/task/taskrc").exists():
return _real_path("$XDG_CONFIG_HOME/task/taskrc") return _expand_path("$XDG_CONFIG_HOME/task/taskrc")
return _real_path("~/.config/task/taskrc") return _expand_path("~/.config/task/taskrc")
def _strtobool(val: str) -> bool: def _strtobool(val: str) -> bool:
@ -258,11 +265,11 @@ class TConf:
"""If set topen will give no feedback on note editing.""" """If set topen will give no feedback on note editing."""
def __post_init__(self): def __post_init__(self):
self.task_rc = _real_path(self.task_rc) self.task_rc = _expand_path(self.task_rc)
self.task_data = _real_path(self.task_data) self.task_data = _expand_path(self.task_data)
if self.notes_dir == NON_EXISTENT_PATH: if self.notes_dir == NON_EXISTENT_PATH:
self.notes_dir = self._default_notes_dir() self.notes_dir = self._default_notes_dir()
self.notes_dir = _real_path(self.notes_dir) self.notes_dir = _expand_path(self.notes_dir)
if not self.notes_editor: if not self.notes_editor:
self.notes_editor = ( self.notes_editor = (
os.getenv("EDITOR") os.getenv("EDITOR")