Skip to content

Commit 7471eda

Browse files
authored
Merge pull request #17 from dwhswenson/refactor_params
Refactor parameters
2 parents 9d1d369 + f1bb7a6 commit 7471eda

File tree

3 files changed

+293
-170
lines changed

3 files changed

+293
-170
lines changed

paths_cli/file_copying.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77
import click
88
from tqdm.auto import tqdm
9-
from paths_cli.parameters import (
10-
Option, Argument, HELP_MULTIPLE, StorageLoader, OPSStorageLoadNames
9+
from paths_cli.param_core import (
10+
Option, Argument, StorageLoader, OPSStorageLoadNames
1111
)
12+
from paths_cli.parameters import HELP_MULTIPLE
1213

1314
INPUT_APPEND_FILE = StorageLoader(
1415
param=Argument('append_file',

paths_cli/param_core.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import click
2+
import os
3+
4+
5+
class AbstractParameter(object):
6+
"""Abstract wrapper for click parameters.
7+
8+
Forbids use setting ``required``, because we do that for each command
9+
individually.
10+
11+
Parameters
12+
----------
13+
args :
14+
args to pass to click parameters
15+
kwargs :
16+
kwargs to pass to click parameters
17+
"""
18+
def __init__(self, *args, **kwargs):
19+
self.args = args
20+
if 'required' in kwargs:
21+
raise ValueError("Can't set required status now")
22+
self.kwargs = kwargs
23+
24+
def clicked(self, required=False):
25+
raise NotImplementedError()
26+
27+
28+
# we'll use tests of the -h option in the .travis.yml to ensure that the
29+
# .clicked methods work
30+
class Option(AbstractParameter):
31+
"""Wrapper for click.option decorators"""
32+
def clicked(self, required=False): # no-cov
33+
"""Create the click decorator"""
34+
return click.option(*self.args, **self.kwargs, required=required)
35+
36+
37+
class Argument(AbstractParameter):
38+
"""Wrapper for click.argument decorators"""
39+
def clicked(self, required=False): # no-cov
40+
"""Create the click decorator"""
41+
return click.argument(*self.args, **self.kwargs, required=required)
42+
43+
44+
class AbstractLoader(object):
45+
"""Abstract object for getting relevant OPS object from the CLI.
46+
47+
Parameters
48+
----------
49+
param : :class:`.AbstractParameter`
50+
the Option or Argument wrapping a click decorator
51+
"""
52+
def __init__(self, param):
53+
self.param = param
54+
55+
@property
56+
def clicked(self): # no-cov
57+
"""Create the click decorator"""
58+
return self.param.clicked
59+
60+
def get(self, *args, **kwargs):
61+
"""Get the desired OPS object, based on the CLI input"""
62+
raise NotImplementedError()
63+
64+
65+
class StorageLoader(AbstractLoader):
66+
"""Open an OPS storage file
67+
68+
Parameters
69+
----------
70+
param : :class:`.AbstractParameter`
71+
the Option or Argument wrapping a click decorator
72+
mode : 'r', 'w', or 'a'
73+
the mode for the file
74+
"""
75+
def __init__(self, param, mode):
76+
super(StorageLoader, self).__init__(param)
77+
self.mode = mode
78+
79+
def _workaround(self, name):
80+
# this is messed up... for some reason, storage doesn't create a new
81+
# file in append mode. That may be a bug
82+
import openpathsampling as paths
83+
if self.mode == 'a' and not os.path.exists(name):
84+
st = paths.Storage(name, mode='w')
85+
st.close()
86+
87+
def get(self, name):
88+
import openpathsampling as paths
89+
self._workaround(name)
90+
return paths.Storage(name, mode=self.mode)
91+
92+
93+
class OPSStorageLoadNames(AbstractLoader):
94+
"""Simple loader that expects its input to be a name or index.
95+
96+
Parameters
97+
----------
98+
param : :class:`.AbstractParameter`
99+
the Option or Argument wrapping a click decorator
100+
store : Str
101+
the name of the store to search
102+
"""
103+
def __init__(self, param, store):
104+
super(OPSStorageLoadNames, self).__init__(param)
105+
self.store = store
106+
107+
def get(self, storage, names):
108+
"""Get the names from the storage
109+
110+
Parameters
111+
----------
112+
storage : :class:`openpathsampling.Storage`
113+
storage file to search in
114+
names : List[Str]
115+
names or numbers (as string) to use as keys to load from
116+
storage
117+
118+
Returns
119+
-------
120+
List[Any] :
121+
the desired objects
122+
"""
123+
int_corrected = []
124+
for name in names:
125+
try:
126+
name = int(name)
127+
except ValueError:
128+
pass
129+
int_corrected.append(name)
130+
131+
return [getattr(storage, self.store)[name]
132+
for name in int_corrected]
133+
134+
135+
class Getter(object):
136+
"""Abstract strategy for getting things from storage
137+
138+
Parameters
139+
----------
140+
store_name : Str
141+
the name of the storage to search
142+
"""
143+
def __init__(self, store_name):
144+
self.store_name = store_name
145+
146+
def _get(self, storage, name):
147+
store = getattr(storage, self.store_name)
148+
try:
149+
return store[name]
150+
except:
151+
return None
152+
153+
154+
class GetByName(Getter):
155+
"""Strategy using the CLI input as name for a stored item"""
156+
def __call__(self, storage, name):
157+
return self._get(storage, name)
158+
159+
160+
class GetByNumber(Getter):
161+
"""Strategy using the CLI input as numeric index of the stored item"""
162+
def __call__(self, storage, name):
163+
try:
164+
num = int(name)
165+
except:
166+
return None
167+
168+
return self._get(storage, num)
169+
170+
171+
class GetPredefinedName(Getter):
172+
"""Strategy predefining name and store, allow default names"""
173+
def __init__(self, store_name, name):
174+
super().__init__(store_name=store_name)
175+
self.name = name
176+
177+
def __call__(self, storage):
178+
return self._get(storage, self.name)
179+
180+
181+
class GetOnly(Getter):
182+
"""Strategy getting item from store if it is the only one"""
183+
def __call__(self, storage):
184+
store = getattr(storage, self.store_name)
185+
if len(store) == 1:
186+
return store[0]
187+
188+
189+
class GetOnlyNamed(Getter):
190+
"""Strategy selecting item from store if it is the only named item"""
191+
def __call__(self, storage):
192+
store = getattr(storage, self.store_name)
193+
named_things = [o for o in store if o.is_named]
194+
if len(named_things) == 1:
195+
return named_things[0]
196+
197+
198+
class GetOnlySnapshot(Getter):
199+
"""Strategy selecting only snapshot from a snapshot store"""
200+
def __init__(self, store_name="snapshots"):
201+
super().__init__(store_name)
202+
203+
def __call__(self, storage):
204+
store = getattr(storage, self.store_name)
205+
if len(store) == 2:
206+
# this is really only 1 snapshot; reversed copy gets saved
207+
return store[0]
208+
209+
210+
def _try_strategies(strategies, storage, **kwargs):
211+
result = None
212+
for strategy in strategies:
213+
result = strategy(storage, **kwargs)
214+
if result is not None:
215+
return result
216+
217+
218+
class OPSStorageLoadSingle(AbstractLoader):
219+
"""Objects that expect to load a single object.
220+
221+
These can sometimes include guesswork to figure out which object is
222+
desired. The details of how that guesswork is performed is determined
223+
by the strategy lists that are given.
224+
225+
Parameters
226+
----------
227+
param : :class:`.AbstractParameter`
228+
the Option or Argument wrapping a click decorator
229+
store : Str
230+
the name of the store to search
231+
value_strategies : List[Callable[(:class:`.Storage`, Str), Any]]
232+
The strategies to be used when the CLI provides a value for this
233+
parameter. Each should be a callable taking a storage and the string
234+
input from the CLI, and should return the desired object or None if
235+
it cannot be found.
236+
none_strategies : List[Callable[:class:`openpathsampling.Storage, Any]]
237+
The strategies to be used when the CLI does not provide a value for
238+
this parameter. Each should be a callable taking a storage, and
239+
returning the desired object or None if it cannot be found.
240+
"""
241+
def __init__(self, param, store, value_strategies=None,
242+
none_strategies=None):
243+
super(OPSStorageLoadSingle, self).__init__(param)
244+
self.store = store
245+
if value_strategies is None:
246+
value_strategies = [GetByName(self.store),
247+
GetByNumber(self.store)]
248+
self.value_strategies = value_strategies
249+
250+
if none_strategies is None:
251+
none_strategies = [GetOnly(self.store),
252+
GetOnlyNamed(self.store)]
253+
self.none_strategies = none_strategies
254+
255+
def get(self, storage, name):
256+
"""Load desired object from storage.
257+
258+
Parameters
259+
----------
260+
storage : openpathsampling.Storage
261+
the input storage to search
262+
name : Str or None
263+
string from CLI providing the identifier (name or index) for
264+
this object; None if not provided
265+
"""
266+
if name is not None:
267+
result = _try_strategies(self.value_strategies, storage,
268+
name=name)
269+
else:
270+
result = _try_strategies(self.none_strategies, storage)
271+
272+
if result is None:
273+
raise RuntimeError("Couldn't find %s", name)
274+
275+
return result

0 commit comments

Comments
 (0)