Skip to content

Commit 82dd1ac

Browse files
authored
Merge pull request #2815 from xliiv/2807-use-pager
dvc/dagascii: Use pager instead of AsciiCanvas._do_draw
2 parents 0a7abf1 + 44df59f commit 82dd1ac

File tree

5 files changed

+85
-129
lines changed

5 files changed

+85
-129
lines changed

dvc/dagascii.py

Lines changed: 43 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
from __future__ import print_function
33
from __future__ import unicode_literals
44

5+
import logging
56
import math
7+
import os
8+
import pydoc
69
import sys
710

811
from grandalf.graphs import Edge
@@ -12,6 +15,42 @@
1215
from grandalf.routing import EdgeViewer
1316
from grandalf.routing import route_with_lines
1417

18+
from dvc.env import DVC_PAGER
19+
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
DEFAULT_PAGER = "less"
25+
DEFAULT_PAGER_FORMATTED = "{} --chop-long-lines --clear-screen".format(
26+
DEFAULT_PAGER
27+
)
28+
29+
30+
def make_pager(cmd):
31+
def pager(text):
32+
return pydoc.tempfilepager(pydoc.plain(text), cmd)
33+
34+
return pager
35+
36+
37+
def find_pager():
38+
if not sys.stdout.isatty():
39+
return pydoc.plainpager
40+
41+
env_pager = os.getenv(DVC_PAGER)
42+
if env_pager:
43+
return make_pager(env_pager)
44+
45+
if os.system("({}) 2>{}".format(DEFAULT_PAGER, os.devnull)) == 0:
46+
return make_pager(DEFAULT_PAGER_FORMATTED)
47+
48+
logger.warning(
49+
"Unable to find `less` in the PATH. Check out "
50+
"man.dvc.org/doc/command-reference/pipeline/show for more info."
51+
)
52+
return pydoc.plainpager
53+
1554

1655
class VertexViewer(object):
1756
"""Class to define vertex box boundaries that will be accounted for during
@@ -60,99 +99,10 @@ def __init__(self, cols, lines):
6099

61100
def draw(self):
62101
"""Draws ASCII canvas on the screen."""
63-
if sys.stdout.isatty(): # pragma: no cover
64-
from asciimatics.screen import Screen
65-
66-
Screen.wrapper(self._do_draw)
67-
else:
68-
for line in self.canvas:
69-
print("".join(line))
70-
71-
def _do_draw(self, screen): # pragma: no cover
72-
# pylint: disable=too-many-locals
73-
# pylint: disable=too-many-branches, too-many-statements
74-
from dvc.system import System
75-
from asciimatics.event import KeyboardEvent
76-
77-
offset_x = 0
78-
offset_y = 0
79-
smaxrow, smaxcol = screen.dimensions
80-
assert smaxrow > 1
81-
assert smaxcol > 1
82-
smaxrow -= 1
83-
smaxcol -= 1
84-
85-
if self.lines + 1 > smaxrow:
86-
max_y = self.lines + 1 - smaxrow
87-
else:
88-
max_y = 0
89-
90-
if self.cols + 1 > smaxcol:
91-
max_x = self.cols + 1 - smaxcol
92-
else:
93-
max_x = 0
94-
95-
while True:
96-
for y in range(smaxrow + 1):
97-
y_index = offset_y + y
98-
line = []
99-
for x in range(smaxcol + 1):
100-
x_index = offset_x + x
101-
if (
102-
len(self.canvas) > y_index
103-
and len(self.canvas[y_index]) > x_index
104-
):
105-
line.append(self.canvas[y_index][x_index])
106-
else:
107-
line.append(" ")
108-
assert len(line) == (smaxcol + 1)
109-
screen.print_at("".join(line), 0, y)
110-
111-
screen.refresh()
112-
113-
# NOTE: get_event() doesn't block by itself,
114-
# so we have to do the blocking ourselves.
115-
#
116-
# NOTE: using this workaround while waiting for PR [1]
117-
# to get merged and released. After that need to adjust
118-
# asciimatics version requirements.
119-
#
120-
# [1] https://github.com/peterbrittain/asciimatics/pull/188
121-
System.wait_for_input(self.TIMEOUT)
122-
123-
event = screen.get_event()
124-
if not isinstance(event, KeyboardEvent):
125-
continue
126-
127-
k = event.key_code
128-
if k == screen.KEY_DOWN or k == ord("s"):
129-
offset_y += 1
130-
elif k == screen.KEY_PAGE_DOWN or k == ord("S"):
131-
offset_y += smaxrow
132-
elif k == screen.KEY_UP or k == ord("w"):
133-
offset_y -= 1
134-
elif k == screen.KEY_PAGE_UP or k == ord("W"):
135-
offset_y -= smaxrow
136-
elif k == screen.KEY_RIGHT or k == ord("d"):
137-
offset_x += 1
138-
elif k == ord("D"):
139-
offset_x += smaxcol
140-
elif k == screen.KEY_LEFT or k == ord("a"):
141-
offset_x -= 1
142-
elif k == ord("A"):
143-
offset_x -= smaxcol
144-
elif k == ord("q") or k == ord("Q"):
145-
break
146-
147-
if offset_y > max_y:
148-
offset_y = max_y
149-
elif offset_y < 0:
150-
offset_y = 0
151-
152-
if offset_x > max_x:
153-
offset_x = max_x
154-
elif offset_x < 0:
155-
offset_x = 0
102+
pager = find_pager()
103+
lines = map("".join, self.canvas)
104+
joined_lines = os.linesep.join(lines)
105+
pager(joined_lines)
156106

157107
def point(self, x, y, char):
158108
"""Create a point on ASCII canvas.

