Compare commits

..

No commits in common. "13c34b08e27b13af631fbf3fb0024288669eb22d" and "9b2dc37279c69d48440e6bdbb695070ceb9ad1b8" have entirely different histories.

6 changed files with 97 additions and 249 deletions

View file

@ -1,27 +0,0 @@
from pathlib import Path
import pytest
from topen import OPTIONS
@pytest.fixture
def fake_id(monkeypatch):
monkeypatch.setattr("sys.argv", ["topen", "0"])
@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)
@pytest.fixture
def fake_rc(tmp_path: Path, monkeypatch):
rc = tmp_path / "test.taskrc"
monkeypatch.setattr(OPTIONS["task_rc"], "default", rc)
return rc

View file

@ -1,11 +1,94 @@
# pyright: reportUnusedImport=false, reportUnusedParameter=false
# ruff: noqa: F401, F811
# ^ Turn off for implicit pytest fixture import
from pathlib import Path
import pytest
from topen import TConf, build_config
from topen import OPTIONS, TConf, 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,
}
@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)
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")
@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
class TestTConf:
def test_paths_are_expanded(self):
@ -33,68 +116,7 @@ class TestTConf:
({"VISUAL": "nvim", "EDITOR": "notepad"}, "notepad"),
],
)
def test_editor_env_resolution(self, isolate_env, monkeypatch, env, expected):
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
class TestBuildConfigPrecedence:
"""
All tests exercise the same key (notes_ext) to keep the assertions short.
Each source sets a different value so we can be sure the right one wins.
"""
def test_defaults_only(self, fake_rc, monkeypatch, isolate_env, fake_id):
cfg = build_config()
assert cfg.notes_ext == "md"
def test_taskrc_overrides_defaults(
self, fake_rc, monkeypatch, isolate_env, fake_id
):
fake_rc.write_text("notes.ext=from-rc\n")
cfg = build_config()
assert cfg.notes_ext == "from-rc"
def test_env_overrides_taskrc(self, fake_rc, monkeypatch, isolate_env, fake_id):
fake_rc.write_text("notes.ext=from-rc\n")
monkeypatch.setenv("TOPEN_NOTES_EXT", "from-env")
cfg = build_config()
assert cfg.notes_ext == "from-env"
def test_cli_overrides_env(self, fake_rc, monkeypatch, isolate_env):
fake_rc.write_text("notes.ext=from-rc\n")
monkeypatch.setenv("TOPEN_NOTES_EXT", "from-env")
monkeypatch.setattr("sys.argv", ["topen", "0", "--extension", "from-cli"])
cfg = build_config()
assert cfg.notes_ext == "from-cli"
def test_cli_overrides_everything(self, fake_rc, monkeypatch, isolate_env):
fake_rc.write_text("notes.ext=from-rc\nnotes.dir=/rc-dir\nnotes.editor=joe")
monkeypatch.setenv("TOPEN_NOTES_EXT", "from-env")
monkeypatch.setenv("TOPEN_NOTES_DIR", "/env-dir")
monkeypatch.setenv("EDITOR", "emacs")
# CLI wins
monkeypatch.setattr(
"sys.argv",
[
"topen",
"0",
"--extension",
"cli-ext",
"--notes-dir",
"cli-dir",
"--editor",
"helix",
],
)
cfg = build_config()
assert cfg.notes_ext == "cli-ext"
assert cfg.notes_dir == Path("cli-dir")
assert cfg.notes_editor == "helix"
# sanity check that the task-id coming from CLI is preserved
def test_cli_supplies_task_id(self, fake_rc, monkeypatch, isolate_env):
monkeypatch.setattr("sys.argv", ["topen", "42"])
cfg = build_config()
assert cfg.task_id == "42"

View file

