Skip to content

Commit fed3b2c

Browse files
authored
ui: unify tables (#5756)
1 parent eccc320 commit fed3b2c

File tree

5 files changed

+340
-29
lines changed

5 files changed

+340
-29
lines changed

dvc/types.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, List, Optional, Union
1+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
22

33
from dvc.path_info import PathInfo, URLInfo
44

@@ -11,3 +11,5 @@
1111

1212
OptStr = Optional[str]
1313
TargetType = Union[List[str], str]
14+
DictStrAny = Dict[str, Any]
15+
DictAny = Dict[Any, Any]

dvc/ui/__init__.py

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import sys
22
from collections import defaultdict
3-
from typing import Any, Dict, Iterable, Optional, TextIO
4-
5-
from funcy import cached_property
3+
from typing import (
4+
TYPE_CHECKING,
5+
Any,
6+
Dict,
7+
Iterable,
8+
Optional,
9+
Sequence,
10+
TextIO,
11+
Union,
12+
)
613

714
from dvc.progress import Tqdm
815
from dvc.utils import colorize
916

17+
if TYPE_CHECKING:
18+
from dvc.ui.table import Headers, Styles, TableData
19+
1020

1121
class Formatter:
1222
def __init__(self, theme: Dict = None, defaults: Dict = None) -> None:
@@ -42,7 +52,7 @@ def output(self) -> TextIO:
4252
def error_output(self) -> TextIO:
4353
return self._error or sys.stderr
4454

45-
def enable(self):
55+
def enable(self) -> None:
4656
self._enabled = True
4757

4858
def success(self, message: str) -> None:
@@ -79,8 +89,9 @@ def write(
7989
end: str = None,
8090
file: TextIO = None,
8191
flush: bool = False,
92+
force: bool = False,
8293
) -> None:
83-
if not self._enabled:
94+
if not self._enabled and not force:
8495
return
8596

8697
file = file or self.output
@@ -126,49 +137,64 @@ def confirm(self, statement: str) -> bool:
126137
return False
127138
return answer.startswith("y")
128139

129-
@cached_property
140+
@property
130141
def rich_console(self):
131142
"""rich_console is only set to stdout for now."""
132143
from rich import console
133144

145+
# FIXME: Getting IO Operation on closed file error
146+
# when testing with capsys, therefore we are creating
147+
# one instance each time as a temporary workaround.
134148
return console.Console(file=self.output)
135149

136-
def rich_table(self, pager: bool = True):
137-
pass
138-
139-
def table(self, header, rows, markdown: bool = False):
140-
from tabulate import tabulate
150+
def table(
151+
self,
152+
data: "TableData",
153+
headers: "Headers" = None,
154+
markdown: bool = False,
155+
rich_table: bool = False,
156+
force: bool = True,
157+
pager: bool = False,
158+
header_styles: Sequence["Styles"] = None,
159+
row_styles: Sequence["Styles"] = None,
160+
borders: Union[bool, str] = False,
161+
) -> None:
162+
from dvc.ui import table as t
141163

142-
if not rows and not markdown:
143-
return ""
164+
if not data and not markdown:
165+
return
144166

145-
ret = tabulate(
146-
rows,
147-
header,
148-
tablefmt="github" if markdown else "plain",
149-
disable_numparse=True,
150-
# None will be shown as "" by default, overriding
151-
missingval="—",
152-
)
167+
if not markdown and rich_table:
168+
if force or self._enabled:
169+
return t.rich_table(
170+
self,
171+
data,
172+
headers,
173+
pager=pager,
174+
header_styles=header_styles,
175+
row_styles=row_styles,
176+
borders=borders,
177+
)
153178

154-
if markdown:
155-
# NOTE: md table is incomplete without the trailing newline
156-
ret += "\n"
179+
return
157180

158-
self.write(ret)
181+
return t.plain_table(
182+
self, data, headers, markdown=markdown, pager=pager, force=force,
183+
)
159184

160185

161186
ui = Console()
162187

163188

164189
if __name__ == "__main__":
165190
ui.enable()
191+
166192
ui.write("No default remote set")
167193
ui.success("Everything is up to date.")
168194
ui.warn("Run queued experiments will be removed.")
169195
ui.error("too few arguments.")
170196

171-
ui.table("keys", {"Path": ["scores.json"], "auc": ["0.5674"]})
197+
ui.table([("scores.json", "0.5674")], headers=["Path", "auc"])
172198
ui.table(
173-
"keys", {"Path": ["scores.json"], "auc": ["0.5674"]}, markdown=True
199+
[("scores.json", "0.5674")], headers=["Path", "auc"], markdown=True
174200
)

dvc/ui/table.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from contextlib import contextmanager
2+
from itertools import zip_longest
3+
from typing import TYPE_CHECKING, Iterator, List, Sequence, Union
4+
5+
from dvc.types import DictStrAny
6+
7+
if TYPE_CHECKING:
8+
from rich.console import Console as RichConsole
9+
from rich.table import Table
10+
from rich.text import Text
11+
12+
from dvc.ui import Console
13+
14+
15+
SHOW_MAX_WIDTH = 1024
16+
17+
18+
CellT = Union[str, "Text"] # Text is mostly compatible with str
19+
Row = Sequence[CellT]
20+
TableData = List[Row]
21+
Headers = Sequence[str]
22+
Styles = DictStrAny
23+
24+
25+
def plain_table(
26+
ui: "Console",
27+
data: TableData,
28+
headers: Headers = None,
29+
markdown: bool = False,
30+
pager: bool = False,
31+
force: bool = True,
32+
) -> None:
33+
from tabulate import tabulate
34+
35+
text: str = tabulate(
36+
data,
37+
headers if headers is not None else (),
38+
tablefmt="github" if markdown else "plain",
39+
disable_numparse=True,
40+
# None will be shown as "" by default, overriding
41+
missingval="-",
42+
)
43+
if markdown:
44+
# NOTE: md table is incomplete without the trailing newline
45+
text += "\n"
46+
47+
if pager:
48+
from dvc.utils.pager import pager as _pager
49+
50+
_pager(text)
51+
else:
52+
ui.write(text, force=force)
53+
54+
55+
@contextmanager
56+
def console_width(
57+
table: "Table", console: "RichConsole", val: int
58+
) -> Iterator[None]:
59+
# NOTE: rich does not have native support for unlimited width
60+
# via pager. we override rich table compression by setting
61+
# console width to the full width of the table
62+
# pylint: disable=protected-access
63+
64+
console_options = console.options
65+
original = console_options.max_width
66+
con_width = console._width
67+
68+
try:
69+
console_options.max_width = val
70+
measurement = table.__rich_measure__(console, console_options)
71+
console._width = measurement.maximum
72+
73+
yield
74+
finally:
75+
console_options.max_width = original
76+
console._width = con_width
77+
78+
79+
def rich_table(
80+
ui: "Console",
81+
data: TableData,
82+
headers: Headers = None,
83+
pager: bool = False,
84+
header_styles: Sequence[Styles] = None,
85+
row_styles: Sequence[Styles] = None,
86+
borders: Union[bool, str] = False,
87+
) -> None:
88+
from rich import box
89+
90+
from dvc.utils.table import Table
91+
92+
border_style = {
93+
True: box.HEAVY_HEAD, # is a default in rich,
94+
False: None,
95+
"simple": box.SIMPLE,
96+
"minimal": box.MINIMAL,
97+
}
98+
99+
table = Table(box=border_style[borders])
100+
hs: Sequence[Styles] = header_styles or []
101+
rs: Sequence[Styles] = row_styles or []
102+
103+
for header, style in zip_longest(headers or [], hs):
104+
table.add_column(header, **(style or {}))
105+
106+
for row, style in zip_longest(data, rs):
107+
table.add_row(*row, **(style or {}))
108+
109+
console = ui.rich_console
110+
111+
if not pager:
112+
console.print(table)
113+
return
114+
115+
from dvc.utils.pager import DvcPager
116+
117+
with console_width(table, console, SHOW_MAX_WIDTH):
118+
with console.pager(pager=DvcPager(), styles=True):
119+
console.print(table)

dvc/utils/pager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def find_pager():
4646
return pydoc.plainpager
4747

4848

49-
def pager(text):
49+
def pager(text: str) -> None:
5050
find_pager()(text)
5151

5252

0 commit comments

Comments
 (0)