Skip to content

Commit

Permalink
docs: added an article on the history and future of Argh
Browse files Browse the repository at this point in the history
  • Loading branch information
neithere committed Oct 21, 2023
1 parent a7b46cf commit 6c20fe4
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Details
tutorial
reference
cookbook
the_story
similar
projects
subparsers
Expand Down
14 changes: 12 additions & 2 deletions docs/source/similar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ supports Python3. Not every "yes" in this table would count as pro.
* opster_ and finaloption_ support nested commands but are based on the
outdated `optparse` library and therefore reimplement some features available
in `argparse`. They also introduce decorators that don't just decorate
functions but change their behaviour, which is bad practice.
functions but change their behaviour, which is a questionable practice.
* simpleopt_ has an odd API and is rather a simple replacement for standard
libraries than an extension.
* opterator_ is based on the outdated `optparse` and does not support nested
Expand All @@ -36,11 +36,20 @@ supports Python3. Not every "yes" in this table would count as pro.
worth migrating but it is surely very flexible and easy to use.
* baker_
* plumbum_
* docopt_
* docopt_ takes an inverted approach: you write the usage docs, it generates a
parser. Then you need to wire the parsing results into you code manually.
* aaargh_
* cliff_
* cement_
* autocommand_
* click_ is a rather popular library, a bit younger than Argh. The authors of
both libraries even gave lightning talks on a PyCon within a few minutes :)
Although I expected it to kill Argh because it comes with Flask, in fact
it takes an approach so different from Argh that they can coexist.
Like Opster, Click's decorator replaces the underlying function (a
questionable practice); it does not derive the CLI arguments from the
function signature but entirely relies on additional decorators, while Argh
strives for the opposite.

.. _argdeclare: http://code.activestate.com/recipes/576935-argdeclare-declarative-interface-to-argparse/
.. _argparse-cli: http://code.google.com/p/argparse-cli/
Expand All @@ -59,3 +68,4 @@ supports Python3. Not every "yes" in this table would count as pro.
.. _cliff: http://pypi.python.org/pypi/cliff
.. _cement: http://builtoncement.com/2.0/
.. _autocommand: https://pypi.python.org/pypi/autocommand/
.. _click: https://click.palletsprojects.com
121 changes: 121 additions & 0 deletions docs/source/the_story.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
The Story of Argh
~~~~~~~~~~~~~~~~~

Early history
-------------

Argh was first drafted by Andy in the airport while waiting for his flight.
The idea was to make a simplified wrapper for Argparse with support for nested
commands. We'll focus on the function arguments vs. CLI arguments here.

This is what Argh began with (around 2010)::

@arg("path", description="path to the file to load")
@arg("--file-format", choices=["yaml", "json"], default="json")
@arg("--dry-run", default=False)
def load(args):
do_something(args.path, args.file_format, args.dry_run)

argh.dispatch_command(load)

You don't have to remember the details of the underlying Argparse interface
(especially for subparsers); you would still declare almost everything, but in
one place, close to the function itself.

"The Natural Way"
-----------------

In late 2012 the behaviour previously available via `@plain_signature`
decorator became standard::


@arg("path", help="path to the file to load")
@arg("--file-format", choices=["yaml", "json"])
def load(path, file_format="json", dry_run=False):
do_something(path, file_format, dry_run)

argh.dispatch_command(load)

This unleashed the killer feature of Argh: now you can write normal functions —
not for argparse but general-purpose ones. Argh would infer the basic CLI
argument definitions straight from the function signature. The types and some
actions (e.g. `store_true`) would be inferred from defaults. You would only need
to use the `@arg` decorator to enhance the information with something that had
no place in function signature of the Python 2.x era.

There's still an little ugly thing about it: you have to mention the argument
name twice, in function signature and the decorator. Also the type cannot be
inferred if there's no default value, so you'd have to use the decorator even
for that.

Hiatus
------

The primary author's new job required focus on other languages for a number of
years and he had no energy to develop his FOSS projects, although he continued
using Argh for his own purposes on a daily basis.

A few forks were created by other developers but none survived. (The forks,
not developers.)

By coincidence, around the beginning of this period a library called Click was
shipped with Flask and it seemed obvious that it will become the new standard
for simple CLI APIs and Argh wouldn't be needed. (Plot twist: it did become
popular but its goals are too different from Argh's to replace it.)

Revival
-------

The author returned to his FOSS projects in early 2023. To his surprise, Argh
was all but dead and its niche as the "natural API" was not occupied by any
other project. It actually made sense to revive it. So a deep modernisation
and refactoring began.

A number of pending issues were resolved and the last version to support
Python 2.x was released with a bunch of bugfixes.

The next few releases have deprecated and removed a lot of outdated features
and paved the way to a better Argh. Some design decisions have been revised
and the streamlined. The work continues.

Goodbye Decorators
------------------

As type hints became mature and widespread in Python code, the old approach
with decorators seems to make less and less sense. A lot more can be now
inferred directly from the signature. In fact, possibly everything.

Here's what Argh is heading for (around 2024).

A minimal example (note how there's literally nothing CLI-specific here)::

def load(path: str, *, file_format: str = "json", dry_run: bool = False) -> str:
return do_something(path, file_format, dry_run)

argh.dispatch_command(load)

A more complete example::

from typing import Annotated
from argh import Choices, Help

def load(
path: Annotated[str, Help("path to the file to load")],
*,
file_format: Annotated[str, Choices(FORMAT_CHOICES))] = DEFAULT_FORMAT,
dry_run: bool = False
) -> str:
return do_something(path, file_format, dry_run)

argh.dispatch_command(load)

The syntax is a subject to change but the essence is clear:

* as few surprises to the reader as possible;
* the function is declared in the normal way, like any other function;
* type hints are used instead of ``@arg("foo", type=int)``
* additional metadata can be injected into type hints when necessary in a way
that won't confuse type checkers (like in FastAPI_, requires Python 3.9+);
* non-kwonly become CLI positionals, kwonly become CLI options.

.. _FastAPI: https://fastapi.tiangolo.com/python-types/#type-hints-with-metadata-annotations

0 comments on commit 6c20fe4

Please sign in to comment.