Skip to content
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
1 change: 0 additions & 1 deletion .github/workflows/manual.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: |
3.8
3.9
3.10
3.11
Expand Down
18 changes: 9 additions & 9 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.8, 3.9, "3.10", 3.11, 3.12, 3.13]
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
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"]
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"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand All @@ -90,7 +90,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
cache: pip
- run: pip install tox
- run: tox -e omegaconf
Expand All @@ -101,7 +101,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
cache: pip
- name: With pydantic<2
run: |
Expand Down Expand Up @@ -134,7 +134,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Build package
run: |
pip install -U build
Expand All @@ -151,7 +151,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
cache: pip
- uses: actions/download-artifact@v4
with:
Expand All @@ -175,7 +175,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
cache: pip
- run: pip install -e .[all,doc]
- name: Run doc tests
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.8,3.9,3.10,3.11,3.12,3.13
-Dsonar.python.version=3.9,3.10,3.11,3.12,3.13
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ The semantic versioning only considers the public API as described in
paths are considered internals and can change in minor and patch releases.


v4.41.0 (2025-08-??)
--------------------

Changed
^^^^^^^
- Removed support for python 3.8 (`#752
<https://github.com/omni-us/jsonargparse/pull/752>`__).


v4.40.2 (2025-08-06)
--------------------

Expand Down
11 changes: 4 additions & 7 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,8 @@ Some notes about this support are:

- Postponed evaluation of types PEP `563 <https://peps.python.org/pep-0563/>`__
(i.e. ``from __future__ import annotations``) is supported. Also supported on
``python<=3.9`` are PEP `585 <https://peps.python.org/pep-0585/>`__ (i.e.
``list[<type>], dict[<type>], ...`` instead of ``List[<type>], Dict[<type>],
...``) and `604 <https://peps.python.org/pep-0604/>`__ (i.e. ``<type> |
<type>`` instead of ``Union[<type>, <type>]``).
``python==3.9`` is PEP `604 <https://peps.python.org/pep-0604/>`__ (i.e.
``<type> | <type>`` instead of ``Union[<type>, <type>]``).

- Types that use components imported inside ``TYPE_CHECKING`` blocks are
supported.
Expand Down Expand Up @@ -1941,9 +1939,8 @@ jsonargparse with the ``signatures`` extras require as explained in section

Many of the types defined in stub files use the latest syntax for type hints,
that is, bitwise or operator ``|`` for unions and generics, e.g.
``list[<type>]`` instead of ``typing.List[<type>]``, see PEPs `604
<https://peps.python.org/pep-0604>`__ and `585
<https://peps.python.org/pep-0585>`__. On python>=3.10 these are fully
``list[<type>]`` instead of ``typing.List[<type>]``, see PEP `604
<https://peps.python.org/pep-0604>`__. On python>=3.10 these are fully
supported. On python<=3.9 backporting these types is attempted and in some cases
it can fail. On failure the type annotation is set to ``Any``.

Expand Down
5 changes: 2 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,8 @@ Other notable features include:
- **Extensive type hint support:** nested types (union, optional), containers
(list, dict, etc.), user-defined generics, restricted types (regex, numbers),
paths, URLs, types from stubs (``*.pyi``), future annotations (PEP `563
<https://peps.python.org/pep-0563/>`__), and backports (PEPs `604
<https://peps.python.org/pep-0604>`__/`585
<https://peps.python.org/pep-0585>`__).
<https://peps.python.org/pep-0563/>`__), and backports (PEP `604
<https://peps.python.org/pep-0604>`__).

- **Keyword arguments introspection:** resolving of parameters used via
``**kwargs``.
Expand Down
8 changes: 5 additions & 3 deletions jsonargparse/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,11 @@ def parse_known_args(self, args=None, namespace=None):
namespace = argcomplete_namespace(caller, self, namespace)

try:
with patch_namespace(), parser_context(
parent_parser=self, lenient_check=True
), ActionTypeHint.subclass_arg_context(self):
with (
patch_namespace(),
parser_context(parent_parser=self, lenient_check=True),
ActionTypeHint.subclass_arg_context(self),
):
kwargs = {}
if _parse_known_has_intermixed:
kwargs["intermixed"] = False
Expand Down
59 changes: 6 additions & 53 deletions jsonargparse/_postponed_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from copy import deepcopy
from dataclasses import is_dataclass
from importlib import import_module
from typing import Any, Dict, ForwardRef, FrozenSet, List, Optional, Set, Tuple, Type, Union, get_type_hints
from typing import Any, ForwardRef, List, Optional, Union, get_type_hints

