Releases: swansonk14/typed-argument-parser
Setup refactor
Support for Pydantic
Pydantic Models and dataclasses can now be tapify
d.
# 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
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
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
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
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
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
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
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
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.