3
3
"""
4
4
5
5
import click
6
+ import sys
6
7
7
8
8
9
class NaturalOrderGroup (click .Group ):
@@ -12,6 +13,7 @@ class NaturalOrderGroup(click.Group):
12
13
13
14
@click.group(cls=NaturalOrderGroup)
14
15
"""
16
+
15
17
def list_commands (self , ctx ):
16
18
"""List command names as they are in commands dict.
17
19
@@ -25,38 +27,67 @@ class CommaSeparatedText(click.ParamType):
25
27
"""
26
28
Comma separated text
27
29
"""
30
+
28
31
def __init__ (self , dtype = click .STRING , simplify = False , length = None ):
29
32
self .dtype = dtype
30
33
self .dtype_name = _get_type_name (dtype )
31
34
self .simplify = simplify
32
35
self .length = length
33
36
if length and length <= 3 :
34
- self .name = ',' .join ([f' { self .dtype_name } ' ] * length )
37
+ self .name = "," .join ([f" { self .dtype_name } " ] * length )
35
38
else :
36
- self .name = ' {}[,{}...]' .format (self .dtype_name , self .dtype_name )
39
+ self .name = " {}[,{}...]" .format (self .dtype_name , self .dtype_name )
37
40
38
41
def convert (self , value , param , ctx ):
42
+ """
43
+ >>> @click.command()
44
+ ... @click.option('--test-param')
45
+ ... def test_cmd():
46
+ ... pass
47
+ ...
48
+ >>> ctx = click.Context(test_cmd)
49
+ >>> param = test_cmd.params[0]
50
+ >>> test_cst1 = CommaSeparatedText()
51
+ >>> test_cst2 = CommaSeparatedText(click.INT, length=2)
52
+ >>> test_cst3 = CommaSeparatedText(click.FLOAT, simplify=True)
53
+ >>>
54
+ >>> test_cst1.convert(None, param, ctx)
55
+ >>> test_cst2.convert('7,2', param, ctx)
56
+ [7, 2]
57
+ >>> test_cst2.convert('7.2', param, ctx)
58
+ Traceback (most recent call last):
59
+ ...
60
+ click.exceptions.BadParameter: 7.2 is not a valid integer
61
+ >>> test_cst2.convert('7', param, ctx)
62
+ Traceback (most recent call last):
63
+ ...
64
+ click.exceptions.BadParameter: 7 is not a valid comma separated list of length 2
65
+ >>> test_cst3.convert('7.2', param, ctx)
66
+ 7.2
67
+ """
39
68
try :
40
69
if value is None :
41
70
converted = None
42
71
else :
43
- converted = list (map (self .dtype , str (value ).split (',' )))
72
+ converted = list (map (self .dtype , str (value ).split ("," )))
44
73
if self .simplify and len (converted ) == 1 :
45
74
converted = converted [0 ]
46
75
except ValueError :
47
76
self .fail (
48
- '{} is not a valid comma separated list of {}' .format (
49
- value , self .dtype_name ),
77
+ "{} is not a valid comma separated list of {}" .format (
78
+ value , self .dtype_name
79
+ ),
50
80
param ,
51
- ctx
81
+ ctx ,
52
82
)
53
83
if self .length :
54
84
if len (converted ) != self .length :
55
85
self .fail (
56
- '{} is not a valid comma separated list of length {}' .format (
57
- value , self .length ),
86
+ "{} is not a valid comma separated list of length {}" .format (
87
+ value , self .length
88
+ ),
58
89
param ,
59
- ctx
90
+ ctx ,
60
91
)
61
92
return converted
62
93
@@ -65,26 +96,50 @@ class Dictionary(click.ParamType):
65
96
"""
66
97
Text to be parsed as a python dict definition
67
98
"""
99
+
68
100
def __init__ (self , keys = None ):
69
- self .name = ' TEXT:VAL[,TEXT:VAL...]'
101
+ self .name = " TEXT:VAL[,TEXT:VAL...]"
70
102
self .keys = keys
71
103
72
104
def convert (self , value , param , ctx ):
105
+ """
106
+ >>> @click.command()
107
+ ... @click.option('--my-param', type=Dictionary(keys=('abc', 'def', 'ghi', 'jkl', 'mno')))
108
+ ... def test_cmd():
109
+ ... pass
110
+ ...
111
+ >>> ctx = click.Context(test_cmd)
112
+ >>> param = test_cmd.params[0]
113
+ >>> dict_param = param.type
114
+ >>> dict_str1 = 'abc:0.1,def:TRUE,ghi:False,jkl:None,mno:some_string'
115
+ >>> dict_str2 = 'abc:0.1,def:TRUE,ghi:False,jkl:None,mnp:some_string'
116
+ >>> dict_str3 = ''
117
+ >>> dict_param.convert(dict_str1, param, ctx)
118
+ {'abc': 0.1, 'def': True, 'ghi': False, 'jkl': None, 'mno': 'some_string'}
119
+ >>> dict_param.convert(dict_str2, param, ctx)
120
+ Traceback (most recent call last):
121
+ ...
122
+ click.exceptions.BadParameter: mnp is not a valid key (('abc', 'def', 'ghi', 'jkl', 'mno'))
123
+ >>> dict_param.convert(dict_str3, param, ctx)
124
+ Traceback (most recent call last):
125
+ ...
126
+ click.exceptions.BadParameter: is not a valid python dict definition
127
+ """
73
128
try :
74
129
converted = dict ()
75
- for token in value .split (',' ):
76
- if ':' not in token :
130
+ for token in value .split ("," ):
131
+ if ":" not in token :
77
132
raise ValueError
78
- key , _ , value = token .partition (':' )
133
+ key , _ , value = token .partition (":" )
79
134
if not key :
80
135
raise ValueError
81
136
if isinstance (self .keys , (list , tuple )) and key not in self .keys :
82
- self .fail (f' { key } is not a valid key ({ self .keys } )' )
83
- if value == ' None' :
137
+ self .fail (f" { key } is not a valid key ({ self .keys } )" )
138
+ if value == " None" :
84
139
value = None
85
- elif value .lower () == ' true' :
140
+ elif value .lower () == " true" :
86
141
value = True
87
- elif value .lower () == ' false' :
142
+ elif value .lower () == " false" :
88
143
value = False
89
144
else :
90
145
try :
@@ -94,39 +149,76 @@ def convert(self, value, param, ctx):
94
149
converted [key ] = value
95
150
return converted
96
151
except ValueError :
97
- self .fail (
98
- f'{ value } is not a valid python dict definition' ,
99
- param ,
100
- ctx
101
- )
152
+ self .fail (f"{ value } is not a valid python dict definition" , param , ctx )
102
153
103
154
104
155
def _get_type_name (obj ):
105
- name = ' text'
156
+ name = " text"
106
157
try :
107
- name = getattr (obj , ' name' )
158
+ name = getattr (obj , " name" )
108
159
except AttributeError :
109
- name = getattr (obj , ' __name__' )
160
+ name = getattr (obj , " __name__" )
110
161
return name
111
162
112
163
113
164
def valid_limit (ctx , param , value ):
165
+ """
166
+ Callback function that checks order of numeric inputs
167
+
168
+ >>> @click.command()
169
+ ... @click.option('--test-param', help='Sample help')
170
+ ... def test_cmd():
171
+ ... pass
172
+ ...
173
+ >>> ctx = click.Context(test_cmd)
174
+ >>> param = test_cmd.params[0]
175
+ >>> valid_limit(ctx, param, value=[0.0125, 3])
176
+ [0.0125, 3]
177
+ >>> valid_limit(ctx, param, value=[0.0125, -0.0125])
178
+ Traceback (most recent call last):
179
+ ...
180
+ click.exceptions.BadParameter: lower limit must not exceed upper limit
181
+ >>> valid_limit(ctx, param, value=[0.0125, 0.0125])
182
+ [0.0125, 0.0125]
183
+ """
114
184
if value [0 ] > value [1 ]:
115
- param .type .fail (
116
- 'lower limit must not exceed upper limit' , param , ctx )
185
+ param .type .fail ("lower limit must not exceed upper limit" , param , ctx )
117
186
return value
118
187
119
188
120
189
def valid_parameter_limits (ctx , param , value ):
190
+ """
191
+ Callback function that checks order of multiple numeric inputs
192
+
193
+ >>> @click.command()
194
+ ... @click.option('--test-param', type=(click.STRING, click.FLOAT, click.FLOAT), multiple=True)
195
+ ... def test_cmd():
196
+ ... pass
197
+ ...
198
+ >>> ctx = click.Context(test_cmd)
199
+ >>> param = test_cmd.params[0]
200
+ >>> valid_parameter_limits(ctx, param, [['a', 0.0, 2.0]])
201
+ [['a', 0.0, 2.0]]
202
+ >>> valid_parameter_limits(ctx, param, [['b', 0.0, 0.0]])
203
+ [['b', 0.0, 0.0]]
204
+ >>> valid_parameter_limits(ctx, param, [['c', 0.0, -1.0]])
205
+ Traceback (most recent call last):
206
+ ...
207
+ click.exceptions.BadParameter: lower limit must not exceed upper limit
208
+ >>> valid_parameter_limits(ctx, param, [['a', 0.0, 2.0], ['c', 0.0, -1.0]])
209
+ Traceback (most recent call last):
210
+ ...
211
+ click.exceptions.BadParameter: lower limit must not exceed upper limit
212
+ """
121
213
for val in value :
122
214
if val [1 ] > val [2 ]:
123
- param .type .fail (
124
- 'lower limit must not exceed upper limit' , param , ctx )
215
+ param .type .fail ("lower limit must not exceed upper limit" , param , ctx )
125
216
return value
126
217
127
218
128
219
def mutually_exclusive_with (param_name ):
129
- internal_name = param_name .strip ('-' ).replace ('-' , '_' ).lower ()
220
+ internal_name = param_name .strip ("-" ).replace ("-" , "_" ).lower ()
221
+
130
222
def valid_mutually_exclusive (ctx , param , value ):
131
223
try :
132
224
other_value = ctx .params [internal_name ]
@@ -135,22 +227,35 @@ def valid_mutually_exclusive(ctx, param, value):
135
227
if (value is None ) == (other_value is None ):
136
228
param .type .fail (
137
229
'mutually exclusive with "{}", one and only one must be '
138
- ' specified.' .format (param_name ),
230
+ " specified." .format (param_name ),
139
231
param ,
140
232
ctx ,
141
233
)
142
234
return value
235
+
143
236
return valid_mutually_exclusive
144
237
145
238
146
239
def required_by (param_name ):
147
- internal_name = param_name .strip ('-' ).replace ('-' , '_' ).lower ()
240
+ internal_name = param_name .strip ("-" ).replace ("-" , "_" ).lower ()
241
+
148
242
def required (ctx , param , value ):
149
243
try :
150
244
other_value = ctx .params [internal_name ]
151
245
except KeyError :
152
246
return value
153
247
if other_value and not value :
154
- param .type .fail ('required by "{}".' .format (param_name ), param , ctx ,)
248
+ param .type .fail (
249
+ 'required by "{}".' .format (param_name ),
250
+ param ,
251
+ ctx ,
252
+ )
155
253
return value
254
+
156
255
return required
256
+
257
+
258
+ if __name__ == "__main__" :
259
+ import doctest
260
+
261
+ sys .exit (doctest .testmod (verbose = True )[0 ])
0 commit comments