Skip to content

Commit 09287b2

Browse files
committed
Add an output pager.
Fixes #50. Requires pallets/click#1572.
1 parent e81e52f commit 09287b2

File tree

3 files changed

+165
-115
lines changed

3 files changed

+165
-115
lines changed

sno/diff_output.py

Lines changed: 82 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
import click
1212

1313
from . import gpkg
14-
from .output_util import dump_json_output, resolve_output_path
14+
from .output_util import (
15+
dump_json_output,
16+
resolve_output_path,
17+
)
1518

1619

1720
@contextlib.contextmanager
@@ -50,82 +53,82 @@ def diff_output_text(*, output_path, **kwargs):
5053
In particular, geometry WKT is abbreviated and null values are represented
5154
by a unicode "␀" character.
5255
"""
53-
fp = resolve_output_path(output_path)
54-
pecho = {'file': fp, 'color': fp.isatty()}
5556
if isinstance(output_path, Path) and output_path.is_dir():
5657
raise click.BadParameter(
5758
"Directory is not valid for --output with --text", param_hint="--output"
5859
)
5960

6061
def _out(dataset, diff):
61-
path = dataset.path
62-
pk_field = dataset.primary_key
63-
prefix = f"{path}:"
64-
repr_excl = [pk_field]
65-
66-
for k, (v_old, v_new) in diff["META"].items():
67-
click.secho(
68-
f"--- {prefix}meta/{k}\n+++ {prefix}meta/{k}", bold=True, **pecho
69-
)
70-
71-
s_old = set(v_old.items())
72-
s_new = set(v_new.items())
73-
74-
diff_add = dict(s_new - s_old)
75-
diff_del = dict(s_old - s_new)
76-
all_keys = set(diff_del.keys()) | set(diff_add.keys())
77-
78-
for k in all_keys:
79-
if k in diff_del:
80-
click.secho(
81-
text_row({k: diff_del[k]}, prefix="- ", exclude=repr_excl),
82-
fg="red",
83-
**pecho,
84-
)
85-
if k in diff_add:
86-
click.secho(
87-
text_row({k: diff_add[k]}, prefix="+ ", exclude=repr_excl),
88-
fg="green",
89-
**pecho,
90-
)
91-
92-
prefix = f"{path}:{pk_field}="
62+
with resolve_output_path(output_path) as fp:
63+
pecho = {'file': fp, 'color': getattr(fp, 'color', fp.isatty())}
64+
path = dataset.path
65+
pk_field = dataset.primary_key
66+
prefix = f"{path}:"
67+
repr_excl = [pk_field]
68+
69+
for k, (v_old, v_new) in diff["META"].items():
70+
click.secho(
71+
f"--- {prefix}meta/{k}\n+++ {prefix}meta/{k}", bold=True, **pecho
72+
)
9373

94-
for k, v_old in diff["D"].items():
95-
click.secho(f"--- {prefix}{k}", bold=True, **pecho)
96-
click.secho(
97-
text_row(v_old, prefix="- ", exclude=repr_excl), fg="red", **pecho
98-
)
74+
s_old = set(v_old.items())
75+
s_new = set(v_new.items())
76+
77+
diff_add = dict(s_new - s_old)
78+
diff_del = dict(s_old - s_new)
79+
all_keys = set(diff_del.keys()) | set(diff_add.keys())
80+
81+
for k in all_keys:
82+
if k in diff_del:
83+
click.secho(
84+
text_row({k: diff_del[k]}, prefix="- ", exclude=repr_excl),
85+
fg="red",
86+
**pecho,
87+
)
88+
if k in diff_add:
89+
click.secho(
90+
text_row({k: diff_add[k]}, prefix="+ ", exclude=repr_excl),
91+
fg="green",
92+
**pecho,
93+
)
94+
95+
prefix = f"{path}:{pk_field}="
96+
97+
for k, v_old in diff["D"].items():
98+
click.secho(f"--- {prefix}{k}", bold=True, **pecho)
99+
click.secho(
100+
text_row(v_old, prefix="- ", exclude=repr_excl), fg="red", **pecho
101+
)
99102

100-
for o in diff["I"]:
101-
click.secho(f"+++ {prefix}{o[pk_field]}", bold=True, **pecho)
102-
click.secho(
103-
text_row(o, prefix="+ ", exclude=repr_excl), fg="green", **pecho
104-
)
103+
for o in diff["I"]:
104+
click.secho(f"+++ {prefix}{o[pk_field]}", bold=True, **pecho)
105+
click.secho(
106+
text_row(o, prefix="+ ", exclude=repr_excl), fg="green", **pecho
107+
)
105108

106-
for _, (v_old, v_new) in diff["U"].items():
107-
click.secho(
108-
f"--- {prefix}{v_old[pk_field]}\n+++ {prefix}{v_new[pk_field]}",
109-
bold=True,
110-
**pecho,
111-
)
109+
for _, (v_old, v_new) in diff["U"].items():
110+
click.secho(
111+
f"--- {prefix}{v_old[pk_field]}\n+++ {prefix}{v_new[pk_field]}",
112+
bold=True,
113+
**pecho,
114+
)
112115

113-
s_old = set(v_old.items())
114-
s_new = set(v_new.items())
116+
s_old = set(v_old.items())
117+
s_new = set(v_new.items())
115118

116-
diff_add = dict(s_new - s_old)
117-
diff_del = dict(s_old - s_new)
118-
all_keys = sorted(set(diff_del.keys()) | set(diff_add.keys()))
119+
diff_add = dict(s_new - s_old)
120+
diff_del = dict(s_old - s_new)
121+
all_keys = sorted(set(diff_del.keys()) | set(diff_add.keys()))
119122

120-
for k in all_keys:
121-
if k in diff_del:
122-
rk = text_row({k: diff_del[k]}, prefix="- ", exclude=repr_excl)
123-
if rk:
124-
click.secho(rk, fg="red", **pecho)
125-
if k in diff_add:
126-
rk = text_row({k: diff_add[k]}, prefix="+ ", exclude=repr_excl)
127-
if rk:
128-
click.secho(rk, fg="green", **pecho)
123+
for k in all_keys:
124+
if k in diff_del:
125+
rk = text_row({k: diff_del[k]}, prefix="- ", exclude=repr_excl)
126+
if rk:
127+
click.secho(rk, fg="red", **pecho)
128+
if k in diff_add:
129+
rk = text_row({k: diff_add[k]}, prefix="+ ", exclude=repr_excl)
130+
if rk:
131+
click.secho(rk, fg="green", **pecho)
129132

130133
yield _out
131134

@@ -367,20 +370,19 @@ def diff_output_html(*, output_path, repo, base, target, dataset_count, **kwargs
367370

368371
if not output_path:
369372
output_path = Path(repo.path) / "DIFF.html"
370-
fo = resolve_output_path(output_path)
371-
372-
# Read all the geojson back in, and stick them in a dict
373-
all_datasets_geojson = {}
374-
for filename in os.listdir(tempdir):
375-
with open(tempdir / filename) as json_file:
376-
all_datasets_geojson[os.path.splitext(filename)[0]] = json.load(
377-
json_file
373+
with resolve_output_path(output_path) as fo:
374+
# Read all the geojson back in, and stick them in a dict
375+
all_datasets_geojson = {}
376+
for filename in os.listdir(tempdir):
377+
with open(tempdir / filename) as json_file:
378+
all_datasets_geojson[os.path.splitext(filename)[0]] = json.load(
379+
json_file
380+
)
381+
fo.write(
382+
template.substitute(
383+
{"title": title, "geojson_data": json.dumps(all_datasets_geojson)}
378384
)
379-
fo.write(
380-
template.substitute(
381-
{"title": title, "geojson_data": json.dumps(all_datasets_geojson)}
382385
)
383-
)
384-
if fo != sys.stdout:
385-
fo.close()
386-
webbrowser.open_new(f"file://{output_path.resolve()}")
386+
if fo != sys.stdout:
387+
fo.close()
388+
webbrowser.open_new(f"file://{output_path.resolve()}")

sno/output_util.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import io
22
import json
3+
import os
4+
import shutil
35
import sys
6+
import threading
7+
from contextlib import closing, contextmanager
8+
from queue import Queue, Empty
9+
10+
import click
411

512
JSON_PARAMS = {
613
"compact": {},
@@ -14,35 +21,54 @@ def dump_json_output(output, output_path, json_style="pretty"):
1421
Dumps the output to JSON in the output file.
1522
"""
1623

