Add a large chunk of the docs for command line interface
will have to finish subcommands later
This commit is contained in:
parent
8a4438c725
commit
af4c28b286
|
@ -4,3 +4,20 @@
|
||||||
|
|
||||||
.. automodule:: wuttjamaican.cmd
|
.. automodule:: wuttjamaican.cmd
|
||||||
:members:
|
: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
|
||||||
|
|
|
@ -9,10 +9,6 @@
|
||||||
|
|
||||||
app
|
app
|
||||||
cmd
|
cmd
|
||||||
cmd.base
|
|
||||||
cmd.date_organize
|
|
||||||
cmd.make_appdir
|
|
||||||
cmd.setup
|
|
||||||
conf
|
conf
|
||||||
db
|
db
|
||||||
db.conf
|
db.conf
|
||||||
|
|
|
@ -6,9 +6,13 @@ Glossary
|
||||||
.. glossary::
|
.. glossary::
|
||||||
:sorted:
|
:sorted:
|
||||||
|
|
||||||
|
ad hoc script
|
||||||
|
Python script (text) file used for ad-hoc automation etc. See
|
||||||
|
also :doc:`narr/cli/scripts`.
|
||||||
|
|
||||||
app
|
app
|
||||||
Depending on context, may refer to the software application
|
Depending on context, may refer to the software application
|
||||||
overall, or the :term:`app handler`.
|
overall, or the :term:`app name`, or the :term:`app handler`.
|
||||||
|
|
||||||
app database
|
app database
|
||||||
The main database used by the :term:`app`. There is normally
|
The main database used by the :term:`app`. There is normally
|
||||||
|
@ -26,15 +30,19 @@ Glossary
|
||||||
:class:`~wuttjamaican.app.AppHandler`.
|
:class:`~wuttjamaican.app.AppHandler`.
|
||||||
|
|
||||||
app name
|
app name
|
||||||
The code-friendly name for the :term:`app`
|
Code-friendly name for the underlying app/config system
|
||||||
(e.g. ``wutta_poser``). This is available on the :term:`config
|
(e.g. ``wutta_poser``).
|
||||||
object` within Python as
|
|
||||||
:attr:`~wuttjamaican.conf.WuttaConfig.appname`.
|
This must usually be specified as part of the call to
|
||||||
|
:func:`~wuttjamaican.conf.make_config()` and is then available on
|
||||||
|
the :term:`config object`
|
||||||
|
:attr:`~wuttjamaican.conf.WuttaConfig.appname` and the :term:`app
|
||||||
|
handler` :attr:`~wuttjamaican.app.AppHandler.appname`.
|
||||||
|
|
||||||
See also the human-friendly :term:`app title`.
|
See also the human-friendly :term:`app title`.
|
||||||
|
|
||||||
app title
|
app title
|
||||||
The human-friendly name for the :term:`app` (e.g. "Wutta Poser").
|
Human-friendly name for the :term:`app` (e.g. "Wutta Poser").
|
||||||
|
|
||||||
See also the code-friendly :term:`app name`.
|
See also the code-friendly :term:`app name`.
|
||||||
|
|
||||||
|
@ -42,7 +50,7 @@ Glossary
|
||||||
A top-level command line interface for the app. Note that
|
A top-level command line interface for the app. Note that
|
||||||
top-level commands don't really "do" anything per se, and are
|
top-level commands don't really "do" anything per se, and are
|
||||||
mostly a way to group :term:`subcommands<subcommand>`. See also
|
mostly a way to group :term:`subcommands<subcommand>`. See also
|
||||||
:class:`~wuttjamaican.commands.base.Command`.
|
:class:`~wuttjamaican.cmd.base.Command`.
|
||||||
|
|
||||||
config
|
config
|
||||||
Depending on context, may refer to any of: :term:`config file`,
|
Depending on context, may refer to any of: :term:`config file`,
|
||||||
|
@ -66,6 +74,17 @@ Glossary
|
||||||
values obtained from the :term:`settings table` as opposed to
|
values obtained from the :term:`settings table` as opposed to
|
||||||
:term:`config file`. See also :doc:`narr/config/settings`.
|
:term:`config file`. See also :doc:`narr/config/settings`.
|
||||||
|
|
||||||
|
entry point
|
||||||
|
This refers to a "setuptools-style" entry point specifically,
|
||||||
|
which is a mechanism used to register "plugins" and the like.
|
||||||
|
This lets the app / config discover features dynamically. Most
|
||||||
|
notably used to register :term:`commands<command>` and
|
||||||
|
:term:`subcommands<subcommand>`.
|
||||||
|
|
||||||
|
For more info see the `Python Packaging User Guide`_.
|
||||||
|
|
||||||
|
.. _Python Packaging User Guide: https://packaging.python.org/en/latest/specifications/entry-points/
|
||||||
|
|
||||||
settings table
|
settings table
|
||||||
Table in the :term:`app database` which is used to store
|
Table in the :term:`app database` which is used to store
|
||||||
:term:`config settings<config setting>`.
|
:term:`config settings<config setting>`.
|
||||||
|
@ -74,4 +93,4 @@ Glossary
|
||||||
A top-level :term:`command` may expose one or more subcommands,
|
A top-level :term:`command` may expose one or more subcommands,
|
||||||
for the overall command line interface. Subcommands are the real
|
for the overall command line interface. Subcommands are the real
|
||||||
workhorse; each can perform a different function. See also
|
workhorse; each can perform a different function. See also
|
||||||
:class:`~wuttjamaican.commands.base.Subcommand`.
|
:class:`~wuttjamaican.cmd.base.Subcommand`.
|
||||||
|
|
111
docs/narr/cli/commands.rst
Normal file
111
docs/narr/cli/commands.rst
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
|
||||||
|
Commands
|
||||||
|
========
|
||||||
|
|
||||||
|
Top-level :term:`commands<command>` are primarily a way to group
|
||||||
|
:term:`subcommands<subcommand>`.
|
||||||
|
|
||||||
|
|
||||||
|
Running a Command
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Top-level commands are installed in such a way that they are available
|
||||||
|
within the ``bin`` folder of the virtual environment. (Or the
|
||||||
|
``Scripts`` folder if on Windows.)
|
||||||
|
|
||||||
|
This folder should be in the ``PATH`` when the virtual environment is
|
||||||
|
activated, in which case you can just run the command by name, e.g.:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
wutta --help
|
||||||
|
|
||||||
|
To actually *do* anything you must also specify a subcommand, e.g.:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
wutta make-appdir
|
||||||
|
|
||||||
|
Many subcommands may accept arguments of their own:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
wutta make-appdir --path=/where/i/want/my/appdir
|
||||||
|
|
||||||
|
But top-level commands also accept global arguments. See the next
|
||||||
|
section for the full list of "global" command options. A complete example
|
||||||
|
then might be like:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
wutta --config=/path/to/my/file.conf make-appdir --path=/where/i/want/my/appdir
|
||||||
|
|
||||||
|
Note that the top-level command will parse its global option args
|
||||||
|
first, and give only what's leftover to the subcommand. Therefore it
|
||||||
|
isn't strictly necessary to specify global options before the
|
||||||
|
subcommand:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
wutta make-appdir --path=/where/i/want/my/appdir --config=/path/to/my/file.conf
|
||||||
|
|
||||||
|
|
||||||
|
``wutta`` command
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
WuttJamaican comes with one top-level command named ``wutta``. Note
|
||||||
|
that the list of available subcommands is shown in the top-level
|
||||||
|
command help.
|
||||||
|
|
||||||
|
See :mod:`wuttjamaican.cmd` for more on the built-in ``wutta``
|
||||||
|
subcommands.
|
||||||
|
|
||||||
|
.. command-output:: wutta -h
|
||||||
|
:returncode: 1
|
||||||
|
|
||||||
|
|
||||||
|
Adding a New Command
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
There is not much to this since top-level commands are mostly just a
|
||||||
|
grouping mechanism.
|
||||||
|
|
||||||
|
First create your :class:`~wuttjamaican.cmd.base.Command` class, and a
|
||||||
|
``main()`` function for it (e.g. in ``poser/commands.py``)::
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from wuttjamaican.cmd import Command
|
||||||
|
|
||||||
|
class PoserCommand(Command):
|
||||||
|
name = 'poser'
|
||||||
|
description = 'my custom top-level command'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
def poser_main(*args):
|
||||||
|
args = list(args) or sys.argv[1:]
|
||||||
|
cmd = PoserCommand()
|
||||||
|
cmd.run(*args)
|
||||||
|
|
||||||
|
Then register the :term:`entry point(s)<entry point>` in your
|
||||||
|
``setup.cfg``. The command name should not contain 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:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
cd /path/to/venv
|
||||||
|
bin/poser --help
|
||||||
|
bin/wutta-poser --help
|
11
docs/narr/cli/index.rst
Normal file
11
docs/narr/cli/index.rst
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
Command Line Interface
|
||||||
|
======================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
overview
|
||||||
|
commands
|
||||||
|
subcommands
|
||||||
|
scripts
|
21
docs/narr/cli/overview.rst
Normal file
21
docs/narr/cli/overview.rst
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
The command line interface is an important part of app automation and
|
||||||
|
may be thought of in a couple ways:
|
||||||
|
|
||||||
|
First there is the :term:`ad hoc script` which is a single file and
|
||||||
|
can be placed anywhere, but is not installed as part of a package.
|
||||||
|
See :doc:`scripts`.
|
||||||
|
|
||||||
|
But the "real" command line interface uses :term:`commands<command>`
|
||||||
|
and :term:`subcommands<subcommand>`; these are installed as part of a
|
||||||
|
package.
|
||||||
|
|
||||||
|
Top-level commands are mostly just a way to group subcommands. Most
|
||||||
|
custom apps would define their own top-level command as well as
|
||||||
|
multiple subcommands. See :doc:`commands` for top-level details.
|
||||||
|
|
||||||
|
Subcommands on the other hand are the real workhorse since they define
|
||||||
|
the action logic. See :doc:`subcommands` for more about those.
|
156
docs/narr/cli/scripts.rst
Normal file
156
docs/narr/cli/scripts.rst
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
|
||||||
|
Ad Hoc Scripts
|
||||||
|
==============
|
||||||
|
|
||||||
|
It can be useful to write :term:`ad hoc scripts<ad hoc script>` for
|
||||||
|
certain things, as opposed to a proper :term:`subcommand`. This is
|
||||||
|
especially true when first getting acquainted with the framework.
|
||||||
|
|
||||||
|
A script is just a text file with Python code. To run it you
|
||||||
|
generally must invoke the Python interpreter somehow and explicitly
|
||||||
|
tell it the path to your script.
|
||||||
|
|
||||||
|
Below we'll walk through creating a script.
|
||||||
|
|
||||||
|
|
||||||
|
Hello World
|
||||||
|
-----------
|
||||||
|
|
||||||
|
First to establish a baseline, here is a starting point script which
|
||||||
|
we'll name ``hello.py``::
|
||||||
|
|
||||||
|
print('hello world')
|
||||||
|
|
||||||
|
Run that like so:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
$ python hello.py
|
||||||
|
hello world
|
||||||
|
|
||||||
|
|
||||||
|
Better Standards
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Keeping it simple, but improving that script per recommended patterns::
|
||||||
|
|
||||||
|
def hello():
|
||||||
|
print('hello world')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
hello()
|
||||||
|
|
||||||
|
Runs the same:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
$ python hello.py
|
||||||
|
hello world
|
||||||
|
|
||||||
|
|
||||||
|
Configurability
|
||||||
|
---------------
|
||||||
|
|
||||||
|
If you have a :term:`config file` e.g. named ``my.conf``:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[hello]
|
||||||
|
name = George
|
||||||
|
|
||||||
|
Then you can make a :term:`config object` to access its values. Note
|
||||||
|
that this also gives you access to the :term:`app handler`::
|
||||||
|
|
||||||
|
from wuttjamaican.conf import make_config
|
||||||
|
|
||||||
|
def hello(config):
|
||||||
|
app = config.get_app()
|
||||||
|
print('hello', config.get('hello.name'))
|
||||||
|
print('from', app.appname)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
config = make_config('my.conf')
|
||||||
|
hello(config)
|
||||||
|
|
||||||
|
You are likely to need more imports; it is generally wise to do those
|
||||||
|
*within the function* as opposed to the top of the module. This is to
|
||||||
|
ensure the :func:`~wuttjamaican.conf.make_config()` call happens
|
||||||
|
before all packages are imported::
|
||||||
|
|
||||||
|
from wuttjamaican.conf import make_config
|
||||||
|
|
||||||
|
def hello(config):
|
||||||
|
|
||||||
|
# do extra imports here
|
||||||
|
from otherpkg import something
|
||||||
|
|
||||||
|
app = config.get_app()
|
||||||
|
print('hello', config.get('hello.name'))
|
||||||
|
print('from', app.appname)
|
||||||
|
|
||||||
|
something(config)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
config = make_config('my.conf')
|
||||||
|
hello(config)
|
||||||
|
|
||||||
|
Output should now be different:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
$ python hello.py
|
||||||
|
hello George
|
||||||
|
from wutta
|
||||||
|
|
||||||
|
|
||||||
|
Logging
|
||||||
|
-------
|
||||||
|
|
||||||
|
Logging behavior is determined by the config file(s). If they contain
|
||||||
|
no directives pertaining to the logging config then some default
|
||||||
|
behavior will be used.
|
||||||
|
|
||||||
|
In any case your script should not need to worry about that, but is
|
||||||
|
free to make logging calls. The configured logging behavior would
|
||||||
|
determine whether such messages are output to the console and/or file
|
||||||
|
etc.
|
||||||
|
|
||||||
|
There are 3 steps to logging:
|
||||||
|
|
||||||
|
* import the :mod:`python:logging` module
|
||||||
|
* call :func:`~python:logging.getLogger()` to get a logger
|
||||||
|
* call methods on the logger, e.g. :meth:`~python:logging.Logger.debug()`
|
||||||
|
|
||||||
|
Here is the script with logging incorporated::
|
||||||
|
|
||||||
|
# nb. it is always safe to import from standard library at the
|
||||||
|
# top of module, that will not interfere with make_config()
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from wuttjamaican.conf import make_config
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.debug("still at top of module")
|
||||||
|
|
||||||
|
def hello(config):
|
||||||
|
|
||||||
|
# do extra imports here
|
||||||
|
from otherpkg import something
|
||||||
|
|
||||||
|
log.debug("saying hello")
|
||||||
|
app = config.get_app()
|
||||||
|
print('hello', config.get('hello.name'))
|
||||||
|
print('from', app.appname)
|
||||||
|
|
||||||
|
log.debug("about to do something")
|
||||||
|
if something(config):
|
||||||
|
log.info("something seems to have worked")
|
||||||
|
else:
|
||||||
|
log.warn("oh no! something failed")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
log.debug("entered the __main__ block")
|
||||||
|
config = make_config('my.conf')
|
||||||
|
log.debug("made config object: %s", config)
|
||||||
|
hello(config)
|
||||||
|
log.debug("all done")
|
5
docs/narr/cli/subcommands.rst
Normal file
5
docs/narr/cli/subcommands.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
Subcommands
|
||||||
|
===========
|
||||||
|
|
||||||
|
TODO
|
|
@ -7,3 +7,4 @@ Documentation
|
||||||
|
|
||||||
install/index
|
install/index
|
||||||
config/index
|
config/index
|
||||||
|
cli/index
|
||||||
|
|
|
@ -49,6 +49,20 @@ class AppHandler:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def appname(self):
|
||||||
|
"""
|
||||||
|
The :term:`app name` for the current app. This is just an
|
||||||
|
alias for :attr:`wuttjamaican.conf.WuttaConfig.appname`.
|
||||||
|
|
||||||
|
Note that this ``appname`` does not necessariy reflect what
|
||||||
|
you think of as the name of your (e.g. custom) app. It is
|
||||||
|
more fundamental than that; your Python package naming and the
|
||||||
|
:term:`app title` are free to use a different name as their
|
||||||
|
basis.
|
||||||
|
"""
|
||||||
|
return self.config.appname
|
||||||
|
|
||||||
def make_appdir(self, path, subfolders=None, **kwargs):
|
def make_appdir(self, path, subfolders=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Establish an :term:`app dir` at the given path.
|
Establish an :term:`app dir` at the given path.
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
"""
|
"""
|
||||||
WuttJamaican - command line interface
|
WuttJamaican - command line interface
|
||||||
|
|
||||||
For convenience, from this ``wuttjamaican.cmd`` namespace you can
|
For convenience, from the ``wuttjamaican.cmd`` namespace you can
|
||||||
access the following:
|
access the following:
|
||||||
|
|
||||||
* :class:`~wuttjamaican.cmd.base.Command`
|
* :class:`~wuttjamaican.cmd.base.Command`
|
||||||
|
|
|
@ -315,9 +315,9 @@ also try: {self.name} <subcommand> -h
|
||||||
version=f"%(prog)s {self.version}")
|
version=f"%(prog)s {self.version}")
|
||||||
|
|
||||||
parser.add_argument('--stdout', metavar='PATH', type=argparse.FileType('w'),
|
parser.add_argument('--stdout', metavar='PATH', type=argparse.FileType('w'),
|
||||||
help="Optional path to which STDOUT should be written.")
|
help="Optional path to which STDOUT should be written")
|
||||||
parser.add_argument('--stderr', metavar='PATH', type=argparse.FileType('w'),
|
parser.add_argument('--stderr', metavar='PATH', type=argparse.FileType('w'),
|
||||||
help="Optional path to which STDERR should be written.")
|
help="Optional path to which STDERR should be written")
|
||||||
|
|
||||||
def make_config(self, args):
|
def make_config(self, args):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -33,7 +33,7 @@ from .base import Subcommand
|
||||||
|
|
||||||
class DateOrganize(Subcommand):
|
class DateOrganize(Subcommand):
|
||||||
"""
|
"""
|
||||||
Organize files in a given directory, according to date
|
Organize files into subfolders according to date
|
||||||
"""
|
"""
|
||||||
name = 'date-organize'
|
name = 'date-organize'
|
||||||
description = __doc__.strip()
|
description = __doc__.strip()
|
||||||
|
|
|
@ -820,9 +820,9 @@ def make_config(
|
||||||
extension_entry_points=None,
|
extension_entry_points=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Make a new config object (presumably for global use), initialized
|
Make a new config (usually :class:`WuttaConfig`) object,
|
||||||
per the given parameters and (usually) further modified by all
|
initialized per the given parameters and (usually) further
|
||||||
registered config extensions.
|
modified by all registered config extensions.
|
||||||
|
|
||||||
This function really does 3 things:
|
This function really does 3 things:
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ class TestAppHandler(TestCase):
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
self.assertIs(self.app.config, self.config)
|
self.assertIs(self.app.config, self.config)
|
||||||
self.assertEqual(self.app.handlers, {})
|
self.assertEqual(self.app.handlers, {})
|
||||||
|
self.assertEqual(self.app.appname, 'wuttatest')
|
||||||
|
|
||||||
def test_make_appdir(self):
|
def test_make_appdir(self):
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue