Skip to content
Permalink
Browse files

REF: Add some more type annotations and check with mypy

  • Loading branch information...
kernc committed Jan 10, 2019
1 parent 1b235a2 commit 1fe4ea4d30c336cb4f9650005aaee4f5890ad716
Showing with 68 additions and 48 deletions.
  1. +3 −2 .travis.yml
  2. +38 −38 pdoc/__init__.py
  3. +2 −0 pdoc/cli.py
  4. +7 −6 pdoc/html_helpers.py
  5. +8 −2 pdoc/test/__init__.py
  6. +10 −0 setup.cfg
@@ -17,9 +17,10 @@ matrix:
- python: '3.7'
name: 'Lint, Coverage'
before_script:
- pip install flake8 coverage
- pip install flake8 coverage mypy
script:
- flake8 --max-line-length=100 .
- flake8
- mypy pdoc
- time catchsegv coverage run setup.py test
after_success:
- bash <(curl -s https://codecov.io/bash)
@@ -329,7 +329,8 @@ def recursive_htmls(mod):
from copy import copy
from functools import lru_cache, reduce
from itertools import tee, groupby
from typing import Dict, Iterable, List, Set, Type, TypeVar, Union
from types import ModuleType
from typing import Dict, Iterable, List, Set, Type, TypeVar, Union, Tuple, Generator, Callable
from warnings import warn

from mako.lookup import TemplateLookup
@@ -345,7 +346,7 @@ def recursive_htmls(mod):
_URL_INDEX_MODULE_SUFFIX = '.m.html' # For modules named literal 'index'
_URL_PACKAGE_SUFFIX = '/index.html'

T = TypeVar('T')
T = TypeVar('T', bound='Doc')

__pdoc__ = {} # type: Dict[str, Union[bool, str]]

@@ -361,7 +362,7 @@ def recursive_htmls(mod):
object's `directories` attribute.
"""
if os.getenv("XDG_CONFIG_HOME"):
tpl_lookup.directories.insert(0, path.join(os.getenv("XDG_CONFIG_HOME"), "pdoc"))
tpl_lookup.directories.insert(0, path.join(os.getenv("XDG_CONFIG_HOME", ''), "pdoc"))


class Context(dict):
@@ -468,7 +469,7 @@ def text(module_name, docfilter=None, **kwargs) -> str:
return mod.text(**kwargs)


def import_module(module: str):
def import_module(module) -> ModuleType:
"""
Return module object matching `module` specification (either a python
module path or a filesystem path to file/directory).
@@ -540,7 +541,8 @@ def _pairwise(iterable):
return zip(a, b)


def _var_docstrings(doc_obj: 'Doc', *, _init_tree: ast.AST = None) -> dict:
def _var_docstrings(doc_obj: Union['Module', 'Class'], *,
_init_tree: ast.FunctionDef = None) -> Dict[str, 'Variable']:
"""
Extracts docstrings for variables of `doc_obj`
(either a `pdoc.Module` or `pdoc.Class`).
@@ -551,20 +553,18 @@ def _var_docstrings(doc_obj: 'Doc', *, _init_tree: ast.AST = None) -> dict:
variables (defined as `self.something` in class' `__init__`),
recognized by `Variable.instance_var == True`.
"""
assert isinstance(doc_obj, (Module, Class))

if _init_tree:
tree = _init_tree
tree = _init_tree # type: Union[ast.Module, ast.FunctionDef]
else:
try:
tree = ast.parse(inspect.getsource(doc_obj.obj))
except (OSError, TypeError, SyntaxError):
warn("Couldn't get/parse source of '{!r}'".format(doc_obj))
return {}
if isinstance(doc_obj, Class):
tree = tree.body[0] # ast.parse creates a dummy ast.Module wrapper we don't need
tree = tree.body[0] # type: ignore # ast.parse creates a dummy ast.Module wrapper

vs = {}
vs = {} # type: Dict[str, Variable]

cls = None
module = doc_obj
@@ -591,9 +591,10 @@ def _var_docstrings(doc_obj: 'Doc', *, _init_tree: ast.AST = None) -> dict:
module_all = set(module_all)

try:
ast_AnnAssign = ast.AnnAssign
ast_AnnAssign = ast.AnnAssign # type: Type
except AttributeError: # Python < 3.6
ast_AnnAssign = type(None)

ast_Assignments = (ast.Assign, ast_AnnAssign)

for assign_node, str_node in _pairwise(ast.iter_child_nodes(tree)):
@@ -653,12 +654,12 @@ def _filter_type(type: Type[T],
return [i for i in values if isinstance(i, type)]


def _toposort(graph: Dict[T, Set[T]]) -> List[T]:
def _toposort(graph: Dict[T, Set[T]]) -> Generator[T, None, None]:
"""
Return items of `graph` sorted in topological order.
Source: https://rosettacode.org/wiki/Topological_sort#Python
"""
items_without_deps = reduce(set.union, graph.values(), set()) - set(graph.keys())
items_without_deps = reduce(set.union, graph.values(), set()) - set(graph.keys()) # type: ignore # noqa: E501
yield from items_without_deps
ordered = items_without_deps
while True:
@@ -751,9 +752,9 @@ def __init__(self, name, module, obj, docstring=None):
def __repr__(self):
return '<{} {!r}>'.format(self.__class__.__name__, self.refname)

@property
@property # type: ignore
@lru_cache()
def source(self):
def source(self) -> str:
"""
Cleaned (dedented) source code of the Python object. If not
available, an empty string.
@@ -834,7 +835,8 @@ class Module(Doc):

__slots__ = ('supermodule', 'doc', '_context', '_is_inheritance_linked')

def __init__(self, module, *, docfilter=None, supermodule=None, context=None):
def __init__(self, module: ModuleType, *, docfilter: Callable[[Doc], bool] = None,
supermodule: 'Module' = None, context: Context = None):
"""
Creates a `Module` documentation object given the actual
module Python object.
@@ -861,7 +863,7 @@ def __init__(self, module, *, docfilter=None, supermodule=None, context=None):
The parent `pdoc.Module` this module is a submodule of, or `None`.
"""

self.doc = {}
self.doc = {} # type: Dict[str, Doc]
"""A mapping from identifier name to a documentation object."""

self._is_inheritance_linked = False
@@ -977,14 +979,14 @@ def _link_inheritance(self):

self._is_inheritance_linked = True

def text(self, **kwargs):
def text(self, **kwargs) -> str:
"""
Returns the documentation for this module as plain text.
"""
txt = _render_template('/text.mako', module=self, **kwargs)
return re.sub("\n\n\n+", "\n\n", txt)

def html(self, external_links=False, link_prefix="", source=True, minify=True, **kwargs):
def html(self, external_links=False, link_prefix="", source=True, minify=True, **kwargs) -> str:
"""
Returns the documentation for this module as
self-contained HTML.
@@ -1025,7 +1027,7 @@ def find_class(self, cls: type):
# If not, see what was here before.
return self.find_ident(cls.__module__ + '.' + cls.__qualname__)

def find_ident(self, name: str):
def find_ident(self, name: str) -> Doc:
"""
Searches this module and **all** other public modules
for an identifier with name `name` in its list of
@@ -1040,7 +1042,7 @@ def find_ident(self, name: str):
self._context.get(self.name + '.' + name) or
External(name))

def _filter_doc_objs(self, type: type = Doc):
def _filter_doc_objs(self, type: Type[T]) -> List[T]:
return sorted(_filter_type(type, self.doc))

def variables(self):
@@ -1162,7 +1164,7 @@ def mro(self, only_documented=False) -> List['Class']:
classes = _filter_type(Class, classes)
return classes

def subclasses(self):
def subclasses(self) -> List['Class']:
"""
Returns a list of subclasses of this class that are visible to the
Python interpreter (obtained from type.__subclasses__()).
@@ -1173,8 +1175,9 @@ def subclasses(self):
return [self.module.find_class(c)
for c in self.obj.__subclasses__()]

def _filter_doc_objs(self, include_inherited=True, filter_func=lambda x: True):
return sorted(obj for obj in self.doc.values()
def _filter_doc_objs(self, type: Type[T], include_inherited=True,
filter_func: Callable[[T], bool] = lambda x: True) -> List[T]:
return sorted(obj for obj in _filter_type(type, self.doc)
if (include_inherited or not obj.inherits) and filter_func(obj))

def class_variables(self, include_inherited=True):
@@ -1183,8 +1186,7 @@ def class_variables(self, include_inherited=True):
alphabetically as a list of `pdoc.Variable`.
"""
return self._filter_doc_objs(
include_inherited,
lambda var: isinstance(var, Variable) and not var.instance_var)
Variable, include_inherited, lambda dobj: not dobj.instance_var)

def instance_variables(self, include_inherited=True):
"""
@@ -1193,8 +1195,7 @@ def instance_variables(self, include_inherited=True):
are those defined in a class's `__init__` as `self.variable = ...`.
"""
return self._filter_doc_objs(
include_inherited,
lambda var: isinstance(var, Variable) and var.instance_var)
Variable, include_inherited, lambda dobj: dobj.instance_var)

def methods(self, include_inherited=True):
"""
@@ -1203,19 +1204,17 @@ def methods(self, include_inherited=True):
with `__init__` always coming first.
"""
return self._filter_doc_objs(
include_inherited,
lambda f: isinstance(f, Function) and f.method)
Function, include_inherited, lambda dobj: dobj.method)

def functions(self, include_inherited=True):
def functions(self, include_inherited=True) -> List['Function']:
"""
Returns all documented static functions as `pdoc.Function`
objects in the class, sorted alphabetically.
"""
return self._filter_doc_objs(
include_inherited,
lambda f: isinstance(f, Function) and not f.method)
Function, include_inherited, lambda dobj: not dobj.method)

def inherited_members(self):
def inherited_members(self) -> List[Tuple['Class', List[Doc]]]:
"""
Returns all inherited members as a list of tuples
(ancestor class, list of ancestor class' members sorted by name),
@@ -1225,7 +1224,7 @@ def inherited_members(self):
for k, g in groupby((i.inherits
for i in self.doc.values() if i.inherits),
key=lambda i: i.cls)),
key=lambda x, _mro_index=self.mro().index: _mro_index(x[0]))
key=lambda x, _mro_index=self.mro().index: _mro_index(x[0])) # type: ignore

def _fill_inheritance(self):
"""
@@ -1278,7 +1277,7 @@ class Function(Doc):
"""
__slots__ = ('cls', 'method')

def __init__(self, name, module, obj, *, cls=None, method=False):
def __init__(self, name, module, obj, *, cls: Class = None, method=False):
"""
Same as `pdoc.Doc.__init__`, except `obj` must be a
Python function object. The docstring is gathered automatically.
@@ -1328,7 +1327,7 @@ def _is_async(self):
return False

@lru_cache()
def params(self):
def params(self) -> List[str]:
"""
Returns a list where each element is a nicely formatted
parameter of this function. This includes argument lists,
@@ -1401,7 +1400,8 @@ class Variable(Doc):
"""
__slots__ = ('cls', 'instance_var')

def __init__(self, name, module, docstring, *, obj=None, cls=None, instance_var=False):
def __init__(self, name, module, docstring, *,
obj=None, cls: Class = None, instance_var=False):
"""
Same as `pdoc.Doc.__init__`, except `cls` should be provided
as a `pdoc.Class` object when this is a class or instance
@@ -102,6 +102,8 @@ def _check_host_port(s):
"and port ({}:{}), set the parameter to :.".format(DEFAULT_HOST, DEFAULT_PORT),
)

args = argparse.Namespace()


class WebDoc(BaseHTTPRequestHandler):
args = None # Set before server instantiated
@@ -4,6 +4,7 @@
import inspect
import re
from functools import partial, lru_cache
from typing import Callable
from warnings import warn

import markdown
@@ -12,7 +13,7 @@


@lru_cache()
def minify_css(css,
def minify_css(css: str,
_whitespace=partial(re.compile(r'\s*([,{:;}])\s*').sub, r'\1'),
_comments=partial(re.compile(r'/\*.*?\*/', flags=re.DOTALL).sub, ''),
_trailing_semicolon=partial(re.compile(r';\s*}').sub, '}')):
@@ -22,7 +23,7 @@ def minify_css(css,
return _trailing_semicolon(_whitespace(_comments(css))).strip()


def minify_html(html,
def minify_html(html: str,
_minify=partial(
re.compile(r'(.*?)(<pre\b.*?</pre\b\s*>)|(.*)', re.IGNORECASE | re.DOTALL).sub,
lambda m, _norm_space=partial(re.compile(r'\s\s+').sub, '\n'): (
@@ -36,7 +37,7 @@ def minify_html(html,
return _minify(html)


def glimpse(text, max_length=153, *, paragraph=True,
def glimpse(text: str, max_length=153, *, paragraph=True,
_split_paragraph=partial(re.compile(r'\s*\n\s*\n\s*').split, maxsplit=1),
_trim_last_word=partial(re.compile(r'\S+$').sub, ''),
_remove_titles=partial(re.compile(r'^(#+|-{4,}|={4,})', re.MULTILINE).sub, ' ')):
@@ -246,8 +247,8 @@ def doctests(text,
return _indent_doctests(text)


def to_html(text, docformat: str = 'numpy,google', *,
module: pdoc.Module = None, link=None,
def to_html(text: str, docformat: str = 'numpy,google', *,
module: pdoc.Module = None, link: Callable[..., str] = None,
# Matches markdown code spans not +directly+ within links.
# E.g. `code` and [foo is `bar`]() but not [`code`](...)
# Also skips \-escaped grave quotes.
@@ -300,7 +301,7 @@ def linkify(match, _is_pyident=re.compile(r'^[a-zA-Z_]\w*(\.\w+)+$').match):
return _md.reset().convert(text)


def extract_toc(text):
def extract_toc(text: str):
"""
Returns HTML Table of Contents containing markdown titles in `text`.
"""
@@ -50,8 +50,8 @@ def chdir(path):
def run(*args, _check=True, **kwargs) -> int:
params = (('--' + key.replace('_', '-'), value)
for key, value in kwargs.items())
params = list(filter(None, chain.from_iterable(params)))
_args = parser.parse_args([*params, *args])
params = list(filter(None, chain.from_iterable(params))) # type: ignore
_args = parser.parse_args([*params, *args]) # type: ignore
try:
returncode = main(_args)
return returncode or 0
@@ -348,6 +348,12 @@ def test_module_allsubmodules(self):
self.assertEqual(sorted(m.name for m in m.submodules()),
[EXAMPLE_MODULE + '._private.module'])

def test_instance_var(self):
pdoc.reset()
mod = pdoc.Module(pdoc.import_module(EXAMPLE_MODULE))
var = mod.doc['B'].doc['instance_var']
self.assertTrue(var.instance_var)

def test_refname(self):
mod = EXAMPLE_MODULE + '.' + 'subpkg'
module = pdoc.Module(pdoc.import_module(mod))
@@ -0,0 +1,10 @@
[flake8]
max-line-length = 100

[mypy]
warn_unused_ignores = True
warn_redundant_casts = True
ignore_missing_imports = True

[mypy-pdoc.test.example_pkg.*]
ignore_errors = True

0 comments on commit 1fe4ea4

Please sign in to comment.
You can’t perform that action at this time.