17-
fp = resolve_output_path(output_path)
18-
19-
if json_style == 'pretty' and fp == sys.stdout and fp.isatty():
20-
# Add syntax highlighting
21-
from pygments import highlight
22-
from pygments.lexers import JsonLexer
23-
from pygments.formatters import TerminalFormatter
24+
with resolve_output_path(output_path) as fp:
25+
if json_style == 'pretty' and getattr(fp, 'color', fp.isatty()):
26+
# Add syntax highlighting
27+
from pygments import highlight
28+
from pygments.lexers import JsonLexer
29+
from pygments.formatters import TerminalFormatter
2430

25-
dumped = json.dumps(output, **JSON_PARAMS[json_style])
26-
highlighted = highlight(dumped.encode(), JsonLexer(), TerminalFormatter())
27-
fp.write(highlighted)
28-
else:
29-
json.dump(output, fp, **JSON_PARAMS[json_style])
31+
dumped = json.dumps(output, **JSON_PARAMS[json_style])
32+
highlighted = highlight(dumped.encode(), JsonLexer(), TerminalFormatter())
33+
fp.write(highlighted)
34+
else:
35+
json.dump(output, fp, **JSON_PARAMS[json_style])
3036

3137

32-
def resolve_output_path(output_path):
38+
@contextmanager
39+
def resolve_output_path(output_path, allow_pager=True):
3340
"""
34-
Takes a path-ish thing, and returns the appropriate writable file-like object.
41+
Context manager.
42+
43+
Takes a path-ish thing, and yields the appropriate writable file-like object.
3544
The path-ish thing could be:
3645
* a pathlib.Path object
3746
* a file-like object
3847
* the string '-' or None (both will return sys.stdout)
48+
49+
If the file is not stdout, it will be closed when exiting the context manager.
50+
51+
If allow_pager=True (the default) and the file is stdout, this will attempt to use a
52+
pager to display long output.
3953
"""
54+
4055
if isinstance(output_path, io.IOBase):
41-
return output_path
56+
# Make this contextmanager re-entrant
57+
yield output_path
4258
elif (not output_path) or output_path == "-":
43-
return sys.stdout
59+
if allow_pager and get_input_mode() == InputMode.INTERACTIVE:
60+
pager_cmd = (
61+
os.environ.get('SNO_PAGER') or os.environ.get('PAGER') or DEFAULT_PAGER
62+
)
63+
64+
with _push_environment('PAGER', pager_cmd):
65+
with click.get_pager_file(color=True) as pager:
66+
yield pager
67+
else:
68+
yield sys.stdout
4469
else:
45-
return output_path.open("w")
70+
with closing(output_path.open("w")) as f:
71+
yield f
4672

