diff --git a/.copier-answers.yml b/.copier-answers.yml index f2e7c7ac..97027f2b 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.9.0 +_commit: 0.9.7 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c71a8d4e..cf5764f4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,4 @@ github: - - pawamoy -ko_fi: pawamoy -liberapay: pawamoy -patreon: pawamoy +- pawamoy custom: - - https://www.paypal.me/pawamoy +- https://www.paypal.me/pawamoy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9b12e44..201e8d52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v2 - name: Set up PDM - uses: pdm-project/setup-pdm@v2.5 + uses: pdm-project/setup-pdm@v2.6 with: python-version: "3.8" @@ -86,7 +86,7 @@ jobs: uses: actions/checkout@v2 - name: Set up PDM - uses: pdm-project/setup-pdm@v2.5 + uses: pdm-project/setup-pdm@v2.6 with: python-version: ${{ matrix.python-version }} @@ -105,7 +105,7 @@ jobs: key: tests-cache-${{ runner.os }}-${{ matrix.python-version }} - name: Install dependencies - run: pdm install -G duty -G tests + run: pdm install --no-editable -G duty -G tests - name: Run the test suite run: pdm run duty test diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3c8079..a13eb8dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ 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). +## [0.7.0](https://github.com/mkdocstrings/python/releases/tag/0.7.0) - 2022-05-28 + +[Compare with 0.6.6](https://github.com/mkdocstrings/python/compare/0.6.6...0.7.0) + +### Packaging / Dependencies +- Depend on mkdocstrings 0.19 ([b6a9a47](https://github.com/mkdocstrings/python/commit/b6a9a4799980c4590a7ce2838e12653f40e43be3) by Timothée Mazzucotelli). + +### Features +- Add config option for annotations paths verbosity ([b6c9893](https://github.com/mkdocstrings/python/commit/b6c989315fb028813a919319ad1818b0b1f597ac) by Timothée Mazzucotelli). +- Use sections titles in SpaCy-styled docstrings ([fe16b54](https://github.com/mkdocstrings/python/commit/fe16b54aea60473575343e3a3c428567b701bd7d) by Timothée Mazzucotelli). +- Wrap objects names in spans to allow custom styling ([0822ff9](https://github.com/mkdocstrings/python/commit/0822ff9d3ffd3fb71fb619a8b557160661eff9c3) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#240](https://github.com/mkdocstrings/mkdocstrings/issues/240) +- Add Jinja blocks around docstring section styles ([aaa79ee](https://github.com/mkdocstrings/python/commit/aaa79eea40d49a64a69badbe732bf5211fbf055a) by Timothée Mazzucotelli). +- Add members and filters options ([24a6136](https://github.com/mkdocstrings/python/commit/24a6136ee6c04a6a49ee74b20e65177868a10ea7) by Timothée Mazzucotelli). +- Add paths option ([dd41182](https://github.com/mkdocstrings/python/commit/dd41182c210f0bb2675ead162adaa01dbbb1949f) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#311](https://github.com/mkdocstrings/mkdocstrings/issues/311), [PR #20](https://github.com/mkdocstrings/python/issues/20) + +### Bug Fixes +- Fix CSS class on labels ([312a709](https://github.com/mkdocstrings/python/commit/312a7092394aab968032cf08195af7445a85052f) by Timothée Mazzucotelli). +- Fix categories rendering ([6407cf4](https://github.com/mkdocstrings/python/commit/6407cf4f2375c894e0c528e932e9b76774a6455e) by Timothée Mazzucotelli). [Issue #14](https://github.com/mkdocstrings/python/issues/14) + +### Code Refactoring +- Disable `show_submodules` by default ([480d0c3](https://github.com/mkdocstrings/python/commit/480d0c373904713313ec76b6e2570dbc35eb527b) by Timothée Mazzucotelli). +- Merge default configuration options in handler ([347ce76](https://github.com/mkdocstrings/python/commit/347ce76d074c0e3841df2d5162b54d3938d00453) by Timothée Mazzucotelli). +- Reduce number of template debug logs ([8fed314](https://github.com/mkdocstrings/python/commit/8fed314243e3981fc7b527c69cee628e87b10220) by Timothée Mazzucotelli). +- Respect `show_root_full_path` for ToC entries (hidden headings) ([8f4c853](https://github.com/mkdocstrings/python/commit/8f4c85328e8b4a45db77f9fc3e536a5008686f37) by Timothée Mazzucotelli). +- Bring consistency on headings style ([59104c4](https://github.com/mkdocstrings/python/commit/59104c4c51c86c774eed76d8508f9f4d3db5463f) by Timothée Mazzucotelli). +- Stop using deprecated base classes ([d5ea1c5](https://github.com/mkdocstrings/python/commit/d5ea1c5cf7884d8c019145f73685a84218e69840) by Timothée Mazzucotelli). + + ## [0.6.6](https://github.com/mkdocstrings/python/releases/tag/0.6.6) - 2022-03-06 [Compare with 0.6.5](https://github.com/mkdocstrings/python/compare/0.6.5...0.6.6) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 734eb110..ba0c5d2b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,21 +14,21 @@ cd python make setup ``` -!!! note - If it fails for some reason, - you'll need to install - [PDM](https://github.com/pdm-project/pdm) - manually. - - You can install it with: - - ```bash - python3 -m pip install --user pipx - pipx install pdm - ``` - - Now you can try running `make setup` again, - or simply `pdm install`. +> NOTE: +> If it fails for some reason, +> you'll need to install +> [PDM](https://github.com/pdm-project/pdm) +> manually. +> +> You can install it with: +> +> ```bash +> python3 -m pip install --user pipx +> pipx install pdm +> ``` +> +> Now you can try running `make setup` again, +> or simply `pdm install`. You now have the dependencies installed. @@ -57,17 +57,14 @@ As usual: 1. create a new branch: `git checkout -b feature-or-bugfix-name` 1. edit the code and/or the documentation -If you updated the documentation or the project dependencies: - -1. run `make docs-regen` -1. run `make docs-serve`, - go to http://localhost:8000 and check that everything looks good - **Before committing:** 1. run `make format` to auto-format the code 1. run `make check` to check everything (fix any warning) 1. run `make test` to run the tests (fix any issue) +1. if you updated the documentation or the project dependencies: + 1. run `make docs-serve` + 1. go to http://localhost:8000 and check that everything looks good 1. follow our [commit message convention](#commit-message-convention) If you are unsure about how to fix or ignore a warning, diff --git a/README.md b/README.md index 3cdd2c6d..f446ee8a 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,34 @@ -# mkdocstrings-python - -[![ci](https://github.com/mkdocstrings/python/workflows/ci/badge.svg)](https://github.com/mkdocstrings/python/actions?query=workflow%3Aci) -[![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://mkdocstrings.github.io/python/) -[![pypi version](https://img.shields.io/pypi/v/python.svg)](https://pypi.org/project/python/) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/python) -[![gitter](https://badges.gitter.im/join%20chat.svg)](https://gitter.im/mkdocstrings/python) - -A Python handler for [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings). - - -![mkdocstrings_python_gif](https://user-images.githubusercontent.com/3999221/77157838-7184db80-6aa2-11ea-9f9a-fe77405202de.gif) - -## Requirements - -mkdocstrings-python requires Python 3.7 or above. - -
-To install Python 3.7, I recommend using pyenv. - -```bash -# install pyenv -git clone https://github.com/pyenv/pyenv ~/.pyenv - -# setup pyenv (you should also put these three lines in .bashrc or similar) -export PATH="${HOME}/.pyenv/bin:${PATH}" -export PYENV_ROOT="${HOME}/.pyenv" -eval "$(pyenv init -)" - -# install Python 3.7 -pyenv install 3.7.12 - -# make it available globally -pyenv global system 3.7.12 -``` -
+

mkdocstrings-python

+ +

A Python handler for mkdocstrings.

+ +

+ + ci + + + documentation + + + pypi version + + + gitpod + + + gitter + +

+ +--- + +

## Installation You can install this handler as a *mkdocstrings* extra: -```toml +```toml title="pyproject.toml" # PEP 621 dependencies declaration # adapt to your dependencies manager [project] @@ -50,7 +39,7 @@ dependencies = [ You can also explicitely depend on the handler: -```toml +```toml title="pyproject.toml" # PEP 621 dependencies declaration # adapt to your dependencies manager [project] @@ -59,6 +48,11 @@ dependencies = [ ] ``` +## Preview + + +![mkdocstrings_python_gif](https://user-images.githubusercontent.com/3999221/77157838-7184db80-6aa2-11ea-9f9a-fe77405202de.gif) + ## Features - **Data collection from source code**: collection of the object-tree and the docstrings is done thanks to diff --git a/docs/credits.md b/docs/credits.md new file mode 100644 index 00000000..02e1dd81 --- /dev/null +++ b/docs/credits.md @@ -0,0 +1,3 @@ +```python exec="yes" +--8<-- "scripts/gen_credits.py" +``` diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 42c77416..e9e796dd 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -1,6 +1,26 @@ /* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; - border-left: 4px solid rgba(230, 230, 230); - margin-bottom: 80px; + border-left: .05rem solid var(--md-typeset-table-color); +} + +/* Mark external links as such */ +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + background-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + position: relative; + top: 0.1em; + margin-left: 0.2em; + margin-right: 0.1em; + + height: 1em; + width: 1em; + border-radius: 100%; + background-color: var(--md-typeset-a-color); +} +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); } diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 00000000..d1d02cca --- /dev/null +++ b/docs/customization.md @@ -0,0 +1,152 @@ +# Customization + +It is possible to customize the output of the generated documentation with CSS +and/or by overriding templates. + +## CSS classes + +The following CSS classes are used in the generated HTML: + +- `doc`: on all the following elements +- `doc-children`: on `div`s containing the children of an object +- `doc-object`: on `div`s containing an object + - `doc-attribute`: on `div`s containing an attribute + - `doc-class`: on `div`s containing a class + - `doc-function`: on `div`s containing a function + - `doc-module`: on `div`s containing a module +- `doc-heading`: on objects headings + - `doc-object-name`: on `span`s wrapping objects names/paths in the heading + - `doc-KIND-name`: as above, specific to the kind of object (module, class, function, attribute) +- `doc-contents`: on `div`s wrapping the docstring then the children (if any) + - `first`: same, but only on the root object's contents `div` +- `doc-labels`: on `span`s wrapping the object's labels + - `doc-label`: on `small` elements containing a label + - `doc-label-LABEL`: same, where `LABEL` is replaced by the actual label + +!!! example "Example with colorful labels" + === "CSS" + ```css + .doc-label { border-radius: 15px; padding: 0 5px; } + .doc-label-special { background-color: blue; color: white; } + .doc-label-private { background-color: red; color: white; } + .doc-label-property { background-color: green; color: white; } + .doc-label-read-only { background-color: yellow; color: black; } + ``` + + === "Result" + +