dvc/env.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
DVC_DAEMON = "DVC_DAEMON"
2+
DVC_PAGER = "DVC_PAGER"

dvc/system.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -218,41 +218,6 @@ def inode(path):
218218
assert inode < 2 ** 64
219219
return inode
220220

221-
@staticmethod
222-
def _wait_for_input_windows(timeout):
223-
import sys
224-
import ctypes
225-
import msvcrt
226-
from ctypes.wintypes import DWORD, HANDLE
227-
228-
# https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-waitforsingleobject
229-
from win32event import WAIT_OBJECT_0, WAIT_TIMEOUT
230-
231-
func = ctypes.windll.kernel32.WaitForSingleObject
232-
func.argtypes = [HANDLE, DWORD]
233-
func.restype = DWORD
234-
235-
rc = func(msvcrt.get_osfhandle(sys.stdin.fileno()), timeout * 1000)
236-
if rc not in [WAIT_OBJECT_0, WAIT_TIMEOUT]:
237-
raise RuntimeError(rc)
238-
239-
@staticmethod
240-
def _wait_for_input_posix(timeout):
241-
import sys
242-
import select
243-
244-
try:
245-
select.select([sys.stdin], [], [], timeout)
246-
except select.error:
247-
pass
248-
249-
@staticmethod
250-
def wait_for_input(timeout):
251-
if System.is_unix():
252-
return System._wait_for_input_posix(timeout)
253-
else:
254-
return System._wait_for_input_windows(timeout)
255-
256221
@staticmethod
257222
def is_symlink(path):
258223
path = fspath(path)

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ def run(self):
6767
"jsonpath-ng>=1.4.3",
6868
"requests>=2.22.0",
6969
"grandalf==0.6",
70-
"asciimatics>=1.10.0",
7170
"distro>=1.3.0",
7271
"appdirs>=1.4.3",
7372
"treelib>=1.5.5",

tests/unit/test_dagascii.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from dvc import dagascii
2+
from dvc.env import DVC_PAGER
3+
4+
5+
def test_find_pager_uses_default_pager_when_found(mocker):
6+
mocker.patch("sys.stdout.isatty", return_value=True)
7+
mocker.patch("os.system", return_value=0)
8+
m_make_pager = mocker.patch.object(dagascii, "make_pager")
9+
10+
dagascii.find_pager()
11+
12+
m_make_pager.assert_called_once_with(dagascii.DEFAULT_PAGER_FORMATTED)
13+
14+
15+
def test_find_pager_returns_plain_pager_when_default_missing(mocker):
16+
mocker.patch("sys.stdout.isatty", return_value=True)
17+
mocker.patch("os.system", return_value=1)
18+
19+
pager = dagascii.find_pager()
20+
21+
assert pager.__name__ == "plainpager"
22+
23+
24+
def test_find_pager_uses_custom_pager_when_env_var_is_defined(
25+
mocker, monkeypatch
26+
):
27+
mocker.patch("sys.stdout.isatty", return_value=True)
28+
m_make_pager = mocker.patch.object(dagascii, "make_pager")
29+
monkeypatch.setenv(DVC_PAGER, dagascii.DEFAULT_PAGER)
30+
31+
dagascii.find_pager()
32+
33+
m_make_pager.assert_called_once_with(dagascii.DEFAULT_PAGER)
34+
35+
36+
def test_find_pager_returns_plain_pager_when_is_not_atty(mocker):
37+
mocker.patch("sys.stdout.isatty", return_value=False)
38+
39+
pager = dagascii.find_pager()
40+
41+
assert pager.__name__ == "plainpager"

0 commit comments

Comments
 (0)