Skip to content

Commit 08eaf4d

Browse files
authored
Add support for python 3.14 (#753)
1 parent aaa8cb2 commit 08eaf4d

File tree

11 files changed

+94
-31
lines changed

11 files changed

+94
-31
lines changed

.github/workflows/tests.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
22+
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"]
2323
steps:
2424
- uses: actions/checkout@v4
2525
- uses: actions/setup-python@v5
@@ -59,7 +59,7 @@ jobs:
5959
strategy:
6060
fail-fast: false
6161
matrix:
62-
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
62+
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"]
6363
steps:
6464
- uses: actions/checkout@v4
6565
- uses: actions/setup-python@v5
@@ -74,7 +74,7 @@ jobs:
7474
strategy:
7575
fail-fast: false
7676
matrix:
77-
python: ["3.10", "3.12"]
77+
python: ["3.10", "3.12", "3.14-dev"]
7878
steps:
7979
- uses: actions/checkout@v4
8080
- uses: actions/setup-python@v5
@@ -249,7 +249,7 @@ jobs:
249249
-Dsonar.exclusions=sphinx/**
250250
-Dsonar.tests=jsonargparse_tests
251251
-Dsonar.python.coverage.reportPaths=coverage_*.xml
252-
-Dsonar.python.version=3.9,3.10,3.11,3.12,3.13
252+
-Dsonar.python.version=3.9,3.10,3.11,3.12,3.13,3.14
253253
env:
254254
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
255255
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ paths are considered internals and can change in minor and patch releases.
1515
v4.41.0 (2025-08-??)
1616
--------------------
1717

18+
Added
19+
^^^^^
20+
- Support for Python 3.14 (`#753
21+
<https://github.com/omni-us/jsonargparse/pull/753>`__).
22+
1823
Changed
1924
^^^^^^^
2025
- Removed support for python 3.8 (`#752

jsonargparse/_typehints.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1615,9 +1615,10 @@ def __init__(self, class_type: Type, lazy_kwargs: dict):
16151615
self.__dict__[name] = seen_methods[id(member)]
16161616
else:
16171617
lazy_method = partial(self._lazy_init_then_call_method, name)
1618-
self.__dict__[name] = lazy_method
16191618
if name == "__call__":
1619+
lazy_method = staticmethod(lazy_method) # type: ignore[assignment]
16201620
self._lazy.__call__ = lazy_method # type: ignore[method-assign]
1621+
self.__dict__[name] = lazy_method
16211622
seen_methods[id(member)] = lazy_method
16221623

16231624
def _lazy_init(self):

jsonargparse_tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ def source_unavailable(obj=None):
192192
yield
193193

194194

195+
@pytest.fixture(autouse=True)
196+
def no_color():
197+
with patch.dict(os.environ, {"NO_COLOR": "true"}):
198+
yield
199+
200+
195201
def get_parser_help(parser: ArgumentParser, strip=False, columns=columns) -> str:
196202
out = StringIO()
197203
with patch.dict(os.environ, {"COLUMNS": columns}):

jsonargparse_tests/test_core.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
import pickle
6+
import sys
67
from calendar import Calendar
78
from contextlib import redirect_stderr
89
from io import StringIO
@@ -266,16 +267,17 @@ def test_default_env_override_false():
266267

267268

268269
def test_env_prefix_true():
269-
parser = ArgumentParser(env_prefix=True, default_env=True, exit_on_error=False)
270-
parser.add_argument("--test_arg", type=str, required=True)
270+
with patch("sys.argv", ["fake.py"]), patch.dict(sys.modules, {"__main__": {}}):
271+
parser = ArgumentParser(env_prefix=True, default_env=True, exit_on_error=False)
272+
assert parser.prog == "fake.py"
273+
parser.add_argument("--test_arg", type=str, required=True)
271274

272-
with patch.dict(os.environ, {"TEST_ARG": "one"}):
273-
pytest.raises(ArgumentError, lambda: parser.parse_args([]))
275+
with patch.dict(os.environ, {"TEST_ARG": "one"}):
276+
pytest.raises(ArgumentError, lambda: parser.parse_args([]))
274277

275-
prefix = os.path.splitext(parser.prog)[0].upper()
276-
with patch.dict(os.environ, {f"{prefix}_TEST_ARG": "one"}):
277-
cfg = parser.parse_args([])
278-
assert "one" == cfg.test_arg
278+
with patch.dict(os.environ, {"FAKE_TEST_ARG": "one"}):
279+
cfg = parser.parse_args([])
280+
assert "one" == cfg.test_arg
279281

280282

281283
def test_env_prefix_false():

jsonargparse_tests/test_dataclass_like.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dataclasses
44
import json
5+
import sys
56
from typing import Any, Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union
67
from unittest.mock import patch
78

@@ -537,8 +538,12 @@ def test_generic_dataclass(parser):
537538
help_str = get_parser_help(parser).lower()
538539
assert "--data.g1 g1 (required, type: int)" in help_str
539540
assert "--data.g2 [item,...] (required, type: tuple[int, int])" in help_str
540-
assert "--data.g3 g3 (required, type: union[str, int])" in help_str
541-
assert "--data.g4 g4 (required, type: dict[str, union[int, bool]])" in help_str
541+
if sys.version_info < (3, 14):
542+
assert "--data.g3 g3 (required, type: union[str, int])" in help_str
543+
assert "--data.g4 g4 (required, type: dict[str, union[int, bool]])" in help_str
544+
else:
545+
assert "--data.g3 g3 (required, type: str | int)" in help_str
546+
assert "--data.g4 g4 (required, type: dict[str, int | bool])" in help_str
542547

543548

544549
@dataclasses.dataclass
@@ -551,8 +556,12 @@ def test_nested_generic_dataclass(parser):
551556
help_str = get_parser_help(parser).lower()
552557
assert "--x.y.g1 g1 (required, type: float)" in help_str
553558
assert "--x.y.g2 [item,...] (required, type: tuple[float, float])" in help_str
554-
assert "--x.y.g3 g3 (required, type: union[str, float])" in help_str
555-
assert "--x.y.g4 g4 (required, type: dict[str, union[float, bool]])" in help_str
559+
if sys.version_info < (3, 14):
560+
assert "--x.y.g3 g3 (required, type: union[str, float])" in help_str
561+
assert "--x.y.g4 g4 (required, type: dict[str, union[float, bool]])" in help_str
562+
else:
563+
assert "--x.y.g3 g3 (required, type: str | float)" in help_str
564+
assert "--x.y.g4 g4 (required, type: dict[str, float | bool])" in help_str
556565

557566

558567
V = TypeVar("V")

jsonargparse_tests/test_postponed_annotations.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dataclasses
44
import os
5+
import sys
56
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
67

78
import pytest
@@ -172,8 +173,12 @@ def function_type_checking_union(p1: Union[bool, TypeCheckingClass1, int], p2: U
172173
def test_get_types_type_checking_union():
173174
types = get_types(function_type_checking_union)
174175
assert list(types) == ["p1", "p2"]
175-
assert str(types["p1"]) == f"typing.Union[bool, {__name__}.TypeCheckingClass1, int]"
176-
assert str(types["p2"]) == f"typing.Union[float, {__name__}.TypeCheckingClass2]"
176+
if sys.version_info < (3, 14):
177+
assert str(types["p1"]) == f"typing.Union[bool, {__name__}.TypeCheckingClass1, int]"
178+
assert str(types["p2"]) == f"typing.Union[float, {__name__}.TypeCheckingClass2]"
179+
else:
180+
assert str(types["p1"]) == f"bool | {__name__}.TypeCheckingClass1 | int"
181+
assert str(types["p2"]) == f"float | {__name__}.TypeCheckingClass2"
177182

178183

179184
def function_type_checking_alias(p1: type_checking_alias, p2: "type_checking_alias"):
@@ -183,8 +188,12 @@ def function_type_checking_alias(p1: type_checking_alias, p2: "type_checking_ali
183188
def test_get_types_type_checking_alias():
184189
types = get_types(function_type_checking_alias)
185190
assert list(types) == ["p1", "p2"]
186-
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
187-
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
191+
if sys.version_info < (3, 14):
192+
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
193+
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
194+
else:
195+
assert str(types["p1"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str]"
196+
assert str(types["p2"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str]"
188197

189198

190199
def function_type_checking_optional_alias(p1: type_checking_alias | None, p2: Optional["type_checking_alias"]):
@@ -194,8 +203,12 @@ def function_type_checking_optional_alias(p1: type_checking_alias | None, p2: Op
194203
def test_get_types_type_checking_optional_alias():
195204
types = get_types(function_type_checking_optional_alias)
196205
assert list(types) == ["p1", "p2"]
197-
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
198-
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
206+
if sys.version_info < (3, 14):
207+
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
208+
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
209+
else:
210+
assert str(types["p1"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str] | None"
211+
assert str(types["p2"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str] | None"
199212

200213

201214
def function_type_checking_list(p1: List[Union["TypeCheckingClass1", TypeCheckingClass2]]):
@@ -206,7 +219,10 @@ def test_get_types_type_checking_list():
206219
types = get_types(function_type_checking_list)
207220
assert list(types) == ["p1"]
208221
lst = "typing.List"
209-
assert str(types["p1"]) == f"{lst}[typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
222+
if sys.version_info < (3, 14):
223+
assert str(types["p1"]) == f"{lst}[typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
224+
else:
225+
assert str(types["p1"]) == f"{lst}[{__name__}.TypeCheckingClass1 | {__name__}.TypeCheckingClass2]"
210226

211227

212228
def function_type_checking_tuple(p1: Tuple[TypeCheckingClass1, "TypeCheckingClass2"]):
@@ -239,7 +255,13 @@ def test_get_types_type_checking_dict():
239255
types = get_types(function_type_checking_dict)
240256
assert list(types) == ["p1"]
241257
dct = "typing.Dict"
242-
assert str(types["p1"]) == f"{dct}[str, typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
258+
if sys.version_info < (3, 14):
259+
assert (
260+
str(types["p1"])
261+
== f"{dct}[str, typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
262+
)
263+
else:
264+
assert str(types["p1"]) == f"{dct}[str, {__name__}.TypeCheckingClass1 | {__name__}.TypeCheckingClass2]"
243265

244266

245267
def function_type_checking_undefined_forward_ref(p1: List["Undefined"], p2: bool): # type: ignore # noqa: F821

jsonargparse_tests/test_signatures.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import sys
45
from pathlib import Path
56
from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar, Union
67
from unittest.mock import patch
@@ -308,10 +309,17 @@ def test_add_class_conditional_kwargs(parser):
308309
expected += [
309310
"help for func (required, type: str)",
310311
"help for kmg1 (type: int, default: 1)",
311-
"help for kmg2 (type: Union[str, float], default: Conditional<ast-resolver> {-, 2.3})",
312312
"help for kmg3 (type: bool, default: Conditional<ast-resolver> {True, False})",
313313
"help for kmg4 (type: int, default: Conditional<ast-resolver> {4, NOT_ACCEPTED})",
314314
]
315+
if sys.version_info < (3, 14):
316+
expected += [
317+
"help for kmg2 (type: Union[str, float], default: Conditional<ast-resolver> {-, 2.3})",
318+
]
319+
else:
320+
expected += [
321+
"help for kmg2 (type: str | float, default: Conditional<ast-resolver> {-, 2.3})",
322+
]
315323
for value in expected:
316324
assert value in help_str
317325

jsonargparse_tests/test_stubs_resolver.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,16 @@ def test_get_params_classmethod():
165165
"debug",
166166
"errorlevel",
167167
]
168-
if sys.version_info >= (3, 12):
168+
if sys.version_info >= (3, 14):
169+
expected = expected[:4] + ["compresslevel", "preset"] + expected[4:]
170+
elif sys.version_info >= (3, 12):
169171
expected = expected[:4] + ["compresslevel"] + expected[4:]
170172
assert expected == get_param_names(params)[: len(expected)]
171173
if sys.version_info >= (3, 10):
172174
assert all(
173-
p.annotation is not inspect._empty for p in params if p.name not in {"fileobj", "compresslevel", "stream"}
175+
p.annotation is not inspect._empty
176+
for p in params
177+
if p.name not in {"fileobj", "compresslevel", "stream", "preset"}
174178
)
175179
with mock_stubs_missing_types():
176180
params = get_params(TarFile, "open")

jsonargparse_tests/test_typehints.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,10 @@ def test_tuple_union(parser, tmp_cwd):
345345
pytest.raises(ArgumentError, lambda: parser.parse_args(['--tuple=[2, "a"]']))
346346
pytest.raises(ArgumentError, lambda: parser.parse_args(['--tuple={"a":1, "b":"2"}']))
347347
help_str = get_parser_help(parser, strip=True)
348-
assert "--tuple [ITEM,...] (type: Tuple[Union[int, EnumABC], Path_fc, NotEmptyStr], default: null)" in help_str
348+
if sys.version_info < (3, 14):
349+
assert "--tuple [ITEM,...] (type: Tuple[Union[int, EnumABC], Path_fc, NotEmptyStr], default: null)" in help_str
350+
else:
351+
assert "--tuple [ITEM,...] (type: Tuple[int | EnumABC, Path_fc, NotEmptyStr], default: null)" in help_str
349352

350353

351354
# list tests
@@ -1404,6 +1407,8 @@ def test_lazy_instance_callable():
14041407
optimizer = lazy_optimizer([1, 2])
14051408
assert optimizer.lr == 0.2
14061409
assert optimizer.params == [1, 2]
1410+
optimizer = lazy_optimizer([3, 4])
1411+
assert optimizer.params == [3, 4]
14071412

14081413

14091414
# other tests

0 commit comments

Comments
 (0)