Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use cliff for CLI layer #100

Merged
merged 8 commits into from
Jan 10, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 16 additions & 19 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,31 @@ These modules are used for the operation of all the various subcommands in
stestr. As of the 1.0.0 release each of these commands should be considered
a stable interface that can be relied on externally.

Each command module conforms to a basic format that is used by ``stestr.cli``
to load each command. The basic structure for these modules is the following
three functions::
Each command module conforms to a basic format that is based on the
`cliff`_ framework. The basic structure for these modules is the
following three functions in each class::

def get_cli_help():
def get_description():
"""This function returns a string that is used for the subcommand help"""
help_str = "A descriptive help string about the command"
return help_str

def get_cli_opts(parser):
def get_parser(prog_name):
"""This function takes a parser and any subcommand arguments are defined
here"""
parser.add_argument(...)

def run(arguments):
"""This function actually runs the command. It takes in a tuple arguments
which the first element is the argparse Namespace object with the
arguments from the parser. The second element is a list of unknown
arguments from the CLI. The expectation of the run method is that it
will process the arguments and call another function that does the
real work. The return value is expected to be an int and is used for
the exit code (assuming the function doesn't call sys.exit() on it's
own)"""
args = arguments[0]
unknown_args = arguments[1]
return call_foo()

The command module will not work if all 3 of these function are not defined.
def take_action(parsed_args):
"""This is where the real work for the command is performed. This is the function
that is called when the command is executed. This function is called being
wrapped by sys.exit() so an integer return is expected that will be used
for the command's return code. The arguments input parsed_args is the
argparse.Namespace object from the parsed CLI options."""
return call_foo(...)

.. _cliff: https://docs.openstack.org/cliff/latest/reference/index.html

The command class will not work if all 3 of these function are not defined.
However, to make the commands externally consumable each module also contains
another public function which performs the real work for the command. Each one
of these functions has a defined stable Python API signature with args and
Expand Down
39 changes: 20 additions & 19 deletions doc/source/internal_arch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,22 @@ the command line interface for performing the different stestr operations.

CLI Layer
---------
The CLI layer is built using modular subcommands in argparse. The stestr.cli
module defines a basic interface using argparse and then loops over a list of
command modules in stestr.commands. Each subcommand has its own module in
stestr.commands and has 3 required functions to work properly:
The CLI layer is built using the `cliff.command`_ module. The
stestr.cli module defines a basic interface using cliff. Each
subcommand has its own module in stestr.commands and has 3 required
functions to work properly:

#. set_cli_opts(parser)
#. get_cli_help()
#. run(arguments)
#. get_parser(prog_name)
#. get_description()
#. take_action(parsed_args)

set_cli_opts(parser)
''''''''''''''''''''
NOTE: To keep the api compatibility in stestr.commands, we still have
each subcommands there.

.. _cliff.command: https://docs.openstack.org/cliff/latest/reference/index.html

get_parser(prog_name)
'''''''''''''''''''''

This function is used to define subcommand arguments. It has a single argparse
parser object passed into it. The intent of this function is to have any command
Expand All @@ -46,24 +51,20 @@ specific arguments defined on the provided parser object by calling

.. _parser.add_argument(): https://docs.python.org/2/library/argparse.html#the-add-argument-method

get_cli_help()
''''''''''''''
get_description()
'''''''''''''''''
The intent of this function is to return an command specific help information.
It is expected to return a string that will be used when the subcommand is
defined in argparse and will be displayed before the arguments when --help is
used on the subcommand.

run(arguments)
''''''''''''''
take_action(parsed_args)
''''''''''''''''''''''''
This is where the real work for the command is performed. This is the function
that is called when the command is executed. This function is called being
wrapped by sys.exit() so an integer return is expected that will be used
for the command's return code. The arguments input arg is a tuple, the first
element is the argparse.Namespace object from the parsed CLI options and the
second element is a list of unknown arguments from the CLI. The expectation
is that this function will call a separate function with a real python API
that does all the real work. (which is the public python interface for the
command)
for the command's return code. The arguments input parsed_args is the
argparse.Namespace object from the parsed CLI options.


Operations for Running Tests
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# process, which may cause wedges in the gate later.
future
pbr!=2.1.0,>=2.0.0 # Apache-2.0
cliff>=2.8.0 # Apache-2.0
python-subunit>=0.18.0 # Apache-2.0/BSD
fixtures>=3.0.0 # Apache-2.0/BSD
six>=1.10.0 # MIT
Expand Down
9 changes: 9 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ packages =
console_scripts =
stestr = stestr.cli:main

stestr.cm =
run = stestr.commands.run:Run
failing = stestr.commands.failing:Failing
init = stestr.commands.init:Init
last = stestr.commands.last:Last
list = stestr.commands.list:List
load = stestr.commands.load:Load
slowest = stestr.commands.slowest:Slowest

[extras]
sql =
subunit2sql>=1.8.0
Expand Down
75 changes: 31 additions & 44 deletions stestr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,41 @@
# License for the specific language governing permissions and limitations
# under the License.

import argparse
import importlib
import os
import sys

from cliff import app
from cliff import commandmanager
from stestr import version

__version__ = version.version_info.version_string_with_vcs()


class StestrCLI(object):

