Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically add type annotations to all API functions #192

Merged
merged 1 commit into from Jul 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/_static/css/custom.css
@@ -1,3 +1,25 @@
.small {
font-size:40%;
}

.rst-content dl:not(.docutils) dl dt {
/* mimick numpydoc’s blockquote style */
font-weight: normal;
background: none transparent;
border-left: none;
margin: 0 0 12px;
padding: 3px 0 0;
font-size: 100%;
}

.rst-content dl:not(.docutils) dl dt code {
font-size: 100%;
font-weight: normal;
background: none transparent;
border: none;
padding: 0 2px;
}

.rst-content dl:not(.docutils) dl dt a.reference>code {
text-decoration: underline;
}
65 changes: 35 additions & 30 deletions docs/_templates/autosummary/class.rst
@@ -1,30 +1,35 @@
{% extends "!autosummary/class.rst" %}

.. this is from numpy's documentation; why does autosummary not create the pages
for the attributes?

{% block methods %}
{% if methods %}
.. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages.
.. autosummary::
:toctree: .
{% for item in all_methods %}
{%- if not item.startswith('_') or item in ['__call__'] %}
{{ name }}.{{ item }}
{%- endif -%}
{%- endfor %}
{% endif %}
{% endblock %}

{% block attributes %}
{% if attributes %}
.. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages.
.. autosummary::
:toctree: .
{% for item in all_attributes %}
{%- if not item.startswith('_') %}
{{ name }}.{{ item }}
{%- endif -%}
{%- endfor %}
{% endif %}
{% endblock %}
:github_url: {{ fullname | modurl }}

{{ fullname | escape | underline}}

.. currentmodule:: {{ module }}

.. add toctree option to make autodoc generate the pages

.. autoclass:: {{ objname }}

{% block attributes %}
{% if attributes %}
.. rubric:: Attributes

.. autosummary::
:toctree: .
{% for item in attributes %}
~{{ fullname }}.{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}

{% block methods %}
{% if methods %}
.. rubric:: Methods

.. autosummary::
:toctree: .
{% for item in methods %}
{%- if item != '__init__' %}
~{{ fullname }}.{{ item }}
{%- endif -%}
{%- endfor %}
{% endif %}
{% endblock %}
116 changes: 62 additions & 54 deletions docs/conf.py
@@ -1,14 +1,12 @@
import ast
import sys
import inspect
import logging
from pathlib import Path
from datetime import datetime
from typing import List, Optional
from typing import Optional

from sphinx.application import Sphinx
from sphinx.ext import autosummary
from sphinx.ext.autosummary import limited_join

# remove PyCharm’s old six module
if 'six' in sys.modules:
Expand Down Expand Up @@ -40,7 +38,8 @@
'sphinx.ext.autosummary',
# 'plot_generator',
# 'plot_directive',
'numpydoc',
'sphinx.ext.napoleon',
'sphinx_autodoc_typehints',
'sphinx.ext.intersphinx',
# 'ipython_directive',
# 'ipython_console_highlighting',
Expand All @@ -52,8 +51,9 @@
# see falexwolf's issue for numpydoc
# autodoc_member_order = 'bysource'
# autodoc_default_flags = ['members']
numpydoc_show_class_members = True
numpydoc_class_members_toctree = True
napoleon_google_docstring = False
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False

