def get_callable_placeholder(function: Callable, owner_class=None, show_module=False) -> CallablePlaceholder: isclass = inspect.isclass(function) orig_fn = function # Get base name. name_parts = [] if show_module: name_parts.append(function.__module__) if owner_class: name_parts.append(owner_class.__name__) if hasattr(function, "__name__"): name_parts.append(function.__name__) else: name_parts.append(type(function).__name__) name_parts.append("__call__") function = function.__call__ if isclass: function = getattr(function, "__init__", None) name = ".".join(name_parts) sig = inspect.signature(function) fn_type: FunctionTypes = get_fn_type(orig_fn) params = [] for p in sig.parameters.values(): annotation = None default_value = None if p.annotation is not inspect._empty: # type: ignore annotation = inspect.formatannotation(p.annotation) if p.default is not inspect._empty: # type: ignore if isinstance(p.default, str): default_value = repr(p.default) else: default_value = str(p.default) if annotation is not None: annotation = cleanup_type(annotation) params.append(CallableArg(p.name, annotation, default_value)) return_annotation = None if sig.return_annotation is not inspect._empty: # type: ignore return_annotation = inspect.formatannotation(sig.return_annotation) if return_annotation is not None: return_annotation = cleanup_type(return_annotation) return CallablePlaceholder( simple=str(sig), name=name, args=params, return_type=return_annotation, fn_type=fn_type, )
def __str__(self): """Debug purposes.""" as_str = self.name # POSITIONAL_ONLY is only in precompiled functions if self.kind == self.POSITIONAL_ONLY: # pragma: no cover as_str = '' if as_str is None else '<{as_str}>'.format( as_str=as_str) # Add annotation if applicable (python 3 only) if self.annotation is not self.empty: # pragma: no cover as_str += ': {annotation!s}'.format( annotation=formatannotation(self.annotation)) value = self.value if self.empty == value: if self.VAR_POSITIONAL == self.kind: value = () elif self.VAR_KEYWORD == self.kind: value = {} as_str += '={value!r}'.format(value=value) if self.default is not self.empty: as_str += ' # {self.default!r}'.format(self=self) if self.kind == self.VAR_POSITIONAL: as_str = '*' + as_str elif self.kind == self.VAR_KEYWORD: as_str = '**' + as_str return as_str
def return_annotation(self): """Returns back return type annotation if a valid one is found""" signature = self._signature(self.func) if not signature or signature.return_annotation == inspect._empty: return "" return inspect.formatannotation(signature.return_annotation)
def __str__(self) -> str: components = [] if self: pos_param = next( findparam(self, lambda p: p.kind is POSITIONAL_ONLY), None, ) has_positional = bool(pos_param) vpo_param = get_var_positional_parameter(self) has_var_positional = bool(vpo_param) for i, param in enumerate(self): last_ = self[i - 1] if (i > 0) else None next_ = self[i + 1] if (len(self) > i + 1) else None if ( not has_var_positional and self[i].kind is KEYWORD_ONLY and (not last_ or last_.kind is not KEYWORD_ONLY) ): components.append('*') components.append(str(param)) if ( has_positional and self[i].kind is POSITIONAL_ONLY and (not next_ or next_.kind is not POSITIONAL_ONLY) ): components.append('/') ra_str = ' -> {}'.format( inspect.formatannotation(self.return_annotation) ) if self.return_annotation is not empty.native else '' return '({}){}'.format(', '.join(components), ra_str)
def inspect_formatargspec( args, varargs=None, varkw=None, defaults=None, kwonlyargs=(), kwonlydefaults={}, annotations={}, formatarg=str, formatvarargs=lambda name: "*" + name, formatvarkw=lambda name: "**" + name, formatvalue=lambda value: "=" + repr(value), formatreturns=lambda text: " -> " + text, formatannotation=formatannotation, ): """Copy formatargspec from python 3.7 standard library. Python 3 has deprecated formatargspec and requested that Signature be used instead, however this requires a full reimplementation of formatargspec() in terms of creating Parameter objects and such. Instead of introducing all the object-creation overhead and having to reinvent from scratch, just copy their compatibility routine. Utimately we would need to rewrite our "decorator" routine completely which is not really worth it right now, until all Python 2.x support is dropped. """ def formatargandannotation(arg): result = formatarg(arg) if arg in annotations: result += ": " + formatannotation(annotations[arg]) return result specs = [] if defaults: firstdefault = len(args) - len(defaults) for i, arg in enumerate(args): spec = formatargandannotation(arg) if defaults and i >= firstdefault: spec = spec + formatvalue(defaults[i - firstdefault]) specs.append(spec) if varargs is not None: specs.append(formatvarargs(formatargandannotation(varargs))) else: if kwonlyargs: specs.append("*") if kwonlyargs: for kwonlyarg in kwonlyargs: spec = formatargandannotation(kwonlyarg) if kwonlydefaults and kwonlyarg in kwonlydefaults: spec += formatvalue(kwonlydefaults[kwonlyarg]) specs.append(spec) if varkw is not None: specs.append(formatvarkw(formatargandannotation(varkw))) result = "(" + ", ".join(specs) + ")" if "return" in annotations: result += formatreturns(formatannotation(annotations["return"])) return result
def formatannotation(annotation: Any) -> str: """ Like `inspect.formatannotation()`, but with a small bugfix for Python 3.9's GenericAlias annotations. """ # a small inconsistency in Python 3.9, # formatannotation(list[str]) returns "list". if isinstance(annotation, type) and get_args(annotation): return repr(annotation) return inspect.formatannotation(annotation)
def inspect_formatargspec( args, varargs=None, varkw=None, defaults=None, kwonlyargs=(), kwonlydefaults={}, annotations={}, formatarg=str, formatvarargs=lambda name: "*" + name, formatvarkw=lambda name: "**" + name, formatvalue=lambda value: "=" + repr(value), formatreturns=lambda text: " -> " + text, formatannotation=formatannotation, ): """Copy formatargspec from python 3.7 standard library. Python 3 has deprecated formatargspec and requested that Signature be used instead, however this requires a full reimplementation of formatargspec() in terms of creating Parameter objects and such. Instead of introducing all the object-creation overhead and having to reinvent from scratch, just copy their compatibility routine. """ def formatargandannotation(arg): result = formatarg(arg) if arg in annotations: result += ": " + formatannotation(annotations[arg]) return result specs = [] if defaults: firstdefault = len(args) - len(defaults) for i, arg in enumerate(args): spec = formatargandannotation(arg) if defaults and i >= firstdefault: spec = spec + formatvalue(defaults[i - firstdefault]) specs.append(spec) if varargs is not None: specs.append(formatvarargs(formatargandannotation(varargs))) else: if kwonlyargs: specs.append("*") if kwonlyargs: for kwonlyarg in kwonlyargs: spec = formatargandannotation(kwonlyarg) if kwonlydefaults and kwonlyarg in kwonlydefaults: spec += formatvalue(kwonlydefaults[kwonlyarg]) specs.append(spec) if varkw is not None: specs.append(formatvarkw(formatargandannotation(varkw))) result = "(" + ", ".join(specs) + ")" if "return" in annotations: result += formatreturns(formatannotation(annotations["return"])) return result
def format_annotation(an): if an == inspect.Parameter.empty: return "" if isinstance(an, types.FunctionType) and ( an.__code__.co_filename.endswith("typing.py") and an.__code__.co_name == "new_type" ): return an.__name__ out = inspect.formatannotation(an) out = out.replace("conducto.pipeline.", "") return out
def _get_args(func: inspect.Signature) -> Iterable[MethodArgument]: found_first = False for argname, arg in func.parameters.items(): if found_first is False and argname in {'self', 'cls'}: continue if found_first is False: found_first = True yield MethodArgument( name=argname, value=inspect.formatannotation(arg.annotation), )
def inspect_formatargspec( args, varargs=None, varkw=None, defaults=None, kwonlyargs=(), kwonlydefaults={}, annotations={}, formatarg=str, formatvarargs=lambda name: '*' + name, formatvarkw=lambda name: '**' + name, formatvalue=lambda value: '=' + repr(value), formatreturns=lambda text: ' -> ' + text, formatannotation=formatannotation): """Copy formatargspec from python 3.7 standard library. Python 3 has deprecated formatargspec and requested that Signature be used instead, however this requires a full reimplementation of formatargspec() in terms of creating Parameter objects and such. Instead of introducing all the object-creation overhead and having to reinvent from scratch, just copy their compatibility routine. Utimately we would need to rewrite our "decorator" routine completely which is not really worth it right now, until all Python 2.x support is dropped. """ def formatargandannotation(arg): result = formatarg(arg) if arg in annotations: result += ': ' + formatannotation(annotations[arg]) return result specs = [] if defaults: firstdefault = len(args) - len(defaults) for i, arg in enumerate(args): spec = formatargandannotation(arg) if defaults and i >= firstdefault: spec = spec + formatvalue(defaults[i - firstdefault]) specs.append(spec) if varargs is not None: specs.append(formatvarargs(formatargandannotation(varargs))) else: if kwonlyargs: specs.append('*') if kwonlyargs: for kwonlyarg in kwonlyargs: spec = formatargandannotation(kwonlyarg) if kwonlydefaults and kwonlyarg in kwonlydefaults: spec += formatvalue(kwonlydefaults[kwonlyarg]) specs.append(spec) if varkw is not None: specs.append(formatvarkw(formatargandannotation(varkw))) result = '(' + ', '.join(specs) + ')' if 'return' in annotations: result += formatreturns(formatannotation(annotations['return'])) return result
def inspect_formatargspec(args, varargs=None, varkw=None, defaults=None, kwonlyargs=(), kwonlydefaults={}, annotations={}, formatarg=str, formatvarargs=lambda name: '*' + name, formatvarkw=lambda name: '**' + name, formatvalue=lambda value: '=' + repr(value), formatreturns=lambda text: ' -> ' + text, formatannotation=formatannotation): """Copy formatargspec from python 3.7 standard library. Python 3 has deprecated formatargspec and requested that Signature be used instead, however this requires a full reimplementation of formatargspec() in terms of creating Parameter objects and such. Instead of introducing all the object-creation overhead and having to reinvent from scratch, just copy their compatibility routine. """ def formatargandannotation(arg): result = formatarg(arg) if arg in annotations: result += ': ' + formatannotation(annotations[arg]) return result specs = [] if defaults: firstdefault = len(args) - len(defaults) for i, arg in enumerate(args): spec = formatargandannotation(arg) if defaults and i >= firstdefault: spec = spec + formatvalue(defaults[i - firstdefault]) specs.append(spec) if varargs is not None: specs.append(formatvarargs(formatargandannotation(varargs))) else: if kwonlyargs: specs.append('*') if kwonlyargs: for kwonlyarg in kwonlyargs: spec = formatargandannotation(kwonlyarg) if kwonlydefaults and kwonlyarg in kwonlydefaults: spec += formatvalue(kwonlydefaults[kwonlyarg]) specs.append(spec) if varkw is not None: specs.append(formatvarkw(formatargandannotation(varkw))) result = '(' + ', '.join(specs) + ')' if 'return' in annotations: result += formatreturns(formatannotation(annotations['return'])) return result
def __str__(self): """do not render separators commented code is what was taken out from the copy/pasted inspect module code :) """ result = [] # render_pos_only_separator = False # render_kw_only_separator = True for param in self.parameters.values(): formatted = str(param) # kind = param.kind # if kind == inspect._POSITIONAL_ONLY: # render_pos_only_separator = True # elif render_pos_only_separator: # # It's not a positional-only parameter, and the flag # # is set to 'True' (there were pos-only params before.) # result.append('/') # render_pos_only_separator = False # if kind == inspect._VAR_POSITIONAL: # # OK, we have an '*args'-like parameter, so we won't need # # a '*' to separate keyword-only arguments # render_kw_only_separator = False # elif kind == inspect._KEYWORD_ONLY and render_kw_only_separator: # # We have a keyword-only parameter to render and we haven't # # rendered an '*args'-like parameter before, so add a '*' # # separator to the parameters list # # ("foo(arg1, *, arg2)" case) # result.append('*') # # This condition should be only triggered once, so # # reset the flag # render_kw_only_separator = False result.append(formatted) # if render_pos_only_separator: # # There were only positional-only parameters, hence the # # flag was not reset to 'False' # result.append('/') rendered = '({})'.format(', '.join(result)) if self.return_annotation is not inspect._empty: anno = inspect.formatannotation(self.return_annotation) rendered += ' -> {}'.format(anno) return rendered
def _init_restype_fromsignature(self): try: sig = inspect.signature(self.callable) except Exception: return # If signature has a return annotation, it's in the # full signature and we don't need it from here. if not sig or sig.return_annotation == inspect._empty: return ann = inspect.formatannotation(sig.return_annotation) if not ann or not self._can_eval(ann): return return 'return ' + ann
def _return_annotation(name, module, obj, link=None): try: annot = typing.get_type_hints(obj).get('return', '') except NameError as e: warn("Error handling return annotation for {}: {}".format(name, e.args[0])) annot = inspect.signature(inspect.unwrap(obj)).return_annotation if annot == inspect.Parameter.empty: annot = '' if not annot: return '' s = inspect.formatannotation(annot).replace(' ', '\N{NBSP}') # Better line breaks if link: from pdoc.html_helpers import _linkify s = re.sub(r'[\w\.]+', partial(_linkify, link=link, module=module), s) return s
def collect(cls): values = [] for name, v in cls._context_members_.items(): attrname = cls._context_attrs_[name] attrdef = getattr(cls, attrname) doc = getattr(attrdef, '__doc__') if inspect.isfunction(attrdef): sig = inspect.signature(attrdef) result = inspect.formatannotation(sig.return_annotation) sig_good_part = ContextMethod( name=name, args=list(_get_args(sig)), result=result, doc=doc, ) elif isinstance(attrdef, property): sig = inspect.signature(attrdef.fget) sig_txt = inspect.formatannotation(sig.return_annotation) sig_good_part = ContextValue(name=name, value=sig_txt, doc=doc) else: sig_good_part = Unknown(name=name, value=repr(attrdef), doc=doc) values.append(sig_good_part) return values
def return_annotation(self, *, link=None): """Formatted function return type annotation or empty string if none.""" try: annot = typing.get_type_hints(self.obj).get('return', '') except NameError as e: warn("Error handling return annotation for {}: {}".format( self.name, e.args[0])) annot = inspect.signature(inspect.unwrap( self.obj)).return_annotation if annot == inspect.Parameter.empty: annot = '' if not annot: return '' s = inspect.formatannotation(annot).replace( ' ', '\N{NBSP}') # Better line breaks if link: from app.pdoc.html_helpers import _linkify s = re.sub(r'[\w\.]+', partial(_linkify, link=link, module=self.module), s) return s
def __str__(self): """do not render separators commented code is what was taken out from the copy/pasted inspect module code :) """ result = [] # render_pos_only_separator = False # render_kw_only_separator = True for param in self.parameters.values(): formatted = str(param) result.append(formatted) rendered = '({})'.format(', '.join(result)) if self.return_annotation is not inspect._empty: anno = inspect.formatannotation(self.return_annotation) rendered += ' -> {}'.format(anno) return rendered
def _render_signature(obj_signature, obj_name): """ This was mostly taken from inspect.Signature.__str__. Look there for the comments. The only change is to add linebreaks when this gets too long. """ result = [] pos_only = False kw_only = True for param in obj_signature.parameters.values(): if param.kind == inspect._POSITIONAL_ONLY: pos_only = True elif pos_only: result.append('/') pos_only = False if param.kind == inspect._VAR_POSITIONAL: kw_only = False elif param.kind == inspect._KEYWORD_ONLY and kw_only: result.append('*') kw_only = False result.append(str(param)) if pos_only: result.append('/') # add up name, parameters, braces (2), and commas if len(obj_name) + sum(len(r) + 2 for r in result) > 75: # This doesn’t fit behind “Signature: ” in an inspect window. rendered = '{}(\n{})'.format(obj_name, ''.join( ' {},\n'.format(r) for r in result) ) else: rendered = '{}({})'.format(obj_name, ', '.join(result)) if obj_signature.return_annotation is not inspect._empty: anno = inspect.formatannotation(obj_signature.return_annotation) rendered += ' -> {}'.format(anno) return rendered
def formatargandannotation(arg): result = formatarg(arg) if arg in annotations: result += ": " + formatannotation(annotations[arg]) return result
def _annotation_to_str(self, annotation): return inspect.formatannotation(annotation)
def _params(func_obj, annotate=False, link=None, module=None): try: signature = inspect.signature(inspect.unwrap(func_obj)) except ValueError: # I guess this is for C builtin functions? # TODO: Extract signature from the first line of the docstring, i.e. # https://github.com/mitmproxy/pdoc/commit/010d996003bc5b72fcf5fa515edbcc0142819919 return ["..."] def safe_default_value(p: inspect.Parameter): value = p.default if value is inspect.Parameter.empty: return p replacement = next((i for i in ( 'os.environ', 'sys.stdin', 'sys.stdout', 'sys.stderr', ) if value is eval(i)), None) if not replacement: if isinstance(value, enum.Enum): replacement = str(value) elif inspect.isclass(value): replacement = value.__module__ + '.' + value.__qualname__ elif ' at 0x' in repr(value): replacement = re.sub(r' at 0x\w+', '', repr(value)) nonlocal link if link and ('<' in repr(value) or '>' in repr(value)): import html replacement = html.escape(replacement or repr(value)) if replacement: class mock: def __repr__(self): return replacement return p.replace(default=mock()) return p params = [] kw_only = False EMPTY = inspect.Parameter.empty if link: from pdoc.html_helpers import _linkify _linkify = partial(_linkify, link=link, module=module) for p in signature.parameters.values(): # type: inspect.Parameter if not _is_public(p.name) and p.default is not EMPTY: continue if p.kind == p.VAR_POSITIONAL: kw_only = True if p.kind == p.KEYWORD_ONLY and not kw_only: kw_only = True params.append('*') p = safe_default_value(p) if not annotate: p = p.replace(annotation=EMPTY) s = str(p) if p.annotation is not EMPTY: if sys.version_info < (3, 7): # PEP8-normalize whitespace s = re.sub(r'(?<!\s)=(?!\s)', ' = ', re.sub(r':(?!\s)', ': ', s, 1), 1) # "Eval" forward-declarations (typing string literals) if isinstance(p.annotation, str): s = s.replace("'%s'" % p.annotation, p.annotation, 1) s = s.replace(' ', '\N{NBSP}') # prevent improper line breaking if link: annotation = inspect.formatannotation(p.annotation) linked_annotation = re.sub(r'[\w\.]+', _linkify, annotation) s = linked_annotation.join(s.rsplit(annotation, 1)) # "rreplace" once params.append(s) return params
def make_parser(func: Callable, add_short_args: bool = True, validate_dict_types: bool = False) -> ArgumentParser: """ Automatically configure an argparse parser for `func`. To get automatic * help strings: write a docstring in the same format as this one (in particular, use ":param param_name: help string here"). * types: use type annotations. The only supported types from `typing` are listed below. * `List[type]` and `Sequence[type]` will use `nargs="+", type=type`. * `Optional[type]` converts inputs using `type`; a `None` is only possible if this is the default value. * `Dict[key_type, val_type]` uses `json.loads` or, if the input doesn't parse as JSON and it's available, `yaml.safe_load` to parse the input. If the parsed value is not a dictionary, an `ArgumentTypeError` is thrown. Tips: * quote the dictionary argument on the command line * JSON: use double-quotes around all strings (`-p '{"key": "val"}'`) * JSON: keys must be strings * YAML: put spaces between keys and values (`-p "{key: val}"` NOT `-p "{key:val}"`) * `bool` uses `str2bool`; values have to be entered like `--debug True` * defaults: just use defaults * required params: this is just the parameters with no default values All command line arguments are configured to be "keyword only". Except for the exceptions noted above, the type annotation will be used directly as the type for the parser. This means it must be a callable which accepts a single string as input and returns the desired Python object. :param func: :param add_short_args: if True, "-<short_name>" is used in addition to "--<param_name>". The short names are created by taking the first character of each _-delimited substring (e.g. "my_arg" -> "ma"). If multiple short names would be the same, none are used. :param validate_dict_types: if True, `ArgumentTypeError` is thrown if any dictionary keys / values aren't of the proper type. """ docstring = func.__doc__ or "" match = re.search(r"(.*?)(?=\n\s*:|$)", docstring, re.DOTALL) description = re.sub(r"\n\s*", " ", match.group(1)).strip() if match else "" parser = ArgumentParser(description=description) signature = inspect.signature(func) if add_short_args: names = signature.parameters.keys() short_names = [ "".join(s[0] for s in name.split("_")) for name in names ] if len(set(short_names)) != len(short_names): # name conflicts add_short_args = False for i, param in enumerate(signature.parameters.values()): kwargs = {} anno = param.annotation origin = getattr(anno, "__origin__", None) if origin in (list, Sequence): # e.g. List[int] kwargs["type"] = anno.__args__[0] kwargs["nargs"] = "+" elif origin == Union: # Optional[T] is converted to Union[T, None] if len(anno.__args__) == 2 and anno.__args__[1] == type(None): anno = anno.__args__[0] origin = getattr(anno, "__origin__", None) if origin in (list, Sequence): kwargs["nargs"] = "+" kwargs["type"] = anno.__args__[0] elif origin == dict: key_type, val_type = anno.__args__ dict_type = make_dict_type(key_type, val_type, validate_dict_types) kwargs["type"] = dict_type elif anno == bool: kwargs["type"] = str2bool else: kwargs["type"] = anno elif origin == dict: key_type, val_type = anno.__args__ dict_type = make_dict_type(key_type, val_type, validate_dict_types) kwargs["type"] = dict_type else: if anno is not param.empty: if anno == bool: kwargs["type"] = str2bool else: kwargs["type"] = anno if param.default is not param.empty: kwargs["default"] = param.default else: kwargs["required"] = True match = re.search(fr":param {param.name}:(.+?)(?=\n\s*:|$)", docstring, re.DOTALL) help_str = re.sub(r"\n\s*", " ", match.group(1)).strip() if match else "" annotation_str = inspect.formatannotation(anno) if "default" in kwargs: help_str += f" [{annotation_str}={kwargs['default']}]" else: help_str += f" [{annotation_str}]" kwargs["help"] = help_str if add_short_args: parser.add_argument(f"-{short_names[i]}", f"--{param.name}", **kwargs) else: parser.add_argument(f"--{param.name}", **kwargs) return parser
def __init__(self, name: str, type: Type): self.name = name self.type = type self.type_show = inspect.formatannotation(type)
def print_docstring(obj, file, depth): """Prints a classes's docstring to a file.""" doc = ClassDoc(obj) if inspect.isclass(obj) else FunctionDoc(obj) printf = functools.partial(print, file=file) printf(h1(obj.__name__)) printf( linkifier.linkify_fences(paragraph(concat_lines(doc["Summary"])), depth)) printf( linkifier.linkify_fences( paragraph(concat_lines(doc["Extended Summary"])), depth)) # We infer the type annotations from the signatures, and therefore rely on the signature # instead of the docstring for documenting parameters try: signature = inspect.signature(obj) except ValueError: signature = ( inspect.Signature() ) # TODO: this is necessary for Cython classes, but it's not correct params_desc = { param.name: " ".join(param.desc) for param in doc["Parameters"] } # Parameters if signature.parameters: printf(h2("Parameters")) for param in signature.parameters.values(): # Name printf(f"- **{param.name}**", end="") # Type annotation if param.annotation is not param.empty: anno = inspect.formatannotation(param.annotation) anno = linkifier.linkify_dotted(anno, depth) printf(f" (*{anno}*)", end="") # Default value if param.default is not param.empty: printf(f" – defaults to `{param.default}`", end="") printf("\n", file=file) # Description desc = params_desc[param.name] if desc: printf(f" {desc}\n") printf("") # Attributes if doc["Attributes"]: printf(h2("Attributes")) for attr in doc["Attributes"]: # Name printf(f"- **{attr.name}**", end="") # Type annotation if attr.type: printf(f" (*{attr.type}*)", end="") printf("\n", file=file) # Description desc = " ".join(attr.desc) if desc: printf(f" {desc}\n") printf("") # Examples if doc["Examples"]: printf(h2("Examples")) in_code = False after_space = False for line in inspect.cleandoc("\n".join(doc["Examples"])).splitlines(): if (in_code and after_space and line and not line.startswith(">>>") and not line.startswith("...")): printf("```\n") in_code = False after_space = False if not in_code and line.startswith(">>>"): printf("```python") in_code = True after_space = False if not line: after_space = True printf(line) if in_code: printf("```") printf("") # Methods if inspect.isclass(obj) and doc["Methods"]: printf(h2("Methods")) printf_indent = lambda x, **kwargs: printf(f" {x}", **kwargs) for meth in doc["Methods"]: printf(paragraph(f'???- note "{meth.name}"')) # Parse method docstring docstring = inherit_docstring(c=obj, meth=meth.name) if not docstring: continue meth_doc = FunctionDoc(func=None, doc=docstring) printf_indent(paragraph(" ".join(meth_doc["Summary"]))) if meth_doc["Extended Summary"]: printf_indent(paragraph(" ".join( meth_doc["Extended Summary"]))) # We infer the type annotations from the signatures, and therefore rely on the signature # instead of the docstring for documenting parameters signature = inherit_signature(obj, meth.name) params_desc = { param.name: " ".join(param.desc) for param in doc["Parameters"] } # Parameters if len(signature.parameters ) > 1: # signature is never empty, but self doesn't count printf_indent("**Parameters**\n") for param in signature.parameters.values(): if param.name == "self": continue # Name printf_indent(f"- **{param.name}**", end="") # Type annotation if param.annotation is not param.empty: printf_indent( f" (*{inspect.formatannotation(param.annotation)}*)", end="") # Default value if param.default is not param.empty: printf_indent(f" – defaults to `{param.default}`", end="") printf_indent("", file=file) # Description desc = params_desc.get(param.name) if desc: printf_indent(f" {desc}") printf_indent("") # Returns if meth_doc["Returns"]: printf_indent("**Returns**\n") return_val = meth_doc["Returns"][0] if signature.return_annotation is not inspect._empty: if inspect.isclass(signature.return_annotation): printf_indent( f"*{signature.return_annotation.__name__}*: ", end="") else: printf_indent(f"*{signature.return_annotation}*: ", end="") printf_indent(return_val.type) printf_indent("") # Notes if doc["Notes"]: printf(h2("Notes")) printf(paragraph("\n".join(doc["Notes"]))) # References if doc["References"]: printf(h2("References")) printf(paragraph("\n".join(doc["References"])))
def is_annotation_matched(var: object, annotation: object) -> bool: """Return True if annotation is matched with the given variable. {Any, List, Set, Tuple, Dict, Union, Callable, Iterable, Iterator} from "typing" are supported. Parameters: var: The variable annotation: The annotation Returns: True if matched, otherwise False. """ var_type = type(var) anno_str = str(annotation).split('[')[0] if var is None and annotation is None: return True elif type(annotation) == type: return issubclass(var_type, annotation) elif anno_str.startswith('typing.'): anno_type = anno_str[7:] if anno_type == 'Any': return True elif anno_type == 'List': sub_annotation = annotation.__args__ if var_type == list: if sub_annotation is None: return True else: return all(map(lambda x: is_annotation_matched(x, sub_annotation[0]), var)) else: return False elif anno_type == 'Set': sub_annotation = annotation.__args__ if var_type == set: if sub_annotation is None: return True else: return all(map(lambda x: is_annotation_matched(x, sub_annotation[0]), var)) else: return False elif anno_type == 'Iterable': # currently we can't check the type of items return issubclass(var_type, collections.abc.Iterable) elif anno_type == 'Iterator': # currently we can't check the type of items return issubclass(var_type, collections.abc.Iterator) elif anno_type == 'Tuple': sub_annotation = annotation.__args__ if var_type == tuple: if sub_annotation is None: return True if len(sub_annotation) != len(var): return False else: return all(map(lambda x, y: is_annotation_matched(x, y), var, sub_annotation)) else: return False elif anno_type == 'Dict': sub_annotation = annotation.__args__ if var_type == dict: if sub_annotation is None: return True else: key_anno, val_anno = sub_annotation return all(map( lambda x: is_annotation_matched(x[0], key_anno) and \ is_annotation_matched(x[1], val_anno), var.items())) else: return False elif anno_type == 'Union': sub_annotation = annotation.__args__ if sub_annotation is None: return False else: return any(map(lambda y: is_annotation_matched(var, y), sub_annotation)) elif anno_type == 'Callable': sub_annotation = annotation.__args__ if callable(var): if sub_annotation is None: return True else: if type(var).__name__ == 'NestModule': # Nest module # filter out resolved / optional params sig = var.sig func_annos = [v.annotation for k, v in sig.parameters.items() \ if not k in var.params.keys() and v.default is inspect.Parameter.empty] else: # regular callable object sig = inspect.signature(var) func_annos = [v.annotation for v in sig.parameters.values()] if len(func_annos) == len(sub_annotation) - 1: return all(map(lambda x, y: x == y, func_annos, sub_annotation)) and \ (sub_annotation[-1] == Any or \ sub_annotation[-1] == object or \ sig.return_annotation == sub_annotation[-1] or (sig.return_annotation is None and sub_annotation[-1] == type(None))) else: return False else: return False raise NotImplementedError('The annotation type %s is not supported' % inspect.formatannotation(annotation))
def _annotation_to_str(self, annotation): if py_version < 30: return '' return inspect.formatannotation(annotation)
def test_formatannotation_still_unfixed(): """when this tests starts to fail, we can remove the workaround in our formatannotation wrapper""" assert formatannotation(list[str]) == "list[str]" assert inspect.formatannotation(list[str]) == "list"