diff --git a/.copier-answers.yml b/.copier-answers.yml index f844b711..7ad1c140 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.4.3 +_commit: 0.7.1 _src_path: gh:pawamoy/copier-pdm.git author_email: pawamoy@pm.me author_fullname: "Timoth\xE9e Mazzucotelli" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5750cd7..019502b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,9 +27,9 @@ jobs: uses: actions/checkout@v2 - name: Set up PDM - uses: pdm-project/setup-pdm@v2 + uses: pdm-project/setup-pdm@v2.5 with: - python-version: 3.8 + python-version: "3.8" - name: Set cache variables id: set_variables @@ -49,15 +49,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 @@ -69,8 +67,17 @@ jobs: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - "3.6" + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11-dev" runs-on: ${{ matrix.os }} @@ -79,7 +86,7 @@ jobs: uses: actions/checkout@v2 - name: Set up PDM - uses: pdm-project/setup-pdm@v2 + uses: pdm-project/setup-pdm@v2.5 with: python-version: ${{ matrix.python-version }} 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 0f1c2b98..c5593cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ 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.17.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.17.0) - 2021-12-27 + +[Compare with 0.16.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.16.2...0.17.0) + +### Features +- Add `show_signature` rendering option ([024ee82](https://github.com/mkdocstrings/mkdocstrings/commit/024ee826bb6f0aa297ba857bc18075d6f4162cad) by Will Da Silva). [Issue #341](https://github.com/mkdocstrings/mkdocstrings/issues/341), [PR #342](https://github.com/mkdocstrings/mkdocstrings/pull/342) +- Support Keyword Args and Yields sections ([1286427](https://github.com/mkdocstrings/mkdocstrings/commit/12864271b7f997af7b421a834919b1e686793905) by Timothée Mazzucotelli). [Issue #205](https://github.com/mkdocstrings/mkdocstrings/issues/205) and [#324](https://github.com/mkdocstrings/mkdocstrings/issues/324), [PR #331](https://github.com/mkdocstrings/mkdocstrings/pull/331) + +### Bug Fixes +- Do minimum work when falling back to re-collecting an object to get its anchor ([f6cf570](https://github.com/mkdocstrings/mkdocstrings/commit/f6cf570255df17db1088b6e6cd94bcc823b3b17f) by Timothée Mazzucotelli). [Issue #329](https://github.com/mkdocstrings/mkdocstrings/issues/329), [PR #330](https://github.com/mkdocstrings/mkdocstrings/pull/330) + +### Code Refactoring +- Return multiple identifiers from fallback method ([78c498c](https://github.com/mkdocstrings/mkdocstrings/commit/78c498c4a6cfc33cc6ceab9829426bd64e518d44) by Timothée Mazzucotelli). [Issue mkdocstrings/autorefs#11](https://github.com/mkdocstrings/autorefs/issues/11), [PR #350](https://github.com/mkdocstrings/mkdocstrings/pull/350) + + ## [0.16.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.16.2) - 2021-10-04 [Compare with 0.16.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.16.1...0.16.2) 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 cb207ff0..d914dc4f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://mkdocstrings.github.io/) [![pypi version](https://img.shields.io/pypi/v/mkdocstrings.svg)](https://pypi.org/project/mkdocstrings/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocstrings)](https://anaconda.org/conda-forge/mkdocstrings) +[![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/mkdocstrings) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://gitter.im/mkdocstrings/community) Automatic documentation from sources, for [MkDocs](https://mkdocs.org/). @@ -80,7 +81,7 @@ Automatic documentation from sources, for [MkDocs](https://mkdocs.org/). 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. + to their [admonition](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) 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 @@ -91,10 +92,6 @@ Automatic documentation from sources, for [MkDocs](https://mkdocs.org/). - **Source code display:** *mkdocstrings* can add a collapsible div containing the highlighted source code of the Python object. -## Roadmap - -See the [Feature Roadmap issue](https://github.com/mkdocstrings/mkdocstrings/issues/183) on the bugtracker. - ## Requirements *mkdocstrings* requires Python 3.6 or above. @@ -119,13 +116,6 @@ pyenv global system 3.6.12 ``` -This project currently only works with the Material theme of MkDocs. -Therefore, it is required that you have it installed. - -``` -pip install mkdocs-material -``` - ## Installation With `pip`: diff --git a/config/flake8.ini b/config/flake8.ini index 2b50d854..e0a4cfbd 100644 --- a/config/flake8.ini +++ b/config/flake8.ini @@ -9,6 +9,10 @@ ignore = A001 # missing docstring in magic method 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) diff --git a/config/mypy.ini b/config/mypy.ini index e88e9042..814e2ac8 100644 --- a/config/mypy.ini +++ b/config/mypy.ini @@ -1,15 +1,5 @@ [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 +warn_unused_ignores = true +show_error_codes = true diff --git a/duties.py b/duties.py index 2dcc706c..879445e2 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 @@ -103,7 +106,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! @@ -113,7 +116,7 @@ def check(ctx): @duty -def check_code_quality(ctx, files=PY_SRC): +def check_quality(ctx, files=PY_SRC): """Check the code quality. Arguments: @@ -131,22 +134,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): """Decorate a duty that builds docs to warn that it's not possible on Python 3.6. @@ -184,14 +201,56 @@ 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, progress=True) + # 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) @@ -282,7 +341,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) @@ -305,16 +364,6 @@ def test(ctx, match: str = ""): ctx: The context instance (passed automatically). match: A pytest expression to filter selected tests. """ - 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( diff --git a/pyproject.toml b/pyproject.toml index 5e3fa81c..ac0bd9de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,27 +4,40 @@ build-backend = "pdm.pep517.api" [project] name = "mkdocstrings" -version = {use_scm = true} description = "Automatic documentation from sources, for MkDocs." authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}] license = {file = "LICENSE"} readme = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.6.2" keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"] -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.6", + "Programming Language :: Python :: 3.7", + "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 = [ - "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,<10.0", - "pytkdocs>=0.2.0,<0.13.0", + "Jinja2>=2.11.1", + "Markdown>=3.3", + "MarkupSafe>=1.1", + "mkdocs>=1.2", + "mkdocs-autorefs>=0.1", + "pymdown-extensions>=6.3", + "pytkdocs>=0.14.0", ] [project.urls] @@ -40,56 +53,64 @@ Funding = "https://github.com/sponsors/mkdocstrings" [project.entry-points."mkdocs.plugins"] mkdocstrings = "mkdocstrings.plugin:MkdocstringsPlugin" - -[project.optional-dependencies] [tool.pdm] package-dir = "src" includes = ["src/mkdocstrings"] +version = {use_scm = true} [tool.pdm.dev-dependencies] -duty = ["duty~=0.6"] +duty = ["duty>=0.7"] 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'", + "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.3; 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", + "autoflake>=1.4", + "black>=21.10b0", + "isort>=5.10", ] maintain = [ # TODO: remove this section when git-changelog is more powerful - "git-changelog~=0.4", + "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", - "curlylint~=0.13", + "darglint>=1.8", + "flake8-bandit>=2.1", + "flake8-black>=0.2", + "flake8-bugbear>=21.9", + "flake8-builtins>=1.5", + "flake8-comprehensions>=3.7", + "flake8-docstrings>=1.6", + "flake8-pytest-style>=1.5", + "flake8-string-format>=0.3", + "flake8-tidy-imports>=4.5", + "flake8-variables-names>=0.0", + "pep8-naming>=0.12", + "wps-light>=0.15", + "curlylint>=0.13", ] 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", + "docutils", + "pygments>=2.10", # python 3.6 + "pytest>=6.2", + "pytest-cov>=3.0", + "pytest-randomly>=3.10", + "pytest-sugar>=0.9", + "pytest-xdist>=2.4", + "sphinx", +] +typing = [ + "mypy>=0.910", + "types-docutils", + "types-markdown>=3.3", + "types-pyyaml", + "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 4ca6e2ce..7b5d9cf2 100755 --- a/scripts/multirun.sh +++ b/scripts/multirun.sh @@ -1,15 +1,15 @@ #!/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 3.10 3.11}" if [ -n "${PYTHON_VERSIONS}" ]; then for python_version in ${PYTHON_VERSIONS}; do - if pdm use -f "${python_version}" &>/dev/null; then + if pdm use -f "python${python_version}" &>/dev/null; then echo "> pdm run $@ (Python ${python_version})" pdm run "$@" else - echo "> pdm use -f ${python_version}: Python version not available?" >&2 + echo "> pdm use -f python${python_version}: Python interpreter not available?" >&2 fi done else diff --git a/scripts/setup.sh b/scripts/setup.sh index cfddbac7..c5df7b4c 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 3.10 3.11}" install_with_pipx() { if ! command -v "$1" &>/dev/null; then @@ -16,11 +16,11 @@ install_with_pipx pdm if [ -n "${PYTHON_VERSIONS}" ]; then for python_version in ${PYTHON_VERSIONS}; do - if pdm use -f "${python_version}" &>/dev/null; then - echo "> Using Python ${python_version} environment" + if pdm use -f "python${python_version}" &>/dev/null; then + echo "> Using Python ${python_version} interpreter" pdm install else - echo "> pdm use -f ${python_version}: Python version not available?" >&2 + echo "> pdm use -f python${python_version}: Python interpreter not available?" >&2 fi done else diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index afe59708..d8aae2e5 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, Tuple +from typing import Mapping, MutableMapping, MutableSequence, Tuple from xml.etree.ElementTree import Element import yaml @@ -79,7 +79,7 @@ def __init__( self._autorefs = autorefs self._updated_env = False - def test(self, parent: Element, block: str) -> bool: # type: ignore + def test(self, parent: Element, block: str) -> bool: """Match our autodoc instructions. Arguments: @@ -108,7 +108,7 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: if match.start() > 0: self.parser.parseBlocks(parent, [block[: match.start()]]) # removes the first line - block = block[match.end() :] # type: ignore + block = block[match.end() :] block, the_rest = self.detab(block) @@ -117,7 +117,7 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: heading_level = match["heading"].count("#") log.debug(f"Matched '::: {identifier}'") - html, handler = self._process_block(identifier, block, heading_level) + html, handler, data = 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) @@ -125,17 +125,20 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: headings = handler.renderer.get_headings() el.extend(headings) - for heading in headings: - page = self._autorefs.current_page - anchor = heading.attrib["id"] + page = self._autorefs.current_page + for anchor in handler.renderer.get_anchors(data): self._autorefs.register_anchor(page, anchor) + for heading in headings: + anchor = heading.attrib["id"] # noqa: WPS440 + self._autorefs.register_anchor(page, anchor) # noqa: WPS441 + if "data-role" in heading.attrib: self._handlers.inventory.register( - name=anchor, + name=anchor, # noqa: WPS441 domain=handler.domain, role=heading.attrib["data-role"], - uri=f"{page}#{anchor}", + uri=f"{page}#{anchor}", # noqa: WPS441 ) parent.append(el) @@ -146,7 +149,12 @@ 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, BaseHandler]: + def _process_block( + self, + identifier: str, + yaml_block: str, + heading_level: int = 0, + ) -> Tuple[str, BaseHandler, CollectorItem]: """Process an autodoc block. Arguments: @@ -159,7 +167,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 handler that was used. + Rendered HTML, the handler that was used, and the collected item. """ config = yaml.safe_load(yaml_block) or {} handler_name = self._handlers.get_handler_name(config) @@ -196,10 +204,10 @@ def _process_block(self, identifier: str, yaml_block: str, heading_level: int = ) raise - return (rendered, handler) + return rendered, handler, data -def get_item_configs(handler_config: dict, config: dict) -> Tuple[Mapping, Mapping]: +def get_item_configs(handler_config: dict, config: dict) -> Tuple[Mapping, MutableMapping]: """Get the selection and rendering configuration merged into the global configuration of the given handler. Arguments: diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index f496e7ed..32da5a20 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -104,9 +104,9 @@ def __init__(self, directory: str, theme: str, custom_templates: Optional[str] = self.env = Environment( autoescape=True, - loader=FileSystemLoader(paths), # type: ignore + loader=FileSystemLoader(paths), 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() @@ -125,18 +125,20 @@ def render(self, data: CollectorItem, config: dict) -> str: The rendered template as HTML. """ # noqa: DAR202 (excess return section) - def get_anchor(self, data: CollectorItem) -> Optional[str]: - """Return the canonical identifier (HTML anchor) for a collected item. - - This must match what the renderer would've actually rendered, - e.g. if rendering the item contains `

...` then the return value should be "foo". + def get_anchors(self, data: CollectorItem) -> Sequence[str]: + """Return the possible identifiers (HTML anchors) for a collected item. Arguments: data: The collected data. Returns: - The HTML anchor (without '#') as a string, or None if this item doesn't have an anchor. - """ # noqa: DAR202 (excess return section) + The HTML anchors (without '#'), or an empty tuple if this item doesn't have an anchor. + """ + # TODO: remove this at some point + try: + return (self.get_anchor(data),) # type: ignore + except AttributeError: + return () def do_convert_markdown(self, text: str, heading_level: int, html_id: str = "") -> Markup: """Render Markdown text; for use inside templates. @@ -329,23 +331,24 @@ def __init__(self, config: dict) -> None: self._handlers: Dict[str, BaseHandler] = {} self.inventory: Inventory = Inventory(project=self._config["site_name"]) - def get_anchor(self, identifier: str) -> Optional[str]: + def get_anchors(self, identifier: str) -> Sequence[str]: """Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it. Arguments: identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseCollector.collect] can accept). Returns: - A string - anchor without '#', or None if there isn't any identifier familiar with it. + A tuple of strings - anchors without '#', or an empty tuple if there isn't any identifier familiar with it. """ for handler in self._handlers.values(): + fallback_config = getattr(handler.collector, "fallback_config", {}) try: - anchor = handler.renderer.get_anchor(handler.collector.collect(identifier, {})) + anchors = handler.renderer.get_anchors(handler.collector.collect(identifier, fallback_config)) except CollectionError: continue - if anchor is not None: - return anchor - return None + if anchors: + return anchors + return () def get_handler_name(self, config: dict) -> str: """Return the handler name defined in an "autodoc" instruction YAML configuration, or the global default handler. @@ -395,11 +398,11 @@ 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( # type: ignore + self._handlers[name] = module.get_handler( self._config["theme_name"], self._config["mkdocstrings"]["custom_templates"], **handler_config, - ) # type: ignore + ) return self._handlers[name] @property diff --git a/src/mkdocstrings/handlers/python.py b/src/mkdocstrings/handlers/python.py index 0aab09ac..0f85a6de 100644 --- a/src/mkdocstrings/handlers/python.py +++ b/src/mkdocstrings/handlers/python.py @@ -10,7 +10,7 @@ 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, BinaryIO, Callable, Iterator, List, Optional, Tuple +from typing import Any, BinaryIO, Callable, Iterator, List, Optional, Sequence, Tuple from markdown import Markdown from markupsafe import Markup @@ -45,6 +45,7 @@ class PythonRenderer(BaseRenderer): "show_object_full_path": False, "show_category_heading": False, "show_if_no_docstring": False, + "show_signature": True, "show_signature_annotations": False, "show_source": True, "show_bases": True, @@ -63,7 +64,8 @@ class PythonRenderer(BaseRenderer): **`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` + **`show_signature`** | `bool` | Show method and function signatures. | `True` + **`show_signature_annotations`** | `bool` | Show the type annotations in method and function 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` @@ -95,8 +97,11 @@ def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignor **{"config": final_config, data["category"]: data, "heading_level": heading_level, "root": True}, ) - def get_anchor(self, data: CollectorItem) -> str: # noqa: D102 (ignore missing docstring) - return data.get("path") + def get_anchors(self, data: CollectorItem) -> Sequence[str]: # noqa: D102 (ignore missing docstring) + try: + return (data["path"],) + except KeyError: + return () def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring) super().update_env(md, config) @@ -141,6 +146,21 @@ class PythonCollector(BaseCollector): Obviously one could use a single filter instead: `"!^_[^_]"`, which is the default. """ + fallback_config = {"docstring_style": "markdown", "filters": ["!.*"]} + """The configuration used when falling back to re-collecting an object to get its anchor. + + This configuration is used in [`Handlers.get_anchors`][mkdocstrings.handlers.base.Handlers.get_anchors]. + + When trying to fix (optional) cross-references, the autorefs plugin will try to collect + an object with every configured handler until one succeeds. It will then try to get + an anchor for it. It's because objects can have multiple identifiers (aliases), + for example their definition path and multiple import paths in Python. + + When re-collecting the object, we have no use for its members, or for its docstring being parsed. + This is why the fallback configuration filters every member out, and uses the Markdown style, + which we know will not generate any warnings. + """ + def __init__(self, setup_commands: Optional[List[str]] = None) -> None: """Initialize the object. diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index e7ff9802..c28beb00 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -63,7 +63,7 @@ def __init__(self, md: Markdown): md: The Markdown instance to read configs from. """ config: Dict[str, Any] = {} - for ext in md.registeredExtensions: # type: ignore + for ext in md.registeredExtensions: if isinstance(ext, HighlightExtension) and (ext.enabled or not config): config = ext.getConfigs() break # This one takes priority, no need to continue looking diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 4e693473..242d5931 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -183,7 +183,7 @@ 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_anchors mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs) config["markdown_extensions"].append(mkdocstrings_extension) diff --git a/src/mkdocstrings/templates/python/material/docstring.html b/src/mkdocstrings/templates/python/material/docstring.html index 0c32c01d..f1f179b3 100644 --- a/src/mkdocstrings/templates/python/material/docstring.html +++ b/src/mkdocstrings/templates/python/material/docstring.html @@ -11,10 +11,18 @@ {% with parameters = section.value %} {% include "parameters.html" with context %} {% endwith %} + {% elif section.type == "keyword_args" %} + {% with kwargs = section.value %} + {% include "keyword_args.html" with context %} + {% endwith %} {% elif section.type == "exceptions" %} {% with exceptions = section.value %} {% include "exceptions.html" with context %} {% endwith %} + {% elif section.type == "yield" %} + {% with yield = section.value %} + {% include "yield.html" with context %} + {% endwith %} {% elif section.type == "return" %} {% with return = section.value %} {% include "return.html" with context %} diff --git a/src/mkdocstrings/templates/python/material/keyword_args.html b/src/mkdocstrings/templates/python/material/keyword_args.html new file mode 100644 index 00000000..3396ffa5 --- /dev/null +++ b/src/mkdocstrings/templates/python/material/keyword_args.html @@ -0,0 +1,20 @@ +{{ log.debug() }} +

