Skip to content

Commit 1fd60a4

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 1fd60a4

File tree

2 files changed

+80
-38
lines changed

2 files changed

+80
-38
lines changed

reproman/interface/ls.py

Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
__docformat__ = 'restructuredtext'
1313

1414
from collections import OrderedDict
15+
from functools import partial
16+
17+
from pyout import Tabular
1518

1619
from .base import Interface
1720
# import reproman.interface.base # Needed for test patching
@@ -55,50 +58,85 @@ class Ls(Interface):
5558

5659
@staticmethod
5760
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('-------------', '----', '--', '------'))
62-
63-
results = OrderedDict()
6461
manager = get_manager()
6562
if not resrefs:
6663
resrefs = (manager.inventory[n]["id"] for n in sorted(manager)
6764
if not n.startswith("_"))
6865

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

100133
if refresh:
101-
manager.save_inventory()
134+
if resources_to_refresh:
135+
for res in resources_to_refresh:
136+
name = res.name
137+
status = table[(name,)]["status"]
138+
manager.inventory[name].update({'status': status})
139+
manager.save_inventory()
102140
else:
103141
ui.message('Use --refresh option to view updated status.')
104-
return results
142+
return table

reproman/interface/tests/test_ls.py

Lines changed: 6 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,16 @@ 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("reproman.interface.ls.Tabular",
71+
TestTabular))
6872
return ls(*args, **kwargs), stream.getvalue()
6973
return fn
7074

0 commit comments

Comments
 (0)