-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
Copy pathdefault_group.py
199 lines (167 loc) · 6.8 KB
/
default_group.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""``DefaultGroup`` allows a subcommand to act as the main command.
In particular, this class is what allows ``manim`` to act as ``manim render``.
.. note::
This is a vendored version of https://github.com/click-contrib/click-default-group/
under the BSD 3-Clause "New" or "Revised" License.
This library isn't used as a dependency, as we need to inherit from
:class:`cloup.Group` instead of :class:`click.Group`.
"""
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Any, Callable
import cloup
from manim.utils.deprecation import deprecated
__all__ = ["DefaultGroup"]
if TYPE_CHECKING:
from click import Command, Context
class DefaultGroup(cloup.Group):
"""Invokes a subcommand marked with ``default=True`` if any subcommand is not
chosen.
Parameters
----------
*args
Positional arguments to forward to :class:`cloup.Group`.
**kwargs
Keyword arguments to forward to :class:`cloup.Group`. The keyword
``ignore_unknown_options`` must be set to ``False``.
Attributes
----------
default_cmd_name : str | None
The name of the default command, if specified through the ``default``
keyword argument. Otherwise, this is set to ``None``.
default_if_no_args : bool
Whether to include or not the default command, if no command arguments
are supplied. This can be specified through the ``default_if_no_args``
keyword argument. Default is ``False``.
"""
def __init__(self, *args: Any, **kwargs: Any):
# To resolve as the default command.
if not kwargs.get("ignore_unknown_options", True):
raise ValueError("Default group accepts unknown options")
self.ignore_unknown_options = True
self.default_cmd_name: str | None = kwargs.pop("default", None)
self.default_if_no_args: bool = kwargs.pop("default_if_no_args", False)
super().__init__(*args, **kwargs)
def set_default_command(self, command: Command) -> None:
"""Sets a command function as the default command.
Parameters
----------
command
The command to set as default.
"""
cmd_name = command.name
self.add_command(command)
self.default_cmd_name = cmd_name
def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
"""Parses the list of ``args`` by forwarding it to
:meth:`cloup.Group.parse_args`. Before doing so, if
:attr:`default_if_no_args` is set to ``True`` and ``args`` is empty,
this function appends to it the name of the default command specified
by :attr:`default_cmd_name`.
Parameters
----------
ctx
The Click context.
args
A list of arguments. If it's empty and :attr:`default_if_no_args`
is ``True``, append the name of the default command to it.
Returns
-------
list[str]
The parsed arguments.
"""
if not args and self.default_if_no_args and self.default_cmd_name:
args.insert(0, self.default_cmd_name)
parsed_args: list[str] = super().parse_args(ctx, args)
return parsed_args
def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
"""Get a command function by its name, by forwarding the arguments to
:meth:`cloup.Group.get_command`. If ``cmd_name`` does not match any of
the command names in :attr:`commands`, attempt to get the default command
instead.
Parameters
----------
ctx
The Click context.
cmd_name
The name of the command to get.
Returns
-------
:class:`click.Command` | None
The command, if found. Otherwise, ``None``.
"""
if cmd_name not in self.commands and self.default_cmd_name:
# No command name matched.
ctx.meta["arg0"] = cmd_name
cmd_name = self.default_cmd_name
return super().get_command(ctx, cmd_name)
def resolve_command(
self, ctx: Context, args: list[str]
) -> tuple[str | None, Command | None, list[str]]:
"""Given a list of ``args`` given by a CLI, find a command which
matches the first element, and return its name (``cmd_name``), the
command function itself (``cmd``) and the rest of the arguments which
shall be passed to the function (``cmd_args``). If not found, return
``None``, ``None`` and the rest of the arguments.
After resolving the command, if the Click context given by ``ctx``
contains an ``arg0`` attribute in its :attr:`click.Context.meta`
dictionary, insert it as the first element of the returned
``cmd_args``.
Parameters
----------
ctx
The Click context.
cmd_name
The name of the command to get.
Returns
-------
cmd_name : str | None
The command name, if found. Otherwise, ``None``.
cmd : :class:`click.Command` | None
The command, if found. Otherwise, ``None``.
cmd_args : list[str]
The rest of the arguments to be passed to ``cmd``.
"""
cmd_name, cmd, args = super().resolve_command(ctx, args)
if "arg0" in ctx.meta:
args.insert(0, ctx.meta["arg0"])
if cmd is not None:
cmd_name = cmd.name
return cmd_name, cmd, args
@deprecated
def command(
self, *args: Any, **kwargs: Any
) -> Callable[[Callable[..., object]], Command]:
"""Return a decorator which converts any function into the default
subcommand for this :class:`DefaultGroup`.
.. warning::
This method is deprecated. Use the ``default`` parameter of
:class:`DefaultGroup` or :meth:`set_default_command` instead.
Parameters
----------
*args
Positional arguments to pass to :meth:`cloup.Group.command`.
**kwargs
Keyword arguments to pass to :meth:`cloup.Group.command`.
Returns
-------
Callable[[Callable[..., object]], click.Command]
A decorator which transforms its input into this
:class:`DefaultGroup`'s default subcommand.
"""
default = kwargs.pop("default", False)
decorator: Callable[[Callable[..., object]], Command] = super().command(
*args, **kwargs
)
if not default:
return decorator
warnings.warn(
"Use default param of DefaultGroup or set_default_command() instead",
DeprecationWarning,
stacklevel=1,
)
def _decorator(f: Callable) -> Command:
cmd = decorator(f)
self.set_default_command(cmd)
return cmd
return _decorator