A library for building modular command-line applications
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arrg-0.1.0.tar.gz.
File metadata
- Download URL: arrg-0.1.0.tar.gz
- Upload date:
- Size: 17.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.6.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58746665a54c361ee1a7b39f414b0bf373f5255d37cc938b22bbbe49daeda155
|
|
| MD5 |
653334f69f12344877f10f06bbbbcc49
|
|
| BLAKE2b-256 |
9053862db83f09ac980a4af0c3d40d423f07e5cad300730a0fea117040e3e62f
|
File details
Details for the file arrg-0.1.0-py3-none-any.whl.
File metadata
- Download URL: arrg-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.6.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b4315b6f5339d6b264808614b20f6a123359532d47b9a0eeecdf10f56cfd8fb
|
|
| MD5 |
0b51fcc7ceaf3c711bf63d912d081d38
|
|
| BLAKE2b-256 |
5ba354cfbbae8928e812fefac1cb1272a5819b26863ae4873728ca809af3b9b5
|