diff --git a/.copier-answers.yml b/.copier-answers.yml
index eab94c2f..cd27ed0e 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
-_commit: 0.4.4
+_commit: 0.7.0
_src_path: gh:pawamoy/copier-pdm
author_email: pawamoy@pm.me
author_fullname: "Timoth\xE9e Mazzucotelli"
@@ -14,6 +14,6 @@ python_package_command_line_name: mkdocstrings-python
python_package_distribution_name: mkdocstrings-python
python_package_import_name: mkdocstrings
repository_name: python
-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 3dc5c7af..7cf7c656 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,15 +50,13 @@ jobs:
run: pdm lock
- name: Install dependencies
- run: |
- pdm install -G duty -G docs -G quality -G typing
- pip install safety
+ run: pdm install -G duty -G docs -G quality -G typing -G security
- name: Check if the documentation builds correctly
run: pdm run duty check-docs
- name: Check the code quality
- run: pdm run duty check-code-quality
+ run: pdm run duty check-quality
- name: Check if the code is correctly typed
run: pdm run duty check-types
@@ -75,10 +73,10 @@ jobs:
- macos-latest
- windows-latest
python-version:
- - "3.6"
- - "3.7"
- "3.8"
- "3.9"
+ - "3.10"
+ - "3.11-dev"
runs-on: ${{ matrix.os }}
diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile
new file mode 100644
index 00000000..33f285c2
--- /dev/null
+++ b/.gitpod.dockerfile
@@ -0,0 +1,7 @@
+FROM gitpod/workspace-full
+USER gitpod
+ENV PIP_USER=no
+ENV PYTHON_VERSIONS=
+RUN pip3 install pipx; \
+ pipx install pdm; \
+ pipx ensurepath
diff --git a/.gitpod.yml b/.gitpod.yml
new file mode 100644
index 00000000..23a3c2b7
--- /dev/null
+++ b/.gitpod.yml
@@ -0,0 +1,13 @@
+vscode:
+ extensions:
+ - ms-python.python
+
+image:
+ file: .gitpod.dockerfile
+
+ports:
+- port: 8000
+ onOpen: notify
+
+tasks:
+- init: make setup
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49212ce3..5b44c751 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,3 +5,18 @@ 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.1.0](https://github.com/mkdocstrings/python/releases/tag/0.1.0) - 2021-12-19
+
+[Compare with first commit](https://github.com/mkdocstrings/python/compare/0032f18c9f902c3e75e0e00114ca8fa6a810c8f5...0.1.0)
+
+### Features
+- Implement handler and add templates ([dbb580a](https://github.com/mkdocstrings/python/commit/dbb580aa79f6b2f8a089c80bdc67d0f7457c2d30) by Timothée Mazzucotelli).
+
+### Bug Fixes
+- Fix separate signature feature ([da6e81c](https://github.com/mkdocstrings/python/commit/da6e81c897899f09e1dae7bb8930ce6782aeb306) by Timothée Mazzucotelli).
+- Fix signature template (parameters annotations) ([b34ead0](https://github.com/mkdocstrings/python/commit/b34ead008773880fd8d1d7a2a41768ec27820520) by Timothée Mazzucotelli).
+- Only show source when present ([c270d68](https://github.com/mkdocstrings/python/commit/c270d68c9e17204606ae12a2159c04563a18ec2b) by Timothée Mazzucotelli).
+
+### Code Refactoring
+- Return all known anchors ([9bbfe14](https://github.com/mkdocstrings/python/commit/9bbfe1442e2aab28bd6fb2618c943d3f698750ab) by Timothée Mazzucotelli).
+- Update for griffe 0.4.0 ([831aabb](https://github.com/mkdocstrings/python/commit/831aabb135db7e75729954adc675af6379f58e24) by Timothée Mazzucotelli).
diff --git a/Makefile b/Makefile
index 97aa6931..58291575 100644
--- a/Makefile
+++ b/Makefile
@@ -4,13 +4,14 @@ SHELL := bash
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
+check_quality_args = files
docs_serve_args = host port
release_args = version
test_args = match
BASIC_DUTIES = \
changelog \
+ check-dependencies \
clean \
coverage \
docs \
@@ -21,9 +22,7 @@ BASIC_DUTIES = \
release
QUALITY_DUTIES = \
- check \
- check-code-quality \
- check-dependencies \
+ check-quality \
check-docs \
check-types \
test
@@ -32,10 +31,19 @@ QUALITY_DUTIES = \
help:
@$(DUTY) --list
+.PHONY: lock
+lock:
+ @pdm lock
+
.PHONY: setup
setup:
@bash scripts/setup.sh
+.PHONY: check
+check:
+ @bash scripts/multirun.sh duty check-quality check-types check-docs
+ @$(DUTY) check-dependencies
+
.PHONY: $(BASIC_DUTIES)
$(BASIC_DUTIES):
@$(DUTY) $@ $(call args,$@)
diff --git a/README.md b/README.md
index 891cf841..fc1eae9e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
# Python for mkdocstrings
-[](https://github.com/pawamoy/python/actions?query=workflow%3Aci)
-[](https://pawamoy.github.io/python/)
+[](https://github.com/mkdocstrings/python/actions?query=workflow%3Aci)
+[](https://mkdocstrings.github.io/python/)
[](https://pypi.org/project/python/)
+[](https://gitpod.io/#https://github.com/mkdocstrings/python)
[](https://gitter.im/python/community)
A Python handler for mkdocstrings.
diff --git a/config/flake8.ini b/config/flake8.ini
index aab3a995..4126bd51 100644
--- a/config/flake8.ini
+++ b/config/flake8.ini
@@ -10,6 +10,8 @@ ignore =
D105
# multi-line docstring summary should start at the first line
D212
+ # does not support Parameters sections
+ D417
# whitespace before ':' (incompatible with Black)
E203
# redundant with E0602 (undefined variable)
@@ -100,5 +102,7 @@ ignore =
WPS428
# redundant with C0415 (not top-level import)
WPS433
+ # multiline attribute docstring
+ WPS462
# implicit dict.get usage (generally false-positive)
WPS529
diff --git a/config/mypy.ini b/config/mypy.ini
index aa988d34..4ac80d76 100644
--- a/config/mypy.ini
+++ b/config/mypy.ini
@@ -1,6 +1,4 @@
[mypy]
ignore_missing_imports = true
exclude = tests/fixtures/
-
-[mypy-toml.*]
-ignore_missing_imports = true
+warn_unused_ignores = true
diff --git a/docs/gen_ref_nav.py b/docs/gen_ref_nav.py
index 3a824137..cc54b86a 100644
--- a/docs/gen_ref_nav.py
+++ b/docs/gen_ref_nav.py
@@ -7,22 +7,27 @@
nav = mkdocs_gen_files.Nav()
for path in sorted(Path("src").glob("**/*.py")):
+ # if str(path) in exclude:
+ # continue
module_path = path.relative_to("src").with_suffix("")
- doc_path = path.relative_to("src", "mkdocstrings").with_suffix(".md")
+ doc_path = path.relative_to("src").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
+ if parts[-1] == "__init__":
+ parts = parts[:-1]
+ doc_path = doc_path.with_name("index.md")
+ full_doc_path = full_doc_path.with_name("index.md")
+ elif parts[-1] == "__main__":
+ continue
+ nav_parts = list(parts)
+ nav[nav_parts] = doc_path
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
- ident = ".".join(module_path.parts)
+ ident = ".".join(parts)
print("::: " + ident, file=fd)
mkdocs_gen_files.set_edit_path(full_doc_path, path)
-# add pages manually:
-# nav["package", "module"] = "path/to/file.md"
-
with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
deleted file mode 100644
index d5221ec5..00000000
--- a/docs/reference/cli.md
+++ /dev/null
@@ -1 +0,0 @@
-::: mkdocstrings.cli
diff --git a/duties.py b/duties.py
index ef43ad63..19c0f6bf 100644
--- a/duties.py
+++ b/duties.py
@@ -1,11 +1,14 @@
"""Development tasks."""
+import importlib
import os
import re
import sys
+import tempfile
+from contextlib import suppress
from functools import wraps
+from io import StringIO
from pathlib import Path
-from shutil import which
from typing import List, Optional, Pattern
from urllib.request import urlopen
@@ -105,7 +108,7 @@ def changelog(ctx):
)
-@duty(pre=["check_code_quality", "check_types", "check_docs", "check_dependencies"])
+@duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies"])
def check(ctx):
"""
Check it all!
@@ -116,7 +119,7 @@ def check(ctx):
@duty
-def check_code_quality(ctx, files=PY_SRC):
+def check_quality(ctx, files=PY_SRC):
"""
Check the code quality.
@@ -135,22 +138,36 @@ def check_dependencies(ctx):
Arguments:
ctx: The context instance (passed automatically).
"""
- nofail = False
- safety = which("safety")
- if not safety:
- pipx = which("pipx")
- if pipx:
- safety = f"{pipx} run safety"
- else:
- safety = "safety"
- nofail = True
- ctx.run(
- f"pdm export -f requirements --without-hashes | {safety} check --stdin --full-report",
- title="Checking dependencies",
- pty=PTY,
- nofail=nofail,
+ # undo possible patching
+ # see https://github.com/pyupio/safety/issues/348
+ for module in sys.modules: # noqa: WPS528
+ if module.startswith("safety.") or module == "safety":
+ del sys.modules[module] # noqa: WPS420
+
+ importlib.invalidate_caches()
+
+ # reload original, unpatched safety
+ from safety.formatter import report
+ from safety.safety import check as safety_check
+ from safety.util import read_requirements
+
+ # retrieve the list of dependencies
+ requirements = ctx.run(
+ ["pdm", "export", "-f", "requirements", "--without-hashes"],
+ title="Exporting dependencies as requirements",
+ allow_overrides=False,
)
+ # check using safety as a library
+ def safety(): # noqa: WPS430
+ packages = list(read_requirements(StringIO(requirements)))
+ vulns = safety_check(packages=packages, ignore_ids="", key="", db_mirror="", cached=False, proxy={})
+ output_report = report(vulns=vulns, full=True, checked_packages=len(packages))
+ if vulns:
+ print(output_report)
+
+ ctx.run(safety, title="Checking dependencies")
+
def no_docs_py36(nofail=True):
"""
@@ -190,15 +207,57 @@ def check_docs(ctx):
ctx.run("mkdocs build -s", title="Building documentation", pty=PTY)
-@duty
-def check_types(ctx):
+@duty # noqa: WPS231
+def check_types(ctx): # noqa: WPS231
"""
Check that the code is correctly typed.
Arguments:
ctx: The context instance (passed automatically).
"""
- ctx.run(f"mypy --config-file config/mypy.ini {PY_SRC}", title="Type-checking", pty=PTY)
+ # NOTE: the following code works around this issue:
+ # https://github.com/python/mypy/issues/10633
+
+ # compute packages directory path
+ py = f"{sys.version_info.major}.{sys.version_info.minor}"
+ pkgs_dir = Path("__pypackages__", py, "lib").resolve()
+
+ # build the list of available packages
+ packages = {}
+ for package in pkgs_dir.glob("*"):
+ if package.suffix not in {".dist-info", ".pth"} and package.name != "__pycache__":
+ packages[package.name] = package
+
+ # handle .pth files
+ for pth in pkgs_dir.glob("*.pth"):
+ with suppress(OSError):
+ for package in Path(pth.read_text().splitlines()[0]).glob("*"): # noqa: WPS440
+ if package.suffix != ".dist-info":
+ packages[package.name] = package
+
+ # create a temporary directory to assign to MYPYPATH
+ with tempfile.TemporaryDirectory() as tmpdir:
+
+ # symlink the stubs
+ ignore = set()
+ for stubs in (path for name, path in packages.items() if name.endswith("-stubs")): # noqa: WPS335
+ Path(tmpdir, stubs.name).symlink_to(stubs, target_is_directory=True)
+ # try to symlink the corresponding package
+ # see https://www.python.org/dev/peps/pep-0561/#stub-only-packages
+ pkg_name = stubs.name.replace("-stubs", "")
+ if pkg_name in packages:
+ ignore.add(pkg_name)
+ Path(tmpdir, pkg_name).symlink_to(packages[pkg_name], target_is_directory=True)
+
+ # create temporary mypy config to ignore stubbed packages
+ newconfig = Path("config", "mypy.ini").read_text()
+ newconfig += "\n" + "\n\n".join(f"[mypy-{pkg}.*]\nignore_errors=true" for pkg in ignore)
+ tmpconfig = Path(tmpdir, "mypy.ini")
+ tmpconfig.write_text(newconfig)
+
+ # set MYPYPATH and run mypy
+ os.environ["MYPYPATH"] = tmpdir
+ ctx.run(f"mypy --config-file {tmpconfig} {PY_SRC}", title="Type-checking", pty=PTY)
@duty(silent=True)
@@ -257,7 +316,11 @@ def docs_deploy(ctx):
Arguments:
ctx: The context instance (passed automatically).
"""
- ctx.run("mkdocs gh-deploy", title="Deploying documentation")
+ ctx.run(
+ "echo 'Not ready yet! It would override mkdocstrings.github.io/python!' && false",
+ title="Deploying documentation",
+ nofail=True,
+ )
@duty
@@ -294,7 +357,7 @@ def release(ctx, version):
ctx.run("git push --tags", title="Pushing tags", pty=False)
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
+ docs_deploy.run()
@duty(silent=True)
diff --git a/mkdocs.yml b/mkdocs.yml
index a7a174fc..1713313f 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,8 +1,8 @@
site_name: "Python for mkdocstrings"
site_description: "A Python handler for mkdocstrings."
-site_url: "https://pawamoy.github.io/python"
-repo_url: "https://github.com/pawamoy/python"
-repo_name: "pawamoy/python"
+site_url: "https://mkdocstrings.github.io/python"
+repo_url: "https://github.com/mkdocstrings/python"
+repo_name: "mkdocstrings/python"
site_dir: "site"
nav:
diff --git a/pyproject.toml b/pyproject.toml
index 43d5dfe1..9b73eb1f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,80 +4,96 @@ build-backend = "pdm.pep517.api"
[project]
name = "mkdocstrings-python"
-version = {use_scm = true}
description = "A Python handler for mkdocstrings."
authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}]
license = {file = "LICENSE"}
readme = "README.md"
-requires-python = ">=3.6"
+requires-python = ">=3.8"
keywords = []
-dynamic = ["version", "classifiers"]
+dynamic = ["version"]
classifiers = [
"Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
"License :: OSI Approved :: ISC License (ISCL)",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Topic :: Documentation",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Documentation",
+ "Topic :: Utilities",
"Typing :: Typed",
]
-dependencies = []
+dependencies = [
+ "griffe~=0.4",
+]
[project.urls]
-Homepage = "https://pawamoy.github.io/python"
-Documentation = "https://pawamoy.github.io/python"
-Changelog = "https://pawamoy.github.io/python/changelog"
-Repository = "https://github.com/pawamoy/python"
-Issues = "https://github.com/pawamoy/python/issues"
-Discussions = "https://github.com/pawamoy/python/discussions"
+Homepage = "https://mkdocstrings.github.io/python"
+Documentation = "https://mkdocstrings.github.io/python"
+Changelog = "https://mkdocstrings.github.io/python/changelog"
+Repository = "https://github.com/mkdocstrings/python"
+Issues = "https://github.com/mkdocstrings/python/issues"
+Discussions = "https://github.com/mkdocstrings/python/discussions"
Gitter = "https://gitter.im/python/community"
-Funding = "https://github.com/sponsors/pawamoy"
-
-[project.scripts]
-mkdocstrings-python = "mkdocstrings.cli:main"
+Funding = "https://github.com/sponsors/mkdocstrings"
[tool.pdm]
-package-dir = "src"
+version = {use_scm = true}
+includes = ["src/mkdocstrings"]
[tool.pdm.dev-dependencies]
-duty = ["duty~=0.6"]
+duty = ["duty~=0.7"]
docs = [
- "mkdocs~=1.1; python_version >= '3.7'",
- "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'",
- "mkdocstrings~=0.15; python_version >= '3.7'",
- "toml~=0.10; python_version >= '3.7'",
+ "mkdocs~=1.2",
+ "mkdocs-coverage~=0.2",
+ "mkdocs-gen-files~=0.3",
+ "mkdocs-literate-nav~=0.4",
+ "mkdocs-material~=8.0",
+ "mkdocstrings~=0.16",
+ "toml~=0.10",
]
format = [
"autoflake~=1.4",
- "black~=20.8b1",
- "isort~=5.8",
+ "black~=21.10b0",
+ "isort~=5.10",
]
maintain = [
# TODO: remove this section when git-changelog is more powerful
"git-changelog~=0.4",
]
quality = [
- "darglint~=1.7",
+ "darglint~=1.8",
"flake8-bandit~=2.1",
"flake8-black~=0.2",
- "flake8-bugbear~=21.3",
+ "flake8-bugbear~=21.9",
"flake8-builtins~=1.5",
- "flake8-comprehensions~=3.4",
+ "flake8-comprehensions~=3.7",
"flake8-docstrings~=1.6",
- "flake8-pytest-style~=1.4",
+ "flake8-pytest-style~=1.5",
"flake8-string-format~=0.3",
- "flake8-tidy-imports~=4.2",
+ "flake8-tidy-imports~=4.5",
"flake8-variables-names~=0.0",
- "pep8-naming~=0.11",
+ "pep8-naming~=0.12",
"wps-light~=0.15",
]
tests = [
"pytest~=6.2",
- "pytest-cov~=2.11",
- "pytest-randomly~=3.6",
+ "pytest-cov~=3.0",
+ "pytest-randomly~=3.10",
"pytest-sugar~=0.9",
- "pytest-xdist~=2.2",
+ "pytest-xdist~=2.4",
+]
+typing = [
+ "mypy~=0.910",
+ "types-markdown~=3.3",
+ "types-toml~=0.10",
]
-typing = ["mypy~=0.812"]
+security = ["safety~=1.10"]
[tool.black]
line-length = 120
diff --git a/scripts/multirun.sh b/scripts/multirun.sh
index 7b5d9cf2..fb5fb2e5 100755
--- a/scripts/multirun.sh
+++ b/scripts/multirun.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e
-PYTHON_VERSIONS="${PYTHON_VERSIONS-3.6 3.7 3.8 3.9 3.10 3.11}"
+PYTHON_VERSIONS="${PYTHON_VERSIONS-3.8 3.9 3.10 3.11}"
if [ -n "${PYTHON_VERSIONS}" ]; then
for python_version in ${PYTHON_VERSIONS}; do
diff --git a/scripts/setup.sh b/scripts/setup.sh
index c5df7b4c..19227388 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 3.10 3.11}"
+PYTHON_VERSIONS="${PYTHON_VERSIONS-3.8 3.9 3.10 3.11}"
install_with_pipx() {
if ! command -v "$1" &>/dev/null; then
diff --git a/src/mkdocstrings/__init__.py b/src/mkdocstrings/__init__.py
deleted file mode 100644
index 1e1fb619..00000000
--- a/src/mkdocstrings/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""
-Python for mkdocstrings package.
-
-A Python handler for mkdocstrings.
-"""
-
-from typing import List
-
-__all__: List[str] = [] # noqa: WPS410 (the only __variable__ we use)
diff --git a/src/mkdocstrings/__main__.py b/src/mkdocstrings/__main__.py
deleted file mode 100644
index 53c0db45..00000000
--- a/src/mkdocstrings/__main__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""
-Entry-point module, in case you use `python -m mkdocstrings`.
-
-Why does this file exist, and why `__main__`? For more info, read:
-
-- https://www.python.org/dev/peps/pep-0338/
-- https://docs.python.org/3/using/cmdline.html#cmdoption-m
-"""
-
-import sys
-
-from mkdocstrings.cli import main
-
-if __name__ == "__main__":
- sys.exit(main(sys.argv[1:]))
diff --git a/src/mkdocstrings/cli.py b/src/mkdocstrings/cli.py
deleted file mode 100644
index d4089fb2..00000000
--- a/src/mkdocstrings/cli.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Why does this file exist, and why not put this in `__main__`?
-#
-# You might be tempted to import things from `__main__` later,
-# but that will cause problems: the code will get executed twice:
-#
-# - When you run `python -m mkdocstrings` python will execute
-# `__main__.py` as a script. That means there won't be any
-# `mkdocstrings.__main__` in `sys.modules`.
-# - When you import `__main__` it will get executed again (as a module) because
-# there's no `mkdocstrings.__main__` in `sys.modules`.
-
-"""Module that contains the command line application."""
-
-import argparse
-from typing import List, Optional
-
-
-def get_parser() -> argparse.ArgumentParser:
- """
- Return the CLI argument parser.
-
- Returns:
- An argparse parser.
- """
- return argparse.ArgumentParser(prog="mkdocstrings-python")
-
-
-def main(args: Optional[List[str]] = None) -> int:
- """
- Run the main program.
-
- This function is executed when you type `mkdocstrings-python` or `python -m mkdocstrings`.
-
- Arguments:
- args: Arguments passed from the command line.
-
- Returns:
- An exit code.
- """
- parser = get_parser()
- opts = parser.parse_args(args=args)
- print(opts) # noqa: WPS421 (side-effect in main is fine)
- return 0
diff --git a/src/mkdocstrings/handlers/python/__init__.py b/src/mkdocstrings/handlers/python/__init__.py
new file mode 100644
index 00000000..5d289836
--- /dev/null
+++ b/src/mkdocstrings/handlers/python/__init__.py
@@ -0,0 +1,73 @@
+"""This module implements a handler for the Python language."""
+
+import posixpath
+from typing import Any, BinaryIO, Iterator, Optional, Tuple
+
+from mkdocstrings.handlers.base import BaseHandler
+from mkdocstrings.handlers.python.collector import PythonCollector
+from mkdocstrings.handlers.python.renderer import PythonRenderer
+from mkdocstrings.inventory import Inventory
+from mkdocstrings.loggers import get_logger
+
+log = get_logger(__name__)
+
+
+class PythonHandler(BaseHandler):
+ """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: Any,
+ ) -> 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(
+ theme: str, # noqa: W0613 (unused argument config)
+ custom_templates: Optional[str] = None,
+ **config: Any,
+) -> PythonHandler:
+ """Simply return an instance of `PythonHandler`.
+
+ Arguments:
+ theme: The theme to use when rendering contents.
+ custom_templates: Directory containing custom templates.
+ **config: Configuration passed to the handler.
+
+ Returns:
+ An instance of `PythonHandler`.
+ """
+ return PythonHandler(
+ collector=PythonCollector(),
+ renderer=PythonRenderer("python", theme, custom_templates),
+ )
diff --git a/src/mkdocstrings/handlers/python/collector.py b/src/mkdocstrings/handlers/python/collector.py
new file mode 100644
index 00000000..ac0b6846
--- /dev/null
+++ b/src/mkdocstrings/handlers/python/collector.py
@@ -0,0 +1,81 @@
+"""This module implements a collector for the Python language.
+
+It collects data with [Griffe](https://github.com/pawamoy/griffe).
+"""
+
+from collections import ChainMap
+
+from griffe import logger as griffe_logger
+
+from mkdocstrings.handlers.base import BaseCollector, CollectionError, CollectorItem
+from mkdocstrings.loggers import get_logger
+
+griffe_logger.get_logger = get_logger # patch logger to blend in MkDocs logs
+from griffe.agents.extensions import load_extensions # noqa: E402
+from griffe.collections import LinesCollection, ModulesCollection # noqa: E402
+from griffe.docstrings.parsers import Parser # noqa: E402
+from griffe.loader import GriffeLoader # noqa: E402
+
+logger = get_logger(__name__)
+
+
+class PythonCollector(BaseCollector):
+ """The class responsible for loading Jinja templates and rendering them.
+
+ It defines some configuration options, implements the `render` method,
+ and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer].
+ """
+
+ default_config: dict = {"docstring_style": "google", "docstring_options": {}}
+ """The default selection options.
+
+ Option | Type | Description | Default
+ ------ | ---- | ----------- | -------
+ **`docstring_style`** | `"google" | "numpy" | "rst" | None` | The docstring style to use. | `"google"`
+ **`docstring_options`** | dict[str, Any] | The options for the docstring parser. | `{}`
+ """
+
+ def __init__(self) -> None:
+ """Initialize the object."""
+ self._modules_collection: ModulesCollection = ModulesCollection()
+ self._lines_collection: LinesCollection = LinesCollection()
+
+ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: WPS231
+ """Collect the documentation tree given an identifier and selection options.
+
+ Arguments:
+ identifier: The dotted-path of a Python object available in the Python path.
+ config: Selection options, used to alter the data collection done by `pytkdocs`.
+
+ Raises:
+ CollectionError: When there was a problem collecting the object documentation.
+
+ Returns:
+ The collected object-tree.
+ """
+ final_config = ChainMap(config, self.default_config)
+
+ module_name = identifier.split(".", 1)[0]
+ if module_name not in self._modules_collection:
+ loader = GriffeLoader(
+ extensions=load_extensions(final_config.get("extensions", [])),
+ docstring_parser=Parser(final_config["docstring_style"]),
+ docstring_options=final_config["docstring_options"],
+ modules_collection=self._modules_collection,
+ lines_collection=self._lines_collection,
+ )
+ try:
+ module = loader.load_module(module_name)
+ except ModuleNotFoundError as error:
+ raise CollectionError from error
+
+ for _ in range(5):
+ if loader.follow_aliases(module):
+ break
+ else:
+ logger.warning("some aliases could not be resolved")
+
+ try:
+ return self._modules_collection[identifier]
+ except KeyError as error: # noqa: WPS440
+ raise CollectionError from error
diff --git a/src/mkdocstrings/handlers/python/renderer.py b/src/mkdocstrings/handlers/python/renderer.py
new file mode 100644
index 00000000..f68babd8
--- /dev/null
+++ b/src/mkdocstrings/handlers/python/renderer.py
@@ -0,0 +1,210 @@
+"""This module implements a renderer for the Python language."""
+
+from __future__ import annotations
+
+import enum
+import re
+import sys
+from collections import ChainMap
+from typing import Any, Sequence
+
+from griffe.dataclasses import Alias, Object
+from markdown import Markdown
+from markupsafe import Markup
+
+from mkdocstrings.extension import PluginError
+from mkdocstrings.handlers.base import BaseRenderer, CollectorItem
+from mkdocstrings.loggers import get_logger
+
+log = get_logger(__name__)
+
+# TODO: CSS classes everywhere in templates
+# TODO: name normalization (filenames, Jinja2 variables, HTML tags, CSS classes)
+# TODO: Jinja2 blocks everywhere in templates
+
+
+class Order(enum.Enum):
+ """Enumeration for the possible members ordering."""
+
+ alphabetical = "alphabetical"
+ source = "source"
+
+
+def _sort_key_alphabetical(item: CollectorItem) -> Any:
+ # chr(sys.maxunicode) is a string that contains the final unicode
+ # character, so if 'name' isn't found on the object, the item will go to
+ # the end of the list.
+ return item.name or chr(sys.maxunicode)
+
+
+def _sort_key_source(item: CollectorItem) -> Any:
+ # if 'lineno' is none, the item will go to the start of the list.
+ return item.lineno if item.lineno is not None else -1
+
+
+order_map = {
+ Order.alphabetical: _sort_key_alphabetical,
+ Order.source: _sort_key_source,
+}
+
+
+class PythonRenderer(BaseRenderer):
+ """The class responsible for loading Jinja templates and rendering them.
+
+ It defines some configuration options, implements the `render` method,
+ and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer].
+
+ Attributes:
+ fallback_theme: The theme to fallback to.
+ default_config: The default rendering options,
+ see [`default_config`][mkdocstrings.handlers.python.PythonRenderer.default_config].
+ """
+
+ fallback_theme = "material"
+
+ default_config: dict = {
+ "show_root_heading": False,
+ "show_root_toc_entry": True,
+ "show_root_full_path": True,
+ "show_root_members_full_path": False,
+ "show_object_full_path": False,
+ "show_category_heading": False,
+ "show_if_no_docstring": False,
+ "show_signature_annotations": False,
+ "separate_signature": False,
+ "line_length": 60,
+ "show_source": True,
+ "show_bases": True,
+ "show_submodules": True,
+ "group_by_category": True,
+ "heading_level": 2,
+ "members_order": Order.alphabetical.value,
+ }
+ """The default rendering options.
+
+ Option | Type | Description | Default
+ ------ | ---- | ----------- | -------
+ **`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False`
+ **`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True`
+ **`show_root_full_path`** | `bool` | Show the full Python path for the root object heading. | `True`
+ **`show_object_full_path`** | `bool` | Show the full Python path of every object. | `False`
+ **`show_root_members_full_path`** | `bool` | Show the full Python path of objects that are children of the root object (for example, classes in a module). When False, `show_object_full_path` overrides. | `False`
+ **`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False`
+ **`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False`
+ **`show_signature_annotations`** | `bool` | Show the type annotations in methods and functions signatures. | `False`
+ **`separate_signature`** | `bool` | Whether to put the whole signature in a foldable code block below the heading. | `False`
+ **`line_length`** | `int` | Maximum line length when formatting code. | `60`
+ **`show_source`** | `bool` | Show the source code of this object. | `True`
+ **`show_bases`** | `bool` | Show the base classes of a class. | `True`
+ **`show_submodules`** | `bool` | When rendering a module, show its submodules recursively. | `True`
+ **`group_by_category`** | `bool` | Group the object's children by categories: attributes, classes, functions, methods, and modules. | `True`
+ **`heading_level`** | `int` | The initial heading level to use. | `2`
+ **`members_order`** | `str` | The members ordering to use. Options: `alphabetical` - order by the members names, `source` - order members as they appear in the source file. | `alphabetical`
+ """ # noqa: E501
+
+ def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring)
+ final_config = ChainMap(config, self.default_config)
+
+ template = self.env.get_template(f"{data.kind.value}.html")
+
+ # Heading level is a "state" variable, that will change at each step
+ # of the rendering recursion. Therefore, it's easier to use it as a plain value
+ # than as an item in a dictionary.
+ heading_level = final_config["heading_level"]
+ try:
+ final_config["members_order"] = Order(final_config["members_order"])
+ except ValueError:
+ choices = "', '".join(item.value for item in Order)
+ raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.")
+
+ return template.render(
+ **{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True},
+ )
+
+ def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring)
+ return list({data.path, data.canonical_path, *data.aliases})
+
+ def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring)
+ super().update_env(md, config)
+ self.env.trim_blocks = True
+ self.env.lstrip_blocks = True
+ self.env.keep_trailing_newline = False
+ self.env.filters["crossref"] = self.do_crossref
+ self.env.filters["multi_crossref"] = self.do_multi_crossref
+ self.env.filters["order_members"] = self.do_order_members
+ self.env.filters["format_signature"] = self.do_format_signature
+
+ def do_format_signature(self, signature: str, line_length: int) -> str:
+ """Format a signature using Black.
+
+ Parameters:
+ signature: The signature to format.
+ line_length: The line length to give to Black.
+
+ Returns:
+ The same code, formatted.
+ """
+ from black import Mode, format_str
+
+ code = signature.strip()
+ if len(code) < line_length:
+ return code
+ mode = Mode(line_length=line_length)
+ formatted = format_str(f"def {code}: pass", mode=mode)
+ # remove starting `def ` and trailing `: pass`
+ return formatted[4:-5].strip()[:-1]
+
+ def do_order_members(self, members: Sequence[Object | Alias], order: Order) -> Sequence[Object | Alias]:
+ """Order members given an ordering method.
+
+ Parameters:
+ members: The members to order.
+ order: The ordering method.
+
+ Returns:
+ The same members, ordered.
+ """
+ return sorted(members, key=order_map[order])
+
+ def do_crossref(self, path: str, brief: bool = True) -> Markup:
+ """Filter to create cross-references.
+
+ Parameters:
+ path: The path to link to.
+ brief: Show only the last part of the path, add full path as hover.
+
+ Returns:
+ Markup text.
+ """
+ full_path = path
+ if brief:
+ path = full_path.split(".")[-1]
+ return Markup("{path}").format(
+ full_path=full_path, path=path
+ )
+
+ def do_multi_crossref(self, text: str, code: bool = True) -> Markup:
+ """Filter to create cross-references.
+
+ Parameters:
+ text: The text to scan.
+ code: Whether to wrap the result in a code tag.
+
+ Returns:
+ Markup text.
+ """
+ group_number = 0
+ variables = {}
+
+ def repl(match): # noqa: WPS430
+ nonlocal group_number # noqa: WPS420
+ group_number += 1
+ path = match.group()
+ path_var = f"path{group_number}"
+ variables[path_var] = path
+ return f"{{{path_var}}}"
+
+ text = re.sub(r"([\w.]+)", repl, text)
+ if code:
+ text = f"{text}"
+ return Markup(text).format(**variables)
diff --git a/src/mkdocstrings/templates/python/material/_base/attribute.html b/src/mkdocstrings/templates/python/material/_base/attribute.html
new file mode 100644
index 00000000..4f8da7e7
--- /dev/null
+++ b/src/mkdocstrings/templates/python/material/_base/attribute.html
@@ -0,0 +1,58 @@
+{{ log.debug() }}
+{% if config.show_if_no_docstring or attribute %}
+
+
+ {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %}
+ {% if config.show_bases and class.bases %}
+ ({% for expression in class.bases -%}
+ {% include "expression.html" with context %}{% if not loop.last %}, {% endif %}
+ {% endfor %})
+ {% endif %}
+
+
+ {% with labels = class.labels %}
+ {% include "labels.html" with context %}
+ {% endwith %}
+
+ {% endfilter %}
+
+ {% else %}
+ {% if config.show_root_toc_entry %}
+ {% filter heading(heading_level,
+ role="class",
+ id=html_id,
+ toc_label=class.path,
+ hidden=True) %}
+ {% endfilter %}
+ {% endif %}
+ {% set heading_level = heading_level - 1 %}
+ {% endif %}
+
+ {{ class.relative_filepath }}{{ section.title or "Attributes:" }}
+| Name | +Type | +Description | +
|---|---|---|
{{ attribute.name }} |
+
+ {% if attribute.annotation %}
+ {% with expression = attribute.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ attribute.description|convert_markdown(heading_level, html_id) }} | +
{{ section.title or "Attributes:" }}
+{% include "expression.html" with context %})
+ {% endwith %}
+ {% endif %}
+ – {{ attribute.description|convert_markdown(heading_level, html_id) }}
+ | ATTRIBUTE | +DESCRIPTION | +
|---|---|
{{ attribute.name }} |
+
+ {{ attribute.description|convert_markdown(heading_level, html_id) }}
+
+ {% if attribute.annotation %}
+
+ TYPE:
+ {% with expression = attribute.annotation %}
+ |
+
{{ section.title or "Examples:" }}
+{% for section_type, sub_section in section.value %} + {% if section_type == "markdown" %} + {{ sub_section|convert_markdown(heading_level, html_id) }} + {% elif section_type == "examples" %} + {{ sub_section|highlight(language="python", linenums=False) }} + {% endif %} +{% endfor %} diff --git a/src/mkdocstrings/templates/python/material/_base/docstring/other_parameters.html b/src/mkdocstrings/templates/python/material/_base/docstring/other_parameters.html new file mode 100644 index 00000000..d31ef7fc --- /dev/null +++ b/src/mkdocstrings/templates/python/material/_base/docstring/other_parameters.html @@ -0,0 +1,72 @@ +{{ log.debug() }} +{% if config.docstring_section_style == "table" %} +{{ section.title or "Other Parameters:" }}
+| Name | +Type | +Description | +
|---|---|---|
{{ parameter.name }} |
+
+ {% if parameter.annotation %}
+ {% with expression = parameter.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ parameter.description|convert_markdown(heading_level, html_id) }} | +
{{ section.title or "Other Parameters:" }}
+{% include "expression.html" with context %})
+ {% endwith %}
+ {% endif %}
+ – {{ parameter.description|convert_markdown(heading_level, html_id) }}
+ | PARAMETER | +DESCRIPTION | +
|---|---|
{{ parameter.name }} |
+
+ {{ parameter.description|convert_markdown(heading_level, html_id) }}
+
+ {% if parameter.annotation %}
+
+ TYPE:
+ {% with expression = parameter.annotation %}
+ |
+
{{ section.title or "Parameters:" }}
+| Name | +Type | +Description | +Default | +
|---|---|---|---|
{{ parameter.name }} |
+
+ {% if parameter.annotation %}
+ {% with expression = parameter.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ parameter.description|convert_markdown(heading_level, html_id) }} | +
+ {% if parameter.default %}
+ {% with expression = parameter.default %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% else %}
+ required
+ {% endif %}
+ |
+
{{ section.title or "Parameters:" }}
+{% include "expression.html" with context %})
+ {% endwith %}
+ {% endif %}
+ – {{ parameter.description|convert_markdown(heading_level, html_id) }}
+ | PARAMETER | +DESCRIPTION | +
|---|---|
{{ parameter.name }} |
+
+ {{ parameter.description|convert_markdown(heading_level, html_id) }}
+
+ {% if parameter.annotation %}
+
+ TYPE:
+ {% with expression = parameter.annotation %}
+ |
+
{{ section.title or "Raises:" }}
+| Type | +Description | +
|---|---|
+ {% if raises.annotation %}
+ {% with expression = raises.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ raises.description|convert_markdown(heading_level, html_id) }} | +
{{ section.title or "Raises:" }}
+{% include "expression.html" with context %}
+ {% endwith %}
+ –
+ {% endif %}
+ {{ raises.description|convert_markdown(heading_level, html_id) }}
+ | RAISES | +DESCRIPTION | +
|---|---|
{{ raises.name }} |
+
+ {{ raises.description|convert_markdown(heading_level, html_id) }}
+
+ {% if raises.annotation %}
+
+ TYPE:
+ {% with expression = raises.annotation %}
+ |
+
{{ section.title or "Receives:" }}
+| Type | +Description | +
|---|---|
+ {% if receives.annotation %}
+ {% with expression = receives.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ receives.description|convert_markdown(heading_level, html_id) }} | + {% endwith %} +
{{ section.title or "Receives:" }}
+{% include "expression.html" with context %}
+ {% endwith %}
+ –
+ {% endif %}
+ {{ receives.description|convert_markdown(heading_level, html_id) }}
+ | RECEIVES | +DESCRIPTION | +
|---|---|
{{ receive.name }} |
+
+ {{ receive.description|convert_markdown(heading_level, html_id) }}
+
+ {% if receive.annotation %}
+
+ TYPE:
+ {% with expression = receive.annotation %}
+ |
+
{{ section.title or "Returns:" }}
+| Name | {% endif %} +Type | +Description | +
|---|---|---|
{% if returns.name %}{{ returns.name }}{% endif %} | {% endif %}
+
+ {% if returns.annotation %}
+ {% with expression = returns.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ returns.description|convert_markdown(heading_level, html_id) }} | +
{{ section.title or "Returns:" }}
+{% include "expression.html" with context %}
+ {% if returns.name %}){% endif %}
+ {% endwith %}
+ {% endif %}
+ – {{ returns.description|convert_markdown(heading_level, html_id) }}
+ | RETURNS |
|
+ {{ returns.description|convert_markdown(heading_level, html_id) }}
+
+ TYPE:
+ {% with expression = returns.annotation %}
+ |
+
{{ section.title or "Warns:" }}
+| Type | +Description | +
|---|---|
+ {% if warns.annotation %}
+ {% with expression = warns.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ warns.description|convert_markdown(heading_level, html_id) }} | +
{{ section.title or "Warns:" }}
+{% include "expression.html" with context %}
+ {% endwith %}
+ –
+ {% endif %}
+ {{ warns.description|convert_markdown(heading_level, html_id) }}
+ | WARNS | +DESCRIPTION | +
|---|---|
{{ warns.name }} |
+
+ {{ warns.description|convert_markdown(heading_level, html_id) }}
+
+ {% if warns.annotation %}
+
+ TYPE:
+ {% with expression = warns.annotation %}
+ |
+
{{ section.title or "Yields:" }}
+| Type | +Description | +
|---|---|
+ {% if yields.annotation %}
+ {% with expression = yields.annotation %}
+ {% include "expression.html" with context %}
+ {% endwith %}
+ {% endif %}
+ |
+ {{ yields.description|convert_markdown(heading_level, html_id) }} | + {% endwith %} +
{{ section.title or "Yields:" }}
+{% include "expression.html" with context %}
+ {% endwith %}
+ –
+ {% endif %}
+ {{ yields.description|convert_markdown(heading_level, html_id) }}
+ | YIELDS | +DESCRIPTION | +
|---|---|
{{ yields.name }} |
+
+ {{ yields.description|convert_markdown(heading_level, html_id) }}
+
+ {% if yields.annotation %}
+
+ TYPE:
+ {% with expression = yields.annotation %}
+ |
+
{{ function.relative_filepath }}{{ property }}
+ {% endfor %}
+
+{% endif %}
diff --git a/src/mkdocstrings/templates/python/material/_base/module.html b/src/mkdocstrings/templates/python/material/_base/module.html
new file mode 100644
index 00000000..ad0cef4f
--- /dev/null
+++ b/src/mkdocstrings/templates/python/material/_base/module.html
@@ -0,0 +1,60 @@
+{{ log.debug() }}
+{% if config.show_if_no_docstring or module %}
+
+ {% if show_full_path %}{{ module.path }}{% else %}{{ module.name }}{% endif %}
+
+ {% with labels = module.labels %}
+ {% include "labels.html" with context %}
+ {% endwith %}
+
+ {% endfilter %}
+
+ {% else %}
+ {% if config.show_root_toc_entry %}
+ {% filter heading(heading_level,
+ role="module",
+ id=html_id,
+ toc_label=module.path,
+ hidden=True) %}
+ {% endfilter %}
+ {% endif %}
+ {% set heading_level = heading_level - 1 %}
+ {% endif %}
+
+ | Exceptions: | +
+
|
+
|---|
| Keyword arguments: | +
+
|
+
|---|
| Parameters: | +
+
|
+
|---|
| Returns: | +
+
|
+
|---|
| Yields: | +
+
|
+
|---|