diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst index 94940e7c27..5a5dcca5d8 100644 --- a/python/CHANGELOG.rst +++ b/python/CHANGELOG.rst @@ -2,6 +2,11 @@ [1.0.4] - 2026-XX-XX -------------------- +**Features** + +- CLI commands that load a tree sequence now accept ``-`` as the input path to + read from stdin. (:issue:`3468`) + -------------------- [1.0.3] - 2026-05-14 -------------------- diff --git a/python/tests/test_cli.py b/python/tests/test_cli.py index 66be01e6d2..56a4cdc7e3 100644 --- a/python/tests/test_cli.py +++ b/python/tests/test_cli.py @@ -73,6 +73,11 @@ def capture_output(func, *args, **kwargs): return stdout_output, stderr_output +class MockStdIn: + def __init__(self, buffer): + self.buffer = buffer + + class TestCli(unittest.TestCase): """ Superclass of tests for the CLI needing temp files. @@ -310,6 +315,16 @@ def test_vcf_allow_position_zero(self, flags, expected): assert args.tree_sequence == tree_sequence assert args.allow_position_zero == expected + def test_vcf_stdin_file(self): + parser = cli.get_tskit_parser() + args = parser.parse_args(["vcf", "-"]) + assert args.tree_sequence == "-" + + def test_vcf_requires_tree_sequence(self): + parser = cli.get_tskit_parser() + with pytest.raises(SystemExit): + parser.parse_args(["vcf"]) + def test_info_default_values(self): parser = cli.get_tskit_parser() cmd = "info" @@ -560,6 +575,13 @@ def test_vcf(self): assert len(stderr) == 0 self.verify_vcf(stdout) + def test_vcf_stdin(self): + with open(self._tree_sequence_file, "rb") as f: + with mock.patch("sys.stdin", MockStdIn(f)): + stdout, stderr = capture_output(cli.tskit_main, ["vcf", "-0", "-"]) + assert len(stderr) == 0 + self.verify_vcf(stdout) + def verify_info(self, ts, output_info): assert str(ts) == output_info diff --git a/python/tskit/cli.py b/python/tskit/cli.py index 50e97d6784..dc56ad4587 100644 --- a/python/tskit/cli.py +++ b/python/tskit/cli.py @@ -45,10 +45,15 @@ def sys_exit(message): def load_tree_sequence(path): + if path in [None, "-"]: + path = getattr(sys.stdin, "buffer", sys.stdin) try: return tskit.load(path) - except OSError as e: - sys_exit(f"Load error: {e}") + except (OSError, EOFError, tskit.FileFormatError) as e: + message = str(e) + if isinstance(e, EOFError) and len(message) == 0: + message = "End of file" + sys_exit(f"Load error: {message}") def run_info(args): @@ -134,7 +139,10 @@ def run_vcf(args): def add_tree_sequence_argument(parser): - parser.add_argument("tree_sequence", help="The tskit tree sequence file") + parser.add_argument( + "tree_sequence", + help="The tskit tree sequence file, or '-' for stdin", + ) def add_precision_argument(parser):