@ -1,48 +0,0 @@
from pathlib import Path
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"}
def test_cli_options(self, monkeypatch):
monkeypatch.setattr(
"sys.argv",
[
"topen",
"123",
"--extension",
"txt",
"--editor",
"vim",
"--annotation",
"HERENOTE",
],
)
assert parse_cli() == {
"task_id": "123",
"notes_ext": "txt",
"notes_editor": "vim",
"notes_annot": "HERENOTE",
}
def test_cli_notes_quiet_is_flag(self, monkeypatch):
monkeypatch.setattr(
"sys.argv",
[
"topen",
"123",
"--quiet",
],
)
assert parse_cli()["notes_quiet"] is True
def test_cli_parses_paths(self, monkeypatch):
monkeypatch.setattr(
"sys.argv",
["topen", "123", "--notes-dir", "/somewhere/else"],
)
assert parse_cli()["notes_dir"] == Path("/somewhere/else")

View file

@ -1,35 +0,0 @@
from pathlib import Path
from topen import parse_env
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")
def test_env_parses_boolean_true(self, isolate_env, monkeypatch):
monkeypatch.setenv("TOPEN_NOTES_QUIET", "true")
env = parse_env()
assert env["notes_quiet"] is True
def test_env_parses_boolean_false(self, isolate_env, monkeypatch):
monkeypatch.setenv("TOPEN_NOTES_QUIET", "false")
env = parse_env()
assert env["notes_quiet"] is False

View file

@ -1,36 +0,0 @@
from pathlib import Path
from topen import parse_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
def test_taskrc_parses_boolean_true(self, fake_rc):
fake_rc.write_text("""
notes.quiet=true
""")
rc_cfg = parse_rc(fake_rc)
assert rc_cfg["notes_quiet"] is True
def test_taskrc_parses_boolean_false(self, fake_rc):
fake_rc.write_text("""
notes.quiet=false
""")
rc_cfg = parse_rc(fake_rc)
assert rc_cfg["notes_quiet"] is False

View file

@ -23,7 +23,7 @@ import subprocess
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Callable, Self, cast
from typing import Any, Self, cast
from tasklib import Task, TaskWarrior
@ -130,9 +130,8 @@ class Opt:
rc: str | None
default: Any = None
metavar: str | None = None
cast: type | Callable = str
cast: type = str
help_text: str = ""
is_flag: bool = False
def _real_path(p: Path | str) -> Path:
@ -147,24 +146,6 @@ def _determine_default_task_rc() -> Path:
return _real_path("~/.config/task/taskrc")
def _strtobool(val: str) -> bool:
"""Convert a string representation of truth.
Coverts either to True or False, raising an error if it does not find a
valid value.
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n',
'no', 'f', 'false', 'off', and '0'.
Raises ValueError if 'val' is anything else.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return True
elif val in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError(f"Invalid boolean value {val}")
OPTIONS: dict[str, Opt] = {
"task_id": Opt(None, None, None, default=None),
"task_rc": Opt(
@ -223,9 +204,8 @@ OPTIONS: dict[str, Opt] = {
"TOPEN_NOTES_QUIET",
"notes.quiet",
default=False,
cast=_strtobool,
help_text="Silence any verbosely displayed information",
is_flag=True,
cast=bool,
help_text="Silence any verbose displayed information",
),
}
@ -287,16 +267,17 @@ class TConf:
def build_config() -> TConf:
"""Return final configuration object."""
defaults = {k: opt.default for k, opt in OPTIONS.items()}
env = parse_env()
cli = parse_cli()
env = parse_env()
rc_path = Path(
cli.get("task_rc") or env.get("task_rc") or OPTIONS["task_rc"].default
).expanduser()
defaults["task_rc"] = rc_path # use XDG-included paths
rc = parse_rc(rc_path) if rc_path.exists() else {}
# we use the 'parsed' XDG-included taskrc locations for defaults
defaults = {k: opt.default for k, opt in OPTIONS.items()}
defaults["task_rc"] = rc_path
merged = defaults | rc | env | cli # later wins
return TConf.from_dict({k: v for k, v in merged.items() if v is not None})
@ -322,15 +303,6 @@ you view the task.
for key, opt in OPTIONS.items():
if opt.cli is None:
continue
if opt.is_flag:
parser.add_argument(
*opt.cli,
dest=key,
help=opt.help_text,
default=None,
action="store_true",
)
continue
parser.add_argument(
*opt.cli,
dest=key,