commands = ['run', 'list', 'slowest', 'failing', 'last', 'init', 'load']
command_module = 'stestr.commands.'
class StestrCLI(app.App):

def __init__(self):
self.parser = self._get_parser()

def _get_parser(self):
self.command_dict = {}
parser = argparse.ArgumentParser()
self._set_common_opts(parser)
subparsers = parser.add_subparsers(help='command help')
for cmd in self.commands:
self.command_dict[cmd] = importlib.import_module(
self.command_module + cmd)
help_str = self.command_dict[cmd].get_cli_help()
command_parser = subparsers.add_parser(cmd, help=help_str)
self.command_dict[cmd].set_cli_opts(command_parser)
command_parser.set_defaults(func=self.command_dict[cmd].run)
super(StestrCLI, self).__init__(
description='stestr application',
version=__version__,
command_manager=commandmanager.CommandManager('stestr.cm'),
deferred_help=True,
)

def initialize_app(self, argv):
self.LOG.debug('initialize_app')

def prepare_to_run_command(self, cmd):
self.LOG.debug('prepare_to_run_command %s', cmd.__class__.__name__)

def clean_up(self, cmd, result, err):
self.LOG.debug('clean_up %s', cmd.__class__.__name__)
if err:
self.LOG.debug('got an error: %s', err)

def build_option_parser(self, description, version, argparse_kwargs=None):
parser = super(StestrCLI,
self).build_option_parser(description, version,
argparse_kwargs)
parser = self._set_common_opts(parser)
return parser

def _set_common_opts(self, parser):
Expand All @@ -49,12 +54,6 @@ def _set_common_opts(self, parser):
"path lookups but does not affect paths "
"supplied to the command.",
default=None, type=str)
parser.add_argument("-q", "--quiet", action="store_true",
default=False,
help="Turn off output other than the primary "
"output for a command and any errors.")
parser.add_argument('--version', action='version',
version=__version__)
parser.add_argument('--config', '-c', dest='config',
default='.stestr.conf',
help="Set a stestr config file to use with this "
Expand Down Expand Up @@ -89,25 +88,13 @@ def _set_common_opts(self, parser):
"both this and the corresponding config file "
"option are set this value will be used.")

return parser


def main():
def main(argv=sys.argv[1:]):
cli = StestrCLI()
args = cli.parser.parse_known_args()
if args[0].here:
os.chdir(args[0].here)
# NOTE(mtreinish): Make sure any subprocesses launch the same version of
# python being run here
if 'PYTHON' not in os.environ:
os.environ['PYTHON'] = sys.executable
if hasattr(args[0], 'func'):
sys.exit(args[0].func(args))
else:
cli.parser.print_help()
# NOTE(andreaf) This point is reached only when using Python 3.x.
# Python 2.x fails with return code 2 in case of no
# command, so using 2 for consistency
sys.exit(2)
return cli.run(argv)


if __name__ == '__main__':
main()
sys.exit(main(sys.argv[1:]))
33 changes: 18 additions & 15 deletions stestr/commands/failing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,33 @@

import sys

from cliff import command
import testtools

from stestr import output
from stestr.repository import util
from stestr import results


def get_cli_help():
return "Show the current failures known by the repository"
class Failing(command.Command):
def get_description(self):
return "Show the current failures known by the repository"

def get_parser(self, prog_name):
parser = super(Failing, self).get_parser(prog_name)
parser.add_argument(
"--subunit", action="store_true",
default=False, help="Show output as a subunit stream.")
parser.add_argument(
"--list", action="store_true",
default=False, help="Show only a list of failing tests.")
return parser

def set_cli_opts(parser):
parser.add_argument(
"--subunit", action="store_true",
default=False, help="Show output as a subunit stream."),
parser.add_argument(
"--list", action="store_true",
default=False, help="Show only a list of failing tests."),
def take_action(self, parsed_args):
args = parsed_args
return failing(repo_type=self.app_args.repo_type,
repo_url=self.app_args.repo_url,
list_tests=args.list, subunit=args.subunit)


def _show_subunit(run):
Expand All @@ -57,12 +66,6 @@ def _get_id():
return output_result, summary_result


def run(arguments):
args = arguments[0]
return failing(repo_type=args.repo_type, repo_url=args.repo_url,
list_tests=args.list, subunit=args.subunit)


def failing(repo_type='file', repo_url=None, list_tests=False, subunit=False,
stdout=sys.stdout):
"""Print the failing tests from the most recent run in the repository
Expand Down
21 changes: 9 additions & 12 deletions stestr/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@

import sys

from cliff import command

from stestr.repository import util


def run(arguments):
args = arguments[0]
init(args.repo_type, args.repo_url)
class Init(command.Command):
def take_action(self, parsed_args):
init(self.app_args.repo_type, self.app_args.repo_url)

def get_description(self):
help_str = "Create a new repository."
return help_str


def init(repo_type='file', repo_url=None, stdout=sys.stdout):
Expand Down Expand Up @@ -50,12 +56,3 @@ def init(repo_type='file', repo_url=None, stdout=sys.stdout):
'Please check if the repository already exists or '
'select a different path\n' % repo_path)
return 1


def set_cli_opts(parser):
pass


def get_cli_help():
help_str = "Create a new repository."
return help_str