/
starutil.py
executable file
·2028 lines (1576 loc) · 67.6 KB
/
starutil.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import os
import sys
import re
import abc
import subprocess
import tempfile
import shutil
import glob
import inspect
import time
# Provide recall and editing facilities for parameter prompts
import readline
readline.parse_and_bind('tab: complete')
# To run the app under the pdb debugger and break at a given line,
# copy the following line to the place where the break point is
# required, and uncomment.
# import pdb; pdb.set_trace()
# Function to return the name of the executing script, without any
# trailing ".py" suffix.
__cmd = None
def cmd():
global __cmd
if __cmd == None:
__cmd = os.path.basename(sys.argv[0])
if __cmd[-3:] == ".py":
__cmd = __cmd[:-3]
return __cmd
# "Protected" function to return the name of the executing script,
# followed by a colon and a space.
def _cmd_token():
return "{0}: ".format(cmd())
# ------------------- Logging ---------------------------
# Constants defining increasing levels of information to display or log
NONE = 0
CRITICAL = 1
PROGRESS = 2
ATASK = 3
DEBUG = 4
# Level of information to display on the screen, and to log. These are
# set to NONE here to prevent any information being displayed or logged
# by default prior to the creation of the ParSys object.
ilevel = NONE
glevel = NONE
# The requested log file.
logfile = None
# File descriptor and name for the currently open log file (if any)
__logfd = None
__logfile = None
# Funtion to ensure a log file is open
def __open_log_file():
global logfile
global __logfd
global __logfile
# If a log file is currently open, close it if the user has assigned a
# different value to the module "logfile" variable.
if __logfd != None:
if logfile != __logfile:
print( "Closing log file {0}".format(__logfile) )
__logfd.close()
__logfd = None
__logfile == None
# If no log file is currently open, open one now.
if __logfd == None:
if logfile == None:
logfile = "{0}.log".format(cmd())
__logfd = open( logfile, "w" )
__logfile = logfile
print( "Logging to file {0}".format(logfile) )
# Funtion to remove the current working directory from the front of a
# file path.
def _rmcwd(path):
cwd = os.getcwd()
if path.find(cwd) == 0:
return path[len(cwd)+1:]
elif path.find("./") == 0:
return path[3:]
else:
return path
def msg_out(text,level=PROGRESS):
"""
Display a string on the screen and store in the starutil log file,
subject to the current screen and log file information levels
specified by the module variables "ilevel" and "glevel". The name of
the log file is specified by the module variable "logfile".If
"logfile" is unset, it will default to "<script>.log" if the name of
the executing script is known, or to "starutil.log" otherwise.
Invocation:
msg_out(text,level=PROGRESS)
Arguments:
text = string
The text to display.
level = integer
The message will be displayed on the screen if "level" is less
than or equal to the current value of "starutil.ilevel". It
will be written to the log file (if a log file is open) if
"level" is less than or equal to the current value of
"starutil.glevel".
"""
global ilevel
global glevel
global __logfd
if level == DEBUG:
text = "debug> {0}".format(text)
if level <= ilevel:
print(text)
if level <= glevel:
__open_log_file()
__logfd.write(text)
def _getLevel( level ):
if level == "NONE":
return NONE
elif level == "CRITICAL":
return CRITICAL
elif level == "PROGRESS":
return PROGRESS
elif level == "ATASK":
return ATASK
elif level == "DEBUG":
return DEBUG
else:
raise UsageError("{0}Illegal information level '{1}' supplied.".format(_cmd_token(),level))
# ------------------- Using ATASKS ---------------------------
def invoke(command,aslist=False,buffer=None):
"""
Invoke an ADAM atask. An AtaskError is raised if the command fails.
The standard output from the command is returned as the function value.
It may also be written to the screen and to the log file, depending
on the value of the "ilevel" and "glevel" module variables.
Invocation:
value = invoke(command,aslist=False,buffer=None)
Arguments:
command = string
The full command including directory and arguments. The
shell_quote function defined in this module can be used to ensure
that any string containing shell metacharacters is properly quoted.
aslist = boolean
If true, then standard output from the command is returned as a
list of lines. If aslist is False, the standard output is
returned as a single string with new lines separated by embedded
newline characters.
buffer = boolean
If true, then standard output from the command is written to a
disk file as the atask runs, and only read back into Python when
the atask completes. Thus if the screen imformation level
("ILEVEL") is set to ATASK or above, the atask output will only
be shown to the user when the atask has completed. If "buffer" is
False, then the atask standard output is fed back to Python as
the atask creates it, and will appear on the screen in real-time.
NOTE - "buffer" should always be set to True if the atask is a
graphics application that may cause a GWM window to be created.
This is because any new gwmXrefresh process created by the atask
will never terminate and so will cause this script to freeze
whilst it waits for the gwmXrefresh process to end. Writing
standard output to a disk file seems to prevent this for some unknown
reason. If "buffer" is not specified, the default is always to
buffer unless the screen information level ("ILEVEL") is set to
ATASK or higher.
Returned Value:
A single string, or a list of strings, holding the standard output
from the command (see "aslist").
Example
out = invoke("$KAPPA_DIR/stats $KAPPA_DIR/m31 clip=\[3,3,3\]")
kappa:stats is run.
"""
global __logfd
global ATASK
global glevel
global ilevel
os.environ["ADAM_NOPROMPT"] = "1"
os.environ["ADAM_EXIT"] = "1"
msg_out( "\n>>> {0}\n".format(command), ATASK )
if buffer == None:
buffer = ( ilevel < ATASK )
# The original scheme used subprocess.check_output to invoke the
# atask. But the process hung for ever if the invoked command
# involved the creation of a gwm graphics window. I presume this is
# because subprocess.chech_call was waiting for the gwmXrefresh
# process (created by the atask) to die, which it never did unless
# the GWM window was closed. So instead we do the following, which
# involves writing standard output from the atask to a disk file, and
# then reading the disk file back into the script. Any attempt to
# use the stdout argument of subprocess.popen() caused the thing to
# freeze again. StringIO would not work either as subprocess requires
# the stdout file object to "have a real file number".
if buffer:
stdout_file ="./starutil.stdout"
p = subprocess.Popen("{0} > {1} 2>&1".format(command,stdout_file), shell=True)
status = p.wait()
fd = open(stdout_file,"r")
outtxt = fd.read().strip()
fd.close()
os.remove(stdout_file)
msg_out( outtxt, ATASK )
if status != 0:
raise AtaskError("\n\n{0}".format(outtxt))
elif aslist:
outtxt = outtxt.split("\n")
# Non-buffering scheme - Does someone knows how to fix the GWM
# freezing issue without resorting to the above buffering scheme~
else:
if aslist:
outtxt = []
else:
outtxt = ""
proc = subprocess.Popen(command,shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
while True:
line = proc.stdout.readline()
while line != None and len(line) > 0:
line = line.rstrip()
msg_out( line, ATASK )
if aslist:
outtxt.append(line)
else:
outtxt = "{0}\n{1}".format(outtxt,line)
line = proc.stdout.readline()
status = proc.poll()
if status != None:
break
time.sleep(1.0)
if status != 0:
if outtxt:
if aslist:
msg = ""
for line in outtxt:
msg = "{0}\n{1}".format(msg,line)
else:
msg = outtxt
raise AtaskError("\n\n{0}".format(msg))
else:
raise AtaskError()
msg_out( "\n", ATASK )
return outtxt
def get_task_par( parname, taskname, **kwargs ):
"""
Get the current value of an ATASK parameter.
Invocation:
value = get_task_par( parname, taskname, default=??? )
Arguments:
parname = string
The name of the task parameter. This may refer to a single value
in a vector valued parameter by appending the one-based index in
parentheses to the end of the parameter name.
taskname = string
The name of the task.
default = string
A default value to return if the specified parameter cannot be
accessed (e.g. if the parameter file does not exist, or does not
contain the required parameter). If "default" is not supplied,
an exception will be raised if the parameter cannot be accessed.
Returned Value:
The parameter value. This will be a single value if the task parameter
is a scalar value, and a list if the task parameter is a vector.
"""
cmd = "$KAPPA_DIR/parget {0} {1} vector=yes".format( shell_quote(parname),taskname)
if 'default' in kwargs:
try:
text = invoke( cmd, False )
result = eval( text.replace('\n', '' ) )
except AtaskError:
result = kwargs['default']
else:
text = invoke( cmd, False )
result = eval( text.replace('\n', '' ) )
return result
def shell_quote(text):
"""
Put single quotes around a string (and escape any embedded single
quotes) so that it can be used as a command line argument when
running an ATASK from the shell.
Invocation:
value = shell_quote(text)
Arguments:
command = string
The full command including directory and arguments. The
Returned Value:
The quoted string.
"""
if text != None:
return "'" + text.replace("'", "'\\''") + "'"
else:
return None
# ------------------- Parameter System ---------------------------
class ParSys(object):
"""
Encapsulates all parameters used by a Python script.
Each parameter is described by an instance of a subclass of the base
"Parameter" class defined within this module. Different subclasses
describe parameters of different types, and also encapsulates the
parameter name, prompt string, default value, current value, limits,
etc., all of which can be specified when the Parameter object is
created, or set subsequently by assigning values to the appropriate
properties of the Parameter object. These Parameter objects must be
created explicitly prior to creating the ParSys object.
A ParSys can be used as a dictionary in which the keys are the
parameter names, and the values are the Parameter objects, as
in:
scalar = parsys_instance["scalar"].value
parsys_instance["NDF"].default = Parameter.UNSET
etc
When a new ParSys object is created, any values supplied on the
command line of the executing script are stored as the initial values
for the corresponding parameters. However, such values are only
validated when the calling script attempts to get the parameter
value. An UnknownParameterError is raised if the command line
includes values for which there are no corresponding Parameters in
the ParSys. If "--help" or "-h" is found on the command line, the
docstring from the top-level executing script is displayed. If no
docstring is available, a string is displayed holding the script
name followed by the names of the script parameters with any
associated defaults, in their expected order.
Creation of a new ParSys object also causes the creation of a new,
empty, ADAM directory in the user's home directory. This ADAM
directory can be deleted using function ParSys.cleanup().
Three parameters controlling the level of information to display and
log are created automatically when the ParSys constructor is called.
These parameters are:
MSG_FILTER = _LITERAL (Read)
Controls the default level of information reported by Starlink
atasks invoked within the executing script. This default can be
over-ridden by including a value for the msg_filter parameter
within the command string passed to the "invoke" function. The
accepted values are the list defined in SUN/104 ("None", "Quiet",
"Normal", "Verbose", etc). ["Normal"]
ILEVEL = _LITERAL (Read)
Controls the level of information displayed on the screen by the
script. It can take any of the following values (note, these values
are purposefully different to the SUN/104 values to avoid confusion
in their effects):
"NONE" - No screen output is created
"CRITICAL" - Only critical messages are displayed such as warnings.
"PROGRESS" - Extra messages indicating script progress are also
displayed.
"ATASK" - Extra messages are also displayed describing each atask
invocation. Lines starting with ">>>" indicate the command name
and parameter values, and subsequent lines hold the screen output
generated by the command. Also, the value of all script
parameters are displayed at this level.
"DEBUG" - Extra messages are also displayed containing unspecified
debugging information.
In adition, the glevel value can be changed by assigning a new
integer value (one of starutil.NONE, starutil.CRITICAL,
starutil.PROGRESS, starutil.ATASK or starutil.DEBUG) to the module
variable starutil.glevel. ["PROGRESS"]
GLEVEL = _LITERAL (Read)
Controls the level of information to write to a text log file.
Allowed values are as for "ILEVEL". The log file to create is
specified via parameter "LOGFILE. In adition, the glevel value
can be changed by assigning a new integer value (one of
starutil.NONE, starutil.CRITICAL, starutil.PROGRESS,
starutil.ATASK or starutil.DEBUG) to the module variable
starutil.glevel. ["ATASK"]
LOGFILE = _LITERAL (Read)
The name of the log file to create if GLEVEL is not NONE. The
default is "<command>.log", where <command> is the name of the
executing script (minus any trailing ".py" suffix), and will be
created in the current directory. Any file with the same name is
over-written. The script can change the logfile if necessary by
assign the new log file path to the module variable
"starutil.logfile". Any old log file will be closed befopre the
new one is opened. []
To Do:
- Add equivalents to the standard starlink "accept" and "prompt"
keywords.
- Add system for specifying logical parameters as "param" or
"noparam"
- Add a system for remembering current values?
Constructor:
parsys_instance = ParSys( params )
params = list
A list of starutil.Parameter objects. These define all parameters
used by the Python application. The order in which they are
stored in the list define their expected order on the command
line when specified by position rather than keyword.
Properties:
cmd = string
The name of the executing python script.
cmdline = string
A reconstruction of the command line that invoked the executing
script.
params = dict
A dictionary holding all the supplied Parameters, indexed by
parameter name.
usage = string
A string that summarises the usage of the running script. It
contains the script name followed by the supplied parameter
kewords with default values, if any, in the supplied order.
Class Methods:
ParSys.cleanup():
Deletes the temporary ADAM directory created when the ParSys was
constructed.
"""
# The full path to the temporary ADAM directory.
adamdir = None
def __init__(self,params):
global ilevel
global glevel
global logfile
# Store a dictionary holding references to all the supplied Parameters,
# indexed by parameter name. Raise an exception if the list
# contains msg_filter, ilevel or glevel.
self.params = {}
for p in params:
self.params[p.name] = p
p._parsys = self
if p.name == "MSG_FILTER" or p.name == "ILEVEL" or p.name == "GLEVEL":
raise UsageError("{0}The list of starutil parameters includes the reserved parameter name {1} (programming error).".format(_cmd_token(),p.name))
# Create the extra message handling parameters, and append them to the
# end of the list.
levs = [ "NONE", "QUIET", "NORMAL", "VERBOSE", "DEBUG" ]
for i in range(1,21):
levs.append("DEBUG{0}".format(i))
p = ParChoice("MSG_FILTER", levs, "ATASK Message level", "NORMAL",
noprompt=True)
params.append(p)
self.params[p.name] = p
levs = [ "NONE", "CRITICAL", "PROGRESS", "ATASK", "DEBUG" ]
p = ParChoice("ILEVEL", levs, "Level of info. to display on screen",
"PROGRESS", noprompt=True)
params.append(p)
self.params[p.name] = p
p = ParChoice("GLEVEL", levs, "Level of info. to store in log file",
"ATASK", noprompt=True)
params.append(p)
self.params[p.name] = p
p = Par0S("LOGFILE", "The log file to receive diagnostic messages",
"{0}.log".format(cmd()), noprompt=True)
params.append(p)
self.params[p.name] = p
# Store a "usage" string.
self.usage = "Usage: {0} ".format(cmd())
for p in params:
if p.noprompt:
self.usage += "[({0}=){1}] ".format(p.name.lower(),p.default)
else:
self.usage += "({0}=)? ".format(p.name.lower())
# Look for "name=value" items on the command line and store them in
# a dictionary ("byName"). Store all other command line items in an
# ordered list ("byPosition"). Also build a copy of the command line.
self.cmdline = "{0} ".format(cmd())
byName = {}
byPosition = []
for item in sys.argv[1:]:
# If "-h" or "--help" is encountered at any point, display the
# docstring from the top-level script, if available. Otherwise,
# display a simple list of the command name and parameters.
# Then exit the script.
if item == "--help" or item == "-h":
try:
frm = inspect.stack()[ -1 ][0]
text = inspect.getargvalues(frm)[3]["__doc__"]
except:
text = None;
if not text:
text = self.usage
print(text)
sys.exit(0)
self.cmdline += "{0} ".format(item)
m = re.search( '^(\w+)=(.*)$', item )
if m:
name = m.group(1).upper()
value = m.group(2)
else:
name = None
value = item
if len(value) > 0:
if value[0] == "\"" or value[0] == "'":
if value[0] == value[-1]:
value = value[1:-1]
if name == None:
byPosition.append(value)
else:
byName[name] = value
# Loop round all legal parameter names, maintaining the zero-based
# index of the current parameter..
pos = -1
for p in params:
pos += 1
name = p.name
# If a value was supplied for the parameter on the command line,
# store it as the initial value for the Parameter and remove it
# from the "byName" dict.
if name in byName:
p._setValue( byName.pop(name) )
# Otherwise use the next positional value (if any).
elif len(byPosition) > 0:
p._setValue( byPosition.pop(0) )
# If the byName dict or the byPosition list is not empty, (i.e. values
# for unknown parameters were supplied on the command line), raise an
# exception.
if len(byName) > 0:
name = byName.keys()[0]
text = "\n\n{0}: Unknown parameter '{1}' specified on the command line.".format(cmd(),name)
elif len(byPosition) > 0:
text = "\n\n{0}: Too many parameter values supplied on command line.".format(cmd())
else:
text = None
if text != None:
text += "\n{0}\n\n{1}\n".format(self.cmdline,self.usage)
raise UnknownParameterError(text)
# Set the default MSG_FILTER value for all atasks by setting the
# MSG_FILTER environment variable.
msg_filter = self.params["MSG_FILTER"].value
if msg_filter != None:
os.environ["MSG_FILTER"] = msg_filter
# Set the logfile name.
logfile = self.params["LOGFILE"].value
# Set the module variables holding the screen and log file
# information levels.
ilevel = _getLevel(self.params["ILEVEL"].value)
glevel = _getLevel(self.params["GLEVEL"].value)
# Create a new ADAM directory in the user's home directory.
if ParSys.adamdir == None:
ParSys.adamdir = tempfile.mkdtemp( prefix="adam_", suffix="_py",
dir=os.environ["HOME"] )
os.environ["ADAM_USER"] = ParSys.adamdir
# Delete the temporary ADAM directory.
@classmethod
def cleanup(cls):
if ParSys.adamdir != None:
shutil.rmtree( ParSys.adamdir )
# Allow the ParSys to be indexed by parameter name (returns the
# Parameter object as the value).
def __len__(self):
return len(self.params)
def __getitem__(self, key):
return self.params[key.upper()]
def __setitem__(self, key, value):
raise UsageError("{0}Attempt to set a read-only item in a ParSys object (programming error).".format(_cmd_token()))
def __delitem__(self, key):
raise UsageError("{0}Attempt to delete a read-only item in a ParSys object (programming error).".format(_cmd_token()))
def __iter__(self):
self.__inext = -1
return self
def next(self):
self.__inext += 1
parnames = self.params.keys()
if self.__inext < len( parnames ):
return self.params[parnames[ self.__inext ]]
else:
raise StopIteration
# A method to deliver an error message to the user without affecting
# control flow
def _error(self,msg):
print("{0}\n".format(msg))
class Unset(object):
''' A singleton object used to represent unset parameter values or defaults '''
def __repr__(self):
return "<unset>"
class Parameter(object):
__metaclass__ = abc.ABCMeta
'''
An abstract base class describing a generic parameter.
Subclasses of Parameter provide facilities for storing and validating
parameters of different types. The Parameter class itself should never
be instantiated. All parameters are initially unset when created.
Class Constants:
Parameter.UNSET = object
A value that can be assigned to the "value" or "default" property
of a Parameter to indicate that the parameter has no value or
default. Note, the Python "None" object is considered to be a
plausable value for the "value" or "default" property.
Properties:
name = string
The parameter name. Always upper case.
prompt = string
The prompt string.
default = object
The default value. This property may be set to Parameter.UNSET
to indicate that the parameter has no default value, and can be
compared with Parameter.UNSET to test if a default has been set
for the parameter.
value = object
The current value of the parameter. This property may be set to
Parameter.UNSET to indicate that the parameter has no value.
This clears the parameter so that a new value will be ontained
when the value property is next accessed. An InvalidValueError
is raised if an invalid value is assigned to the "value" property.
When the "value" property is read, the returned value is
determined as follows:
I) If the parameter already has a set value, it is returned.
II) If the parameter was assigned a valid value on the command line,
the value is returned.
III) If the parameter allows prompting (see property "noprompt"),
a valid value is obtained from the user and returned.
IV) If the user supplies a null (!) value, or if prompting is
not allowed, then the default value (if set) is returned.
V) If no default has been set, a NoValueError is raised and
Parameter.UNSET is returned.
The string value obtained for the parameter may then be converted
to some other data type, depending on the actions of the particular
subclass of Parameter being used. The "raw" property can be used
to retrieve the uninterpreted string value.
noprompt = boolean
If False, then the user may be prompted to obtain a parameter
value. The full prompt string contains the parameter name, the
"prompt" property, and the default value (if set). If the user
enters an abort (!!) then an AbortError is raised. If the user
enters a null (!) then the default value is accepted, if set (if
no default has been set a NoValueError is raised). If an empty
string is supplied, then the default is accepted, if set (if no
default has been set then the user is re-prompted). If a single
or double question mark is supplied, the "help" property is
displayed, and the user is re-prompted. If an invalid value
is supplied, an error message is displayed, and the user is
re-prompted.
help = string
The help string. May be "None".
raw = string (read-only)
The raw string value of the parameter as obtained from the user
before any interpretation or conversion to other data types.
This will be None if no value has yet been obtained for the
parameter, or if a default of None was accepted by the user.
'''
# __unset is the private value used within the Parameter class
# to indicate an unset value or default. It is never changed. Use
# an instance of a custom class to ensure there is no chance of a
# user-supplied value being equal to __unset.
__unset = Unset()
# UNSET is the public value used to indicate an unset value or
# default. It may conceivably be changed by the caller.
UNSET = __unset
# Define the class initialiser
@abc.abstractmethod
def __init__(self, name, prompt=None, default=UNSET,
noprompt=False, help=None ):
# Initial values for all fields.
self.__name = name.strip().upper()
self.__prompt = None
self.__default = Parameter.__unset
self.__noprompt = False
self.__help = None
self.__value = Parameter.__unset
self.__validated = False
self.__raw = None
self._parsys = None
# Use "protected" setter methods to set the supplied field values.
self._setPrompt( prompt )
self._setDefault( default )
self._setNoPrompt( noprompt )
self._setHelp( help )
# Define "protected" accessor methods for all fields
def _getName(self): # Read-only
return self.__name
def _getRaw(self): # Read-only
return self.__raw
def _getPrompt(self):
return self.__prompt
def _setPrompt(self,val):
self.__prompt = "{0}".format(val) if val else None
def _getDefault(self):
return self.__default if self._testDefault() else Parameter.UNSET
def _setDefault(self,val):
if val != Parameter.UNSET:
self.__default = val
else:
self.__default = Parameter.__unset
def _clearDefault(self):
self.__default = Parameter.__unset
def _testDefault(self):
return self.__default != Parameter.__unset
def _getNoPrompt(self):
return self.__noprompt
def _setNoPrompt(self,val):
self.__noprompt = True if val else False
def _getHelp(self):
return self.__help
def _setHelp(self,val):
self.__help = "{0}".format(val) if val else None
def _getValue(self):
return self.__value if self._testValue() else Parameter.UNSET
def _setValue(self,val):
self.__validated = False
if val != Parameter.UNSET:
self.__value = val
else:
self.__value = Parameter.__unset
def _clearValue(self):
self.__validated = False
self.__value = Parameter.__unset
def _testValue(self):
return self.__value != Parameter.__unset
# Define a function to get the public value of the parameter,
# prompting the user if required.
def _getParameterValue(self):
value = self._getValue()
default = self._getDefault()
while not self.__validated:
defaultUsed = False
validate = True
if not self._testValue():
if not self.__noprompt:
value = self.__promptUser()
else:
value = "!"
self.__raw = value
if value == "!" :
if self._testDefault():
value = default
defaultUsed = True
else:
raise NoValueError("\n{0}No value obtained for parameter '{1}'.".format(_cmd_token(),self.__name))
elif value == "!!" :
raise AbortError("\n{0}Aborted prompt for parameter '{1}'.".format(_cmd_token(),name))
elif value == "?" or value == "??":
help = self._getHelp()
if help:
print(help)
else:
text = "No help available for parameter '{0}'.".format(name)
self.__error(text)
validate = False
if validate:
try:
self._setValue( value )
self.__validate()
msg_out( "Parameter {0} is set to {1}\n".format(self.__name,self.__value), ATASK )
except InvalidParameterError as err:
self.__error(err)
self.__raw = None
if defaultUsed:
self._clearDefault()
return self._getValue()
# Define a function to set the public value of the parameter, raising
# an InvalidParameterError if the value is not valid.
def _setParameterValue(self,value):
self._setValue( value )
self.__validate()
# Define public properties for all public fields
name = property(_getName, None, None, "The parameter name")
raw = property(_getRaw, None, None, "The raw string value of the parameter")
prompt = property(_getPrompt, _setPrompt, None, "The user prompt string")
default = property(_getDefault, _setDefault, None, "The default value")
noprompt = property(_getNoPrompt, _setNoPrompt, None, "Do not prompt the user?")
help = property(_getHelp, _setHelp, None, "The parameter help string")
value = property(_getParameterValue, _setParameterValue, None, "The parameter value")
# "Protected" method to check if the current parameter value is a
# valid value for the parameter, raising an InvalidParameterError if
# not. It may modify the stored value if necessary (e.g. changing its
# type) as part of the check.
@abc.abstractmethod
def _isValid(self):
pass
# "Protected" method to validate the current value of a Parameter,
# clearing the value and raising an InvalidParameterError if the
# value is not valid.
def __validate(self):
if not self.__validated:
if not self._testValue():
raise InvalidParameterError("\n{0}Unset value not valid for parameter '{1}'.".format(_cmd_token(),self._getName()))
elif self._getValue() == None and self._getDefault() == None:
self.__validated = True
else:
try:
self._isValid()
self.__validated = True
except InvalidParameterError:
self._clearValue()
raise
# Private function to display an error to the user without raising an
# exception. Delegate it to the enclosing parameter system if possible.
# Do a simple print otherwise.
def __error(self,msg):
if self._parsys:
self._parsys._error(msg)
else:
print("{0}\n".format(msg))
# Prompt the user for a value
def __promptUser(self):
name = self._getName()
pmt = "{0} ".format(name)
prompt = self._getPrompt()
if prompt: