Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package osc for openSUSE:Factory checked in at 2023-04-03 21:49:23 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/osc (Old) and /work/SRC/openSUSE:Factory/.osc.new.9019 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "osc" Mon Apr 3 21:49:23 2023 rev:174 rq:1077044 version:1.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/osc/osc.changes 2023-03-17 17:05:32.066082506 +0100 +++ /work/SRC/openSUSE:Factory/.osc.new.9019/osc.changes 2023-04-03 21:49:25.104879589 +0200 @@ -1,0 +2,15 @@ +Mon Apr 3 11:58:12 UTC 2023 - Daniel Mach <daniel.m...@suse.com> + +- Update to 1.1.0 + - Command-line: + - New class-based commands + - Sort commands before printing help + - No longer read plugins from /var/lib/osc-plugins + - Configuration: + - Do not error out on setting oscrc permissions if the file is owned by another user + - Library: + - Restore 'include_request_from_project' conf option functionality + - Simplify how babysitter works with options and config + - Prefer f-strings over c-style string expansion + +------------------------------------------------------------------- Old: ---- osc-1.0.1.tar.gz New: ---- osc-1.1.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ osc.spec ++++++ --- /var/tmp/diff_new_pack.b2pIoh/_old 2023-04-03 21:49:25.868884003 +0200 +++ /var/tmp/diff_new_pack.b2pIoh/_new 2023-04-03 21:49:25.872884026 +0200 @@ -49,7 +49,7 @@ %endif Name: osc -Version: 1.0.1 +Version: 1.1.0 Release: 0 Summary: Command-line client for the Open Build Service License: GPL-2.0-or-later ++++++ PKGBUILD ++++++ --- /var/tmp/diff_new_pack.b2pIoh/_old 2023-04-03 21:49:25.908884233 +0200 +++ /var/tmp/diff_new_pack.b2pIoh/_new 2023-04-03 21:49:25.912884257 +0200 @@ -1,5 +1,5 @@ pkgname=osc -pkgver=1.0.1 +pkgver=1.1.0 pkgrel=0 pkgdesc="Command-line client for the Open Build Service" arch=('x86_64') ++++++ debian.changelog ++++++ --- /var/tmp/diff_new_pack.b2pIoh/_old 2023-04-03 21:49:25.960884534 +0200 +++ /var/tmp/diff_new_pack.b2pIoh/_new 2023-04-03 21:49:25.964884557 +0200 @@ -1,2 +1,2 @@ -osc (1.0.1-0) unstable; urgency=low +osc (1.1.0-0) unstable; urgency=low ++++++ osc-1.0.1.tar.gz -> osc-1.1.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/NEWS new/osc-1.1.0/NEWS --- old/osc-1.0.1/NEWS 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/NEWS 2023-04-03 13:45:36.000000000 +0200 @@ -1,3 +1,15 @@ +- 1.1.0 + - Command-line: + - New class-based commands + - Sort commands before printing help + - No longer read plugins from /var/lib/osc-plugins + - Configuration: + - Do not error out on setting oscrc permissions if the file is owned by another user + - Library: + - Restore 'include_request_from_project' conf option functionality + - Simplify how babysitter works with options and config + - Prefer f-strings over c-style string expansion + - 1.0.1 - Configuration: - Fix a cut&paste error in setting 'disable_hdrmd5_check' config option diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/api/osc.commandline.rst new/osc-1.1.0/doc/api/osc.commandline.rst --- old/osc-1.0.1/doc/api/osc.commandline.rst 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/doc/api/osc.commandline.rst 2023-04-03 13:45:36.000000000 +0200 @@ -2,7 +2,16 @@ =========== -The `osc.commandline` module provides argument parsing functionality to osc plugins. +The ``osc.commandline`` module provides functionality for creating osc command-line plugins. + + +.. autoclass:: osc.commandline.OscCommand + :inherited-members: + :members: + + +.. autoclass:: osc.commandline.OscMainCommand + :members: main .. automodule:: osc.commandline diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/index.rst new/osc-1.1.0/doc/index.rst --- old/osc-1.0.1/doc/index.rst 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/doc/index.rst 2023-04-03 13:45:36.000000000 +0200 @@ -20,6 +20,7 @@ :maxdepth: 2 api/modules + plugins/index diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/plugins/index.rst new/osc-1.1.0/doc/plugins/index.rst --- old/osc-1.0.1/doc/plugins/index.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.1.0/doc/plugins/index.rst 2023-04-03 13:45:36.000000000 +0200 @@ -0,0 +1,54 @@ +Extending osc with plugins +========================== + + +.. note:: + New in osc 1.1.0 + + +This is a simple tutorial. +More details can be found in the :py:class:`osc.commandline.OscCommand` reference. + + +Steps +----- +1. First, we choose a location where to put the plugin + + .. include:: plugin_locations.rst + +2. Then we pick a file name + + - The file should contain a single command and its name should correspond with the command name. + - The file name should be prefixed with parent command(s) (only if applicable). + - Example: Adding ``list`` subcommand to ``osc request`` -> ``request_list.py``. + +3. And then we write a class that inherits from :py:class:`osc.commandline.OscCommand` and implements our command. + + - The class name should also correspond with the command name incl. the parent prefix. + - Examples follow... + + + + +A simple command +---------------- + +``simple.py`` + + .. literalinclude:: simple.py + + +Command with subcommands +------------------------ + +``request.py`` + + .. literalinclude:: request.py + +``request_list.py`` + + .. literalinclude:: request_list.py + +``request_accept.py`` + + .. literalinclude:: request_accept.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/plugins/plugin_locations.rst new/osc-1.1.0/doc/plugins/plugin_locations.rst --- old/osc-1.0.1/doc/plugins/plugin_locations.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.1.0/doc/plugins/plugin_locations.rst 2023-04-03 13:45:36.000000000 +0200 @@ -0,0 +1,5 @@ + - The directory from where the ``osc.commands`` module gets loaded. + - /usr/lib/osc-plugins + - /usr/local/lib/osc-plugins + - ~/.local/lib/osc-plugins + - ~/.osc-plugins \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/plugins/request.py new/osc-1.1.0/doc/plugins/request.py --- old/osc-1.0.1/doc/plugins/request.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.1.0/doc/plugins/request.py 2023-04-03 13:45:36.000000000 +0200 @@ -0,0 +1,18 @@ +import osc.commandline + + +class RequestCommand(osc.commandline.OscCommand): + """ + Manage requests + """ + + name = "request" + aliases = ["rq"] + + # arguments specified here will get inherited to all subcommands automatically + def init_arguments(self): + self.add_argument( + "-m", + "--message", + metavar="TEXT", + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/plugins/request_accept.py new/osc-1.1.0/doc/plugins/request_accept.py --- old/osc-1.0.1/doc/plugins/request_accept.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.1.0/doc/plugins/request_accept.py 2023-04-03 13:45:36.000000000 +0200 @@ -0,0 +1,19 @@ +import osc.commandline + + +class RequestAcceptCommand(osc.commandline.OscCommand): + """ + Accept request + """ + + name = "accept" + parent = "RequestCommand" + + def init_arguments(self): + self.add_argument( + "id", + type=int, + ) + + def run(self, args): + print(f"Accepting request '{args.id}'") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/plugins/request_list.py new/osc-1.1.0/doc/plugins/request_list.py --- old/osc-1.0.1/doc/plugins/request_list.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.1.0/doc/plugins/request_list.py 2023-04-03 13:45:36.000000000 +0200 @@ -0,0 +1,13 @@ +import osc.commandline + + +class RequestListCommand(osc.commandline.OscCommand): + """ + List requests + """ + + name = "list" + parent = "RequestCommand" + + def run(self, args): + print("Listing requests") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/doc/plugins/simple.py new/osc-1.1.0/doc/plugins/simple.py --- old/osc-1.0.1/doc/plugins/simple.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.1.0/doc/plugins/simple.py 2023-04-03 13:45:36.000000000 +0200 @@ -0,0 +1,32 @@ +import osc.commandline + + +class SimpleCommand(osc.commandline.OscCommand): + """ + A command that does nothing + + More description + of what the command does. + """ + + # command name + name = "simple" + + # options and positional arguments + def init_arguments(self): + self.add_argument( + "--bool-option", + action="store_true", + help="...", + ) + self.add_argument( + "arguments", + metavar="arg", + nargs="+", + help="...", + ) + + # code of the command + def run(self, args): + print(f"Bool option is {args.bool_option}") + print(f"Positional arguments are {args.arguments}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/osc/__init__.py new/osc-1.1.0/osc/__init__.py --- old/osc-1.0.1/osc/__init__.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/osc/__init__.py 2023-04-03 13:45:36.000000000 +0200 @@ -13,7 +13,7 @@ from .util import git_version -__version__ = git_version.get_version('1.0.1') +__version__ = git_version.get_version('1.1.0') # vim: sw=4 et diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/osc/babysitter.py new/osc-1.1.0/osc/babysitter.py --- old/osc-1.0.1/osc/babysitter.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/osc/babysitter.py 2023-04-03 13:45:36.000000000 +0200 @@ -18,6 +18,7 @@ from . import _private from . import commandline +from . import conf as osc_conf from . import oscerr from .OscConfigParser import configparser from .oscssl import CertVerificationError @@ -52,19 +53,21 @@ def run(prg, argv=None): try: try: - if '--debugger' in sys.argv: + # we haven't parsed options yet, that's why we rely on argv directly + if "--debugger" in (argv or sys.argv[1:]): pdb.set_trace() - # here we actually run the program: - return prg.main(argv) + # here we actually run the program + prg.main(argv) + return 0 except: - # look for an option in the prg.options object and in the config - # dict print stack trace, if desired - if getattr(prg.options, 'traceback', None) or getattr(prg.conf, 'config', {}).get('traceback', None) or \ - getattr(prg.options, 'post_mortem', None) or getattr(prg.conf, 'config', {}).get('post_mortem', None): + # If any of these was set via the command-line options, + # the config values are expected to be changed accordingly. + # That's why we're working only with the config. + if osc_conf.config["traceback"] or osc_conf.config["post_mortem"]: traceback.print_exc(file=sys.stderr) # we could use http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215 # enter the debugger, if desired - if getattr(prg.options, 'post_mortem', None) or getattr(prg.conf, 'config', {}).get('post_mortem', None): + if osc_conf.config["post_mortem"]: if sys.stdout.isatty() and not hasattr(sys, 'ps1'): pdb.post_mortem(sys.exc_info()[2]) else: @@ -80,7 +83,7 @@ except oscerr.APIError as e: print('BuildService API error:', e.msg, file=sys.stderr) except oscerr.LinkExpandError as e: - print('Link "%s/%s" cannot be expanded:\n' % (e.prj, e.pac), e.msg, file=sys.stderr) + print(f'Link "{e.prj}/{e.pac}" cannot be expanded:\n', e.msg, file=sys.stderr) print('Use "osc repairlink" to fix merge conflicts.\n', file=sys.stderr) except oscerr.WorkingCopyWrongVersion as e: print(e, file=sys.stderr) @@ -104,8 +107,7 @@ except AttributeError: body = '' - if getattr(prg.options, 'debug', None) or \ - getattr(prg.conf, 'config', {}).get('debug', None): + if osc_conf.config["debug"]: print(e.hdrs, file=sys.stderr) print(body, file=sys.stderr) @@ -116,11 +118,11 @@ msg = _private.api.xml_escape(msg) print(decode_it(msg), file=sys.stderr) if e.code >= 500 and e.code <= 599: - print('\nRequest: %s' % e.filename) + print(f'\nRequest: {e.filename}') print('Headers:') for h, v in e.hdrs.items(): if h != 'Set-Cookie': - print("%s: %s" % (h, v)) + print(f"{h}: {v}") except BadStatusLine as e: print('Server returned an invalid response:', e, file=sys.stderr) @@ -130,7 +132,7 @@ except URLError as e: msg = 'Failed to reach a server' if hasattr(e, '_osc_host_port'): - msg += ' (%s)' % e._osc_host_port + msg += f' ({e._osc_host_port})' msg += ':\n' print(msg, e.reason, file=sys.stderr) except ssl.SSLError as e: @@ -151,8 +153,7 @@ print(e.message, file=sys.stderr) except oscerr.OscIOError as e: print(e.msg, file=sys.stderr) - if getattr(prg.options, 'debug', None) or \ - getattr(prg.conf, 'config', {}).get('debug', None): + if osc_conf.config["debug"]: print(e.e, file=sys.stderr) except (oscerr.WrongOptions, oscerr.WrongArgs) as e: print(e, file=sys.stderr) @@ -174,7 +175,7 @@ except oscerr.PackageError as e: print(e.msg, file=sys.stderr) except PackageError as e: - print('%s:' % e.fname, e.msg, file=sys.stderr) + print(f'{e.fname}:', e.msg, file=sys.stderr) except RPMError as e: print(e, file=sys.stderr) except CertVerificationError as e: @@ -207,6 +208,6 @@ sys.stdout = os.fdopen(sys.stdout.fileno(), sys.stdout.mode, 1) sys.stderr = os.fdopen(sys.stderr.fileno(), sys.stderr.mode, 1) - sys.exit(run(commandline.Osc())) + sys.exit(run(commandline.OscMainCommand())) # vim: sw=4 et diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/osc/cmdln.py new/osc-1.1.0/osc/cmdln.py --- old/osc-1.0.1/osc/cmdln.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/osc/cmdln.py 2023-04-03 13:45:36.000000000 +0200 @@ -81,7 +81,9 @@ def _format_action(self, action): if isinstance(action, argparse._SubParsersAction): parts = [] - for i in action._get_subactions(): + subactions = action._get_subactions() + subactions.sort(key=lambda x: x.metavar) + for i in subactions: if i.help == argparse.SUPPRESS: # don't display commands with suppressed help continue diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/osc/commandline.py new/osc-1.1.0/osc/commandline.py --- old/osc-1.0.1/osc/commandline.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/osc/commandline.py 2023-04-03 13:45:36.000000000 +0200 @@ -6,9 +6,11 @@ import argparse import getpass import glob +import importlib import importlib.util import inspect import os +import pkgutil import re import subprocess import sys @@ -26,6 +28,7 @@ from . import _private from . import build as osc_build from . import cmdln +from . import commands as osc_commands from . import conf from . import oscerr from . import store as osc_store @@ -36,6 +39,490 @@ from .util.helper import _html_escape, format_table +class Command: + #: Name of the command as used in the argument parser. + name: str = None + + #: Optional aliases to the command. + aliases: List[str] = [] + + #: Whether the command is hidden from help. + #: Defaults to ``False``. + hidden: bool = False + + #: Name of the parent command class. + #: Can be prefixed if the parent comes from a different location, + #: for example ``osc.commands.<ClassName>`` when extending osc command with a plugin. + #: See ``OscMainCommand.MODULES`` for available prefixes. + parent: str = None + + def __init__(self, full_name, parent=None): + self.full_name = full_name + self.parent = parent + self.subparsers = None + + if not self.name: + raise ValueError(f"Command '{self.full_name}' has no 'name' set") + + if parent: + self.parser = self.parent.subparsers.add_parser( + self.name, + aliases=self.aliases, + help=self.get_help(), + description=self.get_description(), + formatter_class=cmdln.HelpFormatter, + conflict_handler="resolve", + prog=f"{self.main_command.name} [global opts] {self.name}", + ) + self.parser.set_defaults(_selected_command=self) + else: + self.parser = argparse.ArgumentParser( + prog=self.name, + description=self.get_description(), + formatter_class=cmdln.HelpFormatter, + usage="%(prog)s [global opts] <command> [--help] [opts] [args]", + ) + + # traverse the parent commands and add their options to the current command + cmd = self + while cmd: + cmd.init_arguments() + cmd = cmd.parent + + def __repr__(self): + return f"<osc plugin {self.full_name} at {self.__hash__():#x}>" + + def get_help(self): + """ + Return the help text of the command. + The first line of the docstring is returned by default. + """ + if self.hidden: + return argparse.SUPPRESS + + if not self.__doc__: + return "" + + help_lines = self.__doc__.strip().splitlines() + + if not help_lines: + return "" + + return help_lines[0] + + def get_description(self): + """ + Return the description of the command. + The docstring without the first line is returned by default. + """ + if not self.__doc__: + return "" + + help_lines = self.__doc__.strip().splitlines() + + if not help_lines: + return "" + + # skip the first line that contains help text + help_lines.pop(0) + + # remove any leading empty lines + while help_lines and not help_lines[0]: + help_lines.pop(0) + + result = "\n".join(help_lines) + result = textwrap.dedent(result) + return result + + @property + def main_command(self): + """ + Return reference to the main command that represents the executable + and contains the main instance of ArgumentParser. + """ + if not self.parent: + return self + return self.parent.main_command + + def add_argument(self, *args, **kwargs): + """ + Add a new argument to the command's argument parser. + See `argparse <https://docs.python.org/3/library/argparse.html>`_ documentation for allowed parameters. + """ + cmd = self + + # Let's inspect if the caller was init_arguments() method. + # In such case use the "parser" argument if specified. + frame_1 = inspect.currentframe().f_back + frame_1_info = inspect.getframeinfo(frame_1) + frame_2 = frame_1.f_back + frame_2_info = inspect.getframeinfo(frame_2) + if (frame_1_info.function, frame_2_info.function) == ("init_arguments", "__init__"): + # this method was called from init_arguments() that was called from __init__ + # let's extract the command class from the 2nd frame and ad arguments there + cmd = frame_2.f_locals["self"] + + # suppress global options from command help + if cmd != self and not self.parent: + kwargs["help"] = argparse.SUPPRESS + + # We're adding hidden options from parent commands to their subcommands to allow + # option intermixing. For all such added hidden options we need to suppress their + # defaults because they would override any option set in the parent command. + if cmd != self: + kwargs["default"] = argparse.SUPPRESS + + cmd.parser.add_argument(*args, **kwargs) + + def init_arguments(self): + """ + Override to add arguments to the argument parser. + + .. note:: + Make sure you're adding arguments only by calling ``self.add_argument()``. + Using ``self.parser.add_argument()`` directly is not recommended + because it disables argument intermixing. + """ + + def run(self, args): + """ + Override to implement the command functionality. + + .. note:: + ``args.positional_args`` is a list containing any unknown (unparsed) positional arguments. + + .. note:: + Consider moving any reusable code into a library, + leaving the command-line code only a thin wrapper on top of it. + + If the code is generic enough, it should be added to osc directly. + In such case don't hesitate to open an `issue <https://github.com/openSUSE/osc/issues>`_. + """ + raise NotImplementedError() + + def register(self, command_class, command_full_name): + if not self.subparsers: + # instantiate subparsers on first use + self.subparsers = self.parser.add_subparsers(dest="command", title="commands") + + # Check for parser conflicts. + # This is how Python 3.11+ behaves by default. + if command_class.name in self.subparsers._name_parser_map: + raise argparse.ArgumentError(self.subparsers, f"conflicting subparser: {command_class.name}") + for alias in command_class.aliases: + if alias in self.subparsers._name_parser_map: + raise argparse.ArgumentError(self.subparsers, f"conflicting subparser alias: {alias}") + + command = command_class(command_full_name, parent=self) + return command + + +class MainCommand(Command): + MODULES = () + + def __init__(self): + super().__init__(self.__class__.__name__) + self.command_classes = {} + self.download_progress = None + + def post_parse_args(self, args): + pass + + def run(self, args): + cmd = getattr(args, "_selected_command", None) + if not cmd: + self.parser.error("Please specify a command") + self.post_parse_args(args) + cmd.run(args) + + def load_command(self, cls, module_prefix): + mod_cls_name = f"{module_prefix}.{cls.__name__}" + parent_name = getattr(cls, "parent", None) + if parent_name: + # allow relative references to classes in the the same module/directory + if "." not in parent_name: + parent_name = f"{module_prefix}.{parent_name}" + try: + parent = self.main_command.command_classes[parent_name] + except KeyError: + msg = f"Failed to load command class '{mod_cls_name}' because it references parent '{parent_name}' that doesn't exist" + print(msg, file=sys.stderr) + return None + cmd = parent.register(cls, mod_cls_name) + else: + cmd = self.main_command.register(cls, mod_cls_name) + + cmd.full_name = mod_cls_name + self.main_command.command_classes[mod_cls_name] = cmd + return cmd + + def load_commands(self): + for module_prefix, module_path in self.MODULES: + module_path = os.path.expanduser(module_path) + for loader, module_name, _ in pkgutil.walk_packages(path=[module_path]): + full_name = f"{module_prefix}.{module_name}" + spec = loader.find_spec(full_name) + mod = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(mod) + except Exception as e: # pylint: disable=broad-except + msg = f"Failed to load commands from module '{full_name}': {e}" + print(msg, file=sys.stderr) + continue + for name in dir(mod): + if name.startswith("_"): + continue + cls = getattr(mod, name) + if not inspect.isclass(cls): + continue + if not issubclass(cls, Command): + continue + if cls.__module__ != full_name: + # skip classes that weren't defined directly in the loaded plugin module + continue + self.load_command(cls, module_prefix) + + def parse_args(self, *args, **kwargs): + namespace, unknown_args = self.parser.parse_known_args(*args, **kwargs) + + unrecognized = [i for i in unknown_args if i.startswith("-")] + if unrecognized: + self.parser.error(f"unrecognized arguments: " + " ".join(unrecognized)) + + namespace.positional_args = list(unknown_args) + return namespace + + +class OscCommand(Command): + """ + Inherit from this class to create new commands. + + The first line of the docstring becomes the help text, + the remaining lines become the command description. + """ + + +class OscMainCommand(MainCommand): + name = "osc" + + MODULES = ( + ("osc.commands", osc_commands.__path__[0]), + ("osc.commands.usr_lib", "/usr/lib/osc-plugins"), + ("osc.commands.usr_local_lib", "/usr/local/lib/osc-plugins"), + ("osc.commands.home_local_lib", "~/.local/lib/osc-plugins"), + ("osc.commands.home", "~/.osc-plugins"), + ) + + def __init__(self): + super().__init__() + self.args = None + self.download_progress = None + + def init_arguments(self): + self.add_argument( + "-v", + "--verbose", + action="store_true", + help="increase verbosity", + ) + self.add_argument( + "-q", + "--quiet", + action="store_true", + help="be quiet, not verbose", + ) + self.add_argument( + "--debug", + action="store_true", + help="print info useful for debugging", + ) + self.add_argument( + "--debugger", + action="store_true", + help="jump into the debugger before executing anything", + ) + self.add_argument( + "--post-mortem", + action="store_true", + help="jump into the debugger in case of errors", + ) + self.add_argument( + "--traceback", + action="store_true", + help="print call trace in case of errors", + ) + self.add_argument( + "-H", + "--http-debug", + action="store_true", + help="debug HTTP traffic (filters some headers)", + ) + self.add_argument( + "--http-full-debug", + action="store_true", + help="debug HTTP traffic (filters no headers)", + ) + self.add_argument( + "-A", + "--apiurl", + metavar="URL", + help="Open Build Service API URL or a configured alias", + ) + self.add_argument( + "--config", + dest="conffile", + metavar="FILE", + help="specify alternate configuration file", + ) + self.add_argument( + "--no-keyring", + action="store_true", + help="disable usage of desktop keyring system", + ) + + def post_parse_args(self, args): + # apiurl hasn't been specified by the user + # we need to set it here because the 'default' option of an argument doesn't support lazy evaluation + if args.apiurl is None: + try: + # try reading the apiurl from the working copy + args.apiurl = osc_store.Store(Path.cwd()).apiurl + except oscerr.NoWorkingCopy: + # we can't use conf.config["apiurl"] because it contains the default "https://api.opensuse.org" + # let's leave setting the right value to conf.get_config() + pass + + conf.get_config( + override_apiurl=args.apiurl, + override_conffile=args.conffile, + override_debug=args.debug, + override_http_debug=args.http_debug, + override_http_full_debug=args.http_full_debug, + override_no_keyring=args.no_keyring, + override_post_mortem=args.post_mortem, + override_traceback=args.traceback, + override_verbose=args.verbose, + ) + + # write config values back to args + # this is crucial mainly for apiurl to resolve an alias to full url + for i in ["apiurl", "debug", "http_debug", "http_full_debug", "post_mortem", "traceback", "verbose"]: + setattr(args, i, conf.config[i]) + args.no_keyring = not conf.config["use_keyring"] + + if conf.config["show_download_progress"]: + self.download_progress = create_text_meter() + + if not args.apiurl: + self.parser.error("Could not determine apiurl, use -A/--apiurl to specify one") + + # needed for LegacyOsc class + self.args = args + + def _wrap_legacy_command(self, func_): + class LegacyCommandWrapper(Command): + func = func_ + __doc__ = getattr(func_, "__doc__", "") + aliases = getattr(func_, "aliases", []) + hidden = getattr(func_, "hidden", False) + name = getattr(func_, "name", func_.__name__[3:]) + + def __repr__(self): + result = super().__repr__() + result += f"({self.func.__name__})" + return result + + def init_arguments(self): + options = getattr(self.func, "options", []) + for option_args, option_kwargs in options: + self.add_argument(*option_args, **option_kwargs) + + def run(self, args): + sig = inspect.signature(self.func) + arg_names = list(sig.parameters.keys()) + if arg_names == ["subcmd", "opts"]: + # handler doesn't take positional args via *args + if args.positional_args: + self.parser.error(f"unrecognized arguments: " + " ".join(args.positional_args)) + self.func(args.command, args) + else: + # handler takes positional args via *args + self.func(args.command, args, *args.positional_args) + + return LegacyCommandWrapper + + def load_legacy_commands(self): + # lazy links of attributes that would normally be initialized in the instance of Osc class + class LegacyOsc(Osc): # pylint: disable=used-before-assignment + # pylint: disable=no-self-argument + @property + def argparser(self_): + return self.parser + + # pylint: disable=no-self-argument + @property + def download_progress(self_): + return self.download_progress + + # pylint: disable=no-self-argument + @property + def options(self_): + return self.args + + # pylint: disable=no-self-argument + @options.setter + def options(self_, value): + pass + + # pylint: disable=no-self-argument + @property + def subparsers(self_): + return self.subparsers + + osc_instance = LegacyOsc() + + for name in dir(osc_instance): + if not name.startswith("do_"): + continue + + func = getattr(osc_instance, name) + + if not inspect.ismethod(func) and not inspect.isfunction(func): + continue + + cls = self._wrap_legacy_command(func) + self.load_command(cls, "osc.commands.old") + + @classmethod + def main(cls, argv=None, run=True): + """ + Initialize OscMainCommand, load all commands and run the selected command. + """ + cmd = cls() + cmd.load_commands() + cmd.load_legacy_commands() + if run: + args = cmd.parse_args(args=argv) + cmd.run(args) + else: + args = None + return cmd, args + + +def get_parser(): + """ + Needed by argparse-manpage to generate man pages from the argument parser. + """ + main, _ = OscMainCommand.main(run=False) + return main.parser + + +# ================================================================================ +# The legacy code follows. +# Please do not use it if possible. +# ================================================================================ + + HELP_MULTIBUILD_MANY = """Only work with the specified flavors of a multibuild package. Globs are resolved according to _multibuild file from server. Empty string is resolved to a package without a flavor.""" @@ -43,12 +530,6 @@ HELP_MULTIBUILD_ONE = "Only work with the specified flavor of a multibuild package." -def get_parser(): - osc = Osc() - osc.create_argparser() - return osc.argparser - - def pop_args( args, arg1_name: str = None, @@ -435,7 +916,6 @@ * http://en.opensuse.org/openSUSE:OSC_plugins """ name = 'osc' - conf = None def __init__(self): self.options = None @@ -9481,7 +9961,6 @@ plugin_dirs = [ '/usr/lib/osc-plugins', '/usr/local/lib/osc-plugins', - '/var/lib/osc-plugins', # Kept for backward compatibility os.path.expanduser('~/.local/lib/osc-plugins'), os.path.expanduser('~/.osc-plugins')] for plugin_dir in plugin_dirs: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/osc/conf.py new/osc-1.1.0/osc/conf.py --- old/osc-1.0.1/osc/conf.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/osc/conf.py 2023-04-03 13:45:36.000000000 +0200 @@ -772,8 +772,8 @@ try: os.chmod(conffile, 0o600) except OSError as e: - if e.errno == errno.EROFS: - print('Warning: file \'%s\' may have an insecure mode.', conffile) + if e.errno in (errno.EROFS, errno.EPERM): + print(f"Warning: Configuration file '{conffile}' may have insecure file permissions.") else: raise e diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/osc/core.py new/osc-1.1.0/osc/core.py --- old/osc-1.0.1/osc/core.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/osc/core.py 2023-04-03 13:45:36.000000000 +0200 @@ -4778,6 +4778,38 @@ for root in res.findall('request'): r = Request() r.read(root) + + # post-process results until we switch back to the /search/request + # which seems to be more suitable for such queries + exclude = False + for action in r.actions: + src_project = getattr(action, "src_project", None) + src_package = getattr(action, "src_package", None) + tgt_project = getattr(action, "tgt_project", None) + tgt_package = getattr(action, "tgt_package", None) + + # skip if neither of source and target project matches + if "project" in query and query["project"] not in (src_project, tgt_project): + exclude = True + break + + # skip if neither of source and target package matches + if "package" in query and query["package"] not in (src_package, tgt_package): + exclude = True + break + + if not conf.config["include_request_from_project"]: + if "project" in query and "package" in query: + if (src_project, src_package) == (query["project"], query["package"]): + exclude = True + break + elif "project" in query: + if src_project == query["project"]: + exclude = True + break + if exclude: + continue + requests.append(r) return requests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/osc/util/git_version.py new/osc-1.1.0/osc/util/git_version.py --- old/osc-1.0.1/osc/util/git_version.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/osc/util/git_version.py 2023-04-03 13:45:36.000000000 +0200 @@ -9,7 +9,7 @@ """ # the `version` variable contents get substituted during `git archive` # it requires adding this to .gitattributes: <path to this file> export-subst - version = "1.0.1" + version = "1.1.0" if version.startswith(("$", "%")): # "$": version hasn't been substituted during `git archive` # "%": "Format:" and "$" characters get removed from the version string (a GitHub bug?) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/setup.cfg new/osc-1.1.0/setup.cfg --- old/osc-1.0.1/setup.cfg 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/setup.cfg 2023-04-03 13:45:36.000000000 +0200 @@ -34,6 +34,7 @@ packages = osc osc._private + osc.commands osc.util install_requires = cryptography diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/tests/test_commandline.py new/osc-1.1.0/tests/test_commandline.py --- old/osc-1.0.1/tests/test_commandline.py 2023-03-17 16:05:07.000000000 +0100 +++ new/osc-1.1.0/tests/test_commandline.py 2023-04-03 13:45:36.000000000 +0200 @@ -1,8 +1,12 @@ +import argparse import os import shutil import tempfile import unittest +from osc.commandline import Command +from osc.commandline import MainCommand +from osc.commandline import OscMainCommand from osc.commandline import pop_project_package_from_args from osc.commandline import pop_project_package_repository_arch_from_args from osc.commandline import pop_project_package_targetproject_targetpackage_from_args @@ -11,6 +15,131 @@ from osc.store import Store +class TestMainCommand(MainCommand): + name = "osc-test" + + def init_arguments(self, command=None): + self.add_argument( + "-A", + "--apiurl", + ) + + +class TestCommand(Command): + name = "test-cmd" + + +OSCRC_LOCALHOST = """ +[general] +apiurl = https://localhost + +[https://localhost] +user=Admin +pass=opensuse +""".lstrip() + + +class TestCommandClasses(unittest.TestCase): + def setUp(self): + os.environ.pop("OSC_CONFIG", None) + self.tmpdir = tempfile.mkdtemp(prefix="osc_test") + os.chdir(self.tmpdir) + self.oscrc = None + + def tearDown(self): + os.environ.pop("OSC_CONFIG", None) + try: + shutil.rmtree(self.tmpdir) + except OSError: + pass + + def write_oscrc_localhost(self): + self.oscrc = os.path.join(self.tmpdir, "oscrc") + with open(self.oscrc, "w") as f: + f.write(OSCRC_LOCALHOST) + + def test_load_commands(self): + main = TestMainCommand() + main.load_commands() + + def test_load_command(self): + main = TestMainCommand() + cmd = main.load_command(TestCommand, "test.osc.commands") + self.assertTrue(str(cmd).startswith("<osc plugin test.osc.commands.TestCommand")) + + def test_parent(self): + class Parent(TestCommand): + name = "parent" + + class Child(TestCommand): + name = "child" + parent = "Parent" + + main = TestMainCommand() + main.load_command(Parent, "test.osc.commands") + main.load_command(Child, "test.osc.commands") + + main.parse_args(["parent", "child"]) + + def test_invalid_parent(self): + class Parent(TestCommand): + name = "parent" + + class Child(TestCommand): + name = "child" + parent = "DoesNotExist" + + main = TestMainCommand() + main.load_command(Parent, "test.osc.commands") + main.load_command(Child, "test.osc.commands") + + def test_load_twice(self): + class AnotherCommand(TestCommand): + name = "another-command" + aliases = ["test-cmd"] + + main = TestMainCommand() + main.load_command(TestCommand, "test.osc.commands") + + # conflict between names + self.assertRaises(argparse.ArgumentError, main.load_command, TestCommand, "test.osc.commands") + + # conflict between a name and an alias + self.assertRaises(argparse.ArgumentError, main.load_command, AnotherCommand, "test.osc.commands") + + def test_intermixing(self): + main = TestMainCommand() + main.load_command(TestCommand, "test.osc.commands") + + args = main.parse_args(["test-cmd", "--apiurl", "https://example.com"]) + self.assertEqual(args.apiurl, "https://example.com") + + args = main.parse_args(["--apiurl", "https://example.com", "test-cmd"]) + self.assertEqual(args.apiurl, "https://example.com") + + def test_unknown_options(self): + main = TestMainCommand() + main.load_command(TestCommand, "test.osc.commands") + + args = main.parse_args(["test-cmd", "unknown-arg"]) + self.assertEqual(args.positional_args, ["unknown-arg"]) + + self.assertRaises(SystemExit, main.parse_args, ["test-cmd", "--unknown-option"]) + + def test_default_apiurl(self): + class TestMainCommand(OscMainCommand): + name = "osc-test" + + main = TestMainCommand() + main.load_command(TestCommand, "test.osc.commands") + + self.write_oscrc_localhost() + os.environ["OSC_CONFIG"] = self.oscrc + args = main.parse_args(["test-cmd"]) + main.post_parse_args(args) + self.assertEqual(args.apiurl, "https://localhost") + + class TestPopProjectPackageFromArgs(unittest.TestCase): def _write_store(self, project=None, package=None): store = Store(self.tmpdir, check=False) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.0.1/tests/test_doc_plugins.py new/osc-1.1.0/tests/test_doc_plugins.py --- old/osc-1.0.1/tests/test_doc_plugins.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.1.0/tests/test_doc_plugins.py 2023-04-03 13:45:36.000000000 +0200 @@ -0,0 +1,79 @@ +""" +These tests make sure that the examples in the documentation +about osc plugins are not outdated. +""" + + +import os +import unittest + + +from osc.commandline import MainCommand +from osc.commandline import OscMainCommand + + +PLUGINS_DIR = os.path.join(os.path.dirname(__file__), "..", "doc", "plugins") + + +class TestMainCommand(MainCommand): + name = "osc-test" + MODULES = ( + ("test.osc.commands", PLUGINS_DIR), + ) + + +class TestPopProjectPackageFromArgs(unittest.TestCase): + def test_load_commands(self): + """ + Test if all plugins from the tutorial can be properly loaded + """ + main = TestMainCommand() + main.load_commands() + + def test_simple(self): + """ + Test the 'simple' command + """ + main = TestMainCommand() + main.load_commands() + args = main.parse_args(["simple", "arg1", "arg2"]) + self.assertEqual(args.command, "simple") + self.assertEqual(args.bool_option, False) + self.assertEqual(args.arguments, ["arg1", "arg2"]) + + def test_request_list(self): + """ + Test the 'request list' command + """ + main = TestMainCommand() + main.load_commands() + args = main.parse_args(["request", "list"]) + self.assertEqual(args.command, "list") + self.assertEqual(args.message, None) + + def test_request_accept(self): + """ + Test the 'request accept' command + """ + main = TestMainCommand() + main.load_commands() + args = main.parse_args(["request", "accept", "-m", "a message", "12345"]) + self.assertEqual(args.command, "accept") + self.assertEqual(args.message, "a message") + self.assertEqual(args.id, 12345) + + def test_plugin_locations(self): + osc_paths = [i[1] for i in OscMainCommand.MODULES] + # skip the first line with osc.commands + osc_paths = osc_paths[1:] + + path = os.path.join(PLUGINS_DIR, "plugin_locations.rst") + with open(path, "r") as f: + # s + doc_paths = f.readlines() + # skip the first line with osc.commands + doc_paths = doc_paths[1:] + doc_paths = [i.lstrip(" -") for i in doc_paths] + doc_paths = [i.rstrip("\n") for i in doc_paths] + + self.assertEqual(doc_paths, osc_paths) ++++++ osc.dsc ++++++ --- /var/tmp/diff_new_pack.b2pIoh/_old 2023-04-03 21:49:26.388887006 +0200 +++ /var/tmp/diff_new_pack.b2pIoh/_new 2023-04-03 21:49:26.392887029 +0200 @@ -1,6 +1,6 @@ Format: 1.0 Source: osc -Version: 1.0.1-0 +Version: 1.1.0-0 Binary: osc Maintainer: Adrian Schroeter <adr...@suse.de> Architecture: any