From c24d5cfc57a0552bf0c4c057db4270636ddcc0fa Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Fri, 8 Aug 2025 07:15:05 +0200 Subject: [PATCH 1/4] Add support for python 3.14 --- .github/workflows/tests.yaml | 8 ++-- CHANGELOG.rst | 5 +++ jsonargparse/_typehints.py | 3 +- jsonargparse_tests/conftest.py | 6 +++ jsonargparse_tests/test_core.py | 18 +++++---- jsonargparse_tests/test_dataclass_like.py | 16 ++++++-- .../test_postponed_annotations.py | 37 +++++++++++++++---- jsonargparse_tests/test_signatures.py | 10 ++++- jsonargparse_tests/test_stubs_resolver.py | 8 +++- jsonargparse_tests/test_typehints.py | 7 +++- pyproject.toml | 5 ++- 11 files changed, 92 insertions(+), 31 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b3893cff..6c726a20 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -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"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -74,7 +74,7 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.10", "3.12"] + python: ["3.10", "3.12", "3.14"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -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 }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eeea6bb7..e0cbb279 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 (`#??? + `__). + Changed ^^^^^^^ - Removed support for python 3.8 (`#752 diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index b17c4962..24d6c717 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -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): diff --git a/jsonargparse_tests/conftest.py b/jsonargparse_tests/conftest.py index 0064655f..276e5ed3 100644 --- a/jsonargparse_tests/conftest.py +++ b/jsonargparse_tests/conftest.py @@ -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}): diff --git a/jsonargparse_tests/test_core.py b/jsonargparse_tests/test_core.py index cbef2a8a..d3d1e15c 100644 --- a/jsonargparse_tests/test_core.py +++ b/jsonargparse_tests/test_core.py @@ -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 @@ -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(): diff --git a/jsonargparse_tests/test_dataclass_like.py b/jsonargparse_tests/test_dataclass_like.py index 8ea09cd3..973bcb38 100644 --- a/jsonargparse_tests/test_dataclass_like.py +++ b/jsonargparse_tests/test_dataclass_like.py @@ -537,8 +537,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 @@ -551,8 +555,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") diff --git a/jsonargparse_tests/test_postponed_annotations.py b/jsonargparse_tests/test_postponed_annotations.py index 5741701a..85aa5fac 100644 --- a/jsonargparse_tests/test_postponed_annotations.py +++ b/jsonargparse_tests/test_postponed_annotations.py @@ -172,8 +172,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"): @@ -183,8 +187,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"]): @@ -194,8 +202,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]]): @@ -206,7 +218,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"]): @@ -239,7 +254,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 diff --git a/jsonargparse_tests/test_signatures.py b/jsonargparse_tests/test_signatures.py index 4930dbd8..da706d11 100644 --- a/jsonargparse_tests/test_signatures.py +++ b/jsonargparse_tests/test_signatures.py @@ -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 @@ -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 {-, 2.3})", "help for kmg3 (type: bool, default: Conditional {True, False})", "help for kmg4 (type: int, default: Conditional {4, NOT_ACCEPTED})", ] + if sys.version_info < (3, 14): + expected += [ + "help for kmg2 (type: Union[str, float], default: Conditional {-, 2.3})", + ] + else: + expected += [ + "help for kmg2 (type: str | float, default: Conditional {-, 2.3})", + ] for value in expected: assert value in help_str diff --git a/jsonargparse_tests/test_stubs_resolver.py b/jsonargparse_tests/test_stubs_resolver.py index 8d6c4402..4d4f22bc 100644 --- a/jsonargparse_tests/test_stubs_resolver.py +++ b/jsonargparse_tests/test_stubs_resolver.py @@ -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") diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py index 49b1c813..a9f87c34 100644 --- a/jsonargparse_tests/test_typehints.py +++ b/jsonargparse_tests/test_typehints.py @@ -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 @@ -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 diff --git a/pyproject.toml b/pyproject.toml index faa9c80f..2f6b2b1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -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 = [ @@ -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] From a7642b319f3a07b792a1c11bb48d805764350ae0 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Fri, 8 Aug 2025 07:44:15 +0200 Subject: [PATCH 2/4] Python 3.14-dev release --- .github/workflows/tests.yaml | 9 ++++++--- CHANGELOG.rst | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6c726a20..4b80940f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,7 +19,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + # python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -59,7 +60,8 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + # python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -74,7 +76,8 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.10", "3.12", "3.14"] + # python: ["3.10", "3.12", "3.14"] + python: ["3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e0cbb279..c8a2197b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,8 +17,8 @@ v4.41.0 (2025-08-??) Added ^^^^^ -- Support for Python 3.14 (`#??? - `__). +- Support for Python 3.14 (`#753 + `__). Changed ^^^^^^^ From 189af622d7cce57b9862248e3a5c764b9ad2e36f Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:18:18 +0200 Subject: [PATCH 3/4] Again test all python versions --- .github/workflows/tests.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4b80940f..2ae3c990 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,8 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - python: ["3.14-dev"] + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -60,8 +59,7 @@ jobs: strategy: fail-fast: false matrix: - # python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - python: ["3.14-dev"] + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -76,8 +74,7 @@ jobs: strategy: fail-fast: false matrix: - # python: ["3.10", "3.12", "3.14"] - python: ["3.14-dev"] + python: ["3.10", "3.12", "3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From e33a86e2a6139bd9d2364f5d1b3e81d3dffc5665 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:13:16 +0200 Subject: [PATCH 4/4] Rebase fixes --- jsonargparse_tests/test_dataclass_like.py | 1 + jsonargparse_tests/test_postponed_annotations.py | 1 + 2 files changed, 2 insertions(+) diff --git a/jsonargparse_tests/test_dataclass_like.py b/jsonargparse_tests/test_dataclass_like.py index 973bcb38..c03f7aa6 100644 --- a/jsonargparse_tests/test_dataclass_like.py +++ b/jsonargparse_tests/test_dataclass_like.py @@ -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 diff --git a/jsonargparse_tests/test_postponed_annotations.py b/jsonargparse_tests/test_postponed_annotations.py index 85aa5fac..726dbd46 100644 --- a/jsonargparse_tests/test_postponed_annotations.py +++ b/jsonargparse_tests/test_postponed_annotations.py @@ -2,6 +2,7 @@ import dataclasses import os +import sys from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union import pytest