Skip to content

Commit

Permalink
Automatically add type annotations to all API functions
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep committed Jul 12, 2018
1 parent 1550326 commit a6c9c69
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 125 deletions.
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;
}
63 changes: 33 additions & 30 deletions docs/_templates/autosummary/class.rst
@@ -1,30 +1,33 @@
{% 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::
{% for item in attributes %}
~{{ name }}.{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}

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

.. autosummary::
{% for item in methods %}
{%- if item != '__init__' %}
~{{ name }}.{{ 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

0 comments on commit a6c9c69

Please sign in to comment.