#840 cli: refactor cli code
Closed 4 years ago by julian8628. Opened 6 years ago by julian8628.
julian8628/koji extend-get-opts  into  master

file modified
+10 -229
@@ -26,208 +26,20 @@ 

  

  from __future__ import absolute_import

  from __future__ import division

+ 

  import logging

- import optparse

  import os

- import re

- import six

  import sys

- import types

  

- import six.moves.configparser

  import six.moves.xmlrpc_client

  

  import koji

- import koji.util

- import koji.plugin

- 

- from koji_cli.lib import _, OptionParser, get_epilog_str, greetings, \

-         warn, categories

- from koji_cli.commands import *

- 

- 

- def register_plugin(plugin):

-     """Scan a given plugin for handlers

- 

-     Handlers are functions marked with one of the decorators defined in koji.plugin

-     """

-     for v in six.itervalues(vars(plugin)):

-         if isinstance(v, six.class_types):

-             #skip classes

-             continue

-         if callable(v):

-             if getattr(v, 'exported_cli', False):

-                 if hasattr(v, 'export_alias'):

-                     name = getattr(v, 'export_alias')

-                 else:

-                     name = v.__name__

-                 # copy object to local namespace

-                 globals()[name] = v

- 

- 

- def load_plugins(options, path):

-     """Load plugins specified by our configuration plus system plugins. Order

-     is that system plugins are first, so they can be overridden by

-     user-specified ones with same name."""

-     logger = logging.getLogger('koji.plugins')

-     if os.path.exists(path):

-         tracker = koji.plugin.PluginTracker(path=path)

-         for name in sorted(os.listdir(path)):

-             if not name.endswith('.py'):

-                 continue

-             name = name[:-3]

-             logger.info('Loading plugin: %s', name)

-             tracker.load(name)

-             register_plugin(tracker.get(name))

- 

+ from koji_cli import commands

+ from koji_cli.lib import CommandExports, get_options

  

- def get_options():

-     """process options from command line and config file"""

- 

-     common_commands = ['build', 'help', 'download-build',

-                        'latest-pkg', 'search', 'list-targets']

-     usage = _("%%prog [global-options] command [command-options-and-arguments]"

-                 "\n\nCommon commands: %s" % ', '.join(sorted(common_commands)))

-     parser = OptionParser(usage=usage)

-     parser.disable_interspersed_args()

-     progname = os.path.basename(sys.argv[0]) or 'koji'

-     parser.__dict__['origin_format_help'] = parser.format_help

-     parser.__dict__['format_help'] = lambda formatter=None: (

-         "%(origin_format_help)s%(epilog)s" % ({

-             'origin_format_help': parser.origin_format_help(formatter),

-             'epilog': get_epilog_str()}))

-     parser.add_option("-c", "--config", dest="configFile",

-                       help=_("use alternate configuration file"), metavar="FILE")

-     parser.add_option("-p", "--profile", default=progname,

-                       help=_("specify a configuration profile"))

-     parser.add_option("--keytab", help=_("specify a Kerberos keytab to use"), metavar="FILE")

-     parser.add_option("--principal", help=_("specify a Kerberos principal to use"))

-     parser.add_option("--krbservice", help=_("specify the Kerberos service name for the hub"))

-     parser.add_option("--runas", help=_("run as the specified user (requires special privileges)"))

-     parser.add_option("--user", help=_("specify user"))

-     parser.add_option("--password", help=_("specify password"))

-     parser.add_option("--noauth", action="store_true", default=False,

-                       help=_("do not authenticate"))

-     parser.add_option("--force-auth", action="store_true", default=False,

-                       help=_("authenticate even for read-only operations"))

-     parser.add_option("--authtype", help=_("force use of a type of authentication, options: noauth, ssl, password, or kerberos"))

-     parser.add_option("-d", "--debug", action="store_true",

-                       help=_("show debug output"))

-     parser.add_option("--debug-xmlrpc", action="store_true",

-                       help=_("show xmlrpc debug output"))

-     parser.add_option("-q", "--quiet", action="store_true", default=False,

-                       help=_("run quietly"))

-     parser.add_option("--skip-main", action="store_true", default=False,

-                       help=_("don't actually run main"))

-     parser.add_option("-s", "--server", help=_("url of XMLRPC server"))

-     parser.add_option("--topdir", help=_("specify topdir"))

