1- import random
1+ import shutil
22import os
3- from functools import partial
43import textwrap
54
65from paths_cli .wizard .tools import yes_no , a_an
1413
1514from paths_cli .wizard import pause
1615
17- import shutil
1816
1917class 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
3454class 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 } \n Here'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