diff --git a/.copier-answers.yml b/.copier-answers.yml index 2076d959..f8c074b7 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.15.6 +_commit: 0.15.7 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b4606c..abf5e825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.1.0](https://github.com/mkdocstrings/python/releases/tag/1.1.0) - 2023-05-25 + +[Compare with 1.0.0](https://github.com/mkdocstrings/python/compare/1.0.0...1.1.0) + +### Features + +- Support custom templates through objects' extra data ([8ff2b06](https://github.com/mkdocstrings/python/commit/8ff2b06295e848b9c84867802eb845adf061dc10) by Timothée Mazzucotelli). [PR #70](https://github.com/mkdocstrings/python/pull/70) + ## [1.0.0](https://github.com/mkdocstrings/python/releases/tag/1.0.0) - 2023-05-11 [Compare with 0.10.1](https://github.com/mkdocstrings/python/compare/0.10.1...1.0.0) diff --git a/docs/usage/extensions.md b/docs/usage/extensions.md new file mode 100644 index 00000000..4f6b96b3 --- /dev/null +++ b/docs/usage/extensions.md @@ -0,0 +1,17 @@ +# Extensions + +## :warning: Work in Progress! + +The Python handler supports extensions through +[*mkdocstrings*' handler extensions](https://mkdocstrings.github.io/usage/handlers/#handler-extensions). + +Specifically, additional templates can be added to the handler, +and Griffe extensions can instruct the handler to use a particular template +for a particular object by setting a value in the Griffe object's `extra` dictionary: + +```python title="griffe_extension.py" +obj = ... # get a reference to a Griffe object +if "mkdocstrings" not in obj.extra: + obj.extra["mkdocstrings"] = {} +obj.extra["mkdocstrings"]["template"] = "template_name.html" +``` diff --git a/docs/usage/index.md b/docs/usage/index.md index 041e8d72..6fb703e6 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -75,69 +75,92 @@ plugins: Some options are **global only**, and go directly under the handler's name. -- `import`: this option is used to import Sphinx-compatible objects inventories from other - documentation sites. For example, you can import the standard library - objects inventory like this: +#### `import` - ```yaml title="mkdocs.yml" - plugins: - - mkdocstrings: - handlers: - python: - import: - - https://docs.python-requests.org/en/master/objects.inv - ``` +This option is used to import Sphinx-compatible objects inventories from other +documentation sites. For example, you can import the standard library +objects inventory like this: - When importing an inventory, you enable automatic cross-references - to other documentation sites like the standard library docs - or any third-party package docs. Typically, you want to import - the inventories of your project's dependencies, at least those - that are used in the public API. +```yaml title="mkdocs.yml" +plugins: +- mkdocstrings: + handlers: + python: + import: + - https://docs.python-requests.org/en/master/objects.inv +``` - See [*mkdocstrings*' documentation on inventories][inventories] - for more details. +When importing an inventory, you enable automatic cross-references +to other documentation sites like the standard library docs +or any third-party package docs. Typically, you want to import +the inventories of your project's dependencies, at least those +that are used in the public API. - [inventories]: https://mkdocstrings.github.io/usage/#cross-references-to-other-projects-inventories +See [*mkdocstrings*' documentation on inventories][inventories] +for more details. - NOTE: This global option is common to *all* handlers, however - they might implement it differently (or not even implement it). + [inventories]: https://mkdocstrings.github.io/usage/#cross-references-to-other-projects-inventories -- `paths`: this option is used to provide filesystem paths in which to search for Python modules. - Non-absolute paths are computed as relative to MkDocs configuration file. Example: +Additionally, the Python handler accepts a `domains` option in the import items, +which allows to select the inventory domains to select. +By default the Python handler only selects the `py` domain (for Python objects). +You might find useful to also enable the [`std` domain][std domain]: - ```yaml title="mkdocs.yml" - plugins: - - mkdocstrings: - handlers: - python: - paths: [src] # search packages in the src folder - ``` + [std domain]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#the-standard-domain - More details at [Finding modules](#finding-modules). - -- `load_external_modules`: this option allows resolving aliases (imports) to any external module. - Modules are considered external when they are not part - of the package your are injecting documentation for. - Enabling this option will tell the handler to resolve aliases recursively - when they are made public through the [`__all__`][__all__] variable. - - WARNING: **Use with caution** - This can load a *lot* of modules through [Griffe], - slowing down your build or triggering errors that Griffe does not yet handle. - **We recommend using the [`preload_modules`][] option instead**, - which acts as an include-list rather than as include-all. - - Example: +```yaml title="mkdocs.yml" +plugins: +- mkdocstrings: + handlers: + python: + import: + - url: https://docs.python-requests.org/en/master/objects.inv + domains: [std, py] +``` - ```yaml title="mkdocs.yml" - plugins: - - mkdocstrings: - handlers: - python: - load_external_modules: true - ``` +NOTE: The `import` option is common to *all* handlers, however +they might implement it differently, or not even implement it. + +#### `paths` + +This option is used to provide filesystem paths in which to search for Python modules. +Non-absolute paths are computed as relative to MkDocs configuration file. Example: + +```yaml title="mkdocs.yml" +plugins: +- mkdocstrings: + handlers: + python: + paths: [src] # search packages in the src folder +``` + +More details at [Finding modules](#finding-modules). + +#### `load_external_modules` + +This option allows resolving aliases (imports) to any external module. +Modules are considered external when they are not part +of the package your are injecting documentation for. +Enabling this option will tell the handler to resolve aliases recursively +when they are made public through the [`__all__`][__all__] variable. + +WARNING: **Use with caution** +This can load a *lot* of modules through [Griffe], +slowing down your build or triggering errors that Griffe does not yet handle. +**We recommend using the [`preload_modules`][] option instead**, +which acts as an include-list rather than as include-all. + +Example: + +```yaml title="mkdocs.yml" +plugins: +- mkdocstrings: + handlers: + python: + load_external_modules: true +``` - [__all__]: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package + [__all__]: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package ### Global/local options diff --git a/mkdocs.yml b/mkdocs.yml index 50ba6925..85fd7ef7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,6 +27,7 @@ nav: - Sphinx: usage/docstrings/sphinx.md - Advanced: - Customization: usage/customization.md + - Extensions: usage/extensions.md # defer to gen-files + literate-nav - Code Reference: reference/ - Development: diff --git a/pyproject.toml b/pyproject.toml index 84f0f2f1..511dbcd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,8 @@ Changelog = "https://mkdocstrings.github.io/python/changelog" Repository = "https://github.com/mkdocstrings/python" Issues = "https://github.com/mkdocstrings/python/issues" Discussions = "https://github.com/mkdocstrings/python/discussions" -Gitter = "https://gitter.im/python/community" -Funding = "https://github.com/sponsors/mkdocstrings" +Gitter = "https://gitter.im/mkdocstrings/python" +Funding = "https://github.com/sponsors/pawamoy" [tool.pdm] version = {source = "scm"} diff --git a/scripts/insiders.py b/scripts/insiders.py index add870cb..0d23a45a 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -112,7 +112,32 @@ def load_goals(data: str, funding: int = 0, project: Project | None = None) -> d } -def funding_goals(source: str | list[tuple[str, str, str]], funding: int = 0) -> dict: +def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]: + try: + data = Path(path).read_text() + except OSError as error: + raise RuntimeError(f"Could not load data from disk: {path}") from error + return load_goals(data, funding) + + +def _load_goals_from_url(source_data: tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: + project_name, project_url, data_fragment = source_data + data_url = urljoin(project_url, data_fragment) + try: + with urlopen(data_url) as response: # noqa: S310 + data = response.read() + except HTTPError as error: + raise RuntimeError(f"Could not load data from network: {data_url}") from error + return load_goals(data, funding, project=Project(name=project_name, url=project_url)) + + +def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: + if isinstance(source, str): + return _load_goals_from_disk(source, funding) + return _load_goals_from_url(source, funding) + + +def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]: """Load funding goals from a given data source. Parameters: @@ -123,20 +148,10 @@ def funding_goals(source: str | list[tuple[str, str, str]], funding: int = 0) -> A dictionaries of goals, keys being their target monthly amount. """ if isinstance(source, str): - try: - data = Path(source).read_text() - except OSError as error: - raise RuntimeError(f"Could not load data from disk: {source}") from error - return load_goals(data, funding) + return _load_goals_from_disk(source, funding) goals = {} - for project_name, project_url, data_fragment in source: - data_url = urljoin(project_url, data_fragment) - try: - with urlopen(data_url) as response: # noqa: S310 - data = response.read() - except HTTPError as error: - raise RuntimeError(f"Could not load data from network: {data_url}") from error - source_goals = load_goals(data, funding, project=Project(name=project_name, url=project_url)) + for src in source: + source_goals = _load_goals(src) for amount, goal in source_goals.items(): if amount not in goals: goals[amount] = goal diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 9bfb02f4..c5a06d0c 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -297,7 +297,8 @@ def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: # noqa mutabled_config = dict(copy.deepcopy(config)) final_config = ChainMap(mutabled_config, self.default_config) - template = self.env.get_template(f"{data.kind.value}.html") + template_name = rendering.do_get_template(data) + template = self.env.get_template(template_name) # Heading level is a "state" variable, that will change at each step # of the rendering recursion. Therefore, it's easier to use it as a plain value @@ -335,6 +336,7 @@ def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore self.env.filters["format_signature"] = rendering.do_format_signature self.env.filters["filter_objects"] = rendering.do_filter_objects self.env.filters["stash_crossref"] = lambda ref, length: ref + self.env.filters["get_template"] = rendering.do_get_template def get_anchors(self, data: CollectorItem) -> set[str]: # noqa: D102 (ignore missing docstring) try: diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index d1f0eb75..5c8b0f19 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -248,3 +248,16 @@ def formatter(code: str, line_length: int) -> str: return format_str(code, mode=mode) return formatter + + +def do_get_template(obj: Object) -> str: + """Get the template name used to render an object. + + Parameters: + obj: A Griffe object. + + Returns: + A template name. + """ + extra_data = getattr(obj, "extra", {}).get("mkdocstrings", {}) + return extra_data.get("template", "") or f"{obj.kind.value}.html" diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/children.html b/src/mkdocstrings_handlers/python/templates/material/_base/children.html index 9e27ed0f..dda1c5ff 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/children.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/children.html @@ -27,7 +27,7 @@ {% with heading_level = heading_level + extra_level %} {% for attribute in attributes|order_members(config.members_order, members_list) %} {% if not attribute.is_alias or attribute.is_explicitely_exported %} - {% include "attribute.html" with context %} + {% include attribute|get_template with context %} {% endif %} {% endfor %} {% endwith %} @@ -42,7 +42,7 @@ {% with heading_level = heading_level + extra_level %} {% for class in classes|order_members(config.members_order, members_list) %} {% if not class.is_alias or class.is_explicitely_exported %} - {% include "class.html" with context %} + {% include class|get_template with context %} {% endif %} {% endfor %} {% endwith %} @@ -58,7 +58,7 @@ {% for function in functions|order_members(config.members_order, members_list) %} {% if not (obj.kind.value == "class" and function.name == "__init__" and config.merge_init_into_class) %} {% if not function.is_alias or function.is_explicitely_exported %} - {% include "function.html" with context %} + {% include function|get_template with context %} {% endif %} {% endif %} {% endfor %} @@ -75,7 +75,7 @@ {% with heading_level = heading_level + extra_level %} {% for module in modules|order_members(config.members_order, members_list) %} {% if not module.is_alias or module.is_explicitely_exported %} - {% include "module.html" with context %} + {% include module|get_template with context %} {% endif %} {% endfor %} {% endwith %} @@ -91,26 +91,26 @@ filter_objects(filters=config.filters, members_list=members_list, keep_no_docstrings=config.show_if_no_docstring)| order_members(config.members_order, members_list) %} - {% if not (obj.kind.value == "class" and child.name == "__init__" and config.merge_init_into_class) %} + {% if not (obj.is_class and child.name == "__init__" and config.merge_init_into_class) %} - {% if child.kind.value == "attribute" %} + {% if child.is_attribute %} {% with attribute = child %} - {% include "attribute.html" with context %} + {% include attribute|get_template with context %} {% endwith %} - {% elif child.kind.value == "class" %} + {% elif child.is_class %} {% with class = child %} - {% include "class.html" with context %} + {% include class|get_template with context %} {% endwith %} - {% elif child.kind.value == "function" %} + {% elif child.is_function %} {% with function = child %} - {% include "function.html" with context %} + {% include function|get_template with context %} {% endwith %} - {% elif child.kind.value == "module" and config.show_submodules %} + {% elif child.is_module and config.show_submodules %} {% with module = child %} - {% include "module.html" with context %} + {% include module|get_template with context %} {% endwith %} {% endif %}