Skip to content

Commit 0ced460

Browse files
authored
Merge pull request #171 from Daraan/tap-ignore
Add TapIgnore
2 parents 2e18dc8 + dd5b883 commit 0ced460

File tree

5 files changed

+275
-49
lines changed

5 files changed

+275
-49
lines changed

README.md

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ Running `python square.py --num 2` will print `The square of your number is 4.0.
4141

4242
Tap requires Python 3.10+
4343

44-
To install Tap from PyPI run:
44+
To install Tap from PyPI run:
4545

46-
```
46+
```shell
4747
pip install typed-argument-parser
4848
```
4949

5050
<details>
5151
<summary>To install Tap from source, run the following commands:</summary>
5252

53-
```
53+
```shell
5454
git clone https://github.com/swansonk14/typed-argument-parser.git
5555
cd typed-argument-parser
5656
pip install -e .
@@ -61,51 +61,69 @@ pip install -e .
6161
<details>
6262
<summary>To develop this package, install development requirements (in a virtual environment):</summary>
6363

64-
```
64+
```shell
6565
python -m pip install -e ".[dev]"
6666
```
6767

6868
Use [`flake8`](https://github.com/PyCQA/flake8) linting.
6969

7070
To run tests, run:
7171

72-
```
72+
```shell
7373
pytest
7474
```
7575

7676
</details>
7777

7878
## Table of Contents
7979

