2
0
Fork 0

feat: remove legacy command system

typer is the recommended approach, but any could be used.  we have
nothing significant to offer so better to just not.
This commit is contained in:
Lance Edgar 2024-07-04 14:35:29 -05:00
parent fd76766f56
commit a76cbf29e9
23 changed files with 8 additions and 1193 deletions

View file

@ -1,6 +0,0 @@
``wuttjamaican.cmd.base``
=========================
.. automodule:: wuttjamaican.cmd.base
:members:

View file

@ -1,8 +0,0 @@
``wuttjamaican.cmd.date_organize``
==================================
.. automodule:: wuttjamaican.cmd.date_organize
:members:
.. program-output:: wutta date-organize -h

View file

@ -1,8 +0,0 @@
``wuttjamaican.cmd.make_appdir``
================================
.. automodule:: wuttjamaican.cmd.make_appdir
:members:
.. program-output:: wutta make-appdir -h

View file

@ -1,23 +0,0 @@
``wuttjamaican.cmd``
====================
.. automodule:: wuttjamaican.cmd
:members:
The core framework is contained in :mod:`wuttjamaican.cmd.base`.
Note that :class:`~wuttjamaican.cmd.base.Command` serves as the base
class for top-level :term:`commands<command>` but it also functions as
the top-level ``wutta`` command.
Some :term:`subcommands<subcommand>` are available as well; these are
registered under the ``wutta`` command.
.. toctree::
:maxdepth: 1
cmd.base
cmd.date_organize
cmd.make_appdir
cmd.setup

View file

@ -1,8 +0,0 @@
``wuttjamaican.cmd.setup``
==========================
.. automodule:: wuttjamaican.cmd.setup
:members:
.. program-output:: wutta setup -h

View file

@ -8,7 +8,6 @@
:maxdepth: 1 :maxdepth: 1
app app
cmd
conf conf
db db
db.conf db.conf

View file

@ -43,13 +43,16 @@ be used to avoid unwanted splits.
Specifying via Command Line Specifying via Command Line
--------------------------- ---------------------------
All :term:`commands<command>` accept the ``-c`` or ``--config`` params: While no :term:`commands<command>` are shipped with WuttJamaican,
certain other packages may ship with commands (notably, Rattail). The
convention is to accept a ``--config`` (or ``-c``) param on the
command line, e.g.:
.. code-block:: sh .. code-block:: sh
wutta --config=myapp.conf rattail --config=myapp.conf
wutta -c first.conf -c second.conf rattail -c first.conf -c second.conf
Specifying via Environment Variable Specifying via Environment Variable

View file

@ -43,7 +43,5 @@ For more info see:
* :class:`~wuttjamaican.conf.WuttaConfig` and especially * :class:`~wuttjamaican.conf.WuttaConfig` and especially
:meth:`~wuttjamaican.conf.WuttaConfig.get()` :meth:`~wuttjamaican.conf.WuttaConfig.get()`
You can also define your own command line interface; see: You can also define your own command line interface; see
:doc:`/narr/cli/index`.
* :class:`~wuttjamaican.commands.base.Command`
* :class:`~wuttjamaican.commands.base.Subcommand`

View file

@ -39,16 +39,6 @@ docs = ["Sphinx", "sphinxcontrib-programoutput", "furo"]
tests = ["pytest-cov", "tox"] tests = ["pytest-cov", "tox"]
[project.scripts]
wutta = "wuttjamaican.cmd.base:main"
[project.entry-points."wutta.subcommands"]
date-organize = "wuttjamaican.cmd.date_organize:DateOrganize"
make-appdir = "wuttjamaican.cmd.make_appdir:MakeAppDir"
setup = "wuttjamaican.cmd.setup:Setup"
[project.urls] [project.urls]
Homepage = "https://rattailproject.org/" Homepage = "https://rattailproject.org/"
Repository = "https://kallithea.rattailproject.org/rattail-project/wuttjamaican" Repository = "https://kallithea.rattailproject.org/rattail-project/wuttjamaican"

View file

@ -1,33 +0,0 @@
# -*- 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 the ``wuttjamaican.cmd`` namespace you can
access the following:
* :class:`~wuttjamaican.cmd.base.Command`
* :class:`~wuttjamaican.cmd.base.Subcommand`
"""
from .base import Command, Subcommand

View file

@ -1,441 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttJamaican -- Base package for Wutta Framework
# Copyright © 2023-2024 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.
This base class also serves as the primary ``wutta`` command for
WuttJamaican. Most apps will subclass this and register their own
top-level command, then create subcommands as needed.
For more info see :doc:`/narr/cli/commands`.
.. 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.cmd 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 is based on standard :class:`python:argparse.ArgumentParser`
but 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 :class:`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 :term:`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.cmd.make_appdir.MakeAppDir`.
.. 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 PoserCommand
cmd = PoserCommand()
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)

View file

@ -1,59 +0,0 @@
# -*- 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 ``date-organize``
"""
import os
import shutil
import datetime
from .base import Subcommand
class DateOrganize(Subcommand):
"""
Organize files into subfolders according to date
"""
name = 'date-organize'
description = __doc__.strip()
def add_args(self):
""" """
self.parser.add_argument('folder', metavar='PATH',
help="Path to directory containing files which are "
"to be organized by date.")
def run(self, args):
""" """
today = datetime.date.today()
for filename in sorted(os.listdir(args.folder)):
path = os.path.join(args.folder, filename)
if os.path.isfile(path):
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(path))
if mtime.date() < today:
datedir = mtime.strftime(os.sep.join(('%Y', '%m', '%d')))
datedir = os.path.join(args.folder, datedir)
if not os.path.exists(datedir):
os.makedirs(datedir)
shutil.move(path, datedir)

