Skip to content

Commit 93a8c36

Browse files
authored
Merge pull request #2 from dwhswenson/append
Appending to files
2 parents 5677183 + 9c687f9 commit 93a8c36

File tree

5 files changed

+321
-13
lines changed

5 files changed

+321
-13
lines changed

paths_cli/cli.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import os
88

99
import click
10+
# import click_completion
11+
# click_completion.init()
1012

1113
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
1214

@@ -116,14 +118,14 @@ def format_commands(self, ctx, formatter):
116118
openpathsampling strip-snapshots --help
117119
"""
118120

121+
OPS_CLI = OpenPathSamplingCLI(
122+
name="openpathsampling",
123+
help=_MAIN_HELP,
124+
context_settings=CONTEXT_SETTINGS
125+
)
119126

120127
def main(): # no-cov
121-
cli = OpenPathSamplingCLI(
122-
name="openpathsampling",
123-
help=_MAIN_HELP,
124-
context_settings=CONTEXT_SETTINGS
125-
)
126-
cli()
128+
OPS_CLI()
127129

128130

129131
if __name__ == '__main__': # no-cov

paths_cli/commands/append.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import click
2+
from paths_cli.parameters import (
3+
INPUT_FILE, APPEND_FILE, MULTI_CV, MULTI_ENGINE, MULTI_VOLUME,
4+
MULTI_NETWORK, MULTI_SCHEME, MULTI_TAG
5+
)
6+
7+
@click.command(
8+
'append',
9+
short_help="add objects from INPUT_FILE to another file"
10+
)
11+
@INPUT_FILE.clicked(required=True)
12+
@APPEND_FILE.clicked(required=True)
13+
@MULTI_ENGINE.clicked(required=False)
14+
@MULTI_CV.clicked(required=False)
15+
@MULTI_VOLUME.clicked(required=False)
16+
@MULTI_NETWORK.clicked(required=False)
17+
@MULTI_SCHEME.clicked(required=False)
18+
@MULTI_TAG.clicked(required=False)
19+
@click.option('--save-tag', type=str, default=None,
20+
help=("save object to a tag; requires that only one "
21+
+ "object be specfied. Can also be used to rename "
22+
+ "tagged objects. To append a tagged object without "
23+
+ "a tag, use --save-tag \"\""))
24+
def append(input_file, append_file, engine, cv, volume, network, scheme,
25+
tag, save_tag):
26+
"""Append objects from INPUT_FILE to another file.
27+
"""
28+
storage = INPUT_FILE.get(input_file)
29+
output_storage = APPEND_FILE.get(append_file)
30+
params = [MULTI_ENGINE, MULTI_CV, MULTI_VOLUME, MULTI_NETWORK,
31+
MULTI_SCHEME, MULTI_TAG]
32+
args = [engine, cv, volume, network, scheme, tag]
33+
to_save = []
34+
for arg, param in zip(args, params):
35+
to_save.extend(param.get(storage, arg))
36+
37+
to_save = [obj for obj in to_save if obj is not None]
38+
if save_tag is not None and len(to_save) != 1:
39+
raise RuntimeError("Can't identify the object to tag when saving "
40+
+ str(len(to_save)) + " objects.")
41+
42+
for obj in to_save:
43+
output_storage.save(obj)
44+
45+
if tag and len(tag) == 1 and save_tag is None:
46+
save_tag = tag[0]
47+
48+
if save_tag:
49+
output_storage.tags[save_tag] = to_save[0]
50+
51+
# TO TEST
52+
# 3. "untag" an object by not associating a tag in the new storage
53+
54+
output_storage.close()
55+
56+
57+
CLI = append
58+
SECTION = "Miscellaneous"
59+
REQUIRES_OPS = (1, 0)

paths_cli/parameters.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import click
2+
import os
23
# import openpathsampling as paths
34

45
class AbstractParameter(object):
@@ -14,6 +15,8 @@ def clicked(self, required=False):
1415
# we'll use tests of the -h option in the .travis.yml to ensure that the
1516
# .clicked methods work
1617

18+
HELP_MULTIPLE = "; may be used more than once"
19+
1720
class Option(AbstractParameter):
1821
def clicked(self, required=False): # no-cov
1922
return click.option(*self.args, **self.kwargs, required=required)
@@ -40,18 +43,38 @@ def __init__(self, param, mode):
4043
super(StorageLoader, self).__init__(param)
4144
self.mode = mode
4245

46+
def _workaround(self, name):
47+
# this is messed up... for some reason, storage doesn't create a new
48+
# file in append mode. That may be a bug
49+
import openpathsampling as paths
50+
if self.mode == 'a' and not os.path.exists(name):
51+
st = paths.Storage(name, mode='w')
52+
st.close()
53+
4354
def get(self, name):
4455
import openpathsampling as paths
56+
self._workaround(name)
4557
return paths.Storage(name, mode=self.mode)
4658

4759

4860
class OPSStorageLoadNames(AbstractLoader):
61+
"""Simple loader that expects its input to be a name or index.
62+
"""
4963
def __init__(self, param, store):
5064
super(OPSStorageLoadNames, self).__init__(param)
5165
self.store = store
5266

5367
def get(self, storage, names):
54-
return [getattr(storage, self.store)[name] for name in names]
68+
int_corrected = []
69+
for name in names:
70+
try:
71+
name = int(name)
72+
except ValueError:
73+
pass
74+
int_corrected.append(name)
75+
76+
return [getattr(storage, self.store)[name]
77+
for name in int_corrected]
5578

5679

5780
class OPSStorageLoadSingle(AbstractLoader):
@@ -171,16 +194,47 @@ def init_snap_fallback(parameter, storage, name):
171194

172195
CVS = OPSStorageLoadNames(
173196
param=Option('--cv', type=str, multiple=True,
174-
help='name of CV; may select more than once'),
197+
help='name of CV' + HELP_MULTIPLE),
175198
store='cvs'
176199
)
177200

201+
MULTI_VOLUME = OPSStorageLoadNames(
202+
param=Option('--volume', type=str, multiple=True,
203+
help='name or index of volume' + HELP_MULTIPLE),
204+
store='volumes'
205+
)
206+
207+
MULTI_ENGINE = OPSStorageLoadNames(
208+
param=Option('--engine', type=str, multiple=True,
209+
help='name or index of engine' + HELP_MULTIPLE),
210+
store='engines'
211+
)
212+
213+
178214
STATES = OPSStorageLoadNames(
179-
param=Option('-s', '--state', multiple=True,
180-
help='name of state; may select more than once'),
215+
param=Option('-s', '--state', type=str, multiple=True,
216+
help='name of state' + HELP_MULTIPLE),
181217
store='volumes'
182218
)
183219

220+
MULTI_TAG = OPSStorageLoadNames(
221+
param=Option('--tag', type=str, multiple=True,
222+
help='tag for object' + HELP_MULTIPLE),
223+
store='tags'
224+
)
225+
226+
MULTI_NETWORK = OPSStorageLoadNames(
227+
param=Option('--network', type=str, multiple=True,
228+
help='name or index of network' + HELP_MULTIPLE),
229+
store='networks'
230+
)
231+
232+
MULTI_SCHEME = OPSStorageLoadNames(
233+
param=Option('--scheme', type=str, multiple=True,
234+
help='name or index of move scheme' + HELP_MULTIPLE),
235+
store='schemes'
236+
)
237+
184238
INPUT_FILE = StorageLoader(
185239
param=Argument('input_file',
186240
type=click.Path(exists=True, readable=True)),
@@ -194,5 +248,14 @@ def init_snap_fallback(parameter, storage, name):
194248
mode='w'
195249
)
196250

251+
APPEND_FILE = StorageLoader(
252+
param=Option('-a', '--append-file',
253+
type=click.Path(writable=True, readable=True),
254+
help="file to append to"),
255+
mode='a'
256+
)
257+
197258
N_STEPS_MC = click.option('-n', '--nsteps', type=int,
198259
help="number of Monte Carlo trials to run")
260+
261+
MULTI_CV = CVS
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import os
2+
3+
import pytest
4+
from click.testing import CliRunner
5+
6+
from paths_cli.commands.append import *
7+
8+
import openpathsampling as paths
9+
10+
def make_input_file(tps_network_and_traj):
11+
input_file = paths.Storage("setup.py", mode='w')
12+
for obj in tps_network_and_traj:
13+
input_file.save(obj)
14+
15+
input_file.tags['template'] = input_file.snapshots[0]
16+
input_file.close()
17+
return "setup.py"
18+
19+
def test_append(tps_network_and_traj):
20+
runner = CliRunner()
21+
with runner.isolated_filesystem():
22+
in_file = make_input_file(tps_network_and_traj)
23+
result = runner.invoke(append, [in_file, '-a', 'output.nc',
24+
'--volume', 'A', '--volume', 'B'])
25+
assert result.exit_code == 0
26+
assert result.exception is None
27+
storage = paths.Storage('output.nc', mode='r')
28+
assert len(storage.volumes) == 2
29+
assert len(storage.snapshots) == 0
30+
storage.volumes['A'] # smoke tests that we can load
31+
storage.volumes['B']
32+
storage.close()
33+
34+
result = runner.invoke(append, [in_file, '-a', 'output.nc',
35+
'--tag', 'template'])
36+
storage = paths.Storage('output.nc', mode='r')
37+
assert len(storage.volumes) == 2
38+
assert len(storage.snapshots) == 2 # one snapshot + reverse
39+
40+
@pytest.mark.parametrize('n_objects', [0, 2])
41+
def test_append_tag_error(tps_network_and_traj, n_objects):
42+
objs = {2: ['--volume', "A", '--volume', "B"], 0: []}[n_objects]
43+
runner = CliRunner()
44+
with runner.isolated_filesystem():
45+
in_file = make_input_file(tps_network_and_traj)
46+
result = runner.invoke(append,
47+
[in_file, '-a', "output.nc"] + objs
48+
+ ["--save-tag", "foo"])
49+
assert isinstance(result.exception, RuntimeError)
50+
assert "Can't identify the object to tag" in str(result.exception)
51+
52+
def test_append_tag(tps_network_and_traj):
53+
runner = CliRunner()
54+
with runner.isolated_filesystem():
55+
in_file = make_input_file(tps_network_and_traj)
56+
result = runner.invoke(append,
57+
[in_file, '-a', "output.nc",
58+
'--tag', 'template', '--save-tag', 'foo'])
59+
assert result.exit_code == 0
60+
assert result.exception is None
61+
62+
storage = paths.Storage("output.nc", mode='r')
63+
assert len(storage.snapshots) == 2
64+
assert len(storage.tags) == 1
65+
assert storage.tags['foo'] is not None
66+
storage.close()
67+
68+
def test_append_same_tag(tps_network_and_traj):
69+
runner = CliRunner()
70+
with runner.isolated_filesystem():
71+
in_file = make_input_file(tps_network_and_traj)
72+
result = runner.invoke(append,
73+
[in_file, '-a', "output.nc",
74+
'--tag', 'template'])
75+
assert result.exit_code == 0
76+
assert result.exception is None
77+
78+
storage = paths.Storage("output.nc", mode='r')
79+
assert len(storage.snapshots) == 2
80+
assert len(storage.tags) == 1
81+
assert storage.tags['template'] is not None
82+
storage.close()
83+
84+
def test_append_remove_tag(tps_network_and_traj):
85+
runner = CliRunner()
86+
with runner.isolated_filesystem():
87+
in_file = make_input_file(tps_network_and_traj)
88+
result = runner.invoke(append,
89+
[in_file, '-a', "output.nc",
90+
"--tag", 'template', '--save-tag', ''])
91+
print(result.output)
92+
assert result.exception is None
93+
assert result.exit_code == 0
94+
95+
storage = paths.Storage("output.nc", mode='r')
96+
assert len(storage.snapshots) == 2
97+
assert len(storage.tags) == 0
98+
storage.close()
99+

0 commit comments

Comments
 (0)