diff --git a/.github/workflows/manual.yaml b/.github/workflows/manual.yaml index 0d6919a2..9245d98a 100644 --- a/.github/workflows/manual.yaml +++ b/.github/workflows/manual.yaml @@ -20,7 +20,6 @@ jobs: - uses: actions/setup-python@v5 with: python-version: | - 3.8 3.9 3.10 3.11 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9a4a0bd5..b3893cff 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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 @@ -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 @@ -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 @@ -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 @@ -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: | @@ -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 @@ -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: @@ -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 @@ -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 }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 083a37ee..eeea6bb7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 + `__). + + v4.40.2 (2025-08-06) -------------------- diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index 688919c3..c922ffd5 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -428,10 +428,8 @@ Some notes about this support are: - Postponed evaluation of types PEP `563 `__ (i.e. ``from __future__ import annotations``) is supported. Also supported on - ``python<=3.9`` are PEP `585 `__ (i.e. - ``list[], dict[], ...`` instead of ``List[], Dict[], - ...``) and `604 `__ (i.e. `` | - `` instead of ``Union[, ]``). + ``python==3.9`` is PEP `604 `__ (i.e. + `` | `` instead of ``Union[, ]``). - Types that use components imported inside ``TYPE_CHECKING`` blocks are supported. @@ -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[]`` instead of ``typing.List[]``, see PEPs `604 -`__ and `585 -`__. On python>=3.10 these are fully +``list[]`` instead of ``typing.List[]``, see PEP `604 +`__. 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``. diff --git a/README.rst b/README.rst index ffa6d07b..e33ca0fa 100644 --- a/README.rst +++ b/README.rst @@ -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 - `__), and backports (PEPs `604 - `__/`585 - `__). + `__), and backports (PEP `604 + `__). - **Keyword arguments introspection:** resolving of parameters used via ``**kwargs``. diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 499dd488..1d715e4c 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -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 diff --git a/jsonargparse/_postponed_annotations.py b/jsonargparse/_postponed_annotations.py index 646a4821..f085b7ff 100644 --- a/jsonargparse/_postponed_annotations.py +++ b/jsonargparse/_postponed_annotations.py @@ -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 @@ -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) @@ -193,16 +174,7 @@ 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): @@ -210,11 +182,9 @@ def resolve_subtypes_forward_refs(typehint): 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: @@ -222,15 +192,6 @@ def resolve_subtypes_forward_refs(typehint): 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: @@ -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 @@ -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 @@ -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) diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index 64af9aa5..b17c4962 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -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) @@ -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 diff --git a/jsonargparse/_util.py b/jsonargparse/_util.py index 1b16f59e..d9adbf9b 100644 --- a/jsonargparse/_util.py +++ b/jsonargparse/_util.py @@ -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 diff --git a/jsonargparse_tests/conftest.py b/jsonargparse_tests/conftest.py index 506cd0d7..0064655f 100644 --- a/jsonargparse_tests/conftest.py +++ b/jsonargparse_tests/conftest.py @@ -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 @@ -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 diff --git a/jsonargparse_tests/test_argcomplete.py b/jsonargparse_tests/test_argcomplete.py index 7c3a3a53..9d019773 100644 --- a/jsonargparse_tests/test_argcomplete.py +++ b/jsonargparse_tests/test_argcomplete.py @@ -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 diff --git a/jsonargparse_tests/test_cli.py b/jsonargparse_tests/test_cli.py index fb60012c..53cd67d5 100644 --- a/jsonargparse_tests/test_cli.py +++ b/jsonargparse_tests/test_cli.py @@ -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 diff --git a/jsonargparse_tests/test_dataclass_like.py b/jsonargparse_tests/test_dataclass_like.py index 6ee90059..8ea09cd3 100644 --- a/jsonargparse_tests/test_dataclass_like.py +++ b/jsonargparse_tests/test_dataclass_like.py @@ -2,7 +2,6 @@ import dataclasses import json -import sys from typing import Any, Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union from unittest.mock import patch @@ -556,29 +555,32 @@ def test_nested_generic_dataclass(parser): assert "--x.y.g4 g4 (required, type: dict[str, union[float, bool]])" in help_str -if sys.version_info >= (3, 9): - V = TypeVar("V") +V = TypeVar("V") - @dataclasses.dataclass(frozen=True) - class GenericChild(Generic[V]): - value: V - @dataclasses.dataclass(frozen=True) - class GenericBase(Generic[V]): - children: tuple[GenericChild[V], ...] +@dataclasses.dataclass(frozen=True) +class GenericChild(Generic[V]): + value: V - @dataclasses.dataclass(frozen=True) - class GenericSubclass(GenericBase[str]): - children: tuple[GenericChild[str], ...] - def test_generic_dataclass_subclass(parser): - parser.add_class_arguments(GenericSubclass, "x") - cfg = parser.parse_args(['--x.children=[{"value": "a"}, {"value": "b"}]']) - init = parser.instantiate_classes(cfg) - assert cfg.x.children == (Namespace(value="a"), Namespace(value="b")) - assert isinstance(init.x, GenericSubclass) - assert isinstance(init.x.children[0], GenericChild) - assert isinstance(init.x.children[1], GenericChild) +@dataclasses.dataclass(frozen=True) +class GenericBase(Generic[V]): + children: tuple[GenericChild[V], ...] + + +@dataclasses.dataclass(frozen=True) +class GenericSubclass(GenericBase[str]): + children: tuple[GenericChild[str], ...] + + +def test_generic_dataclass_subclass(parser): + parser.add_class_arguments(GenericSubclass, "x") + cfg = parser.parse_args(['--x.children=[{"value": "a"}, {"value": "b"}]']) + init = parser.instantiate_classes(cfg) + assert cfg.x.children == (Namespace(value="a"), Namespace(value="b")) + assert isinstance(init.x, GenericSubclass) + assert isinstance(init.x.children[0], GenericChild) + assert isinstance(init.x.children[1], GenericChild) # union mixture tests diff --git a/jsonargparse_tests/test_postponed_annotations.py b/jsonargparse_tests/test_postponed_annotations.py index 4eab551f..5741701a 100644 --- a/jsonargparse_tests/test_postponed_annotations.py +++ b/jsonargparse_tests/test_postponed_annotations.py @@ -2,9 +2,7 @@ import dataclasses import os -import sys -from random import Random -from typing import TYPE_CHECKING, Dict, FrozenSet, List, Optional, Set, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union import pytest @@ -19,47 +17,6 @@ from jsonargparse_tests.conftest import capture_logs, source_unavailable -def function_pep585_dict(p1: dict[str, int], p2: dict[int, str] = {1: "a"}): - return p1 - - -def function_pep585_list(p1: list[str], p2: list[float] = [0.1, 2.3]): - return p1 - - -def function_pep585_set(p1: set[str], p2: set[int] = {1, 2}): - return p1 - - -def function_pep585_frozenset(p1: frozenset[str], p2: frozenset[int] = frozenset(range(3))): - return p1 - - -def function_pep585_tuple(p1: tuple[str, float], p2: tuple[int, ...] = (1, 2)): - return p1 - - -def function_pep585_type(p1: type[Random], p2: type[Random] = Random): - return p1 - - -@pytest.mark.skipif(sys.version_info >= (3, 9, 0), reason="python<3.9 is required") -@pytest.mark.parametrize( - ["function", "expected"], - [ - (function_pep585_dict, {"p1": Dict[str, int], "p2": Dict[int, str]}), - (function_pep585_list, {"p1": List[str], "p2": List[float]}), - (function_pep585_set, {"p1": Set[str], "p2": Set[int]}), - (function_pep585_frozenset, {"p1": FrozenSet[str], "p2": FrozenSet[int]}), - (function_pep585_tuple, {"p1": Tuple[str, float], "p2": Tuple[int, ...]}), - (function_pep585_type, {"p1": Type[Random], "p2": Type[Random]}), - ], -) -def test_get_types_pep585(function, expected): - types = get_types(function) - assert types == expected - - def function_pep604(p1: str | None, p2: int | float | bool = 1): return p1 @@ -69,14 +26,6 @@ def test_get_types_pep604(): assert types == {"p1": Union[str, None], "p2": Union[int, float, bool]} -@pytest.mark.skipif(sys.version_info >= (3, 9, 0), reason="python<3.9 is required") -def test_get_types_pep604_source_unavailable(logger): - with source_unavailable(), pytest.raises(TypeError) as ctx, capture_logs(logger) as logs: - get_types(function_pep604, logger) - ctx.match("mock source code not available") - assert "Failed to parse to source code" in logs.getvalue() - - class NeedsBackport: def __init__(self, p1: list | set): self.p1 = p1 @@ -311,7 +260,6 @@ class DataclassForwardRef: p2: Optional["xml.dom.Node"] = None -@pytest.mark.skipif(sys.version_info < (3, 9), reason="not working in python 3.8") def test_get_types_type_checking_dataclass_init_forward_ref(): import xml.dom @@ -323,11 +271,11 @@ def function_source_unavailable(p1: List["TypeCheckingClass1"]): return p1 -@pytest.mark.skipif(sys.version_info >= (3, 9, 0), reason="python<3.9 is required") -def test_get_types_source_unavailable(): - with source_unavailable(): - types = get_types(function_source_unavailable) - assert types == {"p1": List["TypeCheckingClass1"]} +def test_get_types_source_unavailable(logger): + with source_unavailable(function_source_unavailable), pytest.raises(NameError) as ctx, capture_logs(logger) as logs: + get_types(function_source_unavailable, logger) + ctx.match("'TypeCheckingClass1' is not defined") + assert "source code not available" in logs.getvalue() @dataclasses.dataclass @@ -338,8 +286,7 @@ class Data585: def test_get_types_dataclass_pep585(parser): types = get_types(Data585) - list_int = List[int] if sys.version_info < (3, 9) else list[int] - assert types == {"a": list_int, "b": str} + assert types == {"a": list[int], "b": str} parser.add_class_arguments(Data585, "data") cfg = parser.parse_args(["--data.a=[1, 2]"]) assert cfg.data == Namespace(a=[1, 2], b="x") diff --git a/jsonargparse_tests/test_stubs_resolver.py b/jsonargparse_tests/test_stubs_resolver.py index 3a2bb4f0..8d6c4402 100644 --- a/jsonargparse_tests/test_stubs_resolver.py +++ b/jsonargparse_tests/test_stubs_resolver.py @@ -10,7 +10,6 @@ from ipaddress import ip_network from random import Random, SystemRandom, uniform from tarfile import TarFile -from typing import Any from unittest.mock import patch from uuid import UUID, uuid5 @@ -134,10 +133,8 @@ def test_get_params_conditional_python_version(): assert ["a", "version"] == get_param_names(params) if sys.version_info >= (3, 10): assert "int | float | str | bytes | bytearray | None" == str(params[0].annotation) - elif sys.version_info[:2] == (3, 9): - assert "typing.Union[int, float, str, bytes, bytearray, NoneType]" == str(params[0].annotation) else: - assert Any is params[0].annotation + assert "typing.Union[int, float, str, bytes, bytearray, NoneType]" == str(params[0].annotation) assert int is params[1].annotation with mock_stubs_missing_types(): params = get_params(Random, "seed") diff --git a/jsonargparse_tests/test_subclasses.py b/jsonargparse_tests/test_subclasses.py index 86152c79..2f0506c9 100644 --- a/jsonargparse_tests/test_subclasses.py +++ b/jsonargparse_tests/test_subclasses.py @@ -2,7 +2,6 @@ import json import os -import sys import textwrap import warnings from calendar import Calendar, HTMLCalendar, TextCalendar @@ -407,8 +406,7 @@ def test_instantiator_undefined_return(parser, logger): parser.parse_args([f"--cls={__name__}.function_undefined_return", "--cls.p1=2"]) ctx.match("function_undefined_return does not correspond to a subclass of") assert "function_undefined_return does not correspond to a subclass of" in logs.getvalue() - if sys.version_info >= (3, 9): - assert "Unable to evaluate types for" in logs.getvalue() + assert "Unable to evaluate types for" in logs.getvalue() # importable instances diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py index d7fb2c2e..49b1c813 100644 --- a/jsonargparse_tests/test_typehints.py +++ b/jsonargparse_tests/test_typehints.py @@ -207,16 +207,14 @@ def test_type_any_dump(parser): def test_type_typehint_without_arg(parser): - type_class = Type if sys.version_info < (3, 9) else type - parser.add_argument("--type", type=type_class) + parser.add_argument("--type", type=type) cfg = parser.parse_args(["--type=uuid.UUID"]) assert cfg.type is uuid.UUID assert json_or_yaml_load(parser.dump(cfg)) == {"type": "uuid.UUID"} def test_type_typehint_with_arg(parser): - type_class = Type if sys.version_info < (3, 9) else type - parser.add_argument("--cal", type=type_class[Calendar]) + parser.add_argument("--cal", type=type[Calendar]) cfg = parser.parse_args(["--cal=calendar.Calendar"]) assert cfg.cal is Calendar assert json_or_yaml_load(parser.dump(cfg)) == {"cal": "calendar.Calendar"} @@ -753,19 +751,18 @@ def test_invalid_inherited_unpack_typeddict(parser, init_args): parser.parse_args([f"--testclass={json.dumps(test_config)}"]) -if sys.version_info >= (3, 9): +class BottomDict(TypedDict, total=True): + a: int - class BottomDict(TypedDict, total=True): - a: int - class MiddleDict(BottomDict, total=False): - b: int +class MiddleDict(BottomDict, total=False): + b: int - class TopDict(MiddleDict, total=True): - c: int + +class TopDict(MiddleDict, total=True): + c: int -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Python 3.8 lacked runtime inspection of TypedDict required keys") def test_typeddict_totality_inheritance(parser): parser.add_argument("--middledict", type=MiddleDict, required=False) parser.add_argument("--topdict", type=TopDict, required=False) @@ -804,7 +801,6 @@ def test_mapping_default_mapping_proxy_type(parser): assert parser.dump(cfg, format="json") == '{"mapping":{"x":1}}' -@pytest.mark.skipif(sys.version_info < (3, 9), reason="OrderedDict subscriptable since python 3.9") def test_ordered_dict(parser): parser.add_argument("--odict", type=eval("OrderedDict[str, int]")) cfg = parser.parse_args(['--odict={"a":1, "b":2}']) diff --git a/pyproject.toml b/pyproject.toml index 71449d49..faa9c80f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,22 +11,21 @@ authors = [ {name = "Mauricio Villegas", email = "mauricio@omnius.com"}, ] readme = "README.rst" -license = {file = "LICENSE.rst"} -requires-python = ">=3.8" +license = "MIT" +license-files = ["LICENSE.rst"] +requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", @@ -52,8 +51,7 @@ all = [ signatures = [ "jsonargparse[typing-extensions]", "docstring-parser>=0.17", - "typeshed-client>=2.3.0; python_version == '3.8'", - "typeshed-client>=2.8.2; python_version >= '3.9'", + "typeshed-client>=2.8.2", ] jsonschema = [ "jsonschema>=3.2.0", @@ -191,7 +189,7 @@ Villegas = "Villegas" [tool.tox] legacy_tox_ini = """ [tox] -envlist = py{38,39,310,311,312,313}-{all,no}-extras,omegaconf,pydantic-v1,without-pyyaml,without-future-annotations +envlist = py{39,310,311,312,313}-{all,no}-extras,omegaconf,pydantic-v1,without-pyyaml,without-future-annotations skip_missing_interpreters = true [testenv]