Skip to content

Releases: swansonk14/typed-argument-parser

Support for Pydantic

11 Apr 06:01
7398f29
Compare
Choose a tag to compare

Pydantic Models and dataclasses can now be tapifyd.

# square_pydantic.py
from pydantic import BaseModel, Field

from tap import tapify

class Squarer(BaseModel):
    """Squarer with a number to square."""
    num: float = Field(description="The number to square.")

    def get_square(self) -> float:
        """Get the square of the number."""
        return self.num ** 2

if __name__ == '__main__':
    squarer = tapify(Squarer)
    print(f'The square of your number is {squarer.get_square()}.')

For Pydantic v2 models and dataclasses, the argument's description (which is displayed in the -h help message) can
either be specified in the class docstring or the field's description. If both are specified, the description from the
docstring is used. In the example below, the description is provided in the docstring.

For Pydantic v1 models and dataclasses, the argument's description must be provided in the class docstring:

# square_pydantic.py
from pydantic import BaseModel

from tap import tapify

class Squarer(BaseModel):
    """Squarer with a number to square.

    :param num: The number to square.
    """
    num: float

    def get_square(self) -> float:
        """Get the square of the number."""
        return self.num ** 2

if __name__ == '__main__':
    squarer = tapify(Squarer)
    print(f'The square of your number is {squarer.get_square()}.')

This release also includes to_tap_class https://github.com/swansonk14/typed-argument-parser?tab=readme-ov-file#convert-to-a-tap-class, which takes in a class, dataclass, or function, and produces a class that is a Tap class. to_tap_class is a precursor to tapify that gives the user more control over what happens prior to calling parse_args(). For example,

from pydantic import BaseModel

from tap import to_tap_class

class Project(BaseModel):
    package: str
    is_cool: bool = True
    stars: int = 5

if __name__ == "__main__":
    ProjectTap = to_tap_class(Project)
    tap = ProjectTap(description=__doc__)  # from the top of this script
    args = tap.parse_args()
    project = Project(**args.as_dict())
    print(f"Project instance: {project}")

@tinkerware provided a useful and more involved example of how to use to_tap_class: #128 (comment)

Thank you to @kddubey for implementing and testing both to_tap_class and tapify as well as providing the simple example above.

Python 3.12 Support, Better Support for tapify, Bug Fixes

12 Nov 04:51
12da1d3
Compare
Choose a tag to compare

Python 3.12 Support, Better Support for tapify, Bug Fixes

Python 3.12 Support

