Skip to content

Commit 9dd69fa

Browse files
authored
Support future and string annotations (#155)
* Support future and string annotations (#155) * Remove support for Python 3.9
1 parent e3733d3 commit 9dd69fa

File tree

14 files changed

+67
-54
lines changed

14 files changed

+67
-54
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
matrix:
1616
strategy:
1717
matrix:
18-
python-version: ["3.9", "3.10", "3.11", "3.12"]
18+
python-version: ["3.10", "3.11", "3.12"]
1919
name: Pytest on ${{matrix.python-version}}
2020
runs-on: ubuntu-latest
2121

docs/src/release_notes.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ sphinx-codeautolink adheres to
1111
Unreleased
1212
----------
1313
- Declare support for Python 3.12 and 3.13 (:issue:`150`)
14-
- Remove support for Python 3.7 and 3.8 (:issue:`150`)
14+
- Remove support for Python 3.7-3.9 (:issue:`150`, :issue:`157`)
1515
- Fix changed whitespace handling in Pygments 2.19 (:issue:`152`)
16+
- Improve support for future and string annotations (:issue:`155`)
1617

1718
0.15.2 (2024-06-03)
1819
-------------------

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ readme = "readme_pypi.rst"
99
license = {file = "LICENSE"}
1010
dynamic = ["version"]
1111

12-
requires-python = ">=3.9"
12+
requires-python = ">=3.10"
1313
dependencies = [
1414
"sphinx>=3.2.0",
1515
"beautifulsoup4>=4.8.1",
@@ -26,7 +26,6 @@ classifiers = [
2626
"Framework :: Sphinx :: Extension",
2727
"Programming Language :: Python :: 3",
2828
"Programming Language :: Python :: 3 :: Only",
29-
"Programming Language :: Python :: 3.9",
3029
"Programming Language :: Python :: 3.10",
3130
"Programming Language :: Python :: 3.11",
3231
"Programming Language :: Python :: 3.12",

src/sphinx_codeautolink/extension/block.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from __future__ import annotations
44

55
import re
6+
from collections.abc import Callable
67
from copy import copy
78
from dataclasses import dataclass
89
from pathlib import Path
9-
from typing import Callable
1010

1111
from bs4 import BeautifulSoup
1212
from docutils import nodes
@@ -263,7 +263,7 @@ def _format_source_for_error(
263263
guides[ix] = "block source:"
264264
pad = max(len(i) + 1 for i in guides)
265265
guides = [g.ljust(pad) for g in guides]
266-
return "\n".join([g + s for g, s in zip(guides, lines)])
266+
return "\n".join([g + s for g, s in zip(guides, lines, strict=True)])
267267

268268
def _parsing_error_msg(self, error: Exception, language: str, source: str) -> str:
269269
return "\n".join(

src/sphinx_codeautolink/extension/directive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,5 @@ def unknown_visit(self, node) -> None:
136136
if isinstance(node, DeferredExamples):
137137
# Remove surrounding paragraph too
138138
node.parent.parent.remove(node.parent)
139-
if isinstance(node, (ConcatMarker, PrefaceMarker, SkipMarker)):
139+
if isinstance(node, ConcatMarker | PrefaceMarker | SkipMarker):
140140
node.parent.remove(node)

src/sphinx_codeautolink/extension/resolve.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Callable
56
from contextlib import suppress
67
from dataclasses import dataclass
78
from functools import cache
89
from importlib import import_module
910
from inspect import isclass, isroutine
10-
from typing import Any, Callable, Union
11+
from types import UnionType
12+
from typing import Any, Union, get_type_hints
1113

1214
from sphinx_codeautolink.parse import Name, NameBreak
1315

@@ -116,34 +118,27 @@ def call_value(cursor: Cursor) -> None:
116118

117119
def get_return_annotation(func: Callable) -> type | None:
118120
"""Determine the target of a function return type hint."""
119-
annotations = getattr(func, "__annotations__", {})
120-
ret_annotation = annotations.get("return", None)
121+
annotation = get_type_hints(func).get("return")
121122

122123
# Inner type from typing.Optional or Union[None, T]
123-
origin = getattr(ret_annotation, "__origin__", None)
124-
args = getattr(ret_annotation, "__args__", None)
125-
if origin is Union and len(args) == 2: # noqa: PLR2004
124+
origin = getattr(annotation, "__origin__", None)
125+
args = getattr(annotation, "__args__", None)
126+
if (origin is Union or isinstance(annotation, UnionType)) and len(args) == 2: # noqa: PLR2004
126127
nonetype = type(None)
127128
if args[0] is nonetype:
128-
ret_annotation = args[1]
129+
annotation = args[1]
129130
elif args[1] is nonetype:
130-
ret_annotation = args[0]
131-
132-
# Try to resolve a string annotation in the module scope
133-
if isinstance(ret_annotation, str):
134-
location = fully_qualified_name(func)
135-
mod, _ = closest_module(tuple(location.split(".")))
136-
ret_annotation = getattr(mod, ret_annotation, ret_annotation)
131+
annotation = args[0]
137132

138133
if (
139-
not ret_annotation
140-
or not isinstance(ret_annotation, type)
141-
or hasattr(ret_annotation, "__origin__")
134+
not annotation
135+
or not isinstance(annotation, type)
136+
or hasattr(annotation, "__origin__")
142137
):
143138
msg = f"Unable to follow return annotation of {get_name_for_debugging(func)}."
144139
raise CouldNotResolve(msg)
145140

146-
return ret_annotation
141+
return annotation
147142

148143

149144
def fully_qualified_name(thing: type | Callable) -> str:

src/sphinx_codeautolink/parse.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def visit_Import(self, node: ast.Import | ast.ImportFrom, prefix: str = "") -> N
432432
if prefix:
433433
self.save_access(Access(LinkContext.import_from, [], prefix_components))
434434

435-
for import_name, alias in zip(import_names, aliases):
435+
for import_name, alias in zip(import_names, aliases, strict=True):
436436
if not import_star:
437437
components = [
438438
Component(n, *linenos(node), "load") for n in import_name.split(".")
@@ -561,7 +561,7 @@ def visit_MatchClass(self, node: ast.AST) -> None:
561561
accesses.append(access)
562562

563563
assigns = []
564-
for attr, pattern in zip(node.kwd_attrs, node.kwd_patterns):
564+
for attr, pattern in zip(node.kwd_attrs, node.kwd_patterns, strict=True):
565565
target = self.visit(pattern)
566566
attr_comps = [
567567
Component(NameBreak.call, *linenos(node), "load"),

tests/extension/__init__.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@
3030

3131
any_whitespace = re.compile(r"\s*")
3232
ref_tests = [(p.name, p) for p in Path(__file__).with_name("ref").glob("*.txt")]
33-
ref_xfails = {
34-
"ref_fluent_attrs.txt": sys.version_info < (3, 8),
35-
"ref_fluent_call.txt": sys.version_info < (3, 8),
36-
"ref_import_from_complex.txt": sys.version_info < (3, 8),
37-
}
33+
ref_xfails = {}
3834

3935

4036
def assert_links(file: Path, links: list):
@@ -44,7 +40,7 @@ def assert_links(file: Path, links: list):
4440
strings = [any_whitespace.sub("", "".join(b.strings)) for b in blocks]
4541

4642
assert len(strings) == len(links)
47-
for s, link in zip(strings, links):
43+
for s, link in zip(strings, links, strict=False):
4844
assert s == link
4945

5046

@@ -119,7 +115,7 @@ def test_tables(file: Path, tmp_path: Path):
119115
strings = [any_whitespace.sub("", "".join(b.strings)) for b in blocks]
120116

121117
assert len(strings) == len(links)
122-
for s, link in zip(strings, links):
118+
for s, link in zip(strings, links, strict=False):
123119
assert s == link
124120

125121

tests/extension/ref/ref_optional.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
test_project
22
test_project.optional
33
attr
4+
test_project.optional_manual
5+
attr
46
# split
57
# split
68
Test project
@@ -10,5 +12,6 @@ Test project
1012

1113
import test_project
1214
test_project.optional().attr
15+
test_project.optional_manual().attr
1316

1417
.. automodule:: test_project
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
future_project
2+
future_project.optional
3+
attr
4+
future_project.optional_manual
5+
attr
6+
# split
7+
# split
8+
Test project
9+
============
10+
11+
.. code:: python
12+
13+
import future_project
14+
future_project.optional().attr
15+
future_project.optional_manual().attr
16+
17+
.. automodule:: future_project

0 commit comments

Comments
 (0)