-     parser.add_option("--weburl", help=_("url of the Koji web interface"))

-     parser.add_option("--topurl", help=_("url for Koji file access"))

-     parser.add_option("--pkgurl", help=optparse.SUPPRESS_HELP)

-     parser.add_option("--help-commands", action="store_true", default=False, help=_("list commands"))

-     (options, args) = parser.parse_args()

- 

-     # load local config

-     try:

-         result = koji.read_config(options.profile, user_config=options.configFile)

-     except koji.ConfigurationError as e:

-         parser.error(e.args[0])

-         assert False  # pragma: no cover

- 

-     # update options according to local config

-     for name, value in six.iteritems(result):

-         if getattr(options, name, None) is None:

-             setattr(options, name, value)

- 

-     dir_opts = ('topdir', 'cert', 'serverca')

-     for name in dir_opts:

-         # expand paths here, so we don't have to worry about it later

-         value = os.path.expanduser(getattr(options, name))

-         setattr(options, name, value)

- 

-     #honor topdir

-     if options.topdir:

-         koji.BASEDIR = options.topdir

-         koji.pathinfo.topdir = options.topdir

- 

-     #pkgurl is obsolete

-     if options.pkgurl:

-         if options.topurl:

-             warn("Warning: the pkgurl option is obsolete")

-         else:

-             suggest = re.sub(r'/packages/?$', '', options.pkgurl)

-             if suggest != options.pkgurl:

-                 warn("Warning: the pkgurl option is obsolete, using topurl=%r"

-                      % suggest)

-                 options.topurl = suggest

-             else:

-                 warn("Warning: The pkgurl option is obsolete, please use topurl instead")

- 

-     plugins_path = '%s/lib/python%s.%s/site-packages/koji_cli_plugins' % \

-                    (sys.prefix, sys.version_info[0], sys.version_info[1])

-     load_plugins(options, plugins_path)

- 

-     if options.help_commands:

-         list_commands()

-         sys.exit(0)

-     if not args:

-         list_commands()

-         sys.exit(0)

- 

-     aliases = {

-         'cancel-task' : 'cancel',

-         'cxl' : 'cancel',

-         'list-commands' : 'help',

-         'move-pkg': 'move-build',

-         'move': 'move-build',

-         'latest-pkg': 'latest-build',

-         'tag-pkg': 'tag-build',

-         'tag': 'tag-build',

-         'untag-pkg': 'untag-build',

-         'untag': 'untag-build',

-         'watch-tasks': 'watch-task',

-     }

-     cmd = args[0]

-     cmd = aliases.get(cmd, cmd)

-     if cmd.lower() in greetings:

-         cmd = "moshimoshi"

-     cmd = cmd.replace('-', '_')

-     if ('anon_handle_' + cmd) in globals():

-         if not options.force_auth and '--mine' not in args:

-             options.noauth = True

-         cmd = 'anon_handle_' + cmd

-     elif ('handle_' + cmd) in globals():

-         cmd = 'handle_' + cmd

-     else:

-         list_commands()

-         parser.error('Unknown command: %s' % args[0])

-         assert False  # pragma: no cover

- 

-     return options, cmd, args[1:]

- 

- 

- def handle_help(options, session, args):

-     "[info] List available commands"

-     usage = _("usage: %prog help <category> ...")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     # the --admin opt is for backwards compatibility. It is equivalent to: koji help admin

-     parser.add_option("--admin", action="store_true", help=optparse.SUPPRESS_HELP)

- 

-     (options, args) = parser.parse_args(args)

- 

-     chosen = set(args)

-     if options.admin:

-         chosen.add('admin')

-     avail = set(list(categories.keys()) + ['all'])

-     unavail = chosen - avail

-     for arg in unavail:

-         print("No such help category: %s" % arg)

- 

-     if not chosen:

-         list_commands()

-     else:

-         list_commands(chosen)

+ for n, v in six.iteritems(vars(commands)):

+     if not isinstance(v, six.class_types) and callable(v):

+         setattr(CommandExports, n, staticmethod(v))

  

  

  def fix_pyver(options, logger):
@@ -235,7 +47,7 @@ 

      pyver = getattr(options, 'pyver', None)

      if not pyver:

          return

-     if pyver not in [2,3]:

+     if pyver not in [2, 3]:

          logger.warning('Invalid python version requested: %s', pyver)

      if sys.version_info[0] == pyver:

          return
