Skip to content

Commit c1753bd

Browse files
authored
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/
2 parents 11446bb + c4fa39f commit c1753bd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+291
-147
lines changed

CHANGES

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66

77
### Development
88

9+
#### chore: Implement PEP 563 deferred annotation resolution (#440)
10+
11+
- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking.
12+
- Enable Ruff checks for PEP-compliant annotations:
13+
- [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/)
14+
- [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/)
15+
16+
For more details on PEP 563, see: https://peps.python.org/pep-0563/
17+
18+
### Development
19+
920
- Relax `types-docutils` version constraint (#418)
1021

1122
## django-docutils 0.28.0 (2024-12-20)

docs/conf.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Sphinx configuration for Django Docutils."""
22

33
# flake8: noqa: E501
4+
from __future__ import annotations
5+
46
import inspect
57
import os
68
import pathlib
@@ -77,7 +79,7 @@
7779
html_extra_path = ["manifest.json"]
7880
html_theme = "furo"
7981
html_theme_path: list[str] = []
80-
html_theme_options: dict[str, t.Union[str, list[dict[str, str]]]] = {
82+
html_theme_options: dict[str, str | list[dict[str, str]]] = {
8183
"light_logo": "img/icons/logo.svg",
8284
"dark_logo": "img/icons/logo-dark.svg",
8385
"footer_icons": [
@@ -147,7 +149,7 @@
147149
}
148150

149151

150-
def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
152+
def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str:
151153
"""
152154
Determine the URL corresponding to Python object.
153155
@@ -217,13 +219,13 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
217219
)
218220

219221

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

226228

227-
def setup(app: "Sphinx") -> None:
229+
def setup(app: Sphinx) -> None:
228230
"""Sphinx setup hook."""
229231
app.connect("build-finished", remove_tabs_js)

pyproject.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,25 @@ select = [
195195
"PERF", # Perflint
196196
"RUF", # Ruff-specific rules
197197
"D", # pydocstyle
198+
"FA100", # future annotations
198199
]
199200
ignore = [
200201
"COM812", # missing trailing comma, ruff format conflict
201202
]
203+
extend-safe-fixes = [
204+
"UP006",
205+
"UP007",
206+
]
207+
pyupgrade.keep-runtime-typing = false
202208

203209
[tool.ruff.lint.isort]
204210
known-first-party = [
205211
"django_docutils",
206212
]
207213
combine-as-imports = true
214+
required-imports = [
215+
"from __future__ import annotations",
216+
]
208217

209218
[tool.ruff.lint.pydocstyle]
210219
convention = "numpy"
@@ -243,9 +252,10 @@ exclude_also = [
243252
"if settings.DEBUG",
244253
"raise AssertionError",
245254
"raise NotImplementedError",
246-
"if 0:",
247255
"if __name__ == .__main__.:",
248256
"if TYPE_CHECKING:",
257+
"if t.TYPE_CHECKING:",
249258
"class .*\\bProtocol\\):",
250259
"@(abc\\.)?abstractmethod",
260+
"from __future__ import annotations",
251261
]

src/django_docutils/__about__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Metadata package for Django Docutils."""
22

3+
from __future__ import annotations
4+
35
__title__ = "django-docutils"
46
__package_name__ = "django_docutils"
57
__description__ = "Docutils (a.k.a. reStructuredText, reST, RST) support for django."

src/django_docutils/_internal/types.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
.. _typeshed's: https://github.com/python/typeshed/blob/5df8de7/stdlib/_typeshed/__init__.pyi#L115-L118
88
""" # E501
99

10-
from typing import TYPE_CHECKING, Union
10+
from __future__ import annotations
1111

12-
if TYPE_CHECKING:
12+
import typing as t
13+
14+
if t.TYPE_CHECKING:
1315
from os import PathLike
1416

1517
from typing_extensions import TypeAlias
1618

17-
StrPath: "TypeAlias" = Union[str, "PathLike[str]"] # stable
19+
StrPath: TypeAlias = t.Union[str, "PathLike[str]"] # stable
1820
""":class:`os.PathLike` or :class:`str`"""
1921

20-
StrOrBytesPath: "TypeAlias" = Union[
22+
StrOrBytesPath: TypeAlias = t.Union[
2123
str,
2224
bytes,
2325
"PathLike[str]",

src/django_docutils/exc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Exceptions for Django Docutils."""
22

3+
from __future__ import annotations
4+
35

46
class DjangoDocutilsException(Exception):
57
"""Base exception for Django Docutils package."""

src/django_docutils/lib/directives/code.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@
3636
:license: BSD, see LICENSE for details.
3737
"""
3838

39+
from __future__ import annotations
40+
3941
import re
4042
import typing as t
41-
from collections.abc import Callable
4243

4344
from docutils import nodes
4445
from docutils.parsers.rst import Directive, directives
@@ -48,6 +49,8 @@
4849
from pygments.lexers.special import TextLexer
4950

5051
if t.TYPE_CHECKING:
52+
from collections.abc import Callable
53+
5154
from pygments.formatter import Formatter
5255

5356

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

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

src/django_docutils/lib/directives/registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Register douctils directives for django-docutils."""
22

3+
from __future__ import annotations
4+
35
from django.utils.module_loading import import_string
46
from docutils.parsers.rst import directives
57

src/django_docutils/lib/metadata/extract.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Title, Subtitle, and Metadata extraction of reStructuredText."""
22

3-
import typing as t
3+
from __future__ import annotations
44

55
from django.template.defaultfilters import truncatewords
66
from django.utils.html import strip_tags
77
from docutils import nodes
88

99

10-
def extract_title(document: nodes.document) -> t.Optional[str]:
10+
def extract_title(document: nodes.document) -> str | None:
1111
"""Return the title of the document.
1212
1313
Parameters
@@ -77,7 +77,7 @@ def extract_metadata(document: nodes.document) -> dict[str, str]:
7777
return output
7878

7979

80-
def extract_subtitle(document: nodes.document) -> t.Optional[str]:
80+
def extract_subtitle(document: nodes.document) -> str | None:
8181
"""Return the subtitle of the document."""
8282
for node in document.traverse(nodes.PreBibliographic): # type:ignore
8383
if isinstance(node, nodes.subtitle):

src/django_docutils/lib/metadata/process.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ def process_datetime(metadata):
3030
See *processors.py* for more examples.
3131
"""
3232

33+
from __future__ import annotations
34+
3335
from django.utils.module_loading import import_string
3436

3537
from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST

src/django_docutils/lib/metadata/processors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Metadata processing for Django Docutils."""
22

3+
from __future__ import annotations
4+
35
import datetime
46
import typing as t
57

src/django_docutils/lib/metadata/tests/test_extract.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Test metadata and title extraction from reStructuredText."""
22

3-
import pathlib
3+
from __future__ import annotations
4+
5+
import typing as t
46

57
from django.utils.encoding import force_bytes
68
from docutils.core import publish_doctree
@@ -12,6 +14,9 @@
1214
)
1315
from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST
1416

17+
if t.TYPE_CHECKING:
18+
import pathlib
19+
1520

1621
def test_extract_title() -> None:
1722
"""Assert title extraction from reStructuredText."""

src/django_docutils/lib/metadata/tests/test_process.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Test docutils metadata (docinfo) processing in Django Docutils."""
22

3+
from __future__ import annotations
4+
35
import datetime
46

57
from django.utils.encoding import force_bytes

src/django_docutils/lib/publisher.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Docutils Publisher fors for Django Docutils."""
22

3+
from __future__ import annotations
4+
35
import typing as t
46

57
from django.utils.encoding import force_bytes, force_str
68
from django.utils.safestring import mark_safe
79
from docutils import io, nodes
810
from docutils.core import Publisher, publish_doctree as docutils_publish_doctree
911
from docutils.readers.doctree import Reader
10-
from docutils.writers.html5_polyglot import Writer
1112

1213
from .directives.registry import register_django_docutils_directives
1314
from .roles.registry import register_django_docutils_roles
@@ -17,19 +18,20 @@
1718

1819
if t.TYPE_CHECKING:
1920
from docutils import SettingsSpec
21+
from docutils.writers.html5_polyglot import Writer
2022

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

2325

2426
def publish_parts_from_doctree(
2527
document: nodes.document,
26-
destination_path: t.Optional[str] = None,
27-
writer: t.Optional[Writer] = None,
28+
destination_path: str | None = None,
29+
writer: Writer | None = None,
2830
writer_name: str = "pseudoxml",
29-
settings: t.Optional[t.Any] = None,
30-
settings_spec: "t.Optional[SettingsSpec]" = None,
31-
settings_overrides: t.Optional[t.Any] = None,
32-
config_section: t.Optional[str] = None,
31+
settings: t.Any | None = None,
32+
settings_spec: SettingsSpec | None = None,
33+
settings_overrides: t.Any | None = None,
34+
config_section: str | None = None,
3335
enable_exit_status: bool = False,
3436
) -> dict[str, str]:
3537
"""Render docutils doctree into docutils parts."""
@@ -56,8 +58,8 @@ def publish_parts_from_doctree(
5658

5759
def publish_toc_from_doctree(
5860
doctree: nodes.document,
59-
writer: t.Optional[Writer] = None,
60-
) -> t.Optional[str]:
61+
writer: Writer | None = None,
62+
) -> str | None:
6163
"""Publish table of contents from docutils doctree."""
6264
if not writer:
6365
writer = DjangoDocutilsWriter()
@@ -98,7 +100,7 @@ def publish_toc_from_doctree(
98100

99101

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

140142
def publish_html_from_source(
141143
source: str,
142-
**kwargs: "Unpack[PublishHtmlDocTreeKwargs]",
143-
) -> t.Optional[str]:
144+
**kwargs: Unpack[PublishHtmlDocTreeKwargs],
145+
) -> str | None:
144146
"""Return HTML from reStructuredText source string."""
145147
doctree = publish_doctree(source)
146148
return publish_html_from_doctree(doctree, **kwargs)
@@ -150,7 +152,7 @@ def publish_html_from_doctree(
150152
doctree: nodes.document,
151153
show_title: bool = True,
152154
toc_only: bool = False,
153-
) -> t.Optional[str]:
155+
) -> str | None:
154156
"""Return HTML from reStructuredText document (doctree).
155157
156158
Parameters

src/django_docutils/lib/roles/common.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Core parts for Django Docutils roles."""
22

