Add --stdout
and --stderr
args for base Command class
also refactor its `run()` method to allow more customizing ability
This commit is contained in:
parent
13472a5ab5
commit
5c4dcb09f3
|
@ -30,6 +30,7 @@ import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from wuttjamaican import __version__
|
from wuttjamaican import __version__
|
||||||
|
from wuttjamaican.conf import make_config
|
||||||
from wuttjamaican.util import load_entry_points
|
from wuttjamaican.util import load_entry_points
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,7 +202,14 @@ class Command:
|
||||||
Usually of course this method is invoked by way of command
|
Usually of course this method is invoked by way of command
|
||||||
line. But if you need to run it programmatically, you must
|
line. But if you need to run it programmatically, you must
|
||||||
specify the full command line args *except* not the top-level
|
specify the full command line args *except* not the top-level
|
||||||
command name. So for example::
|
command name. So for example to run the equivalent of this
|
||||||
|
command line:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
wutta setup --help
|
||||||
|
|
||||||
|
You could do this in Python::
|
||||||
|
|
||||||
from wuttjamaican.commands import Command
|
from wuttjamaican.commands import Command
|
||||||
|
|
||||||
|
@ -209,42 +217,13 @@ class Command:
|
||||||
assert cmd.name == 'wutta'
|
assert cmd.name == 'wutta'
|
||||||
cmd.run('setup', '--help')
|
cmd.run('setup', '--help')
|
||||||
"""
|
"""
|
||||||
subcommands = ""
|
# build arg parser
|
||||||
for subcmd in self.sorted_subcommands():
|
self.parser = self.make_arg_parser()
|
||||||
subcommands += f" {subcmd.name:<20s} {subcmd.description}\n"
|
self.add_args()
|
||||||
|
|
||||||
epilog = f"""\
|
|
||||||
subcommands:
|
|
||||||
{subcommands}
|
|
||||||
|
|
||||||
also try: {self.name} <subcommand> -h
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.parser = parser = CommandArgumentParser(
|
|
||||||
prog=self.name,
|
|
||||||
description=self.description,
|
|
||||||
add_help=False,
|
|
||||||
usage=f"{self.name} [options] <subcommand> [subcommand-options]",
|
|
||||||
epilog=epilog,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
||||||
|
|
||||||
parser.add_argument('-c', '--config', metavar='PATH',
|
|
||||||
action='append', dest='config_paths',
|
|
||||||
help="Config path (may be specified more than once)")
|
|
||||||
|
|
||||||
parser.add_argument('--plus-config', metavar='PATH',
|
|
||||||
action='append', dest='plus_config_paths',
|
|
||||||
help="Extra configs to load in addition to normal config")
|
|
||||||
|
|
||||||
parser.add_argument('-P', '--progress', action='store_true', default=False,
|
|
||||||
help="Report progress when relevant")
|
|
||||||
|
|
||||||
parser.add_argument('-V', '--version', action='version',
|
|
||||||
version=f"%(prog)s {self.version}")
|
|
||||||
|
|
||||||
# primary parser gets first pass at full args, and stores
|
# primary parser gets first pass at full args, and stores
|
||||||
# everything not used within args.argv
|
# everything not used within args.argv
|
||||||
args = parser.parse_args(args)
|
args = self.parser.parse_args(args)
|
||||||
if not args or not args.argv:
|
if not args or not args.argv:
|
||||||
self.print_help()
|
self.print_help()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -260,17 +239,103 @@ also try: {self.name} <subcommand> -h
|
||||||
self.print_help()
|
self.print_help()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# we should be done needing to print help messages. now it's
|
||||||
|
# safe to redirect STDOUT/STDERR, if necessary
|
||||||
|
if args.stdout:
|
||||||
|
self.stdout = args.stdout
|
||||||
|
if args.stderr:
|
||||||
|
self.stderr = args.stderr
|
||||||
|
|
||||||
# make the config object
|
# make the config object
|
||||||
if not self.config:
|
if not self.config:
|
||||||
from wuttjamaican.conf import make_config
|
self.config = self.make_config(args)
|
||||||
self.config = make_config(args.config_paths,
|
|
||||||
plus_files=args.plus_config_paths)
|
|
||||||
|
|
||||||
# invoke subcommand
|
# invoke subcommand
|
||||||
log.debug("running command line: %s", sys.argv)
|
log.debug("running command line: %s", sys.argv)
|
||||||
subcmd = self.subcommands[subcmd](self)
|
subcmd = self.subcommands[subcmd](self)
|
||||||
|
self.prep_subcommand(subcmd, args)
|
||||||
subcmd._run(*args.argv[1:])
|
subcmd._run(*args.argv[1:])
|
||||||
|
|
||||||
|
# nb. must flush these in case they are file objects
|
||||||
|
self.stdout.flush()
|
||||||
|
self.stderr.flush()
|
||||||
|
|
||||||
|
def make_arg_parser(self):
|
||||||
|
"""
|
||||||
|
Must return a new :class:`argparse.ArgumentParser` instance
|
||||||
|
for use by the main command.
|
||||||
|
|
||||||
|
This will use :class:`CommandArgumentParser` by default.
|
||||||
|
"""
|
||||||
|
subcommands = ""
|
||||||
|
for subcmd in self.sorted_subcommands():
|
||||||
|
subcommands += f" {subcmd.name:<20s} {subcmd.description}\n"
|
||||||
|
|
||||||
|
epilog = f"""\
|
||||||
|
subcommands:
|
||||||
|
{subcommands}
|
||||||
|
|
||||||
|
also try: {self.name} <subcommand> -h
|
||||||
|
"""
|
||||||
|
|
||||||
|
return CommandArgumentParser(
|
||||||
|
prog=self.name,
|
||||||
|
description=self.description,
|
||||||
|
add_help=False,
|
||||||
|
usage=f"{self.name} [options] <subcommand> [subcommand-options]",
|
||||||
|
epilog=epilog,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
|
||||||
|
def add_args(self):
|
||||||
|
"""
|
||||||
|
Configure args for the main command arg parser.
|
||||||
|
|
||||||
|
Anything you setup here will then be available when the
|
||||||
|
command runs. You can add arguments directly to
|
||||||
|
``self.parser``, e.g.::
|
||||||
|
|
||||||
|
self.parser.add_argument('--foo', default='bar', help="Foo value")
|
||||||
|
|
||||||
|
See also docs for :meth:`python:argparse.ArgumentParser.add_argument()`.
|
||||||
|
"""
|
||||||
|
parser = self.parser
|
||||||
|
|
||||||
|
parser.add_argument('-c', '--config', metavar='PATH',
|
||||||
|
action='append', dest='config_paths',
|
||||||
|
help="Config path (may be specified more than once)")
|
||||||
|
|
||||||
|
parser.add_argument('--plus-config', metavar='PATH',
|
||||||
|
action='append', dest='plus_config_paths',
|
||||||
|
help="Extra configs to load in addition to normal config")
|
||||||
|
|
||||||
|
parser.add_argument('-P', '--progress', action='store_true', default=False,
|
||||||
|
help="Report progress when relevant")
|
||||||
|
|
||||||
|
parser.add_argument('-V', '--version', action='version',
|
||||||
|
version=f"%(prog)s {self.version}")
|
||||||
|
|
||||||
|
parser.add_argument('--stdout', metavar='PATH', type=argparse.FileType('w'),
|
||||||
|
help="Optional path to which STDOUT should be written.")
|
||||||
|
parser.add_argument('--stderr', metavar='PATH', type=argparse.FileType('w'),
|
||||||
|
help="Optional path to which STDERR should be written.")
|
||||||
|
|
||||||
|
def make_config(self, args):
|
||||||
|
"""
|
||||||
|
Make the config object in preparation for running a subcommand.
|
||||||
|
|
||||||
|
By default this is a straightforward wrapper around
|
||||||
|
:func:`wuttjamaican.conf.make_config()`.
|
||||||
|
|
||||||
|
:returns: The new config object.
|
||||||
|
"""
|
||||||
|
return make_config(args.config_paths,
|
||||||
|
plus_files=args.plus_config_paths)
|
||||||
|
|
||||||
|
def prep_subcommand(self, subcommand, args):
|
||||||
|
"""
|
||||||
|
Prepare the subcommand for running, as needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CommandArgumentParser(argparse.ArgumentParser):
|
class CommandArgumentParser(argparse.ArgumentParser):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -7,9 +7,10 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from wuttjamaican.commands import base
|
from wuttjamaican.commands import base
|
||||||
from wuttjamaican.commands.setup import Setup
|
from wuttjamaican.commands.setup import Setup
|
||||||
|
from wuttjamaican.testing import FileConfigTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestCommand(TestCase):
|
class TestCommand(FileConfigTestCase):
|
||||||
|
|
||||||
def test_base(self):
|
def test_base(self):
|
||||||
# base command is for 'wutta' and has a 'setup' subcommand
|
# base command is for 'wutta' and has a 'setup' subcommand
|
||||||
|
@ -133,6 +134,37 @@ class TestCommand(TestCase):
|
||||||
cmd.run('hello', '--foo')
|
cmd.run('hello', '--foo')
|
||||||
run_with.assert_called_once_with(foo=True)
|
run_with.assert_called_once_with(foo=True)
|
||||||
|
|
||||||
|
def test_run_uses_stdout_stderr_params(self):
|
||||||
|
myout = self.write_file('my.out', '')
|
||||||
|
myerr = self.write_file('my.err', '')
|
||||||
|
|
||||||
|
class Hello(base.Subcommand):
|
||||||
|
name = 'hello'
|
||||||
|
def run(self, args):
|
||||||
|
self.stdout.write("hello world")
|
||||||
|
self.stderr.write("error text")
|
||||||
|
|
||||||
|
with patch('wuttjamaican.commands.base.sys') as sys:
|
||||||
|
|
||||||
|
cmd = base.Command(subcommands={'hello': Hello})
|
||||||
|
|
||||||
|
# sys.stdout and sys.stderr should be used by default
|
||||||
|
cmd.run('hello')
|
||||||
|
sys.exit.assert_not_called()
|
||||||
|
sys.stdout.write.assert_called_once_with('hello world')
|
||||||
|
sys.stderr.write.assert_called_once_with('error text')
|
||||||
|
|
||||||
|
# but our files may be used instead if specified
|
||||||
|
sys.reset_mock()
|
||||||
|
cmd.run('hello', '--stdout', myout, '--stderr', myerr)
|
||||||
|
sys.exit.assert_not_called()
|
||||||
|
sys.stdout.write.assert_not_called()
|
||||||
|
sys.stderr.write.assert_not_called()
|
||||||
|
with open(myout, 'rt') as f:
|
||||||
|
self.assertEqual(f.read(), 'hello world')
|
||||||
|
with open(myerr, 'rt') as f:
|
||||||
|
self.assertEqual(f.read(), 'error text')
|
||||||
|
|
||||||
|
|
||||||
class TestCommandArgumentParser(TestCase):
|
class TestCommandArgumentParser(TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue