361 lines
13 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

diff --git a/.github/workflows/codeqa-test.yml b/.github/workflows/codeqa-test.yml
index c068ee3..5f0bb03 100644
--- a/.github/workflows/codeqa-test.yml
+++ b/.github/workflows/codeqa-test.yml
@@ -19,9 +19,9 @@ jobs:
path: ~/.cache/pip
key: pip-lint
- name: Install dependencies
- run: pip install flake8 isort
+ run: pip install pyproject-flake8 isort
- name: Run flake8
- run: flake8 sphinx_autodoc_typehints.py tests
+ run: pflake8 sphinx_autodoc_typehints.py tests
- name: Run isort
run: isort -c sphinx_autodoc_typehints.py tests
@@ -31,7 +31,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
- python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-alpha.5]
+ python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
diff --git a/pre-commit-config.sample.yaml b/.pre-commit-config.yaml
similarity index 85%
rename from pre-commit-config.sample.yaml
rename to .pre-commit-config.yaml
index 7f86457..00f00ce 100644
--- a/pre-commit-config.sample.yaml
+++ b/.pre-commit-config.yaml
@@ -22,3 +22,7 @@ repos:
hooks:
- id: isort
additional_dependencies: [toml]
+- repo: https://github.com/csachs/pyproject-flake8
+ rev: v0.0.1a2.post1
+ hooks:
+ - id: pyproject-flake8
diff --git a/pyproject.toml b/pyproject.toml
index eeb699e..68d7eab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,6 +14,9 @@ multi_line_output = 4
[tool.autopep8]
max_line_length = 99
+[tool.flake8]
+max-line-length = 99
+
[tool.pytest.ini_options]
addopts = "-rsx --tb=short"
testpaths = ["tests"]
diff --git a/setup.cfg b/setup.cfg
index c872ff3..4e57ff5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -38,10 +38,3 @@ test =
Sphinx >= 3.2.0
type_comments =
typed_ast >= 1.4.0; python_version < "3.8"
-
-[flake8]
-max-line-length = 99
-
-[tool:pytest]
-addopts = -rsx --tb=short
-testpaths = tests
diff --git a/sphinx_autodoc_typehints.py b/sphinx_autodoc_typehints.py
index 0b69b1c..5a4ed95 100644
--- a/sphinx_autodoc_typehints.py
+++ b/sphinx_autodoc_typehints.py
@@ -170,13 +170,19 @@ def normalize_source_lines(sourcelines: str) -> str:
for i, l in enumerate(sourcelines):
if l.lstrip().startswith("def"):
idx = i
+ whitespace_separator = "def"
break
+ elif l.lstrip().startswith("async def"):
+ idx = i
+ whitespace_separator = "async def"
+ break
+
else:
return "\n".join(sourcelines)
fn_def = sourcelines[idx]
# Get a string representing the amount of leading whitespace
- whitespace = fn_def.split("def")[0]
+ whitespace = fn_def.split(whitespace_separator)[0]
# Add this leading whitespace to all lines before and after the `def`
aligned_prefix = [whitespace + remove_prefix(s, whitespace) for s in sourcelines[:idx]]
@@ -251,15 +257,37 @@ def process_signature(app, what: str, name: str, obj, options, signature, return
return stringify_signature(signature).replace('\\', '\\\\'), None
+def _future_annotations_imported(obj):
+ if sys.version_info < (3, 7):
+ # Only Python ≥ 3.7 supports PEP563.
+ return False
+
+ _annotations = getattr(inspect.getmodule(obj), "annotations", None)
+ if _annotations is None:
+ return False
+
+ # Make sure that annotations is imported from __future__
+ CO_FUTURE_ANNOTATIONS = 0x1000000 # defined in cpython/Lib/__future__.py
+ return _annotations.compiler_flag == CO_FUTURE_ANNOTATIONS
+
+
def get_all_type_hints(obj, name):
rv = {}
try:
rv = get_type_hints(obj)
- except (AttributeError, TypeError, RecursionError):
+ except (AttributeError, TypeError, RecursionError) as exc:
# Introspecting a slot wrapper will raise TypeError, and and some recursive type
# definitions will cause a RecursionError (https://github.com/python/typing/issues/574).
- pass
+
+ # If one is using PEP563 annotations, Python will raise a (e.g.,)
+ # TypeError("TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'")
+ # on 'str | None', therefore we accept TypeErrors with that error message
+ # if 'annotations' is imported from '__future__'.
+ if (isinstance(exc, TypeError)
+ and _future_annotations_imported(obj)
+ and "unsupported operand type" in str(exc)):
+ rv = obj.__annotations__
except NameError as exc:
logger.warning('Cannot resolve forward reference in type annotations of "%s": %s',
name, exc)
diff --git a/tests/roots/test-dummy/dummy_module_future_annotations.py b/tests/roots/test-dummy/dummy_module_future_annotations.py
new file mode 100644
index 0000000..119159d
--- /dev/null
+++ b/tests/roots/test-dummy/dummy_module_future_annotations.py
@@ -0,0 +1,11 @@
+from __future__ import annotations
+
+
+def function_with_py310_annotations(self, x: bool, y: int, z: str | None = None) -> str:
+ """
+ Method docstring.
+
+ :param x: foo
+ :param y: bar
+ :param z: baz
+ """
diff --git a/tests/roots/test-dummy/future_annotations.rst b/tests/roots/test-dummy/future_annotations.rst
new file mode 100644
index 0000000..3d774cb
--- /dev/null
+++ b/tests/roots/test-dummy/future_annotations.rst
@@ -0,0 +1,4 @@
+Dummy Module
+============
+
+.. autofunction:: dummy_module_future_annotations.function_with_py310_annotations
diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py
index 7ef8d73..ca43b71 100644
--- a/tests/test_sphinx_autodoc_typehints.py
+++ b/tests/test_sphinx_autodoc_typehints.py
@@ -6,13 +6,14 @@ import typing
from typing import (
IO, Any, AnyStr, Callable, Dict, Generic, Mapping, Match, NewType, Optional, Pattern, Tuple,
Type, TypeVar, Union)
+from unittest.mock import patch
import pytest
import typing_extensions
from sphinx_autodoc_typehints import (
format_annotation, get_annotation_args, get_annotation_class_name, get_annotation_module,
- process_docstring)
+ normalize_source_lines, process_docstring)
T = TypeVar('T')
U = TypeVar('U', covariant=True)
@@ -85,6 +86,10 @@ class Metaclass(type):
pytest.param(A.Inner, __name__, 'A.Inner', (), id='Inner')
])
def test_parse_annotation(annotation, module, class_name, args):
+ if sys.version_info[:2] >= (3, 10) and annotation == W:
+ module = "test_sphinx_autodoc_typehints"
+ class_name = "W"
+ args = ()
assert get_annotation_module(annotation) == module
assert get_annotation_class_name(annotation, module) == class_name
assert get_annotation_args(annotation, module, class_name) == args
@@ -127,7 +132,10 @@ def test_parse_annotation(annotation, module, class_name, args):
':py:data:`~typing.Any`]',
marks=pytest.mark.skipif((3, 5, 0) <= sys.version_info[:3] <= (3, 5, 2),
reason='Union erases the str on 3.5.0 -> 3.5.2')),
- (Optional[str], ':py:data:`~typing.Optional`\\[:py:class:`str`]'),
+ (Optional[str], ':py:data:`~typing.Optional`\\' + (
+ '[:py:class:`str`]'
+ if sys.version_info[:2] < (3, 10) else
+ '[:py:class:`str`, :py:obj:`None`]')),
(Optional[Union[str, bool]], ':py:data:`~typing.Union`\\[:py:class:`str`, '
':py:class:`bool`, :py:obj:`None`]'),
(Callable, ':py:data:`~typing.Callable`'),
@@ -151,7 +159,10 @@ def test_parse_annotation(annotation, module, class_name, args):
(D, ':py:class:`~%s.D`' % __name__),
(E, ':py:class:`~%s.E`' % __name__),
(E[int], ':py:class:`~%s.E`\\[:py:class:`int`]' % __name__),
- (W, ':py:func:`~typing.NewType`\\(:py:data:`~W`, :py:class:`str`)')
+ (W, ':py:func:`~typing.NewType`\\(:py:data:`~W`, :py:class:`str`)'
+ if sys.version_info[:2] < (3, 10) else
+ ':py:class:`~test_sphinx_autodoc_typehints.W`'
+ )
])
def test_format_annotation(inv, annotation, expected_result):
result = format_annotation(annotation)
@@ -223,17 +234,39 @@ def test_process_docstring_slot_wrapper():
assert not lines
-@pytest.mark.parametrize('always_document_param_types', [True, False])
-@pytest.mark.sphinx('text', testroot='dummy')
-def test_sphinx_output(app, status, warning, always_document_param_types):
+def set_python_path():
test_path = pathlib.Path(__file__).parent
# Add test directory to sys.path to allow imports of dummy module.
if str(test_path) not in sys.path:
sys.path.insert(0, str(test_path))
+
+def maybe_fix_py310(expected_contents):
+ if sys.version_info[:2] >= (3, 10):
+ for old, new in [
+ ("*str** | **None*", '"Optional"["str", "None"]'),
+ ("(*bool*)", '("bool")'),
+ ("(*int*)", '("int")'),
+ (" str", ' "str"'),
+ ('"Optional"["str"]', '"Optional"["str", "None"]'),
+ ('"Optional"["Callable"[["int", "bytes"], "int"]]',
+ '"Optional"["Callable"[["int", "bytes"], "int"], "None"]'),
+ ]:
+ expected_contents = expected_contents.replace(old, new)
+ return expected_contents
+
+
+@pytest.mark.parametrize('always_document_param_types', [True, False])
+@pytest.mark.sphinx('text', testroot='dummy')
+@patch('sphinx.writers.text.MAXWIDTH', 2000)
+def test_sphinx_output(app, status, warning, always_document_param_types):
+ set_python_path()
+
app.config.always_document_param_types = always_document_param_types
app.config.autodoc_mock_imports = ['mailbox']
+ if sys.version_info < (3, 7):
+ app.config.autodoc_mock_imports.append('dummy_module_future_annotations')
app.build()
assert 'build succeeded' in status.getvalue() # Build succeeded
@@ -352,7 +385,7 @@ def test_sphinx_output(app, status, warning, always_document_param_types):
Return type:
"str"
- property a_property
+ property a_property: str
Property docstring
@@ -489,8 +522,7 @@ def test_sphinx_output(app, status, warning, always_document_param_types):
Method docstring.
Parameters:
- **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) --
- foo
+ **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo
Return type:
"ClassWithTypehintsNotInline"
@@ -506,9 +538,7 @@ def test_sphinx_output(app, status, warning, always_document_param_types):
Class docstring.{undoc_params_0}
- __init__(x)
-
- Initialize self. See help(type(self)) for accurate signature.{undoc_params_1}
+ __init__(x){undoc_params_1}
@dummy_module.Decorator(func)
@@ -525,4 +555,56 @@ def test_sphinx_output(app, status, warning, always_document_param_types):
**x** ("Mailbox") -- function
''')
expected_contents = expected_contents.format(**format_args).replace('', '--')
- assert text_contents == expected_contents
+ assert text_contents == maybe_fix_py310(expected_contents)
+
+
+@pytest.mark.skipif(sys.version_info < (3, 7),
+ reason="Future annotations are not implemented in Python < 3.7")
+@pytest.mark.sphinx('text', testroot='dummy')
+@patch('sphinx.writers.text.MAXWIDTH', 2000)
+def test_sphinx_output_future_annotations(app, status, warning):
+ set_python_path()
+
+ app.config.master_doc = "future_annotations"
+ app.build()
+
+ assert 'build succeeded' in status.getvalue() # Build succeeded
+
+ text_path = pathlib.Path(app.srcdir) / '_build' / 'text' / 'future_annotations.txt'
+ with text_path.open('r') as f:
+ text_contents = f.read().replace('', '--')
+ expected_contents = textwrap.dedent('''\
+ Dummy Module
+ ************
+
+ dummy_module_future_annotations.function_with_py310_annotations(self, x, y, z=None)
+
+ Method docstring.
+
+ Parameters:
+ * **x** (*bool*) -- foo
+
+ * **y** (*int*) -- bar
+
+ * **z** (*str** | **None*) -- baz
+
+ Return type:
+ str
+ ''')
+ assert text_contents == maybe_fix_py310(expected_contents)
+
+
+def test_normalize_source_lines_async_def():
+ source = textwrap.dedent("""
+ async def async_function():
+ class InnerClass:
+ def __init__(self): pass
+ """)
+
+ expected = textwrap.dedent("""
+ async def async_function():
+ class InnerClass:
+ def __init__(self): pass
+ """)
+
+ assert normalize_source_lines(source) == expected
diff --git a/tox.ini b/tox.ini
index 54e9bb4..57e4a78 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,6 +9,6 @@ extras = test, type_comments
commands = python -m pytest {posargs}
[testenv:flake8]
-deps = flake8
-commands = flake8 sphinx_autodoc_typehints.py tests
+deps = pyproject-flake8
+commands = pflake8 sphinx_autodoc_typehints.py tests
skip_install = true