• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Code for improving interactive use of Python functions."""
15
16import inspect
17import textwrap
18from typing import Callable
19
20
21def _annotation_name(annotation: object) -> str:
22    if isinstance(annotation, str):
23        return annotation
24
25    return getattr(annotation, '__name__', repr(annotation))
26
27
28def format_parameter(param: inspect.Parameter) -> str:
29    """Formats a parameter for printing in a function signature."""
30    if param.kind == param.VAR_POSITIONAL:
31        name = '*' + param.name
32    elif param.kind == param.VAR_KEYWORD:
33        name = '**' + param.name
34    else:
35        name = param.name
36
37    if param.default is param.empty:
38        default = ''
39    else:
40        default = f' = {param.default}'
41
42    if param.annotation is param.empty:
43        annotation = ''
44    else:
45        annotation = f': {_annotation_name(param.annotation)}'
46
47    return f'{name}{annotation}{default}'
48
49
50def format_signature(name: str, signature: inspect.Signature) -> str:
51    """Formats a function signature as if it were source code.
52
53    Does not yet handle / and * markers.
54    """
55    params = ', '.join(
56        format_parameter(arg) for arg in signature.parameters.values())
57    if signature.return_annotation is signature.empty:
58        return_annotation = ''
59    else:
60        return_annotation = ' -> ' + _annotation_name(
61            signature.return_annotation)
62
63    return f'{name}({params}){return_annotation}'
64
65
66def format_function_help(function: Callable) -> str:
67    """Formats a help string with a declaration and docstring."""
68    signature = format_signature(
69        function.__name__, inspect.signature(function, follow_wrapped=False))
70
71    docs = inspect.getdoc(function) or '(no docstring)'
72    return f'{signature}:\n\n{textwrap.indent(docs, "    ")}'
73
74
75def help_as_repr(function: Callable) -> Callable:
76    """Wraps a function so that its repr() and docstring provide detailed help.
77
78    This is useful for creating commands in an interactive console. In a
79    console, typing a function's name and hitting Enter shows rich documentation
80    with the full function signature, type annotations, and docstring when the
81    function is wrapped with help_as_repr.
82    """
83    def display_help(_):
84        return format_function_help(function)
85
86    return type(
87        function.__name__, (),
88        dict(__call__=staticmethod(function),
89             __doc__=format_function_help(function),
90             __repr__=display_help))()
91