from ._optionals import typing_extensions_import
from ._typehints import mapping_origin_types, sequence_origin_types, tuple_set_origin_types
Expand All @@ -16,28 +16,9 @@
var_map = namedtuple("var_map", "name value")
none_map = var_map(name="NoneType", value=type(None))
union_map = var_map(name="Union", value=Union)
pep585_map = {
"dict": var_map(name="Dict", value=Dict),
"frozenset": var_map(name="FrozenSet", value=FrozenSet),
"list": var_map(name="List", value=List),
"set": var_map(name="Set", value=Set),
"tuple": var_map(name="Tuple", value=Tuple),
"type": var_map(name="Type", value=Type),
}


class BackportTypeHints(ast.NodeTransformer):
def visit_Subscript(self, node: ast.Subscript) -> ast.Subscript:
if isinstance(node.value, ast.Name) and node.value.id in pep585_map:
value = self.new_name_load(pep585_map[node.value.id])
else:
value = node.value # type: ignore[assignment]
return ast.Subscript(
value=value,
slice=self.visit(node.slice),
ctx=ast.Load(),
)

def visit_Constant(self, node: ast.Constant) -> Union[ast.Constant, ast.Name]:
if node.value is None:
return self.new_name_load(none_map)
Expand Down Expand Up @@ -193,44 +174,24 @@ def get_arg_type(arg_ast, aliases):
return exec_vars["___arg_type___"]


def getattr_recursive(obj, attr):
if "." in attr:
attr, *attrs = attr.split(".", 1)
return getattr_recursive(getattr(obj, attr), attrs[0])
return getattr(obj, attr)


def resolve_forward_refs(arg_type, aliases, logger):
if isinstance(arg_type, str) and arg_type in aliases:
arg_type = aliases[arg_type]

def resolve_subtypes_forward_refs(typehint):
if has_subtypes(typehint):
try:
subtypes = []
for arg in typehint.__args__:
if isinstance(arg, ForwardRef):
forward_arg, *forward_args = arg.__forward_arg__.split(".", 1)
forward_arg, *_ = arg.__forward_arg__.split(".", 1)
if forward_arg in aliases:
arg = aliases[forward_arg]
if forward_args:
arg = getattr_recursive(arg, forward_args[0])
else:
raise NameError(f"Name '{forward_arg}' is not defined")
else:
arg = resolve_subtypes_forward_refs(arg)
subtypes.append(arg)
if subtypes != list(typehint.__args__):
typehint_origin = get_typehint_origin(typehint)
if sys.version_info < (3, 10):
if typehint_origin in sequence_origin_types:
typehint_origin = List
elif typehint_origin in tuple_set_origin_types:
typehint_origin = Tuple
elif typehint_origin in mapping_origin_types:
typehint_origin = Dict
elif typehint_origin == type:
typehint_origin = Type
typehint = typehint_origin[tuple(subtypes)]
except Exception as ex:
if logger:
Expand Down Expand Up @@ -292,7 +253,7 @@ def get_types(obj: Any, logger: Optional[logging.Logger] = None) -> dict:
except Exception as ex2:
if isinstance(types, Exception):
if logger:
logger.debug(f"Failed to parse to source code for {obj}", exc_info=ex2)
logger.debug(f"Failed to parse the source code for {obj}", exc_info=ex2)
raise type(types)(f"{repr(types)} + {repr(ex2)}") from ex2 # type: ignore[arg-type]
return types

Expand All @@ -303,19 +264,13 @@ def get_types(obj: Any, logger: Optional[logging.Logger] = None) -> dict:
ex = types
types = {}

if isinstance(node, ast.FunctionDef):
arg_asts = [(a.arg, a.annotation) for a in node.args.args + node.args.kwonlyargs]
else:
arg_asts = [(a.target.id, a.annotation) for a in node.body if isinstance(a, ast.AnnAssign)] # type: ignore[union-attr]
arg_asts = [(a.arg, a.annotation) for a in node.args.args + node.args.kwonlyargs] # type: ignore[union-attr]

