Skip to content

Commit ccaad01

Browse files
authored
Merge pull request #44 from aabiddanda/cli_outfile_edits
cli now supports decompressing to stdout
2 parents 55bc8c4 + fd49159 commit ccaad01

File tree

3 files changed

+105
-22
lines changed

3 files changed

+105
-22
lines changed

docs/cli.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Command line interface
55
======================
66

7-
Tszkip is intended to be used primarily as a command line interface.
7+
Tszip is intended to be used primarily as a command line interface.
88
The interface for tszip is modelled directly on
99
`gzip <http://linuxcommand.org/lc3_man_pages/gzip1.html>`_, and so
1010
it should hopefully be immediately familiar and useful to many people.

tests/test_cli.py

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"""
2323
Test cases for the command line interface for tszip.
2424
"""
25-
import io
2625
import pathlib
2726
import sys
2827
import tempfile
@@ -46,28 +45,30 @@ class TestException(Exception):
4645
__test__ = False
4746

4847

49-
def capture_output(func, *args, **kwargs):
48+
def capture_output(func, *args, binary=False, **kwargs):
5049
"""
5150
Runs the specified function and arguments, and returns the
5251
tuple (stdout, stderr) as strings.
5352
"""
54-
buffer_class = io.BytesIO
55-
if sys.version_info[0] == 3:
56-
buffer_class = io.StringIO
57-
stdout = sys.stdout
58-
sys.stdout = buffer_class()
59-
stderr = sys.stderr
60-
sys.stderr = buffer_class()
61-
62-
try:
63-
func(*args, **kwargs)
64-
stdout_output = sys.stdout.getvalue()
65-
stderr_output = sys.stderr.getvalue()
66-
finally:
67-
sys.stdout.close()
68-
sys.stdout = stdout
69-
sys.stderr.close()
70-
sys.stderr = stderr
53+
with tempfile.TemporaryDirectory() as tmpdir:
54+
stdout_path = pathlib.Path(tmpdir) / "stdout"
55+
stderr_path = pathlib.Path(tmpdir) / "stderr"
56+
mode = "wb+" if binary else "w+"
57+
saved_stdout = sys.stdout
58+
saved_stderr = sys.stderr
59+
with open(stdout_path, mode) as stdout, open(stderr_path, mode) as stderr:
60+
try:
61+
sys.stdout = stdout
62+
sys.stderr = stderr
63+
with mock.patch("signal.signal"):
64+
func(*args, **kwargs)
65+
stdout.seek(0)
66+
stderr.seek(0)
67+
stdout_output = stdout.read()
68+
stderr_output = stderr.read()
69+
finally:
70+
sys.stdout = saved_stdout
71+
sys.stderr = saved_stderr
7172
return stdout_output, stderr_output
7273

7374

@@ -85,6 +86,7 @@ def test_default_values(self):
8586
self.assertEqual(args.force, False)
8687
self.assertEqual(args.decompress, False)
8788
self.assertEqual(args.list, False)
89+
self.assertEqual(args.stdout, False)
8890
self.assertEqual(args.variants_only, False)
8991
self.assertEqual(args.suffix, ".tsz")
9092

@@ -132,6 +134,20 @@ def run_tsunzip(self, command, mock_setup_logging):
132134
self.assertEqual(stdout, "")
133135
self.assertTrue(mock_setup_logging.called)
134136

137+
@mock.patch("tszip.cli.setup_logging")
138+
def run_tszip_stdout(self, command, mock_setup_logging):
139+
stdout, stderr = capture_output(cli.tszip_main, command, binary=True)
140+
self.assertEqual(stderr, b"")
141+
self.assertTrue(mock_setup_logging.called)
142+
return stdout, stderr
143+
144+
@mock.patch("tszip.cli.setup_logging")
145+
def run_tsunzip_stdout(self, command, mock_setup_logging):
146+
stdout, stderr = capture_output(cli.tsunzip_main, command, binary=True)
147+
self.assertEqual(stderr, b"")
148+
self.assertTrue(mock_setup_logging.called)
149+
return stdout, stderr
150+
135151

136152
class TestBadFiles(TestCli):
137153
"""
@@ -264,6 +280,16 @@ def test_bad_file_format(self):
264280
f"Error loading '{self.trees_path}': File not in KAS format"
265281
)
266282

283+
def test_compress_stdout(self):
284+
self.assertTrue(self.trees_path.exists())
285+
with mock.patch("tszip.cli.exit", side_effect=TestException) as mocked_exit:
286+
with self.assertRaises(TestException):
287+
self.run_tszip([str(self.trees_path)] + ["-c"])
288+
mocked_exit.assert_called_once_with(
289+
"Compressing to stdout not currently supported;"
290+
"Please see https://github.com/tskit-dev/tszip/issues/49"
291+
)
292+
267293

268294
class DecompressSemanticsMixin:
269295
"""
@@ -276,6 +302,14 @@ def setUp(self):
276302
self.ts = msprime.simulate(10, mutation_rate=10, random_seed=1)
277303
self.compressed_path = pathlib.Path(self.tmpdir.name) / "msprime.trees.tsz"
278304
tszip.compress(self.ts, self.compressed_path)
305+
self.trees_path1 = pathlib.Path(self.tmpdir.name) / "msprime1.trees"
306+
self.trees_path2 = pathlib.Path(self.tmpdir.name) / "msprime2.trees"
307+
self.ts1 = msprime.simulate(10, mutation_rate=10, random_seed=3)
308+
self.ts2 = msprime.simulate(10, mutation_rate=5, random_seed=4)
309+
self.compressed_path1 = pathlib.Path(self.tmpdir.name) / "msprime1.trees.tsz"
310+
self.compressed_path2 = pathlib.Path(self.tmpdir.name) / "msprime2.trees.tsz"
311+
tszip.compress(self.ts1, self.compressed_path1)
312+
tszip.compress(self.ts2, self.compressed_path2)
279313

