1
0
Fork 0

Add --stdout and --stderr args for base Command class

also refactor its `run()` method to allow more customizing ability
This commit is contained in:
Lance Edgar 2023-11-22 09:11:36 -06:00
parent 13472a5ab5
commit 5c4dcb09f3
2 changed files with 135 additions and 38 deletions

View file

@ -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):
""" """

View file

@ -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):