diff --git a/docs/pages/config.rst b/docs/pages/config.rst index f8868e0..4ed3ab6 100644 --- a/docs/pages/config.rst +++ b/docs/pages/config.rst @@ -39,3 +39,6 @@ Substitute ``tool`` for either ``format``, ``xml_sort``, ``git_info`` or ``make_ See pages about each tool for available options. The options are named the same as the command line arguments, except without any leading dashes and with any other dashes (``-``) replaced by underscores (``_``). + +Note that when a filepath is being used, it is considered relative to the config file it's in. +This is different from when using the option from a commandline, where the current working directory is used as a root. diff --git a/src/tctools/common.py b/src/tctools/common.py index 8cfe264..20dff20 100644 --- a/src/tctools/common.py +++ b/src/tctools/common.py @@ -30,6 +30,8 @@ class Tool(ABC): CONFIG_KEY: Optional[str] = None + PATH_VARIABLES: List[str] = [] # Names of options that are considered file paths + def __init__(self, *args): """Pass e.g. ``sys.args[1:]`` (skipping the script part of the arguments). @@ -54,6 +56,11 @@ def __init__(self, *args): raise ValueError( f"Config field `{key}` is not recognized as a valid option" ) + if key in self.PATH_VARIABLES: + value = self._make_path_from_config(value) + # We should treat these paths as relative to the config file, ignoring + # the current working directory + parser.set_defaults(**{key: value}) for action in parser._actions: @@ -128,6 +135,27 @@ def _find_files_upwards( return None + def _make_path_from_config(self, path: Any) -> Path: + """Turn a relative path from a config file into a global path. + + Otherwise, return it as-is. + + :param path: Config value, can be (list of) `Path` or `str` + """ + + def _fix_path(p): + p = Path(p) + if not p.is_absolute() and self.config_file: + p = self.config_file.parent / p + return p + + if isinstance(path, list): + path = [_fix_path(p) for p in path] + else: + path = _fix_path(path) + + return path + @abstractmethod def run(self) -> int: """Main tool execution.""" @@ -146,6 +174,8 @@ def get_logger(self): class TcTool(Tool, ABC): """Base class for tools sharing TwinCAT functionality.""" + PATH_VARIABLES = ["target"] + def __init__(self, *args): super().__init__(*args) @@ -217,7 +247,7 @@ def find_files(self) -> List[Path]: return files targets = self.args.target - if isinstance(targets, str): + if isinstance(targets, str) or isinstance(targets, Path): targets = [targets] for target in targets: diff --git a/tests/test_common.py b/tests/test_common.py index dc6170a..244e751 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest # noqa from tctools.common import Tool @@ -8,6 +10,8 @@ class MyTool(Tool): CONFIG_KEY = "dummy" + PATH_VARIABLES = ["my_file", "my_targets"] + @classmethod def set_arguments(cls, parser): super().set_arguments(parser) @@ -17,6 +21,16 @@ def set_arguments(cls, parser): action="store", default="default-text", ) + parser.add_argument( + "--my-file", + action="store", + default="default-text", + ) + parser.add_argument( + "--my-targets", + action="store", + default="default-text", + ) return parser def run(self) -> int: @@ -73,3 +87,29 @@ def test_config_file_priority(self, tmp_path, monkeypatch): tool = MyTool() assert tool.args.my_option == "abc987" + + def test_config_file_relative_path(self, tmp_path, monkeypatch): + conf_dir = tmp_path / "project" + work_dir = conf_dir / "subdir1" / "subdir2" + work_dir.mkdir(parents=True) + + conf_file = conf_dir / "tctools.toml" + conf_file.write_text( + """[tctools.dummy] + my_file = "some_file.txt" + my_targets = ["./dir1", "dir2/subdir2/", "//abs_dir/absolute_file.txt"] + """ + ) + + monkeypatch.chdir(work_dir) # Run in child dir of config file dir + + tool = MyTool() + assert not tool.args.my_file.is_relative_to(work_dir) + assert tool.args.my_file == conf_dir / "some_file.txt" + + assert not tool.args.my_targets[0].is_relative_to(work_dir) + assert not tool.args.my_targets[1].is_relative_to(work_dir) + assert not tool.args.my_targets[2].is_relative_to(work_dir) + assert tool.args.my_targets[0] == conf_dir / "dir1" + assert tool.args.my_targets[1] == conf_dir / "dir2" / "subdir2" + assert tool.args.my_targets[2] == Path("//abs_dir/absolute_file.txt")