Skip to content

Commit 02f748e

Browse files
committed
Merge branch 'tapify' into main
2 parents 7858e96 + 1917e81 commit 02f748e

File tree

3 files changed

+202
-44
lines changed

3 files changed

+202
-44
lines changed

README.md

+177-20
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,24 @@ Tap provides the following benefits:
2222

2323
See [this poster](https://docs.google.com/presentation/d/1AirN6gpiq4P1L8K003EsXmobVxP3A4AVEIR2KOEQN7Y/edit?usp=sharing), which we presented at [PyCon 2020](https://us.pycon.org/2020/), for a presentation of some of the relevant concepts we used to guide the development of Tap.
2424

25+
As of version 1.8.0, Tap includes `tapify`, which runs functions or initializes classes with arguments parsed from the command line. We show an example below.
26+
27+
```python
28+
# square.py
29+
from tap import tapify
30+
31+
def square(num: float) -> float:
32+
return num ** 2
33+
34+
if __name__ == '__main__':
35+
print(f'The square of your number is {tapify(square)}.')
36+
```
37+
38+
Running `python square.py --num 2` will print `The square of your number is 4.0.`. Please see [tapify](#tapify) for more details.
39+
2540
## Installation
2641

27-
Tap requires Python 3.6+
42+
Tap requires Python 3.7+
2843

2944
To install Tap from PyPI run:
3045
```
@@ -46,31 +61,26 @@ pip install -e .
4661
* [Tap is Python-native](#tap-is-python-native)
4762
* [Tap features](#tap-features)
4863
+ [Arguments](#arguments)
49-
+ [Help string](#help-string)
50-
+ [Flexibility of `configure`](#flexibility-of--configure-)
64+
+ [Tap help](#tap-help)
65+
+ [Configuring arguments](#configuring-arguments)
5166
- [Adding special argument behavior](#adding-special-argument-behavior)
5267
- [Adding subparsers](#adding-subparsers)
5368
+ [Types](#types)
54-
- [`str`, `int`, and `float`](#-str----int---and--float-)
55-
- [`bool`](#-bool-)
56-
- [`Optional`](#-optional-)
57-
- [`List`](#-list-)
58-
- [`Set`](#-set-)
59-
- [`Tuple`](#-tuple-)
60-
- [`Literal`](#-literal-)
61-
- [`Union`](#-union-)
62-
- [Complex Types](#complex-types)
63-
+ [Argument processing with `process_args`](#argument-processing-with--process-args-)
69+
+ [Argument processing](#argument-processing)
6470
+ [Processing known args](#processing-known-args)
6571
+ [Subclassing](#subclassing)
6672
+ [Printing](#printing)
6773
+ [Reproducibility](#reproducibility)
68-
- [Reproducibility info](#reproducibility-info)
6974
+ [Saving and loading arguments](#saving-and-loading-arguments)
70-
- [Save](#save)
71-
- [Load](#load)
72-
- [Load from dict](#load-from-dict)
7375
+ [Loading from configuration files](#loading-from-configuration-files)
76+
* [tapify](#tapify)
77+
+ [Examples](#examples)
78+
- [Function](#function)
79+
- [Class](#class)
80+
- [Dataclass](#dataclass)
81+
+ [tapify help](#tapify-help)
82+
+ [Command line vs explicit arguments](#command-line-vs-explicit-arguments)
83+
+ [Known args](#known-args)
7484

7585
## Tap is Python-native
7686

@@ -143,7 +153,7 @@ class MyTap(Tap):
143153
default_arg: str = 'default value'
144154
```
145155

146-
### Help string
156+
### Tap help
147157

148158
Single line and/or multiline comments which appear after the argument are automatically parsed into the help string provided when running `python main.py -h`. The type and default values of arguments are also provided in the help string.
149159

@@ -172,7 +182,7 @@ optional arguments:
172182
-h, --help show this help message and exit
173183
```
174184

175-
### Flexibility of `configure`
185+
### Configuring arguments
176186
To specify behavior beyond what can be specified using arguments as class variables, override the `configure` method.
177187
`configure` provides access to advanced argument parsing features such as `add_argument` and `add_subparser`.
178188
Since Tap is a wrapper around argparse, Tap provides all of the same functionality.
@@ -329,7 +339,7 @@ print(f'{args.aged_person.name} is {args.aged_person.age}') # Tapper is 27
329339
```
330340

331341

332-
### Argument processing with `process_args`
342+
### Argument processing
333343

334344
With complex argument parsing, arguments often end up having interdependencies. This means that it may be necessary to disallow certain combinations of arguments or to modify some arguments based on other arguments.
335345

@@ -565,3 +575,150 @@ args = Args(config_files=['my_config_shlex.txt']).parse_args()
565575
to get the resulting `args = {'arg1': 21, 'arg2': 'two three four'}`
566576

567577
The legacy parsing behavior of using standard string split can be re-enabled by passing `legacy_config_parsing=True` to `parse_args`.
578+
579+
## tapify
580+
581+
`tapify` makes it possible to run functions or initialize objects via command line arguments. This is inspired by Google's [Python Fire](https://github.com/google/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.
582+
583+
### Examples
584+
585+
#### Function
586+
587+
```python
588+
# square_function.py
589+
from tap import tapify
590+
591+
def square(num: float) -> float:
592+
"""Square a number.
593+
594+
:param num: The number to square.
595+
"""
596+
return num ** 2
597+
598+
if __name__ == '__main__':
599+
squared = tapify(square)
600+
print(f'The square of your number is {squared}.')
601+
```
602+
603+
Running `python square_function.py --num 5` prints `The square of your number is 25.0.`.
604+
605+
#### Class
606+
607+
```python
608+
# square_class.py
609+
from tap import tapify
610+
611+
class Squarer:
612+
def __init__(self, num: float) -> None:
613+
"""Initialize the Squarer with a number to square.
614+
615+
:param num: The number to square.
616+
"""
617+
self.num = num
618+
619+
def get_square(self) -> float:
620+
"""Get the square of the number."""
621+
return self.num ** 2
622+
623+
if __name__ == '__main__':
624+
squarer = tapify(Squarer)
625+
print(f'The square of your number is {squarer.get_square()}.')
626+
```
627+
628+
Running `python square_class.py --num 2` prints `The square of your number is 4.0.`.
629+
630+
#### Dataclass
631+
632+
```python
633+
# square_dataclass.py
634+
from dataclasses import dataclass
635+
636+
from tap import tapify
637+
638+
@dataclass
639+
class Squarer:
640+
"""Squarer with a number to square.
641+
642+
:param num: The number to square.
643+
"""
644+
num: float
645+
646+
def get_square(self) -> float:
647+
"""Get the square of the number."""
648+
return self.num ** 2
649+
650+
if __name__ == '__main__':
651+
squarer = tapify(Squarer)
652+
print(f'The square of your number is {squarer.get_square()}.')
653+
```
654+
655+
Running `python square_dataclass.py --num -1` prints `The square of your number is 1.0.`.
656+
657+
### tapify help
658+
659+
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:
660+
661+
```
662+
usage: square_function.py [-h] --num NUM
663+
664+
Square a number.
665+
666+
options:
667+
-h, --help show this help message and exit
668+
--num NUM (float, required) The number to square.
669+
```
670+
671+
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.
672+
673+
### Command line vs explicit arguments
674+
675+
`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.
676+
677+
```python
678+
# add.py
679+
from tap import tapify
680+
681+
def add(num_1: float, num_2: float = 0.0, num_3: float = 0.0) -> float:
682+
"""Add numbers.
683+
684+
:param num_1: The first number.
685+
:param num_2: The second number.
686+
:param num_3: The third number.
687+
"""
688+
return num_1 + num_2 + num_3
689+
690+
if __name__ == '__main__':
691+
added = tapify(add, num_2=2.2, num_3=4.1)
692+
print(f'The sum of your numbers is {added}.')
693+
```
694+
695+
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.)
696+
697+
### Known args
698+
699+
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.
700+
701+
```python
702+
# person.py
703+
from tap import tapify
704+
705+
def print_name(name: str) -> None:
706+
"""Print a person's name.
707+
708+
:param name: A person's name.
709+
"""
710+
print(f'My name is {name}.')
711+
712+
def print_age(age: int) -> None:
713+
"""Print a person's age.
714+
715+
:param name: A person's age.
716+
"""
717+
print(f'My age is {age}.')
718+
719+
if __name__ == '__main__':
720+
tapify(print_name, known_only=True)
721+
tapify(print_age, known_only=True)
722+
```
723+
724+
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.

tap/tapify.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212

1313
def tapify(class_or_function: Union[Callable[[InputType], OutputType], OutputType],
14-
args: Optional[List[str]] = None,
1514
known_only: bool = False,
15+
command_line_args: Optional[List[str]] = None,
1616
**func_kwargs) -> OutputType:
1717
"""Tapify initializes a class or runs a function by parsing arguments from the command line.
1818
1919
:param class_or_function: The class or function to run with the provided arguments.
20-
:param args: Arguments to parse. If None, arguments are parsed from the command line.
2120
:param known_only: If true, ignores extra arguments and only parses known arguments.
21+
:param command_line_args: A list of command line style arguments to parse (e.g., ['--arg', 'value']).
22+
If None, arguments are parsed from the command line (default behavior).
2223
:param func_kwargs: Additional keyword arguments for the function. These act as default values when
2324
parsing the command line arguments and overwrite the function defaults but
2425
are overwritten by the parsed command line arguments.
@@ -38,7 +39,7 @@ def tapify(class_or_function: Union[Callable[[InputType], OutputType], OutputTyp
3839
# Get the description of each argument in the class init or function
3940
param_to_description = {param.arg_name: param.description for param in docstring.params}
4041

41-
# Create a Tap object
42+
# Create a Tap object with a description from the docstring of the function or class
4243
tap = Tap(description='\n'.join(filter(None, (docstring.short_description, docstring.long_description))))
4344

4445
# Add arguments of class init or function to the Tap object
@@ -75,10 +76,10 @@ def tapify(class_or_function: Union[Callable[[InputType], OutputType], OutputTyp
7576
raise ValueError(f'Unknown keyword arguments: {func_kwargs}')
7677

7778
# Parse command line arguments
78-
args = tap.parse_args(
79-
args=args,
79+
command_line_args = tap.parse_args(
80+
args=command_line_args,
8081
known_only=known_only
8182
)
8283

8384
# Initialize the class or run the function with the parsed arguments
84-
return class_or_function(**args.as_dict())
85+
return class_or_function(**command_line_args.as_dict())

0 commit comments

Comments
 (0)