From feb75a4fd4b490ea4824b45aa7afecfca5278a84 Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Fri, 27 Oct 2023 15:37:43 -0400 Subject: [PATCH 1/3] Make extension paths relative to config file --- src/mkdocstrings_handlers/python/handler.py | 31 +++++++++++++++++++-- tests/test_handler.py | 31 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 056429e8..378cb62c 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 @@ -263,10 +263,11 @@ def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: parser_name = final_config["docstring_style"] parser_options = final_config["docstring_options"] parser = parser_name and Parser(parser_name) + extensions = self.normalize_extension_paths(final_config.get("extensions", [])) if unknown_module: 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,32 @@ 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 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 '.py' in pth or '/' in pth or '\\' in pth: + # 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..5b2e252d 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -105,3 +105,34 @@ def test_expand_globs_without_changing_directory() -> None: ) for path in list(glob(os.path.abspath(".") + "/*.md")): assert path in handler._paths + + +def test_extension_paths(tmp_path: Path) -> None: + """Assert extension paths are resolved relative to config file.""" + handler = get_handler( + theme="material", + config_file_path=str(tmp_path.joinpath("mkdocs.yml")) + ) + extensions = [ + "path/to/extension.py", + "path/to/extension.py:SomeExtension", + {"path/to/extension.py": {"option": "value"}}, + {"path/to/extension.py:SomeExtension": {"option": "value"}}, + "/absolute/path/to/extension.py", + "/absolute/path/to/extension.py:SomeExtension", + {"/absolute/path/to/extension.py": {"option": "value"}}, + {"/absolute/path/to/extension.py:SomeExtension": {"option": "value"}}, + "dot.notation.path.to.extension" + ] + result = handler.normalize_extension_paths(extensions) + assert result == [ + str(tmp_path.joinpath("path/to/extension.py")), + str(tmp_path.joinpath("path/to/extension.py:SomeExtension")), + {str(tmp_path.joinpath("path/to/extension.py")): {"option": "value"}}, + {str(tmp_path.joinpath("path/to/extension.py:SomeExtension")): {"option": "value"}}, + "/absolute/path/to/extension.py", + "/absolute/path/to/extension.py:SomeExtension", + {"/absolute/path/to/extension.py": {"option": "value"}}, + {"/absolute/path/to/extension.py:SomeExtension": {"option": "value"}}, + "dot.notation.path.to.extension" + ] From e9d208e158fe74f0441a2233cfb1a7457bdb858b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 31 Oct 2023 19:25:42 +0100 Subject: [PATCH 2/3] fixup! Make extension paths relative to config file --- src/mkdocstrings_handlers/python/handler.py | 7 ++- tests/test_handler.py | 59 ++++++++++++--------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 378cb62c..71594968 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -371,11 +371,13 @@ def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: D102 (ig return tuple(anchors) def normalize_extension_paths(self, extensions: Sequence) -> Sequence: - """ Resolve paths relative to config file. """ + """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())) @@ -384,7 +386,7 @@ def normalize_extension_paths(self, extensions: Sequence) -> Sequence: pth = str(ext) options = None - if '.py' in pth or '/' in pth or '\\' in pth: + 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. @@ -394,6 +396,7 @@ def normalize_extension_paths(self, extensions: Sequence) -> Sequence: normalized.append({pth: options}) else: normalized.append(pth) + return normalized diff --git a/tests/test_handler.py b/tests/test_handler.py index 5b2e252d..e1d92c18 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -107,32 +107,41 @@ def test_expand_globs_without_changing_directory() -> None: assert path in handler._paths -def test_extension_paths(tmp_path: Path) -> None: +@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")) + config_file_path=str(tmp_path.joinpath("mkdocs.yml")), ) - extensions = [ - "path/to/extension.py", - "path/to/extension.py:SomeExtension", - {"path/to/extension.py": {"option": "value"}}, - {"path/to/extension.py:SomeExtension": {"option": "value"}}, - "/absolute/path/to/extension.py", - "/absolute/path/to/extension.py:SomeExtension", - {"/absolute/path/to/extension.py": {"option": "value"}}, - {"/absolute/path/to/extension.py:SomeExtension": {"option": "value"}}, - "dot.notation.path.to.extension" - ] - result = handler.normalize_extension_paths(extensions) - assert result == [ - str(tmp_path.joinpath("path/to/extension.py")), - str(tmp_path.joinpath("path/to/extension.py:SomeExtension")), - {str(tmp_path.joinpath("path/to/extension.py")): {"option": "value"}}, - {str(tmp_path.joinpath("path/to/extension.py:SomeExtension")): {"option": "value"}}, - "/absolute/path/to/extension.py", - "/absolute/path/to/extension.py:SomeExtension", - {"/absolute/path/to/extension.py": {"option": "value"}}, - {"/absolute/path/to/extension.py:SomeExtension": {"option": "value"}}, - "dot.notation.path.to.extension" - ] + 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 From 168a2ef5e7663a5fdf9dcd47d4f46038503d2f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 31 Oct 2023 19:30:41 +0100 Subject: [PATCH 3/3] fixup! Make extension paths relative to config file --- src/mkdocstrings_handlers/python/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 71594968..169546fd 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -263,9 +263,9 @@ def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: parser_name = final_config["docstring_style"] parser_options = final_config["docstring_options"] parser = parser_name and Parser(parser_name) - extensions = self.normalize_extension_paths(final_config.get("extensions", [])) if unknown_module: + extensions = self.normalize_extension_paths(final_config.get("extensions", [])) loader = GriffeLoader( extensions=load_extensions(extensions), search_paths=self._paths,