11# -*- coding: utf-8 -*-
22
3- import typing as ty
3+ from typing import Optional , List , Tuple , Dict , Set
4+
45import collections
56import weakref
67import inspect
@@ -91,8 +92,8 @@ class OptionGroup:
9192 :param help: the group help text or None
9293 """
9394
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
9697 self ._name = name if name else ''
9798 self ._help = inspect .cleandoc (help if help else '' )
9899 self ._hidden = hidden
@@ -117,30 +118,18 @@ def help(self) -> str:
117118 return self ._help
118119
119120 @property
120- def name_extra (self ) -> ty . List [str ]:
121+ def name_extra (self ) -> List [str ]:
121122 """Returns extra name attributes for the group
122123 """
123124 return []
124125
125126 @property
126- def forbidden_option_attrs (self ) -> ty . List [str ]:
127+ def forbidden_option_attrs (self ) -> List [str ]:
127128 """Returns the list of forbidden option attributes for the group
128129 """
129130 return []
130131
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 ]]:
144133 """Returns the help record for the group
145134
146135 :param ctx: Click Context object
@@ -149,14 +138,20 @@ def get_help_record(self, ctx: click.Context) -> ty.Optional[ty.Tuple[str, str]]
149138 if all (o .hidden for o in self .get_options (ctx ).values ()):
150139 return None
151140
152- name = self .get_default_name ( ctx )
141+ name = self .name
153142 help_ = self .help if self .help else ''
154143
155144 extra = ', ' .join (self .name_extra )
156145 if extra :
157146 extra = f'[{ extra } ]'
158147
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
160155
161156 return name , help_
162157
@@ -186,17 +181,17 @@ def decorator(func):
186181
187182 return decorator
188183
189- def get_options (self , ctx : click .Context ) -> ty . Dict [str , GroupedOption ]:
184+ def get_options (self , ctx : click .Context ) -> Dict [str , GroupedOption ]:
190185 """Returns the dictionary with group options
191186 """
192187 return self ._options .get (resolve_wrappers (ctx .command .callback ), {})
193188
194- def get_option_names (self , ctx : click .Context ) -> ty . List [str ]:
189+ def get_option_names (self , ctx : click .Context ) -> List [str ]:
195190 """Returns the list with option names ordered by addition in the group
196191 """
197192 return list (reversed (list (self .get_options (ctx ))))
198193
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 :
200195 options = self .get_options (ctx )
201196 text = ''
202197
@@ -250,6 +245,9 @@ def _option_memo(self, func):
250245 option = params [- 1 ]
251246 self ._options [func ][option .name ] = option
252247
248+ def _group_name_str (self ) -> str :
249+ return f"'{ self .name } '" if self .name else "the"
250+
253251
254252class RequiredAnyOptionGroup (OptionGroup ):
255253 """Option group with required any options of this group
@@ -258,29 +256,35 @@ class RequiredAnyOptionGroup(OptionGroup):
258256 """
259257
260258 @property
261- def forbidden_option_attrs (self ) -> ty . List [str ]:
259+ def forbidden_option_attrs (self ) -> List [str ]:
262260 return ['required' ]
263261
264262 @property
265- def name_extra (self ) -> ty . List [str ]:
263+ def name_extra (self ) -> List [str ]:
266264 return super ().name_extra + ['required_any' ]
267265
268266 def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
269267 if option .name in opts :
270268 return
271269
272270 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+ )
276277
277278 option_names = set (self .get_options (ctx ))
278279
279280 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 )
282283
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+ )
284288
285289
286290class RequiredAllOptionGroup (OptionGroup ):
@@ -290,23 +294,25 @@ class RequiredAllOptionGroup(OptionGroup):
290294 """
291295
292296 @property
293- def forbidden_option_attrs (self ) -> ty . List [str ]:
297+ def forbidden_option_attrs (self ) -> List [str ]:
294298 return ['required' , 'hidden' ]
295299
296300 @property
297- def name_extra (self ) -> ty . List [str ]:
301+ def name_extra (self ) -> List [str ]:
298302 return super ().name_extra + ['required_all' ]
299303
300304 def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
301305 option_names = set (self .get_options (ctx ))
302306
303307 if not option_names .issubset (opts ):
308+ group_name = self ._group_name_str ()
304309 required_names = option_names .difference (option_names .intersection (opts ))
310+ option_info = self .get_error_hint (ctx , required_names )
305311
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+ )
310316
311317
312318class MutuallyExclusiveOptionGroup (OptionGroup ):
@@ -317,11 +323,11 @@ class MutuallyExclusiveOptionGroup(OptionGroup):
317323 """
318324
319325 @property
320- def forbidden_option_attrs (self ) -> ty . List [str ]:
326+ def forbidden_option_attrs (self ) -> List [str ]:
321327 return ['required' ]
322328
323329 @property
324- def name_extra (self ) -> ty . List [str ]:
330+ def name_extra (self ) -> List [str ]:
325331 return super ().name_extra + ['mutually_exclusive' ]
326332
327333 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
330336 given_option_count = len (given_option_names )
331337
332338 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+ )
336347
337348
338349class RequiredMutuallyExclusiveOptionGroup (MutuallyExclusiveOptionGroup ):
@@ -343,7 +354,7 @@ class RequiredMutuallyExclusiveOptionGroup(MutuallyExclusiveOptionGroup):
343354 """
344355
345356 @property
346- def name_extra (self ) -> ty . List [str ]:
357+ def name_extra (self ) -> List [str ]:
347358 return super ().name_extra + ['required' ]
348359
349360 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
353364 given_option_names = option_names .intersection (opts )
354365
355366 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+ )
360375
361376
362377class AllOptionGroup (OptionGroup ):
363378 """Option group with required all/none options of this group
364379
365380 `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
367382 """
368383
369384 @property
370- def forbidden_option_attrs (self ) -> ty . List [str ]:
385+ def forbidden_option_attrs (self ) -> List [str ]:
371386 return ['required' , 'hidden' ]
372387
373388 @property
374- def name_extra (self ) -> ty . List [str ]:
389+ def name_extra (self ) -> List [str ]:
375390 return super ().name_extra + ['all_or_none' ]
376391
377392 def handle_parse_result (self , option : GroupedOption , ctx : click .Context , opts : dict ) -> None :
378393 option_names = set (self .get_options (ctx ))
379394
380395 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