4773

4874
class InputMode:
@@ -69,3 +95,25 @@ def is_empty_stream(stream):
6995
return True
7096
stream.seek(pos)
7197
return False
98+
99+
100+
def _setenv(k, v):
101+
if v is None:
102+
del os.environ[k]
103+
else:
104+
os.environ[k] = v
105+
106+
107+
@contextmanager
108+
def _push_environment(k, v):
109+
orig = os.environ.get(k)
110+
_setenv(k, v)
111+
try:
112+
yield
113+
finally:
114+
_setenv(k, orig)
115+
116+
117+
DEFAULT_PAGER = shutil.which('less')
118+
if DEFAULT_PAGER:
119+
DEFAULT_PAGER += ' --quit-if-one-screen --no-init -R'

sno/show.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,24 @@ def patch_output_text(*, target, output_path, **kwargs):
9292
by a unicode "␀" character.
9393
"""
9494
commit = target.head_commit
95-
fp = resolve_output_path(output_path)
96-
pecho = {'file': fp, 'color': fp.isatty()}
97-
with diff.diff_output_text(output_path=fp, **kwargs) as diff_writer:
98-
author = commit.author
99-
author_time_utc = datetime.fromtimestamp(author.time, timezone.utc)
100-
author_timezone = timezone(timedelta(minutes=author.offset))
101-
author_time_in_author_timezone = author_time_utc.astimezone(author_timezone)
102-
103-
click.secho(f'commit {commit.hex}', fg='yellow')
104-
click.secho(f'Author: {author.name} <{author.email}>', **pecho)
105-
click.secho(
106-
f'Date: {author_time_in_author_timezone.strftime("%c %z")}', **pecho
107-
)
108-
click.secho(**pecho)
109-
for line in commit.message.splitlines():
110-
click.secho(f' {line}', **pecho)
111-
click.secho(**pecho)
112-
yield diff_writer
95+
with resolve_output_path(output_path) as fp:
96+
pecho = {'file': fp, 'color': getattr(fp, 'color', fp.isatty())}
97+
with diff.diff_output_text(output_path=fp, **kwargs) as diff_writer:
98+
author = commit.author
99+
author_time_utc = datetime.fromtimestamp(author.time, timezone.utc)
100+
author_timezone = timezone(timedelta(minutes=author.offset))
101+
author_time_in_author_timezone = author_time_utc.astimezone(author_timezone)
102+
103+
click.secho(f'commit {commit.hex}', fg='yellow', **pecho)
104+
click.secho(f'Author: {author.name} <{author.email}>', **pecho)
105+
click.secho(
106+
f'Date: {author_time_in_author_timezone.strftime("%c %z")}', **pecho
107+
)
108+
click.secho(**pecho)
109+
for line in commit.message.splitlines():
110+
click.secho(f' {line}', **pecho)
111+
click.secho(**pecho)
112+
yield diff_writer
113113

114114

115115
@contextlib.contextmanager

0 commit comments

Comments
 (0)