3+
from __future__ import annotations
4+
35
import typing as t
46

57
from docutils import nodes, utils
@@ -13,9 +15,9 @@
1315
def generic_url_role(
1416
name: str,
1517
text: str,
16-
url_handler_fn: "UrlHandlerFn",
17-
innernodeclass: type[t.Union[nodes.Text, nodes.TextElement]] = nodes.Text,
18-
) -> "RoleFnReturnValue":
18+
url_handler_fn: UrlHandlerFn,
19+
innernodeclass: type[nodes.Text | nodes.TextElement] = nodes.Text,
20+
) -> RoleFnReturnValue:
1921
"""Docutils Role for Django Docutils.
2022
2123
This generic role also handles explicit titles (``:role:`yata yata <target>```)
@@ -78,8 +80,8 @@ def url_handler(target):
7880
def generic_remote_url_role(
7981
name: str,
8082
text: str,
81-
url_handler_fn: "RemoteUrlHandlerFn",
82-
innernodeclass: type[t.Union[nodes.Text, nodes.TextElement]] = nodes.Text,
83+
url_handler_fn: RemoteUrlHandlerFn,
84+
innernodeclass: type[nodes.Text | nodes.TextElement] = nodes.Text,
8385
) -> tuple[list[nodes.reference], list[t.Any]]:
8486
"""Docutils Role that can call an external data source for title and URL.
8587

src/django_docutils/lib/roles/email.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""Email role for docutils."""
22

3-
import typing as t
3+
from __future__ import annotations
44

5-
from docutils.parsers.rst.states import Inliner
5+
import typing as t
66

77
from .common import generic_url_role
8-
from .types import RoleFnReturnValue
8+
9+
if t.TYPE_CHECKING:
10+
from docutils.parsers.rst.states import Inliner
11+
12+
from .types import RoleFnReturnValue
913

1014

1115
def email_role(
@@ -14,8 +18,8 @@ def email_role(
1418
text: str,
1519
lineno: int,
1620
inliner: Inliner,
17-
options: t.Optional[dict[str, t.Any]] = None,
18-
content: t.Optional[str] = None,
21+
options: dict[str, t.Any] | None = None,
22+
content: str | None = None,
1923
) -> RoleFnReturnValue:
2024
"""Role for linking to email articles.
2125

0 commit comments

Comments
 (0)