Skip to main content

A library for building modular command-line applications

Project description

arrg

pypi ci downloads

arrg is a Python library for building modular command-line applications using a declarative, class-based approach. It leverages Python type hints and decorators to simplify the creation of complex command-line interfaces with arguments and subcommands, while maintaining compatibility with the standard argparse library.

Installation

Install the package via the Python package manager pip:

pip install arrg

Alternatively, if you use uv, add it to your project:

uv add arrg

Quick Start

Here's a simple example demonstrating the app decorator:

from arrg import app, argument

@app(description="A wonderful command-line interface")
class Arguments:
  input: str = argument()

  def run(self):
    print(self.input)

if __name__ == '__main__':
  Arguments.from_args().run()

The input field defaults to a positional argument with the name input (the field name). Assuming this code lives in a file called main.py, running it with python3 main.py hello will print hello.

Features

Arguments

In arrg, arguments are defined using the argument function on class fields within a class decorated with @app or @subcommand. This function mirrors the add_argument method of a argparse.ArgumentParser, supporting all the familiar parameters like action, nargs, type, choices, default, help, and more.

Arguments can be positional or optional:

from arrg import app, argument

@app
class Arguments:
  input: str = argument()

The argument input here will default as a positional argument with the name input (the field name). Since we're using argparse under the hood, positional and optional arguments are differentiated by name.

Here is another example defining an argument input as an option with a type and a default value.

from arrg import app, argument

@app
class Arguments:
  input: str = argument('--input', type=str, default='foo')

if __name__ == '__main__':
  arguments = Argument.from_args()
  ...

Now you can pass in a --input option to your program and have substituted on your app instance.

Subcommands

Subcommands enable hierarchical command-line interface structures (e.g. git add, git commit). They are defined using the @subcommand decorator and integrated as fields in an @app class.

Here is a basic example:

from arrg import subcommand

@subcommand
class Add:
  numbers: list[float] = argument('--numbers', help='Numbers to add together')

  def run(self):
    print(sum(self.numbers))

Incorporating them into an existing app by adding them as a field looks like:

from arrg import app, argument, subcommand

@subcommand
class Add:
  numbers: list[float] = argument('--numbers', help='Numbers to add together')

  def run(self):
    print(sum(self.numbers))

@app(description='Simple calculator')
class Calculator:
  add: Add

  def run(self):
    if self.add is not None:
      self.add.run()

if __name__ == '__main__':
  Calculator.from_args().run()

Your program will now accept arguments like add --numbers 1 2 3.

This example is present in examples/simple_subcommand.py, try it out!

App inheritance

Apps can inherit from other apps, combining their arguments and subcommands:

@app
class A:
  a: str = argument('--a')

@app
class B(A):
  b: str = argument('--b')

if __name__ == '__main__':
  arguments = B.from_args()
  print(arguments.a + arguments.b)

The fields a and b are accessible from B, so passing in --a foo --b bar will yield foobar.

Subcommands are also inherited:

@subcommand
class C:
  c: str = argument('--c')

@app
class A:
  a: str = argument('--a')
  c: C

@app
class B(A):
  b: str = argument('--b')

if __name__ == '__main__':
  arguments = B.from_args()
  print(arguments.a + arguments.b + arguments.c.c)

Passing in --a foo --b bar c --c baz will yield foobarbaz.

Subcommand inheritance

Like apps, subcommands can also inherit from subcommands. This enables a more modular design for subcommand structures, letting you easily share arguments and behaviours:

@subcommand
class Base:
  quiet: bool = argument('-q', '--quiet', help='Suppress output')
  verbose: bool = argument('-v', '--verbose', help='Enable verbose output')

@subcommand
class Push(Base):
  force: bool = argument('-f', '--force', help='Force push')

@subcommand
class Status(Base):
  all: bool = argument('-a', '--all', help='Show all statuses')

The subcommands Push and Status inherit the options --quiet and --verbose from Base.

Nested subcommands can also benefit from inheritance:

@subcommand
class Base:
  quiet: bool = argument('-q', '--quiet', help='Suppress output')
  verbose: bool = argument('-v', '--verbose', help='Enable verbose output')

@subcommand
class Remote(Base):
  name: str = argument('--name', default='origin')

@app
class Git:
  remote: Remote

if __name__ == '__main__':
  print(Git.from_args())

Passing in remote origin --verbose will yield Git(remote=Remote(quiet=False, verbose=True, name='origin')).

Smart type conversion

arrg automatically converts argument inputs to their annotated types, reducing the need to specify types manually. Supported types include:

  • Primitives: int, float, str, bool
  • Collections: list, dict, tuple, set
  • Optional/Union: Optional[T], Union[T1, T2, ...]
  • Custom Types: datetime.date, datetime.time, uuid.UUID, pathlib.Path, ipaddress.IPv4Address, ipaddress.IPv6Address, re.Pattern
  • Enums and Literals: Custom Enum classes, Literal['a', 'b']

For instance, arrg will automatically resolve your union types:

@app
class Arguments:
  input: t.Union[int, str] = argument('--input')

  def run(self):
    print(f"{self.input} ({type(self.input).__name__})")

if __name__ == '__main__':
  Arguments.from_args().run()
  • --input 42 => 42 (int)
  • --input hello => hello (str)

It will also handle your list types:

@app
class Arguments:
  numbers: list[int] = argument('--numbers')

if __name__ == '__main__':
  print(Arguments.from_args())

Passing in --numbers 1 2 3 will yield Arguments(numbers=[1, 2, 3]).

Of course, you can opt out of these smart type conversion features by specifying the type for arguments yourself.

Argparse API compatibility

arrg aligns with the argparse API for familiarity and interoperability.

As mentioned before, the argument accepts add_argument parameters on an argparse.ArgumentParser instance:

@app
class Arguments:
  verbose: bool = argument('--verbose', action='store_true', help='Verbose output')

Moreover, the @app decorator accepts argparse.ArgumentParser parameters:

@app(description='My app', epilog='More info', prog='mycli')
class Arguments:
  pass

Running --help will display the custom description and epilog.

The @subcommand decorator supports similar options:

@subcommand(name='pr', help='Create pull request', description='Detailed PR creation')
class PullRequest:
  title: str = argument('--title')

These get added to their respective subparser instances.

Prior Art

This library is heavily indebted to the rust crate structopt, for which heavy inspiration was drawn from.

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

arrg-0.1.1.tar.gz (17.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

arrg-0.1.1-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

File details

Details for the file arrg-0.1.1.tar.gz.

File metadata

  • Download URL: arrg-0.1.1.tar.gz
  • Upload date:
  • Size: 17.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.14

File hashes

Hashes for arrg-0.1.1.tar.gz
Algorithm Hash digest
SHA256 c1910ee2d4fed11861807bb7e0e92cb307d67ed50b20e3ee2d110b17bf2c3771
MD5 f311ca31b6e0ce34d078d93113c4b53b
BLAKE2b-256 3210898f0c17cec88869abf7dba7580c61fbc3f8e14d880c5b287cccf86b900a

See more details on using hashes here.

File details

Details for the file arrg-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: arrg-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 14.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.14

File hashes

Hashes for arrg-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bbf8f04d4eb9c9eaf5789882251dea2f7d7cfffd558d734caa2e087862e1e03d
MD5 5aa5320ee738d5373a1327dd92767858
BLAKE2b-256 d8b15790e85caab6ee4eea1ab48b940846d51acf32fbd58f1b14422a5511fa73

See more details on using hashes here.

Supported by

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