Skip to content

Commit

Permalink
Merge pull request #270 from willmcgugan/inspect
Browse files Browse the repository at this point in the history
Add rich.inspect
  • Loading branch information
willmcgugan committed Sep 7, 2020
2 parents 38b11c8 + 3691fb1 commit c3ee3b0
Show file tree
Hide file tree
Showing 18 changed files with 460 additions and 51 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [6.1.0] - Unreleased

### Added

- New inspect module
- Added os.\_Environ to pretty print

### Fixed

- Prevented recursive renderables from getting stuck

## Changed

- force_terminal and force_jupyter can now be used to force the disabled state, or left as None to auto-detect.
- Panel now expands to fit title if supplied

## [6.0.0] - 2020-08-25

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion docs/source/highlighting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Here's a silly example that highlights every character with a different color::
class RainbowHighlighter(Highlighter):
def highlight(self, text):
for index in range(len(text)):
text.stylize(str(randint(16, 255)), index, index + 1)
text.stylize(f"color({randint(16, 255)})", index, index + 1)


rainbow = RainbowHighlighter()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "6.0.0"
version = "6.1.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"
Expand Down
45 changes: 45 additions & 0 deletions rich/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Rich text and beautiful formatting in the terminal."""

from typing import Any, IO, Optional, TYPE_CHECKING

if TYPE_CHECKING:
Expand Down Expand Up @@ -35,5 +37,48 @@ def print(
return write_console.print(*objects, sep=sep, end=end)


def inspect(
obj: Any,
*,
console: "Console" = None,
title: str = None,
help: bool = False,
methods: bool = False,
docs: bool = True,
private: bool = False,
dunder: bool = False,
sort: bool = True,
all: bool = False,
):
"""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 (begining 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.
"""
_console = console or get_console()
from rich._inspect import Inspect

_inspect = Inspect(
obj,
title=title,
help=help,
methods=methods,
docs=docs,
private=private,
dunder=dunder,
sort=sort,
all=all,
)
_console.print(_inspect)


if __name__ == "__main__": # pragma: no cover
print("Hello, **World**")
232 changes: 232 additions & 0 deletions rich/_inspect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
from __future__ import absolute_import

from inspect import cleandoc, getdoc, getfile, isclass, isfunction, ismodule, signature
from typing import Any, Iterable, List, Optional, Tuple

from .console import Console, ConsoleOptions, RenderableType, RenderGroup, RenderResult
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


def _reformat_doc(doc: str) -> str:
"""Reformat docstring."""
doc = cleandoc(doc).strip()
return doc


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 (begining 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.
"""

def __init__(
self,
obj: Any,
*,
title: TextType = None,
help: bool = False,
methods: bool = False,
docs: bool = True,
private: bool = False,
dunder: bool = False,
sort: bool = True,
all: 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

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) -> "RenderableType":
return Panel.fit(
RenderGroup(*self._render()),
title=self.title,
border_style="scope.border",
padding=(0, 1),
)

def _get_signature(self, name: str, obj: Any) -> Text:
"""Get a signature for a callable."""
try:
_signature = str(signature(obj)) + ":"
except ValueError:
_signature = "(...)"

source_filename: Optional[str] = None
try:
source_filename = getfile(obj)
except TypeError:
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)
qual_signature = Text.assemble((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):
yield self._get_signature("", obj)
yield ""

_doc = getdoc(obj)
if _doc is not None:
if not self.help:
_doc = _first_paragraph(_doc)
doc_text = Text(_reformat_doc(_doc), style="inspect.help")
doc_text = highlighter(doc_text)
yield doc_text
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 self.docs:
docs = getdoc(value)
if docs is not None:
_doc = _reformat_doc(str(docs))
if not self.help:
_doc = _first_paragraph(_doc)
_signature_text.append("\n" if "\n" in _doc else " ")
doc = highlighter(_doc)
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
else:
yield self.highlighter(
Text.from_markup(
f"[i]{not_shown_count} attribute(s) not shown.[/i] Use inspect(<OBJECT>, all=True) to see all attributes."
)
)


if __name__ == "__main__": # type: ignore
from rich import print

inspect = Inspect({}, docs=True, methods=True, dunder=True)
print(inspect)

t = Text("Hello, World")
print(Inspect(t))

from rich.style import Style
from rich.color import Color

print(Inspect(Style.parse("bold red on black"), methods=True, docs=True))
print(Inspect(Color.parse("#ffe326"), methods=True, docs=True))

from rich import get_console

print(Inspect(get_console(), methods=False))

print(Inspect(open("foo.txt", "wt"), methods=False))

print(Inspect("Hello", methods=False, dunder=True))
print(Inspect(inspect, methods=False, dunder=False, docs=False))

class Foo:
@property
def broken(self):
1 / 0

f = Foo()
print(Inspect(f))

print(Inspect(object, dunder=True))

print(Inspect(None, dunder=False))

print(Inspect(str, help=True))
print(Inspect(1, help=False))
11 changes: 10 additions & 1 deletion rich/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,18 @@ def system(self) -> ColorSystem:
def get_truecolor(
self, theme: "TerminalTheme" = None, foreground=True
) -> ColorTriplet:
"""Get am equivalent color triplet for this color.
Args:
theme (TerminalTheme, optional): Optional terminal theme, or None to use default. Defaults to None.
foreground (bool, optional): True for a foreground color, or False for background. Defaults to True.
Returns:
ColorTriplet: A color triplet containing RGB components.
"""

if theme is None:
theme = DEFAULT_TERMINAL_THEME
"""Get a color triplet for this color."""
if self.type == ColorType.TRUECOLOR:
assert self.triplet is not None
return self.triplet
Expand Down

2 comments on commit c3ee3b0

@codenotworking
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discovered when using inspect() from a script file, it doesn't need to be wrapped in a print() statement to output to the console.

@willmcgugan
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codenotworking that is correct.

Please sign in to comment.