diff --git a/.github/workflows/manual.yaml b/.github/workflows/manual.yaml index e1086fd4..c4951d7b 100644 --- a/.github/workflows/manual.yaml +++ b/.github/workflows/manual.yaml @@ -20,7 +20,6 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: | - 3.9 3.10 3.11 3.12 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9090d1e7..4fc85e4e 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", "3.14"] + python: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -267,7 +267,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,3.14 + -Dsonar.python.version=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 6a41e592..63b1024e 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.50.0 (unreleased) +-------------------- + +Changed +^^^^^^^ +- Drop support for Python 3.9. The minimum supported Python version is now + 3.10 (`#916 `__). + + v4.49.0 (2026-05-15) -------------------- diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index 1a2f10eb..bd12c1dc 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -443,9 +443,11 @@ Some notes about this support are: limit in nesting depth. - Postponed evaluation of types PEP `563 `__ - (i.e. ``from __future__ import annotations``) is supported. Also supported on - ``python==3.9`` is PEP `604 `__ (i.e. - `` | `` instead of ``Union[, ]``). + (i.e. ``from __future__ import annotations``) is supported. Also supported are + PEP `585 `__ (i.e. ``list[], + dict[], ...`` instead of ``List[], Dict[], ...``) and `604 + `__ (i.e. `` | `` instead of + ``Union[, ]``). - Types that use components imported inside ``TYPE_CHECKING`` blocks are supported. @@ -1875,9 +1877,7 @@ jsonargparse with the ``signatures`` extra 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, 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``. +`__. This syntax is fully supported. Most of the types in the Python standard library have their types in stubs. An example from the standard library would be: diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py index 5b03bb6e..3409417b 100644 --- a/jsonargparse/_actions.py +++ b/jsonargparse/_actions.py @@ -6,7 +6,7 @@ from argparse import Action as ArgparseAction from contextlib import contextmanager from contextvars import ContextVar -from typing import Any, Optional +from typing import Any from ._common import Action, NonParsingAction, is_subclass, is_subclasses_disabled, parser_context from ._loaders_dumpers import get_loader_exceptions, load_value @@ -116,7 +116,7 @@ def apply_config(parser, cfg, dest, value) -> None: with parser_context(single_subcommand=False), previous_config_context(cfg), skip_apply_links(): kwargs = {"env": False, "defaults": False, "_skip_validation": True, "_fail_no_subcommand": False} try: - cfg_path: Optional[Path] = Path(value, mode=_get_config_read_mode()) + cfg_path: Path | None = Path(value, mode=_get_config_read_mode()) except TypeError as ex_path: try: if isinstance(load_value(value), str): @@ -224,7 +224,7 @@ def is_print_config_requested(parser): class _ActionConfigLoad(Action): - def __init__(self, basetype: Optional[type] = None, **kwargs): + def __init__(self, basetype: type | None = None, **kwargs): if len(kwargs) == 0: self._basetype = basetype else: @@ -267,7 +267,7 @@ class _ActionHelpClassPath(NonParsingAction): sub_add_kwargs: dict[str, Any] = {} @classmethod - def get_help_types(cls, typehint) -> Optional[tuple]: + def get_help_types(cls, typehint) -> tuple | None: from ._typehints import get_subclass_or_closed_types return get_subclass_or_closed_types(typehint=typehint, also_lists=True, callable_return=True) diff --git a/jsonargparse/_cli.py b/jsonargparse/_cli.py index 85381c39..25757f72 100644 --- a/jsonargparse/_cli.py +++ b/jsonargparse/_cli.py @@ -1,7 +1,8 @@ """Simple creation of command line interfaces.""" import inspect -from typing import Any, Callable, Optional, Union +from collections.abc import Callable +from typing import Any, Optional, Union from ._actions import ActionConfigFile, _ActionPrintConfig, remove_actions from ._core import ArgumentParser @@ -25,9 +26,9 @@ def CLI(*args, **kwargs): def auto_cli( components: ComponentsType = None, - args: Optional[list[str]] = None, + args: list[str] | None = None, config_help: str = default_config_option_help, - set_defaults: Optional[dict[str, Any]] = None, + set_defaults: dict[str, Any] | None = None, as_positional: bool = True, return_instance: bool = False, fail_untyped: bool = True, diff --git a/jsonargparse/_common.py b/jsonargparse/_common.py index e99dbd38..8029d494 100644 --- a/jsonargparse/_common.py +++ b/jsonargparse/_common.py @@ -3,15 +3,14 @@ import inspect import logging import os +from collections.abc import Callable from contextlib import contextmanager from contextvars import ContextVar from typing import ( # type: ignore[attr-defined] - Callable, Generic, Optional, Protocol, TypeVar, - Union, _GenericAlias, ) @@ -52,17 +51,17 @@ def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType: InstantiatorsDictType = dict[tuple[type, bool], InstantiatorCallable] -parent_parser: ContextVar[Optional[ArgumentParser]] = ContextVar("parent_parser", default=None) +parent_parser: ContextVar[ArgumentParser | None] = ContextVar("parent_parser", default=None) parser_capture: ContextVar[bool] = ContextVar("parser_capture", default=False) -defaults_cache: ContextVar[Optional[Namespace]] = ContextVar("defaults_cache", default=None) -lenient_check: ContextVar[Union[bool, str]] = ContextVar("lenient_check", default=False) +defaults_cache: ContextVar[Namespace | None] = ContextVar("defaults_cache", default=None) +lenient_check: ContextVar[bool | str] = ContextVar("lenient_check", default=False) parsing_defaults: ContextVar[bool] = ContextVar("parsing_defaults", default=False) single_subcommand: ContextVar[bool] = ContextVar("single_subcommand", default=True) validating_defaults: ContextVar[bool] = ContextVar("validating_defaults", default=False) -load_value_mode: ContextVar[Optional[str]] = ContextVar("load_value_mode", default=None) -class_instantiators: ContextVar[Optional[InstantiatorsDictType]] = ContextVar("class_instantiators", default=None) +load_value_mode: ContextVar[str | None] = ContextVar("load_value_mode", default=None) +class_instantiators: ContextVar[InstantiatorsDictType | None] = ContextVar("class_instantiators", default=None) nested_links: ContextVar[list[dict]] = ContextVar("nested_links", default=[]) -applied_instantiation_links: ContextVar[Optional[set]] = ContextVar("applied_instantiation_links", default=None) +applied_instantiation_links: ContextVar[set | None] = ContextVar("applied_instantiation_links", default=None) path_dump_preserve_relative: ContextVar[bool] = ContextVar("path_dump_preserve_relative", default=False) @@ -115,17 +114,17 @@ def get_env_var_bool(name: str) -> bool: def set_parsing_settings( *, - validate_defaults: Optional[bool] = None, - config_read_mode_urls_enabled: Optional[bool] = None, - config_read_mode_fsspec_enabled: Optional[bool] = None, + validate_defaults: bool | None = None, + config_read_mode_urls_enabled: bool | None = None, + config_read_mode_fsspec_enabled: bool | None = None, docstring_parse_style: Optional["docstring_parser.DocstringStyle"] = None, - docstring_parse_attribute_docstrings: Optional[bool] = None, - parse_optionals_as_positionals: Optional[bool] = None, - add_print_completion_argument: Optional[bool] = None, - stubs_resolver_allow_py_files: Optional[bool] = None, - omegaconf_absolute_to_relative_paths: Optional[bool] = None, - subclasses_disabled: Optional[list[Union[type, Callable[[type], bool]]]] = None, - subclasses_enabled: Optional[list[Union[type, str]]] = None, + docstring_parse_attribute_docstrings: bool | None = None, + parse_optionals_as_positionals: bool | None = None, + add_print_completion_argument: bool | None = None, + stubs_resolver_allow_py_files: bool | None = None, + omegaconf_absolute_to_relative_paths: bool | None = None, + subclasses_disabled: list[type | Callable[[type], bool]] | None = None, + subclasses_enabled: list[type | str] | None = None, ) -> None: """ Modify global parser settings that affect parser creation and parsing behavior. @@ -329,7 +328,7 @@ def is_pure_dataclass(cls) -> bool: subclasses_enabled_types: set[type] = set() subclasses_disabled_types: set[type] = set() -subclasses_disabled_selectors: dict[str, Callable[[type], Union[bool, int]]] = { +subclasses_disabled_selectors: dict[str, Callable[[type], bool | int]] = { "is_pure_dataclass": is_pure_dataclass, "is_pydantic_model": is_pydantic_model, "is_attrs_class": is_attrs_class, @@ -351,8 +350,8 @@ def is_subclasses_disabled(cls) -> bool: def subclass_type_behavior( - subclasses_disabled: Optional[list[Union[type, Callable[[type], bool]]]] = None, - subclasses_enabled: Optional[list[Union[type, str]]] = None, + subclasses_disabled: list[type | Callable[[type], bool]] | None = None, + subclasses_enabled: list[type | str] | None = None, ) -> None: """Configures whether class types accept or not subclasses.""" for enable_item in subclasses_enabled or []: @@ -404,7 +403,7 @@ def setup_default_logger(data, level, caller): return logger -def parse_logger(logger: Union[bool, str, dict, logging.Logger], caller): +def parse_logger(logger: bool | str | dict | logging.Logger, caller): if not isinstance(logger, (bool, str, dict, logging.Logger)): raise ValueError(f"Expected logger to be an instance of (bool, str, dict, logging.Logger), but got {logger}.") if isinstance(logger, dict) and len(set(logger) - {"name", "level"}) > 0: @@ -425,7 +424,7 @@ def parse_logger(logger: Union[bool, str, dict, logging.Logger], caller): class LoggerProperty: """Class designed to be inherited by other classes to add a logger property.""" - def __init__(self, *args, logger: Union[bool, str, dict, logging.Logger] = False, **kwargs): + def __init__(self, *args, logger: bool | str | dict | logging.Logger = False, **kwargs): self.logger = logger super().__init__(*args, **kwargs) @@ -444,7 +443,7 @@ def logger(self) -> logging.Logger: return self._logger @logger.setter - def logger(self, logger: Union[bool, str, dict, logging.Logger]): + def logger(self, logger: bool | str | dict | logging.Logger): if logger is None: from ._deprecated import deprecation_warning, logger_property_none_message diff --git a/jsonargparse/_completions.py b/jsonargparse/_completions.py index b1793ca5..fef1ee17 100644 --- a/jsonargparse/_completions.py +++ b/jsonargparse/_completions.py @@ -10,7 +10,7 @@ from enum import Enum from importlib.util import find_spec from subprocess import PIPE, Popen -from typing import Literal, Optional, Union +from typing import Literal, Union from ._actions import ActionConfigFile, ActionFail, _ActionConfigLoad, _ActionHelpClassPath, remove_actions from ._common import NonParsingAction, get_optionals_as_positionals_actions, get_parsing_setting @@ -141,7 +141,7 @@ def get_completion_script(parser, completion_type: str, **kwargs) -> str: return get_shtab_script(parser, completion_type[len("shtab-") :], **kwargs) -def get_shtab_script(parser, shell: str, preambles: Optional[list[str]] = None) -> str: +def get_shtab_script(parser, shell: str, preambles: list[str] | None = None) -> str: import shtab if shell not in shtab.SUPPORTED_SHELLS: diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 75c51e47..3add77ac 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -6,13 +6,11 @@ import logging import os import sys -from collections.abc import Sequence +from collections.abc import Callable, Sequence from contextlib import suppress from typing import ( Any, - Callable, NoReturn, - Optional, Union, ) @@ -168,7 +166,7 @@ def add_argument(self, *args, sub_configs: bool = False, **kwargs): validate_default(self, action) return action - def add_argument_group(self, *args, name: Optional[str] = None, **kwargs) -> "ArgumentGroup": + def add_argument_group(self, *args, name: str | None = None, **kwargs) -> "ArgumentGroup": """Adds a group to the parser. All the arguments from `argparse.ArgumentParser.add_argument_group @@ -227,29 +225,29 @@ def set_defaults(self, *args: dict[str, Any], **kwargs: Any) -> None: class ArgumentGroup(ActionsContainer, argparse._ArgumentGroup): """Extension of argparse._ArgumentGroup to support additional functionalities.""" - dest: Optional[str] = None - parser: Optional[Union["ArgumentParser", ActionsContainer]] = None + dest: str | None = None + parser: Union["ArgumentParser", ActionsContainer] | None = None class ArgumentParser(ParserDeprecations, ActionsContainer, argparse.ArgumentParser): """Parser for command line, configuration files and environment variables.""" formatter_class: type[argparse.HelpFormatter] - groups: Optional[dict[str, ArgumentGroup]] = None + groups: dict[str, ArgumentGroup] | None = None _group_class: type[ArgumentGroup] - _subcommands_action: Optional[ActionSubCommands] = None + _subcommands_action: ActionSubCommands | None = None def __init__( self, *args, - env_prefix: Union[bool, str] = True, + env_prefix: bool | str = True, formatter_class: type[argparse.HelpFormatter] = DefaultHelpFormatter, - logger: Union[logging.Logger, bool, str, dict] = False, - version: Optional[str] = None, - print_config: Optional[str] = "--print_config", + logger: logging.Logger | bool | str | dict = False, + version: str | None = None, + print_config: str | None = "--print_config", parser_mode: str = "yaml" if pyyaml_available else "json", - dump_header: Optional[list[str]] = None, - default_config_files: Optional[list[Union[str, os.PathLike]]] = None, + dump_header: list[str] | None = None, + default_config_files: list[str | os.PathLike] | None = None, default_env: bool = False, **kwargs, ) -> None: @@ -335,7 +333,7 @@ def _parse_optional(self, arg_string): def _parse_common( self, cfg: Namespace, - env: Optional[bool], + env: bool | None, defaults: bool, skip_validation: bool, skip_required: bool = False, @@ -385,8 +383,8 @@ def _parse_common( def _parse_defaults_and_environ( self, defaults: bool = True, - env: Optional[bool] = None, - environ: Optional[Union[dict[str, str], os._Environ]] = None, + env: bool | None = None, + environ: dict[str, str] | os._Environ | None = None, ): cfg = Namespace() if defaults: @@ -404,9 +402,9 @@ def _parse_defaults_and_environ( def parse_args( # type: ignore[override] self, - args: Optional[Sequence[str]] = None, - namespace: Optional[Namespace] = None, - env: Optional[bool] = None, + args: Sequence[str] | None = None, + namespace: Namespace | None = None, + env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: @@ -475,9 +473,9 @@ def parse_args( # type: ignore[override] def parse_object( self, - cfg_obj: Union[Namespace, dict[str, Any]], - cfg_base: Optional[Namespace] = None, - env: Optional[bool] = None, + cfg_obj: Namespace | dict[str, Any], + cfg_base: Namespace | None = None, + env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: @@ -519,7 +517,7 @@ def parse_object( self._logger.debug("Parsed object: %s", cfg_obj) return parsed_cfg - def _load_env_vars(self, env: Union[dict[str, str], os._Environ], defaults: bool) -> Namespace: + def _load_env_vars(self, env: dict[str, str] | os._Environ, defaults: bool) -> Namespace: cfg = Namespace() actions = filter_non_parsing_actions(self._actions) for action in actions: @@ -561,7 +559,7 @@ def _load_env_vars(self, env: Union[dict[str, str], os._Environ], defaults: bool def parse_env( self, - env: Optional[dict[str, str]] = None, + env: dict[str, str] | None = None, defaults: bool = True, **kwargs, ) -> Namespace: @@ -601,9 +599,9 @@ def parse_env( def parse_path( self, - cfg_path: Union[str, os.PathLike], - ext_vars: Optional[dict] = None, - env: Optional[bool] = None, + cfg_path: str | os.PathLike, + ext_vars: dict | None = None, + env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: @@ -639,9 +637,9 @@ def parse_path( def parse_string( self, cfg_str: str, - cfg_path: Union[str, os.PathLike] = "", - ext_vars: Optional[dict] = None, - env: Optional[bool] = None, + cfg_path: str | os.PathLike = "", + ext_vars: dict | None = None, + env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: @@ -689,9 +687,9 @@ def parse_string( def _load_config_parser_mode( self, cfg_str: str, - cfg_path: Union[str, os.PathLike] = "", - ext_vars: Optional[dict] = None, - prev_cfg: Optional[Namespace] = None, + cfg_path: str | os.PathLike = "", + ext_vars: dict | None = None, + prev_cfg: Namespace | None = None, ) -> Namespace: """Loads a configuration string into a namespace. @@ -859,13 +857,13 @@ def _dump_delete_default_entries(self, subcfg, subdefaults): def save( self, cfg: Namespace, - path: Union[str, os.PathLike], + path: str | os.PathLike, format: str = "parser_mode", skip_none: bool = True, skip_validation: bool = False, overwrite: bool = False, multifile: bool = True, - branch: Optional[str] = None, + branch: str | None = None, **kwargs, ) -> None: """Writes to file(s) the given configuration object using the chosen format. @@ -1097,7 +1095,7 @@ def get_completion_script(self, completion_type: str, **kwargs) -> str: ## Other methods ## - def error(self, message: str, ex: Optional[Exception] = None) -> NoReturn: + def error(self, message: str, ex: Exception | None = None) -> NoReturn: """Logs error message if a logger is set and exits or raises an :class:`ArgumentError`.""" self._logger.error(message) if callable(self._error_handler): @@ -1126,7 +1124,7 @@ def validate( cfg: Namespace, skip_none: bool = True, skip_required: bool = False, - branch: Optional[str] = None, + branch: str | None = None, **kwargs, ) -> None: """Checks that the content of a given configuration object conforms with the parser. @@ -1274,10 +1272,10 @@ def print_usage(self, *args, **kwargs) -> None: def _apply_actions( self, - cfg: Union[Namespace, dict[str, Any]], + cfg: Namespace | dict[str, Any], parent_key: str = "", - prev_cfg: Optional[Namespace] = None, - skip_fn: Optional[Callable[[Any], bool]] = None, + prev_cfg: Namespace | None = None, + skip_fn: Callable[[Any], bool] | None = None, ) -> Namespace: """Runs _check_value_key on actions present in config.""" if isinstance(cfg, dict): @@ -1363,7 +1361,7 @@ def merge_config(self, cfg_from: Namespace, cfg_to: Namespace) -> Namespace: return cfg_to def _check_value_key( - self, action: argparse.Action, value: Any, key: str, cfg: Optional[Namespace], append: bool = False + self, action: argparse.Action, value: Any, key: str, cfg: Namespace | None, append: bool = False ) -> Any: """Checks the value for a given action. @@ -1422,7 +1420,7 @@ def default_config_files(self) -> list[str]: return self._default_config_files @default_config_files.setter - def default_config_files(self, default_config_files: Optional[Sequence[Union[str, os.PathLike]]]): + def default_config_files(self, default_config_files: Sequence[str | os.PathLike] | None): if default_config_files is None: self._default_config_files = [] elif isinstance(default_config_files, list) and all( @@ -1472,7 +1470,7 @@ def default_env(self, default_env: bool): subparser.default_env = self._default_env @property - def env_prefix(self) -> Union[bool, str]: + def env_prefix(self) -> bool | str: """The environment variables prefix property. :getter: Returns the current environment variables prefix. @@ -1484,7 +1482,7 @@ def env_prefix(self) -> Union[bool, str]: return self._env_prefix @env_prefix.setter - def env_prefix(self, env_prefix: Union[bool, str]): + def env_prefix(self, env_prefix: bool | str): if env_prefix is None: from ._deprecated import ( deprecation_warning, @@ -1524,7 +1522,7 @@ def parser_mode(self, parser_mode: str): subparser.parser_mode = parser_mode @property - def dump_header(self) -> Optional[list[str]]: + def dump_header(self) -> list[str] | None: """Header to include as comment when dumping a config object. :getter: Returns the current dump header. @@ -1536,7 +1534,7 @@ def dump_header(self) -> Optional[list[str]]: return self._dump_header @dump_header.setter - def dump_header(self, dump_header: Optional[list[str]]): + def dump_header(self, dump_header: list[str] | None): if not ( dump_header is None or (isinstance(dump_header, list) and all(isinstance(x, str) for x in dump_header)) ): diff --git a/jsonargparse/_formatters.py b/jsonargparse/_formatters.py index 1da43cee..7790ea51 100644 --- a/jsonargparse/_formatters.py +++ b/jsonargparse/_formatters.py @@ -13,7 +13,6 @@ from collections.abc import Iterable from io import StringIO from string import Template -from typing import Optional, Union from ._actions import ( ActionConfigFile, @@ -65,7 +64,7 @@ def add_yaml_comments(self, cfg: str) -> str: yaml = ruyaml.YAML() cfg = yaml.load(cfg) - def get_parsers(parser: ArgumentParser, prefix="") -> dict[Optional[str], ArgumentParser]: + def get_parsers(parser: ArgumentParser, prefix="") -> dict[str | None, ArgumentParser]: parsers = {} if parser._subparsers is not None: for key, subparser in parser._subparsers._group_actions[0].choices.items(): # type: ignore[union-attr] @@ -287,7 +286,7 @@ def _expand_help(self, action: Action) -> str: action.default = orig_default return help_str - def _get_type_str(self, action: Action) -> Optional[str]: + def _get_type_str(self, action: Action) -> str | None: type_str = None if isinstance(action, ActionYesNo): type_str = "bool" @@ -297,14 +296,14 @@ def _get_type_str(self, action: Action) -> Optional[str]: type_str = type_to_str(action._typehint) return type_str - def add_usage(self, usage: Optional[str], actions: Iterable[Action], *args, **kwargs) -> None: + def add_usage(self, usage: str | None, actions: Iterable[Action], *args, **kwargs) -> None: actions = [a for a in actions if not isinstance(a, ActionLink)] super().add_usage(usage, actions, *args, **kwargs) def get_env_var( - parser_or_formatter: Union[ArgumentParser, DefaultHelpFormatter], - action: Optional[Action] = None, + parser_or_formatter: ArgumentParser | DefaultHelpFormatter, + action: Action | None = None, ) -> str: """Returns the environment variable name for a given parser or formatter and action.""" if isinstance(parser_or_formatter, DefaultHelpFormatter): diff --git a/jsonargparse/_from_config.py b/jsonargparse/_from_config.py index bd30a69e..6aafc45b 100644 --- a/jsonargparse/_from_config.py +++ b/jsonargparse/_from_config.py @@ -2,7 +2,7 @@ from functools import wraps from os import PathLike from pathlib import Path -from typing import Optional, Type, TypeVar, Union +from typing import TypeVar from ._common import parser_context from ._core import ArgumentParser @@ -39,7 +39,7 @@ class FromConfigMixin: ArgumentParser used for parsing configs. """ - __from_config_init_defaults__: Optional[Union[str, PathLike]] = None + __from_config_init_defaults__: str | PathLike | None = None __from_config_parser_kwargs__: dict = {} def __init_subclass__(cls, **kwargs) -> None: @@ -48,7 +48,7 @@ def __init_subclass__(cls, **kwargs) -> None: _override_init_defaults(cls, cls.__from_config_parser_kwargs__) @classmethod - def from_config(cls: Type[T], config: Union[str, PathLike, dict]) -> T: + def from_config(cls: type[T], config: str | PathLike | dict) -> T: """Instantiate current class based on a config file or dict. Args: @@ -58,7 +58,7 @@ def from_config(cls: Type[T], config: Union[str, PathLike, dict]) -> T: return cls(**kwargs) -def _parse_class_kwargs_from_config(cls: Type[T], config: Union[str, PathLike, dict], **kwargs) -> tuple[dict, Type[T]]: +def _parse_class_kwargs_from_config(cls: type[T], config: str | PathLike | dict, **kwargs) -> tuple[dict, type[T]]: """Parse the init kwargs for ``cls`` from a config file or dict.""" parser = ArgumentParser(exit_on_error=False, **kwargs) if not isinstance(config, dict): @@ -90,7 +90,7 @@ def _parse_class_kwargs_from_config(cls: Type[T], config: Union[str, PathLike, d return parser.instantiate(cfg).as_dict(), cls -def _override_init_defaults(cls: Type[T], parser_kwargs: dict) -> None: +def _override_init_defaults(cls: type[T], parser_kwargs: dict) -> None: """Override ``__init__`` defaults for ``cls`` based on ``__from_config_init_defaults__``.""" config = getattr(cls, "__from_config_init_defaults__", None) if not isinstance(config, (str, PathLike, type(None))): @@ -103,7 +103,7 @@ def _override_init_defaults(cls: Type[T], parser_kwargs: dict) -> None: _override_init_defaults_parent_classes(cls, defaults) -def _override_init_defaults_this_class(cls: Type[T], defaults: dict) -> None: +def _override_init_defaults_this_class(cls: type[T], defaults: dict) -> None: params = inspect.signature(cls.__init__).parameters for name, default in defaults.copy().items(): param = params.get(name) @@ -120,7 +120,7 @@ def _override_init_defaults_this_class(cls: Type[T], defaults: dict) -> None: cls.__init__.__defaults__ = aux[:index] + (default,) + aux[index + 1 :] -def _override_init_defaults_parent_classes(cls: Type[T], defaults: dict) -> None: +def _override_init_defaults_parent_classes(cls: type[T], defaults: dict) -> None: # Gather defaults for parameters in parent classes' __init__ override_parent_params = [] for base in inspect.getmro(cls)[1:]: diff --git a/jsonargparse/_instantiation.py b/jsonargparse/_instantiation.py index a93263b9..dabe2777 100644 --- a/jsonargparse/_instantiation.py +++ b/jsonargparse/_instantiation.py @@ -1,5 +1,4 @@ import inspect -from typing import Union from ._common import ( ClassType, @@ -64,7 +63,7 @@ def instantiate( from ._subcommands import get_subcommand from ._typehints import ActionTypeHint - components: list[Union[ActionTypeHint, _ActionConfigLoad, ArgumentGroup]] = [] + components: list[ActionTypeHint | _ActionConfigLoad | ArgumentGroup] = [] for action in filter_non_parsing_actions(self._actions): # type: ignore[attr-defined] if isinstance(action, ActionTypeHint): components.append(action) diff --git a/jsonargparse/_jsonnet.py b/jsonargparse/_jsonnet.py index 0a1966e2..ccfdc37e 100644 --- a/jsonargparse/_jsonnet.py +++ b/jsonargparse/_jsonnet.py @@ -1,6 +1,6 @@ """Actions to support jsonnet.""" -from typing import Any, Dict, Optional, Union +from typing import Any, Dict from ._actions import _is_action_value_list from ._common import Action, parser_context @@ -25,8 +25,8 @@ class ActionJsonnet(Action): def __init__( self, - ext_vars: Optional[str] = None, - schema: Optional[Union[str, dict]] = None, + ext_vars: str | None = None, + schema: str | dict | None = None, **kwargs, ): """Initializer for ActionJsonnet instance. @@ -119,7 +119,7 @@ def _check_ext_vars_action(parser, action): ext_vars_action.jsonnet_ext_vars = True @staticmethod - def split_ext_vars(ext_vars: Optional[dict[str, Any]]) -> tuple[dict[str, Any], dict[str, Any]]: + def split_ext_vars(ext_vars: dict[str, Any] | None) -> tuple[dict[str, Any], dict[str, Any]]: """Splits an ``ext_vars`` dict into the ``ext_codes`` and ``ext_vars`` required by Jsonnet. Args: @@ -135,8 +135,8 @@ def split_ext_vars(ext_vars: Optional[dict[str, Any]]) -> tuple[dict[str, Any], def parse( self, - jsonnet: Union[str, Path], - ext_vars: Optional[dict[str, Any]] = None, + jsonnet: str | Path, + ext_vars: dict[str, Any] | None = None, with_meta: bool = False, ) -> dict: """Method that can be used to parse Jsonnet independent from an :class:`.ArgumentParser`. diff --git a/jsonargparse/_jsonschema.py b/jsonargparse/_jsonschema.py index f9765e01..a0180362 100644 --- a/jsonargparse/_jsonschema.py +++ b/jsonargparse/_jsonschema.py @@ -1,7 +1,6 @@ """Action to support jsonschemas.""" import os -from typing import Optional, Union from ._actions import _is_action_value_list from ._common import Action, parser_context @@ -20,9 +19,7 @@ class ActionJsonSchema(Action): """Action to parse option as JSON validated by a JSON Schema.""" - def __init__( - self, schema: Optional[Union[str, dict]] = None, sub_config: bool = True, with_meta: bool = True, **kwargs - ): + def __init__(self, schema: str | dict | None = None, sub_config: bool = True, with_meta: bool = True, **kwargs): """Initializer for ActionJsonSchema instance. Args: diff --git a/jsonargparse/_link_arguments.py b/jsonargparse/_link_arguments.py index fc1b51b3..b33c2499 100644 --- a/jsonargparse/_link_arguments.py +++ b/jsonargparse/_link_arguments.py @@ -4,9 +4,10 @@ from argparse import SUPPRESS from argparse import Action as ArgparseAction from collections import defaultdict +from collections.abc import Callable from contextlib import contextmanager from contextvars import ContextVar -from typing import Any, Callable, Optional, Union +from typing import Any from ._actions import ( Action, @@ -32,8 +33,8 @@ def find_parent_or_child_actions( parser: ArgumentParser, key: str, - exclude: Optional[Union[type[ArgparseAction], tuple[type[ArgparseAction], ...]]] = None, -) -> Optional[list[ArgparseAction]]: + exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, +) -> list[ArgparseAction] | None: found: list[ArgparseAction] = [] action = find_parent_action(parser, key, exclude=exclude) if action is not None: @@ -50,8 +51,8 @@ def find_parent_or_child_actions( def find_subclass_action_or_class_group( parser: ArgumentParser, key: str, - exclude: Optional[Union[type[ArgparseAction], tuple[type[ArgparseAction], ...]]] = None, -) -> Optional[Union[ArgparseAction, ArgumentGroup]]: + exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, +) -> ArgparseAction | ArgumentGroup | None: from ._typehints import ActionTypeHint action = find_parent_action(parser, key, exclude=exclude) @@ -117,9 +118,9 @@ class ActionLink(Action): def __init__( self, parser, - source: Union[str, tuple[str, ...]], + source: str | tuple[str, ...], target: str, - compute_fn: Optional[Callable] = None, + compute_fn: Callable | None = None, apply_on: str = "parse", ): if not hasattr(parser, "_links_group"): @@ -218,7 +219,7 @@ def __init__( link_str = getattr(compute_fn, "__name__", str(compute_fn)) + "(" + ", ".join(source) + ")" link_str += " --> " + target - help_str: Optional[str] + help_str: str | None if is_target_subclass and not valid_target_leaf: type_attr = None help_str = f"Use --{self.target[1].dest}.help for details." @@ -496,8 +497,8 @@ def del_target_key(target_key): def find_parent_action_or_group( parser: ArgumentParser, key: str, - exclude: Optional[Union[type[ArgparseAction], tuple[type[ArgparseAction], ...]]] = None, -) -> Optional[Union[ArgparseAction, ArgumentGroup]]: + exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, +) -> ArgparseAction | ArgumentGroup | None: action_or_group = find_parent_action_and_subcommand(parser, key, exclude=exclude)[0] if not action_or_group and parser.groups and key in parser.groups: return parser.groups[key] @@ -528,9 +529,9 @@ class ArgumentLinking: def link_arguments( self, - source: Union[str, tuple[str, ...]], + source: str | tuple[str, ...], target: str, - compute_fn: Optional[Callable] = None, + compute_fn: Callable | None = None, apply_on: str = "parse", ): """Makes an argument value be derived from the values of other arguments. diff --git a/jsonargparse/_loaders_dumpers.py b/jsonargparse/_loaders_dumpers.py index 996615ac..68495786 100644 --- a/jsonargparse/_loaders_dumpers.py +++ b/jsonargparse/_loaders_dumpers.py @@ -3,8 +3,9 @@ import inspect import re from argparse import HelpFormatter +from collections.abc import Callable from contextlib import suppress -from typing import Any, Callable, Optional +from typing import Any from ._common import load_value_mode, parent_parser from ._optionals import ( @@ -153,7 +154,7 @@ def get_load_value_mode() -> str: return mode -def get_loader_exceptions(mode: Optional[str] = None) -> tuple[type[Exception], ...]: +def get_loader_exceptions(mode: str | None = None) -> tuple[type[Exception], ...]: if mode is None: mode = get_load_value_mode() if mode not in loader_exceptions: diff --git a/jsonargparse/_namespace.py b/jsonargparse/_namespace.py index e8874237..cb344d7f 100644 --- a/jsonargparse/_namespace.py +++ b/jsonargparse/_namespace.py @@ -224,9 +224,7 @@ def clone(self, with_meta: bool = True) -> "Namespace": """ return recreate_branches(self, skip_keys=None if with_meta else meta_keys) - def update( - self, value: Union["Namespace", Any], key: Optional[str] = None, only_unset: bool = False - ) -> "Namespace": + def update(self, value: Union["Namespace", Any], key: str | None = None, only_unset: bool = False) -> "Namespace": """Sets or replaces all items from the given nested namespace. Args: diff --git a/jsonargparse/_optionals.py b/jsonargparse/_optionals.py index bb95a49d..c64f6e63 100644 --- a/jsonargparse/_optionals.py +++ b/jsonargparse/_optionals.py @@ -9,7 +9,7 @@ from dataclasses import is_dataclass from importlib.metadata import version from importlib.util import find_spec -from typing import Any, Optional, Union +from typing import Any, Union pyyaml_available = bool(find_spec("yaml")) toml_load_available = bool(find_spec("toml") or find_spec("tomllib")) @@ -189,7 +189,7 @@ def _get_config_read_mode() -> str: return _config_read_mode -def _set_docstring_parse_options(style=None, attribute_docstrings: Optional[bool] = None): +def _set_docstring_parse_options(style=None, attribute_docstrings: bool | None = None): """Sets options for docstring parsing. Args: @@ -336,7 +336,7 @@ def omegaconf_absolute_to_relative_paths(data: dict) -> dict: regex_absolute_path = re.compile(r"\$\{([a-zA-Z][a-zA-Z0-9[\]_.]*)\}") - def _walk(node, current_path: list[Union[str, int]]): + def _walk(node, current_path: list[str | int]): if isinstance(node, dict): return {k: _walk(v, current_path + [k]) for k, v in node.items()} if isinstance(node, list): diff --git a/jsonargparse/_parameter_resolvers.py b/jsonargparse/_parameter_resolvers.py index 8f631512..36a808e1 100644 --- a/jsonargparse/_parameter_resolvers.py +++ b/jsonargparse/_parameter_resolvers.py @@ -2,16 +2,16 @@ import dataclasses import inspect import logging -import sys import textwrap from collections import defaultdict +from collections.abc import Callable from contextlib import contextmanager, suppress from contextvars import ContextVar from copy import deepcopy from functools import partial from importlib import import_module from types import MethodType -from typing import Any, Callable, Optional, Union +from typing import Any, Union from ._common import ( LoggerProperty, @@ -39,11 +39,11 @@ class ParamData: name: str annotation: Any default: Any = inspect._empty - kind: Optional[inspect._ParameterKind] = None - doc: Optional[str] = None - component: Optional[Union[Callable, type, tuple]] = None - parent: Optional[Union[type, tuple]] = None - origin: Optional[Union[str, tuple]] = None + kind: inspect._ParameterKind | None = None + doc: str | None = None + component: Callable | type | tuple | None = None + parent: type | tuple | None = None + origin: str | tuple | None = None ParamList = list[ParamData] @@ -78,7 +78,7 @@ def __init__(self, resolver: str, data: Any) -> None: super().__init__(resolver, iter_to_set_str(data, sep=", ")) -def get_parameter_origins(component, parent) -> Optional[str]: +def get_parameter_origins(component, parent) -> str | None: from ._typehints import get_subclass_types, sequence_origin_types if get_typehint_origin(component) in sequence_origin_types: @@ -262,7 +262,7 @@ def ast_get_call_keyword_names(node): return [kw_node.arg for kw_node in node.keywords if kw_node.arg] -def remove_given_parameters(node, params, removed_params: Optional[set] = None): +def remove_given_parameters(node, params, removed_params: set | None = None): given_args = set(ast_get_call_positional_indexes(node)) given_kwargs = set(ast_get_call_keyword_names(node)) input_params = params @@ -309,8 +309,6 @@ def replace_type_vars(annotation): return type_vars[annotation] if getattr(annotation, "__args__", None): origin = annotation.__origin__ - if sys.version_info < (3, 10) and getattr(origin, "__module__", "") != "typing": - origin = getattr(__import__("typing"), origin.__name__.capitalize(), origin) return origin[tuple(replace_type_vars(a) for a in annotation.__args__)] return annotation @@ -347,7 +345,7 @@ def unpack_typed_dict_kwargs(params: ParamList, kwargs_idx: int) -> int: return kwargs_idx -def add_stub_types(stubs: Optional[dict[str, Any]], params: ParamList, component) -> None: +def add_stub_types(stubs: dict[str, Any] | None, params: ParamList, component) -> None: if not stubs: return for param in params: @@ -481,8 +479,8 @@ def get_mro_parameters(method_name, get_parameters_fn, logger): def get_component_and_parent( - function_or_class: Union[Callable, type], - method_or_property: Optional[Union[str, Callable]] = None, + function_or_class: Callable | type, + method_or_property: str | Callable | None = None, ): if is_subclass(function_or_class, ClassFromFunctionBase) and method_or_property in {None, "__init__"}: function_or_class = function_or_class.wrapped_function # type: ignore[union-attr] @@ -527,8 +525,8 @@ def get_component_and_parent( class ParametersVisitor(LoggerProperty, ast.NodeVisitor): def __init__( self, - function_or_class: Union[Callable, type], - method_or_property: Optional[Union[str, Callable]] = None, + function_or_class: Callable | type, + method_or_property: str | Callable | None = None, **kwargs, ): super().__init__(**kwargs) @@ -598,7 +596,7 @@ def visit_If(self, node): node = ast.If(test=ast.Constant(value=True), body=body, orelse=[]) self.generic_visit(node) - def visit_Import(self, node: Union[ast.Import, ast.ImportFrom]) -> None: + def visit_Import(self, node: ast.Import | ast.ImportFrom) -> None: for alias in node.names: name = alias.asname or alias.name self.import_names[name] = node @@ -644,7 +642,7 @@ def get_component_from_source(self, name, source): self.logger.debug(f"Failed to get '{name}' from '{ast_str(source)}'", exc_info=ex) return aliases.get(name) - def get_node_component(self, node, source) -> Optional[tuple[type, Optional[str]]]: + def get_node_component(self, node, source) -> tuple[type, str | None] | None: function_or_class = method_or_property = None module = inspect.getmodule(self.component) if isinstance(node.func, ast.Name): @@ -729,7 +727,7 @@ def replace_param_default_subclass_specs(self, params: list[ParamData]) -> None: del default["init_args"] param.default = default - def get_call_class_type(self, node) -> Optional[type]: + def get_call_class_type(self, node) -> type | None: names = ast_get_name_and_attrs(getattr(node, "func", None)) class_type = self.get_component_globals().get(names[0]) if names else None for name in names[1:]: @@ -849,7 +847,7 @@ def add_node_origins(self, params: ParamList, node) -> None: origin = self.get_node_origin(node) param.origin = origin - def get_parameters_call_attr(self, attr_name: str, attr_value: ast.AST) -> Optional[ParamList]: + def get_parameters_call_attr(self, attr_name: str, attr_value: ast.AST) -> ParamList | None: self.parse_source_tree() values_to_find = {attr_name: attr_value} values_found = self.find_values_usage(values_to_find) @@ -977,10 +975,10 @@ def is_init_field_attrs(field) -> bool: def get_parameters_from_pydantic_or_attrs( - function_or_class: Union[Callable, type], - method_or_property: Optional[str], + function_or_class: Callable | type, + method_or_property: str | None, logger: logging.Logger, -) -> Optional[ParamList]: +) -> ParamList | None: from ._optionals import attrs_support, pydantic_support if method_or_property or not (pydantic_support or attrs_support): @@ -1033,26 +1031,26 @@ def get_parameters_from_pydantic_or_attrs( def get_parameters_from_ast( - function_or_class: Union[Callable, type], - method_or_property: Optional[str], + function_or_class: Callable | type, + method_or_property: str | None, logger: logging.Logger, -) -> Optional[ParamList]: +) -> ParamList | None: visitor = ParametersVisitor(function_or_class, method_or_property, logger=logger) return visitor.get_parameters() def get_parameters_from_stubs( - function_or_class: Union[Callable, type], - method_or_property: Optional[str], + function_or_class: Callable | type, + method_or_property: str | None, logger: logging.Logger, -) -> Optional[ParamList]: +) -> ParamList | None: component, parent, _ = get_component_and_parent(function_or_class, method_or_property) try: inspect.signature(component) return None except Exception: pass # only from stubs if getting signature fails - params: Optional[ParamList] = None + params: ParamList | None = None resolver = get_stubs_resolver() stub_import = resolver.get_component_imported_info(component, parent) if stub_import: @@ -1082,8 +1080,8 @@ def get_parameters_from_stubs( def get_parameters_by_assumptions( - function_or_class: Union[Callable, type], - method_name: Optional[str], + function_or_class: Callable | type, + method_name: str | None, logger: logging.Logger, ) -> ParamList: component, parent, method_name = get_component_and_parent(function_or_class, method_name) @@ -1102,9 +1100,9 @@ def get_parameters_by_assumptions( def get_signature_parameters( - function_or_class: Union[Callable, type], - method_or_property: Optional[str] = None, - logger: Union[bool, str, dict, logging.Logger] = True, + function_or_class: Callable | type, + method_or_property: str | None = None, + logger: bool | str | dict | logging.Logger = True, ) -> ParamList: """Get parameters by inspecting ASTs, stubs or by inheritance assumptions. diff --git a/jsonargparse/_paths.py b/jsonargparse/_paths.py index 229f5add..079b53d8 100644 --- a/jsonargparse/_paths.py +++ b/jsonargparse/_paths.py @@ -8,7 +8,7 @@ from contextvars import ContextVar from dataclasses import dataclass from io import StringIO -from typing import IO, Any, Optional, Union +from typing import IO, Any, Union from ._deprecated import PathDeprecations from ._optionals import ( @@ -18,7 +18,7 @@ url_support, ) -_current_path_dir: ContextVar[Optional[str]] = ContextVar("_current_path_dir", default=None) +_current_path_dir: ContextVar[str | None] = ContextVar("_current_path_dir", default=None) class _CachedStdin(StringIO): @@ -44,7 +44,7 @@ class _UrlData: url_path: str -def _parse_url(url: str) -> Optional[_UrlData]: +def _parse_url(url: str) -> _UrlData | None: index = url.rfind("://") if index <= 0: return None @@ -108,14 +108,14 @@ class Path(PathDeprecations): standard input or output. """ - _url_data: Optional[_UrlData] + _url_data: _UrlData | None _file_scheme = re.compile("^file:///?") def __init__( self, path: Union[str, os.PathLike, "Path"], mode: str = "fr", - cwd: Optional[Union[str, os.PathLike]] = None, + cwd: str | os.PathLike | None = None, **kwargs, ): """Initializer for Path instance. @@ -346,10 +346,10 @@ def _check_mode(mode: str): @contextmanager -def change_to_path_dir(path: Optional[Union[Path, str]]) -> Iterator[Optional[str]]: +def change_to_path_dir(path: Path | str | None) -> Iterator[str | None]: """A context manager for running code in the directory of a path.""" path_dir = _current_path_dir.get() - chdir: Union[bool, str] = False + chdir: bool | str = False if path is not None: if isinstance(path, str): path = Path(path, mode="d") diff --git a/jsonargparse/_postponed_annotations.py b/jsonargparse/_postponed_annotations.py index 1d250fd6..c2b7db79 100644 --- a/jsonargparse/_postponed_annotations.py +++ b/jsonargparse/_postponed_annotations.py @@ -3,68 +3,17 @@ import logging import sys import textwrap -from collections import namedtuple -from copy import deepcopy from dataclasses import is_dataclass from importlib import import_module -from typing import Any, ForwardRef, Optional, Union, get_type_hints +from typing import Any, ForwardRef, TypeAlias, Union, get_type_hints -from ._optionals import typing_extensions_import from ._typehints import mapping_origin_types, sequence_origin_types, tuple_set_origin_types from ._util import get_typehint_origin -var_map = namedtuple("var_map", "name value") -none_map = var_map(name="NoneType", value=type(None)) -union_map = var_map(name="Union", value=Union) _TRIGGER_MODULE_CACHE_MAXSIZE = 1024 _TRIGGER_MODULE_CACHE: dict[int, dict[str, Any]] = {} -class BackportTypeHints(ast.NodeTransformer): - def visit_Constant(self, node: ast.Constant) -> Union[ast.Constant, ast.Name]: - if node.value is None: - return self.new_name_load(none_map) - return node - - def visit_BinOp(self, node: ast.BinOp) -> Union[ast.BinOp, ast.Subscript]: - out_node: Union[ast.BinOp, ast.Subscript] = node - if isinstance(node.op, ast.BitOr): - elts: list = [] - self.append_union_elts(node.left, elts) - self.append_union_elts(node.right, elts) - out_node = ast.Subscript( - value=self.new_name_load(union_map), - slice=ast.Index( # type: ignore[arg-type,call-arg] - value=ast.Tuple(elts=elts, ctx=ast.Load()), - ctx=ast.Load(), - ), - ctx=ast.Load(), - ) - return out_node - - def append_union_elts(self, node: ast.AST, elts: list) -> None: - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): - self.append_union_elts(node.left, elts) - self.append_union_elts(node.right, elts) - else: - elts.append(self.visit(node)) - - def new_name_load(self, var: var_map) -> ast.Name: - name = f"_{self.__class__.__name__}_{var.name}" - self.exec_vars[name] = var.value - return ast.Name(id=name, ctx=ast.Load()) - - def backport(self, input_ast: ast.AST, exec_vars: dict) -> ast.AST: - typing = __import__("typing") - for key, value in exec_vars.items(): - if getattr(value, "__module__", "") == "collections.abc": - if hasattr(typing, key): - exec_vars[key] = getattr(typing, key) - self.exec_vars = exec_vars - backport_ast = self.visit(deepcopy(input_ast)) - return ast.fix_missing_locations(backport_ast) - - class NamesVisitor(ast.NodeVisitor): def visit_Name(self, node: ast.Name) -> None: self.names_found.append(node.id) @@ -124,7 +73,7 @@ def generic_visit(self, node: ast.AST) -> None: super().generic_visit(node) def update_aliases( - self, module_source: str, module: str, aliases: dict, logger: Optional[logging.Logger] = None + self, module_source: str, module: str, aliases: dict, logger: logging.Logger | None = None ) -> None: self.module = module self.aliases = aliases @@ -158,12 +107,7 @@ def get_arg_type(arg_ast, aliases): exec_vars[name] = value type_ast.body = body + type_ast.body if "TypeAlias" not in exec_vars: - type_alias = typing_extensions_import("TypeAlias") - if type_alias: - exec_vars["TypeAlias"] = type_alias - if sys.version_info < (3, 10): - backporter = BackportTypeHints() - type_ast = backporter.backport(type_ast, exec_vars) + exec_vars["TypeAlias"] = TypeAlias try: exec(compile(type_ast, filename="", mode="exec"), exec_vars, exec_vars) except NameError as ex: @@ -299,7 +243,7 @@ def _enrich_globals_for_string_forward_refs(global_vars: dict[str, Any]) -> None _update_missing_from_module_vars(global_vars, missing, mod_vars) -def get_global_vars(obj: Any, logger: Optional[logging.Logger]) -> dict: +def get_global_vars(obj: Any, logger: logging.Logger | None) -> dict: global_vars = getattr(obj, "__globals__", {}).copy() if is_dataclass(obj): next_mro = inspect.getmro(obj)[1] # type: ignore[arg-type] @@ -319,13 +263,13 @@ def get_global_vars(obj: Any, logger: Optional[logging.Logger]) -> dict: return global_vars -def get_types(obj: Any, logger: Optional[logging.Logger] = None) -> dict: +def get_types(obj: Any, logger: logging.Logger | None = None) -> dict: global_vars = get_global_vars(obj, logger) try: types = get_type_hints(obj, global_vars) except Exception as ex1: types = ex1 - if isinstance(types, dict) and all(not type_requires_eval(t) for t in types.values()): + if not isinstance(types, Exception) and all(not type_requires_eval(t) for t in types.values()): return types try: @@ -335,11 +279,9 @@ def get_types(obj: Any, logger: Optional[logging.Logger] = None) -> dict: node = tree.body[0] assert isinstance(node, (ast.FunctionDef, ast.ClassDef)) except Exception as ex2: - if isinstance(types, Exception): - if logger: - logger.debug(f"Failed to parse the source code for {obj}", exc_info=ex2) - raise type(types)(f"{repr(types)} + {repr(ex2)}") from ex2 - return types + if logger: + 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[misc,arg-type] aliases = __builtins__.copy() # type: ignore[attr-defined] aliases.update(global_vars) diff --git a/jsonargparse/_required.py b/jsonargparse/_required.py index 618f2966..fcca6804 100644 --- a/jsonargparse/_required.py +++ b/jsonargparse/_required.py @@ -1,7 +1,7 @@ from argparse import Action, _SubParsersAction +from collections.abc import Iterator from contextlib import contextmanager from contextvars import ContextVar -from typing import Iterator, Optional, Union from ._type_checking import ArgumentParser @@ -31,7 +31,7 @@ def iter_required_keys(parser: ArgumentParser) -> Iterator[str]: yield key -def set_required(parser: ArgumentParser, key_or_action: Union[str, Action], value: bool = True) -> None: +def set_required(parser: ArgumentParser, key_or_action: str | Action, value: bool = True) -> None: """Sets required state for either an action-backed or virtual key.""" action = key_or_action if isinstance(key_or_action, Action) else None key: str = action.dest if action is not None else key_or_action # type: ignore[assignment] @@ -45,12 +45,12 @@ def set_required(parser: ArgumentParser, key_or_action: Union[str, Action], valu parser._extra_required_keys.discard(key) -def clear_required(parser: ArgumentParser, key_or_action: Union[str, Action]) -> None: +def clear_required(parser: ArgumentParser, key_or_action: str | Action) -> None: """Clears required state for either an action-backed or virtual key.""" set_required(parser, key_or_action, value=False) -def _find_exact_action(parser: ArgumentParser, key: str) -> Optional[Action]: +def _find_exact_action(parser: ArgumentParser, key: str) -> Action | None: for action in parser._actions: if getattr(action, "dest", None) == key: return action diff --git a/jsonargparse/_signatures.py b/jsonargparse/_signatures.py index c503e427..0a7b0ae0 100644 --- a/jsonargparse/_signatures.py +++ b/jsonargparse/_signatures.py @@ -4,7 +4,8 @@ import inspect import re from argparse import SUPPRESS, ArgumentParser -from typing import Any, Callable, Optional, Union +from collections.abc import Callable +from typing import Any, Optional, Union from ._actions import _ActionConfigLoad from ._common import ( @@ -42,11 +43,11 @@ class SignatureArguments(LoggerProperty): def add_class_arguments( self, theclass: type, - nested_key: Optional[str] = None, + nested_key: str | None = None, as_group: bool = True, as_positional: bool = False, - default: Optional[Union[dict, Namespace, type]] = None, - skip: Optional[set[Union[str, int]]] = None, + default: dict | Namespace | type | None = None, + skip: set[str | int] | None = None, instantiate: bool = True, fail_untyped: bool = True, sub_configs: bool = False, @@ -133,10 +134,10 @@ def add_method_arguments( self, theclass: type, themethod: str, - nested_key: Optional[str] = None, + nested_key: str | None = None, as_group: bool = True, as_positional: bool = False, - skip: Optional[set[Union[str, int]]] = None, + skip: set[str | int] | None = None, fail_untyped: bool = True, sub_configs: bool = False, ) -> list[str]: @@ -181,10 +182,10 @@ def add_method_arguments( def add_function_arguments( self, function: Callable, - nested_key: Optional[str] = None, + nested_key: str | None = None, as_group: bool = True, as_positional: bool = False, - skip: Optional[set[Union[str, int]]] = None, + skip: set[str | int] | None = None, fail_untyped: bool = True, sub_configs: bool = False, ) -> list[str]: @@ -231,15 +232,15 @@ def _add_signature_arguments( self, function_or_class, method_name, - nested_key: Optional[str], + nested_key: str | None, as_group: bool = True, as_positional: bool = False, - skip: Optional[set[Union[str, int]]] = None, + skip: set[str | int] | None = None, fail_untyped: bool = True, sub_configs: bool = False, instantiate: bool = True, - linked_targets: Optional[set[str]] = None, - help: Optional[str] = None, + linked_targets: set[str] | None = None, + help: str | None = None, ) -> list[str]: """Adds arguments from parameters of objects based on signatures and docstrings. @@ -317,15 +318,15 @@ def _add_signature_arguments( def _add_signature_parameter( self, container, - nested_key: Optional[str], + nested_key: str | None, param, added_args: list[str], - skip: Optional[set[str]] = None, + skip: set[str] | None = None, fail_untyped: bool = True, as_positional: bool = False, sub_configs: bool = False, instantiate: bool = True, - linked_targets: Optional[set[str]] = None, + linked_targets: set[str] | None = None, default: Any = inspect_empty, **kwargs, ): @@ -448,10 +449,10 @@ def _add_signature_parameter( def add_subclass_arguments( self, - baseclass: Union[type, tuple[type, ...]], + baseclass: type | tuple[type, ...], nested_key: str, as_group: bool = True, - skip: Optional[set[str]] = None, + skip: set[str] | None = None, instantiate: bool = True, required: bool = False, metavar: str = "CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE", @@ -541,7 +542,7 @@ def _create_group_if_requested( doc_group = str(obj[0]) else: doc_group = str(obj) - name = get_object_name(obj) if nested_key is None else nested_key + name = obj.__name__ if nested_key is None else nested_key group = self.add_argument_group(strip_title(doc_group), name=name) if config_load and nested_key is not None: group.add_argument("--" + nested_key, action=_ActionConfigLoad(basetype=config_load_type)) @@ -552,12 +553,6 @@ def _create_group_if_requested( return group -def get_object_name(obj) -> str: - if hasattr(obj, "__name__"): - return obj.__name__ - return str(obj).split(".")[-1].replace("[", "_").replace("]", "") - - def group_instantiate_class(group, cfg): try: value, parent, key = get_value_and_parent(cfg, group.dest) diff --git a/jsonargparse/_stubs_resolver.py b/jsonargparse/_stubs_resolver.py index f8e22704..0398c762 100644 --- a/jsonargparse/_stubs_resolver.py +++ b/jsonargparse/_stubs_resolver.py @@ -4,7 +4,7 @@ from contextlib import suppress from copy import deepcopy from importlib import import_module -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from ._common import get_parsing_setting from ._optionals import import_typeshed_client, typeshed_client_support @@ -40,9 +40,9 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: for alias in node.names: self.imports_found[alias.asname or alias.name] = (node.module, alias.name) - def find(self, node: ast.AST, module_path: str) -> dict[str, tuple[Optional[str], str]]: + def find(self, node: ast.AST, module_path: str) -> dict[str, tuple[str | None, str]]: self.module_path = module_path.split(".") - self.imports_found: dict[str, tuple[Optional[str], str]] = {} + self.imports_found: dict[str, tuple[str | None, str]] = {} self.visit(node) return self.imports_found @@ -73,7 +73,7 @@ def find(self, node: ast.AST) -> dict[str, ast.Assign]: class MethodsVisitor(ast.NodeVisitor): - method_found: Optional[ast.FunctionDef] + method_found: ast.FunctionDef | None def visit_FunctionDef(self, node: ast.FunctionDef) -> None: if not self.method_found and node.name == self.method_name: @@ -91,7 +91,7 @@ def visit_If(self, node: ast.If) -> None: node.body = [] self.generic_visit(node) - def find(self, node: ast.AST, method_name: str) -> Optional[ast.FunctionDef]: + def find(self, node: ast.AST, method_name: str) -> ast.FunctionDef | None: self.method_name = method_name self.method_found = None self.visit(node) @@ -135,11 +135,11 @@ def get_source_module(path: str, component) -> tc.ModulePath: class StubsResolver(tc.Resolver): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - self._module_ast_cache: dict[str, Optional[ast.AST]] = {} + self._module_ast_cache: dict[str, ast.AST | None] = {} self._module_assigns_cache: dict[str, dict[str, ast.Assign]] = {} - self._module_imports_cache: dict[str, dict[str, tuple[Optional[str], str]]] = {} + self._module_imports_cache: dict[str, dict[str, tuple[str | None, str]]] = {} - def get_imported_info(self, path: str, component=None) -> Optional[tc.ImportedInfo]: + def get_imported_info(self, path: str, component=None) -> tc.ImportedInfo | None: resolved = self.get_fully_qualified_name(path) imported_info = None if isinstance(resolved, tc.ImportedInfo): @@ -149,7 +149,7 @@ def get_imported_info(self, path: str, component=None) -> Optional[tc.ImportedIn imported_info = tc.ImportedInfo(source_module=source_module, info=resolved) return imported_info - def get_component_imported_info(self, component, parent) -> Optional[tc.ImportedInfo]: + def get_component_imported_info(self, component, parent) -> tc.ImportedInfo | None: if not parent and inspect.ismethod(component): parent = type(component.__self__) component = getattr(parent, component.__name__) @@ -189,7 +189,7 @@ def get_module_stub_imports(self, module_path: str): def add_import_aliases(self, aliases, stub_import: tc.ImportedInfo): module_path = ".".join(stub_import.source_module) module = import_module_or_none(module_path) - stub_ast: Optional[ast.AST] = None + stub_ast: ast.AST | None = None if isinstance(stub_import.info.ast, (ast.Assign, ast.AnnAssign)): stub_ast = stub_import.info.ast.value elif isinstance(stub_import.info.ast, ast.AST): @@ -244,7 +244,7 @@ def alias_is_unique(aliases, name, source, value): return True -def get_stub_types(params, component, parent, logger) -> Optional[dict[str, Any]]: +def get_stub_types(params, component, parent, logger) -> dict[str, Any] | None: if not typeshed_client_support: return None missing_types = { diff --git a/jsonargparse/_subcommands.py b/jsonargparse/_subcommands.py index f7d914c4..3ab68a0b 100644 --- a/jsonargparse/_subcommands.py +++ b/jsonargparse/_subcommands.py @@ -5,7 +5,7 @@ from argparse import _SubParsersAction from contextlib import contextmanager from contextvars import ContextVar -from typing import NoReturn, Optional, Union +from typing import NoReturn from ._actions import filter_non_parsing_actions from ._common import parsing_defaults, single_subcommand @@ -30,10 +30,10 @@ def is_branch_key(parser, key: str) -> bool: def find_action_and_subcommand( - parser: Union[ArgumentParser, ActionsContainer], + parser: ArgumentParser | ActionsContainer, dest: str, - exclude: Optional[Union[type[ArgparseAction], tuple[type[ArgparseAction], ...]]] = None, -) -> tuple[Optional[ArgparseAction], Optional[str]]: + exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, +) -> tuple[ArgparseAction | None, str | None]: """Finds an action in a parser given its destination key.""" actions = filter_non_parsing_actions(parser._actions) if exclude is not None: @@ -68,18 +68,18 @@ def find_action_and_subcommand( def find_action( - parser: Union[ArgumentParser, ActionsContainer], + parser: ArgumentParser | ActionsContainer, dest: str, - exclude: Optional[Union[type[ArgparseAction], tuple[type[ArgparseAction], ...]]] = None, -) -> Optional[ArgparseAction]: + exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, +) -> ArgparseAction | None: return find_action_and_subcommand(parser, dest, exclude=exclude)[0] def find_parent_action_and_subcommand( parser: ArgumentParser, key: str, - exclude: Optional[Union[type[ArgparseAction], tuple[type[ArgparseAction], ...]]] = None, -) -> tuple[Optional[ArgparseAction], Optional[str]]: + exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, +) -> tuple[ArgparseAction | None, str | None]: action, subcommand = find_action_and_subcommand(parser, key, exclude=exclude) if action is None and "." in key: parts = split_key(key) @@ -93,8 +93,8 @@ def find_parent_action_and_subcommand( def find_parent_action( parser: ArgumentParser, key: str, - exclude: Optional[Union[type[ArgparseAction], tuple[type[ArgparseAction], ...]]] = None, -) -> Optional[ArgparseAction]: + exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, +) -> ArgparseAction | None: return find_parent_action_and_subcommand(parser, key, exclude=exclude)[0] @@ -177,7 +177,7 @@ def get_subcommands( cfg: Namespace, prefix: str = "", fail_no_subcommand: bool = True, -) -> tuple[Optional[list[str]], Optional[list[ArgumentParser]]]: +) -> tuple[list[str] | None, list[ArgumentParser] | None]: """Returns subcommand names and corresponding subparsers.""" if parser._subcommands_action is None: return None, None @@ -233,7 +233,7 @@ def get_subcommand( cfg: Namespace, prefix: str = "", fail_no_subcommand: bool = True, -) -> tuple[Optional[str], Optional[ArgumentParser]]: +) -> tuple[str | None, ArgumentParser | None]: """Returns a single subcommand name and corresponding subparser.""" subcommands, subparsers = get_subcommands( parser, @@ -247,7 +247,7 @@ def get_subcommand( def handle_subcommands( parser: ArgumentParser, cfg: Namespace, - env: Optional[bool], + env: bool | None, defaults: bool, prefix: str = "", fail_no_subcommand: bool = True, diff --git a/jsonargparse/_util.py b/jsonargparse/_util.py index 379380cd..62617b94 100644 --- a/jsonargparse/_util.py +++ b/jsonargparse/_util.py @@ -6,15 +6,13 @@ import warnings from argparse import ArgumentError from collections import namedtuple +from collections.abc import Callable, Iterator from contextlib import contextmanager from contextvars import ContextVar from importlib import import_module from types import BuiltinFunctionType, FunctionType, ModuleType from typing import ( Any, - Callable, - Iterator, - Optional, Type, Union, ) @@ -42,7 +40,7 @@ config_load_stack: ContextVar[tuple[tuple[str, str], ...]] = ContextVar("config_load_stack", default=()) -def argument_error(message: str, default_config_file: Optional[str] = None) -> ArgumentError: +def argument_error(message: str, default_config_file: str | None = None) -> ArgumentError: ex = ArgumentError(None, message) if default_config_file: ex.default_config_file = default_config_file # type: ignore[attr-defined] @@ -66,7 +64,7 @@ def _format_config_load_chain(stack: tuple[tuple[str, str], ...], path_id: tuple @contextmanager -def load_config_path_context(cfg_path: Optional[Path]) -> Iterator[None]: +def load_config_path_context(cfg_path: Path | None) -> Iterator[None]: if cfg_path is None: yield return @@ -96,7 +94,7 @@ def warning(message, category=JsonargparseWarning, stacklevel=1): class CaptureParserException(Exception): - def __init__(self, parser: Optional[ArgumentParser]): + def __init__(self, parser: ArgumentParser | None): self.parser = parser super().__init__("" if parser else "No parse_args call to capture the parser.") @@ -137,11 +135,9 @@ def identity(value): NestedArg = namedtuple("NestedArg", "key val") -def parse_value_or_config( - value: Any, enable_path: bool = True, simple_types: bool = False -) -> tuple[Any, Optional[Path]]: +def parse_value_or_config(value: Any, enable_path: bool = True, simple_types: bool = False) -> tuple[Any, Path | None]: """Parses yaml/json config in a string or a path""" - nested_arg: Union[bool, NestedArg] = False + nested_arg: bool | NestedArg = False if isinstance(value, NestedArg): nested_arg = value value = nested_arg.val @@ -200,7 +196,7 @@ def register_unresolvable_import_paths(*modules: ModuleType): unresolvable_import_paths[val] = f"{module.__name__}.{val.__name__}" -def get_module_var_path(module_path: str, value: Any) -> Optional[str]: +def get_module_var_path(module_path: str, value: Any) -> str | None: module = import_module(module_path) for name, var in vars(module).items(): if var is value: @@ -208,7 +204,7 @@ def get_module_var_path(module_path: str, value: Any) -> Optional[str]: return None -def get_import_path(value: Any) -> Optional[str]: +def get_import_path(value: Any) -> str | None: """Returns the shortest dot import path for the given object.""" path = None value = get_generic_origin(value) diff --git a/jsonargparse/typing.py b/jsonargparse/typing.py index 3a497a4c..94bdcf21 100644 --- a/jsonargparse/typing.py +++ b/jsonargparse/typing.py @@ -6,12 +6,8 @@ import pathlib import re import sys -from typing import Any, Callable, Optional, Union, get_type_hints - -if sys.version_info >= (3, 10): - from typing import TypeAlias as _TypeAlias -else: - _TypeAlias = type +from collections.abc import Callable +from typing import Any, TypeAlias, Union, get_type_hints from ._common import ClassType, is_final_class, is_subclass, path_dump_preserve_relative from ._namespace import Namespace @@ -71,8 +67,8 @@ def class_from_function( func: Callable[..., ClassType], - func_return: Optional[type[ClassType]] = None, - name: Optional[str] = None, + func_return: type[ClassType] | None = None, + name: str | None = None, ) -> type[ClassType]: """Creates a dynamic class which if instantiated is equivalent to calling func. @@ -224,10 +220,10 @@ def extend_base_type( name: str, base_type: type, validation_fn: Callable, - docstring: Optional[str] = None, - extra_attrs: Optional[dict] = None, - register_key: Optional[tuple] = None, -) -> _TypeAlias: + docstring: str | None = None, + extra_attrs: dict | None = None, + register_key: tuple | None = None, +) -> TypeAlias: """Creates and registers an extension of base type. Args: @@ -267,12 +263,12 @@ def __new__(cls, v): def restricted_number_type( - name: Optional[str], + name: str | None, base_type: type, - restrictions: Union[tuple, list[tuple]], + restrictions: tuple | list[tuple], join: str = "and", - docstring: Optional[str] = None, -) -> _TypeAlias: + docstring: str | None = None, +) -> TypeAlias: """Creates or returns an already registered restricted number type class. Args: @@ -341,9 +337,9 @@ def validation_fn(cls, v): def restricted_string_type( name: str, - regex: Union[str, re.Pattern], - docstring: Optional[str] = None, -) -> _TypeAlias: + regex: str | re.Pattern, + docstring: str | None = None, +) -> TypeAlias: """Creates or returns an already registered restricted string type class. Args: @@ -393,7 +389,7 @@ def _serialize_path(path: Path): return str(path) -def path_type(mode: str, docstring: Optional[str] = None, **kwargs) -> _TypeAlias: +def path_type(mode: str, docstring: str | None = None, **kwargs) -> TypeAlias: """Creates or returns an already registered path type class. Args: @@ -443,8 +439,8 @@ def __init__( self, type_class: _TypeClass, serializer: Callable, - deserializer: Optional[Callable], - deserializer_exceptions: Union[type[Exception], tuple[type[Exception], ...]], + deserializer: Callable | None, + deserializer_exceptions: type[Exception] | tuple[type[Exception], ...], type_check: Callable, ): self.type_class = type_class @@ -472,15 +468,15 @@ def deserializer(self, value): def register_type( type_class: _TypeClass, serializer: Callable = str, - deserializer: Optional[Callable] = None, - deserializer_exceptions: Union[type[Exception], tuple[type[Exception], ...]] = ( + deserializer: Callable | None = None, + deserializer_exceptions: type[Exception] | tuple[type[Exception], ...] = ( ValueError, TypeError, AttributeError, ), type_check: Callable = lambda v, t: v.__class__ == t, fail_already_registered: bool = True, - uniqueness_key: Optional[tuple] = None, + uniqueness_key: tuple | None = None, ) -> None: """Registers a new type for use in jsonargparse parsers. @@ -518,7 +514,7 @@ def register_type_on_first_use(import_path: str, *args, **kwargs): ) -def get_registered_type(type_class) -> Optional[RegisteredType]: +def get_registered_type(type_class) -> RegisteredType | None: if type_class not in registered_type_handlers: from contextlib import suppress @@ -529,7 +525,7 @@ def get_registered_type(type_class) -> Optional[RegisteredType]: return registered_type_handlers.get(type_class) -def add_type(type_class: type, uniqueness_key: Optional[tuple], type_check: Optional[Callable] = None): +def add_type(type_class: type, uniqueness_key: tuple | None, type_check: Callable | None = None): assert uniqueness_key not in registered_types if type_class.__name__ in globals(): raise ValueError(f'Type name "{type_class.__name__}" clashes with name already defined in jsonargparse.typing.') @@ -596,7 +592,7 @@ def raise_error(): register_type_on_first_use("datetime.timedelta", deserializer=timedelta_deserializer) -def bytes_serializer(value: Union[bytes, bytearray]) -> str: +def bytes_serializer(value: bytes | bytearray) -> str: from base64 import b64encode return b64encode(value).decode() diff --git a/jsonargparse_tests/conftest.py b/jsonargparse_tests/conftest.py index 7201a9bf..4178ce50 100644 --- a/jsonargparse_tests/conftest.py +++ b/jsonargparse_tests/conftest.py @@ -3,12 +3,12 @@ import platform import re import sys +from collections.abc import Iterator from contextlib import ExitStack, contextmanager, redirect_stderr, redirect_stdout from functools import wraps from importlib.util import find_spec from io import StringIO from pathlib import Path -from typing import Iterator, List from unittest.mock import MagicMock, patch import pytest @@ -254,14 +254,14 @@ def get_parser_help(parser: ArgumentParser, strip=False, columns=columns) -> str return out.getvalue() -def get_parse_args_stdout(parser: ArgumentParser, args: List[str]) -> str: +def get_parse_args_stdout(parser: ArgumentParser, args: list[str]) -> str: out = StringIO() with patch.dict(os.environ, {"COLUMNS": columns}), redirect_stdout(out), pytest.raises(SystemExit): parser.parse_args(args) return out.getvalue() -def get_parse_args_stderr(parser: ArgumentParser, args: List[str]) -> str: +def get_parse_args_stderr(parser: ArgumentParser, args: list[str]) -> str: err = StringIO() with patch.object(parser, "exit_on_error", return_value=True): with patch.dict(os.environ, {"COLUMNS": columns}), redirect_stderr(err), pytest.raises(SystemExit): diff --git a/jsonargparse_tests/pyproject.toml b/jsonargparse_tests/pyproject.toml index 0b2de80c..d8c1a445 100644 --- a/jsonargparse_tests/pyproject.toml +++ b/jsonargparse_tests/pyproject.toml @@ -12,6 +12,7 @@ authors = [ ] license = "MIT" license-files = ["LICENSE.rst"] +requires-python = ">=3.10" dependencies = [ "jsonargparse[test-no-urls]", diff --git a/jsonargparse_tests/test_formatters.py b/jsonargparse_tests/test_formatters.py index 8f8c78ac..f4206bbf 100644 --- a/jsonargparse_tests/test_formatters.py +++ b/jsonargparse_tests/test_formatters.py @@ -2,7 +2,6 @@ import os from pathlib import Path -from typing import Tuple from unittest.mock import patch import pytest @@ -103,7 +102,7 @@ def test_help_action_yes_no(parser): @pytest.fixture -def default_config_files(tmp_cwd) -> Tuple[ArgumentParser, str, Path]: +def default_config_files(tmp_cwd) -> tuple[ArgumentParser, str, Path]: not_exist = "does_not_exist.yaml" exists = Path("config.yaml") exists.write_text(json_or_yaml_dump({"v1": "from yaml v1", "n1.v2": "from yaml v2"})) diff --git a/jsonargparse_tests/test_postponed_annotations.py b/jsonargparse_tests/test_postponed_annotations.py index 90c6d4d4..c7617739 100644 --- a/jsonargparse_tests/test_postponed_annotations.py +++ b/jsonargparse_tests/test_postponed_annotations.py @@ -35,12 +35,12 @@ def function_pep604(p1: str | None, p2: int | float | bool = 1): return p1 # pragma: no cover -def test_get_types_pep604(): +def test_get_types_function_pep604(): types = get_types(function_pep604) - assert types == {"p1": Union[str, None], "p2": Union[int, float, bool]} + assert types == {"p1": str | None, "p2": int | float | bool} -class NeedsBackport: +class ClassPep604: def __init__(self, p1: list | set): self.p1 = p1 # pragma: no cover @@ -56,25 +56,16 @@ def class_method(cls, p1: float | None): @pytest.mark.parametrize( ["method", "expected"], [ - (NeedsBackport.__init__, {"p1": Union[list, set]}), - (NeedsBackport.static_method, {"p1": Union[str, int]}), - (NeedsBackport.class_method, {"p1": Union[float, None]}), + (ClassPep604.__init__, {"p1": list | set}), + (ClassPep604.static_method, {"p1": str | int}), + (ClassPep604.class_method, {"p1": float | None}), ], ) -def test_get_types_methods(method, expected): +def test_get_types_methods_pep604(method, expected): types = get_types(method) assert types == expected -def function_forward_ref(cls: "NeedsBackport", p1: "int"): - return cls # pragma: no cover - - -def test_get_types_forward_ref(): - types = get_types(function_forward_ref) - assert types == {"cls": NeedsBackport, "p1": int} - - def function_undefined_type(p1: not_defined | None, p2: int): # type: ignore # noqa: F821 return p1 # pragma: no cover @@ -277,6 +268,31 @@ def test_get_types_type_checking_dict(): assert str(types["p1"]) == f"{dct}[str, {__name__}.TypeCheckingClass1 | {__name__}.TypeCheckingClass2]" +class DefinedClass: + pass + + +def function_forward_ref(cls: "DefinedClass", p1: "int"): + return cls # pragma: no cover + + +def test_get_types_forward_ref(): + types = get_types(function_forward_ref) + assert types == {"cls": DefinedClass, "p1": int} + + +def function_nested_partial_forward_ref( + p1: List[List["DefinedClass"]], + p2: "Undefined", # type: ignore[name-defined] # noqa: F821 +): + pass + + +def test_nested_partial_forward_ref(parser): + types = get_types(function_nested_partial_forward_ref) + assert types == {"p1": list[list[DefinedClass]], "p2": "Undefined"} + + def function_type_checking_undefined_forward_ref(p1: List["Undefined"], p2: bool): # type: ignore # noqa: F821 return p1 # pragma: no cover diff --git a/jsonargparse_tests/test_stubs_resolver.py b/jsonargparse_tests/test_stubs_resolver.py index 83354970..e236a5d8 100644 --- a/jsonargparse_tests/test_stubs_resolver.py +++ b/jsonargparse_tests/test_stubs_resolver.py @@ -146,10 +146,7 @@ def test_get_params_object_instance_method(): def test_get_params_conditional_python_version(): params = get_params(Random, "seed") assert ["a", "version"] == get_param_names(params) - if sys.version_info >= (3, 10): - assert "int | float | str | bytes | bytearray | None" == str(params[0].annotation) - else: - assert "typing.Union[int, float, str, bytes, bytearray, NoneType]" == str(params[0].annotation) + assert "int | float | str | bytes | bytearray | None" == str(params[0].annotation) assert int is params[1].annotation with mock_stubs_missing_types(): params = get_params(Random, "seed") @@ -185,12 +182,11 @@ def test_get_params_classmethod(parametrize_allow_py_files): 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", "preset"} - ) + assert all( + 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") assert expected == get_param_names(params)[: len(expected)] @@ -208,8 +204,7 @@ def test_get_params_staticmethod(): def test_get_params_function(parametrize_allow_py_files): params = get_params(ip_network) assert ["address", "strict"] == get_param_names(params) - if sys.version_info >= (3, 10): - assert "int | str | bytes | ipaddress.IPv4Address | " in str(params[0].annotation) + assert "int | str | bytes | ipaddress.IPv4Address | " in str(params[0].annotation) assert bool is params[1].annotation with mock_stubs_missing_types(): params = get_params(ip_network) @@ -273,8 +268,7 @@ def test_get_params_complex_function_requests_get(parser): "json", ] assert expected == get_param_names(params) - if sys.version_info >= (3, 10): - assert all(p.annotation is not inspect._empty for p in params) + assert all(p.annotation is not inspect._empty for p in params) parser.add_function_arguments(get, fail_untyped=False) assert ["url", "params"] == list(parser.get_defaults().keys()) @@ -291,8 +285,7 @@ def test_get_params_inspect_signature_failure_function(logger): params = get_params(ip_network, logger=logger) assert ["address", "strict"] == get_param_names(params) assert params[1].annotation == bool - if sys.version_info >= (3, 10): - assert "int | str | bytes | ipaddress.IPv4Address | " in str(params[0].annotation) + assert "int | str | bytes | ipaddress.IPv4Address | " in str(params[0].annotation) assert "get_parameters_from_ast failed" in logs.getvalue() assert "get_parameters_by_assumptions failed" not in logs.getvalue() @@ -336,7 +329,6 @@ def test_get_params_inspect_signature_failure_missing_type(logger): params = get_params(ip_network, logger=logger) assert ["address", "strict"] == get_param_names(params) assert params[1].annotation == inspect._empty - if sys.version_info >= (3, 10): - assert "int | str | bytes | ipaddress.IPv4Address | " in str(params[0].annotation) + assert "int | str | bytes | ipaddress.IPv4Address | " in str(params[0].annotation) assert "get_parameters_from_ast failed" in logs.getvalue() assert "get_parameters_by_assumptions failed" not in logs.getvalue() diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py index 34f4ffe4..7f1d5557 100644 --- a/jsonargparse_tests/test_typehints.py +++ b/jsonargparse_tests/test_typehints.py @@ -860,7 +860,7 @@ def test_mapping_default_mapping_proxy_type(parser): def test_ordered_dict(parser): - parser.add_argument("--odict", type=eval("OrderedDict[str, int]")) + parser.add_argument("--odict", type=OrderedDict[str, int]) cfg = parser.parse_args(['--odict={"a":1, "b":2}']) assert isinstance(cfg.odict, OrderedDict) assert OrderedDict([("a", 1), ("b", 2)]) == cfg.odict @@ -912,17 +912,15 @@ def test_union_unsupported_subtype(parser, logger): assert "Discarding unsupported subtypes" in logs.getvalue() -@pytest.mark.skipif(sys.version_info < (3, 10), reason="new union syntax introduced in python 3.10") def test_union_new_syntax_simple_types(parser): - parser.add_argument("--val", type=eval("int | None")) + parser.add_argument("--val", type=int | None) assert 123 == parser.parse_args(["--val=123"]).val assert None is parser.parse_args(["--val=null"]).val pytest.raises(ArgumentError, lambda: parser.parse_args(["--val=abc"])) -@pytest.mark.skipif(sys.version_info < (3, 10), reason="new union syntax introduced in python 3.10") def test_union_new_syntax_subclass_type(parser): - parser.add_argument("--op", type=eval("Calendar | bool")) + parser.add_argument("--op", type=Calendar | bool) help_str = get_parse_args_stdout(parser, ["--op.help=calendar.TextCalendar"]) assert "--op.firstweekday" in help_str diff --git a/jsonargparse_tests/test_typing.py b/jsonargparse_tests/test_typing.py index 08a2e90a..4662e3d2 100644 --- a/jsonargparse_tests/test_typing.py +++ b/jsonargparse_tests/test_typing.py @@ -51,7 +51,7 @@ def test_public_api(): n for n, v in vars(jsonargparse.typing).items() if n[0] != "_" - and getattr(v, "__module__", "").split(".")[0] not in {"jsonargparse", "typing", "re"} + and getattr(v, "__module__", "").split(".")[0] not in {"jsonargparse", "typing", "collections", "re"} and (inspect.isclass(v) or inspect.isfunction(v)) } assert set() == names - set(jsonargparse.typing.__all__) diff --git a/pyproject.toml b/pyproject.toml index c7cb49f7..0f6f5a45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,14 +13,13 @@ authors = [ readme = "README.rst" license = "MIT" license-files = ["LICENSE.rst"] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -84,7 +83,7 @@ omegaconf = [ "omegaconf>=2.1.1", ] typing-extensions = [ - "typing-extensions>=3.10.0.0; python_version < '3.10'", + "typing-extensions>=4.12.0; python_version < '3.11'", ] test = [ "jsonargparse[test-no-urls]", @@ -190,7 +189,7 @@ Villegas = "Villegas" [tool.tox] legacy_tox_ini = """ [tox] -envlist = py{39,310,311,312,313,314}-{all-extras,no-extras,argparse},omegaconf,pydantic-v1,without-pyyaml,without-future-annotations +envlist = py{310,311,312,313,314}-{all-extras,no-extras,argparse},omegaconf,pydantic-v1,without-pyyaml,without-future-annotations skip_missing_interpreters = true [testenv]