-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
subcommand.py
182 lines (139 loc) · 5.69 KB
/
subcommand.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
#-----------------------------------------------------------------------------
# Copyright (c) 2012 - 2024, Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
''' Provides a base class for defining subcommands of the Bokeh command
line application.
'''
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations
import logging # isort:skip
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
# Standard library imports
from abc import ABCMeta, abstractmethod
from argparse import ArgumentParser, Namespace
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Literal,
Sequence,
Union,
)
# Bokeh imports
from ..util.dataclasses import (
NotRequired,
Unspecified,
dataclass,
entries,
)
if TYPE_CHECKING:
from typing_extensions import TypeAlias
#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------
__all__ = (
'Subcommand',
)
#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------
@dataclass
class Argument:
action: NotRequired[Literal["store", "store_const", "store_true", "append", "append_const", "count", "help", "version", "extend"]] = Unspecified
nargs: NotRequired[int | Literal["?", "*", "+", "..."]] = Unspecified
const: NotRequired[Any] = Unspecified
default: NotRequired[Any] = Unspecified
type: NotRequired[type[Any]] = Unspecified
choices: NotRequired[Sequence[Any]] = Unspecified
required: NotRequired[bool] = Unspecified
help: NotRequired[str] = Unspecified
metavar: NotRequired[str] = Unspecified
Arg: TypeAlias = tuple[Union[str, tuple[str, ...]], Argument]
Args: TypeAlias = tuple[Arg, ...]
class Subcommand(metaclass=ABCMeta):
''' Abstract base class for subcommands
Subclasses should implement an ``invoke(self, args)`` method that accepts
a set of argparse processed arguments as input.
Subclasses should also define the following class attributes:
* ``name`` a name for this subcommand
* ``help`` a help string for argparse to use for this subcommand
* ``args`` the parameters to pass to ``parser.add_argument``
The format of the ``args`` should be a sequence of tuples of the form:
.. code-block:: python
('argname', Argument(
metavar='ARGNAME',
nargs='+',
))
Example:
A simple subcommand "foo" might look like this:
.. code-block:: python
class Foo(Subcommand):
name = "foo"
help = "performs the Foo action"
args = (
('--yell', Argument(
action='store_true',
help="Make it loud",
)),
)
def invoke(self, args):
if args.yell:
print("FOO!")
else:
print("foo")
Then executing ``bokeh foo --yell`` would print ``FOO!`` at the console.
'''
name: ClassVar[str]
help: ClassVar[str]
args: ClassVar[Args] = ()
def __init__(self, parser: ArgumentParser) -> None:
''' Initialize the subcommand with its parser
Args:
parser (Parser) : an Argparse ``Parser`` instance to configure
with the args for this subcommand.
This method will automatically add all the arguments described in
``self.args``. Subclasses can perform any additional customizations
on ``self.parser``.
'''
self.parser = parser
for arg in self.args:
flags, spec = arg
if not isinstance(flags, tuple):
flags = (flags,)
if not isinstance(spec, dict):
kwargs = dict(entries(spec))
else:
# NOTE: allow dict for run time backwards compatibility, but don't include in types
kwargs = spec
self.parser.add_argument(*flags, **kwargs)
@abstractmethod
def invoke(self, args: Namespace) -> bool | None:
''' Takes over main program flow to perform the subcommand.
*This method must be implemented by subclasses.*
subclassed overwritten methods return different types:
bool: Build
None: FileOutput (subclassed by HTML, SVG and JSON. PNG overwrites FileOutput.invoke method), Info, Init, \
Sampledata, Secret, Serve, Static
Args:
args (argparse.Namespace) : command line arguments for the subcommand to parse
Raises:
NotImplementedError
'''
raise NotImplementedError("implement invoke()")
#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------