arrg
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
Enumclasses,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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| arrg-0.1.1.tar.gz | 17.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.