Skip to content

Commit bdfbc6d

Browse files
committed
ENH: ls: Use pyout to render table
The main benefit of using pyout here is that we can query statuses asynchronously, but it also allows us to style the output in more complex ways (automatic truncation, coloring by regexp, ...) with less custom code than we'd need otherwise. This switch to pyout should not change how we interpret or update the resource statuses, but the code for interacting with the resources is a little more complicated because, when --refresh is given, we access the resources in functions that are called asynchronously. And to update the resource statuses, we need to look at the values after the asynchronous calls have returned. Closes #318.
1 parent 1cfe185 commit bdfbc6d

File tree

2 files changed

+77
-38
lines changed

2 files changed

+77
-38
lines changed

reproman/interface/ls.py

Lines changed: 72 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
__docformat__ = 'restructuredtext'
1313

14-
from collections import OrderedDict
14+
from functools import partial
1515

1616
from .base import Interface
1717
# import reproman.interface.base # Needed for test patching
@@ -55,50 +55,86 @@ class Ls(Interface):
5555

5656
@staticmethod
5757
def __call__(resrefs=None, verbose=False, refresh=False):
58-
id_length = 19 # todo: make it possible to output them long
59-
template = '{:<20} {:<20} {:<%(id_length)s} {!s:<10}' % locals()
60-
ui.message(template.format('RESOURCE NAME', 'TYPE', 'ID', 'STATUS'))
61-
ui.message(template.format('-------------', '----', '--', '------'))
58+
from pyout import Tabular
6259

63-
results = OrderedDict()
6460
manager = get_manager()
6561
if not resrefs:
6662
resrefs = (manager.inventory[n]["id"] for n in sorted(manager)
6763
if not n.startswith("_"))
6864

69-
for resref in resrefs:
70-
try:
71-
resource = manager.get_resource(resref)
72-
name = resource.name
73-
except ResourceError as e:
74-
lgr.warning("Manager did not return a resource for %s: %s",
75-
resref, exc_str(e))
76-
continue
77-
65+
table = Tabular(
66+
# Note: We're going with the name as the row key even though ID
67+
# would be the more natural choice because (1) inventory already
68+
# uses the name as the key, so we know it's unique and (2) sadly we
69+
# can't rely on the ID saying set after a .connect() calls (e.g.,
70+
# see docker_container.connect()).
71+
["name", "type", "id", "status"],
72+
style={
73+
"default_": {"width": {"marker": "…", "truncate": "center"}},
74+
"header_": {"underline": True,
75+
"transform": str.upper},
76+
"status": {"color":
77+
{"re_lookup": [["^running$", "green"],
78+
["^(stopped|exited)$", "red"],
79+
["(ERROR|NOT FOUND)", "red"]]},
80+
"bold":
81+
{"re_lookup": [["(ERROR|NOT FOUND)", True]]}}})
82+
83+
def get_status(res):
7884
if refresh:
85+
def fn():
86+
try:
87+
res.connect()
88+
except Exception as e:
89+
status = 'CONNECTION ERROR'
90+
else:
91+
status = res.status if res.id else 'NOT FOUND'
92+
return status
93+
return "querying…", fn
94+
else:
95+
return res.status
96+
97+
# Store a list of actions to do after the table is finalized so that we
98+
# don't interrupt the table's output.
99+
do_after = []
100+
# The refresh happens in an asynchronous call. Keep a list of resources
101+
# that we should ask pyout about once the table is finalized.
102+
resources_to_refresh = []
103+
with table:
104+
for resref in resrefs:
79105
try:
80-
resource.connect()
81-
if not resource.id:
82-
resource.status = 'NOT FOUND'
83-
except Exception as e:
84-
lgr.debug("%s resource query error: %s", name, exc_str(e))
85-
resource.status = 'CONNECTION ERROR'
86-
87-
manager.inventory[name].update({'status': resource.status})
88-
89-
id_ = manager.inventory[name]['id']
90-
msgargs = (
91-
name,
92-
resource.type,
93-
id_[:id_length],
94-
resource.status,
95-
)
96-
ui.message(template.format(*msgargs))
97-
results[(name,)] = dict(zip(["name", "type", "id", "status"],
98-
msgargs))
106+
resource = manager.get_resource(resref)
107+
name = resource.name
108+
except ResourceError as e:
109+
do_after.append(
110+
partial(lgr.warning,
111+
"Manager did not return a resource for %s: %s",
112+
resref,
113+
exc_str(e)))
114+
continue
115+
116+
id_ = manager.inventory[name]['id']
117+
assert id_ == resource.id, "bug in resource logic"
118+
table([name,
119+
resource.type,
120+
id_,
121+
get_status(resource)])
122+
resources_to_refresh.append(resource)
123+
124+
if do_after or not refresh:
125+
# Distinguish between the table and added information.
126+
ui.message("\n")
127+
128+
for fn in do_after:
129+
fn()
99130

100131
if refresh:
101-
manager.save_inventory()
132+
if resources_to_refresh:
133+
for res in resources_to_refresh:
134+
name = res.name
135+
status = table[(name,)]["status"]
136+
manager.inventory[name].update({'status': status})
137+
manager.save_inventory()
102138
else:
103139
ui.message('Use --refresh option to view updated status.')
104-
return results
140+
return table

reproman/interface/tests/test_ls.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
88

99
import contextlib
10+
from functools import partial
1011
from io import StringIO
1112
from unittest.mock import patch
1213

1314
import pytest
1415

16+
from pyout import Tabular
17+
1518
from ...api import ls
1619
from ...resource.base import ResourceManager
1720
from ...tests.skip import skipif
@@ -56,15 +59,15 @@ def resource_manager():
5659
@pytest.fixture(scope="function")
5760
def ls_fn(resource_manager):
5861
stream = StringIO()
62+
TestTabular = partial(Tabular, stream=stream)
5963

6064
def fn(*args, **kwargs):
6165
skipif.no_docker_dependencies()
6266
with contextlib.ExitStack() as stack:
6367
stack.enter_context(patch("docker.Client"))
6468
stack.enter_context(patch("reproman.interface.ls.get_manager",
6569
return_value=resource_manager))
66-
stack.enter_context(patch("reproman.interface.ls.ui._ui.out",
67-
stream))
70+
stack.enter_context(patch("pyout.Tabular", TestTabular))
6871
return ls(*args, **kwargs), stream.getvalue()
6972
return fn
7073

0 commit comments

Comments
 (0)