Skip to content

Commit bc32c3c

Browse files
committed
docstrings/pylint for wizard.py
1 parent 005aeab commit bc32c3c

1 file changed

Lines changed: 161 additions & 19 deletions

File tree

‎paths_cli/wizard/wizard.py‎

Lines changed: 161 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import random
1+
import shutil
22
import os
3-
from functools import partial
43
import textwrap
54

65
from paths_cli.wizard.tools import yes_no, a_an
@@ -14,24 +13,62 @@
1413

1514
from paths_cli.wizard import pause
1615

17-
import shutil
1816

1917
class Console: # no-cov
18+
"""Manage actual I/O for the Wizard.
19+
20+
All direct interaction with the user is performed in this class.
21+
"""
2022
# TODO: add logging so we can output the session
2123
def print(self, *content):
24+
"""Write content to screen"""
2225
print(*content)
2326

2427
def input(self, content):
28+
"""Read user input.
29+
30+
Parameters
31+
----------
32+
content : str
33+
input prompt
34+
"""
2535
return input(content)
2636

2737
@property
2838
def width(self):
39+
"""Terminal width in columns"""
2940
return shutil.get_terminal_size((80, 24)).columns
3041

31-
def draw_hline(self):
32-
self.print('═' * self.width)
42+
def draw_hline(self, char='═'):
43+
"""Draw a separator line.
44+
45+
Parameters
46+
----------
47+
char : str
48+
string to use for line separator
49+
"""
50+
n_chars = self.width // len(char)
51+
self.print(char * n_chars)
52+
3353

