123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- from __future__ import absolute_import
- 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
|