Skip to content

Commit 7f36ff2

Browse files
authored
Merge pull request #164 from kddubey/kddubey/fix-python-314
Prepare for Python 3.14
2 parents 619ee8b + 34f9d27 commit 7f36ff2

File tree

9 files changed

+47
-29
lines changed

9 files changed

+47
-29
lines changed

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,7 @@ pip install -e .
6565
python -m pip install -e ".[dev]"
6666
```
6767

68-
Style:
69-
- Please use [`black`](https://github.com/psf/black) formatting
70-
- Set your vertical line ruler to 121
71-
- Use [`flake8`](https://github.com/PyCQA/flake8) linting.
68+
Use [`flake8`](https://github.com/PyCQA/flake8) linting.
7269

7370
To run tests, run:
7471

@@ -202,7 +199,7 @@ Running `python main.py -h` results in the following:
202199

203200
```
204201
>>> python main.py -h
205-
usage: demo.py --x X [--pi PI] [-h]
202+
usage: main.py --x X [--pi PI] [-h]
206203
207204
optional arguments:
208205
--x X (float, required) What am I?
@@ -919,5 +916,5 @@ class MyCustomTap(to_tap_class(my_class_or_function)):
919916
# Special argument behavior, e.g., override configure and/or process_args
920917
```
921918

922-
Please see `demo_data_model.py` for an example of overriding [`configure`](#configuring-arguments) and
923-
[`process_args`](#argument-processing).
919+
Please see [`demos/demo_data_model.py`](./demos/demo_data_model.py) for an example of overriding
920+
[`configure`](#configuring-arguments) and [`process_args`](#argument-processing).

demo.py renamed to demos/demo.py

File renamed without changes.

demo_data_model.py renamed to demos/demo_data_model.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
"""
2-
Works for Pydantic v1 and v2.
3-
42
Example commands:
53
64
python demo_data_model.py -h
@@ -16,6 +14,7 @@
1614
--arg_bool \
1715
-arg 3.14
1816
"""
17+
1918
from typing import List, Literal, Optional, Union
2019

2120
from pydantic import BaseModel, Field

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ readme = "README.md"
1818
license = { file = "LICENSE.txt" }
1919
dependencies = [
2020
"docstring-parser >= 0.15",
21-
"packaging",
2221
"typing-inspect >= 0.7.1",
2322
]
2423
requires-python = ">=3.9"
@@ -41,6 +40,7 @@ keywords = [
4140

4241
[project.optional-dependencies]
4342
dev-no-pydantic = [
43+
"black",
4444
"pytest",
4545
"pytest-cov",
4646
"flake8",
@@ -62,6 +62,9 @@ version = {attr = "tap.__version__"}
6262
[tool.setuptools.package-data]
6363
tap = ["py.typed"]
6464

65+
[tool.black]
66+
line-length = 121
67+
6568
[project.urls]
6669
Homepage = "https://github.com/swansonk14/typed-argument-parser"
6770
Issues = "https://github.com/swansonk14/typed-argument-parser/issues"

src/tap/tapify.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from typing import Any, Callable, Optional, Sequence, TypeVar, Union
1111

1212
from docstring_parser import Docstring, parse
13-
from packaging.version import Version
1413

1514
try:
1615
import pydantic
@@ -21,7 +20,7 @@
2120
_PydanticField = type("_PydanticField", (object,), {})
2221
_PYDANTIC_FIELD_TYPES = ()
2322
else:
24-
_IS_PYDANTIC_V1 = Version(pydantic.__version__) < Version("2.0.0")
23+
_IS_PYDANTIC_V1 = pydantic.VERSION.startswith("1.")
2524
from pydantic import BaseModel
2625
from pydantic.fields import FieldInfo as PydanticFieldBaseModel
2726
from pydantic.dataclasses import FieldInfo as PydanticFieldDataclass
@@ -191,7 +190,21 @@ def _tap_data_from_class_or_function(
191190

192191
sig = inspect.signature(class_or_function)
193192

194-
for param_name, param in sig.parameters.items():
193+
is_pydantic_v1_dataclass_class = (
194+
_IS_PYDANTIC_V1 and _is_pydantic_dataclass(class_or_function) and inspect.isclass(class_or_function)
195+
)
196+
197+
for idx, (param_name, param) in enumerate(sig.parameters.items()):
198+
if (
199+
idx == 0
200+
and param.annotation == inspect.Parameter.empty
201+
and param_name == "self"
202+
and is_pydantic_v1_dataclass_class
203+
):
204+
# This check gets around a quirk of Pydantic v1 dataclasses. Sometime after Python 3.13.0, the signature
205+
# started including self as the first parameter out of inspect.signature
206+
continue
207+
195208
# Skip **kwargs
196209
if param.kind == inspect.Parameter.VAR_KEYWORD:
197210
has_kwargs = True

tests/test_subparser.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,11 @@ def configure(self):
110110
self.add_subparser("a", SubparserB)
111111
self.add_subparser("a", SubparserA)
112112

113-
if sys.version_info >= (3, 11):
114-
with self.assertRaises(ArgumentError):
113+
if sys.version_info >= (3, 14):
114+
with self.assertRaises(ValueError, msg="conflicting subparser: a"):
115+
Args().parse_args([])
116+
elif sys.version_info >= (3, 11):
117+
with self.assertRaises(ArgumentError, msg="argument {a}: conflicting subparser: a"):
115118
Args().parse_args([])
116119
else:
117120
args = Args().parse_args("a --bar 2".split())
@@ -134,8 +137,11 @@ def configure(self):
134137
self.add_subparsers(help="sub-command1 help")
135138
self.add_subparsers(help="sub-command2 help")
136139

137-
if sys.version_info >= (3, 12, 5):
138-
with self.assertRaises(ArgumentError):
140+
if sys.version_info >= (3, 14):
141+
with self.assertRaises(ValueError, msg="cannot have multiple subparser arguments"):
142+
Args().parse_args([])
143+
elif sys.version_info >= (3, 12, 5):
144+
with self.assertRaises(ArgumentError, msg="cannot have multiple subparser arguments"):
139145
Args().parse_args([])
140146
else:
141147
with self.assertRaises(SystemExit):

tests/test_tapify.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import unittest
1111
from unittest import TestCase
1212

13-
from packaging.version import Version
14-
1513
from tap import tapify
1614

1715

@@ -20,7 +18,7 @@
2018
except ModuleNotFoundError:
2119
_IS_PYDANTIC_V1 = None
2220
else:
23-
_IS_PYDANTIC_V1 = Version(pydantic.__version__) < Version("2.0.0")
21+
_IS_PYDANTIC_V1 = pydantic.VERSION.startswith("1.")
2422

2523

2624
# Suppress prints from SystemExit

tests/test_to_tap_class.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import sys
1010
from typing import Any, Callable, List, Literal, Optional, Type, Union
1111

12-
from packaging.version import Version
1312
import pytest
1413

1514
from tap import to_tap_class, Tap
@@ -21,7 +20,7 @@
2120
except ModuleNotFoundError:
2221
_IS_PYDANTIC_V1 = None
2322
else:
24-
_IS_PYDANTIC_V1 = Version(pydantic.__version__) < Version("2.0.0")
23+
_IS_PYDANTIC_V1 = pydantic.VERSION.startswith("1.")
2524

2625

2726
# To properly test the help message, we need to know how argparse formats it. It changed from 3.9 -> 3.10 -> 3.13
@@ -422,8 +421,8 @@ def test_subclasser_complex_help_message(class_or_function_: Any):
422421
423422
{_OPTIONS_TITLE}:
424423
{_ARG_WITH_ALIAS}
425-
(Union[float, int], default=3) This argument has a long name and will be aliased with a short
426-
one
424+
({type_to_str(Union[float, int])}, default=3) This argument has a long name and will be
425+
aliased with a short one
427426
--arg_int ARG_INT (int, required) some integer
428427
--arg_bool (bool, default=True)
429428
--arg_list [ARG_LIST {_ARG_LIST_DOTS}]
@@ -470,9 +469,7 @@ def test_subclasser_complex_help_message(class_or_function_: Any):
470469
),
471470
(
472471
"--arg_int 1 --baz X --foo b",
473-
SystemExit(
474-
r"error: argument \{a,b}: invalid choice: 'X' \(choose from '?a'?, '?b'?\)"
475-
),
472+
SystemExit(r"error: argument \{a,b}: invalid choice: 'X' \(choose from '?a'?, '?b'?\)"),
476473
),
477474
(
478475
"--arg_int 1 b --baz X --foo",
@@ -544,7 +541,7 @@ def test_subclasser_subparser(
544541
],
545542
)
546543
def test_subclasser_subparser_help_message(
547-
class_or_function_: Any, args_string_and_description_and_expected_message: tuple[str, str]
544+
class_or_function_: Any, args_string_and_description_and_expected_message: tuple[str, str, str]
548545
):
549546
args_string, description, expected_message = args_string_and_description_and_expected_message
550547
_test_subclasser_message(

tests/test_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
import subprocess
6+
import sys
67
from tempfile import TemporaryDirectory
78
from typing import Any, Callable, List, Literal, Dict, Set, Tuple, Union
89
import unittest
@@ -145,7 +146,11 @@ def test_type_to_str(self) -> None:
145146
self.assertEqual(type_to_str(List[bool]), "List[bool]")
146147
self.assertEqual(type_to_str(Set[int]), "Set[int]")
147148
self.assertEqual(type_to_str(Dict[str, int]), "Dict[str, int]")
148-
self.assertEqual(type_to_str(Union[List[int], Dict[float, bool]]), "Union[List[int], Dict[float, bool]]")
149+
150+
if sys.version_info >= (3, 14):
151+
self.assertEqual(type_to_str(Union[List[int], Dict[float, bool]]), "List[int] | Dict[float, bool]")
152+
else:
153+
self.assertEqual(type_to_str(Union[List[int], Dict[float, bool]]), "Union[List[int], Dict[float, bool]]")
149154

150155

151156
def class_decorator(cls):

0 commit comments

Comments
 (0)