280314
def tearDown(self):
281315
del self.tmpdir
@@ -310,6 +344,37 @@ def test_keep(self):
310344
ts = tskit.load(str(outpath))
311345
self.assertEqual(ts.tables, self.ts.tables)
312346

347+
def test_keep_stdout(self):
348+
self.assertTrue(self.compressed_path.exists())
349+
self.run_decompress_stdout([str(self.compressed_path), "--stdout"])
350+
self.assertTrue(self.compressed_path.exists())
351+
self.run_decompress_stdout([str(self.compressed_path), "-c"])
352+
self.assertTrue(self.compressed_path.exists())
353+
354+
def test_valid_stdout(self):
355+
tmp_file = pathlib.Path(self.tmpdir.name) / "stdout.trees"
356+
stdout, stderr = self.run_decompress_stdout(["-c", str(self.compressed_path)])
357+
with open(tmp_file, "wb+") as tmp:
358+
tmp.write(stdout)
359+
ts = tskit.load(str(tmp_file))
360+
self.assertEqual(ts.tables, self.ts.tables)
361+
self.assertTrue(self.compressed_path.exists())
362+
363+
def test_valid_stdout_multiple(self):
364+
tmp_file = pathlib.Path(self.tmpdir.name) / "stdout.trees"
365+
with open(tmp_file, "wb+") as tmp:
366+
stdout, stderr = self.run_decompress_stdout(
367+
["-c", str(self.compressed_path1), str(self.compressed_path2)]
368+
)
369+
tmp.write(stdout)
370+
with open(tmp_file) as out_tmp:
371+
ts1 = tskit.load(out_tmp)
372+
ts2 = tskit.load(out_tmp)
373+
self.assertEqual(ts1.tables, self.ts1.tables)
374+
self.assertEqual(ts2.tables, self.ts2.tables)
375+
self.assertTrue(self.compressed_path1.exists())
376+
self.assertTrue(self.compressed_path2.exists())
377+
313378
def test_overwrite(self):
314379
self.assertTrue(self.compressed_path.exists())
315380
outpath = self.trees_path
@@ -357,11 +422,19 @@ class TestDecompressSemanticsTszip(DecompressSemanticsMixin, TestCli):
357422
def run_decompress(self, args):
358423
self.run_tszip(["-d"] + args)
359424

425+
def run_decompress_stdout(self, args):
426+
x = self.run_tszip_stdout(["-d"] + args)
427+
return x
428+
360429

361430
class TestDecompressSemanticsTsunzip(DecompressSemanticsMixin, TestCli):
362431
def run_decompress(self, args):
363432
self.run_tsunzip(args)
364433

434+
def run_decompress_stdout(self, args):
435+
x = self.run_tsunzip_stdout(args)
436+
return x
437+
365438

366439
class TestList(unittest.TestCase):
367440
"""

tszip/cli.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def tszip_cli_parser():
8181
parser.add_argument(
8282
"-f", "--force", action="store_true", help="Force overwrite of output file"
8383
)
84+
parser.add_argument("-c", "--stdout", action="store_true", help="Write to stdout")
8485
group = parser.add_mutually_exclusive_group()
8586
group.add_argument("-d", "--decompress", action="store_true", help="Decompress")
8687
group.add_argument(
@@ -102,6 +103,11 @@ def check_output(outfile, args):
102103

103104

104105
def run_compress(args):
106+
if args.stdout:
107+
exit(
108+
"Compressing to stdout not currently supported;"
109+
"Please see https://github.com/tskit-dev/tszip/issues/49"
110+
)
105111
setup_logging(args)
106112
for file_arg in args.files:
107113
logger.info(f"Compressing {file_arg}")
@@ -134,8 +140,12 @@ def run_decompress(args):
134140
if not file_arg.endswith(args.suffix):
135141
exit(f"Compressed file must have '{args.suffix}' suffix")
136142
infile = pathlib.Path(file_arg)
137-
outfile = pathlib.Path(file_arg[: -len(args.suffix)])
138-
check_output(outfile, args)
143+
if args.stdout:
144+
args.keep = True
145+
outfile = sys.stdout
146+
else:
147+
outfile = pathlib.Path(file_arg[: -len(args.suffix)])
148+
check_output(outfile, args)
139149
with check_load_errors(file_arg):
140150
ts = tszip.decompress(file_arg)
141151
ts.dump(outfile)

0 commit comments

Comments
 (0)