diff --git a/.copier-answers.yml b/.copier-answers.yml
index 022b9006..f844b711 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,10 +1,10 @@
# Changes here will be overwritten by Copier
-_commit: 0.2.1
-_src_path: gh:pawamoy/copier-poetry
+_commit: 0.4.3
+_src_path: gh:pawamoy/copier-pdm.git
author_email: pawamoy@pm.me
author_fullname: "Timoth\xE9e Mazzucotelli"
author_username: pawamoy
-copyright_date: '2020'
+copyright_date: '2019'
copyright_holder: "Timoth\xE9e Mazzucotelli"
copyright_holder_email: pawamoy@pm.me
copyright_license: ISC License
@@ -14,5 +14,6 @@ python_package_command_line_name: mkdocstrings
python_package_distribution_name: mkdocstrings
python_package_import_name: mkdocstrings
repository_name: mkdocstrings
-repository_namespace: pawamoy
+repository_namespace: mkdocstrings
repository_provider: github.com
+use_precommit: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4aa606c8..b5750cd7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,10 +11,9 @@ defaults:
shell: bash
env:
- LANG: "en_US.utf-8"
- LC_ALL: "en_US.utf-8"
- POETRY_VIRTUALENVS_IN_PROJECT: "true"
- PYTHONIOENCODING: "UTF-8"
+ LANG: en_US.utf-8
+ LC_ALL: en_US.utf-8
+ PYTHONIOENCODING: UTF-8
PYTHONPATH: docs
jobs:
@@ -27,39 +26,44 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- - name: Set up Python
- uses: actions/setup-python@v2
+ - name: Set up PDM
+ uses: pdm-project/setup-pdm@v2
with:
python-version: 3.8
- - name: Set up the cache
- uses: actions/cache@v1
+ - name: Set cache variables
+ id: set_variables
+ run: |
+ echo "::set-output name=PIP_CACHE::$(pip cache dir)"
+ echo "::set-output name=PDM_CACHE::$(pdm config cache_dir)"
+
+ - name: Set up cache
+ uses: actions/cache@v2
with:
- path: .venv
- key: quality-venv-cache-2
+ path: |
+ ${{ steps.set_variables.outputs.PIP_CACHE }}
+ ${{ steps.set_variables.outputs.PDM_CACHE }}
+ key: checks-cache
+
+ - name: Resolving dependencies
+ run: pdm lock
- - name: Set up the project
+ - name: Install dependencies
run: |
- pip install poetry
- poetry install -v || { rm -rf .venv; poetry install -v; }
- poetry update
+ pdm install -G duty -G docs -G quality -G typing
+ pip install safety
- name: Check if the documentation builds correctly
- run: |
- mkdir -p build/coverage
- touch build/coverage/index.html
- poetry run duty check-docs
+ run: pdm run duty check-docs
- name: Check the code quality
- run: poetry run duty check-code-quality
+ run: pdm run duty check-code-quality
- name: Check if the code is correctly typed
- run: poetry run duty check-types
+ run: pdm run duty check-types
- name: Check for vulnerabilities in dependencies
- run: |
- pip install safety
- poetry run duty check-dependencies
+ run: pdm run duty check-dependencies
tests:
@@ -74,22 +78,27 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ - name: Set up PDM
+ uses: pdm-project/setup-pdm@v2
with:
python-version: ${{ matrix.python-version }}
- - name: Set up the cache
- uses: actions/cache@v1
+ - name: Set cache variables
+ id: set_variables
+ run: |
+ echo "::set-output name=PIP_CACHE::$(pip cache dir)"
+ echo "::set-output name=PDM_CACHE::$(pdm config cache_dir)"
+
+ - name: Set up cache
+ uses: actions/cache@v2
with:
- path: .venv
- key: tests-venv-cache-${{ matrix.os }}-py${{ matrix.python-version }}
+ path: |
+ ${{ steps.set_variables.outputs.PIP_CACHE }}
+ ${{ steps.set_variables.outputs.PDM_CACHE }}
+ key: tests-cache-${{ runner.os }}-${{ matrix.python-version }}
- - name: Set up the project
- run: |
- pip install poetry
- poetry install -v || { rm -rf .venv; poetry install -v; }
- poetry update
+ - name: Install dependencies
+ run: pdm install -G duty -G tests -G docs
- name: Run the test suite
- run: poetry run duty test
+ run: pdm run duty test
diff --git a/.gitignore b/.gitignore
index 25973fb9..f6a13b06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,11 +4,14 @@ __pycache__/
dist/
*.egg-info/
build/
+htmlcov/
.coverage*
pip-wheel-metadata/
.pytest_cache/
.python-version
site/
-poetry.lock
+pdm.lock
+.pdm.toml
+__pypackages__/
.mypy_cache/
.venv/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b60b3fcc..ef6d4eac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,21 +5,64 @@ 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.16.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.16.0) - 2021-09-20
+
+[Compare with 0.15.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.15.0...0.16.0)
+
+### Features
+- Add a rendering option to change the sorting of members ([b1fff8b](https://github.com/mkdocstrings/mkdocstrings/commit/b1fff8b8ef4d6d77417fc43ed8be4b578d6437e4) by Joe Rickerby). [Issue #114](https://github.com/mkdocstrings/mkdocstrings/issues/114), [PR #274](https://github.com/mkdocstrings/mkdocstrings/pull/274)
+- Add option to show Python base classes ([436f550](https://github.com/mkdocstrings/mkdocstrings/commit/436f5504ad72ab6d1f5b4303e6b68bc84562c32b) by Brian Koropoff). [Issue #269](https://github.com/mkdocstrings/mkdocstrings/issues/269), [PR #297](https://github.com/mkdocstrings/mkdocstrings/pull/297)
+- Support loading external Python inventories in Sphinx format ([a8418cb](https://github.com/mkdocstrings/mkdocstrings/commit/a8418cb4c6193d35cdc72508b118a712cf0334e1) by Oleh Prypin). [PR #287](https://github.com/mkdocstrings/mkdocstrings/pull/287)
+- Support loading external inventories and linking to them ([8b675f4](https://github.com/mkdocstrings/mkdocstrings/commit/8b675f4671f8bbfd2f337ed043e3682b0a0ad0f6) by Oleh Prypin). [PR #277](https://github.com/mkdocstrings/mkdocstrings/pull/277)
+- Very basic support for MkDocs theme ([974ca90](https://github.com/mkdocstrings/mkdocstrings/commit/974ca9010efca1b8279767faf8efcd2470a8371d) by Oleh Prypin). [PR #272](https://github.com/mkdocstrings/mkdocstrings/pull/272)
+- Generate objects inventory ([14ed959](https://github.com/mkdocstrings/mkdocstrings/commit/14ed959860a784a835cd71f911081f2026d66c81) and [bbd85a9](https://github.com/mkdocstrings/mkdocstrings/commit/bbd85a92fa70bddfe10a907a4d63b8daf0810cb2) by Timothée Mazzucotelli). [Issue #251](https://github.com/mkdocstrings/mkdocstrings/issues/251), [PR #253](https://github.com/mkdocstrings/mkdocstrings/pull/253)
+
+### Bug Fixes
+- Don't render empty code blocks for missing type annotations ([d2e9e1e](https://github.com/mkdocstrings/mkdocstrings/commit/d2e9e1ef3cf304081b07f763843a9722bf9b117e) by Oleh Prypin).
+- Fix custom handler not being used ([6dcf342](https://github.com/mkdocstrings/mkdocstrings/commit/6dcf342fb83b19e385d56d37235f2b23e8c8c767) by Timothée Mazzucotelli). [Issue #259](https://github.com/mkdocstrings/mkdocstrings/issues/259), [PR #263](https://github.com/mkdocstrings/mkdocstrings/pull/263)
+- Don't hide `setup_commands` errors ([92418c4](https://github.com/mkdocstrings/mkdocstrings/commit/92418c4b3e80b67d5116efa73931fc113daa60e9) by Gabriel Vîjială). [PR #258](https://github.com/mkdocstrings/mkdocstrings/pull/258)
+
+### Code Refactoring
+- Move writing extra files to an earlier stage in the build ([3890ab5](https://github.com/mkdocstrings/mkdocstrings/commit/3890ab597692e56d7ece576c166373b66ff4e615) by Oleh Prypin). [PR #275](https://github.com/mkdocstrings/mkdocstrings/pull/275)
+
+
+## [0.15.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.15.2) - 2021-06-09
+
+[Compare with 0.15.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.15.1...0.15.2)
+
+### Packaging
+- MkDocs default schema needs to be obtained differently now ([b3e122b](https://github.com/mkdocstrings/mkdocstrings/commit/b3e122b36d586632738ddedaed7d3df8d5dead44) by Oleh Prypin). [PR #273](https://github.com/mkdocstrings/mkdocstrings/pull/273)
+- Compatibility with MkDocs 1.2: livereload isn't guaranteed now ([36e8024](https://github.com/mkdocstrings/mkdocstrings/commit/36e80248d2ab9e61975f6c83ae517115c9410fc1) by Oleh Prypin). [PR #294](https://github.com/mkdocstrings/mkdocstrings/pull/294)
+
+
+## [0.15.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.15.1) - 2021-05-16
+
+[Compare with 0.15.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.15.0...0.15.1)
+
+### Bug Fixes
+- Prevent error during parallel installations ([fac2c71](https://github.com/mkdocstrings/mkdocstrings/commit/fac2c711351f7b62bf5308f19cfc612a3944588a) by Timothée Mazzucotelli).
+
+### Packaging
+- Support the upcoming major Jinja and MarkupSafe releases ([bb4f9de](https://github.com/mkdocstrings/mkdocstrings/commit/bb4f9de08a77bef85e550d70deb0db13e6aa0c96) by Oleh Prypin). [PR #283](https://github.com/mkdocstrings/mkdocstrings/pull/283)
+- Accept a higher version of mkdocs-autorefs ([c8de08e](https://github.com/mkdocstrings/mkdocstrings/commit/c8de08e177f78290d3baaca2716d1ec64c9059b6) by Oleh Prypin). [PR #282](https://github.com/mkdocstrings/mkdocstrings/pull/282)
+
+
## [0.15.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.15.0) - 2021-02-28
[Compare with 0.14.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.14.0...0.15.0)
### Breaking Changes
-The following two items are *possible* breaking changes:
+The following items are *possible* breaking changes:
- Cross-linking to arbitrary headings now requires to opt-in to the *autorefs* plugin,
which is installed as a dependency of *mkdocstrings*.
See [Cross-references to any Markdown heading](https://mkdocstrings.github.io/usage/#cross-references-to-any-markdown-heading).
-- *mkdocstrings* now respects your code highlighting configured method,
- so if you are using CodeHilite, the `highlight` CSS classes in the rendered HTML will be replaced by `codehilite`.
- In that case make sure to replace `.highlight` by `.codehilite` in any extra CSS rule of yours.
+- *mkdocstrings* now respects your configured code highlighting method,
+ so if you are using the CodeHilite extension, the `.highlight` CSS class in the rendered HTML will become `.codehilite`.
+ So make sure to adapt your extra CSS accordingly. Or just switch to using [pymdownx.highlight](https://facelessuser.github.io/pymdown-extensions/extensions/highlight/), it's better supported by *mkdocstrings* anyway.
See [Syntax highlighting](https://mkdocstrings.github.io/theming/#syntax-highlighting).
+- Most of the [CSS rules that *mkdocstrings* used to recommend](https://mkdocstrings.github.io/handlers/python/#recommended-style-material) for manual addition, now become mandatory (auto-injected into the site). This shouldn't *break* any of your styles, but you are welcome to remove the now-redundant lines that you had copied into `extra_css`, [similarly to this diff](https://github.com/mkdocstrings/mkdocstrings/pull/218/files#diff-7889a1924c66ff9318f1d81c4a3b75658d09bebf0db3b2e4023ba3e40294eb73).
### Features
- Nicer-looking error outputs - no tracebacks from mkdocstrings ([6baf720](https://github.com/mkdocstrings/mkdocstrings/commit/6baf720850d359ddb55713553a757fe7b2283e10) by Oleh Prypin). [PR #230](https://github.com/mkdocstrings/mkdocstrings/pull/230)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 75609fd4..31a02435 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,18 +17,18 @@ make setup
!!! note
If it fails for some reason,
you'll need to install
- [Poetry](https://github.com/python-poetry/poetry)
+ [PDM](https://github.com/pdm-project/pdm)
manually.
You can install it with:
```bash
python3 -m pip install --user pipx
- pipx install poetry
+ pipx install pdm
```
Now you can try running `make setup` again,
- or simply `poetry install`.
+ or simply `pdm install`.
You now have the dependencies installed.
@@ -43,11 +43,9 @@ on multiple Python versions, you can do one of the following:
1. `export PYTHON_VERSIONS= `: this will run the task
with only the current Python version
-2. run the task directly with `poetry run duty TASK`,
- or `duty TASK` if the environment was already activated
- through `poetry shell`
+2. run the task directly with `pdm run duty TASK`
-The Makefile detects if the Poetry environment is activated,
+The Makefile detects if a virtual environment is activated,
so `make` will work the same with the virtualenv activated or not.
## Development
diff --git a/Makefile b/Makefile
index 28074e01..97aa6931 100644
--- a/Makefile
+++ b/Makefile
@@ -1,13 +1,13 @@
.DEFAULT_GOAL := help
SHELL := bash
-DUTY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo poetry run) duty
+DUTY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo pdm run) duty
args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)"))
check_code_quality_args = files
docs_serve_args = host port
release_args = version
-test_args = cleancov match
+test_args = match
BASIC_DUTIES = \
changelog \
diff --git a/README.md b/README.md
index 67ea0f29..cb207ff0 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,9 @@ Automatic documentation from sources, for [MkDocs](https://mkdocs.org/).
---
-
-
----
+**[Features](#features)** - **[Python handler](#python-handler)** - **[Requirements](#requirements)** - **[Installation](#installation)** - **[Quick usage](#quick-usage)**
-- [Features](#features)
- - [Python handler features](#python-handler-features)
-- [Requirements](#requirements)
-- [Installation](#installation)
-- [Quick usage](#quick-usage)
+
## Features
@@ -42,7 +36,7 @@ Automatic documentation from sources, for [MkDocs](https://mkdocs.org/).
*any* Markdown heading into the global referencing scheme.
**Note**: in versions prior to 0.15 *all* Markdown headers were included, but now you need to
- [opt in](https://mkdocstrings.github.io/usage/#cross-references).
+ [opt in](https://mkdocstrings.github.io/usage/#cross-references-to-any-markdown-heading).
- [**Inline injection in Markdown:**](https://mkdocstrings.github.io/usage/)
instead of generating Markdown files, *mkdocstrings* allows you to inject
@@ -61,42 +55,42 @@ Automatic documentation from sources, for [MkDocs](https://mkdocs.org/).
- **Reasonable defaults:**
you should be able to just drop the plugin in your configuration and enjoy your auto-generated docs.
-### Python handler features
+### Python handler
+
+
- **Data collection from source code**: collection of the object-tree and the docstrings is done by
- [`pytkdocs`](https://github.com/pawamoy/pytkdocs). The following features are possible thanks to it:
- - **Support for type annotations:** `pytkdocs` collects your type annotations and *mkdocstrings* uses them
- to display parameters types or return types.
- - **Recursive documentation of Python objects:** just use the module dotted-path as identifier, and you get the full
- module docs. You don't need to inject documentation for each class, function, etc.
- - **Support for documented attribute:** attributes (variables) followed by a docstring (triple-quoted string) will
- be recognized by `pytkdocs` in modules, classes and even in `__init__` methods.
- - **Support for objects properties:** `pytkdocs` detects if a method is a `staticmethod`, a `classmethod`, etc.,
- it also detects if a property is read-only or writable, and more! These properties will be displayed
- next to the object signature by *mkdocstrings*.
- - **Google-style sections support in docstrings:** `pytkdocs` understands `Arguments:`, `Raises:`
- and `Returns:` sections, and returns structured data for *mkdocstrings* to render them.
- - **reStructuredText-style sections support in docstrings:** `pytkdocs` understands all the
- [reStructuredText fields](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html?highlight=python%20domain#info-field-lists),
- and returns structured data for *mkdocstrings* to render them.
- *Note: only RST **style** is supported, not the whole markup.*
- - **Admonition support in docstrings:** blocks like `Note: ` or `Warning: ` will be transformed
- to their [admonition](https://squidfunk.github.io/mkdocs-material/extensions/admonition/) equivalent.
- *We do not support nested admonitions in docstrings!*
- - **Support for reStructuredText in docstrings:** `pytkdocs` can parse simple RST.
+ [`pytkdocs`](https://github.com/pawamoy/pytkdocs).
+
+- **Support for type annotations:** `pytkdocs` collects your type annotations and *mkdocstrings* uses them
+ to display parameters types or return types.
+
+- **Recursive documentation of Python objects:** just use the module dotted-path as identifier, and you get the full
+ module docs. You don't need to inject documentation for each class, function, etc.
+
+- **Support for documented attributes:** attributes (variables) followed by a docstring (triple-quoted string) will
+ be recognized by `pytkdocs` in modules, classes and even in `__init__` methods.
+
+- **Support for objects properties:** `pytkdocs` detects if a method is a `staticmethod`, a `classmethod`, etc.,
+ it also detects if a property is read-only or writable, and more! These properties will be displayed
+ next to the object signature by *mkdocstrings*.
+
+- **Multiple docstring-styles support:** almost complete support for Google-style, Numpy-style,
+ and reStructuredText-style docstrings. *Notes: only RST **style** is supported, not the whole markup.
+ Numpy-style requires an extra dependency from `pytkdocs`: `pytkdocs[numpy-style]`.*
+
+- **Admonition support in docstrings:** blocks like `Note:` or `Warning:` will be transformed
+ to their [admonition](https://squidfunk.github.io/mkdocs-material/extensions/admonition/) equivalent.
+ *We do not support nested admonitions in docstrings!*
+
- **Every object has a TOC entry:** we render a heading for each object, meaning *MkDocs* picks them into the Table
of Contents, which is nicely display by the Material theme. Thanks to *mkdocstrings* cross-reference ability,
- you can even reference other objects within your docstrings, with the classic Markdown syntax:
+ you can reference other objects within your docstrings, with the classic Markdown syntax:
`[this object][package.module.object]` or directly with `[package.module.object][]`
+
- **Source code display:** *mkdocstrings* can add a collapsible div containing the highlighted source code
of the Python object.
-To get an example of what is possible, check *mkdocstrings*'
-own [documentation](https://mkdocstrings.github.io/), auto-generated from sources by itself of course,
-and the following GIF:
-
-
-
## Roadmap
See the [Feature Roadmap issue](https://github.com/mkdocstrings/mkdocstrings/issues/183) on the bugtracker.
@@ -136,7 +130,7 @@ pip install mkdocs-material
With `pip`:
```bash
-python3.6 -m pip install mkdocstrings
+pip install mkdocstrings
```
With `conda`:
@@ -144,6 +138,12 @@ With `conda`:
conda install -c conda-forge mkdocstrings
```
+Note for Python: you'll need an extra dependency to parse Numpy-style docstrings:
+
+```
+pip install pytkdocs[numpy-style]
+```
+
## Quick usage
```yaml
diff --git a/config/coverage.ini b/config/coverage.ini
index 27b21edf..bb43c37b 100644
--- a/config/coverage.ini
+++ b/config/coverage.ini
@@ -1,23 +1,22 @@
-[coverage:paths]
-source =
- src/mkdocstrings
- */site-packages/mkdocstrings
-
[coverage:run]
branch = true
-source =
- src/mkdocstrings
- tests
parallel = true
+source =
+ src/
+ tests/
+
+[coverage:paths]
+equivalent =
+ src/
+ __pypackages__/
[coverage:report]
ignore_errors = True
precision = 2
omit =
- tests/*
-
-[coverage:html]
-directory = build/coverage
+ src/*/__init__.py
+ src/*/__main__.py
+ tests/__init__.py
[coverage:json]
-output = build/coverage.json
+output = htmlcov/coverage.json
diff --git a/config/flake8.ini b/config/flake8.ini
index 3e559fd2..2b50d854 100644
--- a/config/flake8.ini
+++ b/config/flake8.ini
@@ -1,50 +1,129 @@
[flake8]
-exclude = fixtures,docs,site
+exclude = fixtures,site,snippets
max-line-length = 132
strictness = long
docstring-convention = google
+ban-relative-imports = true
ignore =
- # we write docstrings in markdown, not rst
- RST*,
# redundant with W0622 (builtin override), which is more precise about line number
- A001,
+ A001
# missing docstring in magic method
- D105,
- # whitespace before ‘:’ (incompatible with Black)
- E203,
+ D105
+ # whitespace before ':' (incompatible with Black)
+ E203
# redundant with E0602 (undefined variable)
- F821,
+ F821
+ # error suffix foe exception
+ N818
# black already deals with quoting
- Q000,
+ Q000
# use of assert
- S101,
+ S101
# we are not parsing XML
- S405,
+ S405
# line break before binary operator (incompatible with Black)
- W503,
+ W503
# two-lowercase-letters variable DO conform to snake_case naming style
- C0103,
- # redunant with D102 (missing docstring)
- C0116,
+ C0103
+ # redundant with D102 (missing docstring)
+ C0116
# line too long
- C0301,
+ C0301
# too many instance attributes
- R0902,
+ R0902
# too few public methods
- R0903,
+ R0903
# too many public methods
- R0904,
+ R0904
# too many branches
- R0912,
+ R0912
# too many methods
- R0913,
+ R0913
# too many local variables
- R0914,
+ R0914
# too many statements
- R0915,
+ R0915
+ # protected attribute
+ W0212
# redundant with F401 (unused import)
- W0611,
+ W0611
# lazy formatting for logging calls
- W1203,
+ W1203
# short name
VNE001
+ # f-strings
+ WPS305
+ # common variable names (too annoying)
+ WPS110
+ # redundant with W0622 (builtin override), which is more precise about line number
+ WPS125
+ # too many imports
+ WPS201
+ # too many module members
+ WPS202
+ # overused expression
+ WPS204
+ # too many local variables
+ WPS210
+ # too many arguments
+ WPS211
+ # too many expressions
+ WPS213
+ # too many methods
+ WPS214
+ # too deep nesting
+ WPS220
+ # high Jones complexity
+ WPS221
+ # too many elif branches
+ WPS223
+ # string over-use: can't disable it per file?
+ WPS226
+ # too many public instance attributes
+ WPS230
+ # too complex function
+ WPS231
+ # too many variables unpacked
+ WPS236
+ # too complex f-string
+ WPS237
+ # too cumbersome, asks to write class A(object)
+ WPS306
+ # multi-line parameters (incompatible with Black)
+ WPS317
+ # multi-line strings (incompatible with attributes docstrings)
+ WPS322
+ # implicit string concatenation
+ WPS326
+ # explicit string concatenation
+ WPS336
+ # line starts with dot (incompatible with Black)
+ WPS348
+ # blank line before bracket (incompatible with Black)
+ WPS355
+ # raw string
+ WPS360
+ # noqa overuse
+ WPS402
+ # __init__ modules with logic
+ WPS412
+ # del/pass
+ WPS420
+ # print statements
+ WPS421
+ # statement with no effect (not compatible with attribute docstrings)
+ WPS428
+ # magic numbers
+ WPS432
+ # redundant with C0415 (not top-level import)
+ WPS433
+ # multiline usage (variable docstring)
+ WPS462
+ # try finally without except
+ WPS501
+ # implicit dict.get usage (generally false-positive)
+ WPS529
+ # subclassing builtin
+ WPS600
+ # getter/stter (false positives)
+ WPS615
diff --git a/config/mypy.ini b/config/mypy.ini
index 3d1cd433..e88e9042 100644
--- a/config/mypy.ini
+++ b/config/mypy.ini
@@ -1,2 +1,15 @@
[mypy]
ignore_missing_imports = true
+exclude = tests/fixtures/
+
+[mypy-docutils.*]
+ignore_missing_imports = true
+
+[mypy-markdown.*]
+ignore_missing_imports = true
+
+[mypy-toml.*]
+ignore_missing_imports = true
+
+[mypy-yaml.*]
+ignore_missing_imports = true
diff --git a/config/pytest.ini b/config/pytest.ini
index 08aef81e..ad72bbe6 100644
--- a/config/pytest.ini
+++ b/config/pytest.ini
@@ -11,5 +11,6 @@ python_files =
tests.py
addopts =
--cov
- --cov-append
--cov-config config/coverage.ini
+testpaths =
+ tests
diff --git a/docs/css/material.css b/docs/css/material.css
new file mode 100644
index 00000000..9e8c14a6
--- /dev/null
+++ b/docs/css/material.css
@@ -0,0 +1,4 @@
+/* More space at the bottom of the page. */
+.md-main__inner {
+ margin-bottom: 1.5rem;
+}
diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css
new file mode 100644
index 00000000..a83172e5
--- /dev/null
+++ b/docs/css/mkdocstrings.css
@@ -0,0 +1,11 @@
+/* Indentation. */
+div.doc-contents:not(.first) {
+ padding-left: 25px;
+ border-left: 4px solid rgba(230, 230, 230);
+ margin-bottom: 80px;
+}
+
+/* Avoid breaking parameters name, etc. in table cells. */
+td code {
+ word-break: normal !important;
+}
diff --git a/docs/css/style.css b/docs/css/style.css
index 27265bb0..abd97598 100644
--- a/docs/css/style.css
+++ b/docs/css/style.css
@@ -1,10 +1,3 @@
-/* Indentation for mkdocstrings items. */
-div.doc-contents:not(.first) {
- padding-left: 25px;
- border-left: 4px solid rgba(230, 230, 230);
- margin-bottom: 80px;
-}
-
/* Mark external links as such (also in nav) */
a.external:hover::after, a.md-nav__link[href^="https:"]:hover::after {
/* https://primer.style/octicons/link-external-16 */
@@ -15,3 +8,8 @@ a.external:hover::after, a.md-nav__link[href^="https:"]:hover::after {
content: ' ';
display: inline-block;
}
+
+/* More space at the bottom of the page */
+.md-main__inner {
+ margin-bottom: 1.5rem;
+}
diff --git a/docs/gen_credits.py b/docs/gen_credits.py
index d626e220..370d2e7d 100644
--- a/docs/gen_credits.py
+++ b/docs/gen_credits.py
@@ -1,13 +1,15 @@
+"""Generate the credits page."""
+
import functools
+import re
from itertools import chain
from pathlib import Path
+from urllib.request import urlopen
-import httpx
import mkdocs_gen_files
import toml
from jinja2 import StrictUndefined
from jinja2.sandbox import SandboxedEnvironment
-from pip._internal.commands.show import search_packages_info # noqa: WPS436 (no other way?)
def get_credits_data() -> dict:
@@ -17,33 +19,26 @@ def get_credits_data() -> dict:
Data required to render the credits template.
"""
project_dir = Path(__file__).parent.parent
- metadata = toml.load(project_dir / "pyproject.toml")["tool"]["poetry"]
- lock_data = toml.load(project_dir / "poetry.lock")
+ 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"]
- poetry_dependencies = chain(metadata["dependencies"].keys(), metadata["dev-dependencies"].keys())
- direct_dependencies = {dep.lower() for dep in poetry_dependencies}
- direct_dependencies.remove("python")
+ 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
- dependencies = direct_dependencies | indirect_dependencies
-
- packages = {}
- for pkg in search_packages_info(dependencies):
- pkg = {_: pkg[_] for _ in ("name", "home-page")}
- packages[pkg["name"].lower()] = pkg
-
- # all packages might not be credited,
- # like the ones that are now part of the standard library
- # or the ones that are only used on other operating systems,
- # and therefore are not installed,
- # but it's not that important
return {
"project_name": project_name,
"direct_dependencies": sorted(direct_dependencies),
"indirect_dependencies": sorted(indirect_dependencies),
- "package_info": packages,
+ "more_credits": "http://pawamoy.github.io/credits/",
}
@@ -55,13 +50,13 @@ def get_credits():
The credits page Markdown.
"""
jinja_env = SandboxedEnvironment(undefined=StrictUndefined)
- commit = "166758a98d5e544aaa94fda698128e00733497f4"
+ commit = "c78c29caa345b6ace19494a98b1544253cbaf8c1"
template_url = f"https://raw.githubusercontent.com/pawamoy/jinja-templates/{commit}/credits.md"
template_data = get_credits_data()
- template_text = httpx.get(template_url).text
+ 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 f:
- f.write(get_credits())
+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_doc_stubs.py b/docs/gen_doc_stubs.py
deleted file mode 100644
index ccbbdc86..00000000
--- a/docs/gen_doc_stubs.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python
-
-from pathlib import Path
-
-import mkdocs_gen_files
-
-for path in Path("src", "mkdocstrings").glob("**/*.py"):
- doc_path = Path("reference", path.relative_to("src", "mkdocstrings")).with_suffix(".md")
-
- with mkdocs_gen_files.open(doc_path, "w") as f:
- ident = ".".join(path.relative_to("src").with_suffix("").parts)
- print("::: " + ident, file=f)
-
- mkdocs_gen_files.set_edit_path(doc_path, Path("..", path))
diff --git a/docs/gen_ref_nav.py b/docs/gen_ref_nav.py
new file mode 100644
index 00000000..1411abdb
--- /dev/null
+++ b/docs/gen_ref_nav.py
@@ -0,0 +1,28 @@
+"""Generate the code reference pages and navigation."""
+
+from pathlib import Path
+
+import mkdocs_gen_files
+
+nav = mkdocs_gen_files.Nav()
+
+for path in sorted(Path("src").glob("**/*.py")):
+ module_path = path.relative_to("src").with_suffix("")
+ doc_path = path.relative_to("src", "mkdocstrings").with_suffix(".md")
+ full_doc_path = Path("reference", doc_path)
+
+ parts = list(module_path.parts)
+ parts[-1] = f"{parts[-1]}.py"
+ nav[parts] = doc_path
+
+ with mkdocs_gen_files.open(full_doc_path, "w") as fd:
+ ident = ".".join(module_path.parts)
+ print("::: " + ident, file=fd)
+
+ mkdocs_gen_files.set_edit_path(full_doc_path, path)
+
+nav["mkdocs_autorefs", "references.py"] = "autorefs/references.md"
+nav["mkdocs_autorefs", "plugin.py"] = "autorefs/plugin.md"
+
+with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
+ nav_file.writelines(nav.build_literate_nav())
diff --git a/docs/handlers/python.md b/docs/handlers/python.md
index 4ad1077b..967bbd58 100644
--- a/docs/handlers/python.md
+++ b/docs/handlers/python.md
@@ -44,7 +44,7 @@ Option | Type | Description | Default
**`filters`** | `list of str` | List of filtering regular expressions. Prefix with `!` to exclude objects whose name match. The default means *exclude private members*. | `["!^_[^_]"]`
**`members`** | `bool`, or `list of str` | Explicitly select members. True means *all*, false means *none*. | `True`
**`inherited_members`** | `bool` | Also select members inherited from parent classes. | `False`
-**`docstring_style`** | `str` | Docstring style to parse. `pytkdocs` supports `google` and `restructured-text`. | `"google"`
+**`docstring_style`** | `str` | Docstring style to parse. `pytkdocs` supports `google`, `numpy` and `restructured-text`. *Note: Numpy-style requires the `numpy-style` extra of `pytkdocs`.* | `"google"`
**`docstring_options`** | `dict` | Options to pass to the docstring parser. See [Collector: pytkdocs](#collector-pytkdocs) | `{}`
**`new_path_syntax`** | `bool` | Whether to use the new "colon" path syntax when importing objects. | `False`
@@ -107,7 +107,11 @@ It stands for *(Python) Take Docs*, and is supposed to be a pun on MkDocs (*Make
### Supported docstrings styles
-Right now, `pytkdocs` supports the Google-style and reStructuredText-style docstring formats.
+Right now, `pytkdocs` supports the Google-style, Numpy-style and reStructuredText-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.
#### Google-style
@@ -206,6 +210,21 @@ Type annotations are read both in the code and in the docstrings.
show_root_heading: no
show_root_toc_entry: no
+#### Numpy-style
+
+!!! important "Extra dependency required"
+ You'll need an extra dependency to parse Numpy-style docstrings:
+
+ ```
+ pdm add -d --group docs 'pytkdocs[numpy-style]'
+ poetry add -D 'pytkdocs[numpy-style]'
+ pip install 'pytkdocs[numpy-style]'
+ # etc.
+ ```
+
+You can see examples of Numpy-style docstrings
+in [numpydoc's documentation](https://numpydoc.readthedocs.io/en/latest/format.html).
+
#### reStructuredText-style
!!! warning "Partial support"
@@ -240,7 +259,7 @@ Type annotations are read both in the code and in the docstrings.
::: snippets.function_annotations_rst:my_function
selection:
- docstring_style: "restructured-text"
+ docstring_style: "restructured-text"
rendering:
show_root_heading: no
show_root_toc_entry: no
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 20e88195..7bfa2163 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -135,29 +135,6 @@ Make sure the referenced object was both collected and rendered: verify your sel
For false-positives, you can wrap the text in backticks (\`) to prevent `mkdocstrings` from trying to process it.
-## WindowsPath object is not iterable
-
-If you get a traceback like this one:
-
-```
-...
-File "c:\users\me\appdata\local\continuum\anaconda3\lib\site-packages\mkdocstrings\handlers\python.py", line 244, in get_handler
- return PythonHandler(collector=PythonCollector(), renderer=PythonRenderer("python", theme))
-File "c:\users\me\appdata\local\continuum\anaconda3\lib\site-packages\mkdocstrings\handlers\__init__.py", line 124, in __init__
- self.env = Environment(autoescape=True, loader=FileSystemLoader(theme_dir))
-File "c:\users\me\appdata\local\continuum\anaconda3\lib\site-packages\jinja2\loaders.py", line 163, in __init__
- self.searchpath = list(searchpath)
-TypeError: 'WindowsPath' object is not iterable
-```
-
-Try upgrading your installed version of Jinja2:
-
-```
-pip install -U jinja2
-```
-
-Version 2.11.1 seems to be working fine.
-
---
## Python specifics
diff --git a/docs/usage.md b/docs/usage.md
index ce8c0bef..703dcab7 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -112,6 +112,8 @@ The above is equivalent to:
The path is relative to the docs directory.
See [Theming](theming.md).
- `handlers`: the handlers global configuration.
+- `enable_inventory`: whether to enable inventory file generation.
+ See [Cross-references to other projects / inventories](#cross-references-to-other-projects-inventories)
Example:
@@ -163,7 +165,7 @@ Any item that was inserted using the [autodoc syntax](#autodoc-syntax)
cross-reference syntax (`[example][full.path.object1]`).
But the cross-references are also applicable to the items' children that get pulled in.
-#### Finding out the anchor
+### Finding out the anchor
If you're not sure which exact identifier a doc item uses, you can look at its "anchor", which your
Web browser will show in the URL bar when clicking an item's entry in the table of contents.
@@ -250,6 +252,85 @@ The above tip about [Finding out the anchor](#finding-out-the-anchor) also appli
You may also notice that such a heading does not get rendered as a `
` element directly, but rather the level gets shifted to fit the encompassing document structure. If you're curious about the implementation, check out [mkdocstrings.handlers.rendering.HeadingShiftingTreeprocessor][] and others.
+### Cross-references to other projects / inventories
+
+!!! tip "New in version 0.16."
+
+Python developers coming from Sphinx might know about its `intersphinx` extension,
+that allows to cross-reference items between several projects.
+*mkdocstrings* has a similar feature.
+
+To reference an item from another project, you must first tell *mkdocstrings*
+to load the inventory it provides. Each handler will be responsible of loading
+inventories specific to its language. For example, the Python handler
+can load Sphinx-generated inventories (`objects.inv`).
+
+In the following snippet, we load the inventory provided by `requests`:
+
+```yaml
+plugins:
+- mkdocstrings:
+ handlers:
+ python:
+ import:
+ - https://docs.python-requests.org/en/master/objects.inv
+```
+
+Now it is possible to cross-reference `requests`' items! For example:
+
+=== "Markdown"
+ ```md
+ See [requests.request][] to know what parameters you can pass.
+ ```
+
+=== "Result (HTML)"
+ ```html
+
See requests.request
+ to know what parameters you can pass.
+ ```
+
+=== "Result (displayed)"
+ See [requests.request][] to know what parameters you can pass.
+
+You can of course select another version of the inventory, for example:
+
+```yaml
+plugins:
+- mkdocstrings:
+ handlers:
+ python:
+ import:
+ - https://docs.python-requests.org/en/v3.0.0/objects.inv
+```
+
+In case the inventory file is not served under the base documentation URL,
+you can explicitly specify both URLs:
+
+```yaml
+plugins:
+- mkdocstrings:
+ handlers:
+ python:
+ import:
+ - url: https://cdn.example.com/version/objects.inv
+ base_url: https://docs.example.com/version
+```
+
+Absolute URLs to cross-referenced items will then be based
+on `https://docs.example.com/version/` instead of `https://cdn.example.com/version/`.
+
+Reciprocally, *mkdocstrings* also allows to *generate* an inventory file in the Sphinx format.
+It will be enabled by default if the Python handler is used, and generated as `objects.inv` in the final site directory.
+Other projects will be able to cross-reference items from your project!
+
+To explicitely enable or disable the generation of the inventory file, use the global
+`enable_inventory` option:
+
+```yaml
+plugins:
+- mkdocstrings:
+ enable_inventory: false
+```
## Watch directories
diff --git a/duties.py b/duties.py
index be0f8d90..284c97ce 100644
--- a/duties.py
+++ b/duties.py
@@ -3,15 +3,16 @@
import os
import re
import sys
+from functools import wraps
+from pathlib import Path
from shutil import which
from typing import List, Optional, Pattern
+from urllib.request import urlopen
-import httpx
from duty import duty
-from git_changelog.build import Changelog, Version
-from jinja2.sandbox import SandboxedEnvironment
-PY_SRC_LIST = ("src/mkdocstrings", "tests", "duties.py", "docs")
+PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "docs"))
+PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS)
PY_SRC = " ".join(PY_SRC_LIST)
TESTING = os.environ.get("TESTING", "0") in {"1", "true"}
CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""}
@@ -19,16 +20,7 @@
PTY = not WINDOWS and not CI
-def latest(lines: List[str], regex: Pattern) -> Optional[str]:
- """Return the last released version.
-
- Arguments:
- lines: Lines of the changelog file.
- regex: A compiled regex to find version numbers.
-
- Returns:
- The last version.
- """
+def _latest(lines: List[str], regex: Pattern) -> Optional[str]:
for line in lines:
match = regex.search(line)
if match:
@@ -36,46 +28,13 @@ def latest(lines: List[str], regex: Pattern) -> Optional[str]:
return None
-def unreleased(versions: List[Version], last_release: str) -> List[Version]:
- """Return the most recent versions down to latest release.
-
- Arguments:
- versions: All the versions (released and unreleased).
- last_release: The latest release.
-
- Returns:
- A list of versions.
- """
+def _unreleased(versions, last_release):
for index, version in enumerate(versions):
if version.tag == last_release:
return versions[:index]
return versions
-def read_changelog(filepath: str) -> List[str]:
- """Read the changelog file.
-
- Arguments:
- filepath: The path to the changelog file.
-
- Returns:
- The changelog lines.
- """
- with open(filepath, "r") as changelog_file:
- return changelog_file.read().splitlines()
-
-
-def write_changelog(filepath: str, lines: List[str]) -> None:
- """Write the changelog file.
-
- Arguments:
- filepath: The path to the changelog file.
- lines: The lines to write to the file.
- """
- with open(filepath, "w") as changelog_file:
- changelog_file.write("\n".join(lines).rstrip("\n") + "\n")
-
-
def update_changelog(
inplace_file: str,
marker: str,
@@ -92,9 +51,13 @@ def update_changelog(
template_url: The URL to the Jinja template used to render contents.
commit_style: The style of commit messages to parse.
"""
+ from git_changelog.build import Changelog
+ from jinja2.sandbox import SandboxedEnvironment
+
env = SandboxedEnvironment(autoescape=False)
- template = env.from_string(httpx.get(template_url).text)
- changelog = Changelog(".", style=commit_style) # noqa: W0621 (shadowing changelog)
+ template_text = urlopen(template_url).read().decode("utf8") # noqa: S310
+ template = env.from_string(template_text)
+ changelog = Changelog(".", style=commit_style)
if len(changelog.versions_list) == 1:
last_version = changelog.versions_list[0]
@@ -104,13 +67,17 @@ def update_changelog(
last_version.url += planned_tag
last_version.compare_url = last_version.compare_url.replace("HEAD", planned_tag)
- lines = read_changelog(inplace_file)
- last_released = latest(lines, re.compile(version_regex))
+ with open(inplace_file, "r") as changelog_file:
+ lines = changelog_file.read().splitlines()
+
+ last_released = _latest(lines, re.compile(version_regex))
if last_released:
- changelog.versions_list = unreleased(changelog.versions_list, last_released)
+ changelog.versions_list = _unreleased(changelog.versions_list, last_released)
rendered = template.render(changelog=changelog, inplace=True)
lines[lines.index(marker)] = rendered
- write_changelog(inplace_file, lines)
+
+ with open(inplace_file, "w") as changelog_file: # noqa: WPS440
+ changelog_file.write("\n".join(lines).rstrip("\n") + "\n")
@duty
@@ -120,13 +87,15 @@ def changelog(ctx):
Arguments:
ctx: The context instance (passed automatically).
"""
+ commit = "166758a98d5e544aaa94fda698128e00733497f4"
+ template_url = f"https://raw.githubusercontent.com/pawamoy/jinja-templates/{commit}/keepachangelog.md"
ctx.run(
update_changelog,
kwargs={
"inplace_file": "CHANGELOG.md",
"marker": "",
"version_regex": r"^## \[v?(?P[^\]]+)",
- "template_url": "https://raw.githubusercontent.com/pawamoy/jinja-templates/master/keepachangelog.md",
+ "template_url": template_url,
"commit_style": "angular",
},
title="Updating changelog",
@@ -135,12 +104,12 @@ def changelog(ctx):
@duty(pre=["check_code_quality", "check_types", "check_docs", "check_dependencies"])
-def check(ctx): # noqa: W0613 (no use for the context argument)
+def check(ctx):
"""Check it all!
Arguments:
ctx: The context instance (passed automatically).
- """ # noqa: D400 (exclamation mark is funnier)
+ """
@duty
@@ -170,30 +139,48 @@ def check_dependencies(ctx):
else:
safety = "safety"
nofail = True
-
- # Ignore tornado/39462 as there is currently no fix
- # See https://github.com/tornadoweb/tornado/issues/2981
- ignored_cves = "39462"
-
ctx.run(
- "poetry export -f requirements.txt --without-hashes | "
- f"{safety} check --stdin --full-report -i {ignored_cves}",
+ f"pdm export -f requirements --without-hashes | {safety} check --stdin --full-report",
title="Checking dependencies",
pty=PTY,
nofail=nofail,
)
+def no_docs_py36(nofail=True):
+ """Decorate a duty that builds docs to warn that it's not possible on Python 3.6.
+
+ Arguments:
+ nofail: Whether to fail or not.
+
+ Returns:
+ The decorated function.
+ """
+
+ def decorator(func):
+ @wraps(func)
+ def wrapper(ctx):
+ if sys.version_info <= (3, 7, 0):
+ ctx.run(["false"], title="Docs can't be built on Python 3.6", nofail=nofail, quiet=True)
+ else:
+ func(ctx)
+
+ return wrapper
+
+ return decorator
+
+
@duty
+@no_docs_py36()
def check_docs(ctx):
"""Check if the documentation builds correctly.
Arguments:
ctx: The context instance (passed automatically).
"""
- # mkdocs-gen-files works on 3.7+ only
- nofail = sys.version_info < (3, 7)
- ctx.run("mkdocs build -s", title="Building documentation", pty=PTY, nofail=nofail, quiet=nofail)
+ Path("htmlcov").mkdir(parents=True, exist_ok=True)
+ Path("htmlcov/index.html").touch(exist_ok=True)
+ ctx.run("mkdocs build -s", title="Building documentation", pty=PTY)
@duty
@@ -216,8 +203,10 @@ def clean(ctx):
ctx.run("rm -rf .coverage*")
ctx.run("rm -rf .mypy_cache")
ctx.run("rm -rf .pytest_cache")
+ ctx.run("rm -rf tests/.pytest_cache")
ctx.run("rm -rf build")
ctx.run("rm -rf dist")
+ ctx.run("rm -rf htmlcov")
ctx.run("rm -rf pip-wheel-metadata")
ctx.run("rm -rf site")
ctx.run("find . -type d -name __pycache__ | xargs rm -rf")
@@ -225,6 +214,7 @@ def clean(ctx):
@duty
+@no_docs_py36(nofail=False)
def docs(ctx):
"""Build the documentation locally.
@@ -235,6 +225,7 @@ def docs(ctx):
@duty
+@no_docs_py36(nofail=False)
def docs_serve(ctx, host="127.0.0.1", port=8000):
"""Serve the documentation (localhost:8000).
@@ -247,18 +238,19 @@ def docs_serve(ctx, host="127.0.0.1", port=8000):
@duty
+@no_docs_py36(nofail=False)
def docs_deploy(ctx):
"""Deploy the documentation on GitHub pages.
Arguments:
ctx: The context instance (passed automatically).
"""
- ctx.run("git remote set-url org-pages git@github.com:mkdocstrings/mkdocstrings.github.io", silent=True)
+ ctx.run("git remote add org-pages git@github.com:mkdocstrings/mkdocstrings.github.io", silent=True, nofail=True)
ctx.run("mkdocs gh-deploy --remote-name org-pages", title="Deploying documentation")
@duty
-def format(ctx): # noqa: W0622 (we don't mind shadowing the format builtin)
+def format(ctx):
"""Run formatting tools on the code.
Arguments:
@@ -281,16 +273,15 @@ def release(ctx, version):
ctx: The context instance (passed automatically).
version: The new version number to use.
"""
- ctx.run(f"poetry version {version}", title=f"Bumping version in pyproject.toml to {version}", pty=PTY)
ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY)
ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY)
ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY)
if not TESTING:
ctx.run("git push", title="Pushing commits", pty=False)
ctx.run("git push --tags", title="Pushing tags", pty=False)
- ctx.run("poetry build", title="Building dist/wheel", pty=PTY)
- ctx.run("poetry publish", title="Publishing version", pty=PTY)
- docs_deploy.run()
+ ctx.run("pdm build", title="Building dist/wheel", pty=PTY)
+ ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY)
+ docs_deploy.run() # type: ignore
@duty(silent=True)
@@ -300,21 +291,31 @@ def coverage(ctx):
Arguments:
ctx: The context instance (passed automatically).
"""
+ ctx.run("coverage combine", nofail=True)
ctx.run("coverage report --rcfile=config/coverage.ini", capture=False)
ctx.run("coverage html --rcfile=config/coverage.ini")
@duty
-def test(ctx, cleancov: bool = True, match: str = ""):
+def test(ctx, match: str = ""):
"""Run the test suite.
Arguments:
ctx: The context instance (passed automatically).
- cleancov: Whether to remove the `.coverage` file before running the tests.
match: A pytest expression to filter selected tests.
"""
- if cleancov:
- ctx.run("rm -f .coverage", silent=True)
+ try: # noqa: WPS229
+ import sphinx # isort:skip # noqa: F401
+ import docutils # isort:skip # noqa: F401
+ except ImportError:
+ py = f"{sys.version_info.major}.{sys.version_info.minor}"
+ ctx.run(
+ f"pip install sphinx docutils --no-deps -t __pypackages__/{py}/lib",
+ title="Installing additional test dependencies",
+ )
+
+ py_version = f"{sys.version_info.major}{sys.version_info.minor}"
+ os.environ["COVERAGE_FILE"] = f".coverage.{py_version}"
ctx.run(
["pytest", "-c", "config/pytest.ini", "-n", "auto", "-k", match, "tests"],
title="Running tests",
diff --git a/mkdocs.yml b/mkdocs.yml
index 80dbc607..96c2e575 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -4,9 +4,14 @@ site_url: "https://mkdocstrings.github.io/"
repo_url: "https://github.com/mkdocstrings/mkdocstrings"
edit_uri: "blob/master/docs/"
repo_name: "mkdocstrings/mkdocstrings"
+site_dir: "site"
nav:
-- Overview: index.md
+- Home:
+ - Overview: index.md
+ - Changelog: changelog.md
+ - Credits: credits.md
+ - License: license.md
- Usage:
- usage.md
- Theming: theming.md
@@ -15,38 +20,45 @@ nav:
- Python: handlers/python.md
- Crystal: https://mkdocstrings.github.io/crystal/
- Troubleshooting: troubleshooting.md
-- Code Reference:
- - mkdocstrings:
- - handlers:
- - base.py: reference/handlers/base.md
- - rendering.py: reference/handlers/rendering.md
- - python.py: reference/handlers/python.md
- - extension.py: reference/extension.md
- - plugin.py: reference/plugin.md
- - loggers.py: reference/loggers.md
- - mkdocs_autorefs:
- - references.py: reference/autorefs/references.md
- - plugin.py: reference/autorefs/plugin.md
-- Contributing:
- - contributing.md
+# defer to gen-files + literate-nav
+- Code Reference: reference/
+- Development:
+ - Contributing: contributing.md
- Code of Conduct: code_of_conduct.md
- Coverage report: coverage.md
-- Changelog: changelog.md
-- Credits: credits.md
-- License: license.md
+- Author's website: https://pawamoy.github.io/
theme:
name: material
+ icon:
+ logo: material/currency-sign
+ features:
+ - navigation.tabs
+ - navigation.top
palette:
- scheme: slate
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
primary: teal
accent: purple
+ toggle:
+ icon: material/weather-sunny
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: black
+ accent: lime
+ toggle:
+ icon: material/weather-night
+ name: Switch to light mode
extra_css:
- css/style.css
+- css/material.css
+- css/mkdocstrings.css
markdown_extensions:
- admonition
+- pymdownx.details
- pymdownx.emoji
- pymdownx.magiclink
- pymdownx.snippets:
@@ -62,10 +74,11 @@ plugins:
- gen-files:
scripts:
- docs/gen_credits.py
- - docs/gen_doc_stubs.py
+ - docs/gen_ref_nav.py
+- literate-nav:
+ nav_file: SUMMARY.md
- section-index
-- coverage:
- html_report_dir: build/coverage
+- coverage
- mkdocstrings:
handlers:
python:
@@ -74,5 +87,14 @@ plugins:
- sys.path.append("docs")
selection:
new_path_syntax: yes
+ import: # demonstration purpose in the docs
+ - https://docs.python-requests.org/en/master/objects.inv
watch:
- src/mkdocstrings
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/pawamoy
+ - icon: fontawesome/brands/twitter
+ link: https://twitter.com/pawamoy
diff --git a/pyproject.toml b/pyproject.toml
index cf5c7286..c517aa90 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,69 +1,95 @@
[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
+requires = ["pdm-pep517"]
+build-backend = "pdm.pep517.api"
-[tool.poetry]
+[project]
name = "mkdocstrings"
-version = "0.15.0"
+version = {use_scm = true}
description = "Automatic documentation from sources, for MkDocs."
-authors = ["Timothée Mazzucotelli "]
-license = "ISC License"
+authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}]
+license = {file = "LICENSE"}
readme = "README.md"
-repository = "https://github.com/mkdocstrings/mkdocstrings"
-homepage = "https://github.com/mkdocstrings/mkdocstrings"
+requires-python = ">=3.6"
keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"]
-packages = [ { include = "mkdocstrings", from = "src" } ]
-include = [
- "README.md",
- "pyproject.toml"
+dynamic = ["version", "classifiers"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "License :: OSI Approved :: ISC License (ISCL)",
+ "Typing :: Typed",
+]
+dependencies = [
+ "Jinja2>=2.11.1,<4.0",
+ "Markdown~=3.3",
+ "MarkupSafe>=1.1,<3.0",
+ "mkdocs~=1.2",
+ "mkdocs-autorefs>=0.1,<0.4",
+ "pymdown-extensions>=6.3,<9.0",
+ "pytkdocs>=0.2.0,<0.13.0",
]
-[tool.poetry.dependencies]
-python = "^3.6"
-Jinja2 = "^2.11"
-Markdown = "^3.3"
-MarkupSafe = "^1.1"
-mkdocs = "^1.1"
-mkdocs-autorefs = "^0.1"
-pymdown-extensions = ">=6.3, <9.0"
-pytkdocs = ">=0.2.0, <0.12.0"
-
-[tool.poetry.dev-dependencies]
-autoflake = "^1.4"
-black = "^20.8b1"
-duty = "^0.6.0"
-flakehell = "^0.9.0"
-flake8-black = "^0.2.1"
-flake8-builtins = "^1.5.3"
-flake8-tidy-imports = "^4.2.1"
-flake8-variables-names = "^0.0.4"
-flake8-pytest-style = "^1.3.0"
-git-changelog = "^0.4.2"
-httpx = "^0.16.1"
-isort = {version = "^5.7.0", extras = ["pyproject"]}
-jinja2-cli = "^0.7.0"
-mkdocs-coverage = "^0.2.1"
-mkdocs-gen-files = {version = "^0.3.0", markers = "python_version>='3.7'"}
-mkdocs-material = "^6.2.7"
-mkdocs-section-index = "^0.2.3"
-mypy = "^0.782"
-pytest = "^6.2.2"
-pytest-cov = "^2.11.1"
-pytest-randomly = "^3.5.0"
-pytest-sugar = "^0.9.4"
-pytest-xdist = "^2.2.0"
-toml = "^0.10.2"
-darglint = "^1.5.8"
-flake8-bandit = "^2.1.2"
-flake8-bugbear = "^20.11.1"
-flake8-comprehensions = "^3.3.1"
-flake8-docstrings = "^1.5.0"
-flake8-string-format = "^0.3.0"
-pep8-naming = "^0.11.1"
+[project.urls]
+Homepage = "https://mkdocstrings.github.io"
+Documentation = "https://mkdocstrings.github.io"
+Changelog = "https://mkdocstrings.github.io/changelog"
+Repository = "https://github.com/mkdocstrings/mkdocstrings"
+Issues = "https://github.com/mkdocstrings/mkdocstrings/issues"
+Discussions = "https://github.com/mkdocstrings/mkdocstrings/discussions"
+Gitter = "https://gitter.im/mkdocstrings/community"
+Funding = "https://github.com/sponsors/mkdocstrings"
-[tool.poetry.plugins."mkdocs.plugins"]
+[project.entry-points."mkdocs.plugins"]
mkdocstrings = "mkdocstrings.plugin:MkdocstringsPlugin"
+
+[project.optional-dependencies]
+[tool.pdm]
+package-dir = "src"
+includes = ["src/mkdocstrings"]
+
+[tool.pdm.dev-dependencies]
+duty = ["duty~=0.6"]
+docs = [
+ "mkdocs-coverage~=0.2; python_version >= '3.7'",
+ "mkdocs-gen-files~=0.3; python_version >= '3.7'",
+ "mkdocs-literate-nav~=0.4; python_version >= '3.7'",
+ "mkdocs-material~=7.1; python_version >= '3.7'",
+ "mkdocs-section-index~=0.3; python_version >= '3.7'",
+ "toml~=0.10; python_version >= '3.7'",
+]
+format = [
+ "autoflake~=1.4",
+ "black~=20.8b1",
+ "isort~=5.8",
+]
+maintain = [
+ # TODO: remove this section when git-changelog is more powerful
+ "git-changelog~=0.4",
+]
+quality = [
+ "darglint~=1.7",
+ "flake8-bandit~=2.1",
+ "flake8-black~=0.2",
+ "flake8-bugbear~=21.3",
+ "flake8-builtins~=1.5",
+ "flake8-comprehensions~=3.4",
+ "flake8-docstrings~=1.6",
+ "flake8-pytest-style~=1.4",
+ "flake8-string-format~=0.3",
+ "flake8-tidy-imports~=4.2",
+ "flake8-variables-names~=0.0",
+ "pep8-naming~=0.11",
+ "wps-light~=0.15",
+]
+tests = [
+ "pygments~=2.10", # python 3.6
+ "pytest~=6.2",
+ "pytest-cov~=2.11",
+ "pytest-randomly~=3.6",
+ "pytest-sugar~=0.9",
+ "pytest-xdist~=2.2",
+]
+typing = ["mypy~=0.812"]
+
[tool.black]
line-length = 120
exclude = "tests/fixtures"
diff --git a/scripts/multirun.sh b/scripts/multirun.sh
index b483defa..4ca6e2ce 100755
--- a/scripts/multirun.sh
+++ b/scripts/multirun.sh
@@ -1,22 +1,17 @@
#!/usr/bin/env bash
set -e
-PYTHON_VERSIONS="${PYTHON_VERSIONS:-3.6 3.7 3.8 3.9}"
+PYTHON_VERSIONS="${PYTHON_VERSIONS-3.6 3.7 3.8 3.9}"
if [ -n "${PYTHON_VERSIONS}" ]; then
for python_version in ${PYTHON_VERSIONS}; do
- if output=$(poetry env use "${python_version}" 2>&1); then
- if echo "${output}" | grep -q ^Creating; then
- echo "> Environment for Python ${python_version} not created, skipping" >&2
- poetry env remove "${python_version}" &>/dev/null || true
- else
- echo "> poetry run $@ (Python ${python_version})"
- poetry run "$@"
- fi
+ if pdm use -f "${python_version}" &>/dev/null; then
+ echo "> pdm run $@ (Python ${python_version})"
+ pdm run "$@"
else
- echo "> poetry env use ${python_version}: Python version not available?" >&2
+ echo "> pdm use -f ${python_version}: Python version not available?" >&2
fi
done
else
- poetry run "$@"
+ pdm run "$@"
fi
diff --git a/scripts/setup.sh b/scripts/setup.sh
index a626b257..cfddbac7 100755
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e
-PYTHON_VERSIONS="${PYTHON_VERSIONS:-3.6 3.7 3.8 3.9}"
+PYTHON_VERSIONS="${PYTHON_VERSIONS-3.6 3.7 3.8 3.9}"
install_with_pipx() {
if ! command -v "$1" &>/dev/null; then
@@ -12,21 +12,17 @@ install_with_pipx() {
fi
}
-install_with_pipx poetry
+install_with_pipx pdm
if [ -n "${PYTHON_VERSIONS}" ]; then
for python_version in ${PYTHON_VERSIONS}; do
- if output=$(poetry env use "${python_version}" 2>&1); then
- if echo "${output}" | grep -q ^Creating; then
- echo "> Created environment for Python ${python_version}"
- else
- echo "> Using Python ${python_version} environment"
- fi
- poetry install
+ if pdm use -f "${python_version}" &>/dev/null; then
+ echo "> Using Python ${python_version} environment"
+ pdm install
else
- echo "> poetry env use ${python_version}: Python version not available?" >&2
+ echo "> pdm use -f ${python_version}: Python version not available?" >&2
fi
done
else
- poetry install
-fi
\ No newline at end of file
+ pdm install
+fi
diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py
index c9f42e6d..afe59708 100644
--- a/src/mkdocstrings/extension.py
+++ b/src/mkdocstrings/extension.py
@@ -23,7 +23,7 @@
"""
import re
from collections import ChainMap
-from typing import Mapping, MutableSequence, Sequence, Tuple
+from typing import Mapping, MutableSequence, Tuple
from xml.etree.ElementTree import Element
import yaml
@@ -35,13 +35,13 @@
from markdown.treeprocessors import Treeprocessor
from mkdocs_autorefs.plugin import AutorefsPlugin
-from mkdocstrings.handlers.base import CollectionError, CollectorItem, Handlers
+from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem, Handlers
from mkdocstrings.loggers import get_logger
try:
from mkdocs.exceptions import PluginError # New in MkDocs 1.2
except ImportError:
- PluginError = SystemExit
+ PluginError = SystemExit # noqa: WPS440
log = get_logger(__name__)
@@ -79,7 +79,7 @@ def __init__(
self._autorefs = autorefs
self._updated_env = False
- def test(self, parent: Element, block: str) -> bool:
+ def test(self, parent: Element, block: str) -> bool: # type: ignore
"""Match our autodoc instructions.
Arguments:
@@ -117,15 +117,26 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
heading_level = match["heading"].count("#")
log.debug(f"Matched '::: {identifier}'")
- html, headings = self._process_block(identifier, block, heading_level)
+ html, handler = self._process_block(identifier, block, heading_level)
el = Element("div", {"class": "mkdocstrings"})
# The final HTML is inserted as opaque to subsequent processing, and only revealed at the end.
el.text = self.md.htmlStash.store(html)
# So we need to duplicate the headings directly (and delete later), just so 'toc' can pick them up.
+ headings = handler.renderer.get_headings()
el.extend(headings)
for heading in headings:
- self._autorefs.register_anchor(self._autorefs.current_page, heading.attrib["id"])
+ page = self._autorefs.current_page
+ anchor = heading.attrib["id"]
+ self._autorefs.register_anchor(page, anchor)
+
+ if "data-role" in heading.attrib:
+ self._handlers.inventory.register(
+ name=anchor,
+ domain=handler.domain,
+ role=heading.attrib["data-role"],
+ uri=f"{page}#{anchor}",
+ )
parent.append(el)
@@ -135,7 +146,7 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
# list for future processing.
blocks.insert(0, the_rest)
- def _process_block(self, identifier: str, yaml_block: str, heading_level: int = 0) -> Tuple[str, Sequence[Element]]:
+ def _process_block(self, identifier: str, yaml_block: str, heading_level: int = 0) -> Tuple[str, BaseHandler]:
"""Process an autodoc block.
Arguments:
@@ -148,7 +159,7 @@ def _process_block(self, identifier: str, yaml_block: str, heading_level: int =
TemplateNotFound: When a template used for rendering could not be found.
Returns:
- Rendered HTML and the list of heading elements encoutered.
+ Rendered HTML and the handler that was used.
"""
config = yaml.safe_load(yaml_block) or {}
handler_name = self._handlers.get_handler_name(config)
@@ -172,7 +183,7 @@ def _process_block(self, identifier: str, yaml_block: str, heading_level: int =
if not self._updated_env:
log.debug("Updating renderer's env")
- handler.renderer._update_env(self.md, self._config) # noqa: W0212 (protected member OK)
+ handler.renderer._update_env(self.md, self._config) # noqa: WPS437 (protected member OK)
self._updated_env = True
log.debug("Rendering templates")
@@ -185,7 +196,7 @@ def _process_block(self, identifier: str, yaml_block: str, heading_level: int =
)
raise
- return (rendered, handler.renderer.get_headings())
+ return (rendered, handler)
def get_item_configs(handler_config: dict, config: dict) -> Tuple[Mapping, Mapping]:
diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py
index 2a95e775..f496e7ed 100644
--- a/src/mkdocstrings/handlers/base.py
+++ b/src/mkdocstrings/handlers/base.py
@@ -11,7 +11,7 @@
import importlib
from abc import ABC, abstractmethod
from pathlib import Path
-from typing import Any, Dict, Iterable, Optional, Sequence
+from typing import Any, Dict, Iterable, List, Optional, Sequence
from xml.etree.ElementTree import Element, tostring
from jinja2 import Environment, FileSystemLoader
@@ -24,6 +24,7 @@
IdPrependingTreeprocessor,
MkdocstringsInnerExtension,
)
+from mkdocstrings.inventory import Inventory
from mkdocstrings.loggers import get_template_logger
CollectorItem = Any
@@ -95,7 +96,7 @@ def __init__(self, directory: str, theme: str, custom_templates: Optional[str] =
for path in paths:
css_path = path / "style.css"
if css_path.is_file():
- self.extra_css += "\n" + css_path.read_text(encoding="utf-8")
+ self.extra_css += "\n" + css_path.read_text(encoding="utf-8") # noqa: WPS601
break
if custom_templates is not None:
@@ -103,14 +104,14 @@ def __init__(self, directory: str, theme: str, custom_templates: Optional[str] =
self.env = Environment(
autoescape=True,
- loader=FileSystemLoader(paths),
+ loader=FileSystemLoader(paths), # type: ignore
auto_reload=False, # Editing a template in the middle of a build is not useful.
) # type: ignore
self.env.filters["any"] = do_any
self.env.globals["log"] = get_template_logger()
- self._headings = []
- self._md = None # To be populated in `update_env`.
+ self._headings: List[Element] = []
+ self._md: Markdown = None # type: ignore # To be populated in `update_env`.
@abstractmethod
def render(self, data: CollectorItem, config: dict) -> str:
@@ -163,6 +164,7 @@ def do_heading(
content: str,
heading_level: int,
*,
+ role: Optional[str] = None,
hidden: bool = False,
toc_label: Optional[str] = None,
**attributes: str,
@@ -172,6 +174,7 @@ def do_heading(
Arguments:
content: The HTML within the heading.
heading_level: The level of heading (e.g. 3 -> `h3`).
+ role: An optional role for the object bound to this heading.
hidden: If True, only register it for the table of contents, don't render anything.
toc_label: The title to use in the table of contents ('data-toc-label' attribute).
attributes: Any extra HTML attributes of the heading.
@@ -182,8 +185,10 @@ def do_heading(
# First, produce the "fake" heading, for ToC only.
el = Element(f"h{heading_level}", attributes)
if toc_label is None:
- toc_label = content.unescape() if isinstance(el, Markup) else content
+ toc_label = content.unescape() if isinstance(el, Markup) else content # type: ignore
el.set("data-toc-label", toc_label)
+ if role:
+ el.set("data-role", role)
self._headings.append(el)
if hidden:
@@ -285,8 +290,16 @@ class BaseHandler:
Inherit from this class to implement a handler.
It's usually just a combination of a collector and a renderer, but you can make it as complex as you need.
+
+ Attributes:
+ 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.
"""
+ domain: str = "default"
+ enable_inventory: bool = False
+
def __init__(self, collector: BaseCollector, renderer: BaseRenderer) -> None:
"""Initialize the object.
@@ -314,6 +327,7 @@ def __init__(self, config: dict) -> None:
"""
self._config = config
self._handlers: Dict[str, BaseHandler] = {}
+ self.inventory: Inventory = Inventory(project=self._config["site_name"])
def get_anchor(self, identifier: str) -> Optional[str]:
"""Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it.
@@ -329,9 +343,8 @@ def get_anchor(self, identifier: str) -> Optional[str]:
anchor = handler.renderer.get_anchor(handler.collector.collect(identifier, {}))
except CollectionError:
continue
- else:
- if anchor is not None:
- return anchor
+ if anchor is not None:
+ return anchor
return None
def get_handler_name(self, config: dict) -> str:
@@ -343,10 +356,10 @@ def get_handler_name(self, config: dict) -> str:
Returns:
The name of the handler to use.
"""
- config = self._config["mkdocstrings"]
+ global_config = self._config["mkdocstrings"]
if "handler" in config:
return config["handler"]
- return config["default_handler"]
+ return global_config["default_handler"]
def get_handler_config(self, name: str) -> dict:
"""Return the global configuration of the given handler.
@@ -382,7 +395,7 @@ def get_handler(self, name: str, handler_config: Optional[dict] = None) -> BaseH
if handler_config is None:
handler_config = self.get_handler_config(name)
module = importlib.import_module(f"mkdocstrings.handlers.{name}")
- self._handlers[name] = module.get_handler(
+ self._handlers[name] = module.get_handler( # type: ignore
self._config["theme_name"],
self._config["mkdocstrings"]["custom_templates"],
**handler_config,
diff --git a/src/mkdocstrings/handlers/python.py b/src/mkdocstrings/handlers/python.py
index a9e63786..0aab09ac 100644
--- a/src/mkdocstrings/handlers/python.py
+++ b/src/mkdocstrings/handlers/python.py
@@ -5,15 +5,19 @@
import json
import os
+import posixpath
import sys
import traceback
from collections import ChainMap
from subprocess import PIPE, Popen # noqa: S404 (what other option, more secure that PIPE do we have? sockets?)
-from typing import Any, List, Optional
+from typing import Any, BinaryIO, Callable, Iterator, List, Optional, Tuple
from markdown import Markdown
+from markupsafe import Markup
+from mkdocstrings.extension import PluginError
from mkdocstrings.handlers.base import BaseCollector, BaseHandler, BaseRenderer, CollectionError, CollectorItem
+from mkdocstrings.inventory import Inventory
from mkdocstrings.loggers import get_logger
log = get_logger(__name__)
@@ -43,8 +47,10 @@ class PythonRenderer(BaseRenderer):
"show_if_no_docstring": False,
"show_signature_annotations": False,
"show_source": True,
+ "show_bases": True,
"group_by_category": True,
"heading_level": 2,
+ "members_order": "alphabetical",
}
"""The default rendering options.
@@ -59,8 +65,10 @@ class PythonRenderer(BaseRenderer):
**`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False`
**`show_signature_annotations`** | `bool` | Show the type annotations in methods and functions signatures. | `False`
**`show_source`** | `bool` | Show the source code of this object. | `True`
+ **`show_bases`** | `bool` | Show the base classes of a class. | `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`
""" # noqa: E501
def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring)
@@ -72,6 +80,16 @@ def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignor
# 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"]
+ members_order = final_config["members_order"]
+
+ if members_order == "alphabetical":
+ sort_function = _sort_key_alphabetical
+ elif members_order == "source":
+ sort_function = _sort_key_source
+ else:
+ raise PluginError(f"Unknown members_order '{members_order}', choose between 'alphabetical' and 'source'.")
+
+ sort_object(data, sort_function=sort_function)
return template.render(
**{"config": final_config, data["category"]: data, "heading_level": heading_level, "root": True},
@@ -85,6 +103,12 @@ def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore
self.env.trim_blocks = True
self.env.lstrip_blocks = True
self.env.keep_trailing_newline = False
+ self.env.filters["brief_xref"] = self.do_brief_xref
+
+ def do_brief_xref(self, path: str) -> Markup:
+ """Filter to create cross-reference with brief text and full identifier as hover text."""
+ brief = path.split(".")[-1]
+ return Markup("{brief}").format(path=path, brief=brief)
class PythonCollector(BaseCollector):
@@ -152,7 +176,6 @@ def __init__(self, setup_commands: Optional[List[str]] = None) -> None:
self.process = Popen( # noqa: S603,S607 (we trust the input, and we don't want to use the absolute path)
cmd,
universal_newlines=True,
- stderr=PIPE,
stdout=PIPE,
stdin=PIPE,
bufsize=-1,
@@ -236,7 +259,39 @@ def teardown(self) -> None:
class PythonHandler(BaseHandler):
- """The Python handler class, nothing specific here."""
+ """The Python handler class.
+
+ Attributes:
+ 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.
+ """
+
+ domain: str = "py" # to match Sphinx's default domain
+ enable_inventory: bool = True
+
+ @classmethod
+ def load_inventory(
+ cls, in_file: BinaryIO, url: str, base_url: Optional[str] = None, **kwargs
+ ) -> 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).
+
+ Arguments:
+ in_file: The binary file-like object to read the inventory from.
+ url: The URL that this file is being streamed from (used to guess `base_url`).
+ base_url: The URL that this inventory's sub-paths are relative to.
+ **kwargs: Ignore additional arguments passed from the config.
+
+ Yields:
+ Tuples of (item identifier, item URL).
+ """
+ if base_url is None:
+ base_url = posixpath.dirname(url)
+
+ for item in Inventory.parse_sphinx(in_file, domain_filter=("py",)).values(): # noqa: WPS526
+ yield item.name, posixpath.join(base_url, item.uri)
def get_handler(
@@ -282,3 +337,36 @@ def rebuild_category_lists(obj: dict) -> None:
obj["children"] = [child for _, child in obj["children"].items()]
for child in obj["children"]:
rebuild_category_lists(child)
+
+
+def sort_object(obj: CollectorItem, sort_function: Callable[[CollectorItem], Any]) -> None:
+ """Sort the collected object's children.
+
+ Sorts the object's children list, then each category separately, and then recurses into each.
+
+ Arguments:
+ obj: The collected object, as a dict. Note that this argument is mutated.
+ sort_function: The sort key function used to determine the order of elements.
+ """
+ obj["children"].sort(key=sort_function)
+
+ for category in ("attributes", "classes", "functions", "methods", "modules"):
+ obj[category].sort(key=sort_function)
+
+ for child in obj["children"]:
+ sort_object(child, sort_function=sort_function)
+
+
+def _sort_key_alphabetical(item: CollectorItem) -> Any:
+ """Return a sort key for 'alphabetical' sorting of CollectorItems."""
+ # 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.get("name", chr(sys.maxunicode))
+
+
+def _sort_key_source(item: CollectorItem) -> Any:
+ """Return a sort key for 'source' sorting of CollectorItems."""
+ # if 'line_start' isn't found on the object, the item will go to
+ # the start of the list.
+ return item.get("source", {}).get("line_start", -1)
diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py
index ed9049ba..e8383cf2 100644
--- a/src/mkdocstrings/handlers/rendering.py
+++ b/src/mkdocstrings/handlers/rendering.py
@@ -3,7 +3,7 @@
import copy
import re
import textwrap
-from typing import List, Optional
+from typing import Any, Dict, List, Optional
from xml.etree.ElementTree import Element
from markdown import Markdown
@@ -43,8 +43,8 @@ def __init__(self, md: Markdown):
Arguments:
md: The Markdown instance to read configs from.
"""
- config = {}
- for ext in md.registeredExtensions:
+ config: Dict[str, Any] = {}
+ for ext in md.registeredExtensions: # type: ignore
if isinstance(ext, HighlightExtension) and (ext.enabled or not config):
config = ext.getConfigs()
break # This one takes priority, no need to continue looking
@@ -52,7 +52,7 @@ def __init__(self, md: Markdown):
config = ext.getConfigs()
config["language_prefix"] = config["lang_prefix"]
self._css_class = config.pop("css_class", "highlight")
- super().__init__(**{k: v for k, v in config.items() if k in self._highlight_config_keys})
+ super().__init__(**{name: opt for name, opt in config.items() if name in self._highlight_config_keys})
def highlight( # noqa: W0221 (intentionally different params, we're extending the functionality)
self,
@@ -83,7 +83,7 @@ def highlight( # noqa: W0221 (intentionally different params, we're extending t
src = textwrap.dedent(src)
kwargs.setdefault("css_class", self._css_class)
- old_linenums = self.linenums
+ old_linenums = self.linenums # type: ignore
if linenums is not None:
self.linenums = linenums
try:
@@ -185,7 +185,7 @@ def run(self, root: Element):
el = copy.copy(el)
# 'toc' extension's first pass (which we require to build heading stubs/ids) also edits the HTML.
# Undo the permalink edit so we can pass this heading to the outer pass of the 'toc' extension.
- if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class:
+ if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class: # noqa: WPS507
del el[-1]
self.headings.append(el)
diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py
new file mode 100644
index 00000000..9a59d2b4
--- /dev/null
+++ b/src/mkdocstrings/inventory.py
@@ -0,0 +1,132 @@
+"""Module responsible for the objects inventory."""
+
+# Credits to Brian Skinn and the sphobjinv project:
+# https://github.com/bskinn/sphobjinv
+
+import re
+import zlib
+from textwrap import dedent
+from typing import BinaryIO, Collection, List, Optional
+
+
+class InventoryItem:
+ """Inventory item."""
+
+ def __init__(
+ self, name: str, domain: str, role: str, uri: str, priority: str = "1", dispname: Optional[str] = None
+ ):
+ """Initialize the object.
+
+ Arguments:
+ name: The item name.
+ domain: The item domain, like 'python' or 'crystal'.
+ role: The item role, like 'class' or 'method'.
+ uri: The item URI.
+ priority: The item priority. It can help for inventory suggestions.
+ dispname: The item display name.
+ """
+ self.name: str = name
+ self.domain: str = domain
+ self.role: str = role
+ self.uri: str = uri
+ self.priority: str = priority
+ self.dispname: str = dispname or name
+
+ def format_sphinx(self) -> str:
+ """Format this item as a Sphinx inventory line.
+
+ Returns:
+ A line formatted for an `objects.inv` file.
+ """
+ dispname = self.dispname
+ if dispname == self.name:
+ dispname = "-"
+ uri = self.uri
+ if uri.endswith(self.name):
+ uri = uri[: -len(self.name)] + "$"
+ return f"{self.name} {self.domain}:{self.role} {self.priority} {uri} {dispname}"
+
+ sphinx_item_regex = re.compile(r"^(.+?)\s+(\S+):(\S+)\s+(-?\d+)\s+(\S+)\s+(.*)$")
+
+ @classmethod
+ def parse_sphinx(cls, line: str) -> "InventoryItem":
+ """Parse a line from a Sphinx v2 inventory file and return an `InventoryItem` from it."""
+ match = cls.sphinx_item_regex.search(line)
+ if not match:
+ raise ValueError(line)
+ name, domain, role, priority, uri, dispname = match.groups()
+ if uri.endswith("$"):
+ uri = uri[:-1] + name
+ if dispname == "-":
+ dispname = name
+ return cls(name, domain, role, uri, priority, dispname)
+
+
+class Inventory(dict):
+ """Inventory of collected and rendered objects."""
+
+ def __init__(self, items: Optional[List[InventoryItem]] = None, project: str = "project", version: str = "0.0.0"):
+ """Initialize the object.
+
+ Arguments:
+ items: A list of items.
+ project: The project name.
+ version: The project version.
+ """
+ super().__init__()
+ items = items or []
+ for item in items:
+ self[item.name] = item
+ self.project = project
+ self.version = version
+
+ def register(self, *args, **kwargs):
+ """Create and register an item.
+
+ Arguments:
+ *args: Arguments passed to [InventoryItem][mkdocstrings.inventory.InventoryItem].
+ **kwargs: Keyword arguments passed to [InventoryItem][mkdocstrings.inventory.InventoryItem].
+ """
+ item = InventoryItem(*args, **kwargs)
+ self[item.name] = item
+
+ def format_sphinx(self) -> bytes:
+ """Format this inventory as a Sphinx `objects.inv` file.
+
+ Returns:
+ The inventory as bytes.
+ """
+ header = (
+ dedent(
+ f"""
+ # Sphinx inventory version 2
+ # Project: {self.project}
+ # Version: {self.version}
+ # The remainder of this file is compressed using zlib.
+ """
+ )
+ .lstrip()
+ .encode("utf8")
+ )
+
+ lines = [item.format_sphinx().encode("utf8") for item in self.values()]
+ return header + zlib.compress(b"\n".join(lines) + b"\n", 9)
+
+ @classmethod
+ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) -> "Inventory":
+ """Parse a Sphinx v2 inventory file and return an `Inventory` from it.
+
+ Arguments:
+ in_file: The binary file-like object to read from.
+ domain_filter: A collection of domain values to allow (and filter out all other ones).
+
+ Returns:
+ An `Inventory` containing the collected `InventoryItem`s.
+ """
+ for _ in range(4):
+ in_file.readline()
+ lines = zlib.decompress(in_file.read()).splitlines()
+ items = [InventoryItem.parse_sphinx(line.decode("utf8")) for line in lines]
+ if domain_filter:
+ items = [item for item in items if item.domain in domain_filter]
+ return cls(items)
diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py
index 054cd2e0..4e693473 100644
--- a/src/mkdocstrings/plugin.py
+++ b/src/mkdocstrings/plugin.py
@@ -12,10 +12,14 @@
during the [`on_serve` event hook](https://www.mkdocs.org/user-guide/plugins/#on_serve).
"""
+import collections
+import functools
+import gzip
import os
-from typing import Callable, Optional, Tuple
+from concurrent import futures
+from typing import Any, BinaryIO, Callable, Iterable, List, Mapping, Optional, Tuple
+from urllib import request
-from livereload import Server
from mkdocs.config import Config
from mkdocs.config.config_options import Type as MkType
from mkdocs.plugins import BasePlugin
@@ -33,6 +37,9 @@
RENDERING_OPTS_KEY: str = "rendering"
"""The name of the rendering parameter in YAML configuration blocks."""
+InventoryImportType = List[Tuple[str, Mapping[str, Any]]]
+InventoryLoaderType = Callable[..., Iterable[Tuple[str, str]]]
+
class MkdocstringsPlugin(BasePlugin):
"""An `mkdocs` plugin.
@@ -40,6 +47,7 @@ class MkdocstringsPlugin(BasePlugin):
This plugin defines the following event hooks:
- `on_config`
+ - `on_env`
- `on_post_build`
- `on_serve`
@@ -52,6 +60,7 @@ class MkdocstringsPlugin(BasePlugin):
("handlers", MkType(dict, default={})),
("default_handler", MkType(str, default="python")),
("custom_templates", MkType(str, default=None)),
+ ("enable_inventory", MkType(bool, default=None)),
)
"""
The configuration options of `mkdocstrings`, written in `mkdocs.yml`.
@@ -105,7 +114,7 @@ def handlers(self) -> Handlers:
raise RuntimeError("The plugin hasn't been initialized with a config yet")
return self._handlers
- def on_serve(self, server: Server, builder: Callable = None, **kwargs) -> Server: # noqa: W0613 (unused arguments)
+ def on_serve(self, server, builder: Callable, **kwargs): # noqa: W0613 (unused arguments)
"""Watch directories.
Hook for the [`on_serve` event](https://www.mkdocs.org/user-guide/plugins/#on_serve).
@@ -117,18 +126,10 @@ def on_serve(self, server: Server, builder: Callable = None, **kwargs) -> Server
server: The `livereload` server instance.
builder: The function to build the site.
kwargs: Additional arguments passed by MkDocs.
-
- Returns:
- The server instance.
"""
- if builder is None:
- # The builder parameter was added in mkdocs v1.1.1.
- # See issue https://github.com/mkdocs/mkdocs/issues/1952.
- builder = list(server.watcher._tasks.values())[0]["func"] # noqa: W0212 (protected member)
for element in self.config["watch"]:
log.debug(f"Adding directory '{element}' to watcher")
server.watch(element, builder)
- return server
def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613 (unused arguments)
"""Instantiate our Markdown extension.
@@ -155,7 +156,15 @@ def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613 (unused
else:
theme_name = config["theme"].name
+ to_import: InventoryImportType = []
+ for handler_name, conf in self.config["handlers"].items():
+ for import_item in conf.pop("import", ()):
+ if isinstance(import_item, str):
+ import_item = {"url": import_item}
+ to_import.append((handler_name, import_item))
+
extension_config = {
+ "site_name": config["site_name"],
"theme_name": theme_name,
"mdx": config["markdown_extensions"],
"mdx_configs": config["mdx_configs"],
@@ -163,7 +172,7 @@ def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613 (unused
}
self._handlers = Handlers(extension_config)
- try:
+ try: # noqa: WPS229
# If autorefs plugin is explicitly enabled, just use it.
autorefs = config["plugins"]["autorefs"]
log.debug(f"Picked up existing autorefs instance {autorefs!r}")
@@ -174,15 +183,61 @@ def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613 (unused
config["plugins"]["autorefs"] = autorefs
log.debug(f"Added a subdued autorefs instance {autorefs!r}")
# Add collector-based fallback in either case.
- autorefs.get_fallback_anchor = self._handlers.get_anchor
+ autorefs.get_fallback_anchor = self.handlers.get_anchor
- mkdocstrings_extension = MkdocstringsExtension(extension_config, self._handlers, autorefs)
+ mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs)
config["markdown_extensions"].append(mkdocstrings_extension)
config["extra_css"].insert(0, self.css_filename) # So that it has lower priority than user files.
+ self._inv_futures = []
+ if to_import:
+ inv_loader = futures.ThreadPoolExecutor(4)
+ for handler_name, import_item in to_import: # noqa: WPS440
+ future = inv_loader.submit(
+ self._load_inventory, self.get_handler(handler_name).load_inventory, **import_item
+ )
+ self._inv_futures.append(future)
+ inv_loader.shutdown(wait=False)
+
return config
+ @property
+ def inventory_enabled(self) -> bool:
+ """Tell if the inventory is enabled or not.
+
+ Returns:
+ Whether the inventory is enabled.
+ """
+ inventory_enabled = self.config["enable_inventory"]
+ if inventory_enabled is None:
+ inventory_enabled = any(handler.enable_inventory for handler in self.handlers.seen_handlers)
+ return inventory_enabled
+
+ def on_env(self, env, config: Config, **kwargs):
+ """Extra actions that need to happen after all Markdown rendering and before HTML rendering.
+
+ Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env).
+
+ - Write mkdocstrings' extra files into the site dir.
+ - Gather results from background inventory download tasks.
+ """
+ if self._handlers:
+ css_content = "\n".join(handler.renderer.extra_css for handler in self.handlers.seen_handlers)
+ write_file(css_content.encode("utf-8"), os.path.join(config["site_dir"], self.css_filename))
+
+ if self.inventory_enabled:
+ log.debug("Creating inventory file objects.inv")
+ inv_contents = self.handlers.inventory.format_sphinx()
+ write_file(inv_contents, os.path.join(config["site_dir"], "objects.inv"))
+
+ if self._inv_futures:
+ log.debug(f"Waiting for {len(self._inv_futures)} inventory download(s)")
+ futures.wait(self._inv_futures, timeout=30)
+ for page, identifier in collections.ChainMap(*(fut.result() for fut in self._inv_futures)).items():
+ config["plugins"]["autorefs"].register_url(page, identifier)
+ self._inv_futures = []
+
def on_post_build(self, config: Config, **kwargs) -> None: # noqa: W0613,R0201 (unused arguments, cannot be static)
"""Teardown the handlers.
@@ -199,12 +254,12 @@ def on_post_build(self, config: Config, **kwargs) -> None: # noqa: W0613,R0201
config: The MkDocs config object.
kwargs: Additional arguments passed by MkDocs.
"""
- if self._handlers:
- css_content = "\n".join(handler.renderer.extra_css for handler in self.handlers.seen_handlers)
- write_file(css_content.encode("utf-8"), os.path.join(config["site_dir"], self.css_filename))
+ for future in self._inv_futures:
+ future.cancel()
+ if self._handlers:
log.debug("Tearing handlers down")
- self._handlers.teardown()
+ self.handlers.teardown()
def get_handler(self, handler_name: str) -> BaseHandler:
"""Get a handler by its name. See [mkdocstrings.handlers.base.Handlers.get_handler][].
@@ -216,3 +271,26 @@ def get_handler(self, handler_name: str) -> BaseHandler:
An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler].
"""
return self.handlers.get_handler(handler_name)
+
+ @classmethod
+ @functools.lru_cache(maxsize=None)
+ def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs) -> Mapping[str, str]:
+ """Download and process inventory files using a handler.
+
+ Arguments:
+ loader: A function returning a sequence of pairs (identifier, url).
+ url: The URL to download and process.
+ kwargs: Extra arguments to pass to the loader.
+
+ Returns:
+ A mapping from identifier to absolute URL.
+ """
+ log.debug(f"Downloading inventory from {url!r}")
+ req = request.Request(url, headers={"Accept-Encoding": "gzip", "User-Agent": "mkdocstrings/0.15.0"})
+ with request.urlopen(req) as resp: # noqa: S310 (URL audit OK: comes from a checked-in config)
+ content: BinaryIO = resp
+ if "gzip" in resp.headers.get("content-encoding", ""):
+ content = gzip.GzipFile(fileobj=resp) # type: ignore[assignment]
+ result = dict(loader(content, url=url, **kwargs))
+ log.debug(f"Loaded inventory from {url!r}: {len(result)} items")
+ return result
diff --git a/src/mkdocstrings/py.typed b/src/mkdocstrings/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/src/mkdocstrings/templates/python/material/attribute.html b/src/mkdocstrings/templates/python/material/attribute.html
index 4b742509..b40518a9 100644
--- a/src/mkdocstrings/templates/python/material/attribute.html
+++ b/src/mkdocstrings/templates/python/material/attribute.html
@@ -17,6 +17,7 @@
{% endif %}
{% filter heading(heading_level,
+ role="data" if obj == module else "attr",
id=html_id,
class="doc doc-heading",
toc_label=attribute.name) %}
@@ -35,6 +36,7 @@
{% else %}
{% if config.show_root_toc_entry %}
{% filter heading(heading_level,
+ role="data" if obj == module else "attr",
id=html_id,
toc_label=attribute.path,
hidden=True) %}
diff --git a/src/mkdocstrings/templates/python/material/attributes.html b/src/mkdocstrings/templates/python/material/attributes.html
index 0c935472..02a2935b 100644
--- a/src/mkdocstrings/templates/python/material/attributes.html
+++ b/src/mkdocstrings/templates/python/material/attributes.html
@@ -12,7 +12,7 @@
{% for attribute in attributes %}
{{ attribute.name }} |
- {{ attribute.annotation }} |
+ {% if attribute.annotation %}{{ attribute.annotation }}{% endif %} |
{{ attribute.description|convert_markdown(heading_level, html_id) }} |
{% endfor %}
diff --git a/src/mkdocstrings/templates/python/material/children.html b/src/mkdocstrings/templates/python/material/children.html
index 967ad493..7bc56c2d 100644
--- a/src/mkdocstrings/templates/python/material/children.html
+++ b/src/mkdocstrings/templates/python/material/children.html
@@ -17,7 +17,7 @@
{% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %}
{% endif %}
{% with heading_level = heading_level + extra_level %}
- {% for attribute in obj.attributes|sort(attribute="name") %}
+ {% for attribute in obj.attributes %}
{% include "attribute.html" with context %}
{% endfor %}
{% endwith %}
@@ -26,7 +26,7 @@
{% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %}
{% endif %}
{% with heading_level = heading_level + extra_level %}
- {% for class in obj.classes|sort(attribute="name") %}
+ {% for class in obj.classes %}
{% include "class.html" with context %}
{% endfor %}
{% endwith %}
@@ -35,7 +35,7 @@
{% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %}
{% endif %}
{% with heading_level = heading_level + extra_level %}
- {% for function in obj.functions|sort(attribute="name") %}
+ {% for function in obj.functions %}
{% include "function.html" with context %}
{% endfor %}
{% endwith %}
@@ -44,7 +44,7 @@
{% filter heading(heading_level, id=html_id ~ "-methods") %}Methods{% endfilter %}
{% endif %}
{% with heading_level = heading_level + extra_level %}
- {% for method in obj.methods|sort(attribute="name") %}
+ {% for method in obj.methods %}
{% include "method.html" with context %}
{% endfor %}
{% endwith %}
@@ -53,7 +53,7 @@
{% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %}
{% endif %}
{% with heading_level = heading_level + extra_level %}
- {% for module in obj.modules|sort(attribute="name") %}
+ {% for module in obj.modules %}
{% include "module.html" with context %}
{% endfor %}
{% endwith %}
@@ -62,7 +62,7 @@
{% else %}
- {% for child in obj.children|sort(attribute="name") %}
+ {% for child in obj.children %}
{% if child.category == "attribute" %}
{% with attribute = child %}
{% include "attribute.html" with context %}
diff --git a/src/mkdocstrings/templates/python/material/class.html b/src/mkdocstrings/templates/python/material/class.html
index b3e33f84..62a70f57 100644
--- a/src/mkdocstrings/templates/python/material/class.html
+++ b/src/mkdocstrings/templates/python/material/class.html
@@ -17,11 +17,19 @@
{% endif %}
{% filter heading(heading_level,
+ role="class",
id=html_id,
class="doc doc-heading",
toc_label=class.name) %}
- {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %}
+
+ {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %}
+ {% if config.show_bases and class.bases and class.bases != ['object'] %}
+ ({% for base in class.bases -%}
+ {{ base|brief_xref() }}{% if not loop.last %}, {% endif %}
+ {% endfor %})
+ {% endif %}
+
{% with properties = class.properties %}
{% include "properties.html" with context %}
@@ -32,6 +40,7 @@
{% else %}
{% if config.show_root_toc_entry %}
{% filter heading(heading_level,
+ role="class",
id=html_id,
toc_label=class.path,
hidden=True) %}
diff --git a/src/mkdocstrings/templates/python/material/function.html b/src/mkdocstrings/templates/python/material/function.html
index 5ac592b9..6f8e6c77 100644
--- a/src/mkdocstrings/templates/python/material/function.html
+++ b/src/mkdocstrings/templates/python/material/function.html
@@ -17,6 +17,7 @@
{% endif %}
{% filter heading(heading_level,
+ role="function",
id=html_id,
class="doc doc-heading",
toc_label=function.name ~ "()") %}
@@ -35,6 +36,7 @@
{% else %}
{% if config.show_root_toc_entry %}
{% filter heading(heading_level,
+ role="function",
id=html_id,
toc_label=function.path,
hidden=True) %}
diff --git a/src/mkdocstrings/templates/python/material/method.html b/src/mkdocstrings/templates/python/material/method.html
index 19e9a530..807009e5 100644
--- a/src/mkdocstrings/templates/python/material/method.html
+++ b/src/mkdocstrings/templates/python/material/method.html
@@ -17,6 +17,7 @@
{% endif %}
{% filter heading(heading_level,
+ role="method",
id=html_id,
class="doc doc-heading",
toc_label=method.name ~ "()") %}
@@ -35,6 +36,7 @@
{% else %}
{% if config.show_root_toc_entry %}
{% filter heading(heading_level,
+ role="method",
id=html_id,
toc_label=method.path,
hidden=True) %}
diff --git a/src/mkdocstrings/templates/python/material/module.html b/src/mkdocstrings/templates/python/material/module.html
index bff6fdcf..ba8f4eac 100644
--- a/src/mkdocstrings/templates/python/material/module.html
+++ b/src/mkdocstrings/templates/python/material/module.html
@@ -17,6 +17,7 @@
{% endif %}
{% filter heading(heading_level,
+ role="module",
id=html_id,
class="doc doc-heading",
toc_label=module.name) %}
@@ -32,6 +33,7 @@
{% else %}
{% if config.show_root_toc_entry %}
{% filter heading(heading_level,
+ role="module",
id=html_id,
toc_label=module.path,
hidden=True) %}
diff --git a/src/mkdocstrings/templates/python/material/parameters.html b/src/mkdocstrings/templates/python/material/parameters.html
index 8ff69147..321318e0 100644
--- a/src/mkdocstrings/templates/python/material/parameters.html
+++ b/src/mkdocstrings/templates/python/material/parameters.html
@@ -13,7 +13,7 @@
{% for parameter in parameters %}
{{ parameter.name }} |
- {{ parameter.annotation }} |
+ {% if parameter.annotation %}{{ parameter.annotation }}{% endif %} |
{{ parameter.description|convert_markdown(heading_level, html_id) }} |
{% if parameter.default %}{{ parameter.default }}{% else %}required{% endif %} |
diff --git a/src/mkdocstrings/templates/python/material/return.html b/src/mkdocstrings/templates/python/material/return.html
index cb108a1b..f4282491 100644
--- a/src/mkdocstrings/templates/python/material/return.html
+++ b/src/mkdocstrings/templates/python/material/return.html
@@ -9,7 +9,7 @@
- {{ return.annotation }} |
+ {% if return.annotation %}{{ return.annotation }}{% endif %} |
{{ return.description|convert_markdown(heading_level, html_id) }} |
diff --git a/src/mkdocstrings/templates/python/mkdocs/exceptions.html b/src/mkdocstrings/templates/python/mkdocs/exceptions.html
new file mode 100644
index 00000000..f5b592f5
--- /dev/null
+++ b/src/mkdocstrings/templates/python/mkdocs/exceptions.html
@@ -0,0 +1,7 @@
+{{ log.debug() }}
+
+ - Exceptions:
+ {% for exception in exceptions %}
+
- {{ ("`" + exception.annotation + "`: " + exception.description)|convert_markdown(heading_level, html_id) }}
+ {% endfor %}
+
diff --git a/src/mkdocstrings/templates/python/mkdocs/parameters.html b/src/mkdocstrings/templates/python/mkdocs/parameters.html
new file mode 100644
index 00000000..39db7ea3
--- /dev/null
+++ b/src/mkdocstrings/templates/python/mkdocs/parameters.html
@@ -0,0 +1,7 @@
+{{ log.debug() }}
+
+ - Parameters:
+ {% for parameter in parameters %}
+
- {{ ("**" + parameter.name + ":** " + ("`" + parameter.annotation + "` – " if parameter.annotation else "") + parameter.description)|convert_markdown(heading_level, html_id) }}
+ {% endfor %}
+
diff --git a/src/mkdocstrings/templates/python/mkdocs/return.html b/src/mkdocstrings/templates/python/mkdocs/return.html
new file mode 100644
index 00000000..270823c4
--- /dev/null
+++ b/src/mkdocstrings/templates/python/mkdocs/return.html
@@ -0,0 +1,5 @@
+{{ log.debug() }}
+
+ - Returns:
+
- {{ (("`" + return.annotation + "` – " if return.annotation else "") + return.description)|convert_markdown(heading_level, html_id) }}
+
diff --git a/src/mkdocstrings/templates/python/mkdocs/style.css b/src/mkdocstrings/templates/python/mkdocs/style.css
new file mode 100644
index 00000000..9db45032
--- /dev/null
+++ b/src/mkdocstrings/templates/python/mkdocs/style.css
@@ -0,0 +1,11 @@
+.doc-contents {
+ padding-left: 20px;
+}
+
+.doc-contents dd>p {
+ margin-bottom: 0.5rem;
+}
+
+.doc-contents dl+dl {
+ margin-top: -0.5rem;
+}
diff --git a/src/mkdocstrings/templates/python/readthedocs/parameters.html b/src/mkdocstrings/templates/python/readthedocs/parameters.html
index 5ae18219..197a411e 100644
--- a/src/mkdocstrings/templates/python/readthedocs/parameters.html
+++ b/src/mkdocstrings/templates/python/readthedocs/parameters.html
@@ -10,7 +10,7 @@
{% for parameter in parameters %}
- - {{ ("**" + parameter.name + "** (`" + parameter.annotation + "`) – " + parameter.description)|convert_markdown(heading_level, html_id) }}
+ - {{ ("**" + parameter.name + "**" + (" (`" + parameter.annotation + "`)" if parameter.annotation else "") + " – " + parameter.description)|convert_markdown(heading_level, html_id) }}
{% endfor %}
|
diff --git a/src/mkdocstrings/templates/python/readthedocs/return.html b/src/mkdocstrings/templates/python/readthedocs/return.html
index f30b9a25..7e45ecaf 100644
--- a/src/mkdocstrings/templates/python/readthedocs/return.html
+++ b/src/mkdocstrings/templates/python/readthedocs/return.html
@@ -9,7 +9,7 @@
Returns: |
- - {{ ("`" + return.annotation + "` – " + return.description)|convert_markdown(heading_level, html_id) }}
+ - {{ (("`" + return.annotation + "` – ") if return.annotation else "") + return.description)|convert_markdown(heading_level, html_id) }}
|
diff --git a/tests/test_extension.py b/tests/test_extension.py
index e2e92903..3d4a5294 100644
--- a/tests/test_extension.py
+++ b/tests/test_extension.py
@@ -1,4 +1,5 @@
"""Tests for the extension module."""
+import re
import sys
from collections import ChainMap
from textwrap import dedent
@@ -7,14 +8,23 @@
from markdown import Markdown
from mkdocs import config
+try:
+ from mkdocs.config.defaults import get_schema
+except ImportError:
+
+ def get_schema(): # noqa: WPS440
+ """Fallback for old versions of MkDocs."""
+ return config.DEFAULT_SCHEMA
+
@pytest.fixture(name="ext_markdown")
def fixture_ext_markdown(request, tmp_path):
"""Yield a Markdown instance with MkdocstringsExtension, with config adjustments."""
- conf = config.Config(schema=config.DEFAULT_SCHEMA)
+ conf = config.Config(schema=get_schema())
conf_dict = {
"site_name": "foo",
+ "site_url": "https://example.org/",
"site_dir": str(tmp_path),
"plugins": [{"mkdocstrings": {"default_handler": "python"}}],
**getattr(request, "param", {}),
@@ -82,8 +92,7 @@ def test_keeps_preceding_text(ext_markdown):
def test_reference_inside_autodoc(ext_markdown):
"""Assert cross-reference Markdown extension works correctly."""
output = ext_markdown.convert("::: tests.fixtures.cross_reference")
- snippet = 'Link to something.Else.'
- assert snippet in output
+ assert re.search(r"Link to <.*something\.Else.*>something\.Else<.*>\.", output)
@pytest.mark.skipif(sys.version_info < (3, 8), reason="typing.Literal requires Python 3.8")
@@ -152,3 +161,9 @@ def test_no_double_toc(ext_markdown, expect_permalink):
},
{"level": 1, "id": "bb", "name": "bb", "children": []},
]
+
+
+def test_use_custom_handler(ext_markdown):
+ """Assert that we use the custom handler declared in an individual autodoc instruction."""
+ with pytest.raises(ModuleNotFoundError):
+ ext_markdown.convert("::: tests.fixtures.headings\n handler: not_here")
diff --git a/tests/test_inventory.py b/tests/test_inventory.py
new file mode 100644
index 00000000..471ed941
--- /dev/null
+++ b/tests/test_inventory.py
@@ -0,0 +1,51 @@
+"""Tests for the inventory module."""
+
+import sys
+from io import BytesIO
+from os.path import join
+
+import pytest
+from mkdocs.commands.build import build
+from mkdocs.config import load_config
+
+from mkdocstrings.inventory import Inventory, InventoryItem
+
+sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed")
+
+
+@pytest.mark.parametrize(
+ "our_inv",
+ [
+ Inventory(),
+ Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url")]),
+ Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#object_path")]),
+ Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#other_anchor")]),
+ ],
+)
+def test_sphinx_load_inventory_file(our_inv):
+ """Perform the 'live' inventory load test."""
+ buffer = BytesIO(our_inv.format_sphinx())
+ sphinx_inv = sphinx.InventoryFile.load(buffer, "", join)
+
+ sphinx_inv_length = sum(len(sphinx_inv[key]) for key in sphinx_inv)
+ assert sphinx_inv_length == len(our_inv.values())
+
+ for item in our_inv.values():
+ assert item.name in sphinx_inv[f"{item.domain}:{item.role}"]
+
+
+@pytest.mark.skipif(sys.version_info < (3, 7), reason="using plugins that require Python 3.7")
+def test_sphinx_load_mkdocstrings_inventory_file():
+ """Perform the 'live' inventory load test on mkdocstrings own inventory."""
+ mkdocs_config = load_config()
+ build(mkdocs_config)
+ own_inv = mkdocs_config["plugins"]["mkdocstrings"].handlers.inventory
+
+ with open("site/objects.inv", "rb") as fp:
+ sphinx_inv = sphinx.InventoryFile.load(fp, "", join)
+
+ sphinx_inv_length = sum(len(sphinx_inv[key]) for key in sphinx_inv)
+ assert sphinx_inv_length == len(own_inv.values())
+
+ for item in own_inv.values():
+ assert item.name in sphinx_inv[f"{item.domain}:{item.role}"]
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
deleted file mode 100644
index 3bdad73c..00000000
--- a/tests/test_plugin.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Tests for the plugin module."""
-
-import sys
-
-import pytest
-from mkdocs.commands.build import build
-from mkdocs.config.base import load_config
-
-
-@pytest.mark.skipif(sys.version_info < (3, 7), reason="using plugins that require Python 3.7")
-@pytest.mark.xfail(sys.version_info >= (3, 9), reason="pytkdocs is failing on Python 3.9")
-def test_plugin(tmp_path):
- """Build our own documentation."""
- config = load_config()
- config["site_dir"] = tmp_path
- build(config)
diff --git a/tests/test_python_handler.py b/tests/test_python_handler.py
new file mode 100644
index 00000000..315b5760
--- /dev/null
+++ b/tests/test_python_handler.py
@@ -0,0 +1,86 @@
+"""Tests for the handlers.python module."""
+
+from copy import deepcopy
+
+from mkdocstrings.handlers.python import ( # noqa: WPS450
+ _sort_key_alphabetical,
+ _sort_key_source,
+ rebuild_category_lists,
+ sort_object,
+)
+
+
+def test_members_order():
+ """Assert that members sorting functions work correctly."""
+ subcategories = {key: [] for key in ("attributes", "classes", "functions", "methods", "modules")}
+ categories = {"children": {}, **subcategories}
+ collected = {
+ "name": "root",
+ "children": {
+ "b": {"name": "b", "source": {"line_start": 0}, **categories},
+ "a": {"name": "a", **categories},
+ "z": {"name": "z", "source": {"line_start": 100}, **categories},
+ "no_name": {"source": {"line_start": 10}, **categories},
+ "c": {
+ "name": "c",
+ "source": {"line_start": 30},
+ "children": {
+ "z": {"name": "z", "source": {"line_start": 200}, **categories},
+ "a": {"name": "a", "source": {"line_start": 20}, **categories},
+ },
+ **subcategories,
+ },
+ },
+ "attributes": ["b", "c", "no_name", "z", "a"],
+ "classes": [],
+ "functions": [],
+ "methods": [],
+ "modules": [],
+ }
+ rebuild_category_lists(collected)
+ alphebetical = deepcopy(collected)
+ sort_object(alphebetical, _sort_key_alphabetical)
+
+ rebuilt_categories = {"children": [], **subcategories}
+ assert (
+ alphebetical["children"]
+ == alphebetical["attributes"]
+ == [
+ {"name": "a", **rebuilt_categories},
+ {"name": "b", "source": {"line_start": 0}, **rebuilt_categories},
+ {
+ "name": "c",
+ "source": {"line_start": 30},
+ "children": [
+ {"name": "a", "source": {"line_start": 20}, **rebuilt_categories},
+ {"name": "z", "source": {"line_start": 200}, **rebuilt_categories},
+ ],
+ **subcategories,
+ },
+ {"name": "z", "source": {"line_start": 100}, **rebuilt_categories},
+ {"source": {"line_start": 10}, **rebuilt_categories},
+ ]
+ )
+
+ source = deepcopy(collected)
+ sort_object(source, _sort_key_source)
+
+ assert (
+ source["children"]
+ == source["attributes"]
+ == [
+ {"name": "a", **rebuilt_categories},
+ {"name": "b", "source": {"line_start": 0}, **rebuilt_categories},
+ {"source": {"line_start": 10}, **rebuilt_categories},
+ {
+ "name": "c",
+ "source": {"line_start": 30},
+ "children": [
+ {"name": "a", "source": {"line_start": 20}, **rebuilt_categories},
+ {"name": "z", "source": {"line_start": 200}, **rebuilt_categories},
+ ],
+ **subcategories,
+ },
+ {"name": "z", "source": {"line_start": 100}, **rebuilt_categories},
+ ]
+ )