From a9ff196aa075870269cc9bc3cb350a23dcd44478 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Wed, 23 Jan 2019 14:50:12 +0100 Subject: [PATCH 1/8] Use spacing for default arguments This is a readability improvement, especially one type hints get added. --- pdoc/__init__.py | 4 ++-- pdoc/test/__init__.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 232c24a0..d72a01e4 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -1355,7 +1355,7 @@ def params(self) -> List[str]: 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]))) + params.append("%s = %s" % (param, repr(s.defaults[defind]))) else: params.append(param) if s.varargs is not None: @@ -1367,7 +1367,7 @@ def params(self) -> List[str]: params.append("*") for param in kwonlyargs: try: - params.append("%s=%s" % (param, repr(s.kwonlydefaults[param]))) + params.append("%s = %s" % (param, repr(s.kwonlydefaults[param]))) except KeyError: params.append(param) diff --git a/pdoc/test/__init__.py b/pdoc/test/__init__.py index aa353423..b40a126d 100644 --- a/pdoc/test/__init__.py +++ b/pdoc/test/__init__.py @@ -132,7 +132,7 @@ def test_html(self): '>sys.version<', 'B.CONST docstring', 'B.var docstring', - 'b=1', + 'b = 1', '*args', '**kwargs', '__init__ docstring', @@ -264,7 +264,7 @@ def test_text(self): '__init__ docstring', 'instance_var', 'instance var docstring', - 'b=1', + 'b = 1', '*args', '**kwargs', 'B.f docstring', @@ -507,7 +507,7 @@ def test_Function_private_params(self): 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(func.params(), ['_ok', 'a', '_a', '*args', 'c = None']) func = pdoc.Function('f', pdoc.Module(pdoc), lambda a, b, *, _c=1: None) From 5b5053d6e97b168d5fbb63ed03812b0888605ac9 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Wed, 23 Jan 2019 22:09:38 +0100 Subject: [PATCH 2/8] Function.params: Refactor to keep names and defaults separate --- pdoc/__init__.py | 73 +++++++++++++++++++++++++++------------- pdoc/templates/html.mako | 2 +- pdoc/templates/text.mako | 2 +- pdoc/test/__init__.py | 6 ++-- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/pdoc/__init__.py b/pdoc/__init__.py index d72a01e4..05f416c5 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -330,7 +330,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 NamedTuple, Optional, Union, Type, TypeVar +from typing import Callable, Iterable, Generator from warnings import warn from mako.lookup import TemplateLookup @@ -636,11 +638,16 @@ def _var_docstrings(doc_obj: Union['Module', 'Class'], *, return vs -def _is_public(ident_name): +def _is_public(ident): """ Returns `True` if `ident_name` matches the export criteria for an identifier name. """ + if isinstance(ident, Parameter): + ident_name = ident.name.lstrip('*') + else: + ident_name = ident + return not ident_name.startswith("_") @@ -1282,6 +1289,19 @@ def _link_inheritance(self): del self._super_members +class Parameter(NamedTuple('Parameter', + [('name', str), ('default', Optional[str])])): + """Representation of a function parameter, incl. default value.""" + + def __str__(self) -> str: + r = self.name + + if self.default is not None: + r += ' = ' + self.default + + return r + + class Function(Doc): """ Representation of documentation for a function or method. @@ -1338,58 +1358,63 @@ def _is_async(self): return False @lru_cache() - def params(self) -> List[str]: + def params(self) -> List[Parameter]: """ - 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)) except TypeError: # I guess this is for C builtin functions? - return ["..."] + return [Parameter('...', None)] params = [] + for i, param in enumerate(s.args): + default = None 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) + default = repr(s.defaults[defind]) + + params.append(Parameter(name=param, + default=default)) kwonlyargs = getattr(s, "kwonlyargs", None) + if kwonlyargs or s.varargs is not None: + params.append(Parameter(name='*' + s.varargs if s.varargs else '*', + default=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) + if param in s.kwonlydefaults: + default = repr(s.kwonlydefaults[param]) + else: + default = None + + params.append(Parameter(name=param, default=default)) keywords = getattr(s, "varkw", getattr(s, "keywords", None)) if keywords is not None: - params.append("**%s" % keywords) + params.append(Parameter(name='**' + keywords, default=None)) # 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('*'): + if p.name.startswith('*'): break - while len(params) > 1 and not _is_public(params[-2]) and '=' in params[-2]: + while len(params) > 1 and not _is_public(params[-2]) and params[-2].default is not None: params.pop(-2) for p in iter_params: - if _is_public(p.lstrip('*')): + if _is_public(p.name): params.append(p) - while params and not _is_public(params[-1]) and '=' in params[-1]: + while params and not _is_public(params[-1]) and params[-1].default is not None: params.pop(-1) - if params and params[-1] == '*': + if params and params[-1].name == "*": params.pop(-1) # TODO: The only thing now missing for Python 3 are type annotations diff --git a/pdoc/templates/html.mako b/pdoc/templates/html.mako index 97222418..6afa07dd 100644 --- a/pdoc/templates/html.mako +++ b/pdoc/templates/html.mako @@ -98,7 +98,7 @@ <%def name="show_func(f)">
- ${f.funcdef()} ${ident(f.name)}(${', '.join(f.params()) | h}) + ${f.funcdef()} ${ident(f.name)}(${', '.join(map(str, f.params())) | h})
${show_desc(f)}
diff --git a/pdoc/templates/text.mako b/pdoc/templates/text.mako index 043b9567..1e10f12c 100644 --- a/pdoc/templates/text.mako +++ b/pdoc/templates/text.mako @@ -12,7 +12,7 @@ <%def name="function(func)" buffered="True"> -`${func.name}(${", ".join(func.params())})` +`${func.name}(${", ".join(map(str, func.params()))})` ${func.docstring | deflist} diff --git a/pdoc/test/__init__.py b/pdoc/test/__init__.py index b40a126d..6ab18274 100644 --- a/pdoc/test/__init__.py +++ b/pdoc/test/__init__.py @@ -503,15 +503,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']) 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)) From 070027793270e7beeffe89d92795fa44e29b21bb Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Wed, 23 Jan 2019 22:11:58 +0100 Subject: [PATCH 3/8] git: Ignore the .eggs directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0b0cf722..d439a604 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist build *.egg-info *.py[cod] +/.eggs/ pdoc/_version.py From 9977ffe5344e2b54dee10fb8c8c443c8c80ac21e Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Wed, 23 Jan 2019 22:19:08 +0100 Subject: [PATCH 4/8] WiP: Support PEP 484 type hints --- pdoc/__init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 05f416c5..f9acdbd6 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -1290,12 +1290,17 @@ def _link_inheritance(self): class Parameter(NamedTuple('Parameter', - [('name', str), ('default', Optional[str])])): - """Representation of a function parameter, incl. default value.""" + [('name', str), + ('hint', Optional[str]), + ('default', Optional[str])])): + """Representation of a function parameter, incl. type hint & default value.""" def __str__(self) -> str: r = self.name + if self.hint is not None: + r += ': ' + self.hint + if self.default is not None: r += ' = ' + self.default @@ -1369,7 +1374,7 @@ def params(self) -> List[Parameter]: s = inspect.getfullargspec(inspect.unwrap(self.obj)) except TypeError: # I guess this is for C builtin functions? - return [Parameter('...', None)] + return [Parameter('...', None, None)] params = [] @@ -1380,11 +1385,13 @@ def params(self) -> List[Parameter]: default = repr(s.defaults[defind]) params.append(Parameter(name=param, + hint=None, # XXXTODO default=default)) kwonlyargs = getattr(s, "kwonlyargs", None) if kwonlyargs or s.varargs is not None: params.append(Parameter(name='*' + s.varargs if s.varargs else '*', + hint=None, default=None)) if kwonlyargs: @@ -1394,11 +1401,12 @@ def params(self) -> List[Parameter]: else: default = None - params.append(Parameter(name=param, default=default)) + params.append(Parameter(name=param, default=default, + hint=None)) # XXXTODO keywords = getattr(s, "varkw", getattr(s, "keywords", None)) if keywords is not None: - params.append(Parameter(name='**' + keywords, default=None)) + params.append(Parameter(name='**' + keywords, hint=None, default=None)) # Remove "_private" params following catch-all *args and from the end iter_params = iter(params) From aebd82031e8c418c6fd0c87394d53973af4e20b7 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Sat, 16 Mar 2019 18:07:34 +0100 Subject: [PATCH 5/8] pdoc: Refactor Parameters and Function.params to use the inspect API --- pdoc/__init__.py | 124 +++++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 68 deletions(-) diff --git a/pdoc/__init__.py b/pdoc/__init__.py index f9acdbd6..4750fddb 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -331,7 +331,7 @@ def recursive_htmls(mod): from itertools import tee, groupby from types import ModuleType from typing import Dict, List, Set, Tuple -from typing import NamedTuple, Optional, Union, Type, TypeVar +from typing import Optional, Union, Type, TypeVar from typing import Callable, Iterable, Generator from warnings import warn @@ -638,16 +638,11 @@ def _var_docstrings(doc_obj: Union['Module', 'Class'], *, return vs -def _is_public(ident): +def _is_public(ident_name): """ Returns `True` if `ident_name` matches the export criteria for an identifier name. """ - if isinstance(ident, Parameter): - ident_name = ident.name.lstrip('*') - else: - ident_name = ident - return not ident_name.startswith("_") @@ -1289,23 +1284,45 @@ def _link_inheritance(self): del self._super_members -class Parameter(NamedTuple('Parameter', - [('name', str), - ('hint', Optional[str]), - ('default', Optional[str])])): +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 = self.name + r = Parameter.STARS.get(self.kind, '') + r += self.name - if self.hint is not None: - r += ': ' + self.hint + if self.annotation is not Parameter.empty: + # FIXME: This is probably not the correct thing to do with complex types + r += ': ' + self.annotation.__name__ - if self.default is not None: - r += ' = ' + self.default + 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): """ @@ -1363,7 +1380,7 @@ def _is_async(self): return False @lru_cache() - def params(self) -> List[Parameter]: + def params(self) -> List[Union[str, Parameter]]: """ Returns a list representing each parameter of this function, with a nicely-formatted string conversion. This includes argument lists, @@ -1371,62 +1388,33 @@ def params(self) -> List[Parameter]: 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 [Parameter('...', None, None)] - params = [] - - for i, param in enumerate(s.args): - default = None - if s.defaults is not None and len(s.args) - i <= len(s.defaults): - defind = len(s.defaults) - (len(s.args) - i) - default = repr(s.defaults[defind]) - - params.append(Parameter(name=param, - hint=None, # XXXTODO - default=default)) - - kwonlyargs = getattr(s, "kwonlyargs", None) - if kwonlyargs or s.varargs is not None: - params.append(Parameter(name='*' + s.varargs if s.varargs else '*', - hint=None, - default=None)) - - if kwonlyargs: - for param in kwonlyargs: - if param in s.kwonlydefaults: - default = repr(s.kwonlydefaults[param]) - else: - default = None - - params.append(Parameter(name=param, default=default, - hint=None)) # XXXTODO - - keywords = getattr(s, "varkw", getattr(s, "keywords", None)) - if keywords is not None: - params.append(Parameter(name='**' + keywords, hint=None, default=None)) - - # 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.name.startswith('*'): + + parameters = [ Parameter.of_inspect(p) for p in sig.parameters.values() ] + kw_params = [ p for p in parameters + if p.should_show and p.kind in Parameter.KEYWORD ] + + pos_params, tmp = [], [] + for p in parameters: + if p.kind not in Parameter.POSITIONAL: break - while len(params) > 1 and not _is_public(params[-2]) and params[-2].default is not None: - params.pop(-2) - for p in iter_params: - if _is_public(p.name): - params.append(p) - while params and not _is_public(params[-1]) and params[-1].default is not None: - params.pop(-1) - if params and params[-1].name == "*": - params.pop(-1) - - # TODO: The only thing now missing for Python 3 are type annotations - return params + + if p.should_show: + pos_params += tmp + pos_params.append(p) + tmp = [] + + else: + tmp.append(p) + + if not pos_params and kw_params: + return ['*'] + kw_params + + return pos_params + kw_params def __lt__(self, other): # Push __init__ to the top. From c9dbec0481c5afe385573c8290b2c98298cd2da7 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Sat, 16 Mar 2019 18:12:32 +0100 Subject: [PATCH 6/8] pdoc.Function.params: Simplify computing positional parameters It's impossible for a positional parameter with no default value to follow one which has a default value: >>> def f(x, y=1, z): pass SyntaxError: non-default argument follows default argument --- pdoc/__init__.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 4750fddb..3a52a2fc 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -1395,22 +1395,11 @@ def params(self) -> List[Union[str, Parameter]]: 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 ] - pos_params, tmp = [], [] - for p in parameters: - if p.kind not in Parameter.POSITIONAL: - break - - if p.should_show: - pos_params += tmp - pos_params.append(p) - tmp = [] - - else: - tmp.append(p) - if not pos_params and kw_params: return ['*'] + kw_params From d7d061ae4612b50d7d6130c6c5a2fcfbdba95033 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Sat, 16 Mar 2019 18:25:28 +0100 Subject: [PATCH 7/8] pdoc.Parameter.__repr__: Handle more complex types --- pdoc/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 3cef62f8..ef3fd3bb 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -1340,8 +1340,14 @@ def __str__(self) -> str: r += self.name if self.annotation is not Parameter.empty: - # FIXME: This is probably not the correct thing to do with complex types - r += ': ' + self.annotation.__name__ + 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) From 1cef049dab5eabc92ccef2db500d3b20cff3b135 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Sat, 16 Mar 2019 18:29:11 +0100 Subject: [PATCH 8/8] pdoc.test: Update for PEP484 type hints support --- pdoc/test/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pdoc/test/__init__.py b/pdoc/test/__init__.py index 89f7c1f9..af6818cd 100644 --- a/pdoc/test/__init__.py +++ b/pdoc/test/__init__.py @@ -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', @@ -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',