View file

@ -1,68 +0,0 @@
# -*- 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)

View file

@ -1,42 +0,0 @@
# -*- 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")

View file

@ -1,33 +0,0 @@
# -*- 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
"""
import warnings
warnings.warn("wuttjamaican.commands package is deprecated; "
"please use wuttjamaican.cmd instead",
DeprecationWarning, stacklevel=2)
from wuttjamaican.cmd import *

View file

@ -1,33 +0,0 @@
# -*- 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 - base classes for cli
"""
import warnings
warnings.warn("wuttjamaican.commands package is deprecated; "
"please use wuttjamaican.cmd instead",
DeprecationWarning, stacklevel=2)
from wuttjamaican.cmd.base import *

View file

@ -1,33 +0,0 @@
# -*- 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 warnings
warnings.warn("wuttjamaican.commands package is deprecated; "
"please use wuttjamaican.cmd instead",
DeprecationWarning, stacklevel=2)
from wuttjamaican.cmd.make_appdir import *

View file

@ -1,33 +0,0 @@
# -*- 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``
"""
import warnings
warnings.warn("wuttjamaican.commands package is deprecated; "
"please use wuttjamaican.cmd instead",
DeprecationWarning, stacklevel=2)
from wuttjamaican.cmd.setup import *

View file

View file

@ -1,240 +0,0 @@
# -*- coding: utf-8; -*-
import argparse
import sys
from unittest import TestCase
from unittest.mock import MagicMock, patch
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):
# base command is for 'wutta' and has a 'setup' subcommand
cmd = base.Command()
self.assertEqual(cmd.name, 'wutta')
self.assertIn('setup', cmd.subcommands)
self.assertEqual(str(cmd), 'wutta')
def test_subcommand_entry_points(self):
with patch('wuttjamaican.cmd.base.load_entry_points') as load_entry_points:
# empty entry points
load_entry_points.side_effect = lambda group: {}
cmd = base.Command()
self.assertEqual(cmd.subcommands, {})
# typical entry points
load_entry_points.side_effect = lambda group: {'setup': Setup}
cmd = base.Command()
self.assertEqual(cmd.subcommands, {'setup': Setup})
self.assertEqual(cmd.subcommands['setup'].name, 'setup')
# legacy entry points
# nb. mock returns entry points only when legacy name is used
load_entry_points.side_effect = lambda group: {} if 'subcommands' in group else {'setup': Setup}
cmd = base.Command()
self.assertEqual(cmd.subcommands, {'setup': Setup})
self.assertEqual(cmd.subcommands['setup'].name, 'setup')
def test_sorted_subcommands(self):
cmd = base.Command(subcommands={'foo': 'FooSubcommand',
'bar': 'BarSubcommand'})
srtd = cmd.sorted_subcommands()
self.assertEqual(srtd, ['BarSubcommand', 'FooSubcommand'])
def test_run_may_print_help(self):
class Hello(base.Subcommand):
name = 'hello'
cmd = base.Command(subcommands={'hello': Hello})
# first run is not "tested" per se but gives us some coverage.
# (this will *actually* print help, b/c no args specified)
try:
cmd.run()
except SystemExit:
pass
# from now on we mock the help
print_help = MagicMock()
cmd.print_help = print_help
# help is shown if no subcommand is given
try:
cmd.run()
except SystemExit:
pass
print_help.assert_called_once_with()
# help is shown if -h is given
print_help.reset_mock()
try:
cmd.run('-h')
except SystemExit:
pass
print_help.assert_called_once_with()
# help is shown if --help is given
print_help.reset_mock()
try:
cmd.run('--help')
except SystemExit:
pass
print_help.assert_called_once_with()
# help is shown if bad arg is given
print_help.reset_mock()
try:
cmd.run('--this-means-nothing')
except SystemExit:
pass
print_help.assert_called_once_with()
# help is shown if bad subcmd is given
print_help.reset_mock()
try:
cmd.run('make-sandwich')
except SystemExit:
pass
print_help.assert_called_once_with()
# main help is *not* shown if subcommand *and* -h are given
# (sub help is shown instead in that case)
print_help.reset_mock()
try:
cmd.run('hello', '-h')
except SystemExit:
pass
print_help.assert_not_called()
def test_run_invokes_subcommand(self):
class Hello(base.Subcommand):
name = 'hello'
def add_args(self):
self.parser.add_argument('--foo', action='store_true')
def run(self, args):
self.run_with(foo=args.foo)
run_with = Hello.run_with = MagicMock()
cmd = base.Command(subcommands={'hello': Hello})
# omit --foo in which case that is false by default
cmd.run('hello')
run_with.assert_called_once_with(foo=False)
# specify --foo in which case that is true
run_with.reset_mock()
cmd.run('hello', '--foo')
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.cmd.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):
def test_parse_args(self):
kw = {
'prog': 'wutta',
'add_help': False,
}
# nb. examples below assume a command line like:
# bin/wutta foo --bar
# first here is what default parser does
parser = argparse.ArgumentParser(**kw)
parser.add_argument('subcommand', nargs='*')
try:
args = parser.parse_args(['foo', '--bar'])
except SystemExit:
# nb. parser was not happy, tried to exit process
args = None
else:
self.assertEqual(args.subcommand, ['foo', '--bar'])
self.assertFalse(hasattr(args, 'argv'))
# now here is was custom parser does
# (moves extras to argv for subcommand parser)
parser = base.CommandArgumentParser(**kw)
parser.add_argument('subcommand', nargs='*')
args = parser.parse_args(['foo', '--bar'])
self.assertEqual(args.subcommand, ['foo'])
self.assertEqual(args.argv, ['--bar'])
class TestSubcommand(TestCase):
def test_basic(self):
cmd = base.Command()
subcmd = base.Subcommand(cmd)
subcmd.name = 'foobar'
self.assertEqual(repr(subcmd), 'Subcommand(name=foobar)')
# TODO: this doesn't really test anything per se, but at least
# gives us the coverage..
subcmd._run()
class TestMain(TestCase):
# nb. this doesn't test anything per se but gives coverage
def test_explicit_args(self):
try:
base.main('--help')
except SystemExit:
pass
def test_implicit_args(self):
def true_exit(*args):
sys.exit(*args)
with patch('wuttjamaican.cmd.base.sys') as mocksys:
mocksys.argv = ['wutta', '--help']
mocksys.exit = true_exit
try:
base.main()
except SystemExit:
pass

View file

@ -1,34 +0,0 @@
# -*- coding: utf-8; -*-
import datetime
import os
from wuttjamaican.conf import WuttaConfig
from wuttjamaican.cmd import Command, date_organize
from wuttjamaican.testing import FileConfigTestCase
class TestDateOrganize(FileConfigTestCase):
def test_run(self):
dates = [
datetime.date(2023, 11, 21),
datetime.date(2023, 11, 20),
datetime.date(2023, 10, 15),
datetime.date(2023, 9, 10),
]
for date in dates:
dt = datetime.datetime.combine(date, datetime.time(0))
filename = date.strftime('%Y%m%d.txt')
path = self.write_file(filename, '')
os.utime(path, (dt.timestamp(), dt.timestamp()))
cmd = Command(subcommands={
'date-organize': date_organize.DateOrganize,
})
cmd.run('date-organize', self.tempdir)
self.assertEqual(os.listdir(self.tempdir), ['2023'])
self.assertEqual(sorted(os.listdir(os.path.join(self.tempdir, '2023'))),
['09', '10', '11'])

View file

@ -1,53 +0,0 @@
# -*- coding: utf-8; -*-
import os
import shutil
import tempfile
from unittest import TestCase
from unittest.mock import patch
from wuttjamaican.conf import WuttaConfig
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):
def setUp(self):
self.config = WuttaConfig(appname='wuttatest')
self.command = Command(self.config, subcommands={
'make-appdir': make_appdir.MakeAppDir,
})
def test_run(self):
# appdir is created, and 3 subfolders added by default
tempdir = tempfile.mkdtemp()
appdir = os.path.join(tempdir, 'app')
self.assertFalse(os.path.exists(appdir))
self.command.run('make-appdir', '--path', appdir)
self.assertTrue(os.path.exists(appdir))
self.assertEqual(len(os.listdir(appdir)), 3)
shutil.rmtree(tempdir)
# subfolders still added if appdir already exists
tempdir = tempfile.mkdtemp()
self.assertTrue(os.path.exists(tempdir))
self.assertEqual(len(os.listdir(tempdir)), 0)
self.command.run('make-appdir', '--path', tempdir)
self.assertEqual(len(os.listdir(tempdir)), 3)
shutil.rmtree(tempdir)
# mock out sys.prefix to get coverage
with patch('wuttjamaican.cmd.make_appdir.sys') as sys:
tempdir = tempfile.mkdtemp()
appdir = os.path.join(tempdir, 'app')
sys.prefix = tempdir
self.assertFalse(os.path.exists(appdir))
self.command.run('make-appdir')
self.assertTrue(os.path.exists(appdir))
self.assertEqual(len(os.listdir(appdir)), 3)
shutil.rmtree(tempdir)

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from wuttjamaican.cmd import Command, setup
# nb. do this just for coverage
from wuttjamaican.commands.setup import Setup as legacy
class TestSetup(TestCase):
def setUp(self):
self.command = Command()
self.subcommand = setup.Setup(self.command)
def test_run(self):
# TODO: this doesn't really test anything yet
self.subcommand._run()