From 5035e9269fe11664fd25e438ac8f746721b3de0a Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Tue, 31 Oct 2023 15:26:07 -0400 Subject: [PATCH] fix: Make extension paths relative to config file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #112: https://github.com/mkdocstrings/python/pull/112 Co-authored-by: Timothée Mazzucotelli --- src/mkdocstrings_handlers/python/handler.py | 34 ++++++++++++++++-- tests/test_handler.py | 40 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 056429e8..169546fd 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -9,7 +9,7 @@ import sys from collections import ChainMap from contextlib import suppress -from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, Iterator, Mapping +from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, Iterator, Mapping, Sequence from griffe.collections import LinesCollection, ModulesCollection from griffe.docstrings.parsers import Parser @@ -265,8 +265,9 @@ def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: parser = parser_name and Parser(parser_name) if unknown_module: + extensions = self.normalize_extension_paths(final_config.get("extensions", [])) loader = GriffeLoader( - extensions=load_extensions(final_config.get("extensions", [])), + extensions=load_extensions(extensions), search_paths=self._paths, docstring_parser=parser, docstring_options=parser_options, @@ -369,6 +370,35 @@ def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: D102 (ig return tuple(anchors) return tuple(anchors) + def normalize_extension_paths(self, extensions: Sequence) -> Sequence: + """Resolve extension paths relative to config file.""" + if self._config_file_path is None: + return extensions + + base_path = os.path.dirname(self._config_file_path) + normalized = [] + + for ext in extensions: + if isinstance(ext, dict): + pth, options = next(iter(ext.items())) + pth = str(pth) + else: + pth = str(ext) + options = None + + if pth.endswith(".py") or ".py:" in pth or "/" in pth or "\\" in pth: # noqa: SIM102 + # This is a sytem path. Normalize it. + if not os.path.isabs(pth): + # Make path absolute relative to config file path. + pth = os.path.normpath(os.path.join(base_path, pth)) + + if options is not None: + normalized.append({pth: options}) + else: + normalized.append(pth) + + return normalized + def get_handler( *, diff --git a/tests/test_handler.py b/tests/test_handler.py index 4971e132..e1d92c18 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -105,3 +105,43 @@ def test_expand_globs_without_changing_directory() -> None: ) for path in list(glob(os.path.abspath(".") + "/*.md")): assert path in handler._paths + + +@pytest.mark.parametrize( + ("expect_change", "extension"), + [ + (True, "extension.py"), + (True, "extension.py:SomeExtension"), + (True, "path/to/extension.py"), + (True, "path/to/extension.py:SomeExtension"), + (True, {"extension.py": {"option": "value"}}), + (True, {"extension.py:SomeExtension": {"option": "value"}}), + (True, {"path/to/extension.py": {"option": "value"}}), + (True, {"path/to/extension.py:SomeExtension": {"option": "value"}}), + (False, "/absolute/path/to/extension.py"), + (False, "/absolute/path/to/extension.py:SomeExtension"), + (False, {"/absolute/path/to/extension.py": {"option": "value"}}), + (False, {"/absolute/path/to/extension.py:SomeExtension": {"option": "value"}}), + (False, "dot.notation.path.to.extension"), + (False, "dot.notation.path.to.pyextension"), + (False, {"dot.notation.path.to.extension": {"option": "value"}}), + (False, {"dot.notation.path.to.pyextension": {"option": "value"}}), + ], +) +def test_extension_paths(tmp_path: Path, expect_change: bool, extension: str | dict) -> None: + """Assert extension paths are resolved relative to config file.""" + handler = get_handler( + theme="material", + config_file_path=str(tmp_path.joinpath("mkdocs.yml")), + ) + normalized = handler.normalize_extension_paths([extension])[0] + if expect_change: + if isinstance(normalized, str) and isinstance(extension, str): + assert normalized == str(tmp_path.joinpath(extension)) + elif isinstance(normalized, dict) and isinstance(extension, dict): + pth, options = next(iter(extension.items())) + assert normalized == {str(tmp_path.joinpath(pth)): options} + else: + raise ValueError("Normalization must not change extension items type") + else: + assert normalized == extension