pax_global_header00006660000000000000000000000064135545445170014527gustar00rootroot0000000000000052 comment=3ac21e85537a6406d886ce99314bdb3a49b0b5d4 sphinx-autodoc-typehints-1.9.0/000077500000000000000000000000001355454451700165305ustar00rootroot00000000000000sphinx-autodoc-typehints-1.9.0/.gitignore000066400000000000000000000001571355454451700205230ustar00rootroot00000000000000.project .pydevproject .idea .tox .coverage .cache .eggs/ *.egg-info/ *.pyc __pycache__/ dist/ build/ .vscode/ sphinx-autodoc-typehints-1.9.0/.travis.yml000066400000000000000000000031541355454451700206440ustar00rootroot00000000000000dist: xenial language: python python: "3.6" stages: - name: static analysis - name: test - name: deploy to pypi if: type = push AND tag =~ ^\d+\.\d+\.\d+ jobs: include: - stage: static analysis env: TOXENV=flake8 - stage: test env: TOXENV=py35 python: "3.5" after_success: &after_success - pip install coveralls - coveralls - stage: test env: TOXENV=py36 python: "3.6" after_success: *after_success - stage: test env: TOXENV=py37 python: "3.7" after_success: *after_success - stage: test env: TOXENV=py38 python: "3.8" after_success: *after_success - stage: deploy to pypi install: skip script: skip deploy: provider: pypi user: agronholm password: secure: duaV12IvSrtlrjcqkbOToB0YTQkFRMM3SADKPVL4JapNYbhGCHsNgauAptnIZrTIFy3B2ZQH5QxOu1xapR3LHbwCrh9VV6QYTU1BFV8ju5gTcnCWcuN0Sr42LuwB3v5sCjijMrNIfo04ovhgJKCPfOiFV3bsXv+PSUm221qLixG8vHmoP2Vqhb+8+McV/JeMMjxfMv/XFb3fWoQwaspERVu/Xt4f/taJ7JFNOJBjYYwYY79mxE6TJOTypgnrgypO0YyqjrvVsjFNuCH3QeQYtDIcJRTekp/Oo9hiNt6T4nuf3X09F9vKhFuGXtpmdwjnIktQb2jkP4FSHGJ3z/6UJP7yPgMXaFezzih5WjBVuMwDu9HOo4EHE+0hgkL5aQfbFulF2moE7PGEqhTWZkEzxGKc/ds+YbfYGigrcpuCm+KvDtQHAUkrIa8mEw5wM5+QGiiBGEzxZ6ifsZzxADEoCNshU3r6rHBWlA4ze5Q0PFCC7Jns2uqe51+9qqBz+cGKjQafn+1DwGBIr/tZusx8cjRJpsvZ116Zq7viCfzBmxEt4yA5UPYmpljS7bBSJrbrXVRNZGmAm9oO5adI99MnrQsDdVMM3KoC0R3JOmiGaKuM573am57EZ6c/hKKqyLs4MS6WLkYbygNPq3N0bQG6JKtvVKGPB1xTA116Mve6cJc= distributions: sdist bdist_wheel on: tags: true install: pip install tox script: tox sphinx-autodoc-typehints-1.9.0/CHANGELOG.rst000066400000000000000000000114051355454451700205520ustar00rootroot000000000000001.9.0 ===== * Added support for typing_extensions_ * Added the ``typehints_document_rtype`` option (PR by Simon-Martin Schröder) * Fixed metaclasses as annotations causing ``TypeError`` * Fixed rendering of ``typing.Literal`` * Fixed OSError when generating docs for SQLAlchemy mapped classes * Fixed unparametrized generic classes being rendered with their type parameters (e.g. ``Dict[~KT, ~VT]``) .. _typing_extensions: https://pypi.org/project/typing-extensions/ 1.8.0 ===== * Fixed regression which caused ``TypeError`` or ``OSError`` when trying to set annotations due to PR #87 * Fixed unintentional mangling of annotation type names * Added proper ``:py:data`` targets for ``NoReturn``, ``ClassVar`` and ``Tuple`` * Added support for inline type comments (like ``(int, str) -> None``) (PR by Bernát Gábor) * Use the native AST parser for type comment support on Python 3.8+ 1.7.0 ===== * Dropped support for Python 3.4 * Fixed unwrapped local functions causing errors (PR by Kimiyuki Onaka) * Fixed ``AttributeError`` when documenting the ``__init__()`` method of a data class * Added support for type hint comments (PR by Markus Unterwaditzer) * Added flag for rendering classes with their fully qualified names (PR by Holly Becker) 1.6.0 ===== * Fixed ``TypeError`` when formatting annotations from a class that inherits from a concrete generic type (report and tests by bpeake-illuscio) * Added support for ``typing_extensions.Protocol`` (PR by Ian Good) * Added support for ``typing.NewType`` (PR by George Leslie-Waksman) 1.5.2 ===== * Emit a warning instead of crashing when an unresolvable forward reference is encountered in type annotations 1.5.1 ===== * Fixed escape characters in parameter default values getting lost during signature processing * Replaced use of the ``config-inited`` event (which inadvertently required Sphinx 1.8) with the ``builder-inited`` event 1.5.0 ===== * The setting of the ``typing.TYPECHECKING`` flag is now configurable using the ``set_type_checking_flag`` option 1.4.0 ===== * The extension now sets ``typing.TYPECHECKING`` to ``True`` during setup to include conditional imports which may be used in type annotations * Fixed parameters with trailing underscores (PR by Daniel Knell) * Fixed KeyError with private methods (PR by Benito Palacios Sánchez) * Fixed deprecation warning about the use of formatargspec (PR by Y. Somda) * The minimum Sphinx version is now v1.7.0 1.3.1 ===== * Fixed rendering of generic types outside the typing module (thanks to Tim Poterba for the PR) 1.3.0 ===== * Fixed crash when processing docstrings from nested classes (thanks to dilyanpalauzov for the fix) * Added support for Python 3.7 * Dropped support for Python 3.5.0 and 3.5.1 1.2.5 ===== * Ensured that ``:rtype:`` doesn't get joined with a paragraph of text (thanks to Bruce Merry for the PR) 1.2.4 ===== * Removed support for ``backports.typing`` as it has been removed from the PyPI * Fixed first parameter being cut out from class methods and static methods (thanks to Josiah Wolf Oberholtzer for the PR) 1.2.3 ===== * Fixed `process_signature()` clobbering any explicitly overridden signatures from the docstring 1.2.2 ===== * Explicitly prefix ``:class:``, ``:mod:`` et al with ``:py:``, in case ``py`` is not the default domain of the project (thanks Monty Taylor) 1.2.1 ===== * Fixed `ValueError` when `getargspec()` encounters a built-in function * Fixed `AttributeError` when `Any` is combined with another type in a `Union` (thanks Davis Kirkendall) 1.2.0 ===== * Fixed compatibility with Python 3.6 and 3.5.3 * Fixed ``NameError`` when processing signatures of wrapped functions with type hints * Fixed handling of slotted classes with no ``__init__()`` method * Fixed Sphinx warning about parallel reads * Fixed return type being added to class docstring from its ``__init__()`` method (thanks to Manuel Krebber for the patch) * Fixed return type hints of ``@property`` methods being omitted (thanks to pknight for the patch) * Added a test suite (thanks Manuel Krebber) 1.1.0 ===== * Added proper support for ``typing.Tuple`` (pull request by Manuel Krebber) 1.0.6 ===== * Fixed wrong placement of ``:rtype:`` if a multi-line ``:param:`` or a ``:returns:`` is used 1.0.5 ===== * Fixed coroutine functions' signatures not being processed when using sphinxcontrib-asyncio 1.0.4 ===== * Fixed compatibility with Sphinx 1.4 1.0.3 ===== * Fixed "self" parameter not being removed from exception class constructor signatures * Fixed process_signature() erroneously removing the first argument of a static method 1.0.2 ===== * Fixed exception classes not being processed like normal classes 1.0.1 ===== * Fixed errors caused by forward references not being looked up with the right globals 1.0.0 ===== * Initial release sphinx-autodoc-typehints-1.9.0/LICENSE000066400000000000000000000021521355454451700175350ustar00rootroot00000000000000This is the MIT license: http://www.opensource.org/licenses/mit-license.php Copyright (c) Alex Grönholm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sphinx-autodoc-typehints-1.9.0/README.rst000066400000000000000000000104121355454451700202150ustar00rootroot00000000000000sphinx-autodoc-typehints ======================== This extension allows you to use Python 3 annotations for documenting acceptable argument types and return value types of functions. This allows you to use type hints in a very natural fashion, allowing you to migrate from this: .. code-block:: python def format_unit(value, unit): """ Formats the given value as a human readable string using the given units. :param float|int value: a numeric value :param str unit: the unit for the value (kg, m, etc.) :rtype: str """ return '{} {}'.format(value, unit) to this: .. code-block:: python from typing import Union def format_unit(value: Union[float, int], unit: str) -> str: """ Formats the given value as a human readable string using the given units. :param value: a numeric value :param unit: the unit for the value (kg, m, etc.) """ return '{} {}'.format(value, unit) Installation and setup ---------------------- First, use pip to download and install the extension:: $ pip install sphinx-autodoc-typehints Then, add the extension to your ``conf.py``: .. code-block:: python extensions = [ 'sphinx.ext.autodoc', 'sphinx_autodoc_typehints' ] Options ------- The following configuration options are accepted: * ``set_type_checking_flag`` (default: ``False``): if ``True``, set ``typing.TYPE_CHECKING`` to ``True`` to enable "expensive" typing imports * ``typehints_fully_qualified`` (default: ``False``): if ``True``, class names are always fully qualified (e.g. ``module.for.Class``). If ``False``, just the class name displays (e.g. ``Class``) * ``always_document_param_types`` (default: ``False``): If ``False``, do not add type info for undocumented parameters. If ``True``, add stub documentation for undocumented parameters to be able to add type info. * ``typehints_document_rtype`` (default: ``True``): If ``False``, never add an ``:rtype:`` directive. If ``True``, add the ``:rtype:`` directive if no existing ``:rtype:`` is found. How it works ------------ The extension listens to the ``autodoc-process-signature`` and ``autodoc-process-docstring`` Sphinx events. In the former, it strips the annotations from the function signature. In the latter, it injects the appropriate ``:type argname:`` and ``:rtype:`` directives into the docstring. Only arguments that have an existing ``:param:`` directive in the docstring get their respective ``:type:`` directives added. The ``:rtype:`` directive is added if and only if no existing ``:rtype:`` is found. Compatibility with sphinx.ext.napoleon -------------------------------------- To use `sphinx.ext.napoleon`_ with sphinx-autodoc-typehints, make sure you load `sphinx.ext.napoleon`_ first, **before** sphinx-autodoc-typehints. See `Issue 15`_ on the issue tracker for more information. .. _sphinx.ext.napoleon: http://www.sphinx-doc.org/en/stable/ext/napoleon.html .. _Issue 15: https://github.com/agronholm/sphinx-autodoc-typehints/issues/15 Dealing with circular imports ----------------------------- Sometimes functions or classes from two different modules need to reference each other in their type annotations. This creates a circular import problem. The solution to this is the following: #. Import only the module, not the classes/functions from it #. Use forward references in the type annotations (e.g. ``def methodname(self, param1: 'othermodule.OtherClass'):``) On Python 3.7, you can even use ``from __future__ import annotations`` and remove the quotes. Using type hint comments ------------------------ If you're documenting code that needs to stay compatible with Python 2.7, you cannot use regular type annotations. Instead, you must either be using Python 3.8 or later or have typed_ast_ installed. The package extras ``type_comments`` will pull in the appropiate dependencies automatically. Then you can add type hint comments in the following manner: .. code-block:: python def myfunction(arg1, arg2): # type: (int, str) -> int return 42 or alternatively: .. code-block:: python def myfunction( arg1, # type: int arg2 # type: str ): # type: (...) -> int return 42 .. _typed_ast: https://pypi.org/project/typed-ast/ sphinx-autodoc-typehints-1.9.0/pyproject.toml000066400000000000000000000002301355454451700214370ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 40.0.4", "setuptools_scm >= 2.0.0", "wheel >= 0.29.0", ] build-backend = 'setuptools.build_meta' sphinx-autodoc-typehints-1.9.0/setup.cfg000066400000000000000000000025661355454451700203620ustar00rootroot00000000000000[metadata] name = sphinx-autodoc-typehints description = Type hints (PEP 484) support for the Sphinx autodoc extension long_description = file: README.rst author = Alex Grönholm author_email = alex.gronholm@nextday.fi license = MIT project_urls = Change log = https://github.com/agronholm/sphinx-autodoc-typehints/blob/master/CHANGELOG.rst Source code = https://github.com/agronholm/sphinx-autodoc-typehints Issue tracker = https://github.com/agronholm/sphinx-autodoc-typehints/issues classifiers = Development Status :: 5 - Production/Stable Framework :: Sphinx :: Extension Intended Audience :: Developers License :: OSI Approved :: MIT License Framework :: Sphinx :: Extension Topic :: Documentation :: Sphinx Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] py_modules = sphinx_autodoc_typehints python_requires = >=3.5.2 install_requires = Sphinx >= 2.1 [options.extras_require] test = pytest >= 3.1.0 typing_extensions >= 3.5 dataclasses; python_version == "3.6" sphobjinv >= 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 sphinx-autodoc-typehints-1.9.0/setup.py000066400000000000000000000003571355454451700202470ustar00rootroot00000000000000from setuptools import setup setup( use_scm_version={ 'version_scheme': 'post-release', 'local_scheme': 'dirty-tag' }, setup_requires=[ 'setuptools_scm >= 1.7.0', 'setuptools >= 36.2.7' ] ) sphinx-autodoc-typehints-1.9.0/sphinx_autodoc_typehints.py000066400000000000000000000352341355454451700242470ustar00rootroot00000000000000import inspect import sys import textwrap import typing from typing import get_type_hints, TypeVar, Generic from sphinx.util import logging from sphinx.util.inspect import Signature try: from typing_extensions import Protocol except ImportError: Protocol = None logger = logging.getLogger(__name__) pydata_annotations = {'Any', 'AnyStr', 'Callable', 'ClassVar', 'Literal', 'NoReturn', 'Optional', 'Tuple', 'Union'} def format_annotation(annotation, fully_qualified=False): if inspect.isclass(annotation) and annotation.__module__ == 'builtins': if annotation.__qualname__ == 'NoneType': return '``None``' else: return ':py:class:`{}`'.format(annotation.__qualname__) annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) if annotation_cls.__module__ in ('typing', 'typing_extensions'): class_name = str(annotation).split('[')[0].split('.')[-1] params = None module = 'typing' extra = '' origin = getattr(annotation, '__origin__', None) if inspect.isclass(origin): annotation_cls = annotation.__origin__ try: mro = annotation_cls.mro() if Generic in mro or (Protocol and Protocol in mro): module = annotation_cls.__module__ except TypeError: pass # annotation_cls was either the "type" object or typing.Type if class_name == 'Any': return ':py:data:`{}typing.Any`'.format("" if fully_qualified else "~") elif class_name == '~AnyStr': return ':py:data:`{}typing.AnyStr`'.format("" if fully_qualified else "~") elif isinstance(annotation, TypeVar): return '\\%r' % annotation elif class_name == 'Union': if hasattr(annotation, '__union_params__'): params = annotation.__union_params__ elif hasattr(annotation, '__args__'): params = annotation.__args__ if params and len(params) == 2 and (hasattr(params[1], '__qualname__') and params[1].__qualname__ == 'NoneType'): class_name = 'Optional' params = (params[0],) elif class_name == 'Tuple' and hasattr(annotation, '__tuple_params__'): params = annotation.__tuple_params__ if annotation.__tuple_use_ellipsis__: params += (Ellipsis,) elif class_name == 'Callable': arg_annotations = result_annotation = None if hasattr(annotation, '__result__'): arg_annotations = annotation.__args__ result_annotation = annotation.__result__ elif getattr(annotation, '__args__', None): arg_annotations = annotation.__args__[:-1] result_annotation = annotation.__args__[-1] if arg_annotations in (Ellipsis, (Ellipsis,)): params = [Ellipsis, result_annotation] elif arg_annotations is not None: params = [ '\\[{}]'.format( ', '.join( format_annotation(param, fully_qualified) for param in arg_annotations)), result_annotation ] elif class_name == 'Literal': annotation_args = getattr(annotation, '__args__', ()) or annotation.__values__ extra = '\\[{}]'.format(', '.join(repr(arg) for arg in annotation_args)) elif class_name == 'ClassVar' and hasattr(annotation, '__type__'): # < py3.7 params = (annotation.__type__,) elif hasattr(annotation, 'type_var'): # Type alias class_name = annotation.name params = (annotation.type_var,) elif getattr(annotation, '__args__', None) is not None: params = annotation.__args__ elif hasattr(annotation, '__parameters__'): params = annotation.__parameters__ if params and annotation is not getattr(sys.modules[module], class_name): extra = '\\[{}]'.format(', '.join( format_annotation(param, fully_qualified) for param in params)) return '{prefix}`{qualify}{module}.{name}`{extra}'.format( prefix=':py:data:' if class_name in pydata_annotations else ':py:class:', qualify="" if fully_qualified else "~", module=module, name=class_name, extra=extra ) elif annotation is Ellipsis: return '...' elif (inspect.isfunction(annotation) and annotation.__module__ == 'typing' and hasattr(annotation, '__name__') and hasattr(annotation, '__supertype__')): return ':py:func:`{qualify}typing.NewType`\\(:py:data:`~{name}`, {extra})'.format( qualify="" if fully_qualified else "~", name=annotation.__name__, extra=format_annotation(annotation.__supertype__, fully_qualified), ) elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)): if not inspect.isclass(annotation): annotation_cls = annotation.__origin__ extra = '' try: mro = annotation_cls.mro() except TypeError: pass else: if Generic in mro or (Protocol and Protocol in mro): params = (getattr(annotation, '__parameters__', None) or getattr(annotation, '__args__', None)) if params: extra = '\\[{}]'.format(', '.join( format_annotation(param, fully_qualified) for param in params)) return ':py:class:`{qualify}{module}.{name}`{extra}'.format( qualify="" if fully_qualified else "~", module=annotation.__module__, name=annotation_cls.__qualname__, extra=extra ) return str(annotation) def process_signature(app, what: str, name: str, obj, options, signature, return_annotation): if not callable(obj): return if what in ('class', 'exception'): obj = getattr(obj, '__init__', getattr(obj, '__new__', None)) if not getattr(obj, '__annotations__', None): return obj = inspect.unwrap(obj) signature = Signature(obj) parameters = [ param.replace(annotation=inspect.Parameter.empty) for param in signature.signature.parameters.values() ] if '' in obj.__qualname__: logger.warning( 'Cannot treat a function defined as a local function: "%s" (use @functools.wraps)', name) return if parameters: if what in ('class', 'exception'): del parameters[0] elif what == 'method': outer = inspect.getmodule(obj) for clsname in obj.__qualname__.split('.')[:-1]: outer = getattr(outer, clsname) method_name = obj.__name__ if method_name.startswith("__") and not method_name.endswith("__"): # If the method starts with double underscore (dunder) # Python applies mangling so we need to prepend the class name. # This doesn't happen if it always ends with double underscore. class_name = obj.__qualname__.split('.')[-2] method_name = "_{c}{m}".format(c=class_name, m=method_name) method_object = outer.__dict__[method_name] if outer else obj if not isinstance(method_object, (classmethod, staticmethod)): del parameters[0] signature.signature = signature.signature.replace( parameters=parameters, return_annotation=inspect.Signature.empty) return signature.format_args().replace('\\', '\\\\'), None def get_all_type_hints(obj, name): rv = {} try: rv = get_type_hints(obj) except (AttributeError, TypeError, RecursionError): # 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 except NameError as exc: logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc) rv = obj.__annotations__ if rv: return rv rv = backfill_type_hints(obj, name) try: obj.__annotations__ = rv except (AttributeError, TypeError): return rv try: rv = get_type_hints(obj) except (AttributeError, TypeError): pass except NameError as exc: logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc) rv = obj.__annotations__ return rv def backfill_type_hints(obj, name): parse_kwargs = {} if sys.version_info < (3, 8): try: import typed_ast.ast3 as ast except ImportError: return {} else: import ast parse_kwargs = {'type_comments': True} def _one_child(module): children = module.body # use the body to ignore type comments if len(children) != 1: logger.warning( 'Did not get exactly one node from AST for "%s", got %s', name, len(children)) return return children[0] try: obj_ast = ast.parse(textwrap.dedent(inspect.getsource(obj)), **parse_kwargs) except (OSError, TypeError): return {} obj_ast = _one_child(obj_ast) if obj_ast is None: return {} try: type_comment = obj_ast.type_comment except AttributeError: return {} if not type_comment: return {} try: comment_args_str, comment_returns = type_comment.split(' -> ') except ValueError: logger.warning('Unparseable type hint comment for "%s": Expected to contain ` -> `', name) return {} rv = {} if comment_returns: rv['return'] = comment_returns args = load_args(obj_ast) comment_args = split_type_comment_args(comment_args_str) is_inline = len(comment_args) == 1 and comment_args[0] == "..." if not is_inline: if args and args[0].arg in ("self", "cls") and len(comment_args) != len(args): comment_args.insert(0, None) # self/cls may be omitted in type comments, insert blank if len(args) != len(comment_args): logger.warning('Not enough type comments found on "%s"', name) return rv for at, arg in enumerate(args): arg_key = getattr(arg, "arg", None) if arg_key is None: continue if is_inline: # the type information now is tied to the argument value = getattr(arg, "type_comment", None) else: # type data from comment value = comment_args[at] if value is not None: rv[arg_key] = value return rv def load_args(obj_ast): func_args = obj_ast.args args = [] pos_only = getattr(func_args, 'posonlyargs', None) if pos_only: args.extend(pos_only) args.extend(func_args.args) if func_args.vararg: args.append(func_args.vararg) args.extend(func_args.kwonlyargs) if func_args.kwarg: args.append(func_args.kwarg) return args def split_type_comment_args(comment): def add(val): result.append(val.strip().lstrip("*")) # remove spaces, and var/kw arg marker comment = comment.strip().lstrip("(").rstrip(")") result = [] if not comment: return result brackets, start_arg_at, at = 0, 0, 0 for at, char in enumerate(comment): if char in ("[", "("): brackets += 1 elif char in ("]", ")"): brackets -= 1 elif char == "," and brackets == 0: add(comment[start_arg_at:at]) start_arg_at = at + 1 add(comment[start_arg_at: at + 1]) return result def process_docstring(app, what, name, obj, options, lines): if isinstance(obj, property): obj = obj.fget if callable(obj): if what in ('class', 'exception'): obj = getattr(obj, '__init__') obj = inspect.unwrap(obj) type_hints = get_all_type_hints(obj, name) for argname, annotation in type_hints.items(): if argname == 'return': continue # this is handled separately later if argname.endswith('_'): argname = '{}\\_'.format(argname[:-1]) formatted_annotation = format_annotation( annotation, fully_qualified=app.config.typehints_fully_qualified) searchfor = ':param {}:'.format(argname) insert_index = None for i, line in enumerate(lines): if line.startswith(searchfor): insert_index = i break if insert_index is None and app.config.always_document_param_types: lines.append(searchfor) insert_index = len(lines) if insert_index is not None: lines.insert( insert_index, ':type {}: {}'.format(argname, formatted_annotation) ) if 'return' in type_hints and what not in ('class', 'exception'): formatted_annotation = format_annotation( type_hints['return'], fully_qualified=app.config.typehints_fully_qualified) insert_index = len(lines) for i, line in enumerate(lines): if line.startswith(':rtype:'): insert_index = None break elif line.startswith(':return:') or line.startswith(':returns:'): insert_index = i if insert_index is not None and app.config.typehints_document_rtype: if insert_index == len(lines): # Ensure that :rtype: doesn't get joined with a paragraph of text, which # prevents it being interpreted. lines.append('') insert_index += 1 lines.insert(insert_index, ':rtype: {}'.format(formatted_annotation)) def builder_ready(app): if app.config.set_type_checking_flag: typing.TYPE_CHECKING = True def setup(app): app.add_config_value('set_type_checking_flag', False, 'html') app.add_config_value('always_document_param_types', False, 'html') app.add_config_value('typehints_fully_qualified', False, 'env') app.add_config_value('typehints_document_rtype', True, 'env') app.connect('builder-inited', builder_ready) app.connect('autodoc-process-signature', process_signature) app.connect('autodoc-process-docstring', process_docstring) return dict(parallel_read_safe=True) sphinx-autodoc-typehints-1.9.0/tests/000077500000000000000000000000001355454451700176725ustar00rootroot00000000000000sphinx-autodoc-typehints-1.9.0/tests/conftest.py000066400000000000000000000023431355454451700220730ustar00rootroot00000000000000import os import sys import pathlib import shutil import pytest from sphinx.testing.path import path from sphobjinv import Inventory pytest_plugins = 'sphinx.testing.fixtures' collect_ignore = ['roots'] @pytest.fixture(scope='session') def inv(pytestconfig): inv_dict = pytestconfig.cache.get('python/objects.inv', None) if inv_dict is not None: return Inventory(inv_dict) print("Downloading objects.inv") url = 'https://docs.python.org/{v.major}.{v.minor}/objects.inv'.format(v=sys.version_info) inv = Inventory(url=url) pytestconfig.cache.set('python/objects.inv', inv.json_dict()) return inv @pytest.fixture(autouse=True) def remove_sphinx_projects(sphinx_test_tempdir): # Remove any directory which appears to be a Sphinx project from # the temporary directory area. # See https://github.com/sphinx-doc/sphinx/issues/4040 roots_path = pathlib.Path(sphinx_test_tempdir) for entry in roots_path.iterdir(): try: if entry.is_dir() and pathlib.Path(entry, '_build').exists(): shutil.rmtree(str(entry)) except PermissionError: pass @pytest.fixture def rootdir(): return path(os.path.dirname(__file__) or '.').abspath() / 'roots' sphinx-autodoc-typehints-1.9.0/tests/roots/000077500000000000000000000000001355454451700210405ustar00rootroot00000000000000sphinx-autodoc-typehints-1.9.0/tests/roots/test-dummy/000077500000000000000000000000001355454451700231505ustar00rootroot00000000000000sphinx-autodoc-typehints-1.9.0/tests/roots/test-dummy/conf.py000066400000000000000000000004031355454451700244440ustar00rootroot00000000000000import pathlib import sys # Make dummy_module.py available for autodoc. sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = 'index' extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx_autodoc_typehints', ] sphinx-autodoc-typehints-1.9.0/tests/roots/test-dummy/dummy_module.py000066400000000000000000000101631355454451700262230ustar00rootroot00000000000000import typing from typing import Callable, Union try: from dataclasses import dataclass except ImportError: def dataclass(cls): return cls def get_local_function(): def wrapper(self) -> str: """ Wrapper """ return wrapper class Class: """ Initializer docstring. :param x: foo :param y: bar :param z: baz """ def __init__(self, x: bool, y: int, z: typing.Optional[str] = None) -> None: pass def a_method(self, x: bool, y: int, z: typing.Optional[str] = None) -> str: """ Method docstring. :param x: foo :param y: bar :param z: baz """ def _private_method(self, x: str) -> str: """ Private method docstring. :param x: foo """ def __dunder_method(self, x: str) -> str: """ Dunder method docstring. :param x: foo """ def __magic_custom_method__(self, x: str) -> str: """ Magic dunder method docstring. :param x: foo """ @classmethod def a_classmethod(cls, x: bool, y: int, z: typing.Optional[str] = None) -> str: """ Classmethod docstring. :param x: foo :param y: bar :param z: baz """ @staticmethod def a_staticmethod(x: bool, y: int, z: typing.Optional[str] = None) -> str: """ Staticmethod docstring. :param x: foo :param y: bar :param z: baz """ @property def a_property(self) -> str: """ Property docstring """ class InnerClass: """ Inner class. """ def inner_method(self, x: bool) -> str: """ Inner method. :param x: foo """ def __dunder_inner_method(self, x: bool) -> str: """ Dunder inner method. :param x: foo """ locally_defined_callable_field = get_local_function() class DummyException(Exception): """ Exception docstring :param message: blah """ def __init__(self, message: str) -> None: super().__init__(message) def function(x: bool, y: int, z_: typing.Optional[str] = None) -> str: """ Function docstring. :param x: foo :param y: bar :param z\\_: baz :return: something :rtype: bytes """ def function_with_escaped_default(x: str = '\b'): """ Function docstring. :param x: foo """ def function_with_unresolvable_annotation(x: 'a.b.c'): # noqa: F821 """ Function docstring. :param x: foo """ def function_with_typehint_comment( x, # type: int y # type: str ): # type: (...) -> None """ Function docstring. :param x: foo :param y: bar """ class ClassWithTypehints(object): """ Class docstring. :param x: foo """ def __init__( self, x # type: int ): # type: (...) -> None pass def foo( self, x # type: str ): # type: (...) -> int """ Method docstring. :param x: foo """ return 42 def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # type: (Union[str, bytes], *str, bytes, **int) -> None """ Function docstring. :param x: foo :param y: bar :param z: baz :param kwargs: some kwargs """ class ClassWithTypehintsNotInline(object): """ Class docstring. :param x: foo """ def __init__(self, x=None): # type: (Callable[[int, bytes], int]) -> None pass def foo(self, x=1): # type: (Callable[[int, bytes], int]) -> int """ Method docstring. :param x: foo """ return x(1, b'') @classmethod def mk(cls, x=None): # type: (Callable[[int, bytes], int]) -> ClassWithTypehintsNotInline """ Method docstring. :param x: foo """ return cls(x) def undocumented_function(x: int) -> str: """Hi""" return str(x) @dataclass class DataClass: """Class docstring.""" sphinx-autodoc-typehints-1.9.0/tests/roots/test-dummy/index.rst000066400000000000000000000014701355454451700250130ustar00rootroot00000000000000Dummy Module ============ .. autoclass:: dummy_module.Class :members: :undoc-members: :private-members: :special-members: __magic_custom_method__ .. autoexception:: dummy_module.DummyException :members: :undoc-members: .. autofunction:: dummy_module.function .. autofunction:: dummy_module.function_with_escaped_default .. autofunction:: dummy_module.function_with_unresolvable_annotation .. autofunction:: dummy_module.function_with_typehint_comment .. autoclass:: dummy_module.ClassWithTypehints :members: .. autofunction:: dummy_module.function_with_typehint_comment_not_inline .. autoclass:: dummy_module.ClassWithTypehintsNotInline :members: .. autofunction:: dummy_module.undocumented_function .. autoclass:: dummy_module.DataClass :undoc-members: :special-members: __init__ sphinx-autodoc-typehints-1.9.0/tests/test_sphinx_autodoc_typehints.py000066400000000000000000000345721355454451700264540ustar00rootroot00000000000000import pathlib import re import sys import textwrap import typing from collections import defaultdict from typing import ( Any, AnyStr, Callable, Dict, Generic, Mapping, NewType, Optional, Pattern, Tuple, TypeVar, Union, Type) import pytest import typing_extensions from sphinx_autodoc_typehints import format_annotation, process_docstring try: from typing import ClassVar # not available prior to Python 3.5.3 except ImportError: ClassVar = None try: from typing import NoReturn # not available prior to Python 3.6.5 except ImportError: NoReturn = None try: from typing import Literal except ImportError: Literal = defaultdict(lambda: None) T = TypeVar('T') U = TypeVar('U', covariant=True) V = TypeVar('V', contravariant=True) W = NewType('W', str) class A: def get_type(self): return type(self) class B(Generic[T]): pass class C(B[str]): pass class D(typing_extensions.Protocol): pass class E(typing_extensions.Protocol[T]): pass class Slotted: __slots__ = () class Metaclass(type): pass @pytest.mark.parametrize('annotation, expected_result', [ (str, ':py:class:`str`'), (int, ':py:class:`int`'), (type(None), '``None``'), (type, ':py:class:`type`'), (Type, ':py:class:`~typing.Type`'), (Type[A], ':py:class:`~typing.Type`\\[:py:class:`~%s.A`]' % __name__), pytest.param(NoReturn, ':py:data:`~typing.NoReturn`', marks=[pytest.mark.skipif(NoReturn is None, reason='typing.NoReturn is not available')]), pytest.param(ClassVar[str], ':py:data:`~typing.ClassVar`\\[:py:class:`str`]', marks=[pytest.mark.skipif(ClassVar is None, reason='typing.ClassVar is not available')]), (Any, ':py:data:`~typing.Any`'), (AnyStr, ':py:data:`~typing.AnyStr`'), (Generic[T], ':py:class:`~typing.Generic`\\[\\~T]'), (Mapping, ':py:class:`~typing.Mapping`'), (Mapping[T, int], ':py:class:`~typing.Mapping`\\[\\~T, :py:class:`int`]'), (Mapping[str, V], ':py:class:`~typing.Mapping`\\[:py:class:`str`, \\-V]'), (Mapping[T, U], ':py:class:`~typing.Mapping`\\[\\~T, \\+U]'), (Mapping[str, bool], ':py:class:`~typing.Mapping`\\[:py:class:`str`, ' ':py:class:`bool`]'), (Dict, ':py:class:`~typing.Dict`'), (Dict[T, int], ':py:class:`~typing.Dict`\\[\\~T, :py:class:`int`]'), (Dict[str, V], ':py:class:`~typing.Dict`\\[:py:class:`str`, \\-V]'), (Dict[T, U], ':py:class:`~typing.Dict`\\[\\~T, \\+U]'), (Dict[str, bool], ':py:class:`~typing.Dict`\\[:py:class:`str`, ' ':py:class:`bool`]'), (Tuple, ':py:data:`~typing.Tuple`'), (Tuple[str, bool], ':py:data:`~typing.Tuple`\\[:py:class:`str`, ' ':py:class:`bool`]'), (Tuple[int, int, int], ':py:data:`~typing.Tuple`\\[:py:class:`int`, ' ':py:class:`int`, :py:class:`int`]'), (Tuple[str, ...], ':py:data:`~typing.Tuple`\\[:py:class:`str`, ...]'), (Union, ':py:data:`~typing.Union`'), (Union[str, bool], ':py:data:`~typing.Union`\\[:py:class:`str`, ' ':py:class:`bool`]'), pytest.param(Union[str, Any], ':py:data:`~typing.Union`\\[:py:class:`str`, ' ':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`]'), (Callable, ':py:data:`~typing.Callable`'), (Callable[..., int], ':py:data:`~typing.Callable`\\[..., :py:class:`int`]'), (Callable[[int], int], ':py:data:`~typing.Callable`\\[\\[:py:class:`int`], ' ':py:class:`int`]'), (Callable[[int, str], bool], ':py:data:`~typing.Callable`\\[\\[:py:class:`int`, ' ':py:class:`str`], :py:class:`bool`]'), (Callable[[int, str], None], ':py:data:`~typing.Callable`\\[\\[:py:class:`int`, ' ':py:class:`str`], ``None``]'), (Callable[[T], T], ':py:data:`~typing.Callable`\\[\\[\\~T], \\~T]'), (Pattern, ':py:class:`~typing.Pattern`'), (Pattern[str], ':py:class:`~typing.Pattern`\\[:py:class:`str`]'), pytest.param(Literal['a', 1], ":py:data:`~typing.Literal`\\['a', 1]", marks=[pytest.mark.skipif(isinstance(Literal, defaultdict), reason='Requires Python 3.8+')]), (Metaclass, ':py:class:`~%s.Metaclass`' % __name__), (A, ':py:class:`~%s.A`' % __name__), (B, ':py:class:`~%s.B`\\[\\~T]' % __name__), (B[int], ':py:class:`~%s.B`\\[:py:class:`int`]' % __name__), (C, ':py:class:`~%s.C`' % __name__), (D, ':py:class:`~%s.D`' % __name__), (E, ':py:class:`~%s.E`\\[\\~T]' % __name__), (E[int], ':py:class:`~%s.E`\\[:py:class:`int`]' % __name__), (W, ':py:func:`~typing.NewType`\\(:py:data:`~W`, :py:class:`str`)') ]) def test_format_annotation(inv, annotation, expected_result): result = format_annotation(annotation) assert result == expected_result # Test with the "fully_qualified" flag turned on if 'typing' in expected_result or __name__ in expected_result: expected_result = expected_result.replace('~typing', 'typing') expected_result = expected_result.replace('~' + __name__, __name__) assert format_annotation(annotation, fully_qualified=True) == expected_result # Test for the correct role (class vs data) using the official Sphinx inventory if 'typing' in expected_result: m = re.match('^:py:(?Pclass|data|func):`~(?P[^`]+)`', result) assert m, 'No match' name = m.group('name') role = next((o.role for o in inv.objects if o.name == name), None) if name in {'typing.Pattern', 'typing.Match', 'typing.NoReturn'}: if sys.version_info < (3, 6): assert role is None, 'No entry in Python 3.5’s objects.inv' return assert role is not None, 'Name {} not found'.format(name) assert m.group('role') == ('func' if role == 'function' else role) @pytest.mark.parametrize('library', [typing, typing_extensions], ids=['typing', 'typing_extensions']) @pytest.mark.parametrize('annotation, params, expected_result', [ ('Literal', ('a', 1), ":py:data:`~typing.Literal`\\['a', 1]"), ('Type', None, ':py:class:`~typing.Type`'), ('Type', (A,), ':py:class:`~typing.Type`\\[:py:class:`~%s.A`]' % __name__) ]) def test_format_annotation_both_libs(inv, library, annotation, params, expected_result): try: annotation_cls = getattr(library, annotation) except AttributeError: pytest.skip('{} not available in the {} module'.format(annotation, library.__name__)) ann = annotation_cls if params is None else annotation_cls[params] result = format_annotation(ann) assert result == expected_result def test_process_docstring_slot_wrapper(): lines = [] process_docstring(None, 'class', 'SlotWrapper', Slotted, None, lines) 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): 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)) app.config.always_document_param_types = always_document_param_types app.build() assert 'build succeeded' in status.getvalue() # Build succeeded # There should be a warning about an unresolved forward reference warnings = warning.getvalue().strip() assert 'Cannot resolve forward reference in type annotations of ' in warnings, warnings if always_document_param_types: undoc_params = ''' Parameters: **x** ("int") --''' else: undoc_params = "" text_path = pathlib.Path(app.srcdir) / '_build' / 'text' / 'index.txt' with text_path.open('r') as f: text_contents = f.read().replace('–', '--') expected_contents = textwrap.dedent('''\ Dummy Module ************ class dummy_module.Class(x, y, z=None) Initializer docstring. Parameters: * **x** ("bool") – foo * **y** ("int") – bar * **z** ("Optional"["str"]) – baz class InnerClass Inner class. _InnerClass__dunder_inner_method(x) Dunder inner method. Parameters: **x** ("bool") -- foo Return type: "str" inner_method(x) Inner method. Parameters: **x** ("bool") -- foo Return type: "str" _Class__dunder_method(x) Dunder method docstring. Parameters: **x** ("str") -- foo Return type: "str" __magic_custom_method__(x) Magic dunder method docstring. Parameters: **x** ("str") -- foo Return type: "str" _private_method(x) Private method docstring. Parameters: **x** ("str") -- foo Return type: "str" classmethod a_classmethod(x, y, z=None) Classmethod docstring. Parameters: * **x** ("bool") – foo * **y** ("int") – bar * **z** ("Optional"["str"]) – baz Return type: "str" a_method(x, y, z=None) Method docstring. Parameters: * **x** ("bool") – foo * **y** ("int") – bar * **z** ("Optional"["str"]) – baz Return type: "str" property a_property Property docstring Return type: "str" static a_staticmethod(x, y, z=None) Staticmethod docstring. Parameters: * **x** ("bool") – foo * **y** ("int") – bar * **z** ("Optional"["str"]) – baz Return type: "str" locally_defined_callable_field() -> str Wrapper Return type: "str" exception dummy_module.DummyException(message) Exception docstring Parameters: **message** ("str") – blah dummy_module.function(x, y, z_=None) Function docstring. Parameters: * **x** ("bool") – foo * **y** ("int") – bar * **z_** ("Optional"["str"]) – baz Returns: something Return type: bytes dummy_module.function_with_escaped_default(x='\\x08') Function docstring. Parameters: **x** ("str") – foo dummy_module.function_with_unresolvable_annotation(x) Function docstring. Parameters: **x** (*a.b.c*) – foo dummy_module.function_with_typehint_comment(x, y) Function docstring. Parameters: * **x** ("int") – foo * **y** ("str") – bar Return type: "None" class dummy_module.ClassWithTypehints(x) Class docstring. Parameters: **x** ("int") -- foo foo(x) Method docstring. Parameters: **x** ("str") -- foo Return type: "int" dummy_module.function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs) Function docstring. Parameters: * **x** ("Union"["str", "bytes", "None"]) -- foo * **y** ("str") -- bar * **z** ("bytes") -- baz * **kwargs** ("int") -- some kwargs Return type: "None" class dummy_module.ClassWithTypehintsNotInline(x=None) Class docstring. Parameters: **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo foo(x=1) Method docstring. Parameters: **x** ("Callable"[["int", "bytes"], "int"]) -- foo Return type: "int" classmethod mk(x=None) Method docstring. Parameters: **x** (*Callable**[**[**int**, **bytes**]**, **int**]*) -- foo Return type: ClassWithTypehintsNotInline dummy_module.undocumented_function(x) Hi{undoc_params} Return type: "str" class dummy_module.DataClass Class docstring. __init__() '''.format(undoc_params=undoc_params)).replace('–', '--') if sys.version_info < (3, 6): expected_contents += ''' Initialize self. See help(type(self)) for accurate signature. ''' else: expected_contents += ''' Return type: "None" ''' assert text_contents == expected_contents sphinx-autodoc-typehints-1.9.0/tox.ini000066400000000000000000000004571355454451700200510ustar00rootroot00000000000000[tox] minversion = 3.3.0 envlist = py35, py36, py37, py38, flake8 skip_missing_interpreters = true isolated_build = true [testenv] extras = test, type_comments commands = python -m pytest {posargs} [testenv:flake8] deps = flake8 commands = flake8 sphinx_autodoc_typehints.py tests skip_install = true