feat: add wutta
top-level command with make-uuid
subcommand
i think it only makes sense to have an "opinion" for command line interface in this project, and we probably need more `wutta` subcommands too but we'll see. main motivation for this currently is to allow poser apps to define their own CLI, in particular e.g. `poser install`
This commit is contained in:
parent
cb147c203d
commit
2deba45588
20 changed files with 446 additions and 47 deletions
41
docs/narr/cli/builtin.rst
Normal file
41
docs/narr/cli/builtin.rst
Normal file
|
@ -0,0 +1,41 @@
|
|||
|
||||
Built-in Commands
|
||||
=================
|
||||
|
||||
WuttJamaican comes with one top-level :term:`command`, and some
|
||||
:term:`subcommands<subcommand>`.
|
||||
|
||||
It uses `Typer`_ for the underlying CLI framework.
|
||||
|
||||
.. _Typer: https://typer.tiangolo.com/
|
||||
|
||||
|
||||
``wutta``
|
||||
---------
|
||||
|
||||
This is the top-level command. Its purpose is to expose subcommands
|
||||
pertaining to WuttJamaican.
|
||||
|
||||
It is installed to the virtual environment in the ``bin`` folder (or
|
||||
``Scripts`` on Windows):
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
cd /path/to/venv
|
||||
bin/wutta --help
|
||||
|
||||
Defined in: :mod:`wuttjamaican.cli`
|
||||
|
||||
.. program-output:: wutta --help
|
||||
|
||||
|
||||
.. _wutta-make-uuid:
|
||||
|
||||
``wutta make-uuid``
|
||||
-------------------
|
||||
|
||||
Print a new universally-unique identifier to standard output.
|
||||
|
||||
Defined in: :mod:`wuttjamaican.cli.make_uuid`
|
||||
|
||||
.. program-output:: wutta make-uuid --help
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
Commands
|
||||
========
|
||||
|
||||
WuttJamaican in fact does not directly provide a way to define a
|
||||
command line interface for your app.
|
||||
|
||||
The reason is that several good frameworks exist already. You are
|
||||
encouraged to use one of the following to define
|
||||
:term:`commands<command>` and :term:`subcommands<subcommand>` as
|
||||
needed:
|
||||
|
||||
* `Typer <https://typer.tiangolo.com/>`_
|
||||
* `Click <https://click.palletsprojects.com/en/latest/>`_
|
||||
* :mod:`python:argparse`
|
||||
|
||||
For even more options see:
|
||||
|
||||
* `awesome-cli-framework <https://github.com/shadawck/awesome-cli-frameworks/blob/master/README.md#python>`_
|
||||
* `Hitchhiker’s Guide to Python <https://docs.python-guide.org/scenarios/cli/>`_
|
||||
* `Python Wiki <https://wiki.python.org/moin/CommandlineTools>`_
|
||||
|
||||
Or if that is overkill you can always just use :doc:`scripts`.
|
105
docs/narr/cli/custom.rst
Normal file
105
docs/narr/cli/custom.rst
Normal file
|
@ -0,0 +1,105 @@
|
|||
|
||||
Custom Commands
|
||||
===============
|
||||
|
||||
WuttJamaican comes with :doc:`/narr/cli/builtin`.
|
||||
|
||||
Using the same framework, each :term:`package` can define additional
|
||||
top-level :term:`command(s)<command>` and
|
||||
:term:`subcommands<subcommand>` as needed.
|
||||
|
||||
|
||||
Top-Level Command
|
||||
-----------------
|
||||
|
||||
You must "define" *and* "register" your top-level command. Assuming a
|
||||
basic Poser example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
poser-project
|
||||
├── poser
|
||||
│ ├── __init__.py
|
||||
│ └── cli.py
|
||||
└── pyproject.toml
|
||||
|
||||
Add the command definition to the ``poser.cli`` module::
|
||||
|
||||
from wuttjamaican.cli import make_typer
|
||||
|
||||
poser_typer = make_typer(
|
||||
name='poser',
|
||||
help="Poser - the killer app"
|
||||
)
|
||||
|
||||
Then register the command as script in ``pyproject.toml``:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
[project.scripts]
|
||||
poser = "poser.cli:poser_typer"
|
||||
|
||||
Then reinstall your project:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install -e ~/src/poser
|
||||
|
||||
And now you can run your command:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
poser --help
|
||||
|
||||
But it won't really do anything until you add subcommands.
|
||||
|
||||
|
||||
Subcommands
|
||||
-----------
|
||||
|
||||
You must "define" the subcommand of course, but do not need to
|
||||
"register" it. (That happens via function decorator; see below.)
|
||||
|
||||
However you *do* need to ensure all modules containing subcommands are
|
||||
"eagerly imported" so the runtime discovery process finds everything.
|
||||
|
||||
Here we'll define the ``poser hello`` subcommand, by adding it to our
|
||||
``poser.cli`` module (from example above)::
|
||||
|
||||
import sys
|
||||
import typer
|
||||
from wuttjamaican.cli import make_typer
|
||||
|
||||
# top-level command
|
||||
poser_typer = make_typer(
|
||||
name='poser',
|
||||
help="Poser - the killer app"
|
||||
)
|
||||
|
||||
# nb. function decorator will auto-register the subcommand
|
||||
@poser_typer.command()
|
||||
def hello(
|
||||
ctx: typer.Context,
|
||||
):
|
||||
"""
|
||||
Hello world example
|
||||
"""
|
||||
config = ctx.parent.wutta_config
|
||||
app = config.get_app()
|
||||
|
||||
name = config.get('hello.name', default="WhoAreYou")
|
||||
sys.stdout.write(f'hello {name}\n')
|
||||
|
||||
title = app.get_title()
|
||||
sys.stdout.write(f'from {title}\n')
|
||||
|
||||
# TODO: you may need to import other modules here, if they contain
|
||||
# subcommands and would not be automatically imported otherwise.
|
||||
# nb. *this* current module *is* automatically imported, only
|
||||
# because of the top-level command registration in pyproject.toml
|
||||
|
||||
No need to re-install, you can now use the subcommand:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
poser hello --help
|
|
@ -2,9 +2,24 @@
|
|||
Command Line Interface
|
||||
======================
|
||||
|
||||
Most apps will need some sort of command line usage, via cron or
|
||||
otherwise. There are two main aspects to it:
|
||||
|
||||
There is a proper CLI framework based on `Typer`_, with top-level
|
||||
:term:`commands<command>` and :term:`subcommands<subcommand>`. The
|
||||
``wutta`` command is built-in and includes some subcommands, but each
|
||||
app can define more of either as needed. Such (sub)commands are
|
||||
installed as part of a :term:`package`.
|
||||
|
||||
.. _Typer: https://typer.tiangolo.com
|
||||
|
||||
But sometimes you just need an :term:`ad hoc script` which is a single
|
||||
file and can be placed anywhere, usually *not* installed as part of a
|
||||
package.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
overview
|
||||
commands
|
||||
builtin
|
||||
custom
|
||||
scripts
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
Overview
|
||||
========
|
||||
|
||||
Many apps will need some sort of command line usage, via cron or
|
||||
otherwise. There are two main aspects to it:
|
||||
|
||||
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
|
||||
:term:`package`. See :doc:`scripts`.
|
||||
|
||||
But a "true" command line interface may define
|
||||
:term:`commands<command>` and :term:`subcommands<subcommand>`, which
|
||||
are then installed as part of a package. See :doc:`commands` for more
|
||||
about that.
|
|
@ -33,7 +33,7 @@ Run that like so:
|
|||
|
||||
|
||||
Better Standards
|
||||
----------------
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Keeping it simple, but improving that script per recommended patterns::
|
||||
|
||||
|
@ -69,7 +69,7 @@ that this also gives you access to the :term:`app handler`::
|
|||
def hello(config):
|
||||
app = config.get_app()
|
||||
print('hello', config.get('hello.name'))
|
||||
print('from', app.appname)
|
||||
print('from', app.get_title())
|
||||
|
||||
if __name__ == '__main__':
|
||||
config = make_config('my.conf')
|
||||
|
@ -81,7 +81,7 @@ Output should now be different:
|
|||
|
||||
$ python hello.py
|
||||
hello George
|
||||
from wutta
|
||||
from WuttJamaican
|
||||
|
||||
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
|
||||
|
@ -97,7 +97,7 @@ before all packages are imported::
|
|||
|
||||
app = config.get_app()
|
||||
print('hello', config.get('hello.name'))
|
||||
print('from', app.appname)
|
||||
print('from', app.get_title())
|
||||
|
||||
something(config)
|
||||
|
||||
|
@ -143,7 +143,7 @@ Here is the script with logging incorporated::
|
|||
log.debug("saying hello")
|
||||
app = config.get_app()
|
||||
print('hello', config.get('hello.name'))
|
||||
print('from', app.appname)
|
||||
print('from', app.get_title())
|
||||
|
||||
log.debug("about to do something")
|
||||
if something(config):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue