diff --git a/pyproject.toml b/pyproject.toml index b531488..2e0a644 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ ci = "https://github.com/machow/quartodoc/actions" [project.optional-dependencies] dev = [ + "griffe-pydantic", "pytest<8.0.0", "pytest-cov", "jupyterlab", diff --git a/quartodoc/_griffe_compat.py b/quartodoc/_griffe_compat.py index af59b70..c931c70 100644 --- a/quartodoc/_griffe_compat.py +++ b/quartodoc/_griffe_compat.py @@ -10,6 +10,8 @@ from griffe import Parser, parse, parse_numpy from griffe import AliasResolutionError + + from griffe import load_extensions except ImportError: from griffe.loader import GriffeLoader from griffe.collections import ModulesCollection, LinesCollection @@ -20,3 +22,5 @@ from griffe.docstrings.parsers import Parser, parse from griffe.exceptions import AliasResolutionError + + from griffe import load_extensions diff --git a/quartodoc/autosummary.py b/quartodoc/autosummary.py index c6bea4e..d7a6fde 100644 --- a/quartodoc/autosummary.py +++ b/quartodoc/autosummary.py @@ -8,8 +8,10 @@ from ._griffe_compat import GriffeLoader, ModulesCollection, LinesCollection from ._griffe_compat import dataclasses as dc from ._griffe_compat import Parser, parse +from ._griffe_compat import load_extensions from fnmatch import fnmatchcase +from functools import partial from plum import dispatch # noqa from pathlib import Path from types import ModuleType @@ -24,7 +26,7 @@ from .pandoc.components import Attr -from typing import Any +from typing import Any, Callable _log = logging.getLogger(__name__) @@ -468,6 +470,9 @@ class Builder: items: list[layout.Item] """Documented items by this builder""" + _get_object: "Callable[[str], dc.Object | dc.Alias]" + """Internal function called to load each item.""" + def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -494,6 +499,7 @@ def __init__( dynamic: bool | None = None, parser="numpy", render_interlinks: bool = False, + griffe_extensions: "list[str | dict[str, dict[str, Any]]] | None" = None, _fast_inventory=False, ): self.layout = self.load_layout( @@ -507,6 +513,9 @@ def __init__( self.sidebar = sidebar self.css = css self.parser = parser + self.griffe_extensions = ( + load_extensions(*griffe_extensions) if griffe_extensions else None + ) self.renderer = Renderer.from_config(renderer) if render_interlinks: @@ -517,12 +526,26 @@ def __init__( if out_index is not None: self.out_index = out_index + self._get_object = partial( + get_object, + loader=self._create_griffe_loader(self.griffe_extensions), + ) self.rewrite_all_pages = rewrite_all_pages self.source_dir = str(Path(source_dir).absolute()) if source_dir else None self.dynamic = dynamic self._fast_inventory = _fast_inventory + # TODO: annotation + def _create_griffe_loader(self, extensions): + return GriffeLoader( + docstring_parser=Parser(self.parser), + docstring_options=get_parser_defaults(self.parser), + modules_collection=ModulesCollection(), + lines_collection=LinesCollection(), + extensions=self.griffe_extensions, + ) + def load_layout(self, sections: dict, package: str, options=None): # TODO: currently returning the list of sections, to make work with # previous code. We should make Layout a first-class citizen of the @@ -556,7 +579,13 @@ def build(self, filter: str = "*"): # shaping and collection ---- _log.info("Generating blueprint.") - blueprint = blueprint(self.layout, dynamic=self.dynamic, parser=self.parser) + # TODO: set loader + blueprint = blueprint( + self.layout, + dynamic=self.dynamic, + parser=self.parser, + get_object=self._get_object, + ) _log.info("Collecting pages and inventory items.") pages, self.items = collect(blueprint, base_dir=self.dir) diff --git a/quartodoc/builder/blueprint.py b/quartodoc/builder/blueprint.py index e796a89..5c118b5 100644 --- a/quartodoc/builder/blueprint.py +++ b/quartodoc/builder/blueprint.py @@ -35,7 +35,7 @@ from .utils import PydanticTransformer, ctx_node, WorkaroundKeyError -from typing import overload, TYPE_CHECKING +from typing import Callable, overload, TYPE_CHECKING _log = logging.getLogger(__name__) @@ -437,7 +437,11 @@ def blueprint(el: Auto, package: str) -> Doc: def blueprint( - el: _Base, package: str = None, dynamic: None | bool = None, parser="numpy" + el: _Base, + package: str = None, + dynamic: None | bool = None, + parser="numpy", + get_object: "None | Callable" = None, ) -> _Base: """Convert a configuration element to something that is ready to render. @@ -449,6 +453,8 @@ def blueprint( A base package name. If specified, this is prepended to the names of any objects. dynamic: Whether to dynamically load objects. Defaults to using static analysis. + get_object: + The function to call to load the item. If specified, parser is ignored. Examples -------- @@ -463,7 +469,7 @@ def blueprint( """ - trans = BlueprintTransformer(parser=parser) + trans = BlueprintTransformer(get_object=get_object, parser=parser) if package is not None: trans.crnt_package = package diff --git a/quartodoc/tests/example_pydantic.py b/quartodoc/tests/example_pydantic.py new file mode 100644 index 0000000..7abfa91 --- /dev/null +++ b/quartodoc/tests/example_pydantic.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class AModel(BaseModel): + a: int + """The a attribute.""" diff --git a/quartodoc/tests/test_builder.py b/quartodoc/tests/test_builder.py index 932bc97..7c0b4ca 100644 --- a/quartodoc/tests/test_builder.py +++ b/quartodoc/tests/test_builder.py @@ -82,3 +82,22 @@ def test_builder_generate_sidebar(tmp_path, snapshot): d_sidebar = builder._generate_sidebar(bp) assert yaml.dump(d_sidebar) == snapshot + + +def test_builder_griffe_extensions(): + cfg = yaml.safe_load( + """ + quartodoc: + package: quartodoc.tests.example_pydantic + griffe_extensions: + - griffe_pydantic: + schema: true + """ + ) + builder = Builder.from_quarto_config(cfg) + obj = builder._get_object("quartodoc.tests.example_pydantic.AModel") + + # TODO: what to check here? + assert False + + builder.build()