Move cli framework to wuttjamaican.cmd
subpackage
deprecate `wuttjamaican.commands`
This commit is contained in:
parent
37e42eebbc
commit
c3914738d5
6
docs/api/wuttjamaican/cmd.base.rst
Normal file
6
docs/api/wuttjamaican/cmd.base.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.cmd.base``
|
||||
=========================
|
||||
|
||||
.. automodule:: wuttjamaican.cmd.base
|
||||
:members:
|
8
docs/api/wuttjamaican/cmd.make_appdir.rst
Normal file
8
docs/api/wuttjamaican/cmd.make_appdir.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
``wuttjamaican.cmd.make_appdir``
|
||||
================================
|
||||
|
||||
.. automodule:: wuttjamaican.cmd.make_appdir
|
||||
:members:
|
||||
|
||||
.. program-output:: wutta make-appdir -h
|
6
docs/api/wuttjamaican/cmd.rst
Normal file
6
docs/api/wuttjamaican/cmd.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.cmd``
|
||||
====================
|
||||
|
||||
.. automodule:: wuttjamaican.cmd
|
||||
:members:
|
8
docs/api/wuttjamaican/cmd.setup.rst
Normal file
8
docs/api/wuttjamaican/cmd.setup.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
``wuttjamaican.cmd.setup``
|
||||
==========================
|
||||
|
||||
.. automodule:: wuttjamaican.cmd.setup
|
||||
:members:
|
||||
|
||||
.. program-output:: wutta setup -h
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``wuttjamaican.commands.base``
|
||||
==============================
|
||||
|
||||
.. automodule:: wuttjamaican.commands.base
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``wuttjamaican.commands.make_appdir``
|
||||
=====================================
|
||||
|
||||
.. automodule:: wuttjamaican.commands.make_appdir
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``wuttjamaican.commands``
|
||||
=========================
|
||||
|
||||
.. automodule:: wuttjamaican.commands
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``wuttjamaican.commands.setup``
|
||||
===============================
|
||||
|
||||
.. automodule:: wuttjamaican.commands.setup
|
||||
:members:
|
|
@ -8,10 +8,10 @@
|
|||
:maxdepth: 1
|
||||
|
||||
app
|
||||
commands
|
||||
commands.base
|
||||
commands.make_appdir
|
||||
commands.setup
|
||||
cmd
|
||||
cmd.base
|
||||
cmd.make_appdir
|
||||
cmd.setup
|
||||
conf
|
||||
db
|
||||
db.conf
|
||||
|
|
|
@ -17,6 +17,7 @@ release = '0.1'
|
|||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinxcontrib.programoutput',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.todo',
|
||||
]
|
||||
|
|
|
@ -41,15 +41,15 @@ where = src
|
|||
|
||||
[options.extras_require]
|
||||
db = SQLAlchemy<2
|
||||
docs = Sphinx
|
||||
docs = Sphinx; sphinxcontrib-programoutput
|
||||
tests = pytest-cov; tox
|
||||
|
||||
|
||||
[options.entry_points]
|
||||
|
||||
console_scripts =
|
||||
wutta = wuttjamaican.commands.base:main
|
||||
wutta = wuttjamaican.cmd.base:main
|
||||
|
||||
wutta.subcommands =
|
||||
make-appdir = wuttjamaican.commands.make_appdir:MakeAppDir
|
||||
setup = wuttjamaican.commands.setup:Setup
|
||||
make-appdir = wuttjamaican.cmd.make_appdir:MakeAppDir
|
||||
setup = wuttjamaican.cmd.setup:Setup
|
||||
|
|
33
src/wuttjamaican/cmd/__init__.py
Normal file
33
src/wuttjamaican/cmd/__init__.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# Wutta Framework is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - command line interface
|
||||
|
||||
For convenience, from this ``wuttjamaican.cmd`` namespace you can
|
||||
access the following:
|
||||
|
||||
* :class:`~wuttjamaican.cmd.base.Command`
|
||||
* :class:`~wuttjamaican.cmd.base.Subcommand`
|
||||
"""
|
||||
|
||||
from .base import Command, Subcommand
|
528
src/wuttjamaican/cmd/base.py
Normal file
528
src/wuttjamaican/cmd/base.py
Normal file
|
@ -0,0 +1,528 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# Wutta Framework is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - command framework
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from wuttjamaican import __version__
|
||||
from wuttjamaican.conf import make_config
|
||||
from wuttjamaican.util import load_entry_points
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command:
|
||||
"""
|
||||
Primary command for the application.
|
||||
|
||||
A primary command will usually have multiple subcommands it can
|
||||
run. The typical command line interface is like:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<command> [command-options] <subcommand> [subcommand-options]
|
||||
|
||||
:class:`Subcommand` will contain most of the logic, in terms of
|
||||
what actually happens when it runs. Top-level commands are mostly
|
||||
a stub for sake of logically grouping the subcommands.
|
||||
|
||||
:param config: Optional config object to use.
|
||||
|
||||
Usually a command is being ran via actual command line, and
|
||||
there is no config object yet so it must create one. (It does
|
||||
this within its :meth:`run()` method.)
|
||||
|
||||
But if you already have a config object you can specify it here
|
||||
and it will be used instead.
|
||||
|
||||
:param name: Optional value to assign to :attr:`name`. Usually
|
||||
this is declared within the command class definition, but if
|
||||
needed it can be provided dynamically.
|
||||
|
||||
:param stdout: Optional replacement to use for :attr:`stdout`.
|
||||
|
||||
:param stderr: Optional replacement to use for :attr:`stderr`.
|
||||
|
||||
:param subcommands: Optional dictionary to use for
|
||||
:attr:`subcommands`, instead of loading those via entry points.
|
||||
|
||||
The base class serves as the primary ``wutta`` command for
|
||||
WuttJamaican. Most apps will subclass this and register their own
|
||||
top-level command, and create subcommands as needed.
|
||||
|
||||
To do that, first create your Command class, and a ``main()``
|
||||
entry point function for it (e.g. in ``poser/commands.py``)::
|
||||
|
||||
import sys
|
||||
from wuttjamaican.commands import Command
|
||||
|
||||
class Poser(Command):
|
||||
name = 'poser'
|
||||
description = 'my custom top-level command'
|
||||
version = '0.1'
|
||||
|
||||
def poser_main(*args):
|
||||
args = list(args) or sys.argv[1:]
|
||||
cmd = Poser()
|
||||
cmd.run(*args)
|
||||
|
||||
Then register the ``main()`` entry point(s) in your ``setup.cfg``.
|
||||
The command name should be "one word" (no spaces) but may include
|
||||
hyphens or underscore etc.
|
||||
|
||||
You can register more than one top-level command if needed; these
|
||||
could refer to the same ``main()`` function (in which case they
|
||||
are really aliases) or can use different functions:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
poser = poser.commands:poser_main
|
||||
wutta-poser = poser.commands:wutta_poser_main
|
||||
|
||||
Next time your (``poser``) package is installed, the command will be
|
||||
available, so you can e.g.:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
cd /path/to/venv
|
||||
bin/poser --help
|
||||
bin/wutta-poser --help
|
||||
|
||||
And see :class:`Subcommand` for info about adding those.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
Name of the primary command, e.g. ``wutta``
|
||||
|
||||
.. attribute:: description
|
||||
|
||||
Description of the app itself or the primary command.
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
Version string for the app or primary command.
|
||||
|
||||
.. attribute:: stdout
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDOUT. By default this is just ``sys.stdout``.
|
||||
|
||||
.. attribute:: stderr
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDERR. By default this is just ``sys.stderr``.
|
||||
|
||||
.. attribute:: subcommands
|
||||
|
||||
Dictionary of available subcommand classes, keyed by subcommand
|
||||
name. These are usually loaded from setuptools entry points.
|
||||
"""
|
||||
name = 'wutta'
|
||||
version = __version__
|
||||
description = "Wutta Software Framework"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config=None,
|
||||
name=None,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
subcommands=None):
|
||||
|
||||
self.config = config
|
||||
self.name = name or self.name
|
||||
self.stdout = stdout or sys.stdout
|
||||
self.stderr = stderr or sys.stderr
|
||||
|
||||
# nb. default entry point is like 'wutta_poser.subcommands'
|
||||
safe_name = self.name.replace('-', '_')
|
||||
self.subcommands = subcommands or load_entry_points(f'{safe_name}.subcommands')
|
||||
if not self.subcommands:
|
||||
|
||||
# nb. legacy entry point is like 'wutta_poser.commands'
|
||||
self.subcommands = load_entry_points(f'{safe_name}.commands')
|
||||
if self.subcommands:
|
||||
msg = (f"entry point group '{safe_name}.commands' uses deprecated name; "
|
||||
f"please define '{safe_name}.subcommands' instead")
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
log.warning(msg)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def sorted_subcommands(self):
|
||||
"""
|
||||
Get the list of subcommand classes, sorted by name.
|
||||
"""
|
||||
return [self.subcommands[name]
|
||||
for name in sorted(self.subcommands)]
|
||||
|
||||
def print_help(self):
|
||||
"""
|
||||
Print usage help text for the main command.
|
||||
"""
|
||||
self.parser.print_help()
|
||||
|
||||
def run(self, *args):
|
||||
"""
|
||||
Parse command line arguments and execute appropriate
|
||||
subcommand.
|
||||
|
||||
Or, if requested, or args are ambiguous, show help for either
|
||||
the top-level or subcommand.
|
||||
|
||||
Usually of course this method is invoked by way of command
|
||||
line. But if you need to run it programmatically, you must
|
||||
specify the full command line args *except* not the top-level
|
||||
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
|
||||
|
||||
cmd = Command()
|
||||
assert cmd.name == 'wutta'
|
||||
cmd.run('setup', '--help')
|
||||
"""
|
||||
# build arg parser
|
||||
self.parser = self.make_arg_parser()
|
||||
self.add_args()
|
||||
|
||||
# primary parser gets first pass at full args, and stores
|
||||
# everything not used within args.argv
|
||||
args = self.parser.parse_args(args)
|
||||
if not args or not args.argv:
|
||||
self.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# then argv should include <subcommand> [subcommand-options]
|
||||
subcmd = args.argv[0]
|
||||
if subcmd in self.subcommands:
|
||||
if '-h' in args.argv or '--help' in args.argv:
|
||||
subcmd = self.subcommands[subcmd](self)
|
||||
subcmd.print_help()
|
||||
sys.exit(0)
|
||||
else:
|
||||
self.print_help()
|
||||
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
|
||||
if not self.config:
|
||||
self.config = self.make_config(args)
|
||||
|
||||
# invoke subcommand
|
||||
log.debug("running command line: %s", sys.argv)
|
||||
subcmd = self.subcommands[subcmd](self)
|
||||
self.prep_subcommand(subcmd, args)
|
||||
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):
|
||||
"""
|
||||
Custom argument parser for use with :class:`Command`.
|
||||
|
||||
This overrides some of the parsing logic which is specific to the
|
||||
primary command object, to separate command options from
|
||||
subcommand options.
|
||||
|
||||
This is documented as FYI but you probably should not need to know
|
||||
about or try to use this yourself. It will be used automatically
|
||||
by ``Command`` or a subclass thereof.
|
||||
"""
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
args.argv = argv
|
||||
return args
|
||||
|
||||
|
||||
class Subcommand:
|
||||
"""
|
||||
Base class for application subcommands.
|
||||
|
||||
Subcommands are where the real action happens. Each must define
|
||||
the :meth:`run()` method with whatever logic is needed. They can
|
||||
also define :meth:`add_args()` to expose options.
|
||||
|
||||
Subcommands always belong to a top-level command - the association
|
||||
is made by way of entry point registration, and the constructor
|
||||
for this class.
|
||||
|
||||
:param command: Reference to top-level :class:`Command` object.
|
||||
|
||||
Note that unlike :class:`Command`, the base ``Subcommand`` does
|
||||
not correspond to any real subcommand for WuttJamaican. (It's
|
||||
*only* a base class.) For a real example see
|
||||
:class:`~wuttjamaican.commands.setup.Setup`.
|
||||
|
||||
In your project you can define new subcommands for any top-level
|
||||
command. For instance to add a ``hello`` subcommand to the
|
||||
``poser`` command example (cf. :class:`Command` docs):
|
||||
|
||||
First create a Subcommand class (e.g. by adding to
|
||||
``poser/commands.py``)::
|
||||
|
||||
from wuttjamaican.commands import Subcommand
|
||||
|
||||
class Hello(Subcommand):
|
||||
\"""
|
||||
Say hello to the user
|
||||
\"""
|
||||
name = 'hello'
|
||||
description = __doc__.strip()
|
||||
|
||||
def add_args(self):
|
||||
self.parser.add_argument('--foo', default='bar', help="Foo value")
|
||||
|
||||
def run(self, args):
|
||||
print("hello, foo value is:", args.foo)
|
||||
|
||||
You may notice there is nothing in that subcommand definition
|
||||
which ties it to the ``poser`` top-level command. That is done by
|
||||
way of another entry point in your ``setup.cfg`` file.
|
||||
|
||||
As with top-level commands, you can "alias" the same subcommand so
|
||||
it appears under multiple top-level commands. Note that if the
|
||||
top-level command name contains a hyphen, that must be replaced
|
||||
with underscore for sake of the subcommand entry point:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[options.entry_points]
|
||||
poser.subcommands =
|
||||
hello = poser.commands:Hello
|
||||
wutta_poser.subcommands =
|
||||
hello = poser.commands:Hello
|
||||
|
||||
Next time your (``poser``) package is installed, the subcommand
|
||||
will be available, so you can e.g.:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
cd /path/to/venv
|
||||
bin/poser hello --help
|
||||
bin/wutta-poser hello --help
|
||||
|
||||
.. attribute:: stdout
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDOUT. This is inherited from :attr:`Command.stdout`.
|
||||
|
||||
.. attribute:: stderr
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDERR. This is inherited from :attr:`Command.stderr`.
|
||||
"""
|
||||
name = 'UNDEFINED'
|
||||
description = "TODO: not defined"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command,
|
||||
):
|
||||
self.command = command
|
||||
self.stdout = self.command.stdout
|
||||
self.stderr = self.command.stderr
|
||||
self.config = self.command.config
|
||||
if self.config:
|
||||
self.app = self.config.get_app()
|
||||
|
||||
# build arg parser
|
||||
self.parser = self.make_arg_parser()
|
||||
self.add_args()
|
||||
|
||||
def __repr__(self):
|
||||
return f"Subcommand(name={self.name})"
|
||||
|
||||
def make_arg_parser(self):
|
||||
"""
|
||||
Must return a new :class:`argparse.ArgumentParser` instance
|
||||
for use by the subcommand.
|
||||
"""
|
||||
return argparse.ArgumentParser(
|
||||
prog=f'{self.command.name} {self.name}',
|
||||
description=self.description)
|
||||
|
||||
def add_args(self):
|
||||
"""
|
||||
Configure additional args for the subcommand arg parser.
|
||||
|
||||
Anything you setup here will then be available within
|
||||
:meth:`run()`. 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()`.
|
||||
"""
|
||||
|
||||
def print_help(self):
|
||||
"""
|
||||
Print usage help text for the subcommand.
|
||||
"""
|
||||
self.parser.print_help()
|
||||
|
||||
def _run(self, *args):
|
||||
args = self.parser.parse_args(args)
|
||||
return self.run(args)
|
||||
|
||||
def run(self, args):
|
||||
"""
|
||||
Run the subcommand logic. Subclass should override this.
|
||||
|
||||
:param args: Reference to the
|
||||
:class:`python:argparse.Namespace` object, as returned by
|
||||
the subcommand arg parser.
|
||||
|
||||
The ``args`` should have values for everything setup in
|
||||
:meth:`add_args()`. For example if you added the ``--foo``
|
||||
arg then here in ``run()`` you can do::
|
||||
|
||||
print("foo value is:", args.foo)
|
||||
|
||||
Usually of course this method is invoked by way of command
|
||||
line. But if you need to run it programmatically, you should
|
||||
*not* try to invoke this method directly. Instead create the
|
||||
``Command`` object and invoke its :meth:`~Command.run()`
|
||||
method.
|
||||
|
||||
For a command line like ``bin/poser hello --foo=baz`` then,
|
||||
you might do this::
|
||||
|
||||
from poser.commands import Poser
|
||||
|
||||
cmd = Poser()
|
||||
assert cmd.name == 'poser'
|
||||
cmd.run('hello', '--foo=baz')
|
||||
"""
|
||||
self.stdout.write("TODO: command logic not yet implemented\n")
|
||||
|
||||
|
||||
def main(*args):
|
||||
"""
|
||||
Primary entry point for the ``wutta`` command.
|
||||
"""
|
||||
args = list(args) or sys.argv[1:]
|
||||
|
||||
cmd = Command()
|
||||
cmd.run(*args)
|
68
src/wuttjamaican/cmd/make_appdir.py
Normal file
68
src/wuttjamaican/cmd/make_appdir.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# Wutta Framework is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - subcommand ``make-appdir``
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .base import Subcommand
|
||||
|
||||
|
||||
class MakeAppDir(Subcommand):
|
||||
"""
|
||||
Make or refresh the "app dir" for virtual environment
|
||||
"""
|
||||
name = 'make-appdir'
|
||||
description = __doc__.strip()
|
||||
|
||||
def add_args(self):
|
||||
""" """
|
||||
self.parser.add_argument('--path', metavar='APPDIR',
|
||||
help="Optional path to desired app dir. If not specified "
|
||||
"it will be named ``app`` and placed in the root of the "
|
||||
"virtual environment.")
|
||||
|
||||
def run(self, args):
|
||||
"""
|
||||
This may be used during setup to establish the :term:`app dir`
|
||||
for a virtual environment. This folder will contain config
|
||||
files, log files etc. used by the app.
|
||||
"""
|
||||
if args.path:
|
||||
appdir = os.path.abspath(args.path)
|
||||
else:
|
||||
appdir = os.path.join(sys.prefix, 'app')
|
||||
|
||||
self.make_appdir(appdir, args)
|
||||
self.stdout.write(f"established appdir: {appdir}\n")
|
||||
|
||||
def make_appdir(self, appdir, args):
|
||||
"""
|
||||
Make the :term:`app dir` for the given path.
|
||||
|
||||
Calls :meth:`~wuttjamaican.app.AppHandler.make_appdir()` to do
|
||||
the heavy lifting.
|
||||
"""
|
||||
self.app.make_appdir(appdir)
|
42
src/wuttjamaican/cmd/setup.py
Normal file
42
src/wuttjamaican/cmd/setup.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# Wutta Framework is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - subcommand ``setup``
|
||||
"""
|
||||
|
||||
from .base import Subcommand
|
||||
|
||||
|
||||
class Setup(Subcommand):
|
||||
"""
|
||||
Install and configure various software
|
||||
"""
|
||||
name = 'setup'
|
||||
description = __doc__.strip()
|
||||
|
||||
def run(self, args):
|
||||
"""
|
||||
TODO: Eventually this will run an interactive setup utility.
|
||||
But it doesn't yet.
|
||||
"""
|
||||
self.stderr.write("TODO: setup is not yet implemented\n")
|
|
@ -21,13 +21,13 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - command line
|
||||
|
||||
For convenience, from this ``wuttjamaican.commands`` namespace you can
|
||||
access the following:
|
||||
|
||||
* :class:`~wuttjamaican.commands.base.Command`
|
||||
* :class:`~wuttjamaican.commands.base.Subcommand`
|
||||
WuttJamaican - command line interface
|
||||
"""
|
||||
|
||||
from .base import Command, Subcommand
|
||||
import warnings
|
||||
|
||||
warnings.warn("wuttjamaican.commands package is deprecated; "
|
||||
"please use wuttjamaican.cmd instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
from wuttjamaican.cmd import *
|
||||
|
|
|
@ -21,508 +21,13 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - command framework
|
||||
WuttJamaican - base classes for cli
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from wuttjamaican import __version__
|
||||
from wuttjamaican.conf import make_config
|
||||
from wuttjamaican.util import load_entry_points
|
||||
warnings.warn("wuttjamaican.commands package is deprecated; "
|
||||
"please use wuttjamaican.cmd instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command:
|
||||
"""
|
||||
Primary command for the application.
|
||||
|
||||
A primary command will usually have multiple subcommands it can
|
||||
run. The typical command line interface is like:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<command> [command-options] <subcommand> [subcommand-options]
|
||||
|
||||
:class:`Subcommand` will contain most of the logic, in terms of
|
||||
what actually happens when it runs. Top-level commands are mostly
|
||||
a stub for sake of logically grouping the subcommands.
|
||||
|
||||
:param config: Optional config object to use.
|
||||
|
||||
Usually a command is being ran via actual command line, and
|
||||
there is no config object yet so it must create one. (It does
|
||||
this within its :meth:`run()` method.)
|
||||
|
||||
But if you already have a config object you can specify it here
|
||||
and it will be used instead.
|
||||
|
||||
:param name: Optional value to assign to :attr:`name`. Usually
|
||||
this is declared within the command class definition, but if
|
||||
needed it can be provided dynamically.
|
||||
|
||||
:param stdout: Optional replacement to use for :attr:`stdout`.
|
||||
|
||||
:param stderr: Optional replacement to use for :attr:`stderr`.
|
||||
|
||||
:param subcommands: Optional dictionary to use for
|
||||
:attr:`subcommands`, instead of loading those via entry points.
|
||||
|
||||
The base class serves as the primary ``wutta`` command for
|
||||
WuttJamaican. Most apps will subclass this and register their own
|
||||
top-level command, and create subcommands as needed.
|
||||
|
||||
To do that, first create your Command class, and a ``main()``
|
||||
entry point function for it (e.g. in ``poser/commands.py``)::
|
||||
|
||||
import sys
|
||||
from wuttjamaican.commands import Command
|
||||
|
||||
class Poser(Command):
|
||||
name = 'poser'
|
||||
description = 'my custom top-level command'
|
||||
version = '0.1'
|
||||
|
||||
def poser_main(*args):
|
||||
args = list(args) or sys.argv[1:]
|
||||
cmd = Poser()
|
||||
cmd.run(*args)
|
||||
|
||||
Then register the ``main()`` entry point(s) in your ``setup.cfg``.
|
||||
The command name should be "one word" (no spaces) but may include
|
||||
hyphens or underscore etc.
|
||||
|
||||
You can register more than one top-level command if needed; these
|
||||
could refer to the same ``main()`` function (in which case they
|
||||
are really aliases) or can use different functions:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
poser = poser.commands:poser_main
|
||||
wutta-poser = poser.commands:wutta_poser_main
|
||||
|
||||
Next time your (``poser``) package is installed, the command will be
|
||||
available, so you can e.g.:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
cd /path/to/venv
|
||||
bin/poser --help
|
||||
bin/wutta-poser --help
|
||||
|
||||
And see :class:`Subcommand` for info about adding those.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
Name of the primary command, e.g. ``wutta``
|
||||
|
||||
.. attribute:: description
|
||||
|
||||
Description of the app itself or the primary command.
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
Version string for the app or primary command.
|
||||
|
||||
.. attribute:: stdout
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDOUT. By default this is just ``sys.stdout``.
|
||||
|
||||
.. attribute:: stderr
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDERR. By default this is just ``sys.stderr``.
|
||||
|
||||
.. attribute:: subcommands
|
||||
|
||||
Dictionary of available subcommand classes, keyed by subcommand
|
||||
name. These are usually loaded from setuptools entry points.
|
||||
"""
|
||||
name = 'wutta'
|
||||
version = __version__
|
||||
description = "Wutta Software Framework"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config=None,
|
||||
name=None,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
subcommands=None):
|
||||
|
||||
self.config = config
|
||||
self.name = name or self.name
|
||||
self.stdout = stdout or sys.stdout
|
||||
self.stderr = stderr or sys.stderr
|
||||
|
||||
# nb. default entry point is like 'wutta_poser.subcommands'
|
||||
safe_name = self.name.replace('-', '_')
|
||||
self.subcommands = subcommands or load_entry_points(f'{safe_name}.subcommands')
|
||||
if not self.subcommands:
|
||||
|
||||
# nb. legacy entry point is like 'wutta_poser.commands'
|
||||
self.subcommands = load_entry_points(f'{safe_name}.commands')
|
||||
if self.subcommands:
|
||||
msg = (f"entry point group '{safe_name}.commands' uses deprecated name; "
|
||||
f"please define '{safe_name}.subcommands' instead")
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
log.warning(msg)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def sorted_subcommands(self):
|
||||
"""
|
||||
Get the list of subcommand classes, sorted by name.
|
||||
"""
|
||||
return [self.subcommands[name]
|
||||
for name in sorted(self.subcommands)]
|
||||
|
||||
def print_help(self):
|
||||
"""
|
||||
Print usage help text for the main command.
|
||||
"""
|
||||
self.parser.print_help()
|
||||
|
||||
def run(self, *args):
|
||||
"""
|
||||
Parse command line arguments and execute appropriate
|
||||
subcommand.
|
||||
|
||||
Or, if requested, or args are ambiguous, show help for either
|
||||
the top-level or subcommand.
|
||||
|
||||
Usually of course this method is invoked by way of command
|
||||
line. But if you need to run it programmatically, you must
|
||||
specify the full command line args *except* not the top-level
|
||||
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
|
||||
|
||||
cmd = Command()
|
||||
assert cmd.name == 'wutta'
|
||||
cmd.run('setup', '--help')
|
||||
"""
|
||||
# build arg parser
|
||||
self.parser = self.make_arg_parser()
|
||||
self.add_args()
|
||||
|
||||
# primary parser gets first pass at full args, and stores
|
||||
# everything not used within args.argv
|
||||
args = self.parser.parse_args(args)
|
||||
if not args or not args.argv:
|
||||
self.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# then argv should include <subcommand> [subcommand-options]
|
||||
subcmd = args.argv[0]
|
||||
if subcmd in self.subcommands:
|
||||
if '-h' in args.argv or '--help' in args.argv:
|
||||
subcmd = self.subcommands[subcmd](self)
|
||||
subcmd.print_help()
|
||||
sys.exit(0)
|
||||
else:
|
||||
self.print_help()
|
||||
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
|
||||
if not self.config:
|
||||
self.config = self.make_config(args)
|
||||
|
||||
# invoke subcommand
|
||||
log.debug("running command line: %s", sys.argv)
|
||||
subcmd = self.subcommands[subcmd](self)
|
||||
self.prep_subcommand(subcmd, args)
|
||||
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):
|
||||
"""
|
||||
Custom argument parser for use with :class:`Command`.
|
||||
|
||||
This overrides some of the parsing logic which is specific to the
|
||||
primary command object, to separate command options from
|
||||
subcommand options.
|
||||
|
||||
This is documented as FYI but you probably should not need to know
|
||||
about or try to use this yourself. It will be used automatically
|
||||
by ``Command`` or a subclass thereof.
|
||||
"""
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
args.argv = argv
|
||||
return args
|
||||
|
||||
|
||||
class Subcommand:
|
||||
"""
|
||||
Base class for application subcommands.
|
||||
|
||||
Subcommands are where the real action happens. Each must define
|
||||
the :meth:`run()` method with whatever logic is needed. They can
|
||||
also define :meth:`add_args()` to expose options.
|
||||
|
||||
Subcommands always belong to a top-level command - the association
|
||||
is made by way of entry point registration, and the constructor
|
||||
for this class.
|
||||
|
||||
:param command: Reference to top-level :class:`Command` object.
|
||||
|
||||
Note that unlike :class:`Command`, the base ``Subcommand`` does
|
||||
not correspond to any real subcommand for WuttJamaican. (It's
|
||||
*only* a base class.) For a real example see
|
||||
:class:`~wuttjamaican.commands.setup.Setup`.
|
||||
|
||||
In your project you can define new subcommands for any top-level
|
||||
command. For instance to add a ``hello`` subcommand to the
|
||||
``poser`` command example (cf. :class:`Command` docs):
|
||||
|
||||
First create a Subcommand class (e.g. by adding to
|
||||
``poser/commands.py``)::
|
||||
|
||||
from wuttjamaican.commands import Subcommand
|
||||
|
||||
class Hello(Subcommand):
|
||||
\"""
|
||||
Say hello to the user
|
||||
\"""
|
||||
name = 'hello'
|
||||
description = __doc__.strip()
|
||||
|
||||
def add_args(self):
|
||||
self.parser.add_argument('--foo', default='bar', help="Foo value")
|
||||
|
||||
def run(self, args):
|
||||
print("hello, foo value is:", args.foo)
|
||||
|
||||
You may notice there is nothing in that subcommand definition
|
||||
which ties it to the ``poser`` top-level command. That is done by
|
||||
way of another entry point in your ``setup.cfg`` file.
|
||||
|
||||
As with top-level commands, you can "alias" the same subcommand so
|
||||
it appears under multiple top-level commands. Note that if the
|
||||
top-level command name contains a hyphen, that must be replaced
|
||||
with underscore for sake of the subcommand entry point:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[options.entry_points]
|
||||
poser.subcommands =
|
||||
hello = poser.commands:Hello
|
||||
wutta_poser.subcommands =
|
||||
hello = poser.commands:Hello
|
||||
|
||||
Next time your (``poser``) package is installed, the subcommand
|
||||
will be available, so you can e.g.:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
cd /path/to/venv
|
||||
bin/poser hello --help
|
||||
bin/wutta-poser hello --help
|
||||
|
||||
.. attribute:: stdout
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDOUT. This is inherited from :attr:`Command.stdout`.
|
||||
|
||||
.. attribute:: stderr
|
||||
|
||||
Reference to file-like object which should be used for writing
|
||||
to STDERR. This is inherited from :attr:`Command.stderr`.
|
||||
"""
|
||||
name = 'UNDEFINED'
|
||||
description = "TODO: not defined"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command,
|
||||
):
|
||||
self.command = command
|
||||
self.stdout = self.command.stdout
|
||||
self.stderr = self.command.stderr
|
||||
self.config = self.command.config
|
||||
if self.config:
|
||||
self.app = self.config.get_app()
|
||||
|
||||
# build arg parser
|
||||
self.parser = self.make_arg_parser()
|
||||
self.add_args()
|
||||
|
||||
def __repr__(self):
|
||||
return f"Subcommand(name={self.name})"
|
||||
|
||||
def make_arg_parser(self):
|
||||
"""
|
||||
Must return a new :class:`argparse.ArgumentParser` instance
|
||||
for use by the subcommand.
|
||||
"""
|
||||
return argparse.ArgumentParser(
|
||||
prog=f'{self.command.name} {self.name}',
|
||||
description=self.description)
|
||||
|
||||
def add_args(self):
|
||||
"""
|
||||
Configure additional args for the subcommand arg parser.
|
||||
|
||||
Anything you setup here will then be available within
|
||||
:meth:`run()`. 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()`.
|
||||
"""
|
||||
|
||||
def print_help(self):
|
||||
"""
|
||||
Print usage help text for the subcommand.
|
||||
"""
|
||||
self.parser.print_help()
|
||||
|
||||
def _run(self, *args):
|
||||
args = self.parser.parse_args(args)
|
||||
return self.run(args)
|
||||
|
||||
def run(self, args):
|
||||
"""
|
||||
Run the subcommand logic. Subclass should override this.
|
||||
|
||||
:param args: Reference to the
|
||||
:class:`python:argparse.Namespace` object, as returned by
|
||||
the subcommand arg parser.
|
||||
|
||||
The ``args`` should have values for everything setup in
|
||||
:meth:`add_args()`. For example if you added the ``--foo``
|
||||
arg then here in ``run()`` you can do::
|
||||
|
||||
print("foo value is:", args.foo)
|
||||
|
||||
Usually of course this method is invoked by way of command
|
||||
line. But if you need to run it programmatically, you should
|
||||
*not* try to invoke this method directly. Instead create the
|
||||
``Command`` object and invoke its :meth:`~Command.run()`
|
||||
method.
|
||||
|
||||
For a command line like ``bin/poser hello --foo=baz`` then,
|
||||
you might do this::
|
||||
|
||||
from poser.commands import Poser
|
||||
|
||||
cmd = Poser()
|
||||
assert cmd.name == 'poser'
|
||||
cmd.run('hello', '--foo=baz')
|
||||
"""
|
||||
self.stdout.write("TODO: command logic not yet implemented\n")
|
||||
|
||||
|
||||
def main(*args):
|
||||
"""
|
||||
Primary entry point for the ``wutta`` command.
|
||||
"""
|
||||
args = list(args) or sys.argv[1:]
|
||||
|
||||
cmd = Command()
|
||||
cmd.run(*args)
|
||||
from wuttjamaican.cmd.base import *
|
||||
|
|
|
@ -21,42 +21,13 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - subcommand: make-appdir
|
||||
WuttJamaican - subcommand ``make-appdir``
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from .base import Subcommand
|
||||
warnings.warn("wuttjamaican.commands package is deprecated; "
|
||||
"please use wuttjamaican.cmd instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
|
||||
class MakeAppDir(Subcommand):
|
||||
"""
|
||||
Make or refresh the "app dir" for virtual environment
|
||||
"""
|
||||
name = 'make-appdir'
|
||||
description = __doc__.strip()
|
||||
|
||||
def add_args(self):
|
||||
""" """
|
||||
self.parser.add_argument('--path', metavar='APPDIR',
|
||||
help="Optional path to desired app dir. If not specified "
|
||||
"it will be named ``app`` and placed in the root of the "
|
||||
"virtual environment.")
|
||||
|
||||
def run(self, args):
|
||||
"""
|
||||
This may be used during setup to establish the :term:`app dir`
|
||||
for a virtual environment. This folder will contain config
|
||||
files, log files etc. used by the app.
|
||||
|
||||
Calls :meth:`~wuttjamaican.app.AppHandler.make_appdir()` to do
|
||||
the heavy lifting.
|
||||
"""
|
||||
if args.path:
|
||||
appdir = os.path.abspath(args.path)
|
||||
else:
|
||||
appdir = os.path.join(sys.prefix, 'app')
|
||||
|
||||
self.app.make_appdir(appdir)
|
||||
self.stdout.write(f"established appdir: {appdir}\n")
|
||||
from wuttjamaican.cmd.make_appdir import *
|
||||
|
|
|
@ -21,22 +21,13 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttJamaican - setup command
|
||||
WuttJamaican - subcommand ``setup``
|
||||
"""
|
||||
|
||||
from .base import Subcommand
|
||||
import warnings
|
||||
|
||||
warnings.warn("wuttjamaican.commands package is deprecated; "
|
||||
"please use wuttjamaican.cmd instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
class Setup(Subcommand):
|
||||
"""
|
||||
Install and configure various software
|
||||
"""
|
||||
name = 'setup'
|
||||
description = __doc__.strip()
|
||||
|
||||
def run(self, args):
|
||||
"""
|
||||
TODO: Eventually this will run an interactive setup utility.
|
||||
But it doesn't yet.
|
||||
"""
|
||||
self.stderr.write("TODO: setup is not yet implemented\n")
|
||||
from wuttjamaican.cmd.setup import *
|
||||
|
|
|
@ -5,11 +5,15 @@ import sys
|
|||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from wuttjamaican.commands import base
|
||||
from wuttjamaican.commands.setup import Setup
|
||||
from wuttjamaican.cmd import base
|
||||
from wuttjamaican.cmd.setup import Setup
|
||||
from wuttjamaican.testing import FileConfigTestCase
|
||||
|
||||
|
||||
# nb. do this just for coverage
|
||||
from wuttjamaican.commands.base import Command as legacy
|
||||
|
||||
|
||||
class TestCommand(FileConfigTestCase):
|
||||
|
||||
def test_base(self):
|
||||
|
@ -20,7 +24,7 @@ class TestCommand(FileConfigTestCase):
|
|||
self.assertEqual(str(cmd), 'wutta')
|
||||
|
||||
def test_subcommand_entry_points(self):
|
||||
with patch('wuttjamaican.commands.base.load_entry_points') as load_entry_points:
|
||||
with patch('wuttjamaican.cmd.base.load_entry_points') as load_entry_points:
|
||||
|
||||
# empty entry points
|
||||
load_entry_points.side_effect = lambda group: {}
|
||||
|
@ -144,7 +148,7 @@ class TestCommand(FileConfigTestCase):
|
|||
self.stdout.write("hello world")
|
||||
self.stderr.write("error text")
|
||||
|
||||
with patch('wuttjamaican.commands.base.sys') as sys:
|
||||
with patch('wuttjamaican.cmd.base.sys') as sys:
|
||||
|
||||
cmd = base.Command(subcommands={'hello': Hello})
|
||||
|
||||
|
@ -226,7 +230,7 @@ class TestMain(TestCase):
|
|||
def true_exit(*args):
|
||||
sys.exit(*args)
|
||||
|
||||
with patch('wuttjamaican.commands.base.sys') as mocksys:
|
||||
with patch('wuttjamaican.cmd.base.sys') as mocksys:
|
||||
mocksys.argv = ['wutta', '--help']
|
||||
mocksys.exit = true_exit
|
||||
|
|
@ -7,7 +7,11 @@ from unittest import TestCase
|
|||
from unittest.mock import patch
|
||||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttjamaican.commands import Command, make_appdir
|
||||
from wuttjamaican.cmd import Command, make_appdir
|
||||
|
||||
|
||||
# nb. do this just for coverage
|
||||
from wuttjamaican.commands.make_appdir import MakeAppDir as legacy
|
||||
|
||||
|
||||
class TestMakeAppDir(TestCase):
|
||||
|
@ -38,7 +42,7 @@ class TestMakeAppDir(TestCase):
|
|||
shutil.rmtree(tempdir)
|
||||
|
||||
# mock out sys.prefix to get coverage
|
||||
with patch('wuttjamaican.commands.make_appdir.sys') as sys:
|
||||
with patch('wuttjamaican.cmd.make_appdir.sys') as sys:
|
||||
tempdir = tempfile.mkdtemp()
|
||||
appdir = os.path.join(tempdir, 'app')
|
||||
sys.prefix = tempdir
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
from unittest import TestCase
|
||||
|
||||
from wuttjamaican.commands import setup, Command
|
||||
from wuttjamaican.cmd import Command, setup
|
||||
|
||||
|
||||
# nb. do this just for coverage
|
||||
from wuttjamaican.commands.setup import Setup as legacy
|
||||
|
||||
|
||||
class TestSetup(TestCase):
|
Loading…
Reference in a new issue