././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1784696114.2778919 jsonargparse-4.50.0/0000755000175100017510000000000015230046462013746 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/LICENSE.rst0000644000175100017510000000213315230046455015563 0ustar00runnerrunnerThe MIT License (MIT) Copyright (c) 2019-present, Mauricio Villegas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1784696114.276722 jsonargparse-4.50.0/PKG-INFO0000644000175100017510000003024615230046462015050 0ustar00runnerrunnerMetadata-Version: 2.4 Name: jsonargparse Version: 4.50.0 Summary: Minimal effort CLIs derived from type hints and parse from command line, config files and environment variables. Author-email: Mauricio Villegas License-Expression: MIT Project-URL: Documentation-stable, https://jsonargparse.readthedocs.io/en/stable/ Project-URL: Documentation-latest, https://jsonargparse.readthedocs.io/en/latest/ Project-URL: Changes, https://jsonargparse.readthedocs.io/en/stable/changelog.html Project-URL: GitHub, https://github.com/mauvilsa/jsonargparse Project-URL: PyPI, https://pypi.org/project/jsonargparse Project-URL: SonarCloud, https://sonarcloud.io/dashboard?id=mauvilsa_jsonargparse Project-URL: Codecov, https://codecov.io/gh/mauvilsa/jsonargparse Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Intended Audience :: Developers Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE.rst Requires-Dist: PyYAML>=3.13 Provides-Extra: all Requires-Dist: jsonargparse[signatures]; extra == "all" Requires-Dist: jsonargparse[jsonschema]; extra == "all" Requires-Dist: jsonargparse[jsonnet]; extra == "all" Requires-Dist: jsonargparse[toml]; extra == "all" Requires-Dist: jsonargparse[urls]; extra == "all" Requires-Dist: jsonargparse[fsspec]; extra == "all" Requires-Dist: jsonargparse[ruamel]; extra == "all" Requires-Dist: jsonargparse[omegaconf]; extra == "all" Requires-Dist: jsonargparse[typing-extensions]; extra == "all" Provides-Extra: signatures Requires-Dist: jsonargparse[typing-extensions]; extra == "signatures" Requires-Dist: docstring-parser>=0.17; extra == "signatures" Requires-Dist: typeshed-client>=2.8.2; extra == "signatures" Provides-Extra: jsonschema Requires-Dist: jsonschema>=3.2.0; extra == "jsonschema" Provides-Extra: jsonnet Requires-Dist: jsonnet>=0.21.0; extra == "jsonnet" Provides-Extra: toml Requires-Dist: toml>=0.10.2; extra == "toml" Provides-Extra: urls Requires-Dist: requests>=2.18.4; extra == "urls" Provides-Extra: fsspec Requires-Dist: fsspec>=0.8.4; extra == "fsspec" Provides-Extra: shtab Requires-Dist: shtab>=1.7.1; extra == "shtab" Provides-Extra: argcomplete Requires-Dist: argcomplete>=3.5.1; extra == "argcomplete" Provides-Extra: ruamel Requires-Dist: ruamel.yaml>=0.18.15; extra == "ruamel" Provides-Extra: ruyaml Requires-Dist: jsonargparse[ruamel]; extra == "ruyaml" Provides-Extra: omegaconf Requires-Dist: omegaconf>=2.1.1; extra == "omegaconf" Provides-Extra: typing-extensions Requires-Dist: typing-extensions>=4.12.0; python_version < "3.11" and extra == "typing-extensions" Provides-Extra: test Requires-Dist: jsonargparse[test-no-urls]; extra == "test" Requires-Dist: jsonargparse[shtab]; extra == "test" Requires-Dist: jsonargparse[argcomplete]; extra == "test" Requires-Dist: types-PyYAML>=6.0.11; extra == "test" Requires-Dist: types-requests<2.34,>=2.28.9; extra == "test" Requires-Dist: requests<2.34; extra == "test" Requires-Dist: responses>=0.12.0; extra == "test" Requires-Dist: pydantic>=2.3.0; extra == "test" Requires-Dist: attrs>=22.2.0; extra == "test" Provides-Extra: test-no-urls Requires-Dist: pytest>=6.2.5; extra == "test-no-urls" Requires-Dist: pytest-subtests>=0.8.0; extra == "test-no-urls" Provides-Extra: coverage Requires-Dist: jsonargparse[test-no-urls]; extra == "coverage" Requires-Dist: pytest-cov>=4.0.0; extra == "coverage" Provides-Extra: dev Requires-Dist: jsonargparse[test]; extra == "dev" Requires-Dist: jsonargparse[coverage]; extra == "dev" Requires-Dist: jsonargparse[doc]; extra == "dev" Requires-Dist: pre-commit>=2.19.0; extra == "dev" Requires-Dist: tox>=4.0.0; extra == "dev" Requires-Dist: tox-uv>=1.0.0; extra == "dev" Requires-Dist: build>=0.10.0; extra == "dev" Provides-Extra: doc Requires-Dist: Sphinx>=1.7.9; extra == "doc" Requires-Dist: sphinx-rtd-theme>=1.2.2; extra == "doc" Requires-Dist: autodocsumm>=0.1.10; extra == "doc" Requires-Dist: sphinx-autodoc-typehints>=1.19.5; extra == "doc" Provides-Extra: maintainer Requires-Dist: bump2version>=0.5.11; extra == "maintainer" Requires-Dist: twine>=4.0.2; extra == "maintainer" Dynamic: license-file .. image:: https://readthedocs.org/projects/jsonargparse/badge/?version=stable :target: https://readthedocs.org/projects/jsonargparse/ .. image:: https://github.com/mauvilsa/jsonargparse/actions/workflows/tests.yaml/badge.svg :target: https://github.com/mauvilsa/jsonargparse/actions/workflows/tests.yaml .. image:: https://codecov.io/gh/mauvilsa/jsonargparse/branch/main/graph/badge.svg :target: https://codecov.io/gh/mauvilsa/jsonargparse .. image:: https://sonarcloud.io/api/project_badges/measure?project=mauvilsa_jsonargparse&metric=alert_status :target: https://sonarcloud.io/dashboard?id=mauvilsa_jsonargparse .. image:: https://badge.fury.io/py/jsonargparse.svg :target: https://badge.fury.io/py/jsonargparse jsonargparse ============ Docs: https://jsonargparse.readthedocs.io/ | Source: https://github.com/mauvilsa/jsonargparse/ ``jsonargparse`` is a library for creating command-line interfaces (CLIs) and making Python apps easily configurable. It is a well-maintained project with frequent releases, adhering to high standards of development: semantic versioning, deprecation periods, changelog, automated testing, and full test coverage. Although ``jsonargparse`` might not be widely recognized yet, it already boasts a `substantial user base `__. Most notably, it serves as the framework behind pytorch-lightning's `LightningCLI `__. Teaser examples --------------- CLI with minimal boilerplate: .. code-block:: python from jsonargparse import auto_cli def main_function(...): # your main parameters with type hints here ... # your main code here if __name__ == "__main__": auto_cli(main_function) # parses arguments and runs main_function Minimal boilerplate but manually parsing: .. code-block:: python from jsonargparse import auto_parser parser = auto_parser(main_function) cfg = parser.parse_args() ... Powerful argparse-like low level parsers: .. code-block:: python from jsonargparse import ArgumentParser parser = ArgumentParser() parser.add_argument("--config", action="config") # support config files 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 ... cfg = parser.parse_args() init = parser.instantiate(cfg) ... Features -------- ``jsonargparse`` is user-friendly and encourages the development of **clean, high-quality code**. It encompasses numerous powerful features, some unique to ``jsonargparse``, while also combining advantages found in similar packages: - **Automatic** creation of CLIs, like `Fire `__, `Typer `__, `Clize `__ and `Tyro `__. - Use **type hints** for argument validation, like `Typer `__, `Tap `__ and `Tyro `__. - Use of **docstrings** for automatic generation of help, like `Tap `__, `Tyro `__ and `SimpleParsing `__. - Parse from **configuration files** and **environment variables**, like `OmegaConf `__, `dynaconf `__, `confuse `__ and `configargparse `__. - **Dataclasses** support, like `SimpleParsing `__ and `Tyro `__. Other notable features include: - **Extensive type hint support:** nested types (union, optional), containers (list, dict, etc.), protocols, user-defined generics, restricted types (regex, numbers), paths, URLs, types from stubs (``*.pyi``), future annotations (PEP `563 `__), and backports (PEP `604 `__). - **Keyword arguments introspection:** resolving of parameters used via ``**kwargs``. - **Dependency injection:** support types that expect a class instance and callables that return a class instance. - **Structured configs:** parse config files with more understandable non-flat hierarchies. - **Config file formats:** `json `__, `yaml `__, `toml `__, `jsonnet `__ and extensible to more formats. - **Relative paths:** within config files and parsing of config paths referenced inside other configs. - **Argument linking:** directing parsed values to multiple parameters, preventing unnecessary interpolation in configs. - **Variable interpolation:** powered by `OmegaConf `__. - **Tab completion:** powered by `shtab `__ or `argcomplete `__. Design principles ----------------- - **Non-intrusive/decoupled:** There is no requirement for unrelated modifications throughout a codebase, maintaining the `separation of concerns principle `__. In simpler terms, changes should make sense even without the CLI. No need to inherit from a special class, add decorators, or use CLI-specific type hints. - **Minimal boilerplate:** A recommended practice is to write code with function/class parameters having meaningful names, accurate type hints, and descriptive docstrings. Reuse these wherever they appear to automatically generate the CLI, following the `don't repeat yourself principle `__. A notable advantage is that when parameters are added or types changed, the CLI will remain synchronized, avoiding the need to update the CLI's implementation. - **Dependency injection:** Using as type hint a class or a callable that instantiates a class, a practice known as `dependency injection `__, is a sound design pattern for developing loosely coupled and highly configurable software. Such type hints should be supported with minimal restrictions. .. _installation: Installation ============ You can install using `pip `__ as: .. code-block:: bash pip install jsonargparse By default, the only dependency installed with ``jsonargparse`` is `PyYAML `__. However, several optional features can be enabled by specifying one or more of the following extras (optional dependencies): ``signatures``, ``jsonschema``, ``jsonnet``, ``urls``, ``fsspec``, ``toml``, ``ruamel``, ``omegaconf``, ``shtab``, and ``argcomplete``. Additionally, the ``all`` extras can be used to enable all optional features (excluding tab completion ones). To install ``jsonargparse`` with extras, use the following syntax: .. code-block:: bash pip install "jsonargparse[signatures,urls]" # Enable signatures and URLs features pip install "jsonargparse[all]" # Enable all optional features To install the latest development version, use the following command: .. code-block:: bash pip install "jsonargparse[signatures] @ https://github.com/mauvilsa/jsonargparse/zipball/main" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/README.rst0000644000175100017510000001713015230046455015441 0ustar00runnerrunner.. image:: https://readthedocs.org/projects/jsonargparse/badge/?version=stable :target: https://readthedocs.org/projects/jsonargparse/ .. image:: https://github.com/mauvilsa/jsonargparse/actions/workflows/tests.yaml/badge.svg :target: https://github.com/mauvilsa/jsonargparse/actions/workflows/tests.yaml .. image:: https://codecov.io/gh/mauvilsa/jsonargparse/branch/main/graph/badge.svg :target: https://codecov.io/gh/mauvilsa/jsonargparse .. image:: https://sonarcloud.io/api/project_badges/measure?project=mauvilsa_jsonargparse&metric=alert_status :target: https://sonarcloud.io/dashboard?id=mauvilsa_jsonargparse .. image:: https://badge.fury.io/py/jsonargparse.svg :target: https://badge.fury.io/py/jsonargparse jsonargparse ============ Docs: https://jsonargparse.readthedocs.io/ | Source: https://github.com/mauvilsa/jsonargparse/ ``jsonargparse`` is a library for creating command-line interfaces (CLIs) and making Python apps easily configurable. It is a well-maintained project with frequent releases, adhering to high standards of development: semantic versioning, deprecation periods, changelog, automated testing, and full test coverage. Although ``jsonargparse`` might not be widely recognized yet, it already boasts a `substantial user base `__. Most notably, it serves as the framework behind pytorch-lightning's `LightningCLI `__. Teaser examples --------------- CLI with minimal boilerplate: .. code-block:: python from jsonargparse import auto_cli def main_function(...): # your main parameters with type hints here ... # your main code here if __name__ == "__main__": auto_cli(main_function) # parses arguments and runs main_function Minimal boilerplate but manually parsing: .. code-block:: python from jsonargparse import auto_parser parser = auto_parser(main_function) cfg = parser.parse_args() ... Powerful argparse-like low level parsers: .. code-block:: python from jsonargparse import ArgumentParser parser = ArgumentParser() parser.add_argument("--config", action="config") # support config files 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 ... cfg = parser.parse_args() init = parser.instantiate(cfg) ... Features -------- ``jsonargparse`` is user-friendly and encourages the development of **clean, high-quality code**. It encompasses numerous powerful features, some unique to ``jsonargparse``, while also combining advantages found in similar packages: - **Automatic** creation of CLIs, like `Fire `__, `Typer `__, `Clize `__ and `Tyro `__. - Use **type hints** for argument validation, like `Typer `__, `Tap `__ and `Tyro `__. - Use of **docstrings** for automatic generation of help, like `Tap `__, `Tyro `__ and `SimpleParsing `__. - Parse from **configuration files** and **environment variables**, like `OmegaConf `__, `dynaconf `__, `confuse `__ and `configargparse `__. - **Dataclasses** support, like `SimpleParsing `__ and `Tyro `__. Other notable features include: - **Extensive type hint support:** nested types (union, optional), containers (list, dict, etc.), protocols, user-defined generics, restricted types (regex, numbers), paths, URLs, types from stubs (``*.pyi``), future annotations (PEP `563 `__), and backports (PEP `604 `__). - **Keyword arguments introspection:** resolving of parameters used via ``**kwargs``. - **Dependency injection:** support types that expect a class instance and callables that return a class instance. - **Structured configs:** parse config files with more understandable non-flat hierarchies. - **Config file formats:** `json `__, `yaml `__, `toml `__, `jsonnet `__ and extensible to more formats. - **Relative paths:** within config files and parsing of config paths referenced inside other configs. - **Argument linking:** directing parsed values to multiple parameters, preventing unnecessary interpolation in configs. - **Variable interpolation:** powered by `OmegaConf `__. - **Tab completion:** powered by `shtab `__ or `argcomplete `__. Design principles ----------------- - **Non-intrusive/decoupled:** There is no requirement for unrelated modifications throughout a codebase, maintaining the `separation of concerns principle `__. In simpler terms, changes should make sense even without the CLI. No need to inherit from a special class, add decorators, or use CLI-specific type hints. - **Minimal boilerplate:** A recommended practice is to write code with function/class parameters having meaningful names, accurate type hints, and descriptive docstrings. Reuse these wherever they appear to automatically generate the CLI, following the `don't repeat yourself principle `__. A notable advantage is that when parameters are added or types changed, the CLI will remain synchronized, avoiding the need to update the CLI's implementation. - **Dependency injection:** Using as type hint a class or a callable that instantiates a class, a practice known as `dependency injection `__, is a sound design pattern for developing loosely coupled and highly configurable software. Such type hints should be supported with minimal restrictions. .. _installation: Installation ============ You can install using `pip `__ as: .. code-block:: bash pip install jsonargparse By default, the only dependency installed with ``jsonargparse`` is `PyYAML `__. However, several optional features can be enabled by specifying one or more of the following extras (optional dependencies): ``signatures``, ``jsonschema``, ``jsonnet``, ``urls``, ``fsspec``, ``toml``, ``ruamel``, ``omegaconf``, ``shtab``, and ``argcomplete``. Additionally, the ``all`` extras can be used to enable all optional features (excluding tab completion ones). To install ``jsonargparse`` with extras, use the following syntax: .. code-block:: bash pip install "jsonargparse[signatures,urls]" # Enable signatures and URLs features pip install "jsonargparse[all]" # Enable all optional features To install the latest development version, use the following command: .. code-block:: bash pip install "jsonargparse[signatures] @ https://github.com/mauvilsa/jsonargparse/zipball/main" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1784696114.272569 jsonargparse-4.50.0/jsonargparse/0000755000175100017510000000000015230046462016444 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/__init__.py0000644000175100017510000000324615230046455020564 0ustar00runnerrunnerfrom argparse import ( ONE_OR_MORE, OPTIONAL, PARSER, REMAINDER, SUPPRESS, ZERO_OR_MORE, ArgumentError, ) from ._actions import * # noqa: F403 from ._actions import ActionConfigFile # noqa: F401 from ._cli import * # noqa: F403 from ._cli import CLI # noqa: F401 from ._common import * # noqa: F403 from ._core import * # noqa: F403 from ._deprecated import * # noqa: F403 from ._formatters import * # noqa: F403 from ._from_config import * # noqa: F403 from ._instantiation import * # noqa: F403 from ._jsonnet import * # noqa: F403 from ._jsonschema import * # noqa: F403 from ._loaders_dumpers import * # noqa: F403 from ._namespace import * # noqa: F403 from ._paths import Path # noqa: F401 from ._subcommands import * # noqa: F403 from ._util import * # noqa: F403 from .typing import class_from_function, lazy_instance # noqa: F401 __all__ = [ "ArgumentError", "OPTIONAL", "REMAINDER", "SUPPRESS", "PARSER", "ONE_OR_MORE", "ZERO_OR_MORE", ] from . import ( _actions, _cli, _common, _core, _deprecated, _formatters, _from_config, _instantiation, _jsonnet, _jsonschema, _loaders_dumpers, _namespace, _subcommands, _util, ) __all__ += _cli.__all__ __all__ += _core.__all__ __all__ += _from_config.__all__ __all__ += _subcommands.__all__ __all__ += _jsonschema.__all__ __all__ += _jsonnet.__all__ __all__ += _actions.__all__ __all__ += _namespace.__all__ __all__ += _formatters.__all__ __all__ += _common.__all__ __all__ += _instantiation.__all__ __all__ += _loaders_dumpers.__all__ __all__ += _util.__all__ __all__ += _deprecated.__all__ __version__ = "4.50.0" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_actions.py0000644000175100017510000005273015230046455020626 0ustar00runnerrunner"""Collection of useful actions to define arguments.""" import re import sys from argparse import SUPPRESS, _HelpAction, _VersionAction from argparse import Action as ArgparseAction from contextlib import contextmanager from contextvars import ContextVar from typing import Any from ._common import Action, NonParsingAction, get_parsing_setting, is_subclass, is_subclasses_disabled, parser_context from ._loaders_dumpers import get_loader_exceptions, load_value from ._namespace import Namespace from ._optionals import _get_config_read_mode, ruamel_support from ._paths import change_to_path_dir from ._type_checking import ArgumentParser from ._util import ( Path, argument_error, default_config_option_help, get_import_path, import_object, indent_text, iter_to_set_str, load_config_path_context, merge_config, parse_value_or_config, ) __all__ = [ "ActionFail", "ActionYesNo", "ActionParser", ] def _is_action_value_list(action: ArgparseAction) -> bool: """Checks whether an action produces a list value. Args: action: An argparse action to check. Returns: bool: True if produces list otherwise False. """ if action.nargs in {"*", "+"} or (isinstance(action.nargs, int) and action.nargs != 0): return True return False def remove_actions(parser, types): def remove(actions): rm_actions = [a for a in actions if isinstance(a, types)] for action in rm_actions: actions.remove(action) remove(parser._actions) for action_group in parser._action_groups: remove(action_group._group_actions) non_parsing_actions = (_HelpAction, _VersionAction, NonParsingAction) def filter_non_parsing_actions(actions): if isinstance(actions, list): return [a for a in actions if not isinstance(a, non_parsing_actions)] return {k: a for k, a in actions.items() if not isinstance(a, non_parsing_actions)} class ActionConfigFile(Action): """Action to indicate that an argument is a configuration file or a configuration string.""" def __init__(self, **kwargs): """Initializer for ActionConfigFile instance.""" if "default" in kwargs: self.set_default_error() opt_name = kwargs["option_strings"] opt_name = opt_name[0] if len(opt_name) == 1 else [x for x in opt_name if x[0:2] == "--"][0] if "." in opt_name: raise ValueError("ActionConfigFile must be a top level option.") if "help" not in kwargs: kwargs["help"] = "Path to a configuration file." super().__init__(**kwargs) def __call__(self, parser, cfg, values, option_string=None): """Parses the given configuration and adds all the corresponding keys to the namespace. Raises: TypeError: If there are problems parsing the configuration. """ self.apply_config(parser, cfg, self.dest, values) @staticmethod def set_default_error(): raise ValueError("ActionConfigFile does not accept a default, use default_config_files.") @staticmethod def _ensure_single_config_argument(container, action): if (action == "config" or is_subclass(action, ActionConfigFile)) and any( isinstance(a, ActionConfigFile) for a in container._actions ): raise ValueError("A parser is only allowed to have a single 'config' argument.") @staticmethod def _add_print_config_argument(container, action): if isinstance(action, ActionConfigFile) and getattr(container, "_print_config", None) is not None: if "%s" in container._print_config: container._print_config = container._print_config % action.dest assert container._print_config.startswith("--") container.add_argument(container._print_config, action=_ActionPrintConfig) @staticmethod def apply_config(parser, cfg, dest, value) -> None: from ._link_arguments import skip_apply_links 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: Path | None = Path(value, mode=_get_config_read_mode()) except TypeError as ex_path: try: if isinstance(load_value(value), str): raise ex_path cfg_path = None cfg_file = parser.parse_string(value, **kwargs) except (TypeError, ValueError) + get_loader_exceptions() as ex_str: raise TypeError(f'Parser key "{dest}": {ex_str}') from ex_str else: cfg_file = parser.parse_path(value, **kwargs) cfg_merged = merge_config(parser, cfg_file, cfg) cfg.__dict__.update(cfg_merged.__dict__) if cfg.get(dest) is get_parsing_setting("unset_sentinel"): cfg[dest] = [] cfg[dest].append(cfg_path) def completer(self, prefix, **kwargs): from ._completions import get_files_completer files_completer = get_files_completer() return sorted(files_completer(prefix, **kwargs)) previous_config: ContextVar = ContextVar("previous_config", default=None) @contextmanager def previous_config_context(cfg): token = previous_config.set(cfg) try: yield finally: previous_config.reset(token) print_config_skip: ContextVar = ContextVar("print_config_skip", default=False) class _ActionPrintConfig(NonParsingAction): def __init__( self, option_strings, dest=SUPPRESS, default=SUPPRESS, ): super().__init__( option_strings=option_strings, dest=dest, default=default, nargs=1, metavar="\b[=flags]", help=( "Print the configuration after applying all other arguments and exit. The optional " "flags customizes the output and are one or more keywords separated by comma. The " "supported flags are:%s skip_default, skip_unset." ) % (" comments," if ruamel_support else ""), ) def __call__(self, parser, namespace, value, option_string=None): from ._deprecated import deprecated_skip_null, deprecated_valid_flags kwargs = {"subparser": parser, "key": None, "skip_unset": False, "skip_validation": False} valid_flags = {"": None, "skip_default": "skip_default", "skip_unset": "skip_unset"} | deprecated_valid_flags if ruamel_support: valid_flags["comments"] = "with_comments" flags = value[0].split(",") invalid_flags = [f for f in flags if f not in valid_flags] if len(invalid_flags) > 0: raise argument_error(f'Invalid option "{invalid_flags[0]}" for {option_string}') for flag in [f for f in flags if f != ""]: mapped = valid_flags[flag] if deprecated_skip_null(flag): kwargs["skip_unset"] = True else: kwargs[mapped] = True while hasattr(parser, "parent_parser"): kwargs["key"] = parser.subcommand if kwargs["key"] is None else parser.subcommand + "." + kwargs["key"] parser = parser.parent_parser parser.print_config = kwargs @staticmethod @contextmanager def skip_print_config(): t = print_config_skip.set(True) try: yield finally: print_config_skip.reset(t) @staticmethod def print_config_if_requested(parser, cfg): if hasattr(parser, "print_config") and not print_config_skip.get(): key = parser.print_config.pop("key") subparser = parser.print_config.pop("subparser") if key is not None: cfg = cfg[key] with parser_context(lenient_check=True): sys.stdout.write(subparser.dump(cfg, **parser.print_config)) delattr(parser, "print_config") parser.exit() @staticmethod def is_print_config_requested(parser): while parser: if hasattr(parser, "print_config"): return True parser = getattr(parser, "parent_parser", None) return False class _ActionConfigLoad(Action): def __init__(self, basetype: type | None = None, **kwargs): if len(kwargs) == 0: self._basetype = basetype else: self.basetype = kwargs.pop("_basetype", None) kwargs["metavar"] = "CONFIG" kwargs["help"] = default_config_option_help kwargs["default"] = SUPPRESS super().__init__(**kwargs) def __call__(self, *args, **kwargs): if len(args) == 0: kwargs["_basetype"] = self._basetype return _ActionConfigLoad(**kwargs) parser, namespace, value = args[:3] loaded_value = self._load_config(value, parser) if isinstance(namespace.get(self.dest), Namespace): loaded_value = merge_config( parser, Namespace({self.dest: loaded_value}), Namespace({self.dest: namespace[self.dest]}) )[self.dest] namespace[self.dest] = loaded_value return None def _load_config(self, value, parser): try: cfg, cfg_path = parse_value_or_config(value) if not isinstance(cfg, dict): raise TypeError(f'Parser key "{self.dest}": Unable to load config "{value}"') with load_config_path_context(cfg_path), change_to_path_dir(cfg_path): cfg = parser._apply_actions(cfg, parent_key=self.dest) return cfg except (TypeError,) + get_loader_exceptions() as ex: str_ex = indent_text(f"- {ex}") raise TypeError(f'Parser key "{self.dest}":\nUnable to load config {value!r}\n{str_ex}') from ex def check_type(self, value, parser): return self._load_config(value, parser) class _ActionHelpClassPath(NonParsingAction): sub_add_kwargs: dict[str, Any] = {} @classmethod 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) def __init__(self, typehint=None, **kwargs): if typehint is not None: self._typehint = typehint else: self.update_init_kwargs(kwargs) super().__init__(**kwargs) def update_init_kwargs(self, kwargs): from ._typehints import is_protocol self._typehint = kwargs.pop("_typehint") self._help_types = self.get_help_types(self._typehint) assert self._help_types and all(isinstance(b, type) for b in self._help_types) self._single_class = len(self._help_types) == 1 and is_subclasses_disabled(self._help_types[0]) self._basename = iter_to_set_str(t.__name__ for t in self._help_types) if len(self._help_types) == 1: kwargs["nargs"] = 0 if self._single_class else "?" if self._single_class: msg = "" else: kwargs["metavar"] = "CLASS_PATH_OR_NAME" self._kind = "subclass of" if any(is_protocol(b) for b in self._help_types): self._kind = "subclass or implementer of protocol" msg = f"the given {self._kind} " kwargs["default"] = SUPPRESS kwargs["help"] = f"Show the help for {msg}{self._basename} and exit." def __call__(self, *args, **kwargs): if len(args) == 0: kwargs["_typehint"] = self._typehint return type(self)(**kwargs) return self.print_help(args) def print_help(self, call_args): from ._typehints import ( adapt_partial_callable_class, implements_protocol, resolve_class_path_by_name, ) parser, _, value, option_string = call_args try: if self.nargs == 0 or (self.nargs == "?" and value is None): val_class = self._help_types[0] else: val_class = import_object(resolve_class_path_by_name(self._help_types, value)) except Exception as ex: raise TypeError(f"{option_string}: {ex}") from ex if not any(is_subclass(val_class, b) or implements_protocol(val_class, b) for b in self._help_types): raise TypeError(f'{option_string}: Class "{value}" is not a {self._kind} {self._basename}') dest = re.sub("\\.help$", "", self.dest) subparser = type(parser)(description=f"Help for {option_string}={get_import_path(val_class)}") val = Namespace(class_path=get_import_path(val_class)) sub_add_kwargs = dict(self.sub_add_kwargs) if "skip" in sub_add_kwargs: sub_add_kwargs["skip"] = set(sub_add_kwargs["skip"]) _, partial_skip_args = adapt_partial_callable_class(self._typehint, val) if partial_skip_args: sub_add_kwargs.setdefault("skip", set()).update(partial_skip_args) subparser.add_class_arguments(val_class, dest, **sub_add_kwargs) subparser._inner_parser = True remove_actions(subparser, (_HelpAction, _ActionPrintConfig, _ActionConfigLoad)) args = self.get_args_after_opt(parser.args) if args: subparser.parse_args(args) raise argument_error(f"Expected a nested --*.help option, got: {args}.") else: subparser.print_help() parser.exit() def get_args_after_opt(self, args): opt_str = self.option_strings[0] for num, arg in enumerate(args): parts = arg.split("=", 1) if parts[0] == opt_str: if len(parts) == 1: num += 1 break return args[num + 1 :] class ActionFail(Action): """Action that always fails parsing with a given error.""" def __init__(self, message: str = "option unavailable", **kwargs): """Initializer for ActionFail instance. Args: message: Text for the error to show. Use ``%(option)s``/``%(value)s`` to include the option and/or value. """ if len(kwargs) == 0: self._message = message else: self._message = kwargs.pop("_message") kwargs["default"] = SUPPRESS kwargs["required"] = False if kwargs["option_strings"] == []: kwargs["nargs"] = "?" super().__init__(**kwargs) def __call__(self, *args, **kwargs): """Always fails with given message.""" if len(args) == 0: kwargs["_message"] = self._message return ActionFail(**kwargs) parser, _, value, option = args parser.error(self._message % {"value": value, "option": option}) class ActionYesNo(Action): """Paired options ``--[yes_prefix]opt``, ``--[no_prefix]opt`` to set ``True`` or ``False`` respectively.""" def __init__(self, yes_prefix: str = "", no_prefix: str = "no_", **kwargs): """Initializer for ActionYesNo instance. Args: yes_prefix: Prefix for yes option. no_prefix: Prefix for no option. Raises: ValueError: If a parameter is invalid. """ if len(kwargs) == 0: self._yes_prefix = yes_prefix self._no_prefix = no_prefix else: self._yes_prefix = kwargs.pop("_yes_prefix") if "_yes_prefix" in kwargs else "" self._no_prefix = kwargs.pop("_no_prefix") if "_no_prefix" in kwargs else "no_" if len(kwargs["option_strings"]) == 0: raise ValueError(f"{type(self).__name__} not intended for positional arguments ({kwargs['dest']}).") opt_name = kwargs["option_strings"][0] if not opt_name.startswith("--" + self._yes_prefix): raise ValueError(f'Expected option string to start with "--{self._yes_prefix}".') if self._no_prefix is not None: kwargs["option_strings"] += [re.sub("^--" + self._yes_prefix, "--" + self._no_prefix, opt_name)] if self._no_prefix is None and "nargs" in kwargs and kwargs["nargs"] != 1: raise ValueError("ActionYesNo with no_prefix=None only supports nargs=1.") if "nargs" in kwargs and kwargs["nargs"] in {"?", 1}: kwargs["metavar"] = "{true,yes,false,no}" if kwargs["nargs"] == 1: kwargs["nargs"] = None else: kwargs["nargs"] = 0 kwargs["metavar"] = None if "default" not in kwargs: kwargs["default"] = False kwargs["type"] = ActionYesNo._boolean_type super().__init__(**kwargs) def __call__(self, *args, **kwargs): """Sets the corresponding key to True or False depending on the option string used.""" if len(args) == 0: kwargs["_yes_prefix"] = self._yes_prefix kwargs["_no_prefix"] = self._no_prefix return ActionYesNo(**kwargs) value = args[2] if isinstance(args[2], bool) else True if self._no_prefix is not None and args[3].startswith("--" + self._no_prefix): setattr(args[1], self.dest, not value) else: setattr(args[1], self.dest, value) return None def _add_dest_prefix(self, prefix): self.dest = prefix + "." + self.dest self.option_strings[0] = re.sub( "^--" + self._yes_prefix, "--" + self._yes_prefix + prefix + ".", self.option_strings[0] ) if self._no_prefix is not None: self.option_strings[-1] = re.sub( "^--" + self._no_prefix, "--" + self._no_prefix + prefix + ".", self.option_strings[-1] ) def _check_type(self, value): return ActionYesNo._boolean_type(value) @staticmethod def _boolean_type(x): if isinstance(x, str) and x.lower() in {"true", "yes", "false", "no"}: x = True if x.lower() in {"true", "yes"} else False elif not isinstance(x, bool): raise TypeError(f"Value not boolean: {x}.") return x def completer(self, **kwargs): """Used by argcomplete to support tab completion of arguments.""" return ["true", "false", "yes", "no"] class ActionParser: """Action to parse option with a given parser optionally loading from file if string value.""" def __init__( self, parser: ArgumentParser, ): """Initializer for ActionParser instance. Args: parser: A parser to parse the option with. Raises: ValueError: If the parser parameter is invalid. """ self._parser = parser if not isinstance(self._parser, import_object("jsonargparse.ArgumentParser")): raise ValueError("Expected parser keyword argument to be an ArgumentParser.") @staticmethod def _is_valid_action_parser(parser, action) -> bool: if not isinstance(action, ActionParser): return False if action._parser == parser: raise ValueError("Parser cannot be added as a subparser of itself.") return True @staticmethod def _move_parser_actions(parser, args, kwargs): subparser = kwargs.pop("action")._parser title = kwargs.pop("title", kwargs.pop("help", None)) description = kwargs.pop("description", subparser.description) if len(kwargs) > 0: raise ValueError(f"ActionParser does not accept the following parameters: {set(kwargs)}") if not (len(args) == 1 and args[0].startswith("--")): raise ValueError(f"ActionParser only accepts a single optional key but got {args}") prefix = args[0][2:] def add_prefix(key): return re.sub("^--", "--" + prefix + ".", key) option_string_actions = {} for key, action in filter_non_parsing_actions(subparser._option_string_actions).items(): option_string_actions[add_prefix(key)] = action isect = set(option_string_actions).intersection(set(parser._option_string_actions)) if len(isect) > 0: raise ValueError(f"ActionParser conflicting keys: {isect}") actions = [] dest = prefix.replace("-", "_") for action in filter_non_parsing_actions(subparser._actions): if isinstance(action, ActionYesNo): action._add_dest_prefix(prefix) else: action.dest = dest + "." + action.dest action.option_strings = [add_prefix(key) for key in action.option_strings] actions.append(action) base_action_group = subparser._action_groups[1] base_action_group.title = title if description is not None: base_action_group.description = description base_action_group.parser = parser base_action_group._actions = filter_non_parsing_actions(base_action_group._actions) base_action_group._group_actions = filter_non_parsing_actions(base_action_group._group_actions) extra_action_groups = subparser._action_groups[2:] for group in extra_action_groups: if group.dest is not None: group.dest = dest + "." + group.dest parser.add_argument(args[0], action=_ActionConfigLoad) parser._option_string_actions.update(option_string_actions) parser._actions.extend(actions) parser._action_groups.extend([base_action_group] + extra_action_groups) subparser._option_string_actions = {} subparser._actions = [] subparser._action_groups = [] return base_action_group ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_cli.py0000644000175100017510000002215515230046455017733 0ustar00runnerrunner"""Simple creation of command line interfaces.""" import inspect from collections.abc import Callable from typing import Any from ._actions import ActionConfigFile, _ActionPrintConfig, remove_actions from ._core import ArgumentParser from ._deprecated import deprecation_warning_cli_return_parser, get_implicit_auto_cli_components from ._namespace import Namespace, dict_to_namespace from ._optionals import get_doc_short_description from ._util import capture_parser, default_config_option_help __all__ = ["auto_cli", "auto_parser"] ComponentType = Callable | type DictComponentsType = dict[str, "ComponentType | DictComponentsType"] ComponentsType = ComponentType | list[ComponentType] | DictComponentsType | None def CLI(*args, **kwargs): """Alias of :func:`auto_cli`.""" return auto_cli(*args, _stacklevel=3, **kwargs) def auto_cli( components: ComponentsType = None, args: list[str] | None = None, config_help: str = default_config_option_help, set_defaults: dict[str, Any] | None = None, as_positional: bool = True, return_instance: bool = False, fail_untyped: bool = True, parser_class: type[ArgumentParser] = ArgumentParser, **kwargs, ): """Simple creation of command line interfaces. Previously called ``jsonargparse.CLI``, renamed to follow the standard of functions in lowercase. Creates an argument parser from one or more functions/classes, parses arguments and runs one of the functions or class methods depending on what was parsed. Args: components: One or more functions/classes to include in the command line interface. args: List of arguments to parse or ``None`` to use ``sys.argv``. config_help: Help string for config file option in help. set_defaults: Dictionary of values to override components defaults. as_positional: Whether to add required parameters as positional arguments. return_instance: Whether class components should be instantiated directly and returned, i.e. without exposing class methods as subcommands. fail_untyped: Whether to raise exception if a required parameter does not have a type. parser_class: The :class:`ArgumentParser` subclass to use. **kwargs: Used to instantiate :class:`.ArgumentParser`. Returns: The value returned by the executed function or class method. """ return_parser = kwargs.pop("return_parser", False) stacklevel = kwargs.pop("_stacklevel", 2) if components is None: components = get_implicit_auto_cli_components(stacklevel) if isinstance(components, list) and len(components) == 1: components = components[0] elif not components: raise ValueError("components parameter expected to be non-empty") if isinstance(components, list): unexpected = [c for c in components if not (inspect.isclass(c) or callable(c))] elif isinstance(components, dict): ns = dict_to_namespace(components) unexpected = [c for k, c in ns.items() if not k.endswith("._help") and not (inspect.isclass(c) or callable(c))] else: unexpected = [c for c in [components] if not (inspect.isclass(c) or callable(c))] if unexpected: raise ValueError(f"Unexpected components, not class or function: {unexpected}") parser = parser_class(**kwargs) if isinstance(components, (list, dict)) or not has_parameter(components, "config"): parser.add_argument("--config", action=ActionConfigFile, help=config_help) if not isinstance(components, (list, dict)): _add_component_to_parser(components, parser, as_positional, return_instance, fail_untyped, config_help) if set_defaults is not None: parser.set_defaults(set_defaults) if return_parser: deprecation_warning_cli_return_parser(stacklevel) return parser cfg = parser.parse_args(args) init = parser.instantiate(cfg) return _run_component(components, init) elif isinstance(components, list): components = {c.__name__: c for c in components} _add_subcommands(components, parser, config_help, as_positional, return_instance, fail_untyped) if set_defaults is not None: parser.set_defaults(set_defaults) if return_parser: deprecation_warning_cli_return_parser(stacklevel) return parser cfg = parser.parse_args(args) init = parser.instantiate(cfg) components_ns = dict_to_namespace(components) subcommand = init.get("subcommand") while isinstance(init.get(subcommand), Namespace) and isinstance(init[subcommand].get("subcommand"), str): subsubcommand = subcommand + "." + init[subcommand].get("subcommand") if subsubcommand in components_ns: subcommand = subsubcommand else: break component = components_ns[subcommand] return _run_component(component, init.get(subcommand)) def auto_parser(*args, **kwargs) -> ArgumentParser: """Same as :func:`.auto_cli`, but returns the parser, doesn't parse arguments or run. This is a shorthand for ``capture_parser(lambda: auto_cli(*args, **kwargs))``. """ return capture_parser(lambda: auto_cli(*args, **kwargs)) def get_help_str(component, logger): if isinstance(component, dict): return component.get("_help") help_str = get_doc_short_description(component, logger=logger) if not help_str: help_str = str(component) return help_str def _add_subcommands( components, parser: ArgumentParser, config_help: str, as_positional: bool, return_instance: bool, fail_untyped: bool, ) -> None: subcommands = parser.add_subcommands(required=True) for name, component in components.items(): if name == "_help": continue description = get_help_str(component, parser.logger) subparser = type(parser)(description=description) if isinstance(component, dict) or not has_parameter(component, "config"): subparser.add_argument("--config", action=ActionConfigFile, help=config_help) subcommands.add_subcommand(name, subparser, help=description) if isinstance(component, dict): _add_subcommands(component, subparser, config_help, as_positional, return_instance, fail_untyped) else: added_args = _add_component_to_parser( component, subparser, as_positional, return_instance, fail_untyped, config_help ) if not added_args: remove_actions(subparser, (ActionConfigFile, _ActionPrintConfig)) def has_parameter(component, name) -> bool: return name in inspect.signature(component).parameters def _add_component_to_parser( component, parser: ArgumentParser, as_positional: bool, return_instance: bool, fail_untyped: bool, config_help: str, ): kwargs: dict = {"as_positional": as_positional, "fail_untyped": fail_untyped, "sub_configs": True} if inspect.isclass(component): class_methods = [ k for k, v in inspect.getmembers(component) if (callable(v) or isinstance(v, property)) and k[0] != "_" ] if return_instance or not class_methods: added_args = parser.add_class_arguments(component, as_group=False, **kwargs) if not parser.description: parser.description = get_help_str(component, parser.logger) return added_args added_args = parser.add_class_arguments(component, **kwargs) subcommands = parser.add_subcommands(required=True) for method in class_methods: method_object = getattr(component, method) description = get_help_str(method_object, parser.logger) subparser = type(parser)(description=description) if not isinstance(method_object, property): if not has_parameter(method_object, "config"): subparser.add_argument("--config", action=ActionConfigFile, help=config_help) added_subargs = subparser.add_method_arguments(component, method, as_group=False, **kwargs) added_args += [f"{method}.{a}" for a in added_subargs] if not added_subargs: remove_actions(subparser, (ActionConfigFile, _ActionPrintConfig)) subcommands.add_subcommand(method, subparser, help=get_help_str(method_object, parser.logger)) else: added_args = parser.add_function_arguments(component, as_group=False, **kwargs) if not parser.description: parser.description = get_help_str(component, parser.logger) return added_args def _run_component(component, cfg): cfg.pop("config", None) subcommand = cfg.pop("subcommand") if inspect.isclass(component) and subcommand: subcommand_cfg = cfg.pop(subcommand, {}) subcommand_cfg.pop("config", None) component_obj = component(**cfg) if isinstance(getattr(component, subcommand), property): return getattr(component_obj, subcommand) component = getattr(component_obj, subcommand) cfg = subcommand_cfg if inspect.iscoroutinefunction(component): return __import__("asyncio").run(component(**cfg)) return component(**cfg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_common.py0000644000175100017510000005022615230046455020454 0ustar00runnerrunnerimport argparse import dataclasses 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] Generic, Protocol, TypeVar, _GenericAlias, ) from ._namespace import Namespace from ._optionals import ( _set_config_read_mode, _set_docstring_parse_options, capture_typing_extension_shadows, get_alias_target, get_annotated_base_type, is_alias_type, is_annotated, is_attrs_class, is_pydantic_model, typing_extensions_import, ) from ._type_checking import ActionsContainer, ArgumentParser, docstring_parser __all__ = [ "Unset", "set_parsing_settings", ] ClassType = TypeVar("ClassType") _UnpackGenericAlias = typing_extensions_import("_UnpackAlias") unpack_meta_types = set() if _UnpackGenericAlias: unpack_meta_types.add(_UnpackGenericAlias) capture_typing_extension_shadows(_UnpackGenericAlias, "_UnpackGenericAlias", unpack_meta_types) class _UnsetType: """Sentinel class for unset argument values.""" _instance = None _SERIALIZED = "==UNSET==" def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __repr__(self): return "Unset" def __bool__(self): return False Unset = _UnsetType() class InstantiatorCallable(Protocol): def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType: pass # pragma: no cover InstantiatorsDictType = dict[tuple[type, bool], InstantiatorCallable] parent_parser: ContextVar[ArgumentParser | None] = ContextVar("parent_parser", default=None) parser_capture: ContextVar[bool] = ContextVar("parser_capture", 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[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[set | None] = ContextVar("applied_instantiation_links", default=None) path_dump_preserve_relative: ContextVar[bool] = ContextVar("path_dump_preserve_relative", default=False) parser_context_vars = { "parent_parser": parent_parser, "parser_capture": parser_capture, "defaults_cache": defaults_cache, "lenient_check": lenient_check, "parsing_defaults": parsing_defaults, "single_subcommand": single_subcommand, "validating_defaults": validating_defaults, "load_value_mode": load_value_mode, "class_instantiators": class_instantiators, "nested_links": nested_links, "applied_instantiation_links": applied_instantiation_links, "path_dump_preserve_relative": path_dump_preserve_relative, } @contextmanager def parser_context(**kwargs): context_var_tokens = [] for name, value in kwargs.items(): context_var = parser_context_vars[name] token = context_var.set(value) context_var_tokens.append((context_var, token)) try: yield finally: for context_var, token in context_var_tokens: context_var.reset(token) parsing_settings: dict = { "validate_defaults": False, "parse_optionals_as_positionals": False, "add_print_completion_argument": False, "stubs_resolver_allow_py_files": False, "omegaconf_absolute_to_relative_paths": False, "unset_sentinel": None, } def get_env_var_bool(name: str) -> bool: raw_value = os.getenv(name, "") value = raw_value.lower() if value not in {"true", "false", ""}: raise ValueError(f"Invalid boolean value for environment variable {name}: {raw_value}") return value == "true" 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: "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, stubs_resolver_allow_py_files: bool | None = None, omegaconf_absolute_to_relative_paths: bool | None = None, unset_sentinel: 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. Args: validate_defaults: Whether default values must be valid according to the argument type. The default is ``False``, meaning no default validation, like in argparse. config_read_mode_urls_enabled: Whether to read config files from URLs using requests package. Default is ``False``. config_read_mode_fsspec_enabled: Whether to read config files from fsspec supported file systems. Default is ``False``. docstring_parse_style: The docstring style to expect. Default is ``DocstringStyle.AUTO``. docstring_parse_attribute_docstrings: Whether to parse attribute docstrings (slower). Default is ``False``. parse_optionals_as_positionals: If ``True``, the parser will take extra positional command line arguments as values for optional arguments. This means that optional arguments can be given by name ``--key=value`` as usual, but also as positional. The extra positionals are applied to optionals in the order that they were added to the parser. By default, this is ``False``. add_print_completion_argument: If ``True``, top-level parsers automatically include ``--print_completion`` argument when ``shtab`` is installed. stubs_resolver_allow_py_files: Whether the stubs resolver should search in ``.py`` files in addition to ``.pyi`` files. omegaconf_absolute_to_relative_paths: If ``True``, when loading configs with ``omegaconf+`` parser mode, absolute interpolation paths are converted to relative. This is only intended for backward compatibility with ``omegaconf`` parser mode. unset_sentinel: If ``True``, parsers will use the :obj:`.Unset` sentinel for arguments that have not been given a value (instead of ``None``). This allows distinguishing between ``None`` as an explicitly given value and an argument that was not provided at all. If ``False``, uses ``None`` (the default, argparse-compatible behavior) unless overridden by ``argument_default``. subclasses_disabled: List of types or functions, so that when parsing only the exact type hints (not their subclasses) are accepted. Descendants of the configured types are also disabled. Functions should return ``True`` for types to disable. subclasses_enabled: List of types or disable function names, so that subclasses are accepted. Types given here have precedence over those in ``subclasses_disabled``. Giving a function name removes the corresponding function from ``subclasses_disabled``. By default, the following disable functions are registered: ``is_pure_dataclass``, ``is_pydantic_model``, ``is_attrs_class`` and ``is_final_class``. """ # validate_defaults if isinstance(validate_defaults, bool): parsing_settings["validate_defaults"] = validate_defaults elif validate_defaults is not None: raise ValueError(f"validate_defaults must be a boolean, but got {validate_defaults}.") # config_read_mode if config_read_mode_urls_enabled is not None: _set_config_read_mode(urls_enabled=config_read_mode_urls_enabled) if config_read_mode_fsspec_enabled is not None: _set_config_read_mode(fsspec_enabled=config_read_mode_fsspec_enabled) # docstring_parse if docstring_parse_style is not None: _set_docstring_parse_options(style=docstring_parse_style) if docstring_parse_attribute_docstrings is not None: _set_docstring_parse_options(attribute_docstrings=docstring_parse_attribute_docstrings) # parse_optionals_as_positionals if isinstance(parse_optionals_as_positionals, bool): parsing_settings["parse_optionals_as_positionals"] = parse_optionals_as_positionals elif parse_optionals_as_positionals is not None: raise ValueError(f"parse_optionals_as_positionals must be a boolean, but got {parse_optionals_as_positionals}.") # add_print_completion_argument if isinstance(add_print_completion_argument, bool): parsing_settings["add_print_completion_argument"] = add_print_completion_argument elif add_print_completion_argument is not None: raise ValueError(f"add_print_completion_argument must be a boolean, but got {add_print_completion_argument}.") # stubs resolver if isinstance(stubs_resolver_allow_py_files, bool): parsing_settings["stubs_resolver_allow_py_files"] = stubs_resolver_allow_py_files elif stubs_resolver_allow_py_files is not None: raise ValueError(f"stubs_resolver_allow_py_files must be a boolean, but got {stubs_resolver_allow_py_files}.") # omegaconf_absolute_to_relative_paths if isinstance(omegaconf_absolute_to_relative_paths, bool): parsing_settings["omegaconf_absolute_to_relative_paths"] = omegaconf_absolute_to_relative_paths elif omegaconf_absolute_to_relative_paths is not None: raise ValueError( f"omegaconf_absolute_to_relative_paths must be a boolean, but got {omegaconf_absolute_to_relative_paths}." ) # unset_sentinel if isinstance(unset_sentinel, bool): parsing_settings["unset_sentinel"] = Unset if unset_sentinel else None elif unset_sentinel is not None: raise ValueError(f"unset_sentinel must be a boolean, but got {unset_sentinel}.") # subclass behavior if subclasses_disabled or subclasses_enabled: subclass_type_behavior( subclasses_disabled=subclasses_disabled, subclasses_enabled=subclasses_enabled, ) def get_parsing_setting(name: str): if name not in parsing_settings: raise ValueError(f"Unknown parsing setting {name}.") if name == "add_print_completion_argument": var_name = "JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT" if var_name in os.environ: return get_env_var_bool(var_name) return parsing_settings[name] def validate_default(container: ActionsContainer, action: argparse.Action): if ( action.default is get_parsing_setting("unset_sentinel") or not get_parsing_setting("validate_defaults") or not hasattr(action, "_check_type") ): return try: from ._core import ArgumentGroup if isinstance(container, ArgumentGroup): container = container.parser with parser_context(parent_parser=container, validating_defaults=True): default = action.default action.default = None action.default = action._check_type_(default) # type: ignore[attr-defined] except Exception as ex: raise ValueError(f"Default value is not valid: {ex}") from ex def get_optionals_as_positionals_actions(parser, include_positionals=False): from jsonargparse._actions import ActionConfigFile, ActionFail, _ActionConfigLoad, filter_non_parsing_actions from jsonargparse._completions import PrintCompletionAction from jsonargparse._typehints import ActionTypeHint actions = [] for action in filter_non_parsing_actions(parser._actions): if isinstance(action, (_ActionConfigLoad, ActionConfigFile, ActionFail, PrintCompletionAction)): continue if ActionTypeHint.is_subclass_typehint(action, all_subtypes=False): continue if action.nargs not in {1, None}: continue if not include_positionals and action.option_strings == []: continue actions.append(action) return actions def supports_optionals_as_positionals(parser): return ( get_parsing_setting("parse_optionals_as_positionals") and not parser._subcommands_action and not getattr(parser, "_inner_parser", False) ) def is_subclass(cls, class_or_tuple) -> bool: """Extension of issubclass that supports non-class arguments and generics.""" try: class_or_tuple = get_generic_origins(class_or_tuple) if inspect.isclass(cls): return issubclass(cls, class_or_tuple) elif is_generic_class(cls): return issubclass(cls.__origin__, class_or_tuple) except TypeError: pass # TypeError means that cls is not a class return False def is_instance(obj, class_or_tuple) -> bool: """Extension of isinstance that supports generics.""" class_or_tuple = get_generic_origins(class_or_tuple) return isinstance(obj, class_or_tuple) def is_final_class(cls) -> bool: """Checks whether a class is final, i.e. decorated with ``typing.final``.""" return getattr(cls, "__final__", False) def is_generic_class(cls) -> bool: return isinstance(cls, _GenericAlias) and getattr(cls, "__module__", "") != "typing" def is_unpack_typehint(cls) -> bool: return any(isinstance(cls, unpack_type) for unpack_type in unpack_meta_types) def get_generic_origin(cls): return cls.__origin__ if is_generic_class(cls) else cls def get_generic_origins(class_or_tuple): if isinstance(class_or_tuple, tuple): return tuple(get_generic_origin(cls) for cls in class_or_tuple) return get_generic_origin(class_or_tuple) def get_unaliased_type(cls): new_cls = cls while True: cur_cls = new_cls if is_annotated(new_cls): new_cls = get_annotated_base_type(new_cls) if is_alias_type(new_cls): new_cls = get_alias_target(new_cls) if new_cls == cur_cls: break return cur_cls def is_pure_dataclass(cls) -> bool: classes = [c for c in inspect.getmro(cls) if c not in {object, Generic}] return all(dataclasses.is_dataclass(c) for c in classes) subclasses_enabled_types: set[type] = set() subclasses_disabled_types: set[type] = set() 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, "is_final_class": is_final_class, } def is_subclasses_disabled(cls) -> bool: if is_generic_class(cls): return is_subclasses_disabled(cls.__origin__) if not inspect.isclass(cls): return False subclass_disabled = any(selector(cls) for selector in subclasses_disabled_selectors.values()) if not subclass_disabled: subclass_disabled = any(issubclass(cls, disable_type) for disable_type in subclasses_disabled_types) if subclass_disabled: subclass_disabled = not any(issubclass(cls, enable_type) for enable_type in subclasses_enabled_types) return subclass_disabled def subclass_type_behavior( 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 []: if isinstance(enable_item, str): if enable_item not in subclasses_disabled_selectors: raise ValueError(f"There is no function '{enable_item}' registered in subclasses_disabled") subclasses_disabled_selectors.pop(enable_item) elif inspect.isclass(enable_item): subclasses_enabled_types.add(enable_item) else: raise ValueError( f"Expected 'subclasses_enabled' list items to be types or strings, but got {enable_item!r}" ) for disable_item in subclasses_disabled or []: if inspect.isclass(disable_item): subclasses_disabled_types.add(disable_item) elif inspect.isfunction(disable_item): subclasses_disabled_selectors[disable_item.__name__] = disable_item else: raise ValueError( f"Expected 'subclasses_disabled' list items to be types or functions, but got {disable_item!r}" ) # logging logging_levels = {"CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"} null_logger = logging.getLogger("jsonargparse_null_logger") null_logger.addHandler(logging.NullHandler()) null_logger.parent = None def setup_default_logger(data, level, caller): name = caller if isinstance(data, str): name = data elif isinstance(data, dict) and "name" in data: name = data["name"] logger = logging.getLogger(name) logger.parent = None if len(logger.handlers) == 0: handler = logging.StreamHandler() handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) logger.addHandler(handler) level = getattr(logging, level) for handler in logger.handlers: handler.setLevel(level) return logger 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: value = {k: v for k, v in logger.items() if k not in {"name", "level"}} raise ValueError(f"Unexpected data to configure logger: {value}.") if logger is False: return null_logger level = "WARNING" if isinstance(logger, dict) and "level" in logger: level = logger["level"] if level not in logging_levels: raise ValueError(f"Got logger level {level!r} but must be one of {logging_levels}.") if not isinstance(logger, logging.Logger): logger = setup_default_logger(logger, level, caller) return logger class LoggerProperty: """Class designed to be inherited by other classes to add a logger property.""" def __init__(self, *args, logger: bool | str | dict | logging.Logger = False, **kwargs): self.logger = logger super().__init__(*args, **kwargs) @property def logger(self) -> logging.Logger: """The logger property for the class. :getter: Returns the current logger. :setter: Sets the given logging.Logger as logger or sets the default logger if given True/str(logger name)/dict(name, level), or disables logging if given False. Raises: ValueError: If an invalid logger value is given. """ return self._logger @logger.setter def logger(self, logger: bool | str | dict | logging.Logger): if logger is None: from ._deprecated import deprecation_warning, logger_property_none_message deprecation_warning((LoggerProperty.logger, None), logger_property_none_message, stacklevel=6) logger = False if not logger and debug_mode_active(): logger = {"level": "DEBUG"} self._logger = parse_logger(logger, type(self).__name__) def debug_mode_active() -> bool: return get_env_var_bool("JSONARGPARSE_DEBUG") # base classes class Action(LoggerProperty, argparse.Action): """Base for jsonargparse Action classes.""" def _check_type_(self, value, **kwargs): if not hasattr(self, "_check_type_kwargs"): self._check_type_kwargs = set(inspect.signature(self._check_type).parameters) kwargs = {k: v for k, v in kwargs.items() if k in self._check_type_kwargs} return self._check_type(value, **kwargs) class NonParsingAction(Action): """Base for jsonargparse utility Action classes.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_completions.py0000644000175100017510000003541215230046455021520 0ustar00runnerrunnerimport argparse import inspect import locale import os import re from collections import defaultdict from contextlib import contextmanager, suppress from contextvars import ContextVar from copy import copy from enum import Enum from importlib.util import find_spec from subprocess import PIPE, Popen 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 from ._parameter_resolvers import get_signature_parameters from ._typehints import ( ActionTypeHint, callable_origin_types, get_all_subclass_paths, get_callable_return_type, get_typehint_origin, is_subclass, type_to_str, ) from ._util import NoneType, Path, import_object, merge_config, unique def handle_completions(parser): handle_argcomplete_autocomplete(parser) add_print_completion_argument(parser) def add_print_completion_argument(parser): if getattr(parser, "parent_parser", None) or not find_spec("shtab"): return print_completion_argument = get_parsing_setting("add_print_completion_argument") if not print_completion_argument and "--print_shtab" not in parser._option_string_actions: parser.add_argument( "--print_shtab", action=ActionFail( message="%(option)s is no longer supported. Use set_parsing_settings(" "add_print_completion_argument=True) or " "JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT=true to add --print_completion." ), help=argparse.SUPPRESS, ) elif print_completion_argument and "--print_completion" not in parser._option_string_actions: parser.add_argument("--print_completion", action=PrintCompletionAction) # argcomplete def handle_argcomplete_autocomplete(parser): if find_spec("argcomplete") and "_ARGCOMPLETE" in os.environ: import argcomplete from ._common import parser_context patch_argcomplete_support() with parser_context(load_value_mode=parser.parser_mode): argcomplete.autocomplete(parser) def patch_argcomplete_support(): import argcomplete.finders parse_known_args = argcomplete.finders.IntrospectiveArgumentParser.parse_known_args if getattr(parse_known_args, "__jsonargparse_patched__", False): return def parse_known_args(self, args=None, namespace=None): return self._parse_known_args_internal(args=args, namespace=namespace, argcomplete=True) parse_known_args.__jsonargparse_patched__ = True argcomplete.finders.IntrospectiveArgumentParser.parse_known_args = parse_known_args def get_argcomplete_namespace(parser, namespace): namespace.__class__ = __import__("jsonargparse").Namespace return merge_config(parser, parser.get_defaults(skip_validation=True), namespace).as_flat() def get_files_completer(): from argcomplete.completers import FilesCompleter return FilesCompleter() def argcomplete_warn_redraw_prompt(prefix, message): import argcomplete if prefix != "": argcomplete.warn(message) with suppress(Exception): proc = Popen(f"ps -p {os.getppid()} -oppid=".split(), stdout=PIPE, stderr=PIPE) stdout, _ = proc.communicate() shell_pid = int(stdout.decode().strip()) os.kill(shell_pid, 28) _ = "_" if locale.getlocale()[1] != "UTF-8" else "\xa0" return [_ + message.replace(" ", _), ""] # shtab shtab_shell: ContextVar = ContextVar("shtab_shell") shtab_prog: ContextVar = ContextVar("shtab_prog") shtab_preambles: ContextVar = ContextVar("shtab_preambles") class PrintCompletionAction(NonParsingAction): def __init__( self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, **kwargs, ): import shtab super().__init__( option_strings=option_strings, dest=dest, default=default, choices=[f"shtab-{shell}" for shell in shtab.SUPPORTED_SHELLS], help="Print shell completion script.", ) def __call__(self, parser, namespace, completion_type, option_string=None): print(parser.get_completion_script(completion_type)) argparse.ArgumentParser.exit(parser, 0) def get_completion_script(parser, completion_type: str, **kwargs) -> str: if not completion_type.startswith("shtab-"): raise ValueError(f"Unsupported completion_type: {completion_type}.") if not find_spec("shtab"): raise ValueError(f"shtab package is required for completion type '{completion_type}'.") return get_shtab_script(parser, completion_type[len("shtab-") :], **kwargs) def get_shtab_script(parser, shell: str, preambles: list[str] | None = None) -> str: import shtab if shell not in shtab.SUPPORTED_SHELLS: raise ValueError(f"Unsupported completion_type: shtab-{shell}.") prog = norm_name(parser.prog) assert prog if not preambles: preambles = [] if shell == "bash": preambles += [bash_compgen_typehint.strip().replace("%s", prog)] with prepare_actions_context(shell, prog, preambles): shtab_prepare_actions(parser) return shtab.complete(parser, shell, preamble="\n".join(preambles)) @contextmanager def prepare_actions_context(shell, prog, preambles): token_shell = shtab_shell.set(shell) token_prog = shtab_prog.set(prog) token_preambles = shtab_preambles.set(preambles) try: yield finally: shtab_shell.reset(token_shell) shtab_prog.reset(token_prog) shtab_preambles.reset(token_preambles) def norm_name(name: str) -> str: return re.sub(r"\W+", "_", name) def shtab_prepare_actions(parser) -> None: remove_actions(parser, (PrintCompletionAction,)) legacy_action = parser._option_string_actions.get("--print_shtab") if legacy_action and legacy_action in parser._actions: parser._actions.remove(legacy_action) if parser._subcommands_action: for subparser in parser._subcommands_action._name_parser_map.values(): shtab_prepare_actions(subparser) if get_parsing_setting("parse_optionals_as_positionals"): for action in get_optionals_as_positionals_actions(parser): clone = copy(action) clone.option_strings = [] clone.nargs = "?" parser._actions.append(clone) for action in parser._actions: shtab_prepare_action(action, parser) def shtab_prepare_action(action, parser) -> None: import shtab if action.choices or hasattr(action, "complete"): return complete = None if isinstance(action, (ActionConfigFile, _ActionConfigLoad)): complete = shtab.FILE elif isinstance(action, ActionTypeHint): typehint = action._typehint if get_typehint_origin(typehint) == Union: assert hasattr(typehint, "__args__") subtypes = [s for s in typehint.__args__ if s not in {NoneType, str, dict, list, tuple, bytes}] if len(subtypes) == 1: typehint = subtypes[0] if is_subclass(typehint, Path): assert hasattr(typehint, "_mode") if "f" in typehint._mode: complete = shtab.FILE elif "d" in typehint._mode: complete = shtab.DIRECTORY elif is_subclass(typehint, os.PathLike): complete = shtab.FILE if complete: action.complete = complete return choices = None if isinstance(action, ActionTypeHint): skip = getattr(action, "sub_add_kwargs", {}).get("skip", set()) prefix = action.option_strings[0] if action.option_strings else None choices, require_prefix = get_typehint_choices(action._typehint, prefix, parser, skip) if shtab_shell.get() == "bash": message = f"Expected type: {type_to_str(action._typehint)}" if action.option_strings == []: message = f"Argument: {action.dest}; " + message add_bash_typehint_completion( parser, action, message, choices, require_prefix=require_prefix, ) choices = None elif isinstance(action, _ActionHelpClassPath): choices = get_help_class_choices(action._typehint) if choices: action.choices = choices bash_compgen_typehint_name = "_jsonargparse_%s_compgen_typehint" bash_compgen_typehint = """ _jsonargparse_%%s_matched_choices() { local TOTAL=$(echo "$1" | wc -w | tr -d " ") if [ "$TOTAL" != 0 ]; then local MATCH=$(echo "$2" | wc -w | tr -d " ") printf "; $MATCH/$TOTAL matched choices" fi } %(name)s() { local REQUIRE_PREFIX="$4" local MATCH=() if [ "$REQUIRE_PREFIX" = 1 ] && [ -z "$2" ]; then MATCH=() else MATCH=( $(IFS=" " compgen -W "$1" "$2") ) fi if [ ${#MATCH[@]} = 0 ]; then if [ "$COMP_TYPE" = 63 ]; then MATCHED=$(_jsonargparse_%%s_matched_choices "$1" "${MATCH[*]}") printf "%(b)s\\n$3$MATCHED\\n%(n)s" >&2 kill -WINCH $$ fi else for match in "${MATCH[@]}"; do echo "$match" done if [ "$COMP_TYPE" = 63 ]; then MATCHED=$(_jsonargparse_%%s_matched_choices "$1" "${MATCH[*]}") printf "%(b)s\\n$3$MATCHED%(n)s" >&2 fi fi } """ % { "name": bash_compgen_typehint_name, "b": "$(tput setaf 5)", "n": "$(tput sgr0)", } def add_bash_typehint_completion(parser, action, message, choices, require_prefix=False) -> None: fn_typehint = norm_name(bash_compgen_typehint_name % shtab_prog.get()) fn_name = parser.prog.replace(" [options] ", "_") fn_name = norm_name(f"_jsonargparse_{fn_name}_{action.dest}_typehint") fn = '{fn_name}(){{ {fn_typehint} "{choices}" "$1" "{message}" {require_prefix}; }}'.format( fn_name=fn_name, fn_typehint=fn_typehint, choices=" ".join(choices), message=message, require_prefix=1 if require_prefix else 0, ) shtab_preambles.get().append(fn) action.complete = {"bash": fn_name} def get_typehint_choices(typehint, prefix, parser, skip, added_subclasses=None) -> tuple[list[str], bool]: if not added_subclasses: added_subclasses = set() def get_choices_state(typehint) -> tuple[list[str], bool, bool]: if typehint is bool: return ["true", "false"], True, False if typehint is NoneType: return ["null"], True, False if is_subclass(typehint, Enum): return list(typehint.__members__), True, False origin = get_typehint_origin(typehint) if origin == Literal: choices = [] for arg in typehint.__args__: if isinstance(arg, bool): choices.append(str(arg).lower()) elif arg is None: choices.append("null") elif isinstance(arg, (str, int, float)): choices.append(str(arg)) return choices, True, False if origin == Union: choices = [] has_explicit_choices = False has_open_values = False for subtype in typehint.__args__: if subtype in added_subclasses or subtype is object: continue subchoices, subexplicit, subopen = get_choices_state(subtype) choices.extend(subchoices) has_explicit_choices = has_explicit_choices or subexplicit has_open_values = has_open_values or subopen return choices, has_explicit_choices, has_open_values if ActionTypeHint.is_subclass_typehint(typehint): added_subclasses.add(typehint) choices = add_subactions_and_get_subclass_choices(typehint, prefix, parser, skip, added_subclasses) return choices, True, False if origin in callable_origin_types: return_type = get_callable_return_type(typehint) if return_type and ActionTypeHint.is_subclass_typehint(return_type): num_args = len(typehint.__args__) - 1 skip.add(num_args) choices = add_subactions_and_get_subclass_choices(return_type, prefix, parser, skip, added_subclasses) return choices, True, False return [], False, return_type is None return [], False, True choices, has_explicit_choices, has_open_values = get_choices_state(typehint) require_prefix = get_typehint_origin(typehint) == Union and has_explicit_choices and has_open_values return choices, require_prefix def add_subactions_and_get_subclass_choices(typehint, prefix, parser, skip, added_subclasses) -> list[str]: choices = [] paths = get_all_subclass_paths(typehint) init_args = defaultdict(list) subclasses = defaultdict(list) for path in paths: choices.append(path) try: cls = import_object(path) params = get_signature_parameters(cls, None, parser._logger) except Exception as ex: parser._logger.debug(f"Unable to get signature parameters for '{path}': {ex}") continue num_skip = next((s for s in skip if isinstance(s, int)), 0) if num_skip > 0: params = params[num_skip:] for param in params: if param.name not in skip: init_args[param.name].append(param.annotation) subclasses[param.name].append(path.rsplit(".", 1)[-1]) if prefix is not None: for name, subtypes in init_args.items(): option_string = f"{prefix}.{name}" if option_string not in parser._option_string_actions: action = parser.add_argument(option_string) for subtype in unique(subtypes): subchoices, require_prefix = get_typehint_choices( subtype, option_string, parser, skip, added_subclasses ) if shtab_shell.get() == "bash": message = f"Expected type: {type_to_str(subtype)}; " message += f"Accepted by subclasses: {', '.join(subclasses[name])}" add_bash_typehint_completion( parser, action, message, subchoices, require_prefix=require_prefix, ) elif subchoices: action.choices = subchoices return choices def get_help_class_choices(typehint) -> list[str]: choices = [] if get_typehint_origin(typehint) == Union: for subtype in typehint.__args__: if inspect.isclass(subtype): choices.extend(get_help_class_choices(subtype)) else: choices = get_all_subclass_paths(typehint) return choices ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_core.py0000644000175100017510000020153215230046455020112 0ustar00runnerrunner"""Extensions of core argparse classes.""" import argparse import glob import inspect import logging import os import sys from collections.abc import Callable, Sequence from contextlib import suppress from typing import ( Any, NoReturn, ) from ._actions import ( ActionConfigFile, ActionParser, _ActionConfigLoad, _ActionPrintConfig, _is_action_value_list, filter_non_parsing_actions, previous_config, ) from ._common import ( debug_mode_active, get_optionals_as_positionals_actions, get_parsing_setting, is_subclasses_disabled, lenient_check, parser_context, supports_optionals_as_positionals, validate_default, ) from ._completions import get_argcomplete_namespace, handle_completions from ._completions import ( get_completion_script as get_completion_script_internal, ) from ._deprecated import ( ParserDeprecations, deprecated_skip_check, deprecated_skip_none, deprecated_yaml_comments, renamed_parameter_warning, ) from ._formatters import DefaultHelpFormatter, get_env_var from ._instantiation import InstantiateMethod from ._jsonnet import ActionJsonnet from ._jsonschema import ActionJsonSchema from ._link_arguments import ActionLink, ArgumentLinking from ._loaders_dumpers import ( check_valid_dump_format, dump_using_format, get_loader_exceptions, load_value, loaders, ) from ._namespace import ( Namespace, NSKeyError, get_non_meta_sorted_keys, is_meta_key, recreate_branches, remove_meta, split_key_leaf, split_key_root, ) from ._optionals import ( _get_config_read_mode, fsspec_support, import_fsspec, import_jsonnet, omegaconf_apply, pyyaml_available, ) from ._parameter_resolvers import UnknownDefault from ._paths import change_to_path_dir from ._required import ( iter_required_keys, restore_suppressed_required, set_required, suppress_required_actions, ) from ._signatures import SignatureArguments from ._subcommands import ( ActionSubCommands, find_action, find_action_and_subcommand, find_parent_action_and_subcommand, get_subcommand, handle_subcommands, is_branch_key, parse_kwargs_context, ) from ._typehints import ActionTypeHint, is_subclass_spec, subclasses_disabled_remove_class_path from ._util import ( Path, argument_error, get_argument_group_class, get_private_kwargs, identity, load_config_path_context, merge_config, return_parser_if_captured, ) __all__ = ["ArgumentParser"] _parse_known_has_intermixed = "intermixed" in inspect.signature(argparse.ArgumentParser._parse_known_args).parameters class ActionsContainer(ArgumentLinking, InstantiateMethod, SignatureArguments, argparse._ActionsContainer): """Extension of ``argparse._ActionsContainer`` to support additional functionalities.""" _action_groups: Sequence["ArgumentGroup"] # type: ignore[assignment] def __init__(self, *args, **kwargs) -> None: """Initializer for ActionsContainer instance.""" super().__init__(*args, **kwargs) self.register("type", None, identity) self.register("action", "parsers", ActionSubCommands) self.register("action", "config", ActionConfigFile) def add_argument(self, *args, sub_configs: bool = False, **kwargs): """Adds an argument to the parser or argument group. All the arguments from `argparse.ArgumentParser.add_argument `_ are supported. Additionally it accepts: Args: sub_configs: Whether to try parsing a sub-config when argument is a complex type. """ from ._deprecated import add_argument_enable_path_deprecation deprecated_val = add_argument_enable_path_deprecation(kwargs) if deprecated_val is not None: sub_configs = deprecated_val parser = self.parser if hasattr(self, "parser") else self if kwargs.get("action") is not None: if ActionParser._is_valid_action_parser(parser, kwargs["action"]): return ActionParser._move_parser_actions(parser, args, kwargs) ActionConfigFile._ensure_single_config_argument(self, kwargs["action"]) if "type" in kwargs: if is_subclasses_disabled(kwargs["type"]): nested_key = args[0].lstrip("-") self.add_class_arguments(kwargs.pop("type"), nested_key, sub_configs=sub_configs, **kwargs) return find_action(parser, nested_key) if ActionTypeHint.is_supported_typehint(kwargs["type"]): args = ActionTypeHint.prepare_add_argument( args=args, kwargs=kwargs, enable_path=sub_configs, container=super(), logger=self._logger, ) if "choices" in kwargs and not isinstance(kwargs["choices"], (list, tuple)): kwargs["choices"] = tuple(kwargs["choices"]) action = super().add_argument(*args, **kwargs) action.logger = self._logger # type: ignore[attr-defined] ActionConfigFile._add_print_config_argument(self, action) ActionJsonnet._check_ext_vars_action(parser, action) if is_meta_key(action.dest): raise ValueError(f'Argument with destination name "{action.dest}" not allowed.') if ( action.option_strings == [] and "default" in kwargs and kwargs["default"] is not argparse.SUPPRESS and action.nargs not in ("*", "?") ): raise ValueError("Positional arguments not allowed to have a default value.") unset_sentinel = get_parsing_setting("unset_sentinel") if unset_sentinel is not None and "default" not in kwargs and action.default is None: action.default = unset_sentinel validate_default(self, action) return action 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 `_ are supported. Additionally it accepts: Args: name: Name of the group. If set, the group object will be included in the ``parser.groups`` dict. Returns: The group object. Raises: ValueError: If a group with the same name already exists. """ parser = self.parser if hasattr(self, "parser") else self if name is not None and name in parser.groups: # type: ignore[union-attr] raise ValueError(f"Group with name {name} already exists.") group_class = getattr(parser, "_group_class", ArgumentGroup) group = group_class(parser, *args, logger=parser._logger, **kwargs) group.parser = parser parser._action_groups.append(group) # type: ignore[union-attr] if name is not None: parser.groups[name] = group # type: ignore[union-attr] return group def set_defaults(self, *args: dict[str, Any], **kwargs: Any) -> None: """Sets default values from dictionary or keyword arguments. Args: *args: Dictionary defining the default values to set. **kwargs: Sets default values based on keyword arguments. Raises: KeyError: If key not defined in the parser. """ for arg in args: for dest, default in arg.items(): dest = dest.replace("-", "_") action = find_action(self, dest) if action is None: raise NSKeyError(f'No action for key "{dest}" to set its default.') elif isinstance(action, ActionConfigFile): ActionConfigFile.set_default_error() elif isinstance(action, _ActionConfigLoad): default = {f"{dest}.{k}": v for k, v in default.items()} self.set_defaults(default) continue if isinstance(action, ActionTypeHint): default = action.normalize_default(default) self._defaults[dest] = action.default = default if kwargs: self.set_defaults(kwargs) class ArgumentGroup(ActionsContainer, argparse._ArgumentGroup): """Extension of argparse._ArgumentGroup to support additional functionalities.""" dest: str | None = None parser: "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: dict[str, ArgumentGroup] | None = None _group_class: type[ArgumentGroup] _subcommands_action: ActionSubCommands | None = None def __init__( self, *args, env_prefix: bool | str = True, formatter_class: type[argparse.HelpFormatter] = DefaultHelpFormatter, 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: list[str] | None = None, default_config_files: list[str | os.PathLike] | None = None, default_env: bool = False, **kwargs, ) -> None: """Initializer for ArgumentParser instance. All the arguments from the initializer of `argparse.ArgumentParser `_ are supported. Additionally it accepts: Args: env_prefix: Prefix for environment variables. ``True`` to derive from ``prog``. formatter_class: Class for printing help messages. logger: Logger to use or configuration for logger. version: Program version which will be printed by the ``--version`` argument. print_config: Name for print config argument, ``%s`` is replaced by config dest, set ``None`` to disable. parser_mode: Mode for parsing values: ``yaml``, ``json``, ``jsonnet`` or added via :func:`.set_loader`. dump_header: Header to include as comment when dumping a config object. default_config_files: Default config file locations, e.g. ``['~/.config/myapp/*.yaml']``. default_env: Set the default value on whether to parse environment variables. """ super().__init__(*args, formatter_class=formatter_class, logger=logger, **kwargs) self._group_class = get_argument_group_class(self) if self.groups is None: self.groups = {} self._extra_required_keys: set[str] = set() self.save_path_content: set[str] = set() self.default_config_files = default_config_files self.default_env = default_env self.env_prefix = env_prefix self.parser_mode = parser_mode self.dump_header = dump_header self._print_config = print_config if version is not None: self.add_argument( "--version", action="version", version="%(prog)s " + version, help="Print version and exit." ) ## Parsing methods ## def _parse_known_args_internal(self, args=None, namespace=None, *, argcomplete: bool = False): if argcomplete: namespace = get_argcomplete_namespace(self, namespace) try: with ( parser_context(parent_parser=self, lenient_check=True), ActionTypeHint.subclass_arg_context(self), suppress_required_actions(self), ): kwargs = {} if _parse_known_has_intermixed: kwargs["intermixed"] = False namespace, args = self._parse_known_args(args, namespace, **kwargs) except argparse.ArgumentError as ex: self.error(str(ex), ex) return namespace, args def _positional_optionals(self, cfg, unk): if len(unk) == 0 or not supports_optionals_as_positionals(self): return cfg, unk for action in get_optionals_as_positionals_actions(self, include_positionals=True): if action.option_strings == []: if cfg.get(action.dest) is get_parsing_setting("unset_sentinel"): self._logger.debug(f"Positional argument {action.dest} missing, aborting _positional_optionals") break continue value = unk.pop(0) try: cfg[action.dest] = self._check_value_key(action, value, action.dest, cfg) except (TypeError, ValueError) as ex: if isinstance(value, str) and value.startswith("--"): raise argument_error(f"unrecognized arguments: {' '.join([value] + unk)}") from ex raise if len(unk) == 0: break return cfg, unk def _parse_optional(self, arg_string): subclass_arg = ActionTypeHint.parse_argv_item(arg_string) if subclass_arg: return subclass_arg if arg_string == self._print_config: arg_string += "=" return super()._parse_optional(arg_string) def _parse_common( self, cfg: Namespace, env: bool | None, defaults: bool, skip_validation: bool, skip_required: bool = False, skip_subcommands: bool = False, fail_no_subcommand: bool = True, ) -> Namespace: """Common parsing code used by other parse methods. Args: cfg: The configuration object. env: Whether to merge with the parsed environment, ``None`` to use the parser's default. defaults: Whether to merge with the parser's defaults. skip_validation: Whether to skip validation of configuration. skip_required: Whether to skip check of required arguments. skip_subcommands: Whether to skip subcommand processing. fail_no_subcommand: Whether to fail if no subcommand given. Returns: A config object with all parsed values. """ if env is None and self._default_env: env = True if not skip_subcommands: handle_subcommands(self, cfg, env=env, defaults=defaults, fail_no_subcommand=fail_no_subcommand) if defaults: with parser_context(lenient_check=True): ActionTypeHint.add_sub_defaults(self, cfg) with parser_context(parent_parser=self): if not lenient_check.get() and self.parser_mode == "omegaconf+": cfg = omegaconf_apply(self, cfg) _ActionPrintConfig.print_config_if_requested(self, cfg) try: ActionLink.apply_parsing_links(self, cfg) except Exception as ex: self.error(str(ex), ex) if not skip_validation: self.validate(cfg, skip_required=skip_required) if not lenient_check.get(): cfg = subclasses_disabled_remove_class_path(cfg) return cfg def _parse_defaults_and_environ( self, defaults: bool = True, env: bool | None = None, environ: dict[str, str] | os._Environ | None = None, ): cfg = Namespace() if defaults: with parser_context(lenient_check=True): # required for omegaconf+ cfg = self.get_defaults(skip_validation=True) if env or (env is None and self._default_env): if environ is None: environ = os.environ with parser_context(load_value_mode=self.parser_mode): cfg_env = self._load_env_vars(env=environ, defaults=defaults) cfg = merge_config(self, cfg_env, cfg) return cfg def parse_args( # type: ignore[override] self, args: Sequence[str] | None = None, namespace: Namespace | None = None, env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: """Parses command line argument strings. All the arguments from `argparse.ArgumentParser.parse_args `_ are supported. Additionally it accepts: Args: args: List of arguments to parse or ``None`` to use ``sys.argv``. env: Whether to merge with the parsed environment, ``None`` to use the parser's default. defaults: Whether to merge with the parser's defaults. Returns: A config object with all parsed values. Raises: ArgumentError: If the parsing fails and ``exit_on_error=False``. """ skip_validation, namespace_as_config = get_private_kwargs( kwargs, _skip_validation=False, _namespace_as_config=False ) return_parser_if_captured(self) handle_completions(self) if args is None: args = sys.argv[1:] else: args = list(args) if not all(isinstance(a, str) for a in args): self.error(f"All arguments are expected to be strings: {args}") self.args = args try: cfg = self._parse_defaults_and_environ(defaults, env) if namespace: if namespace_as_config: cfg = self._parse_defaults_and_environ(defaults, env=False) cfg = merge_config(self, namespace, cfg) if env or (env is None and self._default_env): with parser_context(load_value_mode=self.parser_mode): cfg_env = self._load_env_vars(env=os.environ, defaults=defaults) cfg = merge_config(self, cfg_env, cfg) else: cfg = merge_config(self, namespace, cfg) with parse_kwargs_context({"env": env, "defaults": defaults}): cfg, unk = self._parse_known_args_internal(args=args, namespace=cfg) cfg, unk = self._positional_optionals(cfg, unk) if unk: self.error(f"unrecognized arguments: {' '.join(unk)}") parsed_cfg = self._parse_common( cfg=cfg, env=env, defaults=defaults, skip_validation=skip_validation, ) except (TypeError, KeyError, argparse.ArgumentError) as ex: self.error(str(ex), ex) self._logger.debug("Parsed command line arguments: %s", args) return parsed_cfg @renamed_parameter_warning({"cfg_obj": "obj", "cfg_base": "namespace"}, stacklevel=2) def parse_object( self, obj: Namespace | dict[str, Any], namespace: Namespace | None = None, env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: """Parses configuration given as an object. Args: obj: The configuration object. env: Whether to merge with the parsed environment, ``None`` to use the parser's default. defaults: Whether to merge with the parser's defaults. Returns: A config object with all parsed values. Raises: ArgumentError: If the parsing fails and ``exit_on_error=False``. """ skip_validation, skip_required = get_private_kwargs(kwargs, _skip_validation=False, _skip_required=False) try: cfg = self._parse_defaults_and_environ(defaults, env) if namespace: cfg = merge_config(self, namespace, cfg) cfg = self._apply_actions(cfg) cfg_apply = self._apply_actions(obj, prev_cfg=cfg) cfg = merge_config(self, cfg_apply, cfg) parsed_cfg = self._parse_common( cfg=cfg, env=env, defaults=defaults, skip_validation=skip_validation, skip_required=skip_required, ) except (TypeError, KeyError) as ex: self.error(str(ex), ex) self._logger.debug("Parsed object: %s", obj) return parsed_cfg 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: env_var = get_env_var(self, action) if env_var in env and isinstance(action, ActionConfigFile): ActionConfigFile.apply_config(self, cfg, action.dest, env[env_var]) for action in actions: env_var = get_env_var(self, action) if env_var in env and isinstance(action, ActionSubCommands): env_val = env[env_var] if env_val in action.choices: cfg[action.dest] = subcommand = self._check_value_key(action, env_val, action.dest, cfg) pcfg = action._name_parser_map[env_val].parse_env(env=env, defaults=defaults, _skip_validation=True) for k, v in vars(pcfg).items(): cfg[subcommand + "." + k] = v for action in actions: env_var = get_env_var(self, action) if env_var in env and not isinstance(action, (ActionConfigFile, ActionSubCommands)): env_val = env[env_var] if isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction)): env_val_lower = env_val.lower() if env_val_lower == "true": env_val = True elif env_val_lower == "false": env_val = False else: raise argparse.ArgumentError( action, f"Invalid boolean value for environment variable {env_var}: {env_val}" ) elif _is_action_value_list(action): try: list_env_val = load_value(env_val) env_val = list_env_val if isinstance(list_env_val, list) else [env_val] except get_loader_exceptions(): env_val = [env_val] cfg[action.dest] = self._check_value_key(action, env_val, action.dest, cfg) self._apply_actions(cfg) return cfg def parse_env( self, env: dict[str, str] | None = None, defaults: bool = True, **kwargs, ) -> Namespace: """Parses environment variables. Args: env: The environment object to use, if ``None`` then ``os.environ`` is used. defaults: Whether to merge with the parser's defaults. Returns: A config object with all parsed values. Raises: ArgumentError: If the parsing fails and ``exit_on_error=False``. """ skip_validation, skip_subcommands = get_private_kwargs(kwargs, _skip_validation=False, _skip_subcommands=False) try: cfg = self._parse_defaults_and_environ(defaults, env=True, environ=env) kwargs = { "env": True, "defaults": defaults, "skip_validation": skip_validation, "skip_subcommands": skip_subcommands, } if skip_validation: kwargs["fail_no_subcommand"] = False parsed_cfg = self._parse_common(cfg=cfg, **kwargs) except (TypeError, KeyError) as ex: self.error(str(ex), ex) self._logger.debug("Parsed environment variables") return parsed_cfg @renamed_parameter_warning({"cfg_path": "path"}, stacklevel=1) def parse_path( self, path: str | os.PathLike, ext_vars: dict | None = None, env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: """Parses a configuration file given its path. Args: path: Path to the configuration file to parse. ext_vars: Optional external variables used for parsing jsonnet. env: Whether to merge with the parsed environment, ``None`` to use the parser's default. defaults: Whether to merge with the parser's defaults. Returns: A config object with all parsed values. Raises: ArgumentError: If the parsing fails and ``exit_on_error=False``. """ fpath = Path(path, mode=_get_config_read_mode()) with load_config_path_context(fpath), change_to_path_dir(fpath): content = fpath.read_text() parsed_cfg = self.parse_string( content=content, path=os.path.basename(path), ext_vars=ext_vars, env=env, defaults=defaults, **kwargs, ) self._logger.debug("Parsed configuration from path: %s", path) return parsed_cfg @renamed_parameter_warning({"cfg_str": "content", "cfg_path": "path"}, stacklevel=2) def parse_string( self, content: str, path: str | os.PathLike = "", ext_vars: dict | None = None, env: bool | None = None, defaults: bool = True, **kwargs, ) -> Namespace: """Parses configuration given as a string. Args: content: The configuration content. path: Optional path to original config path, just for error printing. ext_vars: Optional external variables used for parsing jsonnet. env: Whether to merge with the parsed environment, ``None`` to use the parser's default. defaults: Whether to merge with the parser's defaults. Returns: A config object with all parsed values. Raises: ArgumentError: If the parsing fails and ``exit_on_error=False``. """ skip_validation, fail_no_subcommand = get_private_kwargs( kwargs, _skip_validation=False, _fail_no_subcommand=True ) try: with parser_context(load_value_mode=self.parser_mode): cfg = self._load_config_parser_mode(content, path, ext_vars, previous_config.get()) if defaults or env: cfg_base = self._parse_defaults_and_environ(defaults, env) cfg = merge_config(self, cfg, cfg_base) parsed_cfg = self._parse_common( cfg=cfg, env=env, defaults=defaults, skip_validation=skip_validation, fail_no_subcommand=fail_no_subcommand, ) except (TypeError, KeyError) as ex: self.error(str(ex), ex) self._logger.debug("Parsed %s string: %s", self.parser_mode, content) return parsed_cfg def _load_config_parser_mode( self, content: str, path: str | os.PathLike = "", ext_vars: dict | None = None, prev_cfg: Namespace | None = None, ) -> Namespace: """Loads a configuration string into a namespace. Args: content: The configuration content. path: Optional path to original config path, just for error printing. ext_vars: Optional external variables used for parsing jsonnet. Raises: TypeError: If there is an invalid value according to the parser. """ try: cfg_dict = load_value(content, path=path, ext_vars=ext_vars) except get_loader_exceptions() as ex: raise TypeError(f"Problems parsing config: {ex}") from ex if not isinstance(cfg_dict, dict): raise TypeError(f"Unexpected config: {content}") return self._apply_actions(cfg_dict, prev_cfg=prev_cfg) ## Methods for adding to the parser ## add_argument = ActionsContainer.add_argument add_argument_group = ActionsContainer.add_argument_group add_function_arguments = SignatureArguments.add_function_arguments add_method_arguments = SignatureArguments.add_method_arguments add_class_arguments = SignatureArguments.add_class_arguments add_subclass_arguments = SignatureArguments.add_subclass_arguments link_arguments = ArgumentLinking.link_arguments def add_subcommands(self, required: bool = True, dest: str = "subcommand", **kwargs) -> ActionSubCommands: """Adds subcommand parsers to the ArgumentParser. The aim is the same as `argparse.ArgumentParser.add_subparsers `_ the difference being that dest by default is ``subcommand`` and the parsed values of the subcommand are stored in a nested namespace using the subcommand's name as base key. Args: required: Whether the subcommand must be provided. dest: Destination key where the chosen subcommand name is stored. **kwargs: All options that `argparse.ArgumentParser.add_subparsers `_ accepts. """ if "description" not in kwargs: kwargs["description"] = "For more details of each subcommand, add it as an argument followed by --help." default_config_files = self.default_config_files self.default_config_files = [] subcommands: ActionSubCommands = super().add_subparsers(dest=dest, **kwargs) # type: ignore[assignment] self.default_config_files = default_config_files if required: set_required(self, subcommands) subcommands.parent_parser = self subcommands.env_prefix = get_env_var(self) self._subcommands_action = subcommands return subcommands ## Methods for serializing config objects ## @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def dump( self, namespace: Namespace, format: str = "parser_mode", skip_unset: bool = True, skip_default: bool = False, skip_validation: bool = False, with_comments: bool = False, skip_link_targets: bool = True, **kwargs, ) -> str: """Generates a serialized string for the given configuration object. Args: namespace: The configuration object to dump. format: The output format: ``yaml``, ``json``, ``json_indented``, ``toml``, ``parser_mode`` or ones added via :func:`.set_dumper`. skip_unset: Whether to exclude entries whose value is the configured None/Unset value. skip_default: Whether to exclude entries whose value is the same as the default. skip_validation: Whether to skip parser checking. with_comments: Whether to add help content as comments. Currently only supported for ``format="yaml"``. skip_link_targets: Whether to exclude link targets. Returns: The configuration in the chosen format. Raises: TypeError: If any of the values of namespace is invalid according to the parser. """ with_comments = deprecated_yaml_comments(kwargs, with_comments, stacklevel=4) skip_validation = deprecated_skip_check(ArgumentParser.dump, kwargs, skip_validation, stacklevel=4) skip_unset = deprecated_skip_none(ArgumentParser.dump, kwargs, skip_unset, stacklevel=4) if kwargs: raise ValueError(f"Unexpected keyword parameters: {set(kwargs)}") check_valid_dump_format(format) cfg = namespace.clone(with_meta=False) with parser_context(load_value_mode=self.parser_mode): if not skip_validation: self.validate(cfg) if skip_link_targets: ActionLink.strip_link_target_keys(self, cfg) dump_kwargs = {"skip_validation": skip_validation, "skip_unset": skip_unset} self._dump_cleanup_actions(cfg, self._actions, dump_kwargs) cfg = subclasses_disabled_remove_class_path(cfg) cfg_dict = cfg.as_dict() if skip_default: defaults = self.get_defaults(skip_validation=True) ActionLink.strip_link_target_keys(self, defaults) self._dump_cleanup_actions(defaults, self._actions, {"skip_validation": True, "skip_unset": skip_unset}) self._dump_delete_default_entries(cfg_dict, defaults.as_dict()) with parser_context(parent_parser=self): return dump_using_format(self, cfg_dict, dump_format=format, with_comments=with_comments) def _dump_cleanup_actions(self, cfg, actions, dump_kwargs, prefix=""): skip_unset = dump_kwargs["skip_unset"] unset_sentinel = get_parsing_setting("unset_sentinel") for action in filter_non_parsing_actions(actions): action_dest = prefix + action.dest if ( (action.help == argparse.SUPPRESS and not isinstance(action, _ActionConfigLoad)) or isinstance(action, ActionConfigFile) or (skip_unset and action_dest in cfg and cfg[action_dest] is unset_sentinel) ): cfg.pop(action_dest, None) elif isinstance(action, ActionSubCommands): cfg.pop(action_dest, None) for key, subparser in action.choices.items(): self._dump_cleanup_actions(cfg, subparser._actions, dump_kwargs, prefix=prefix + key + ".") elif isinstance(action, ActionLink): action = action.target[1] if isinstance(action, ActionTypeHint): value = cfg.get(action_dest) if action_dest in cfg and value is not unset_sentinel: with parser_context(parent_parser=self, lenient_check=True): if dump_kwargs.get("skip_validation"): with suppress(ValueError): value = action.serialize(value, dump_kwargs=dump_kwargs) else: value = action.serialize(value, dump_kwargs=dump_kwargs) cfg.update(value, action_dest) def _dump_delete_default_entries(self, subcfg, subdefaults): for key in list(subcfg.keys()): if key in subdefaults: val = subcfg[key] default = subdefaults[key] class_object_val = None if is_subclass_spec(val): if val["class_path"] != default.get("class_path"): with parser_context(parent_parser=self): parser = ActionTypeHint.get_class_parser(val["class_path"]) default = {"init_args": parser.get_defaults().as_dict()} class_object_val = val val = val.get("init_args") default = default.get("init_args") if val == default: del subcfg[key] elif isinstance(val, dict) and isinstance(default, dict): self._dump_delete_default_entries(val, default) if class_object_val and class_object_val.get("init_args") == {}: del class_object_val["init_args"] @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def save( self, namespace: Namespace, path: str | os.PathLike, format: str = "parser_mode", skip_unset: bool = True, skip_validation: bool = False, overwrite: bool = False, multifile: bool = True, branch: str | None = None, **kwargs, ) -> None: """Writes to file(s) the given configuration object using the chosen format. Args: namespace: The configuration object to save. path: Path to the location where to save config. format: The output format: ``yaml``, ``json``, ``json_indented``, ``parser_mode`` or ones added via :func:`.set_dumper`. skip_unset: Whether to exclude entries whose value is the configured None/Unset value. skip_validation: Whether to skip parser checking. overwrite: Whether to overwrite existing files. multifile: Whether to save multiple config files by using the ``__path__`` metas. Raises: TypeError: If any of the values of namespace is invalid according to the parser. """ skip_validation = deprecated_skip_check(ArgumentParser.save, kwargs, skip_validation, stacklevel=4) skip_unset = deprecated_skip_none(ArgumentParser.save, kwargs, skip_unset, stacklevel=4) if kwargs: raise ValueError(f"Unexpected keyword parameters: {set(kwargs)}") check_valid_dump_format(format) def check_overwrite(path): if not overwrite and os.path.isfile(path.absolute): raise ValueError(f"Refusing to overwrite existing file: {path.absolute}") dump_kwargs = {"format": format, "skip_unset": skip_unset, "skip_validation": skip_validation} if fsspec_support: try: path_sw = Path(path, mode="sw") except TypeError: pass else: if path_sw.is_fsspec: if multifile: raise NotImplementedError(f"multifile=True not supported for fsspec paths: {path}") fsspec = import_fsspec("ArgumentParser.save") with fsspec.open(path, "w") as f: f.write(self.dump(namespace, **dump_kwargs)) return path_fc = Path(path, mode="fc") check_overwrite(path_fc) if not multifile: with open(path_fc.absolute, "w") as f: f.write(self.dump(namespace, **dump_kwargs)) else: cfg = namespace.clone() if not skip_validation: with parser_context(load_value_mode=self.parser_mode): self.validate(cfg.clone(with_meta=False), branch=branch) ActionLink.strip_link_target_keys(self, cfg) def is_path_action(key): action = find_action(self, key) return isinstance(action, (ActionJsonSchema, ActionJsonnet, ActionTypeHint, _ActionConfigLoad)) def save_path(val): val_path = Path(os.path.basename(val["__path__"].absolute), mode="fc") check_overwrite(val_path) val_out = remove_meta(val) if isinstance(val, Namespace): val_out = val_out.as_dict() if "__orig__" in val: val_str = val["__orig__"] else: is_json = str(val_path).lower().endswith(".json") val_str = dump_using_format(self, val_out, "json_indented" if is_json else format) with open(val_path.absolute, "w") as f: f.write(val_str) return os.path.basename(val_path) def save_paths(cfg): for key in get_non_meta_sorted_keys(cfg): val = cfg[key] if isinstance(val, (Namespace, dict)) and "__path__" in val: if is_path_action(key): cfg[key] = save_path(val) elif isinstance(val, list): if is_path_action(key): for num, item in enumerate(val): if isinstance(item, (Namespace, dict)) and "__path__" in item: val[num] = save_path(item) elif isinstance(val, Path) and key in self.save_path_content and "r" in val.mode: val_path = Path(os.path.basename(val.absolute), mode="fc") check_overwrite(val_path) with open(val_path.absolute, "w") as f: f.write(val.read_text()) cfg[key] = type(val)(str(val_path)) with change_to_path_dir(path_fc), parser_context(parent_parser=self): save_paths(cfg) dump_kwargs["skip_validation"] = True with open(path_fc.absolute, "w") as f: f.write(self.dump(cfg, **dump_kwargs)) ## Methods related to defaults ## def _get_default_config_files(self) -> list[Path]: if getattr(self, "_inner_parser", False): return [] default_config_files = [] for pattern in self.default_config_files: default_config_files += sorted(glob.glob(os.path.expanduser(pattern))) if len(default_config_files) > 0: with suppress(TypeError): return [Path(v, mode=_get_config_read_mode()) for v in default_config_files] return [] def get_default(self, dest: str) -> Any: """Gets a single default value for the given destination key. Args: dest: Destination key from which to get the default. Raises: KeyError: If key or its default not defined in the parser. """ action, _ = find_parent_action_and_subcommand(self, dest) if action is None or dest != action.dest: raise NSKeyError(f'No action for key "{dest}" to get its default.') def check_suppressed_default(): if action.default == argparse.SUPPRESS: raise NSKeyError(f'Action for key "{dest}" does not specify a default.') if not self._get_default_config_files(): check_suppressed_default() return action.default defaults = self.get_defaults() if action.dest not in defaults: check_suppressed_default() return defaults.get(action.dest) def get_defaults(self, skip_validation: bool = False, **kwargs) -> Namespace: """Returns a namespace with all default values. Args: skip_validation: Whether to skip validation of defaults. Returns: An object with all default values as attributes. """ skip_validation = deprecated_skip_check(ArgumentParser.get_defaults, kwargs, skip_validation) cfg = Namespace() for action in filter_non_parsing_actions(self._actions): if ( action.default != argparse.SUPPRESS and action.dest != argparse.SUPPRESS and not isinstance(action.default, UnknownDefault) ): default = recreate_branches(action.default) cfg[action.dest] = default self._logger.debug("Loaded parser defaults: %s", cfg) default_config_files = self._get_default_config_files() for default_config_file in default_config_files: with ( load_config_path_context(default_config_file), change_to_path_dir(default_config_file), parser_context(parent_parser=self, parsing_defaults=True), ): default_config_file_content = default_config_file.read_text() if not default_config_file_content.strip(): continue cfg_file = self._load_config_parser_mode(default_config_file_content, prev_cfg=cfg) cfg = merge_config(self, cfg_file, cfg) try: with _ActionPrintConfig.skip_print_config(): cfg = self._parse_common( cfg=cfg, env=False, defaults=False, skip_validation=skip_validation, skip_required=True, fail_no_subcommand=False, ) except (TypeError, KeyError, argparse.ArgumentError) as ex: raise argument_error( f"Problem in default config file '{default_config_file}': {ex.args[0]}", default_config_file=str(default_config_file), ) from ex meta = cfg.get("__default_config__") if isinstance(meta, list): meta.append(default_config_file) elif isinstance(meta, Path): cfg["__default_config__"] = [meta, default_config_file] else: cfg["__default_config__"] = default_config_file self._logger.debug("Parsed default configuration from path: %s", default_config_file) with parser_context(validating_defaults=True): ActionTypeHint.add_sub_defaults(self, cfg) return cfg set_defaults = ActionsContainer.set_defaults ## Completion script methods ## def _raise_invalidated_by_completion_script(self, *args, **kwargs) -> NoReturn: raise ValueError( "Parser instance was invalidated by get_completion_script() and cannot be reused. " "Create a new parser instance." ) def _invalidate_by_completion_script(self) -> None: for name in dir(self): if name.startswith("_"): continue static_attr = inspect.getattr_static(self, name, None) if inspect.isroutine(static_attr): attr = getattr(self, name, None) if inspect.ismethod(attr): setattr(self, name, self._raise_invalidated_by_completion_script) def get_completion_script(self, completion_type: str, **kwargs) -> str: """Returns shell completion script for a completion type.""" completion_script = get_completion_script_internal(self, completion_type, **kwargs) self._invalidate_by_completion_script() return completion_script ## Other methods ## 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): self._error_handler(self, message) if not self.exit_on_error: raise argument_error(message) from ex elif debug_mode_active(): self._logger.debug("Debug enabled, thus raising exception instead of exit.") raise argument_error(message) from ex parser = getattr(ex, "subcommand_parser", None) or self if getattr(ex, "default_config_file", None): parser.default_config_files = [] parser.print_usage(sys.stderr) help_action = next((a for a in parser._actions if isinstance(a, argparse._HelpAction)), None) if help_action: prog = parser.prog.replace(" [options]", "") sys.stderr.write(f"tip: For details of accepted options run: {prog} {help_action.option_strings[-1]}\n") sys.stderr.write(f"error: {message}\n") self.exit(2) @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def validate( self, namespace: Namespace, skip_unset: bool = True, skip_required: bool = False, branch: str | None = None, **kwargs, ) -> None: """Checks that the content of a given configuration object conforms with the parser. Args: namespace: The configuration object to check. skip_unset: Whether to skip checking of values that are the configured None/Unset value. skip_required: Whether to skip checking required arguments. branch: Base key in case cfg corresponds only to a branch. Raises: TypeError: If any of the values are not valid. KeyError: If a key in cfg is not defined in the parser. """ skip_unset = deprecated_skip_none(ArgumentParser.validate, kwargs, skip_unset, stacklevel=3) prefix = get_private_kwargs(kwargs, _prefix="") cfg = ccfg = namespace.clone() if isinstance(branch, str): branch_cfg = cfg cfg = Namespace() cfg[branch] = branch_cfg def check_required(cfg, parser, prefix): missing = [] for reqkey in iter_required_keys(parser): try: val = cfg[reqkey] if val is get_parsing_setting("unset_sentinel"): raise TypeError except (KeyError, TypeError): missing.append(f"{prefix}{reqkey}") subcommand, subparser = get_subcommand(parser, cfg, fail_no_subcommand=False) if subcommand is not None and subparser is not None: missing.extend(check_required(cfg.get(subcommand), subparser, prefix + subcommand + ".")) if prefix == "" and missing: raise TypeError(f"the following arguments are required: {', '.join(missing)}") return missing def check_values(cfg): sorted_keys = {k: find_action(self, k) for k in get_non_meta_sorted_keys(cfg)} for key, action in sorted_keys.items(): parent_action = None if action is None: if is_branch_key(self, key): continue parent_action, subcommand = find_parent_action_and_subcommand(self, key, exclude=_ActionConfigLoad) if parent_action: parent_key = subcommand + "." + parent_action.dest if subcommand else parent_action.dest if key.startswith(parent_key + ".") and sorted_keys.get(parent_key) is parent_action: # only check action once with entire value continue val = cfg[key] if action is not None: if (val is get_parsing_setting("unset_sentinel") and skip_unset) or lenient_check.get(): continue try: self._check_value_key(action, val, key, ccfg) except TypeError as ex: if not (val == {} and ActionTypeHint.is_subclass_typehint(action)): raise ex else: if isinstance(parent_action, ActionSubCommands) and "." in key: subcommand, subkey = split_key_root(key) ex = NSKeyError(f"Subcommand '{subcommand}' does not accept option '{subkey}'") ex.subcommand_parser = parent_action._name_parser_map[subcommand] raise ex group_key = next((g for g in self.groups if key.startswith(g + ".")), None) if group_key: subkey = key[len(group_key) + 1 :] raise NSKeyError(f"Group '{group_key}' does not accept option '{subkey}'") if self._subcommands_action: if cfg.get(self._subcommands_action.dest): subcommand = f"'{cfg[self._subcommands_action.dest]}'" else: subcommand = f"{{{list(self._subcommands_action.choices)[0]},...}}" raise NSKeyError(f"Option '{key}' is not accepted before subcommand {subcommand}") raise NSKeyError(f"Option '{key}' is not accepted") with parser_context(load_value_mode=self.parser_mode): check_values(cfg) if not skip_required and not lenient_check.get(): check_required(cfg, self, prefix) instantiate = renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2)(InstantiateMethod.instantiate) @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def strip_unknown(self, namespace: Namespace) -> Namespace: """Removes all unknown keys from a configuration object. Args: namespace: The configuration object to strip. Returns: The stripped configuration object. """ cfg = namespace.clone() del_keys = [] for key in cfg.keys(): if find_action(self, key) is None and not is_meta_key(key): del_keys.append(key) for key in del_keys: del cfg[key] return cfg @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def get_config_files(self, namespace: Namespace) -> list[str]: """Returns a list of loaded config file paths. Args: namespace: The configuration object. Returns: Paths to loaded config files. """ cfg_files = [] if "__default_config__" in namespace: cfg_files.append(namespace["__default_config__"]) unset_sentinel = get_parsing_setting("unset_sentinel") for action in filter_non_parsing_actions(self._actions): if ( isinstance(action, ActionConfigFile) and action.dest in namespace and namespace[action.dest] is not unset_sentinel ): cfg_files.extend(p for p in namespace[action.dest] if p is not None) return cfg_files def format_help(self) -> str: defaults = None if len(self._default_config_files) > 0: note = "no existing default config file found." try: defaults = self.get_defaults() if "__default_config__" in defaults: config_files = defaults["__default_config__"] if isinstance(config_files, list): config_files = [str(x) for x in config_files] note = f"default values below are the ones overridden by the contents of: {config_files}" except argparse.ArgumentError as ex: note = f"tried getting defaults considering default_config_files but failed due to: {ex}" group = self._default_config_files_group group.description = f"{self._default_config_files}, Note: {note}" with restore_suppressed_required(), parser_context(parent_parser=self, defaults_cache=defaults): help_str = super().format_help() return help_str def print_usage(self, *args, **kwargs) -> None: with parser_context(parent_parser=self): return super().print_usage(*args, **kwargs) def _apply_actions( self, cfg: Namespace | dict[str, Any], parent_key: str = "", 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): cfg = Namespace(cfg) if parent_key: cfg_branch = cfg cfg = Namespace() cfg[parent_key] = cfg_branch keys = [parent_key + "." + k for k in cfg_branch.keys(branches=True, nested=False)] else: keys = list(cfg.keys(branches=True, nested=False)) if prev_cfg: prev_cfg = prev_cfg.clone() else: prev_cfg = Namespace() config_keys: set[str] = set() num = 0 while num < len(keys): key = keys[num] exclude = _ActionConfigLoad if key in config_keys else None action, subcommand = find_action_and_subcommand(self, key, exclude=exclude) if isinstance(action, ActionJsonnet): ext_vars_key = action._ext_vars if ext_vars_key and ext_vars_key not in keys[:num]: keys = keys[:num] + [ext_vars_key] + [k for k in keys[num:] if k != ext_vars_key] continue num += 1 if action is None or isinstance(action, ActionSubCommands): value = cfg[key] if isinstance(value, dict): value = Namespace(value) if isinstance(value, Namespace): new_keys = value.keys(branches=True, nested=False) keys += [key + "." + k for k in new_keys if key + "." + k not in keys] cfg[key] = value continue action_dest = action.dest if subcommand is None else subcommand + "." + action.dest append = False if action_dest not in cfg and key.endswith("+"): append = True cfg[action_dest] = cfg.pop(key) value = cfg[action_dest] if skip_fn and skip_fn(value): continue with parser_context(parent_parser=self, lenient_check=True): value = self._check_value_key(action, value, action_dest, prev_cfg, append=append) if isinstance(action, _ActionConfigLoad): config_keys.add(action_dest) keys.append(action_dest) elif isinstance(action, ActionConfigFile): if isinstance(value, str): cfg.pop(action_dest) preserve = Namespace({k: cfg[k] for k in keys[num:]}) ActionConfigFile.apply_config(self, cfg, action_dest, value) cfg.update(preserve) continue elif getattr(action, "jsonnet_ext_vars", False): prev_cfg[action_dest] = value cfg[action_dest] = value return cfg[parent_key] if parent_key else cfg def _check_value_key( self, action: argparse.Action, value: Any, key: str, cfg: Namespace | None, append: bool = False ) -> Any: """Checks the value for a given action. Args: action: The action used for parsing. value: The value to parse. key: The configuration key. Raises: TypeError: If the value is not valid. """ unset_sentinel = get_parsing_setting("unset_sentinel") if value is unset_sentinel and lenient_check.get(): return value is_subcommand = isinstance(action, ActionSubCommands) if is_subcommand and action.choices: leaf_key = split_key_leaf(key)[-1] if leaf_key == action.dest: return value subparser = action._name_parser_map[leaf_key] # type: ignore[attr-defined] subparser.validate(value, _prefix=key + ".") elif isinstance(action, _ActionConfigLoad): if isinstance(value, str): value = action.check_type(value, self) elif hasattr(action, "_check_type"): with parser_context(parent_parser=self): value = action._check_type_(value, cfg=cfg, append=append, mode=self.parser_mode) # type: ignore[attr-defined] elif action.type is not None: try: if action.nargs in {None, "?"} or action.nargs == 0: value = action.type(value) # type: ignore[operator] elif value is not None: for k, v in enumerate(value): value[k] = action.type(v) # type: ignore[operator] except (TypeError, ValueError) as ex: raise TypeError(f'Parser key "{key}": {ex}') from ex if not is_subcommand and action.choices: vals = value if _is_action_value_list(action) else [value] assert isinstance(vals, list) for val in vals: if val not in action.choices: raise TypeError(f'Parser key "{key}": {val!r} not among choices {action.choices}') return value ## Properties ## @property def default_config_files(self) -> list[str]: """Default config file locations. :getter: Returns the current default config file locations. :setter: Sets new default config file locations, e.g. ``['~/.config/myapp/*.yaml']``. Raises: ValueError: If an invalid value is given. """ return self._default_config_files @default_config_files.setter 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( isinstance(x, (str, os.PathLike)) for x in default_config_files ): self._default_config_files = [os.fspath(d) for d in default_config_files] else: raise ValueError("default_config_files expects None or List[str | os.PathLike].") if len(self._default_config_files) > 0: if not hasattr(self, "_default_config_files_group"): group_title = "default config file locations" group_class = getattr(self, "_group_class", ArgumentGroup) group = group_class(self, title=group_title) self._action_groups = [group] + self._action_groups # type: ignore[operator] self._default_config_files_group = group elif hasattr(self, "_default_config_files_group"): self._action_groups = [g for g in self._action_groups if g != self._default_config_files_group] delattr(self, "_default_config_files_group") @property def default_env(self) -> bool: """Whether by default environment variables parsing is enabled. If the ``JSONARGPARSE_DEFAULT_ENV`` environment variable is set to true or false, that value will take precedence. :getter: Returns the current default environment variables parsing setting. :setter: Sets the default environment variables parsing setting. Raises: ValueError: If an invalid value is given. """ return self._default_env @default_env.setter def default_env(self, default_env: bool): os_default_env = os.getenv("JSONARGPARSE_DEFAULT_ENV", "").lower() if os_default_env in {"true", "false"}: self._default_env = os_default_env == "true" elif isinstance(default_env, bool): self._default_env = default_env else: raise ValueError("default_env expects a boolean.") if self._subcommands_action: for subparser in self._subcommands_action._name_parser_map.values(): subparser.default_env = self._default_env @property def env_prefix(self) -> bool | str: """The environment variables prefix property. :getter: Returns the current environment variables prefix. :setter: Sets the environment variables prefix. Raises: ValueError: If an invalid value is given. """ return self._env_prefix @env_prefix.setter def env_prefix(self, env_prefix: bool | str): if env_prefix is None: from ._deprecated import ( deprecation_warning, env_prefix_property_none_message, ) deprecation_warning(ArgumentParser, env_prefix_property_none_message, stacklevel=3) env_prefix = False elif env_prefix is True: env_prefix = os.path.splitext(self.prog)[0] elif not isinstance(env_prefix, (bool, str)): raise ValueError("env_prefix expects a string or a boolean.") self._env_prefix = env_prefix @property def parser_mode(self) -> str: """Mode for parsing config files, ``yaml``, ``json``, ``jsonnet`` or ones added via :func:`.set_loader`. :getter: Returns the current parser mode. :setter: Sets the parser mode. Raises: ValueError: If an invalid value is given. """ return self._parser_mode @parser_mode.setter def parser_mode(self, parser_mode: str): accepted = set(loaders).union({"omegaconf", "omegaconf+"}) if parser_mode not in accepted: raise ValueError(f"The only accepted values for parser_mode are {accepted}.") if parser_mode == "jsonnet": import_jsonnet("parser_mode=jsonnet") self._parser_mode = parser_mode if self._subcommands_action: for subparser in self._subcommands_action._name_parser_map.values(): subparser.parser_mode = parser_mode @property def dump_header(self) -> list[str] | None: """Header to include as comment when dumping a config object. :getter: Returns the current dump header. :setter: Sets the dump header. Raises: ValueError: If an invalid value is given. """ return self._dump_header @dump_header.setter 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)) ): raise ValueError("Expected dump_header to be None or a list of strings.") self._dump_header = dump_header # Not supported methods def parse_known_args(self, *args, **kwargs) -> NoReturn: """Raises ``NotImplementedError`` since typos in configs would go unnoticed.""" raise NotImplementedError("parse_known_args not supported because typos in configs would go unnoticed.") def add_subparsers(self, *args, **kwargs) -> NoReturn: """Raises ``NotImplementedError`` since jsonargparse uses ``add_subcommands``.""" raise NotImplementedError("In jsonargparse subcommands are added using the add_subcommands method.") from ._deprecated import parse_as_dict_patch # noqa: E402 if "SPHINX_BUILD" not in os.environ: parse_as_dict_patch() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_deprecated.py0000644000175100017510000011543715230046455021272 0ustar00runnerrunner"""Deprecated code.""" import functools import inspect import os import sys from argparse import ArgumentError from enum import Enum from importlib import import_module from pathlib import Path from types import ModuleType from typing import Any, Callable, Dict, Optional, Set, Union, overload from ._common import Action, InstantiatorsDictType, null_logger from ._common import LoggerProperty as InternalLoggerProperty from ._instantiation import _register_instantiator from ._namespace import Namespace from ._type_checking import ArgumentParser, ruamelCommentedMap __all__ = [ "ActionEnum", "ActionJsonnetExtVars", "ActionOperators", "ActionPath", "ActionPathList", "HelpFormatterDeprecations", "LoggerProperty", "PathDeprecations", "ParserDeprecations", "ParserError", "compose_dataclasses", "get_config_read_mode", "dict_to_namespace", "namespace_to_dict", "null_logger", "set_docstring_parse_options", "set_config_read_mode", "set_url_support", "strip_meta", "usage_and_exit_error_handler", ] _message_add_argument_enable_path = """ ``enable_path`` parameter of ``add_argument`` was deprecated in v4.49.0 and will be removed in v5.0.0. Use ``sub_configs`` instead. """ _message_action_json_schema_enable_path = """ ``enable_path`` parameter of ``ActionJsonSchema`` was deprecated in v4.49.0 and will be removed in v5.0.0. Use ``sub_config`` instead. """ shown_deprecation_warnings: Set[Any] = set() class JsonargparseDeprecationWarning(DeprecationWarning): pass def deprecation_warning(component, message, stacklevel=1): env_var = os.environ.get("JSONARGPARSE_DEPRECATION_WARNINGS", "").lower() show_warnings = env_var != "off" all_warnings = env_var == "all" if show_warnings and (component not in shown_deprecation_warnings or all_warnings): from ._util import warning if len(shown_deprecation_warnings) == 0 and not all_warnings: warning( """ By default only one JsonargparseDeprecationWarning per type is shown. To see all warnings set environment variable JSONARGPARSE_DEPRECATION_WARNINGS=all and to disable the warnings set JSONARGPARSE_DEPRECATION_WARNINGS=off. """, JsonargparseDeprecationWarning, stacklevel=stacklevel + 2, ) warning(message, JsonargparseDeprecationWarning, stacklevel=stacklevel + 2) shown_deprecation_warnings.add(component) def deprecated(message): def deprecated_decorator(component): warning = "\n\n.. warning::\n " + message + "\n" component.__doc__ = ("" if component.__doc__ is None else component.__doc__) + warning if inspect.isclass(component): @functools.wraps(component.__init__) def init_wrap(self, *args, **kwargs): deprecation_warning(component, message) self._original_init(*args, **kwargs) component._original_init = component.__init__ component.__init__ = init_wrap decorated = component else: @functools.wraps(component) def decorated(*args, **kwargs): deprecation_warning(component, message) return component(*args, **kwargs) return decorated return deprecated_decorator def add_argument_enable_path_deprecation(kwargs: dict, stacklevel: int = 1) -> Optional[bool]: """Handle deprecated ``enable_path`` parameter in ``add_argument``. If ``enable_path`` is present in kwargs, emit a deprecation warning and return its value (popping it from kwargs). Returns ``None`` if not present. """ if "enable_path" in kwargs: deprecation_warning( add_argument_enable_path_deprecation, _message_add_argument_enable_path, stacklevel=stacklevel + 1, ) return kwargs.pop("enable_path") return None def action_json_schema_enable_path_deprecation(kwargs: dict, stacklevel: int = 1) -> Optional[bool]: """Handle deprecated ``enable_path`` parameter in ``ActionJsonSchema``. If ``enable_path`` is present in kwargs, emit a deprecation warning and return its value (popping it from kwargs). Returns ``None`` if not present. """ if "enable_path" in kwargs: deprecation_warning( action_json_schema_enable_path_deprecation, _message_action_json_schema_enable_path, stacklevel=stacklevel + 1, ) return kwargs.pop("enable_path") return None def parse_as_dict_patch(): """Adds parse_as_dict support to ArgumentParser as a patch. This is a temporal backward compatible support for parse_as_dict to have cleaner code in v4.0.0 and warn users about the deprecation and future removal. """ from ._core import ArgumentParser assert not hasattr(ArgumentParser, "_unpatched_init") message_parse_as_dict = """ ``parse_as_dict`` parameter was deprecated in v4.0.0 and will be removed in v5.0.0. After removal, the parse_*, dump, save and instantiate_classes methods will only return Namespace and/or accept Namespace objects. If needed for some use case, config objects can be converted to a nested dict using the Namespace.as_dict method. """ message_with_meta = """ ``with_meta`` parameter was deprecated in v4.44.0 and will be removed in v5.0.0. After removal, config objects will always include metadata. To remove metadata from a config object, do ``.clone(with_meta=False)``. """ # Patch __init__ def patched_init(self, *args, parse_as_dict: bool = False, **kwargs): self._parse_as_dict = parse_as_dict if parse_as_dict: deprecation_warning(patched_init, message_parse_as_dict) self._unpatched_init(*args, **kwargs) ArgumentParser._unpatched_init = ArgumentParser.__init__ ArgumentParser.__init__ = patched_init # Patch parse methods def patch_parse_method(method_name): unpatched_method_name = "_unpatched_" + method_name def patched_parse( self, *args, with_meta: Optional[bool] = None, _skip_validation: bool = False, **kwargs, ) -> Union[Namespace, Dict[str, Any]]: parse_method = getattr(self, unpatched_method_name) cfg = parse_method(*args, _skip_validation=_skip_validation, **kwargs) if isinstance(with_meta, bool): deprecation_warning(patched_parse, message_with_meta) if not (with_meta or (with_meta is None and self._default_meta)): cfg = cfg.clone(with_meta=False) return cfg.as_dict() if self._parse_as_dict and not _skip_validation else cfg patched_parse.__name__ = method_name patched_parse.__qualname__ = f"ArgumentParser.{method_name}" setattr(ArgumentParser, unpatched_method_name, getattr(ArgumentParser, method_name)) setattr(ArgumentParser, method_name, patched_parse) patch_parse_method("parse_args") patch_parse_method("parse_object") patch_parse_method("parse_env") patch_parse_method("parse_string") # Patch dump def patched_dump(self, cfg: Union[Namespace, Dict[str, Any]], *args, **kwargs) -> str: if isinstance(cfg, dict): cfg = self.parse_object(cfg, _skip_validation=True) return self._unpatched_dump(cfg, *args, **kwargs) ArgumentParser._unpatched_dump = ArgumentParser.dump ArgumentParser.dump = patched_dump # Patch save def patched_save(self, cfg: Union[Namespace, Dict[str, Any]], *args, multifile: bool = True, **kwargs) -> None: if multifile and isinstance(cfg, dict): cfg = self.parse_object(cfg, _skip_validation=True) return self._unpatched_save(cfg, *args, multifile=multifile, **kwargs) ArgumentParser._unpatched_save = ArgumentParser.save ArgumentParser.save = patched_save @deprecated(""" ActionEnum was deprecated in v3.9.0 and will be removed in v5.0.0. Enums now should be given directly as a type as explained in :ref:`enums`. """) class ActionEnum: """An action based on an Enum that maps to-from strings and enum values.""" def __init__(self, **kwargs): if "enum" in kwargs: from ._common import is_subclass if not is_subclass(kwargs["enum"], Enum): raise ValueError("Expected enum to be an subclass of Enum.") self._type = kwargs["enum"] else: raise ValueError("Expected enum keyword argument.") def __call__(self, *args, **kwargs): if kwargs.get("type"): raise ValueError("ActionEnum doesn't allow a type.") from ._typehints import ActionTypeHint return ActionTypeHint(typehint=self._type)(**kwargs) @deprecated(""" ActionOperators was deprecated in v3.0.0 and will be removed in v5.0.0. Now types should be used as explained in :ref:`restricted-numbers`. """) class ActionOperators: """Action to restrict a value with comparison operators.""" def __init__(self, **kwargs): if "expr" in kwargs: restrictions = [kwargs["expr"]] if isinstance(kwargs["expr"], tuple) else kwargs["expr"] register_key = (tuple(sorted(restrictions)), kwargs.get("type", int), kwargs.get("join", "and")) from .typing import registered_types, restricted_number_type if register_key in registered_types: self._type = registered_types[register_key] else: self._type = restricted_number_type( None, kwargs.get("type", int), kwargs["expr"], kwargs.get("join", "and") ) else: raise ValueError("Expected expr keyword argument.") def __call__(self, *args, **kwargs): if kwargs.get("type"): raise ValueError("ActionOperators doesn't allow a type.") from ._typehints import ActionTypeHint return ActionTypeHint(typehint=self._type)(**kwargs) @deprecated(""" ActionPath was deprecated in v3.11.0 and will be removed in v5.0.0. Paths now should be given directly as a type as explained in :ref:`parsing-paths`. """) class ActionPath: """Action to check and store a path.""" def __init__( self, mode: str, skip_check: bool = False, ): from .typing import path_type self._type = path_type(mode, skip_check=skip_check) def __call__(self, *args, **kwargs): if kwargs.get("type"): raise ValueError("ActionPath doesn't allow a type.") from ._typehints import ActionTypeHint return ActionTypeHint(typehint=self._type)(**kwargs) @deprecated(""" ActionPathList was deprecated in v4.20.0 and will be removed in v5.0.0. Instead use as type ``List[]`` with ``sub_configs=True``. """) class ActionPathList(Action): """Action to check and store a list of file paths read from a plain text file or stream.""" def __init__(self, mode: Optional[str] = None, rel: str = "cwd", **kwargs): """Initializer for ActionPathList instance. Args: mode: The required type and access permissions among [fdrwxcuFDRWX] as a keyword argument (uppercase means not), e.g. ActionPathList(mode='fr'). rel: Whether relative paths are with respect to current working directory 'cwd' or the list's parent directory 'list'. Raises: ValueError: If any of the parameters (mode or rel) are invalid. """ if mode is not None: from .typing import path_type self._type = path_type(mode) self._rel = rel if self._rel not in {"cwd", "list"}: raise ValueError(f'rel must be either "cwd" or "list", got {self._rel}.') elif "_type" not in kwargs: raise ValueError("Expected mode keyword argument.") else: self._type = kwargs.pop("_type") self._rel = kwargs.pop("_rel") super().__init__(**kwargs) def __call__(self, *args, **kwargs): """Parses an argument as a PathList and if valid sets the parsed value to the corresponding key. Raises: TypeError: If the argument is not a valid PathList. """ if len(args) == 0: if "nargs" in kwargs and kwargs["nargs"] not in {"+", 1}: raise ValueError('ActionPathList only supports nargs of 1 or "+".') kwargs["_type"] = self._type kwargs["_rel"] = self._rel return ActionPathList(**kwargs) setattr(args[1], self.dest, self._check_type(args[2])) return None def _check_type(self, value): if value == []: return value from ._actions import _is_action_value_list islist = _is_action_value_list(self) if not islist and not isinstance(value, list): value = [value] if isinstance(value, list) and all(not isinstance(v, self._type) for v in value): path_list_files = value value = [] for path_list_file in path_list_files: try: with sys.stdin if path_list_file == "-" else open(path_list_file) as f: path_list = [x.strip() for x in f.readlines()] except FileNotFoundError as ex: raise TypeError(f"Problems reading path list: {path_list_file} :: {ex}") from ex cwd = os.getcwd() if self._rel == "list" and path_list_file != "-": os.chdir(os.path.abspath(os.path.join(path_list_file, os.pardir))) try: for num, val in enumerate(path_list): try: path_list[num] = self._type(val) except TypeError as ex: raise TypeError(f"Path number {num + 1} in list {path_list_file}, {ex}") from ex finally: os.chdir(cwd) value += path_list return value @deprecated(""" set_url_support was deprecated in v3.12.0 and will be removed in v5.0.0. Optional config read modes should now be set using function set_parsing_settings. """) def set_url_support(enabled: bool): """Enables/disables URL support for config read mode.""" from ._optionals import _get_config_read_mode, _set_config_read_mode _set_config_read_mode( urls_enabled=enabled, fsspec_enabled=True if "s" in _get_config_read_mode() else False, ) @deprecated(""" set_config_read_mode was deprecated in v4.39.0 and will be removed in v5.0.0. Optional config read modes should now be set using function set_parsing_settings. """) def set_config_read_mode( urls_enabled: bool = False, fsspec_enabled: bool = False, ): """Enables/disables optional config read modes.""" from ._optionals import _set_config_read_mode _set_config_read_mode( urls_enabled=urls_enabled, fsspec_enabled=fsspec_enabled, ) @deprecated(""" get_config_read_mode was deprecated in v4.39.0 and will be removed in v5.0.0. The config read mode is internal and thus shouldn't be used. """) def get_config_read_mode() -> str: """Returns the current config reading mode.""" from ._optionals import _get_config_read_mode return _get_config_read_mode() @deprecated(""" set_docstring_parse_options was deprecated in v4.39.0 and will be removed in v5.0.0. Docstring parse options should now be set using function set_parsing_settings. """) def set_docstring_parse_options(style=None, attribute_docstrings: Optional[bool] = None): """Sets options for docstring parsing.""" from ._optionals import _set_docstring_parse_options _set_docstring_parse_options( style=style, attribute_docstrings=attribute_docstrings, ) cli_return_parser_message = """ The return_parser parameter was deprecated in v4.5.0 and will be removed in v5.0.0. Instead of this use function capture_parser. """ auto_cli_implicit_components_message = """ Implicit components discovery in auto_cli was deprecated in v4.49.0 and will be removed in v5.0.0. Pass components explicitly, explicit is better than implicit. """ def get_implicit_auto_cli_components(stacklevel): deprecation_warning("auto_cli.components", auto_cli_implicit_components_message, stacklevel=stacklevel + 1) caller = inspect.stack()[stacklevel][0] module = inspect.getmodule(caller) components = [ v for v in vars(module).values() if ((inspect.isclass(v) or callable(v)) and inspect.getmodule(v) is module) ] if len(components) == 0: raise ValueError( "Either components parameter must be given or there must be at least one " "function or class among the locals in the context where CLI is called." ) return components def deprecation_warning_cli_return_parser(stacklevel): deprecation_warning("CLI.__init__.return_parser", cli_return_parser_message, stacklevel=stacklevel) logger_property_none_message = """ Setting the logger property to None was deprecated in v4.10.0 and will raise an exception in v5.0.0. Use False instead. """ env_prefix_property_none_message = """ Setting the env_prefix property to None was deprecated in v4.11.0 and will raise an exception in v5.0.0. Use True instead. """ path_skip_check_message = """ The skip_check parameter of Path was deprecated in v4.20.0 and will be removed in v5.0.0. There is no reason to use a Path type if its checks are disabled. Instead use a type such as str or os.PathLike. """ def path_skip_check_deprecation(stacklevel=2): deprecation_warning("Path.__init__", path_skip_check_message, stacklevel=stacklevel) path_immutable_attrs_message = """ Path objects are not meant to be mutable. To make this more explicit, attributes have been renamed and changed into properties without setters. Please update your code to use the new property names and don't modify path attributes. The changes are: ``rel_path`` -> ``relative`` and ``abs_path`` -> ``absolute``, ``cwd`` no name change, ``skip_check`` will be removed. """ path_call_message = """ Calling Path objects is deprecated and will be removed in v5.0.0. Use the ``absolute`` or ``relative`` properties instead. """ path_get_content_message = """ ``Path.get_content`` was deprecated in v4.49.0 and will be removed in v5.0.0. Instead use ``Path.read_text`` for text and ``Path.open`` for binary data. """ class PathDeprecations: """Deprecated methods for Path.""" @property def rel_path(self): deprecation_warning("Path attr get", path_immutable_attrs_message) return self._relative @rel_path.setter def rel_path(self, rel_path): deprecation_warning("Path attr set", path_immutable_attrs_message) self._relative = rel_path @property def abs_path(self): deprecation_warning("Path attr get", path_immutable_attrs_message) return self._absolute @abs_path.setter def abs_path(self, abs_path): deprecation_warning("Path attr set", path_immutable_attrs_message) self._absolute = abs_path @property def cwd(self): return self._cwd @cwd.setter def cwd(self, cwd): deprecation_warning("Path attr set", path_immutable_attrs_message) self._cwd = cwd def _deprecated_kwargs(self, kwargs): from ._util import get_private_kwargs self._skip_check = get_private_kwargs(kwargs, skip_check=False) if self._skip_check: path_skip_check_deprecation() def _repr_skip_check(self, name): if self._skip_check: name += "_skip_check" return name @property def skip_check(self): return self._skip_check @skip_check.setter def skip_check(self, skip_check): deprecation_warning("Path attr set", path_immutable_attrs_message) self._skip_check = skip_check @deprecated(path_call_message) def __call__(self, absolute: bool = True) -> str: return self._absolute if absolute else self._relative def get_content(self, mode: str = "r"): deprecation_warning("Path.get_content", path_get_content_message) if self._std_io: # type: ignore[attr-defined] from ._paths import _read_cached_stdin return _read_cached_stdin() elif self._is_url: # type: ignore[attr-defined] from ._optionals import import_requests assert mode == "r" requests = import_requests("Path.get_content") response = requests.get(self._absolute) response.raise_for_status() return response.text elif self._is_fsspec: # type: ignore[attr-defined] from ._optionals import import_fsspec fsspec = import_fsspec("Path.get_content") with fsspec.open(self._absolute, mode) as handle: with handle as input_file: return input_file.read() else: with open(self._absolute, mode) as input_file: return input_file.read() @deprecated(""" usage_and_exit_error_handler was deprecated in v4.20.0 and will be removed in v5.0.0. With the removal of error_handler, there is no longer a need for this function. """) def usage_and_exit_error_handler(parser: ArgumentParser, message: str) -> None: """Prints the usage and exits with error code 2 (same behavior as argparse). Args: parser: The parser object. message: The message describing the error being handled. """ parser.print_usage(sys.stderr) args = {"prog": parser.prog, "message": message} sys.stderr.write("%(prog)s: error: %(message)s\n" % args) parser.exit(2) error_handler_message = """ ArgumentParser's error_handler was deprecated in v4.20.0 and will be removed in v5.0.0. Instead use the new exit_on_error parameter from argparse. """ def deprecation_warning_error_handler(stacklevel): deprecation_warning("ArgumentParser.error_handler", error_handler_message, stacklevel=stacklevel) default_meta_message = """ ``default_meta`` property was deprecated in v4.44.0 and will be removed in v5.0.0. After removal, config objects will always include metadata. To remove metadata from a config object, do ``.clone(with_meta=False)``. """ class ParserDeprecations: """Helper class for ArgumentParser deprecations. Will be removed in v5.0.0.""" _instantiators: Optional[InstantiatorsDictType] = None def __init__(self, *args, error_handler=False, default_meta=None, **kwargs): super().__init__(*args, **kwargs) self.error_handler = error_handler if default_meta is None: self._default_meta = True else: self.default_meta = default_meta @property @deprecated("error_handler property is deprecated and will be removed in v5.0.0.") def error_handler(self) -> Optional[Callable[[ArgumentParser, str], None]]: """Property for the error_handler function that is called when there are parsing errors. :getter: Returns the current error_handler function. :setter: Sets a new error_handler function (Callable[self, message:str] or None). Raises: ValueError: If an invalid value is given. """ return self._error_handler @error_handler.setter def error_handler(self, error_handler): if error_handler is not False: stacklevel = 2 stack = inspect.stack()[1] if stack.filename.endswith(os.fspath(Path("jsonargparse", "_deprecated.py"))): stacklevel = 5 deprecation_warning_error_handler(stacklevel) if callable(error_handler) or error_handler in {None, False}: self._error_handler = error_handler else: raise ValueError("error_handler can be either a Callable or None.") @property @deprecated(default_meta_message) def default_meta(self) -> bool: """Whether by default metadata is included in config objects. :getter: Returns the current default metadata setting. :setter: Sets the default metadata setting. Raises: ValueError: If an invalid value is given. """ return self._default_meta @default_meta.setter def default_meta(self, default_meta: bool): if isinstance(default_meta, bool): deprecation_warning("ArgumentParser.default_meta", default_meta_message) self._default_meta = default_meta else: raise ValueError("default_meta expects a boolean.") @deprecated(""" ``instantiate_classes`` was deprecated in v4.49.0 and will be removed in v5.0.0. Instead use ``instantiate``. """) def instantiate_classes(self, cfg: Union[Namespace, Dict[str, Any]], **kwargs) -> Union[Namespace, Dict[str, Any]]: if isinstance(cfg, dict): cfg = self._apply_actions(cfg) # type: ignore[attr-defined] cfg = self.instantiate(cfg, **kwargs) # type: ignore[attr-defined] return cfg.as_dict() if self._parse_as_dict else cfg # type: ignore[attr-defined] @deprecated(""" instantiate_subclasses was deprecated in v4.0.0 and will be removed in v5.0.0. Instead use instantiate. """) def instantiate_subclasses(self, cfg: Namespace) -> Namespace: return self.instantiate(cfg, instantiate_groups=False) # type: ignore[attr-defined] @deprecated(""" add_dataclass_arguments was deprecated in v4.35.0 and will be removed in v5.0.0. Instead use add_class_arguments. """) def add_dataclass_arguments(self, *args, **kwargs): if "title" in kwargs: kwargs["help"] = kwargs.pop("title") return self.add_class_arguments(*args, **kwargs) @deprecated(""" ArgumentParser.check_config was deprecated in v4.35.0 and will be removed in v5.0.0. Instead use validate. """) def check_config(self, *args, **kwargs): return self.validate(*args, **kwargs) @deprecated(""" ``ArgumentParser.add_instantiator`` was deprecated in v4.49.0 and will be removed in v5.0.0. Use the global function ``jsonargparse.add_instantiator`` instead. """) def add_instantiator( self, instantiator, class_type, subclasses: bool = True, prepend: bool = False, ) -> None: if self._instantiators is None: self._instantiators = {} _register_instantiator(self._instantiators, instantiator, class_type, subclasses=subclasses, prepend=prepend) def _get_parser_instantiators(self) -> InstantiatorsDictType: instantiators = self._instantiators or {} if hasattr(self, "parent_parser"): parent_instantiators = self.parent_parser._get_parser_instantiators() instantiators = instantiators.copy() instantiators.update({k: v for k, v in parent_instantiators.items() if k not in instantiators}) return instantiators @deprecated(""" ``ArgumentParser.merge_config`` was deprecated in v4.50.0 and will be removed in v5.0.0. There is no replacement since this is for internal use. """) def merge_config(self, cfg_from: Namespace, cfg_to: Namespace) -> Namespace: from ._util import merge_config return merge_config(self, cfg_from, cfg_to) def deprecated_skip_check(component, kwargs: dict, skip_validation: bool, stacklevel: int = 3) -> bool: skip_check = kwargs.pop("skip_check", None) if skip_check is not None: skip_validation = skip_check deprecation_warning( component, ( "skip_check parameter was deprecated in v4.35.0 and will be removed in " "v5.0.0. Instead use skip_validation." ), stacklevel=stacklevel, ) return skip_validation deprecated_valid_flags = {"skip_null": "skip_null"} def deprecated_skip_none(component, kwargs: dict, skip_unset: bool, stacklevel: int = 3) -> bool: skip_none = kwargs.pop("skip_none", None) if skip_none is not None: skip_unset = skip_none deprecation_warning( component, ("skip_none parameter was deprecated in v4.49.0 and will be removed in v5.0.0. Instead use skip_unset."), stacklevel=stacklevel, ) return skip_unset def deprecated_skip_null(flag: str) -> bool: if flag == "skip_null": deprecation_warning( "skip_null", ( "skip_null flag for --print_config was deprecated in v4.49.0 and will be removed in " "v5.0.0. Instead use skip_unset." ), ) return True return False def deprecated_yaml_comments(kwargs: dict, with_comments: bool, stacklevel: int = 3) -> bool: yaml_comments = kwargs.pop("yaml_comments", None) if yaml_comments is not None: deprecation_warning( deprecated_yaml_comments, ( "yaml_comments parameter was deprecated in v4.44.0 and will be removed in " "v5.0.0. Instead use with_comments." ), stacklevel=stacklevel, ) return yaml_comments return with_comments ParserError = ArgumentError def deprecated_module(module_name, mappings=None): module_path = f"jsonargparse.{module_name}" module = ModuleType(module_path, f"deprecated {module_path}") sys.modules[module_path] = module @deprecated(f""" Only use the public API as described in https://jsonargparse.readthedocs.io/en/stable/#api-reference. Importing from {module_path} is kept only to avoid breaking code that does not correctly use the public API. It will no longer be available from v5.0.0. """) def __getattr__(name): new_module = f"_{module_name}" if mappings and name in mappings: new_module, name = mappings[name] if module_name == "typehints" and name == "lazy_instance": from jsonargparse.typing import lazy_instance return lazy_instance return getattr(import_module(f"jsonargparse.{new_module}"), name) module.__getattr__ = __getattr__ module.__dict__["__file__"] = str(Path(__file__).parent / f"{module_name}.py") module.__dict__["__path__"] = module_path deprecated_module("actions") deprecated_module("cli") deprecated_module("core") deprecated_module("deprecated") deprecated_module("formatters") deprecated_module("jsonnet") deprecated_module("jsonschema") deprecated_module("link_arguments") deprecated_module("loaders_dumpers") deprecated_module("namespace") deprecated_module("parameter_resolvers") deprecated_module("signatures") deprecated_module("typehints") deprecated_module("util") deprecated_module( "optionals", { "import_docstring_parse": ("_optionals", "import_docstring_parser"), }, ) @deprecated(""" ActionJsonnetExtVars was deprecated in v4.24.0 and will be removed in v5.0.0. Instead use ``type=dict``. """) class ActionJsonnetExtVars: """Action to add argument to provide ext_vars for jsonnet parsing.""" def __call__(self, *args, **kwargs): from ._typehints import ActionTypeHint action = ActionTypeHint(typehint=dict)(**kwargs) action.jsonnet_ext_vars = True return action @deprecated(""" LoggerProperty was deprecated in v4.40.0 and will be removed from the public API in v5.0.0. There is no replacement since jsonargparse is not a logging library. A similar class can be found in reconplogger package. """) class LoggerProperty(InternalLoggerProperty): """Adds a logger property, intended for internal use.""" @deprecated(""" namespace_to_dict was deprecated in v4.40.0 and will be removed in v5.0.0. Instead you can use ``.clone().as_dict()`` or ``.as_dict()``. """) def namespace_to_dict(namespace: Namespace) -> Dict[str, Any]: """Returns a copy of a nested namespace converted into a nested dictionary.""" return namespace.clone().as_dict() @deprecated(""" dict_to_namespace was deprecated in v4.43.0 and will be removed in v5.0.0. No replacement is provided because blindly converting a dictionary to a namespace may not yield the same results as using a parser, which could lead to confusion. """) def dict_to_namespace(cfg_dict: dict[str, Any]) -> Namespace: """Converts a nested dictionary into a nested namespace.""" from ._namespace import dict_to_namespace as _dict_to_namespace return _dict_to_namespace(cfg_dict) @overload def strip_meta(cfg: "Namespace") -> "Namespace": ... # pragma: no cover @overload def strip_meta(cfg: Dict[str, Any]) -> Dict[str, Any]: ... # pragma: no cover @deprecated(""" strip_meta was deprecated in v4.43.0 and will be removed in v5.0.0. Instead use ``.clone(with_meta=False)``. """) def strip_meta(cfg): """Removes all metadata keys from a configuration object.""" from ._namespace import remove_meta return remove_meta(cfg) def is_meta_key(key: str) -> bool: from ._namespace import meta_keys, split_key_leaf leaf_key = split_key_leaf(key)[-1] return leaf_key in meta_keys class NamespaceDeprecations: """Helper class for Namespace deprecations. Will be removed in v5.0.0.""" @deprecated(""" get_sorted_keys method was deprecated in v4.49.0 and will be removed in v5.0.0. There is no replacement since this is for internal use and developers can call .keys() and then sort. """) def get_sorted_keys(self, branches: bool = True, key_filter: Callable = is_meta_key) -> list[str]: """Deprecated method""" from ._namespace import split_key keys = [k for k in self.keys() if not key_filter(k)] # type: ignore[attr-defined] if branches: for key in [k for k in keys if "." in k]: key_split = split_key(key) for num in range(len(key_split) - 1): parent_key = ".".join(key_split[: num + 1]) if parent_key not in keys: keys.append(parent_key) keys.sort(key=lambda x: -len(split_key(x))) return keys @deprecated(""" get_value_and_parent method was deprecated in v4.49.0 and will be removed in v5.0.0. There is no replacement since this is for internal use and developers can get the parent and leaf separately. """) def get_value_and_parent(self, key: str) -> tuple[Any, Namespace, str]: """Deprecated method""" leaf_key, parent_ns, _ = self._parse_required_key(key) # type: ignore[attr-defined] return parent_ns[leaf_key], parent_ns, leaf_key def _patch_namespace_deprecations() -> None: Namespace.get_sorted_keys = NamespaceDeprecations.get_sorted_keys # type: ignore[attr-defined] Namespace.get_value_and_parent = NamespaceDeprecations.get_value_and_parent # type: ignore[attr-defined] _patch_namespace_deprecations() class HelpFormatterDeprecations: """Helper class for DefaultHelpFormatter deprecations. Will be removed in v5.0.0.""" def __init__(self, *args, **kwargs): from jsonargparse._formatters import YAMLCommentFormatter super().__init__(*args, **kwargs) self._yaml_formatter = YAMLCommentFormatter(self) @deprecated("The add_yaml_comments method is deprecated and will be removed in v5.0.0.") def add_yaml_comments(self, cfg: str) -> str: """Adds help text as yaml comments.""" return self._yaml_formatter.add_yaml_comments(cfg) @deprecated("The set_yaml_start_comment method is deprecated and will be removed in v5.0.0.") def set_yaml_start_comment(self, text: str, cfg: ruamelCommentedMap): """Sets the start comment to a ruamel.yaml object. Args: text: The content to use for the comment. cfg: The ruamel.yaml object. """ self._yaml_formatter.set_yaml_start_comment(text, cfg) @deprecated("The set_yaml_group_comment method is deprecated and will be removed in v5.0.0.") def set_yaml_group_comment(self, text: str, cfg: ruamelCommentedMap, key: str, depth: int): """Sets the comment for a group to a ruamel.yaml object. Args: text: The content to use for the comment. cfg: The parent ruamel.yaml object. key: The key of the group. depth: The nested level of the group. """ self._yaml_formatter.set_yaml_group_comment(text, cfg, key, depth) @deprecated("The set_yaml_argument_comment method is deprecated and will be removed in v5.0.0.") def set_yaml_argument_comment(self, text: str, cfg: ruamelCommentedMap, key: str, depth: int): """Sets the comment for an argument to a ruamel.yaml object. Args: text: The content to use for the comment. cfg: The parent ruamel.yaml object. key: The key of the argument. depth: The nested level of the argument. """ self._yaml_formatter.set_yaml_argument_comment(text, cfg, key, depth) @deprecated(""" compose_dataclasses is deprecated and will be removed in v5.0.0. There is no direct replacement, whoever is interested can copy the code from an old release. """) def compose_dataclasses(*args): """Returns a dataclass inheriting all given dataclasses and properly handling __post_init__.""" import dataclasses @dataclasses.dataclass class ComposedDataclass(*args): def __post_init__(self): for arg in args: if hasattr(arg, "__post_init__"): arg.__post_init__(self) return ComposedDataclass def deprecated_implicit_subcommand(component, subcommand_keys: list[str], subcommand: str, dest: str): stack = inspect.stack() deprecation_warning( component, ( f"Multiple subcommand settings provided ({', '.join(subcommand_keys)}) without an " f"explicit '{dest}' key. Subcommand '{subcommand}' will be used. From v5.0.0 " "providing an explicit subcommand will be required." ), stacklevel=7 if Path(stack[6].filename).name == "_deprecated.py" else 6, ) def renamed_parameter_warning(renames: dict[str, str], stacklevel: int = 1): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for old_name, new_name in renames.items(): if old_name in kwargs: deprecation_warning( func, ( f"Parameter '{old_name}' was renamed to '{new_name}' in v4.50.0. " "The old name will stop working in v5.0.0." ), stacklevel=stacklevel, ) if new_name not in kwargs: kwargs[new_name] = kwargs.pop(old_name) return func(*args, **kwargs) return wrapper return decorator ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_formatters.py0000644000175100017510000003030115230046455021342 0ustar00runnerrunner"""Formatter classes.""" import re from argparse import ( OPTIONAL, SUPPRESS, ZERO_OR_MORE, Action, HelpFormatter, _HelpAction, _SubParsersAction, ) from collections.abc import Iterable from io import StringIO from string import Template from ._actions import ( ActionConfigFile, ActionYesNo, _ActionConfigLoad, filter_non_parsing_actions, non_parsing_actions, ) from ._common import ( defaults_cache, get_optionals_as_positionals_actions, parent_parser, supports_optionals_as_positionals, ) from ._deprecated import HelpFormatterDeprecations from ._link_arguments import ActionLink from ._namespace import Namespace from ._optionals import import_ruamel from ._subcommands import ActionSubCommands, find_action from ._type_checking import ArgumentParser, ruamelCommentedMap from ._typehints import ActionTypeHint, type_to_str __all__ = ["DefaultHelpFormatter"] class PercentTemplate(Template): delimiter = "%" pattern = r""" \%\((?: (?P\%\%)| (?P[_a-z][_a-z0-9]*)\)s| (?P[_a-z][_a-z0-9]*)\)s| (?P) ) """ # type: ignore[assignment] class YAMLCommentFormatter: """Formatter class for adding YAML comments to configuration files.""" def __init__(self, help_formatter: HelpFormatter): self.help_formatter = help_formatter def add_yaml_comments(self, cfg: str) -> str: """Adds help text as yaml comments.""" from ._core import ArgumentParser ruyaml = import_ruamel("add_yaml_comments") yaml = ruyaml.YAML() cfg = yaml.load(cfg) 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] full_key = (prefix + "." if prefix else "") + key parsers[full_key] = subparser parsers.update(get_parsers(subparser, prefix=full_key)) parsers[None] = parser return parsers parser = parent_parser.get() assert isinstance(parser, ArgumentParser) parsers = get_parsers(parser) group_titles = {} for parser_n_key, parser_n in parsers.items(): group_titles[parser_n_key] = parser_n.description prefix = "" if parser_n_key is None else parser_n_key + "." for group in parser_n._action_groups: actions = filter_non_parsing_actions(group._group_actions) actions = [ a for a in actions if not isinstance(a, (_ActionConfigLoad, ActionConfigFile, ActionSubCommands)) ] keys = {re.sub(r"\.?[^.]+$", "", a.dest) for a in actions if "." in a.dest} for key in keys: group_titles[prefix + key] = group.title def set_comments(cfg, prefix="", depth=0): for key in cfg.keys(): full_key = (prefix + "." if prefix else "") + key action = find_action(parser, full_key) text = None if full_key in group_titles and isinstance(cfg[key], dict): text = group_titles[full_key] elif action is not None and action.help != SUPPRESS: text = self.help_formatter._expand_help(action) if isinstance(cfg[key], dict): if text: self.set_yaml_group_comment(text, cfg, key, depth) set_comments(cfg[key], full_key, depth + 1) elif text: self.set_yaml_argument_comment(text, cfg, key, depth) if parser.description is not None: self.set_yaml_start_comment(parser.description, cfg) set_comments(cfg) out = StringIO() yaml.dump(cfg, out) return out.getvalue() def set_yaml_start_comment( self, text: str, cfg: ruamelCommentedMap, ): """Sets the start comment to a ruamel.yaml object. Args: text: The content to use for the comment. cfg: The ruamel.yaml object. """ cfg.yaml_set_start_comment(text) def set_yaml_group_comment( self, text: str, cfg: ruamelCommentedMap, key: str, depth: int, ): """Sets the comment for a group to a ruamel.yaml object. Args: text: The content to use for the comment. cfg: The parent ruamel.yaml object. key: The key of the group. depth: The nested level of the group. """ cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth) def set_yaml_argument_comment( self, text: str, cfg: ruamelCommentedMap, key: str, depth: int, ): """Sets the comment for an argument to a ruamel.yaml object. Args: text: The content to use for the comment. cfg: The parent ruamel.yaml object. key: The key of the argument. depth: The nested level of the argument. """ cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth) class DefaultHelpFormatter(HelpFormatterDeprecations, HelpFormatter): """Help message formatter that includes types, default values and env var names. This class is an extension of `argparse.HelpFormatter `_. Default values are always included. Furthermore, if the parser is configured with ``default_env=True`` command line options are preceded by ``ARG:`` and the respective environment variable name is included preceded by ``ENV:``. """ def _get_help_string(self, action: Action) -> str: if getattr(action, "_jsonargparse_preexpanded_help", False): return action.help or "" action_help = action.help or "" if isinstance(action, ActionConfigFile): return action_help if isinstance(action, _HelpAction): help_str = action_help[0].upper() + action_help[1:] if help_str[-1] != ".": help_str += "." return help_str help_str = "" if action.required: help_str = "required" if "%(type)" not in action_help and self._get_type_str(action) is not None: help_str += (", " if help_str else "") + "type: %(type)s" if ( "%(default)" not in action_help and action.default != SUPPRESS and (action.default is not None or not action.required) and (action.option_strings or action.nargs in {OPTIONAL, ZERO_OR_MORE}) ): help_str += (", " if help_str else "") + "default: %(default)s" if isinstance(action, ActionTypeHint): help_str += action.extra_help() return action_help + (" (" + help_str + ")" if help_str else "") def _format_action(self, action: Action) -> str: if action.help is None and action.help != SUPPRESS: help_text = self._expand_help(action) if help_text and help_text.strip(): action.help = help_text action._jsonargparse_preexpanded_help = True # type: ignore[attr-defined] try: return super()._format_action(action) finally: del action._jsonargparse_preexpanded_help # type: ignore[attr-defined] action.help = None return super()._format_action(action) def _format_usage(self, usage, actions, *args, **kwargs) -> str: actions = filter_non_parsing_actions(actions) usage = super()._format_usage(usage, actions, *args, **kwargs) parser = parent_parser.get() if not parser: return usage if supports_optionals_as_positionals(parser): actions = get_optionals_as_positionals_actions(parser) if len(actions) > 0: extra_positionals = "" for action in reversed(actions): extra_positionals = f"{action.dest} {extra_positionals}" if extra_positionals else action.dest extra_positionals = f"[{extra_positionals}]" usage_lines = usage.rstrip().split("\n") last_line = usage_lines[-1] + " " + extra_positionals text_width = self._width - self._current_indent if len(usage_lines) == 1 or len(last_line) <= text_width: usage_lines[-1] = last_line else: indent = re.sub(r"^( +)[^ ].*$", r"\1", usage_lines[-1]) usage_lines.append(indent + extra_positionals) note = "note: extra positionals are parsed as optionals in the order shown above." usage = "\n".join(usage_lines) + f"\n\n{note}\n\n" return usage def _format_action_invocation(self, action: Action) -> str: parser = parent_parser.get() assert parser is not None if isinstance(action, ActionSubCommands): value = "Available subcommands:" if parser.default_env: value = f"ENV: {get_env_var(self, action)}\n\n {value}" return value if not parser.default_env: return super()._format_action_invocation(action) # Subcommand choices (individual subcommands in the list) don't get ARG: prefix or ENV: line if isinstance(action, _SubParsersAction._ChoicesPseudoAction): return super()._format_action_invocation(action) extr = "" if not isinstance(action, non_parsing_actions): extr += "\n ENV: " + get_env_var(self, action) return "ARG: " + super()._format_action_invocation(action) + extr def _get_default_metavar_for_optional(self, action: Action) -> str: return action.dest.rsplit(".")[-1].upper() def _expand_help(self, action: Action) -> str: params = dict(vars(action), prog=self._prog) if params.get("choices") is not None: choices_str = ", ".join([str(c) for c in params["choices"]]) params["choices"] = choices_str type_str = self._get_type_str(action) if type_str is not None: params["type"] = type_str orig_default = action.default if params.get("default") == SUPPRESS: del params["default"] elif "default" in params: defaults = defaults_cache.get() if defaults is not None: params["default"] = action.default = defaults.get(action.dest) if params["default"] is None: params["default"] = "null" elif isinstance(params["default"], Namespace): params["default"] = params["default"].as_dict() help_str = PercentTemplate(self._get_help_string(action)).safe_substitute(params) action.default = orig_default return help_str def _get_type_str(self, action: Action) -> str | None: type_str = None if isinstance(action, ActionYesNo): type_str = "bool" elif action.type is not None: type_str = type_to_str(action.type) elif isinstance(action, ActionTypeHint): type_str = type_to_str(action._typehint) return type_str 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: 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): parser = parent_parser.get() else: parser = parser_or_formatter assert parser is not None env_var = "" if isinstance(parser.env_prefix, str): env_var = parser.env_prefix.replace("-", "_") + "_" if action: env_var += action.dest env_var = env_var.replace(".", "__").upper() return env_var ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_from_config.py0000644000175100017510000001661415230046455021457 0ustar00runnerrunnerimport inspect from functools import wraps from os import PathLike from pathlib import Path from typing import TypeVar from ._common import parser_context from ._core import ArgumentParser from ._loaders_dumpers import get_loader_exceptions, load_value from ._optionals import _get_config_read_mode from ._paths import change_to_path_dir from ._required import clear_required, iter_required_keys from ._typehints import is_subclass_spec, resolve_class_path_by_name from ._util import import_object, load_config_path_context __all__ = ["FromConfigMixin"] T = TypeVar("T") OVERRIDE_KINDS = {inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD} class FromConfigMixin: """Mixin class that adds from config support to classes. This mixin does two things: 1. Adds support for overriding ``__init__`` defaults by defining a ``__from_config_init_defaults__`` class attribute pointing to a config file path. The overriding of defaults happens on subclass creation time. Inspecting the signature will give the overridden defaults. 2. Adds a ``from_config`` ``@classmethod``, that instantiates the class based on a config file or dict. If ``config_read_mode_fsspec_enabled=True`` is set, then config paths can be URLs. Attributes: __from_config_init_defaults__: Optional path to a config file for overriding ``__init__`` defaults. __from_config_parser_kwargs__: Additional kwargs to pass to the ArgumentParser used for parsing configs. """ __from_config_init_defaults__: str | PathLike | None = None __from_config_parser_kwargs__: dict = {} def __init_subclass__(cls, **kwargs) -> None: """Override ``__init__`` defaults for the subclass based on a config file.""" super().__init_subclass__(**kwargs) _override_init_defaults(cls, cls.__from_config_parser_kwargs__) @classmethod def from_config(cls: type[T], config: str | PathLike | dict) -> T: """Instantiate current class based on a config file or dict. Args: config: Path to a config file or a dict with config values. """ kwargs, cls = _parse_class_kwargs_from_config(cls, config, **cls.__from_config_parser_kwargs__) # type: ignore[attr-defined] return cls(**kwargs) 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) cfg_path = None if not isinstance(config, dict): from .typing import Path cfg_path = Path(config, mode=_get_config_read_mode()) with ( load_config_path_context(cfg_path), change_to_path_dir(cfg_path), parser_context(load_value_mode=parser.parser_mode), ): cfg_str = cfg_path.read_text() try: config = load_value(cfg_str, path=str(config)) except get_loader_exceptions() as ex: raise TypeError(f"Problems parsing config '{config}': {ex}") from ex if not isinstance(config, dict): raise TypeError(f"Expected config to be a dict or parse into a dict: {config}") if is_subclass_spec(config): class_path = resolve_class_path_by_name(cls, config["class_path"]) obj = import_object(class_path) if not issubclass(obj, cls): raise TypeError(f"Class '{class_path}' is not a subclass of '{cls.__name__}'") cls = obj config = {**config.get("init_args", {}), **config.get("dict_kwargs", {})} parser.add_class_arguments(cls) for required in iter_required_keys(parser): clear_required(parser, required) with load_config_path_context(cfg_path), change_to_path_dir(cfg_path): cfg = parser.parse_object(config, defaults=False) return parser.instantiate(cfg).as_dict(), cls 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))): raise TypeError("__from_config_init_defaults__ must be str, PathLike, or None") if not (isinstance(config, (str, PathLike)) and Path(config).is_file()): return defaults, cls = _parse_class_kwargs_from_config(cls, config, **parser_kwargs) _override_init_defaults_this_class(cls, defaults) _override_init_defaults_parent_classes(cls, defaults) 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) if param and param.kind in OVERRIDE_KINDS: if param.default == inspect._empty: raise TypeError(f"Overriding of required parameters not allowed: '{param.name}'") defaults.pop(name) if param.kind == inspect.Parameter.KEYWORD_ONLY: cls.__init__.__kwdefaults__[name] = default # type: ignore[index] else: required = [p for p in params.values() if p.kind in OVERRIDE_KINDS and p.default == inspect._empty] index = list(params).index(name) - len(required) aux = cls.__init__.__defaults__ or () cls.__init__.__defaults__ = aux[:index] + (default,) + aux[index + 1 :] 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:]: if not defaults: break params = inspect.signature(base.__init__).parameters # type: ignore[misc] names = [name for name in defaults if name in params] for name in names: new_param = inspect.Parameter( name=name, kind=inspect.Parameter.KEYWORD_ONLY, default=defaults.pop(name), annotation=params[name].annotation, ) override_parent_params.append(new_param) if not override_parent_params: return # Override defaults for parameters in parent classes' __init__ via a wrapper original_init = cls.__init__ original_sig = inspect.signature(cls.__init__) parameters = list(original_sig.parameters.values()) # Find and pop the **kwargs parameter, if it exists kwargs_param = None if parameters and parameters[-1].kind == inspect.Parameter.VAR_KEYWORD: kwargs_param = parameters.pop() # Add new parameters for param in override_parent_params: parameters.append(param) # Add **kwargs back at the end if kwargs_param: parameters.append(kwargs_param) # Create and set __init__ wrapper with new signature parent_defaults = {p.name: p.default for p in override_parent_params} @wraps(original_init) def wrapper(*args, **kwargs): for name, default in parent_defaults.items(): if name not in kwargs: kwargs[name] = default return original_init(*args, **kwargs) wrapper.__signature__ = original_sig.replace(parameters=parameters) # type: ignore[attr-defined] cls.__init__ = wrapper # type: ignore[method-assign] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_instantiation.py0000644000175100017510000002225015230046455022044 0ustar00runnerrunnerimport inspect from ._common import ( ClassType, InstantiatorCallable, InstantiatorsDictType, applied_instantiation_links, class_instantiators, get_parsing_setting, is_subclass, parser_context, ) from ._namespace import Namespace, get_value_and_parent, split_key __all__ = ["add_instantiator"] _global_class_instantiators: InstantiatorsDictType = {} class InstantiateMethod: def instantiate( self, namespace: Namespace, instantiate_groups: bool = True, ) -> Namespace: """Instantiates all signature components in a configuration namespace. Processes the configuration recursively, converting each signature component registered with the parser into its corresponding Python object: - **Class/subclass type arguments** (``add_argument`` with a class type or ``add_class_arguments``/``add_subclass_arguments``): An object with ``class_path`` and optionally ``init_args`` is replaced by an instance of the referenced class, created by calling ``class_type(**init_args)``. For the case of classes with disabled subclasses, the namespace can have directly the init args without the ``class_path`` + ``init_args`` wrapper. - **Callable type arguments**: A dot-import string pointing to a function or method is resolved to the callable object. When ``class_path``/``init_args`` is given instead and the class instantiates into a callable (or is a subclass of the callable's return type), the result is either a class instance or — when not all call arguments are provided yet — a :func:`functools.partial` bound to the given ``init_args``. - **Instantiation order**: Components are processed in the order determined by argument links applied on instantiation. Args: namespace: The configuration object to use. Must have been produced by one of the ``parse_*`` methods and not modified in a way that breaks the structure expected by the parser. instantiate_groups: Whether class groups should be instantiated. Returns: A new configuration object where every registered signature component has been replaced by its corresponding Python object. """ from ._actions import _ActionConfigLoad, filter_non_parsing_actions from ._core import ArgumentGroup from ._link_arguments import ActionLink from ._subcommands import get_subcommand from ._typehints import ActionTypeHint 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) elif isinstance(action, ActionLink) and isinstance(action.target[1], ActionTypeHint): components.append(action.target[1]) if instantiate_groups: skip = {c.dest for c in components} groups = [ g for g in self._action_groups # type: ignore[attr-defined] if hasattr(g, "instantiate_class") and g.dest not in skip ] components.extend(groups) components.sort(key=lambda x: -len(split_key(x.dest))) # type: ignore[arg-type] order = ActionLink.instantiation_order(self) components = ActionLink.reorder(order, components) cfg = namespace.clone(with_meta=False) unset_sentinel = get_parsing_setting("unset_sentinel") for component in components: ActionLink.apply_instantiation_links(self, cfg, target=component.dest) if isinstance(component, ActionTypeHint): try: value, parent, key = get_value_and_parent(cfg, component.dest) except (KeyError, AttributeError): pass else: if value is not unset_sentinel: with parser_context( parent_parser=self, nested_links=ActionLink.get_nested_links(self, component), class_instantiators=get_class_instantiators(self), applied_instantiation_links=cfg.get("__applied_instantiation_links__"), ): parent[key] = component.instantiate_classes(value) elif hasattr(component, "instantiate_class"): with parser_context( load_value_mode=self.parser_mode, # type: ignore[attr-defined] class_instantiators=get_class_instantiators(self), applied_instantiation_links=cfg.get("__applied_instantiation_links__"), ): component.instantiate_class(component, cfg) ActionLink.apply_instantiation_links(self, cfg, order=order) subcommand, subparser = get_subcommand(self, cfg, fail_no_subcommand=False) # type: ignore[arg-type] if subcommand is not None and subparser is not None: cfg[subcommand] = subparser.instantiate(cfg[subcommand], instantiate_groups=instantiate_groups) return cfg def add_instantiator( instantiator: InstantiatorCallable, class_type: type[ClassType], subclasses: bool = True, prepend: bool = False, ) -> None: """Adds a custom instantiator for a class type. Used by ``ArgumentParser.instantiate``. Instantiator functions are expected to have as signature ``(class_type: Type[ClassType], *args, **kwargs) -> ClassType``. For reference, the default instantiator is ``return class_type(*args, **kwargs)``. In some use cases, the instantiator function might need access to values applied by instantiation links. For this, the instantiator function can have an additional keyword parameter ``applied_instantiation_links: dict``. This parameter will be populated with a dictionary having as keys the targets of the instantiation links and corresponding values that were applied. Args: instantiator: Function that instantiates a class. class_type: The class type to instantiate. subclasses: Whether to instantiate subclasses of ``class_type``. prepend: Whether to prepend the instantiator to the existing instantiators. """ _register_instantiator( _global_class_instantiators, instantiator, class_type, subclasses=subclasses, prepend=prepend ) def _register_instantiator( registry: InstantiatorsDictType, instantiator: InstantiatorCallable, class_type: type[ClassType], subclasses: bool = True, prepend: bool = False, ) -> None: """Registers an instantiator in the given registry dict (in-place).""" key = (class_type, subclasses) items = {k: v for k, v in registry.items() if k != key} if prepend: registry.clear() registry.update({key: instantiator, **items}) else: items[key] = instantiator registry.clear() registry.update(items) def _get_global_class_instantiators() -> InstantiatorsDictType: """Returns the global instantiators registry.""" return _global_class_instantiators def default_class_instantiator(class_type: type[ClassType], *args, **kwargs) -> ClassType: return class_type(*args, **kwargs) class ClassInstantiator: def __init__(self, instantiators: InstantiatorsDictType) -> None: self.instantiators = instantiators def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType: for (cls, subclasses), instantiator in self.instantiators.items(): if class_type is cls or (subclasses and is_subclass(class_type, cls)): param_names = set(inspect.signature(instantiator).parameters) if "applied_instantiation_links" in param_names: applied_links = applied_instantiation_links.get() or set() kwargs["applied_instantiation_links"] = { action.target[0]: action.applied_value for action in applied_links } return instantiator(class_type, *args, **kwargs) return default_class_instantiator(class_type, *args, **kwargs) def get_class_instantiator() -> InstantiatorCallable: instantiators = class_instantiators.get() if not instantiators: return default_class_instantiator return ClassInstantiator(instantiators) def get_class_instantiators(parser) -> InstantiatorsDictType: """Gathers all instantiators applicable to the given parser.""" instantiators = parser._get_parser_instantiators() context_instantiators = class_instantiators.get() if context_instantiators: instantiators = instantiators.copy() instantiators.update({k: v for k, v in context_instantiators.items() if k not in instantiators}) global_instantiators = _get_global_class_instantiators() if global_instantiators: instantiators = instantiators.copy() instantiators.update({k: v for k, v in global_instantiators.items() if k not in instantiators}) return instantiators ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_jsonnet.py0000644000175100017510000001621315230046455020642 0ustar00runnerrunner"""Actions to support jsonnet.""" from typing import Any, Dict from ._actions import _is_action_value_list from ._common import Action, get_parsing_setting, parser_context from ._jsonschema import ActionJsonSchema from ._loaders_dumpers import get_loader_exceptions, load_value from ._optionals import ( _get_config_read_mode, get_jsonschema_exceptions, import_jsonnet, import_jsonschema, pyyaml_available, ) from ._subcommands import find_action from ._typehints import ActionTypeHint from ._util import NoneType, Path, argument_error __all__ = ["ActionJsonnet"] class ActionJsonnet(Action): """Action to parse a Jsonnet, optionally validating against a JSON Schema.""" def __init__( self, ext_vars: str | None = None, schema: str | dict | None = None, **kwargs, ): """Initializer for ActionJsonnet instance. Args: ext_vars: Key where to find the external variables required to parse the Jsonnet. schema: Schema to validate values against. Raises: ValueError: If a parameter is invalid. jsonschema.exceptions.SchemaError: If the schema is invalid. """ if "_validator" not in kwargs: import_jsonnet("ActionJsonnet") if not isinstance(ext_vars, (str, NoneType)): raise ValueError("ext_vars has to be either None or a string.") self._ext_vars = ext_vars if schema is not None: jsonvalidator = import_jsonschema("ActionJsonnet")[1] if isinstance(schema, str): mode = "yaml" if pyyaml_available else "json" with parser_context(load_value_mode=mode): try: schema = load_value(schema) except get_loader_exceptions(mode) as ex: raise ValueError(f"Problems parsing schema: {ex}") from ex jsonvalidator.check_schema(schema) self._validator = ActionJsonSchema._extend_jsonvalidator_with_default(jsonvalidator)(schema) else: self._validator = None else: self._ext_vars = kwargs.pop("_ext_vars") self._validator = kwargs.pop("_validator") super().__init__(**kwargs) def __call__(self, *args, **kwargs): """Parses an argument as Jsonnet using ``ext_vars`` if defined. Raises: TypeError: If the argument is not valid. """ if len(args) == 0: kwargs["_ext_vars"] = self._ext_vars kwargs["_validator"] = self._validator if "help" in kwargs and "%s" in kwargs["help"] and self._validator is not None: import json kwargs["help"] = kwargs["help"] % json.dumps(self._validator.schema, sort_keys=True) return ActionJsonnet(**kwargs) setattr(args[1], self.dest, self._check_type_(args[2], cfg=args[1])) return None def _check_type(self, value, cfg): islist = _is_action_value_list(self) ext_vars = {} if cfg: ext_vars = cfg.get(self._ext_vars, {}) if not islist: value = [value] for num, val in enumerate(value): try: if isinstance(val, str): val = self.parse(val, ext_vars=ext_vars, with_meta=True) elif self._validator is not None: self._validator.validate(val) value[num] = val except (TypeError, RuntimeError) + get_jsonschema_exceptions() + get_loader_exceptions() as ex: elem = "" if not islist else " element " + str(num + 1) raise TypeError(f'Parser key "{self.dest}"{elem}: {ex}') from ex return value if islist else value[0] @staticmethod def _check_ext_vars_action(parser, action): if isinstance(action, ActionJsonnet) and action._ext_vars: ext_vars_action = find_action(parser, action._ext_vars) if not ext_vars_action: raise ValueError(f"No argument found for ext_vars='{action._ext_vars}'") ext_vars_type = isinstance(ext_vars_action, ActionTypeHint) and ext_vars_action._typehint if ext_vars_type not in {dict, Dict}: raise ValueError( f"Type for ext_vars='{action._ext_vars}' argument must be dict, given: {ext_vars_type}" ) if ext_vars_action.default is get_parsing_setting("unset_sentinel"): ext_vars_action.default = {} if not isinstance(ext_vars_action.default, dict): raise ValueError( f"Default value for the ext_vars='{action._ext_vars}' argument " f"must be dict or None, given: {ext_vars_action.default}" ) ext_vars_action.jsonnet_ext_vars = True @staticmethod 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: ext_vars: External variables. Values can be strings or any other basic type. """ if ext_vars is None: ext_vars = {} import json ext_codes = {k: json.dumps(v) for k, v in ext_vars.items() if not isinstance(v, str)} ext_vars = {k: v for k, v in ext_vars.items() if isinstance(v, str)} return ext_vars, ext_codes def parse( self, 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`. Args: jsonnet: Either a path to a Jsonnet file or the Jsonnet content. ext_vars: External variables. Values can be strings or any other basic type. with_meta: Whether to include metadata in config object. Returns: The parsed Jsonnet object. Raises: TypeError: If the input is neither a path to an existent file nor a Jsonnet. """ _jsonnet = import_jsonnet("ActionJsonnet") ext_vars, ext_codes = self.split_ext_vars(ext_vars) fpath = None fname = "snippet" snippet = jsonnet try: fpath = Path(jsonnet, mode=_get_config_read_mode()) except TypeError: pass else: fname = jsonnet(absolute=False) if isinstance(jsonnet, Path) else jsonnet snippet = fpath.read_text() try: with parser_context(load_value_mode="yaml" if pyyaml_available else "json"): values = load_value(_jsonnet.evaluate_snippet(fname, snippet, ext_vars=ext_vars, ext_codes=ext_codes)) except RuntimeError as ex: raise argument_error(f"Problems evaluating Jsonnet '{fname}': {ex}") from ex if self._validator is not None: self._validator.validate(values) if with_meta: if fpath is not None: values["__path__"] = fpath values["__orig__"] = snippet return values ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_jsonschema.py0000644000175100017510000001347215230046455021320 0ustar00runnerrunner"""Action to support jsonschemas.""" import os from ._actions import _is_action_value_list from ._common import Action, parser_context from ._loaders_dumpers import get_loader_exceptions, load_value from ._namespace import remove_meta from ._optionals import ( get_jsonschema_exceptions, import_jsonschema, pyyaml_available, ) from ._util import parse_value_or_config __all__ = ["ActionJsonSchema"] class ActionJsonSchema(Action): """Action to parse option as JSON validated by a JSON Schema.""" def __init__(self, schema: str | dict | None = None, sub_config: bool = True, with_meta: bool = True, **kwargs): """Initializer for ActionJsonSchema instance. Args: schema: Schema to validate values against. sub_config: Whether to try to load JSON from path. with_meta: Whether to include metadata. Raises: ValueError: If a parameter is invalid. jsonschema.exceptions.SchemaError: If the schema is invalid. """ from ._deprecated import action_json_schema_enable_path_deprecation deprecated_val = action_json_schema_enable_path_deprecation(kwargs) if deprecated_val is not None: sub_config = deprecated_val if schema is not None: if isinstance(schema, str): mode = "yaml" if pyyaml_available else "json" with parser_context(load_value_mode=mode): try: schema = load_value(schema) except get_loader_exceptions(mode) as ex: raise ValueError(f"Problems parsing schema: {ex}") from ex jsonvalidator = import_jsonschema("ActionJsonSchema")[1] jsonvalidator.check_schema(schema) self._validator = self._extend_jsonvalidator_with_default(jsonvalidator)(schema) self._sub_config = sub_config self._with_meta = with_meta elif "_validator" not in kwargs: raise ValueError("Expected schema keyword argument.") else: self._validator = kwargs.pop("_validator") self._sub_config = kwargs.pop("_sub_config") self._with_meta = kwargs.pop("_with_meta") super().__init__(**kwargs) def __call__(self, *args, **kwargs): """Parses an argument validating against the corresponding JSON Schema. Raises: TypeError: If the argument is not valid. """ if len(args) == 0: kwargs["_validator"] = self._validator kwargs["_sub_config"] = self._sub_config kwargs["_with_meta"] = self._with_meta if "help" in kwargs and isinstance(kwargs["help"], str) and "%s" in kwargs["help"]: import json kwargs["help"] = kwargs["help"] % json.dumps(self._validator.schema, sort_keys=True) class_type = kwargs.pop("_class_type", ActionJsonSchema) return class_type(**kwargs) val = self._check_type(args[2]) if not self._with_meta: val = remove_meta(val) setattr(args[1], self.dest, val) return None def _check_type(self, value): islist = _is_action_value_list(self) if not islist: value = [value] for num, val in enumerate(value): try: val, fpath = parse_value_or_config(val, enable_path=self._sub_config) path_meta = val.pop("__path__") if isinstance(val, dict) and "__path__" in val else None self._validator.validate(val) if path_meta is not None: val["__path__"] = path_meta if isinstance(val, dict) and fpath is not None: val["__path__"] = fpath value[num] = val except (TypeError, ValueError) + get_jsonschema_exceptions() + get_loader_exceptions() as ex: elem = "" if not islist else " element " + str(num + 1) raise TypeError(f'Parser key "{self.dest}"{elem}: {ex}') from ex return value if islist else value[0] @staticmethod def _extend_jsonvalidator_with_default(validator_class): """Extends a json schema validator so that it fills in default values.""" validate_properties = validator_class.VALIDATORS["properties"] def set_defaults(validator, properties, instance, schema): valid = True for validation in validate_properties(validator, properties, instance, schema): if isinstance(validation, jsonschema.exceptions.ValidationError): valid = False yield validation if valid: for prop, subschema in properties.items(): if "default" in subschema: instance.setdefault(prop, subschema["default"]) jsonschema = import_jsonschema("ActionJsonSchema")[0] return jsonschema.validators.extend(validator_class, {"properties": set_defaults}) def completer(self, prefix, **kwargs): """Used by argcomplete, validates value and shows expected type.""" if chr(int(os.environ["COMP_TYPE"])) == "?": from ._completions import argcomplete_warn_redraw_prompt try: if prefix.strip() == "": raise ValueError() self._validator.validate(load_value(prefix)) msg = "value already valid, " except (ValueError,) + get_jsonschema_exceptions() + get_loader_exceptions(): msg = "value not yet valid, " else: import json schema = json.dumps(self._validator.schema, indent=2, sort_keys=True).replace("\n", "\n ") msg += f"required to be valid according to schema:\n {schema}\n" return argcomplete_warn_redraw_prompt(prefix, msg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_link_arguments.py0000644000175100017510000005530215230046455022206 0ustar00runnerrunner"""Code related to argument linking.""" import re 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 from ._actions import ( Action, ActionConfigFile, _ActionConfigLoad, _ActionPrintConfig, filter_non_parsing_actions, ) from ._common import parser_context from ._namespace import Namespace, split_key, split_key_leaf from ._parameter_resolvers import get_signature_parameters from ._required import clear_required from ._subcommands import ( ActionSubCommands, find_parent_action, find_parent_action_and_subcommand, get_subcommand, get_subcommands, ) from ._type_checking import ArgumentGroup, ArgumentParser def find_parent_or_child_actions( parser: ArgumentParser, key: str, 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: found = [action] else: actions = filter_non_parsing_actions(parser._actions) if exclude is not None: actions = [a for a in actions if not isinstance(a, exclude)] prefix = key + "." found = [a for a in actions if a.dest.startswith(prefix)] return None if found == [] else found def find_subclass_action_or_class_group( parser: ArgumentParser, key: str, exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, ) -> ArgparseAction | ArgumentGroup | None: from ._typehints import ActionTypeHint action = find_parent_action(parser, key, exclude=exclude) if ActionTypeHint.is_subclass_typehint(action): return action key_set = {key, split_key_leaf(key)[0]} for group in parser._action_groups: if getattr(group, "dest", None) in key_set and hasattr(group, "instantiate_class"): return group return None apply_config_skip: ContextVar = ContextVar("apply_config_skip", default=False) @contextmanager def skip_apply_links(): t = apply_config_skip.set(True) try: yield finally: apply_config_skip.reset(t) class DirectedGraph: def __init__(self): self.nodes = [] self.edges_dict = defaultdict(list) def add_edge(self, source, target): for node in [source, target]: if node not in self.nodes: self.nodes.append(node) source_targets_list = self.edges_dict[self.nodes.index(source)] target_index = self.nodes.index(target) if target_index not in source_targets_list: source_targets_list.append(target_index) def get_topological_order(self): exploring = [False] * len(self.nodes) visited = [False] * len(self.nodes) order = [] for source in range(len(self.nodes)): if not visited[source]: self.topological_sort(source, exploring, visited, order) return [self.nodes[n] for n in order] def topological_sort(self, source, exploring, visited, order): exploring[source] = True for target in self.edges_dict[source]: if exploring[target]: raise ValueError( f"Graph has cycles, found while checking {self.nodes[source]} --> {self.nodes[target]}" ) elif not visited[target]: self.topological_sort(target, exploring, visited, order) visited[source] = True exploring[source] = False order.insert(0, source) class ActionLink(Action): def __init__( self, parser, source: str | tuple[str, ...], target: str, compute_fn: Callable | None = None, apply_on: str = "parse", ): if not hasattr(parser, "_links_group"): parser._links_group = parser.add_argument_group("Linked arguments") self.parser = parser self._target = target self._source = source = (source,) if isinstance(source, str) else source self.apply_on = apply_on self.compute_fn = compute_fn self._initial_input_checks(source, target) # Set and check source actions or group exclude = (ActionLink, _ActionConfigLoad, ActionSubCommands, ActionConfigFile) if apply_on == "instantiate": self.source = [(s, find_subclass_action_or_class_group(parser, s, exclude=exclude)) for s in source] for key, action in self.source: if action is None: raise ValueError( f"Links applied on instantiation require source to be a subclass action or a class group: {key}" ) else: self.source = [ (s, find_parent_or_child_actions(parser, s, exclude=exclude)) # type: ignore[misc] for s in source ] # Set and check target action self.target = (target, find_parent_action_or_group(parser, target, exclude=exclude)) for key, action in self.source + [self.target]: if action is None: raise ValueError(f'No action for key "{key}".') assert self.target[1] is not None from ._core import ArgumentGroup from ._typehints import ActionTypeHint is_target_group = isinstance(self.target[1], ArgumentGroup) is_target_subclass = ActionTypeHint.is_subclass_typehint(self.target[1], all_subtypes=False, also_lists=True) valid_target_init_arg = is_target_subclass and target.startswith(f"{self.target[1].dest}.init_args.") valid_target_leaf = self.target[1].dest == target if not valid_target_leaf and is_target_subclass and not valid_target_init_arg: prefix = f"{self.target[1].dest}.init_args." raise ValueError(f'Target key expected to start with "{prefix}", got "{target}".') # Remove target group and child actions if is_target_group: parser._action_groups.remove(self.target[1]) del parser.groups[target] for action in list(parser._actions): if action.dest == target or action.dest.startswith(f"{target}."): parser._actions.remove(action) # Replace target action with link action elif not is_target_subclass or valid_target_leaf: assert isinstance(self.target[1], ArgparseAction) for key in self.target[1].option_strings: parser._option_string_actions[key] = self parser._actions[parser._actions.index(self.target[1])] = self for group in parser._action_groups: if self.target[1] in group._group_actions: group._group_actions.remove(self.target[1]) if is_target_subclass: help_dest = f"{self.target[1].dest}.help" for action in group._group_actions: if action.dest == help_dest: group._group_actions.remove(action) break if group._group_actions and all(isinstance(a, _ActionConfigLoad) for a in group._group_actions): group.description = ( f"Group '{group._group_actions[0].dest}': All arguments are derived from links." ) group._group_actions.clear() # Remove target from required clear_required(parser, target) if is_target_subclass and not valid_target_leaf: sub_add_kwargs = self.target[1].sub_add_kwargs # type: ignore[union-attr] if "linked_targets" not in sub_add_kwargs: sub_add_kwargs["linked_targets"] = set() subtarget = target.split(".init_args.", 1)[1] sub_add_kwargs["linked_targets"].add(subtarget) # Add link action to group to show in help parser._links_group._group_actions.append(self) # Check instantiation link does not create cycle if apply_on == "instantiate": try: self.instantiation_order(parser) except ValueError as ex: raise ValueError(f"Invalid link {source[0]} --> {target}: {ex}") from ex # Initialize link action if compute_fn is None: link_str = source[0] else: link_str = getattr(compute_fn, "__name__", str(compute_fn)) + "(" + ", ".join(source) + ")" link_str += " --> " + target 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." else: if is_target_group: type_attr = self.target[1].group_class # type: ignore[union-attr] help_str = self.target[1].title # type: ignore[union-attr] else: assert isinstance(self.target[1], ArgparseAction) type_attr = getattr(self.target[1], "_typehint", self.target[1].type) help_str = self.target[1].help if help_str is None: help_str = f"Target '{self.target[1].dest}' lacks type and help" super().__init__( [link_str], dest=target, default=SUPPRESS, metavar=f"[applied on {self.apply_on}]", type=type_attr, help=help_str, ) def get_kwargs(self) -> dict: return { "source": self._source, "target": self._target, "apply_on": self.apply_on, "compute_fn": self.compute_fn, } def _initial_input_checks(self, source, target): # Check apply_on if self.apply_on not in {"parse", "instantiate"}: raise ValueError("apply_on must be 'parse' or 'instantiate'.") # Check compute function if self.compute_fn is None and not (isinstance(source, str) or len(source) == 1): raise ValueError("Multiple source keys requires a compute function.") if self.apply_on == "parse": # Check source link_actions = self.parser._links_group._group_actions existing_targets = {a.target[0] for a in link_actions} if target in existing_targets: raise ValueError(f'Target "{target}" is already a target of another link.') for src in [source] if isinstance(source, str) else source: if src in existing_targets: raise ValueError(f'Source "{src}" not allowed since it is the target of another link.') # Check target existing_sources = {s[0] for a in link_actions for s in a.source if a.apply_on == "parse"} if target in existing_sources: raise ValueError(f'Target "{target}" not allowed since it is the source of another link.') def __call__(self, *args, **kwargs): source = ", ".join(s[0] for s in self.source) raise TypeError(f'Linked "{self.target[0]}" must be given via "{source}".') def _check_type(self, value, cfg=None): return self.parser._check_value_key(self.target[1], value, self.target[0], cfg) def call_compute_fn(self, args): try: assert callable(self.compute_fn) return self.compute_fn(*args) except Exception as ex: link = self.option_strings[0] args = ", ".join(str(a) for a in args) raise ValueError(f"Call to compute_fn of link '{link}' with args ({args}) failed: {ex}") from ex @staticmethod def apply_parsing_links(parser: ArgumentParser, cfg: Namespace) -> None: if apply_config_skip.get() or _ActionPrintConfig.is_print_config_requested(parser): return subcommand, subparser = get_subcommand(parser, cfg, fail_no_subcommand=False) if subcommand and subcommand in cfg: ActionLink.apply_parsing_links(subparser, cfg[subcommand]) # type: ignore[arg-type] if not hasattr(parser, "_links_group"): return for action in get_link_actions(parser, "parse"): from ._typehints import ActionTypeHint args = [] skip_link = False for source_key, source_action in action.source: if ActionTypeHint.is_subclass_typehint(source_action[0]) and source_key not in cfg: # type: ignore[index] parser.logger.debug( f"Link '{action.option_strings[0]}' ignored since source '{source_key}' not found in namespace." ) skip_link = True break for source_action_n in [a for a in source_action if a.dest in cfg]: # type: ignore[union-attr] parser._check_value_key(source_action_n, cfg[source_action_n.dest], source_action_n.dest, None) args.append(cfg[source_key]) if skip_link: continue if action.compute_fn is None: value = args[0] # Automatic namespace to dict based on link target type hint target_key, target_action = action.target if isinstance(value, Namespace) and isinstance(target_action, ActionTypeHint): same_key = target_key == target_action.dest if ( same_key and target_action.is_mapping_typehint(target_action._typehint) ) or target_action.is_init_arg_mapping_typehint(target_key, cfg): value = value.as_dict() else: # Automatic namespace to dict based on compute_fn param type hint params = get_signature_parameters(action.compute_fn) for n, param in enumerate(params): if ( n < len(args) and isinstance(args[n], Namespace) and ActionTypeHint.is_mapping_typehint(param.annotation) ): args[n] = args[n].as_dict() # Compute value value = action.call_compute_fn(args) ActionLink.set_target_value(action, value, cfg, parser.logger) parser.logger.debug(f"Applied link '{action.option_strings[0]}'.") @staticmethod def apply_instantiation_links(parser, cfg, target=None, order=None): if not hasattr(parser, "_links_group"): return applied_key = "__applied_instantiation_links__" applied_links = cfg.pop(applied_key) if applied_key in cfg else set() link_actions = get_link_actions(parser, "instantiate", skip=applied_links) if order and link_actions: link_actions = ActionLink.reorder(order, link_actions) for action in link_actions: target_key = action.target[0] if not ( order or target_key == target or target_key.startswith(f"{target}.") ) or is_nested_instantiation_link(action): continue source_objects = [] for source_key, source_action in action.source: source_object = cfg[source_action.dest] if source_key == source_action.dest: source_objects.append(source_object) else: attr = split_key_leaf(source_key)[1] from ._typehints import ActionTypeHint if ActionTypeHint.is_subclass_typehint(source_action) and not hasattr(source_object, attr): parser.logger.debug( f"Link '{action.option_strings[0]}' ignored since attribute '{attr}' not found " f"in source {source_object}." ) continue source_objects.append(getattr(source_object, attr)) if not source_objects: continue elif action.compute_fn is None: value = source_objects[0] else: value = action.call_compute_fn(source_objects) ActionLink.set_target_value(action, value, cfg, parser.logger) action.applied_value = value applied_links.add(action) parser.logger.debug(f"Applied link '{action.option_strings[0]}'.") if target: cfg[applied_key] = applied_links @staticmethod def get_nested_links(parser, action): def trim_param_keys(params: dict): params = params.copy() params["source"] = tuple(k[len(f"{action.dest}.") :] for k in params["source"]) params["target"] = params["target"][len(f"{action.dest}.init_args.") :] return params links = [] for link in get_link_actions(parser, "instantiate"): if link.target[1] is action and is_nested_instantiation_link(link): links.append(trim_param_keys(link.get_kwargs())) return links @staticmethod def set_target_value(action: "ActionLink", value: Any, cfg: Namespace, logger) -> None: target_key, target_action = action.target assert target_action from ._typehints import ActionTypeHint if ActionTypeHint.is_subclass_typehint(target_action, all_subtypes=False, also_lists=True): if target_key == target_action.dest: target_action._check_type(value) # type: ignore[union-attr] else: assert isinstance(target_action.dest, str) parent = cfg.get(target_action.dest) child_key = target_key[len(target_action.dest) + 1 :] if isinstance(parent, list) and any(isinstance(i, Namespace) and child_key in i for i in parent): for item in parent: if child_key in item: item[child_key] = value return if target_key not in cfg: logger.debug(f"Link '{action.option_strings[0]}' ignored since target not found.") return cfg[target_key] = value @staticmethod def instantiation_order(parser): actions = get_link_actions(parser, "instantiate") if actions: targets = set() graph = DirectedGraph() # Add instantiation links as edges for action in actions: target = re.sub(r"\.init_args$", "", split_key_leaf(action.target[0])[0]) for _, source_action in action.source: graph.add_edge(source_action.dest, target) targets.add(target) # Add instantiation target prefixes as edges targets = sorted(targets, key=lambda x: len(split_key(x))) seen_targets = {targets[0]} for target in targets[1:]: parts = [x.replace("|", ".") for x in target.replace("init_args.", "init_args|").split(".")] for num in range(len(parts) - 1): target_prefix = ".".join(parts[: num + 1]) if target_prefix in seen_targets: graph.add_edge(target, target_prefix) seen_targets.add(target) return graph.get_topological_order() return [] @staticmethod def reorder(order, components): ordered = [] for key in order: after = [] for component in components: if key == component.dest or component.dest.startswith(key + "."): ordered.append(component) else: after.append(component) components = after return ordered + components @staticmethod def strip_link_target_keys(parser, cfg): def del_target_key(target_key): cfg.pop(target_key, None) if "." not in target_key: return parent_key, _ = split_key_leaf(target_key) if parent_key in cfg and not cfg[parent_key]: del cfg[parent_key] for action in [a for a in parser._actions if isinstance(a, ActionLink)]: del_target_key(action.target[0]) from ._typehints import ActionTypeHint for action in [a for a in parser._actions if isinstance(a, ActionTypeHint) and hasattr(a, "sub_add_kwargs")]: for key in action.sub_add_kwargs.get("linked_targets", []): del_target_key(f"{action.dest}.init_args.{key}") with parser_context(single_subcommand=False): subcommands, subparsers = get_subcommands(parser, cfg) if subcommands is not None: for num, subcommand in enumerate(subcommands): if subcommand in cfg: ActionLink.strip_link_target_keys(subparsers[num], cfg[subcommand]) def find_parent_action_or_group( parser: ArgumentParser, key: str, 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] return action_or_group def get_link_actions(parser: ArgumentParser, apply_on: str, skip=set()) -> list[ActionLink]: if not hasattr(parser, "_links_group"): return [] return [a for a in parser._links_group._group_actions if a.apply_on == apply_on and a not in skip] def is_nested_instantiation_link(action: ActionLink) -> bool: from ._typehints import ActionTypeHint target_key, target_action = action.target assert target_action return ( target_key.startswith(f"{target_action.dest}.init_args.") and ActionTypeHint.is_subclass_typehint(target_action) and all(a is target_action for _, a in action.source) and all(k.startswith(f"{target_action.dest}.") for k, _ in action.source) ) class ArgumentLinking: """Method for linking arguments.""" def link_arguments( self, source: str | tuple[str, ...], target: str, compute_fn: Callable | None = None, apply_on: str = "parse", ): """Makes an argument value be derived from the values of other arguments. Refer to :ref:`argument-linking` for a detailed explanation and examples. Args: source: Key(s) from which the target value is derived. target: Key to where the value is set. compute_fn: Function to compute target value from source. apply_on: At what point to set target value, 'parse' or 'instantiate'. Raises: ValueError: If an invalid parameter is given. """ ActionLink(self, source, target, compute_fn, apply_on) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_loaders_dumpers.py0000644000175100017510000002770615230046455022363 0ustar00runnerrunner"""Code related to loading and dumping.""" import inspect import re from argparse import HelpFormatter from collections.abc import Callable from contextlib import suppress from typing import Any from ._common import Unset, get_parsing_setting, load_value_mode, parent_parser from ._optionals import ( import_jsonnet, import_toml_dumps, import_toml_loads, omegaconf_support, pyyaml_available, ruamel_support, ) from ._type_checking import ArgumentParser __all__ = [ "get_loader", "set_loader", "set_dumper", ] not_loaded = object() yaml_default_loader = None def load_basic(value): value = value.strip() if value == "true": return True if value == "false": return False if value == "null": return None try: if value.isdigit() or (value.startswith("-") and value[1:].isdigit()): return int(value) if value.replace(".", "", 1).replace("e", "", 1).replace("-", "", 2).isdigit() and ( "e" in value or "." in value ): return float(value) except ValueError: pass # if parsing fails, return not_loaded return not_loaded def get_yaml_default_loader(): global yaml_default_loader if yaml_default_loader: return yaml_default_loader import yaml class DefaultLoader(getattr(yaml, "CSafeLoader", yaml.SafeLoader)): pass # https://stackoverflow.com/a/37958106/2732151 def remove_implicit_resolver(cls, tag_to_remove): if "yaml_implicit_resolvers" not in cls.__dict__: cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy() for first_letter, mappings in cls.yaml_implicit_resolvers.items(): cls.yaml_implicit_resolvers[first_letter] = [ (tag, regexp) for tag, regexp in mappings if tag != tag_to_remove ] remove_implicit_resolver(DefaultLoader, "tag:yaml.org,2002:timestamp") remove_implicit_resolver(DefaultLoader, "tag:yaml.org,2002:float") DefaultLoader.add_implicit_resolver( "tag:yaml.org,2002:float", re.compile( """^(?: [-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)? |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+) |\\.[0-9_]+(?:[eE][-+][0-9]+)? |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]* |[-+]?\\.(?:inf|Inf|INF) |\\.(?:nan|NaN|NAN))$""", re.X, ), list("-+0123456789."), ) yaml_default_loader = DefaultLoader return yaml_default_loader def yaml_load(stream): import yaml value = yaml.load(stream, Loader=get_yaml_default_loader()) if isinstance(value, dict) and value and all(v is None for v in value.values()): if len(value) == 1 and stream.strip() == next(iter(value)) + ":": value = stream else: keys = set(stream.strip(" {}").replace(" ", "").split(",")) if len(keys) > 0 and keys == set(value): value = stream return value def json_load(value): import json return json.loads(value) def toml_load(value): toml_loads, _ = import_toml_loads("toml_load") return toml_loads(value) def jsonnet_load(stream, path="", ext_vars=None): from ._jsonnet import ActionJsonnet ext_vars, ext_codes = ActionJsonnet.split_ext_vars(ext_vars) _jsonnet = import_jsonnet("jsonnet_load") try: val = _jsonnet.evaluate_snippet(path, stream, ext_vars=ext_vars, ext_codes=ext_codes) except RuntimeError: try: return json_or_yaml_load(stream) except json_or_yaml_loader_exceptions as ex: raise ValueError(str(ex)) from ex return json_or_yaml_load(val) loaders: dict[str, Callable] = { "yaml": yaml_load, "json": json_load, "toml": toml_load, } loader_exceptions: dict[str, tuple[type[Exception], ...]] = {} loader_json_superset: dict[str, bool] = { "yaml": True, "json": True, "toml": False, } loader_params: dict[str, set[str]] = {} def get_load_value_mode() -> str: mode = load_value_mode.get() if mode is None: parser = parent_parser.get() assert parser is not None mode = parser.parser_mode return mode 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: if mode == "yaml": loader_exceptions[mode] = (__import__("yaml").YAMLError,) elif mode == "json": loader_exceptions[mode] = (__import__("json").JSONDecodeError,) elif mode == "toml": loader_exceptions[mode] = (import_toml_loads("get_loader_exceptions")[1],) elif mode == "jsonnet": return get_loader_exceptions("yaml" if pyyaml_available else "json") + (ValueError,) return loader_exceptions[mode] def json_or_yaml_load(value): if pyyaml_available: if isinstance(value, str) and value.strip() == "": return value return yaml_load(value) return json_load(value) def basic_json_or_yaml_load(value): loaded_value = load_basic(value) if loaded_value is not not_loaded: return loaded_value return json_or_yaml_load(value) json_or_yaml_loader_exceptions = get_loader_exceptions("yaml" if pyyaml_available else "json") def load_list_or_dict(value: str): strip = value.strip() if (strip.startswith("[") and strip.endswith("]")) or (strip.startswith("{") and strip.endswith("}")): import json with suppress(json.JSONDecodeError): return json.loads(strip) return not_loaded def load_value(value: str, simple_types: bool = False, **kwargs): if value.strip() == "-": return value loaded_value = load_basic(value) mode = get_load_value_mode() if loaded_value is not_loaded and not loader_json_superset.get(mode, True): loaded_value = load_list_or_dict(value) if loaded_value is not_loaded: if mode not in loaders and mode in {"omegaconf", "omegaconf+"}: set_omegaconf_loader(mode) loader = loaders[mode] load_kwargs = {} if kwargs and mode in loader_params: params = loader_params[mode] load_kwargs = {k: v for k, v in kwargs.items() if k in params} loaded_value = loader(value, **load_kwargs) if not simple_types and isinstance(loaded_value, (int, float, bool, str)): loaded_value = value return loaded_value dump_yaml_kwargs = { "default_flow_style": False, "allow_unicode": True, "sort_keys": False, } dump_json_kwargs = { "ensure_ascii": False, "sort_keys": False, } def replace_unset(data): """Recursively replaces Unset sentinel values with the serialized string ``"==UNSET=="``.""" if get_parsing_setting("unset_sentinel") is None: return data if data is Unset: return Unset._SERIALIZED if isinstance(data, dict): return {k: replace_unset(v) for k, v in data.items()} if isinstance(data, list): return [replace_unset(v) for v in data] return data def yaml_dump(data): import yaml return yaml.safe_dump(data, **dump_yaml_kwargs) def yaml_comments_dump(data, parser): dump = dumpers["yaml"](data) formatter_class = create_help_formatter_with_comments(parser.formatter_class) formatter = formatter_class(parser.prog) return formatter.add_yaml_comments(dump) def json_compact_dump(data): import json return json.dumps(data, separators=(",", ":"), **dump_json_kwargs) def json_indented_dump(data): import json return json.dumps(data, indent=2, **dump_json_kwargs) + "\n" def toml_dump(data): toml_dumps = import_toml_dumps("toml_dump") return toml_dumps(data) dumpers: dict[str, Callable] = { "yaml": yaml_dump, "json": json_compact_dump, "json_compact": json_compact_dump, "json_indented": json_indented_dump, "toml": toml_dump, "jsonnet": json_indented_dump, } if ruamel_support: dumpers["yaml_comments"] = yaml_comments_dump comment_prefix: dict[str, str] = { "yaml": "# ", "yaml_comments": "# ", "jsonnet": "// ", "toml": "# ", } def check_valid_dump_format(dump_format: str): if dump_format not in {"parser_mode"}.union(set(dumpers)): raise ValueError(f'Unknown output format "{dump_format}".') def dump_using_format(parser: ArgumentParser, data: dict, dump_format: str, with_comments: bool = False) -> str: if dump_format == "parser_mode": dump_format = parser.parser_mode if parser.parser_mode in dumpers else "yaml" if with_comments: if f"{dump_format}_comments" not in dumpers: if dump_format == "yaml": raise ValueError("ruamel.yaml is required for dumping YAML with comments.") raise ValueError(f"Dumping with comments is not supported for format '{dump_format}'.") dump_format = f"{dump_format}_comments" data = replace_unset(data) args = (data, parser) if dump_format.endswith("_comments") else (data,) dump = dumpers[dump_format](*args) if parser.dump_header and comment_prefix.get(dump_format): prefix = comment_prefix[dump_format] header = "\n".join(prefix + line for line in parser.dump_header) dump = f"{header}\n{dump}" return dump def set_loader( mode: str, loader_fn: Callable[[str], Any], exceptions: tuple[type[Exception], ...] = (), json_superset: bool = True, ): """Sets the value loader function to be used when parsing with a certain mode. The ``loader_fn`` function must accept as input a single str type parameter and return any of the basic types {str, bool, int, float, list, dict, None}. If this function is not based on PyYAML for things to work correctly the exceptions types that can be raised when parsing a value fails should be provided. Args: mode: The parser mode for which to set its loader function. Example: "yaml". loader_fn: The loader function to set. Example: ``yaml.safe_load``. exceptions: Exceptions that the loader can raise when load fails. Example: (yaml.YAMLError,). json_superset: Whether the loader can load JSON data. """ loaders[mode] = loader_fn loader_exceptions[mode] = exceptions loader_json_superset[mode] = json_superset params = set(list(inspect.signature(loader_fn).parameters)[1:]) if params: loader_params[mode] = params def get_loader(mode: str): """Returns the current loader function for a given mode.""" return loaders[mode] def set_dumper(format_name: str, dumper_fn: Callable[[Any], str]): """Sets the dumping function for a given format name. Args: format_name: Name to use for dumping with this function. Example: ``yaml_custom``. dumper_fn: The dumper function to set. Example: ``yaml.safe_dump``. """ dumpers[format_name] = dumper_fn def set_omegaconf_loader(mode="omegaconf"): if omegaconf_support and mode not in loaders: from ._optionals import get_omegaconf_loader loader = get_omegaconf_loader(mode) set_loader(mode, loader, get_loader_exceptions("yaml")) set_loader("jsonnet", jsonnet_load, get_loader_exceptions("jsonnet")) def create_help_formatter_with_comments(formatter_class: type[HelpFormatter]) -> type[HelpFormatter]: """Creates a dynamic class that combines a formatter with YAML comment functionality. Args: formatter_class: The base formatter class to extend. Returns: A new class that inherits from both the formatter and YAMLCommentFormatter. """ from ._formatters import YAMLCommentFormatter class DynamicHelpFormatter(formatter_class): # type: ignore[valid-type,misc] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._yaml_formatter = YAMLCommentFormatter(self) def add_yaml_comments(self, cfg: str) -> str: """Adds help text as yaml comments.""" return self._yaml_formatter.add_yaml_comments(cfg) return DynamicHelpFormatter ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_namespace.py0000644000175100017510000002666415230046455021131 0ustar00runnerrunner"""Classes and functions related to namespace objects.""" import argparse from collections import OrderedDict from collections.abc import Iterator from typing import Any __all__ = ["Namespace"] subclasses_disabled_meta_key = "__subclasses_disabled__" meta_keys = {"__default_config__", "__path__", "__orig__", subclasses_disabled_meta_key} class NSKeyError(KeyError): def __str__(self): return str(self.args[0]) def split_key(key: str) -> list[str]: return key.split(".") def split_key_root(key: str) -> list[str]: return key.split(".", 1) def split_key_leaf(key: str) -> list[str]: return key.rsplit(".", 1) def is_meta_key(key: str) -> bool: leaf_key = split_key_leaf(key)[-1] return leaf_key in meta_keys def recreate_branches(data, skip_keys=None): new_data = data if isinstance(data, (Namespace, dict)) and not isinstance(data, OrderedDict): new_data = type(data)() for key, val in getattr(data, "__dict__", data).items(): if skip_keys is None or key not in skip_keys: new_data[key] = recreate_branches(val, skip_keys) elif isinstance(data, list): new_data = [recreate_branches(v, skip_keys) for v in data] return new_data class Namespace(argparse.Namespace): """Extension of argparse's Namespace to support nesting and subscript access.""" def __init__(self, *args, **kwargs): """Initializer for Namespace instance. Instantiating a Namespace with initial values most commonly is done by providing keyword arguments, e.g. ``Namespace(name1=value1, name2=value2)``. Alternatively a single positional ``Namespace`` or ``dict`` object can be given. """ if len(args) == 0: super().__init__(**kwargs) else: if len(kwargs) != 0 or len(args) != 1 or not isinstance(args[0], (argparse.Namespace, dict)): raise ValueError("Expected a single positional parameter of type Namespace or dict.") 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, "Namespace | None", str]: """Parses a key for the nested namespace. Args: key: The key that is being parsed. Returns: Tuple with three elements corresponding to: - The leaf key. - The parent namespace object. - The parent namespace key. Raises: KeyError: When given invalid key. """ if not isinstance(key, str): raise NSKeyError(f"Key must be a string, got: {key!r}.") if " " in key: raise NSKeyError(f'Spaces not allowed in keys: "{key}".') key_split = split_key(key) if any(k == "" for k in key_split): raise NSKeyError(f'Empty nested key: "{key}".') key_split = [add_clash_mark(k) for k in key_split] leaf_key = key_split[-1] parent_ns: Namespace = self parent_key = "" if len(key_split) > 1: parent_key = ".".join(key_split[:-1]) for subkey in key_split[:-1]: if hasattr(parent_ns, subkey) or (isinstance(parent_ns, dict) and subkey in parent_ns): parent_ns = parent_ns[subkey] if parent_ns is not None and not isinstance(parent_ns, (Namespace, dict)): return leaf_key, None, parent_key else: return leaf_key, None, parent_key return leaf_key, parent_ns, parent_key def _parse_required_key(self, key: str) -> tuple[str, "Namespace", str]: """Same as _parse_key but raises KeyError if key not found.""" leaf_key, parent_ns, parent_key = self._parse_key(key) if parent_ns is None or not hasattr(parent_ns, leaf_key): raise NSKeyError(f'Key "{key}" not found in namespace.') return leaf_key, parent_ns, parent_key def _create_nested_namespace(self, key: str) -> "Namespace": """Creates a nested namespace object. Args: key: The key where the nested namespace is created. Returns: The created nested namespace. """ parent_ns = self for key in split_key(key): if not isinstance(getattr(parent_ns, key, None), Namespace): setattr(parent_ns, key, Namespace()) parent_ns = getattr(parent_ns, key) return parent_ns def __setattr__(self, name: str, value: Any) -> None: """Sets an attribute to a possibly nested namespace.""" if "." in name: self.__setitem__(name, value) else: super().__setattr__(add_clash_mark(name), value) def __setitem__(self, key: str, item: Any) -> None: """Sets an item to a possibly nested namespace.""" leaf_key, parent_ns, parent_key = self._parse_key(key) if parent_ns is None: parent_ns = self._create_nested_namespace(parent_key) if isinstance(parent_ns, dict): parent_ns[leaf_key] = item else: setattr(parent_ns, leaf_key, item) def __getitem__(self, key: str) -> Any: """Gets an item from a possibly nested namespace.""" leaf_key, parent_ns, _ = self._parse_required_key(key) return getattr(parent_ns, leaf_key) def __delitem__(self, key: str) -> None: """Deletes an item from a possibly nested namespace.""" leaf_key, parent_ns, _ = self._parse_key(key) del parent_ns.__dict__[leaf_key] def __contains__(self, key: str) -> bool: """Checks if an item is set possibly in a nested namespace.""" if not isinstance(key, str): return False try: leaf_key, parent_ns, _ = self._parse_required_key(key) except KeyError: return False return leaf_key in parent_ns.__dict__ def __bool__(self) -> bool: """Returns False if namespace is empty, otherwise True.""" return bool(self.__dict__) def as_dict(self) -> dict[str, Any]: """Converts the nested namespaces into nested dictionaries.""" dic = {} for key, val in vars(self).items(): if isinstance(val, Namespace): val = val.as_dict() elif isinstance(val, dict) and val != {} and all(isinstance(v, Namespace) for v in val.values()): val = {k: v.as_dict() for k, v in val.items()} elif isinstance(val, list) and val != [] and all(isinstance(v, Namespace) for v in val): val = [v.as_dict() for v in val] dic[del_clash_mark(key)] = val return dic def as_flat(self) -> argparse.Namespace: """Converts the nested namespaces into a single argparse flat namespace.""" flat = argparse.Namespace() for key, val in self.items(): setattr(flat, key, val) return flat def items(self, branches: bool = False, nested: bool = True) -> Iterator[tuple[str, Any]]: """Returns a generator of all leaf (key, value) items, optionally including branches.""" for key, val in vars(self).items(): key = del_clash_mark(key) if isinstance(val, Namespace): if branches: yield key, val if not nested: continue for subkey, subval in val.items(branches): yield key + "." + del_clash_mark(subkey), subval else: yield key, val def keys(self, branches: bool = False, nested: bool = True) -> Iterator[str]: """Returns a generator of all leaf keys, optionally including branches.""" for key, _ in self.items(branches=branches, nested=nested): yield key def values(self, branches: bool = False) -> Iterator[Any]: """Returns a generator of all leaf values, optionally including branches.""" for _, val in self.items(branches): yield val def clone(self, with_meta: bool = True) -> "Namespace": """Creates an new copy of the nested namespace. Args: with_meta: Whether to include metadata keys in the copy. """ return recreate_branches(self, skip_keys=None if with_meta else meta_keys) 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: value: A namespace to update multiple values or other type to set in a single key. key: Branch key where to set the value. Required if value is not namespace. only_unset: Whether to only set the value if not set in namespace. """ if not isinstance(value, Namespace): if not key: raise NSKeyError("Key is required if value not a Namespace.") if not only_unset or key not in self: self[key] = value else: if key and not isinstance(self.get(key), Namespace): self[key] = Namespace() prefix = key + "." if key else "" for subkey, subval in value.items(): if not only_unset or prefix + subkey not in self: self[prefix + subkey] = subval return self def get(self, key: str, default: Any = None) -> Any: """Returns the value for the given key if it exists, otherwise the default.""" try: return self[key] except (KeyError, TypeError): return default def pop(self, key: str, default: Any = None) -> Any: """Removes the given key and returns its value if it exists, otherwise the default.""" leaf_key, parent_ns, _ = self._parse_key(key) if not parent_ns: return default return parent_ns.__dict__.pop(leaf_key, default) clash_names: set[str] = set(dir(Namespace)) clash_mark = "\u200b" def add_clash_mark(key: str) -> str: if key in clash_names: key = clash_mark + key return key def del_clash_mark(key: str) -> str: if key[0] == clash_mark: key = key[1:] return key def expand_dict(data: dict) -> Namespace: for k, v in data.items(): if isinstance(v, dict) and all(isinstance(k, str) for k in v): data[k] = expand_dict(v) elif isinstance(v, list): for nn, vv in enumerate(v): if isinstance(vv, dict) and all(isinstance(k, str) for k in vv): data[k][nn] = expand_dict(vv) return Namespace(**data) def dict_to_namespace(data: dict[str, Any]) -> Namespace: data = recreate_branches(data) 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))) return keys def get_value_and_parent(namespace: Namespace, key: str) -> tuple[Any, Namespace, str]: leaf_key, parent_ns, _ = namespace._parse_required_key(key) return parent_ns[leaf_key], parent_ns, leaf_key # Temporal to provide backward compatibility in pytorch-lightning from importlib.util import find_spec # noqa: E402 if find_spec("yaml"): import yaml yaml.SafeDumper.add_representer(Namespace, lambda d, x: d.represent_mapping("tag:yaml.org,2002:map", x.as_dict())) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_optionals.py0000644000175100017510000003436715230046455021204 0ustar00runnerrunner"""Code related to optional dependencies.""" import inspect import os import re import sys from contextlib import contextmanager from copy import deepcopy from dataclasses import is_dataclass from importlib.metadata import version from importlib.util import find_spec from typing import Any, Union pyyaml_available = bool(find_spec("yaml")) toml_load_available = bool(find_spec("toml") or find_spec("tomllib")) toml_dump_available = bool(find_spec("toml")) typing_extensions_support = find_spec("typing_extensions") is not None typeshed_client_support = find_spec("typeshed_client") is not None jsonschema_support = find_spec("jsonschema") is not None jsonnet_support = find_spec("_jsonnet") is not None url_support = find_spec("requests") is not None docstring_parser_support = find_spec("docstring_parser") is not None fsspec_support = find_spec("fsspec") is not None ruamel_support = bool(find_spec("ruamel") and find_spec("ruamel.yaml")) omegaconf_support = find_spec("omegaconf") is not None attrs_support = find_spec("attrs") is not None _config_read_mode = "fr" _docstring_parse_options = { "style": None, "attribute_docstrings": False, } def typing_extensions_import(name): if typing_extensions_support: return getattr(__import__("typing_extensions"), name, False) else: return getattr(__import__("typing"), name, False) def capture_typing_extension_shadows(typehint, name: str, *collections) -> None: """ Ensure different origins for types in typing_extensions are captured. """ if (typehint is False or getattr(typehint, "__module__", None) == "typing_extensions") and hasattr( __import__("typing"), name ): for collection in collections: collection.add(getattr(__import__("typing"), name)) def final(cls): """Decorator to make a class ``final``, i.e., it shouldn't be subclassed. It is the same as ``typing.final`` or an equivalent implementation depending on the python version and whether typing-extensions is installed. """ setattr(cls, "__final__", True) return cls def is_compatible_final(final) -> bool: @final class FinalClass: pass return getattr(FinalClass, "__final__", False) # __final__ available in stdlib from python 3.11 fallback_final = final stdlib_final = typing_extensions_import("final") if stdlib_final and is_compatible_final(stdlib_final) and "SPHINX_BUILD" not in os.environ: final = stdlib_final def import_typeshed_client(): if typeshed_client_support: import typeshed_client return typeshed_client else: return __import__("argparse").Namespace(ImportedInfo=object, ModulePath=object, Resolver=object) class UndefinedException(Exception): pass def get_jsonschema_exceptions(): from jsonschema.exceptions import ValidationError return (ValidationError,) @contextmanager def missing_package_raise(package, importer): try: yield None except ImportError as ex: raise ImportError(f"{package} package is required by {importer} :: {ex}") from ex def import_toml_loads(importer): if find_spec("tomllib"): import tomllib return tomllib.loads, tomllib.TOMLDecodeError else: with missing_package_raise("toml", importer): import toml return toml.loads, toml.TomlDecodeError def import_toml_dumps(importer): with missing_package_raise("toml", importer): import toml return toml.dumps def import_jsonschema(importer): with missing_package_raise("jsonschema", importer): import jsonschema return jsonschema, jsonschema.Draft7Validator def import_jsonnet(importer): with missing_package_raise("jsonnet", importer): import _jsonnet return _jsonnet def import_requests(importer): with missing_package_raise("requests", importer): import requests return requests def import_docstring_parser(importer): with missing_package_raise("docstring-parser", importer): import docstring_parser return docstring_parser def import_fsspec(importer): with missing_package_raise("fsspec", importer): import fsspec return fsspec def import_ruamel(importer): with missing_package_raise("ruamel.yaml", importer): import ruamel.yaml return ruamel.yaml def _set_config_read_mode( urls_enabled: bool = False, fsspec_enabled: bool = False, ): """Enables/disables optional config read modes. Args: urls_enabled: Whether to read config files from URLs using requests package. fsspec_enabled: Whether to read config files from fsspec supported file systems. """ imports = { "u": import_requests, "s": import_fsspec, } def update_mode(flag, enabled): global _config_read_mode if enabled: imports[flag]("_set_config_read_mode") if flag not in _config_read_mode: _config_read_mode = _config_read_mode.replace("f", "f" + flag) else: _config_read_mode = _config_read_mode.replace(flag, "") update_mode("u", urls_enabled) update_mode("s", fsspec_enabled) def _get_config_read_mode() -> str: """Returns the current config reading mode.""" return _config_read_mode def _set_docstring_parse_options(style=None, attribute_docstrings: bool | None = None): """Sets options for docstring parsing. Args: style (docstring_parser.DocstringStyle): The docstring style to expect. attribute_docstrings: Whether to parse attribute docstrings (slower). """ global _docstring_parse_options dp = import_docstring_parser("_set_docstring_parse_options") if style is not None: if not isinstance(style, dp.DocstringStyle): raise ValueError(f"Expected style to be of type {dp.DocstringStyle}.") _docstring_parse_options["style"] = style if attribute_docstrings is not None: if not isinstance(attribute_docstrings, bool): raise ValueError("Expected attribute_docstrings to be boolean.") _docstring_parse_options["attribute_docstrings"] = attribute_docstrings def get_docstring_parse_options(): if _docstring_parse_options["style"] is None: dp = import_docstring_parser("get_docstring_parse_options") _docstring_parse_options["style"] = dp.DocstringStyle.AUTO return _docstring_parse_options def parse_docstring(component, params=False, logger=None): dp = import_docstring_parser("parse_docstring") options = get_docstring_parse_options() try: if params and options["attribute_docstrings"]: return dp.parse_from_object(component, style=options["style"]) else: return dp.parse(component.__doc__, style=options["style"]) except (ValueError, dp.ParseError) as ex: if logger: logger.debug(f"Failed parsing docstring for {component}: {ex}") return None def parse_docs(component, parent, logger): docs = {} if docstring_parser_support: if is_dataclass(parent) and component.__name__ == "__init__": next_mro = inspect.getmro(parent)[1] if is_dataclass(next_mro): docs.update(parse_docs(next_mro, next_mro.__init__, logger)) doc_sources = [component] if inspect.isclass(parent) and component.__name__ == "__init__": doc_sources += [parent] for src in doc_sources: doc = parse_docstring(src, params=True, logger=logger) if doc: for param in doc.params: docs[param.arg_name] = param.description return docs def get_doc_short_description(function_or_class, method_name=None, logger=None): if docstring_parser_support: component = function_or_class if inspect.isclass(function_or_class): if not method_name: docstring = parse_docstring(function_or_class, params=False, logger=logger) if docstring and docstring.short_description: return docstring.short_description component = getattr(function_or_class, method_name or "__init__") docstring = parse_docstring(component, params=False, logger=logger) if docstring: return docstring.short_description return None def get_omegaconf_loader(mode): """Returns a yaml loader function based on OmegaConf which supports variable interpolation.""" import io from ._loaders_dumpers import yaml_load with missing_package_raise("omegaconf", "get_omegaconf_loader"): from omegaconf import OmegaConf assert mode in {"omegaconf", "omegaconf+"} if mode == "omegaconf+": from ._common import get_parsing_setting def omegaconf_plus_load(value): value = yaml_load(value) if isinstance(value, dict) and get_parsing_setting("omegaconf_absolute_to_relative_paths"): value = omegaconf_absolute_to_relative_paths(value) return value return omegaconf_plus_load def omegaconf_load(value): value_pyyaml = yaml_load(value) if isinstance(value_pyyaml, (str, int, float, bool)) or value_pyyaml is None: return value_pyyaml value_omegaconf = OmegaConf.to_object(OmegaConf.load(io.StringIO(value))) str_ref = dict.fromkeys([value], None) return value_pyyaml if value_omegaconf == str_ref else value_omegaconf return omegaconf_load def omegaconf_apply(parser, cfg): if "${" not in str(cfg): return cfg with missing_package_raise("omegaconf", "omegaconf_apply"): from omegaconf import OmegaConf from ._common import parser_context with parser_context(path_dump_preserve_relative=True): cfg_dict = parser.dump(cfg, skip_validation=True, skip_unset=False, skip_link_targets=False) cfg_omegaconf = OmegaConf.create(cfg_dict) cfg_dict = OmegaConf.to_container(cfg_omegaconf, resolve=True) return parser._apply_actions(cfg_dict) def omegaconf_tokenize(path: str) -> list[str]: """Very small tokenizer: 'a.b[0].c' -> ['a','b','0','c'].""" return [t for t in path.replace("]", "").replace("[", ".").split(".") if t] def omegaconf_tokens_to_path(tokens: list[str]) -> str: """Render tokens back to a normalized path: ['a','0','b'] -> 'a[0].b'.""" s = "" for t in tokens: if t.isdigit(): s += f"[{t}]" else: s += ("" if s == "" else ".") + t return s def omegaconf_absolute_to_relative_paths(data: dict) -> dict: """ Return a new nested dict/list where absolute ${...} interpolations are rewritten to relative form from the node where they appear. """ data = deepcopy(data) regex_absolute_path = re.compile(r"\$\{([a-zA-Z][a-zA-Z0-9[\]_.]*)\}") 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): return [_walk(v, current_path + [i]) for i, v in enumerate(node)] if isinstance(node, str): def _replace(m: re.Match) -> str: dst_tokens = omegaconf_tokenize(m.group(1)) # compute common prefix length i = 0 while i < len(current_path) and i < len(dst_tokens) and str(current_path[i]) == dst_tokens[i]: i += 1 up = max(1, len(current_path) - i) dots = "." * up down = omegaconf_tokens_to_path(dst_tokens[i:]) return "${" + dots + down + "}" return regex_absolute_path.sub(_replace, node) return node return _walk(data, []) annotated_alias = typing_extensions_import("_AnnotatedAlias") def is_annotated(typehint: type) -> bool: return annotated_alias and isinstance(typehint, annotated_alias) def get_annotated_base_type(typehint: type) -> type: return typehint.__origin__ # type: ignore[attr-defined] type_alias_type = typing_extensions_import("TypeAliasType") if sys.version_info >= (3, 12): from typing import TypeAliasType as typing_type_alias_type else: typing_type_alias_type = None def is_alias_type(typehint: Any) -> bool: return (type_alias_type and isinstance(typehint, type_alias_type)) or ( typing_type_alias_type and isinstance(typehint, typing_type_alias_type) # type: ignore[truthy-function] ) def get_alias_target(typehint: type) -> bool: return typehint.__value__ # type: ignore[attr-defined] def get_pydantic_support() -> int: support = "0" if find_spec("pydantic"): support = version("pydantic") return int(support.split(".", 1)[0]) pydantic_support = get_pydantic_support() def get_pydantic_supports_field_init() -> bool: if find_spec("pydantic"): support = version("pydantic") major, minor = tuple(int(x) for x in support.split(".")[:2]) return major > 2 or (major == 2 and minor >= 6) return False pydantic_supports_field_init = get_pydantic_supports_field_init() def is_pydantic_model(class_type) -> int: if pydantic_support: classes = inspect.getmro(class_type) if pydantic_support and inspect.isclass(class_type) else [] for cls in classes: if getattr(cls, "__module__", "").startswith("pydantic") and getattr(cls, "__name__", "") == "BaseModel": import pydantic if issubclass(cls, pydantic.BaseModel): return pydantic_support elif pydantic_support > 1 and issubclass(cls, pydantic.v1.BaseModel): return 1 return 0 def is_attrs_class(class_type) -> bool: if attrs_support: import attrs if attrs.has(class_type): return True return False def get_module(value): return getattr(type(value), "__module__", "").split(".", 1)[0] def is_annotated_validator(typehint: type) -> bool: from ._util import get_typehint_origin return ( pydantic_support > 1 and is_annotated(typehint) and any(get_module(m) in {"pydantic", "annotated_types"} for m in typehint.__metadata__) # type: ignore[attr-defined] and get_typehint_origin(typehint.__origin__) != Union # type: ignore[attr-defined] ) def validate_annotated(value, typehint: type): from pydantic import TypeAdapter return TypeAdapter(typehint).validate_python(value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_parameter_resolvers.py0000644000175100017510000013061115230046455023245 0ustar00runnerrunnerimport ast import dataclasses import inspect import logging 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, Union from ._common import ( LoggerProperty, get_generic_origin, get_unaliased_type, is_generic_class, is_subclass, is_unpack_typehint, parse_logger, ) from ._optionals import get_annotated_base_type, is_annotated, is_pydantic_model, parse_docs from ._postponed_annotations import evaluate_postponed_annotations from ._stubs_resolver import get_arg_type, get_stub_types, get_stubs_resolver from ._util import ( ClassFromFunctionBase, get_import_path, get_typehint_origin, iter_to_set_str, unique, ) @dataclasses.dataclass class ParamData: name: str annotation: Any default: Any = inspect._empty 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] parameter_attributes = [s[1:] for s in inspect.Parameter.__slots__] kinds = inspect._ParameterKind ast_assign_type: tuple[type[ast.AST], ...] = (ast.AnnAssign, ast.Assign) param_kwargs_pop_or_get = "**.pop|get():" ignore_params = { "transformers.BertModel.from_pretrained": {"config_file_name"}, } class SourceNotAvailable(Exception): "Raised when the source code for some component is not available." class UnknownDefault: def __init__(self, resolver: str, data: Any = inspect._empty) -> None: self.resolver = resolver self.data = data def __repr__(self) -> str: value = f"{type(self).__name__.replace('Default', '')}<{self.resolver}>" if self.data != inspect._empty: value = f"{value} {self.data}" return value class ConditionalDefault(UnknownDefault): def __init__(self, resolver: str, data: Any) -> None: super().__init__(resolver, iter_to_set_str(data, sep=", ")) 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: component = get_subclass_types(component, also_lists=True) if isinstance(component, tuple): assert parent is None or len(component) == len(parent) return iter_to_set_str(get_parameter_origins(c, parent[n] if parent else None) for n, c in enumerate(component)) if parent: return f"{get_import_path(parent)}.{component.__name__}" return get_import_path(component) def is_staticmethod(attr) -> bool: return isinstance(attr, staticmethod) def is_method(attr) -> bool: return (inspect.isfunction(attr) or attr.__class__.__name__ == "cython_function_or_method") and not is_staticmethod( attr ) def is_property(attr) -> bool: return isinstance(attr, property) def is_method_or_property(attr) -> bool: return is_method(attr) or is_property(attr) def is_classmethod(parent, component) -> bool: if parent: with suppress(AttributeError): return isinstance(inspect.getattr_static(parent, component.__name__), classmethod) return False def is_lambda(value: Any) -> bool: return callable(value) and getattr(value, "__name__", "") == "" def ast_str(node): return getattr(ast, "unparse", ast.dump)(node) def ast_variable_load(name): return ast.Name(id=name, ctx=ast.Load()) def ast_attribute_load(container, name): return ast.Attribute(value=ast.Name(id=container, ctx=ast.Load()), attr=name, ctx=ast.Load()) def ast_is_assign_with_value(node, value) -> bool: return isinstance(node, ast_assign_type) and ast.dump(node.value) == ast.dump(value) # type: ignore[attr-defined] def ast_get_assign_targets(node): return node.targets if isinstance(node, ast.Assign) else [node.target] def ast_is_not(node) -> bool: return isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not) dict_ast = ast.dump(ast_variable_load("dict")) def ast_is_dict_assign(node): return isinstance(node, ast_assign_type) and ( isinstance(node.value, ast.Dict) or (isinstance(node.value, ast.Call) and ast.dump(node.value.func) == dict_ast) ) def ast_is_dict_assign_with_value(node, value): if ast_is_dict_assign(node) and getattr(node.value, "keywords", None): value_dump = ast.dump(value) for keyword in [k.value for k in node.value.keywords]: if ast.dump(keyword) == value_dump: return True return False def ast_is_call_with_value(node, value_dump) -> bool: for argtype in ["args", "keywords"]: for arg in getattr(node, argtype): if isinstance(getattr(arg, "value", None), ast.AST) and ast.dump(arg.value) == value_dump: return True return False ast_constant_attr = {ast.Constant: "value"} ast_constant_types = tuple(ast_constant_attr) def ast_is_constant(node): return isinstance(node, ast_constant_types) def ast_get_constant_value(node): assert ast_is_constant(node) return getattr(node, ast_constant_attr[node.__class__]) def ast_get_name_and_attrs(node) -> list[str]: names = [] while isinstance(node, ast.Attribute): names.append(node.attr) node = node.value if isinstance(node, ast.Name): names.append(node.id) return names[::-1] def ast_is_kwargs_pop_or_get(node, value_dump) -> bool: return ( isinstance(node.func, ast.Attribute) and value_dump == ast.dump(node.func.value) and node.func.attr in {"pop", "get"} and len(node.args) == 2 and isinstance(ast_get_constant_value(node.args[0]), str) ) def ast_is_super_call(node) -> bool: return ( isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Call) and isinstance(node.func.value.func, ast.Name) and node.func.value.func.id == "super" ) def ast_is_supported_super_call(node, self_name, log_debug) -> bool: supported = False args = node.func.value.args if not args and not node.func.value.keywords: supported = True elif ( args and len(args) == 2 and all(isinstance(a, ast.Name) for a in args) and self_name == args[1].id and not node.func.value.keywords ): classes, idx = current_mro.get() module = inspect.getmodule(classes[idx]) for offset, cls in enumerate(classes[idx:]): if args[0].id == cls.__name__ and cls is getattr(module, cls.__name__, None): current_mro.set((classes, idx + offset)) supported = True break if not supported: log_debug(f"unsupported super parameters: {ast_str(node)}") return supported def ast_is_attr_assign(node, container): for target in ast_get_assign_targets(node) if isinstance(node, ast_assign_type) else []: if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == container: return target.attr return False def ast_get_call_kwarg_with_value(node, value): value_dump = ast.dump(value) kwarg = None for arg in node.keywords: if isinstance(getattr(arg, "value", None), ast.AST) and ast.dump(arg.value) == value_dump: kwarg = arg break return kwarg def ast_get_call_positional_indexes(node): return [n for n, a in enumerate(node.args) if not isinstance(a, ast.Starred)] 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: set | None = None): given_args = set(ast_get_call_positional_indexes(node)) given_kwargs = set(ast_get_call_keyword_names(node)) input_params = params params = [p for n, p in enumerate(params) if n not in given_args] params = [p for p in params if p.name not in given_kwargs] if removed_params is not None and len(params) < len(input_params): removed_params.update(p.name for p in input_params if p.name in given_kwargs) return params def get_arg_kind_index(params, kind): return next((n for n, p in enumerate(params) if p.kind == kind), -1) def get_signature_parameters_and_indexes(component, parent, logger): signature_source = component if is_classmethod(parent, component): signature_source = component.__func__ params = list(inspect.signature(signature_source).parameters.values()) if parent: params = params[1:] args_idx = get_arg_kind_index(params, kinds.VAR_POSITIONAL) kwargs_idx = get_arg_kind_index(params, kinds.VAR_KEYWORD) doc_params = parse_docs(component, parent, logger) for num, param in enumerate(params): params[num] = ParamData( doc=doc_params.get(param.name), parent=parent, component=component, **{a: getattr(param, a) for a in parameter_attributes}, ) evaluate_postponed_annotations(params, signature_source, parent, logger) stubs = get_stub_types(params, signature_source, parent, logger) replace_generic_type_vars(params, parent) return params, args_idx, kwargs_idx, doc_params, stubs def replace_generic_type_vars(params: ParamList, parent) -> None: if is_generic_class(parent) and parent.__args__ and getattr(parent.__origin__, "__parameters__", None): type_vars = dict(zip(parent.__origin__.__parameters__, parent.__args__)) def replace_type_vars(annotation): if annotation in type_vars: return type_vars[annotation] if getattr(annotation, "__args__", None): origin = annotation.__origin__ return origin[tuple(replace_type_vars(a) for a in annotation.__args__)] return annotation for param in params: param.annotation = replace_type_vars(param.annotation) def unpack_typed_dict_kwargs(params: ParamList, kwargs_idx: int, logger=None) -> int: from ._typehints import ( NotRequired, get_typed_dict_annotations, get_typed_dict_required_keys, not_required_types, ) kwargs = params[kwargs_idx] annotation = kwargs.annotation if is_unpack_typehint(annotation): params.pop(kwargs_idx) annotation_args: tuple = getattr(annotation, "__args__", ()) assert len(annotation_args) == 1, "Unpack requires a single type argument" typed_dict = annotation_args[0] dict_annotations = get_typed_dict_annotations(typed_dict, logger) required_keys = get_typed_dict_required_keys(typed_dict, dict_annotations) new_params = [] for nm, annot in dict_annotations.items(): if nm not in required_keys and get_typehint_origin(annot) not in not_required_types: # Mark optional keys (e.g. from total=False) as NotRequired so that they # are added as non-required arguments. annot = NotRequired[annot] new_params.append( ParamData( name=nm, annotation=annot, default=inspect._empty, kind=inspect._ParameterKind.KEYWORD_ONLY, doc=None, component=kwargs.component, parent=kwargs.parent, origin=kwargs.origin, ) ) # insert in-place assert kwargs_idx == len(params), "trailing params should yield a syntax error" params.extend(new_params) return -1 return kwargs_idx def add_stub_types(stubs: dict[str, Any] | None, params: ParamList, component) -> None: if not stubs: return for param in params: if param.annotation == inspect._empty and param.name in stubs: param.annotation = stubs[param.name] known_params = {p.name for p in params} for name, stub in stubs.items(): if name not in known_params: params.append( ParamData( name=name, annotation=stub, default=UnknownDefault("stubs-resolver"), kind=kinds.KEYWORD_ONLY, component=component, ) ) ast_literals = {ast.dump(ast.parse(v, mode="eval").body): partial(ast.literal_eval, v) for v in ["{}", "[]"]} def is_param_subclass_instance_default(param: ParamData) -> bool: from ._typehints import ActionTypeHint, get_optional_arg, get_subclass_types annotation = get_optional_arg(param.annotation) class_types = get_subclass_types(annotation, callable_return=True) return bool( (class_types and isinstance(param.default, class_types)) or ( is_lambda(param.default) and ActionTypeHint.is_callable_typehint(annotation) and getattr(annotation, "__args__", None) and ActionTypeHint.is_subclass_typehint(annotation.__args__[-1], all_subtypes=False) ) ) def split_args_and_kwargs(params: ParamList) -> tuple[ParamList, ParamList]: args = [p for p in params if p.kind == kinds.POSITIONAL_ONLY] kwargs = [p for p in params if p.kind in {kinds.KEYWORD_ONLY, kinds.POSITIONAL_OR_KEYWORD}] return args, kwargs def replace_args_and_kwargs(params: ParamList, args: ParamList, kwargs: ParamList) -> ParamList: args_idx = get_arg_kind_index(params, kinds.VAR_POSITIONAL) kwargs_idx = get_arg_kind_index(params, kinds.VAR_KEYWORD) if args_idx >= 0: params = params[:args_idx] + args + params[args_idx + 1 :] if kwargs_idx >= 0: kwargs_idx += len(args) - 1 if kwargs_idx >= 0: existing_names = {p.name for p in params[:kwargs_idx] + params[kwargs_idx + 1 :]} kwargs = [p for p in kwargs if p.name not in existing_names] params = params[:kwargs_idx] + kwargs + params[kwargs_idx + 1 :] return params def group_parameters(params_list: list[ParamList]) -> ParamList: if len(params_list) == 1: for param in params_list[0]: if not isinstance(param.origin, tuple): param.origin = None return params_list[0] grouped = [] non_get_pop_count = 0 params_dict = defaultdict(list) for params in params_list: if not (params[0].origin or "").startswith(param_kwargs_pop_or_get): # type: ignore[union-attr] non_get_pop_count += 1 for param in params: if param.kind != kinds.POSITIONAL_ONLY: params_dict[param.name].append(param) for params in params_dict.values(): gparam = params[0] types = unique(p.annotation for p in params if p.annotation is not inspect._empty) defaults = unique(p.default for p in params if p.default is not inspect._empty) if len(params) >= non_get_pop_count and len(types) <= 1 and len(defaults) <= 1: gparam.origin = None else: gparam.parent = tuple(p.parent for p in params) gparam.component = tuple(p.component for p in params) gparam.origin = tuple(p.origin for p in params) if len(params) < non_get_pop_count: defaults += ["NOT_ACCEPTED"] gparam.default = ConditionalDefault("ast-resolver", defaults) if len(types) > 1: gparam.annotation = Union[tuple(types)] if types else inspect._empty docs = [p.doc for p in params if p.doc] gparam.doc = docs[0] if docs else None grouped.append(gparam) return grouped def has_dunder_new_method(cls, attr_name): classes = inspect.getmro(get_generic_origin(cls))[1:] return ( attr_name == "__init__" and cls.__new__ is not object.__new__ and not any(cls.__new__ is c.__new__ for c in classes) ) current_mro: ContextVar = ContextVar("current_mro", default=(None, None)) @contextmanager def mro_context(parent): token = None if parent: classes, idx = current_mro.get() if not classes or classes[idx] is not parent: classes = [c for c in inspect.getmro(parent) if c is not object] token = current_mro.set((classes, 0)) try: yield finally: if token: current_mro.reset(token) def get_mro_parameters(method_name, get_parameters_fn, logger): classes, idx = current_mro.get() for num, cls in enumerate(classes[idx + 1 :], start=idx + 1): method = getattr(cls, method_name, None) remainder = classes[num + 1 :] + [object] if method and not any(method is getattr(c, method_name, None) for c in remainder): current_mro.set((classes, num)) return get_parameters_fn(cls, method, logger=logger) return [] def get_component_and_parent( 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] if isinstance(function_or_class, MethodType): method_or_property = function_or_class.__name__ function_or_class = function_or_class.__self__ else: method_or_property = None elif inspect.isclass(get_generic_origin(function_or_class)) and method_or_property is None: method_or_property = "__init__" elif method_or_property and not isinstance(method_or_property, str): method_or_property = method_or_property.__name__ parent = component = None if method_or_property: try: attr = inspect.getattr_static(get_generic_origin(function_or_class), method_or_property) except AttributeError as ex: raise AttributeError(f"Attribute '{method_or_property}' not found in {function_or_class}") from ex if is_staticmethod(attr): component = getattr(function_or_class, method_or_property) return component, parent, method_or_property parent = function_or_class if has_dunder_new_method(function_or_class, method_or_property): component = getattr(function_or_class, "__new__") elif is_method(attr): component = attr elif is_property(attr): component = attr.fget elif isinstance(attr, classmethod): component = getattr(function_or_class, method_or_property) elif attr is not object.__init__: raise ValueError( f"Invalid or unsupported input: class={function_or_class}, method_or_property={method_or_property}" ) else: if not callable(function_or_class): raise ValueError(f"Non-callable input: function={function_or_class}") component = function_or_class return component, parent, method_or_property class ParametersVisitor(LoggerProperty, ast.NodeVisitor): def __init__( self, function_or_class: Callable | type, method_or_property: str | Callable | None = None, **kwargs, ): super().__init__(**kwargs) self.component, self.parent, _ = get_component_and_parent(function_or_class, method_or_property) def log_debug(self, message) -> None: self.logger.debug(f"AST resolver: {message}") def parse_source_tree(self): """Parses the component's AST and sets the component and parent nodes.""" if hasattr(self, "component_node"): return try: source = textwrap.dedent(inspect.getsource(self.component)) tree = ast.parse(source) assert isinstance(tree, ast.Module) and len(tree.body) == 1 self.component_node = tree.body[0] self.self_name = self.component_node.args.args[0].arg if self.parent else None except Exception as ex: raise SourceNotAvailable(f"Problems getting source code for {self.component}: {ex}") from ex def visit_Assign(self, node): do_generic_visit = True for key, value in self.find_values.items(): if ast_is_assign_with_value(node, value): self.add_value(key, node) do_generic_visit = False break elif ast_is_dict_assign_with_value(node, value): self.add_value(key, node) do_generic_visit = False if do_generic_visit: if ast_is_dict_assign(node): for target in [deepcopy(t) for t in ast_get_assign_targets(node)]: target.ctx = ast.Load() self.dict_assigns[ast.dump(target)] = node else: self.generic_visit(node) def visit_AnnAssign(self, node): if node.value is not None: self.visit_Assign(node) def visit_Call(self, node): for key, value in self.find_values.items(): value_dump = ast.dump(value) if ast_is_call_with_value(node, value_dump): if isinstance(node.func, ast.Attribute): value_dump = ast.dump(node.func.value) if value_dump in self.dict_assigns: self.add_value(key, self.dict_assigns[value_dump]) continue self.add_value(key, node) elif ast_is_kwargs_pop_or_get(node, value_dump): self.add_value(key, node) self.generic_visit(node) def visit_If(self, node): is_test_not = ast_is_not(node.test) test_node = node.test.operand if is_test_not else node.test component_globals = self.get_component_globals() if isinstance(test_node, ast.Name) and test_node.id in component_globals: condition = bool(component_globals[test_node.id]) if is_test_not: condition = not condition body = node.body if condition else node.orelse node = ast.If(test=ast.Constant(value=True), body=body, orelse=[]) self.generic_visit(node) 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 top_level = name.split(".")[0] if top_level != name: self.import_names[top_level] = node def visit_ImportFrom(self, node: ast.ImportFrom) -> None: self.visit_Import(node) def add_value(self, key, node): source = None if isinstance(node, ast.Call): name = False if isinstance(node.func, ast.Name): name = node.func.id elif isinstance(node.func, ast.Attribute): names = ast_get_name_and_attrs(node.func) name = names[0] if names else False if name and name in self.import_names: source = self.import_names[name] self.values_found.append((key, node, source)) def find_values_usage(self, values): self.find_values = values self.values_found = [] self.dict_assigns = {} self.import_names = {} self.visit(self.component_node) return self.values_found def get_component_globals(self): return vars(import_module(self.component.__module__)) def get_component_from_source(self, name, source): aliases = {} ast_exec = ast.parse("") ast_exec.body = [source] try: exec(compile(ast_exec, filename="", mode="exec"), aliases, aliases) except Exception as ex: if self.logger: 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) -> tuple[type, str | None] | None: function_or_class = method_or_property = None module = inspect.getmodule(self.component) if isinstance(node.func, ast.Name): if is_classmethod(self.parent, self.component) and node.func.id == self.self_name: function_or_class = self.parent elif hasattr(module, node.func.id): function_or_class = getattr(module, node.func.id) elif source: function_or_class = self.get_component_from_source(node.func.id, source) elif isinstance(node.func, ast.Attribute): names = ast_get_name_and_attrs(node.func) if len(names) == 2 and self.parent and names[0] == self.self_name: function_or_class = self.parent method_or_property = names[-1] elif len(names) >= 2: container = None if source: container = self.get_component_from_source(names[0], source) if container is None and hasattr(module, names[0]): container = getattr(module, names[0]) for attr_name in names[1:-1]: container = getattr(container, attr_name, None) if container is not None else None if inspect.isclass(container): function_or_class = container method_or_property = names[-1] elif container is not None and hasattr(container, names[-1]): function_or_class = getattr(container, names[-1]) if not function_or_class: self.log_debug(f"not supported: {ast_str(node)}") return None return function_or_class, method_or_property def match_call_that_uses_attr(self, node, source, attr_name): params = None if isinstance(node, ast.Call): params = [] value = ast_attribute_load(self.self_name, attr_name) kwarg = ast_get_call_kwarg_with_value(node, value) if kwarg: if kwarg.arg: self.log_debug(f"kwargs attribute given as keyword parameter not supported: {ast_str(node)}") else: get_param_args = self.get_node_component(node, source) if get_param_args: try: params = get_signature_parameters(*get_param_args, logger=self.logger) except Exception: self.log_debug(f"failed to get parameters for call that uses attr: {get_param_args}") params = remove_given_parameters(node, params) return params def replace_param_default_subclass_specs(self, params: list[ParamData]) -> None: params = [p for p in params if is_param_subclass_instance_default(p)] if params: self.parse_source_tree() default_nodes = self.get_default_nodes({p.name for p in params}) assert len(params) == len(default_nodes) from ._typehints import get_subclass_types for param, default_node in zip(params, default_nodes): lambda_default = is_lambda(param.default) node = default_node num_positionals = 0 if lambda_default: node = default_node.body num_positionals = len(param.annotation.__args__) - 1 class_type = self.get_call_class_type(node) subclass_types = get_subclass_types(param.annotation, callable_return=True) if not (class_type and subclass_types and is_subclass(class_type, subclass_types)): continue default: dict = {"class_path": get_import_path(class_type), "init_args": {}} for kwarg in node.keywords: if kwarg.arg and ast_is_constant(kwarg.value): default["init_args"][kwarg.arg] = ast_get_constant_value(kwarg.value) else: default.clear() break if not default or len(node.args) - num_positionals > 0: self.log_debug(f"unsupported class instance default: {ast_str(default_node)}") elif default: if not default["init_args"]: del default["init_args"] param.default = default 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:]: class_type = getattr(class_type, name, None) return class_type if inspect.isclass(class_type) else None def get_default_nodes(self, param_names: set): node = self.component_node.args arg_nodes = getattr(node, "posonlyargs", []) + node.args default_nodes = [None] * (len(arg_nodes) - len(node.defaults)) + node.defaults default_nodes = [d for n, d in enumerate(default_nodes) if arg_nodes[n].arg in param_names] for kw_arg, kw_default in zip(node.kwonlyargs, node.kw_defaults): if kw_arg.arg in param_names and kw_default is not None: default_nodes.append(kw_default) return default_nodes def get_kwargs_pop_or_get_parameter(self, node, component, parent, doc_params): name = ast_get_constant_value(node.args[0]) if ast_is_constant(node.args[1]): default = ast_get_constant_value(node.args[1]) else: default = ast.dump(node.args[1]) if default in ast_literals: default = ast_literals[default]() else: default = UnknownDefault("ast-resolver") self.log_debug(f"unsupported kwargs pop/get default: {ast_str(node)}") return ParamData( name=name, annotation=inspect._empty, default=default, kind=kinds.KEYWORD_ONLY, doc=doc_params.get(name), parent=parent, component=component, origin=param_kwargs_pop_or_get + self.get_node_origin(node), ) def get_parameters_args_and_kwargs(self) -> tuple[ParamList, ParamList]: self.parse_source_tree() args_name = getattr(self.component_node.args.vararg, "arg", None) kwargs_name = getattr(self.component_node.args.kwarg, "arg", None) values_to_find = {} if args_name: values_to_find[args_name] = ast_variable_load(args_name) if kwargs_name: values_to_find[kwargs_name] = ast_variable_load(kwargs_name) values_found = self.find_values_usage(values_to_find) if not values_found: return [], [] params_list = [] removed_params: set[str] = set() kwargs_value = kwargs_name and values_to_find[kwargs_name] kwargs_value_dump = kwargs_value and ast.dump(kwargs_value) for node, source in [(v, s) for k, v, s in values_found if k == kwargs_name]: if isinstance(node, ast.Call): if ast_is_kwargs_pop_or_get(node, kwargs_value_dump): param = self.get_kwargs_pop_or_get_parameter(node, self.component, self.parent, self.doc_params) params_list.append([param]) continue kwarg = ast_get_call_kwarg_with_value(node, kwargs_value) params = [] if kwarg.arg: self.log_debug(f"kwargs given as keyword parameter not supported: {ast_str(node)}") elif self.parent and ast_is_super_call(node): if ast_is_supported_super_call(node, self.self_name, self.log_debug): params = get_mro_parameters( node.func.attr, # type: ignore[attr-defined] get_signature_parameters, self.logger, ) else: get_param_args = self.get_node_component(node, source) if get_param_args: params = get_signature_parameters(*get_param_args, logger=self.logger) params = remove_given_parameters(node, params, removed_params) if params: self.add_node_origins(params, node) params_list.append(params) elif isinstance(node, ast_assign_type): self_attr = self.parent and ast_is_attr_assign(node, self.self_name) if self_attr: params = self.get_parameters_attr_use_in_members(self_attr) if params: self.add_node_origins(params, node) params_list.append(params) else: self.log_debug(f"unsupported type of assign: {ast_str(node)}") params = group_parameters(params_list) params = [p for p in params if p.name not in removed_params] return split_args_and_kwargs(params) def get_parameters_attr_use_in_members(self, attr_name) -> ParamList: attr_value = ast_attribute_load(self.self_name, attr_name) member_names = [ name for name, _ in inspect.getmembers(self.parent) if not name.startswith("__") and is_method_or_property(inspect.getattr_static(self.parent, name)) ] for member_name in member_names: assert self.parent is not None visitor = ParametersVisitor(self.parent, member_name, logger=self.logger) kwargs = visitor.get_parameters_call_attr(attr_name, attr_value) if kwargs is not None: return kwargs self.log_debug(f"did not find use of {self.self_name}.{attr_name} in members of {self.parent}") return [] def get_node_origin(self, node) -> str: return f"{get_parameter_origins(self.component, self.parent)}:{node.lineno}" def add_node_origins(self, params: ParamList, node) -> None: origin = None for param in params: if param.origin is None: if not origin: origin = self.get_node_origin(node) param.origin = origin 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) matched = [] if values_found: for _, node, source in values_found: match = self.match_call_that_uses_attr(node, source, attr_name) if match: self.add_node_origins(match, node) matched.append(match) matched = group_parameters(matched) return matched or None def remove_ignore_parameters(self, params: ParamList) -> ParamList: import_path = get_import_path(self.component) if import_path in ignore_params: params = [p for p in params if p.name not in ignore_params[import_path]] return params def get_parameters(self) -> ParamList: if self.component is None: return [] params, args_idx, kwargs_idx, doc_params, stubs = get_signature_parameters_and_indexes( self.component, self.parent, self.logger ) self.replace_param_default_subclass_specs(params) if kwargs_idx >= 0: kwargs_idx = unpack_typed_dict_kwargs(params, kwargs_idx, self.logger) if args_idx >= 0 or kwargs_idx >= 0: self.doc_params = doc_params with mro_context(self.parent): args, kwargs = self.get_parameters_args_and_kwargs() params = replace_args_and_kwargs(params, args, kwargs) add_stub_types(stubs, params, self.component) params = self.remove_ignore_parameters(params) return params def get_field_data_pydantic1_model(field, name, doc_params): default = field.default if field.required: default = inspect._empty elif field.default_factory: default = field.default_factory() return { "annotation": field.annotation, "default": default, "doc": field.field_info.description or doc_params.get(name), } def get_field_data_pydantic2_dataclass(field, name, doc_params): from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined default = inspect._empty # Identify the default. if isinstance(field.default, FieldInfo): # Pydantic 2 dataclasses stuff their FieldInfo into a # stdlib dataclasses.field's `default`; this is where the # actual default and default_factory live. if field.default.default is not PydanticUndefined: default = field.default.default elif field.default.default_factory is not PydanticUndefined: default = field.default.default_factory() elif field.default is not dataclasses.MISSING: default = field.default elif field.default_factory is not dataclasses.MISSING: default = field.default_factory() # Get the type, stripping Annotated like get_type_hints does. if is_annotated(field.type): field_type = get_annotated_base_type(field.type) else: field_type = field.type return { "annotation": field_type, "default": default, "doc": doc_params.get(name), } def get_field_data_pydantic2_model(field, name, doc_params): default = field.default if field.is_required(): default = inspect._empty elif field.default_factory: default = field.default_factory() return { "annotation": field.rebuild_annotation(), "default": default, "doc": field.description or doc_params.get(name), } def get_field_data_attrs(field, name, doc_params): import attrs default = field.default if default is attrs.NOTHING: default = inspect._empty elif isinstance(default, attrs.Factory): default = default.factory() return { "annotation": field.type, "default": default, "doc": doc_params.get(name), } def is_init_field_pydantic2_dataclass(field) -> bool: from pydantic.fields import FieldInfo if isinstance(field.default, FieldInfo): # FieldInfo.init is new in pydantic 2.6 return getattr(field.default, "init", None) is not False return field.init is not False def is_init_field_attrs(field) -> bool: return field.init is not False def get_parameters_from_pydantic_or_attrs( function_or_class: Callable | type, method_or_property: str | None, logger: logging.Logger, ) -> ParamList | None: from ._optionals import attrs_support, pydantic_support if method_or_property or not (pydantic_support or attrs_support): return None function_or_class = get_unaliased_type(function_or_class) fields_iterator = get_field_data = None if pydantic_support: pydantic_model = is_pydantic_model(function_or_class) if pydantic_model == 1: fields_iterator = function_or_class.__fields__.items() get_field_data = get_field_data_pydantic1_model is_init_field = lambda _: True elif pydantic_model > 1: fields_iterator = function_or_class.model_fields.items() get_field_data = get_field_data_pydantic2_model is_init_field = lambda _: True elif dataclasses.is_dataclass(function_or_class) and hasattr(function_or_class, "__pydantic_fields__"): fields_iterator = dataclasses.fields(function_or_class) fields_iterator = {v.name: v for v in fields_iterator}.items() get_field_data = get_field_data_pydantic2_dataclass is_init_field = is_init_field_pydantic2_dataclass if not fields_iterator and attrs_support: import attrs if attrs.has(function_or_class): fields_iterator = {f.name: f for f in attrs.fields(function_or_class)}.items() get_field_data = get_field_data_attrs is_init_field = is_init_field_attrs if not fields_iterator or not get_field_data: return None params = [] doc_params = parse_docs(function_or_class, None, logger) for name, field in fields_iterator: if is_init_field(field): params.append( ParamData( name=name, kind=kinds.KEYWORD_ONLY, component=function_or_class, **get_field_data(field, name, doc_params), ) ) evaluate_postponed_annotations(params, function_or_class, None, logger) return params def get_parameters_from_ast( function_or_class: Callable | type, method_or_property: str | None, logger: logging.Logger, ) -> ParamList | None: visitor = ParametersVisitor(function_or_class, method_or_property, logger=logger) return visitor.get_parameters() def get_parameters_from_stubs( function_or_class: Callable | type, method_or_property: str | None, logger: logging.Logger, ) -> 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: ParamList | None = None resolver = get_stubs_resolver() stub_import = resolver.get_component_imported_info(component, parent) if stub_import: origin = get_parameter_origins(component, parent) aliases = resolver.get_aliases(stub_import) arg_asts = stub_import.info.ast.args.args + stub_import.info.ast.args.kwonlyargs params = [] for num, arg_ast in enumerate(arg_asts): if parent and num == 0: continue try: annotation = get_arg_type(arg_ast.annotation, aliases) except Exception: annotation = inspect._empty params.append( ParamData( name=arg_ast.arg, annotation=annotation, default=UnknownDefault("stubs-resolver"), kind=inspect._ParameterKind.KEYWORD_ONLY, component=component, parent=parent, origin=origin, ) ) return params def get_parameters_by_assumptions( 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) params, args_idx, kwargs_idx, _, stubs = get_signature_parameters_and_indexes(component, parent, logger) if parent and (args_idx >= 0 or kwargs_idx >= 0): with mro_context(parent): subparams = get_mro_parameters(method_name, get_parameters_by_assumptions, logger) if subparams: args, kwargs = split_args_and_kwargs(subparams) params = replace_args_and_kwargs(params, args, kwargs) params = replace_args_and_kwargs(params, [], []) add_stub_types(stubs, params, component) return params def get_signature_parameters( 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. In contrast to inspect.signature, it follows the use of *args and **kwargs attempting to find all accepted named parameters. Args: function_or_class: The callable object from which to get the signature parameters. method_or_property: For classes, the name of the method or property from which to get the signature parameters. If not provided it returns the parameters for ``__init__``. logger: Useful for debugging. Only logs at ``DEBUG`` level. """ get_component_and_parent(function_or_class, method_or_property) # verify input logger = parse_logger(logger, "get_signature_parameters") params = None for get_parameters in [ get_parameters_from_pydantic_or_attrs, get_parameters_from_ast, get_parameters_from_stubs, get_parameters_by_assumptions, ]: try: params = get_parameters(function_or_class, method_or_property, logger) except Exception as ex: logger.debug( "%s failed: function_or_class=%s, method_or_property=%s: %s", get_parameters.__name__, function_or_class, method_or_property, ex, exc_info=ex, ) if params is not None: break return params or [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_paths.py0000644000175100017510000003417215230046455020305 0ustar00runnerrunnerimport os import re import stat import sys from collections import Counter from collections.abc import Iterator from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass from io import StringIO from typing import IO, Any from ._deprecated import PathDeprecations from ._optionals import ( fsspec_support, import_fsspec, import_requests, url_support, ) _current_path_dir: ContextVar[str | None] = ContextVar("_current_path_dir", default=None) class _CachedStdin(StringIO): """Used to allow reading sys.stdin multiple times.""" def _get_cached_stdin() -> _CachedStdin: if not isinstance(sys.stdin, _CachedStdin): sys.stdin = _CachedStdin(sys.stdin.read()) return sys.stdin def _read_cached_stdin() -> str: stdin = _get_cached_stdin() value = stdin.read() stdin.seek(0) return value @dataclass class _UrlData: scheme: str url_path: str def _parse_url(url: str) -> _UrlData | None: index = url.rfind("://") if index <= 0: return None return _UrlData( scheme=url[: index + 3], url_path=url[index + 3 :], ) def _is_absolute_path(path: str) -> bool: if path.find("://") > 0: return True return os.path.isabs(path) def _resolve_relative_path(path: str) -> str: parts = path.split("/") resolved: list[str] = [] for part in parts: if part == "..": resolved.pop() elif part != ".": resolved.append(part) return "/".join(resolved) def _known_to_fsspec(path: str) -> bool: import_fsspec("_known_to_fsspec") from fsspec.registry import known_implementations for protocol in known_implementations: if path.startswith(protocol + "://") or path.startswith(protocol + "::"): return True return False class PathError(TypeError): """Exception raised for errors in the Path class.""" class Path(PathDeprecations): """Base class for Path types. Stores a (possibly relative) path and the corresponding absolute path. From the object the absolute path can be obtained without having to remember the working directory (or parent remote path) from when the object was created. When a Path instance is created, it is checked that: the path exists, whether it is a file or directory and whether it has the required access permissions (f=file, d=directory, r=readable, w=writable, x=executable, c=creatable, u=url, s=fsspec or in uppercase meaning not, i.e., F=not-file, D=not-directory, R=not-readable, W=not-writable and X=not-executable). The creatable flag "c" can be given one or two times. If given once, the parent directory must exist and be writable. If given twice, the parent directory does not have to exist, but should be allowed to create. An instance of Path class can also refer to the standard input or output. To do that, path must be set with the value "-"; it is a common practice. Then, getting the content or opening it will automatically be done on standard input or output. """ _url_data: _UrlData | None _file_scheme = re.compile("^file:///?") def __init__( self, path: "str | os.PathLike | Path", mode: str = "fr", cwd: str | os.PathLike | None = None, **kwargs, ): """Initializer for Path instance. Args: path: The path to check and store. mode: The required type and access permissions among ``[fdrwxcuFDRWX]``. cwd: Working directory for relative paths. If ``None`` then ``os.getcwd()`` is used. Raises: ValueError: If the provided mode is invalid. PathError: If the path does not exist or does not agree with the mode. """ self._deprecated_kwargs(kwargs) self._check_mode(mode) self._std_io = False is_url = False is_fsspec = False if isinstance(path, Path): self._std_io = path._std_io is_url = path.is_url is_fsspec = path.is_fsspec url_data = path._url_data cwd = path.cwd abs_path = path.absolute path = path.relative elif isinstance(path, (str, os.PathLike)): if path == "-": self._std_io = True path = os.fspath(path) cwd = os.fspath(cwd) if cwd else None abs_path = os.path.expanduser(path) if self._file_scheme.match(abs_path): abs_path = self._file_scheme.sub("" if os.name == "nt" else "/", abs_path) is_absolute = _is_absolute_path(abs_path) url_data = _parse_url(abs_path) cwd_url_data = _parse_url(cwd or _current_path_dir.get() or os.getcwd()) if ("u" in mode or "s" in mode) and (url_data or (cwd_url_data and not is_absolute)): if cwd_url_data and not is_absolute: abs_path = _resolve_relative_path(cwd_url_data.url_path + "/" + path) abs_path = cwd_url_data.scheme + abs_path url_data = _parse_url(abs_path) if cwd is None: cwd = _current_path_dir.get() or os.getcwd() if "u" in mode and url_support: is_url = True elif "s" in mode and fsspec_support and _known_to_fsspec(abs_path): is_fsspec = True else: if cwd is None: cwd = os.getcwd() abs_path = abs_path if is_absolute else os.path.join(cwd, abs_path) url_data = None else: raise PathError("Expected path to be a string, os.PathLike or a Path object.") if not self._skip_check and is_url: if "r" in mode: requests = import_requests("Path with URL support") try: requests.head(abs_path).raise_for_status() except requests.HTTPError as ex: raise PathError(f"{abs_path} HEAD not accessible :: {ex}") from ex elif not self._skip_check and is_fsspec: fsspec_mode = "".join(c for c in mode if c in {"r", "w"}) if fsspec_mode: fsspec = import_fsspec("Path") try: handle = fsspec.open(abs_path, fsspec_mode) handle.open() handle.close() except (FileNotFoundError, KeyError) as ex: raise PathError(f"Path does not exist: {abs_path!r}") from ex except PermissionError as ex: raise PathError(f"Path exists but no permission to access: {abs_path!r}") from ex elif not self._skip_check and not self._std_io: ptype = "Directory" if "d" in mode else "File" if "c" in mode: pdir = os.path.realpath(os.path.join(abs_path, "..")) if not os.path.isdir(pdir) and mode.count("c") == 2: ppdir = None while not os.path.isdir(pdir) and pdir != ppdir: ppdir = pdir pdir = os.path.realpath(os.path.join(pdir, "..")) if not os.path.isdir(pdir): raise PathError(f"{ptype} is not creatable since parent directory does not exist: {abs_path!r}") if not os.access(pdir, os.W_OK): raise PathError(f"{ptype} is not creatable since parent directory not writable: {abs_path!r}") if "d" in mode and os.access(abs_path, os.F_OK) and not os.path.isdir(abs_path): raise PathError(f"{ptype} is not creatable since path already exists: {abs_path!r}") if "f" in mode and os.access(abs_path, os.F_OK) and not os.path.isfile(abs_path): raise PathError(f"{ptype} is not creatable since path already exists: {abs_path!r}") elif "d" in mode or "f" in mode: if not os.access(abs_path, os.F_OK): raise PathError(f"{ptype} does not exist: {abs_path!r}") if "d" in mode and not os.path.isdir(abs_path): raise PathError(f"Path is not a directory: {abs_path!r}") if "f" in mode and not (os.path.isfile(abs_path) or stat.S_ISFIFO(os.stat(abs_path).st_mode)): raise PathError(f"Path is not a file: {abs_path!r}") if "r" in mode and not os.access(abs_path, os.R_OK): raise PathError(f"{ptype} is not readable: {abs_path!r}") if "w" in mode and not os.access(abs_path, os.W_OK): raise PathError(f"{ptype} is not writable: {abs_path!r}") if "x" in mode and not os.access(abs_path, os.X_OK): raise PathError(f"{ptype} is not executable: {abs_path!r}") if "D" in mode and os.path.isdir(abs_path): raise PathError(f"Path is a directory: {abs_path!r}") if "F" in mode and (os.path.isfile(abs_path) or stat.S_ISFIFO(os.stat(abs_path).st_mode)): raise PathError(f"Path is a file: {abs_path!r}") if "R" in mode and os.access(abs_path, os.R_OK): raise PathError(f"{ptype} is readable: {abs_path!r}") if "W" in mode and os.access(abs_path, os.W_OK): raise PathError(f"{ptype} is writable: {abs_path!r}") if "X" in mode and os.access(abs_path, os.X_OK): raise PathError(f"{ptype} is executable: {abs_path!r}") self._relative = path self._absolute = abs_path self._cwd = cwd self._mode = mode self._is_url = is_url self._is_fsspec = is_fsspec self._url_data = url_data @property def relative(self) -> str: """Returns the relative representation of the path (how the path was given on instance creation).""" return self._relative @property def absolute(self) -> str: """Returns the absolute representation of the path.""" return self._absolute @property def mode(self) -> str: return self._mode @property def is_url(self) -> bool: return self._is_url @property def is_fsspec(self) -> bool: return self._is_fsspec def __str__(self): return self._relative def __repr__(self): name = "Path_" + self._mode name = self._repr_skip_check(name) cwd = "" if self._relative != self._absolute: cwd = ", cwd=" + self._cwd return f"{name}({self._relative}{cwd})" def __fspath__(self) -> str: return self._absolute def __eq__(self, other: Any) -> bool: if isinstance(other, Path): return self._absolute == other._absolute elif isinstance(other, str): return str(self) == other return False def read_text(self) -> str: """Returns the text contents of the file or the remote path.""" if self._std_io: return _read_cached_stdin() elif self._is_url: requests = import_requests("Path.read_text") response = requests.get(self._absolute) response.raise_for_status() return response.text elif self._is_fsspec: fsspec = import_fsspec("Path.read_text") with fsspec.open(self._absolute, "r") as handle: with handle as input_file: return input_file.read() else: with open(self._absolute) as input_file: return input_file.read() @contextmanager def open(self, mode: str = "r") -> Iterator[IO]: """Return an opened file object for the path.""" if self._std_io: if "r" in mode: yield _get_cached_stdin() elif "w" in mode: yield sys.stdout elif self._is_url: yield StringIO(self.read_text()) elif self._is_fsspec: fsspec = import_fsspec("Path.open") with fsspec.open(self._absolute, mode) as handle: yield handle else: with open(self._absolute, mode) as handle: yield handle @contextmanager def relative_path_context(self) -> Iterator[str]: """Context manager to use this path's parent (directory or URL) for relative paths defined within.""" with change_to_path_dir(self) as path_dir: assert isinstance(path_dir, str) yield path_dir @staticmethod def _check_mode(mode: str): if not isinstance(mode, str): raise ValueError("Expected mode to be a string.") if len(set(mode) - set("fdrwxcusFDRWX")) > 0: raise ValueError("Expected mode to only include [fdrwxcusFDRWX] flags.") for flag, count in Counter(mode).items(): if count > (2 if flag == "c" else 1): raise ValueError(f'Too many occurrences ({count}) for flag "{flag}".') if "f" in mode and "d" in mode: raise ValueError('Both modes "f" and "d" not possible.') if "u" in mode and "d" in mode: raise ValueError('Both modes "d" and "u" not possible.') if "s" in mode and "d" in mode: raise ValueError('Both modes "d" and "s" not possible.') @contextmanager 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: bool | str = False if path is not None: if isinstance(path, str): path = Path(path, mode="d") if path._url_data and (path.is_url or path.is_fsspec): scheme = path._url_data.scheme path_dir = path._url_data.url_path else: scheme = "" path_dir = path.absolute chdir = True if "d" not in path.mode: path_dir = os.path.dirname(path_dir) path_dir = scheme + path_dir token = _current_path_dir.set(path_dir) if chdir and path_dir: chdir = os.getcwd() path_dir = os.path.abspath(path_dir) os.chdir(path_dir) try: yield path_dir finally: _current_path_dir.reset(token) if chdir: os.chdir(chdir) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_postponed_annotations.py0000644000175100017510000003204715230046455023615 0ustar00runnerrunnerimport ast import inspect import logging import sys import textwrap from dataclasses import is_dataclass from importlib import import_module from typing import Any, ForwardRef, TypeAlias, Union, get_type_hints from ._typehints import mapping_origin_types, sequence_origin_types, tuple_set_origin_types from ._util import get_typehint_origin _TRIGGER_MODULE_CACHE_MAXSIZE = 1024 _TRIGGER_MODULE_CACHE: dict[int, dict[str, Any]] = {} class NamesVisitor(ast.NodeVisitor): def visit_Name(self, node: ast.Name) -> None: self.names_found.append(node.id) def find(self, node: ast.AST) -> list: from ._util import unique self.names_found: list[str] = [] self.visit(node) self.names_found = unique(self.names_found) return self.names_found class TypeCheckingVisitor(ast.NodeVisitor): type_checking_names: list[str] = [] def visit_Import(self, node: ast.Import) -> None: for alias in node.names: if alias.name == "typing": name = ast.dump( ast.Attribute( value=ast.Name(id=alias.asname or "typing", ctx=ast.Load()), attr="TYPE_CHECKING", ctx=ast.Load(), ) ) self.type_checking_names.append(name) break def visit_ImportFrom(self, node: ast.ImportFrom) -> None: if node.module == "typing": for alias in node.names: if alias.name == "TYPE_CHECKING": name = ast.dump(ast.Name(id=alias.asname or "TYPE_CHECKING", ctx=ast.Load())) self.type_checking_names.append(name) break def visit_If(self, node: ast.If) -> None: if ( isinstance(node.test, (ast.Name, ast.Attribute)) and any(ast.dump(node.test) == n for n in self.type_checking_names) ) or ( isinstance(node.test, ast.BoolOp) and isinstance(node.test.op, (ast.And, ast.Or)) and any(ast.dump(v) == n for n in self.type_checking_names for v in node.test.values) ): ast_exec = ast.parse("") ast_exec.body = node.body try: exec(compile(ast_exec, filename="", mode="exec"), self.aliases, self.aliases) except Exception as ex: if self.logger: self.logger.debug(f"Failed to execute 'TYPE_CHECKING' block in '{self.module}'", exc_info=ex) def generic_visit(self, node: ast.AST) -> None: if isinstance(node, (ast.If, ast.Module)): super().generic_visit(node) def update_aliases( self, module_source: str, module: str, aliases: dict, logger: logging.Logger | None = None ) -> None: self.module = module self.aliases = aliases self.logger = logger module_tree = ast.parse(module_source) self.visit(module_tree) def get_arg_type(arg_ast, aliases): type_ast = ast.parse("___arg_type___ = 0") type_ast.body[0].value = arg_ast exec_vars = {} bad_aliases = {} add_asts = False for name in NamesVisitor().find(arg_ast): value = aliases[name] if isinstance(value, tuple): value = value[1] if isinstance(value, Exception): bad_aliases[name] = value elif isinstance(value, ast.AST): add_asts = True else: exec_vars[name] = value if add_asts: body = [] for name, (_, value) in aliases.items(): if isinstance(value, ast.AST): body.append(ast.fix_missing_locations(value)) elif not isinstance(value, Exception): exec_vars[name] = value type_ast.body = body + type_ast.body if "TypeAlias" not in exec_vars: exec_vars["TypeAlias"] = TypeAlias try: exec(compile(type_ast, filename="", mode="exec"), exec_vars, exec_vars) except NameError as ex: ex_from = None for name, alias_exception in bad_aliases.items(): if str(ex) == f"name '{name}' is not defined": ex_from = alias_exception break raise ex from ex_from return exec_vars["___arg_type___"] def resolve_forward_refs(arg_type, aliases, logger): def resolve_subtypes_forward_refs(typehint): if has_subtypes(typehint): try: subtypes = [] for arg in typehint.__args__: if isinstance(arg, (ForwardRef, str)): forward_arg = arg.__forward_arg__ if isinstance(arg, ForwardRef) else arg forward_arg, *_ = forward_arg.split(".", 1) if forward_arg in aliases: arg = aliases[forward_arg] else: raise NameError(f"Name '{forward_arg}' is not defined") else: arg = resolve_subtypes_forward_refs(arg) subtypes.append(arg) if subtypes != list(typehint.__args__): typehint_origin = get_typehint_origin(typehint) typehint = typehint_origin[tuple(subtypes)] except Exception as ex: if logger: logger.debug(f"Failed to resolve forward refs in {typehint}", exc_info=ex) return typehint return resolve_subtypes_forward_refs(arg_type) def has_subtypes(typehint): typehint_origin = get_typehint_origin(typehint) if typehint_origin is type and hasattr(typehint, "__args__"): return True return ( typehint_origin == Union or typehint_origin in sequence_origin_types or typehint_origin in tuple_set_origin_types or typehint_origin in mapping_origin_types ) def type_requires_eval(typehint): if has_subtypes(typehint): return any(type_requires_eval(a) for a in getattr(typehint, "__args__", [])) return isinstance(typehint, (str, ForwardRef)) def _collect_string_fwd_ref_names(typehint: Any, result: set[str]) -> None: if isinstance(typehint, str): result.add(typehint.split(".")[0]) elif isinstance(typehint, ForwardRef): result.add(typehint.__forward_arg__.split(".")[0]) elif has_subtypes(typehint): for arg in getattr(typehint, "__args__", ()): _collect_string_fwd_ref_names(arg, result) def _update_missing_from_module_vars(global_vars: dict, missing: set[str], mod_vars: dict[str, Any]) -> None: for name in missing.copy(): if name in mod_vars: global_vars[name] = mod_vars[name] missing.discard(name) def _cache_trigger_bindings(trigger_id: int, mod_vars: dict[str, Any], names: set[str]) -> None: cached_bindings = _TRIGGER_MODULE_CACHE.get(trigger_id) if cached_bindings is None: if _TRIGGER_MODULE_CACHE_MAXSIZE > 0 and len(_TRIGGER_MODULE_CACHE) >= _TRIGGER_MODULE_CACHE_MAXSIZE: del _TRIGGER_MODULE_CACHE[next(iter(_TRIGGER_MODULE_CACHE))] cached_bindings = {} _TRIGGER_MODULE_CACHE[trigger_id] = cached_bindings for name in names: if name in mod_vars: cached_bindings[name] = mod_vars[name] def _enrich_globals_for_string_forward_refs(global_vars: dict[str, Any]) -> None: """Add to global_vars types referenced as string forward refs in generic aliases but missing from it. Handles the case where a generic alias such as ``list["ForwardReferenced"]`` was defined in module A and imported into module B, but ``ForwardReferenced`` was not imported into module B. """ # Collect all string/ForwardRef names nested inside generic alias args needed: set[str] = set() trigger_value_ids: set[int] = set() for value in global_vars.values(): # Only consider generic/type-hint-like values (with subtypes) or ForwardRef. # Avoid treating arbitrary string globals (e.g., __name__, __doc__) as forward refs. if not (hasattr(value, "__args__") or isinstance(value, ForwardRef)): continue before = len(needed) _collect_string_fwd_ref_names(value, needed) if len(needed) > before: trigger_value_ids.add(id(value)) missing = needed - set(global_vars.keys()) if not missing: return # Reuse the previously discovered bindings before scanning sys.modules again. for trigger_id in trigger_value_ids: cached_bindings = _TRIGGER_MODULE_CACHE.get(trigger_id, {}) _update_missing_from_module_vars(global_vars, missing, cached_bindings) if not missing: return # Find candidate modules: those that define the same trigger values (by identity). # This lets us trace generic aliases back to their origin module. for mod in sys.modules.values(): if mod is None or not missing: continue try: mod_vars = vars(mod) except TypeError: continue matched_trigger_ids = {id(value) for value in mod_vars.values() if id(value) in trigger_value_ids} if not matched_trigger_ids: continue for trigger_id in matched_trigger_ids: _cache_trigger_bindings(trigger_id, mod_vars, needed) _update_missing_from_module_vars(global_vars, missing, mod_vars) 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] if is_dataclass(next_mro): global_vars.update(get_global_vars(next_mro, logger)) for key, value in vars(import_module(obj.__module__)).items(): # needed for pydantic-v1 if key not in global_vars: global_vars[key] = value try: module_source = inspect.getsource(sys.modules[obj.__module__]) if obj.__module__ in sys.modules else "" if "TYPE_CHECKING" in module_source: TypeCheckingVisitor().update_aliases(module_source, obj.__module__, global_vars, logger) except Exception as ex: if logger: logger.debug(f"Failed to update aliases for TYPE_CHECKING blocks in {obj.__module__}", exc_info=ex) _enrich_globals_for_string_forward_refs(global_vars) return global_vars 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 not isinstance(types, Exception) and all(not type_requires_eval(t) for t in types.values()): return types try: source = textwrap.dedent(inspect.getsource(obj)) tree = ast.parse(source) assert isinstance(tree, ast.Module) and len(tree.body) == 1 node = tree.body[0] assert isinstance(node, (ast.FunctionDef, ast.ClassDef)) except Exception as ex2: 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) ex = None if isinstance(types, Exception): ex = types types = {} arg_asts = [(a.arg, a.annotation) for a in node.args.args + node.args.kwonlyargs] # type: ignore[union-attr] for name, annotation in arg_asts: if annotation and (name not in types or type_requires_eval(types[name])): try: arg_type = get_arg_type(annotation, aliases) types[name] = resolve_forward_refs(arg_type, aliases, logger) except Exception as ex3: types[name] = ex3 if all(isinstance(t, Exception) for t in types.values()): raise ex or next(iter(types.values())) return types def evaluate_postponed_annotations(params, component, parent, logger): if not (params and any(type_requires_eval(p.annotation) for p in params)): return try: if is_dataclass(parent) and component.__name__ == "__init__": types = get_types(parent, logger) else: types = get_types(component, logger) except Exception as ex: logger.debug(f"Unable to evaluate types for {component}", exc_info=ex) return for param in params: if param.name in types: param_type = types[param.name] if isinstance(param_type, Exception): logger.debug(f"Unable to evaluate type of {param.name} from {component}", exc_info=param_type) continue param.annotation = param_type def get_return_type(component, logger=None): return_type = inspect.signature(component).return_annotation if type_requires_eval(return_type): global_vars = get_global_vars(component, logger) try: return_type = get_type_hints(component, global_vars)["return"] except Exception as ex: if logger: logger.debug(f"Unable to evaluate types for {component}", exc_info=ex) return None return return_type ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_required.py0000644000175100017510000000663415230046455021010 0ustar00runnerrunnerfrom argparse import Action, _SubParsersAction from collections.abc import Iterator from contextlib import contextmanager from contextvars import ContextVar from ._type_checking import ArgumentParser _suppressed_required_actions: ContextVar[tuple[Action, ...]] = ContextVar("_suppressed_required_actions", default=()) def _iter_required_action_keys(parser: ArgumentParser) -> Iterator[str]: """Yields required destinations backed by real argparse actions.""" for action in parser._actions: if action.required: yield action.dest def _iter_extra_required_keys(parser: ArgumentParser) -> Iterator[str]: """Yields required keys tracked outside concrete argparse actions.""" yield from parser._extra_required_keys def iter_required_keys(parser: ArgumentParser) -> Iterator[str]: """Yields required keys with action-backed ones first.""" yielded = set() for key in _iter_required_action_keys(parser): yielded.add(key) yield key for key in sorted(_iter_extra_required_keys(parser)): if key not in yielded: yield key 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] if action is None: action = _find_exact_action(parser, key) if action is not None and getattr(action, "dest", None) == key: action.required = value elif value: parser._extra_required_keys.add(key) else: parser._extra_required_keys.discard(key) 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) -> Action | None: for action in parser._actions: if getattr(action, "dest", None) == key: return action return None @contextmanager def suppress_required_actions(parser: ArgumentParser): """Temporarily disables required enforcement on real argparse actions.""" suppressed = [] previously_suppressed = _suppressed_required_actions.get() visited = set() def visit(subparser): if id(subparser) in visited: return visited.add(id(subparser)) for action in subparser._actions: if action.required: suppressed.append(action) action.required = False if isinstance(action, _SubParsersAction): for choice_parser in action.choices.values(): visit(choice_parser) visit(parser) token = _suppressed_required_actions.set(previously_suppressed + tuple(suppressed)) try: yield finally: _suppressed_required_actions.reset(token) for action in reversed(suppressed): action.required = True @contextmanager def restore_suppressed_required(): """Temporarily restores required=True for actions suppressed by suppress_required_actions.""" suppressed = _suppressed_required_actions.get() for action in suppressed: action.required = True try: yield finally: for action in suppressed: action.required = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_signatures.py0000644000175100017510000006332515230046455021354 0ustar00runnerrunner"""Methods to add arguments based on class/method/function signatures.""" import dataclasses import inspect import os import re from argparse import SUPPRESS, ArgumentParser from collections.abc import Callable from typing import Any, Optional, Union from ._actions import _ActionConfigLoad from ._common import ( LoggerProperty, get_generic_origin, get_parsing_setting, get_unaliased_type, is_final_class, is_subclass, is_subclasses_disabled, ) from ._deprecated import deprecation_warning, renamed_parameter_warning from ._instantiation import get_class_instantiator from ._namespace import Namespace, get_value_and_parent from ._optionals import attrs_support, get_doc_short_description, is_attrs_class, is_pydantic_model from ._parameter_resolvers import ParamData, get_parameter_origins, get_signature_parameters from ._required import set_required from ._typehints import ( ActionTypeHint, callable_instances, get_subclass_names, is_list_pathlike, is_optional, not_required_types, sequence_origin_types, ) from ._util import NoneType, get_import_path, get_private_kwargs, get_typehint_origin, iter_to_set_str from .typing import _LazyInitBaseClass, register_pydantic_type kinds = inspect._ParameterKind inspect_empty = inspect._empty class SignatureArguments(LoggerProperty): """Methods to add arguments based on signatures to an :class:`ArgumentParser` instance.""" @renamed_parameter_warning({"theclass": "class_type"}) def add_class_arguments( self, class_type: type, nested_key: str | None = None, as_group: bool = True, as_positional: bool = False, default: dict | Namespace | type | None = None, skip: set[str | int] | None = None, instantiate: bool = True, fail_untyped: bool = True, sub_configs: bool = False, **kwargs, ) -> list[str]: """Adds arguments from a class based on its type hints and docstrings. Note: Keyword arguments without at least one valid type are ignored. Args: class_type: Class from which to add arguments. nested_key: Key for nested namespace. as_group: Whether arguments should be added to a new argument group. as_positional: Whether to add required parameters as positional arguments. default: Default value used to override parameter defaults. skip: Names of parameters or number of positionals that should be skipped. instantiate: Whether the class group should be instantiated by :meth:`instantiate <.ArgumentParser.instantiate>`. fail_untyped: Whether to raise exception if a required parameter does not have a type. sub_configs: Whether subclass type hints should be loadable from inner config file. Returns: The list of arguments added. Raises: ValueError: When not given a class. ValueError: When there are required parameters without at least one valid type. """ unaliased_class_type = get_unaliased_type(class_type) if not inspect.isclass(get_generic_origin(unaliased_class_type)): raise ValueError(f"Expected 'class_type' parameter to be a class type, got: {class_type}") if not ( isinstance(default, (NoneType, dict, Namespace)) or (isinstance(default, _LazyInitBaseClass) and isinstance(default, unaliased_class_type)) or ( not is_final_class(default.__class__) and is_subclasses_disabled(default.__class__) and isinstance(default, unaliased_class_type) ) ): raise ValueError( f"Expected 'default' to be dict, Namespace, lazy instance or dataclass-like, got: {default}" ) linked_targets, help_, _ = get_private_kwargs( kwargs, linked_targets=None, help=None, required=None, # Ignored because provided when adding signatures, remove with dataclass inheritance support ) added_args = self._add_signature_arguments( class_type, None, nested_key, as_group, as_positional, skip, fail_untyped, sub_configs=sub_configs, instantiate=instantiate, linked_targets=linked_targets, help=help_, ) if default: skip = skip or set() prefix = nested_key + "." if nested_key else "" defaults = default if isinstance(default, _LazyInitBaseClass): defaults = default.lazy_get_init_args().as_dict() elif is_convertible_to_dict(default.__class__): defaults = convert_to_dict(default) args = {k[len(prefix) :] for k in added_args} skip_not_added = [k for k in defaults if k not in args] if skip_not_added: skip.update(skip_not_added) # skip init=False if defaults: defaults = {prefix + k: v for k, v in defaults.items() if k not in skip} # type: ignore[union-attr] self.set_defaults(**defaults) # type: ignore[attr-defined] return added_args @renamed_parameter_warning({"theclass": "class_type", "themethod": "method_name"}) def add_method_arguments( self, class_type: type, method_name: str, nested_key: str | None = None, as_group: bool = True, as_positional: bool = False, skip: set[str | int] | None = None, fail_untyped: bool = True, sub_configs: bool = False, ) -> list[str]: """Adds arguments from a class based on its type hints and docstrings. Note: Keyword arguments without at least one valid type are ignored. Args: class_type: Class which includes the method. method_name: Name of the method for which to add arguments. nested_key: Key for nested namespace. as_group: Whether arguments should be added to a new argument group. as_positional: Whether to add required parameters as positional arguments. skip: Names of parameters or number of positionals that should be skipped. fail_untyped: Whether to raise exception if a required parameter does not have a type. sub_configs: Whether subclass type hints should be loadable from inner config file. Returns: The list of arguments added. Raises: ValueError: When not given a class or the name of a method of the class. ValueError: When there are required parameters without at least one valid type. """ unaliased_type = get_unaliased_type(class_type) if not inspect.isclass(get_generic_origin(unaliased_type)): raise ValueError('Expected "class_type" argument to be a class object.') if not hasattr(unaliased_type, method_name) or not callable(getattr(unaliased_type, method_name)): raise ValueError('Expected "method_name" argument to be a callable member of the class.') return self._add_signature_arguments( class_type, method_name, nested_key, as_group, as_positional, skip, fail_untyped, sub_configs=sub_configs, ) def add_function_arguments( self, function: Callable, nested_key: str | None = None, as_group: bool = True, as_positional: bool = False, skip: set[str | int] | None = None, fail_untyped: bool = True, sub_configs: bool = False, ) -> list[str]: """Adds arguments from a function based on its type hints and docstrings. Note: Keyword arguments without at least one valid type are ignored. Args: function: Function from which to add arguments. nested_key: Key for nested namespace. as_group: Whether arguments should be added to a new argument group. as_positional: Whether to add required parameters as positional arguments. skip: Names of parameters or number of positionals that should be skipped. fail_untyped: Whether to raise exception if a required parameter does not have a type. sub_configs: Whether subclass type hints should be loadable from inner config file. Returns: The list of arguments added. Raises: ValueError: When not given a callable. ValueError: When there are required parameters without at least one valid type. """ if not callable(function): raise ValueError('Expected "function" argument to be a callable object.') method_name = None if hasattr(function, "__class__") and callable_instances(function.__class__): function = function.__class__ method_name = "__call__" return self._add_signature_arguments( function, method_name, nested_key, as_group, as_positional, skip, fail_untyped, sub_configs=sub_configs, ) def _add_signature_arguments( self, function_or_class, method_name, nested_key: str | None, as_group: bool = True, as_positional: bool = False, skip: set[str | int] | None = None, fail_untyped: bool = True, sub_configs: bool = False, instantiate: bool = True, linked_targets: set[str] | None = None, help: str | None = None, ) -> list[str]: """Adds arguments from parameters of objects based on signatures and docstrings. Args: function_or_class: Object from which to add arguments. method_name: Class method from which to add arguments. nested_key: Key for nested namespace. as_group: Whether arguments should be added to a new argument group. as_positional: Whether to add required parameters as positional arguments. skip: Names of parameters or number of positionals that should be skipped. fail_untyped: Whether to raise exception if a required parameter does not have a type. sub_configs: Whether subclass type hints should be loadable from inner config file. instantiate: Whether the class group should be instantiated. Returns: The list of arguments added. Raises: ValueError: When there are required parameters without at least one valid type. """ params = get_signature_parameters(function_or_class, method_name, logger=self.logger) skip_positionals = [s for s in (skip or []) if isinstance(s, int) and s != 0] if skip_positionals: if len(skip_positionals) > 1 or any(p <= 0 for p in skip_positionals): raise ValueError(f"Unexpected number of positionals to skip: {skip_positionals}") names = {p.name for p in params[: skip_positionals[0]]} params = params[skip_positionals[0] :] self.logger.debug( f"Skipping parameters {names} because {skip_positionals[0]} positionals requested to be skipped." ) prefix = "--" + (nested_key + "." if nested_key else "") for param in params: if skip and param.name in skip: continue if prefix + param.name in self._option_string_actions: # type: ignore[attr-defined] raise ValueError( f"Unable to add parameter '{param.name}' from {function_or_class} because " f"argument '{prefix + param.name}' already exists." ) ## Create group if requested ## if help is not None: doc_group = help else: doc_group = get_doc_short_description(function_or_class, method_name, logger=self.logger) component = getattr(function_or_class, method_name) if method_name else function_or_class container = self._create_group_if_requested( component, nested_key, as_group, doc_group, config_load=len(params) > 0, instantiate=instantiate, ) ## Add parameter arguments ## added_args: list[str] = [] for param in params: self._add_signature_parameter( container, nested_key, param, added_args, skip={s for s in (skip or []) if isinstance(s, str)}, fail_untyped=fail_untyped, sub_configs=sub_configs, linked_targets=linked_targets, as_positional=as_positional, ) return added_args def _add_signature_parameter( self, container, nested_key: str | None, param, added_args: list[str], skip: set[str] | None = None, fail_untyped: bool = True, as_positional: bool = False, sub_configs: bool = False, instantiate: bool = True, linked_targets: set[str] | None = None, default: Any = inspect_empty, **kwargs, ): name = param.name kind = param.kind annotation = param.annotation if default == inspect_empty: default = param.default if default == inspect_empty: if is_optional(annotation): if os.environ.get("JSONARGPARSE_DEPRECATION_WARNINGS", "").lower() == "all": deprecation_warning( "signature_optional_parameter_without_default", "Optional type parameters without a default are currently not required. " "In v5 they will be required.", stacklevel=4, ) unset_sentinel = get_parsing_setting("unset_sentinel") default = unset_sentinel if unset_sentinel is not None else None elif get_typehint_origin(annotation) in not_required_types: default = SUPPRESS # Determine argument characteristics based on parameter kind and default value if kind == kinds.POSITIONAL_ONLY: is_required = True # Always required is_non_positional = False # Can be positional elif kind == kinds.KEYWORD_ONLY: is_required = default == inspect_empty # Required if no default is_non_positional = True # Must use --flag style elif kind in {kinds.POSITIONAL_OR_KEYWORD, None}: # POSITIONAL_OR_KEYWORD or programmatically created parameters without kind is_required = default == inspect_empty # Required if no default is_non_positional = False # Can be positional else: raise RuntimeError(f"The code should never reach here: kind={kind}") # pragma: no cover src = get_parameter_origins(param.component, param.parent) skip_message = f'Skipping parameter "{name}" from "{src}" because of: ' if not fail_untyped and annotation == inspect_empty: if is_required and os.environ.get("JSONARGPARSE_DEPRECATION_WARNINGS", "").lower() == "all": deprecation_warning( "fail_untyped_false_required_parameter", "With fail_untyped=False, required parameters without a type annotation are currently " "set to optional with default None. In v5 the type will be set to Any but the parameter " "will remain required.", stacklevel=4, ) annotation = Any default = None if is_required else default is_required = False is_required_link_target = False if is_required and linked_targets is not None and name in linked_targets: default = None is_required = False is_required_link_target = True if not is_required and name[0] == "_": return elif skip and name in skip: self.logger.debug(skip_message + "Parameter requested to be skipped.") return if is_factory_class(default): default = param.parent.__dataclass_fields__[name].default_factory() if annotation == inspect_empty and not is_required: annotation = Union[type(default), Any] if "help" not in kwargs: kwargs["help"] = param.doc if not is_required: kwargs["default"] = default if default is None and not is_optional(annotation, object) and not is_required_link_target: annotation = Optional[annotation] elif not as_positional or is_non_positional: kwargs["required"] = True is_subclass_typehint = False nested_skip: set[str] = set() subclasses_disabled = is_subclasses_disabled(annotation) dest = (nested_key + "." if nested_key else "") + name args = [dest if is_required and as_positional and not is_non_positional else "--" + dest] if param.origin: parser = container if not isinstance(container, ArgumentParser): parser = getattr(container, "parser") group_name = "; ".join(str(o) for o in param.origin) if group_name in parser.groups: container = parser.groups[group_name] else: container = parser.add_argument_group( f"Conditional arguments [origins: {group_name}]", name=group_name, ) if annotation in {str, int, float, bool} or is_subclass(annotation, (str, int, float)) or subclasses_disabled: kwargs["type"] = annotation register_pydantic_type(annotation) elif annotation != inspect_empty: try: is_subclass_typehint = ActionTypeHint.is_subclass_typehint(annotation, all_subtypes=False) is_return_subclass_typehint = ActionTypeHint.is_return_subclass_typehint(annotation) kwargs["type"] = annotation sub_add_kwargs: dict = {"fail_untyped": fail_untyped, "sub_configs": sub_configs} if is_subclass_typehint or is_return_subclass_typehint: prefix = f"{name}.init_args." nested_skip = {s[len(prefix) :] for s in skip or [] if s.startswith(prefix)} sub_add_kwargs["skip"] = nested_skip else: register_pydantic_type(annotation) enable_path = sub_configs and ( is_subclass_typehint or is_return_subclass_typehint or is_list_pathlike(annotation) ) args = ActionTypeHint.prepare_add_argument( args=args, kwargs=kwargs, enable_path=enable_path, container=container, logger=self.logger, sub_add_kwargs=sub_add_kwargs, ) except ValueError as ex: self.logger.debug(skip_message + str(ex)) if "type" in kwargs or "action" in kwargs: sub_add_kwargs = { "fail_untyped": fail_untyped, "sub_configs": sub_configs, "instantiate": instantiate, } if subclasses_disabled: kwargs.update(sub_add_kwargs) with ActionTypeHint.allow_default_instance_context(): action = container.add_argument(*args, **kwargs) if action is not None: # None when class without any parameters action.sub_add_kwargs = sub_add_kwargs if nested_skip: action.sub_add_kwargs["skip"] = nested_skip added_args.append(dest) elif is_required and fail_untyped: raise ValueError( "With fail_untyped=True, all mandatory parameters must have a supported" f" type. Parameter '{name}' from '{src}' does not specify a type." ) def add_subclass_arguments( self, baseclass: type | tuple[type, ...], nested_key: str, as_group: bool = True, skip: set[str] | None = None, instantiate: bool = True, required: bool = False, metavar: str = "CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE", help: str = ( 'One or more arguments specifying "class_path" and "init_args" for any subclass of %(baseclass_name)s.' ), **kwargs, ): """Adds arguments to allow specifying any subclass of the given base class. This adds an argument that requires a dictionary with a ``class_path`` entry which must be a import dot notation expression. Optionally any init arguments for the class can be given in the ``init_args`` entry. Since subclasses can have different init arguments, the help does not show the details of the arguments of the base class. Instead a help argument is added that will print the details for a given class path. Args: baseclass: Base class or classes to use to check subclasses. nested_key: Key for nested namespace. as_group: Whether arguments should be added to a new argument group. skip: Names of parameters that should be skipped. required: Whether the argument group is required. metavar: Variable string to show in the argument's help. help: Description of argument to show in the help. **kwargs: Additional parameters like in :meth:`add_class_arguments`. Raises: ValueError: When given an invalid base class. """ if type(baseclass) is not tuple: baseclass = (baseclass,) assert isinstance(baseclass, tuple) if not baseclass or not all(ActionTypeHint.is_subclass_typehint(c, also_lists=True) for c in baseclass): raise ValueError(f"Expected 'baseclass' to be a subclass type or a tuple of subclass types: {baseclass}") doc_group = None if len(baseclass) == 1: doc_group = get_doc_short_description(baseclass[0], logger=self.logger) group = self._create_group_if_requested( baseclass, nested_key, as_group, doc_group, config_load=False, required=required, instantiate=False, ) added_args: list[str] = [] if skip is not None: skip = {f"{nested_key}.init_args." + s for s in skip} param = ParamData(name=nested_key, annotation=Union[baseclass], component=baseclass) str_baseclass = iter_to_set_str(get_subclass_names(param.annotation)) kwargs.update( { "metavar": metavar, "help": (help % {"baseclass_name": str_baseclass}), } ) if "default" not in kwargs: kwargs["default"] = None self._add_signature_parameter( group, None, param, added_args, skip, sub_configs=True, instantiate=instantiate, **kwargs ) def _create_group_if_requested( self, obj, nested_key, as_group, doc_group, config_load=True, config_load_type=None, required=False, instantiate=True, ): if required: if nested_key is None: raise ValueError("A nested_key is mandatory to make required.") set_required(self, nested_key) group = self if as_group: if doc_group is None: if isinstance(obj, tuple) and len(obj) == 1: doc_group = str(obj[0]) else: doc_group = str(obj) 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)) if inspect.isclass(obj) and nested_key is not None and instantiate: group.dest = nested_key.replace("-", "_") group.group_class = obj group.instantiate_class = group_instantiate_class return group def group_instantiate_class(group, cfg): try: value, parent, key = get_value_and_parent(cfg, group.dest) except KeyError: value = {} parent = cfg key = group.dest instantiator_fn = get_class_instantiator() parent[key] = instantiator_fn(group.group_class, **value) def strip_title(value): if value is not None: value = re.sub(r"\.$", "", value.strip()) return value def is_factory_class(value): return value.__class__ == dataclasses._HAS_DEFAULT_FACTORY_CLASS def is_convertible_to_dict(value): return dataclasses.is_dataclass(value) or is_attrs_class(value) or is_pydantic_model(value) def convert_to_dict(value) -> dict: if attrs_support: import attrs if attrs.has(type(value)): return attrs.asdict(value) value_type = type(value) init_args = {} for name, attr in vars(value).items(): attr_type = type(attr) if is_convertible_to_dict(attr_type): attr = convert_to_dict(attr) elif attr_type in sequence_origin_types: attr = attr.copy() for num, item in enumerate(attr): if is_convertible_to_dict(type(item)): attr[num] = convert_to_dict(item) init_args[name] = attr if is_subclasses_disabled(value_type): return init_args return {"class_path": get_import_path(value_type), "init_args": init_args} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_stubs_resolver.py0000644000175100017510000002612515230046455022246 0ustar00runnerrunnerimport ast import inspect import sys from contextlib import suppress from copy import deepcopy from importlib import import_module from typing import TYPE_CHECKING, Any from ._common import get_parsing_setting from ._optionals import import_typeshed_client, typeshed_client_support from ._postponed_annotations import NamesVisitor, get_arg_type if TYPE_CHECKING: # pragma: no cover import typeshed_client as tc else: tc = import_typeshed_client() kinds = inspect._ParameterKind def import_module_or_none(path: str): if path.endswith(".__init__"): path = path[:-9] try: return import_module(path) except ModuleNotFoundError: return None class ImportsVisitor(ast.NodeVisitor): def visit_ImportFrom(self, node: ast.ImportFrom) -> None: if node.level: module_path = self.module_path[: -node.level] if node.module: module_path.append(node.module) node = deepcopy(node) node.module = ".".join(module_path) node.level = 0 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[str | None, str]]: self.module_path = module_path.split(".") self.imports_found: dict[str, tuple[str | None, str]] = {} self.visit(node) return self.imports_found def ast_annassign_to_assign(node: ast.AnnAssign) -> ast.Assign: return ast.Assign( targets=[node.target], value=node.value, # type: ignore[arg-type] lineno=node.lineno, end_lineno=node.lineno, ) class AssignsVisitor(ast.NodeVisitor): def visit_Assign(self, node: ast.Assign) -> None: for target in node.targets: if hasattr(target, "id"): self.assigns_found[target.id] = node def visit_AnnAssign(self, node: ast.AnnAssign) -> None: if hasattr(node.target, "id"): self.assigns_found[node.target.id] = ast_annassign_to_assign(node) def find(self, node: ast.AST) -> dict[str, ast.Assign]: self.assigns_found: dict[str, ast.Assign] = {} self.visit(node) return self.assigns_found class MethodsVisitor(ast.NodeVisitor): method_found: ast.FunctionDef | None def visit_FunctionDef(self, node: ast.FunctionDef) -> None: if not self.method_found and node.name == self.method_name: self.method_found = node def visit_If(self, node: ast.If) -> None: test_ast = ast.parse("___test___ = 0") test_ast.body[0].value = node.test # type: ignore[attr-defined] exec_vars = {"sys": sys} with suppress(Exception): exec(compile(test_ast, filename="", mode="exec"), exec_vars, exec_vars) if exec_vars["___test___"]: node.orelse = [] else: node.body = [] self.generic_visit(node) def find(self, node: ast.AST, method_name: str) -> ast.FunctionDef | None: self.method_name = method_name self.method_found = None self.visit(node) return self.method_found stubs_resolver = None def get_stubs_resolver(): global stubs_resolver if not stubs_resolver: allow_py_files = get_parsing_setting("stubs_resolver_allow_py_files") search_context = tc.get_search_context(allow_py_files=allow_py_files) stubs_resolver = StubsResolver(search_context=search_context) return stubs_resolver def get_mro_method_parent(parent, method_name): while hasattr(parent, "__dict__") and method_name not in parent.__dict__: try: parent = inspect.getmro(parent)[1] except IndexError: parent = None return None if parent is object else parent def get_source_module(path: str, component) -> tc.ModulePath: if component is None: module_path, name = path.rsplit(".", 1) component = getattr(import_module_or_none(module_path), name, None) if component is not None: module = inspect.getmodule(component) assert module is not None module_path = module.__name__ if getattr(module, "__file__", "").endswith("__init__.py"): module_path += ".__init__" return tc.ModulePath(tuple(module_path.split("."))) class StubsResolver(tc.Resolver): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) 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[str | None, str]]] = {} 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): imported_info = resolved elif isinstance(resolved, tc.NameInfo): source_module = get_source_module(path, component) imported_info = tc.ImportedInfo(source_module=source_module, info=resolved) return imported_info 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__) if not parent: return self.get_imported_info(f"{component.__module__}.{component.__name__}", component) parent = get_mro_method_parent(parent, component.__name__) stub_import = parent and self.get_imported_info(f"{parent.__module__}.{parent.__name__}", component) if stub_import and isinstance(stub_import.info.ast, ast.AST): method_ast = MethodsVisitor().find(stub_import.info.ast, component.__name__) assert method_ast name_info = tc.NameInfo(name=component.__qualname__, is_exported=False, ast=method_ast) stub_import = tc.ImportedInfo(source_module=stub_import.source_module, info=name_info) return stub_import def get_aliases(self, imported_info: tc.ImportedInfo): aliases: dict[str, tuple[str, Any]] = {} self.add_import_aliases(aliases, imported_info) return aliases def get_module_stub_ast(self, module_path: str): if module_path not in self._module_ast_cache: self._module_ast_cache[module_path] = tc.get_stub_ast(module_path, search_context=self.ctx) return self._module_ast_cache[module_path] def get_module_stub_assigns(self, module_path: str): if module_path not in self._module_assigns_cache: module_ast = self.get_module_stub_ast(module_path) self._module_assigns_cache[module_path] = AssignsVisitor().find(module_ast) return self._module_assigns_cache[module_path] def get_module_stub_imports(self, module_path: str): if module_path not in self._module_imports_cache: module_ast = self.get_module_stub_ast(module_path) self._module_imports_cache[module_path] = ImportsVisitor().find(module_ast, module_path) return self._module_imports_cache[module_path] 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: 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): stub_ast = stub_import.info.ast if stub_ast: self.add_module_aliases(aliases, module_path, module, stub_ast) return module_path, stub_import.info.ast def add_module_aliases(self, aliases, module_path, module, node, skip=set()): names = NamesVisitor().find(node) if node else [] for name in names: if alias_already_added(aliases, name, module_path) or name in skip: continue source = module_path if name in __builtins__: source = "__builtins__" value = __builtins__[name] elif hasattr(module, name): value = getattr(module, name) elif name in self.get_module_stub_assigns(module_path): value = self.get_module_stub_assigns(module_path)[name] self.add_module_aliases(aliases, module_path, module, value.value, skip={name}) elif name in self.get_module_stub_imports(module_path): imported_module_path, imported_name = self.get_module_stub_imports(module_path)[name] imported_module = import_module_or_none(imported_module_path) if hasattr(imported_module, imported_name): source = imported_module_path value = getattr(imported_module, imported_name) else: stub_import = self.get_imported_info(f"{imported_module_path}.{imported_name}") source, value = self.add_import_aliases(aliases, stub_import) else: value = NotImplementedError(f"{name!r} from {module_path!r} not in builtins, module or stub") if alias_already_added(aliases, name, source): continue if not alias_is_unique(aliases, name, source, value): value = NotImplementedError( f"non-unique alias {name!r}: {aliases[name][1]} ({aliases[name][0]}) vs {value} ({source})" ) aliases[name] = (source, value) def alias_already_added(aliases, name, source): return name in aliases and aliases[name][0] in {"__builtins__", source} def alias_is_unique(aliases, name, source, value): if name in aliases: src, val = aliases[name] if src != source: return val is value return True def get_stub_types(params, component, parent, logger) -> dict[str, Any] | None: if not typeshed_client_support: return None missing_types = { p.name: n for n, p in enumerate(params) if p.kind not in {kinds.VAR_POSITIONAL, kinds.VAR_KEYWORD} and p.annotation == inspect._empty } if not missing_types: return None resolver = get_stubs_resolver() stub_import = resolver.get_component_imported_info(component, parent) if not stub_import: return None known_params = {p.name for p in params} aliases = resolver.get_aliases(stub_import) arg_asts = stub_import.info.ast.args.args + stub_import.info.ast.args.kwonlyargs types = {} for arg_ast in arg_asts[1:] if parent else arg_asts: name = arg_ast.arg if arg_ast.annotation and (name in missing_types or name not in known_params): try: types[name] = get_arg_type(arg_ast.annotation, aliases) except Exception as ex: logger.debug( f"Failed to parse type stub for {component.__qualname__!r} parameter {name!r}", exc_info=ex ) if name not in known_params: types[name] = inspect._empty # pragma: no cover return types ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_subcommands.py0000644000175100017510000002550415230046455021500 0ustar00runnerrunner"""Subcommands action and helper functions.""" from argparse import Action as ArgparseAction from argparse import _SubParsersAction from contextlib import contextmanager from contextvars import ContextVar from typing import NoReturn from ._actions import filter_non_parsing_actions from ._common import parsing_defaults, single_subcommand from ._deprecated import deprecated_implicit_subcommand from ._namespace import Namespace, NSKeyError, split_key, split_key_root from ._type_checking import ActionsContainer, ArgumentParser from ._util import merge_config __all__ = ["ActionSubCommands"] parse_kwargs: ContextVar = ContextVar("parse_kwargs", default={}) def is_branch_key(parser, key: str) -> bool: root_key = split_key_root(key)[0] for action in filter_non_parsing_actions(parser._actions): if isinstance(action, ActionSubCommands) and root_key in action._name_parser_map: subparser = action._name_parser_map[root_key] return is_branch_key(subparser, split_key_root(key)[1]) elif action.dest.startswith(key + "."): return True return False def find_action_and_subcommand( parser: ArgumentParser | ActionsContainer, dest: 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: actions = [a for a in actions if not isinstance(a, exclude)] # Subcommand names should take precedence over option-string fallback # (e.g. subcommand "info" vs option "--info"). for action in actions: if not isinstance(action, ActionSubCommands): continue if dest in action._name_parser_map: return action, None root_dest = split_key_root(dest)[0] if root_dest in action._name_parser_map: subcommand, subdest = split_key_root(dest) subparser = action._name_parser_map[subcommand] subaction, subsubcommand = find_action_and_subcommand(subparser, subdest, exclude=exclude) if subsubcommand is not None: subcommand += "." + subsubcommand return subaction, subcommand fallback_action = None for action in actions: if action.dest == dest or f"--{dest}" in action.option_strings: from ._actions import ActionFail, _ActionConfigLoad if isinstance(action, (_ActionConfigLoad, ActionFail)): fallback_action = action else: return action, None return fallback_action, None def find_action( parser: ArgumentParser | ActionsContainer, dest: str, 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: 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) for n in reversed(range(len(parts) - 1)): action, subcommand = find_action_and_subcommand(parser, ".".join(parts[: n + 1]), exclude=exclude) if action is not None: break return action, subcommand def find_parent_action( parser: ArgumentParser, key: str, exclude: type[ArgparseAction] | tuple[type[ArgparseAction], ...] | None = None, ) -> ArgparseAction | None: return find_parent_action_and_subcommand(parser, key, exclude=exclude)[0] class ActionSubCommands(_SubParsersAction): """Extension of argparse._SubParsersAction to modify subcommands functionality.""" parent_parser: ArgumentParser env_prefix: str def add_parser(self, *args, **kwargs) -> NoReturn: """Raises a ``NotImplementedError`` since jsonargparse uses ``add_subcommand``.""" raise NotImplementedError("In jsonargparse subcommands are added using the add_subcommand method.") def add_subcommand(self, name: str, parser: ArgumentParser, **kwargs) -> ArgumentParser: """Adds a parser as a subcommand parser. In contrast to `argparse.ArgumentParser.add_subparsers `_ add_parser requires to be given a parser as argument. Args: name: The name for the subcommand. parser: The parser to use for the subcommand. """ if parser._subparsers is not None: raise ValueError("Multiple levels of subcommands must be added in level order.") if self.dest == name: raise ValueError(f"A subcommand name can't be the same as the subcommands dest: '{name}'.") parser.prog = f"{self._prog_prefix} [options] {name}" parser.env_prefix = f"{self.env_prefix}{name}_" parser.default_env = self.parent_parser.default_env parser.parent_parser = self.parent_parser # type: ignore[attr-defined] parser.parser_mode = self.parent_parser.parser_mode parser._error_handler = self.parent_parser._error_handler parser.exit_on_error = self.parent_parser.exit_on_error parser.formatter_class = self.parent_parser.formatter_class parser.logger = self.parent_parser.logger parser.subcommand = name # type: ignore[attr-defined] # create a pseudo-action to hold the choice help aliases = kwargs.pop("aliases", ()) help_arg = None if "help" in kwargs: help_arg = kwargs.pop("help") choice_action = self._ChoicesPseudoAction(name, aliases, help_arg) self._choices_actions.append(choice_action) # add the parser to the name-parser map self._name_parser_map[name] = parser for alias in aliases: self._name_parser_map[alias] = parser return parser def __call__(self, parser, namespace, values, option_string=None): """Adds subcommand dest and parses subcommand arguments.""" subcommand = values[0] arg_strings = values[1:] # set the parser name namespace[self.dest] = subcommand # parse arguments if subcommand in self._name_parser_map: subparser = self._name_parser_map[subcommand] subnamespace = namespace.get(subcommand).clone() if subcommand in namespace else None kwargs = dict(_skip_validation=True, _namespace_as_config=True, **parse_kwargs.get()) namespace[subcommand] = subparser.parse_args(arg_strings, namespace=subnamespace, **kwargs) @contextmanager def parse_kwargs_context(kwargs): parse_kwargs.set(kwargs) yield def get_subcommands( parser: ArgumentParser, cfg: Namespace, prefix: str = "", fail_no_subcommand: bool = True, ) -> tuple[list[str] | None, list[ArgumentParser] | None]: """Returns subcommand names and corresponding subparsers.""" if parser._subcommands_action is None: return None, None action = parser._subcommands_action require_single = single_subcommand.get() and not parsing_defaults.get() # Get subcommand settings keys subcommand_keys = [k for k in action.choices if isinstance(cfg.get(prefix + k), Namespace)] # Get subcommand subcommand = None dest = prefix + action.dest if dest in cfg and cfg.get(dest) is not None: subcommand = cfg[dest] if parsing_defaults.get(): raise NSKeyError(f"A specific subcommand can't be provided in defaults, got '{subcommand}'") elif len(subcommand_keys) > 0 and (fail_no_subcommand or require_single): cfg[dest] = subcommand = subcommand_keys[0] if len(subcommand_keys) > 1: deprecated_implicit_subcommand(get_subcommands, subcommand_keys, subcommand, dest) # v5.0.0 replace deprecated_implicit_subcommand with raise ValueError # Remove extra subcommand settings if subcommand and len(subcommand_keys) > 1: for key in [k for k in subcommand_keys if k != subcommand]: del cfg[prefix + key] if subcommand: subcommand_keys = [subcommand] if fail_no_subcommand: if subcommand is None and not (fail_no_subcommand and action.required): return None, None if action.required and subcommand not in action._name_parser_map: # If subcommand is required and no subcommand is provided, # present the user with a friendly error message to remind them of # the available subcommands and to select one. available_subcommands = list(action._name_parser_map) if len(available_subcommands) <= 5: candidate_subcommands_str = "{" + ",".join(available_subcommands) + "}" else: candidate_subcommands_str = "{" + ",".join(available_subcommands[:5]) + ", ...}" raise NSKeyError(f'expected "{dest}" to be one of {candidate_subcommands_str}, but it was not provided.') return subcommand_keys, [action._name_parser_map.get(s) for s in subcommand_keys] # type: ignore[misc] def get_subcommand( parser: ArgumentParser, cfg: Namespace, prefix: str = "", fail_no_subcommand: bool = True, ) -> tuple[str | None, ArgumentParser | None]: """Returns a single subcommand name and corresponding subparser.""" subcommands, subparsers = get_subcommands( parser, cfg, prefix=prefix, fail_no_subcommand=fail_no_subcommand, ) return subcommands[0] if subcommands else None, subparsers[0] if subparsers else None def handle_subcommands( parser: ArgumentParser, cfg: Namespace, env: bool | None, defaults: bool, prefix: str = "", fail_no_subcommand: bool = True, ) -> None: """Takes care of parsing subcommand values.""" subcommands, subparsers = get_subcommands(parser, cfg, prefix=prefix, fail_no_subcommand=fail_no_subcommand) if not subcommands or not subparsers: return for subcommand, subparser in zip(subcommands, subparsers): # Merge environment variable values and default values subnamespace = None key = prefix + subcommand if env: subnamespace = subparser.parse_env(defaults=defaults, _skip_validation=True) elif defaults: subnamespace = subparser.get_defaults(skip_validation=True) # Update all subcommand settings if subnamespace is not None: cfg[key] = merge_config(subparser, cfg.get(key, Namespace()), subnamespace) # Handle inner subcommands if subparser._subparsers is not None: handle_subcommands(subparser, cfg, env, defaults, key + ".", fail_no_subcommand=fail_no_subcommand) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_type_checking.py0000644000175100017510000000106715230046455021777 0ustar00runnerrunnerfrom typing import TYPE_CHECKING __all__ = [ "ArgumentGroup", "ActionsContainer", "ArgumentParser", "docstring_parser", "ruamelCommentedMap", ] if TYPE_CHECKING: # pragma: no cover import docstring_parser from ruamel.yaml.comments import CommentedMap as ruamelCommentedMap from ._core import ActionsContainer, ArgumentGroup, ArgumentParser else: for name in __all__: globals()[name] = type(name, (), {}) if name == "docstring_parser": globals()[name].DocstringStyle = type("DocstringStyle", (), {}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_typehints.py0000644000175100017510000021406015230046455021211 0ustar00runnerrunner"""Action to support type hints.""" import inspect import os import re import sys from argparse import ArgumentError from collections import OrderedDict, abc, defaultdict, deque from contextlib import contextmanager, suppress from contextvars import ContextVar from copy import deepcopy from enum import Enum from functools import partial from importlib import import_module from types import FunctionType, MappingProxyType from typing import ( Any, Callable, Deque, Dict, ForwardRef, FrozenSet, Iterable, List, Literal, Mapping, MutableMapping, MutableSequence, MutableSet, NoReturn, Sequence, Set, Tuple, Type, TypedDict, TypeVar, Union, ) from ._actions import ( Action, ActionConfigFile, ActionFail, _ActionHelpClassPath, _ActionPrintConfig, _is_action_value_list, remove_actions, ) from ._common import ( get_parsing_setting, get_unaliased_type, is_generic_class, is_instance, is_subclass, is_subclasses_disabled, lenient_check, nested_links, parent_parser, parser_context, validating_defaults, ) from ._instantiation import get_class_instantiator from ._loaders_dumpers import ( basic_json_or_yaml_load, get_loader_exceptions, json_or_yaml_loader_exceptions, load_value, ) from ._namespace import Namespace, subclasses_disabled_meta_key from ._optionals import ( capture_typing_extension_shadows, get_alias_target, is_alias_type, is_annotated, is_annotated_validator, typing_extensions_import, validate_annotated, ) from ._paths import Path, PathError, change_to_path_dir from ._required import clear_required from ._subcommands import find_action, find_parent_action, parse_kwargs from ._type_checking import ArgumentParser from ._util import ( NestedArg, NoneType, get_import_path, get_typehint_origin, import_object, indent_text, iter_to_set_str, load_config_path_context, object_path_serializer, parse_value_or_config, warning, ) from .typing import _LazyInitBaseClass, get_registered_type, is_pydantic_type NotRequired = typing_extensions_import("NotRequired") Required = typing_extensions_import("Required") _TypedDictMeta = typing_extensions_import("_TypedDictMeta") Unpack = typing_extensions_import("Unpack") get_type_hints = typing_extensions_import("get_type_hints") def _capture_typing_extension_shadows(name: str, *collections) -> None: """ Ensure different origins for types in typing_extensions are captured. """ current_module = sys.modules[__name__] typehint = getattr(current_module, name) return capture_typing_extension_shadows(typehint, name, *collections) root_types = { str, int, float, bool, Any, Literal, Type, type, Union, List, list, FrozenSet, Deque, deque, Iterable, Sequence, MutableSequence, abc.Iterable, abc.Sequence, abc.MutableSequence, Tuple, tuple, Set, FrozenSet, set, frozenset, MutableSet, abc.MutableSet, Dict, dict, Mapping, MutableMapping, abc.Mapping, abc.MutableMapping, OrderedDict, Callable, abc.Callable, NotRequired, Required, Unpack, } leaf_types = { str, int, float, bool, NoneType, } leaf_or_root_types = leaf_types.union(root_types) tuple_set_origin_types = {Tuple, tuple, Set, set, frozenset, MutableSet, abc.Set, abc.MutableSet} sequence_origin_types = { List, list, Deque, deque, Iterable, Sequence, MutableSequence, abc.Iterable, abc.Sequence, abc.MutableSequence, } mapping_origin_types = { Dict, dict, Mapping, MappingProxyType, MutableMapping, abc.Mapping, abc.MutableMapping, OrderedDict, } sequence_or_mapping_origin_types = sequence_origin_types.union(mapping_origin_types) callable_origin_types = {Callable, abc.Callable} literal_types = {Literal} _capture_typing_extension_shadows("Literal", root_types, literal_types) not_required_types = {NotRequired} _capture_typing_extension_shadows("NotRequired", root_types, not_required_types) required_types = {Required} _capture_typing_extension_shadows("Required", root_types, required_types) not_required_required_types = not_required_types.union(required_types) typed_dict_types = {TypedDict} _capture_typing_extension_shadows("TypedDict", typed_dict_types) typed_dict_meta_types = {_TypedDictMeta} _capture_typing_extension_shadows("_TypedDictMeta", typed_dict_meta_types) unpack_types = {Unpack} _capture_typing_extension_shadows("Unpack", unpack_types) subclass_arg_parser: ContextVar = ContextVar("subclass_arg_parser") allow_default_instance: ContextVar = ContextVar("allow_default_instance", default=False) sub_defaults: ContextVar = ContextVar("sub_defaults", default=False) def get_parse_optional_num_return() -> int: parser = __import__("argparse").ArgumentParser() parser.add_argument("--test") arg_parsed = parser._parse_optional("--test=x") return len(arg_parsed) parse_optional_num_return = get_parse_optional_num_return() def freeze(value): if isinstance(value, dict): return tuple(sorted(((k, freeze(v)) for k, v in value.items()), key=lambda item: repr(item[0]))) if isinstance(value, set): return tuple(sorted((freeze(v) for v in value), key=repr)) if isinstance(value, (list, tuple)): return tuple(freeze(v) for v in value) return value _cached_class_parsers: dict[tuple, ArgumentParser] = {} def cached_get_class_parser(*, val_class, sub_add_kwargs, skip_args, parent_parser, nested_links): if isinstance(val_class, str): val_class = import_object(val_class) parser_class = type(parent_parser) cache_key = ( val_class, parser_class, parent_parser.parser_mode, freeze(sub_add_kwargs), freeze(skip_args), freeze(nested_links), get_parsing_setting("unset_sentinel"), ) if cache_key in _cached_class_parsers: parser = _cached_class_parsers[cache_key] parser.logger = parent_parser.logger return parser kwargs = dict(sub_add_kwargs) if sub_add_kwargs else {} if skip_args: kwargs.setdefault("skip", set()).update(skip_args) parser = parser_class(exit_on_error=False, logger=parent_parser.logger, parser_mode=parent_parser.parser_mode) remove_actions(parser, (ActionConfigFile, _ActionPrintConfig)) if inspect.isclass(val_class) or inspect.isclass(get_typehint_origin(val_class)): parser.add_class_arguments(val_class, **kwargs) else: kwargs = {k: v for k, v in kwargs.items() if k != "instantiate"} parser.add_function_arguments(val_class, **kwargs) if "linked_targets" in kwargs: for key in kwargs["linked_targets"]: clear_required(parser, key) for link_kwargs in nested_links: parser.link_arguments(**link_kwargs) parser._inner_parser = True _cached_class_parsers[cache_key] = parser return parser class ActionTypeHint(Action): """Action to parse a type hint.""" def __init__(self, typehint: type | None = None, enable_path: bool = False, **kwargs): """Initializer for ActionTypeHint instance. Args: typehint: The type hint to use for parsing. enable_path: Whether to try to load parsed value from path. Raises: ValueError: If a parameter is invalid. """ if typehint is not None: if not self.is_supported_typehint(typehint, full=True): raise ValueError(f"Unsupported type hint {typehint}.") if get_typehint_origin(typehint) == Union: assert hasattr(typehint, "__args__") subtype_supported = [ subtype is NoneType or self.is_supported_typehint(subtype, full=True) for subtype in typehint.__args__ ] if sum(subtype_supported) < len(subtype_supported): discard = {typehint.__args__[n] for n, s in enumerate(subtype_supported) if not s} kwargs["logger"].debug(f"Discarding unsupported subtypes {discard} from {typehint}") subtypes = tuple(t for t, s in zip(typehint.__args__, subtype_supported) if s) typehint = Union[subtypes] self._typehint = typehint self._enable_path = False if is_pathlike(typehint) else enable_path elif "_typehint" not in kwargs: raise ValueError("Expected typehint keyword argument.") else: self._typehint = kwargs.pop("_typehint") self._enable_path = kwargs.pop("_enable_path") self.sub_add_kwargs: dict = {} if "metavar" not in kwargs: kwargs["metavar"] = typehint_metavar(self._typehint) super().__init__(**kwargs) self._supports_append = self.supports_append(self._typehint) self.default = self.normalize_default(self.default) def normalize_default(self, default): from ._signatures import convert_to_dict, is_convertible_to_dict is_subclass_type = self.is_subclass_typehint(self._typehint, all_subtypes=False) if isinstance(default, _LazyInitBaseClass): default = default.lazy_get_init_data().as_dict() elif is_convertible_to_dict(default.__class__): default = convert_to_dict(default) elif is_subclass_type and isinstance(default, dict) and "class_path" in default: default = subclass_spec_as_namespace(default) default.class_path = normalize_import_path(default.class_path, self._typehint) elif is_enum_type(self._typehint) and isinstance(default, Enum): default = default.name elif is_callable_type(self._typehint) and callable(default) and not inspect.isclass(default): default = get_import_path(default) elif ActionTypeHint.is_return_subclass_typehint(self._typehint) and inspect.isclass(default): default = {"class_path": get_import_path(default)} elif is_subclass_type and not allow_default_instance.get(): from ._parameter_resolvers import UnknownDefault default_type = type(default) if not is_subclass(default_type, UnknownDefault) and self.is_subclass_typehint(default_type): raise ValueError("Subclass types require as default either a dict with class_path or a lazy instance.") return default @staticmethod def prepare_add_argument(args, kwargs, enable_path, container, logger, sub_add_kwargs=None): if kwargs.get("action") is not None: return args typehint = kwargs.pop("type") if args[0].startswith("--") and ActionTypeHint.supports_append(typehint): args = tuple(list(args) + [args[0] + "+"]) if get_registered_type(typehint) is None and _ActionHelpClassPath.get_help_types(typehint): help_option = f"--{args[0]}.help" if args[0][0] != "-" else f"{args[0]}.help" help_action = container.add_argument(help_option, action=_ActionHelpClassPath(typehint=typehint)) if sub_add_kwargs: help_action.sub_add_kwargs = sub_add_kwargs kwargs["action"] = ActionTypeHint(typehint=typehint, enable_path=enable_path, logger=logger) if kwargs.get("choices"): kwargs["type"] = lambda v: adapt_typehints(v, typehint) return args @staticmethod def is_supported_typehint(typehint, full=False): """Whether the given type hint is supported.""" if get_registered_type(typehint) is not None: return True typehint = get_unaliased_type(typehint) if is_subclass(typehint, Namespace): raise ValueError("jsonargparse.Namespace is only intended for parsing results and not supported as a type.") supported = ( typehint in root_types or get_typehint_origin(typehint) in root_types or get_registered_type(typehint) is not None or is_subclass(typehint, Enum) or is_subclasses_disabled(typehint) or ActionTypeHint.is_subclass_typehint(typehint) ) if full and supported: typehint_origin = get_typehint_origin(typehint) or typehint if typehint not in root_types and typehint_origin in root_types and typehint_origin not in literal_types: num_supported_args = 0 subtypes = getattr(typehint, "__args__", []) subtypes = [s for s in subtypes if s is not NoneType] for subtype in subtypes: if ( subtype == Ellipsis or (typehint_origin == type and isinstance(subtype, TypeVar)) or subtype in leaf_types or ActionTypeHint.is_supported_typehint(subtype, full=True) ): num_supported_args += 1 elif typehint_origin != Union: return False if typehint_origin == Union and subtypes and num_supported_args == 0: return False return supported @staticmethod def is_subclass_typehint(typehint, all_subtypes=True, also_lists=False): typehint = typehint_from_action(typehint) if typehint is None: return False typehint = get_unaliased_type(typehint) typehint_origin = get_typehint_origin(typehint) if typehint_origin == Union or (also_lists and typehint_origin in sequence_origin_types): subtypes = [a for a in typehint.__args__ if a != NoneType] test = all if all_subtypes else any k = {"also_lists": also_lists} return test(ActionTypeHint.is_subclass_typehint(s, **k) for s in subtypes) return is_single_subclass_type(typehint, typehint_origin) @staticmethod def is_return_subclass_typehint(typehint): typehint = get_unaliased_type(get_optional_arg(get_unaliased_type(typehint))) typehint_origin = get_typehint_origin(typehint) if typehint_origin in callable_origin_types or is_instance_factory_protocol(typehint): return_type = get_callable_return_type(typehint) if ActionTypeHint.is_subclass_typehint(return_type): return True return False @staticmethod def is_mapping_typehint(typehint): typehint = get_unaliased_type(typehint) typehint_origin = get_typehint_origin(typehint) or typehint if ( typehint in mapping_origin_types or typehint_origin in mapping_origin_types or is_optional(typehint, tuple(mapping_origin_types)) ): return True return False @staticmethod def is_callable_typehint(typehint): typehint = typehint_from_action(typehint) typehint_origin = get_typehint_origin(get_optional_arg(get_unaliased_type(typehint))) return typehint_origin in callable_origin_types def is_init_arg_mapping_typehint(self, key, cfg): result = False class_path = cfg.get(f"{self.dest}.class_path") if ( isinstance(class_path, str) and key.startswith(f"{self.dest}.init_args.") and self.is_subclass_typehint(self) ): sub_add_kwargs = dict(self.sub_add_kwargs) sub_add_kwargs.pop("linked_targets", None) parser = ActionTypeHint.get_class_parser(class_path, sub_add_kwargs=sub_add_kwargs) key = re.sub(f"^{self.dest}.init_args.", "", key) typehint = getattr(find_action(parser, key), "_typehint", None) result = self.is_mapping_typehint(typehint) return result @staticmethod def parse_argv_item(arg_string): parser = subclass_arg_parser.get() action = None sep = None if arg_string.startswith("--"): arg_base, explicit_arg = (arg_string, None) if "=" in arg_string: arg_base, sep, explicit_arg = arg_string.partition("=") if "." in arg_base and arg_base not in parser._option_string_actions: action = find_parent_action(parser, arg_base[2:]) typehint = typehint_from_action(action) if typehint or isinstance(action, ActionFail): if parse_optional_num_return == 4: return action, arg_base, sep, explicit_arg elif parse_optional_num_return == 1: return [(action, arg_base, sep, explicit_arg)] return action, arg_base, explicit_arg return None @staticmethod def discard_init_args_on_class_path_change(parser_or_action, prev_cfg, cfg): if isinstance(prev_cfg, dict): return keys = list(prev_cfg.keys(branches=True)) num = 0 while num < len(keys): key = keys[num] prev_val = prev_cfg.get(key) val = cfg.get(key) if is_subclass_spec(prev_val) and is_subclass_spec(val): action = parser_or_action if not isinstance(parser_or_action, ActionTypeHint): action = find_action(parser_or_action, key) if isinstance(action, ActionTypeHint): discard_init_args_on_class_path_change(action, prev_val, val) prev_sub_cfg = prev_val.get("init_args") if prev_sub_cfg: sub_add_kwargs = getattr(action, "sub_add_kwargs", {}) subparser = ActionTypeHint.get_class_parser(val["class_path"], sub_add_kwargs) sub_cfg = val.get("init_args", Namespace()) ActionTypeHint.discard_init_args_on_class_path_change(subparser, prev_sub_cfg, sub_cfg) keys = keys[: num + 1] + [k for k in keys[num + 1 :] if not k.startswith(key + ".")] num += 1 @staticmethod @contextmanager def subclass_arg_context(parser): subclass_arg_parser.set(parser) yield @staticmethod @contextmanager def allow_default_instance_context(): token = allow_default_instance.set(True) try: yield finally: allow_default_instance.reset(token) @staticmethod @contextmanager def sub_defaults_context(): t = sub_defaults.set(True) try: yield finally: sub_defaults.reset(t) @staticmethod def add_sub_defaults(parser, cfg): def skip_sub_defaults_apply(v): return not ( isinstance(v, (str, Namespace, dict)) or is_subclass_spec(v) or (isinstance(v, list) and any(is_subclass_spec(e) for e in v)) or (isinstance(v, dict) and any(is_subclass_spec(e) for e in v.values())) ) with ActionTypeHint.sub_defaults_context(): parser._apply_actions(cfg, skip_fn=skip_sub_defaults_apply, prev_cfg=cfg.clone()) @staticmethod def supports_append(action): typehint = typehint_from_action(action) typehint_origin = get_typehint_origin(typehint) return typehint and ( typehint_origin in sequence_origin_types or ( typehint_origin == Union and any(get_typehint_origin(x) in sequence_origin_types for x in typehint.__args__) ) ) def serialize(self, value, dump_kwargs=None): sub_add_kwargs = getattr(self, "sub_add_kwargs", {}) with dump_kwargs_context(dump_kwargs): if _is_action_value_list(self): return [ adapt_typehints( v, self._typehint, default=self.default, serialize=True, sub_add_kwargs=sub_add_kwargs, logger=self.logger, ) for v in value ] return adapt_typehints( value, self._typehint, default=self.default, serialize=True, sub_add_kwargs=sub_add_kwargs, logger=self.logger, ) def __call__(self, *args, **kwargs): """Parses an argument validating against the corresponding type hint. Raises: TypeError: If the argument is not valid. """ if len(args) == 0: kwargs["_typehint"] = self._typehint kwargs["_enable_path"] = self._enable_path if "nargs" in kwargs and kwargs["nargs"] == 0: raise ValueError("ActionTypeHint does not allow nargs=0.") return ActionTypeHint(**kwargs) parser, cfg, val, opt_str = args if not (self.nargs == "?" and val is None): if isinstance(opt_str, str) and opt_str.startswith(f"--{self.dest}."): if opt_str.startswith(f"--{self.dest}.init_args."): sub_opt = opt_str[len(f"--{self.dest}.init_args.") :] else: sub_opt = opt_str[len(f"--{self.dest}.") :] val = NestedArg(key=sub_opt, val=val) append = opt_str == f"--{self.dest}+" val = self._check_type_(val, append=append, cfg=cfg, mode=parser.parser_mode) if is_subclass_spec(val): prev_val = cfg.get(self.dest) if is_subclass_spec(prev_val) and "init_args" in prev_val: ActionTypeHint.discard_init_args_on_class_path_change( self, prev_val.init_args, val.get("init_args"), ) cfg.update(val, self.dest) return None def _check_type(self, value, append=False, cfg=None, mode=None): islist = _is_action_value_list(self) if not islist: value = [value] for num, val in enumerate(value): try: orig_val = val enable_path = self._enable_path and not isinstance(val, NestedArg) try: val, config_path = parse_value_or_config(val, enable_path=enable_path) except get_loader_exceptions(): config_path = None path_meta = val.pop("__path__", None) if isinstance(val, dict) else None unset_sentinel = get_parsing_setting("unset_sentinel") prev_val = cfg.get(self.dest) if cfg else unset_sentinel if prev_val is unset_sentinel and not sub_defaults.get() and is_subclass_spec(self.default): prev_val = Namespace(class_path=self.default["class_path"]) kwargs = { "sub_add_kwargs": getattr(self, "sub_add_kwargs", {}), "prev_val": prev_val, "orig_val": orig_val, "append": append, "enable_path": enable_path, "logger": self.logger, } try: with load_config_path_context(config_path), change_to_path_dir(config_path): val = adapt_typehints(val, self._typehint, **kwargs) except ValueError as ex: if orig_val == "-" and isinstance(getattr(ex, "parent", None), PathError): raise ex try: if isinstance(orig_val, str): with load_config_path_context(config_path), change_to_path_dir(config_path): val = adapt_typehints(orig_val, self._typehint, default=self.default, **kwargs) ex = None except ValueError: if ( lenient_check.get() and mode == "omegaconf+" and isinstance(orig_val, str) and "${" in orig_val ): ex = None elif self._enable_path and config_path is None and isinstance(orig_val, str): msg = f"\n- Expected a path but {orig_val} either not accessible or invalid\n- " raise type(ex)(msg + str(ex)) from ex if ex: raise ex if path_meta is not None: val["__path__"] = path_meta if isinstance(val, (Namespace, dict)) and config_path is not None: val["__path__"] = config_path value[num] = val except (TypeError, ValueError) as ex: if self._is_valid_string(val): value[num] = val else: elem = "" if not islist else f" element {num + 1}" error = indent_text(str(ex)) raise TypeError(f'Parser key "{self.dest}"{elem}:\n{error}') from ex return value if islist else value[0] def _is_valid_string(self, value): typehint = self._typehint return isinstance(value, str) and ( typehint is str or (get_typehint_origin(typehint) == Union and str in typehint.__args__) ) def instantiate_classes(self, value): islist = _is_action_value_list(self) if not islist: value = [value] sub_add_kwargs = getattr(self, "sub_add_kwargs", {}) for num, val in enumerate(value): value[num] = adapt_typehints( val, self._typehint, default=self.default, instantiate_classes=True, sub_add_kwargs=sub_add_kwargs, logger=self.logger, ) return value if islist else value[0] @staticmethod def get_class_parser(val_class, sub_add_kwargs=None, skip_args=None): return cached_get_class_parser( val_class=val_class, sub_add_kwargs=sub_add_kwargs, skip_args=skip_args, parent_parser=parent_parser.get(), nested_links=nested_links.get(), ) def extra_help(self): extra = "" typehint = get_optional_arg(self._typehint) typehint = get_callable_return_type(typehint) or typehint if get_typehint_origin(typehint) is type: typehint = typehint.__args__[0] if self.is_subclass_typehint(typehint, all_subtypes=False): class_paths = get_all_subclass_paths(typehint) if class_paths: extra = ", known subclasses: " + ", ".join(class_paths) return extra def completer(self, prefix, **kwargs): """Used by argcomplete, validates value and shows expected type.""" from ._completions import argcomplete_warn_redraw_prompt, get_files_completer if self.choices: return [str(c) for c in self.choices] elif self._typehint == bool: return ["true", "false"] elif is_optional(self._typehint, bool): return ["true", "false", "null"] elif is_subclass(self._typehint, Enum): enum = self._typehint return list(enum.__members__) elif is_optional(self._typehint, Enum): enum = get_optional_arg(self._typehint) return list(enum.__members__) + ["null"] elif is_optional(self._typehint, Path): files_completer = get_files_completer() return ["null"] + sorted(files_completer(prefix, **kwargs)) elif chr(int(os.environ["COMP_TYPE"])) == "?": try: if prefix.strip() == "": raise ValueError() self._check_type(prefix) msg = "value already valid, " except (TypeError, ValueError) + get_loader_exceptions(): msg = "value not yet valid, " msg += "expected type " + type_to_str(self._typehint) return argcomplete_warn_redraw_prompt(prefix, msg) def is_pathlike(typehint) -> bool: if get_typehint_origin(typehint) == Union: return any(is_pathlike(t) for t in typehint.__args__) return is_subclass(typehint, os.PathLike) def is_list_pathlike(typehint) -> bool: typehint_origin = get_typehint_origin(typehint) if typehint_origin in sequence_origin_types: subtype = typehint.__args__[0] return is_pathlike(subtype) return False 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 def raise_union_unexpected_value(subtypes, val: Any, exceptions: list[Exception]) -> NoReturn: str_exceptions = [indent_text(str(e), first_line=False) for e in exceptions] errors = indent_text("- " + "\n- ".join(str_exceptions)) errors = errors.replace(f". Got value: {val}", "").replace(f" {val} ", " ") raise ValueError( f"Does not validate against any of the Union subtypes\nSubtypes: {subtypes}" f"\nErrors:\n{errors}\nGiven value type: {type(val)}\nGiven value: {val}" ) from exceptions[0] def resolve_forward_ref(ref, global_vars=None): if not isinstance(ref, ForwardRef) or not ref.__forward_module__: return ref aliases = __builtins__.copy() aliases.update(vars(import_module(ref.__forward_module__))) if global_vars: aliases.update(global_vars) return aliases.get(ref.__forward_arg__, ref) def get_typed_dict_annotations(typed_dict, logger=None) -> dict: from ._postponed_annotations import get_global_vars # Includes the names from TYPE_CHECKING blocks, given as localns so that each base # keeps resolving with the globals of the module in which it was defined. global_vars = get_global_vars(typed_dict, logger) try: # Resolves forward references (e.g. from "from __future__ import annotations") and # gathers inherited keys, while include_extras keeps the Required/NotRequired wrappers. return get_type_hints(typed_dict, None, global_vars, include_extras=True) except Exception as ex: if logger: logger.debug(f"Failed to resolve the annotations of {typed_dict}", exc_info=ex) # A single key failing (e.g. a missing import or a typo) makes the resolution of the # entire TypedDict fail. Thus, resolve one by one to keep the keys that do work. return {k: resolve_forward_ref(v, global_vars) for k, v in typed_dict.__annotations__.items()} def get_typed_dict_required_keys(typed_dict, annotations: dict) -> set: # The totality of each class (including inheritance) is reflected in __required_keys__, # even when the annotations are postponed. Required and NotRequired instead may not be # reflected there (e.g. below Python 3.11 or with postponed annotations), so they are # adjusted based on the resolved annotations. required_keys = set(getattr(typed_dict, "__required_keys__", set(annotations))) required_keys.update({k for k, v in annotations.items() if get_typehint_origin(v) in required_types}) required_keys.difference_update({k for k, v in annotations.items() if get_typehint_origin(v) in not_required_types}) return required_keys def adapt_typehints( val, typehint, serialize=False, instantiate_classes=False, prev_val=None, orig_val=None, append=False, enable_path=False, sub_add_kwargs=None, default=None, logger=None, ): if type(val) in {str, bool, int, float} and val == default: return val adapt_kwargs = { "serialize": serialize, "instantiate_classes": instantiate_classes, "prev_val": prev_val, "orig_val": orig_val, "append": append, "enable_path": enable_path, "sub_add_kwargs": sub_add_kwargs or {}, "logger": logger, } subtypehints = getattr(typehint, "__args__", None) typehint_origin = get_typehint_origin(typehint) or typehint unset_sentinel = get_parsing_setting("unset_sentinel") # Any if typehint == Any: type_val = type(val) if get_registered_type(type_val) or is_subclass(type_val, Enum): val = adapt_typehints(val, type_val, **adapt_kwargs) elif isinstance(val, str): with suppress(*get_loader_exceptions()): val, _ = parse_value_or_config(val, enable_path=False, simple_types=True) val = adapt_classes_any(val, serialize, instantiate_classes, sub_add_kwargs) # Literal elif typehint_origin in literal_types: if val not in subtypehints and isinstance(val, str): subtypes = tuple(type(v) for v in subtypehints if type(v) is not str) if subtypes: val = adapt_typehints(val, Union[subtypes], **adapt_kwargs) if val not in subtypehints: raise_unexpected_value(f"Expected a {typehint}", val) # Basic types elif typehint in leaf_types: if isinstance(val, str) and typehint is not str: with suppress(*json_or_yaml_loader_exceptions): val = basic_json_or_yaml_load(val) if typehint is float and isinstance(val, int) and not isinstance(val, bool): val = float(val) if not isinstance(val, typehint) or (typehint in (int, float) and isinstance(val, bool)): raise_unexpected_value(f"Expected a {typehint}", val) # Annotated elif is_annotated(typehint): if not serialize and is_annotated_validator(typehint): try: val = validate_annotated(val, typehint) except Exception as ex: raise_unexpected_value(str(ex), val, ex) else: val = adapt_typehints(val, typehint_origin, **adapt_kwargs) # Registered types elif get_registered_type(typehint): registered_type = get_registered_type(typehint) if serialize: val = registered_type.serializer(val) elif not serialize and not registered_type.is_value_of_type(val): val = registered_type.deserializer(val) # Enum elif is_subclass(typehint, Enum): if serialize: if isinstance(val, typehint): val = val.name elif not isinstance(val, typehint): try: val = typehint[val] except KeyError as ex: raise_unexpected_value( f"Expected a member of {typehint}: {iter_to_set_str(typehint.__members__)}", val, ex ) # Type elif typehint in {Type, type} or typehint_origin in {Type, type}: if serialize: val = object_path_serializer(val) elif not serialize and not isinstance(val, type): path = val val = import_object(val) if (typehint in {Type, type} and not isinstance(val, type)) or ( typehint not in {Type, type} and not is_subclass(val, subtypehints[0]) ): raise_unexpected_value(f"Expected an import path corresponding to a {typehint}", path) # Union elif typehint_origin == Union: vals = [] sorted_subtypes = sort_subtypes_for_union(subtypehints, val, prev_val, append) for subtype in sorted_subtypes: try: vals.append(adapt_typehints(val, subtype, **adapt_kwargs)) break except Exception as ex: if subtype is str and not isinstance(val, str) and isinstance(orig_val, str): vals.append(orig_val) continue vals.append(ex) if all(isinstance(v, Exception) for v in vals): raise_union_unexpected_value(sorted_subtypes, val, vals) val = next((v for v in reversed(vals) if not isinstance(v, Exception))) # Tuple or Set elif typehint_origin in tuple_set_origin_types: if not isinstance(val, (list, tuple, set, frozenset)): raise_unexpected_value(f"Expected a {typehint_origin}", val) val = list(val) if subtypehints is not None: is_tuple = typehint_origin in {Tuple, tuple} is_ellipsis = is_ellipsis_tuple(typehint) if is_tuple and not is_ellipsis and len(val) != len(subtypehints): raise_unexpected_value(f"Expected a tuple with {len(subtypehints)} elements", val) for n, v in enumerate(val): subtypehint = subtypehints[0 if is_ellipsis or not is_tuple else n] val[n] = adapt_typehints(v, subtypehint, **adapt_kwargs) if not serialize: if typehint_origin in {Tuple, tuple}: val = tuple(val) elif typehint_origin is frozenset: val = frozenset(val) else: val = set(val) # List, Iterable or Sequence elif typehint_origin in sequence_origin_types: if append: adapt_kwargs.pop("prev_val") if prev_val is unset_sentinel: prev_val = [] elif not isinstance(prev_val, list): try: prev_val = [adapt_typehints(prev_val, subtypehints[0], **adapt_kwargs)] except Exception: prev_val = [] val_is_list = isinstance(val, list) val = prev_val + (val if val_is_list else [val]) prev_val = prev_val + [None] * (len(val) - len(prev_val) if val_is_list else 1) list_path = None if enable_path and type(val) is str: if validating_defaults.get(): return val with suppress(TypeError): from ._optionals import _get_config_read_mode list_path = Path(val, mode=_get_config_read_mode()) val = list_path.read_text().splitlines() if isinstance(val, NestedArg) and subtypehints is not None: val = (prev_val[:-1] if isinstance(prev_val, list) else []) + [val] elif isinstance(val, Iterable) and not isinstance(val, (list, str)) and type(val) not in mapping_origin_types: val = list(val) elif not isinstance(val, list): raise_unexpected_value(f"Expected a {typehint_origin}", val) if subtypehints is not None: for n, v in enumerate(val): if isinstance(prev_val, list) and len(prev_val) == len(val): adapt_kwargs_n = {**deepcopy(adapt_kwargs), "prev_val": prev_val[n]} else: adapt_kwargs_n = deepcopy(adapt_kwargs) with change_to_path_dir(list_path): val[n] = adapt_typehints(v, subtypehints[0], **adapt_kwargs_n) if typehint_origin is deque: val = list(val) if serialize else deque(val) # Dict, Mapping elif typehint_origin in mapping_origin_types: if isinstance(val, NestedArg): if isinstance(prev_val, dict): if isinstance(val.key, str) and "." in val.key: key_prefix, key_suffix = val.key.split(".", 1) val = {**prev_val, key_prefix: {key_suffix: val.val}} else: val = {**prev_val, val.key: val.val} else: val = {val.key: val.val} elif isinstance(val, MappingProxyType): val = dict(val) elif not isinstance(val, dict): raise_unexpected_value(f"Expected a {typehint_origin}", val) if subtypehints is not None: if subtypehints[0] == int: cast = str if serialize else int val = {cast(k): v for k, v in val.items()} for k, v in val.items(): if "linked_targets" in adapt_kwargs["sub_add_kwargs"]: kwargs = deepcopy(adapt_kwargs) sub_add_kwargs = kwargs["sub_add_kwargs"] sub_add_kwargs["linked_targets"] = { t[len(k + ".") :] for t in sub_add_kwargs["linked_targets"] if t.startswith(k + ".") } sub_add_kwargs["linked_targets"] = { t[len("init_args.") :] if t.startswith("init_args.") else t for t in sub_add_kwargs["linked_targets"] } else: kwargs = adapt_kwargs.copy() if kwargs.get("prev_val"): if isinstance(kwargs["prev_val"], dict): kwargs["prev_val"] = kwargs["prev_val"].get(k) else: kwargs["prev_val"] = None val[k] = adapt_typehints(v, subtypehints[1], **kwargs) if type(typehint) in typed_dict_meta_types: dict_annotations = get_typed_dict_annotations(typehint, logger) required_keys = get_typed_dict_required_keys(typehint, dict_annotations) missing_keys = required_keys - val.keys() if missing_keys: raise_unexpected_value(f"Missing required keys: {missing_keys}", val) extra_keys = val.keys() - dict_annotations.keys() if extra_keys: raise_unexpected_value(f"Unexpected keys: {extra_keys}", val) for k, v in val.items(): val[k] = adapt_typehints(v, dict_annotations[k], **adapt_kwargs) if typehint_origin is MappingProxyType and not serialize: val = MappingProxyType(val) elif typehint_origin is OrderedDict: val = dict(val) if serialize else OrderedDict(val) # TypedDict NotRequired and Required elif typehint_origin in not_required_required_types: assert len(subtypehints) == 1, "(Not)Required requires a single type argument" val = adapt_typehints(val, subtypehints[0], **adapt_kwargs) # Callable elif ( typehint_origin in callable_origin_types or typehint in callable_origin_types or is_instance_factory_protocol(typehint, logger) ): if serialize: if is_subclass_spec(val): val, partial_skip_args = adapt_partial_callable_class(typehint, val) val = adapt_class_type(val, True, False, sub_add_kwargs, partial_skip_args=partial_skip_args) else: val = object_path_serializer(val) else: try: val_input = val if isinstance(val, str): class_path = val return_type = get_callable_return_type(typehint) if "." not in val and return_type: class_path = resolve_class_path_by_name(return_type, val) val_obj = import_object(class_path) if inspect.isclass(val_obj): val = Namespace(class_path=class_path) elif callable(val_obj): val = val_obj else: raise ImportError(f"Unexpected import object {val_obj}") if isinstance(val, (dict, Namespace, NestedArg)): if prev_val is unset_sentinel: return_type = get_callable_return_type(typehint) if return_type and not inspect.isabstract(return_type): with suppress(ValueError): prev_val = Namespace(class_path=get_import_path(return_type)) val = subclass_spec_as_namespace(val, prev_val) if not is_subclass_spec(val): raise ImportError( f"Dict must include a class_path and optionally init_args, but got {val_input}" ) val, partial_skip_args = adapt_partial_callable_class(typehint, val) val_class = import_object(val["class_path"]) if inspect.isclass(val_class) and not (partial_skip_args or callable_instances(val_class)): base_type = get_callable_return_type(typehint) or typehint raise ImportError( f"Expected '{val['class_path']}' to be a class that instantiates into callable " f"or a subclass of {base_type}." ) val["class_path"] = get_import_path(val_class) val = adapt_class_type( val, False, instantiate_classes, sub_add_kwargs, partial_skip_args=partial_skip_args, prev_val=prev_val, ) except (ImportError, AttributeError, ArgumentError) as ex: raise_unexpected_value(f"Type {typehint} expects a function or a callable class: {ex}", val, ex) # Subclass elif inspect.isclass(typehint_origin): if is_instance_or_supports_protocol(val, typehint): if serialize: val = serialize_class_instance(val) return val if serialize and isinstance(val, str): return val prev_implicit_defaults = False if prev_val is unset_sentinel and not inspect.isabstract(typehint) and not is_protocol(typehint): with suppress(ValueError): # implicit prev_val class_path prev_val = Namespace(class_path=get_import_path(typehint)) if parse_kwargs.get().get("defaults") is True: prev_implicit_defaults = True if isinstance(prev_val, (dict, Namespace)) and "class_path" not in prev_val: # implicit prev_val init_args prev_val = Namespace(class_path=None, init_args=Namespace(prev_val)) val_input = val if (isinstance(prev_val, (dict, Namespace)) and prev_val["class_path"] is None) or ( isinstance(val, NestedArg) and is_subclasses_disabled(typehint) ): class_type_path = Namespace(class_path=get_import_path(typehint)) val = subclass_spec_as_namespace(val, class_type_path) else: val = subclass_spec_as_namespace(val, prev_val) if val and not is_subclass_spec(val) and "init_args" not in val: # implicit val class_path val = Namespace(class_path=get_import_path(typehint), init_args=val) if not is_subclass_spec(val): msg = "Does not implement protocol" if is_protocol(typehint) else "Not a valid subclass of" raise_unexpected_value( f"{msg} {typehint.__name__}. Got value: {val_input}\n" "Subclass types expect one of:\n" "- a class path (str)\n" "- a dict with class_path entry\n" "- a dict without class_path but with init_args entry (class path given previously)\n" "- a dict with parameters accepted by the base class (implicit class_path)" ) try: class_path = resolve_class_path_by_name(typehint, val["class_path"]) val_class = import_object(class_path) if is_instance_or_supports_protocol(val_class, typehint): return val_class # importable instance if is_protocol(val_class): raise_unexpected_value(f"Expected an instantiatable class, but {val['class_path']} is a protocol") subclass = True if not is_subclass_or_implements_protocol(val_class, typehint): subclass = False if not inspect.isclass(val_class) and callable(val_class): from ._postponed_annotations import get_return_type return_type = get_return_type(val_class, logger) if is_subclass_or_implements_protocol(return_type, typehint): subclass = True elif prev_implicit_defaults: inner_parser = ActionTypeHint.get_class_parser(typehint, sub_add_kwargs) prev_val.init_args = inner_parser.get_defaults() if prev_val.class_path != class_path: inner_parser = ActionTypeHint.get_class_parser(val_class, sub_add_kwargs) for key in inner_parser.get_defaults().keys(): prev_val.init_args.pop(key, None) if not subclass: msg = "implement protocol" if is_protocol(typehint) else "correspond to a subclass of" raise_unexpected_value(f"Import path {val['class_path']} does not {msg} {typehint.__name__}") val["class_path"] = class_path val = adapt_class_type( val, serialize, instantiate_classes, sub_add_kwargs, prev_val=prev_val, typehint=typehint, ) except (ImportError, AttributeError, AssertionError, ArgumentError) as ex: class_path = val if isinstance(val, str) else val["class_path"] error = indent_text(str(ex)) raise_unexpected_value(f"Problem with given class_path {class_path!r}:\n{error}", exception=ex) # TypeAliasType -- 3.12 `type x = y` or manually via typing_extensions elif is_alias_type(typehint): return adapt_typehints(val, get_alias_target(typehint), **adapt_kwargs) else: raise RuntimeError(f"The code should never reach here: typehint={typehint}") # pragma: no cover return val protocol_irrelevant_dunder_methods = { "__init__", "__new__", "__del__", "__getattr__", "__getattribute__", "__setattr__", "__delattr__", "__reduce__", "__reduce_ex__", "__getstate__", "__setstate__", "__subclasshook__", } def implements_protocol(value, protocol) -> bool: from jsonargparse._parameter_resolvers import get_signature_parameters from jsonargparse._postponed_annotations import get_return_type if not inspect.isclass(value) or value is object or not is_protocol(protocol): return False members = 0 for name, _ in inspect.getmembers(protocol, predicate=inspect.isfunction): is_dunder = name.startswith("__") and name.endswith("__") if (not is_dunder and name.startswith("_")) or (is_dunder and name in protocol_irrelevant_dunder_methods): continue if not hasattr(value, name): return False members += 1 try: value_params = get_signature_parameters(value, name) except ValueError: return False proto_params = get_signature_parameters(protocol, name) if [(p.name, p.annotation) for p in proto_params] != [(p.name, p.annotation) for p in value_params]: return False proto_return = get_return_type(inspect.getattr_static(protocol, name)) value_return = get_return_type(inspect.getattr_static(value, name)) if proto_return != value_return: return False return True if members else False def is_protocol(class_type) -> bool: return getattr(class_type, "_is_protocol", False) def is_subclass_or_implements_protocol(value, class_type) -> bool: if is_protocol(class_type): return implements_protocol(value, class_type) return is_subclass(value, class_type) def is_instance_or_supports_protocol(value, class_type): if is_protocol(class_type): return is_subclass_or_implements_protocol(value.__class__, class_type) return is_instance(value, class_type) def is_instance_factory_protocol(class_type, logger=None): if not is_protocol(class_type) or not callable_instances(class_type): return False from ._postponed_annotations import get_return_type return_type = get_return_type(class_type.__call__, logger) return ActionTypeHint.is_subclass_typehint(return_type) _subclass_spec_keys = {"class_path", "init_args", "dict_kwargs", "__path__", subclasses_disabled_meta_key} def is_subclass_spec(val): is_class = isinstance(val, (dict, Namespace)) and "class_path" in val if is_class: keys = getattr(val, "__dict__", val).keys() is_class = len(set(keys) - _subclass_spec_keys) == 0 return is_class def subclass_spec_as_namespace(val, prev_val=None): if not isinstance(val, (str, dict, Namespace, NestedArg)): return None if isinstance(val, str): return Namespace(class_path=val) if isinstance(val, NestedArg): key, val = val if "." not in key: root_key = key else: if key.startswith("dict_kwargs."): root_key = "dict_kwargs" key = key[len("dict_kwargs.") :] val = {key: val} else: root_key = "init_args" val = NestedArg(key=key, val=val) val = Namespace({root_key: val}) if isinstance(prev_val, str): prev_val = Namespace(class_path=prev_val) if isinstance(val, dict): val = Namespace(val) if "init_args" in val and isinstance(val["init_args"], dict): val["init_args"] = Namespace(val["init_args"]) if not is_subclass_spec(val) and isinstance(prev_val, (Namespace, dict)) and "class_path" in prev_val: if "init_args" in val or "dict_kwargs" in val: val["class_path"] = prev_val["class_path"] else: val = Namespace(class_path=prev_val["class_path"], init_args=val) return val def get_callable_return_type(typehint): return_type = None if is_instance_factory_protocol(typehint): from ._postponed_annotations import get_return_type return_type = get_return_type(typehint.__call__) elif get_typehint_origin(typehint) in callable_origin_types: args = getattr(typehint, "__args__", None) if isinstance(args, tuple) and len(args) > 0: return_type = args[-1] return return_type def is_single_class_type(typehint, typehint_origin, closed_class): if not ( ( (inspect.isclass(typehint) and typehint_origin is None) or (is_generic_class(typehint) and inspect.isclass(typehint.__origin__)) ) and typehint not in leaf_or_root_types and not get_registered_type(typehint) and not is_pydantic_type(typehint) and not is_subclass(typehint, (Path, Enum)) and getattr(typehint_origin, "__module__", "") != "builtins" ): return False if not closed_class: return not is_subclasses_disabled(typehint) return True is_single_subclass_type = partial(is_single_class_type, closed_class=False) is_single_subclass_or_closed_type = partial(is_single_class_type, closed_class=True) def yield_class_types(typehint, is_single, also_lists=False, callable_return=False): typehint = typehint_from_action(typehint) if typehint is None: return typehint = get_unaliased_type(get_optional_arg(get_unaliased_type(typehint))) typehint_origin = get_typehint_origin(typehint) kwargs = {"is_single": is_single, "also_lists": also_lists, "callable_return": callable_return} if callable_return and (typehint_origin in callable_origin_types or is_instance_factory_protocol(typehint)): return_type = get_callable_return_type(typehint) if return_type: yield from yield_class_types(return_type, **kwargs) elif typehint_origin == Union or (also_lists and typehint_origin in sequence_origin_types): for subtype in typehint.__args__: yield from yield_class_types(subtype, **kwargs) if is_single(typehint, typehint_origin): yield typehint def get_subclass_types(typehint, also_lists=False, callable_return=False): types = tuple( yield_class_types( typehint, is_single=is_single_subclass_type, also_lists=also_lists, callable_return=callable_return ) ) return types or None def get_subclass_or_closed_types(typehint, also_lists=False, callable_return=False): types = tuple( yield_class_types( typehint, is_single=is_single_subclass_or_closed_type, also_lists=also_lists, callable_return=callable_return, ) ) return types or None def get_subclass_names(typehint, callable_return=False): return tuple( t.__name__ for t in yield_class_types(typehint, is_single=is_single_subclass_type, callable_return=callable_return) ) def adapt_partial_callable_class(callable_type, subclass_spec): partial_skip_args = None return_type = get_callable_return_type(callable_type) if return_type: subclass_types = get_subclass_types(return_type) class_type = import_object(resolve_class_path_by_name(return_type, subclass_spec.class_path)) if subclass_types and is_subclass(class_type, subclass_types): subclass_spec = subclass_spec.clone() subclass_spec["class_path"] = get_import_path(class_type) if is_protocol(callable_type): from ._parameter_resolvers import get_signature_parameters params = get_signature_parameters(callable_type, "__call__") partial_skip_args = set() positionals = [p for p in params if "POSITIONAL_ONLY" in str(p.kind)] if positionals: partial_skip_args.add(len(positionals)) partial_skip_args.update(p.name for p in params if "POSITIONAL_ONLY" not in str(p.kind)) else: partial_skip_args = {len(callable_type.__args__) - 1} return subclass_spec, partial_skip_args def get_all_subclass_paths(cls: type) -> list[str]: subclass_list = [] def is_local(cl): return ".." in getattr(cl, "__qualname__", "..") def is_private(class_path): return "._" in class_path def add_subclasses(cl): if hasattr(cl, "__args__") and get_typehint_origin(cl) in sequence_origin_types.union({Union}): for arg in cl.__args__: add_subclasses(arg) return try: class_path = get_import_path(cl) except (ImportError, AttributeError) as err: # Attribute is added in case of dot notation imports warning(f"Hit failing import with following error: {err}") return if is_local(cl) or is_subclass(cl, _LazyInitBaseClass): return if not (inspect.isabstract(cl) or is_private(class_path) or is_protocol(cl)): if class_path in subclass_list: return subclass_list.append(class_path) for subclass in cl.__subclasses__() if hasattr(cl, "__subclasses__") else []: add_subclasses(subclass) if get_typehint_origin(cls) in callable_origin_types: cls = cls.__args__[-1] # type: ignore[attr-defined] if get_typehint_origin(cls) in {Union, Type, type}: for arg in cls.__args__: # type: ignore[union-attr] if ActionTypeHint.is_subclass_typehint(arg, also_lists=True) and arg not in {object, type}: add_subclasses(arg) else: add_subclasses(cls) return subclass_list 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): for cls_n in cls: class_path = resolve_class_path_by_name(cls_n, name) if "." in class_path: break return class_path subclass_dict = defaultdict(list) for subclass in get_all_subclass_paths(cls): subclass_name = subclass.rsplit(".", 1)[1] subclass_dict[subclass_name].append(subclass) if name in subclass_dict: name_subclasses = subclass_dict[name] if len(name_subclasses) > 1: raise ValueError( f"Multiple subclasses with name {name}. Give the full class path to " f"avoid ambiguity: {', '.join(name_subclasses)}." ) class_path = name_subclasses[0] return class_path def normalize_import_path(class_path, typehint): if "." not in class_path: class_path = resolve_class_path_by_name(typehint, class_path) return get_import_path(import_object(class_path)) dump_kwargs: ContextVar = ContextVar("dump_kwargs", default={}) @contextmanager def dump_kwargs_context(kwargs): dump_kwargs.set(kwargs if kwargs else {}) yield def discard_init_args_on_class_path_change(parser_or_action, prev_val, value): if prev_val and "init_args" in prev_val and prev_val["class_path"] != value["class_path"]: parser = parser_or_action if isinstance(parser_or_action, ActionTypeHint): sub_add_kwargs = getattr(parser_or_action, "sub_add_kwargs", {}) parser = ActionTypeHint.get_class_parser(value["class_path"], sub_add_kwargs) del_args = {} prev_val = subclass_spec_as_namespace(prev_val) for key, val in list(prev_val.init_args.items(branches=True, nested=False)): action = find_action(parser, key) if action: with parser_context(lenient_check=False, load_value_mode=parser.parser_mode): try: parser._check_value_key(action, val, key, Namespace()) except Exception: action = None if not action: del_args[key] = prev_val.init_args.pop(key) if del_args: parser_or_action.logger.debug( f"Due to class_path change from {prev_val['class_path']!r} to {value['class_path']!r}, " f"discarding init_args: {del_args}." ) def adapt_class_type( value, serialize, instantiate_classes, sub_add_kwargs, prev_val=None, partial_skip_args=None, typehint=None, ): prev_val = subclass_spec_as_namespace(prev_val) value = subclass_spec_as_namespace(value) if is_generic_class(typehint): val_class = typehint else: val_class = import_object(value.class_path) parser = ActionTypeHint.get_class_parser(val_class, sub_add_kwargs, skip_args=partial_skip_args) # No need to re-create the linked arg but just "inform" the corresponding parser actions that it exists upstream. for target in sub_add_kwargs.get("linked_targets", []): split_index = target.find(".") if split_index != -1: split = ".init_args." if target[split_index:].startswith(".init_args.") else "." parent_key, key = target.split(split, maxsplit=1) try: action = next(a for a in parser._actions if a.dest == parent_key) except StopIteration: continue sub_add_kwargs = getattr(action, "sub_add_kwargs") sub_add_kwargs.setdefault("linked_targets", set()) sub_add_kwargs["linked_targets"].add(key) discard_init_args_on_class_path_change(parser, prev_val, value) dict_kwargs = value.pop("dict_kwargs", {}) init_args = value.get("init_args", Namespace()) if instantiate_classes: init_args = parser.instantiate(init_args) if not sub_add_kwargs.get("instantiate", True): if init_args: value["init_args"] = init_args return value instantiator_fn = get_class_instantiator() if partial_skip_args: return partial( instantiator_fn, val_class, **{**init_args, **dict_kwargs}, ) return instantiator_fn(val_class, **{**init_args, **dict_kwargs}) prev_init_args = prev_val.get("init_args") if isinstance(prev_val, Namespace) else None if isinstance(init_args, NestedArg): value["init_args"] = parser.parse_args( [f"--{init_args.key}={init_args.val}"], namespace=prev_init_args, defaults=sub_defaults.get(), ) return _subclasses_disabled_mark(value, typehint) if serialize: if init_args: value["init_args"] = load_value(parser.dump(init_args, **dump_kwargs.get())) else: if isinstance(dict_kwargs, dict): for key in list(dict_kwargs): if find_action(parser, key): init_args[key] = dict_kwargs.pop(key) elif dict_kwargs: init_args["dict_kwargs"] = dict_kwargs dict_kwargs = None init_args = parser.parse_object(init_args, namespace=prev_init_args, defaults=sub_defaults.get()) if init_args: value["init_args"] = init_args if dict_kwargs: if prev_val and prev_val.get("class_path") == value["class_path"] and prev_val.get("dict_kwargs"): dict_kwargs = {**prev_val.get("dict_kwargs"), **dict_kwargs} value["dict_kwargs"] = {} for key, val in dict_kwargs.items(): if isinstance(val, str): with suppress(get_loader_exceptions()): val = load_value(val, simple_types=True) value["dict_kwargs"][key] = val return _subclasses_disabled_mark(value, typehint) def _subclasses_disabled_mark(value, typehint): if is_subclasses_disabled(typehint) and value.class_path == get_import_path(typehint): value[subclasses_disabled_meta_key] = True return value def subclasses_disabled_remove_class_path(value): if not isinstance(value, (Namespace, dict)): return value items = vars(value).items() if isinstance(value, Namespace) else value.items() for key, val in items: if isinstance(val, (Namespace, dict)): value[key] = subclasses_disabled_remove_class_path(val) elif isinstance(val, list): value[key] = [subclasses_disabled_remove_class_path(item) for item in val] elif isinstance(val, tuple): value[key] = tuple(subclasses_disabled_remove_class_path(item) for item in val) if value.pop(subclasses_disabled_meta_key, False): return Namespace({**value.get("init_args", {}), **value.get("dict_kwargs", {})}) return value def adapt_classes_any(val, serialize, instantiate_classes, sub_add_kwargs): if is_subclass_spec(val): orig_val = val val = subclass_spec_as_namespace(val) init_args = val.get("init_args") if init_args and not instantiate_classes: for subkey, subval in init_args.items(branches=True, nested=False): init_args[subkey] = adapt_classes_any(subval, serialize, instantiate_classes, sub_add_kwargs) val["init_args"] = init_args try: val = adapt_class_type(val, serialize, instantiate_classes, sub_add_kwargs) except Exception: return orig_val elif isinstance(val, list): for num, subval in enumerate(val): val[num] = adapt_classes_any(subval, serialize, instantiate_classes, sub_add_kwargs) elif isinstance(val, dict): for key, subval in val.items(): val[key] = adapt_classes_any(subval, serialize, instantiate_classes, sub_add_kwargs) return val def sort_subtypes_for_union(subtypes, val, prev_val, append): if len(subtypes) > 1: if isinstance(val, str): key_fn = lambda x: ( x != NoneType, get_typehint_origin(x) not in sequence_or_mapping_origin_types, ) else: key_fn = lambda x: x != NoneType subtypes = sorted(subtypes, key=key_fn) if append or (isinstance(prev_val, list) and isinstance(val, NestedArg)): key_fn = lambda x: ( x != NoneType, get_typehint_origin(x) not in sequence_origin_types, ) subtypes = sorted(subtypes, key=key_fn) return subtypes def is_ellipsis_tuple(typehint): return typehint.__origin__ in {Tuple, tuple} and len(typehint.__args__) > 1 and typehint.__args__[1] == Ellipsis def is_optional(annotation, ref_type=None): """Checks whether a type annotation is an optional for one type class.""" return ( get_typehint_origin(annotation) == Union and len(annotation.__args__) == 2 and any(NoneType == a for a in annotation.__args__) and (ref_type is None or all(is_subclass(a, ref_type) for a in annotation.__args__ if a != NoneType)) ) def get_optional_arg(annotation, ref_type=None): if is_optional(annotation, ref_type): annotation = next(a for a in annotation.__args__ if a != NoneType) return annotation def is_enum_type(annotation): return is_subclass(annotation, Enum) or ( get_typehint_origin(annotation) == Union and any(is_subclass(a, Enum) for a in annotation.__args__) ) def is_callable_type(annotation): def is_callable(a): return (get_typehint_origin(a) or a) in callable_origin_types or a in callable_origin_types return is_callable(annotation) or ( get_typehint_origin(annotation) == Union and any(is_callable(a) for a in annotation.__args__) ) def typehint_from_action(action_or_typehint): if isinstance(action_or_typehint, Action): action_or_typehint = getattr(action_or_typehint, "_typehint", None) return action_or_typehint def type_to_str(obj): if obj in {bool, tuple} or is_subclass(obj, (int, float, str, Path, Enum)): return obj.__name__ return re.sub(r"[A-Za-z0-9_<>.]+\.", "", str(obj)).replace("NoneType", "null") def literal_to_str(val): return "null" if val is None else str(val) def typehint_metavar(typehint): """Generates a metavar for some types.""" metavar = None typehint_origin = get_typehint_origin(typehint) or typehint if typehint == bool: metavar = "{true,false}" elif is_optional(typehint, bool): metavar = "{true,false,null}" elif typehint_origin in literal_types: args = typehint.__args__ metavar = iter_to_set_str(literal_to_str(a) for a in args) elif is_subclass(typehint, Enum): enum = typehint metavar = iter_to_set_str(enum.__members__) elif is_optional(typehint, Enum): enum = typehint.__args__[0] metavar = iter_to_set_str(list(enum.__members__) + ["null"]) elif is_list_pathlike(typehint): metavar = "'[\"PATH1\",...]' | LIST_OF_PATHS_FILE | -" elif typehint_origin in tuple_set_origin_types or typehint_origin in sequence_origin_types: metavar = "[ITEM,...]" return metavar def serialize_class_instance(val): with suppress(Exception): import_path = get_import_path(val) if import_path and import_object(import_path) is val: return import_path val = f"Unable to serialize instance {val}" warning(val) return val def callable_instances(cls: type): # https://stackoverflow.com/a/71568161/2732151 return isinstance(getattr(cls, "__call__", None), FunctionType) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/_util.py0000644000175100017510000003035515230046455020142 0ustar00runnerrunner"""Collection of general functions and classes.""" import inspect import os import textwrap 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, Type, Union, ) from ._common import ( get_generic_origin, parser_capture, parser_context, ) from ._loaders_dumpers import json_compact_dump, load_value from ._namespace import Namespace from ._optionals import _get_config_read_mode from ._paths import Path from ._type_checking import ArgumentParser __all__ = [ "capture_parser", "register_unresolvable_import_paths", ] NoneType = type(None) default_config_option_help = "Path to a configuration file." config_load_stack: ContextVar[tuple[tuple[str, str], ...]] = ContextVar("config_load_stack", default=()) 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] return ex def merge_config(parser, source: Namespace, target: Namespace) -> Namespace: """Merges the first configuration into the second configuration. Args: parser: The parser object. source: The configuration from which to merge. target: The configuration into which to merge. Returns: A new object with the merged configuration. """ from ._typehints import ActionTypeHint source = source.clone() target = target.clone() with parser_context(parent_parser=parser): ActionTypeHint.discard_init_args_on_class_path_change(parser, target, source) target.update(source) return target def _config_path_id(cfg_path: Path) -> tuple[str, str]: path_id = cfg_path.absolute if not (cfg_path.is_url or cfg_path.is_fsspec): path_id = os.path.realpath(path_id) return path_id, str(cfg_path) def _format_config_load_chain(stack: tuple[tuple[str, str], ...], path_id: tuple[str, str]) -> str: chain = list(stack) + [path_id] for num, (stack_path, _) in enumerate(chain): if stack_path == path_id[0]: chain = chain[num:] break return " -> ".join(display for _, display in chain) @contextmanager def load_config_path_context(cfg_path: Path | None) -> Iterator[None]: if cfg_path is None: yield return path_id = _config_path_id(cfg_path) stack = config_load_stack.get() if path_id[0] in {path for path, _ in stack}: chain = _format_config_load_chain(stack, path_id) raise TypeError(f"Config file loop detected: {chain}") token = config_load_stack.set(stack + (path_id,)) try: yield finally: config_load_stack.reset(token) class JsonargparseWarning(UserWarning): pass def warning(message, category=JsonargparseWarning, stacklevel=1): message = textwrap.fill(textwrap.dedent(message), 110).strip() warnings.warn( "\n" + textwrap.indent(message, " ") + "\n", category=category, stacklevel=stacklevel + 1, ) class CaptureParserException(Exception): def __init__(self, parser: ArgumentParser | None): self.parser = parser super().__init__("" if parser else "No parse_args call to capture the parser.") def capture_parser(function: Callable, *args, **kwargs) -> ArgumentParser: """Returns the parser object used within the execution of a function. The function execution is stopped on the start of the call to :meth:`parse_args <.ArgumentParser.parse_args>`. No parsing is done or execution of instructions after the :meth:`parse_args <.ArgumentParser.parse_args>`. Args: function: A callable that internally creates a parser and calls :meth:`parse_args <.ArgumentParser.parse_args>`. *args: Positional arguments used to run the function. **kwargs: Keyword arguments used to run the function. Raises: CaptureParserException: If the function does not call :meth:`parse_args <.ArgumentParser.parse_args>`. """ try: with parser_context(parser_capture=True): function(*args, **kwargs) except CaptureParserException as ex: return ex.parser # type: ignore[return-value] raise CaptureParserException(None) def return_parser_if_captured(parser: ArgumentParser): if parser_capture.get(): raise CaptureParserException(parser) def identity(value): return value NestedArg = namedtuple("NestedArg", "key val") 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: bool | NestedArg = False if isinstance(value, NestedArg): nested_arg = value value = nested_arg.val cfg_path = None if enable_path and type(value) is str and value != "-": try: cfg_path = Path(value, mode=_get_config_read_mode()) except TypeError: pass else: with load_config_path_context(cfg_path), cfg_path.relative_path_context(): value = load_value(cfg_path.read_text(), simple_types=simple_types) if type(value) is str and value.strip() != "": parsed_val = load_value(value, simple_types=simple_types) if type(parsed_val) is not str: value = parsed_val if isinstance(value, dict) and cfg_path is not None: value["__path__"] = cfg_path if nested_arg: value = NestedArg(key=nested_arg.key, val=value) # type: ignore[union-attr] return value, cfg_path def import_object(name: str): """Returns an object in a module given its dot import path.""" if not isinstance(name, str) or "." not in name: raise ValueError(f"Expected a dot import path string: {name}") if not all(x.isidentifier() for x in name.split(".")): raise ValueError(f"Unexpected import path format: {name}") name_module, name_object = name.rsplit(".", 1) try: parent = __import__(name_module, fromlist=[name_object]) except ModuleNotFoundError as ex: if "." not in name_module: raise ex name_module, name_object1 = name_module.rsplit(".", 1) parent = getattr(__import__(name_module, fromlist=[name_object1]), name_object1) return getattr(parent, name_object) unresolvable_import_paths: dict[Any, str] = {} def register_unresolvable_import_paths(*modules: ModuleType): """Saves import paths of module objects for which its import path is unresolvable from the object alone. Objects with unresolvable import paths have the ``__module__`` attribute set to ``None``. """ for module in modules: for val in vars(module).values(): if ( getattr(val, "__module__", None) is None and getattr(val, "__name__", None) and type(val) in {BuiltinFunctionType, FunctionType, Type, type} ): unresolvable_import_paths[val] = f"{module.__name__}.{val.__name__}" 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: return module_path + "." + name return None 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) if hasattr(value, "__self__") and inspect.isclass(value.__self__) and inspect.ismethod(value): module_path = getattr(value.__self__, "__module__", None) qualname = f"{value.__self__.__name__}.{value.__name__}" else: module_path = getattr(value, "__module__", None) qualname = getattr(value, "__qualname__", "") if module_path is None: path = unresolvable_import_paths.get(value) if path: module_path, _ = path.rsplit(".", 1) elif (not qualname and not inspect.isclass(value)) or ( inspect.ismethod(value) and not inspect.isclass(value.__self__) ): path = get_module_var_path(module_path, value) elif qualname: path = module_path + "." + qualname if not path: raise ValueError(f"Not possible to determine the import path for object {value}.") if qualname and module_path and ("." in qualname or "." in module_path): module_parts = module_path.split(".") for num in range(len(module_parts)): module_path = ".".join(module_parts[: num + 1]) module = import_module(module_path) if "." in qualname: obj_name, attr = qualname.rsplit(".", 1) obj = getattr(module, obj_name, None) if getattr(module, attr, None) is value: path = module_path + "." + attr break elif getattr(obj, attr, None) == value: path = module_path + "." + qualname break elif getattr(module, qualname, None) is value: path = module_path + "." + qualname break return path def object_path_serializer(value): try: path = get_import_path(value) reimported = import_object(path) if value is not reimported: raise ValueError return path except Exception as ex: raise ValueError(f"Only possible to serialize an importable object, given {value}: {ex}") from ex def get_typehint_origin(typehint): if not hasattr(typehint, "__origin__"): typehint_class = get_import_path(typehint.__class__) if typehint_class == "types.UnionType": return Union if typehint_class in {"typing._TypedDictMeta", "typing_extensions._TypedDictMeta"}: return dict return getattr(typehint, "__origin__", None) def hash_item(item): try: if isinstance(item, (dict, list)): item_hash = hash(json_compact_dump(item)) else: item_hash = hash(item) except Exception: item_hash = hash(repr(item)) return item_hash def unique(iterable): unique_items = [] seen = set() for item in iterable: key = hash_item(item) if key not in seen: unique_items.append(item) seen.add(key) return unique_items def iter_to_set_str(val, sep=","): val = unique(val) if len(val) == 1: return str(val[0]) return "{" + sep.join(str(x) for x in val) + "}" def indent_text(text: str, first_line: bool = True) -> str: if first_line: return textwrap.indent(text, " ") lines = text.splitlines() if len(lines) == 1: return text return lines[0] + os.linesep + textwrap.indent(os.linesep.join(lines[1:]), " ") def get_private_kwargs(data, **kwargs): extracted = [data.pop(name, default) for name, default in kwargs.items()] if data: raise ValueError(f"Unexpected keyword parameters: {set(data)}") return extracted[0] if len(extracted) == 1 else extracted class ClassFromFunctionBase: wrapped_function: Callable def get_argument_group_class(parser): import ast from ._core import ActionsContainer, ArgumentGroup if parser.__class__.add_argument != ActionsContainer.add_argument: try: add_argument = parser.__class__.add_argument source = inspect.getsource(add_argument) source = "class _ArgumentGroupAutoSubclass(ArgumentGroup):\n" + source class_ast = ast.parse(source) code = compile(class_ast, filename="", mode="exec") namespace = {**add_argument.__globals__, "ArgumentGroup": ArgumentGroup} exec(code, namespace) group_class = namespace["_ArgumentGroupAutoSubclass"] group_class.__module__ = parser.__class__.__module__ add_argument.__globals__[group_class.__name__] = group_class return group_class except Exception as ex: parser.logger.debug( f"Failed to create ArgumentGroup subclass based on {parser.__class__.__name__}: {ex}", exc_info=ex ) return ArgumentGroup ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/py.typed0000644000175100017510000000000015230046455020133 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/jsonargparse/typing.py0000644000175100017510000006302215230046455020335 0ustar00runnerrunner"""Collection of types and type generators.""" import inspect import operator import os import pathlib import re import sys from collections.abc import Callable from typing import Any, TypeAlias, get_type_hints from ._common import ClassType, is_final_class, is_subclass, path_dump_preserve_relative from ._deprecated import renamed_parameter_warning from ._namespace import Namespace from ._optionals import final, is_alias_type, pydantic_support from ._paths import Path, change_to_path_dir from ._util import ClassFromFunctionBase, get_import_path, get_private_kwargs, import_object __all__ = [ "final", "is_final_class", "register_type", "extend_base_type", "restricted_number_type", "restricted_string_type", "path_type", "class_from_function", "lazy_instance", "PositiveInt", "NonNegativeInt", "PositiveFloat", "NonNegativeFloat", "ClosedUnitInterval", "OpenUnitInterval", "SecretStr", "NotEmptyStr", "Email", "Path", "Path_fr", "Path_fc", "Path_dw", "Path_dc", "Path_drw", ] _operators1 = { operator.gt: ">", operator.ge: ">=", operator.lt: "<", operator.le: "<=", operator.eq: "==", operator.ne: "!=", } _operators2 = {v: k for k, v in _operators1.items()} if sys.version_info >= (3, 12): from typing import TypeAliasType as TypeAliasType _TypeClass = type | TypeAliasType else: _TypeClass = type registered_types: dict[tuple, _TypeClass] = {} registered_type_handlers: dict[_TypeClass, "RegisteredType"] = {} registration_pending: dict[str, Callable] = {} def class_from_function( func: Callable[..., ClassType], func_return: type[ClassType] | None = None, name: str | None = None, ) -> type[ClassType]: """Creates a dynamic class which if instantiated is equivalent to calling func. Args: func: A function that returns an instance of a class. func_return: The return type of the function. Required if func does not have a return type annotation. name: The name of the class. Defaults to function name suffixed with ``_class``. """ from functools import wraps if func_return is None: func_return = inspect.signature(func).return_annotation if func_return is inspect.Signature.empty: raise ValueError(f"{func} does not have a return type annotation") if isinstance(func_return, str): try: func_return = get_type_hints(func)["return"] except Exception as ex: func_return = inspect.signature(func).return_annotation raise ValueError(f"Unable to dereference {func_return}, the return type of {func}: {ex}") from ex if not name: name = func.__qualname__.replace(".", "__") + "_class" caller_module = inspect.getmodule(inspect.stack()[1][0]) or inspect.getmodule(class_from_function) assert caller_module if hasattr(caller_module, name): cls = getattr(caller_module, name) mro = inspect.getmro(cls) if inspect.isclass(cls) else () if ( len(mro) > 1 and mro[1] is func_return and is_subclass(cls, ClassFromFunctionBase) and cls.wrapped_function is func and cls.__name__ == name ): return cls raise ValueError(f"{caller_module.__name__} already defines {name!r}, please use a different name") @wraps(func) def __new__(cls, *args, **kwargs): return func(*args, **kwargs) class ClassFromFunction(func_return, ClassFromFunctionBase): # type: ignore[valid-type,misc] pass setattr(caller_module, name, ClassFromFunction) ClassFromFunction.wrapped_function = func ClassFromFunction.__new__ = __new__ # type: ignore[method-assign] ClassFromFunction.__doc__ = func.__doc__ ClassFromFunction.__module__ = caller_module.__name__ ClassFromFunction.__name__ = name ClassFromFunction.__qualname__ = name return ClassFromFunction def _check_lazy_kwargs(class_type: type, lazy_kwargs: dict): if lazy_kwargs: from argparse import ArgumentError from ._core import ArgumentParser parser = ArgumentParser(exit_on_error=False) parser.add_class_arguments(class_type) try: parser.parse_object(lazy_kwargs) except ArgumentError as ex: raise ValueError(str(ex)) from ex class _LazyInitBaseClass: def __init__(self, class_type: type, lazy_kwargs: dict): assert not issubclass(class_type, _LazyInitBaseClass) _check_lazy_kwargs(class_type, lazy_kwargs) self._lazy = type(self) self._lazy_class_type = class_type self._lazy_kwargs = lazy_kwargs self._lazy_methods = {} seen_methods: dict = {} for name, member in inspect.getmembers(class_type, predicate=inspect.isfunction): method = getattr(self, name) if not inspect.ismethod(method) or name == "__init__": continue assert name not in self.__dict__ self._lazy_methods[name] = method if id(member) in seen_methods: self.__dict__[name] = seen_methods[id(member)] else: from functools import partial lazy_method = partial(self._lazy_init_then_call_method, name) if name == "__call__": lazy_method = staticmethod(lazy_method) self._lazy.__call__ = lazy_method # type: ignore[method-assign] self.__dict__[name] = lazy_method seen_methods[id(member)] = lazy_method def _lazy_init(self): for name in self._lazy_methods: if name == "__call__": self._lazy.__call__ = self._lazy_methods[name] del self.__dict__[name] super().__init__(**self._lazy_kwargs) def _lazy_init_then_call_method(self, method_name, *args, **kwargs): self._lazy_init() return self._lazy_methods[method_name](*args, **kwargs) def lazy_get_init_args(self) -> Namespace: return Namespace(self._lazy_kwargs) def lazy_get_init_data(self): init_args = self.lazy_get_init_args() init = Namespace(class_path=get_import_path(self._lazy_class_type)) if len(self._lazy_kwargs) > 0: init["init_args"] = init_args return init def lazy_instance(class_type: type[ClassType], **kwargs) -> ClassType: """Instantiates a lazy instance of the given type. By lazy it is meant that the ``__init__`` is delayed until the first time that a method of the instance is called. It also provides a `lazy_get_init_data` method useful for serializing. Args: class_type: The class to instantiate. **kwargs: Any keyword arguments to use for instantiation. """ caller_module = inspect.getmodule(inspect.stack()[1][0]) class_name = f"LazyInstance_{class_type.__name__}" if hasattr(caller_module, class_name): lazy_init_class = getattr(caller_module, class_name) assert is_subclass(lazy_init_class, _LazyInitBaseClass) and is_subclass(lazy_init_class, class_type) else: lazy_init_class = type( class_name, (_LazyInitBaseClass, class_type), {"__doc__": f"Class for lazy instances of {class_type}"}, ) if caller_module is not None: lazy_init_class.__module__ = getattr(caller_module, "__name__", __name__) setattr(caller_module, lazy_init_class.__qualname__, lazy_init_class) return lazy_init_class(class_type, kwargs) def extend_base_type( name: str, base_type: type, validation_fn: Callable, docstring: str | None = None, extra_attrs: dict | None = None, register_key: tuple | None = None, ) -> TypeAlias: """Creates and registers an extension of base type. Args: name: How the new type will be called. base_type: The type from which the created type is extended. validation_fn: Function that validates the value on instantiation/casting. Gets two arguments: ``class_type`` and ``value``. docstring: The ``__doc__`` attribute value for the created type. extra_attrs: Attributes set to the type class that the ``validation_fn`` can access. register_key: Used to determine the uniqueness of registered types. Raises: ValueError: If the type has already been registered with a different name. """ if register_key in registered_types: registered_type = registered_types[register_key] if registered_type.__name__ != name: raise ValueError(f"Same type already registered with a different name: {registered_type.__name__}.") return registered_type class TypeCore: _validation_fn = validation_fn _type = base_type def __new__(cls, v): cls._validation_fn(cls, v) return super().__new__(cls, cls._type(v)) if extra_attrs is not None: for key, value in extra_attrs.items(): setattr(TypeCore, key, value) created_type = type(name, (TypeCore, base_type), {"__doc__": docstring}) add_type(created_type, register_key) return created_type def restricted_number_type( name: str | None, base_type: type, restrictions: tuple | list[tuple], join: str = "and", docstring: str | None = None, ) -> TypeAlias: """Creates or returns an already registered restricted number type class. Args: name: Name for the type or ``None`` for an automatic name. base_type: One of ``{int, float}``. restrictions: Tuples of pairs (comparison, reference), e.g. ``('>', 0)``. join: How to combine multiple comparisons, one of ``{'or', 'and'}``. docstring: Docstring for the type class. Returns: The created or retrieved type class. """ if base_type not in {int, float}: raise ValueError("Expected base_type to be one of {int, float}.") if join not in {"or", "and"}: raise ValueError("Expected join to be one of {'or', 'and'}.") restrictions = [restrictions] if isinstance(restrictions, tuple) else restrictions if ( not isinstance(restrictions, list) or not all(isinstance(x, tuple) and len(x) == 2 for x in restrictions) or not all(x[0] in _operators2 and x[1] == base_type(x[1]) for x in restrictions) ): raise ValueError( "Expected restrictions to be a list of tuples each with a comparison operator " f"(> >= < <= == !=) and a reference value of type {base_type.__name__}." ) register_key = (tuple(sorted(restrictions)), base_type, join) restrictions = [(_operators2[x[0]], x[1]) for x in restrictions] expression = (" " + join + " ").join(["v" + _operators1[op] + str(ref) for op, ref in restrictions]) if name is None: name = base_type.__name__ for num, (comparison, ref) in enumerate(restrictions): name += "_" + join + "_" if num > 0 else "_" name += comparison.__name__ + str(ref).replace(".", "") extra_attrs = { "_restrictions": restrictions, "_expression": expression, "_join": join, "_type": base_type, } def validation_fn(cls, v): if isinstance(v, bool): raise ValueError(f"{v} not a number") if cls._type == int and isinstance(v, float) and not float.is_integer(v): raise ValueError(f"{v} not an integer") vv = cls._type(v) check = [comparison(vv, ref) for comparison, ref in cls._restrictions] if (cls._join == "and" and not all(check)) or (cls._join == "or" and not any(check)): raise ValueError(f"{v} does not conform to restriction {cls._expression}") return extend_base_type( name=name, base_type=base_type, validation_fn=validation_fn, register_key=register_key, docstring=docstring, extra_attrs=extra_attrs, ) def restricted_string_type( name: str, regex: str | re.Pattern, docstring: str | None = None, ) -> TypeAlias: """Creates or returns an already registered restricted string type class. Args: name: Name for the type or ``None`` for an automatic name. regex: Regular expression that the string must match. docstring: Docstring for the type class. Returns: The created or retrieved type class. """ if isinstance(regex, str): regex = re.compile(regex) expression = "matching " + regex.pattern extra_attrs = { "_regex": regex, "_expression": expression, "_type": str, } def validation_fn(cls, v): if not cls._regex.match(v): raise ValueError(f"{v} does not match regular expression {cls._regex.pattern}") return extend_base_type( name=name, base_type=str, validation_fn=validation_fn, register_key=(expression, str), docstring=docstring, extra_attrs=extra_attrs, ) def _is_path_type(value, class_type): return isinstance(value, Path) def _serialize_path(path: Path): if not isinstance(path, Path): raise ValueError("Expected a Path instance.") if path_dump_preserve_relative.get() and path.relative != path.absolute: return { "relative": path._relative, "cwd": path._cwd, } return str(path) def path_type(mode: str, docstring: str | None = None, **kwargs) -> TypeAlias: """Creates or returns an already registered path type class. Args: mode: The required type and access permissions among ``[fdrwxcuFDRWX]``. docstring: Docstring for the type class. Returns: The created or retrieved type class. """ Path._check_mode(mode) name = "Path_" + mode key_name = "path " + "".join(sorted(mode)) skip_check = get_private_kwargs(kwargs, skip_check=False) if skip_check: from ._deprecated import path_skip_check_deprecation path_skip_check_deprecation(stacklevel=4) name += "_skip_check" key_name += " skip_check" register_key = (key_name, str) if register_key in registered_types: return registered_types[register_key] class PathType(Path): _expression = name _mode = mode _skip_check = skip_check _type = _serialize_path def __init__(self, v, **k): if isinstance(v, dict) and set(v) == {"cwd", "relative"}: with change_to_path_dir(v["cwd"]): super().__init__(v["relative"], mode=self._mode, skip_check=self._skip_check, **k) else: super().__init__(v, mode=self._mode, skip_check=self._skip_check, **k) restricted_type = type(name, (PathType,), {"__doc__": docstring}) add_type(restricted_type, register_key, type_check=_is_path_type) return restricted_type class RegisteredType: def __init__( self, class_type: _TypeClass, serializer: Callable, deserializer: Callable | None, deserializer_exceptions: type[Exception] | tuple[type[Exception], ...], type_check: Callable, ): self.class_type = class_type self.serializer = serializer self.base_deserializer = class_type if deserializer is None else deserializer self.deserializer_exceptions = deserializer_exceptions self.type_check = type_check def __eq__(self, other): return all(getattr(self, k) == getattr(other, k) for k in ["class_type", "serializer", "base_deserializer"]) def is_value_of_type(self, value): return self.type_check(value, self.class_type) def deserializer(self, value): try: return self.base_deserializer(value) except self.deserializer_exceptions as ex: class_type_name = getattr(self.class_type, "__name__", str(self.class_type)) ex2 = ValueError(f"Not of type {class_type_name}: {ex}") ex2.parent = ex raise ex2 from ex @renamed_parameter_warning({"type_class": "class_type"}) def register_type( class_type: _TypeClass, serializer: Callable = str, 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: tuple | None = None, ) -> None: """Registers a new type for use in jsonargparse parsers. Args: class_type: The class to be registered. Python 3.12+ also supports ``TypeAliasType`` aliases. serializer: Function that converts an instance of the class to a basic type. deserializer: Function that converts a basic type to an instance of the class. Default instantiates ``class_type``. deserializer_exceptions: Exceptions that deserializer raises when it fails. type_check: Function to check if a value is of ``class_type``. Gets as arguments the value and ``class_type``. fail_already_registered: Whether to fail if type has already been registered. uniqueness_key: Key to determine uniqueness of type. """ if sys.version_info[:2] < (3, 12) and not inspect.isclass(class_type): raise ValueError(f"Expected class_type to be a class, got {type(class_type)}") elif sys.version_info[:2] >= (3, 12) and not (inspect.isclass(class_type) or is_alias_type(class_type)): raise ValueError(f"Expected class_type to be a class or a type alias, got {type(class_type)}") type_handler = RegisteredType(class_type, serializer, deserializer, deserializer_exceptions, type_check) fail_already_registered = globals().get("_fail_already_registered", fail_already_registered) if not uniqueness_key and fail_already_registered and get_registered_type(class_type): if type_handler == registered_type_handlers[class_type]: return raise ValueError(f'Type "{class_type}" already registered with different serializer and/or deserializer.') registered_type_handlers[class_type] = type_handler if uniqueness_key is not None: registered_types[uniqueness_key] = class_type def register_type_on_first_use(import_path: str, *args, **kwargs): registration_pending[import_path] = lambda: register_type( import_object(import_path), *args, **kwargs, ) def get_registered_type(class_type) -> RegisteredType | None: if class_type not in registered_type_handlers: from contextlib import suppress with suppress(AttributeError, ValueError): import_path = get_import_path(class_type) if import_path in registration_pending: registration_pending.pop(import_path)() return registered_type_handlers.get(class_type) def add_type(class_type: type, uniqueness_key: tuple | None, type_check: Callable | None = None): assert uniqueness_key not in registered_types if class_type.__name__ in globals(): raise ValueError(f'Type name "{class_type.__name__}" clashes with name already defined in jsonargparse.typing.') globals()[class_type.__name__] = class_type kwargs = {"uniqueness_key": uniqueness_key} if type_check is not None: kwargs["type_check"] = type_check # type: ignore[assignment] register_type(class_type, class_type._type, **kwargs) # type: ignore[attr-defined] _fail_already_registered = False PositiveInt = restricted_number_type("PositiveInt", int, (">", 0), docstring="int restricted to be >0") NonNegativeInt = restricted_number_type("NonNegativeInt", int, (">=", 0), docstring="int restricted to be ≥0") PositiveFloat = restricted_number_type("PositiveFloat", float, (">", 0), docstring="float restricted to be >0") NonNegativeFloat = restricted_number_type("NonNegativeFloat", float, (">=", 0), docstring="float restricted to be ≥0") ClosedUnitInterval = restricted_number_type( "ClosedUnitInterval", float, [(">=", 0), ("<=", 1)], docstring="float restricted to be ≥0 and ≤1" ) OpenUnitInterval = restricted_number_type( "OpenUnitInterval", float, [(">", 0), ("<", 1)], docstring="float restricted to be >0 and <1" ) NotEmptyStr = restricted_string_type( "NotEmptyStr", r"^.*[^ ].*$", docstring=r"str restricted to not-empty pattern ``^.*[^ ].*$``" ) Email = restricted_string_type( "Email", r"^[^@ ]+@[^@ ]+\.[^@ ]+$", docstring=r"str restricted to the email pattern ``^[^@ ]+@[^@ ]+\.[^@ ]+$``" ) Path_fr = path_type("fr", docstring="path to a file that exists and is readable") Path_fc = path_type("fc", docstring="path to a file that can be created if it does not exist") Path_dw = path_type("dw", docstring="path to a directory that exists and is writable") Path_dc = path_type("dc", docstring="path to a directory that can be created if it does not exist") Path_drw = path_type("drw", docstring="path to a directory that exists and is readable and writable") register_type(os.PathLike, str, str) register_type(complex) register_type_on_first_use("decimal.Decimal", float) register_type_on_first_use("uuid.UUID") for _path in [pathlib.Path, pathlib.PosixPath, pathlib.WindowsPath]: register_type(_path, str, _path, type_check=isinstance) def timedelta_deserializer(value): def raise_error(): raise ValueError(f'Expected a string with form "h:m:s" or "d days, h:m:s" but got "{value}"') if not isinstance(value, str): raise_error() pattern = r"(?P\d+):(?P\d+):(?P\d[\.\d+]*)" if "day" in value: pattern = r"(?P[-\d]+) day[s]*, " + pattern match = re.match(pattern, value) if not match: raise_error() kwargs = {key: float(val) for key, val in match.groupdict().items()} from datetime import timedelta return timedelta(**kwargs) register_type_on_first_use("datetime.timedelta", deserializer=timedelta_deserializer) def bytes_serializer(value: bytes | bytearray) -> str: from base64 import b64encode return b64encode(value).decode() def bytes_deserializer(value: str) -> bytes: from base64 import b64decode return b64decode(value) def bytearray_deserializer(value: str) -> bytearray: from base64 import b64decode return bytearray(b64decode(value)) register_type_on_first_use("builtins.bytes", serializer=bytes_serializer, deserializer=bytes_deserializer) register_type_on_first_use("builtins.bytearray", serializer=bytes_serializer, deserializer=bytearray_deserializer) def range_serializer(value): if value.step == 1: if value.start == 0: return f"range({value.stop})" return f"range({value.start}, {value.stop})" return f"range({value.start}, {value.stop}, {value.step})" re_range_stop = re.compile(r"^(-?\d+)$") re_range_start_stop = re.compile(r"^(-?\d+),(-?\d+)$") re_range_start_stop_step = re.compile(r"^(-?\d+),(-?\d+),(-?\d+)$") def range_deserializer(value): value = value.strip() if value.startswith("range(") and value.endswith(")"): value = value[6:-1].replace(" ", "") match = re_range_stop.match(value) if match: return range(int(match[1])) match = re_range_start_stop.match(value) if match: return range(int(match[1]), int(match[2])) match = re_range_start_stop_step.match(value) if match: return range(int(match[1]), int(match[2]), int(match[3])) raise ValueError("Expected 'range()' or 'range(, )' or 'range(, , )'") register_type(range, serializer=range_serializer, deserializer=range_deserializer) class SecretStr: """Holds a secret string that serializes to ``**********``.""" def __init__(self, value: str): self._value = value def __str__(self) -> str: return "**********" def __len__(self) -> int: return len(self._value) def __eq__(self, other: Any) -> bool: return isinstance(other, self.__class__) and self._value == other._value def __hash__(self) -> int: return hash(self._value) def get_secret_value(self) -> str: """Returns the actual secret value.""" return self._value register_type(SecretStr) register_type_on_first_use("pydantic.SecretStr") def pydantic_deserializer(class_type): from pydantic import create_model # pylint: disable=no-name-in-module pydantic_model = create_model("pydantic_model", pydantic_field=(class_type, ...)) def deserialize(value): return pydantic_model(pydantic_field=value).pydantic_field return deserialize def pydantic_serializer(class_type): serializer = str for base in [int, float, bool, list, dict, (set, list)]: if not isinstance(base, tuple): base = (base, base) if issubclass(class_type, base[0]): serializer = base[1] break return serializer pydantic_type_modules = { "pydantic_core._pydantic_core", "pydantic.types", "pydantic.networks", "pydantic_extra_types", } def is_pydantic_type(class_type): return ( pydantic_support and inspect.isclass(class_type) and any(getattr(t, "__module__", "") in pydantic_type_modules for t in inspect.getmro(class_type)) ) def register_pydantic_type(class_type): from ._optionals import is_annotated if is_annotated(class_type): class_type = class_type.__origin__ if not is_pydantic_type(class_type): return if not get_registered_type(class_type): from pydantic import ValidationError register_type( class_type=class_type, serializer=pydantic_serializer(class_type), deserializer=pydantic_deserializer(class_type), deserializer_exceptions=(ValidationError, TypeError), ) del _fail_already_registered ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1784696114.2736757 jsonargparse-4.50.0/jsonargparse.egg-info/0000755000175100017510000000000015230046462020136 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696114.0 jsonargparse-4.50.0/jsonargparse.egg-info/PKG-INFO0000644000175100017510000003024615230046462021240 0ustar00runnerrunnerMetadata-Version: 2.4 Name: jsonargparse Version: 4.50.0 Summary: Minimal effort CLIs derived from type hints and parse from command line, config files and environment variables. Author-email: Mauricio Villegas License-Expression: MIT Project-URL: Documentation-stable, https://jsonargparse.readthedocs.io/en/stable/ Project-URL: Documentation-latest, https://jsonargparse.readthedocs.io/en/latest/ Project-URL: Changes, https://jsonargparse.readthedocs.io/en/stable/changelog.html Project-URL: GitHub, https://github.com/mauvilsa/jsonargparse Project-URL: PyPI, https://pypi.org/project/jsonargparse Project-URL: SonarCloud, https://sonarcloud.io/dashboard?id=mauvilsa_jsonargparse Project-URL: Codecov, https://codecov.io/gh/mauvilsa/jsonargparse Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Intended Audience :: Developers Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE.rst Requires-Dist: PyYAML>=3.13 Provides-Extra: all Requires-Dist: jsonargparse[signatures]; extra == "all" Requires-Dist: jsonargparse[jsonschema]; extra == "all" Requires-Dist: jsonargparse[jsonnet]; extra == "all" Requires-Dist: jsonargparse[toml]; extra == "all" Requires-Dist: jsonargparse[urls]; extra == "all" Requires-Dist: jsonargparse[fsspec]; extra == "all" Requires-Dist: jsonargparse[ruamel]; extra == "all" Requires-Dist: jsonargparse[omegaconf]; extra == "all" Requires-Dist: jsonargparse[typing-extensions]; extra == "all" Provides-Extra: signatures Requires-Dist: jsonargparse[typing-extensions]; extra == "signatures" Requires-Dist: docstring-parser>=0.17; extra == "signatures" Requires-Dist: typeshed-client>=2.8.2; extra == "signatures" Provides-Extra: jsonschema Requires-Dist: jsonschema>=3.2.0; extra == "jsonschema" Provides-Extra: jsonnet Requires-Dist: jsonnet>=0.21.0; extra == "jsonnet" Provides-Extra: toml Requires-Dist: toml>=0.10.2; extra == "toml" Provides-Extra: urls Requires-Dist: requests>=2.18.4; extra == "urls" Provides-Extra: fsspec Requires-Dist: fsspec>=0.8.4; extra == "fsspec" Provides-Extra: shtab Requires-Dist: shtab>=1.7.1; extra == "shtab" Provides-Extra: argcomplete Requires-Dist: argcomplete>=3.5.1; extra == "argcomplete" Provides-Extra: ruamel Requires-Dist: ruamel.yaml>=0.18.15; extra == "ruamel" Provides-Extra: ruyaml Requires-Dist: jsonargparse[ruamel]; extra == "ruyaml" Provides-Extra: omegaconf Requires-Dist: omegaconf>=2.1.1; extra == "omegaconf" Provides-Extra: typing-extensions Requires-Dist: typing-extensions>=4.12.0; python_version < "3.11" and extra == "typing-extensions" Provides-Extra: test Requires-Dist: jsonargparse[test-no-urls]; extra == "test" Requires-Dist: jsonargparse[shtab]; extra == "test" Requires-Dist: jsonargparse[argcomplete]; extra == "test" Requires-Dist: types-PyYAML>=6.0.11; extra == "test" Requires-Dist: types-requests<2.34,>=2.28.9; extra == "test" Requires-Dist: requests<2.34; extra == "test" Requires-Dist: responses>=0.12.0; extra == "test" Requires-Dist: pydantic>=2.3.0; extra == "test" Requires-Dist: attrs>=22.2.0; extra == "test" Provides-Extra: test-no-urls Requires-Dist: pytest>=6.2.5; extra == "test-no-urls" Requires-Dist: pytest-subtests>=0.8.0; extra == "test-no-urls" Provides-Extra: coverage Requires-Dist: jsonargparse[test-no-urls]; extra == "coverage" Requires-Dist: pytest-cov>=4.0.0; extra == "coverage" Provides-Extra: dev Requires-Dist: jsonargparse[test]; extra == "dev" Requires-Dist: jsonargparse[coverage]; extra == "dev" Requires-Dist: jsonargparse[doc]; extra == "dev" Requires-Dist: pre-commit>=2.19.0; extra == "dev" Requires-Dist: tox>=4.0.0; extra == "dev" Requires-Dist: tox-uv>=1.0.0; extra == "dev" Requires-Dist: build>=0.10.0; extra == "dev" Provides-Extra: doc Requires-Dist: Sphinx>=1.7.9; extra == "doc" Requires-Dist: sphinx-rtd-theme>=1.2.2; extra == "doc" Requires-Dist: autodocsumm>=0.1.10; extra == "doc" Requires-Dist: sphinx-autodoc-typehints>=1.19.5; extra == "doc" Provides-Extra: maintainer Requires-Dist: bump2version>=0.5.11; extra == "maintainer" Requires-Dist: twine>=4.0.2; extra == "maintainer" Dynamic: license-file .. image:: https://readthedocs.org/projects/jsonargparse/badge/?version=stable :target: https://readthedocs.org/projects/jsonargparse/ .. image:: https://github.com/mauvilsa/jsonargparse/actions/workflows/tests.yaml/badge.svg :target: https://github.com/mauvilsa/jsonargparse/actions/workflows/tests.yaml .. image:: https://codecov.io/gh/mauvilsa/jsonargparse/branch/main/graph/badge.svg :target: https://codecov.io/gh/mauvilsa/jsonargparse .. image:: https://sonarcloud.io/api/project_badges/measure?project=mauvilsa_jsonargparse&metric=alert_status :target: https://sonarcloud.io/dashboard?id=mauvilsa_jsonargparse .. image:: https://badge.fury.io/py/jsonargparse.svg :target: https://badge.fury.io/py/jsonargparse jsonargparse ============ Docs: https://jsonargparse.readthedocs.io/ | Source: https://github.com/mauvilsa/jsonargparse/ ``jsonargparse`` is a library for creating command-line interfaces (CLIs) and making Python apps easily configurable. It is a well-maintained project with frequent releases, adhering to high standards of development: semantic versioning, deprecation periods, changelog, automated testing, and full test coverage. Although ``jsonargparse`` might not be widely recognized yet, it already boasts a `substantial user base `__. Most notably, it serves as the framework behind pytorch-lightning's `LightningCLI `__. Teaser examples --------------- CLI with minimal boilerplate: .. code-block:: python from jsonargparse import auto_cli def main_function(...): # your main parameters with type hints here ... # your main code here if __name__ == "__main__": auto_cli(main_function) # parses arguments and runs main_function Minimal boilerplate but manually parsing: .. code-block:: python from jsonargparse import auto_parser parser = auto_parser(main_function) cfg = parser.parse_args() ... Powerful argparse-like low level parsers: .. code-block:: python from jsonargparse import ArgumentParser parser = ArgumentParser() parser.add_argument("--config", action="config") # support config files 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 ... cfg = parser.parse_args() init = parser.instantiate(cfg) ... Features -------- ``jsonargparse`` is user-friendly and encourages the development of **clean, high-quality code**. It encompasses numerous powerful features, some unique to ``jsonargparse``, while also combining advantages found in similar packages: - **Automatic** creation of CLIs, like `Fire `__, `Typer `__, `Clize `__ and `Tyro `__. - Use **type hints** for argument validation, like `Typer `__, `Tap `__ and `Tyro `__. - Use of **docstrings** for automatic generation of help, like `Tap `__, `Tyro `__ and `SimpleParsing `__. - Parse from **configuration files** and **environment variables**, like `OmegaConf `__, `dynaconf `__, `confuse `__ and `configargparse `__. - **Dataclasses** support, like `SimpleParsing `__ and `Tyro `__. Other notable features include: - **Extensive type hint support:** nested types (union, optional), containers (list, dict, etc.), protocols, user-defined generics, restricted types (regex, numbers), paths, URLs, types from stubs (``*.pyi``), future annotations (PEP `563 `__), and backports (PEP `604 `__). - **Keyword arguments introspection:** resolving of parameters used via ``**kwargs``. - **Dependency injection:** support types that expect a class instance and callables that return a class instance. - **Structured configs:** parse config files with more understandable non-flat hierarchies. - **Config file formats:** `json `__, `yaml `__, `toml `__, `jsonnet `__ and extensible to more formats. - **Relative paths:** within config files and parsing of config paths referenced inside other configs. - **Argument linking:** directing parsed values to multiple parameters, preventing unnecessary interpolation in configs. - **Variable interpolation:** powered by `OmegaConf `__. - **Tab completion:** powered by `shtab `__ or `argcomplete `__. Design principles ----------------- - **Non-intrusive/decoupled:** There is no requirement for unrelated modifications throughout a codebase, maintaining the `separation of concerns principle `__. In simpler terms, changes should make sense even without the CLI. No need to inherit from a special class, add decorators, or use CLI-specific type hints. - **Minimal boilerplate:** A recommended practice is to write code with function/class parameters having meaningful names, accurate type hints, and descriptive docstrings. Reuse these wherever they appear to automatically generate the CLI, following the `don't repeat yourself principle `__. A notable advantage is that when parameters are added or types changed, the CLI will remain synchronized, avoiding the need to update the CLI's implementation. - **Dependency injection:** Using as type hint a class or a callable that instantiates a class, a practice known as `dependency injection `__, is a sound design pattern for developing loosely coupled and highly configurable software. Such type hints should be supported with minimal restrictions. .. _installation: Installation ============ You can install using `pip `__ as: .. code-block:: bash pip install jsonargparse By default, the only dependency installed with ``jsonargparse`` is `PyYAML `__. However, several optional features can be enabled by specifying one or more of the following extras (optional dependencies): ``signatures``, ``jsonschema``, ``jsonnet``, ``urls``, ``fsspec``, ``toml``, ``ruamel``, ``omegaconf``, ``shtab``, and ``argcomplete``. Additionally, the ``all`` extras can be used to enable all optional features (excluding tab completion ones). To install ``jsonargparse`` with extras, use the following syntax: .. code-block:: bash pip install "jsonargparse[signatures,urls]" # Enable signatures and URLs features pip install "jsonargparse[all]" # Enable all optional features To install the latest development version, use the following command: .. code-block:: bash pip install "jsonargparse[signatures] @ https://github.com/mauvilsa/jsonargparse/zipball/main" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696114.0 jsonargparse-4.50.0/jsonargparse.egg-info/SOURCES.txt0000644000175100017510000000173515230046462022030 0ustar00runnerrunnerLICENSE.rst README.rst pyproject.toml jsonargparse/__init__.py jsonargparse/_actions.py jsonargparse/_cli.py jsonargparse/_common.py jsonargparse/_completions.py jsonargparse/_core.py jsonargparse/_deprecated.py jsonargparse/_formatters.py jsonargparse/_from_config.py jsonargparse/_instantiation.py jsonargparse/_jsonnet.py jsonargparse/_jsonschema.py jsonargparse/_link_arguments.py jsonargparse/_loaders_dumpers.py jsonargparse/_namespace.py jsonargparse/_optionals.py jsonargparse/_parameter_resolvers.py jsonargparse/_paths.py jsonargparse/_postponed_annotations.py jsonargparse/_required.py jsonargparse/_signatures.py jsonargparse/_stubs_resolver.py jsonargparse/_subcommands.py jsonargparse/_type_checking.py jsonargparse/_typehints.py jsonargparse/_util.py jsonargparse/py.typed jsonargparse/typing.py jsonargparse.egg-info/PKG-INFO jsonargparse.egg-info/SOURCES.txt jsonargparse.egg-info/dependency_links.txt jsonargparse.egg-info/requires.txt jsonargparse.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696114.0 jsonargparse-4.50.0/jsonargparse.egg-info/dependency_links.txt0000644000175100017510000000000115230046462024204 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696114.0 jsonargparse-4.50.0/jsonargparse.egg-info/requires.txt0000644000175100017510000000235715230046462022545 0ustar00runnerrunnerPyYAML>=3.13 [all] jsonargparse[signatures] jsonargparse[jsonschema] jsonargparse[jsonnet] jsonargparse[toml] jsonargparse[urls] jsonargparse[fsspec] jsonargparse[ruamel] jsonargparse[omegaconf] jsonargparse[typing-extensions] [argcomplete] argcomplete>=3.5.1 [coverage] jsonargparse[test-no-urls] pytest-cov>=4.0.0 [dev] jsonargparse[test] jsonargparse[coverage] jsonargparse[doc] pre-commit>=2.19.0 tox>=4.0.0 tox-uv>=1.0.0 build>=0.10.0 [doc] Sphinx>=1.7.9 sphinx-rtd-theme>=1.2.2 autodocsumm>=0.1.10 sphinx-autodoc-typehints>=1.19.5 [fsspec] fsspec>=0.8.4 [jsonnet] jsonnet>=0.21.0 [jsonschema] jsonschema>=3.2.0 [maintainer] bump2version>=0.5.11 twine>=4.0.2 [omegaconf] omegaconf>=2.1.1 [ruamel] ruamel.yaml>=0.18.15 [ruyaml] jsonargparse[ruamel] [shtab] shtab>=1.7.1 [signatures] jsonargparse[typing-extensions] docstring-parser>=0.17 typeshed-client>=2.8.2 [test] jsonargparse[test-no-urls] jsonargparse[shtab] jsonargparse[argcomplete] types-PyYAML>=6.0.11 types-requests<2.34,>=2.28.9 requests<2.34 responses>=0.12.0 pydantic>=2.3.0 attrs>=22.2.0 [test-no-urls] pytest>=6.2.5 pytest-subtests>=0.8.0 [toml] toml>=0.10.2 [typing-extensions] [typing-extensions:python_version < "3.11"] typing-extensions>=4.12.0 [urls] requests>=2.18.4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696114.0 jsonargparse-4.50.0/jsonargparse.egg-info/top_level.txt0000644000175100017510000000001515230046462022664 0ustar00runnerrunnerjsonargparse ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1784696109.0 jsonargparse-4.50.0/pyproject.toml0000644000175100017510000001554015230046455016671 0ustar00runnerrunner[build-system] requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] name = "jsonargparse" dynamic = ["version"] description = "Minimal effort CLIs derived from type hints and parse from command line, config files and environment variables." authors = [ {name = "Mauricio Villegas", email = "mauricio@omnius.com"}, ] readme = "README.rst" license = "MIT" license-files = ["LICENSE.rst"] 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.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", ] dependencies = [ "PyYAML>=3.13", ] [project.optional-dependencies] all = [ "jsonargparse[signatures]", "jsonargparse[jsonschema]", "jsonargparse[jsonnet]", "jsonargparse[toml]", "jsonargparse[urls]", "jsonargparse[fsspec]", "jsonargparse[ruamel]", "jsonargparse[omegaconf]", "jsonargparse[typing-extensions]", ] signatures = [ "jsonargparse[typing-extensions]", "docstring-parser>=0.17", "typeshed-client>=2.8.2", ] jsonschema = [ "jsonschema>=3.2.0", ] jsonnet = [ "jsonnet>=0.21.0", ] toml = [ "toml>=0.10.2", ] urls = [ "requests>=2.18.4", ] fsspec = [ "fsspec>=0.8.4", ] shtab = [ "shtab>=1.7.1", ] argcomplete = [ "argcomplete>=3.5.1", ] ruamel = [ "ruamel.yaml>=0.18.15", ] ruyaml = [ "jsonargparse[ruamel]", ] omegaconf = [ "omegaconf>=2.1.1", ] typing-extensions = [ "typing-extensions>=4.12.0; python_version < '3.11'", ] test = [ "jsonargparse[test-no-urls]", "jsonargparse[shtab]", "jsonargparse[argcomplete]", "types-PyYAML>=6.0.11", "types-requests>=2.28.9,<2.34", "requests<2.34", "responses>=0.12.0", "pydantic>=2.3.0", "attrs>=22.2.0", ] test-no-urls = [ "pytest>=6.2.5", "pytest-subtests>=0.8.0", ] coverage = [ "jsonargparse[test-no-urls]", "pytest-cov>=4.0.0", ] dev = [ "jsonargparse[test]", "jsonargparse[coverage]", "jsonargparse[doc]", "pre-commit>=2.19.0", "tox>=4.0.0", "tox-uv>=1.0.0", "build>=0.10.0", ] doc = [ "Sphinx>=1.7.9", "sphinx-rtd-theme>=1.2.2", "autodocsumm>=0.1.10", "sphinx-autodoc-typehints>=1.19.5", ] maintainer = [ "bump2version>=0.5.11", "twine>=4.0.2", ] [project.urls] Documentation-stable = "https://jsonargparse.readthedocs.io/en/stable/" Documentation-latest = "https://jsonargparse.readthedocs.io/en/latest/" Changes = "https://jsonargparse.readthedocs.io/en/stable/changelog.html" GitHub = "https://github.com/mauvilsa/jsonargparse" PyPI = "https://pypi.org/project/jsonargparse" SonarCloud = "https://sonarcloud.io/dashboard?id=mauvilsa_jsonargparse" Codecov = "https://codecov.io/gh/mauvilsa/jsonargparse" [tool.setuptools] platforms = ["Any"] packages = ["jsonargparse"] [tool.setuptools.dynamic] version = {attr = "jsonargparse.__version__"} [tool.setuptools.package-data] jsonargparse = ["py.typed"] [tool.pytest.ini_options] addopts = "-s" testpaths = ["jsonargparse_tests"] [tool.coverage.run] relative_files = true source = ["jsonargparse"] [tool.mypy] allow_redefinition = true warn_unused_ignores = true disable_error_code = "annotation-unchecked" [tool.ruff] fix = true line-length = 120 [tool.ruff.lint] select = [ "E", "W", # pycodestyle "F", # pyflakes "I", # isort ] extend-select = [ "RUF100", # unused-noqa ] ignore = [ "E731", # Do not convert lambda assigns to a def "E721", # Allow comparing types with type() ] [tool.typos.default.extend-identifiers] Villegas = "Villegas" [tool.tox] legacy_tox_ini = """ [tox] 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] extras = all-extras: test,coverage,all no-extras: coverage argparse: coverage changedir = jsonargparse_tests allowlist_externals = rm passenv = UV_EXCLUDE_NEWER # Ensure uv installs into the tox env even if UV_SYSTEM_PYTHON=1 set outside setenv = UV_SYSTEM_PYTHON = 0 commands = all-extras: python -m pytest {posargs} no-extras: python -m pytest {posargs} argparse: rm -f tests_argparse_{env_name}.py argparse: ./argparse_tests_generate.py --output_file tests_argparse_{env_name}.py --verbose false argparse: python -m pytest -W ignore::pytest.PytestUnknownMarkWarning --tb=no -m "not (not_supported or implementation_specific or investigate)" tests_argparse_{env_name}.py {posargs} usedevelop = true [testenv:.pkg] setenv = {[testenv]setenv} [testenv:omegaconf] extras = test,coverage,all changedir = jsonargparse_tests setenv = {[testenv]setenv} JSONARGPARSE_OMEGACONF_FULL_TEST = true commands = python -m pytest {posargs} [testenv:pydantic-v1] extras = coverage deps = ./jsonargparse_tests commands = # Test with pydantic<2 python -c "\ from pathlib import Path; \ from shutil import copy; \ copy('conftest.py', Path(r'{envtmpdir}', 'conftest.py')); \ copy('test_pydantic.py', Path(r'{envtmpdir}', 'test_pydantic.py'))\ " uv pip install --python {envpython} "pydantic<2" python -m pytest {posargs} {envtmpdir}/test_pydantic.py # Test with pydantic>=2 importing from pydantic.v1 python -c "\ import re, pathlib; \ path = pathlib.Path(r'{envtmpdir}', 'test_pydantic.py'); \ content = path.read_text(); \ content = re.sub(r'import pydantic', 'import pydantic.v1 as pydantic', content); \ content = re.sub(r'^annotated = [^\\n]*', 'annotated = False', content, flags=re.MULTILINE); \ path.write_text(content)\ " uv pip install --python {envpython} "pydantic>=2" python -m pytest {posargs} {envtmpdir}/test_pydantic.py [testenv:without-pyyaml] extras = test,coverage,all commands = uv pip uninstall --python {envpython} argcomplete omegaconf pyyaml responses ruamel.yaml ruamel.yaml.clib types-PyYAML python -m pytest {posargs} [testenv:without-future-annotations] extras = test,coverage,all deps = ./jsonargparse_tests allowlist_externals = sh commands = sh -c "\ rm -rf /tmp/_without_future_annotations; \ mkdir /tmp/_without_future_annotations; \ cp *.py /tmp/_without_future_annotations; \ sed -i -e '/^from __future__ import annotations$/d' /tmp/_without_future_annotations/*.py; \ " python -m pytest /tmp/_without_future_annotations {posargs} commands_post = sh -c "rm -rf /tmp/_without_future_annotations" """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1784696114.2782161 jsonargparse-4.50.0/setup.cfg0000644000175100017510000000004615230046462015567 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0