mirror of
https://github.com/instaloader/instaloader.git
synced 2024-09-19 03:42:23 +02:00
229 lines
9.2 KiB
Python
229 lines
9.2 KiB
Python
|
# sphinx_autodoc_typehints from Mar 18 2018.
|
||
|
# 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.inspect import getargspec
|
||
|
from sphinx.ext.autodoc import formatargspec
|
||
|
|
||
|
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
|
||
|
|
||
|
|
||
|
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)
|
||
|
try:
|
||
|
argspec = getargspec(obj)
|
||
|
except (TypeError, ValueError):
|
||
|
return
|
||
|
|
||
|
if argspec.args:
|
||
|
if what in ('class', 'exception'):
|
||
|
del argspec.args[0]
|
||
|
elif what == 'method':
|
||
|
outer = inspect.getmodule(obj)
|
||
|
for clsname in obj.__qualname__.split('.')[:-1]:
|
||
|
outer = getattr(outer, clsname)
|
||
|
method_object = outer.__dict__[obj.__name__]
|
||
|
if not isinstance(method_object, (classmethod, staticmethod)):
|
||
|
del argspec.args[0]
|
||
|
|
||
|
return formatargspec(obj, *argspec[:-1]), 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
|
||
|
|
||
|
for argname, annotation in type_hints.items():
|
||
|
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
|