Skip to content

Paramater Types

arc uses Python type hints for data conversion / validation.

When Possible, arc uses builtin and standard library data types for arguments. But if no type is available, or the builtin types don't provide the neccessary functionality, arc may implement a custom type.

Standard Libary Types

str

This is considered the default type is no type is specified. str(v) is used which, in most cases, will be comparable to no change

int

arc uses int(v) to convert the value. Note that decimal input (1.4) will result in an error, not a narrowing operation.

float

Likewise, arc uses float(v). Ingeter values will be converted to a float (2 -> 2.0)

bool

Used to denote a Flag

bytes

Converted using v.encode()

Collection Types

list

Collection types (list, set, tuple) can be used to gather multiple values into a single parameter. When used as a positional parameter list acts similarly to *args

list_argument.py
import arc


@arc.command()
def main(names: list = []):
    if not names:
        print("No names :(")
    else:
        for name in names:
            print(name)


main()
$ python list_argument.py 
No names :(

$ python list_argument.py Jonathen Joseph Jotaro
Jonathen
Joseph
Jotaro
Because list can accept any number of values, you won't be able to add additional arguments after names. Any other positional arguments would have to come before names.

When used as an option, it allows the option to be used multiple times:

list_option.py
import arc


@arc.command()
def main(*, names: list = []):
    if not names:
        print("No names :(")
    else:
        for name in names:
            print(name)


main()
$ python list_option.py 
No names :(

$ python list_option.py --names Josuke --names Giorno --names Joylene
Josuke
Giorno
Joylene
Collections can be sub-typed so that each item will be converted to the proper type:
sum.py
import arc


@arc.command()
def main(nums: list[int] = []):
    if not nums:
        print("Provide some integers to sum up")
    else:
        total = 0
        for val in nums:
            total += val
        print("The total is: ", total)


main()
$ python sum.py 1 2 3 4 5 6 7 8 9
The total is:  45

set

Similar to list, but will filter out any non-unique elements.

set_argument.py
import arc


@arc.command()
def main(vals: set):
    print("Unique values:")
    print("\n".join(vals))


main()
$ python set_argument.py 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4
Unique values:
3
1
2
4

tuple

Similar to list, but with some additional functionality.

According to PEP 484:

  • tuple represents an arbitrarily sized tuple of any type. In arc, this will behave the same as list
  • tuple[int, ...] represents an arbitrarily sized tuple of integers. In arc, this will behave the same as list[int]
  • tuple[int, int] represents a size-two tuple of integers. In arc, this behavior is unique to tuple as the parameter will only select 2 values from input.

dict

Allows a list of comma-seperated key-value pairs. Can be typed generically on both keys and values.

dict_argument.py
import arc


@arc.command()
def command(numbers: dict[str, int]):
    print(numbers)


command()
$ python dict_argument.py one=1,two=2,three=3
{'one': 1, 'two': 2, 'three': 3}

typing.Union

Allows the input to be multiple different types.

union_argument.py
from typing import Union
import arc


@arc.command()
def main(number: Union[int, str]):
    print(type(number))


main()
$ python union_argument.py 5
<class 'int'>

$ python union_argument.py hello
<class 'str'>

arc will attempt to coerce the input into each type, from left to right. The first to succeed will be passed along to the command.

Warning

Currently arc's behavior with collections in union types is not defined. As such, it is not reccomended that you give an argument a type similar to typing.Union[list, ...]

typing.Literal

Enforces that the input must be a specific sub-set of values

literal_argument.py
from typing import Literal
import arc


@arc.command()
def main(word: Literal["foo", "bar", 1]):
    print(word, type(word))


main()
$ python literal_argument.py foo
foo <class 'str'>

$ python literal_argument.py 1
1 <class 'int'>

$ python literal_argument.py other
Invalid value for word: must be foo, bar or 1

Note

arc compares the input to the string-ified version of each value in the Literal. So for the second example above, the comparison that succedded was "1" == "1" not 1 == 1.

typing.TypedDict

Constrains a dictionary input to a specific subset of keys and specific value types.

typing.Optional

Indicates an optional parameter with a default of None. The following are functionality equivalent

optional_argument.py
from typing import Optional
from arc import CLI

cli = CLI()


@cli.command()
def c1(val: Optional[int]):
    print(val)


@cli.command()
def c2(val: int = None):
    print(val)


cli()
$ python optional_argument.py c1
None

$ python optional_argument.py c1 1
1

$ python optional_argument.py c2
None

$ python optional_argument.py c2 2
2

pathlib.Path

Path won't perform any validation checks to assert that the input is a valid path, but it just offers the convenience of working with path objects rather than strings. Check the ValidPath custom type for additional validations

enum.Enum

Similar to typing.Literal, restricts the input to a specific sub-set of values

paint.py
from enum import Enum
import arc
from arc.params import Param


class Color(Enum):
    RED = "red"
    YELLOW = "yellow"
    GREEN = "green"


@arc.command()
def paint(color: Color = Param(prompt="What color do you want to paint?")):
    if color == Color.RED:
        print("You painted the walls the bloodiest of reds")
    elif color == Color.YELLOW:
        print("You painted the walls the most fabulous yellow")
    else:
        print("You painted the walls the deepest of greens")


paint()
$ python paint.py red
You painted the walls the bloodiest of reds

$ python paint.py blue
Invalid value for color: must be red, yellow or green

ipaddress.IPv4Address

Uses ipaddress.IPv4Address(v) for conversion, so anything valid there is valid here.

ipaddress.IPv6Address

Same as above

Arc Types

arc provides a variety of additional types exported from the arc.types module:

Warning

Most custom arc types are usable as a valid arc parameter type and as a typical Python class. For example, you can create a SemVar object easily like so: SemVer.parse('1.2.3'). In some particular instances, a type can only be used as a argument type and not as a valid constructor. These types will be denoted with 🛇 following the type name.

Context

Gives you access to the current execution context instance which serves as a central data and functionality object

State

Reference State for details

Range

TODO

SemVer

TODO

User (UNIX ONLY)

A representation of a unix user.

Group (UNIX ONLY)

A representation of a unix group.

File System Types

File

TODO

ValidPath

Subclass of pathlib.Path but asserts that the provided path actually exists

FilePath

Subclass of ValidPath but asserts that the path both exists and is a file

DirectoryPath

Subclass of ValidPath but assets that the path both exists and is a directory

Networking Types

IpAddress 🛇

Can be either IPv4Address or IPv6Address. Equivelant to typing.Union[IPv4Address, IPv6Address]

Url

Parses the strings input using urllib.parse.urlparse

HttpUrl

Url that asserts the scheme to be http or https

PostgresUrl

Url that asserts the scheme to be postgresql or postgres

Number Types

Note

For any types that simply change the base of the input (like Binary or Hex), it is essentially equivelant to int(v, base=<base>).

Binary

Accepts integers as binary stings (0101010110).

Oct

Accepts integers in base 8

Hex

Accepts integers in base 16

PositveInt

Enforces that the integer must be greater than 0

NegativeInt

Enforces that the integer must be less than 0

PositveFloat

Enforces that the float must be greater than 0

NegativeFloat

Enforces that the float must be less than 0

AnyNumber 🛇

Accepts floats, and integers in any base.

String Types

Char

Enforces that the string can only be a single character long

Strict Types

strictstr

strictint

strictfloat

stricturl

strictpath

Custom Types

When implementing your own types, you can make them compatible with arc by implementing the __convert__() class method. Arc will call this with the input from the command line, and you are expected to parse the input and return an instance of the type.

custom_type.py
import arc


class CustomType:
    def __init__(self, val: int):
        self.val = val

    def __str__(self):
        return f"CustomType(val={self.val})"

    @classmethod
    def __convert__(cls, value: str):
        if value.isnumeric():
            return cls(int(value))
        else:
            raise arc.ConversionError(value, "must be an integer")


@arc.command()
def main(foo: CustomType):
    print(foo)


main()
$ python custom_type.py 2
CustomType(val=2)

$ python custom_type.py string
Invalid value for foo: must be an integer

Some notes about custom types:

  • In additon to value, you can also add the following arguments to the signature (in the given order, but the names don't need to match):
  • info: Description of the provided type. Instance of arc.types.TypeInfo
  • ctx: The current execution context. Instance of arc.context.Context
Tip

Any type that implements __enter__() and __exit__(), will be treated like a context manager. The manager will be opened before the command executes, and closed afterwards.

Type Aliases

Type aliases are the way in which arc implements support for builtin and standard library Python types, but can also be used for any type. You can use type aliases to provide support fo any third party library types. For example, supporting numpy arrays

import arc
from arc.types import Alias
import numpy as np

# Inherits from Alias. The 'of' parameter declares what types(s) this
# alias handles (can be a single type or tuple of types).
# Reads like a sentence: "NDArrayAlias is the Alias of np.ndarray"
class NDArrayAlias(Alias, of=np.ndarry):

    @classmethod
    def __convert__(cls, value: str):
        return np.ndarray(value.split(","))


@arc.command()
def main(array: np.ndarray):
    print(repr(array))


main()
$ python example.py x,y,z
array(['x', 'y', 'z'], dtype='<U1')
All other principles about custom types hold for alias types.

Note that this is a simplified example, a more complete implementation would support the use of generics using numpy.typing

Generic Types

TODO