|
35 | 35 | from . import oscerr |
36 | 36 | from . import output |
37 | 37 | from . import store as osc_store |
| 38 | +from .commandline_common import * |
38 | 39 | from .core import * |
39 | 40 | from .grabber import OscFileGrabber |
40 | 41 | from .meter import create_text_meter |
|
44 | 45 | from .util.helper import _html_escape, format_table |
45 | 46 |
|
46 | 47 |
|
47 | | -# python3.6 requires reading sys.real_prefix to detect virtualenv |
48 | | -IN_VENV = getattr(sys, "real_prefix", sys.base_prefix) != sys.prefix |
49 | | - |
50 | | - |
51 | | -class Command: |
52 | | - #: Name of the command as used in the argument parser. |
53 | | - name: str = None |
54 | | - |
55 | | - #: Optional aliases to the command. |
56 | | - aliases: List[str] = [] |
57 | | - |
58 | | - #: Whether the command is hidden from help. |
59 | | - #: Defaults to ``False``. |
60 | | - hidden: bool = False |
61 | | - |
62 | | - #: Name of the parent command class. |
63 | | - #: Can be prefixed if the parent comes from a different location, |
64 | | - #: for example ``osc.commands.<ClassName>`` when extending osc command with a plugin. |
65 | | - #: See ``OscMainCommand.MODULES`` for available prefixes. |
66 | | - parent: str = None |
67 | | - |
68 | | - def __init__(self, full_name, parent=None): |
69 | | - self.full_name = full_name |
70 | | - self.parent = parent |
71 | | - self.subparsers = None |
72 | | - |
73 | | - if not self.name: |
74 | | - raise ValueError(f"Command '{self.full_name}' has no 'name' set") |
75 | | - |
76 | | - if parent: |
77 | | - self.parser = self.parent.subparsers.add_parser( |
78 | | - self.name, |
79 | | - aliases=self.aliases, |
80 | | - help=self.get_help(), |
81 | | - description=self.get_description(), |
82 | | - formatter_class=cmdln.HelpFormatter, |
83 | | - conflict_handler="resolve", |
84 | | - prog=f"{self.main_command.name} [global opts] {self.name}", |
85 | | - ) |
86 | | - self.parser.set_defaults(_selected_command=self) |
87 | | - else: |
88 | | - self.parser = argparse.ArgumentParser( |
89 | | - description=self.get_description(), |
90 | | - formatter_class=cmdln.HelpFormatter, |
91 | | - usage="%(prog)s [global opts] <command> [--help] [opts] [args]", |
92 | | - ) |
93 | | - |
94 | | - # traverse the parent commands and add their options to the current command |
95 | | - commands = [] |
96 | | - cmd = self |
97 | | - while cmd: |
98 | | - commands.append(cmd) |
99 | | - cmd = cmd.parent |
100 | | - # iterating backwards to give the command's options a priority over parent/global options |
101 | | - for cmd in reversed(commands): |
102 | | - cmd.init_arguments() |
103 | | - |
104 | | - def __repr__(self): |
105 | | - return f"<osc plugin {self.full_name} at {self.__hash__():#x}>" |
106 | | - |
107 | | - def get_help(self): |
108 | | - """ |
109 | | - Return the help text of the command. |
110 | | - The first line of the docstring is returned by default. |
111 | | - """ |
112 | | - if self.hidden: |
113 | | - return argparse.SUPPRESS |
114 | | - |
115 | | - if not self.__doc__: |
116 | | - return "" |
117 | | - |
118 | | - help_lines = self.__doc__.strip().splitlines() |
119 | | - |
120 | | - if not help_lines: |
121 | | - return "" |
122 | | - |
123 | | - return help_lines[0] |
124 | | - |
125 | | - def get_description(self): |
126 | | - """ |
127 | | - Return the description of the command. |
128 | | - The docstring without the first line is returned by default. |
129 | | - """ |
130 | | - if not self.__doc__: |
131 | | - return "" |
132 | | - |
133 | | - help_lines = self.__doc__.strip().splitlines() |
134 | | - |
135 | | - if not help_lines: |
136 | | - return "" |
137 | | - |
138 | | - # skip the first line that contains help text |
139 | | - help_lines.pop(0) |
140 | | - |
141 | | - # remove any leading empty lines |
142 | | - while help_lines and not help_lines[0]: |
143 | | - help_lines.pop(0) |
144 | | - |
145 | | - result = "\n".join(help_lines) |
146 | | - result = textwrap.dedent(result) |
147 | | - return result |
148 | | - |
149 | | - @property |
150 | | - def main_command(self): |
151 | | - """ |
152 | | - Return reference to the main command that represents the executable |
153 | | - and contains the main instance of ArgumentParser. |
154 | | - """ |
155 | | - if not self.parent: |
156 | | - return self |
157 | | - return self.parent.main_command |
158 | | - |
159 | | - def add_argument(self, *args, **kwargs): |
160 | | - """ |
161 | | - Add a new argument to the command's argument parser. |
162 | | - See `argparse <https://docs.python.org/3/library/argparse.html>`_ documentation for allowed parameters. |
163 | | - """ |
164 | | - cmd = self |
165 | | - |
166 | | - # Let's inspect if the caller was init_arguments() method. |
167 | | - # In such case use the "parser" argument if specified. |
168 | | - frame_1 = inspect.currentframe().f_back |
169 | | - frame_1_info = inspect.getframeinfo(frame_1) |
170 | | - frame_2 = frame_1.f_back |
171 | | - frame_2_info = inspect.getframeinfo(frame_2) |
172 | | - if (frame_1_info.function, frame_2_info.function) == ("init_arguments", "__init__"): |
173 | | - # this method was called from init_arguments() that was called from __init__ |
174 | | - # let's extract the command class from the 2nd frame and ad arguments there |
175 | | - cmd = frame_2.f_locals["self"] |
176 | | - |
177 | | - # suppress global options from command help |
178 | | - if cmd != self and not self.parent: |
179 | | - kwargs["help"] = argparse.SUPPRESS |
180 | | - |
181 | | - # We're adding hidden options from parent commands to their subcommands to allow |
182 | | - # option intermixing. For all such added hidden options we need to suppress their |
183 | | - # defaults because they would override any option set in the parent command. |
184 | | - if cmd != self: |
185 | | - kwargs["default"] = argparse.SUPPRESS |
186 | | - |
187 | | - cmd.parser.add_argument(*args, **kwargs) |
188 | | - |
189 | | - def init_arguments(self): |
190 | | - """ |
191 | | - Override to add arguments to the argument parser. |
192 | | -
|
193 | | - .. note:: |
194 | | - Make sure you're adding arguments only by calling ``self.add_argument()``. |
195 | | - Using ``self.parser.add_argument()`` directly is not recommended |
196 | | - because it disables argument intermixing. |
197 | | - """ |
198 | | - |
199 | | - def run(self, args): |
200 | | - """ |
201 | | - Override to implement the command functionality. |
202 | | -
|
203 | | - .. note:: |
204 | | - ``args.positional_args`` is a list containing any unknown (unparsed) positional arguments. |
205 | | -
|
206 | | - .. note:: |
207 | | - Consider moving any reusable code into a library, |
208 | | - leaving the command-line code only a thin wrapper on top of it. |
209 | | -
|
210 | | - If the code is generic enough, it should be added to osc directly. |
211 | | - In such case don't hesitate to open an `issue <https://github.com/openSUSE/osc/issues>`_. |
212 | | - """ |
213 | | - raise NotImplementedError() |
214 | | - |
215 | | - def register(self, command_class, command_full_name): |
216 | | - if not self.subparsers: |
217 | | - # instantiate subparsers on first use |
218 | | - self.subparsers = self.parser.add_subparsers(dest="command", title="commands") |
219 | | - |
220 | | - # Check for parser conflicts. |
221 | | - # This is how Python 3.11+ behaves by default. |
222 | | - if command_class.name in self.subparsers._name_parser_map: |
223 | | - raise argparse.ArgumentError(self.subparsers, f"conflicting subparser: {command_class.name}") |
224 | | - for alias in command_class.aliases: |
225 | | - if alias in self.subparsers._name_parser_map: |
226 | | - raise argparse.ArgumentError(self.subparsers, f"conflicting subparser alias: {alias}") |
227 | | - |
228 | | - command = command_class(command_full_name, parent=self) |
229 | | - return command |
230 | | - |
231 | | - |
232 | | -class MainCommand(Command): |
233 | | - MODULES = () |
234 | | - |
235 | | - def __init__(self): |
236 | | - super().__init__(self.__class__.__name__) |
237 | | - self.command_classes = {} |
238 | | - self.download_progress = None |
239 | | - |
240 | | - def post_parse_args(self, args): |
241 | | - pass |
242 | | - |
243 | | - def run(self, args): |
244 | | - cmd = getattr(args, "_selected_command", None) |
245 | | - if not cmd: |
246 | | - self.parser.error("Please specify a command") |
247 | | - self.post_parse_args(args) |
248 | | - return cmd.run(args) |
249 | | - |
250 | | - def load_command(self, cls, module_prefix): |
251 | | - mod_cls_name = f"{module_prefix}.{cls.__name__}" |
252 | | - parent_name = getattr(cls, "parent", None) |
253 | | - if parent_name: |
254 | | - # allow relative references to classes in the the same module/directory |
255 | | - if "." not in parent_name: |
256 | | - parent_name = f"{module_prefix}.{parent_name}" |
257 | | - try: |
258 | | - parent = self.main_command.command_classes[parent_name] |
259 | | - except KeyError: |
260 | | - msg = f"Failed to load command class '{mod_cls_name}' because it references parent '{parent_name}' that doesn't exist" |
261 | | - print(msg, file=sys.stderr) |
262 | | - return None |
263 | | - cmd = parent.register(cls, mod_cls_name) |
264 | | - else: |
265 | | - cmd = self.main_command.register(cls, mod_cls_name) |
266 | | - |
267 | | - cmd.full_name = mod_cls_name |
268 | | - self.main_command.command_classes[mod_cls_name] = cmd |
269 | | - return cmd |
270 | | - |
271 | | - def load_commands(self): |
272 | | - if IN_VENV: |
273 | | - output.print_msg("Running in virtual environment, skipping loading plugins installed outside the virtual environment.", print_to="debug") |
274 | | - |
275 | | - for module_prefix, module_path in self.MODULES: |
276 | | - module_path = os.path.expanduser(module_path) |
277 | | - |
278 | | - # some plugins have their modules installed next to them instead of site-packages |
279 | | - if module_path not in sys.path: |
280 | | - sys.path.append(module_path) |
281 | | - |
282 | | - for loader, module_name, _ in pkgutil.iter_modules(path=[module_path]): |
283 | | - full_name = f"{module_prefix}.{module_name}" |
284 | | - spec = loader.find_spec(full_name) |
285 | | - mod = importlib.util.module_from_spec(spec) |
286 | | - try: |
287 | | - spec.loader.exec_module(mod) |
288 | | - except Exception as e: # pylint: disable=broad-except |
289 | | - msg = f"Failed to load commands from module '{full_name}': {e}" |
290 | | - print(msg, file=sys.stderr) |
291 | | - continue |
292 | | - for name in dir(mod): |
293 | | - if name.startswith("_"): |
294 | | - continue |
295 | | - cls = getattr(mod, name) |
296 | | - if not inspect.isclass(cls): |
297 | | - continue |
298 | | - if not issubclass(cls, Command): |
299 | | - continue |
300 | | - if cls.__module__ != full_name: |
301 | | - # skip classes that weren't defined directly in the loaded plugin module |
302 | | - continue |
303 | | - self.load_command(cls, module_prefix) |
304 | | - |
305 | | - def parse_args(self, *args, **kwargs): |
306 | | - namespace, unknown_args = self.parser.parse_known_args(*args, **kwargs) |
307 | | - |
308 | | - unrecognized = [i for i in unknown_args if i.startswith("-")] |
309 | | - if unrecognized: |
310 | | - self.parser.error(f"unrecognized arguments: " + " ".join(unrecognized)) |
311 | | - |
312 | | - namespace.positional_args = list(unknown_args) |
313 | | - return namespace |
314 | | - |
315 | | - |
316 | 48 | class OscCommand(Command): |
317 | 49 | """ |
318 | 50 | Inherit from this class to create new commands. |
|
0 commit comments