@@ -253,45 +65,14 @@ 

          logger.exception('Unable to execute with requested python version')

  

  

- def list_commands(categories_chosen=None):

-     if categories_chosen is None or "all" in categories_chosen:

-         categories_chosen = list(categories.keys())

-     else:

-         # copy list since we're about to modify it

-         categories_chosen = list(categories_chosen)

-     categories_chosen.sort()

-     handlers = []

-     for name,value in globals().items():

-         if name.startswith('handle_'):

-             alias = name.replace('handle_','')

-             alias = alias.replace('_','-')

-             handlers.append((alias,value))

-         elif name.startswith('anon_handle_'):

-             alias = name.replace('anon_handle_','')

-             alias = alias.replace('_','-')

-             handlers.append((alias,value))

-     handlers.sort()

-     print(_("Available commands:"))

-     for category in categories_chosen:

-         print(_("\n%s:" % categories[category]))

-         for alias,handler in handlers:

-             desc = handler.__doc__ or ''

-             if desc.startswith('[%s] ' % category):

-                 desc = desc[len('[%s] ' % category):]

-             elif category != 'misc' or desc.startswith('['):

-                 continue

-             print("        %-25s %s" % (alias, desc))

- 

-     print("%s" % get_epilog_str().rstrip("\n"))

- 

- 

  if __name__ == "__main__":

      global options

      options, command, args = get_options()

  

      logger = logging.getLogger("koji")

      handler = logging.StreamHandler(sys.stderr)

-     handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s'))

