Skip to content

Commit f8af4e8

Browse files
authored
Merge pull request #374 from tovrstra/cli-allow-changes
Add command-line option `--allow-changes` to `iodata-convert`
2 parents 4dceda6 + 0a4d979 commit f8af4e8

2 files changed

Lines changed: 108 additions & 45 deletions

File tree

iodata/__main__.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"""CLI for file conversion."""
2121

2222
import argparse
23+
from typing import Optional
2324

2425
import numpy as np
2526

@@ -31,7 +32,7 @@
3132
__version__ = "0.0.0.post0"
3233

3334

34-
__all__ = ()
35+
__all__ = ("convert",)
3536

3637

3738
DESCRIPTION = """\
@@ -83,6 +84,14 @@ def parse_args():
8384
parser.add_argument(
8485
"-o", "--outfmt", help="Select the output format, overrides automatic detection."
8586
)
87+
parser.add_argument(
88+
"-c",
89+
"--allow-changes",
90+
default=False,
91+
action="store_true",
92+
help="Allow (not trivially reversible) conversion of the input data to make it compatible "
93+
"with the output format. Warnings will be emitted for all changes made.",
94+
)
8695
parser.add_argument(
8796
"-m",
8897
"--many",
@@ -95,7 +104,14 @@ def parse_args():
95104
return parser.parse_args()
96105

97106

98-
def convert(infn, outfn, many, infmt, outfmt):
107+
def convert(
108+
infn: str,
109+
outfn: str,
110+
many: bool = False,
111+
infmt: Optional[str] = None,
112+
outfmt: Optional[str] = None,
113+
allow_changes: bool = False,
114+
):
99115
"""Convert file from one format to another.
100116
101117
Parameters
@@ -110,12 +126,15 @@ def convert(infn, outfn, many, infmt, outfmt):
110126
The input format.
111127
outfmt
112128
The output format.
129+
allow_changes
130+
Allow prepare_dump functions to modify the data
131+
to make it compatible with the output format.
113132
114133
"""
115134
if many:
116-
dump_many(load_many(infn, fmt=infmt), outfn, fmt=outfmt)
135+
dump_many(load_many(infn, fmt=infmt), outfn, allow_changes=allow_changes, fmt=outfmt)
117136
else:
118-
dump_one(load_one(infn, fmt=infmt), outfn, fmt=outfmt)
137+
dump_one(load_one(infn, fmt=infmt), outfn, allow_changes=allow_changes, fmt=outfmt)
119138

120139

121140
def main():
@@ -124,7 +143,7 @@ def main():
124143
np.seterr(divide="raise", over="raise", invalid="raise")
125144

126145
args = parse_args()
127-
convert(args.input, args.output, args.many, args.infmt, args.outfmt)
146+
convert(args.input, args.output, args.many, args.infmt, args.outfmt, args.allow_changes)
128147

129148

130149
if __name__ == "__main__":

iodata/test/test_cli.py

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,59 +18,118 @@
1818
# --
1919
"""Unit tests for iodata.__main__."""
2020

21-
import functools
2221
import os
2322
import subprocess
2423
import sys
24+
from functools import partial
2525
from importlib.resources import as_file, files
26+
from typing import Optional
27+
from warnings import warn
2628

29+
import pytest
2730
from numpy.testing import assert_allclose, assert_equal
2831

29-
from ..__main__ import convert
32+
from ..__main__ import convert as convfn
3033
from ..api import load_many, load_one
34+
from ..utils import FileFormatError, PrepareDumpError, PrepareDumpWarning
35+
36+
37+
def _convscript(
38+
infn: str,
39+
outfn: str,
40+
many: bool = False,
41+
infmt: Optional[str] = None,
42+
outfmt: Optional[str] = None,
43+
allow_changes: bool = False,
44+
):
45+
"""Simulate the convert function by calling iodata-convert in a subprocess."""
46+
args = [sys.executable, "-m", "iodata.__main__", infn, outfn]
47+
if many:
48+
args.append("-m")
49+
if infmt is not None:
50+
args.append(f"--infmt={infmt}")
51+
if outfmt is not None:
52+
args.append(f"--outfmt={outfmt}")
53+
if allow_changes:
54+
args.append("-c")
55+
cp = subprocess.run(args, capture_output=True, check=False, encoding="utf8")
56+
if cp.returncode == 0:
57+
if allow_changes and "PrepareDumpWarning" in cp.stderr:
58+
warn(PrepareDumpWarning(cp.stderr), stacklevel=2)
59+
else:
60+
if "PrepareDumpError" in cp.stderr:
61+
raise PrepareDumpError(cp.stderr)
62+
if "FileFormatError" in cp.stderr:
63+
raise FileFormatError(cp.stderr)
64+
raise RuntimeError(f"Failure not processed.\n{cp.stderr}")
3165

3266

3367
def _check_convert_one(myconvert, tmpdir):
3468
outfn = os.path.join(tmpdir, "tmp.xyz")
3569
with as_file(files("iodata.test.data").joinpath("hf_sto3g.fchk")) as infn:
36-
myconvert(infn, outfn)
70+
myconvert(infn, outfn, allow_changes=False)
3771
iodata = load_one(outfn)
3872
assert iodata.natom == 2
3973
assert_equal(iodata.atnums, [9, 1])
4074
assert_allclose(iodata.atcoords, [[0.0, 0.0, 0.190484394], [0.0, 0.0, -1.71435955]])
4175

4276

43-
def test_convert_one_autofmt(tmpdir):
44-
myconvert = functools.partial(convert, many=False, infmt=None, outfmt=None)
45-
_check_convert_one(myconvert, tmpdir)
77+
def _check_convert_one_changes(myconvert, tmpdir):
78+
outfn = os.path.join(tmpdir, "tmp.mkl")
79+
with as_file(files("iodata.test.data").joinpath("hf_sto3g.fchk")) as infn:
80+
with pytest.raises(PrepareDumpError):
81+
myconvert(infn, outfn, allow_changes=False)
82+
assert not os.path.isfile(outfn)
83+
with pytest.warns(PrepareDumpWarning):
84+
myconvert(infn, outfn, allow_changes=True)
85+
iodata = load_one(outfn)
86+
assert iodata.natom == 2
87+
assert_equal(iodata.atnums, [9, 1])
88+
assert_allclose(iodata.atcoords, [[0.0, 0.0, 0.190484394], [0.0, 0.0, -1.71435955]])
4689

4790

48-
def test_convert_one_manfmt(tmpdir):
49-
myconvert = functools.partial(convert, many=False, infmt="fchk", outfmt="xyz")
91+
@pytest.mark.parametrize("convert", [convfn, _convscript])
92+
def test_convert_one_autofmt(tmpdir, convert):
93+
myconvert = partial(convfn, many=False, infmt=None, outfmt=None)
5094
_check_convert_one(myconvert, tmpdir)
5195

5296

53-
def test_script_one_autofmt(tmpdir):
54-
def myconvert(infn, outfn):
55-
subprocess.run([sys.executable, "-m", "iodata.__main__", infn, outfn], check=True)
97+
@pytest.mark.parametrize("convert", [convfn, _convscript])
98+
def test_convert_one_autofmt_changes(tmpdir, convert):
99+
myconvert = partial(convert, many=False, infmt=None, outfmt=None)
100+
_check_convert_one_changes(myconvert, tmpdir)
101+
56102

103+
@pytest.mark.parametrize("convert", [convfn, _convscript])
104+
def test_convert_one_manfmt(tmpdir, convert):
105+
myconvert = partial(convert, many=False, infmt="fchk", outfmt="xyz")
57106
_check_convert_one(myconvert, tmpdir)
58107

59108

60-
def test_script_one_manfmt(tmpdir):
61-
def myconvert(infn, outfn):
62-
subprocess.run(
63-
[sys.executable, "-m", "iodata.__main__", infn, outfn, "-i", "fchk", "-o", "xyz"],
64-
check=True,
65-
)
109+
@pytest.mark.parametrize("convert", [convfn, _convscript])
110+
def test_convert_one_nonexisting_infmt(tmpdir, convert):
111+
myconvert = partial(convert, many=False, infmt="blablabla", outfmt="xyz")
112+
with pytest.raises(FileFormatError):
113+
_check_convert_one(myconvert, tmpdir)
114+
115+
116+
@pytest.mark.parametrize("convert", [convfn, _convscript])
117+
def test_convert_one_nonexisting_outfmt(tmpdir, convert):
118+
myconvert = partial(convert, many=False, infmt="fchk", outfmt="blablabla")
119+
with pytest.raises(FileFormatError):
120+
_check_convert_one(myconvert, tmpdir)
66121

67-
_check_convert_one(myconvert, tmpdir)
122+
123+
@pytest.mark.parametrize("convert", [convfn, _convscript])
124+
def test_convert_one_manfmt_changes(tmpdir, convert):
125+
myconvert = partial(convert, many=False, infmt="fchk", outfmt="molekel")
126+
_check_convert_one_changes(myconvert, tmpdir)
68127

69128

70129
def _check_convert_many(myconvert, tmpdir):
71130
outfn = os.path.join(tmpdir, "tmp.xyz")
72131
with as_file(files("iodata.test.data").joinpath("peroxide_relaxed_scan.fchk")) as infn:
73-
myconvert(infn, outfn)
132+
myconvert(infn, outfn, allow_changes=False)
74133
trj = list(load_many(outfn))
75134
assert len(trj) == 13
76135
for iodata in trj:
@@ -80,28 +139,13 @@ def _check_convert_many(myconvert, tmpdir):
80139
assert_allclose(trj[5].atcoords[0], [0.0, 1.32466211, 0.0], atol=1e-5)
81140

82141

83-
def test_convert_many_autofmt(tmpdir):
84-
myconvert = functools.partial(convert, many=True, infmt=None, outfmt=None)
142+
@pytest.mark.parametrize("convert", [convfn, _convscript])
143+
def test_convert_many_autofmt(tmpdir, convert):
144+
myconvert = partial(convert, many=True, infmt=None, outfmt=None)
85145
_check_convert_many(myconvert, tmpdir)
86146

87147

88-
def test_convert_many_manfmt(tmpdir):
89-
myconvert = functools.partial(convert, many=True, infmt="fchk", outfmt="xyz")
90-
_check_convert_many(myconvert, tmpdir)
91-
92-
93-
def test_script_many_autofmt(tmpdir):
94-
def myconvert(infn, outfn):
95-
subprocess.run([sys.executable, "-m", "iodata.__main__", infn, outfn, "-m"], check=True)
96-
97-
_check_convert_many(myconvert, tmpdir)
98-
99-
100-
def test_script_many_manfmt(tmpdir):
101-
def myconvert(infn, outfn):
102-
subprocess.run(
103-
[sys.executable, "-m", "iodata.__main__", infn, outfn, "-m", "-i", "fchk", "-o", "xyz"],
104-
check=True,
105-
)
106-
148+
@pytest.mark.parametrize("convert", [convfn, _convscript])
149+
def test_convert_many_manfmt(tmpdir, convert):
150+
myconvert = partial(convert, many=True, infmt="fchk", outfmt="xyz")
107151
_check_convert_many(myconvert, tmpdir)

0 commit comments

Comments
 (0)