Skip to content

Commit 541b6da

Browse files
committed
Improve git-obs startup time by loading less python modules
1 parent a901206 commit 541b6da

File tree

3 files changed

+288
-273
lines changed

3 files changed

+288
-273
lines changed

osc/commandline.py

Lines changed: 1 addition & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from . import oscerr
3636
from . import output
3737
from . import store as osc_store
38+
from .commandline_common import *
3839
from .core import *
3940
from .grabber import OscFileGrabber
4041
from .meter import create_text_meter
@@ -44,275 +45,6 @@
4445
from .util.helper import _html_escape, format_table
4546

4647

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-
31648
class OscCommand(Command):
31749
"""
31850
Inherit from this class to create new commands.

0 commit comments

Comments
 (0)