3454
class Wizard:
55+
"""Friendly interactive Wizard
56+
57+
This class handles most of the user-facing interaction with the wizard,
58+
including various conveniences for asking for certain user selections
59+
(such as selecting between a number of possible choices.)
60+
61+
An instance of this class includes information about how the Wizard will
62+
guide the user through various stages of simulation set-up.
63+
64+
The main method, ``Wizard.run_wizard()``, performs an entire simulation
65+
set-up for that instance.
66+
67+
Parameters
68+
----------
69+
steps : List[:class:`.WizardStep`]
70+
ordered list of steps in this particular simulation set-up process
71+
"""
3572
def __init__(self, steps):
3673
self.steps = steps
3774
self.requirements = {
@@ -60,7 +97,8 @@ def _patch(self): # no-cov
6097
self._patched = True
6198
StorageLoader.has_simstore_patch = True
6299

63-
def debug(content): # no-cov
100+
def debug(self, content): # no-cov
101+
"""Print to console without pretty-printing"""
64102
# debug does no pretty-printing
65103
self.console.print(content)
66104

@@ -84,6 +122,7 @@ def _speak(self, content, preface):
84122
@get_object
85123
def ask(self, question, options=None, default=None, helper=None,
86124
autohelp=False):
125+
"""Ask the user a question."""
87126
if helper is None:
88127
helper = Helper(None)
89128
if isinstance(helper, str):
@@ -92,30 +131,60 @@ def ask(self, question, options=None, default=None, helper=None,
92131
self.console.print()
93132
if result == "":
94133
if not autohelp:
95-
return
134+
return None
96135
result = "?" # autohelp in this case
97136

98137
if helper and result[0] in ["?", "!"]:
99138
self.say(helper(result))
100-
return
139+
return None
101140
return result
102141

103142
def say(self, content, preface="🧙 "):
143+
"""Let the wizard make a statement.
144+
145+
Parameters
146+
----------
147+
content : str or List[str]
148+
Content to be presented to user. Input will be wrapped to fit
149+
the user's terminal. If a list of strings, each element is
150+
printed with a blank line separating them.
151+
preface : str
152+
preface, used only on the first line of the first element of the
153+
``content``.
154+
"""
104155
self._speak(content, preface)
105156
self.console.print() # adds a blank line
106157

107158
def start(self, content):
159+
"""Specialized version of :method:`.say` for starting an object"""
108160
# eventually, this will tweak so that we preface with a line and use
109-
# green text here
161+
# green text here TODO: possibly remove?
110162
self.say(content)
111163

112164
def bad_input(self, content, preface="👺 "):
165+
"""Specialized version of :method:`.say` for printing errors"""
113166
# just changes the default preface; maybe print 1st line red?
114167
self.say(content, preface)
115168

116169
@get_object
117170
def ask_enumerate_dict(self, question, options, helper=None,
118171
autohelp=False):
172+
"""Ask the user to select from a set of options.
173+
174+
Parameters
175+
----------
176+
question : str
177+
the question to ask the user (asked before the options are
178+
listed)
179+
options: Dict[str, Any]
180+
mapping of the string name (shown to the user in the list of
181+
options) to the object to return
182+
183+
Returns
184+
-------
185+
Any :
186+
the object the user selected by either name or number
187+
"""
119188
self.say(question)
120189
opt_string = "\n".join([f" {(i+1):>3}. {opt}"
121190
for i, opt in enumerate(options)])
@@ -138,6 +207,8 @@ def ask_enumerate_dict(self, question, options, helper=None,
138207

139208
def ask_enumerate(self, question, options):
140209
"""Ask the user to select from a list of options"""
210+
# NOTE: new code should use ask_enumerate_dict. If we were past the
211+
# beta stage, this would probably issue a PendingDeprecationWarning
141212
self.say(question)
142213
opt_string = "\n".join([f" {(i+1):>3}. {opt}"
143214
for i, opt in enumerate(options)])
@@ -161,31 +232,51 @@ def ask_enumerate(self, question, options):
161232

162233
@get_object
163234
def ask_load(self, question, loader, helper=None, autohelp=False):
235+
"""Load from user input according to ``loader`` method
236+
237+
Parameters
238+
----------
239+
question : str
240+
string to ask the user
241+
loader : Callable[str] -> Any
242+
method that converts user input into the desired format for the
243+
object
244+
helper : :class:`.Helper`
245+
object to handle user requests for help
246+
"""
164247
as_str = self.ask(question, helper=helper)
165248
try:
166249
result = loader(as_str)
167250
except Exception as e:
168251
self.exception(f"Sorry, I couldn't understand the input "
169252
f"'{as_str}'.", e)
170-
return
253+
return None
171254
return result
172255

173-
# this should match the args for wizard.ask
174256
@get_object
175257
def ask_custom_eval(self, question, options=None, default=None,
176258
helper=None, type_=float):
259+
"""Get user input and convert using custom_eval.
260+
261+
.. note::
262+
New code should use ask_load. If we were past beta, this would
263+
have a PendingDeprecationWarning.
264+
"""
177265
as_str = self.ask(question, options=options, default=default,
178266
helper=helper)
179267
try:
180268
result = type_(custom_eval(as_str))
181269
except Exception as e:
182270
self.exception(f"Sorry, I couldn't understand the input "
183271
f"'{as_str}'", e)
184-
return
272+
return None
185273
return result
186274

187-
188275
def obj_selector(self, store_name, text_name, create_func):
276+
"""Select an object from the wizard's pseudo-storage
277+
"""
278+
# TODO: this seems like something that might be possible to refactor
279+
# out
189280
opts = {name: lambda wiz, o=obj: o
190281
for name, obj in getattr(self, store_name).items()}
191282
create_new = f"Create a new {text_name}"
@@ -198,10 +289,27 @@ def obj_selector(self, store_name, text_name, create_func):
198289
return obj
199290

200291
def exception(self, msg, exception):
292+
"""Specialized version of :method:`.bad_input` for exceptions"""
201293
self.bad_input(f"{msg}\nHere's the error I got:\n"
202294
f"{exception.__class__.__name__}: {exception}")
203295

204-
def name(self, obj, obj_type, store_name, default=None):
296+
def name(self, obj, obj_type, store_name):
297+
"""Name a newly created object.
298+
299+
Parameters
300+
----------
301+
obj : Any
302+
the new object
303+
obj_type : str
304+
user-facing name of the object type
305+
store_name : str
306+
name of the OPS store in which to save this object type
307+
308+
Returns
309+
-------
310+
Any :
311+
named object
312+
"""
205313
self.say(f"Now let's name your {obj_type}.")
206314
name = None
207315
while name is None:
@@ -220,8 +328,23 @@ def name(self, obj, obj_type, store_name, default=None):
220328
+ name_joke(name, obj_type))
221329
return obj
222330

223-
224331
def register(self, obj, obj_type, store_name):
332+
"""Register a newly-created object in the storage
333+
334+
Parameters
335+
----------
336+
obj : Any
337+
the new object
338+
obj_type : str
339+
user-facing name of the object type
340+
store_name : str
341+
name of the OPS store in which to save this object type
342+
343+
Returns
344+
-------
345+
Any :
346+
input object, possibly after being named
347+
"""
225348
if not obj.is_named:
226349
obj = self.name(obj, obj_type, store_name)
227350
store_dict = getattr(self, store_name)
@@ -230,31 +353,38 @@ def register(self, obj, obj_type, store_name):
230353

231354
@get_object
232355
def get_storage(self):
356+
"""Create a file to store the object database to.
357+
358+
Returns
359+
-------
360+
:class:`openpathsampling.experimental.storage.Storage` :
361+
the storage file object
362+
"""
233363
from openpathsampling.experimental.storage import Storage
234364
filename = self.ask("Where would you like to save your setup "
235365
"database?")
236366
if not filename.endswith(".db"):
237367
self.bad_input("Files produced by this wizard must end in "
238368
"'.db'.")
239-
return
369+
return None
240370

241371
if os.path.exists(filename):
242372
overwrite = self.ask(f"{filename} exists. Overwrite it?",
243373
options=["[Y]es", "[N]o"])
244374
overwrite = yes_no(overwrite)
245375
if not overwrite:
246-
return
376+
return None
247377

248378
try:
249379
storage = Storage(filename, mode='w')
250380
except Exception as e:
251381
self.exception(FILE_LOADING_ERROR_MSG, e)
252-
return
382+
return None
253383

254384
return storage
255385

256-
257386
def _storage_description_line(self, store_name):
387+
"""List number of each type of object to be stored"""
258388
store = getattr(self, store_name)
259389
if len(store) == 0:
260390
return ""
@@ -266,6 +396,13 @@ def _storage_description_line(self, store_name):
266396
return line
267397

268398
def save_to_file(self, storage):
399+
"""Save Wizard's pseudostorage to a real file.
400+
401+
Parameters
402+
----------
403+
storage : :class:`openpathsampling.experimental.storage.Storage`
404+
storage file to save to
405+
"""
269406
store_names = ['engines', 'cvs', 'states', 'networks', 'schemes']
270407
lines = [self._storage_description_line(store_name)
271408
for store_name in store_names]
@@ -280,6 +417,7 @@ def save_to_file(self, storage):
280417
self.say("Success! Everything has been stored in your file.")
281418

282419
def _req_do_another(self, req):
420+
"""Check whether requirement requires us to do another"""
283421
store, min_, max_ = req
284422
if store is None:
285423
return (True, False)
@@ -290,6 +428,7 @@ def _req_do_another(self, req):
290428
return requires, allows
291429

292430
def _ask_do_another(self, obj_type):
431+
"""Ask the user whether to do another"""
293432
do_another = None
294433
while do_another is None:
295434
do_another_char = self.ask(
@@ -303,6 +442,7 @@ def _ask_do_another(self, obj_type):
303442
return do_another
304443

305444
def _do_one(self, step, req):
445+
"""Create a single object of the sort generated by ``step``"""
306446
try:
307447
obj = step.func(self)
308448
except RestartObjectException:
@@ -320,6 +460,7 @@ def _do_one(self, step, req):
320460
return do_another
321461

322462
def run_wizard(self):
463+
"""Run this Wizard."""
323464
self.start("Hi! I'm the OpenPathSampling Wizard.")
324465
# TODO: next line is only temporary
325466
self.say("Today I'll help you set up a 2-state TPS simulation.")
@@ -343,6 +484,7 @@ def run_wizard(self):
343484

344485
@get_object
345486
def _ask_save(self):
487+
"""Ask user whether to save before quitting"""
346488
do_save_char = self.ask("Before quitting, would you like to save "
347489
"the objects you've created so far?")
348490
try:

0 commit comments

Comments
 (0)