ref: Load all options from single dictionary source

This commit is contained in:
Marty Oehme 2025-11-26 18:38:18 +01:00
parent fccfb85026
commit b20d56a007
Signed by: Marty
GPG key ID: 4E535BC19C61886E

185
topen.py
View file

@ -21,7 +21,6 @@ import configparser
import os import os
import subprocess import subprocess
import sys import sys
from collections import namedtuple
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Any, Self
@ -112,6 +111,83 @@ def add_annotation_if_missing(task: Task, annotation_content: str) -> None:
_ = whisper(f"Added annotation: {annotation_content}") _ = whisper(f"Added annotation: {annotation_content}")
@dataclass()
class Opt:
"""Assembled metadata for a single configuration option."""
cli: tuple[str, ...] | None
env: str | None
rc: str | None
default: Any = None
metavar: str | None = None
cast: type = str
help_text: str = ""
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"),
metavar="FILE",
cast=Path,
help_text="Location of taskwarrior config file",
),
"task_data": Opt(
("--task-data",),
"TASKDATA",
"data.location",
default=Path("~/.task"),
metavar="DIR",
cast=Path,
help_text="Location of taskwarrior data directory",
),
"notes_dir": Opt(
("-d", "--notes-dir"),
"TOPEN_NOTES_DIR",
"notes.dir",
default=None, # resolved later in TConf.__post_init__
metavar="DIR",
cast=Path,
help_text="Location of topen notes files",
),
"notes_ext": Opt(
("--extension",),
"TOPEN_NOTES_EXT",
"notes.ext",
default="md",
metavar="EXT",
help_text="Extension of note files",
),
"notes_annot": Opt(
("--annotation",),
"TOPEN_NOTES_ANNOT",
"notes.annot",
default="Note",
metavar="NOTE",
help_text="Annotation content to set within taskwarrior",
),
"notes_editor": Opt(
("--editor",),
"TOPEN_NOTES_EDITOR",
"notes.editor",
default=os.getenv("EDITOR") or os.getenv("VISUAL") or "nano",
metavar="CMD",
help_text="Program to open note files with",
),
"notes_quiet": Opt(
("--quiet",),
"TOPEN_NOTES_QUIET",
"notes.quiet",
default=False,
cast=bool,
help_text="Silence any verbose displayed information",
),
}
@dataclass() @dataclass()
class TConf: class TConf:
"""Topen Configuration """Topen Configuration
@ -189,93 +265,50 @@ you view the task.
_ = parser.add_argument( _ = parser.add_argument(
"id", help="The id/uuid of the taskwarrior task for which we edit notes" "id", help="The id/uuid of the taskwarrior task for which we edit notes"
) )
_ = parser.add_argument( for key, opt in OPTIONS.items():
"-d", if opt.cli is None:
"--notes-dir", continue
metavar="DIR", parser.add_argument(
help="Location of topen notes files", *opt.cli,
) dest=key,
_ = parser.add_argument( metavar=opt.metavar,
"--quiet", help=opt.help_text,
action="store_true", default=None,
help="Silence any verbose displayed information", )
) args = parser.parse_args()
_ = parser.add_argument( cli_vals = {k: v for k, v in vars(args).items() if v is not None}
"--extension", metavar="EXT", help="Extension of note files" cli_vals["task_id"] = cli_vals.pop("id")
) return cli_vals
_ = parser.add_argument(
"--annotation",
metavar="NOTE",
help="Annotation content to set within taskwarrior",
)
_ = parser.add_argument(
"--editor", metavar="CMD", help="Program to open note files with"
)
_ = parser.add_argument(
"--task-rc", metavar="FILE", help="Location of taskwarrior config file"
)
_ = parser.add_argument(
"--task-data", metavar="DIR", help="Location of taskwarrior data directory"
)
p = parser.parse_args()
return _filtered_dict(
{
"task_id": p.id,
"task_rc": p.task_rc,
"task_data": p.task_data,
"notes_dir": p.notes_dir,
"notes_ext": p.extension,
"notes_annot": p.annotation,
"notes_editor": p.editor,
"notes_quiet": p.quiet,
}
)
def parse_env() -> dict: def parse_env() -> dict[str, Any]:
"""Parse environment variable options. """Parse environment variable options.
Returns them as a simple dict object. Returns them as a simple dict object.
""" """
return _filtered_dict( out: dict[str, Any] = {}
{ for key, opt in OPTIONS.items():
"task_rc": os.getenv("TASKRC"), if opt.env and (val := os.getenv(opt.env)) is not None:
"task_data": os.getenv("TASKDATA"), out[key] = opt.cast(val)
"notes_dir": os.getenv("TOPEN_NOTES_DIR"), return out
"notes_ext": os.getenv("TOPEN_NOTES_EXT"),
"notes_annot": os.getenv("TOPEN_NOTES_ANNOT"),
"notes_editor": os.getenv("TOPEN_NOTES_EDITOR"),
"notes_quiet": os.getenv("TOPEN_NOTES_QUIET"),
}
)
def parse_conf(conf_file: Path) -> dict: def parse_conf(rc_path: Path) -> dict:
"""Parse taskrc configuration file options. """Parse taskrc configuration file options.
Returns them as a simple dict object. Returns them as a simple dict object.
Uses dot.annotation for options just like taskwarrior settings. Uses dot.annotation for options just like taskwarrior settings.
""" """
cfg = configparser.ConfigParser(allow_unnamed_section=True, allow_no_value=True) cfg = configparser.ConfigParser(allow_unnamed_section=True, allow_no_value=True)
with open(conf_file.expanduser()) as f: with rc_path.expanduser().open() as fr:
cfg.read_string("[GENERAL]\n" + f.read()) cfg.read_string("[GENERAL]\n" + fr.read())
ConfTrans = namedtuple("ParsedToTConf", ["name", "tconf_name"]) out: dict[str, Any] = {}
return _filtered_dict( for key, opt in OPTIONS.items():
{ if opt.rc and cfg.has_option("GENERAL", opt.rc):
opt.tconf_name: cfg.get("GENERAL", opt.name) raw = cfg.get("GENERAL", opt.rc)
for opt in [ out[key] = opt.cast(raw)
ConfTrans("data.location", "task_data"), return out
ConfTrans("notes.dir", "notes_dir"),
ConfTrans("notes.ext", "notes_ext"),
ConfTrans("notes.annot", "notes_annot"),
ConfTrans("notes.editor", "notes_editor"),
ConfTrans("notes.quiet", "notes_quiet"),
]
if cfg.has_option("GENERAL", opt.name)
}
)
IS_QUIET = False IS_QUIET = False
@ -290,11 +323,5 @@ def _real_path(p: Path | str) -> Path:
return Path(os.path.expandvars(p)).expanduser() return Path(os.path.expandvars(p)).expanduser()
# A None-filtered dict which only contains
# keys which have a value.
def _filtered_dict(d: dict) -> dict:
return {k: v for (k, v) in d.items() if v}
if __name__ == "__main__": if __name__ == "__main__":
main() main()