80-
* [Installation](#installation)
81-
* [Table of Contents](#table-of-contents)
82-
* [Tap is Python-native](#tap-is-python-native)
83-
* [Tap features](#tap-features)
84-
+ [Arguments](#arguments)
85-
+ [Tap help](#tap-help)
86-
+ [Configuring arguments](#configuring-arguments)
87-
- [Adding special argument behavior](#adding-special-argument-behavior)
88-
- [Adding subparsers](#adding-subparsers)
89-
+ [Types](#types)
90-
+ [Argument processing](#argument-processing)
91-
+ [Processing known args](#processing-known-args)
92-
+ [Subclassing](#subclassing)
93-
+ [Printing](#printing)
94-
+ [Reproducibility](#reproducibility)
95-
+ [Saving and loading arguments](#saving-and-loading-arguments)
96-
+ [Loading from configuration files](#loading-from-configuration-files)
97-
* [tapify](#tapify)
98-
+ [Examples](#examples)
99-
- [Function](#function)
100-
- [Class](#class)
101-
- [Dataclass](#dataclass)
102-
+ [tapify help](#tapify-help)
103-
+ [Command line vs explicit arguments](#command-line-vs-explicit-arguments)
104-
+ [Known args](#known-args)
105-
* [Convert to a `Tap` class](#convert-to-a-tap-class)
106-
+ [`to_tap_class` examples](#to_tap_class-examples)
107-
- [Simple](#simple)
108-
- [Complex](#complex)
80+
- [Typed Argument Parser (Tap)](#typed-argument-parser-tap)
81+
- [Installation](#installation)
82+
- [Table of Contents](#table-of-contents)
83+
- [Tap is Python-native](#tap-is-python-native)
84+
- [Tap features](#tap-features)
85+
- [Arguments](#arguments)
86+
- [Tap help](#tap-help)
87+
- [Configuring arguments](#configuring-arguments)
88+
- [Adding special argument behavior](#adding-special-argument-behavior)
89+
- [Adding subparsers](#adding-subparsers)
90+
- [Types](#types)
91+
- [`str`, `int`, and `float`](#str-int-and-float)
92+
- [`bool`](#bool)
93+
- [`Optional`](#optional)
94+
- [`List`](#list)
95+
- [`Set`](#set)
96+
- [`Tuple`](#tuple)
97+
- [`Literal`](#literal)
98+
- [`Union`](#union)
99+
- [Complex Types](#complex-types)
100+
- [Ignore Attribute](#ignore-attribute)
101+
- [Argument processing](#argument-processing)
102+
- [Processing known args](#processing-known-args)
103+
- [Subclassing](#subclassing)
104+
- [Printing](#printing)
105+
- [Reproducibility](#reproducibility)
106+
- [Reproducibility info](#reproducibility-info)
107+
- [Conversion Tap to and from dictionaries](#conversion-tap-to-and-from-dictionaries)
108+
- [Saving and loading arguments](#saving-and-loading-arguments)
109+
- [Save](#save)
110+
- [Load](#load)
111+
- [Load from dict](#load-from-dict)
112+
- [Loading from configuration files](#loading-from-configuration-files)
113+
- [tapify](#tapify)
114+
- [Examples](#examples)
115+
- [Function](#function)
116+
- [Class](#class)
117+
- [Dataclass](#dataclass)
118+
- [Pydantic](#pydantic)
119+
- [tapify help](#tapify-help)
120+
- [Command line vs explicit arguments](#command-line-vs-explicit-arguments)
121+
- [Known args](#known-args)
122+
- [Explicit boolean arguments](#explicit-boolean-arguments)
123+
- [Convert to a `Tap` class](#convert-to-a-tap-class)
124+
- [`to_tap_class` examples](#to_tap_class-examples)
125+
- [Simple](#simple)
126+
- [Complex](#complex)
109127

110128
## Tap is Python-native
111129

@@ -361,6 +379,33 @@ args = Args().parse_args('--aged_person Tapper,27'.split())
361379
print(f'{args.aged_person.name} is {args.aged_person.age}') # Tapper is 27
362380
```
363381

382+
### Ignore Attribute
383+
384+
Sometimes you may want to define attributes that should not be parsed as command line arguments, but you still want to type them.
385+
This can be achieved by using `TapIgnore`.
386+
387+
```python
388+
from tap import Tap, TapIgnore
389+
390+
class MyTap(Tap):
391+
# Regular arguments (will be parsed)
392+
package: str
393+
stars: int = 5
394+
395+
# Ignored attributes (will not be parsed)
396+
internal_counter: TapIgnore[int] = 0
397+
398+
args = MyTap().parse_args(["--help"])
399+
```
400+
401+
```txt
402+
usage: ipython --package PACKAGE [--stars STARS] [-h]
403+
404+
options:
405+
--package PACKAGE (str, required)
406+
--stars STARS (int, default=5)
407+
-h, --help show this help message and exit
408+
```
364409

365410
### Argument processing
366411

@@ -863,7 +908,7 @@ Running `python person.py --name Jesse --age 1` prints `My name is Jesse.` follo
863908

864909
### Explicit boolean arguments
865910

866-
Tapify supports explicit specification of boolean arguments (see [bool](#bool) for more details). By default, `explicit_bool=False` and it can be set with `tapify(..., explicit_bool=True)`.
911+
Tapify supports explicit specification of boolean arguments (see [bool](#bool) for more details). By default, `explicit_bool=False` and it can be set with `tapify(..., explicit_bool=True)`.
867912

868913
## Convert to a `Tap` class
869914

@@ -903,7 +948,7 @@ if __name__ == "__main__":
903948

904949
Running `python main.py --package tap` will print `Project instance: package='tap' is_cool=True stars=5`.
905950

906-
### Complex
951+
#### Complex
907952

908953
The general pattern is:
909954

src/tap/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
__version__ = "1.11.0"
66

77
from argparse import ArgumentError, ArgumentTypeError
8+
89
from tap.tap import Tap
910
from tap.tapify import tapify, to_tap_class
11+
from tap.utils import TapIgnore
1012

1113
__all__ = [
1214
"ArgumentError",
1315
"ArgumentTypeError",
1416
"Tap",
17+
"TapIgnore",
1518
"tapify",
1619
"to_tap_class",
1720
"__version__",

src/tap/tap.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,22 @@
88
from pprint import pformat
99
from shlex import quote, split
1010
from types import MethodType, UnionType
11-
from typing import Any, Callable, Iterable, List, Optional, Sequence, Set, Tuple, TypeVar, Union, get_type_hints
11+
from typing import (
12+
Annotated,
13+
Any,
14+
Callable,
15+
Iterable,
16+
List,
17+
Optional,
18+
Sequence,
19+
Set,
20+
Tuple,
21+
TypeVar,
22+
Union,
23+
get_type_hints,
24+
get_origin as typing_get_origin,
25+
get_args as typing_get_args,
26+
)
1227
from typing_inspect import is_literal_type
1328

1429
from tap.utils import (
@@ -23,6 +38,7 @@
2338
type_to_str,
2439
get_literals,
2540
boolean_type,
41+
_TapIgnoreMarker,
2642
TupleTypeEnforcer,
2743
define_python_object_encoder,
2844
as_python_object,
@@ -92,6 +108,7 @@ def __init__(
92108

93109
# Get annotations from self and all super classes up through tap
94110
self._annotations = self._get_annotations()
111+
self._annotations_with_extras = self._get_annotations(include_extras=True)
95112

96113
# Set the default description to be the docstring
97114
kwargs.setdefault("description", self.__doc__)
@@ -300,10 +317,21 @@ def add_argument(self, *name_or_flags, **kwargs) -> None:
300317
variable = get_argument_name(*name_or_flags).replace("-", "_")
301318
self.argument_buffer[variable] = (name_or_flags, kwargs)
302319

320+
def _is_ignored_argument(self, variable: str, annotations: Optional[dict[str, Any]] = None) -> bool:
321+
annotations = self._annotations_with_extras if annotations is None else annotations
322+
if variable in annotations:
323+
var_type = annotations[variable]
324+
if typing_get_origin(var_type) is Annotated and _TapIgnoreMarker in typing_get_args(var_type):
325+
return True
326+
return False
327+
303328
def _add_arguments(self) -> None:
304329
"""Add arguments to self in the order they are defined as class variables (so the help string is in order)."""
305330
# Add class variables (in order)
306331
for variable in self.class_variables:
332+
if self._is_ignored_argument(variable):
333+
continue
334+
307335
if variable in self.argument_buffer:
308336
name_or_flags, kwargs = self.argument_buffer[variable]
309337
self._add_argument(*name_or_flags, **kwargs)
@@ -313,6 +341,8 @@ def _add_arguments(self) -> None:
313341
# Add any arguments that were added manually in configure but aren't class variables (in order)
314342
for variable, (name_or_flags, kwargs) in self.argument_buffer.items():
315343
if variable not in self.class_variables:
344+
if self._is_ignored_argument(variable):
345+
continue
316346
self._add_argument(*name_or_flags, **kwargs)
317347

318348
def process_args(self) -> None:
@@ -483,7 +513,7 @@ def parse_args(
483513
return self
484514

485515
@classmethod
486-
def _get_from_self_and_super(cls, extract_func: Callable[[type], dict]) -> dict[str, Any] | dict:
516+
def _get_from_self_and_super(cls, extract_func: Callable[[type], dict]) -> dict[str, Any]:
487517
"""Returns a dictionary mapping variable names to values.
488518
489519
Variables and values are extracted from classes using key starting
@@ -531,11 +561,16 @@ def _get_class_dict(self) -> dict[str, Any]:
531561

532562
return class_dict
533563

534-
def _get_annotations(self) -> dict[str, Any]:
535-
"""Returns a dictionary mapping variable names to their type annotations."""
536-
return self._get_from_self_and_super(extract_func=lambda super_class: dict(get_type_hints(super_class)))
564+
def _get_annotations(self, include_extras: bool = False) -> dict[str, Any]:
565+
"""
566+
Returns a dictionary mapping variable names to their type annotations.
567+
Keep Annotations and other extras if include_extras is True.
568+
"""
569+
return self._get_from_self_and_super(
570+
extract_func=lambda super_class: dict(get_type_hints(super_class, include_extras=include_extras))
571+
)
537572

538-
def _get_class_variables(self) -> dict:
573+
def _get_class_variables(self, exclude_tap_ignores: bool = True) -> dict:
539574
"""Returns a dictionary mapping class variables names to their additional information."""
540575
class_variable_names = {**self._get_annotations(), **self._get_class_dict()}.keys()
541576

@@ -560,7 +595,13 @@ def _get_class_variables(self) -> dict:
560595
class_variables = {}
561596
for variable in class_variable_names:
562597
class_variables[variable] = {"comment": ""}
563-
598+
if exclude_tap_ignores:
599+
extra_annotations = self._get_annotations(include_extras=True)
600+
return {
601+
var: data
602+
for var, data in class_variables.items()
603+
if not self._is_ignored_argument(var, extra_annotations)
604+
}
564605
return class_variables
565606

566607
def _get_argument_names(self) -> set[str]:

src/tap/utils.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
1-
from argparse import ArgumentParser, ArgumentTypeError
21
import ast
3-
from base64 import b64encode, b64decode
42
import inspect
5-
from io import StringIO
6-
from json import JSONEncoder
73
import os
84
import pickle
95
import re
106
import subprocess
7+
import sys
118
import textwrap
129
import tokenize
10+
import warnings
11+
from argparse import ArgumentParser, ArgumentTypeError
12+
from base64 import b64decode, b64encode
13+
from io import StringIO
14+
from json import JSONEncoder
1315
from types import UnionType
1416
from typing import (
17+
Annotated,
1518
Any,
1619
Callable,
1720
Generator,
1821
Iterable,
1922
Iterator,
2023
Literal,
2124
Optional,
25+
TypeAlias,
26+
TypeVar,
2227
)
23-
from typing_inspect import get_args as typing_inspect_get_args, get_origin as typing_inspect_get_origin
24-
import warnings
2528

29+
from typing_inspect import get_args as typing_inspect_get_args
30+
from typing_inspect import get_origin as typing_inspect_get_origin
2631

2732
NO_CHANGES_STATUS = """nothing to commit, working tree clean"""
2833
PRIMITIVES = (str, int, float, bool)
@@ -594,3 +599,26 @@ def get_args(tp: Any) -> tuple[type, ...]:
594599
return tp.__args__
595600

596601
return typing_inspect_get_args(tp)
602+
603+
604+
_T = TypeVar("_T")
605+
606+
607+
class _TapIgnoreMarker:
608+
"""Internal marker that if present in a type annotation indicates that the argument should be ignored."""
609+
610+
611+
# TODO: Python 3.12 turn this into a TypeAliasType for better IDE tooltips
612+
TapIgnore: TypeAlias = Annotated[_T, _TapIgnoreMarker]
613+
"""
614+
Type annotation to indicate that an argument should be ignored by Tap and not be added as an argument.
615+
616+
Usage:
617+
from tap import Tap, TapIgnore
618+
619+
class Args(Tap):
620+
a: int
621+
622+
# TapIgnore is generic and preserves the type of the ignored attribute
623+
e: TapIgnore[int] = 5
624+
"""

0 commit comments

Comments
 (0)