Skip to content

Commit fba6855

Browse files
authored
Merge pull request #23 from click-contrib/v0.5.2
v0.5.2 - Do not use default option group name. An empty group name will not be displayed (#22) - Slightly edited error messages - All arguments except name in optgroup decorator must be keyword-only
2 parents 223ae0c + c463e31 commit fba6855

File tree

8 files changed

+130
-89
lines changed

8 files changed

+130
-89
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v0.5.2 (28.11.2020)
4+
5+
* Do not use default option group name. An empty group name will not be displayed
6+
* Slightly edited error messages
7+
* All arguments except `name` in `optgroup` decorator must be keyword-only
8+
39
## v0.5.1 (14.06.2020)
410

511
* Fix incompatibility with autocomplete: out of the box Click completion and click-repl (Issue [#14](https://github.com/click-contrib/click-option-group/issues/14))

click_option_group/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
Option groups missing in Click
77
8-
:copyright: © 2019 by Eugene Prilepin
8+
:copyright: © 2019-2020 by Eugene Prilepin
99
:license: BSD, see LICENSE for more details.
1010
"""
1111

click_option_group/_core.py

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

3-
import typing as ty
3+
from typing import Optional, List, Tuple, Dict, Set
4+
45
import collections
56
import weakref
67
import 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

254252
class 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

286290
class 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

312318
class 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

338349
class 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

362377
class 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'\nMissing 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+
)

click_option_group/_decorators.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

3-
import typing as ty
3+
from typing import Optional, NamedTuple, List, Tuple, Dict, Any, Type
4+
45
import collections.abc as abc
56
import collections
67
import warnings
@@ -15,9 +16,9 @@
1516
)
1617

1718

18-
class OptionStackItem(ty.NamedTuple):
19-
param_decls: ty.Tuple[str, ...]
20-
attrs: ty.Dict[str, ty.Any]
19+
class OptionStackItem(NamedTuple):
20+
param_decls: Tuple[str, ...]
21+
attrs: Dict[str, Any]
2122
param_count: int
2223

2324

@@ -61,29 +62,42 @@ class _OptGroup:
6162
"""
6263

6364
def __init__(self) -> None:
64-
self._decorating_state: ty.Dict[abc.Callable, ty.List[OptionStackItem]] = collections.defaultdict(list)
65-
self._not_attached_options: ty.Dict[abc.Callable, ty.List[click.Option]] = collections.defaultdict(list)
65+
self._decorating_state: Dict[abc.Callable, List[OptionStackItem]] = collections.defaultdict(list)
66+
self._not_attached_options: Dict[abc.Callable, List[click.Option]] = collections.defaultdict(list)
6667
self._outer_frame_index = 1
6768

68-
def __call__(self, name: ty.Optional[str] = None, help: ty.Optional[str] = None,
69-
cls: ty.Optional[ty.Type[OptionGroup]] = None, **attrs):
69+
def __call__(self,
70+
name: Optional[str] = None, *,
71+
help: Optional[str] = None,
72+
cls: Optional[Type[OptionGroup]] = None, **attrs):
73+
"""Creates a new group and collects its options
74+
75+
Creates the option group and registers all grouped options
76+
which were added by `option` decorator.
77+
78+
:param name: Group name or None for deault name
79+
:param help: Group help or None for empty help
80+
:param cls: Option group class that should be inherited from `OptionGroup` class
81+
:param attrs: Additional parameters of option group class
82+
"""
7083
try:
7184
self._outer_frame_index = 2
72-
return self.group(name, cls=cls, help=help, **attrs)
85+
return self.group(name, help=help, cls=cls, **attrs)
7386
finally:
7487
self._outer_frame_index = 1
7588

76-
def group(self, name: ty.Optional[str] = None, *,
77-
cls: ty.Optional[ty.Type[OptionGroup]] = None,
78-
help: ty.Optional[str] = None, **attrs):
89+
def group(self,
90+
name: Optional[str] = None, *,
91+
help: Optional[str] = None,
92+
cls: Optional[Type[OptionGroup]] = None, **attrs):
7993
"""The decorator creates a new group and collects its options
8094
8195
Creates the option group and registers all grouped options
8296
which were added by `option` decorator.
8397
8498
:param name: Group name or None for deault name
85-
:param cls: Option group class that should be inherited from `OptionGroup` class
8699
:param help: Group help or None for empty help
100+
:param cls: Option group class that should be inherited from `OptionGroup` class
87101
:param attrs: Additional parameters of option group class
88102
"""
89103

click_option_group/_helpers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

3-
import typing as ty
3+
from typing import List, Tuple
4+
45
import collections.abc as abc
56
import random
67
import string
@@ -11,7 +12,7 @@
1112
FAKE_OPT_NAME_LEN = 30
1213

1314

14-
def get_callback_and_params(func) -> ty.Tuple[abc.Callable, ty.List[click.Option]]:
15+
def get_callback_and_params(func) -> Tuple[abc.Callable, List[click.Option]]:
1516
"""Returns callback function and its parameters list
1617
1718
:param func: decorated function or click Command

click_option_group/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# -*- coding: utf-8 -*-
22

3-
__version__ = '0.5.1'
3+
__version__ = '0.5.2'

0 commit comments

Comments
 (0)