From 2efcf778f444cb1ebca87c54c6bc5834487f47ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 5 Feb 2025 15:33:25 +0100 Subject: [PATCH 1/5] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/workflows/ci.yml | 34 ++++-- .github/workflows/release.yml | 17 +-- .gitignore | 1 + .gitpod.dockerfile | 6 - .gitpod.yml | 13 --- CONTRIBUTING.md | 5 +- README.md | 10 -- config/ruff.toml | 2 +- devdeps.txt | 32 ------ duties.py | 7 +- mkdocs.yml | 7 +- pyproject.toml | 50 ++++++-- scripts/gen_credits.py | 12 +- scripts/get_version.py | 27 +++++ scripts/make | 211 +--------------------------------- scripts/make.py | 191 ++++++++++++++++++++++++++++++ 17 files changed, 314 insertions(+), 313 deletions(-) delete mode 100644 .gitpod.dockerfile delete mode 100644 .gitpod.yml delete mode 100644 devdeps.txt create mode 100644 scripts/get_version.py mode change 100755 => 120000 scripts/make create mode 100755 scripts/make.py diff --git a/.copier-answers.yml b/.copier-answers.yml index 72eced0..98e0704 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.4.0 +_commit: 1.5.7 _src_path: gh:pawamoy/copier-uv author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93a7505..de566ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,17 +25,20 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - - name: Fetch all tags - run: git fetch --depth=1 --tags - - - name: Set up Python + - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - - name: Install uv - run: pip install uv + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml - name: Install dependencies run: make setup @@ -61,12 +64,12 @@ jobs: - macos-latest - windows-latest python-version: - - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" + - "3.14" resolution: - highest - lowest-direct @@ -76,20 +79,27 @@ jobs: - os: windows-latest resolution: lowest-direct runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.python-version == '3.13' }} + continue-on-error: ${{ matrix.python-version == '3.14' }} steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - - name: Set up Python + - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install uv - run: pip install uv + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + cache-suffix: py${{ matrix.python-version }} - name: Install dependencies env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07d2809..d09c514 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,15 +11,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Fetch all tags - run: git fetch --depth=1 --tags + with: + fetch-depth: 0 + fetch-tags: true - name: Setup Python - uses: actions/setup-python@v4 - - name: Install git-changelog - run: pip install git-changelog + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Setup uv + uses: astral-sh/setup-uv@v3 - name: Prepare release notes - run: git-changelog --release-notes > release-notes.md + run: uv tool run git-changelog --release-notes > release-notes.md - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: body_path: release-notes.md diff --git a/.gitignore b/.gitignore index 41fee62..9fea047 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ /.pdm-build/ /htmlcov/ /site/ +uv.lock # cache .cache/ diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile deleted file mode 100644 index 1590b41..0000000 --- a/.gitpod.dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM gitpod/workspace-full -USER gitpod -ENV PIP_USER=no -RUN pip3 install pipx; \ - pipx install uv; \ - pipx ensurepath diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 23a3c2b..0000000 --- a/.gitpod.yml +++ /dev/null @@ -1,13 +0,0 @@ -vscode: - extensions: - - ms-python.python - -image: - file: .gitpod.dockerfile - -ports: -- port: 8000 - onOpen: notify - -tasks: -- init: make setup diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1a5066..37283bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,12 +23,11 @@ make setup > You can install it with: > > ```bash -> python3 -m pip install --user pipx -> pipx install uv +> curl -LsSf https://astral.sh/uv/install.sh | sh > ``` > > Now you can try running `make setup` again, -> or simply `uv install`. +> or simply `uv sync`. You now have the dependencies installed. diff --git a/README.md b/README.md index 3c15a5c..5dbbf75 100644 --- a/README.md +++ b/README.md @@ -3,26 +3,16 @@ [![ci](https://github.com/mkdocstrings/griffe-typingdoc/workflows/ci/badge.svg)](https://github.com/mkdocstrings/griffe-typingdoc/actions?query=workflow%3Aci) [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/griffe-typingdoc/) [![pypi version](https://img.shields.io/pypi/v/griffe-typingdoc.svg)](https://pypi.org/project/griffe-typingdoc/) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/griffe-typingdoc) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#griffe-typingdoc:gitter.im) Griffe extension for [PEP 727 – Documentation Metadata in Typing](https://peps.python.org/pep-0727/). ## Installation -With `pip`: - ```bash pip install griffe-typingdoc ``` -With [`pipx`](https://github.com/pipxproject/pipx): - -```bash -python3.8 -m pip install --user pipx -pipx install griffe-typingdoc -``` - To use the extension in a MkDocs project, use this configuration: diff --git a/config/ruff.toml b/config/ruff.toml index 41d7e45..7aed449 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py38" +target-version = "py39" line-length = 120 [lint] diff --git a/devdeps.txt b/devdeps.txt deleted file mode 100644 index e0afd7e..0000000 --- a/devdeps.txt +++ /dev/null @@ -1,32 +0,0 @@ -# dev -editables>=0.5 - -# maintenance -build>=1.2 -git-changelog>=2.5 -twine>=5.0; python_version < '3.13' - -# ci -duty>=1.4 -ruff>=0.4 -pytest>=8.2 -pytest-cov>=5.0 -pytest-randomly>=3.15 -pytest-xdist>=3.6 -mypy>=1.10 -types-markdown>=3.6 -types-pyyaml>=6.0 - -# docs -black>=24.4 -markdown-callouts>=0.4 -markdown-exec>=1.8 -mkdocs>=1.6 -mkdocs-coverage>=1.0 -mkdocs-gen-files>=0.5 -mkdocs-git-committers-plugin-2>=2.3 -mkdocs-literate-nav>=0.6 -mkdocs-material>=9.5 -mkdocs-minify-plugin>=0.8 -mkdocstrings[python]>=0.25 -tomli>=2.0; python_version < '3.11' diff --git a/duties.py b/duties.py index 138ae30..2625f8f 100644 --- a/duties.py +++ b/duties.py @@ -7,11 +7,13 @@ from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING from duty import duty, tools if TYPE_CHECKING: + from collections.abc import Iterator + from duty.context import Context @@ -53,7 +55,7 @@ def changelog(ctx: Context, bump: str = "") -> None: ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") -@duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) +@duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) def check(ctx: Context) -> None: """Check it all!""" @@ -82,6 +84,7 @@ def check_docs(ctx: Context) -> None: @duty def check_types(ctx: Context) -> None: """Check that the code is correctly typed.""" + os.environ["FORCE_COLOR"] = "1" ctx.run( tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), diff --git a/mkdocs.yml b/mkdocs.yml index 95842a4..6e7d619 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -131,14 +131,15 @@ plugins: show_root_heading: true show_root_full_path: false show_signature_annotations: true + show_source: true show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true - unwrap_annotated: true -- git-committers: +- git-revision-date-localized: enabled: !ENV [DEPLOY, false] - repository: mkdocstrings/griffe-typingdoc + enable_creation_date: true + type: timeago - minify: minify_html: !ENV [DEPLOY, false] - group: diff --git a/pyproject.toml b/pyproject.toml index bbfb0e2..88ef5e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Griffe extension for PEP 727 – Documentation Metadata in Typing authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}] license = {text = "ISC"} readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = [] dynamic = ["version"] classifiers = [ @@ -17,12 +17,12 @@ classifiers = [ "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", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", @@ -44,12 +44,12 @@ Discussions = "https://github.com/mkdocstrings/griffe-typingdoc/discussions" Gitter = "https://gitter.im/mkdocstrings/griffe-typingdoc" Funding = "https://github.com/sponsors/pawamoy" -[tool.pdm] -version = {source = "scm"} +[tool.pdm.version] +source = "call" +getter = "scripts.get_version:get_version" [tool.pdm.build] -package-dir = "src" -editable-backend = "editables" +# Include as much as possible in the source distribution, to help redistributors. excludes = ["**/.pytest_cache"] source-includes = [ "config", @@ -57,7 +57,6 @@ source-includes = [ "scripts", "share", "tests", - "devdeps.txt", "duties.py", "mkdocs.yml", "*.md", @@ -65,6 +64,43 @@ source-includes = [ ] [tool.pdm.build.wheel-data] +# Manual pages can be included in the wheel. +# Depending on the installation tool, they will be accessible to users. +# pipx supports it, uv does not yet, see https://github.com/astral-sh/uv/issues/4731. data = [ {path = "share/**/*", relative-to = "."}, ] + +[dependency-groups] +dev = [ + # maintenance + "build>=1.2", + "git-changelog>=2.5", + "twine>=5.1", + + # ci + "duty>=1.4", + "ruff>=0.4", + "pytest>=8.2", + "pytest-cov>=5.0", + "pytest-randomly>=3.15", + "pytest-xdist>=3.6", + "mypy>=1.10", + "types-markdown>=3.6", + "types-pyyaml>=6.0", + + # docs + "black>=24.4", + "markdown-callouts>=0.4", + "markdown-exec>=1.8", + "mkdocs>=1.6", + "mkdocs-coverage>=1.0", + "mkdocs-gen-files>=0.5", + "mkdocs-git-revision-date-localized-plugin>=1.2", + "mkdocs-literate-nav>=0.6", + "mkdocs-material>=9.5", + "mkdocs-minify-plugin>=0.8", + "mkdocstrings[python]>=0.25", + # YORE: EOL 3.10: Remove line. + "tomli>=2.0; python_version < '3.11'", +] \ No newline at end of file diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index cfff255..749e0ae 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -5,17 +5,18 @@ import os import sys from collections import defaultdict +from collections.abc import Iterable from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent -from typing import Dict, Iterable, Union +from typing import Union from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment from packaging.requirements import Requirement -# TODO: Remove once support for Python 3.10 is dropped. +# YORE: EOL 3.10: Replace block with line 2. if sys.version_info >= (3, 11): import tomllib else: @@ -26,11 +27,10 @@ pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] -with project_dir.joinpath("devdeps.txt").open() as devdeps_file: - devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))] +devdeps = [dep for dep in pyproject["dependency-groups"]["dev"] if not dep.startswith("-e")] -PackageMetadata = Dict[str, Union[str, Iterable[str]]] -Metadata = Dict[str, PackageMetadata] +PackageMetadata = dict[str, Union[str, Iterable[str]]] +Metadata = dict[str, PackageMetadata] def _merge_fields(metadata: dict) -> PackageMetadata: diff --git a/scripts/get_version.py b/scripts/get_version.py new file mode 100644 index 0000000..f4a30a8 --- /dev/null +++ b/scripts/get_version.py @@ -0,0 +1,27 @@ +"""Get current project version from Git tags or changelog.""" + +import re +from contextlib import suppress +from pathlib import Path + +from pdm.backend.hooks.version import SCMVersion, Version, default_version_formatter, get_version_from_scm + +_root = Path(__file__).parent.parent +_changelog = _root / "CHANGELOG.md" +_changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") +_default_scm_version = SCMVersion(Version("0.0.0"), None, False, None, None) # noqa: FBT003 + + +def get_version() -> str: + """Get current project version from Git tags or changelog.""" + scm_version = get_version_from_scm(_root) or _default_scm_version + if scm_version.version <= Version("0.1"): # Missing Git tags? + with suppress(OSError, StopIteration): # noqa: SIM117 + with _changelog.open("r", encoding="utf8") as file: + match = next(filter(None, map(_changelog_version_re.match, file))) + scm_version = scm_version._replace(version=Version(match.group(1))) + return default_version_formatter(scm_version) + + +if __name__ == "__main__": + print(get_version()) diff --git a/scripts/make b/scripts/make deleted file mode 100755 index d898022..0000000 --- a/scripts/make +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -"""Management commands.""" - -from __future__ import annotations - -import os -import shutil -import subprocess -import sys -from contextlib import contextmanager -from pathlib import Path -from typing import Any, Iterator - -PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split() - -exe = "" -prefix = "" - - -def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None: - """Run a shell command.""" - if capture_output: - return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 - subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 - return None - - -@contextmanager -def environ(**kwargs: str) -> Iterator[None]: - """Temporarily set environment variables.""" - original = dict(os.environ) - os.environ.update(kwargs) - try: - yield - finally: - os.environ.clear() - os.environ.update(original) - - -def uv_install() -> None: - """Install dependencies using uv.""" - uv_opts = "" - if "UV_RESOLUTION" in os.environ: - uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}" - requirements = shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True) - shell("uv pip install -r -", input=requirements, text=True) - if "CI" not in os.environ: - shell("uv pip install --no-deps -e .") - else: - shell("uv pip install --no-deps .") - - -def setup() -> None: - """Setup the project.""" - if not shutil.which("uv"): - raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") - - print("Installing dependencies (default environment)") # noqa: T201 - default_venv = Path(".venv") - if not default_venv.exists(): - shell("uv venv --python python") - uv_install() - - if PYTHON_VERSIONS: - for version in PYTHON_VERSIONS: - print(f"\nInstalling dependencies (python{version})") # noqa: T201 - venv_path = Path(f".venvs/{version}") - if not venv_path.exists(): - shell(f"uv venv --python {version} {venv_path}") - with environ(VIRTUAL_ENV=str(venv_path.resolve())): - uv_install() - - -def activate(path: str) -> None: - """Activate a virtual environment.""" - global exe, prefix # noqa: PLW0603 - - if (bin := Path(path, "bin")).exists(): - activate_script = bin / "activate_this.py" - elif (scripts := Path(path, "Scripts")).exists(): - activate_script = scripts / "activate_this.py" - exe = ".exe" - prefix = f"{path}/Scripts/" - else: - raise ValueError(f"make: activate: Cannot find activation script in {path}") - - if not activate_script.exists(): - raise ValueError(f"make: activate: Cannot find activation script in {path}") - - exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102 - - -def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: - """Run a command in a virtual environment.""" - kwargs = {"check": True, **kwargs} - if version == "default": - activate(".venv") - subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 - else: - activate(f".venvs/{version}") - os.environ["MULTIRUN"] = "1" - subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 - - -def multirun(cmd: str, *args: str, **kwargs: Any) -> None: - """Run a command for all configured Python versions.""" - if PYTHON_VERSIONS: - for version in PYTHON_VERSIONS: - run(version, cmd, *args, **kwargs) - else: - run("default", cmd, *args, **kwargs) - - -def allrun(cmd: str, *args: str, **kwargs: Any) -> None: - """Run a command in all virtual environments.""" - run("default", cmd, *args, **kwargs) - if PYTHON_VERSIONS: - multirun(cmd, *args, **kwargs) - - -def clean() -> None: - """Delete build artifacts and cache files.""" - paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] - for path in paths_to_clean: - shell(f"rm -rf {path}") - - cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"] - for dirpath in Path(".").rglob("*"): - if any(dirpath.match(pattern) for pattern in cache_dirs) and not (dirpath.match(".venv") or dirpath.match(".venvs")): - shutil.rmtree(path, ignore_errors=True) - - -def vscode() -> None: - """Configure VSCode to work on this project.""" - Path(".vscode").mkdir(parents=True, exist_ok=True) - shell("cp -v config/vscode/* .vscode") - - -def main() -> int: - """Main entry point.""" - args = list(sys.argv[1:]) - if not args or args[0] == "help": - if len(args) > 1: - run("default", "duty", "--help", args[1]) - else: - print("Available commands") # noqa: T201 - print(" help Print this help. Add task name to print help.") # noqa: T201 - print(" setup Setup all virtual environments (install dependencies).") # noqa: T201 - print(" run Run a command in the default virtual environment.") # noqa: T201 - print(" multirun Run a command for all configured Python versions.") # noqa: T201 - print(" allrun Run a command in all virtual environments.") # noqa: T201 - print(" 3.x Run a command in the virtual environment for Python 3.x.") # noqa: T201 - print(" clean Delete build artifacts and cache files.") # noqa: T201 - print(" vscode Configure VSCode to work on this project.") # noqa: T201 - try: - run("default", "python", "-V", capture_output=True) - except (subprocess.CalledProcessError, ValueError): - pass - else: - print("\nAvailable tasks") # noqa: T201 - run("default", "duty", "--list") - return 0 - - while args: - cmd = args.pop(0) - - if cmd == "run": - run("default", *args) - return 0 - - if cmd == "multirun": - multirun(*args) - return 0 - - if cmd == "allrun": - allrun(*args) - return 0 - - if cmd.startswith("3."): - run(cmd, *args) - return 0 - - opts = [] - while args and (args[0].startswith("-") or "=" in args[0]): - opts.append(args.pop(0)) - - if cmd == "clean": - clean() - elif cmd == "setup": - setup() - elif cmd == "vscode": - vscode() - elif cmd == "check": - multirun("duty", "check-quality", "check-types", "check-docs") - run("default", "duty", "check-api") - elif cmd in {"check-quality", "check-docs", "check-types", "test"}: - multirun("duty", cmd, *opts) - else: - run("default", "duty", cmd, *opts) - - return 0 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except subprocess.CalledProcessError as process: - if process.output: - print(process.output, file=sys.stderr) # noqa: T201 - sys.exit(process.returncode) diff --git a/scripts/make b/scripts/make new file mode 120000 index 0000000..c2eda0d --- /dev/null +++ b/scripts/make @@ -0,0 +1 @@ +make.py \ No newline at end of file diff --git a/scripts/make.py b/scripts/make.py new file mode 100755 index 0000000..3d42729 --- /dev/null +++ b/scripts/make.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Management commands.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from contextlib import contextmanager +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Iterator + + +PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() + + +def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None: + """Run a shell command.""" + if capture_output: + return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 + subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 + return None + + +@contextmanager +def environ(**kwargs: str) -> Iterator[None]: + """Temporarily set environment variables.""" + original = dict(os.environ) + os.environ.update(kwargs) + try: + yield + finally: + os.environ.clear() + os.environ.update(original) + + +def uv_install(venv: Path) -> None: + """Install dependencies using uv.""" + with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"): + if "CI" in os.environ: + shell("uv sync --no-editable") + else: + shell("uv sync") + + +def setup() -> None: + """Setup the project.""" + if not shutil.which("uv"): + raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") + + print("Installing dependencies (default environment)") + default_venv = Path(".venv") + if not default_venv.exists(): + shell("uv venv") + uv_install(default_venv) + + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + print(f"\nInstalling dependencies (python{version})") + venv_path = Path(f".venvs/{version}") + if not venv_path.exists(): + shell(f"uv venv --python {version} {venv_path}") + with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())): + uv_install(venv_path) + + +def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in a virtual environment.""" + kwargs = {"check": True, **kwargs} + uv_run = ["uv", "run", "--no-sync"] + if version == "default": + with environ(UV_PROJECT_ENVIRONMENT=".venv"): + subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 + else: + with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"): + subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 + + +def multirun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command for all configured Python versions.""" + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + run(version, cmd, *args, **kwargs) + else: + run("default", cmd, *args, **kwargs) + + +def allrun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in all virtual environments.""" + run("default", cmd, *args, **kwargs) + if PYTHON_VERSIONS: + multirun(cmd, *args, **kwargs) + + +def clean() -> None: + """Delete build artifacts and cache files.""" + paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] + for path in paths_to_clean: + shutil.rmtree(path, ignore_errors=True) + + cache_dirs = {".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"} + for dirpath in Path(".").rglob("*/"): + if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs: + shutil.rmtree(dirpath, ignore_errors=True) + + +def vscode() -> None: + """Configure VSCode to work on this project.""" + shutil.copytree("config/vscode", ".vscode", dirs_exist_ok=True) + + +def main() -> int: + """Main entry point.""" + args = list(sys.argv[1:]) + if not args or args[0] == "help": + if len(args) > 1: + run("default", "duty", "--help", args[1]) + else: + print( + dedent( + """ + Available commands + help Print this help. Add task name to print help. + setup Setup all virtual environments (install dependencies). + run Run a command in the default virtual environment. + multirun Run a command for all configured Python versions. + allrun Run a command in all virtual environments. + 3.x Run a command in the virtual environment for Python 3.x. + clean Delete build artifacts and cache files. + vscode Configure VSCode to work on this project. + """, + ), + flush=True, + ) + if os.path.exists(".venv"): + print("\nAvailable tasks", flush=True) + run("default", "duty", "--list") + return 0 + + while args: + cmd = args.pop(0) + + if cmd == "run": + run("default", *args) + return 0 + + if cmd == "multirun": + multirun(*args) + return 0 + + if cmd == "allrun": + allrun(*args) + return 0 + + if cmd.startswith("3."): + run(cmd, *args) + return 0 + + opts = [] + while args and (args[0].startswith("-") or "=" in args[0]): + opts.append(args.pop(0)) + + if cmd == "clean": + clean() + elif cmd == "setup": + setup() + elif cmd == "vscode": + vscode() + elif cmd == "check": + multirun("duty", "check-quality", "check-types", "check-docs") + run("default", "duty", "check-api") + elif cmd in {"check-quality", "check-docs", "check-types", "test"}: + multirun("duty", cmd, *opts) + else: + run("default", "duty", cmd, *opts) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.CalledProcessError as process: + if process.output: + print(process.output, file=sys.stderr) + sys.exit(process.returncode) From e462fd177e3f13d25ab85ea518db0621e67458ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 5 Feb 2025 15:35:45 +0100 Subject: [PATCH 2/5] style: Format --- src/griffe_typingdoc/_docstrings.py | 4 +++- src/griffe_typingdoc/_dynamic.py | 4 +--- src/griffe_typingdoc/_extension.py | 3 ++- src/griffe_typingdoc/_static.py | 6 ++++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/griffe_typingdoc/_docstrings.py b/src/griffe_typingdoc/_docstrings.py index 3749ab0..e32842c 100644 --- a/src/griffe_typingdoc/_docstrings.py +++ b/src/griffe_typingdoc/_docstrings.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Iterator +from typing import TYPE_CHECKING, Any from griffe import ( DocstringParameter, @@ -22,6 +22,8 @@ ) if TYPE_CHECKING: + from collections.abc import Iterator + from griffe import Function, Parameter diff --git a/src/griffe_typingdoc/_dynamic.py b/src/griffe_typingdoc/_dynamic.py index 2e91248..6c7a049 100644 --- a/src/griffe_typingdoc/_dynamic.py +++ b/src/griffe_typingdoc/_dynamic.py @@ -2,9 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any - -from typing_extensions import get_type_hints +from typing import TYPE_CHECKING, Any, get_type_hints from griffe_typingdoc._docstrings import _to_parameters_section diff --git a/src/griffe_typingdoc/_extension.py b/src/griffe_typingdoc/_extension.py index dea68be..105d378 100644 --- a/src/griffe_typingdoc/_extension.py +++ b/src/griffe_typingdoc/_extension.py @@ -10,9 +10,10 @@ if TYPE_CHECKING: import ast + from typing import Annotated from griffe.dataclasses import Attribute, Module, Object - from typing_extensions import Annotated, Doc + from typing_extensions import Doc class TypingDocExtension(Extension): diff --git a/src/griffe_typingdoc/_static.py b/src/griffe_typingdoc/_static.py index 8aa1f96..fa2b20d 100644 --- a/src/griffe_typingdoc/_static.py +++ b/src/griffe_typingdoc/_static.py @@ -5,7 +5,7 @@ import inspect from ast import literal_eval from collections import defaultdict -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any from griffe import Expr, ExprCall, ExprSubscript, ExprTuple, ParameterKind @@ -22,6 +22,8 @@ ) if TYPE_CHECKING: + from collections.abc import Sequence + from griffe import Function from griffe.dataclasses import Attribute from griffe.docstrings.dataclasses import ( @@ -106,7 +108,7 @@ def _parameters_docs(func: Function, **kwargs: Any) -> DocstringSectionParameter stars = {ParameterKind.var_positional: "*", ParameterKind.var_keyword: "**"}.get(parameter.kind, "") # type: ignore[arg-type] param_name = f"{stars}{parameter.name}" metadata = _metadata(parameter.annotation) - description = f'{metadata.get("deprecated", "")} {metadata.get("doc", "")}'.lstrip() + description = f"{metadata.get('deprecated', '')} {metadata.get('doc', '')}".lstrip() params_doc[param_name]["annotation"] = parameter.annotation params_doc[param_name]["description"] = description if params_doc: From abee85ff2677d49b3866483e74b8ecb75a6fc306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 5 Feb 2025 15:35:57 +0100 Subject: [PATCH 3/5] ci: Fix type ignore comments --- src/griffe_typingdoc/_static.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/griffe_typingdoc/_static.py b/src/griffe_typingdoc/_static.py index fa2b20d..3c254a8 100644 --- a/src/griffe_typingdoc/_static.py +++ b/src/griffe_typingdoc/_static.py @@ -124,12 +124,12 @@ def _other_parameters_docs(func: Function, **kwargs: Any) -> DocstringSectionPar "typing.Annotated", "typing_extensions.Annotated", }: - annotation = annotation.slice.elements[0] # type: ignore[attr-defined] + annotation = annotation.slice.elements[0] # type: ignore[union-attr] if isinstance(annotation, ExprSubscript) and annotation.canonical_path in { "typing.Unpack", "typing_extensions.Unpack", }: - slice_path = annotation.slice.canonical_path + slice_path = annotation.slice.canonical_path # type: ignore[union-attr] typed_dict = func.modules_collection[slice_path] params_doc = { attr.name: {"annotation": attr.annotation, "description": _metadata(attr.annotation).get("doc", "")} @@ -149,13 +149,13 @@ def _yields_docs(func: Function, **kwargs: Any) -> DocstringSectionYields | None if isinstance(annotation, ExprSubscript): if annotation.canonical_path in {"typing.Generator", "typing_extensions.Generator"}: - yield_annotation = annotation.slice.elements[0] # type: ignore[attr-defined] + yield_annotation = annotation.slice.elements[0] # type: ignore[union-attr] elif annotation.canonical_path in {"typing.Iterator", "typing_extensions.Iterator"}: yield_annotation = annotation.slice if yield_annotation: if isinstance(yield_annotation, ExprSubscript) and yield_annotation.is_tuple: - yield_elements = yield_annotation.slice.elements # type: ignore[attr-defined] + yield_elements = yield_annotation.slice.elements # type: ignore[union-attr] else: yield_elements = [yield_annotation] yields_section = _to_yields_section({"annotation": element, **_metadata(element)} for element in yield_elements) @@ -173,11 +173,11 @@ def _receives_docs(func: Function, **kwargs: Any) -> DocstringSectionReceives | "typing.Generator", "typing_extensions.Generator", }: - receive_annotation = annotation.slice.elements[1] # type: ignore[attr-defined] + receive_annotation = annotation.slice.elements[1] # type: ignore[union-attr] if receive_annotation: if isinstance(receive_annotation, ExprSubscript) and receive_annotation.is_tuple: - receive_elements = receive_annotation.slice.elements # type: ignore[attr-defined] + receive_elements = receive_annotation.slice.elements # type: ignore[union-attr] else: receive_elements = [receive_annotation] receives_section = _to_receives_section( @@ -197,7 +197,7 @@ def _returns_docs(func: Function, **kwargs: Any) -> DocstringSectionReturns | No "typing.Generator", "typing_extensions.Generator", }: - return_annotation = annotation.slice.elements[2] # type: ignore[attr-defined] + return_annotation = annotation.slice.elements[2] # type: ignore[union-attr] elif isinstance(annotation, ExprSubscript) and annotation.canonical_path in { "typing.Annotated", "typing_extensions.Annotated", @@ -206,7 +206,7 @@ def _returns_docs(func: Function, **kwargs: Any) -> DocstringSectionReturns | No if return_annotation: if isinstance(return_annotation, ExprSubscript) and return_annotation.is_tuple: - return_elements = return_annotation.slice.elements # type: ignore[attr-defined] + return_elements = return_annotation.slice.elements # type: ignore[union-attr] else: return_elements = [return_annotation] returns_section = _to_returns_section( From 514467c06f4381fca9c5f96a860c8abd87c1827b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 18 Feb 2025 01:18:43 +0100 Subject: [PATCH 4/5] refactor: Only add docstring sections when elements are annotated with `Doc` Issue-13: https://github.com/mkdocstrings/griffe-typingdoc/issues/13 --- src/griffe_typingdoc/_docstrings.py | 12 +-- src/griffe_typingdoc/_static.py | 62 ++++++++------- tests/test_extension.py | 112 ++++++++++++++++++++++++++-- 3 files changed, 146 insertions(+), 40 deletions(-) diff --git a/src/griffe_typingdoc/_docstrings.py b/src/griffe_typingdoc/_docstrings.py index e32842c..df4d0d8 100644 --- a/src/griffe_typingdoc/_docstrings.py +++ b/src/griffe_typingdoc/_docstrings.py @@ -22,7 +22,7 @@ ) if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Iterable from griffe import Function, Parameter @@ -60,7 +60,7 @@ def _to_other_parameters_section(params_dict: dict[str, dict[str, Any]]) -> Docs ) -def _to_yields_section(yield_data: Iterator[dict[str, Any]]) -> DocstringSectionYields: +def _to_yields_section(yield_data: Iterable[dict[str, Any]]) -> DocstringSectionYields: return DocstringSectionYields( [ DocstringYield( @@ -73,7 +73,7 @@ def _to_yields_section(yield_data: Iterator[dict[str, Any]]) -> DocstringSection ) -def _to_receives_section(receive_data: Iterator[dict[str, Any]]) -> DocstringSectionReceives: +def _to_receives_section(receive_data: Iterable[dict[str, Any]]) -> DocstringSectionReceives: return DocstringSectionReceives( [ DocstringReceive( @@ -86,7 +86,7 @@ def _to_receives_section(receive_data: Iterator[dict[str, Any]]) -> DocstringSec ) -def _to_returns_section(return_data: Iterator[dict[str, Any]]) -> DocstringSectionReturns: +def _to_returns_section(return_data: Iterable[dict[str, Any]]) -> DocstringSectionReturns: return DocstringSectionReturns( [ DocstringReturn( @@ -99,7 +99,7 @@ def _to_returns_section(return_data: Iterator[dict[str, Any]]) -> DocstringSecti ) -def _to_warns_section(warn_data: Iterator[dict[str, Any]]) -> DocstringSectionWarns: +def _to_warns_section(warn_data: Iterable[dict[str, Any]]) -> DocstringSectionWarns: return DocstringSectionWarns( [ DocstringWarn( @@ -111,7 +111,7 @@ def _to_warns_section(warn_data: Iterator[dict[str, Any]]) -> DocstringSectionWa ) -def _to_raises_section(raise_data: Iterator[dict[str, Any]]) -> DocstringSectionRaises: +def _to_raises_section(raise_data: Iterable[dict[str, Any]]) -> DocstringSectionRaises: return DocstringSectionRaises( [ DocstringRaise( diff --git a/src/griffe_typingdoc/_static.py b/src/griffe_typingdoc/_static.py index 3c254a8..778219a 100644 --- a/src/griffe_typingdoc/_static.py +++ b/src/griffe_typingdoc/_static.py @@ -103,16 +103,17 @@ def _attribute_docs(attr: Attribute, **kwargs: Any) -> str: # noqa: ARG001 def _parameters_docs(func: Function, **kwargs: Any) -> DocstringSectionParameters | None: # noqa: ARG001 - params_doc: dict[str, dict[str, Any]] = defaultdict(dict) + params_data: dict[str, dict[str, Any]] = defaultdict(dict) for parameter in _no_self_params(func): stars = {ParameterKind.var_positional: "*", ParameterKind.var_keyword: "**"}.get(parameter.kind, "") # type: ignore[arg-type] param_name = f"{stars}{parameter.name}" metadata = _metadata(parameter.annotation) - description = f"{metadata.get('deprecated', '')} {metadata.get('doc', '')}".lstrip() - params_doc[param_name]["annotation"] = parameter.annotation - params_doc[param_name]["description"] = description - if params_doc: - return _to_parameters_section(params_doc, func) + if "deprecated" in metadata or "doc" in metadata: + description = f"{metadata.get('deprecated', '')} {metadata.get('doc', '')}".lstrip() + params_data[param_name]["description"] = description + params_data[param_name]["annotation"] = parameter.annotation + if params_data: + return _to_parameters_section(params_data, func) return None @@ -131,20 +132,19 @@ def _other_parameters_docs(func: Function, **kwargs: Any) -> DocstringSectionPar }: slice_path = annotation.slice.canonical_path # type: ignore[union-attr] typed_dict = func.modules_collection[slice_path] - params_doc = { - attr.name: {"annotation": attr.annotation, "description": _metadata(attr.annotation).get("doc", "")} + params_data = { + attr.name: {"annotation": attr.annotation, "description": description} for attr in typed_dict.members.values() + if (description := _metadata(attr.annotation).get("doc")) is not None } - if params_doc: - return _to_other_parameters_section(params_doc) + if params_data: + return _to_other_parameters_section(params_data) break return None def _yields_docs(func: Function, **kwargs: Any) -> DocstringSectionYields | None: # noqa: ARG001 - yields_section = None yield_annotation = None - annotation = func.returns if isinstance(annotation, ExprSubscript): @@ -158,15 +158,19 @@ def _yields_docs(func: Function, **kwargs: Any) -> DocstringSectionYields | None yield_elements = yield_annotation.slice.elements # type: ignore[union-attr] else: yield_elements = [yield_annotation] - yields_section = _to_yields_section({"annotation": element, **_metadata(element)} for element in yield_elements) + yield_data = [ + {"annotation": element, **metadata} + for element in yield_elements + if "doc" in (metadata := _metadata(element)) + ] + if yield_data: + return _to_yields_section(yield_data) - return yields_section + return None def _receives_docs(func: Function, **kwargs: Any) -> DocstringSectionReceives | None: # noqa: ARG001 - receives_section = None receive_annotation = None - annotation = func.returns if isinstance(annotation, ExprSubscript) and annotation.canonical_path in { @@ -180,17 +184,19 @@ def _receives_docs(func: Function, **kwargs: Any) -> DocstringSectionReceives | receive_elements = receive_annotation.slice.elements # type: ignore[union-attr] else: receive_elements = [receive_annotation] - receives_section = _to_receives_section( - {"annotation": element, **_metadata(element)} for element in receive_elements - ) + receive_data = [ + {"annotation": element, **metadata} + for element in receive_elements + if "doc" in (metadata := _metadata(element)) + ] + if receive_data: + return _to_receives_section(receive_data) - return receives_section + return None def _returns_docs(func: Function, **kwargs: Any) -> DocstringSectionReturns | None: # noqa: ARG001 - returns_section = None return_annotation = None - annotation = func.returns if isinstance(annotation, ExprSubscript) and annotation.canonical_path in { @@ -209,11 +215,15 @@ def _returns_docs(func: Function, **kwargs: Any) -> DocstringSectionReturns | No return_elements = return_annotation.slice.elements # type: ignore[union-attr] else: return_elements = [return_annotation] - returns_section = _to_returns_section( - {"annotation": element, **_metadata(element)} for element in return_elements - ) + return_data = [ + {"annotation": element, **metadata} + for element in return_elements + if "doc" in (metadata := _metadata(element)) + ] + if return_data: + return _to_returns_section(return_data) - return returns_section + return None def _warns_docs(attr_or_func: Attribute | Function, **kwargs: Any) -> DocstringSectionWarns | None: # noqa: ARG001 diff --git a/tests/test_extension.py b/tests/test_extension.py index b7ca900..4688390 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -1,5 +1,6 @@ """Tests for the Griffe extension.""" +import pytest from griffe import DocstringSectionKind, Extensions, GriffeLoader, temporary_visited_package from griffe_typingdoc import TypingDocExtension @@ -184,21 +185,116 @@ class Options(TypedDict): extensions=Extensions(TypingDocExtension()), ) as package: sections = package["A.__init__"].docstring.parsed - assert len(sections) == 3 + assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.text - assert sections[1].kind is DocstringSectionKind.parameters - assert sections[2].kind is DocstringSectionKind.other_parameters - foo = sections[2].value[0] + assert sections[1].kind is DocstringSectionKind.other_parameters + foo = sections[1].value[0] assert foo.name == "foo" assert foo.description == "Foo's description." assert str(foo.annotation).startswith("Annotated[int") sections = package["B.__init__"].docstring.parsed - assert len(sections) == 3 + assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.text - assert sections[1].kind is DocstringSectionKind.parameters - assert sections[2].kind is DocstringSectionKind.other_parameters - bar = sections[2].value[0] + assert sections[1].kind is DocstringSectionKind.other_parameters + bar = sections[1].value[0] assert bar.name == "bar" assert bar.description == "Bar's description." assert str(bar.annotation).startswith("Annotated[str") + + +@pytest.mark.parametrize( + "annotation", + ["int", "Annotated[int, '']"], +) +def test_ignore_unannotated_params(annotation: str) -> None: + """Ignore parameters that are not annotated with `Doc`.""" + with temporary_visited_package( + "package", + { + "__init__.py": f"{typing_imports}\ndef f(a: {annotation}):\n '''Docstring.'''", + }, + extensions=Extensions(TypingDocExtension()), + ) as package: + sections = package["f"].docstring.parsed + assert len(sections) == 1 + assert sections[0].kind is DocstringSectionKind.text + + +@pytest.mark.parametrize( + "annotation", + ["int", "Annotated[int, '']"], +) +def test_ignore_unannotated_other_params(annotation: str) -> None: + """Ignore other parameters that are not annotated with `Doc`.""" + with temporary_visited_package( + "package", + { + "__init__.py": f""" + {typing_imports} + from typing import TypedDict + class Kwargs(TypedDict): + a: {annotation} + def f(**kwargs: Unpack[Kwargs]): + '''Docstring.''' + """, + }, + extensions=Extensions(TypingDocExtension()), + ) as package: + sections = package["f"].docstring.parsed + assert len(sections) == 1 + assert sections[0].kind is DocstringSectionKind.text + + +@pytest.mark.parametrize( + "annotation", + ["int", "Annotated[int, '']"], +) +def test_ignore_unannotated_returns(annotation: str) -> None: + """Ignore return values that are not annotated with `Doc`.""" + with temporary_visited_package( + "package", + { + "__init__.py": f"{typing_imports}\ndef f() -> {annotation}:\n '''Docstring.'''", + }, + extensions=Extensions(TypingDocExtension()), + ) as package: + sections = package["f"].docstring.parsed + assert len(sections) == 1 + assert sections[0].kind is DocstringSectionKind.text + + +@pytest.mark.parametrize( + "annotation", + ["int", "Annotated[int, '']"], +) +def test_ignore_unannotated_yields(annotation: str) -> None: + """Ignore yields that are not annotated with `Doc`.""" + with temporary_visited_package( + "package", + { + "__init__.py": f"{typing_imports}\ndef f() -> Iterator[{annotation}]:\n '''Docstring.'''", + }, + extensions=Extensions(TypingDocExtension()), + ) as package: + sections = package["f"].docstring.parsed + assert len(sections) == 1 + assert sections[0].kind is DocstringSectionKind.text + + +@pytest.mark.parametrize( + "annotation", + ["int", "Annotated[int, '']"], +) +def test_ignore_unannotated_receives(annotation: str) -> None: + """Ignore receives that are not annotated with `Doc`.""" + with temporary_visited_package( + "package", + { + "__init__.py": f"{typing_imports}\ndef f() -> Generator[int, {annotation}, None]:\n '''Docstring.'''", + }, + extensions=Extensions(TypingDocExtension()), + ) as package: + sections = package["f"].docstring.parsed + assert len(sections) == 1 + assert sections[0].kind is DocstringSectionKind.text From e68c68815cffa46320f3ba42b867fc1175e804e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 18 Feb 2025 01:24:53 +0100 Subject: [PATCH 5/5] chore: Prepare release 0.2.8 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb8130..27cb2fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.2.8](https://github.com/mkdocstrings/griffe-typingdoc/releases/tag/0.2.8) - 2025-02-18 + +[Compare with 0.2.7](https://github.com/mkdocstrings/griffe-typingdoc/compare/0.2.7...0.2.8) + +### Code Refactoring + +- Only add docstring sections when elements are annotated with `Doc` ([514467c](https://github.com/mkdocstrings/griffe-typingdoc/commit/514467c06f4381fca9c5f96a860c8abd87c1827b) by Timothée Mazzucotelli). [Issue-13](https://github.com/mkdocstrings/griffe-typingdoc/issues/13) + ## [0.2.7](https://github.com/mkdocstrings/griffe-typingdoc/releases/tag/0.2.7) - 2024-09-10 [Compare with 0.2.6](https://github.com/mkdocstrings/griffe-typingdoc/compare/0.2.6...0.2.7)