Skip to content

Commit a6a54d0

Browse files
authored
Merge pull request #1696 from willmcgugan/rich-cast-protocol
Rich cast protocol
2 parents 1897318 + 0349c2c commit a6a54d0

File tree

7 files changed

+302
-268
lines changed

7 files changed

+302
-268
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [10.13.1] - 2021-11-09
8+
## [10.14.0] - Unreleased
99

1010
### Fixed
1111

@@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
### Added
1818

1919
- Added file protocol to URL highlighter https://github.com/willmcgugan/rich/issues/1681
20+
- Added rich.protocol.rich_cast
21+
22+
### Changed
23+
24+
- Allowed `__rich__` to work recursively
2025

2126
## [10.13.0] - 2021-11-07
2227

poetry.lock

Lines changed: 221 additions & 251 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ include = ["rich/py.typed"]
2727

2828
[tool.poetry.dependencies]
2929
python = "^3.6.2"
30-
typing-extensions = { version = "^3.7.4", python = "<3.8" }
30+
typing-extensions = { version = ">=3.7.4, <5.0", python = "<3.8" }
3131
dataclasses = { version = ">=0.7,<0.9", python = "<3.7" }
3232
pygments = "^2.6.0"
3333
commonmark = "^0.9.0"

rich/console.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
Mapping,
2626
NamedTuple,
2727
Optional,
28+
Set,
2829
TextIO,
2930
Tuple,
3031
Type,
@@ -53,6 +54,7 @@
5354
from .measure import Measurement, measure_renderables
5455
from .pager import Pager, SystemPager
5556
from .pretty import Pretty, is_expandable
57+
from .protocol import rich_cast
5658
from .region import Region
5759
from .scope import render_scope
5860
from .screen import Screen
@@ -1220,8 +1222,8 @@ def render(
12201222
# No space to render anything. This prevents potential recursion errors.
12211223
return
12221224
render_iterable: RenderResult
1223-
if hasattr(renderable, "__rich__") and not isclass(renderable):
1224-
renderable = renderable.__rich__() # type: ignore
1225+
1226+
renderable = rich_cast(renderable)
12251227
if hasattr(renderable, "__rich_console__") and not isclass(renderable):
12261228
render_iterable = renderable.__rich_console__(self, _options) # type: ignore
12271229
elif isinstance(renderable, str):
@@ -1439,15 +1441,7 @@ def check_text() -> None:
14391441
del text[:]
14401442

14411443
for renderable in objects:
1442-
# I promise this is sane
1443-
# This detects an object which claims to have all attributes, such as MagicMock.mock_calls
1444-
if hasattr(
1445-
renderable, "jwevpw_eors4dfo6mwo345ermk7kdnfnwerwer"
1446-
): # pragma: no cover
1447-
renderable = repr(renderable)
1448-
rich_cast = getattr(renderable, "__rich__", None)
1449-
if rich_cast:
1450-
renderable = rich_cast()
1444+
renderable = rich_cast(renderable)
14511445
if isinstance(renderable, str):
14521446
append_text(
14531447
self.render_str(

rich/measure.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Callable, Iterable, NamedTuple, Optional, TYPE_CHECKING
33

44
from . import errors
5-
from .protocol import is_renderable
5+
from .protocol import is_renderable, rich_cast
66

77
if TYPE_CHECKING:
88
from .console import Console, ConsoleOptions, RenderableType
@@ -97,8 +97,7 @@ def get(
9797
return Measurement(0, 0)
9898
if isinstance(renderable, str):
9999
renderable = console.render_str(renderable, markup=options.markup)
100-
if hasattr(renderable, "__rich__"):
101-
renderable = renderable.__rich__() # type: ignore
100+
renderable = rich_cast(renderable)
102101
if is_renderable(renderable):
103102
get_console_width: Optional[
104103
Callable[["Console", "ConsoleOptions"], "Measurement"]

rich/protocol.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from typing import Any
1+
from typing import Any, Callable, cast, Set, TYPE_CHECKING
2+
from inspect import isclass
3+
4+
if TYPE_CHECKING:
5+
from rich.console import RenderableType
6+
7+
_GIBBERISH = """aihwerij235234ljsdnp34ksodfipwoe234234jlskjdf"""
28

39

410
def is_renderable(check_object: Any) -> bool:
@@ -8,3 +14,29 @@ def is_renderable(check_object: Any) -> bool:
814
or hasattr(check_object, "__rich__")
915
or hasattr(check_object, "__rich_console__")
1016
)
17+
18+
19+
def rich_cast(renderable: object) -> "RenderableType":
20+
"""Cast an object to a renderable by calling __rich__ if present.
21+
22+
Args:
23+
renderable (object): A potentially renderable object
24+
25+
Returns:
26+
object: The result of recursively calling __rich__.
27+
"""
28+
from rich.console import RenderableType
29+
30+
rich_visited_set: Set[type] = set() # Prevent potential infinite loop
31+
while hasattr(renderable, "__rich__") and not isclass(renderable):
32+
# Detect object which claim to have all the attributes
33+
if hasattr(renderable, _GIBBERISH):
34+
return repr(renderable)
35+
cast_method = getattr(renderable, "__rich__")
36+
renderable = cast_method()
37+
renderable_type = type(renderable)
38+
if renderable_type in rich_visited_set:
39+
break
40+
rich_visited_set.add(renderable_type)
41+
42+
return cast(RenderableType, renderable)

tests/test_protocol.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,37 @@ def test_abc():
3333
assert not isinstance(foo, str)
3434
assert not isinstance("foo", RichRenderable)
3535
assert not isinstance([], RichRenderable)
36+
37+
38+
def test_cast_deep():
39+
class B:
40+
def __rich__(self) -> Foo:
41+
return Foo()
42+
43+
class A:
44+
def __rich__(self) -> B:
45+
return B()
46+
47+
console = Console(file=io.StringIO())
48+
console.print(A())
49+
assert console.file.getvalue() == "Foo\n"
50+
51+
52+
def test_cast_recursive():
53+
class B:
54+
def __rich__(self) -> "A":
55+
return A()
56+
57+
def __repr__(self) -> str:
58+
return "<B>"
59+
60+
class A:
61+
def __rich__(self) -> B:
62+
return B()
63+
64+
def __repr__(self) -> str:
65+
return "<A>"
66+
67+
console = Console(file=io.StringIO())
68+
console.print(A())
69+
assert console.file.getvalue() == "<B>\n"

0 commit comments

Comments
 (0)