Skip to content

Commit

Permalink
ENH: ls: Use pyout to render table
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kyleam committed Apr 1, 2019
1 parent 1cfe185 commit bdfbc6d
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 38 deletions.
108 changes: 72 additions & 36 deletions reproman/interface/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

__docformat__ = 'restructuredtext'

from collections import OrderedDict
from functools import partial

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

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

results = OrderedDict()
manager = get_manager()
if not resrefs:
resrefs = (manager.inventory[n]["id"] for n in sorted(manager)
if not n.startswith("_"))

for resref in resrefs:
try:
resource = manager.get_resource(resref)
name = resource.name
except ResourceError as e:
lgr.warning("Manager did not return a resource for %s: %s",
resref, exc_str(e))
continue

table = Tabular(
# Note: We're going with the name as the row key even though ID
# would be the more natural choice because (1) inventory already
# uses the name as the key, so we know it's unique and (2) sadly we
# can't rely on the ID saying set after a .connect() calls (e.g.,
# see docker_container.connect()).
["name", "type", "id", "status"],
style={
"default_": {"width": {"marker": "…", "truncate": "center"}},
"header_": {"underline": True,
"transform": str.upper},
"status": {"color":
{"re_lookup": [["^running$", "green"],
["^(stopped|exited)$", "red"],
["(ERROR|NOT FOUND)", "red"]]},
"bold":
{"re_lookup": [["(ERROR|NOT FOUND)", True]]}}})

def get_status(res):
if refresh:
def fn():
try:
res.connect()
except Exception as e:
status = 'CONNECTION ERROR'
else:
status = res.status if res.id else 'NOT FOUND'
return status
return "querying…", fn
else:
return res.status

# Store a list of actions to do after the table is finalized so that we
# don't interrupt the table's output.
do_after = []
# The refresh happens in an asynchronous call. Keep a list of resources
# that we should ask pyout about once the table is finalized.
resources_to_refresh = []
with table:
for resref in resrefs:
try:
resource.connect()
if not resource.id:
resource.status = 'NOT FOUND'
except Exception as e:
lgr.debug("%s resource query error: %s", name, exc_str(e))
resource.status = 'CONNECTION ERROR'

manager.inventory[name].update({'status': resource.status})

id_ = manager.inventory[name]['id']
msgargs = (
name,
resource.type,
id_[:id_length],
resource.status,
)
ui.message(template.format(*msgargs))
results[(name,)] = dict(zip(["name", "type", "id", "status"],
msgargs))
resource = manager.get_resource(resref)
name = resource.name
except ResourceError as e:
do_after.append(
partial(lgr.warning,
"Manager did not return a resource for %s: %s",
resref,
exc_str(e)))
continue

id_ = manager.inventory[name]['id']
assert id_ == resource.id, "bug in resource logic"
table([name,
resource.type,
id_,
get_status(resource)])
resources_to_refresh.append(resource)

if do_after or not refresh:
# Distinguish between the table and added information.
ui.message("\n")

for fn in do_after:
fn()

if refresh:
manager.save_inventory()
if resources_to_refresh:
for res in resources_to_refresh:
name = res.name
status = table[(name,)]["status"]
manager.inventory[name].update({'status': status})
manager.save_inventory()
else:
ui.message('Use --refresh option to view updated status.')
return results
return table
7 changes: 5 additions & 2 deletions reproman/interface/tests/test_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##

import contextlib
from functools import partial
from io import StringIO
from unittest.mock import patch

import pytest

from pyout import Tabular

from ...api import ls
from ...resource.base import ResourceManager
from ...tests.skip import skipif
Expand Down Expand Up @@ -56,15 +59,15 @@ def resource_manager():
@pytest.fixture(scope="function")
def ls_fn(resource_manager):
stream = StringIO()
TestTabular = partial(Tabular, stream=stream)

def fn(*args, **kwargs):
skipif.no_docker_dependencies()
with contextlib.ExitStack() as stack:
stack.enter_context(patch("docker.Client"))
stack.enter_context(patch("reproman.interface.ls.get_manager",
return_value=resource_manager))
stack.enter_context(patch("reproman.interface.ls.ui._ui.out",
stream))
stack.enter_context(patch("pyout.Tabular", TestTabular))
return ls(*args, **kwargs), stream.getvalue()
return fn

Expand Down

0 comments on commit bdfbc6d

Please sign in to comment.