Keyword arguments:

+ + + + + + + + + + {% for kwarg in kwargs %} + + + + + + {% endfor %} + +
NameTypeDescription
{{ kwarg.name }}{% if kwarg.annotation %}{{ kwarg.annotation }}{% endif %}{{ kwarg.description|convert_markdown(heading_level, html_id) }}
diff --git a/src/mkdocstrings/templates/python/material/signature.html b/src/mkdocstrings/templates/python/material/signature.html index e1f815da..90cc4180 100644 --- a/src/mkdocstrings/templates/python/material/signature.html +++ b/src/mkdocstrings/templates/python/material/signature.html @@ -1,5 +1,5 @@ {{ log.debug() }} -{%- if signature -%} +{%- if signature and config.show_signature -%} {%- with -%} {%- set ns = namespace(render_pos_only_separator=True, render_kw_only_separator=True, equal="=") -%} diff --git a/src/mkdocstrings/templates/python/material/yield.html b/src/mkdocstrings/templates/python/material/yield.html new file mode 100644 index 00000000..d8a450ca --- /dev/null +++ b/src/mkdocstrings/templates/python/material/yield.html @@ -0,0 +1,16 @@ +{{ log.debug() }} +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
{% if yield.annotation %}{{ yield.annotation }}{% endif %}{{ yield.description|convert_markdown(heading_level, html_id) }}
diff --git a/src/mkdocstrings/templates/python/mkdocs/keyword_args.html b/src/mkdocstrings/templates/python/mkdocs/keyword_args.html new file mode 100644 index 00000000..16af0ea0 --- /dev/null +++ b/src/mkdocstrings/templates/python/mkdocs/keyword_args.html @@ -0,0 +1,7 @@ +{{ log.debug() }} +
+
Keyword arguments:
+ {% for kwarg in kwargs %} +
{{ ("**" + kwarg.name + ":** " + ("`" + kwarg.annotation + "` – " if kwarg.annotation else "") + kwarg.description)|convert_markdown(heading_level, html_id) }}
+ {% endfor %} +
diff --git a/src/mkdocstrings/templates/python/mkdocs/yield.html b/src/mkdocstrings/templates/python/mkdocs/yield.html new file mode 100644 index 00000000..dad0f0e9 --- /dev/null +++ b/src/mkdocstrings/templates/python/mkdocs/yield.html @@ -0,0 +1,5 @@ +{{ log.debug() }} +
+
Yields:
+
{{ (("`" + yield.annotation + "` – " if yield.annotation else "") + yield.description)|convert_markdown(heading_level, html_id) }}
+
diff --git a/src/mkdocstrings/templates/python/readthedocs/keyword_args.html b/src/mkdocstrings/templates/python/readthedocs/keyword_args.html new file mode 100644 index 00000000..3c15308e --- /dev/null +++ b/src/mkdocstrings/templates/python/readthedocs/keyword_args.html @@ -0,0 +1,19 @@ +{{ log.debug() }} + + + + + + + + + + + +
Keyword arguments: +
    + {% for kwarg in kwargs %} +
  • {{ ("**" + kwarg.name + "**" + (" (`" + kwarg.annotation + "`)" if kwarg.annotation else "") + " – " + kwarg.description)|convert_markdown(heading_level, html_id) }}
  • + {% endfor %} +
+
diff --git a/src/mkdocstrings/templates/python/readthedocs/yield.html b/src/mkdocstrings/templates/python/readthedocs/yield.html new file mode 100644 index 00000000..7591f62c --- /dev/null +++ b/src/mkdocstrings/templates/python/readthedocs/yield.html @@ -0,0 +1,17 @@ +{{ log.debug() }} + + + + + + + + + + + +
Yields: +
    +
  • {{ ((("`" + yield.annotation + "` – ") if yield.annotation else "") + yield.description)|convert_markdown(heading_level, html_id) }}
  • +
+