• 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    )
58    if signature.return_annotation is signature.empty:
59        return_annotation = ''
60    else:
61        return_annotation = ' -> ' + _annotation_name(
62            signature.return_annotation
63        )
64
65    return f'{name}({params}){return_annotation}'
66
67
68def format_function_help(function: Callable) -> str:
69    """Formats a help string with a declaration and docstring."""
70    signature = format_signature(
71        function.__name__, inspect.signature(function, follow_wrapped=False)
72    )
73
74    docs = inspect.getdoc(function) or '(no docstring)'
75    return f'{signature}:\n\n{textwrap.indent(docs, "    ")}'
76
77
78def help_as_repr(function: Callable) -> Callable:
79    """Wraps a function so that its repr() and docstring provide detailed help.
80
81    This is useful for creating commands in an interactive console. In a
82    console, typing a function's name and hitting Enter shows rich documentation
83    with the full function signature, type annotations, and docstring when the
84    function is wrapped with help_as_repr.
85    """
86
87    def display_help(_):
88        return format_function_help(function)
89
90    return type(
91        function.__name__,
92        (),
93        dict(
94            __call__=staticmethod(function),
95            __doc__=format_function_help(function),
96            __repr__=display_help,
97        ),
98    )()
99