Skip to content

Commit 97279c4

Browse files
committed
Support future and string annotations (#155)
1 parent e3733d3 commit 97279c4

File tree

6 files changed

+51
-32
lines changed

6 files changed

+51
-32
lines changed

docs/src/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Unreleased
1313
- Declare support for Python 3.12 and 3.13 (:issue:`150`)
1414
- Remove support for Python 3.7 and 3.8 (:issue:`150`)
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
-------------------

src/sphinx_codeautolink/extension/resolve.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from functools import cache
88
from importlib import import_module
99
from inspect import isclass, isroutine
10-
from typing import Any, Callable, Union
10+
from types import UnionType
11+
from typing import Any, Callable, Union, get_type_hints
1112

1213
from sphinx_codeautolink.parse import Name, NameBreak
1314

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

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

122122
# 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
123+
origin = getattr(annotation, "__origin__", None)
124+
args = getattr(annotation, "__args__", None)
125+
if (origin is Union or isinstance(annotation, UnionType)) and len(args) == 2: # noqa: PLR2004
126126
nonetype = type(None)
127127
if args[0] is nonetype:
128-
ret_annotation = args[1]
128+
annotation = args[1]
129129
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)
130+
annotation = args[0]
137131

138132
if (
139-
not ret_annotation
140-
or not isinstance(ret_annotation, type)
141-
or hasattr(ret_annotation, "__origin__")
133+
not annotation
134+
or not isinstance(annotation, type)
135+
or hasattr(annotation, "__origin__")
142136
):
143137
msg = f"Unable to follow return annotation of {get_name_for_debugging(func)}."
144138
raise CouldNotResolve(msg)
145139

146-
return ret_annotation
140+
return annotation
147141

148142

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

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

tests/extension/ref/ref_optional_manual.txt

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# noqa: INP001
2+
from __future__ import annotations
3+
4+
from typing import Optional
5+
6+
7+
class Foo:
8+
"""Foo test class."""
9+
10+
attr: str = "test"
11+
12+
13+
def optional() -> Optional[Foo]: # noqa: UP007
14+
"""Return optional type."""
15+
16+
17+
def optional_manual() -> None | Foo:
18+
"""Return manually constructed optional type."""

0 commit comments

Comments
 (0)