pytermgui.inspector
This module provides introspection utilities.
The inspect
method can be used to create an Inspector
widget, which can
then be used to see what is happening inside any python object. This method is
usually preferred for instantiating an Inspector
, as it sets up overwriteable default
arguments passed to the new widget.
These defaults are meant to hide the non-important information when they are not needed,
in order to allow the least amount of code for the most usability. For example, by
default, when passed a class, inspect
will clip the docstrings to their first lines,
but show all methods. When an class' method is given it will hide show the full
docstring, and also use the method's fully qualified name.
View Source
"""This module provides introspection utilities. The `inspect` method can be used to create an `Inspector` widget, which can then be used to see what is happening inside any python object. This method is usually preferred for instantiating an `Inspector`, as it sets up overwriteable default arguments passed to the new widget. These defaults are meant to hide the non-important information when they are not needed, in order to allow the least amount of code for the most usability. For example, by default, when passed a class, `inspect` will clip the docstrings to their first lines, but show all methods. When an class' method is given it will hide show the full docstring, and also use the method's fully qualified name. """ # pylint: disable=too-many-instance-attributes # Note: There are a lot of `type: ignore`-s in this file. These show up in places where # mypy sees potential for an error, but the possible error is already counteracted. from __future__ import annotations from enum import Enum, auto as _auto from typing import Any, Iterator from inspect import ( signature, isclass, ismodule, isfunction, getdoc, getfile, ) from .parser import tim from .helpers import real_length from .prettifiers import prettify from .ansi_interface import terminal from .widgets import Widget, Container, Label, boxes try: from typing import get_origin # pylint: disable=ungrouped-imports except NameError: def get_origin(_: object) -> Any: # type: ignore """Spoofs typing.get_origin, which is used to determine type-hints. Since this function is only available >=3.8, we need to have some implementation on it for 3.7. The code checks for the origin to be non-null, as that is the value returned by this method on non-typing objects. This will cause annotations to show up on 3.7, but not on 3.8+. """ return None __all__ = ["Inspector", "inspect"] class ObjectType(Enum): """All types an object can be.""" LIVE = _auto() """An instance that does not fit the other types.""" CLASS = _auto() """A class object.""" MODULE = _auto() """A module object.""" BUILTIN = _auto() """Some sort of a builtin object. As builtins are often implemented in C, a lot of the standard python APIs won't work on them, so we need to treat them separately.""" FUNCTION = _auto() """A callable object, that is not a class.""" def _is_builtin(target: object) -> bool: """Determines if the given target is a builtin.""" try: signature(target) # type: ignore return False except (ValueError, TypeError): return True def _determine_type(target: object) -> ObjectType: """Determines the type of an object.""" if ismodule(target): return ObjectType.MODULE if _is_builtin(target): return ObjectType.BUILTIN if isclass(target): return ObjectType.CLASS if isfunction(target) or callable(target): return ObjectType.FUNCTION return ObjectType.LIVE def _is_type_alias(obj: object) -> bool: """Determines whether the given object is (likely) a type alias.""" return get_origin(obj) is not None INDENTED_EMPTY_BOX = boxes.Box( [ " ", " x", "", ] ) def inspect(target: object, **inspector_args) -> Inspector: """Inspects an object. Args: obj: The object to inspect. show_private: Whether `_private` attributes should be shown. show_dunder: Whether `__dunder__` attributes should be shown. show_methods: Whether methods should be shown when encountering a class. show_full_doc: If not set, docstrings are cut to only include their first line. show_qualname: Show fully-qualified name, e.g. `module.submodule.name` instead of `name`. """ def _conditionally_overwrite_kwarg(**kwargs) -> None: for key, value in kwargs.items(): if not key in inspector_args: inspector_args[key] = value if ismodule(target): _conditionally_overwrite_kwarg( show_dunder=False, show_private=False, show_full_doc=False, show_methods=True, show_qualname=False, ) elif isclass(target): _conditionally_overwrite_kwarg( show_dunder=True, show_private=False, show_full_doc=False, show_methods=True, show_qualname=False, ) elif callable(target): _conditionally_overwrite_kwarg( show_dunder=False, show_private=False, show_full_doc=True, show_methods=False, show_qualname=True, ) else: _conditionally_overwrite_kwarg( show_dunder=False, show_private=False, show_full_doc=True, show_methods=True, show_qualname=False, ) inspector = Inspector(**inspector_args).inspect(target) return inspector class Inspector(Container): """A widget to inspect any Python object.""" def __init__( # pylint: disable=too-many-arguments self, target: object = None, show_private: bool = False, show_dunder: bool = False, show_methods: bool = False, show_full_doc: bool = False, show_qualname: bool = True, **attrs: Any, ): """Initializes an inspector. Note that most of the time, using `inspect` to do this is going to be more useful. Some styles of the inspector can be changed using the `inspector-name`, `inspector-file` and `inspector-keyword` markup aliases. The rest of the highlighting is done using `pprint`, with all of its respective colors. Args: show_private: Whether `_private` attributes should be shown. show_dunder: Whether `__dunder__` attributes should be shown. show_methods: Whether methods should be shown when encountering a class. show_full_doc: If not set, docstrings are cut to only include their first line. show_qualname: Show fully-qualified name, e.g. `module.submodule.name` instead of `name`. """ if "box" not in attrs: attrs["box"] = "EMPTY" super().__init__(**attrs) self.width = terminal.width self.show_private = show_private self.show_dunder = show_dunder self.show_methods = show_methods self.show_full_doc = show_full_doc self.show_qualname = show_qualname # TODO: Fix attr-showing self.show_attrs = False self.target: object if target is not None: self.inspect(target) self.target = target def _get_header(self) -> Container: """Creates a header containing the name and location of the object.""" header = Container(box="SINGLE") line = "[inspector-name]" if self.target_type is ObjectType.MODULE: line += self.target.__name__ # type: ignore else: cls = ( self.target if isclass(self.target) or isfunction(self.target) else self.target.__class__ ) line += cls.__module__ + "." + cls.__qualname__ # type: ignore header += line try: file = getfile(self.target) # type: ignore except TypeError: return header header += f"Located in [inspector-file !link(file://{file})]{file}" return header def _get_definition(self) -> Label: """Returns the definition str of self.target.""" target = self.target if self.show_qualname: name = getattr(target, "__qualname__", type(target).__name__) else: name = getattr(target, "__name__", type(target).__name__) definition = "[inspector-keyword]" if self.target_type == ObjectType.LIVE: target = type(target) otype = _determine_type(target) if otype == ObjectType.CLASS: definition += "class " elif otype == ObjectType.FUNCTION: definition += "def " definition += "[/ inspector-identifier]" + name + "[/]" try: definition += self.highlight(str(signature(target))) + ":" # type: ignore except (TypeError, ValueError): definition += "(...)" return Label(definition, parent_align=0, non_first_padding=4) def _get_docs(self, padding: int) -> Iterator[Label]: """Returns a list of Labels of the object's documentation.""" if self.target.__doc__ is None: return doc = getdoc(self.target) if doc is None: return for i, line in enumerate(doc.splitlines()): if i == 1 and not self.show_full_doc: return line = line.replace("[", r"\[") yield Label( "[102]" + line, parent_align=0, padding=padding, non_first_padding=4 ) def _get_keys(self) -> list[str]: """Gets all inspectable keys of an object. It first checks for an `__all__` attribute, and substitutes `dir` if not found. Then, if there are too many keys and the given target is a module it tries to list all of the present submodules. """ keys = getattr(self.target, "__all__", dir(self.target)) if not self.show_dunder: keys = [key for key in keys if not key.startswith("__")] if not self.show_private: keys = [key for key in keys if not (key.startswith("_") and key[1] != "_")] if not self.show_methods: keys = [key for key in keys if not callable(getattr(self.target, key))] keys.sort(key=lambda item: callable(getattr(self.target, item, None))) return keys def _get_preview(self) -> Container: """Gets a Container with self.target inside.""" preview = Container(static_width=self.width // 2, parent_align=0, box="SINGLE") if isinstance(self.target, str): preview.lazy_add(prettify(tim.get_markup(self.target), parse=False)) return preview for line in prettify(self.target).splitlines(): if real_length(line) > preview.width - preview.sidelength: preview.width = real_length(line) + preview.sidelength preview.lazy_add(Label(tim.get_markup(line), parent_align=0)) return preview @staticmethod def highlight(text: str) -> str: """Applies highlighting to a given string. This highlight includes keywords, builtin types and more. Args: text: The string to highlight. Returns: Unparsed markup. """ def _split(text: str, chars: str = " ,:|()[]{}") -> list[tuple[str, str]]: """Splits given text by the given chars. Args: text: The text to split. chars: A string of characters we will split by. Returns: A tuple of (delimiter, word) tuples. Delimiter is one of the characters of `chars`. """ last_delim = "" output = [] word = "" for char in text: if char in chars: output.append((last_delim, word)) last_delim = char word = "" continue word += char output.append((last_delim, word)) return output buff = "" for (delim, word) in _split(text): stripped = word.strip("'") if delim == "[": delim = r"\[" pretty = prettify(stripped, indent=0, parse=False) if pretty != f"[pprint-str]'{stripped}'[/]": buff += delim + pretty continue buff += delim + word return buff def inspect(self, target: object) -> Inspector: """Inspects a given object, and sets self.target to it. Returns: Self, with the new content based on the inspection. """ self.target = target self.target_type = _determine_type(target) # Header if self.box is not INDENTED_EMPTY_BOX: self.lazy_add(self._get_header()) # Body if self.target_type is not ObjectType.MODULE: self.lazy_add(self._get_definition()) padding = 0 if self.target_type is ObjectType.MODULE else 4 for item in self._get_docs(padding): self.lazy_add(item) keys = self._get_keys() for key in keys: attr = getattr(target, key, None) # Don't show type aliases if _is_type_alias(attr): continue # Only show functions if they are not lambdas if isfunction(attr) and not attr.__name__ == "<lambda>": self.lazy_add( Inspector( box=INDENTED_EMPTY_BOX, show_dunder=self.show_dunder, show_private=self.show_private, show_full_doc=self.show_full_doc, show_qualname=self.show_qualname, ).inspect(attr) ) continue if not self.show_attrs: continue for i, line in enumerate(prettify(attr, parse=False).splitlines()): if i == 0: line = f"- {key}: {line}" self.lazy_add(Label(line, parent_align=0)) # Footer if self.target_type in [ObjectType.LIVE, ObjectType.BUILTIN]: self.lazy_add(self._get_preview()) return self def debug(self) -> str: """Returns identifiable information used in repr.""" if terminal.is_interactive and not terminal.displayhook_installed: return "\n".join(self.get_lines()) return Widget.debug(self)
View Source
class Inspector(Container): """A widget to inspect any Python object.""" def __init__( # pylint: disable=too-many-arguments self, target: object = None, show_private: bool = False, show_dunder: bool = False, show_methods: bool = False, show_full_doc: bool = False, show_qualname: bool = True, **attrs: Any, ): """Initializes an inspector. Note that most of the time, using `inspect` to do this is going to be more useful. Some styles of the inspector can be changed using the `inspector-name`, `inspector-file` and `inspector-keyword` markup aliases. The rest of the highlighting is done using `pprint`, with all of its respective colors. Args: show_private: Whether `_private` attributes should be shown. show_dunder: Whether `__dunder__` attributes should be shown. show_methods: Whether methods should be shown when encountering a class. show_full_doc: If not set, docstrings are cut to only include their first line. show_qualname: Show fully-qualified name, e.g. `module.submodule.name` instead of `name`. """ if "box" not in attrs: attrs["box"] = "EMPTY" super().__init__(**attrs) self.width = terminal.width self.show_private = show_private self.show_dunder = show_dunder self.show_methods = show_methods self.show_full_doc = show_full_doc self.show_qualname = show_qualname # TODO: Fix attr-showing self.show_attrs = False self.target: object if target is not None: self.inspect(target) self.target = target def _get_header(self) -> Container: """Creates a header containing the name and location of the object.""" header = Container(box="SINGLE") line = "[inspector-name]" if self.target_type is ObjectType.MODULE: line += self.target.__name__ # type: ignore else: cls = ( self.target if isclass(self.target) or isfunction(self.target) else self.target.__class__ ) line += cls.__module__ + "." + cls.__qualname__ # type: ignore header += line try: file = getfile(self.target) # type: ignore except TypeError: return header header += f"Located in [inspector-file !link(file://{file})]{file}" return header def _get_definition(self) -> Label: """Returns the definition str of self.target.""" target = self.target if self.show_qualname: name = getattr(target, "__qualname__", type(target).__name__) else: name = getattr(target, "__name__", type(target).__name__) definition = "[inspector-keyword]" if self.target_type == ObjectType.LIVE: target = type(target) otype = _determine_type(target) if otype == ObjectType.CLASS: definition += "class " elif otype == ObjectType.FUNCTION: definition += "def " definition += "[/ inspector-identifier]" + name + "[/]" try: definition += self.highlight(str(signature(target))) + ":" # type: ignore except (TypeError, ValueError): definition += "(...)" return Label(definition, parent_align=0, non_first_padding=4) def _get_docs(self, padding: int) -> Iterator[Label]: """Returns a list of Labels of the object's documentation.""" if self.target.__doc__ is None: return doc = getdoc(self.target) if doc is None: return for i, line in enumerate(doc.splitlines()): if i == 1 and not self.show_full_doc: return line = line.replace("[", r"\[") yield Label( "[102]" + line, parent_align=0, padding=padding, non_first_padding=4 ) def _get_keys(self) -> list[str]: """Gets all inspectable keys of an object. It first checks for an `__all__` attribute, and substitutes `dir` if not found. Then, if there are too many keys and the given target is a module it tries to list all of the present submodules. """ keys = getattr(self.target, "__all__", dir(self.target)) if not self.show_dunder: keys = [key for key in keys if not key.startswith("__")] if not self.show_private: keys = [key for key in keys if not (key.startswith("_") and key[1] != "_")] if not self.show_methods: keys = [key for key in keys if not callable(getattr(self.target, key))] keys.sort(key=lambda item: callable(getattr(self.target, item, None))) return keys def _get_preview(self) -> Container: """Gets a Container with self.target inside.""" preview = Container(static_width=self.width // 2, parent_align=0, box="SINGLE") if isinstance(self.target, str): preview.lazy_add(prettify(tim.get_markup(self.target), parse=False)) return preview for line in prettify(self.target).splitlines(): if real_length(line) > preview.width - preview.sidelength: preview.width = real_length(line) + preview.sidelength preview.lazy_add(Label(tim.get_markup(line), parent_align=0)) return preview @staticmethod def highlight(text: str) -> str: """Applies highlighting to a given string. This highlight includes keywords, builtin types and more. Args: text: The string to highlight. Returns: Unparsed markup. """ def _split(text: str, chars: str = " ,:|()[]{}") -> list[tuple[str, str]]: """Splits given text by the given chars. Args: text: The text to split. chars: A string of characters we will split by. Returns: A tuple of (delimiter, word) tuples. Delimiter is one of the characters of `chars`. """ last_delim = "" output = [] word = "" for char in text: if char in chars: output.append((last_delim, word)) last_delim = char word = "" continue word += char output.append((last_delim, word)) return output buff = "" for (delim, word) in _split(text): stripped = word.strip("'") if delim == "[": delim = r"\[" pretty = prettify(stripped, indent=0, parse=False) if pretty != f"[pprint-str]'{stripped}'[/]": buff += delim + pretty continue buff += delim + word return buff def inspect(self, target: object) -> Inspector: """Inspects a given object, and sets self.target to it. Returns: Self, with the new content based on the inspection. """ self.target = target self.target_type = _determine_type(target) # Header if self.box is not INDENTED_EMPTY_BOX: self.lazy_add(self._get_header()) # Body if self.target_type is not ObjectType.MODULE: self.lazy_add(self._get_definition()) padding = 0 if self.target_type is ObjectType.MODULE else 4 for item in self._get_docs(padding): self.lazy_add(item) keys = self._get_keys() for key in keys: attr = getattr(target, key, None) # Don't show type aliases if _is_type_alias(attr): continue # Only show functions if they are not lambdas if isfunction(attr) and not attr.__name__ == "<lambda>": self.lazy_add( Inspector( box=INDENTED_EMPTY_BOX, show_dunder=self.show_dunder, show_private=self.show_private, show_full_doc=self.show_full_doc, show_qualname=self.show_qualname, ).inspect(attr) ) continue if not self.show_attrs: continue for i, line in enumerate(prettify(attr, parse=False).splitlines()): if i == 0: line = f"- {key}: {line}" self.lazy_add(Label(line, parent_align=0)) # Footer if self.target_type in [ObjectType.LIVE, ObjectType.BUILTIN]: self.lazy_add(self._get_preview()) return self def debug(self) -> str: """Returns identifiable information used in repr.""" if terminal.is_interactive and not terminal.displayhook_installed: return "\n".join(self.get_lines()) return Widget.debug(self)
A widget to inspect any Python object.
View Source
def __init__( # pylint: disable=too-many-arguments self, target: object = None, show_private: bool = False, show_dunder: bool = False, show_methods: bool = False, show_full_doc: bool = False, show_qualname: bool = True, **attrs: Any, ): """Initializes an inspector. Note that most of the time, using `inspect` to do this is going to be more useful. Some styles of the inspector can be changed using the `inspector-name`, `inspector-file` and `inspector-keyword` markup aliases. The rest of the highlighting is done using `pprint`, with all of its respective colors. Args: show_private: Whether `_private` attributes should be shown. show_dunder: Whether `__dunder__` attributes should be shown. show_methods: Whether methods should be shown when encountering a class. show_full_doc: If not set, docstrings are cut to only include their first line. show_qualname: Show fully-qualified name, e.g. `module.submodule.name` instead of `name`. """ if "box" not in attrs: attrs["box"] = "EMPTY" super().__init__(**attrs) self.width = terminal.width self.show_private = show_private self.show_dunder = show_dunder self.show_methods = show_methods self.show_full_doc = show_full_doc self.show_qualname = show_qualname # TODO: Fix attr-showing self.show_attrs = False self.target: object if target is not None: self.inspect(target) self.target = target
Initializes an inspector.
Note that most of the time, using inspect
to do this is going to be more
useful.
Some styles of the inspector can be changed using the inspector-name
,
inspector-file
and inspector-keyword
markup aliases. The rest of the
highlighting is done using pprint
, with all of its respective colors.
Args
- show_private: Whether
_private
attributes should be shown. - show_dunder: Whether
__dunder__
attributes should be shown. - show_methods: Whether methods should be shown when encountering a class.
- show_full_doc: If not set, docstrings are cut to only include their first line.
- show_qualname: Show fully-qualified name, e.g.
module.submodule.name
instead ofname
.
View Source
@staticmethod def highlight(text: str) -> str: """Applies highlighting to a given string. This highlight includes keywords, builtin types and more. Args: text: The string to highlight. Returns: Unparsed markup. """ def _split(text: str, chars: str = " ,:|()[]{}") -> list[tuple[str, str]]: """Splits given text by the given chars. Args: text: The text to split. chars: A string of characters we will split by. Returns: A tuple of (delimiter, word) tuples. Delimiter is one of the characters of `chars`. """ last_delim = "" output = [] word = "" for char in text: if char in chars: output.append((last_delim, word)) last_delim = char word = "" continue word += char output.append((last_delim, word)) return output buff = "" for (delim, word) in _split(text): stripped = word.strip("'") if delim == "[": delim = r"\[" pretty = prettify(stripped, indent=0, parse=False) if pretty != f"[pprint-str]'{stripped}'[/]": buff += delim + pretty continue buff += delim + word return buff
Applies highlighting to a given string.
This highlight includes keywords, builtin types and more.
Args
- text: The string to highlight.
Returns
Unparsed markup.
View Source
def inspect(self, target: object) -> Inspector: """Inspects a given object, and sets self.target to it. Returns: Self, with the new content based on the inspection. """ self.target = target self.target_type = _determine_type(target) # Header if self.box is not INDENTED_EMPTY_BOX: self.lazy_add(self._get_header()) # Body if self.target_type is not ObjectType.MODULE: self.lazy_add(self._get_definition()) padding = 0 if self.target_type is ObjectType.MODULE else 4 for item in self._get_docs(padding): self.lazy_add(item) keys = self._get_keys() for key in keys: attr = getattr(target, key, None) # Don't show type aliases if _is_type_alias(attr): continue # Only show functions if they are not lambdas if isfunction(attr) and not attr.__name__ == "<lambda>": self.lazy_add( Inspector( box=INDENTED_EMPTY_BOX, show_dunder=self.show_dunder, show_private=self.show_private, show_full_doc=self.show_full_doc, show_qualname=self.show_qualname, ).inspect(attr) ) continue if not self.show_attrs: continue for i, line in enumerate(prettify(attr, parse=False).splitlines()): if i == 0: line = f"- {key}: {line}" self.lazy_add(Label(line, parent_align=0)) # Footer if self.target_type in [ObjectType.LIVE, ObjectType.BUILTIN]: self.lazy_add(self._get_preview()) return self
Inspects a given object, and sets self.target to it.
Returns
Self, with the new content based on the inspection.
View Source
def debug(self) -> str: """Returns identifiable information used in repr.""" if terminal.is_interactive and not terminal.displayhook_installed: return "\n".join(self.get_lines()) return Widget.debug(self)
Returns identifiable information used in repr.
Inherited Members
- pytermgui.widgets.layouts.Container
- styles
- chars
- keys
- serialized
- vertical_align
- allow_fullscreen
- overflow
- sidelength
- selectables
- selectables_length
- selected
- box
- lazy_add
- get_lines
- set_widgets
- serialize
- pop
- remove
- set_recursive_depth
- select
- scroll
- scroll_end
- center
- handle_mouse
- execute_binding
- handle_key
- wipe
View Source
def inspect(target: object, **inspector_args) -> Inspector: """Inspects an object. Args: obj: The object to inspect. show_private: Whether `_private` attributes should be shown. show_dunder: Whether `__dunder__` attributes should be shown. show_methods: Whether methods should be shown when encountering a class. show_full_doc: If not set, docstrings are cut to only include their first line. show_qualname: Show fully-qualified name, e.g. `module.submodule.name` instead of `name`. """ def _conditionally_overwrite_kwarg(**kwargs) -> None: for key, value in kwargs.items(): if not key in inspector_args: inspector_args[key] = value if ismodule(target): _conditionally_overwrite_kwarg( show_dunder=False, show_private=False, show_full_doc=False, show_methods=True, show_qualname=False, ) elif isclass(target): _conditionally_overwrite_kwarg( show_dunder=True, show_private=False, show_full_doc=False, show_methods=True, show_qualname=False, ) elif callable(target): _conditionally_overwrite_kwarg( show_dunder=False, show_private=False, show_full_doc=True, show_methods=False, show_qualname=True, ) else: _conditionally_overwrite_kwarg( show_dunder=False, show_private=False, show_full_doc=True, show_methods=True, show_qualname=False, ) inspector = Inspector(**inspector_args).inspect(target) return inspector
Inspects an object.
Args
- obj: The object to inspect.
- show_private: Whether
_private
attributes should be shown. - show_dunder: Whether
__dunder__
attributes should be shown. - show_methods: Whether methods should be shown when encountering a class.
- show_full_doc: If not set, docstrings are cut to only include their first line.
- show_qualname: Show fully-qualified name, e.g.
module.submodule.name
instead ofname
.