Skip to content

Commit 4eb3e26

Browse files
committed
Add "MultiValueOption" to options.py
1 parent a7da947 commit 4eb3e26

File tree

4 files changed

+183
-13
lines changed

4 files changed

+183
-13
lines changed

consolekit/_types.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3
2+
#
3+
# _types.py
4+
#
5+
# Copyright © 2020 Dominic Davis-Foster <[email protected]>
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
21+
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
22+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23+
# OR OTHER DEALINGS IN THE SOFTWARE.
24+
#
25+
26+
# stdlib
27+
from typing import Any, Callable, Optional, Tuple, Union
28+
29+
# 3rd party
30+
from click import Context, Parameter, ParamType
31+
32+
_ConvertibleType = Union[
33+
type,
34+
ParamType,
35+
Tuple[Union[type, ParamType], ...],
36+
Callable[[str], Any],
37+
Callable[[Optional[str]], Any]
38+
]
39+
40+
Callback = Callable[[Context, Parameter, str], Any]

consolekit/input.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
from click.termui import _build_prompt, hidden_prompt_func
8484
from click.types import ParamType, Path, convert_type
8585

86+
# this package
87+
from consolekit._types import _ConvertibleType
88+
8689
__all__ = ["prompt", "confirm", "stderr_input", "choice"]
8790

8891
if not bool(getattr(sys, "ps1", sys.flags.interactive)): # pragma: no cover
@@ -97,14 +100,6 @@
97100
# Attribute error on PyPy, ImportError on Windows etc.
98101
pass
99102

100-
_ConvertibleType = Union[
101-
type,
102-
ParamType,
103-
Tuple[Union[type, ParamType], ...],
104-
Callable[[str], Any],
105-
Callable[[Optional[str]], Any]
106-
]
107-
108103

109104
def prompt(
110105
text: str,

consolekit/options.py

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,55 @@
2727
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
2828
# OR OTHER DEALINGS IN THE SOFTWARE.
2929
#
30+
# MultiValueOption based on https://stackoverflow.com/a/48394004
31+
# Copyright (c) 2018 Stephen Rauch <https://stackoverflow.com/users/7311767/stephen-rauch>
32+
# CC BY-SA 3.0
33+
#
34+
# MultiValueOption based on https://github.com/pallets/click
35+
# Copyright 2014 Pallets
36+
# | Redistribution and use in source and binary forms, with or without modification,
37+
# | are permitted provided that the following conditions are met:
38+
# |
39+
# | * Redistributions of source code must retain the above copyright notice,
40+
# | this list of conditions and the following disclaimer.
41+
# | * Redistributions in binary form must reproduce the above copyright notice,
42+
# | this list of conditions and the following disclaimer in the documentation
43+
# | and/or other materials provided with the distribution.
44+
# | * Neither the name of the copyright holder nor the names of its contributors
45+
# | may be used to endorse or promote products derived from this software without
46+
# | specific prior written permission.
47+
# |
48+
# | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
49+
# | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
50+
# | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
51+
# | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
52+
# | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
53+
# | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
54+
# | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
55+
# | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
56+
# | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
57+
# | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
58+
# | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
59+
#
3060

3161
# stdlib
32-
from typing import Any, Callable
62+
from typing import Any, Callable, List, Optional, cast
3363

3464
# 3rd party
3565
import click
36-
from click import Context, Option
66+
from click import Context, Option, OptionParser
3767

38-
__all__ = ["verbose_option", "version_option", "colour_option", "force_option", "no_pager_option"]
68+
# this package
69+
from consolekit._types import Callback, _ConvertibleType
70+
71+
__all__ = [
72+
"verbose_option",
73+
"version_option",
74+
"colour_option",
75+
"force_option",
76+
"no_pager_option",
77+
"MultiValueOption",
78+
]
3979

4080

4181
def verbose_option(help_text: str = "Show verbose output.") -> Callable:
@@ -78,7 +118,7 @@ def version_option(callback: Callable[[Context, Option, int], Any]) -> Callable:
78118
expose_value=False,
79119
is_eager=True,
80120
help="Show the version and exit.",
81-
callback=callback, # type: ignore
121+
callback=cast(Callback, callback),
82122
)
83123

84124