ee48863: @AlexWaygood introduced support for Python 3.12 (#122).

Better Support for tapify

612fabe: @shorie000 corrected the signature of tapify to indicate that it accepts a class type rather than a class object as input (#119).

80735dc: Resolved an issue in tapify (#114) so that it can now handle **kwargs in function and class definitions.

# main.py
from tap import tapify

def foo(a: int, b: int = 2, **kwargs) -> str:
    print(f"sum = {a + b}")
    for key, value in kwargs.items():
        print(f"{key}: {value}")

if __name__ == "__main__":
    tapify(concat)

Running python main.py --a 1 --b 3 --c kwa --d arg will print:

sum = 4
c: kwa
d: arg

Note that all keys and values in kwargs will be strings.

Bug Fixes

18b13d1: Resolved an issue in Tap (#118) where tuples of Literals are parsed incorrectly.

Now, code such as the following example will run correctly.

from tap import Tap
from typing import Literal

class Args(Tap):
    arg: tuple[Literal['a', 'b'], ...]

args = Args().parse_args(['--arg', 'a', 'b'])

70137fd: Resolved an issue where arguments in dynamically created Tap classes have help strings that display arguments in the wrong order (#121).

Summer Cleaning

28 Jun 23:02
cddb053
Compare
Choose a tag to compare

Summer Cleaning

This release contains a few small bug fixes.

68cd6f4: In save, fixes threading of the argument repo_path so that it is used.

d26f0b8: Changes the type hints of all path-like arguments (e.g., path in the method save) to support both str and pathlib.Path.

c48ef74: Drops support for Python 3.7 due to end-of-life and removes the dependency on typing_extensions (since the Literal type is built into Python 3.8+).

7f12646, 33e7c53, 33e7c53: Moves the deepcopy of default arguments to avoid pickle errors.

33e7c53, 33e7c53: Fixes bugs that we introduced in the above commits ;)

Tapify

06 Mar 20:05
eb55a9b
Compare
Choose a tag to compare

Tapify

tapify makes it possible to run functions or initialize objects via command line arguments. This is inspired by Google's Python Fire, but tapify also automatically casts command line arguments to the appropriate types based on the type hints. Under the hood, tapify implicitly creates a Tap object and uses it to parse the command line arguments, which it then uses to run the function or initialize the class. We show a few examples below.

Examples

Function

# square_function.py
from tap import tapify

def square(num: float) -> float:
    """Square a number.

    :param num: The number to square.
    """
    return num ** 2

if __name__ == '__main__':
    squared = tapify(square)
    print(f'The square of your number is {squared}.')

Running python square_function.py --num 5 prints The square of your number is 25.0..

Class

# square_class.py
from tap import tapify

class Squarer:
    def __init__(self, num: float) -> None:
        """Initialize the Squarer with a number to square.

        :param  num: The number to square.
        """
        self.num = num

    def get_square(self) -> float:
        """Get the square of the number."""
        return self.num ** 2

if __name__ == '__main__':
    squarer = tapify(Squarer)
    print(f'The square of your number is {squarer.get_square()}.')

Running python square_class.py --num 2 prints The square of your number is 4.0..

Dataclass

# square_dataclass.py
from dataclasses import dataclass

from tap import tapify

@dataclass
class Squarer:
    """Squarer with a number to square.

     :param  num: The number to square.
    """
    num: float

    def get_square(self) -> float:
        """Get the square of the number."""
        return self.num ** 2

if __name__ == '__main__':
    squarer = tapify(Squarer)
    print(f'The square of your number is {squarer.get_square()}.')

Running python square_dataclass.py --num -1 prints The square of your number is 1.0..

Help

The help string on the command line is set based on the docstring for the function or class. For example, running python square_function.py -h will print:

usage: square_function.py [-h] --num NUM

Square a number.

options:
  -h, --help  show this help message and exit
  --num NUM   (float, required) The number to square.

Note that for classes, if there is a docstring in the __init__ method, then tapify sets the help string description to that docstring. Otherwise, it uses the docstring from the top of the class.

Command line vs explicit arguments

tapify can simultaneously use both arguments passed from the command line and arguments passed in explicitly in the tapify call. Arguments provided in the tapify call override function defaults, and arguments provided via the command line override both arguments provided in the tapify call and function defaults. We show an example below.

# add.py
from tap import tapify

def add(num_1: float, num_2: float = 0.0, num_3: float = 0.0) -> float:
    """Add numbers.

    :param num_1: The first number.
    :param num_2: The second number.
    :param num_3: The third number.
    """
    return num_1 + num_2 + num_3

if __name__ == '__main__':
    added = tapify(add, num_2=2.2, num_3=4.1)
    print(f'The sum of your numbers is {added}.')

Running python add.py --num_1 1.0 --num_2 0.9 prints The sum of your numbers is 6.0.. (Note that add took num_1 = 1.0 and num_2 = 0.9 from the command line and num_3=4.1 from the tapify call due to the order of precedence.)

Known args

Calling tapify with known_only=True allows tapify to ignore additional arguments from the command line that are not needed for the function or class. If known_only=False (the default), then tapify will raise an error when additional arguments are provided. We show an example below where known_only=True might be useful for running multiple tapify calls.

# person.py
from tap import tapify

def print_name(name: str) -> None:
    """Print a person's name.

    :param name: A person's name.
    """
    print(f'My name is {name}.')

def print_age(age: int) -> None:
    """Print a person's age.

    :param name: A person's age.
    """
    print(f'My age is {age}.')

if __name__ == '__main__':
    tapify(print_name, known_only=True)
    tapify(print_age, known_only=True)

Running python person.py --name Jesse --age 1 prints My name is Jesse. followed by My age is 1.. Without known_only=True, the tapify calls would raise an error due to the extra argument.

Other Changes

#76 Add description support for Tap based on the __doc__ attribute.

#81 Fix help text around positional arguments (resolves #79).

#82 Add README warning about unpickling untrusted data (resolves #73).

#83 Fix handling of decorated classes (resolves #80).

#91 Add support for single-quoted documentation (resolves #84).

#96 Add support for Python 3.11.

Python 3.10 Type Support

01 Jan 02:49
8967a51
Compare
Choose a tag to compare

Support for Python 3.10 Union Types

This release adds support for the | operator in place of Union for Python 3.10. For example:

Using the Union operator in Python 3.6+

from tap import Tap
from typing import Union

def to_number(string: str) -> Union[float, int]:
    return float(string) if '.' in string else int(string)

class Args(Tap):
    number: Union[float, int]

    def configure(self):
        self.add_argument('--number', type=to_number)

args = Args().parse_args()  
print(args.number)  # 7.0

Using the | operator in Python 3.10+

from tap import Tap

def to_number(string: str) -> float | int:
    return float(string) if '.' in string else int(string)

class Args(Tap):
    number: float | int

    def configure(self):
        self.add_argument('--number', type=to_number)

args = Args().parse_args()
print(args.number)  # 7.0

This was implemented in #66 and addresses #64.

Other Changes

ea8254d Exposed argparse's errors ArgumentError and ArgumentTypeError through Tap so that they can be imported directly with from tap import ArgumentError rather than from argparse import ArgumentError. This addresses #46.

b1f909a Changed the name of the master branch to main.

#60 #61 #62 Added shlex support for config files to allow quoted strings and comments, building on #51.

Changing reproducibility info git path

15 Aug 20:42
cb99d5e
Compare
Choose a tag to compare

Changing reproducibility info git path

Previously, the reproducibility information came from the git repo in the current working directory. Now, by default, the reproducibility information comes from the git repo of the directory containing the file that is run, regardless of the current working directory. Specify a particular git repo by setting the repo_path parameter in the save method.

Python 3.9 Type Support

20 Jun 15:29
Compare
Choose a tag to compare

Support for list, set, and tuple

If you're using Python 3.9+, then you can type arguments with list, set, and tuple as well as their boxed variants (e.g., list[int]).

Bug fixes and minor improvements

8e29d9d Made the saved reproducibility information work for arguments with spaces such as --arg "Multiple words" .
5b2eab6 Added support for pickling Tap objects.
#54 Fixed an issue where Tap overwrote actions (thanks to @cebtenzzre).
f30ffbe Prevent changing underscores to dashes in positional arguments.
#48 underscores_to_dashes now works for default/required values (thanks to @marcoffee).

Fixing typing-inspect version

07 Jun 09:28
25644c3
Compare
Choose a tag to compare

The latest version of typing-inspect, version 0.7.0, has a bug which causes Tap to break. This release forces installation of typing-inspect version 0.6.0, which is compatible with Tap. This release also includes several minor fixes.

Complex Types and Improved Testing Infrastructure

27 Mar 18:18
b51d06c
Compare
Choose a tag to compare

Complex Types

Tap now supports types boxed in an Optional such as Optional[Tuple[bool, str, int]].

Tap also supports arbitrary objects with one parameter in their constructor. For example:

from tap import Tap
from pathlib import Path

class Args(Tap):
    arg: Path

args = Args().parse_args(['--arg', 'file.txt'])
print(args.arg, type(args.arg))  # file.txt <class 'pathlib.PosixPath'>

Improved Testing Infrastructure

Tap has now transitioned from Travis CI to GitHub Actions for running tests. Additionally, tests are now run on Windows and MacOS in addition to Linux. Code coverage is also determined and is displayed on the README.

Added Support for Configuration Files

27 Nov 16:43
Compare
Choose a tag to compare

Configuration files may now be loaded along with arguments with the optional flag config_files: List[str]. This addresses #25, thanks to @arbellea.

For example, if you have the config file my_config.txt

--arg1 1
--arg2 two

then you can write

from tap import Tap

class Args(Tap):
    arg1: int
    arg2: str

args = Args(config_files=['my_config']).parse_args()

Arguments passed in from the command line overwrite arguments from the configuration files. Arguments in configuration files that appear later in the list overwrite the arguments in previous configuration files.