mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
More docstrings, functionality, and unit tests for improved CLI class
This commit is contained in:
246
ipalib/cli.py
246
ipalib/cli.py
@@ -84,23 +84,7 @@ class help(frontend.Application):
|
|||||||
print 'Purpose: %s' % cmd.doc
|
print 'Purpose: %s' % cmd.doc
|
||||||
self.application.build_parser(cmd).print_help()
|
self.application.build_parser(cmd).print_help()
|
||||||
|
|
||||||
def print_commands(self):
|
|
||||||
std = set(self.api.Command) - set(self.api.Application)
|
|
||||||
print '\nStandard IPA commands:'
|
|
||||||
for key in sorted(std):
|
|
||||||
cmd = self.api.Command[key]
|
|
||||||
self.print_cmd(cmd)
|
|
||||||
print '\nSpecial CLI commands:'
|
|
||||||
for cmd in self.api.Application():
|
|
||||||
self.print_cmd(cmd)
|
|
||||||
print '\nUse the --help option to see all the global options'
|
|
||||||
print ''
|
|
||||||
|
|
||||||
def print_cmd(self, cmd):
|
|
||||||
print ' %s %s' % (
|
|
||||||
to_cli(cmd.name).ljust(self.mcl),
|
|
||||||
cmd.doc,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class console(frontend.Application):
|
class console(frontend.Application):
|
||||||
@@ -222,6 +206,10 @@ class KWCollector(object):
|
|||||||
|
|
||||||
|
|
||||||
class CLI(object):
|
class CLI(object):
|
||||||
|
"""
|
||||||
|
All logic for dispatching over command line interface.
|
||||||
|
"""
|
||||||
|
|
||||||
__d = None
|
__d = None
|
||||||
__mcl = None
|
__mcl = None
|
||||||
|
|
||||||
@@ -230,6 +218,144 @@ class CLI(object):
|
|||||||
self.argv = tuple(argv)
|
self.argv = tuple(argv)
|
||||||
self.__done = set()
|
self.__done = set()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Run a command (or attempt to at least).
|
||||||
|
|
||||||
|
This method requires several initialization steps to be completed
|
||||||
|
first, all of which all automatically called with a single call to
|
||||||
|
`CLI.finalize()`. The initialization steps are broken into separate
|
||||||
|
methods simply to make it easy to write unit tests.
|
||||||
|
|
||||||
|
The initialization involves these steps:
|
||||||
|
|
||||||
|
1. `CLI.parse_globals` parses the global options, which get stored
|
||||||
|
in ``CLI.options``, and stores the remaining args in
|
||||||
|
``CLI.cmd_argv``.
|
||||||
|
|
||||||
|
2. `CLI.bootstrap` initializes the environment information in
|
||||||
|
``CLI.api.env``.
|
||||||
|
|
||||||
|
3. `CLI.load_plugins` registers all plugins, including the
|
||||||
|
CLI-specific plugins.
|
||||||
|
|
||||||
|
4. `CLI.finalize` instantiates all plugins and performs the
|
||||||
|
remaining initialization needed to use the `plugable.API`
|
||||||
|
instance.
|
||||||
|
"""
|
||||||
|
self.__doing('run')
|
||||||
|
self.finalize()
|
||||||
|
return
|
||||||
|
if len(self.cmd_argv) < 1:
|
||||||
|
self.print_commands()
|
||||||
|
print 'Usage: ipa [global-options] COMMAND'
|
||||||
|
sys.exit(2)
|
||||||
|
key = self.cmd_argv[0]
|
||||||
|
if key not in self:
|
||||||
|
self.print_commands()
|
||||||
|
print 'ipa: ERROR: unknown command %r' % key
|
||||||
|
sys.exit(2)
|
||||||
|
return self.run_cmd(
|
||||||
|
self[key],
|
||||||
|
list(s.decode('utf-8') for s in args[1:])
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
"""
|
||||||
|
Fully initialize ``CLI.api`` `plugable.API` instance.
|
||||||
|
|
||||||
|
This method first calls `CLI.load_plugins` to perform some dependant
|
||||||
|
initialization steps, after which `plugable.API.finalize` is called.
|
||||||
|
|
||||||
|
Finally, the CLI-specific commands are passed a reference to this
|
||||||
|
`CLI` instance by calling `frontend.Application.set_application`.
|
||||||
|
"""
|
||||||
|
self.__doing('finalize')
|
||||||
|
self.load_plugins()
|
||||||
|
self.api.finalize()
|
||||||
|
for a in self.api.Application():
|
||||||
|
a.set_application(self)
|
||||||
|
assert self.__d is None
|
||||||
|
self.__d = dict(
|
||||||
|
(c.name.replace('_', '-'), c) for c in self.api.Command()
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_plugins(self):
|
||||||
|
"""
|
||||||
|
Load all standard plugins plus the CLI-specific plugins.
|
||||||
|
|
||||||
|
This method first calls `CLI.bootstrap` to preform some dependant
|
||||||
|
initialization steps, after which `plugable.API.load_plugins` is
|
||||||
|
called.
|
||||||
|
|
||||||
|
Finally, all the CLI-specific plugins are registered.
|
||||||
|
"""
|
||||||
|
self.__doing('load_plugins')
|
||||||
|
self.bootstrap()
|
||||||
|
self.api.load_plugins()
|
||||||
|
for klass in cli_application_commands:
|
||||||
|
self.api.register(klass)
|
||||||
|
|
||||||
|
def bootstrap(self):
|
||||||
|
"""
|
||||||
|
Initialize the ``CLI.api.env`` environment variables.
|
||||||
|
|
||||||
|
This method first calls `CLI.parse_globals` to perform some dependant
|
||||||
|
initialization steps. Then, using environment variables that may have
|
||||||
|
been passed in the global options, the ``overrides`` are constructed
|
||||||
|
and `plugable.API.bootstrap` is called.
|
||||||
|
"""
|
||||||
|
self.__doing('bootstrap')
|
||||||
|
self.parse_globals()
|
||||||
|
self.api.env.verbose = self.options.verbose
|
||||||
|
if self.options.config_file:
|
||||||
|
self.api.env.conf = self.options.config_file
|
||||||
|
overrides = {}
|
||||||
|
if self.options.environment:
|
||||||
|
for a in self.options.environment.split(','):
|
||||||
|
a = a.split('=', 1)
|
||||||
|
if len(a) < 2:
|
||||||
|
parser.error('badly specified environment string,'\
|
||||||
|
'use var1=val1[,var2=val2]..')
|
||||||
|
overrides[a[0].strip()] = a[1].strip()
|
||||||
|
overrides['context'] = 'cli'
|
||||||
|
self.api.bootstrap(**overrides)
|
||||||
|
|
||||||
|
def parse_globals(self):
|
||||||
|
"""
|
||||||
|
Parse out the global options.
|
||||||
|
|
||||||
|
This method parses the global options out of the ``CLI.argv`` instance
|
||||||
|
attribute, after which two new instance attributes are available:
|
||||||
|
|
||||||
|
1. ``CLI.options`` - an ``optparse.Values`` instance containing
|
||||||
|
the global options.
|
||||||
|
|
||||||
|
2. ``CLI.cmd_argv`` - a tuple containing the remainder of
|
||||||
|
``CLI.argv`` after the global options have been consumed.
|
||||||
|
"""
|
||||||
|
self.__doing('parse_globals')
|
||||||
|
parser = optparse.OptionParser()
|
||||||
|
parser.disable_interspersed_args()
|
||||||
|
parser.add_option('-a', dest='prompt_all', action='store_true',
|
||||||
|
help='Prompt for all missing options interactively')
|
||||||
|
parser.add_option('-n', dest='interactive', action='store_false',
|
||||||
|
help='Don\'t prompt for any options interactively')
|
||||||
|
parser.add_option('-c', dest='config_file',
|
||||||
|
help='Specify different configuration file')
|
||||||
|
parser.add_option('-e', dest='environment',
|
||||||
|
help='Specify or override environment variables')
|
||||||
|
parser.add_option('-v', dest='verbose', action='store_true',
|
||||||
|
help='Verbose output')
|
||||||
|
parser.set_defaults(
|
||||||
|
prompt_all=False,
|
||||||
|
interactive=True,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
(options, args) = parser.parse_args(list(self.argv))
|
||||||
|
self.options = options
|
||||||
|
self.cmd_argv = tuple(args)
|
||||||
|
|
||||||
def __doing(self, name):
|
def __doing(self, name):
|
||||||
if name in self.__done:
|
if name in self.__done:
|
||||||
raise StandardError(
|
raise StandardError(
|
||||||
@@ -237,11 +363,28 @@ class CLI(object):
|
|||||||
)
|
)
|
||||||
self.__done.add(name)
|
self.__done.add(name)
|
||||||
|
|
||||||
def __do_if_not_done(self, name):
|
def print_commands(self):
|
||||||
if name not in self.__done:
|
std = set(self.api.Command) - set(self.api.Application)
|
||||||
getattr(self, name)()
|
print '\nStandard IPA commands:'
|
||||||
|
for key in sorted(std):
|
||||||
|
cmd = self.api.Command[key]
|
||||||
|
self.print_cmd(cmd)
|
||||||
|
print '\nSpecial CLI commands:'
|
||||||
|
for cmd in self.api.Application():
|
||||||
|
self.print_cmd(cmd)
|
||||||
|
print '\nUse the --help option to see all the global options'
|
||||||
|
print ''
|
||||||
|
|
||||||
|
def print_cmd(self, cmd):
|
||||||
|
print ' %s %s' % (
|
||||||
|
to_cli(cmd.name).ljust(self.mcl),
|
||||||
|
cmd.doc,
|
||||||
|
)
|
||||||
|
|
||||||
def isdone(self, name):
|
def isdone(self, name):
|
||||||
|
"""
|
||||||
|
Return True in method named ``name`` has already been called.
|
||||||
|
"""
|
||||||
return name in self.__done
|
return name in self.__done
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
@@ -252,7 +395,7 @@ class CLI(object):
|
|||||||
assert self.__d is not None, 'you must call finalize() first'
|
assert self.__d is not None, 'you must call finalize() first'
|
||||||
return self.__d[key]
|
return self.__d[key]
|
||||||
|
|
||||||
def finalize(self):
|
def old_finalize(self):
|
||||||
api = self.api
|
api = self.api
|
||||||
for klass in cli_application_commands:
|
for klass in cli_application_commands:
|
||||||
api.register(klass)
|
api.register(klass)
|
||||||
@@ -261,29 +404,8 @@ class CLI(object):
|
|||||||
a.set_application(self)
|
a.set_application(self)
|
||||||
self.build_map()
|
self.build_map()
|
||||||
|
|
||||||
def build_map(self):
|
|
||||||
assert self.__d is None
|
|
||||||
self.__d = dict(
|
|
||||||
(c.name.replace('_', '-'), c) for c in self.api.Command()
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.finalize()
|
|
||||||
set_default_env(self.api.env)
|
|
||||||
args = self.parse_globals()
|
|
||||||
if len(args) < 1:
|
|
||||||
self.print_commands()
|
|
||||||
print 'Usage: ipa [global-options] COMMAND'
|
|
||||||
sys.exit(2)
|
|
||||||
key = args[0]
|
|
||||||
if key not in self:
|
|
||||||
self.print_commands()
|
|
||||||
print 'ipa: ERROR: unknown command %r' % key
|
|
||||||
sys.exit(2)
|
|
||||||
return self.run_cmd(
|
|
||||||
self[key],
|
|
||||||
list(s.decode('utf-8') for s in args[1:])
|
|
||||||
)
|
|
||||||
|
|
||||||
def run_cmd(self, cmd, argv):
|
def run_cmd(self, cmd, argv):
|
||||||
kw = self.parse(cmd, argv)
|
kw = self.parse(cmd, argv)
|
||||||
@@ -370,45 +492,9 @@ class CLI(object):
|
|||||||
parser.add_option(o)
|
parser.add_option(o)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def parse_globals(self):
|
|
||||||
self.__doing('parse_globals')
|
|
||||||
parser = optparse.OptionParser()
|
|
||||||
parser.disable_interspersed_args()
|
|
||||||
parser.add_option('-a', dest='prompt_all', action='store_true',
|
|
||||||
help='Prompt for all missing options interactively')
|
|
||||||
parser.add_option('-n', dest='interactive', action='store_false',
|
|
||||||
help='Don\'t prompt for any options interactively')
|
|
||||||
parser.add_option('-c', dest='config_file',
|
|
||||||
help='Specify different configuration file')
|
|
||||||
parser.add_option('-e', dest='environment',
|
|
||||||
help='Specify or override environment variables')
|
|
||||||
parser.add_option('-v', dest='verbose', action='store_true',
|
|
||||||
help='Verbose output')
|
|
||||||
parser.set_defaults(
|
|
||||||
prompt_all=False,
|
|
||||||
interactive=True,
|
|
||||||
verbose=False,
|
|
||||||
)
|
|
||||||
(options, args) = parser.parse_args(list(self.argv))
|
|
||||||
self.options = options
|
|
||||||
self.cmd_argv = tuple(args)
|
|
||||||
|
|
||||||
def bootstrap(self):
|
|
||||||
self.__doing('bootstrap')
|
|
||||||
self.parse_globals()
|
|
||||||
self.api.env.verbose = self.options.verbose
|
|
||||||
if self.options.config_file:
|
|
||||||
self.api.env.conf = self.options.config_file
|
|
||||||
overrides = {}
|
|
||||||
if self.options.environment:
|
|
||||||
for a in self.options.environment.split(','):
|
|
||||||
a = a.split('=', 1)
|
|
||||||
if len(a) < 2:
|
|
||||||
parser.error('badly specified environment string,'\
|
|
||||||
'use var1=val1[,var2=val2]..')
|
|
||||||
overrides[a[0].strip()] = a[1].strip()
|
|
||||||
overrides['context'] = 'cli'
|
|
||||||
self.api.bootstrap(**overrides)
|
|
||||||
|
|
||||||
def get_usage(self, cmd):
|
def get_usage(self, cmd):
|
||||||
return ' '.join(self.get_usage_iter(cmd))
|
return ' '.join(self.get_usage_iter(cmd))
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ DEFAULT_CONFIG = (
|
|||||||
('verbose', False),
|
('verbose', False),
|
||||||
('debug', False),
|
('debug', False),
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************
|
# ********************************************************
|
||||||
# The remaining keys are never set from the values here!
|
# The remaining keys are never set from the values here!
|
||||||
# ********************************************************
|
# ********************************************************
|
||||||
|
|||||||
@@ -115,6 +115,17 @@ class test_CLI(ClassChecker):
|
|||||||
assert o.api is api
|
assert o.api is api
|
||||||
return (o, api, home)
|
return (o, api, home)
|
||||||
|
|
||||||
|
def check_cascade(self, *names):
|
||||||
|
(o, api, home) = self.new()
|
||||||
|
method = getattr(o, names[0])
|
||||||
|
for name in names:
|
||||||
|
assert o.isdone(name) is False
|
||||||
|
method()
|
||||||
|
for name in names:
|
||||||
|
assert o.isdone(name) is True
|
||||||
|
e = raises(StandardError, method)
|
||||||
|
assert str(e) == 'CLI.%s() already called' % names[0]
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
"""
|
"""
|
||||||
Test the `ipalib.cli.CLI.__init__` method.
|
Test the `ipalib.cli.CLI.__init__` method.
|
||||||
@@ -201,3 +212,36 @@ class test_CLI(ClassChecker):
|
|||||||
assert api.env.from_default_conf == 'set in default.conf'
|
assert api.env.from_default_conf == 'set in default.conf'
|
||||||
assert api.env.from_cli_conf == 'set in cli.conf'
|
assert api.env.from_cli_conf == 'set in cli.conf'
|
||||||
assert list(api.env) == sorted(keys + added)
|
assert list(api.env) == sorted(keys + added)
|
||||||
|
|
||||||
|
def test_load_plugins(self):
|
||||||
|
"""
|
||||||
|
Test the `ipalib.cli.CLI.load_plugins` method.
|
||||||
|
"""
|
||||||
|
self.check_cascade(
|
||||||
|
'load_plugins',
|
||||||
|
'bootstrap',
|
||||||
|
'parse_globals'
|
||||||
|
)
|
||||||
|
(o, api, home) = self.new()
|
||||||
|
assert api.isdone('load_plugins') is False
|
||||||
|
o.load_plugins()
|
||||||
|
assert api.isdone('load_plugins') is True
|
||||||
|
|
||||||
|
def test_finalize(self):
|
||||||
|
"""
|
||||||
|
Test the `ipalib.cli.CLI.finalize` method.
|
||||||
|
"""
|
||||||
|
self.check_cascade(
|
||||||
|
'finalize',
|
||||||
|
'load_plugins',
|
||||||
|
'bootstrap',
|
||||||
|
'parse_globals'
|
||||||
|
)
|
||||||
|
|
||||||
|
(o, api, home) = self.new()
|
||||||
|
assert api.isdone('finalize') is False
|
||||||
|
assert 'Command' not in api
|
||||||
|
o.finalize()
|
||||||
|
assert api.isdone('finalize') is True
|
||||||
|
assert list(api.Command) == \
|
||||||
|
sorted(k.__name__ for k in cli.cli_application_commands)
|
||||||
|
|||||||
Reference in New Issue
Block a user