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