2
0
Fork 0

Add a large chunk of the docs for command line interface

will have to finish subcommands later
This commit is contained in:
Lance Edgar 2023-11-22 21:40:26 -06:00
parent 8a4438c725
commit af4c28b286
15 changed files with 371 additions and 19 deletions

View file

@ -4,3 +4,20 @@
.. 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

@ -9,10 +9,6 @@
app
cmd
cmd.base
cmd.date_organize
cmd.make_appdir
cmd.setup
conf
db
db.conf

View file

@ -6,9 +6,13 @@ Glossary
.. glossary::
:sorted:
ad hoc script
Python script (text) file used for ad-hoc automation etc. See
also :doc:`narr/cli/scripts`.
app
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
The main database used by the :term:`app`. There is normally
@ -26,15 +30,19 @@ Glossary
:class:`~wuttjamaican.app.AppHandler`.
app name
The code-friendly name for the :term:`app`
(e.g. ``wutta_poser``). This is available on the :term:`config
object` within Python as
:attr:`~wuttjamaican.conf.WuttaConfig.appname`.
Code-friendly name for the underlying app/config system
(e.g. ``wutta_poser``).
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`.
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`.
@ -42,7 +50,7 @@ Glossary
A top-level command line interface for the app. Note that
top-level commands don't really "do" anything per se, and are
mostly a way to group :term:`subcommands<subcommand>`. See also
:class:`~wuttjamaican.commands.base.Command`.
:class:`~wuttjamaican.cmd.base.Command`.
config
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
: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
Table in the :term:`app database` which is used to store
:term:`config settings<config setting>`.
@ -74,4 +93,4 @@ Glossary
A top-level :term:`command` may expose one or more subcommands,
for the overall command line interface. Subcommands are the real
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
View 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
View file

@ -0,0 +1,11 @@
Command Line Interface
======================
.. toctree::
:maxdepth: 2
overview
commands
subcommands
scripts

View 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
View 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")

View file

@ -0,0 +1,5 @@
Subcommands
===========
TODO

View file

@ -7,3 +7,4 @@ Documentation
install/index
config/index
cli/index

View file

@ -49,6 +49,20 @@ class AppHandler:
self.config = config
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):
"""
Establish an :term:`app dir` at the given path.

View file

@ -23,7 +23,7 @@
"""
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:
* :class:`~wuttjamaican.cmd.base.Command`

View file

@ -315,9 +315,9 @@ also try: {self.name} <subcommand> -h
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.")
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.")
help="Optional path to which STDERR should be written")
def make_config(self, args):
"""

View file

@ -33,7 +33,7 @@ from .base import Subcommand
class DateOrganize(Subcommand):
"""
Organize files in a given directory, according to date
Organize files into subfolders according to date
"""
name = 'date-organize'
description = __doc__.strip()

View file

@ -820,9 +820,9 @@ def make_config(
extension_entry_points=None,
**kwargs):
"""
Make a new config object (presumably for global use), initialized
per the given parameters and (usually) further modified by all
registered config extensions.
Make a new config (usually :class:`WuttaConfig`) object,
initialized per the given parameters and (usually) further
modified by all registered config extensions.
This function really does 3 things:

View file

@ -24,6 +24,7 @@ class TestAppHandler(TestCase):
def test_init(self):
self.assertIs(self.app.config, self.config)
self.assertEqual(self.app.handlers, {})
self.assertEqual(self.app.appname, 'wuttatest')
def test_make_appdir(self):