@@ -138,3 +178,98 @@ def no_pager_option(help_text="Disable the output pager.") -> Callable:
138178
default=False,
139179
help=help_text,
140180
)
181+
182+
183+
class MultiValueOption(click.Option):
184+
"""
185+
Subclass of :class:`click.Option` that behaves like argparse's ``nargs='+'``.
186+
187+
:param param_decls: The parameter declarations for this option or argument.
188+
This is a list of flags or argument names.
189+
:param show_default: Controls if the default value should be shown on the help page.
190+
Normally, defaults are not shown.
191+
If this value is a string, it shows the string instead of the value.
192+
This is particularly useful for dynamic options.
193+
:param help: The help string.
194+
:param hidden: Hide this option from help outputs.
195+
:param type: The type that should be used. Either a :class:`click.ParamType` or a Python type.
196+
The later is converted into the former automatically if supported.
197+
:param required: Controls whether this is optional.
198+
:param default: The default value if omitted.
199+
This can also be a callable, in which case it's invoked when the default is needed without any arguments.
200+
:param callback: A callback that should be executed after the parameter was matched.
201+
This is called as ``fn(ctx, param, value)`` and needs to return the value.
202+
:param metavar: How the value is represented in the help page.
203+
:param expose_value: If :py:obj:`True` then the value is passed onwards to the command callback
204+
and stored on the context, otherwise it's skipped.
205+
:param is_eager: Eager values are processed before non eager ones.
206+
207+
.. versionadded:: 0.6.0
208+
"""
209+
210+
def __init__(
211+
self,
212+
param_decls: Optional[List[str]] = None,
213+
show_default: bool = False,
214+
help: Optional[str] = None,
215+
hidden: bool = False,
216+
type: Optional[_ConvertibleType] = None,
217+
required: bool = False,
218+
default: Optional[Any] = None,
219+
callback: Optional[Callback] = None,
220+
metavar: Optional[str] = None,
221+
expose_value: bool = True,
222+
is_eager: bool = False,
223+
):
224+
225+
super().__init__(
226+
show_default=show_default,
227+
help=help,
228+
hidden=hidden,
229+
param_decls=param_decls,
230+
type=type,
231+
required=required,
232+
default=default,
233+
callback=callback,
234+
metavar=metavar,
235+
expose_value=expose_value,
236+
is_eager=is_eager,
237+
)
238+
self._previous_parser_process: Optional[Callable] = None
239+
self._eat_all_parser: Optional[click.parser.Option] = None
240+
241+
def add_to_parser(self, parser: OptionParser, ctx: Context):
242+
"""
243+
244+
:param parser:
245+
:param ctx:
246+
"""
247+
248+
def parser_process(value, state):
249+
# method to hook to the parser.process
250+
done = False
251+
value = [value]
252+
# grab everything up to the next option
253+
while state.rargs and not done:
254+
for prefix in self._eat_all_parser.prefixes: # type: ignore
255+
if state.rargs[0].startswith(prefix):
256+
done = True
257+
if not done:
258+
value.append(state.rargs.pop(0))
259+
260+
value = tuple(value)
261+
262+
# call the actual process
263+
self._previous_parser_process(value, state) # type: ignore
264+
265+
retval = super().add_to_parser(parser, ctx)
266+
267+
for name in self.opts:
268+
our_parser: Optional[click.parser.Option] = parser._long_opt.get(name) or parser._short_opt.get(name)
269+
if our_parser:
270+
self._eat_all_parser = our_parser
271+
self._previous_parser_process = our_parser.process
272+
our_parser.process = parser_process # type: ignore
273+
break
274+
275+
return retval

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ commands =
135135
[flake8]
136136
max-line-length = 120
137137
select = E301 E303 E304 E305 E306 E502 W291 W293 W391 E226 E225 E241 E231 W292 E265 E111 E112 E113 E121 E122 E125 E127 E128 E129 E131 E133 E201 E202 E203 E211 E222 E223 E224 E225 E227 E228 E242 E251 E261 E262 E271 E272 E402 E703 E711 E712 E713 E714 E721 W504 E302 YTT101 YTT102 YTT103 YTT201 YTT202 YTT203 YTT204 YTT301 YTT302 YTT303 STRFTIME001 STRFTIME002 SXL001 PT001 PT002 PT003 PT005 PT006 PT007 PT008 PT009 PT010 PT011 PT012 PT013 PT014 PT015 PT016 PT017 PT018 PT019 PT020 PT021 RST201 RST202 RST203 RST204 RST205 RST206 RST207 RST208 RST210 RST211 RST212 RST213 RST214 RST215 RST216 RST217 RST218 RST219 RST299 RST301 RST302 RST303 RST304 RST305 RST306 RST399 RST401 RST499 RST900 RST901 RST902 RST903 Q001 Q002 Q003 A001 A002 A003 TYP001 TYP002 TYP003 TYP004 TYP005 TYP006 Y001,Y002 Y003 Y004 Y005 Y006 Y007 Y008 Y009 Y010 Y011 Y012 Y013 Y014 Y015 Y090 Y091 D100 D101 D102 D103 D104 D106 D201 D204 D207 D208 D209 D210 D211 D212 D213 D214 D215 D300 D301 D400 D402 D403 D404 D415 D417 DALL000
138-
exclude = .git,__pycache__,doc-source,old,build,dist,make_conda_recipe.py,__pkginfo__.py,setup.py
138+
exclude = .git,__pycache__,doc-source,old,build,dist,make_conda_recipe.py,__pkginfo__.py,setup.py,.tox,venv
139139
rst-directives =
140140
TODO
141141
envvar

0 commit comments

Comments
 (0)