diff --git a/.github/workflows/manual.yaml b/.github/workflows/manual.yaml index c4951d7b..0107b6dd 100644 --- a/.github/workflows/manual.yaml +++ b/.github/workflows/manual.yaml @@ -23,6 +23,8 @@ jobs: 3.10 3.11 3.12 + 3.13 + 3.14 - run: pip install -e ".[dev]" - run: tox -- --cov=../jsonargparse --cov-append - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.gitignore b/.gitignore index 8fd2237e..1b0a064e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# Only ignore files produced by working as described in CONTRIBUTING.rst. +# For other untracked files, consider `git config status.showUntrackedFiles no`. __pycache__ *.egg-info .coverage @@ -12,15 +14,3 @@ build dist htmlcov **/tests_argparse* - -# JetBrains IDEs -.idea/ - -# some agnetic files -.claude/logs/ -.claude/state/ -tasks/ -*.local.md - -# local dev environment files -uv.lock diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1c9cdd44..9f6bc04f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -125,7 +125,6 @@ tests. tox # Run tests using tox on available python versions pytest # Run tests using pytest on the python of the environment - pytest --cov # Run tests and generate coverage report pre-commit run -a --hook-stage pre-push # Run pre-push git hooks (tests, doctests, mypy, coverage) Tests can be run in any environment without the source code. Before v4.47.0, the diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index bd12c1dc..f294ae2c 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -426,10 +426,9 @@ syntax. For example, an argument that can be ``None`` or a float in the range .. testcode:: - from typing import Optional, Union from jsonargparse.typing import PositiveInt, OpenUnitInterval - parser.add_argument("--op", type=Optional[Union[PositiveInt, OpenUnitInterval]]) + parser.add_argument("--op", type=PositiveInt | OpenUnitInterval | None) The types in :py:mod:`jsonargparse.typing` are included for convenience since they are useful in argument parsing use cases and not available in standard @@ -1373,11 +1372,8 @@ Take for example a class with its init and a method with docstrings as follows: .. testcode:: class_method - from typing import Union - - class MyClass(MyBaseClass): - def __init__(self, foo: dict[str, Union[int, list[int]]], **kwargs): + def __init__(self, foo: dict[str, int | list[int]], **kwargs): """Initializer for MyClass. Args: @@ -2453,14 +2449,14 @@ An example of a target being in a subclass is: .. testcode:: class Logger: - def __init__(self, save_dir: Optional[str] = None): + def __init__(self, save_dir: str | None = None): self.save_dir = save_dir class Trainer: def __init__( self, - save_dir: Optional[str] = None, - logger: Union[bool, Logger, list[Logger]] = False, + save_dir: str | None = None, + logger: bool | Logger | list[Logger] = False, ): self.logger = logger @@ -2967,11 +2963,10 @@ instructions to the user for guidance. Take for example the parser: #!/usr/bin/env python3 - from typing import Optional from jsonargparse import ArgumentParser parser = ArgumentParser() - parser.add_argument("--bool", type=Optional[bool]) + parser.add_argument("--bool", type=bool | None) parser.parse_args() @@ -2982,7 +2977,7 @@ matches, then the value is completed without printing guidance. For example: .. code-block:: bash $ example.py --bool - Expected type: Optional[bool]; 3/3 matched choices + Expected type: bool | None; 3/3 matched choices true false null $ example.py --bool f $ example.py --bool false diff --git a/README.rst b/README.rst index 9f1a17d4..3419218c 100644 --- a/README.rst +++ b/README.rst @@ -60,7 +60,7 @@ Powerful argparse-like low level parsers: parser = ArgumentParser() parser.add_argument("--config", action="config") # support config files - parser.add_argument("--opt", type=Union[int, Literal["off"]]) # complex arguments via type hints + parser.add_argument("--opt", type=int | Literal["off"]) # complex arguments via type hints parser.add_function_arguments(main_function, "function") # add function parameters parser.add_class_arguments(SomeClass, "class") # add class parameters ... diff --git a/jsonargparse/_cli.py b/jsonargparse/_cli.py index 25757f72..7772f2b7 100644 --- a/jsonargparse/_cli.py +++ b/jsonargparse/_cli.py @@ -2,7 +2,7 @@ import inspect from collections.abc import Callable -from typing import Any, Optional, Union +from typing import Any from ._actions import ActionConfigFile, _ActionPrintConfig, remove_actions from ._core import ArgumentParser @@ -14,9 +14,9 @@ __all__ = ["auto_cli", "auto_parser"] -ComponentType = Union[Callable, type] -DictComponentsType = dict[str, Union[ComponentType, "DictComponentsType"]] -ComponentsType = Optional[Union[ComponentType, list[ComponentType], DictComponentsType]] +ComponentType = Callable | type +DictComponentsType = dict[str, "ComponentType | DictComponentsType"] +ComponentsType = ComponentType | list[ComponentType] | DictComponentsType | None def CLI(*args, **kwargs): diff --git a/jsonargparse/_common.py b/jsonargparse/_common.py index 8029d494..b169c794 100644 --- a/jsonargparse/_common.py +++ b/jsonargparse/_common.py @@ -8,7 +8,6 @@ from contextvars import ContextVar from typing import ( # type: ignore[attr-defined] Generic, - Optional, Protocol, TypeVar, _GenericAlias, @@ -117,7 +116,7 @@ def set_parsing_settings( 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_style: "docstring_parser.DocstringStyle | None" = None, docstring_parse_attribute_docstrings: bool | None = None, parse_optionals_as_positionals: bool | None = None, add_print_completion_argument: bool | None = None, diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 3add77ac..d7699103 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -11,7 +11,6 @@ from typing import ( Any, NoReturn, - Union, ) from ._actions import ( @@ -226,7 +225,7 @@ class ArgumentGroup(ActionsContainer, argparse._ArgumentGroup): """Extension of argparse._ArgumentGroup to support additional functionalities.""" dest: str | None = None - parser: Union["ArgumentParser", ActionsContainer] | None = None + parser: "ArgumentParser | ActionsContainer | None" = None class ArgumentParser(ParserDeprecations, ActionsContainer, argparse.ArgumentParser): diff --git a/jsonargparse/_namespace.py b/jsonargparse/_namespace.py index cb344d7f..092965bd 100644 --- a/jsonargparse/_namespace.py +++ b/jsonargparse/_namespace.py @@ -3,7 +3,7 @@ import argparse from collections import OrderedDict from collections.abc import Iterator -from typing import Any, Optional, Union +from typing import Any __all__ = ["Namespace"] @@ -33,12 +33,6 @@ def is_meta_key(key: str) -> bool: return leaf_key in meta_keys -def remove_meta(cfg: Union["Namespace", dict]): - if cfg: - cfg = recreate_branches(cfg, skip_keys=meta_keys) - return cfg - - def recreate_branches(data, skip_keys=None): new_data = data if isinstance(data, (Namespace, dict)) and not isinstance(data, OrderedDict): @@ -70,7 +64,7 @@ def __init__(self, *args, **kwargs): for key, val in args[0].items() if isinstance(args[0], dict) else vars(args[0]).items(): self[key] = val - def _parse_key(self, key: str) -> tuple[str, Optional["Namespace"], str]: + def _parse_key(self, key: str) -> tuple[str, "Namespace | None", str]: """Parses a key for the nested namespace. Args: @@ -224,7 +218,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: str | None = None, only_unset: bool = False) -> "Namespace": + def update(self, value: "Namespace | Any", key: str | None = None, only_unset: bool = False) -> "Namespace": """Sets or replaces all items from the given nested namespace. Args: @@ -293,6 +287,12 @@ def dict_to_namespace(data: dict[str, Any]) -> Namespace: return expand_dict(data) +def remove_meta(cfg: Namespace | dict): + if cfg: + cfg = recreate_branches(cfg, skip_keys=meta_keys) + return cfg + + def get_non_meta_sorted_keys(namespace: Namespace) -> list[str]: keys = [k for k in namespace.keys(branches=True) if not is_meta_key(k)] keys.sort(key=lambda x: -len(split_key(x))) diff --git a/jsonargparse/_paths.py b/jsonargparse/_paths.py index 079b53d8..c1bc8573 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, Union +from typing import IO, Any from ._deprecated import PathDeprecations from ._optionals import ( @@ -113,7 +113,7 @@ class Path(PathDeprecations): def __init__( self, - path: Union[str, os.PathLike, "Path"], + path: "str | os.PathLike | Path", mode: str = "fr", cwd: str | os.PathLike | None = None, **kwargs, diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index 301fbb7b..be6a8e7d 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -28,7 +28,6 @@ MutableSequence, MutableSet, NoReturn, - Optional, Sequence, Set, Tuple, @@ -281,7 +280,7 @@ def cached_get_class_parser(*, val_class, sub_add_kwargs, skip_args, parent_pars class ActionTypeHint(Action): """Action to parse a type hint.""" - def __init__(self, typehint: Optional[type] = None, enable_path: bool = False, **kwargs): + def __init__(self, typehint: type | None = None, enable_path: bool = False, **kwargs): """Initializer for ActionTypeHint instance. Args: @@ -763,7 +762,7 @@ def is_list_pathlike(typehint) -> bool: return False -def raise_unexpected_value(message: str, val: Any = inspect._empty, exception: Optional[Exception] = None) -> NoReturn: +def raise_unexpected_value(message: str, val: Any = inspect._empty, exception: Exception | None = None) -> NoReturn: if val is not inspect._empty: message += f". Got value: {val}" raise ValueError(message) from exception @@ -1441,7 +1440,7 @@ def add_subclasses(cl): return subclass_list -def resolve_class_path_by_name(cls: Union[type, tuple[type]], name: str) -> str: +def resolve_class_path_by_name(cls: type | tuple[type], name: str) -> str: class_path = name if "." not in class_path: if isinstance(cls, tuple): diff --git a/jsonargparse/typing.py b/jsonargparse/typing.py index 94bdcf21..1cd7a1af 100644 --- a/jsonargparse/typing.py +++ b/jsonargparse/typing.py @@ -7,7 +7,7 @@ import re import sys from collections.abc import Callable -from typing import Any, TypeAlias, Union, get_type_hints +from typing import Any, TypeAlias, get_type_hints from ._common import ClassType, is_final_class, is_subclass, path_dump_preserve_relative from ._namespace import Namespace @@ -56,7 +56,7 @@ if sys.version_info >= (3, 12): from typing import TypeAliasType as TypeAliasType - _TypeClass = Union[type, TypeAliasType] + _TypeClass = type | TypeAliasType else: _TypeClass = type diff --git a/sphinx/conf.py b/sphinx/conf.py index 1e5d9f17..a01766a0 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -82,7 +82,7 @@ def check_output(self, want, got, optionflags): from calendar import Calendar from dataclasses import dataclass from io import StringIO -from typing import Callable, Iterable, List, Protocol +from typing import Callable, Iterable, Protocol import jsonargparse_tests from jsonargparse import * from jsonargparse import _common