Skip to content

WiP: Add support for PEP 484 type hints #7

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

Closed
wants to merge 9 commits into from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ build
*.eggs/*
*.egg-info
*.py[cod]
/.eggs/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line necessary with *.eggs/* already specified three lines above?


pdoc/_version.py

Expand Down
116 changes: 66 additions & 50 deletions pdoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,9 @@ def recursive_htmls(mod):
from functools import lru_cache, reduce
from itertools import tee, groupby
from types import ModuleType
from typing import Dict, Iterable, List, Set, Type, TypeVar, Union, Tuple, Generator, Callable
from typing import Dict, List, Set, Tuple
from typing import Optional, Union, Type, TypeVar
from typing import Callable, Iterable, Generator
from warnings import warn

from mako.lookup import TemplateLookup
Expand Down Expand Up @@ -1310,6 +1312,52 @@ def _link_inheritance(self):
del self._super_members


class Parameter(inspect.Parameter):
"""Representation of a function parameter, incl. type hint & default value."""

POSITIONAL = (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.VAR_POSITIONAL,
)

KEYWORD = (
inspect.Parameter.KEYWORD_ONLY,
inspect.Parameter.VAR_KEYWORD
)

STARS = {
inspect.Parameter.VAR_POSITIONAL: '*',
inspect.Parameter.VAR_KEYWORD: '**',
}

@property
def should_show(self) -> bool:
return _is_public(self.name) or self.default is Parameter.empty

def __str__(self) -> str:
r = Parameter.STARS.get(self.kind, '')
r += self.name

if self.annotation is not Parameter.empty:
r += ': '

if isinstance(self.annotation, str):
r += self.annotation
elif hasattr(self.annotation, '__name__'):
r += self.annotation.__name__
else:
r += repr(self.annotation)

if self.default is not Parameter.empty:
r += ' = ' + repr(self.default)

return r

def of_inspect(p: inspect.Parameter) -> 'Parameter':
return Parameter(p.name, p.kind, default=p.default, annotation=p.annotation)


class Function(Doc):
"""
Representation of documentation for a function or method.
Expand Down Expand Up @@ -1366,62 +1414,30 @@ def _is_async(self):
return False

@lru_cache()
def params(self) -> List[str]:
def params(self) -> List[Union[str, Parameter]]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm strongly opposed to breaking established API by returning something else than strings. It makes no sense, really, as any code that were to do something more than str(param) will have to instance check.

Maybe is better:

def params(self, *, types: bool = False) -> List[str]

with types=True returning the strings interpolated with typing info?

pdoc API-wise, I think it's easy enough to let the user do on their own if needed:

inspect.signature(pdoc_func_obj.obj)

In that way, class Parameter(inspect.Parameter) is not even required, in exchange for some of the former complexity of this method (should be much less with inspect API).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

types= parameter can then be a template tunable in config.mako:

<%!
# Template configuration. Copy over and adapt as required.
html_lang = 'en'
show_inherited_members = False
extract_module_toc_into_sidebar = True
list_class_variables_in_index = True
sort_identifiers = True
# Set the style keyword such as 'atom-one-light' or 'github-gist'
# Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles
# Demo: https://highlightjs.org/static/demo/
hljs_style = 'github'
%>

"""
Returns a list where each element is a nicely formatted
parameter of this function. This includes argument lists,
keyword arguments and default values, and it doesn't include any
Returns a list representing each parameter of this function, with a
nicely-formatted string conversion. This includes argument lists,
keyword arguments, and default values, and it doesn't include any
optional arguments whose names begin with an underscore.
"""
try:
s = inspect.getfullargspec(inspect.unwrap(self.obj))
sig = inspect.signature(inspect.unwrap(self.obj))
except TypeError:
# I guess this is for C builtin functions?
return ["..."]
return [Parameter('...', None, None)]

params = []
for i, param in enumerate(s.args):
if s.defaults is not None and len(s.args) - i <= len(s.defaults):
defind = len(s.defaults) - (len(s.args) - i)
params.append("%s=%s" % (param, repr(s.defaults[defind])))
else:
params.append(param)
if s.varargs is not None:
params.append("*%s" % s.varargs)

kwonlyargs = getattr(s, "kwonlyargs", None)
if kwonlyargs:
if s.varargs is None:
params.append("*")
for param in kwonlyargs:
try:
params.append("%s=%s" % (param, repr(s.kwonlydefaults[param])))
except KeyError:
params.append(param)

keywords = getattr(s, "varkw", getattr(s, "keywords", None))
if keywords is not None:
params.append("**%s" % keywords)

# Remove "_private" params following catch-all *args and from the end
iter_params = iter(params)
params = []
for p in iter_params:
params.append(p)
if p.startswith('*'):
break
while len(params) > 1 and not _is_public(params[-2]) and '=' in params[-2]:
params.pop(-2)
for p in iter_params:
if _is_public(p.lstrip('*')):
params.append(p)
while params and not _is_public(params[-1]) and '=' in params[-1]:
params.pop(-1)
if params and params[-1] == '*':
params.pop(-1)

# TODO: The only thing now missing for Python 3 are type annotations
return params

parameters = [ Parameter.of_inspect(p) for p in sig.parameters.values() ]
pos_params = [ p for p in parameters
if p.should_show and p.kind in Parameter.POSITIONAL ]
kw_params = [ p for p in parameters
if p.should_show and p.kind in Parameter.KEYWORD ]

if not pos_params and kw_params:
return ['*'] + kw_params

return pos_params + kw_params

def __lt__(self, other):
# Push __init__ to the top.
Expand Down
2 changes: 1 addition & 1 deletion pdoc/templates/html.mako
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@

<%def name="show_func(f)">
<dt id="${f.refname}"><code class="name flex">
<span>${f.funcdef()} ${ident(f.name)}</span>(<span>${', '.join(f.params()) | h})</span>
<span>${f.funcdef()} ${ident(f.name)}</span>(<span>${', '.join(map(str, f.params())) | h})</span>
</code></dt>
<dd>${show_desc(f)}</dd>
</%def>
Expand Down
2 changes: 1 addition & 1 deletion pdoc/templates/text.mako
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</%def>

<%def name="function(func)" buffered="True">
`${func.name}(${", ".join(func.params())})`
`${func.name}(${", ".join(map(str, func.params()))})`
${func.docstring | deflist}
</%def>

Expand Down
10 changes: 5 additions & 5 deletions pdoc/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def test_html(self):
'>sys.version<',
'B.CONST docstring',
'B.var docstring',
'b=1',
'b: int = 1',
'*args',
'**kwargs',
'__init__ docstring',
Expand Down Expand Up @@ -264,7 +264,7 @@ def test_text(self):
'__init__ docstring',
'instance_var',
'instance var docstring',
'b=1',
'b: int = 1',
'*args',
'**kwargs',
'B.f docstring',
Expand Down Expand Up @@ -511,15 +511,15 @@ def test_context(self):
def test_Function_private_params(self):
func = pdoc.Function('f', pdoc.Module(pdoc),
lambda a, _a, _b=None: None)
self.assertEqual(func.params(), ['a', '_a'])
self.assertEqual([str(p) for p in func.params()], ['a', '_a'])

func = pdoc.Function('f', pdoc.Module(pdoc),
lambda _ok, a, _a, *args, _b=None, c=None, _d=None: None)
self.assertEqual(func.params(), ['_ok', 'a', '_a', '*args', 'c=None'])
self.assertEqual([str(p) for p in func.params()], ['_ok', 'a', '_a', '*args', 'c = None'])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP 8 suggests to use spaces around equals sign only when type is specified:

def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...


func = pdoc.Function('f', pdoc.Module(pdoc),
lambda a, b, *, _c=1: None)
self.assertEqual(func.params(), ['a', 'b'])
self.assertEqual([str(p) for p in func.params()], ['a', 'b'])

def test_url(self):
mod = pdoc.Module(pdoc.import_module(EXAMPLE_MODULE))
Expand Down