+     handler.setFormatter(

+         logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s'))

      handler.setLevel(logging.DEBUG)

      logger.addHandler(handler)

      if options.debug:
@@ -307,7 +88,7 @@ 

      session = koji.ClientSession(options.server, session_opts)

      rv = 0

      try:

-         rv = locals()[command].__call__(options, session, args)

+         rv = getattr(CommandExports, command).__call__(options, session, args)

          if not rv:

              rv = 0

      except (KeyboardInterrupt, SystemExit):

file modified
+4
@@ -37,5 +37,9 @@ 

  ;certificate of the CA that issued the HTTP server certificate

  ;serverca = ~/.koji/serverca.crt

  

+ ;plugin paths, separated by ':'

+ ;plugin_paths = ~/.koji/plugins

+ 

+ ;[not_implemented_yet]

  ;enabled plugins for CLI, runroot and save_failed_tree are available

  ;plugins =

file modified
+251 -2
@@ -1,16 +1,20 @@ 

  # coding=utf-8

  from __future__ import absolute_import

  from __future__ import division

+ 

+ import logging

  import optparse

  import os

  import random

- import requests

- import six

+ import re

  import socket

  import string

  import sys

  import time

  from contextlib import closing

+ 

+ import requests

+ import six

  from six.moves import range

  

  try:
@@ -19,6 +23,7 @@ 

      krbV = None

  

  import koji

+ import koji.plugin

  

  # fix OptionParser for python 2.3 (optparse verion 1.4.1+)

  # code taken from optparse version 1.5a2
@@ -636,3 +641,247 @@ 

                  t['sub'] = True

  

      return tasklist

+ 

+ 

+ def register_plugin(plugin):

+     """Scan a given plugin for handlers

+ 

+     Handlers are functions marked with one of the decorators defined in koji.plugin

+     """

+     for v in six.itervalues(vars(plugin)):

+         if isinstance(v, six.class_types):

+             #skip classes

+             continue

+         if callable(v):

+             if getattr(v, 'exported_cli', False):

+                 if hasattr(v, 'export_alias'):

+                     name = getattr(v, 'export_alias')

+                 else:

+                     name = v.__name__

+                 # copy object to local namespace

+                 setattr(CommandExports, name, staticmethod(v))

+ 

+ 

+ def load_plugins(options, paths):

+     """Load plugins specified by our configuration plus system plugins. Order

+     is that system plugins are first, so they can be overridden by

+     user-specified ones with same name."""

+     logger = logging.getLogger('koji.plugins')

+     tracker = koji.plugin.PluginTracker(path=paths)

+     names = set()

+     for path in paths:

+         if os.path.exists(path):

+             for name in sorted(os.listdir(path)):

+                 if not name.endswith('.py'):

+                     continue

+                 name = name[:-3]

+                 names.add(name)

+     for name in names:

+         logger.info('Loading plugin: %s', name)

+         tracker.load(name)

+         register_plugin(tracker.get(name))

+ 

+ 

+ def get_options(no_cmd=False):

+     """process options from command line and config file.

+ 

+     If no_cmd is False, command is required, and it must be registered.

+     Returns (options, cmd, args)

+ 

+     If no_cmd is True, command would be an option.

+     Returns (options, args)

+     It's usually used by standalone cli script,

+     but global options and customized options should be divided by '--'.

+     """

+ 

+     common_commands = ['build', 'help', 'download-build',

+                        'latest-pkg', 'search', 'list-targets']

+     usage = _("%%prog [global-options] command [command-options-and-arguments]"

+                 "\n\nCommon commands: %s" % ', '.join(sorted(common_commands)))

+     parser = OptionParser(usage=usage)

+     parser.disable_interspersed_args()

+     progname = os.path.basename(sys.argv[0]) or 'koji'

+     parser.__dict__['origin_format_help'] = parser.format_help

+     parser.__dict__['format_help'] = lambda formatter=None: (

+         "%(origin_format_help)s%(epilog)s" % ({

+             'origin_format_help': parser.origin_format_help(formatter),

+             'epilog': get_epilog_str()}))

+     parser.add_option("-c", "--config", dest="configFile",

+                       help=_("use alternate configuration file"), metavar="FILE")

+     parser.add_option("-p", "--profile", default=progname,

+                       help=_("specify a configuration profile"))

+     parser.add_option("--keytab", help=_("specify a Kerberos keytab to use"), metavar="FILE")

+     parser.add_option("--principal", help=_("specify a Kerberos principal to use"))

+     parser.add_option("--krbservice", help=_("specify the Kerberos service name for the hub"))

+     parser.add_option("--runas", help=_("run as the specified user (requires special privileges)"))

+     parser.add_option("--user", help=_("specify user"))

+     parser.add_option("--password", help=_("specify password"))

+     parser.add_option("--noauth", action="store_true", default=False,

+                       help=_("do not authenticate"))

+     parser.add_option("--force-auth", action="store_true", default=False,

+                       help=_("authenticate even for read-only operations"))

+     parser.add_option("--authtype", help=_("force use of a type of authentication, options: noauth, ssl, password, or kerberos"))

+     parser.add_option("-d", "--debug", action="store_true",

+                       help=_("show debug output"))

+     parser.add_option("--debug-xmlrpc", action="store_true",

+                       help=_("show xmlrpc debug output"))

+     parser.add_option("-q", "--quiet", action="store_true", default=False,

+                       help=_("run quietly"))

+     parser.add_option("--skip-main", action="store_true", default=False,

+                       help=_("don't actually run main"))

+     parser.add_option("-s", "--server", help=_("url of XMLRPC server"))

+     parser.add_option("--topdir", help=_("specify topdir"))

+     parser.add_option("--weburl", help=_("url of the Koji web interface"))

+     parser.add_option("--topurl", help=_("url for Koji file access"))

+     parser.add_option("--pkgurl", help=optparse.SUPPRESS_HELP)

+     parser.add_option("--plugin-paths", help=_("specify plugin paths divided by ':'"))

+     parser.add_option("--help-commands", action="store_true", default=False, help=_("list commands"))

+     (options, args) = parser.parse_args()

+ 

+     # load local config

+     try:

+         result = koji.read_config(options.profile, user_config=options.configFile)

+     except koji.ConfigurationError as e:

+         parser.error(e.args[0])

+         assert False  # pragma: no cover

+ 

+     # update options according to local config

+     for name, value in six.iteritems(result):

+         if getattr(options, name, None) is None:

+             setattr(options, name, value)

+ 

+     dir_opts = ('topdir', 'cert', 'serverca')

+     for name in dir_opts:

+         # expand paths here, so we don't have to worry about it later

+         value = os.path.expanduser(getattr(options, name))

+         setattr(options, name, value)

+ 

+     #honor topdir

+     if options.topdir:

+         koji.BASEDIR = options.topdir

+         koji.pathinfo.topdir = options.topdir

+ 

+     #pkgurl is obsolete

+     if options.pkgurl:

+         if options.topurl:

+             warn("Warning: the pkgurl option is obsolete")

+         else:

+             suggest = re.sub(r'/packages/?$', '', options.pkgurl)

+             if suggest != options.pkgurl:

+                 warn("Warning: the pkgurl option is obsolete, using topurl=%r"

+                      % suggest)

+                 options.topurl = suggest

+             else:

+                 warn("Warning: The pkgurl option is obsolete, please use topurl instead")

+ 

+ 

+     # update plugin_paths to list

+     plugin_paths = options.plugin_paths or []

+     if plugin_paths:

+         plugin_paths = [os.path.expanduser(p) for p in plugin_paths.split(':')]

+     # always load plugins from koji_cli_plugins module

+     plugin_paths.append('%s/lib/python%s.%s/site-packages/koji_cli_plugins' %

+                         (sys.prefix, sys.version_info[0], sys.version_info[1]))

+     setattr(options, 'plugin_paths', plugin_paths)

+     load_plugins(options, plugin_paths)

+ 

+     if no_cmd:

+         return options, args

+     if options.help_commands:

+         list_commands()

+         sys.exit(0)

+     if not args:

+         list_commands()

+         sys.exit(0)

+ 

+     aliases = {

+         'cancel-task' : 'cancel',

+         'cxl' : 'cancel',

+         'list-commands' : 'help',

+         'move-pkg': 'move-build',

+         'move': 'move-build',

+         'latest-pkg': 'latest-build',

+         'tag-pkg': 'tag-build',

+         'tag': 'tag-build',

+         'untag-pkg': 'untag-build',

+         'untag': 'untag-build',

+         'watch-tasks': 'watch-task',

+     }

+     cmd = args[0]

+     cmd = aliases.get(cmd, cmd)

+     if cmd.lower() in greetings:

+         cmd = "moshimoshi"

+     cmd = cmd.replace('-', '_')

+     if hasattr(CommandExports, 'anon_handle_' + cmd):

+         if not options.force_auth and '--mine' not in args:

+             options.noauth = True

+         cmd = 'anon_handle_' + cmd

+     elif hasattr(CommandExports, 'handle_' + cmd):

+         cmd = 'handle_' + cmd

+     else:

+         list_commands()

+         parser.error('Unknown command: %s' % args[0])

+         assert False  # pragma: no cover

+ 

+     return options, cmd, args[1:]

+ 

+ 

+ def list_commands(categories_chosen=None):

+     if categories_chosen is None or "all" in categories_chosen:

+         categories_chosen = list(categories.keys())

+     else:

+         # copy list since we're about to modify it

+         categories_chosen = list(categories_chosen)

+     categories_chosen.sort()

+     handlers = []

+     for name, value in six.iteritems(vars(CommandExports)):

+         if name.startswith('handle_'):

+             alias = name.replace('handle_', '')

+             alias = alias.replace('_', '-')

+             handlers.append((alias, value))

+         elif name.startswith('anon_handle_'):

+             alias = name.replace('anon_handle_', '')

+             alias = alias.replace('_', '-')

+             handlers.append((alias, value))

+     handlers.sort()

+     print(_("Available commands:"))

+     for category in categories_chosen:

+         print(_("\n%s:" % categories[category]))

+         for alias, handler in handlers:

+             if isinstance(handler, staticmethod):

+                 handler = handler.__func__

+             desc = handler.__doc__ or ''

+             if desc.startswith('[%s] ' % category):

+                 desc = desc[len('[%s] ' % category):]

+             elif category != 'misc' or desc.startswith('['):

+                 continue

+             print("        %-25s %s" % (alias, desc))

+ 

+     print("%s" % get_epilog_str().rstrip("\n"))

+ 

+ 

+ class CommandExports(object):

+ 

+     @staticmethod

+     def handle_help(options, session, args):

+         "[info] List available commands"

+         usage = _("usage: %prog help <category> ...")

+         usage += _("\n(Specify the --help global option for a list of other help options)")

+         parser = OptionParser(usage=usage)

+         # the --admin opt is for backwards compatibility. It is equivalent to: koji help admin

+         parser.add_option("--admin", action="store_true", help=optparse.SUPPRESS_HELP)

+ 

+         (options, args) = parser.parse_args(args)

+ 

+         chosen = set(args)

+         if options.admin:

+             chosen.add('admin')

+         avail = set(list(categories.keys()) + ['all'])

+         unavail = chosen - avail

+         for arg in unavail:

+             print("No such help category: %s" % arg)

+ 

+         if not chosen:

+             list_commands()

+         else:

+             list_commands(chosen)

file modified
+2 -1
@@ -1661,7 +1661,8 @@ 

          'authtype': None,

          'debug': False,

          'debug_xmlrpc': False,

-         'pyver' : None,

+         'pyver': None,

+         'plugin_paths': None,

      }

  

      result = config_defaults.copy()

