import inspect from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature from typing import Any, Collection, Iterable, Optional, Tuple, Type, Union from .console import Group, RenderableType from .control import escape_control_codes from .highlighter import ReprHighlighter from .jupyter import JupyterMixin from .panel import Panel from .pretty import Pretty from .table import Table from .text import Text, TextType def _first_paragraph(doc: str) -> str: """Get the first paragraph from a docstring.""" paragraph, _, _ = doc.partition("\n\n") return paragraph class Inspect(JupyterMixin): """A renderable to inspect any Python Object. Args: obj (Any): An object to inspect. title (str, optional): Title to display over inspect result, or None use type. Defaults to None. help (bool, optional): Show full help text rather than just first paragraph. Defaults to False. methods (bool, optional): Enable inspection of callables. Defaults to False. docs (bool, optional): Also render doc strings. Defaults to True. private (bool, optional): Show private attributes (beginning with underscore). Defaults to False. dunder (bool, optional): Show attributes starting with double underscore. Defaults to False. sort (bool, optional): Sort attributes alphabetically. Defaults to True. all (bool, optional): Show all attributes. Defaults to False. value (bool, optional): Pretty print value of object. Defaults to True. """ def __init__( self, obj: Any, *, title: Optional[TextType] = None, help: bool = False, methods: bool = False, docs: bool = True, private: bool = False, dunder: bool = False, sort: bool = True, all: bool = True, value: bool = True, ) -> None: self.highlighter = ReprHighlighter() self.obj = obj self.title = title or self._make_title(obj) if all: methods = private = dunder = True self.help = help self.methods = methods self.docs = docs or help self.private = private or dunder self.dunder = dunder self.sort = sort self.value = value def _make_title(self, obj: Any) -> Text: """Make a default title.""" title_str = ( str(obj) if (isclass(obj) or callable(obj) or ismodule(obj)) else str(type(obj)) ) title_text = self.highlighter(title_str) return title_text def __rich__(self) -> Panel: return Panel.fit( Group(*self._render()), title=self.title, border_style="scope.border", padding=(0, 1), ) def _get_signature(self, name: str, obj: Any) -> Optional[Text]: """Get a signature for a callable.""" try: _signature = str(signature(obj)) + ":" except ValueError: _signature = "(...)" except TypeError: return None source_filename: Optional[str] = None try: source_filename = getfile(obj) except (OSError, TypeError): # OSError is raised if obj has no source file, e.g. when defined in REPL. pass callable_name = Text(name, style="inspect.callable") if source_filename: callable_name.stylize(f"link file://{source_filename}") signature_text = self.highlighter(_signature) qualname = name or getattr(obj, "__qualname__", name) # If obj is a module, there may be classes (which are callable) to display if inspect.isclass(obj): prefix = "class" elif inspect.iscoroutinefunction(obj): prefix = "async def" else: prefix = "def" qual_signature = Text.assemble( (f"{prefix} ", f"inspect.{prefix.replace(' ', '_')}"), (qualname, "inspect.callable"), signature_text, ) return qual_signature def _render(self) -> Iterable[RenderableType]: """Render object.""" def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]: key, (_error, value) = item return (callable(value), key.strip("_").lower()) def safe_getattr(attr_name: str) -> Tuple[Any, Any]: """Get attribute or any exception.""" try: return (None, getattr(obj, attr_name)) except Exception as error: return (error, None) obj = self.obj keys = dir(obj) total_items = len(keys) if not self.dunder: keys = [key for key in keys if not key.startswith("__")] if not self.private: keys = [key for key in keys if not key.startswith("_")] not_shown_count = total_items - len(keys) items = [(key, safe_getattr(key)) for key in keys] if self.sort: items.sort(key=sort_items) items_table = Table.grid(padding=(0, 1), expand=False) items_table.add_column(justify="right") add_row = items_table.add_row highlighter = self.highlighter if callable(obj): signature = self._get_signature("", obj) if signature is not None: yield signature yield "" if self.docs: _doc = self._get_formatted_doc(obj) if _doc is not None: doc_text = Text(_doc, style="inspect.help") doc_text = highlighter(doc_text) yield doc_text yield "" if self.value and not (isclass(obj) or callable(obj) or ismodule(obj)): yield Panel( Pretty(obj, indent_guides=True, max_length=10, max_string=60), border_style="inspect.value.border", ) yield "" for key, (error, value) in items: key_text = Text.assemble( ( key, "inspect.attr.dunder" if key.startswith("__") else "inspect.attr", ), (" =", "inspect.equals"), ) if error is not None: warning = key_text.copy() warning.stylize("inspect.error") add_row(warning, highlighter(repr(error))) continue if callable(value): if not self.methods: continue _signature_text = self._get_signature(key, value) if _signature_text is None: add_row(key_text, Pretty(value, highlighter=highlighter)) else: if self.docs: docs = self._get_formatted_doc(value) if docs is not None: _signature_text.append("\n" if "\n" in docs else " ") doc = highlighter(docs) doc.stylize("inspect.doc") _signature_text.append(doc) add_row(key_text, _signature_text) else: add_row(key_text, Pretty(value, highlighter=highlighter)) if items_table.row_count: yield items_table elif not_shown_count: yield Text.from_markup( f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] " f"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options." ) def _get_formatted_doc(self, object_: Any) -> Optional[str]: """ Extract the docstring of an object, process it and returns it. The processing consists in cleaning up the doctring's indentation, taking only its 1st paragraph if `self.help` is not True, and escape its control codes. Args: object_ (Any): the object to get the docstring from. Returns: Optional[str]: the processed docstring, or None if no docstring was found. """ docs = getdoc(object_) if docs is None: return None docs = cleandoc(docs).strip() if not self.help: docs = _first_paragraph(docs) return escape_control_codes(docs) def get_object_types_mro(obj: Union[object, Type[Any]]) -> Tuple[type, ...]: """Returns the MRO of an object's class, or of the object itself if it's a class.""" if not hasattr(obj, "__mro__"): # N.B. we cannot use `if type(obj) is type` here because it doesn't work with # some types of classes, such as the ones that use abc.ABCMeta. obj = type(obj) return getattr(obj, "__mro__", ()) def get_object_types_mro_as_strings(obj: object) -> Collection[str]: """ Returns the MRO of an object's class as full qualified names, or of the object itself if it's a class. Examples: `object_types_mro_as_strings(JSONDecoder)` will return `['json.decoder.JSONDecoder', 'builtins.object']` """ return [ f'{getattr(type_, "__module__", "")}.{getattr(type_, "__qualname__", "")}' for type_ in get_object_types_mro(obj) ] def is_object_one_of_types( obj: object, fully_qualified_types_names: Collection[str] ) -> bool: """ Returns `True` if the given object's class (or the object itself, if it's a class) has one of the fully qualified names in its MRO. """ for type_name in get_object_types_mro_as_strings(obj): if type_name in fully_qualified_types_names: return True return False