|
27 | 27 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
28 | 28 | # OR OTHER DEALINGS IN THE SOFTWARE.
|
29 | 29 | #
|
| 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 | +# |
30 | 60 |
|
31 | 61 | # stdlib
|
32 |
| -from typing import Any, Callable |
| 62 | +from typing import Any, Callable, List, Optional, cast |
33 | 63 |
|
34 | 64 | # 3rd party
|
35 | 65 | import click
|
36 |
| -from click import Context, Option |
| 66 | +from click import Context, Option, OptionParser |
37 | 67 |
|
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 | + ] |
39 | 79 |
|
40 | 80 |
|
41 | 81 | def verbose_option(help_text: str = "Show verbose output.") -> Callable:
|
@@ -78,7 +118,7 @@ def version_option(callback: Callable[[Context, Option, int], Any]) -> Callable:
|
78 | 118 | expose_value=False,
|
79 | 119 | is_eager=True,
|
80 | 120 | help="Show the version and exit.",
|
81 |
| - callback=callback, # type: ignore |
| 121 | + callback=cast(Callback, callback), |
82 | 122 | )
|
83 | 123 |
|
84 | 124 |
|
@@ -138,3 +178,98 @@ def no_pager_option(help_text="Disable the output pager.") -> Callable:
|
138 | 178 | default=False,
|
139 | 179 | help=help_text,
|
140 | 180 | )
|
| 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 |
0 commit comments