@@ -0,0 +1,5 @@ 

+ from koji.plugin import export_cli

+ 

+ @export_cli

+ def foo5():

+     pass

@@ -0,0 +1,7 @@ 

+ from koji.plugin import export_cli, export_as

+ 

+ @export_as('foo6')

+ @export_cli

+ def foo():

+     pass

+ 

@@ -1,32 +1,35 @@ 

  from __future__ import absolute_import

- import mock

+ 

  import os

- import six

  import unittest

  

+ import mock

+ import six

+ 

+ import koji_cli.lib

  from . import loadcli

+ 

  cli = loadcli.cli

  

  

  class TestListCommands(unittest.TestCase):

- 

      def setUp(self):

          self.options = mock.MagicMock()

          self.session = mock.MagicMock()

          self.args = mock.MagicMock()

-         self.original_parser = cli.OptionParser

-         cli.OptionParser = mock.MagicMock()

-         self.parser = cli.OptionParser.return_value

+         self.original_parser = koji_cli.lib.OptionParser

+         koji_cli.lib.OptionParser = mock.MagicMock()

+         self.parser = koji_cli.lib.OptionParser.return_value

  

      def tearDown(self):

-         cli.OptionParser = self.original_parser

+         koji_cli.lib.OptionParser = self.original_parser

  

      # Show long diffs in error output...

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      def test_list_commands(self, stdout):

