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