1
1
# -*- coding: utf-8 -*-
2
2
3
- import typing as ty
3
+ from typing import Optional , List , Tuple , Dict , Set
4
+
4
5
import collections
5
6
import weakref
6
7
import inspect
@@ -91,8 +92,8 @@ class OptionGroup:
91
92
:param help: the group help text or None
92
93
"""
93
94
94
- def __init__ (self , name : ty . Optional [str ] = None , * ,
95
- hidden = False , help : ty . Optional [str ] = None ) -> None : # noqa
95
+ def __init__ (self , name : Optional [str ] = None , * ,
96
+ hidden = False , help : Optional [str ] = None ) -> None : # noqa
96
97
self ._name = name if name else ''
97
98
self ._help = inspect .cleandoc (help if help else '' )
98
99
self ._hidden = hidden
@@ -117,30 +118,18 @@ def help(self) -> str:
117
118
return self ._help
118
119
119
120
@property
120
- def name_extra (self ) -> ty . List [str ]:
121
+ def name_extra (self ) -> List [str ]:
121
122
"""Returns extra name attributes for the group
122
123
"""
123
124
return []
124
125
125
126
@property
126
- def forbidden_option_attrs (self ) -> ty . List [str ]:
127
+ def forbidden_option_attrs (self ) -> List [str ]:
127
128
"""Returns the list of forbidden option attributes for the group
128
129
"""
129
130
return []
130
131
131
- def get_default_name (self , ctx : click .Context ) -> str :
132
- """Returns default name for the group
133
-
134
- :param ctx: Click Context object
135
- :return: group default name
136
- """
137
- if self .name :
138
- return self .name
139
-
140
- option_names = '|' .join (self .get_option_names (ctx ))
141
- return f'({ option_names } )'
142
-
143
- def get_help_record (self , ctx : click .Context ) -> ty .Optional [ty .Tuple [str , str ]]:
132
+ def get_help_record (self , ctx : click .Context ) -> Optional [Tuple [str , str ]]:
144
133
"""Returns the help record for the group
145
134
146
135
:param ctx: Click Context object
@@ -149,14 +138,20 @@ def get_help_record(self, ctx: click.Context) -> ty.Optional[ty.Tuple[str, str]]
149
138
if all (o .hidden for o in self .get_options (ctx ).values ()):
150
139
return None
151
140
152
- name = self .get_default_name ( ctx )
141
+ name = self .name
153
142
help_ = self .help if self .help else ''
154
143
155
144
extra = ', ' .join (self .name_extra )
156
145
if extra :
157
146
extra = f'[{ extra } ]'
158
147
159
- name = f'{ name } : { extra } '
148
+ if name :
149
+ name = f'{ name } : { extra } '
150
+ elif extra :
151
+ name = f'{ extra } :'
152
+
153
+ if not name and not help_ :
154
+ return None
160
155
161
156
return name , help_
162
157
@@ -186,17 +181,17 @@ def decorator(func):
186
181
187
182
return decorator
188
183
189
- def get_options (self , ctx : click .Context ) -> ty . Dict [str , GroupedOption ]:
184
+ def get_options (self , ctx : click .Context ) -> Dict [str , GroupedOption ]:
190
185
"""Returns the dictionary with group options
191
186
"""
192
187
return self ._options .get (resolve_wrappers (ctx .command .callback ), {})
193
188
194
- def get_option_names (self , ctx : click .Context ) -> ty . List [str ]:
189
+ def get_option_names (self , ctx : click .Context ) -> List [str ]:
195
190
"""Returns the list with option names ordered by addition in the group
196
191
"""
197
192
return list (reversed (list (self .get_options (ctx ))))
198
193
199
- def get_error_hint (self , ctx , option_names : ty . Optional [ty . Set [str ]] = None ) -> str :
194
+ def get_error_hint (self , ctx , option_names : Optional [Set [str ]] = None ) -> str :
200
195
options = self .get_options (ctx )
201
196
text = ''
202
197
@@ -250,6 +245,9 @@ def _option_memo(self, func):
250
245
option = params [- 1 ]
251
246
self ._options [func ][option .name ] = option
252
247
248
+ def _group_name_str (self ) -> str :
249
+ return f"'{ self .name } '" if self .name else "the"
250
+
253
251
254
252
class RequiredAnyOptionGroup (OptionGroup ):
255
253
"""Option group with required any options of this group
@@ -258,29 +256,35 @@ class RequiredAnyOptionGroup(OptionGroup):
258
256
"""
259
257
260
258
@property
261
- def forbidden_option_attrs (self ) -> ty . List [str ]:
259
+ def forbidden_option_attrs (self ) -> List [str ]:
262
260
return ['required' ]
263
261
264
262
@property
265
- def name_extra (self ) -> ty . List [str ]:
263
+ def name_extra (self ) -> List [str ]:
266
264
return super ().name_extra + ['required_any' ]
267
265
268
266
def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
269
267
if option .name in opts :
270
268
return
271
269
272
270
if all (o .hidden for o in self .get_options (ctx ).values ()):
273
- error_text = (f'Need at least one non-hidden option in RequiredAnyOptionGroup '
274
- f'"{ self .get_default_name (ctx )} ".' )
275
- raise TypeError (error_text )
271
+ cls_name = self .__class__ .__name__
272
+ group_name = self ._group_name_str ()
273
+
274
+ raise TypeError (
275
+ f"Need at least one non-hidden option in { group_name } option group ({ cls_name } )."
276
+ )
276
277
277
278
option_names = set (self .get_options (ctx ))
278
279
279
280
if not option_names .intersection (opts ):
280
- error_text = f'Missing one of the required options from " { self .get_default_name ( ctx ) } " option group:'
281
- error_text += f' \n { self .get_error_hint (ctx )} '
281
+ group_name = self ._group_name_str ()
282
+ option_info = self .get_error_hint (ctx )
282
283
283
- raise click .UsageError (error_text , ctx = ctx )
284
+ raise click .UsageError (
285
+ f"At least one of the following options from { group_name } option group is required:\n { option_info } " ,
286
+ ctx = ctx
287
+ )
284
288
285
289
286
290
class RequiredAllOptionGroup (OptionGroup ):
@@ -290,23 +294,25 @@ class RequiredAllOptionGroup(OptionGroup):
290
294
"""
291
295
292
296
@property
293
- def forbidden_option_attrs (self ) -> ty . List [str ]:
297
+ def forbidden_option_attrs (self ) -> List [str ]:
294
298
return ['required' , 'hidden' ]
295
299
296
300
@property
297
- def name_extra (self ) -> ty . List [str ]:
301
+ def name_extra (self ) -> List [str ]:
298
302
return super ().name_extra + ['required_all' ]
299
303
300
304
def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
301
305
option_names = set (self .get_options (ctx ))
302
306
303
307
if not option_names .issubset (opts ):
308
+ group_name = self ._group_name_str ()
304
309
required_names = option_names .difference (option_names .intersection (opts ))
310
+ option_info = self .get_error_hint (ctx , required_names )
305
311
306
- error_text = f'Missing required options from " { self . get_default_name ( ctx ) } " option group:'
307
- error_text += f' \n { self . get_error_hint ( ctx , required_names ) } '
308
-
309
- raise click . UsageError ( error_text , ctx = ctx )
312
+ raise click . UsageError (
313
+ f"Missing required options from { group_name } option group: \n { option_info } " ,
314
+ ctx = ctx
315
+ )
310
316
311
317
312
318
class MutuallyExclusiveOptionGroup (OptionGroup ):
@@ -317,11 +323,11 @@ class MutuallyExclusiveOptionGroup(OptionGroup):
317
323
"""
318
324
319
325
@property
320
- def forbidden_option_attrs (self ) -> ty . List [str ]:
326
+ def forbidden_option_attrs (self ) -> List [str ]:
321
327
return ['required' ]
322
328
323
329
@property
324
- def name_extra (self ) -> ty . List [str ]:
330
+ def name_extra (self ) -> List [str ]:
325
331
return super ().name_extra + ['mutually_exclusive' ]
326
332
327
333
def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
@@ -330,9 +336,14 @@ def handle_parse_result(self, option: GroupedOption, ctx: click.Context, opts: d
330
336
given_option_count = len (given_option_names )
331
337
332
338
if given_option_count > 1 :
333
- error_text = 'The given mutually exclusive options cannot be used at the same time:'
334
- error_text += f'\n { self .get_error_hint (ctx , given_option_names )} '
335
- raise click .UsageError (error_text , ctx = ctx )
339
+ group_name = self ._group_name_str ()
340
+ option_info = self .get_error_hint (ctx , given_option_names )
341
+
342
+ raise click .UsageError (
343
+ f"Mutually exclusive options from { group_name } option group "
344
+ f"cannot be used at the same time:\n { option_info } " ,
345
+ ctx = ctx
346
+ )
336
347
337
348
338
349
class RequiredMutuallyExclusiveOptionGroup (MutuallyExclusiveOptionGroup ):
@@ -343,7 +354,7 @@ class RequiredMutuallyExclusiveOptionGroup(MutuallyExclusiveOptionGroup):
343
354
"""
344
355
345
356
@property
346
- def name_extra (self ) -> ty . List [str ]:
357
+ def name_extra (self ) -> List [str ]:
347
358
return super ().name_extra + ['required' ]
348
359
349
360
def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
@@ -353,34 +364,40 @@ def handle_parse_result(self, option: GroupedOption, ctx: click.Context, opts: d
353
364
given_option_names = option_names .intersection (opts )
354
365
355
366
if len (given_option_names ) == 0 :
356
- error_text = ('Missing one of the required mutually exclusive options from '
357
- f'"{ self .get_default_name (ctx )} " option group:' )
358
- error_text += f'\n { self .get_error_hint (ctx )} '
359
- raise click .UsageError (error_text , ctx = ctx )
367
+ group_name = self ._group_name_str ()
368
+ option_info = self .get_error_hint (ctx )
369
+
370
+ raise click .UsageError (
371
+ "Missing one of the required mutually exclusive options from "
372
+ f"{ group_name } option group:\n { option_info } " ,
373
+ ctx = ctx
374
+ )
360
375
361
376
362
377
class AllOptionGroup (OptionGroup ):
363
378
"""Option group with required all/none options of this group
364
379
365
380
`AllOptionGroup` defines the behavior:
366
- - All options from the group must be set or None must be set.
381
+ - All options from the group must be set or None must be set
367
382
"""
368
383
369
384
@property
370
- def forbidden_option_attrs (self ) -> ty . List [str ]:
385
+ def forbidden_option_attrs (self ) -> List [str ]:
371
386
return ['required' , 'hidden' ]
372
387
373
388
@property
374
- def name_extra (self ) -> ty . List [str ]:
389
+ def name_extra (self ) -> List [str ]:
375
390
return super ().name_extra + ['all_or_none' ]
376
391
377
392
def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
378
393
option_names = set (self .get_options (ctx ))
379
394
380
395
if not option_names .isdisjoint (opts ) and option_names .intersection (opts ) != option_names :
381
- error_text = f'All options should be specified or None should be specified from the group ' \
382
- f'"{ self .get_default_name (ctx )} ".'
383
- error_text += f'\n Missing required options from "{ self .get_default_name (ctx )} " option group.'
384
- error_text += f'\n { self .get_error_hint (ctx )} '
385
- error_text += '\n '
386
- raise click .UsageError (error_text , ctx = ctx )
396
+ group_name = self ._group_name_str ()
397
+ option_info = self .get_error_hint (ctx )
398
+
399
+ raise click .UsageError (
400
+ f"All options from { group_name } option group should be specified or none should be specified. "
401
+ f"Missing required options:\n { option_info } " ,
402
+ ctx = ctx
403
+ )
0 commit comments