Skip to content
Permalink
Browse files

ENH: Linkify known types in type annotations

  • Loading branch information...
kernc committed Apr 22, 2019
1 parent ebf2abc commit 060947401d31db4fcd236569cf415f5418f1c6c4
Showing with 68 additions and 46 deletions.
  1. +20 −9 pdoc/__init__.py
  2. +29 −29 pdoc/html_helpers.py
  3. +5 −5 pdoc/templates/html.mako
  4. +14 −3 pdoc/test/__init__.py
@@ -351,7 +351,7 @@ def recursive_htmls(mod):
import sys
import typing
from copy import copy
from functools import lru_cache, reduce
from functools import lru_cache, reduce, partial
from itertools import tee, groupby
from types import ModuleType
from typing import Dict, Iterable, List, Set, Type, TypeVar, Union, Tuple, Generator, Callable
@@ -1226,7 +1226,7 @@ def subclasses(self) -> List['Class']:
return [self.module.find_class(c)
for c in type.__subclasses__(self.obj)]

def params(self, *, annotate=False) -> List['str']:
def params(self, *, annotate=False, link=None) -> List['str']:
"""
Return a list of formatted parameters accepted by the
class constructor (method `__init__`). See `pdoc.Function.params`.
@@ -1238,7 +1238,8 @@ class constructor (method `__init__`). See `pdoc.Function.params`.
if name in exclusions or qualname in exclusions or refname in exclusions:
return []

params = Function._params(self.obj.__init__, annotate=annotate)
params = Function._params(self.obj.__init__,
annotate=annotate, link=link, module=self.module)
params = params[1:] if params[0] == 'self' else params
return params

@@ -1394,8 +1395,7 @@ def _is_async(self):
except AttributeError:
return False

@property
def return_annotation(self):
def return_annotation(self, *, link=None):
"""Formatted function return type annotation or empty string if none."""
try:
annot = typing.get_type_hints(self.obj).get('return', '')
@@ -1406,9 +1406,13 @@ def return_annotation(self):
annot = ''
if not annot:
return ''
return inspect.formatannotation(annot).replace(' ', '\xA0') # NBSP for better line breaks
s = inspect.formatannotation(annot).replace(' ', '\xA0') # NBSP for better line breaks
if link:
from pdoc.html_helpers import _linkify
s = re.sub(r'[\w\.]+', partial(_linkify, link=link, module=self.module), s)
return s

def params(self, *, annotate: bool = False) -> List[str]:
def params(self, *, annotate: bool = False, link: Callable[[Doc], str] = None) -> List[str]:
"""
Returns a list where each element is a nicely formatted
parameter of this function. This includes argument lists,
@@ -1420,10 +1424,10 @@ def params(self, *, annotate: bool = False) -> List[str]:
[PEP 484]: https://www.python.org/dev/peps/pep-0484/
"""
return self._params(self.obj, annotate=annotate)
return self._params(self.obj, annotate=annotate, link=link, module=self.module)

@staticmethod
def _params(func_obj, annotate=False):
def _params(func_obj, annotate=False, link=None, module=None):
try:
signature = inspect.signature(inspect.unwrap(func_obj))
except ValueError:
@@ -1442,6 +1446,10 @@ def __repr__(self):
kw_only = False
EMPTY = inspect.Parameter.empty

if link:
from pdoc.html_helpers import _linkify
_linkify = partial(_linkify, link=link, module=module)

for p in signature.parameters.values(): # type: inspect.Parameter
if not _is_public(p.name) and p.default is not EMPTY:
continue
@@ -1466,6 +1474,9 @@ def __repr__(self):
s = re.sub(r'(?<=: )[\'"]|[\'"](?= = )', '', s, 2)
s = s.replace(' ', '\xA0') # Neater line breaking with NBSPs

if link:
s = re.sub(r'[\w\.]+', _linkify, s)

params.append(s)

return params
@@ -5,7 +5,7 @@
import os.path
import re
from functools import partial, lru_cache
from typing import Callable
from typing import Callable, Match
from warnings import warn

import markdown
@@ -86,15 +86,6 @@ def glimpse(text: str, max_length=153, *, paragraph=True,
)


class ReferenceWarning(UserWarning):
"""
This warning is raised in `to_html` when a object reference in markdown
doesn't match any documented objects.
Look for this warning to catch typos / references to obsolete symbols.
"""


class _ToMarkdown:
"""
This class serves as a namespace for methods converting common
@@ -335,29 +326,38 @@ def to_markdown(text: str, docformat: str = 'numpy,google', *,
text = _ToMarkdown.doctests(text)

if module and link:

def linkify(match, _is_pyident=re.compile(r'^[a-zA-Z_]\w*(\.\w+)+$').match):
nonlocal link, module
matched = match.group(0)
refname = matched[1:-1]
dobj = module.find_ident(refname)
if isinstance(dobj, pdoc.External):
if not _is_pyident(refname):
return matched
# If refname in documentation has a typo or is obsolete, warn.
# XXX: Assume at least the first part of refname, i.e. the package, is correct.
module_part = module.find_ident(refname.split('.')[0])
if not isinstance(module_part, pdoc.External):
warn('Code reference `{}` in module "{}" does not match any '
'documented object.'.format(refname, module.refname),
ReferenceWarning, stacklevel=3)
return link(dobj, fmt='`{}`')

text = _code_refs(linkify, text)
text = _code_refs(partial(_linkify, link=link, module=module, fmt='`{}`'), text)

return text


class ReferenceWarning(UserWarning):
"""
This warning is raised in `to_html` when a object reference in markdown
doesn't match any documented objects.
Look for this warning to catch typos / references to obsolete symbols.
"""


def _linkify(match: Match, link: Callable[..., str], module: pdoc.Module,
_is_pyident=re.compile(r'^[a-zA-Z_]\w*(\.\w+)+$').match, **kwargs):
matched = match.group(0)
refname = matched.strip('`')
dobj = module.find_ident(refname)
if isinstance(dobj, pdoc.External):
if not _is_pyident(refname):
return matched
# If refname in documentation has a typo or is obsolete, warn.
# XXX: Assume at least the first part of refname, i.e. the package, is correct.
module_part = module.find_ident(refname.split('.')[0])
if not isinstance(module_part, pdoc.External):
warn('Code reference `{}` in module "{}" does not match any '
'documented object.'.format(refname, module.refname),
ReferenceWarning, stacklevel=3)
return link(dobj, **kwargs)


def extract_toc(text: str):
"""
Returns HTML Table of Contents containing markdown titles in `text`.
@@ -105,12 +105,12 @@
<%def name="show_func(f)">
<dt id="${f.refname}"><code class="name flex">
<%
params = ', '.join(f.params(annotate=show_type_annotations))
returns = show_type_annotations and f.return_annotation or ''
params = ', '.join(f.params(annotate=show_type_annotations, link=link))
returns = show_type_annotations and f.return_annotation(link=link) or ''
if returns:
returns = ' 🡢\xA0' + returns
%>
<span>${f.funcdef()} ${ident(f.name)}</span>(<span>${params | h})${returns | h}</span>
<span>${f.funcdef()} ${ident(f.name)}</span>(<span>${params})${returns}</span>
</code></dt>
<dd>${show_desc(f)}</dd>
</%def>
@@ -181,12 +181,12 @@
methods = c.methods(show_inherited_members, sort=sort_identifiers)
mro = c.mro()
subclasses = c.subclasses()
params = ', '.join(c.params(annotate=show_type_annotations))
params = ', '.join(c.params(annotate=show_type_annotations, link=link))
%>
<dt id="${c.refname}"><code class="flex name class">
<span>class ${ident(c.name)}</span>
% if params:
<span>(</span><span>${params | h})</span>
<span>(</span><span>${params})</span>
% endif
</code></dt>
@@ -19,6 +19,8 @@
from unittest.mock import patch
from urllib.request import urlopen

import typing

import pdoc
from pdoc.cli import main, parser
from pdoc.html_helpers import (
@@ -578,17 +580,26 @@ def test_Function_params(self):
self.assertEqual(func.params(), ['a=os.environ'])

# typed
def f(a: int, *b, c: str = ''): pass
def f(a: int, *b, c: typing.List[pdoc.Doc] = ''): pass
func = pdoc.Function('f', mod, f)
self.assertEqual(func.params(), ['a', '*b', "c=''"])
self.assertEqual(func.params(annotate=True), ['a:\xA0int', '*b', "c:\xA0str\xA0=\xA0''"])
self.assertEqual(func.params(annotate=True),
['a:\xA0int', '*b', "c:\xA0List[pdoc.Doc]\xA0=\xA0''"])

# typed, linked
def link(dobj):
return '<a href="{}">{}</a>'.format(dobj.url(relative_to=mod), dobj.qualname)

self.assertEqual(func.params(annotate=True, link=link),
['a:\xA0int', '*b',
"c:\xa0List[<a href=\"#pdoc.Doc\">Doc</a>]\xa0=\xa0''"])

def test_Function_return_annotation(self):
import typing

def f() -> typing.List[typing.Union[str, pdoc.Doc]]: pass
func = pdoc.Function('f', pdoc.Module(pdoc), f)
self.assertEqual(func.return_annotation, 'List[Union[str,\xA0pdoc.Doc]]')
self.assertEqual(func.return_annotation(), 'List[Union[str,\xA0pdoc.Doc]]')

@ignore_warnings
def test_Class_docstring(self):

0 comments on commit 0609474

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