Skip to main content

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.

Metadata

Release files for arrg 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for arrg 0.1.1
File Size Uploaded Provenance
arrg-0.1.1.tar.gz 17.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for arrg 0.1.1
File Interpreter ABI Platform
arrg-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 31.4 kB

Release files / arrg-0.1.1.tar.gz

Download URL arrg-0.1.1.tar.gz
Size 17.1 kB
Tags Source
SHA-256 checksum
How to use checksums
c1910ee2d4fed11861807bb7e0e92cb307d67ed50b20e3ee2d110b17bf2c3771
BLAKE2b-256 checksum
How to use checksums
3210898f0c17cec88869abf7dba7580c61fbc3f8e14d880c5b287cccf86b900a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.6.14

Release files / arrg-0.1.1-py3-none-any.whl

Download URL arrg-0.1.1-py3-none-any.whl
Size 14.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bbf8f04d4eb9c9eaf5789882251dea2f7d7cfffd558d734caa2e087862e1e03d
BLAKE2b-256 checksum
How to use checksums
d8b15790e85caab6ee4eea1ab48b940846d51acf32fbd58f1b14422a5511fa73
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.6.14

Security for arrg 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page