for name, annotation in arg_asts:
if annotation and (name not in types or type_requires_eval(types[name])):
try:
if isinstance(annotation, ast.Constant) and annotation.value in aliases:
types[name] = aliases[annotation.value]
else:
arg_type = get_arg_type(annotation, aliases)
types[name] = resolve_forward_refs(arg_type, aliases, logger)
arg_type = get_arg_type(annotation, aliases)
types[name] = resolve_forward_refs(arg_type, aliases, logger)
except Exception as ex3:
types[name] = ex3

Expand Down Expand Up @@ -355,8 +310,6 @@ def get_return_type(component, logger=None):
global_vars = get_global_vars(component, logger)
try:
return_type = get_type_hints(component, global_vars)["return"]
if isinstance(return_type, ForwardRef):
return_type = resolve_forward_refs(return_type.__forward_arg__, global_vars, logger)
except Exception as ex:
if logger:
logger.debug(f"Unable to evaluate types for {component}", exc_info=ex)
Expand Down
13 changes: 0 additions & 13 deletions jsonargparse/_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,17 +947,6 @@ def adapt_typehints(
required_keys.difference_update(
{k for k, v in typehint.__annotations__.items() if get_typehint_origin(v) in not_required_types}
)
# The standard library TypedDict in Python 3.8 does not store runtime information
# about which (if any) keys are optional. See https://bugs.python.org/issue38834.
# Thus, fall back to totality and explicitly Required keys
elif typehint.__total__:
required_keys = {
k for k, v in typehint.__annotations__.items() if get_typehint_origin(v) not in not_required_types
}
else:
required_keys = {
k for k, v in typehint.__annotations__.items() if get_typehint_origin(v) in required_types
}
missing_keys = required_keys - val.keys()
if missing_keys:
raise_unexpected_value(f"Missing required keys: {missing_keys}", val)
Expand Down Expand Up @@ -1118,8 +1107,6 @@ def adapt_typehints(
return adapt_typehints(val, get_alias_target(typehint), **adapt_kwargs)

else:
if str(typehint) == "+VT_co":
return val # required for typing.Mapping in python 3.8
raise RuntimeError(f"The code should never reach here: typehint={typehint}") # pragma: no cover

return val
Expand Down
2 changes: 0 additions & 2 deletions jsonargparse/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,6 @@ def class_from_function(
if isinstance(func_return, str):
try:
func_return = get_type_hints(func)["return"]
if isinstance(func_return, __import__("typing").ForwardRef):
func_return = func_return._evaluate(func.__globals__, {})
except Exception as ex:
func_return = inspect.signature(func).return_annotation
raise ValueError(f"Unable to dereference {func_return}, the return type of {func}: {ex}") from ex
Expand Down
5 changes: 4 additions & 1 deletion jsonargparse_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import platform
import re
import sys
from contextlib import ExitStack, contextmanager, redirect_stderr, redirect_stdout
from functools import wraps
from importlib.util import find_spec
Expand Down Expand Up @@ -184,7 +185,9 @@ def capture_logs(logger: logging.Logger) -> Iterator[StringIO]:


@contextmanager
def source_unavailable():
def source_unavailable(obj=None):
if obj and obj.__module__ in sys.modules:
del sys.modules[obj.__module__]
with patch("inspect.getsource", side_effect=OSError("mock source code not available")):
yield

Expand Down
13 changes: 8 additions & 5 deletions jsonargparse_tests/test_argcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ def complete_line(parser, value):

def test_handle_completions(parser):
parser.add_argument("--option")
with patch("argcomplete.autocomplete") as mock, patch.dict(
os.environ,
{
"_ARGCOMPLETE": "1",
},
with (
patch("argcomplete.autocomplete") as mock,
patch.dict(
os.environ,
{
"_ARGCOMPLETE": "1",
},
),
):
parser.parse_args([])
assert mock.called
Expand Down
1 change: 0 additions & 1 deletion jsonargparse_tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ def conditional_function(fn: "Literal['A', 'B']", *args, **kwargs):
raise NotImplementedError(fn)


@pytest.mark.skipif(condition=sys.version_info < (3, 9), reason="python>=3.9 is required")
def test_literal_conditional_function():
out = get_cli_stdout(conditional_function, args=["--help"])
assert "Conditional arguments" in out
Expand Down
Loading
Loading