intersphinx_mapping = dict(
python=('https://docs.python.org/3', None),
Expand Down Expand Up @@ -226,61 +226,69 @@ def api_image(qualname: str) -> Optional[str]:
DEFAULT_FILTERS.update(modurl=modurl, api_image=api_image)


# -- Prettier Autodoc -----------------------------------------------------
# TODO: replace with sphinx.ext.napoleon
# -- Prettier Param docs --------------------------------------------


def unparse(ast_node: ast.expr, plain: bool = False) -> str:
if isinstance(ast_node, ast.Attribute):
if plain:
return ast_node.attr
else:
v = unparse(ast_node.value, plain)
return f'{v}.{ast_node.attr}'
elif isinstance(ast_node, ast.Index):
return unparse(ast_node.value)
elif isinstance(ast_node, ast.Name):
return ast_node.id
elif isinstance(ast_node, ast.Subscript):
v = unparse(ast_node.value, plain)
s = unparse(ast_node.slice, plain)
return f'{v}[{s}]'
elif isinstance(ast_node, ast.Tuple):
return ', '.join(unparse(e) for e in ast_node.elts)
else:
t = type(ast_node)
raise NotImplementedError(f'can’t unparse {t}')

from typing import Dict, List, Tuple

def mangle_signature(sig: str, max_chars: int = 30) -> str:
fn = ast.parse(f'def f{sig}: pass').body[0]
from docutils import nodes
from sphinx import addnodes
from sphinx.domains.python import PyTypedField, PyObject
from sphinx.environment import BuildEnvironment

args_all = [a.arg for a in fn.args.args]
n_a = len(args_all) - len(fn.args.defaults)
args: List[str] = args_all[:n_a]
opts: List[str] = args_all[n_a:]

# Produce a more compact signature
s = limited_join(', ', args, max_chars=max_chars - 2)
if opts:
if not s:
opts_str = limited_join(', ', opts, max_chars=max_chars - 4)
s = f'[{opts_str}]'
elif len(s) < max_chars - 4 - 2 - 3:
opts_str = limited_join(', ', opts, max_chars=max_chars - len(sig) - 4 - 2)
s += f'[, {opts_str}]'
class PrettyTypedField(PyTypedField):
list_type = nodes.definition_list

if False: # if fn.returns: # do not show return type in docs
ret = unparse(fn.returns, plain=True)
return f'({s}) -> {ret}'
return f'({s})'
def make_field(
self,
types: Dict[str, List[nodes.Node]],
domain: str,
items: Tuple[str, List[nodes.inline]],
env: BuildEnvironment = None
) -> nodes.field:
def makerefs(rolename, name, node):
return self.make_xrefs(rolename, domain, name, node, env=env)

def handle_item(fieldarg: str, content: List[nodes.inline]) -> nodes.definition_list_item:
head = nodes.term()
head += makerefs(self.rolename, fieldarg, addnodes.literal_strong)
fieldtype = types.pop(fieldarg, None)
if fieldtype is not None:
head += nodes.Text(' : ')
if len(fieldtype) == 1 and isinstance(fieldtype[0], nodes.Text):
typename = ''.join(n.astext() for n in fieldtype)
head += makerefs(self.typerolename, typename, addnodes.literal_emphasis)
else:
head += fieldtype

autosummary.mangle_signature = mangle_signature
body_content = nodes.paragraph('', '', *content)
body = nodes.definition('', body_content)

return nodes.definition_list_item('', head, body)

if __name__ == '__main__':
print(mangle_signature('(filename: typing.Union[str, pathlib.Path], delim: int=0) -> anndata.base.AnnData'))
print(mangle_signature('(a, *, b=1) -> int'))
print(mangle_signature('(a, b=1, *c) -> Union[str, pathlib.Path]'))
print(mangle_signature('(a, b=1, *c, d=1)'))
fieldname = nodes.field_name('', self.label)
if len(items) == 1 and self.can_collapse:
fieldarg, content = items[0]
bodynode = handle_item(fieldarg, content)
else:
bodynode = self.list_type()
for fieldarg, content in items:
bodynode += handle_item(fieldarg, content)
fieldbody = nodes.field_body('', bodynode)
return nodes.field('', fieldname, fieldbody)


# replace matching field types with ours
PyObject.doc_field_types = [
PrettyTypedField(
ft.name,
names=ft.names,
typenames=ft.typenames,
label=ft.label,
rolename=ft.rolename,
typerolename=ft.typerolename,
can_collapse=ft.can_collapse,
) if isinstance(ft, PyTypedField) else ft
for ft in PyObject.doc_field_types
]
4 changes: 1 addition & 3 deletions docs/requires.txt
@@ -1,8 +1,6 @@
# use highlighting until the merge of https://github.com/rtfd/readthedocs.org/pull/4096
sphinx_rtd_theme>=0.3.1
# numpydoc 0.8.0 doesn't render parameters properly and formats attributes like parameters
# we want neither of this
numpydoc==0.7.0
sphinx_autodoc_typehints
# same as ../requires.txt, but omitting the c++ packages
anndata>=0.5.8
matplotlib>=2.2
Expand Down
5 changes: 5 additions & 0 deletions scanpy/api/__init__.py
Expand Up @@ -28,3 +28,8 @@
# some stuff that is not actually documented...
from .. import utils
from .. import rtools


import sys
utils.annotate_doc_types(sys.modules[__name__], 'scanpy')
del sys