+ special + private + property + read-only +

+ + +### Recommended style (Material) + +Here are some CSS rules for the +[*Material for MkDocs*](https://squidfunk.github.io/mkdocs-material/) theme: + +```css +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid var(--md-typeset-table-color); +} + +/* Mark external links as such. */ +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + background-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + position: relative; + top: 0.1em; + margin-left: 0.2em; + margin-right: 0.1em; + + height: 1em; + width: 1em; + border-radius: 100%; + background-color: var(--md-typeset-a-color); +} +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} + +``` + +### Recommended style (ReadTheDocs) + +Here are some CSS rules for the built-in *ReadTheDocs* theme: + +```css +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid rgba(200, 200, 200, 0.2); +} +``` + +## Templates + +Templates are organized into the following tree: + +```tree result="text" +theme/ + attribute.html + children.html + class.html + docstring/ + admonition.html + attributes.html + examples.html + other_parameters.html + parameters.html + raises.html + receives.html + returns.html + warns.html + yields.html + docstring.html + expression.html + function.html + labels.html + module.html + signature.html +``` + +See them [in the repository](https://github.com/mkdocstrings/python/tree/master/src/mkdocstrings_handlers/python/templates/). +See the general *mkdocstrings* documentation to learn how to override them: https://mkdocstrings.github.io/theming/#templates. + +In preparation for Jinja2 blocks, which will improve customization, +each one of these templates extends in fact a base version in `theme/_base`. Example: + +```html+jinja title="theme/docstring/admonition.html" +{% extends "_base/docstring/admonition.html" %} +``` + +```html+jinja title="theme/_base/docstring/admonition.html" +{{ log.debug() }} +
+ {{ section.title|convert_markdown(heading_level, html_id, strip_paragraph=True) }} + {{ section.value.contents|convert_markdown(heading_level, html_id) }} +
+``` + +It means you will be able to customize only *parts* of a template +without having to fully copy-paste it in your project: + +```jinja title="templates/theme/docstring.html" +{% extends "_base/docstring.html" %} +{% block contents %} + {{ block.super }} + Additional contents +{% endblock contents %} +``` + +WARNING: **Block-level customization is not ready yet. We welcome [suggestions](https://github.com/mkdocstrings/python/issues/new).** diff --git a/docs/gen_credits.py b/docs/gen_credits.py deleted file mode 100644 index 370d2e7d..00000000 --- a/docs/gen_credits.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Generate the credits page.""" - -import functools -import re -from itertools import chain -from pathlib import Path -from urllib.request import urlopen - -import mkdocs_gen_files -import toml -from jinja2 import StrictUndefined -from jinja2.sandbox import SandboxedEnvironment - - -def get_credits_data() -> dict: - """Return data used to generate the credits file. - - Returns: - Data required to render the credits template. - """ - project_dir = Path(__file__).parent.parent - metadata = toml.load(project_dir / "pyproject.toml")["project"] - metadata_pdm = toml.load(project_dir / "pyproject.toml")["tool"]["pdm"] - lock_data = toml.load(project_dir / "pdm.lock") - project_name = metadata["name"] - - all_dependencies = chain( - metadata.get("dependencies", []), - chain(*metadata.get("optional-dependencies", {}).values()), - chain(*metadata_pdm.get("dev-dependencies", {}).values()), - ) - direct_dependencies = {re.sub(r"[^\w-].*$", "", dep) for dep in all_dependencies} - direct_dependencies = {dep.lower() for dep in direct_dependencies} - indirect_dependencies = {pkg["name"].lower() for pkg in lock_data["package"]} - indirect_dependencies -= direct_dependencies - - return { - "project_name": project_name, - "direct_dependencies": sorted(direct_dependencies), - "indirect_dependencies": sorted(indirect_dependencies), - "more_credits": "http://pawamoy.github.io/credits/", - } - - -@functools.lru_cache(maxsize=None) -def get_credits(): - """Return credits as Markdown. - - Returns: - The credits page Markdown. - """ - jinja_env = SandboxedEnvironment(undefined=StrictUndefined) - commit = "c78c29caa345b6ace19494a98b1544253cbaf8c1" - template_url = f"https://raw.githubusercontent.com/pawamoy/jinja-templates/{commit}/credits.md" - template_data = get_credits_data() - template_text = urlopen(template_url).read().decode("utf8") # noqa: S310 - return jinja_env.from_string(template_text).render(**template_data) - - -with mkdocs_gen_files.open("credits.md", "w") as fd: - fd.write(get_credits()) -mkdocs_gen_files.set_edit_path("credits.md", "gen_credits.py") diff --git a/docs/gen_ref_nav.py b/docs/gen_ref_nav.py index 15febda8..1b9fbda1 100755 --- a/docs/gen_ref_nav.py +++ b/docs/gen_ref_nav.py @@ -6,24 +6,25 @@ nav = mkdocs_gen_files.Nav() -for path in sorted(Path("src").glob("**/*.py")): +for path in sorted(Path("src").rglob("*.py")): module_path = path.relative_to("src").with_suffix("") doc_path = path.relative_to("src").with_suffix(".md") full_doc_path = Path("reference", doc_path) - parts = list(module_path.parts) + parts = tuple(module_path.parts) + if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") elif parts[-1] == "__main__": continue - nav_parts = list(parts) - nav[nav_parts] = doc_path + + nav[parts] = doc_path.as_posix() with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) - print("::: " + ident, file=fd) + fd.write(f"::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, path) diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 00000000..5b42478c Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/usage.md b/docs/usage.md index d9329414..de28ca16 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,86 +1,93 @@ -!!! warning "This is the documentation for the NEW, EXPERIMENTAL Python handler." - To read the documentation for the LEGACY handler, - go to the [legacy handler documentation](https://mkdocstrings.github.io/python-legacy). - -## Handler options - -Like every handler, the Python handler accepts the common -[`selection`](#selection) and [`rendering`](#rendering) options, -both as **global** and **local** options. -The `selection` options gives you control over the selection of Python objects, -while the `rendering` options lets you change how the documentation is rendered. - -### Selection - -The following options are directly passed to the handler's collector. -See [Collector: Griffe](#collector-griffe) to learn more about Griffe. - -Option | Description ------- | ----------- -**`docstring_style`** | Type: `str`. Docstring style to parse: `google` (default), `numpy` or `sphinx`. -**`docstring_options`** | Type: `dict`. Options to pass to the docstring parser. See [Collector: Griffe](#collector-griffe). - -!!! example "Configuration example" - === "Global" - ```yaml - # mkdocs.yml - plugins: - - mkdocstrings: - handlers: - python: - selection: - docstring_style: google - ``` - - === "Local" - ```yaml - ::: my_package - selection: - docstring_style: sphinx - ``` - -### Rendering - -::: mkdocstrings_handlers.python.renderer.PythonRenderer.default_config - rendering: - show_root_toc_entry: false +# Usage -These options affect how the documentation is rendered. - -!!! example "Configuration example" - === "Global" - ```yaml - # mkdocs.yml - plugins: - - mkdocstrings: - handlers: - python: - rendering: - show_root_heading: yes - ``` - - === "Local" - ```md - ## `ClassA` - - ::: my_package.my_module.ClassA - rendering: - show_root_heading: no - heading_level: 3 - ``` - -## Collector: Griffe +TIP: **This is the documentation for the NEW Python handler.** +To read the documentation for the LEGACY handler, +go to the [legacy handler documentation](https://mkdocstrings.github.io/python-legacy). The tool used by the Python handler to collect documentation from Python source code -is [Griffe](https://mkdocstrings.github.io/griffe). Griffe can mean "signature" in french. +is [Griffe](https://mkdocstrings.github.io/griffe). The word "griffe" can sometimes be used instead of "signature" in french. +Griffe is able to visit the Abstract Syntax Tree (AST) of the source code to extract useful information. +It is also able to execute the code (by importing it) and introspect objects in memory +when source code is not available. Finally, it can parse docstrings following different styles, +see [Supported docstrings styles](#supported-docstrings-styles). + +Like every handler, the Python handler accepts both **global** and **local** options. + +## Global-only options + +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: + + ```yaml title="mkdocs.yml" + plugins: + - mkdocstrings: + handlers: + python: + import: + - https://docs.python-requests.org/en/master/objects.inv + ``` + + 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. + + NOTE: This global 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). + +## Global/local options + +The other options can be used both globally *and* locally, under the `options` key. +For example, globally: + +```yaml title="mkdocs.yml" +plugins: +- mkdocstrings: + handlers: + python: + options: + do_something: true +``` + +...and locally, overriding the global configuration: + +```md title="docs/some_page.md" +::: package.module.class + options: + do_something: false +``` -### Supported docstrings styles +These options affect how the documentation is collected from sources and renderered: +headings, members, docstrings, etc. + +::: mkdocstrings_handlers.python.handler.PythonHandler.default_config + options: + show_root_toc_entry: false + +## Supported docstrings styles Griffe supports the Google-style, Numpy-style and Sphinx-style docstring formats. The style used by default is the Google-style. You can configure what style you want to use with -the `docstring_style` and `docstring_options` [selection options](#selection), -both globally or per autodoc instruction. +the `docstring_style` and `docstring_options` options, +both globally or locally, i.e. per autodoc instruction. - Google: see [Napoleon's documentation](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). - Numpy: see [Numpydoc's documentation](https://numpydoc.readthedocs.io/en/latest/format.html). @@ -88,13 +95,12 @@ both globally or per autodoc instruction. See the supported docstring sections on [Griffe's documentation](https://mkdocstrings.github.io/griffe/docstrings/). -!!! note - As Numpy-style is partially supported by the underlying parser, - you may experience problems in the building process if your docstring - has a `Methods` section in the class docstring - (see [#366](https://github.com/mkdocstrings/mkdocstrings/issues/366)). +NOTE: As Numpy-style is partially supported by the underlying parser, +you may experience problems in the building process if your docstring +has a `Methods` section in the class docstring +(see [#366](https://github.com/mkdocstrings/mkdocstrings/issues/366)). -#### Google-style admonitions +### Google-style admonitions With Google-style docstrings, any section that is not recognized will be transformed into its admonition equivalent. For example: @@ -102,80 +108,187 @@ For example: === "Docstring" ```python """ - Important: + Note: It looks like a section, but it will be rendered as an admonition. - Tip: You can even chose a title. + Tip: You can even choose a title. This admonition has a custom title! """ ``` === "Result" - !!! important - It looks like a section, but it will be rendered as an admonition. + NOTE: It looks like a section, but it will be rendered as an admonition. - !!! tip "You can even chose a title." - This admonition has a custom title! + TIP: **You can even choose a title.** + This admonition has a custom title! ## Finding modules -In order for Griffe to find your packages and modules, -you can take advantage of the usual Python loading mechanisms: +There are multiple ways to tell the handler where to find your packages/modules. -- install your package in the current virtualenv: - ```bash - . venv/bin/activate - pip install -e . +**The recommended method is to use the `paths` option, as it's the only one +that works with the `-f` option of MkDocs, allowing to build the documentation +from any location on the file system.** Indeed, the paths provided with the +`paths` option are computed as relative to the configuration file (mkdocs.yml), +so that the current working directory has no impact on the build process: +*you can build the docs from any location on your filesystem*. + +### Using the `paths` option + +TIP: **This is the recommended method.** + +1. mkdocs.yml in root, package in root + ```tree + root/ + mkdocs.yml + package/ ``` - - ```bash - poetry install + + ```yaml title="mkdocs.yml" + plugins: + - mkdocstrings: + handlers: + python: + paths: [.] # actually not needed, default ``` - - ...etc. - -- or add your package(s) parent directory in the `PYTHONPATH`. - -(*The following instructions assume your Python package is in the `src` directory.*) +1. mkdocs.yml in root, package in subfolder + ```tree + root/ + mkdocs.yml + src/ + package/ + ``` + + ```yaml title="mkdocs.yml" + plugins: + - mkdocstrings: + handlers: + python: + paths: [src] + ``` + +1. mkdocs.yml in subfolder, package in root + ```tree + root/ + docs/ + mkdocs.yml + package/ + ``` + + ```yaml title="mkdocs.yml" + plugins: + - mkdocstrings: + handlers: + python: + paths: [..] + ``` + +1. mkdocs.yml in subfolder, package in subfolder + ```tree + root/ + docs/ + mkdocs.yml + src/ + package/ + ``` + + ```yaml title="mkdocs.yml" + plugins: + - mkdocstrings: + handlers: + python: + paths: [../src] + ``` + +Except for case 1, which is supported by default, **we strongly recommend +to set the path to your packages using this option, even if it works without it** +(for example because your project manager automatically adds `src` to PYTHONPATH), +to make sure anyone can build your docs from any location on their filesystem. + +### Using the PYTHONPATH environment variable + +WARNING: **This method has limitations.** +This method might work for you, with your current setup, +but not for others trying your build your docs with their own setup/environment. +We recommend to use the [`paths` method](#using-the-paths-option) instead. + +You can take advantage of the usual Python loading mechanisms. In Bash and other shells, you can run your command like this (note the prepended `PYTHONPATH=...`): -```console -$ PYTHONPATH=src mkdocs serve -``` +1. mkdocs.yml in root, package in root + ```tree + root/ + mkdocs.yml + package/ + ``` + + ```bash + PYTHONPATH=. mkdocs build # actually not needed, default + ``` -You can also export that variable, -but this is **not recommended** as it could affect other Python processes: +1. mkdocs.yml in root, package in subfolder + ```tree + root/ + mkdocs.yml + src/ + package/ + ``` -```bash -export PYTHONPATH=src # Linux/Bash and similar -setx PYTHONPATH src # Windows, USE AT YOUR OWN RISKS -``` + ```bash + PYTHONPATH=src mkdocs build + ``` -## Recommended style (Material) +1. mkdocs.yml in subfolder, package in root + ```tree + root/ + docs/ + mkdocs.yml + package/ + ``` -Here are some CSS rules for the -[*Material for MkDocs*](https://squidfunk.github.io/mkdocs-material/) theme: + ```bash + PYTHONPATH=. mkdocs build -f docs/mkdocs.yml + ``` -```css -/* Indentation. */ -div.doc-contents:not(.first) { - padding-left: 25px; - border-left: .05rem solid var(--md-default-fg-color--lightest); - margin-bottom: 80px; -} -``` +1. mkdocs.yml in subfolder, package in subfolder + ```tree + root/ + docs/ + mkdocs.yml + src/ + package/ + ``` -## Recommended style (ReadTheDocs) + ```bash + PYTHONPATH=src mkdocs build -f docs/mkdocs.yml + ``` + +### Installing your package in the current Python environment -Here are some CSS rules for the built-in *ReadTheDocs* theme: +WARNING: **This method has limitations.** +This method might work for you, with your current setup, +but not for others trying your build your docs with their own setup/environment. +We recommend to use the [`paths` method](#using-the-paths-option) instead. -```css -/* Indentation. */ -div.doc-contents:not(.first) { - padding-left: 25px; - border-left: .05rem solid rgba(200, 200, 200, 0.2); - margin-bottom: 60px; -} -``` +Install your package in the current environment, and run MkDocs: + +=== "pip" + ```bash + . venv/bin/activate + pip install -e . + mkdocs build + ``` + +=== "PDM" + ```bash + pdm install + pdm run mkdocs build + ``` + +=== "Poetry" + ```bash + poetry install + poetry run mkdocs build + ``` diff --git a/logo.png b/logo.png new file mode 120000 index 00000000..d9c1f0ca --- /dev/null +++ b/logo.png @@ -0,0 +1 @@ +docs/logo.png \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 0d35a92b..6b851eed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,11 +4,13 @@ site_url: "https://mkdocstrings.github.io/python" repo_url: "https://github.com/mkdocstrings/python" repo_name: "mkdocstrings/python" site_dir: "site" +watch: [README.md, CONTRIBUTING.md, CHANGELOG.md, src/mkdocstrings_handlers] nav: - Home: - Overview: index.md - Usage: usage.md + - Customization: customization.md - Changelog: changelog.md - Credits: credits.md - License: license.md @@ -22,10 +24,10 @@ nav: theme: name: material - icon: - logo: material/currency-sign + logo: logo.png features: - navigation.tabs + - navigation.tabs.sticky - navigation.top palette: - media: "(prefers-color-scheme: light)" @@ -49,6 +51,8 @@ extra_css: markdown_extensions: - admonition +- callouts: + strip_period: no - pymdownx.emoji - pymdownx.magiclink - pymdownx.snippets: @@ -62,9 +66,9 @@ markdown_extensions: plugins: - search +- markdown-exec - gen-files: scripts: - - docs/gen_credits.py - docs/gen_ref_nav.py - literate-nav: nav_file: SUMMARY.md @@ -73,19 +77,19 @@ plugins: - mkdocstrings: handlers: python: + paths: [src] import: - https://docs.python.org/3/objects.inv - https://mkdocstrings.github.io/objects.inv - https://mkdocstrings.github.io/griffe/objects.inv - selection: + options: docstring_style: google docstring_options: ignore_init_summary: yes - rendering: - show_submodules: no merge_init_into_class: yes - watch: - - src/mkdocstrings_handlers + separate_signature: yes + show_source: no + show_root_full_path: no extra: social: diff --git a/pyproject.toml b/pyproject.toml index 7f7b5c29..409a40be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "pdm.pep517.api" name = "mkdocstrings-python" description = "A Python handler for mkdocstrings." authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}] -license = {file = "LICENSE"} +license-expression = "ISC" readme = "README.md" requires-python = ">=3.7" keywords = [] @@ -14,7 +14,6 @@ dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: ISC License (ISCL)", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", @@ -30,7 +29,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "mkdocstrings>=0.18", + "mkdocstrings>=0.19", "griffe>=0.11.1", ] @@ -47,17 +46,19 @@ Funding = "https://github.com/sponsors/mkdocstrings" [tool.pdm] version = {use_scm = true} includes = ["src/mkdocstrings_handlers"] +editable-backend = "editables" [tool.pdm.dev-dependencies] duty = ["duty>=0.7"] docs = [ - "mkdocs>=1.2", + "mkdocs>=1.3", "mkdocs-coverage>=0.2", "mkdocs-gen-files>=0.3", "mkdocs-literate-nav>=0.4", "mkdocs-material>=7.3", "mkdocs-section-index>=0.3", - "mkdocstrings>=0.16", + "markdown-callouts>=0.2", + "markdown-exec>=0.5", "toml>=0.10", ] format = [ @@ -66,7 +67,6 @@ format = [ "isort>=5.10", ] maintain = [ - # TODO: remove this section when git-changelog is more powerful "git-changelog>=0.4", ] quality = [ @@ -88,7 +88,6 @@ tests = [ "pytest>=6.2", "pytest-cov>=3.0", "pytest-randomly>=3.10", - "pytest-sugar>=0.9", "pytest-xdist>=2.4", ] typing = [ diff --git a/scripts/fixsetup.sh b/scripts/fixsetup.sh deleted file mode 100755 index 94b63559..00000000 --- a/scripts/fixsetup.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -e - -PYTHON_VERSIONS="${PYTHON_VERSIONS-3.7 3.8 3.9 3.10 3.11}" - -for python_version in ${PYTHON_VERSIONS}; do - rm -rf "__pypackages__/${python_version}/lib/mkdocstrings" - rm -f "__pypackages__/${python_version}/lib/mkdocstrings.pth" - cp -r ../mkdocstrings/src/mkdocstrings "__pypackages__/${python_version}/lib/" -done diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py new file mode 100644 index 00000000..a21a1e4a --- /dev/null +++ b/scripts/gen_credits.py @@ -0,0 +1,110 @@ +import re +from itertools import chain +from pathlib import Path +from textwrap import dedent + +import toml +from jinja2 import StrictUndefined +from jinja2.sandbox import SandboxedEnvironment + +try: + from importlib.metadata import metadata, PackageNotFoundError +except ImportError: + from importlib_metadata import metadata, PackageNotFoundError + +project_dir = Path(".") +pyproject = toml.load(project_dir / "pyproject.toml") +project = pyproject["project"] +pdm = pyproject["tool"]["pdm"] +lock_data = toml.load(project_dir / "pdm.lock") +lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} +project_name = project["name"] +regex = re.compile(r"(?P[\w.-]+)(?P.*)$") + +def get_license(pkg_name): + try: + data = metadata(pkg_name) + except PackageNotFoundError: + return "?" + license = data.get("License", "").strip() + multiple_lines = bool(license.count("\n")) + # TODO: remove author logic once all my packages licenses are fixed + author = "" + if multiple_lines or not license or license == "UNKNOWN": + for header, value in data.items(): + if header == "Classifier" and value.startswith("License ::"): + license = value.rsplit("::", 1)[1].strip() + elif header == "Author-email": + author = value + if license == "Other/Proprietary License" and "pawamoy" in author: + license = "ISC" + return license or "?" + +def get_deps(base_deps): + deps = {} + for dep in base_deps: + parsed = regex.match(dep).groupdict() + dep_name = parsed["dist"].lower() + deps[dep_name] = {"license": get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + + again = True + while again: + again = False + for pkg_name in lock_pkgs: + if pkg_name in deps: + for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): + parsed = regex.match(pkg_dependency).groupdict() + dep_name = parsed["dist"].lower() + if dep_name not in deps: + deps[dep_name] = {"license": get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + again = True + + return deps + +dev_dependencies = get_deps(chain(*pdm.get("dev-dependencies", {}).values())) +prod_dependencies = get_deps( + chain( + project.get("dependencies", []), + chain(*project.get("optional-dependencies", {}).values()), + ) +) + +template_data = { + "project_name": project_name, + "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), + "more_credits": "http://pawamoy.github.io/credits/", +} +template_text = dedent( + """ + These projects were used to build `{{ project_name }}`. **Thank you!** + + [`python`](https://www.python.org/) | + [`pdm`](https://pdm.fming.dev/) | + [`copier-pdm`](https://github.com/pawamoy/copier-pdm) + + {% macro dep_line(dep) -%} + [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} + {%- endmacro %} + + ### Runtime dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in prod_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + ### Development dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in dev_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} + """ +) +jinja_env = SandboxedEnvironment(undefined=StrictUndefined) +print(jinja_env.from_string(template_text).render(**template_data)) diff --git a/scripts/setup.sh b/scripts/setup.sh index f0a41cf8..188eaebc 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -14,7 +14,17 @@ install_with_pipx() { install_with_pipx pdm +restore_previous_python_version() { + if pdm use -f "$1" &>/dev/null; then + echo "> Restored previous Python version: ${1##*/}" + fi +} + if [ -n "${PYTHON_VERSIONS}" ]; then + if old_python_version="$(pdm config python.path 2>/dev/null)"; then + echo "> Currently selected Python version: ${old_python_version##*/}" + trap "restore_previous_python_version ${old_python_version}" EXIT + fi for python_version in ${PYTHON_VERSIONS}; do if pdm use -f "python${python_version}" &>/dev/null; then echo "> Using Python ${python_version} interpreter" diff --git a/src/mkdocstrings_handlers/python/__init__.py b/src/mkdocstrings_handlers/python/__init__.py index 4823a66f..706d85ee 100644 --- a/src/mkdocstrings_handlers/python/__init__.py +++ b/src/mkdocstrings_handlers/python/__init__.py @@ -3,3 +3,7 @@ from mkdocstrings_handlers.python.handler import get_handler __all__ = ["get_handler"] # noqa: WPS410 + +# TODO: CSS classes everywhere in templates +# TODO: name normalization (filenames, Jinja2 variables, HTML tags, CSS classes) +# TODO: Jinja2 blocks everywhere in templates diff --git a/src/mkdocstrings_handlers/python/collector.py b/src/mkdocstrings_handlers/python/collector.py deleted file mode 100644 index ebddaaac..00000000 --- a/src/mkdocstrings_handlers/python/collector.py +++ /dev/null @@ -1,96 +0,0 @@ -"""This module implements a collector for the Python language. - -It collects data with [Griffe](https://github.com/pawamoy/griffe). -""" - -from __future__ import annotations - -from collections import ChainMap -from contextlib import suppress - -from griffe.agents.extensions import load_extensions -from griffe.collections import LinesCollection, ModulesCollection -from griffe.docstrings.parsers import Parser -from griffe.exceptions import AliasResolutionError -from griffe.loader import GriffeLoader -from mkdocstrings.handlers.base import BaseCollector, CollectionError, CollectorItem -from mkdocstrings.loggers import get_logger - -logger = get_logger(__name__) - - -class PythonCollector(BaseCollector): - """The class responsible for loading Jinja templates and rendering them. - - It defines some configuration options, implements the `render` method, - and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer]. - """ - - default_config: dict = {"docstring_style": "google", "docstring_options": {}} - """The default selection options. - - Option | Type | Description | Default - ------ | ---- | ----------- | ------- - **`docstring_style`** | `"google" | "numpy" | "sphinx" | None` | The docstring style to use. | `"google"` - **`docstring_options`** | `dict[str, Any]` | The options for the docstring parser. | `{}` - """ - - fallback_config: dict = {"fallback": True} - - def __init__(self) -> None: - """Initialize the collector.""" - self._modules_collection: ModulesCollection = ModulesCollection() - self._lines_collection: LinesCollection = LinesCollection() - - def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: WPS231 - """Collect the documentation tree given an identifier and selection options. - - Arguments: - identifier: The dotted-path of a Python object available in the Python path. - config: Selection options, used to alter the data collection done by `pytkdocs`. - - Raises: - CollectionError: When there was a problem collecting the object documentation. - - Returns: - The collected object-tree. - """ - module_name = identifier.split(".", 1)[0] - unknown_module = module_name not in self._modules_collection - if config.get("fallback", False) and unknown_module: - raise CollectionError("Not loading additional modules during fallback") - - final_config = ChainMap(config, self.default_config) - parser_name = final_config["docstring_style"] - parser_options = final_config["docstring_options"] - parser = parser_name and Parser(parser_name) - - if unknown_module: - loader = GriffeLoader( - extensions=load_extensions(final_config.get("extensions", [])), - docstring_parser=parser, - docstring_options=parser_options, - modules_collection=self._modules_collection, - lines_collection=self._lines_collection, - ) - try: - loader.load_module(module_name) - except ImportError as error: - raise CollectionError(str(error)) from error - - unresolved, iterations = loader.resolve_aliases(only_exported=True, only_known_modules=True) - if unresolved: - logger.warning(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations") - - try: - doc_object = self._modules_collection[identifier] - except KeyError as error: # noqa: WPS440 - raise CollectionError(f"{identifier} could not be found") from error - - if not unknown_module: - with suppress(AliasResolutionError): - if doc_object.docstring is not None: - doc_object.docstring.parser = parser - doc_object.docstring.parser_options = parser_options - - return doc_object diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index b1018e7f..301f02fc 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -1,15 +1,30 @@ """This module implements a handler for the Python language.""" +from __future__ import annotations + +import os import posixpath +import re +import sys +from collections import ChainMap +from contextlib import suppress from typing import Any, BinaryIO, Iterator, Optional, Tuple +from griffe.agents.extensions import load_extensions +from griffe.collections import LinesCollection, ModulesCollection +from griffe.docstrings.parsers import Parser +from griffe.exceptions import AliasResolutionError +from griffe.loader import GriffeLoader from griffe.logger import patch_loggers -from mkdocstrings.handlers.base import BaseHandler +from markdown import Markdown +from mkdocstrings.extension import PluginError +from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem from mkdocstrings.inventory import Inventory from mkdocstrings.loggers import get_logger -from mkdocstrings_handlers.python.collector import PythonCollector -from mkdocstrings_handlers.python.renderer import PythonRenderer +from mkdocstrings_handlers.python import rendering + +logger = get_logger(__name__) patch_loggers(get_logger) @@ -21,10 +36,109 @@ class PythonHandler(BaseHandler): domain: The cross-documentation domain/language for this handler. enable_inventory: Whether this handler is interested in enabling the creation of the `objects.inv` Sphinx inventory file. + fallback_theme: The fallback theme. + fallback_config: The configuration used to collect item during autorefs fallback. + default_config: The default rendering options, + see [`default_config`][mkdocstrings_handlers.python.handler.PythonHandler.default_config]. """ domain: str = "py" # to match Sphinx's default domain enable_inventory: bool = True + fallback_theme = "material" + fallback_config: dict = {"fallback": True} + default_config: dict = { + "docstring_style": "google", + "docstring_options": {}, + "show_root_heading": False, + "show_root_toc_entry": True, + "show_root_full_path": True, + "show_root_members_full_path": False, + "show_object_full_path": False, + "show_category_heading": False, + "show_if_no_docstring": False, + "show_signature": True, + "show_signature_annotations": False, + "separate_signature": False, + "line_length": 60, + "merge_init_into_class": False, + "show_source": True, + "show_bases": True, + "show_submodules": False, + "group_by_category": True, + "heading_level": 2, + "members_order": rendering.Order.alphabetical.value, + "docstring_section_style": "table", + "members": None, + "filters": ["!^_[^_]"], + "annotations_path": "brief", + } + """ + Attributes: Headings options: + heading_level (int): The initial heading level to use. Default: `2`. + show_root_heading (bool): Show the heading of the object at the root of the documentation tree + (i.e. the object referenced by the identifier after `:::`). Default: `False`. + show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`. + show_root_full_path (bool): Show the full Python path for the root object heading. Default: `True`. + show_root_members_full_path (bool): Show the full Python path of the root members. Default: `False`. + show_object_full_path (bool): Show the full Python path of every object. Default: `False`. + show_category_heading (bool): When grouped by categories, show a heading for each category. Default: `False`. + + Attributes: Members options: + members (list[str] | False | None): An explicit list of members to render. Default: `None`. + members_order (str): The members ordering to use. Options: `alphabetical` - order by the members names, + `source` - order members as they appear in the source file. Default: `"alphabetical"`. + filters (list[str] | None): A list of filters applied to filter objects based on their name. + A filter starting with `!` will exclude matching objects instead of including them. + The `members` option takes precedence over `filters` (filters will still be applied recursively + to lower members in the hierarchy). Default: `["!^_[^_]"]`. + group_by_category (bool): Group the object's children by categories: attributes, classes, functions, and modules. Default: `True`. + show_submodules (bool): When rendering a module, show its submodules recursively. Default: `False`. + + Attributes: Docstrings options: + docstring_style (str): The docstring style to use: `google`, `numpy`, `sphinx`, or `None`. Default: `"google"`. + docstring_options (dict): The options for the docstring parser. See parsers under [`griffe.docstrings`][]. + docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `"table"`. + line_length (int): Maximum line length when formatting code/signatures. Default: `60`. + merge_init_into_class (bool): Whether to merge the `__init__` method into the class' signature and docstring. Default: `False`. + show_if_no_docstring (bool): Show the object heading even if it has no docstring or children with docstrings. Default: `False`. + + Attributes: Signatures/annotations options: + annotations_path (str): The verbosity for annotations path: `brief` (recommended), or `source` (as written in the source). Default: `"brief"`. + show_signature (bool): Show methods and functions signatures. Default: `True`. + show_signature_annotations (bool): Show the type annotations in methods and functions signatures. Default: `False`. + separate_signature (bool): Whether to put the whole signature in a code block below the heading. Default: `False`. + + Attributes: Additional options: + show_bases (bool): Show the base classes of a class. Default: `True`. + show_source (bool): Show the source code of this object. Default: `True`. + """ # noqa: E501 + + def __init__( + self, *args: Any, config_file_path: str | None = None, paths: list[str] | None = None, **kwargs: Any + ) -> None: + """Initialize the handler. + + Parameters: + *args: Handler name, theme and custom templates. + config_file_path: The MkDocs configuration file path. + paths: A list of paths to use as Griffe search paths. + **kwargs: Same thing, but with keyword arguments. + """ + super().__init__(*args, **kwargs) + self._config_file_path = config_file_path + paths = paths or [] + if not paths and config_file_path: + paths.append(os.path.dirname(config_file_path)) + search_paths = [path for path in sys.path if path] # eliminate empty path + for path in reversed(paths): + if not os.path.isabs(path): + if config_file_path: + path = os.path.abspath(os.path.join(os.path.dirname(config_file_path), path)) + if path not in search_paths: + search_paths.insert(0, path) + self._paths = search_paths + self._modules_collection: ModulesCollection = ModulesCollection() + self._lines_collection: LinesCollection = LinesCollection() @classmethod def load_inventory( @@ -36,7 +150,7 @@ def load_inventory( ) -> Iterator[Tuple[str, str]]: """Yield items and their URLs from an inventory file streamed from `in_file`. - This implements mkdocstrings' `load_inventory` "protocol" (see plugin.py). + This implements mkdocstrings' `load_inventory` "protocol" (see [`mkdocstrings.plugin`][mkdocstrings.plugin]). Arguments: in_file: The binary file-like object to read the inventory from. @@ -53,10 +167,96 @@ def load_inventory( for item in Inventory.parse_sphinx(in_file, domain_filter=("py",)).values(): # noqa: WPS526 yield item.name, posixpath.join(base_url, item.uri) + def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: D102,WPS231 + module_name = identifier.split(".", 1)[0] + unknown_module = module_name not in self._modules_collection + if config.get("fallback", False) and unknown_module: + raise CollectionError("Not loading additional modules during fallback") + + final_config = ChainMap(config, self.default_config) + parser_name = final_config["docstring_style"] + parser_options = final_config["docstring_options"] + parser = parser_name and Parser(parser_name) + + if unknown_module: + loader = GriffeLoader( + extensions=load_extensions(final_config.get("extensions", [])), + search_paths=self._paths, + docstring_parser=parser, + docstring_options=parser_options, + modules_collection=self._modules_collection, + lines_collection=self._lines_collection, + ) + try: + loader.load_module(module_name) + except ImportError as error: + raise CollectionError(str(error)) from error + + unresolved, iterations = loader.resolve_aliases(only_exported=True, only_known_modules=True) + if unresolved: + logger.warning(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations") + + try: + doc_object = self._modules_collection[identifier] + except KeyError as error: # noqa: WPS440 + raise CollectionError(f"{identifier} could not be found") from error + + if not unknown_module: + with suppress(AliasResolutionError): + if doc_object.docstring is not None: + doc_object.docstring.parser = parser + doc_object.docstring.parser_options = parser_options + + return doc_object + + def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring) + final_config = ChainMap(config, self.default_config) + + template = self.env.get_template(f"{data.kind.value}.html") + + # 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 + # than as an item in a dictionary. + heading_level = final_config["heading_level"] + try: + final_config["members_order"] = rendering.Order(final_config["members_order"]) + except ValueError: + choices = "', '".join(item.value for item in rendering.Order) + raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.") + + if final_config["filters"]: + final_config["filters"] = [ + (re.compile(filtr.lstrip("!")), filtr.startswith("!")) for filtr in final_config["filters"] + ] + + return template.render( + **{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True}, + ) + + def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring) + super().update_env(md, config) + self.env.trim_blocks = True + self.env.lstrip_blocks = True + self.env.keep_trailing_newline = False + self.env.filters["crossref"] = rendering.do_crossref + self.env.filters["multi_crossref"] = rendering.do_multi_crossref + self.env.filters["order_members"] = rendering.do_order_members + self.env.filters["format_code"] = rendering.do_format_code + self.env.filters["format_signature"] = rendering.do_format_signature + self.env.filters["filter_objects"] = rendering.do_filter_objects + + def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring) + try: + return list({data.path, data.canonical_path, *data.aliases}) + except AliasResolutionError: + return [data.path] + def get_handler( theme: str, # noqa: W0613 (unused argument config) custom_templates: Optional[str] = None, + config_file_path: str | None = None, + paths: list[str] | None = None, **config: Any, ) -> PythonHandler: """Simply return an instance of `PythonHandler`. @@ -64,12 +264,17 @@ def get_handler( Arguments: theme: The theme to use when rendering contents. custom_templates: Directory containing custom templates. + config_file_path: The MkDocs configuration file path. + paths: A list of paths to use as Griffe search paths. **config: Configuration passed to the handler. Returns: An instance of `PythonHandler`. """ return PythonHandler( - collector=PythonCollector(), - renderer=PythonRenderer("python", theme, custom_templates), + handler="python", + theme=theme, + custom_templates=custom_templates, + config_file_path=config_file_path, + paths=paths, ) diff --git a/src/mkdocstrings_handlers/python/renderer.py b/src/mkdocstrings_handlers/python/renderer.py deleted file mode 100644 index 93c91195..00000000 --- a/src/mkdocstrings_handlers/python/renderer.py +++ /dev/null @@ -1,249 +0,0 @@ -"""This module implements a renderer for the Python language.""" - -from __future__ import annotations - -import enum -import re -import sys -from collections import ChainMap -from functools import lru_cache -from typing import Any, Sequence - -from griffe.dataclasses import Alias, Object -from griffe.exceptions import AliasResolutionError -from markdown import Markdown -from markupsafe import Markup -from mkdocstrings.extension import PluginError -from mkdocstrings.handlers.base import BaseRenderer, CollectorItem -from mkdocstrings.loggers import get_logger - -logger = get_logger(__name__) -# TODO: CSS classes everywhere in templates -# TODO: name normalization (filenames, Jinja2 variables, HTML tags, CSS classes) -# TODO: Jinja2 blocks everywhere in templates - - -class Order(enum.Enum): - """Enumeration for the possible members ordering.""" - - alphabetical = "alphabetical" - source = "source" - - -def _sort_key_alphabetical(item: CollectorItem) -> Any: - # chr(sys.maxunicode) is a string that contains the final unicode - # character, so if 'name' isn't found on the object, the item will go to - # the end of the list. - return item.name or chr(sys.maxunicode) - - -def _sort_key_source(item: CollectorItem) -> Any: - # if 'lineno' is none, the item will go to the start of the list. - return item.lineno if item.lineno is not None else -1 - - -order_map = { - Order.alphabetical: _sort_key_alphabetical, - Order.source: _sort_key_source, -} - - -class PythonRenderer(BaseRenderer): - """The class responsible for loading Jinja templates and rendering them. - - It defines some configuration options, implements the `render` method, - and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer]. - - Attributes: - fallback_theme: The theme to fallback to. - default_config: The default rendering options, - see [`default_config`][mkdocstrings_handlers.python.renderer.PythonRenderer.default_config]. - """ - - fallback_theme = "material" - - default_config: dict = { - "show_root_heading": False, - "show_root_toc_entry": True, - "show_root_full_path": True, - "show_root_members_full_path": False, - "show_object_full_path": False, - "show_category_heading": False, - "show_if_no_docstring": False, - "show_signature": True, - "show_signature_annotations": False, - "separate_signature": False, - "line_length": 60, - "merge_init_into_class": False, - "show_source": True, - "show_bases": True, - "show_submodules": True, - "group_by_category": True, - "heading_level": 2, - "members_order": Order.alphabetical.value, - "docstring_section_style": "table", - } - """The default rendering options. - - Option | Type | Description | Default - ------ | ---- | ----------- | ------- - **`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False` - **`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True` - **`show_root_full_path`** | `bool` | Show the full Python path for the root object heading. | `True` - **`show_object_full_path`** | `bool` | Show the full Python path of every object. | `False` - **`show_root_members_full_path`** | `bool` | Show the full Python path of objects that are children of the root object (for example, classes in a module). When False, `show_object_full_path` overrides. | `False` - **`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False` - **`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False` - **`show_signature`** | `bool` | Show method and function signatures. | `True` - **`show_signature_annotations`** | `bool` | Show the type annotations in method and function signatures. | `False` - **`separate_signature`** | `bool` | Whether to put the whole signature in a code block below the heading. | `False` - **`line_length`** | `int` | Maximum line length when formatting code. | `60` - **`merge_init_into_class`** | `bool` | Whether to merge the `__init__` method into the class' signature and docstring. | `False` - **`show_source`** | `bool` | Show the source code of this object. | `True` - **`show_bases`** | `bool` | Show the base classes of a class. | `True` - **`show_submodules`** | `bool` | When rendering a module, show its submodules recursively. | `True` - **`group_by_category`** | `bool` | Group the object's children by categories: attributes, classes, functions, methods, and modules. | `True` - **`heading_level`** | `int` | The initial heading level to use. | `2` - **`members_order`** | `str` | The members ordering to use. Options: `alphabetical` - order by the members names, `source` - order members as they appear in the source file. | `alphabetical` - **`docstring_section_style`** | `str` | The style used to render docstring sections. Options: `table`, `list`, `spacy`. | `table` - """ # noqa: E501 - - def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring) - final_config = ChainMap(config, self.default_config) - - template = self.env.get_template(f"{data.kind.value}.html") - - # 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 - # than as an item in a dictionary. - heading_level = final_config["heading_level"] - try: - final_config["members_order"] = Order(final_config["members_order"]) - except ValueError: - choices = "', '".join(item.value for item in Order) - raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.") - - return template.render( - **{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True}, - ) - - def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring) - try: - return list({data.path, data.canonical_path, *data.aliases}) - except AliasResolutionError: - return [data.path] - - def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring) - super().update_env(md, config) - self.env.trim_blocks = True - self.env.lstrip_blocks = True - self.env.keep_trailing_newline = False - self.env.filters["crossref"] = self.do_crossref - self.env.filters["multi_crossref"] = self.do_multi_crossref - self.env.filters["order_members"] = self.do_order_members - self.env.filters["format_code"] = self.do_format_code - self.env.filters["format_signature"] = self.do_format_signature - - def do_format_code(self, code: str, line_length: int) -> str: - """Format code using Black. - - Parameters: - code: The code to format. - line_length: The line length to give to Black. - - Returns: - The same code, formatted. - """ - code = code.strip() - if len(code) < line_length: - return code - formatter = _get_black_formatter() - return formatter(code, line_length) - - def do_format_signature(self, signature: str, line_length: int) -> str: - """Format a signature using Black. - - Parameters: - signature: The signature to format. - line_length: The line length to give to Black. - - Returns: - The same code, formatted. - """ - code = signature.strip() - if len(code) < line_length: - return code - formatter = _get_black_formatter() - formatted = formatter(f"def {code}: pass", line_length) - # remove starting `def ` and trailing `: pass` - return formatted[4:-5].strip()[:-1] - - def do_order_members(self, members: Sequence[Object | Alias], order: Order) -> Sequence[Object | Alias]: - """Order members given an ordering method. - - Parameters: - members: The members to order. - order: The ordering method. - - Returns: - The same members, ordered. - """ - return sorted(members, key=order_map[order]) - - def do_crossref(self, path: str, brief: bool = True) -> Markup: - """Filter to create cross-references. - - Parameters: - path: The path to link to. - brief: Show only the last part of the path, add full path as hover. - - Returns: - Markup text. - """ - full_path = path - if brief: - path = full_path.split(".")[-1] - return Markup("{path}").format( - full_path=full_path, path=path - ) - - def do_multi_crossref(self, text: str, code: bool = True) -> Markup: - """Filter to create cross-references. - - Parameters: - text: The text to scan. - code: Whether to wrap the result in a code tag. - - Returns: - Markup text. - """ - group_number = 0 - variables = {} - - def repl(match): # noqa: WPS430 - nonlocal group_number # noqa: WPS420 - group_number += 1 - path = match.group() - path_var = f"path{group_number}" - variables[path_var] = path - return f"{{{path_var}}}" - - text = re.sub(r"([\w.]+)", repl, text) - if code: - text = f"{text}" - return Markup(text).format(**variables) - - -@lru_cache(maxsize=1) -def _get_black_formatter(): - try: - from black import Mode, format_str - except ModuleNotFoundError: - logger.warning("Formatting signatures requires Black to be installed.") - return lambda text, _: text - - def formatter(code, line_length): # noqa: WPS430 - mode = Mode(line_length=line_length) - return format_str(code, mode=mode) - - return formatter diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py new file mode 100644 index 00000000..8e5f7d85 --- /dev/null +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -0,0 +1,209 @@ +"""This module implements rendering utilities.""" + +from __future__ import annotations + +import enum +import re +import sys +from functools import lru_cache +from typing import Any, Pattern, Sequence + +from griffe.dataclasses import Alias, Object +from markupsafe import Markup +from mkdocstrings.handlers.base import CollectorItem +from mkdocstrings.loggers import get_logger + +logger = get_logger(__name__) + + +class Order(enum.Enum): + """Enumeration for the possible members ordering.""" + + alphabetical = "alphabetical" + source = "source" + + +def _sort_key_alphabetical(item: CollectorItem) -> Any: + # chr(sys.maxunicode) is a string that contains the final unicode + # character, so if 'name' isn't found on the object, the item will go to + # the end of the list. + return item.name or chr(sys.maxunicode) + + +def _sort_key_source(item: CollectorItem) -> Any: + # if 'lineno' is none, the item will go to the start of the list. + return item.lineno if item.lineno is not None else -1 + + +order_map = { + Order.alphabetical: _sort_key_alphabetical, + Order.source: _sort_key_source, +} + + +def do_format_code(code: str, line_length: int) -> str: + """Format code using Black. + + Parameters: + code: The code to format. + line_length: The line length to give to Black. + + Returns: + The same code, formatted. + """ + code = code.strip() + if len(code) < line_length: + return code + formatter = _get_black_formatter() + return formatter(code, line_length) + + +def do_format_signature(signature: str, line_length: int) -> str: + """Format a signature using Black. + + Parameters: + signature: The signature to format. + line_length: The line length to give to Black. + + Returns: + The same code, formatted. + """ + code = signature.strip() + if len(code) < line_length: + return code + formatter = _get_black_formatter() + formatted = formatter(f"def {code}: pass", line_length) + # remove starting `def ` and trailing `: pass` + return formatted[4:-5].strip()[:-1] + + +def do_order_members( + members: Sequence[Object | Alias], + order: Order, + members_list: list[str] | None, +) -> Sequence[Object | Alias]: + """Order members given an ordering method. + + Parameters: + members: The members to order. + order: The ordering method. + members_list: An optional member list (manual ordering). + + Returns: + The same members, ordered. + """ + if members_list: + sorted_members = [] + members_dict = {member.name: member for member in members} + for name in members_list: + if name in members_dict: + sorted_members.append(members_dict[name]) + return sorted_members + return sorted(members, key=order_map[order]) + + +def do_crossref(path: str, brief: bool = True) -> Markup: + """Filter to create cross-references. + + Parameters: + path: The path to link to. + brief: Show only the last part of the path, add full path as hover. + + Returns: + Markup text. + """ + full_path = path + if brief: + path = full_path.split(".")[-1] + return Markup("{path}").format(full_path=full_path, path=path) + + +def do_multi_crossref(text: str, code: bool = True) -> Markup: + """Filter to create cross-references. + + Parameters: + text: The text to scan. + code: Whether to wrap the result in a code tag. + + Returns: + Markup text. + """ + group_number = 0 + variables = {} + + def repl(match): # noqa: WPS430 + nonlocal group_number # noqa: WPS420 + group_number += 1 + path = match.group() + path_var = f"path{group_number}" + variables[path_var] = path + return f"{{{path_var}}}" + + text = re.sub(r"([\w.]+)", repl, text) + if code: + text = f"{text}" + return Markup(text).format(**variables) + + +def _keep_object(name, filters): + keep = None + rules = set() + for regex, exclude in filters: + rules.add(exclude) + if regex.search(name): + keep = not exclude + if keep is None: + if rules == {False}: # noqa: WPS531 + # only included stuff, no match = reject + return False + # only excluded stuff, or included and excluded stuff, no match = keep + return True + return keep + + +def do_filter_objects( + objects_dictionary: dict[str, Object | Alias], + filters: list[tuple[bool, Pattern]] | None = None, + members_list: list[str] | None = None, + keep_no_docstrings: bool = True, +) -> list[Object | Alias]: + """Filter a dictionary of objects based on their docstrings. + + Parameters: + objects_dictionary: The dictionary of objects. + filters: Filters to apply, based on members' names. + Each element is a tuple: a pattern, and a boolean indicating whether + to reject the object if the pattern matches. + members_list: An optional, explicit list of members to keep. + When given and empty, return an empty list. + When given and not empty, ignore filters and docstrings presence/absence. + keep_no_docstrings: Whether to keep objects with no/empty docstrings (recursive check). + + Returns: + A list of objects. + """ + if members_list is not None: + if not members_list: + return [] + return [obj for obj in objects_dictionary.values() if obj.name in set(members_list)] + objects = list(objects_dictionary.values()) + if filters: + objects = [obj for obj in objects if _keep_object(obj.name, filters)] + if keep_no_docstrings: + return objects + return [obj for obj in objects if obj.has_docstrings] + + +@lru_cache(maxsize=1) +def _get_black_formatter(): + try: + from black import Mode, format_str + except ModuleNotFoundError: + logger.warning("Formatting signatures requires Black to be installed.") + return lambda text, _: text + + def formatter(code, line_length): # noqa: WPS430 + mode = Mode(line_length=line_length) + return format_str(code, mode=mode) + + return formatter diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html index 527c38fd..019e7fae 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html @@ -1,72 +1,69 @@ {{ log.debug("Rendering " + attribute.path) }} -{% if config.show_if_no_docstring or attribute.has_docstrings %} -
- {% with html_id = attribute.path %} +
+{% with html_id = attribute.path %} - {% if not root or config.show_root_heading %} + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} - - {% filter heading(heading_level, - role="data" if attribute.parent.kind.value == "module" else "attr", - id=html_id, - class="doc doc-heading", - toc_label=attribute.name) %} + {% if not root or config.show_root_heading %} - {% if config.separate_signature %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% else %} - {% filter highlight(language="python", inline=True) %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% if attribute.annotation %}: {{ attribute.annotation }}{% endif %} - {% if attribute.value %} = {{ attribute.value }}{% endif %} - {% endfilter %} - {% endif %} - - {% with labels = attribute.labels %} - {% include "labels.html" with context %} - {% endwith %} - - {% endfilter %} + {% filter heading(heading_level, + role="data" if attribute.parent.kind.value == "module" else "attr", + id=html_id, + class="doc doc-heading", + toc_label=attribute.name) %} {% if config.separate_signature %} - {% filter highlight(language="python", inline=False) %} - {% filter format_code(config.line_length) %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% if attribute.annotation %}: {{ attribute.annotation|safe }}{% endif %} - {% if attribute.value %} = {{ attribute.value|safe }}{% endif %} - {% endfilter %} + {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} + {% else %} + {% filter highlight(language="python", inline=True) %} + {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} + {% if attribute.annotation %}: {{ attribute.annotation }}{% endif %} + {% if attribute.value %} = {{ attribute.value }}{% endif %} {% endfilter %} {% endif %} - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="data" if attribute.parent.kind.value == "module" else "attr", - id=html_id, - toc_label=attribute.path, - hidden=True) %} + {% with labels = attribute.labels %} + {% include "labels.html" with context %} + {% endwith %} + + {% endfilter %} + + {% if config.separate_signature %} + {% filter highlight(language="python", inline=False) %} + {% filter format_code(config.line_length) %} + {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} + {% if attribute.annotation %}: {{ attribute.annotation|safe }}{% endif %} + {% if attribute.value %} = {{ attribute.value|safe }}{% endif %} {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} + {% endfilter %} {% endif %} -
- {% with docstring_sections = attribute.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} -
+ {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="data" if attribute.parent.kind.value == "module" else "attr", + id=html_id, + toc_label=attribute.path if config.show_root_full_path else attribute.name, + hidden=True) %} + {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} - {% endwith %} +
+ {% with docstring_sections = attribute.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %}
-{% endif %} +{% endwith %} +
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/children.html b/src/mkdocstrings_handlers/python/templates/material/_base/children.html index 6a5b40f5..71755ea7 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/children.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/children.html @@ -1,8 +1,14 @@ -{{ log.debug("Rendering children of " + obj.path) }} {% if obj.members %} + {{ log.debug("Rendering children of " + obj.path) }}
+ {% if root_members %} + {% set members_list = config.members %} + {% else %} + {% set members_list = none %} + {% endif %} + {% if config.group_by_category %} {% with %} @@ -13,51 +19,67 @@ {% set extra_level = 0 %} {% endif %} - {% if config.show_category_heading and obj.attributes.values()|any %} - {% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for attribute in obj.attributes.values()|order_members(config.members_order) %} - {% if not attribute.is_alias or attribute.is_explicitely_exported %} - {% include "attribute.html" with context %} + {% with attributes = obj.attributes|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if attributes %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %} {% endif %} - {% endfor %} + {% 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 %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} - {% if config.show_category_heading and obj.classes.values()|any %} - {% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for class in obj.classes.values()|order_members(config.members_order) %} - {% if not class.is_alias or class.is_explicitely_exported %} - {% include "class.html" with context %} + {% with classes = obj.classes|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if classes %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %} {% endif %} - {% endfor %} + {% 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 %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} - {% if config.show_category_heading and obj.functions.values()|any %} - {% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for function in obj.functions.values()|order_members(config.members_order) %} - {% 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 %} - {% endif %} + {% with functions = obj.functions|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if functions %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %} {% endif %} - {% endfor %} + {% with heading_level = heading_level + extra_level %} + {% 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 %} + {% endif %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} {% if config.show_submodules %} - {% if config.show_category_heading and obj.modules.values()|any %} - {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for module in obj.modules.values()|order_members(config.members_order) %} - {% if not module.is_alias or module.is_explicitely_exported %} - {% include "module.html" with context %} + {% with modules = obj.modules|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if modules %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} {% endif %} - {% endfor %} + {% 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 %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} {% endif %} @@ -65,7 +87,9 @@ {% else %} - {% for child in obj.members.values()|order_members(config.members_order) %} + {% for child in obj.members| + filter_objects(config.filters, members_list, 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) %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/class.html b/src/mkdocstrings_handlers/python/templates/material/_base/class.html index 0e629dbe..ff102c88 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html @@ -1,116 +1,113 @@ {{ log.debug("Rendering " + class.path) }} -{% if config.show_if_no_docstring or class.has_docstrings %} -
- {% with html_id = class.path %} - - {% if not root or config.show_root_heading %} - - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} +
+{% with html_id = class.path %} + + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} + + {% if not root or config.show_root_heading %} + + {% filter heading(heading_level, + role="class", + id=html_id, + class="doc doc-heading", + toc_label=class.name) %} + + {% if config.separate_signature %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {% elif config.merge_init_into_class and "__init__" in class.members -%} + {%- with function = class.members["__init__"] -%} + {%- filter highlight(language="python", inline=True) -%} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {%- include "signature.html" with context -%} + {%- endfilter -%} + {%- endwith -%} {% else %} - {% set show_full_path = config.show_object_full_path %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} {% endif %} - {% filter heading(heading_level, - role="class", - id=html_id, - class="doc doc-heading", - toc_label=class.name) %} + {% with labels = class.labels %} + {% include "labels.html" with context %} + {% endwith %} - {% if config.separate_signature %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {% elif config.merge_init_into_class and "__init__" in class.members -%} - {%- with function = class.members["__init__"] -%} - {%- filter highlight(language="python", inline=True) -%} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {%- include "signature.html" with context -%} - {%- endfilter -%} - {%- endwith -%} - {% else %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {% endif %} + {% endfilter %} - {% with labels = class.labels %} - {% include "labels.html" with context %} + {% if config.separate_signature and config.merge_init_into_class %} + {% if "__init__" in class.members %} + {% with function = class.members["__init__"] %} + {% filter highlight(language="python", inline=False) %} + {% filter format_signature(config.line_length) %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {% include "signature.html" with context %} + {% endfilter %} + {% endfilter %} {% endwith %} + {% endif %} + {% endif %} + {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="class", + id=html_id, + toc_label=class.path if config.show_root_full_path else class.name, + hidden=True) %} {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} + +
+ {% if config.show_bases and class.bases %} +

+ Bases: {% for expression in class.bases -%} + {% include "expression.html" with context %}{% if not loop.last %}, {% endif %} + {% endfor -%} +

+ {% endif %} - {% if config.separate_signature and config.merge_init_into_class %} - {% if "__init__" in class.members %} - {% with function = class.members["__init__"] %} - {% filter highlight(language="python", inline=False) %} - {% filter format_signature(config.line_length) %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} - {% endfilter %} - {% endwith %} - {% endif %} - {% endif %} + {% with docstring_sections = class.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="class", - id=html_id, - toc_label=class.path, - hidden=True) %} - {% endfilter %} + {% if config.merge_init_into_class %} + {% if "__init__" in class.members and class.members["__init__"].has_docstring %} + {% with docstring_sections = class.members["__init__"].docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} {% endif %} - {% set heading_level = heading_level - 1 %} {% endif %} -
- {% if config.show_bases and class.bases %} -

- Bases: {% for expression in class.bases -%} - {% include "expression.html" with context %}{% if not loop.last %}, {% endif %} - {% endfor -%} -

- {% endif %} - - {% with docstring_sections = class.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} - + {% if config.show_source %} {% if config.merge_init_into_class %} - {% if "__init__" in class.members and class.members["__init__"].has_docstring %} - {% with docstring_sections = class.members["__init__"].docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} - {% endif %} - {% endif %} - - {% if config.show_source %} - {% if config.merge_init_into_class %} - {% if "__init__" in class.members and class.members["__init__"].source %} -
- Source code in {{ class.relative_filepath }} - {{ class.members["__init__"].source|highlight(language="python", linestart=class.members["__init__"].lineno, linenums=True) }} -
- {% endif %} - {% elif class.source %} + {% if "__init__" in class.members and class.members["__init__"].source %}
Source code in {{ class.relative_filepath }} - {{ class.source|highlight(language="python", linestart=class.lineno, linenums=True) }} + {{ class.members["__init__"].source|highlight(language="python", linestart=class.members["__init__"].lineno, linenums=True) }}
{% endif %} + {% elif class.source %} +
+ Source code in {{ class.relative_filepath }} + {{ class.source|highlight(language="python", linestart=class.lineno, linenums=True) }} +
{% endif %} + {% endif %} - {% with obj = class %} - {% set root = False %} - {% set heading_level = heading_level + 1 %} - {% include "children.html" with context %} - {% endwith %} -
- - {% endwith %} + {% with obj = class %} + {% set root = False %} + {% set heading_level = heading_level + 1 %} + {% include "children.html" with context %} + {% endwith %}
-{% endif %} +{% endwith %} +
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html index b473a8f8..bd1b6963 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html @@ -1,5 +1,5 @@ -{{ log.debug("Rendering docstring") }} {% if docstring_sections %} + {{ log.debug("Rendering docstring") }} {% for section in docstring_sections %} {% if section.kind.value == "text" %} {{ section.value|convert_markdown(heading_level, html_id) }} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html index 5d0dc85f..9a1409c0 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering attributes section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Attributes:" }}

@@ -25,7 +26,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Attributes:" }}

    {% for attribute in section.value %} @@ -40,11 +43,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -69,4 +74,5 @@ {% endfor %}
ATTRIBUTE{{ (section.title or "ATTRIBUTE").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html index 709a799c..4b9f7339 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering other parameters section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Other Parameters:" }}

@@ -25,7 +26,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Other Parameters:" }}

    {% for parameter in section.value %} @@ -40,11 +43,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -69,4 +74,5 @@ {% endfor %}
PARAMETER{{ (section.title or "PARAMETER").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html index 4a28ae1c..cc954596 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering parameters section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Parameters:" }}

@@ -35,7 +36,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Parameters:" }}

    {% for parameter in section.value %} @@ -50,11 +53,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -87,4 +92,5 @@ {% endfor %}
PARAMETER{{ (section.title or "PARAMETER").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html index 71d76e1f..32ad3506 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering raises section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Raises:" }}

@@ -23,7 +24,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Raises:" }}

    {% for raises in section.value %} @@ -38,11 +41,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -63,4 +68,5 @@ {% endfor %}
RAISES{{ (section.title or "RAISES").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html index 0a6c5b5c..7946329b 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering receives section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %} {% set name_column = section.value|selectattr("name")|any %}

{{ section.title or "Receives:" }}

@@ -26,7 +27,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Receives:" }}

    {% for receives in section.value %} @@ -43,11 +46,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -82,4 +87,5 @@ {% endfor %}
RECEIVES{{ (section.title or "RECEIVES").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html index b62c809a..0d620d12 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering returns section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %} {% set name_column = section.value|selectattr("name")|any %}

{{ section.title or "Returns:" }}

@@ -26,7 +27,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Returns:" }}

    {% for returns in section.value %} @@ -43,11 +46,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -82,4 +87,5 @@ {% endfor %}
RETURNS{{ (section.title or "RETURNS").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html index 4b2d6865..e5a59a84 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering warns section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Warns:" }}

@@ -23,7 +24,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Warns:" }}

    {% for warns in section.value %} @@ -38,11 +41,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -63,4 +68,5 @@ {% endfor %}
WARNS{{ (section.title or "WARNS").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html index b24ce805..22d6828f 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering yields section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %} {% set name_column = section.value|selectattr("name")|any %}

{{ section.title or "Yields:" }}

@@ -26,7 +27,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Yields:" }}

    {% for yields in section.value %} @@ -43,11 +46,13 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} - + @@ -82,4 +87,5 @@ {% endfor %}
YIELDS{{ (section.title or "YIELDS").rstrip(":").upper() }} DESCRIPTION
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html index 76a50da7..3347e272 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html @@ -6,5 +6,7 @@ {%- elif original_expression is string -%} {{ original_expression }} {%- else -%} - {{ original_expression.source }} + {%- with annotation = original_expression|attr(config.annotations_path) -%} + {{ annotation }} + {%- endwith -%} {%- endif -%} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/function.html b/src/mkdocstrings_handlers/python/templates/material/_base/function.html index 13639f57..b9b1696c 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/function.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/function.html @@ -1,77 +1,74 @@ {{ log.debug("Rendering " + function.path) }} -{% if config.show_if_no_docstring or function.has_docstrings %} -
- {% with html_id = function.path %} +
+{% with html_id = function.path %} - {% if not root or config.show_root_heading %} + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} + {% if not root or config.show_root_heading %} - {% filter heading(heading_level, - role="function", - id=html_id, - class="doc doc-heading", - toc_label=function.name ~ "()") %} - - {% if config.separate_signature %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% else %} - {% filter highlight(language="python", inline=True) %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} - {% endif %} - - {% with labels = function.labels %} - {% include "labels.html" with context %} - {% endwith %} - - {% endfilter %} + {% filter heading(heading_level, + role="function", + id=html_id, + class="doc doc-heading", + toc_label=function.name ~ "()") %} {% if config.separate_signature %} - {% filter highlight(language="python", inline=False) %} - {% filter format_signature(config.line_length) %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% else %} + {% filter highlight(language="python", inline=True) %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% include "signature.html" with context %} {% endfilter %} {% endif %} - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="function", - id=html_id, - toc_label=function.path, - hidden=True) %} + {% with labels = function.labels %} + {% include "labels.html" with context %} + {% endwith %} + + {% endfilter %} + + {% if config.separate_signature %} + {% filter highlight(language="python", inline=False) %} + {% filter format_signature(config.line_length) %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% include "signature.html" with context %} {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} + {% endfilter %} {% endif %} -
- {% with docstring_sections = function.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} + {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="function", + id=html_id, + toc_label=function.path if config.show_root_full_path else function.name, + hidden=True) %} + {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} - {% if config.show_source and function.source %} -
- Source code in {{ function.relative_filepath }} - {{ function.source|highlight(language="python", linestart=function.lineno, linenums=True) }} -
- {% endif %} -
+
+ {% with docstring_sections = function.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} - {% endwith %} + {% if config.show_source and function.source %} +
+ Source code in {{ function.relative_filepath }} + {{ function.source|highlight(language="python", linestart=function.lineno, linenums=True) }} +
+ {% endif %}
-{% endif %} +{% endwith %} +
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/labels.html b/src/mkdocstrings_handlers/python/templates/material/_base/labels.html index a7e8ec38..4f2f72d9 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/labels.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/labels.html @@ -1,6 +1,6 @@ -{{ log.debug("Rendering labels") }} {% if labels %} - + {{ log.debug("Rendering labels") }} + {% for label in labels %} {{ label }} {% endfor %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/module.html b/src/mkdocstrings_handlers/python/templates/material/_base/module.html index 54e4d4e4..8e14d354 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/module.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/module.html @@ -1,60 +1,63 @@ {{ log.debug("Rendering " + module.path) }} -{% if config.show_if_no_docstring or module.has_docstrings %} -
- {% with html_id = module.path %} +
+{% with html_id = module.path %} + + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} + + {% if not root or config.show_root_heading %} + + {% filter heading(heading_level, + role="module", + id=html_id, + class="doc doc-heading", + toc_label=module.name) %} + + {% with module_name = module.path if show_full_path else module.name %} + {% if config.separate_signature %} + {{ module_name }} + {% else %} + {{ module_name }} + {% endif %} + {% endwith %} - {% if not root or config.show_root_heading %} + {% with labels = module.labels %} + {% include "labels.html" with context %} + {% endwith %} - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} + {% endfilter %} + {% else %} + {% if config.show_root_toc_entry %} {% filter heading(heading_level, role="module", id=html_id, - class="doc doc-heading", - toc_label=module.name) %} - - {% if show_full_path %}{{ module.path }}{% else %}{{ module.name }}{% endif %} - - {% with labels = module.labels %} - {% include "labels.html" with context %} - {% endwith %} - + toc_label=module.path if config.show_root_full_path else module.name, + hidden=True) %} {% endfilter %} - - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="module", - id=html_id, - toc_label=module.path, - hidden=True) %} - {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} {% endif %} - -
- {% with docstring_sections = module.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} - - {% with obj = module %} - {% set root = False %} - {% set heading_level = heading_level + 1 %} - {% include "children.html" with context %} - {% endwith %} -
- - {% endwith %} + {% set heading_level = heading_level - 1 %} + {% endif %} + +
+ {% with docstring_sections = module.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} + + {% with obj = module %} + {% set root = False %} + {% set heading_level = heading_level + 1 %} + {% include "children.html" with context %} + {% endwith %}
-{% endif %} +{% endwith %} +
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/signature.html b/src/mkdocstrings_handlers/python/templates/material/_base/signature.html index fb116880..ca4bbd44 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/signature.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/signature.html @@ -1,5 +1,5 @@ -{{ log.debug("Rendering signature of " + function.path) }} {%- if config.show_signature -%} + {{ log.debug("Rendering signature") }} {%- with -%} {%- set ns = namespace(render_pos_only_separator=True, render_kw_only_separator=True, equal="=") -%} diff --git a/tests/conftest.py b/tests/conftest.py index ab919d29..6e7766b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -86,16 +86,16 @@ def fixture_ext_markdown(plugin): return plugin.md -@pytest.fixture(name="renderer") -def fixture_renderer(plugin): - """Return a PythonRenderer instance. +@pytest.fixture(name="handler") +def fixture_handler(plugin): + """Return a handler instance. Parameters: plugin: Pytest fixture: [tests.conftest.fixture_plugin][]. Returns: - A renderer instance. + A handler instance. """ handler = plugin.handlers.get_handler("python") - handler.renderer._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 - return handler.renderer + handler._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 + return handler diff --git a/tests/test_collector.py b/tests/test_collector.py deleted file mode 100644 index 53544c77..00000000 --- a/tests/test_collector.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Tests for the `collector` module.""" - -import pytest - -from mkdocstrings_handlers.python.collector import CollectionError, PythonCollector - - -def test_collect_missing_module(): - """Assert error is raised for missing modules.""" - collector = PythonCollector() - with pytest.raises(CollectionError): - collector.collect("aaaaaaaa", {}) - - -def test_collect_missing_module_item(): - """Assert error is raised for missing items within existing modules.""" - collector = PythonCollector() - with pytest.raises(CollectionError): - collector.collect("mkdocstrings.aaaaaaaa", {}) - - -def test_collect_module(): - """Assert existing module can be collected.""" - collector = PythonCollector() - assert collector.collect("mkdocstrings", {}) - - -def test_collect_with_null_parser(): - """Assert we can pass `None` as parser when collecting.""" - collector = PythonCollector() - assert collector.collect("mkdocstrings", {"docstring_style": None}) diff --git a/tests/test_handler.py b/tests/test_handler.py new file mode 100644 index 00000000..5a49a8b1 --- /dev/null +++ b/tests/test_handler.py @@ -0,0 +1,60 @@ +"""Tests for the `handler` module.""" + +import pytest +from griffe.docstrings.dataclasses import DocstringSectionExamples, DocstringSectionKind + +from mkdocstrings_handlers.python.handler import CollectionError, get_handler + + +def test_collect_missing_module(): + """Assert error is raised for missing modules.""" + handler = get_handler(theme="material") + with pytest.raises(CollectionError): + handler.collect("aaaaaaaa", {}) + + +def test_collect_missing_module_item(): + """Assert error is raised for missing items within existing modules.""" + handler = get_handler(theme="material") + with pytest.raises(CollectionError): + handler.collect("mkdocstrings.aaaaaaaa", {}) + + +def test_collect_module(): + """Assert existing module can be collected.""" + handler = get_handler(theme="material") + assert handler.collect("mkdocstrings", {}) + + +def test_collect_with_null_parser(): + """Assert we can pass `None` as parser when collecting.""" + handler = get_handler(theme="material") + assert handler.collect("mkdocstrings", {"docstring_style": None}) + + +@pytest.mark.parametrize( + "handler", + [ + {"theme": "mkdocs"}, + {"theme": "readthedocs"}, + {"theme": {"name": "material"}}, + ], + indirect=["handler"], +) +def test_render_docstring_examples_section(handler): + """Assert docstrings' examples section can be rendered. + + Parameters: + handler: A handler instance (parametrized). + """ + section = DocstringSectionExamples( + value=[ + (DocstringSectionKind.text, "This is an example."), + (DocstringSectionKind.examples, ">>> print('Hello')\nHello"), + ], + ) + template = handler.env.get_template("docstring/examples.html") + rendered = template.render(section=section) + assert "

This is an example.

" in rendered + assert "print" in rendered + assert "Hello" in rendered diff --git a/tests/test_renderer.py b/tests/test_renderer.py deleted file mode 100644 index 31232e12..00000000 --- a/tests/test_renderer.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Tests for the `renderer` module.""" - -import pytest -from griffe.docstrings.dataclasses import DocstringSection, DocstringSectionKind - - -@pytest.mark.parametrize( - "renderer", - [ - {"theme": "mkdocs"}, - {"theme": "readthedocs"}, - {"theme": {"name": "material"}}, - ], - indirect=["renderer"], -) -def test_render_docstring_examples_section(renderer): - """Assert docstrings' examples section can be rendered. - - Parameters: - renderer: A renderer instance (parametrized). - """ - section = DocstringSection( - DocstringSectionKind.examples, - value=[ - (DocstringSectionKind.text, "This is an example."), - (DocstringSectionKind.examples, ">>> print('Hello')\nHello"), - ], - ) - template = renderer.env.get_template("docstring/examples.html") - rendered = template.render(section=section) - assert "

This is an example.

" in rendered - assert "print" in rendered - assert "Hello" in rendered - - -def test_format_code_and_signature(renderer): - """Assert code and signatures can be Black-formatted. - - Parameters: - renderer: A renderer instance (parametrized). - """ - assert renderer.do_format_code("print('Hello')", 100) - assert renderer.do_format_code('print("Hello")', 100) - assert renderer.do_format_signature("(param: str = 'hello') -> 'Class'", 100) - assert renderer.do_format_signature('(param: str = "hello") -> "Class"', 100) diff --git a/tests/test_rendering.py b/tests/test_rendering.py new file mode 100644 index 00000000..533eaf34 --- /dev/null +++ b/tests/test_rendering.py @@ -0,0 +1,42 @@ +"""Tests for the `rendering` module.""" + +import re +from dataclasses import dataclass + +import pytest + +from mkdocstrings_handlers.python import rendering + + +def test_format_code_and_signature(): + """Assert code and signatures can be Black-formatted.""" + assert rendering.do_format_code("print('Hello')", 100) + assert rendering.do_format_code('print("Hello")', 100) + assert rendering.do_format_signature("(param: str = 'hello') -> 'Class'", 100) + assert rendering.do_format_signature('(param: str = "hello") -> "Class"', 100) + + +@dataclass +class _FakeObject: + name: str + + +@pytest.mark.parametrize( + ("names", "filter_params", "expected_names"), + [ + (["aa", "ab", "ac", "da"], {"filters": [(re.compile("^a[^b]"), True)]}, {"ab", "da"}), + (["aa", "ab", "ac", "da"], {"members_list": ["aa", "ab"]}, {"aa", "ab"}), + ], +) +def test_filter_objects(names, filter_params, expected_names): + """Assert the objects filter works correctly. + + Parameters: + names: Names of the objects. + filter_params: Parameters passed to the filter function. + expected_names: Names expected to be kept. + """ + objects = {name: _FakeObject(name) for name in names} + filtered = rendering.do_filter_objects(objects, **filter_params) + filtered_names = {obj.name for obj in filtered} + assert set(filtered_names) == set(expected_names) diff --git a/tests/test_themes.py b/tests/test_themes.py index 44426fb6..b1e7d5d5 100644 --- a/tests/test_themes.py +++ b/tests/test_themes.py @@ -35,6 +35,6 @@ def test_render_themes_templates_python(module, plugin): plugin: Pytest fixture: [tests.conftest.fixture_plugin][]. """ handler = plugin.handlers.get_handler("python") - handler.renderer._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 - data = handler.collector.collect(module, {}) - handler.renderer.render(data, {}) + handler._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 + data = handler.collect(module, {}) + handler.render(data, {})