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 utilities for printing complex JSON objects #1099

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1464947
Add code to create a RichTable for a complex object
rickwporter Dec 20, 2024
9d585ce
Add .DS_Store to .gitignore for Mac users
rickwporter Dec 20, 2024
62c8569
Add print_rich_object() to print a complex object in json|yaml|text f…
rickwporter Dec 20, 2024
7106ed1
Refactor: add function that allows for providing own console
rickwporter Dec 20, 2024
9efbf73
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Dec 20, 2024
944d48b
Add missing dependency
rickwporter Dec 20, 2024
0c0f863
More dependency work
rickwporter Dec 20, 2024
f454bf0
Split/strip output to avoid tests failing because pre-commit removes …
rickwporter Dec 20, 2024
d1d6a30
Use startswith to check rich output
rickwporter Dec 21, 2024
53ebe69
Fix the right test this time... sigh
rickwporter Dec 21, 2024
32dc2b1
Use List/Dict from typing instead of builtins
rickwporter Dec 22, 2024
c411de3
Try to avoid CR issues on Windows
rickwporter Dec 22, 2024
df57719
Another typing fix
rickwporter Dec 22, 2024
29c61e2
Improve debugability
rickwporter Dec 22, 2024
53abdbb
Attempt to avoid differences in non-ASCII terminal outputs
rickwporter Dec 22, 2024
38b114f
Add TableConfig to allow table customization
rickwporter Jan 1, 2025
d6f760c
Allow print_rich_object() to take a TableConfig
rickwporter Jan 1, 2025
a57f8e8
Different approach for testing
rickwporter Jan 1, 2025
08ce680
More test updates -- use local copies and full comparison (for easier…
rickwporter Jan 2, 2025
61162a5
Merge branch 'master' into rich-table
rickwporter Jan 7, 2025
97e4bec
Merge branch 'master' into rich-table
rickwporter Jan 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ htmlcov
coverage.xml
.coverage*
.cache

# for Mac users who browse to HTML coverage results
.DS_Store
2 changes: 2 additions & 0 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ ruff ==0.8.6
# Needed explicitly by typer-slim
rich >=10.11.0
shellingham >=1.3.0
PyYAML >= 6.0
types-PyYAML >= 6.0
2 changes: 1 addition & 1 deletion scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export _TYPER_FORCE_DISABLE_TERMINAL=1
# Run autocompletion install tests in the CI
export _TYPER_RUN_INSTALL_COMPLETION_TESTS=1
# It seems xdist-pytest ensures modified sys.path to import relative modules in examples keeps working
pytest --cov --cov-report=term-missing -o console_output_style=progress --numprocesses=auto ${@}
pytest --cov --cov-report=term-missing -o console_output_style=progress --showlocals --numprocesses=auto ${@}
335 changes: 335 additions & 0 deletions tests/test_rich_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
from copy import deepcopy
from itertools import zip_longest

import pytest
from rich.box import HEAVY_HEAD
from typer.rich_table import RichTable, TableConfig, rich_table_factory

SIMPLE_DICT = {
"abc": "def",
"ghi": False,
"jkl": ["mno", "pqr", "stu"],
"vwx": [1, 2, 4],
2: 3,
"yxa": None,
}

LONG_VALUES = {
"mid-url": "https://typer.tiangolo.com/virtual-environments/#install-packages-directly",
"really looooooooooooooooooonnnng key value": "sna",
"long value": "a" * 75,
"long": "ftp://typer.tiangolo.com/virtual-environments/#install-packages-directly?12345678901234568901234567890123",
}

INNER_LIST = {
"prop 1": "simple",
"prOp B": [
{"name": "sna", "abc": "def", "ghi": True},
{"name": "foo", "abc": "def", "ghi": None},
{"name": "bar", "abc": "def", "ghi": 1.2345},
{"abc": "def", "ghi": "blah"},
],
"Prop III": None,
}


def test_rich_table_defaults_outer():
columns = ["col 1", "Column B", "III"]
uut = RichTable(*columns, outer=True)
assert len(uut.columns) == len(columns)
assert uut.highlight
assert uut.row_styles == []
assert uut.caption_justify == "left"
assert uut.border_style is None
assert uut.leading == 0

assert uut.show_header
assert uut.show_edge
assert uut.box == HEAVY_HEAD

for name, column in zip_longest(columns, uut.columns):
assert column.header == name
assert column.overflow == "ignore"
assert column.no_wrap
assert column.justify == "left"


def test_rich_table_defaults_inner():
columns = ["col 1", "Column B", "III"]
uut = RichTable(*columns, outer=False)
assert len(uut.columns) == len(columns)
assert uut.highlight
assert uut.row_styles == []
assert uut.caption_justify == "left"
assert uut.border_style is None
assert uut.leading == 0

assert not uut.show_header
assert not uut.show_edge
assert uut.box is None

for name, column in zip_longest(columns, uut.columns):
assert column.header == name
assert column.overflow == "ignore"
assert column.no_wrap
assert column.justify == "left"


def test_create_table_not_obj():
with pytest.raises(ValueError) as excinfo:
rich_table_factory([1, 2, 3])

assert excinfo.match("Unable to create table for type list")


def test_create_table_simple_dict():
uut = rich_table_factory(SIMPLE_DICT)

# basic outer table stuff for object
assert len(uut.columns) == 2
assert uut.show_header
assert uut.show_edge
assert not uut.show_lines

# data-driven info
assert uut.row_count == 6


def test_create_table_list_nameless_dict():
items = [SIMPLE_DICT, SIMPLE_DICT, {"foo": "bar"}]
uut = rich_table_factory(items)

# basic outer table stuff for object
assert len(uut.columns) == 1
assert uut.show_header
assert uut.show_edge
assert uut.show_lines

# data-driven info
assert uut.row_count == len(items)


def test_create_table_list_named_dict():
names = ["sna", "foo", "bar", "baz"]
items = []
for name in names:
item = deepcopy(SIMPLE_DICT)
item["name"] = name
items.append(item)

uut = rich_table_factory(items)

# basic outer table stuff for object
assert len(uut.columns) == 2
assert uut.show_header
assert uut.show_edge
assert uut.show_lines

# data-driven info
assert uut.row_count == len(items)
assert uut.caption == f"Found {len(items)} items"

col0 = uut.columns[0]
col1 = uut.columns[1]
for left, right, name, item in zip_longest(col0._cells, col1._cells, names, items):
assert left == name
inner_keys = right.columns[0]._cells
item_keys = [str(k) for k in item.keys() if k != "name"]
assert inner_keys == item_keys


def test_create_table_truncted():
data = deepcopy(LONG_VALUES)

uut = rich_table_factory(data)

assert uut.row_count == 4
col0 = uut.columns[0]
col1 = uut.columns[1]

assert col0.header == "Property"
assert col1.header == "Value"

# url has longer length than "normal" fields
index = 0
left = col0._cells[index]
right = col1._cells[index]
assert left == "mid-url"
assert (
right
== "https://typer.tiangolo.com/virtual-environments/#install-packages-directly"
)

# keys get truncated at 35 characters
index = 1
left = col0._cells[index]
right = col1._cells[index]
assert left == "really looooooooooooooooooonnnng..."
assert right == "sna"

# non-url values get truncated at 50 characters
index = 2
left = col0._cells[index]
right = col1._cells[index]
assert left == "long value"
assert right == "a" * 47 + "..."

# really long urls get truncated at 100 characters
index = 3
left = col0._cells[index]
right = col1._cells[index]
assert left == "long"
assert (
right
== "ftp://typer.tiangolo.com/virtual-environments/#install-packages-directly?123456789012345689012345..."
)


def test_create_table_inner_list():
data = deepcopy(INNER_LIST)

uut = rich_table_factory(data)
assert uut.row_count == 3
assert len(uut.columns) == 2
col0 = uut.columns[0]
col1 = uut.columns[1]

left = col0._cells[0]
right = col1._cells[0]
assert left == "prop 1"
assert right == "simple"

left = col0._cells[2]
right = col1._cells[2]
assert left == "Prop III"
assert right == "None"

left = col0._cells[1]
inner = col1._cells[1]
assert left == "prOp B"
assert len(inner.columns) == 2
assert inner.row_count == 4
names = inner.columns[0]._cells
assert names == ["sna", "foo", "bar", "Unknown"]


def test_create_table_config_truncated():
config = TableConfig(url_max_len=16, value_max_len=20, key_max_len=4)
data = deepcopy(LONG_VALUES)

uut = rich_table_factory(data, config)

assert uut.row_count == 4
col0 = uut.columns[0]
col1 = uut.columns[1]

# keys truncated at 4 characters, and urls at 14
index = 0
left = col0._cells[index]
right = col1._cells[index]
assert left == "m..."
assert right == "https://typer..."

# keys truncated at 4 characters, and short fields are not truncated
index = 1
left = col0._cells[index]
right = col1._cells[index]
assert left == "r..."
assert right == "sna"

# non-url values get truncated at 20 characters
index = 2
left = col0._cells[index]
right = col1._cells[index]
assert left == "l..."
assert right == "a" * 17 + "..."

# really long urls get truncated at 16 characters
index = 3
left = col0._cells[index]
right = col1._cells[index]
assert left == "l..."
assert right == "ftp://typer.t..."


def test_create_table_config_fields():
config = TableConfig(
url_max_len=16,
value_max_len=50,
key_max_len=100,
url_prefixes=["https://"], # do NOT recognize ftp as a prefix
property_label="foo",
value_label="bar",
)
data = deepcopy(LONG_VALUES)

uut = rich_table_factory(data, config)

assert uut.row_count == 4
col0 = uut.columns[0]
col1 = uut.columns[1]

assert col0.header == "foo"
assert col1.header == "bar"

# keys truncated at 4 characters, and urls at 16
index = 0
left = col0._cells[index]
right = col1._cells[index]
assert left == "mid-url"
assert right == "https://typer..."

# keys truncated at 4 characters, and short fields are not truncated
index = 1
left = col0._cells[index]
right = col1._cells[index]
assert left == "really looooooooooooooooooonnnng key value"
assert right == "sna"

# non-url values get truncated at 20 characters
index = 2
left = col0._cells[index]
right = col1._cells[index]
assert left == "long value"
assert right == "a" * 47 + "..."

# ftp is NOT a URL, so it gets truncated at 50 characerts
index = 3
left = col0._cells[index]
right = col1._cells[index]
assert left == "long"
assert right == "ftp://typer.tiangolo.com/virtual-environments/#..."


def test_create_table_config_inner_list():
data = deepcopy(INNER_LIST)
config = TableConfig(
key_fields=["ghi"],
properties_label="Different",
)

uut = rich_table_factory(data, config)
assert uut.row_count == 3
assert len(uut.columns) == 2
col0 = uut.columns[0]
col1 = uut.columns[1]

left = col0._cells[0]
right = col1._cells[0]
assert left == "prop 1"
assert right == "simple"

left = col0._cells[2]
right = col1._cells[2]
assert left == "Prop III"
assert right == "None"

left = col0._cells[1]
inner = col1._cells[1]
assert left == "prOp B"
assert len(inner.columns) == 2
assert inner.columns[0].header == "Ghi"
assert inner.columns[1].header == "Different"
assert inner.row_count == 4
names = inner.columns[0]._cells
assert names == ["True", "None", "1.2345", "blah"]
Loading
Loading