-         cli.list_commands()

+         koji_cli.lib.list_commands()

          actual = stdout.getvalue()

          if six.PY2:

              actual = actual.replace('nosetests', 'koji')
@@ -42,7 +45,7 @@ 

          options, arguments = mock.MagicMock(), mock.MagicMock()

          options.admin = True

          self.parser.parse_args.return_value = [options, arguments]

-         cli.handle_help(self.options, self.session, self.args)

+         koji_cli.lib.CommandExports.handle_help(self.options, self.session, self.args)

          actual = stdout.getvalue()

          if six.PY2:

              actual = actual.replace('nosetests', 'koji')

@@ -1,20 +1,24 @@ 

  from __future__ import absolute_import

- import mock

+ 

  import os

  import unittest

  

- from . import loadcli

- cli = loadcli.cli

+ import mock

  

+ from koji_cli import lib

  

- class TestLoadPlugins(unittest.TestCase):

  

+ class TestLoadPlugins(unittest.TestCase):

      @mock.patch('logging.getLogger')

      def test_load_plugins(self, getLogger):

          options = mock.MagicMock()

-         cli.load_plugins(options, os.path.dirname(__file__) + '/data/plugins')

-         self.assertTrue(callable(cli.foobar))

-         self.assertTrue(callable(cli.foo2))

-         self.assertFalse(hasattr(cli, 'foo3'))

-         self.assertFalse(hasattr(cli, 'foo4'))

-         self.assertFalse(hasattr(cli, 'sth'))

+         lib.load_plugins(options, [os.path.dirname(__file__) + '/data/plugins',

+                                    os.path.dirname(

+                                        __file__) + '/data/plugins2'])

+         self.assertTrue(callable(lib.CommandExports.foobar))

+         self.assertTrue(callable(lib.CommandExports.foo2))

+         self.assertTrue(hasattr(lib.CommandExports, 'foo6'))

+         self.assertFalse(hasattr(lib.CommandExports, 'foo3'))

+         self.assertFalse(hasattr(lib.CommandExports, 'foo4'))

+         self.assertFalse(hasattr(lib.CommandExports, 'foo5'))

+         self.assertFalse(hasattr(lib.CommandExports, 'sth'))

  • add no_cmd option to get_options
  • add multiple plugin paths supporting
  • move functions from koji bin to koji_cli.lib
  • add aCommandExports to move cli handlers into this namespace instead of globals()

1 new commit added

  • fix unittest: cli/test_list_commands
6 years ago

@julian8628 Is this still relevant / plan for 1.21?

Pull-Request has been closed by julian8628

4 years ago

I would close this PR and start new work on issue #1534 in 1.21