Skip to content

Commit

Permalink
chore: Implement PEP 563 deferred annotation resolution (#440)
Browse files Browse the repository at this point in the history
- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking
- Enable Ruff checks for PEP-compliant annotations:
  - [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/)
  - [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/)

For more details on PEP 563, see: https://peps.python.org/pep-0563/
  • Loading branch information
tony authored Jan 4, 2025
2 parents 11446bb + c4fa39f commit c1753bd
Show file tree
Hide file tree
Showing 46 changed files with 291 additions and 147 deletions.
11 changes: 11 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@

### Development

#### chore: Implement PEP 563 deferred annotation resolution (#440)

- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking.
- Enable Ruff checks for PEP-compliant annotations:
- [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/)
- [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/)

For more details on PEP 563, see: https://peps.python.org/pep-0563/

### Development

- Relax `types-docutils` version constraint (#418)

## django-docutils 0.28.0 (2024-12-20)
Expand Down
10 changes: 6 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Sphinx configuration for Django Docutils."""

# flake8: noqa: E501
from __future__ import annotations

import inspect
import os
import pathlib
Expand Down Expand Up @@ -77,7 +79,7 @@
html_extra_path = ["manifest.json"]
html_theme = "furo"
html_theme_path: list[str] = []
html_theme_options: dict[str, t.Union[str, list[dict[str, str]]]] = {
html_theme_options: dict[str, str | list[dict[str, str]]] = {
"light_logo": "img/icons/logo.svg",
"dark_logo": "img/icons/logo-dark.svg",
"footer_icons": [
Expand Down Expand Up @@ -147,7 +149,7 @@
}


def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str:
"""
Determine the URL corresponding to Python object.
Expand Down Expand Up @@ -217,13 +219,13 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
)


def remove_tabs_js(app: "Sphinx", exc: Exception) -> None:
def remove_tabs_js(app: Sphinx, exc: Exception) -> None:
"""Fix for sphinx-inline-tabs#18."""
if app.builder.format == "html" and not exc:
tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js"
tabs_js.unlink(missing_ok=True)


def setup(app: "Sphinx") -> None:
def setup(app: Sphinx) -> None:
"""Sphinx setup hook."""
app.connect("build-finished", remove_tabs_js)
12 changes: 11 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,25 @@ select = [
"PERF", # Perflint
"RUF", # Ruff-specific rules
"D", # pydocstyle
"FA100", # future annotations
]
ignore = [
"COM812", # missing trailing comma, ruff format conflict
]
extend-safe-fixes = [
"UP006",
"UP007",
]
pyupgrade.keep-runtime-typing = false

[tool.ruff.lint.isort]
known-first-party = [
"django_docutils",
]
combine-as-imports = true
required-imports = [
"from __future__ import annotations",
]

[tool.ruff.lint.pydocstyle]
convention = "numpy"
Expand Down Expand Up @@ -243,9 +252,10 @@ exclude_also = [
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"if t.TYPE_CHECKING:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
"from __future__ import annotations",
]
2 changes: 2 additions & 0 deletions src/django_docutils/__about__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Metadata package for Django Docutils."""

from __future__ import annotations

__title__ = "django-docutils"
__package_name__ = "django_docutils"
__description__ = "Docutils (a.k.a. reStructuredText, reST, RST) support for django."
Expand Down
10 changes: 6 additions & 4 deletions src/django_docutils/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
.. _typeshed's: https://github.com/python/typeshed/blob/5df8de7/stdlib/_typeshed/__init__.pyi#L115-L118
""" # E501

from typing import TYPE_CHECKING, Union
from __future__ import annotations

if TYPE_CHECKING:
import typing as t

if t.TYPE_CHECKING:
from os import PathLike

from typing_extensions import TypeAlias

StrPath: "TypeAlias" = Union[str, "PathLike[str]"] # stable
StrPath: TypeAlias = t.Union[str, "PathLike[str]"] # stable
""":class:`os.PathLike` or :class:`str`"""

StrOrBytesPath: "TypeAlias" = Union[
StrOrBytesPath: TypeAlias = t.Union[
str,
bytes,
"PathLike[str]",
Expand Down
2 changes: 2 additions & 0 deletions src/django_docutils/exc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Exceptions for Django Docutils."""

from __future__ import annotations


class DjangoDocutilsException(Exception):
"""Base exception for Django Docutils package."""
Expand Down
7 changes: 5 additions & 2 deletions src/django_docutils/lib/directives/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@
:license: BSD, see LICENSE for details.
"""

from __future__ import annotations

import re
import typing as t
from collections.abc import Callable

from docutils import nodes
from docutils.parsers.rst import Directive, directives
Expand All @@ -48,6 +49,8 @@
from pygments.lexers.special import TextLexer

if t.TYPE_CHECKING:
from collections.abc import Callable

from pygments.formatter import Formatter


Expand All @@ -72,7 +75,7 @@ def patch_bash_session_lexer() -> None:
DEFAULT = HtmlFormatter(cssclass="highlight code-block", noclasses=INLINESTYLES)

#: Add name -> formatter pairs for every variant you want to use
VARIANTS: dict[str, "Formatter[str]"] = {
VARIANTS: dict[str, Formatter[str]] = {
# 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True),
}

Expand Down
2 changes: 2 additions & 0 deletions src/django_docutils/lib/directives/registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Register douctils directives for django-docutils."""

from __future__ import annotations

from django.utils.module_loading import import_string
from docutils.parsers.rst import directives

Expand Down
6 changes: 3 additions & 3 deletions src/django_docutils/lib/metadata/extract.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Title, Subtitle, and Metadata extraction of reStructuredText."""

import typing as t
from __future__ import annotations

from django.template.defaultfilters import truncatewords
from django.utils.html import strip_tags
from docutils import nodes


def extract_title(document: nodes.document) -> t.Optional[str]:
def extract_title(document: nodes.document) -> str | None:
"""Return the title of the document.
Parameters
Expand Down Expand Up @@ -77,7 +77,7 @@ def extract_metadata(document: nodes.document) -> dict[str, str]:
return output


def extract_subtitle(document: nodes.document) -> t.Optional[str]:
def extract_subtitle(document: nodes.document) -> str | None:
"""Return the subtitle of the document."""
for node in document.traverse(nodes.PreBibliographic): # type:ignore
if isinstance(node, nodes.subtitle):
Expand Down
2 changes: 2 additions & 0 deletions src/django_docutils/lib/metadata/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def process_datetime(metadata):
See *processors.py* for more examples.
"""

from __future__ import annotations

from django.utils.module_loading import import_string

from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST
Expand Down
2 changes: 2 additions & 0 deletions src/django_docutils/lib/metadata/processors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Metadata processing for Django Docutils."""

from __future__ import annotations

import datetime
import typing as t

Expand Down
7 changes: 6 additions & 1 deletion src/django_docutils/lib/metadata/tests/test_extract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Test metadata and title extraction from reStructuredText."""

import pathlib
from __future__ import annotations

import typing as t

from django.utils.encoding import force_bytes
from docutils.core import publish_doctree
Expand All @@ -12,6 +14,9 @@
)
from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST

if t.TYPE_CHECKING:
import pathlib


def test_extract_title() -> None:
"""Assert title extraction from reStructuredText."""
Expand Down
2 changes: 2 additions & 0 deletions src/django_docutils/lib/metadata/tests/test_process.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Test docutils metadata (docinfo) processing in Django Docutils."""

from __future__ import annotations

import datetime

from django.utils.encoding import force_bytes
Expand Down
28 changes: 15 additions & 13 deletions src/django_docutils/lib/publisher.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Docutils Publisher fors for Django Docutils."""

from __future__ import annotations

import typing as t

from django.utils.encoding import force_bytes, force_str
from django.utils.safestring import mark_safe
from docutils import io, nodes
from docutils.core import Publisher, publish_doctree as docutils_publish_doctree
from docutils.readers.doctree import Reader
from docutils.writers.html5_polyglot import Writer

from .directives.registry import register_django_docutils_directives
from .roles.registry import register_django_docutils_roles
Expand All @@ -17,19 +18,20 @@

if t.TYPE_CHECKING:
from docutils import SettingsSpec
from docutils.writers.html5_polyglot import Writer

docutils_settings = DJANGO_DOCUTILS_LIB_RST.get("docutils", {})


def publish_parts_from_doctree(
document: nodes.document,
destination_path: t.Optional[str] = None,
writer: t.Optional[Writer] = None,
destination_path: str | None = None,
writer: Writer | None = None,
writer_name: str = "pseudoxml",
settings: t.Optional[t.Any] = None,
settings_spec: "t.Optional[SettingsSpec]" = None,
settings_overrides: t.Optional[t.Any] = None,
config_section: t.Optional[str] = None,
settings: t.Any | None = None,
settings_spec: SettingsSpec | None = None,
settings_overrides: t.Any | None = None,
config_section: str | None = None,
enable_exit_status: bool = False,
) -> dict[str, str]:
"""Render docutils doctree into docutils parts."""
Expand All @@ -56,8 +58,8 @@ def publish_parts_from_doctree(

def publish_toc_from_doctree(
doctree: nodes.document,
writer: t.Optional[Writer] = None,
) -> t.Optional[str]:
writer: Writer | None = None,
) -> str | None:
"""Publish table of contents from docutils doctree."""
if not writer:
writer = DjangoDocutilsWriter()
Expand Down Expand Up @@ -98,7 +100,7 @@ def publish_toc_from_doctree(


def publish_doctree(
source: t.Union[str, bytes],
source: str | bytes,
settings_overrides: t.Any = docutils_settings,
) -> nodes.document:
"""Split off ability to get doctree (a.k.a. document).
Expand Down Expand Up @@ -139,8 +141,8 @@ class PublishHtmlDocTreeKwargs(TypedDict):

def publish_html_from_source(
source: str,
**kwargs: "Unpack[PublishHtmlDocTreeKwargs]",
) -> t.Optional[str]:
**kwargs: Unpack[PublishHtmlDocTreeKwargs],
) -> str | None:
"""Return HTML from reStructuredText source string."""
doctree = publish_doctree(source)
return publish_html_from_doctree(doctree, **kwargs)
Expand All @@ -150,7 +152,7 @@ def publish_html_from_doctree(
doctree: nodes.document,
show_title: bool = True,
toc_only: bool = False,
) -> t.Optional[str]:
) -> str | None:
"""Return HTML from reStructuredText document (doctree).
Parameters
Expand Down
12 changes: 7 additions & 5 deletions src/django_docutils/lib/roles/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Core parts for Django Docutils roles."""

from __future__ import annotations

import typing as t

from docutils import nodes, utils
Expand All @@ -13,9 +15,9 @@
def generic_url_role(
name: str,
text: str,
url_handler_fn: "UrlHandlerFn",
innernodeclass: type[t.Union[nodes.Text, nodes.TextElement]] = nodes.Text,
) -> "RoleFnReturnValue":
url_handler_fn: UrlHandlerFn,
innernodeclass: type[nodes.Text | nodes.TextElement] = nodes.Text,
) -> RoleFnReturnValue:
"""Docutils Role for Django Docutils.
This generic role also handles explicit titles (``:role:`yata yata <target>```)
Expand Down Expand Up @@ -78,8 +80,8 @@ def url_handler(target):
def generic_remote_url_role(
name: str,
text: str,
url_handler_fn: "RemoteUrlHandlerFn",
innernodeclass: type[t.Union[nodes.Text, nodes.TextElement]] = nodes.Text,
url_handler_fn: RemoteUrlHandlerFn,
innernodeclass: type[nodes.Text | nodes.TextElement] = nodes.Text,
) -> tuple[list[nodes.reference], list[t.Any]]:
"""Docutils Role that can call an external data source for title and URL.
Expand Down
14 changes: 9 additions & 5 deletions src/django_docutils/lib/roles/email.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"""Email role for docutils."""

import typing as t
from __future__ import annotations

from docutils.parsers.rst.states import Inliner
import typing as t

from .common import generic_url_role
from .types import RoleFnReturnValue

if t.TYPE_CHECKING:
from docutils.parsers.rst.states import Inliner

from .types import RoleFnReturnValue


def email_role(
Expand All @@ -14,8 +18,8 @@ def email_role(
text: str,
lineno: int,
inliner: Inliner,
options: t.Optional[dict[str, t.Any]] = None,
content: t.Optional[str] = None,
options: dict[str, t.Any] | None = None,
content: str | None = None,
) -> RoleFnReturnValue:
"""Role for linking to email articles.
Expand Down
Loading

0 comments on commit c1753bd

Please sign in to comment.