# sphinx_autodoc_typehints from Nov 20 2018 (version 1.5.2). # https://github.com/agronholm/sphinx-autodoc-typehints # (slightly modified to fix class links, maybe related to agronholm/sphinx-autodoc-typehints#38) # # This 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. # import inspect from typing import get_type_hints, TypeVar, Any, AnyStr, Generic, Union from sphinx.util import logging from sphinx.util.inspect import Signature try: from inspect import unwrap except ImportError: def unwrap(func, *, stop=None): """This is the inspect.unwrap() method copied from Python 3.5's standard library.""" if stop is None: def _is_wrapper(f): return hasattr(f, '__wrapped__') else: def _is_wrapper(f): return hasattr(f, '__wrapped__') and not stop(f) f = func # remember the original func for error reporting memo = {id(f)} # Memoise by id to tolerate non-hashable objects while _is_wrapper(func): func = func.__wrapped__ id_func = id(func) if id_func in memo: raise ValueError('wrapper loop when unwrapping {!r}'.format(f)) memo.add(id_func) return func logger = logging.getLogger(__name__) def format_annotation(annotation): 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) class_name = None if annotation_cls.__module__ == 'typing': params = None prefix = ':py:class:' module = 'typing' extra = '' if inspect.isclass(getattr(annotation, '__origin__', None)): annotation_cls = annotation.__origin__ try: if Generic in annotation_cls.mro(): module = annotation_cls.__module__ except TypeError: pass # annotation_cls was either the "type" object or typing.Type if annotation is Any: return ':py:data:`~typing.Any`' elif annotation is AnyStr: return ':py:data:`~typing.AnyStr`' elif isinstance(annotation, TypeVar): return '\\%r' % annotation elif (annotation is Union or getattr(annotation, '__origin__', None) is Union or hasattr(annotation, '__union_params__')): prefix = ':py:data:' 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 annotation_cls.__qualname__ == 'Tuple' and hasattr(annotation, '__tuple_params__'): params = annotation.__tuple_params__ if annotation.__tuple_use_ellipsis__: params += (Ellipsis,) elif annotation_cls.__qualname__ == 'Callable': prefix = ':py:data:' 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) for param in arg_annotations)), result_annotation ] 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: extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params)) if not class_name: class_name = annotation_cls.__qualname__.title() return '{}`~{}.{}`{}'.format(prefix, module, class_name, extra) elif annotation is Ellipsis: return '...' elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)): if not inspect.isclass(annotation): annotation_cls = annotation.__origin__ extra = '' if Generic in annotation_cls.mro(): params = (getattr(annotation, '__parameters__', None) or getattr(annotation, '__args__', None)) extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params)) module = annotation.__module__.split('.')[0] # hack to 'fix' class linking for Instaloader project return ':py:class:`~{}.{}`{}'.format(module, annotation_cls.__qualname__, 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 = unwrap(obj) signature = Signature(obj) parameters = [ param.replace(annotation=inspect.Parameter.empty) for param in signature.signature.parameters.values() ] 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 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 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 = unwrap(obj) try: type_hints = get_type_hints(obj) except (AttributeError, TypeError): # Introspecting a slot wrapper will raise TypeError return except NameError as exc: logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc) type_hints = obj.__annotations__ for argname, annotation in type_hints.items(): if argname.endswith('_'): argname = '{}\\_'.format(argname[:-1]) formatted_annotation = format_annotation(annotation) if argname == 'return': if what in ('class', 'exception'): # Don't add return type None from __init__() continue 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: 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)) else: searchfor = ':param {}:'.format(argname) for i, line in enumerate(lines): if line.startswith(searchfor): lines.insert(i, ':type {}: {}'.format(argname, formatted_annotation)) break