Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for named tuples to pretty #2031

Merged
merged 9 commits into from Mar 8, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,4 @@

# Changelog

All notable changes to this project will be documented in this file.
Expand All @@ -14,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added ProgressColumn `MofNCompleteColumn` to display raw `completed/total` column (similar to DownloadColumn,
but displays values as ints, does not convert to floats or add bit/bytes units).
https://github.com/Textualize/rich/pull/1941
- Add support for namedtuples to `Pretty` https://github.com/Textualize/rich/pull/2031

### Fixed

Expand Down
56 changes: 55 additions & 1 deletion rich/pretty.py
Expand Up @@ -524,6 +524,28 @@ def __str__(self) -> str:
)


def _is_namedtuple(obj: Any) -> bool:
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved
"""Checks if an object is most likely a namedtuple. It is possible
to craft an object that passes this check and isn't a namedtuple, but
there is only a minuscule chance of this happening unintentionally.

Args:
obj (Any): The object to test

Returns:
bool: True if the object is a namedtuple. False otherwise.
"""
base_classes = getattr(type(obj), "__bases__", [])
if len(base_classes) != 1 or base_classes[0] != tuple:
darrenburns marked this conversation as resolved.
Show resolved Hide resolved
return False

fields = getattr(obj, "_fields", None)
if not fields or not isinstance(fields, tuple):
darrenburns marked this conversation as resolved.
Show resolved Hide resolved
return False

return all(type(field) == str for field in fields)


def traverse(
_object: Any,
max_length: Optional[int] = None,
Expand Down Expand Up @@ -731,7 +753,23 @@ def iter_attrs() -> Iterable[
append(child_node)

pop_visited(obj_id)

elif _is_namedtuple(obj):
darrenburns marked this conversation as resolved.
Show resolved Hide resolved
children = []
append = children.append
if reached_max_depth:
node = Node(value_repr="...")
else:
node = Node(
open_brace=f"{obj.__class__.__name__}(",
close_brace=")",
children=children,
)
for last, (key, value) in loop_last(obj._asdict().items()):
child_node = _traverse(value, depth=depth + 1)
child_node.key_repr = key
child_node.last = last
child_node.key_separator = "="
append(child_node)
elif _safe_isinstance(obj, _CONTAINERS):
for container_type in _CONTAINERS:
if _safe_isinstance(obj, container_type):
Expand Down Expand Up @@ -878,6 +916,15 @@ def __repr__(self) -> str:
1 / 0
return "this will fail"

from typing import NamedTuple

class StockKeepingUnit(NamedTuple):
name: str
description: str
price: float
category: str
reviews: List[str]

d = defaultdict(int)
d["foo"] = 5
data = {
Expand All @@ -904,6 +951,13 @@ def __repr__(self) -> str:
]
),
"atomic": (False, True, None),
"namedtuple": StockKeepingUnit(
"Sparkling British Spring Water",
"Carbonated spring water",
0.9,
"water",
["its amazing!", "its terrible!"],
),
"Broken": BrokenRepr(),
}
data["foo"].append(data) # type: ignore
Expand Down
52 changes: 51 additions & 1 deletion tests/test_pretty.py
Expand Up @@ -3,7 +3,7 @@
from array import array
from collections import UserDict, defaultdict
from dataclasses import dataclass, field
from typing import List
from typing import List, NamedTuple

import attr
import pytest
Expand Down Expand Up @@ -169,6 +169,56 @@ def test_pretty_dataclass():
assert result == "ExampleDataclass(foo=1000, bar=..., baz=['foo', 'bar', 'baz'])"


class StockKeepingUnit(NamedTuple):
name: str
description: str
price: float
category: str
reviews: List[str]


def test_pretty_namedtuple():
console = Console(color_system=None)
console.begin_capture()

example_namedtuple = StockKeepingUnit(
"Sparkling British Spring Water",
"Carbonated spring water",
0.9,
"water",
["its amazing!", "its terrible!"],
)

result = pretty_repr(example_namedtuple)

print(result)
assert (
result
== """StockKeepingUnit(
name='Sparkling British Spring Water',
description='Carbonated spring water',
price=0.9,
category='water',
reviews=['its amazing!', 'its terrible!']
)"""
)


def test_pretty_namedtuple_fields_invalid_type():
class LooksLikeANamedTupleButIsnt(tuple):
_fields = "blah"

instance = LooksLikeANamedTupleButIsnt()
result = pretty_repr(instance)
assert result == "()" # Treated as tuple


def test_pretty_namedtuple_max_depth():
instance = {"unit": StockKeepingUnit("a", "b", 1.0, "c", ["d", "e"])}
result = pretty_repr(instance, max_depth=1)
assert result == "{'unit': ...}"


def test_small_width():
test = ["Hello world! 12345"]
result = pretty_repr(test, max_width=10)
Expand Down