Skip to content

Add support for python 3.14 #753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down Expand Up @@ -59,7 +59,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand All @@ -74,7 +74,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.12"]
python: ["3.10", "3.12", "3.14-dev"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down Expand Up @@ -249,7 +249,7 @@ jobs:
-Dsonar.exclusions=sphinx/**
-Dsonar.tests=jsonargparse_tests
-Dsonar.python.coverage.reportPaths=coverage_*.xml
-Dsonar.python.version=3.9,3.10,3.11,3.12,3.13
-Dsonar.python.version=3.9,3.10,3.11,3.12,3.13,3.14
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ paths are considered internals and can change in minor and patch releases.
v4.41.0 (2025-08-??)
--------------------

Added
^^^^^
- Support for Python 3.14 (`#753
<https://github.com/omni-us/jsonargparse/pull/753>`__).

Changed
^^^^^^^
- Removed support for python 3.8 (`#752
Expand Down
3 changes: 2 additions & 1 deletion jsonargparse/_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1615,9 +1615,10 @@ def __init__(self, class_type: Type, lazy_kwargs: dict):
self.__dict__[name] = seen_methods[id(member)]
else:
lazy_method = partial(self._lazy_init_then_call_method, name)
self.__dict__[name] = lazy_method
if name == "__call__":
lazy_method = staticmethod(lazy_method) # type: ignore[assignment]
self._lazy.__call__ = lazy_method # type: ignore[method-assign]
self.__dict__[name] = lazy_method
seen_methods[id(member)] = lazy_method

def _lazy_init(self):
Expand Down
6 changes: 6 additions & 0 deletions jsonargparse_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ def source_unavailable(obj=None):
yield


@pytest.fixture(autouse=True)
def no_color():
with patch.dict(os.environ, {"NO_COLOR": "true"}):
yield


def get_parser_help(parser: ArgumentParser, strip=False, columns=columns) -> str:
out = StringIO()
with patch.dict(os.environ, {"COLUMNS": columns}):
Expand Down
18 changes: 10 additions & 8 deletions jsonargparse_tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import os
import pickle
import sys
from calendar import Calendar
from contextlib import redirect_stderr
from io import StringIO
Expand Down Expand Up @@ -266,16 +267,17 @@ def test_default_env_override_false():


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

with patch.dict(os.environ, {"TEST_ARG": "one"}):
pytest.raises(ArgumentError, lambda: parser.parse_args([]))
with patch.dict(os.environ, {"TEST_ARG": "one"}):
pytest.raises(ArgumentError, lambda: parser.parse_args([]))

prefix = os.path.splitext(parser.prog)[0].upper()
with patch.dict(os.environ, {f"{prefix}_TEST_ARG": "one"}):
cfg = parser.parse_args([])
assert "one" == cfg.test_arg
with patch.dict(os.environ, {"FAKE_TEST_ARG": "one"}):
cfg = parser.parse_args([])
assert "one" == cfg.test_arg


def test_env_prefix_false():
Expand Down
17 changes: 13 additions & 4 deletions jsonargparse_tests/test_dataclass_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dataclasses
import json
import sys
from typing import Any, Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union
from unittest.mock import patch

Expand Down Expand Up @@ -537,8 +538,12 @@ def test_generic_dataclass(parser):
help_str = get_parser_help(parser).lower()
assert "--data.g1 g1 (required, type: int)" in help_str
assert "--data.g2 [item,...] (required, type: tuple[int, int])" in help_str
assert "--data.g3 g3 (required, type: union[str, int])" in help_str
assert "--data.g4 g4 (required, type: dict[str, union[int, bool]])" in help_str
if sys.version_info < (3, 14):
assert "--data.g3 g3 (required, type: union[str, int])" in help_str
assert "--data.g4 g4 (required, type: dict[str, union[int, bool]])" in help_str
else:
assert "--data.g3 g3 (required, type: str | int)" in help_str
assert "--data.g4 g4 (required, type: dict[str, int | bool])" in help_str


@dataclasses.dataclass
Expand All @@ -551,8 +556,12 @@ def test_nested_generic_dataclass(parser):
help_str = get_parser_help(parser).lower()
assert "--x.y.g1 g1 (required, type: float)" in help_str
assert "--x.y.g2 [item,...] (required, type: tuple[float, float])" in help_str
assert "--x.y.g3 g3 (required, type: union[str, float])" in help_str
assert "--x.y.g4 g4 (required, type: dict[str, union[float, bool]])" in help_str
if sys.version_info < (3, 14):
assert "--x.y.g3 g3 (required, type: union[str, float])" in help_str
assert "--x.y.g4 g4 (required, type: dict[str, union[float, bool]])" in help_str
else:
assert "--x.y.g3 g3 (required, type: str | float)" in help_str
assert "--x.y.g4 g4 (required, type: dict[str, float | bool])" in help_str


V = TypeVar("V")
Expand Down
38 changes: 30 additions & 8 deletions jsonargparse_tests/test_postponed_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dataclasses
import os
import sys
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union

import pytest
Expand Down Expand Up @@ -172,8 +173,12 @@ def function_type_checking_union(p1: Union[bool, TypeCheckingClass1, int], p2: U
def test_get_types_type_checking_union():
types = get_types(function_type_checking_union)
assert list(types) == ["p1", "p2"]
assert str(types["p1"]) == f"typing.Union[bool, {__name__}.TypeCheckingClass1, int]"
assert str(types["p2"]) == f"typing.Union[float, {__name__}.TypeCheckingClass2]"
if sys.version_info < (3, 14):
assert str(types["p1"]) == f"typing.Union[bool, {__name__}.TypeCheckingClass1, int]"
assert str(types["p2"]) == f"typing.Union[float, {__name__}.TypeCheckingClass2]"
else:
assert str(types["p1"]) == f"bool | {__name__}.TypeCheckingClass1 | int"
assert str(types["p2"]) == f"float | {__name__}.TypeCheckingClass2"


def function_type_checking_alias(p1: type_checking_alias, p2: "type_checking_alias"):
Expand All @@ -183,8 +188,12 @@ def function_type_checking_alias(p1: type_checking_alias, p2: "type_checking_ali
def test_get_types_type_checking_alias():
types = get_types(function_type_checking_alias)
assert list(types) == ["p1", "p2"]
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
if sys.version_info < (3, 14):
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str]]"
else:
assert str(types["p1"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str]"
assert str(types["p2"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str]"


def function_type_checking_optional_alias(p1: type_checking_alias | None, p2: Optional["type_checking_alias"]):
Expand All @@ -194,8 +203,12 @@ def function_type_checking_optional_alias(p1: type_checking_alias | None, p2: Op
def test_get_types_type_checking_optional_alias():
types = get_types(function_type_checking_optional_alias)
assert list(types) == ["p1", "p2"]
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
if sys.version_info < (3, 14):
assert str(types["p1"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
assert str(types["p2"]) == f"typing.Union[int, {__name__}.TypeCheckingClass2, typing.List[str], NoneType]"
else:
assert str(types["p1"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str] | None"
assert str(types["p2"]) == f"int | {__name__}.TypeCheckingClass2 | typing.List[str] | None"


def function_type_checking_list(p1: List[Union["TypeCheckingClass1", TypeCheckingClass2]]):
Expand All @@ -206,7 +219,10 @@ def test_get_types_type_checking_list():
types = get_types(function_type_checking_list)
assert list(types) == ["p1"]
lst = "typing.List"
assert str(types["p1"]) == f"{lst}[typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
if sys.version_info < (3, 14):
assert str(types["p1"]) == f"{lst}[typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
else:
assert str(types["p1"]) == f"{lst}[{__name__}.TypeCheckingClass1 | {__name__}.TypeCheckingClass2]"


def function_type_checking_tuple(p1: Tuple[TypeCheckingClass1, "TypeCheckingClass2"]):
Expand Down Expand Up @@ -239,7 +255,13 @@ def test_get_types_type_checking_dict():
types = get_types(function_type_checking_dict)
assert list(types) == ["p1"]
dct = "typing.Dict"
assert str(types["p1"]) == f"{dct}[str, typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
if sys.version_info < (3, 14):
assert (
str(types["p1"])
== f"{dct}[str, typing.Union[{__name__}.TypeCheckingClass1, {__name__}.TypeCheckingClass2]]"
)
else:
assert str(types["p1"]) == f"{dct}[str, {__name__}.TypeCheckingClass1 | {__name__}.TypeCheckingClass2]"


def function_type_checking_undefined_forward_ref(p1: List["Undefined"], p2: bool): # type: ignore # noqa: F821
Expand Down
10 changes: 9 additions & 1 deletion jsonargparse_tests/test_signatures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar, Union
from unittest.mock import patch
Expand Down Expand Up @@ -308,10 +309,17 @@ def test_add_class_conditional_kwargs(parser):
expected += [
"help for func (required, type: str)",
"help for kmg1 (type: int, default: 1)",
"help for kmg2 (type: Union[str, float], default: Conditional<ast-resolver> {-, 2.3})",
"help for kmg3 (type: bool, default: Conditional<ast-resolver> {True, False})",
"help for kmg4 (type: int, default: Conditional<ast-resolver> {4, NOT_ACCEPTED})",
]
if sys.version_info < (3, 14):
expected += [
"help for kmg2 (type: Union[str, float], default: Conditional<ast-resolver> {-, 2.3})",
]
else:
expected += [
"help for kmg2 (type: str | float, default: Conditional<ast-resolver> {-, 2.3})",
]
for value in expected:
assert value in help_str

Expand Down
8 changes: 6 additions & 2 deletions jsonargparse_tests/test_stubs_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,16 @@ def test_get_params_classmethod():
"debug",
"errorlevel",
]
if sys.version_info >= (3, 12):
if sys.version_info >= (3, 14):
expected = expected[:4] + ["compresslevel", "preset"] + expected[4:]
elif sys.version_info >= (3, 12):
expected = expected[:4] + ["compresslevel"] + expected[4:]
assert expected == get_param_names(params)[: len(expected)]
if sys.version_info >= (3, 10):
assert all(
p.annotation is not inspect._empty for p in params if p.name not in {"fileobj", "compresslevel", "stream"}
p.annotation is not inspect._empty
for p in params
if p.name not in {"fileobj", "compresslevel", "stream", "preset"}
)
with mock_stubs_missing_types():
params = get_params(TarFile, "open")
Expand Down
7 changes: 6 additions & 1 deletion jsonargparse_tests/test_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,10 @@ def test_tuple_union(parser, tmp_cwd):
pytest.raises(ArgumentError, lambda: parser.parse_args(['--tuple=[2, "a"]']))
pytest.raises(ArgumentError, lambda: parser.parse_args(['--tuple={"a":1, "b":"2"}']))
help_str = get_parser_help(parser, strip=True)
assert "--tuple [ITEM,...] (type: Tuple[Union[int, EnumABC], Path_fc, NotEmptyStr], default: null)" in help_str
if sys.version_info < (3, 14):
assert "--tuple [ITEM,...] (type: Tuple[Union[int, EnumABC], Path_fc, NotEmptyStr], default: null)" in help_str
else:
assert "--tuple [ITEM,...] (type: Tuple[int | EnumABC, Path_fc, NotEmptyStr], default: null)" in help_str


# list tests
Expand Down Expand Up @@ -1404,6 +1407,8 @@ def test_lazy_instance_callable():
optimizer = lazy_optimizer([1, 2])
assert optimizer.lr == 0.2
assert optimizer.params == [1, 2]
optimizer = lazy_optimizer([3, 4])
assert optimizer.params == [3, 4]


# other tests
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Intended Audience :: Developers",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
Expand Down Expand Up @@ -93,7 +94,7 @@ test = [
"types-PyYAML>=6.0.11",
"types-requests>=2.28.9",
"responses>=0.12.0",
"pydantic>=2.3.0",
"pydantic>=2.3.0; python_version < '3.14'", # restricted until pydantic installs correctly in 3.14
"attrs>=22.2.0",
]
test-no-urls = [
Expand Down Expand Up @@ -189,7 +190,7 @@ Villegas = "Villegas"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py{39,310,311,312,313}-{all,no}-extras,omegaconf,pydantic-v1,without-pyyaml,without-future-annotations
envlist = py{39,310,311,312,313,314}-{all,no}-extras,omegaconf,pydantic-v1,without-pyyaml,without-future-annotations
skip_missing_interpreters = true

[testenv]
Expand Down
Loading