Feud
Build powerful CLIs with simple idiomatic Python, driven by type hints. Not all arguments are bad.
Install / Use
/learn @eonu/FeudREADME
About
Designing a good CLI can quickly spiral into chaos without the help of an intuitive CLI framework.
Feud builds on Click for argument parsing, along with Pydantic for typing, to make CLI building a breeze.
Features
Simplicity
Click is often considered the defacto command-line building utility for Python –
offering far more functionality and better ease-of-use than the standard
library's argparse.
Despite this, for even the simplest of CLIs, code written using Click can be
somewhat verbose and often requires frequently looking up documentation.
Consider the following example command for serving local files on a HTTP server.
In red is a typical Click implementation, and in green is the Feud equivalent.
<table> <tr> <td>Example: Command for running a HTTP web server.
</td> </tr> <tr> <td># serve.py
- import click
+ import feud
+ from typing import Literal
- @click.command
- @click.argument("port", type=int, help="Server port.")
- @click.option("--watch/--no-watch", type=bool, default=True, help="Watch source code for changes.")
- @click.option("--env", type=click.Choice(["dev", "prod"]), default="dev", help="Environment mode.")
- def serve(port, watch, env):
+ def serve(port: int, *, watch: bool = True, env: Literal["dev", "prod"] = "dev"):
- """Start a local HTTP server."""
+ """Start a local HTTP server.
+
+ Parameters
+ ----------
+ port:
+ Server port.
+ watch:
+ Watch source code for changes.
+ env:
+ Environment mode.
+ """
if __name__ == "__main__":
- serve()
+ feud.run(serve)
</td>
</tr>
<tr>
<td>
<details>
<summary>
<b>Click here to view the generated help screen.</b>
</summary>
<p>
Help screen for the serve command.
$ python serve.py --help
Usage: serve.py [OPTIONS] PORT
Start a local HTTP server.
╭─ Arguments ────────────────────────────────────────────────────────╮
│ * PORT INTEGER [required] │
╰────────────────────────────────────────────────────────────────────╯
╭─ Options ──────────────────────────────────────────────────────────╮
│ --watch/--no-watch Watch source code for changes. │
│ [default: watch] │
│ --env [dev|prod] Environment mode. [default: dev] │
│ --help Show this message and exit. │
╰────────────────────────────────────────────────────────────────────╯
</p>
</details>
</td>
</tr>
<tr>
<td>
<details>
<summary>
<b>Click here to see usage examples.</b>
</summary>
<p>
python serve.py 8080python serve.py 3000 --watch --env devpython serve.py 4567 --no-watch --env prod
The core design principle behind Feud is to make it as easy as possible for even beginner Python developers to quickly create sophisticated CLIs.
The above function is written in idiomatic Python, adhering to language standards and using basic core language features such as type hints and docstrings to declare the relevant information about the CLI, but relying on Feud to carry out the heavy lifting of converting these language elements into a fully-fledged CLI.
Grouping commands
While a single command is often all that you need, Feud makes it straightforward to logically group together related commands into a group represented by a class with commands defined within it.
<table> <tr> <td>Example: Commands for creating, deleting and listing blog posts.
</td> </tr> <tr> <td># post.py
import feud
from datetime import date
class Post(feud.Group):
"""Manage blog posts."""
def create(id: int, *, title: str, desc: str | None = None):
"""Create a blog post."""
def delete(*ids: int):
"""Delete blog posts."""
def list(*, between: tuple[date, date] | None = None):
"""View all blog posts, optionally filtering by date range."""
if __name__ == "__main__":
feud.run(Post)
</td>
</tr>
<tr>
<td>
<details>
<summary>
<b>Click here to view the generated help screen.</b>
</summary>
<p>
Help screen for the post group.
$ python post.py --help
Usage: post.py [OPTIONS] COMMAND [ARGS]...
Manage blog posts.
╭─ Options ──────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰────────────────────────────────────────────────────────────────────╯
╭─ Commands ─────────────────────────────────────────────────────────╮
│ create Create a blog post. │
│ delete Delete blog posts. │
│ list View all blog posts, optionally filtering by date range. │
╰────────────────────────────────────────────────────────────────────╯
Help screen for the list command within the post group.
$ python post.py list --help
Usage: post.py list [OPTIONS]
View all blog posts, optionally filtering by date range.
╭─ Options ──────────────────────────────────────────────────────────╮
│ --between <DATE DATE>... │
│ --help Show this message and exit. │
╰────────────────────────────────────────────────────────────────────╯
</p>
</details>
</td>
</tr>
<tr>
<td>
<details>
<summary>
<b>Click here to see usage examples.</b>
</summary>
<p>
python post.py create 1 --title "My First Post"python post.py create 2 --title "My First Detailed Post" --desc "Hi!"python post.py delete 1 2python post.py listpython post.py list --between 2020-01-30 2021-01-30
Alternatively, if you already have some functions defined that you would like
to run as commands, you can simply provide them to feud.run and it will
automatically generate and run a group with those commands.
# post.py
import feud
from datetime import date
def create_post(id: int, *, title: str, desc: str | None = None):
"""Create a blog post."""
def delete_posts(*ids: int):
"""Delete blog posts."""
def list_posts(*, between: tuple[date, date] | None = None):
"""View all blog posts, optionally filtering by date range."""
if __name__ == "__main__":
feud.run([create_post, delete_posts, list_posts])
You can also use a dict to rename the generated commands:
feud.run({"create": create_post, "delete": delete_posts, "list": list_posts})
For more complex applications, you can also nest commands in sub-groups:
feud.run({"list": list_posts, "modify": [create_post, delete_posts]})
If commands are defined in another module, you can also run the module directly and Feud will pick up all runnable objects:
import post
feud.run(post)
You can even call feud.run() without providing any object, and it will
automatically discover all runnable objects in the current module.
As you can see, building a CLI using Feud does not require learning many new magic methods or a domain-specific language – you can just use the simple Python you know and ❤️!
Registering command sub-groups
Groups can be registered as sub-groups under other groups. This is a common pattern in CLIs, allowing for interfaces packed with lots of functionality, but still organized in a sensible way.
<table> <tr> <td>Example: CLI with the following structure for running and managing a blog.
blog: Group to manage and serve a blog.serve: Command to run the blog HTTP server.post: Sub-group to manage blog posts.create: Command to create a blog post.delete: Command to delete blog posts.list: Command to view all blog posts.
# blog.py
import feud
from datetime import date
from typing import Literal
class Blog(feud.Group):
"""Manage and serve a blog."""
def serve(port: int, *, watch: bool = True, env: Literal["dev", "prod"] = "dev"):
"""Start a local HTTP server."""
class Post(feud.Gr
