Skip to main content

Build powerful CLIs with simple idiomatic Python, driven by type hints. Not all arguments are bad.

Project description

Feud

Not all arguments are bad.

Build powerful CLIs with simple idiomatic Python, driven by type hints.

About · Features · Installation · Build status · Documentation · Related projects · Contributing · Licensing


About

[!CAUTION]
Writing command-line interfaces can get messy!

It is not uncommon for CLIs to consist of many commands, subcommands, arguments, options and aliases, on top of dealing with other aspects such as documentation and input types when it comes to argument parsing.

Designing such an interface can quickly spiral into chaos without the help of an intuitive CLI builder.

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.

- import click
+ import feud
+ from typing import Literal

- @click.command
+ @feud.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.\f
+
+     Parameters
+     ----------
+     port:
+         Server port.
+     watch:
+         Watch source code for changes.
+     env:
+         Environment mode.
+     """

Let's take a closer look at the Feud implementation.

Example: Command for running a HTTP web server.

# serve.py

import feud
from typing import Literal

def serve(port: int, *, watch: bool = True, env: Literal["dev", "prod"] = "dev"):
    """Start a local HTTP server.\f

    Parameters
    ----------
    port:
        Server port.
    watch:
        Watch source code for changes.
    env:
        Environment mode.
    """

if __name__ == "__main__":
    feud.run(serve)
Click here to view the generated help screen.

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.      │
╰────────────────────────────────────────────────────────────────────╯

Click here to see usage examples.

  • python serve.py 8080
  • python serve.py 3000 --watch --env dev
  • python 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 all of 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.

Example: Commands for creating, deleting and listing blog posts.

# 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: list[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)
Click here to view the generated help screen.

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.           │
╰────────────────────────────────────────────────────────────────────╯

Click here to see usage examples.

  • 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 2
  • python post.py list
  • python post.py list --between 2020-01-30 2021-01-30

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.

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

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.Group):
    """Manage blog posts."""

    def create(id: int, *, title: str, desc: str | None = None):
        """Create a blog post."""

    def delete(ids: list[int]):
        """Delete blog posts."""

    def list(*, between: tuple[date, date] | None = None):
        """View all blog posts, optionally filtering by date range."""

Blog.register(Post)

if __name__ == "__main__":
    feud.run(Blog)
Click here to view the generated help screen.

Help screen for the blog group.

$ python blog.py --help

 Usage: blog.py [OPTIONS] COMMAND [ARGS]...

 Manage and serve a blog.

╭─ Options ──────────────────────────────────────────────────────────╮
│ --help      Show this message and exit.                            │
╰────────────────────────────────────────────────────────────────────╯
╭─ Commands ─────────────────────────────────────────────────────────╮
│ post         Manage blog posts.                                    │
│ serve        Start a local HTTP server.                            │
╰────────────────────────────────────────────────────────────────────╯

Help screen for the serve command in the blog group.

$ python blog.py serve --help

 Usage: blog.py serve [OPTIONS] PORT

 Start a local HTTP server.

╭─ Arguments ────────────────────────────────────────────────────────╮
│ *  PORT    INTEGER  [required]                                     │
╰────────────────────────────────────────────────────────────────────╯
╭─ Options ──────────────────────────────────────────────────────────╮
│ --watch/--no-watch                [default: watch]                 │
│ --env                 [dev|prod]  [default: dev]                   │
│ --help                            Show this message and exit.      │
╰────────────────────────────────────────────────────────────────────╯

Help screen for the post sub-group in the blog group.

$ python blog.py post --help

 Usage: blog.py post [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 sub-group.

$ python blog.py post list --help

 Usage: blog.py post list [OPTIONS]

 View all blog posts, optionally filtering by date range.

╭─ Options ──────────────────────────────────────────────────────────╮
│ --between    <DATE DATE>...                                        │
│ --help                       Show this message and exit.           │
╰────────────────────────────────────────────────────────────────────╯

Click here to see usage examples.

  • python blog.py serve 8080 --no-watch --env prod
  • python blog.py post create 1 --title "My First Post!"
  • python blog.py post list --between 2020-01-30 2021-01-30

Powerful typing

Feud is powered by Pydantic – a validation library with extensive support for many data types, including:

  • simple types such as integers and dates,
  • complex types such as emails, IP addresses, file/directory paths, database connection strings,
  • constrained types (e.g. positive/negative integers or past/future dates).

pydantic-extra-types is an optional dependency offering additional types such as:

  • country names,
  • payment card numbers,
  • phone numbers,
  • colours,
  • latitude/longitude.

Custom annotated types with user-defined validation functions can also be defined with Pydantic.

Example: Command for generating audio samples from text prompts using a machine learning model, and storing produced audio files in an output directory.

# generate.py

import feud
from pydantic import FilePath, DirectoryPath, conlist, constr

def generate(
    prompts: conlist(constr(max_length=12), min_length=1, max_length=5),
    *,
    model: FilePath,
    output: DirectoryPath,
):
    """Generates audio from prompts using a trained model."""

if __name__ == "__main__":
    feud.run(generate)
Click here to view the generated help screen.

Help screen for the generate command.

$ python generate.py --help

 Usage: generate.py [OPTIONS] [PROMPTS]...

 Generates audio from prompts using a trained model.

╭─ Arguments ────────────────────────────────────────────────────────╮
│ PROMPTS    TEXT                                                    │
╰────────────────────────────────────────────────────────────────────╯
╭─ Options ──────────────────────────────────────────────────────────╮
│ *  --model     FILE       [required]                               │
│ *  --output    DIRECTORY  [required]                               │
│    --help                 Show this message and exit.              │
╰────────────────────────────────────────────────────────────────────╯

Click here to see usage examples.

If we run the script without prompts, we get an error that at least one prompt must be provided.

$ python generate.py --model models/real_model.pt --output audio/

 Usage: generate.py [OPTIONS] [PROMPTS]...

 Try 'generate.py --help' for help
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ 1 validation error for command 'generate'                                    │
│ [PROMPTS]...                                                                 │
│   List should have at least 1 item after validation, not 0 [input_value=()]  │
╰──────────────────────────────────────────────────────────────────────────────╯

If we provide a prompt longer than 12 characters, we also get an error.

$ python generate.py "dog barking" "cat meowing" "fish blubbing" --model models/real_model.pt --output audio/

 Usage: generate.py [OPTIONS] [PROMPTS]...

 Try 'generate.py --help' for help
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ 1 validation error for command 'generate'                                    │
│ [PROMPTS]... [2]                                                             │
│   String should have at most 12 characters [input_value='fish blubbing']     │
╰──────────────────────────────────────────────────────────────────────────────╯

FilePath indicates that the file must already exist, so we get an error if we provide a non-existent file.

$ python generate.py "dog barking" "cat meowing" --model models/fake_model.pt

 Usage: generate.py [OPTIONS] [PROMPTS]...

 Try 'generate.py --help' for help
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ Invalid value for '--model': File 'models/fake_model.pt' does not exist.     │
╰──────────────────────────────────────────────────────────────────────────────╯

DirectoryPath indicates that the path must be a directory, so we get an error if we provide a file.

$ python generate.py "dog barking" "cat meowing" --output audio.txt

 Usage: generate.py [OPTIONS] [PROMPTS]...

 Try 'generate.py --help' for help
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ Invalid value for '--output': Directory 'audio.txt' is a file.               │
╰──────────────────────────────────────────────────────────────────────────────╯

Highly configurable and extensible

[!IMPORTANT]
Feud is not the new Click - it is an extension of Click and directly depends it.

While designed to be simpler than Click, this comes with the trade-off that Feud is also more opinionated than Click and only directly implements a subset of its functionality.

However, Feud was designed to allow for Click to seamlessly slot in whenever manual overrides are necessary.

Example: Use click.password_option to securely prompt the user for a password, but still validate based on the type hint (length should be ≥ 10 characters).

# login.py

import feud
from feud import click
from pydantic import constr

@click.password_option("--password", help="The user's password (≥ 10 characters).")
def login(*, username: str, password: constr(min_length=10)):
    """Log in as a user.\f

    Parameters
    ----------
    username:
        The user's username.
    """

if __name__ == "__main__":
    feud.run(login)
Click here to view the generated help screen.

Help screen for the login command.

$ python login.py --help

 Usage: login.py [OPTIONS]

 Log in as a user.

╭─ Options ──────────────────────────────────────────────────────────╮
│ *  --username    TEXT  The user's username. [required]             │
│    --password    TEXT  The user's password (≥ 10 characters).      │
│    --help              Show this message and exit.                 │
╰────────────────────────────────────────────────────────────────────╯

Click here to see usage examples.

$ python login.py --username alice

Password: ***
Repeat for confirmation: ***

 Usage: login.py [OPTIONS]

 Try 'login.py --help' for help
╭─ Error ────────────────────────────────────────────────────────────╮
│ 1 validation error for command 'login'                             │
│ --password                                                         │
│   String should have at least 10 characters [input_value=hidden]   │
╰────────────────────────────────────────────────────────────────────╯

Installation

You can install Feud using pip.

The latest stable version of Feud can be installed with the following command.

pip install feud[all]

This installs Feud with the optional dependencies:

  • rich-click (can install individually with pip install feud[rich])
    Provides improved formatting for CLIs produced by Feud.
  • pydantic-extra-types (can install individually with pip install feud[extra-types])
    Provides additional types that can be used as type hints for Feud commands.

To install Feud without any optional dependencies, simply run pip install feud.

Improved formatting with Rich

Below is a demonstration of the difference between using Feud with and without rich-click.

With Rich Without Rich

Build status

master dev
CircleCI Build (Master) CircleCI Build (Development)

Documentation

  • API reference: Library documentation for public modules, classes and functions.

Related projects

Feud either relies heavily on, or was inspired by the following packages. It would be greatly appreciated if you also supported the below maintainers and the work they have done that Feud has built upon.

Click

by @pallets

Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary.

Feud is essentially a wrapper around Click that takes classes and functions with type hints and intelligently 'compiles' them into a ready-to-use Click generated CLI.

Rich Click

by @ewels

Richly rendered command line interfaces in click.

A shim around Click that renders help output nicely using Rich.

Pydantic

by @samuelcolvin

Data validation using Python type hints.

Pydantic is a validation package that makes it easy to declaratively validate input data based on type hints.

The package offers support for common standard library types (e.g. int, float, str, date/datetime), plus more complex types which can also be used as type hints in Feud commands for input validation.

Typer

by @tiangolo

Typer is a library for building CLI applications that users will love using and developers will love creating.

Typer shares a similar ideology to Feud, in that building CLIs should be simple and not require learning new functions or constantly referring to library documentation. Typer is also based on Click.

One source of motivation for creating Feud is that at the time of creation, Pydantic was not yet supported as a type system for Typer. It is worth noting that Pydantic as an optional dependency is on Typer's tentative roadmap, so it will with no doubt be interesting to see how the implementation compares to Feud!

Typer is a more complete library for building CLIs overall, but currently lacks support for more complex types such as those offered by Pydantic.

Contributing

All contributions to this repository are greatly appreciated. Contribution guidelines can be found here.

We're living in an imperfect world!
Feud is in a public beta-test phase, likely with lots of bugs. Please leave feedback if you come across anything strange!

Licensing

Feud is released under the MIT license.


Feud © 2023-2025, Edwin Onuonga - Released under the MIT license.
Authored and maintained by Edwin Onuonga.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

feud-0.1.0a9.tar.gz (46.3 kB view hashes)

Uploaded Source

Built Distribution

feud-0.1.0a9-py3-none-any.whl (48.8 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page