diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 71412d35..5a3559b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,45 @@ The semantic versioning only considers the public API as described in paths are considered internals and can change in minor and patch releases. +v5.0.0 (unreleased) +------------------- + +Changed +^^^^^^^ +- The print config argument now defaults to ``--print_``, + instead of always being ``--print_config`` (`#969 + `__). +- ``instantiate_subclass_spec_in_any`` now defaults to ``False``, so a subclass + spec given for a type that accepts any value is kept as is, instead of being + imported and instantiated (`#969 + `__). +- Import paths denied by ``import_path_denylist`` now always fail, instead of + only failing when ``import_path_denylist`` or ``import_path_allowlist`` is + given a value (`#969 `__). +- In the add signature methods, a parameter with an ``Optional`` type and no + default is now required, and with ``fail_untyped=False`` a required parameter + without a type annotation now gets type ``Untyped`` and stays required, + instead of both becoming optional with default ``None`` (`#969 + `__). +- A config that has multiple subcommand settings now requires the subcommand to + be given explicitly, instead of taking the first one (`#969 + `__). +- Config objects always include metadata, i.e. ``clone(with_meta=False)`` is now + the only way to strip it (`#969 + `__). + +Removed +^^^^^^^ +- All features deprecated in v4 are now removed, see :ref:`migrate-v5` for the + complete list and how to update code (`#969 + `__). +- ``ruyaml`` extras require, superseded by ``ruamel`` (`#969 + `__). +- The ``yaml.SafeDumper`` representer for ``Namespace``, which was only added + for backward compatibility in pytorch-lightning (`#969 + `__). + + v4.52.0 (2026-09-01) -------------------- diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index c75de94d..0b5e2874 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -2172,14 +2172,11 @@ would also accept subclasses of ``MyClass``, and the config would be: .. note:: A parameter of type ``Any``, ``object``, or ``Untyped``, accepts a dict with - ``class_path`` and ``init_args``, and the class is parsed and instantiated. - - This instantiation is deprecated. From v5.0.0 the subclass spec is kept as - is, so that the code receiving it decides whether to instantiate it. Set - ``instantiate_subclass_spec_in_any=False`` in :func:`.set_parsing_settings` - to get this behavior now and silence the deprecation warning. Setting it to - ``True`` keeps the instantiation, but is discouraged, since it means that a - config can instantiate any class, which is a security risk. + ``class_path`` and ``init_args``, and the spec is kept as is, so that the + code receiving it decides whether to instantiate it. Set + ``instantiate_subclass_spec_in_any=True`` in :func:`.set_parsing_settings` + to have ``instantiate`` build the class, though this is discouraged, since + it means that a config can instantiate any class, which is a security risk. A value that looks like a subclass spec, i.e. has a ``class_path``, but can't be parsed as one, e.g. because the class fails to import, is by @@ -2276,11 +2273,11 @@ The denylist is not the only thing that limits what a config can reach. Type hints do as well, since a ``class_path`` is only accepted where the annotation allows one, and must name a subclass of the annotated type. The exceptions are ``Any`` and ``object``, which accept a subclass spec of any class, see -:ref:`sub-classes`. Setting ``instantiate_subclass_spec_in_any=False``, which is -the default from v5.0.0, keeps these values as plain dicts, so nothing is -imported or instantiated and the code that receives the dict decides what to do -with it. The denylist still applies when ``validate_subclass_spec_in_any=True``, -since validating a spec requires importing the class it names. +:ref:`sub-classes`. By default ``instantiate_subclass_spec_in_any`` is +``False``, so these values are kept as plain dicts, nothing is imported or +instantiated and the code that receives the dict decides what to do with it. +The denylist still applies when ``validate_subclass_spec_in_any=True``, since +validating a spec requires importing the class it names. .. note:: @@ -2299,14 +2296,6 @@ since validating a spec requires importing the class it names. into the config, and the resolvers that the application registers are equally reachable. Avoid these parser modes for untrusted configs. -.. note:: - - Until v5.0.0 a denied import path only gives a deprecation warning and the - import proceeds, so that existing configs don't break. Giving a value to - ``import_path_denylist`` or ``import_path_allowlist``, an empty list - included, makes denied import paths fail instead. From v5.0.0 they always - fail. - .. _sub-config-files: diff --git a/jsonargparse/__init__.py b/jsonargparse/__init__.py index b7c34bbe..3aaf7419 100644 --- a/jsonargparse/__init__.py +++ b/jsonargparse/__init__.py @@ -14,7 +14,6 @@ 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 @@ -43,7 +42,6 @@ _cli, _common, _core, - _deprecated, _formatters, _from_config, _instantiation, @@ -68,7 +66,6 @@ __all__ += _instantiation.__all__ __all__ += _loaders_dumpers.__all__ __all__ += _util.__all__ -__all__ += _deprecated.__all__ __version__ = "4.52.0" diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py index 33ff1d44..b9b9d976 100644 --- a/jsonargparse/_actions.py +++ b/jsonargparse/_actions.py @@ -1,7 +1,6 @@ """Collection of useful actions to define arguments.""" import inspect -import os import re import sys from argparse import SUPPRESS, _HelpAction, _VersionAction @@ -118,21 +117,6 @@ 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 - elif ( - container._print_config == "--print_config" - and action.dest != "config" - and os.getenv("JSONARGPARSE_DEPRECATION_WARNINGS", "").lower() == "all" - ): - from ._deprecated import deprecation_warning - - deprecation_warning( - "print_config_default_name", - "From v5.0.0 the print config argument will by default reuse the name of the config " - 'argument as "--print_%s". The current default is always "--print_config", but in v5.0.0 ' - f'with a config argument named "{action.dest}" it will become "--print_{action.dest}". ' - 'To keep the current name set print_config="--print_config" explicitly.', - stacklevel=2, - ) assert container._print_config.startswith("--") container.add_argument(container._print_config, action=_ActionPrintConfig) @@ -204,10 +188,8 @@ def __init__( ) 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 + valid_flags = {"": None, "skip_default": "skip_default", "skip_unset": "skip_unset"} if ruamel_support: valid_flags["comments"] = "with_comments" flags = value[0].split(",") @@ -215,11 +197,7 @@ def __call__(self, parser, namespace, value, option_string=None): 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 + kwargs[valid_flags[flag]] = True while hasattr(parser, "parent_parser"): kwargs["key"] = parser.subcommand if kwargs["key"] is None else parser.subcommand + "." + kwargs["key"] parser = parser.parent_parser diff --git a/jsonargparse/_cli.py b/jsonargparse/_cli.py index 7976ae60..cfd45f91 100644 --- a/jsonargparse/_cli.py +++ b/jsonargparse/_cli.py @@ -6,7 +6,6 @@ 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 ._signatures import FailUntyped @@ -22,11 +21,11 @@ def CLI(*args, **kwargs): """Alias of :func:`auto_cli`.""" - return auto_cli(*args, _stacklevel=3, **kwargs) + return auto_cli(*args, **kwargs) def auto_cli( - components: ComponentsType = None, + components: ComponentsType, args: list[str] | None = None, config_help: str = default_config_option_help, set_defaults: dict[str, Any] | None = None, @@ -65,12 +64,6 @@ def auto_cli( 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] @@ -95,9 +88,6 @@ def auto_cli( _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) @@ -109,9 +99,6 @@ def auto_cli( 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) diff --git a/jsonargparse/_common.py b/jsonargparse/_common.py index 883aa165..782f92c7 100644 --- a/jsonargparse/_common.py +++ b/jsonargparse/_common.py @@ -9,7 +9,6 @@ from contextvars import ContextVar from typing import ( # type: ignore[attr-defined] Generic, - Protocol, TypeVar, _GenericAlias, ) @@ -68,14 +67,6 @@ def __bool__(self): 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) @@ -84,7 +75,6 @@ def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType: 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) @@ -99,7 +89,6 @@ def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType: "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, @@ -280,14 +269,13 @@ class ImportDenied(ImportError, ValueError): parsing_settings: dict = { "validate_defaults": False, "validate_subclass_spec_in_any": False, - "instantiate_subclass_spec_in_any": None, # v5.0.0: change default to False + "instantiate_subclass_spec_in_any": 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, "import_path_verdicts": {entry: False for entry in default_import_path_denylist}, - "import_paths_enforced": False, # v5.0.0: change default to True } @@ -321,7 +309,6 @@ def set_import_path_verdicts(denylist: list[str] | None, allowlist: list[str] | else: logger.debug(f"Import path {entry!r} changed to {state}") parsing_settings["import_path_verdicts"] = verdicts - parsing_settings["import_paths_enforced"] = True def denying_import_path_entry(path: str) -> str | None: @@ -344,11 +331,7 @@ def check_import_path(path: str) -> None: f"Importing '{path}' is not allowed, denied by the {entry!r} entry of the import path denylist. " "Add the import path to import_path_allowlist in set_parsing_settings to allow it." ) - if get_parsing_setting("import_paths_enforced"): - raise ImportDenied(message) - from ._deprecated import import_paths_not_enforced - - import_paths_not_enforced(path, message) + raise ImportDenied(message) def get_env_var_bool(name: str) -> bool: @@ -396,12 +379,10 @@ def set_parsing_settings( instantiate_subclass_spec_in_any: Whether ``instantiate`` builds the class when a value for a type that accepts any value, i.e. ``Any``, ``object`` or ``Unvalidated<...>``, is a valid subclass spec. If - ``False``, the value is kept as a subclass spec, which the code that - receives it can instantiate itself if desired. Currently the default - is ``True`` and a deprecation warning is emitted, since from v5.0.0 - the default will be ``False``. Enabling it is discouraged because it - means that any class can be instantiated, so only do it for trusted - configs. + ``False``, the default, the value is kept as a subclass spec, which + the code that receives it can instantiate itself if desired. + Enabling it is discouraged because it means that any class can be + instantiated, so only do it for trusted configs. 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 @@ -779,11 +760,6 @@ def logger(self) -> logging.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__) diff --git a/jsonargparse/_completions.py b/jsonargparse/_completions.py index 2987b45b..52da2201 100644 --- a/jsonargparse/_completions.py +++ b/jsonargparse/_completions.py @@ -13,7 +13,7 @@ from subprocess import PIPE, Popen from typing import Literal, Union -from ._actions import ActionConfigFile, ActionFail, _ActionConfigLoad, _ActionHelpClassPath, remove_actions +from ._actions import ActionConfigFile, _ActionConfigLoad, _ActionHelpClassPath, remove_actions from ._common import ( NonParsingAction, get_optionals_as_positionals_actions, @@ -47,17 +47,7 @@ def add_print_completion_argument(parser): if getattr(parser, "parent_parser", None): 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: + if print_completion_argument and "--print_completion" not in parser._option_string_actions: parser.add_argument("--print_completion", action=PrintCompletionAction) @@ -197,9 +187,6 @@ def norm_name(name: str) -> str: 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) diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 05115844..1bb1c621 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -37,13 +37,6 @@ 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 @@ -139,11 +132,6 @@ def add_argument(self, *args, sub_configs: bool = False, **kwargs): 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"]): @@ -253,7 +241,7 @@ class ArgumentGroup(ActionsContainer, argparse._ArgumentGroup): parser: "ArgumentParser | ActionsContainer | None" = None -class ArgumentParser(ParserDeprecations, ActionsContainer, argparse.ArgumentParser): +class ArgumentParser(ActionsContainer, argparse.ArgumentParser): """Parser for command line, configuration files and environment variables.""" formatter_class: type[argparse.HelpFormatter] @@ -268,7 +256,7 @@ def __init__( formatter_class: type[argparse.HelpFormatter] = DefaultHelpFormatter, logger: logging.Logger | bool | str | dict = False, version: str | None = None, - print_config: str | None = "--print_config", + print_config: str | None = "--print_%s", parser_mode: str = "yaml" if pyyaml_available else "json", dump_header: list[str] | None = None, default_config_files: list[str | os.PathLike] | None = None, @@ -508,7 +496,6 @@ def parse_args( # type: ignore[override] 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], @@ -638,7 +625,6 @@ def parse_env( 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, @@ -676,7 +662,6 @@ def parse_path( 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, @@ -792,7 +777,6 @@ def add_subcommands(self, required: bool = True, dest: str = "subcommand", **kwa ## Methods for serializing config objects ## - @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def dump( self, namespace: Namespace, @@ -802,7 +786,6 @@ def dump( skip_validation: bool = False, with_comments: bool = False, skip_link_targets: bool = True, - **kwargs, ) -> str: """Generates a serialized string for the given configuration object. @@ -822,12 +805,6 @@ def dump( 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) @@ -911,7 +888,6 @@ def _dump_delete_default_entries(self, subcfg, subdefaults): 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, @@ -922,7 +898,6 @@ def save( overwrite: bool = False, multifile: bool = True, branch: str | None = None, - **kwargs, ) -> None: """Writes to file(s) the given configuration object using the chosen format. @@ -939,17 +914,13 @@ def save( 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} + dump_kwargs: dict = {"format": format, "skip_unset": skip_unset, "skip_validation": skip_validation} if fsspec_support: try: @@ -1066,7 +1037,7 @@ def check_suppressed_default(): check_suppressed_default() return defaults.get(action.dest) - def get_defaults(self, skip_validation: bool = False, **kwargs) -> Namespace: + def get_defaults(self, skip_validation: bool = False) -> Namespace: """Returns a namespace with all default values. Args: @@ -1075,7 +1046,6 @@ def get_defaults(self, skip_validation: bool = False, **kwargs) -> Namespace: 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 ( @@ -1158,8 +1128,6 @@ def get_completion_script(self, completion_type: str, **kwargs) -> str: 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(): @@ -1179,7 +1147,6 @@ def error(self, message: str, ex: Exception | None = None) -> NoReturn: sys.stderr.write(f"error: {message}\n") self.exit(2) - @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def validate( self, namespace: Namespace, @@ -1200,7 +1167,6 @@ def validate( 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): @@ -1269,9 +1235,8 @@ def 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) + instantiate = InstantiateMethod.instantiate - @renamed_parameter_warning({"cfg": "namespace"}, stacklevel=2) def strip_unknown(self, namespace: Namespace) -> Namespace: """Removes all unknown keys from a configuration object. @@ -1293,7 +1258,6 @@ def strip_unknown(self, namespace: Namespace) -> Namespace: 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. @@ -1544,15 +1508,7 @@ def env_prefix(self) -> bool | str: @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: + if 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.") @@ -1611,9 +1567,3 @@ def parse_known_args(self, *args, **kwargs) -> NoReturn: 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() diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py deleted file mode 100644 index 3105ef28..00000000 --- a/jsonargparse/_deprecated.py +++ /dev/null @@ -1,1131 +0,0 @@ -"""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, - ) - - -instantiate_subclass_spec_in_any_message = """ - Instantiating a subclass spec given as value for a type that accepts any value, i.e. ``Any``, - ``object`` or ``Unvalidated<...>``, was deprecated in v4.51.0. From v5.0.0 these values will no - longer be instantiated, the subclass spec being kept as is, so that the code that receives it - decides whether to instantiate it. Set ``instantiate_subclass_spec_in_any=False`` in - ``set_parsing_settings`` to get the future behavior now and silence this warning. Setting it to - ``True`` keeps the current behavior, but it is discouraged since it means that a config is able - to instantiate any class, which can be a security risk. -""" - - -def unset_instantiate_subclass_spec_in_any() -> bool: - """Value used for the ``instantiate_subclass_spec_in_any`` setting when it is unset. - - Remove in v5.0.0, changing the default of the setting from ``None`` to - ``False``, and thus making ``_typehints.instantiate_subclass_spec_in_any`` - only get the setting. - """ - deprecation_warning("instantiate_subclass_spec_in_any", instantiate_subclass_spec_in_any_message) - return True - - -def import_paths_not_enforced(path: str, message: str) -> None: - """Warns instead of failing when an import path given as a value is denied. - - Remove in v5.0.0, changing the default of the ``import_paths_enforced`` - setting from ``False`` to ``True``, and thus making - ``_common.check_import_path`` always raise. - """ - deprecation_warning( - f"import_path_denied:{path}", - f""" - {message} Currently this only warns, but from v5.0.0 it will fail. To get the future - behavior now and silence this warning, call set_parsing_settings with - import_path_denylist and/or import_path_allowlist. - """, - ) - - -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 diff --git a/jsonargparse/_formatters.py b/jsonargparse/_formatters.py index c291c7ce..11ed703f 100644 --- a/jsonargparse/_formatters.py +++ b/jsonargparse/_formatters.py @@ -29,7 +29,6 @@ parent_parser, supports_optionals_as_positionals, ) -from ._deprecated import HelpFormatterDeprecations from ._link_arguments import ActionLink from ._namespace import Namespace from ._optionals import import_ruamel @@ -312,7 +311,7 @@ def set_yaml_argument_comment( cfg.yaml_set_comment_before_after_key(key, before="\n" + text, indent=2 * depth) -class DefaultHelpFormatter(HelpFormatterDeprecations, HelpFormatter): +class DefaultHelpFormatter(HelpFormatter): """Help message formatter that includes types, default values and env var names. This class is an extension of `argparse.HelpFormatter diff --git a/jsonargparse/_instantiation.py b/jsonargparse/_instantiation.py index 8828cb44..fd9d346e 100644 --- a/jsonargparse/_instantiation.py +++ b/jsonargparse/_instantiation.py @@ -1,20 +1,20 @@ import inspect +from typing import Protocol -from ._common import ( - ClassType, - InstantiatorCallable, - InstantiatorsDictType, - applied_instantiation_links, - class_instantiators, - get_parsing_setting, - is_subclass, - parser_context, -) +from ._common import ClassType, applied_instantiation_links, 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 InstantiatorCallable(Protocol): + def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType: + pass # pragma: no cover + + +InstantiatorsDictType = dict[tuple[type, bool], InstantiatorCallable] + +_class_instantiators: InstantiatorsDictType = {} class InstantiateMethod: @@ -98,14 +98,12 @@ def instantiate( 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) @@ -146,72 +144,25 @@ def add_instantiator( 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} + items = {k: v for k, v in _class_instantiators.items() if k != key} if prepend: - registry.clear() - registry.update({key: instantiator, **items}) + _class_instantiators.clear() + _class_instantiators.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: + _class_instantiators.clear() + _class_instantiators.update(items) + + +def dynamic_class_instantiator(class_type: type[ClassType], *args, **kwargs) -> ClassType: + for (cls, subclasses), instantiator in _class_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 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 diff --git a/jsonargparse/_jsonnet.py b/jsonargparse/_jsonnet.py index 15ad2c1f..242f6014 100644 --- a/jsonargparse/_jsonnet.py +++ b/jsonargparse/_jsonnet.py @@ -162,7 +162,7 @@ def parse( except TypeError: pass else: - fname = jsonnet(absolute=False) if isinstance(jsonnet, Path) else jsonnet + fname = jsonnet(absolute=False) if isinstance(jsonnet, Path) else jsonnet # type: ignore[operator] snippet = fpath.read_text() try: with parser_context(load_value_mode="yaml" if pyyaml_available else "json"): diff --git a/jsonargparse/_jsonschema.py b/jsonargparse/_jsonschema.py index a0180362..81eb06be 100644 --- a/jsonargparse/_jsonschema.py +++ b/jsonargparse/_jsonschema.py @@ -31,11 +31,6 @@ def __init__(self, schema: str | dict | None = None, sub_config: bool = True, wi 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" diff --git a/jsonargparse/_loaders_dumpers.py b/jsonargparse/_loaders_dumpers.py index ccc89b8f..e2f837ac 100644 --- a/jsonargparse/_loaders_dumpers.py +++ b/jsonargparse/_loaders_dumpers.py @@ -243,8 +243,6 @@ def replace_unset(data): 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 diff --git a/jsonargparse/_namespace.py b/jsonargparse/_namespace.py index 90af3dd8..1c6e1df1 100644 --- a/jsonargparse/_namespace.py +++ b/jsonargparse/_namespace.py @@ -275,13 +275,9 @@ def del_clash_mark(key: str) -> str: 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) + for key, val in data.items(): + if isinstance(val, dict) and all(isinstance(k, str) for k in val): + data[key] = expand_dict(val) return Namespace(**data) @@ -305,12 +301,3 @@ def get_non_meta_sorted_keys(namespace: Namespace) -> list[str]: 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())) diff --git a/jsonargparse/_paths.py b/jsonargparse/_paths.py index c1bc8573..faabc9cc 100644 --- a/jsonargparse/_paths.py +++ b/jsonargparse/_paths.py @@ -10,7 +10,6 @@ from io import StringIO from typing import IO, Any -from ._deprecated import PathDeprecations from ._optionals import ( fsspec_support, import_fsspec, @@ -85,7 +84,7 @@ class PathError(TypeError): """Exception raised for errors in the Path class.""" -class Path(PathDeprecations): +class Path: """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 @@ -116,7 +115,6 @@ def __init__( path: "str | os.PathLike | Path", mode: str = "fr", cwd: str | os.PathLike | None = None, - **kwargs, ): """Initializer for Path instance. @@ -129,7 +127,6 @@ def __init__( 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 @@ -173,14 +170,14 @@ def __init__( else: raise PathError("Expected path to be a string, os.PathLike or a Path object.") - if not self._skip_check and is_url: + if 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: + elif is_fsspec: fsspec_mode = "".join(c for c in mode if c in {"r", "w"}) if fsspec_mode: fsspec = import_fsspec("Path") @@ -192,7 +189,7 @@ def __init__( 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: + elif 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, "..")) @@ -256,6 +253,10 @@ def absolute(self) -> str: def mode(self) -> str: return self._mode + @property + def cwd(self) -> str: + return self._cwd + @property def is_url(self) -> bool: return self._is_url @@ -269,7 +270,6 @@ def __str__(self): def __repr__(self): name = "Path_" + self._mode - name = self._repr_skip_check(name) cwd = "" if self._relative != self._absolute: cwd = ", cwd=" + self._cwd diff --git a/jsonargparse/_signatures.py b/jsonargparse/_signatures.py index eae2b9e8..b1955a6a 100644 --- a/jsonargparse/_signatures.py +++ b/jsonargparse/_signatures.py @@ -2,7 +2,6 @@ import dataclasses import inspect -import os import re from argparse import SUPPRESS, ArgumentParser from collections.abc import Callable @@ -12,14 +11,12 @@ 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 ._instantiation import dynamic_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 @@ -58,7 +55,6 @@ def validate_fail_untyped(fail_untyped) -> None: 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, @@ -150,7 +146,6 @@ def add_class_arguments( return added_args - @renamed_parameter_warning({"theclass": "class_type", "themethod": "method_name"}) def add_method_arguments( self, class_type: type, @@ -383,23 +378,12 @@ def _add_signature_parameter( annotation = unvalidatable_replaced 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 - self.logger.debug( - f'Parameter "{name}" from "{src}" is NotRequired and does not have a default, ' - "so it is not included in the parsed namespace unless given." - ) + if default == inspect_empty and get_typehint_origin(annotation) in not_required_types: + default = SUPPRESS + self.logger.debug( + f'Parameter "{name}" from "{src}" is NotRequired and does not have a default, ' + "so it is not included in the parsed namespace unless given." + ) # Determine argument characteristics based on parameter kind and default value if kind == kinds.POSITIONAL_ONLY: is_required = True # Always required @@ -418,17 +402,7 @@ def _add_signature_parameter( # are meant to agree with the requiredness that the signature itself defines. annotation = strip_required_typehint(annotation, is_required, f'parameter "{name}" from "{src}"') if is_required and annotation == inspect_empty and fail_untyped is False: - if 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 Untyped but the " - "parameter will remain required.", - stacklevel=4, - ) annotation = Untyped - default = None - is_required = False is_required_link_target = False if is_required and linked_targets is not None and name in linked_targets: default = None @@ -677,8 +651,7 @@ def group_instantiate_class(group, cfg): value = {} parent = cfg key = group.dest - instantiator_fn = get_class_instantiator() - parent[key] = instantiator_fn(group.group_class, **value) + parent[key] = dynamic_class_instantiator(group.group_class, **value) def strip_title(value): diff --git a/jsonargparse/_subcommands.py b/jsonargparse/_subcommands.py index 3714cd13..4f4095da 100644 --- a/jsonargparse/_subcommands.py +++ b/jsonargparse/_subcommands.py @@ -8,7 +8,6 @@ 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 @@ -130,7 +129,6 @@ def add_subcommand(self, name: str, parser: ArgumentParser, **kwargs) -> Argumen 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 @@ -199,8 +197,10 @@ def get_subcommands( 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 + raise ValueError( + f"Multiple subcommand settings ({', '.join(subcommand_keys)}) without providing an " + f"explicit '{dest}' key." + ) # Remove extra subcommand settings if subcommand and len(subcommand_keys) > 1: diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index 80831bc3..be1c180a 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -74,7 +74,7 @@ parser_context, validating_defaults, ) -from ._instantiation import get_class_instantiator +from ._instantiation import dynamic_class_instantiator from ._loaders_dumpers import ( basic_json_or_yaml_load, get_loader_exceptions, @@ -2518,18 +2518,17 @@ def adapt_class_type( value["init_args"] = init_args return value - instantiator_fn = get_class_instantiator() # only the top level keys, since a value can be a namespace, e.g. a subclass spec # kept as is for an Any typed parameter, which must not be expanded into kwargs init_kwargs = dict(init_args.items(branches=True, nested=False)) if partial_skip_args is not None: # an empty set for a factory that takes no arguments return partial( - instantiator_fn, + dynamic_class_instantiator, val_class, **{**init_kwargs, **dict_kwargs}, ) - return instantiator_fn(val_class, **{**init_kwargs, **dict_kwargs}) + return dynamic_class_instantiator(val_class, **{**init_kwargs, **dict_kwargs}) prev_init_args = prev_val.get("init_args") if isinstance(prev_val, Namespace) else None @@ -2606,19 +2605,9 @@ def subclasses_disabled_remove_class_path(value): return value -def instantiate_subclass_spec_in_any() -> bool: - """Whether a subclass spec given as value for a type that accepts any value is instantiated.""" - setting = get_parsing_setting("instantiate_subclass_spec_in_any") - if setting is None: # remove in v5.0.0, when the setting default becomes False - from ._deprecated import unset_instantiate_subclass_spec_in_any - - setting = unset_instantiate_subclass_spec_in_any() - return setting - - def adapt_classes_any(val, typehint, serialize, instantiate_classes, sub_add_kwargs, logger=None): if is_subclass_spec(val): - if instantiate_classes and not instantiate_subclass_spec_in_any(): + if instantiate_classes and not get_parsing_setting("instantiate_subclass_spec_in_any"): return val orig_val = val val = subclass_spec_as_namespace(val) diff --git a/jsonargparse/typing.py b/jsonargparse/typing.py index 184d13b1..0a1a550b 100644 --- a/jsonargparse/typing.py +++ b/jsonargparse/typing.py @@ -10,11 +10,10 @@ from typing import Any, TypeAlias, get_type_hints from ._common import ClassType, get_settings_logger, 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 +from ._util import ClassFromFunctionBase, get_import_path, import_object __all__ = [ "final", @@ -390,7 +389,7 @@ def _serialize_path(path: Path): return str(path) -def path_type(mode: str, docstring: str | None = None, **kwargs) -> TypeAlias: +def path_type(mode: str, docstring: str | None = None) -> TypeAlias: """Creates or returns an already registered path type class. Args: @@ -404,14 +403,6 @@ def path_type(mode: str, docstring: str | None = None, **kwargs) -> TypeAlias: 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] @@ -419,15 +410,14 @@ def path_type(mode: str, docstring: str | None = None, **kwargs) -> TypeAlias: 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) + super().__init__(v["relative"], mode=self._mode, **k) else: - super().__init__(v, mode=self._mode, skip_check=self._skip_check, **k) + super().__init__(v, mode=self._mode, **k) restricted_type = type(name, (PathType,), {"__doc__": docstring}) add_type(restricted_type, register_key, type_check=_is_path_type) @@ -473,12 +463,9 @@ def deserializer(self, value): def get_registrant_module() -> str: """Returns the name of the module that called the caller of this function.""" frame: Any = sys._getframe(2) - while frame.f_globals.get("__name__") == "jsonargparse._deprecated": # skip the deprecation decorator - frame = frame.f_back return frame.f_globals.get("__name__", "unknown") -@renamed_parameter_warning({"type_class": "class_type"}) def register_type( class_type: _TypeClass, serializer: Callable = str, @@ -558,9 +545,9 @@ def add_type(class_type: type, uniqueness_key: tuple | None, type_check: Callabl 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} + kwargs: dict = {"uniqueness_key": uniqueness_key} if type_check is not None: - kwargs["type_check"] = type_check # type: ignore[assignment] + kwargs["type_check"] = type_check register_type(class_type, class_type._type, **kwargs) # type: ignore[attr-defined] diff --git a/jsonargparse_tests/__main__.py b/jsonargparse_tests/__main__.py index f72776f3..49d136d4 100644 --- a/jsonargparse_tests/__main__.py +++ b/jsonargparse_tests/__main__.py @@ -1,5 +1,4 @@ """Run all unit tests in package.""" -# pragma: no cover import os import sys diff --git a/jsonargparse_tests/argparse_tests_generate.py b/jsonargparse_tests/argparse_tests_generate.py index 09f227a6..c3fbb92e 100755 --- a/jsonargparse_tests/argparse_tests_generate.py +++ b/jsonargparse_tests/argparse_tests_generate.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# pragma: no cover """Generate argparse compatibility tests from CPython's test_argparse.py. This script downloads the test_argparse.py file from the CPython repository, diff --git a/jsonargparse_tests/conftest.py b/jsonargparse_tests/conftest.py index a13b5620..458339fc 100644 --- a/jsonargparse_tests/conftest.py +++ b/jsonargparse_tests/conftest.py @@ -148,11 +148,11 @@ def subsubparser() -> ArgumentParser: @pytest.fixture def clear_instantiators(): - from jsonargparse._instantiation import _global_class_instantiators + from jsonargparse._instantiation import _class_instantiators - _global_class_instantiators.clear() + _class_instantiators.clear() yield - _global_class_instantiators.clear() + _class_instantiators.clear() @pytest.fixture @@ -169,7 +169,7 @@ def example_parser() -> ArgumentParser: @pytest.fixture def print_parser(parser, subparser) -> ArgumentParser: parser.description = "cli tool" - parser.add_argument("--cfg", action="config") + parser.add_argument("--config", action="config") parser.add_argument("--v0", help=SUPPRESS, default="0") parser.add_argument("--v1", help="Option v1.", default=1) parser.add_argument("--g1.v2", help="Option v2.", default="2") diff --git a/jsonargparse_tests/test_cli.py b/jsonargparse_tests/test_cli.py index a835aa64..1b781983 100644 --- a/jsonargparse_tests/test_cli.py +++ b/jsonargparse_tests/test_cli.py @@ -81,6 +81,10 @@ def test_single_function_return(cli_fn): assert 1.2 == cli_fn(single_function, args=["1.2"]) +def test_single_function_in_list_no_subcommand(): + assert 1.2 == auto_cli([single_function], args=["1.2"]) + + def test_single_function_set_defaults(): def run_cli(): auto_cli(single_function, set_defaults={"a1": 3.4}) diff --git a/jsonargparse_tests/test_core.py b/jsonargparse_tests/test_core.py index a6e0fb7f..54c02194 100644 --- a/jsonargparse_tests/test_core.py +++ b/jsonargparse_tests/test_core.py @@ -604,13 +604,6 @@ def test_dump_skip_validation(parser): assert "-" in dump -def test_dump_unexpected_kwarg(parser): - parser.add_argument("--key", type=int) - cfg = Namespace(key="-") - with pytest.raises(ValueError, match="Unexpected keyword parameter"): - parser.dump(cfg, unexpected=True) - - @skip_if_no_pyyaml def test_dump_order(parser, subtests): args = {} diff --git a/jsonargparse_tests/test_dataclasses.py b/jsonargparse_tests/test_dataclasses.py index 8de2cd26..8c865d5f 100644 --- a/jsonargparse_tests/test_dataclasses.py +++ b/jsonargparse_tests/test_dataclasses.py @@ -222,8 +222,10 @@ class OptionalWithDefault: def test_add_class_optional_without_default(parser): parser.add_class_arguments(OptionalWithDefault) assert parser.get_defaults() == Namespace(param=None) - assert parser.parse_args([]) == Namespace(param=None) - assert parser.parse_args(["--param=null"]) == Namespace(param=None) + with pytest.raises(ArgumentError, match="the following arguments are required: param"): + parser.parse_args([]) + with pytest.raises(ArgumentError, match="the following arguments are required: param"): + parser.parse_args(["--param=null"]) @dataclasses.dataclass diff --git a/jsonargparse_tests/test_deprecated.py b/jsonargparse_tests/test_deprecated.py deleted file mode 100644 index f4a0b8c0..00000000 --- a/jsonargparse_tests/test_deprecated.py +++ /dev/null @@ -1,1494 +0,0 @@ -from __future__ import annotations - -import dataclasses -import json -import os -import pathlib -import sys -from calendar import Calendar -from contextlib import contextmanager, redirect_stderr, redirect_stdout -from enum import Enum -from importlib import import_module -from inspect import getmodule as inspect_getmodule -from io import StringIO -from types import ModuleType -from typing import Any, Optional -from unittest.mock import patch -from warnings import catch_warnings - -import pytest - -from jsonargparse import ( - CLI, - ActionJsonnet, - ActionJsonSchema, - ArgumentError, - ArgumentParser, - Namespace, - auto_cli, - compose_dataclasses, - get_config_read_mode, - set_config_read_mode, - set_docstring_parse_options, - set_parsing_settings, - set_url_support, -) -from jsonargparse._common import ImportDenied -from jsonargparse._deprecated import ( - ActionEnum, - ActionJsonnetExtVars, - ActionOperators, - ActionPath, - ActionPathList, - LoggerProperty, - ParserError, - deprecation_warning, - dict_to_namespace, - namespace_to_dict, - shown_deprecation_warnings, - strip_meta, - usage_and_exit_error_handler, -) -from jsonargparse._formatters import DefaultHelpFormatter -from jsonargparse._optionals import ( - docstring_parser_support, - get_docstring_parse_options, - import_ruamel, - jsonnet_support, - pyyaml_available, - ruamel_support, - url_support, -) -from jsonargparse._util import argument_error, import_object -from jsonargparse.typing import Path -from jsonargparse_tests.conftest import ( - get_parser_help, - is_posix, - patch_parsing_settings, - responses_activate, - skip_if_docstring_parser_unavailable, - skip_if_fsspec_unavailable, - skip_if_jsonschema_unavailable, - skip_if_requests_unavailable, - skip_if_responses_unavailable, -) -from jsonargparse_tests.test_dataclasses import DataClassA -from jsonargparse_tests.test_jsonnet import example_2_jsonnet -from jsonargparse_tests.test_paths import paths # noqa: F401 -from jsonargparse_tests.test_signatures import WithMethods -from jsonargparse_tests.test_subclasses import CustomInstantiationBase, instantiator -from jsonargparse_tests.test_subcommands import subcommands_parser # noqa: F401 - - -@pytest.fixture(autouse=True, scope="module") -def no_pyyaml_skip(): - if not pyyaml_available: - pytest.skip("pyyaml package is required") - - -@pytest.fixture(autouse=True) -def clear_shown_deprecation_warnings(): - yield - shown_deprecation_warnings.clear() - - -@contextmanager -def suppress_stderr(): - with open(os.devnull, "w") as fnull: - with redirect_stderr(fnull): - yield - - -source = pathlib.Path(__file__).read_text().splitlines() - - -def assert_deprecation_warn(warns, message, code): - assert message in str(warns[-1].message) - if code is None: - return # pragma: no cover - assert pathlib.Path(warns[-1].filename).name == pathlib.Path(__file__).name - assert code in source[warns[-1].lineno - 1] - - -def test_deprecation_warning(): - with catch_warnings(record=True) as w: - message = "Deprecation warning" - deprecation_warning(None, message) - assert 2 == len(w) - assert "only one JsonargparseDeprecationWarning per type is shown" in str(w[0].message) - assert message == str(w[1].message).strip() - - -class MyEnum(Enum): - A = 1 - B = 2 - C = 3 - - -def func(a1: MyEnum = MyEnum["A"]): - return a1 # pragma: no cover - - -def test_ActionEnum(): - parser = ArgumentParser(exit_on_error=False) - with catch_warnings(record=True) as w: - action = ActionEnum(enum=MyEnum) - assert_deprecation_warn( - w, - message="ActionEnum was deprecated", - code="ActionEnum(enum=MyEnum)", - ) - parser.add_argument("--enum", action=action, default=MyEnum.C, help="Description") - - for val in ["A", "B", "C"]: - assert MyEnum[val] == parser.parse_args(["--enum=" + val]).enum - for val in ["X", "b", 2]: - pytest.raises(ArgumentError, lambda: parser.parse_args(["--enum=" + str(val)])) - - cfg = parser.parse_args(["--enum=C"]).clone(with_meta=False) - assert "enum: C\n" == parser.dump(cfg) - - help_str = get_parser_help(parser) - assert "Description (type: MyEnum, default: C)" in help_str - - parser = ArgumentParser() - parser.add_function_arguments(func) - assert MyEnum["A"] == parser.get_defaults().a1 - assert MyEnum["B"] == parser.parse_args(["--a1=B"]).a1 - - pytest.raises(ValueError, ActionEnum) - pytest.raises(ValueError, lambda: ActionEnum(enum=object)) - pytest.raises(ValueError, lambda: parser.add_argument("--bad1", type=MyEnum, action=True)) - pytest.raises(ValueError, lambda: parser.add_argument("--bad2", type=float, action=action)) - - -def test_ActionOperators(): - parser = ArgumentParser(prog="app", exit_on_error=False) - with catch_warnings(record=True) as w: - parser.add_argument("--le0", action=ActionOperators(expr=("<", 0))) - assert_deprecation_warn( - w, - message="ActionOperators was deprecated", - code='ActionOperators(expr=("<", 0))', - ) - parser.add_argument( - "--gt1.a.le4", - action=ActionOperators(expr=[(">", 1.0), ("<=", 4.0)], join="and", type=float), - ) - parser.add_argument( - "--lt5.o.ge10.o.eq7", - action=ActionOperators(expr=[("<", 5), (">=", 10), ("==", 7)], join="or", type=int), - ) - parser.add_argument("--ge0", nargs=3, action=ActionOperators(expr=(">=", 0))) - - assert 1.5 == parser.parse_args(["--gt1.a.le4", "1.5"]).gt1.a.le4 - assert 4.0 == parser.parse_args(["--gt1.a.le4", "4.0"]).gt1.a.le4 - pytest.raises(ArgumentError, lambda: parser.parse_args(["--gt1.a.le4", "1.0"])) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--gt1.a.le4", "5.5"])) - - assert 1.5 == parser.parse_string("gt1:\n a:\n le4: 1.5").gt1.a.le4 - assert 4.0 == parser.parse_string("gt1:\n a:\n le4: 4.0").gt1.a.le4 - pytest.raises(ArgumentError, lambda: parser.parse_string("gt1:\n a:\n le4: 1.0")) - pytest.raises(ArgumentError, lambda: parser.parse_string("gt1:\n a:\n le4: 5.5")) - - assert 1.5 == parser.parse_env({"APP_GT1__A__LE4": "1.5"}).gt1.a.le4 - assert 4.0 == parser.parse_env({"APP_GT1__A__LE4": "4.0"}).gt1.a.le4 - pytest.raises(ArgumentError, lambda: parser.parse_env({"APP_GT1__A__LE4": "1.0"})) - pytest.raises(ArgumentError, lambda: parser.parse_env({"APP_GT1__A__LE4": "5.5"})) - - assert 2 == parser.parse_args(["--lt5.o.ge10.o.eq7", "2"]).lt5.o.ge10.o.eq7 - assert 7 == parser.parse_args(["--lt5.o.ge10.o.eq7", "7"]).lt5.o.ge10.o.eq7 - assert 10 == parser.parse_args(["--lt5.o.ge10.o.eq7", "10"]).lt5.o.ge10.o.eq7 - pytest.raises(ArgumentError, lambda: parser.parse_args(["--lt5.o.ge10.o.eq7", "5"])) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--lt5.o.ge10.o.eq7", "8"])) - - assert [0, 1, 2] == parser.parse_args(["--ge0", "0", "1", "2"]).ge0 - - pytest.raises(ValueError, lambda: parser.add_argument("--op1", action=ActionOperators)) - action = ActionOperators(expr=("<", 0)) - pytest.raises(ValueError, lambda: parser.add_argument("--op2", type=float, action=action)) - pytest.raises(ValueError, lambda: parser.add_argument("--op3", nargs=0, action=action)) - pytest.raises(ValueError, ActionOperators) - pytest.raises(ValueError, lambda: ActionOperators(expr="<")) - pytest.raises(ValueError, lambda: ActionOperators(expr=[("<", 5), (">=", 10)], join="xor")) - - -@skip_if_requests_unavailable -def test_url_support_true(): - with catch_warnings(record=True) as w: - assert "fr" == get_config_read_mode() - assert_deprecation_warn( - w, - message="get_config_read_mode was deprecated", - code="get_config_read_mode()", - ) - with catch_warnings(record=True) as w: - set_url_support(True) - assert_deprecation_warn( - w, - message="set_url_support was deprecated", - code="set_url_support(True)", - ) - assert "fur" == get_config_read_mode() - set_url_support(False) - assert "fr" == get_config_read_mode() - - -@pytest.mark.skipif(url_support, reason="requests package should not be installed") -def test_url_support_false(): - with catch_warnings(record=True) as w: - assert "fr" == get_config_read_mode() - assert_deprecation_warn( - w, - message="get_config_read_mode was deprecated", - code="get_config_read_mode()", - ) - with catch_warnings(record=True) as w: - with pytest.raises(ImportError): - set_url_support(True) - assert "set_url_support was deprecated" in str(w[-1].message) - assert "fr" == get_config_read_mode() - set_url_support(False) - assert "fr" == get_config_read_mode() - - -@skip_if_fsspec_unavailable -def test_set_config_read_mode(): - with catch_warnings(record=True) as w: - assert "fr" == get_config_read_mode() - assert_deprecation_warn( - w, - message="get_config_read_mode was deprecated", - code="get_config_read_mode()", - ) - with catch_warnings(record=True) as w: - set_config_read_mode(fsspec_enabled=True) - assert_deprecation_warn( - w, - message="set_config_read_mode was deprecated", - code="set_config_read_mode(fsspec_enabled=True)", - ) - assert "fsr" == get_config_read_mode() - set_config_read_mode(fsspec_enabled=False) - assert "fr" == get_config_read_mode() - - -def test_instantiate_subclasses(): - parser = ArgumentParser(exit_on_error=False) - parser.add_argument("--cal", type=Calendar) - cfg = parser.parse_object({"cal": {"class_path": "calendar.Calendar"}}) - with catch_warnings(record=True) as w: - cfg_init = parser.instantiate_subclasses(cfg) - assert_deprecation_warn( - w, - message="instantiate_subclasses was deprecated", - code="parser.instantiate_subclasses(cfg)", - ) - assert isinstance(cfg_init["cal"], Calendar) - - -def test_instantiate_classes(): - parser = ArgumentParser(exit_on_error=False) - parser.add_argument("--cal", type=Calendar) - cfg = parser.parse_object({"cal": {"class_path": "calendar.Calendar"}}) - with catch_warnings(record=True) as w: - cfg_init = parser.instantiate_classes(cfg) - assert_deprecation_warn( - w, - message="``instantiate_classes`` was deprecated", - code="cfg_init = parser.instantiate_classes(cfg)", - ) - assert isinstance(cfg_init["cal"], Calendar) - - -def test_add_instantiator_method_deprecated(parser): - parser.add_argument("--cls", type=CustomInstantiationBase) - with catch_warnings(record=True) as w: - parser.add_instantiator(instantiator("custom"), CustomInstantiationBase) - assert_deprecation_warn( - w, - message="``ArgumentParser.add_instantiator`` was deprecated", - code='parser.add_instantiator(instantiator("custom"), CustomInstantiationBase)', - ) - cfg = parser.parse_args(["--cls=CustomInstantiationBase"]) - init = parser.instantiate(cfg) - assert isinstance(init.cls, CustomInstantiationBase) - assert init.cls.call == "custom" - - -def function(a1: float): - return a1 # pragma: no cover - - -def test_single_function_cli(): - with catch_warnings(record=True) as w: - parser = CLI(function, return_parser=True, set_defaults={"a1": 3.4}) - assert_deprecation_warn( - w, - message="return_parser parameter was deprecated", - code="CLI(function, return_parser=True,", - ) - assert isinstance(parser, ArgumentParser) - - -def cmd1(a1: int): - return a1 # pragma: no cover - - -def cmd2(a2: str = "X"): - return a2 # pragma: no cover - - -def test_multiple_functions_cli(): - with catch_warnings(record=True) as w: - parser = CLI([cmd1, cmd2], return_parser=True, set_defaults={"cmd2.a2": "Z"}) - assert_deprecation_warn( - w, - message="return_parser parameter was deprecated", - code="CLI([cmd1, cmd2], return_parser=True,", - ) - assert isinstance(parser, ArgumentParser) - - -@contextmanager -def mock_getmodule_locals(parent_fn, locals_list=[]): - module_name = "_" + parent_fn.__name__ - - mock_module = ModuleType(module_name) - for obj in locals_list + [CLI, auto_cli]: - setattr(mock_module, obj.__name__, obj) - sys.modules[module_name] = mock_module - - for obj in locals_list: - obj.__module__ = module_name - - def patched_getmodule(obj, *args): - if obj in locals_list or (parent_fn.__name__ in str(obj)): - return mock_module - return inspect_getmodule(obj, *args) - - with patch("inspect.getmodule", side_effect=patched_getmodule): - yield - del sys.modules[module_name] - - -@pytest.mark.parametrize("cli_fn", [CLI, auto_cli]) -def test_automatic_components_empty_context(cli_fn): - def empty_context(): - cli_fn() - - with mock_getmodule_locals(empty_context): - with pytest.raises(ValueError, match="Either components parameter must be given or"): - with catch_warnings(record=True) as w: - empty_context() - assert "explicit is better than implicit" in str(w[-1].message) - - -@pytest.mark.parametrize("cli_fn", [CLI, auto_cli]) -def test_automatic_components_context_function(cli_fn): - def function(a1: float): - return a1 - - def non_empty_context_function(): - return cli_fn(args=["6.7"]) - - with mock_getmodule_locals(non_empty_context_function, [function]): - with catch_warnings(record=True) as w: - assert 6.7 == non_empty_context_function() - assert "explicit is better than implicit" in str(w[-1].message) - - -@pytest.mark.parametrize("cli_fn", [CLI, auto_cli]) -def test_automatic_components_context_class(cli_fn): - class ClassX: - def __init__(self, i1: str): - self.i1 = i1 - - def method(self, m1: int): - return self.i1, m1 - - def non_empty_context_class(): - return cli_fn(args=["a", "method", "2"]) - - with mock_getmodule_locals(non_empty_context_class, [ClassX]): - with catch_warnings(record=True) as w: - assert ("a", 2) == non_empty_context_class() - assert "explicit is better than implicit" in str(w[-1].message) - - -class InheritsLoggerProperty(LoggerProperty): - pass - - -def test_logger_property(): - with catch_warnings(record=True) as w: - InheritsLoggerProperty() - assert_deprecation_warn( - w, - message="LoggerProperty was deprecated", - code="InheritsLoggerProperty()", - ) - - -def test_logger_property_none(): - with catch_warnings(record=True) as w: - ArgumentParser(logger=None) - assert_deprecation_warn( - w, - message=" Setting the logger property to None was deprecated", - code="ArgumentParser(logger=None)", - ) - - -def test_env_prefix_none(): - with catch_warnings(record=True) as w: - ArgumentParser(env_prefix=None) - assert_deprecation_warn( - w, - message="env_prefix", - code="ArgumentParser(env_prefix=None)", - ) - - -def test_error_handler_parameter(): - with catch_warnings(record=True) as w: - parser = ArgumentParser(error_handler=usage_and_exit_error_handler) - code = "ArgumentParser(error_handler=usage_" - if not is_posix: # pragma: no cover - code = None # for some reason the stack trace differs in windows - assert_deprecation_warn( - w, - message="error_handler was deprecated in v4.20.0", - code=code, - ) - with catch_warnings(record=True) as w: - assert parser.error_handler == usage_and_exit_error_handler - assert_deprecation_warn( - w, - message="error_handler property is deprecated", - code="parser.error_handler", - ) - with suppress_stderr(), pytest.raises(SystemExit), catch_warnings(record=True): - parser.parse_args(["--invalid"]) - - -def test_error_handler_property(): - def custom_error_handler(self, message): - print("custom_error_handler") - self.exit(2) - - parser = ArgumentParser() - with catch_warnings(record=True) as w: - parser.error_handler = custom_error_handler - assert_deprecation_warn( - w, - message="error_handler was deprecated in v4.20.0", - code="parser.error_handler = custom_error_handler", - ) - with catch_warnings(record=True) as w: - assert parser.error_handler == custom_error_handler - assert_deprecation_warn( - w, - message="error_handler property is deprecated", - code="parser.error_handler", - ) - - out = StringIO() - with redirect_stdout(out), pytest.raises(SystemExit): - parser.parse_args(["--invalid"]) - assert out.getvalue() == "custom_error_handler\n" - - with pytest.raises(ValueError): - parser.error_handler = "invalid" - - -def test_ParserError(): - assert isinstance(argument_error(""), ParserError) - - -def test_parse_as_dict(tmp_cwd): - with open("config.json", "w") as f: - f.write("{}") - with catch_warnings(record=True) as w: - parser = ArgumentParser(parse_as_dict=True) - assert_deprecation_warn( - w, - message="``parse_as_dict`` parameter was deprecated", - code="ArgumentParser(parse_as_dict=True)", - ) - assert {} == parser.parse_args([]) - assert {} == parser.parse_env([]) - assert {} == parser.parse_string("{}") - assert {} == parser.parse_object({}) - assert {} == parser.parse_path("config.json") - with catch_warnings(record=True) as w: - result = parser.instantiate_classes({}) - assert {} == result - assert_deprecation_warn( - w, - message="``instantiate_classes`` was deprecated", - code="result = parser.instantiate_classes({})", - ) - assert "{}\n" == parser.dump({}) - parser.save({}, "config.yaml") - with open("config.yaml") as f: - assert "{}\n" == f.read() - - -def test_default_meta_property(parser): - with catch_warnings(record=True) as w: - assert True is parser.default_meta - assert_deprecation_warn( - w, - message="``default_meta`` property was deprecated", - code="True is parser.default_meta", - ) - with catch_warnings(record=True) as w: - parser.default_meta = False - assert_deprecation_warn( - w, - message="``default_meta`` property was deprecated", - code="parser.default_meta = False", - ) - assert False is parser.default_meta - parser = ArgumentParser(default_meta=False) - assert False is parser.default_meta - parser.default_meta = True - assert True is parser.default_meta - with pytest.raises(ValueError) as ctx: - parser.default_meta = "invalid" - ctx.match("default_meta expects a boolean") - - -def test_parse_with_meta_parameter(parser): - with catch_warnings(record=True) as w: - parser.parse_args([], with_meta=False) - assert_deprecation_warn( - w, - message="``with_meta`` parameter was deprecated in v4.44.0 and will be removed in v5.0.0", - code="parser.parse_args([], with_meta=False)", - ) - - -def test_deprecated_skip_check_method(parser): - parser.add_argument("--key", type=int) - cfg = Namespace(key=1) - with catch_warnings(record=True) as w: - parser.check_config(cfg) - assert_deprecation_warn( - w, - message="ArgumentParser.check_config was deprecated", - code="parser.check_config(cfg)", - ) - - -def test_deprecated_dump_skip_check_parameter(parser): - parser.add_argument("--key", type=int) - cfg = Namespace(key="-") - with catch_warnings(record=True) as w: - dump = parser.dump(cfg, skip_check=True) - assert "-" in dump - assert_deprecation_warn( - w, - message="skip_check parameter was deprecated", - code="parser.dump(cfg, skip_check=True)", - ) - - -def test_ActionPath(tmp_cwd): - os.mkdir(os.path.join(tmp_cwd, "example")) - rel_yaml_file = os.path.join("..", "example", "example.yaml") - abs_yaml_file = os.path.realpath(os.path.join(tmp_cwd, "example", rel_yaml_file)) - with open(abs_yaml_file, "w") as output_file: - output_file.write("file: " + rel_yaml_file + "\ndir: " + str(tmp_cwd) + "\n") - - parser = ArgumentParser(exit_on_error=False) - parser.add_argument("--cfg", action="config") - with catch_warnings(record=True) as w: - parser.add_argument("--file", action=ActionPath(mode="fr")) - assert_deprecation_warn( - w, - message="ActionPath was deprecated", - code='ActionPath(mode="fr")', - ) - parser.add_argument("--dir", action=ActionPath(mode="drw")) - parser.add_argument("--files", nargs="+", action=ActionPath(mode="fr")) - - cfg = parser.parse_args(["--cfg", abs_yaml_file]) - assert str(tmp_cwd) == os.path.realpath(cfg.dir.absolute) - assert abs_yaml_file == os.path.realpath(cfg.cfg[0].relative) - assert abs_yaml_file == os.path.realpath(cfg.cfg[0].absolute) - assert rel_yaml_file == cfg.file.relative - assert abs_yaml_file == os.path.realpath(cfg.file.absolute) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--cfg", abs_yaml_file + "~"])) - - cfg = parser.parse_args(["--cfg", "file: " + abs_yaml_file + "\ndir: " + str(tmp_cwd) + "\n"]) - assert str(tmp_cwd) == os.path.realpath(cfg.dir.absolute) - assert cfg.cfg[0] is None - assert abs_yaml_file == os.path.realpath(cfg.file.absolute) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--cfg", '{"k":"v"}'])) - - cfg = parser.parse_args(["--file", abs_yaml_file, "--dir", str(tmp_cwd)]) - assert str(tmp_cwd) == os.path.realpath(cfg.dir.absolute) - assert abs_yaml_file == os.path.realpath(cfg.file.absolute) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--dir", abs_yaml_file])) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--file", str(tmp_cwd)])) - - cfg = parser.parse_args(["--files", abs_yaml_file, abs_yaml_file]) - assert isinstance(cfg.files, list) - assert 2 == len(cfg.files) - assert abs_yaml_file == os.path.realpath(cfg.files[-1].absolute) - - pytest.raises(TypeError, lambda: parser.add_argument("--op1", action=ActionPath)) - pytest.raises( - ValueError, - lambda: parser.add_argument("--op3", action=ActionPath(mode="+")), - ) - pytest.raises( - ValueError, - lambda: parser.add_argument("--op4", type=str, action=ActionPath(mode="fr")), - ) - - -def test_ActionPath_skip_check(tmp_cwd): - parser = ArgumentParser(exit_on_error=False) - with catch_warnings(record=True) as w: - parser.add_argument("--file", action=ActionPath(mode="fr", skip_check=True)) - assert_deprecation_warn( - w, - message="skip_check parameter of Path was deprecated", - code='ActionPath(mode="fr", skip_check=True)', - ) - cfg = parser.parse_args(["--file=not-exist"]) - assert isinstance(cfg.file, Path) - assert str(cfg.file) == "not-exist" - assert parser.dump(cfg) == "file: not-exist\n" - assert repr(cfg.file).startswith("Path_fr_skip_check") - - -def test_ActionPath_dump(tmp_cwd): - parser = ArgumentParser() - with catch_warnings(record=True): - parser.add_argument("--path", action=ActionPath(mode="fc")) - cfg = parser.parse_string("path: path") - assert parser.dump(cfg) == "path: path\n" - - parser = ArgumentParser() - parser.add_argument("--paths", nargs="+", action=ActionPath(mode="fc")) - cfg = parser.parse_args(["--paths", "path1", "path2"]) - assert parser.dump(cfg) == "paths:\n- path1\n- path2\n" - - -def test_ActionPath_nargs_questionmark(tmp_cwd): - parser = ArgumentParser() - parser.add_argument("val", type=int) - with catch_warnings(record=True): - parser.add_argument("path", nargs="?", action=ActionPath(mode="fc")) - assert None is parser.parse_args(["1"]).path - assert None is not parser.parse_args(["2", "file"]).path - - -def test_Path_attr_set(tmp_cwd): - path = Path("file", "fc") - with catch_warnings(record=True) as w: - path.rel_path = "file" - path.abs_path = os.path.join(tmp_cwd, "file") - path.skip_check = False - path.cwd = str(tmp_cwd) - assert "Path objects are not meant to be mutable" in str(w[-1].message) - with catch_warnings(record=True) as w: - assert path.rel_path == "file" - assert path.abs_path == os.path.join(tmp_cwd, "file") - assert path.skip_check is False - assert "Path objects are not meant to be mutable" in str(w[-1].message) - - -def test_path_call(paths): # noqa: F811 - path = Path(paths.file_rw, "frw") - with catch_warnings(record=True) as w: - assert path(False) == str(paths.file_rw) - assert_deprecation_warn( - w, - message="Calling Path objects is deprecated", - code="assert path(False) == ", - ) - assert path(True) == str(paths.tmp_path / paths.file_rw) - assert path() == str(paths.tmp_path / paths.file_rw) - - -def test_file_path_get_content(paths): # noqa: F811 - path = Path(paths.file_r, "fr") - with catch_warnings(record=True) as w: - content = path.get_content() - assert_deprecation_warn( - w, - message="``Path.get_content`` was deprecated", - code="content = path.get_content()", - ) - assert "file contents" == content - - -def test_std_input_path_get_content(): - input_text_to_test = "a text here\n" - with patch("sys.stdin", StringIO(input_text_to_test)), catch_warnings(record=True) as w: - path = Path("-", mode="fr") - assert input_text_to_test == path.get_content() - assert_deprecation_warn( - w, - message="``Path.get_content`` was deprecated", - code="input_text_to_test == path.get_content()", - ) - - -@skip_if_responses_unavailable -@responses_activate -def test_path_url_200(): - import responses - - existing = "http://example.com/existing-url" - existing_body = "url contents" - responses.add(responses.GET, existing, status=200, body=existing_body) - responses.add(responses.HEAD, existing, status=200) - path = Path(existing, mode="ur") - with catch_warnings(record=True) as w: - assert existing_body == path.get_content() - assert_deprecation_warn( - w, - message="``Path.get_content`` was deprecated", - code="existing_body == path.get_content()", - ) - - -@skip_if_fsspec_unavailable -def test_path_fsspec_memory(): - import fsspec - - file_content = "content in memory" - memfile = "memfile.txt" - path = Path(f"memory://{memfile}", mode="sw") - with fsspec.open(path, "w") as f: - f.write(file_content) - with catch_warnings(record=True) as w: - assert file_content == path.get_content() - assert_deprecation_warn( - w, - message="``Path.get_content`` was deprecated", - code="file_content == path.get_content()", - ) - - -def test_ActionPathList(tmp_cwd): - tmpdir = os.path.join(tmp_cwd, "subdir") - os.mkdir(tmpdir) - pathlib.Path(os.path.join(tmpdir, "file1")).touch() - pathlib.Path(os.path.join(tmpdir, "file2")).touch() - pathlib.Path(os.path.join(tmpdir, "file3")).touch() - pathlib.Path(os.path.join(tmpdir, "file4")).touch() - pathlib.Path(os.path.join(tmpdir, "file5")).touch() - list_file = os.path.join(tmpdir, "files.lst") - list_file2 = os.path.join(tmpdir, "files2.lst") - list_file3 = os.path.join(tmpdir, "files3.lst") - list_file4 = os.path.join(tmpdir, "files4.lst") - with open(list_file, "w") as output_file: - output_file.write("file1\nfile2\nfile3\nfile4\n") - with open(list_file2, "w") as output_file: - output_file.write("file5\n") - pathlib.Path(list_file3).touch() - with open(list_file4, "w") as output_file: - output_file.write("file1\nfile2\nfile6\n") - - parser = ArgumentParser(prog="app", exit_on_error=False) - with catch_warnings(record=True) as w: - parser.add_argument("--list", nargs="+", action=ActionPathList(mode="fr", rel="list")) - assert_deprecation_warn( - w, - message="ActionPathList was deprecated", - code="ActionPathList(mode=", - ) - parser.add_argument("--list_cwd", action=ActionPathList(mode="fr", rel="cwd")) - - cfg = parser.parse_args(["--list", list_file]) - assert 4 == len(cfg.list) - assert ["file1", "file2", "file3", "file4"] == [str(x) for x in cfg.list] - - cfg = parser.parse_args(["--list", list_file, list_file2]) - assert 5 == len(cfg.list) - assert ["file1", "file2", "file3", "file4", "file5"] == [str(x) for x in cfg.list] - - assert 0 == len(parser.parse_args(["--list", list_file3]).list) - - cwd = os.getcwd() - os.chdir(tmpdir) - cfg = parser.parse_args(["--list_cwd", list_file]) - assert 4 == len(cfg.list_cwd) - assert ["file1", "file2", "file3", "file4"] == [str(x) for x in cfg.list_cwd] - os.chdir(cwd) - - pytest.raises(ArgumentError, lambda: parser.parse_args(["--list"])) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--list", list_file4])) - pytest.raises(ArgumentError, lambda: parser.parse_args(["--list", "no-such-file"])) - - pytest.raises(ValueError, lambda: parser.add_argument("--op1", action=ActionPathList)) - pytest.raises( - ValueError, - lambda: parser.add_argument("--op2", action=ActionPathList(mode="fr"), nargs="*"), - ) - pytest.raises( - ValueError, - lambda: parser.add_argument("--op3", action=ActionPathList(mode="fr", rel=".")), - ) - - -@skip_if_docstring_parser_unavailable -def test_import_import_docstring_parse(): - from jsonargparse._optionals import import_docstring_parser - - with catch_warnings(record=True) as w: - from jsonargparse.optionals import import_docstring_parse - - assert_deprecation_warn( - w, - message="Only use the public API", - code="from jsonargparse.optionals import import_docstring_parse", - ) - assert import_docstring_parse is import_docstring_parser - - -@skip_if_docstring_parser_unavailable -def test_docstring_parse_options(): - from docstring_parser import DocstringStyle - - options = get_docstring_parse_options() - options["style"] = None - - with catch_warnings(record=True) as w: - for style in [DocstringStyle.NUMPYDOC, DocstringStyle.GOOGLE]: - set_docstring_parse_options(style=style) - assert options["style"] == style - assert_deprecation_warn( - w, - message="set_docstring_parse_options was deprecated", - code="set_docstring_parse_options(style=style)", - ) - - -def test_import_from_deprecated(): - import jsonargparse.deprecated as deprecated - - with catch_warnings(record=True) as w: - func = deprecated.set_url_support - - assert_deprecation_warn( - w, - message="Only use the public API", - code="func = deprecated.set_url_support", - ) - assert func is set_url_support - - -@pytest.mark.parametrize( - ["module", "attr"], - [ - ("actions", "ActionYesNo"), - ("cli", "CLI"), - ("core", "ArgumentParser"), - ("formatters", "DefaultHelpFormatter"), - ("jsonnet", "ActionJsonnet"), - ("jsonschema", "ActionJsonSchema"), - ("link_arguments", "ArgumentLinking"), - ("loaders_dumpers", "set_loader"), - ("namespace", "Namespace"), - ("typehints", "lazy_instance"), - ("util", "Path"), - ("parameter_resolvers", "ParamData"), - ], -) -def test_import_from_module(module, attr): - module = import_module(f"jsonargparse.{module}") - with catch_warnings(record=True) as w: - getattr(module, attr) - assert_deprecation_warn( - w, - message="Only use the public API", - code="getattr(module, attr)", - ) - - -@pytest.mark.skipif(not jsonnet_support, reason="jsonnet package is required") -def test_action_jsonnet_ext_vars(parser): - with catch_warnings(record=True) as w: - parser.add_argument("--ext_vars", action=ActionJsonnetExtVars()) - assert_deprecation_warn( - w, - message="ActionJsonnetExtVars was deprecated", - code="action=ActionJsonnetExtVars()", - ) - parser.add_argument("--jsonnet", action=ActionJsonnet(ext_vars="ext_vars")) - - cfg = parser.parse_args(["--ext_vars", '{"param": 123}', "--jsonnet", example_2_jsonnet]) - assert 123 == cfg.jsonnet["param"] - assert 9 == len(cfg.jsonnet["records"]) - assert "#8" == cfg.jsonnet["records"][-2]["ref"] - assert 15.5 == cfg.jsonnet["records"][-2]["val"] - - -def test_add_dataclass_arguments(parser, subtests): - with catch_warnings(record=True) as w: - parser.add_dataclass_arguments(DataClassA, "a", default=DataClassA(), title="CustomA title") - assert_deprecation_warn( - w, - message="add_dataclass_arguments was deprecated", - code='parser.add_dataclass_arguments(DataClassA, "a", default=DataClassA(), title="CustomA title")', - ) - - with subtests.test("get_defaults"): - cfg = parser.get_defaults() - assert dataclasses.asdict(DataClassA()) == cfg["a"].as_dict() - dump = __import__("yaml").safe_load(parser.dump(cfg)) - assert dataclasses.asdict(DataClassA()) == dump["a"] - - with subtests.test("instantiate_classes"): - init = parser.instantiate(cfg) - assert isinstance(init["a"], DataClassA) - - with subtests.test("docstrings in help"): - help_str = get_parser_help(parser) - if docstring_parser_support: - assert "CustomA title:" in help_str - - -def test_dict_to_namespace(): - ns1 = Namespace(a=1, b=Namespace(c=2), d=[Namespace(e=3)]) - dic = {"a": 1, "b": {"c": 2}, "d": [{"e": 3}]} - with catch_warnings(record=True) as w: - ns2 = dict_to_namespace(dic) - assert ns1 == ns2 - assert_deprecation_warn( - w, - message="dict_to_namespace was deprecated", - code="ns2 = dict_to_namespace(dic)", - ) - - -def test_namespace_to_dict(): - ns = Namespace() - ns["w"] = 1 - ns["x.y"] = 2 - ns["x.z"] = 3 - with catch_warnings(record=True) as w: - dic1 = namespace_to_dict(ns) - dic2 = ns.as_dict() - assert dic1 == dic2 - assert dic1 is not dic2 - assert_deprecation_warn( - w, - message="namespace_to_dict was deprecated", - code="dic1 = namespace_to_dict(ns)", - ) - - -def test_strip_meta(): - ns = Namespace(x=1, __path__="path") - with catch_warnings(record=True) as w: - result = strip_meta(ns) - assert result == Namespace(x=1) - assert_deprecation_warn( - w, - message="strip_meta was deprecated", - code="result = strip_meta(ns)", - ) - result = strip_meta(ns.as_dict()) - assert result == {"x": 1} - - -def test_namespace_get_sorted_keys(): - ns = Namespace(a=Namespace(b=1)) - with catch_warnings(record=True) as w: - keys = ns.get_sorted_keys() - assert keys == ["a.b", "a"] - assert_deprecation_warn( - w, - message="get_sorted_keys method was deprecated", - code="keys = ns.get_sorted_keys()", - ) - - -def test_namespace_get_value_and_parent(): - ns = Namespace(a=Namespace(b=1)) - with catch_warnings(record=True) as w: - value, parent, key = ns.get_value_and_parent("a.b") - assert value == 1 - assert parent == ns.a - assert key == "b" - assert_deprecation_warn( - w, - message="get_value_and_parent method was deprecated", - code='value, parent, key = ns.get_value_and_parent("a.b")', - ) - - -@pytest.mark.skipif(not ruamel_support, reason="ruamel.yaml package is required") -def test_DefaultHelpFormatter_yaml_comments(parser): - parser.add_argument("--arg", type=int, help="Description") - formatter = DefaultHelpFormatter(prog="test") - from jsonargparse._common import parent_parser - - parent_parser.set(parser) - ruyaml = import_ruamel("test_DefaultHelpFormatter_yaml_comments") - yaml = ruyaml.YAML() - cfg = yaml.load("arg: 1") - - with catch_warnings(record=True) as w: - formatter.add_yaml_comments("arg: 1") - assert "add_yaml_comments method is deprecated and will be removed in v5.0.0" in str(w[-1].message) - assert "formatter.add_yaml_comments(" in source[w[-1].lineno - 1] - - with catch_warnings(record=True) as w: - formatter.set_yaml_start_comment("start", cfg) - assert "set_yaml_start_comment method is deprecated and will be removed in v5.0.0" in str(w[-1].message) - assert "formatter.set_yaml_start_comment(" in source[w[-1].lineno - 1] - - with catch_warnings(record=True) as w: - formatter.set_yaml_group_comment("group", cfg, "arg", 0) - assert "set_yaml_group_comment method is deprecated and will be removed in v5.0.0" in str(w[-1].message) - assert "formatter.set_yaml_group_comment(" in source[w[-1].lineno - 1] - - with catch_warnings(record=True) as w: - formatter.set_yaml_argument_comment("arg", cfg, "arg", 0) - assert "set_yaml_argument_comment method is deprecated and will be removed in v5.0.0" in str(w[-1].message) - assert "formatter.set_yaml_argument_comment(" in source[w[-1].lineno - 1] - - -@pytest.mark.skipif(not ruamel_support, reason="ruamel.yaml package is required") -def test_deprecated_dump_yaml_comments_parameter(parser): - parser.add_argument("--arg", type=int, default=1, help="Description") - cfg = parser.get_defaults() - with catch_warnings(record=True) as w: - parser.dump(cfg, yaml_comments=True) - assert_deprecation_warn( - w, - message="yaml_comments parameter was deprecated in v4.44.0 and will be removed in v5.0.0", - code="parser.dump(cfg, yaml_comments=True)", - ) - - -@dataclasses.dataclass -class ComposeA: - a: int = 1 - - def __post_init__(self): - self.a += 1 - - -@dataclasses.dataclass -class ComposeB: - b: str = "1" - - -def test_compose_dataclasses(): - with catch_warnings(record=True) as w: - ComposeAB = compose_dataclasses(ComposeA, ComposeB) - assert_deprecation_warn( - w, - message="compose_dataclasses is deprecated", - code="ComposeAB = compose_dataclasses(ComposeA, ComposeB)", - ) - assert 2 == len(dataclasses.fields(ComposeAB)) - assert {"a": 3, "b": "2"} == dataclasses.asdict(ComposeAB(a=2, b="2")) # pylint: disable=unexpected-keyword-arg - - -def test_add_argument_enable_path_deprecated(parser, tmp_cwd): - import json - - data = {"a": 1} - pathlib.Path("data.yaml").write_text(json.dumps(data)) - - with catch_warnings(record=True) as w: - parser.add_argument("--data", type=dict, enable_path=True) - assert_deprecation_warn( - w, - message="``enable_path`` parameter of ``add_argument`` was deprecated", - code='parser.add_argument("--data", type=dict, enable_path=True)', - ) - cfg = parser.parse_args(["--data=data.yaml"]) - path_value = cfg["data"].pop("__path__") - assert "data.yaml" == str(path_value) - assert data == cfg["data"] - - -@skip_if_jsonschema_unavailable -def test_action_json_schema_enable_path_deprecated(parser): - - schema = {"type": "object", "properties": {"x": {"type": "integer"}}} - - with catch_warnings(record=True) as w: - action = ActionJsonSchema(schema=schema, enable_path=False) - assert_deprecation_warn( - w, - message="``enable_path`` parameter of ``ActionJsonSchema`` was deprecated", - code="ActionJsonSchema(schema=schema, enable_path=False)", - ) - parser.add_argument("--obj", action=action) - cfg = parser.parse_args(['--obj={"x": 1}']) - assert cfg.obj == {"x": 1} - - -# skip_none deprecation tests - - -def test_deprecated_dump_skip_none_parameter(parser): - parser.add_argument("--val", type=int) - cfg = parser.parse_args(["--val=1"]) - with catch_warnings(record=True) as w: - dump = parser.dump(cfg, skip_none=True) - assert "val: 1" in dump - assert_deprecation_warn( - w, - message="skip_none parameter was deprecated", - code="parser.dump(cfg, skip_none=True)", - ) - - -def test_deprecated_save_skip_none_parameter(parser, tmp_cwd): - parser.add_argument("--val", type=int) - cfg = parser.parse_args(["--val=1"]) - with catch_warnings(record=True) as w: - parser.save(cfg, "out.yaml", skip_none=True) - assert_deprecation_warn( - w, - message="skip_none parameter was deprecated", - code='parser.save(cfg, "out.yaml", skip_none=True)', - ) - with open("out.yaml") as f: - assert "val: 1" in f.read() - - -def test_deprecated_validate_skip_none_parameter(parser): - parser.add_argument("--val", type=int) - cfg = parser.parse_args(["--val=1"]) - with catch_warnings(record=True) as w: - parser.validate(cfg, skip_none=True) - assert_deprecation_warn( - w, - message="skip_none parameter was deprecated", - code="parser.validate(cfg, skip_none=True)", - ) - - -def test_save_unexpected_kwarg(parser, tmp_cwd): - parser.add_argument("--val", type=int) - cfg = parser.parse_args(["--val=1"]) - with pytest.raises(ValueError, match="Unexpected keyword parameters"): - parser.save(cfg, "out.yaml", unknown_kwarg=True) - - -def test_dump_unexpected_kwarg(parser): - parser.add_argument("--val", type=int) - cfg = parser.parse_args(["--val=1"]) - with pytest.raises(ValueError, match="Unexpected keyword parameters"): - parser.dump(cfg, unknown_kwarg=True) - - -def test_deprecated_print_config_skip_null(parser): - from jsonargparse_tests.conftest import get_parse_args_stdout - - parser.add_argument("--cfg", action="config") - parser.add_argument("--op", type=int) - with catch_warnings(record=True) as w: - get_parse_args_stdout(parser, ["--print_config=skip_null"]) - assert_deprecation_warn( - w, - message="skip_null flag for --print_config was deprecated", - code=None, - ) - - -def test_deprecated_print_config_default_name(monkeypatch): - monkeypatch.delenv("JSONARGPARSE_DEPRECATION_WARNINGS", raising=False) - - # No warning when the config dest is "config". - parser = ArgumentParser(exit_on_error=False) - with catch_warnings(record=True) as w: - parser.add_argument("--config", action="config") - assert w == [] - - # No warning without JSONARGPARSE_DEPRECATION_WARNINGS=all, even with another dest. - parser = ArgumentParser(exit_on_error=False) - with catch_warnings(record=True) as w: - parser.add_argument("--cfg", action="config") - assert w == [] - - monkeypatch.setenv("JSONARGPARSE_DEPRECATION_WARNINGS", "all") - - # No warning with all when the config dest is "config". - parser = ArgumentParser(exit_on_error=False) - with catch_warnings(record=True) as w: - parser.add_argument("--config", action="config") - assert w == [] - - # No warning when print_config already uses %s. - parser = ArgumentParser(exit_on_error=False, print_config="--print_%s") - with catch_warnings(record=True) as w: - parser.add_argument("--cfg", action="config") - assert w == [] - - # Warning with all when the config dest differs from "config". - parser = ArgumentParser(exit_on_error=False) - with catch_warnings(record=True) as w: - parser.add_argument("--cfg", action="config") - assert_deprecation_warn( - w, - message='become "--print_cfg"', - code='parser.add_argument("--cfg", action="config")', - ) - - -def test_subcommands_parse_string_first_implicit_subcommand(subcommands_parser): # noqa: F811 - with catch_warnings(record=True) as w: - cfg = subcommands_parser.parse_string('{"a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}') - assert_deprecation_warn( - w, - message="Multiple subcommand settings provided", - code="cfg = subcommands_parser.parse_string(", - ) - assert "Subcommand 'a' will be" in str(w[1].message) - assert cfg.subcommand == "a" - assert "b" not in cfg - - -def test_subcommands_implicit_in_default_config_files(parser, tmp_cwd): - parser.default_config_files = ["defaults.json"] - subs = parser.add_subcommands(required=True, dest="sub") - sub1 = ArgumentParser() - sub1.add_argument("--sub1val") - subs.add_subcommand("sub1", sub1) - sub2 = ArgumentParser() - sub2.add_argument("--sub2val") - subs.add_subcommand("sub2", sub2) - - defaults: dict = { - "sub1": {"sub1val": 2}, - "sub2": {"sub2val": 3}, - } - pathlib.Path("defaults.json").write_text(json.dumps(defaults)) - - with catch_warnings(record=True) as w: - cfg = parser.parse_args([]) - assert_deprecation_warn( - w, - message="Multiple subcommand settings provided", - code="cfg = parser.parse_args([])", - ) - assert "Subcommand 'sub1' will be" in str(w[1].message) - assert cfg.sub == "sub1" - assert cfg.sub1 == Namespace(sub1val=2) - assert "sub2" not in cfg - - -def test_deprecated_merge_config(parser): - for key in [1, 2, 3]: - parser.add_argument(f"--op{key}", type=int) - cfg_from = Namespace(op1=1, op2=None) - cfg_to = Namespace(op1=None, op2=2, op3=3) - with catch_warnings(record=True) as w: - cfg = parser.merge_config(cfg_from, cfg_to) - assert cfg == Namespace(op1=1, op2=None, op3=3) - assert_deprecation_warn( - w, - message="``ArgumentParser.merge_config`` was deprecated", - code="cfg = parser.merge_config(cfg_from, cfg_to)", - ) - - -def test_add_class_arguments_class_type_rename(parser): - with catch_warnings(record=True) as w: - parser.add_class_arguments(theclass=ComposeA) - assert_deprecation_warn( - w, - message="Parameter 'theclass' was renamed to 'class_type'", - code="parser.add_class_arguments(theclass=ComposeA)", - ) - - -def test_add_method_arguments_class_type_rename(parser): - with catch_warnings(record=True) as w: - parser.add_method_arguments(theclass=WithMethods, themethod="normal_method") - assert_deprecation_warn( - w, - message="Parameter 'theclass' was renamed to 'class_type'", - code='parser.add_method_arguments(theclass=WithMethods, themethod="normal_method")', - ) - - -def test_add_method_arguments_method_name_rename(parser): - with catch_warnings(record=True) as w: - parser.add_method_arguments(WithMethods, themethod="normal_method") - assert_deprecation_warn( - w, - message="Parameter 'themethod' was renamed to 'method_name'", - code='parser.add_method_arguments(WithMethods, themethod="normal_method")', - ) - - -def test_parse_object_obj_rename(parser): - parser.add_argument("--p1", type=float) - with catch_warnings(record=True) as w: - assert parser.parse_object(cfg_obj={"p1": 1.2}) == Namespace(p1=1.2) - assert_deprecation_warn( - w, - message="Parameter 'cfg_obj' was renamed to 'obj'", - code='parser.parse_object(cfg_obj={"p1": 1.2})', - ) - - -def test_parse_object_namespace_rename(parser): - parser.add_argument("--p1", type=float) - parser.add_argument("--p2", type=int) - with catch_warnings(record=True) as w: - assert parser.parse_object({"p1": 2.1}, cfg_base=Namespace(p2=3)) == Namespace(p1=2.1, p2=3) - assert_deprecation_warn( - w, - message="Parameter 'cfg_base' was renamed to 'namespace'", - code='parser.parse_object({"p1": 2.1}, cfg_base=Namespace(p2=3))', - ) - - -def test_parse_path_path_rename(parser, tmp_cwd): - parser.add_argument("--p1", type=float) - pathlib.Path("cfg.json").write_text('{"p1": 2.1}') - with catch_warnings(record=True) as w: - assert parser.parse_path(cfg_path="cfg.json") == Namespace(p1=2.1) - assert_deprecation_warn( - w, - message="Parameter 'cfg_path' was renamed to 'path'", - code='parser.parse_path(cfg_path="cfg.json")', - ) - - -def test_parse_string_content_rename(parser): - parser.add_argument("--p1", type=float) - with catch_warnings(record=True) as w: - assert parser.parse_string(cfg_str='{"p1": 2.1}') == Namespace(p1=2.1) - assert_deprecation_warn( - w, - message="Parameter 'cfg_str' was renamed to 'content'", - code="parser.parse_string(cfg_str='{\"p1\": 2.1}')", - ) - - -def test_parse_string_path_rename(parser): - parser.add_argument("--p1", type=float) - with catch_warnings(record=True) as w: - assert parser.parse_string(content='{"p1": 2.1}', cfg_path="cfg.yaml") == Namespace(p1=2.1) - assert_deprecation_warn( - w, - message="Parameter 'cfg_path' was renamed to 'path'", - code='parser.parse_string(content=\'{"p1": 2.1}\', cfg_path="cfg.yaml")', - ) - - -def test_optional_parameter_without_default_deprecation(parser, monkeypatch): - def without_default(value: Optional[int]): - pass # pragma: no cover - - monkeypatch.delenv("JSONARGPARSE_DEPRECATION_WARNINGS", raising=False) - with catch_warnings(record=True) as warnings: - parser.add_function_arguments(without_default) - assert warnings == [] - - monkeypatch.setenv("JSONARGPARSE_DEPRECATION_WARNINGS", "all") - - def without_default_warning(other: Optional[int]): - pass # pragma: no cover - - with catch_warnings(record=True) as warnings: - parser.add_function_arguments(without_default_warning) - assert len(warnings) == 1 - assert "Optional type parameters without a default" in str(warnings[0].message) - assert "In v5 they will be required" in str(warnings[0].message) - - assert parser.get_defaults() == Namespace(value=None, other=None) - - -def test_fail_untyped_false_required_parameter_deprecation(parser, monkeypatch): - def with_untyped_required(a1, a2=None): - pass # pragma: no cover - - monkeypatch.delenv("JSONARGPARSE_DEPRECATION_WARNINGS", raising=False) - with catch_warnings(record=True) as warnings: - parser.add_function_arguments(with_untyped_required, fail_untyped=False) - assert warnings == [] - - monkeypatch.setenv("JSONARGPARSE_DEPRECATION_WARNINGS", "all") - - def with_untyped_required_warning(b1, b2=None): - pass # pragma: no cover - - with catch_warnings(record=True) as warnings: - parser.add_function_arguments(with_untyped_required_warning, fail_untyped=False) - assert len(warnings) == 1 - assert "fail_untyped=False" in str(warnings[0].message) - assert "In v5 the type will be set to Untyped but the parameter will remain required" in str(warnings[0].message) - - assert parser.get_defaults() == Namespace(a1=None, a2=None, b1=None, b2=None) - - -def test_instantiate_subclass_spec_in_any_deprecation(parser): - shown_deprecation_warnings.clear() - parser.add_argument("--any", type=Any) - spec = {"class_path": "calendar.TextCalendar", "init_args": {"firstweekday": 2}} - cfg = parser.parse_args([f"--any={json.dumps(spec)}"]) - - with catch_warnings(record=True) as w: - init = parser.instantiate(cfg) - assert isinstance(init.any, Calendar) - assert init.any.firstweekday == 2 - assert len(w) == 2 - assert "will no longer be instantiated" in str(w[-1].message) - assert "instantiate_subclass_spec_in_any" in str(w[-1].message) - - with catch_warnings(record=True) as w: - init = parser.instantiate(cfg) - assert isinstance(init.any, Calendar) - assert w == [] - - -# denied import paths only warn deprecation tests - - -@patch_parsing_settings -def test_denied_import_path_not_enforced_warns_and_imports(): - shown_deprecation_warnings.clear() - with catch_warnings(record=True) as w: - assert import_object("subprocess.Popen") is not None - assert len(w) == 2 - assert "subprocess.Popen" in str(w[-1].message) - assert "from v5.0.0 it will fail" in str(w[-1].message) - - -@patch_parsing_settings -def test_denied_import_path_warns_once_per_path(): - shown_deprecation_warnings.clear() - with catch_warnings(record=True) as w: - import_object("subprocess.Popen") - import_object("subprocess.Popen") - import_object("subprocess.run") - assert len(w) == 3 - assert "subprocess.Popen" in str(w[1].message) - assert "subprocess.run" in str(w[2].message) - - -@pytest.mark.parametrize( - "settings", - [ - {"import_path_denylist": []}, - {"import_path_allowlist": []}, - {"import_path_denylist": ["calendar"]}, - {"import_path_allowlist": ["calendar"]}, - ], -) -@patch_parsing_settings -def test_denied_import_path_enforced_when_setting_given(settings): - set_parsing_settings(**settings) - with pytest.raises(ImportDenied): - import_object("subprocess.Popen") diff --git a/jsonargparse_tests/test_formatters.py b/jsonargparse_tests/test_formatters.py index f4206bbf..93dc0c36 100644 --- a/jsonargparse_tests/test_formatters.py +++ b/jsonargparse_tests/test_formatters.py @@ -30,11 +30,11 @@ def test_help_action_version(parser): def test_help_action_config_file(parser): - parser.add_argument("-c", "--cfg", help="Config in yaml/json.", action="config") + parser.add_argument("-c", "--config", help="Config in yaml/json.", action="config") help_str = get_parser_help(parser) assert "ARG: --print_config" in help_str - assert "ARG: -c CFG, --cfg CFG" in help_str or "ARG: -c, --cfg CFG" in help_str - assert "ENV: APP_CFG" in help_str + assert "ARG: -c CONFIG, --config CONFIG" in help_str or "ARG: -c, --config CONFIG" in help_str + assert "ENV: APP_CONFIG" in help_str assert "Config in yaml/json." in help_str assert "APP_PRINT_CONFIG" not in help_str diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py index 3156125c..9a9c6b73 100644 --- a/jsonargparse_tests/test_link_arguments.py +++ b/jsonargparse_tests/test_link_arguments.py @@ -36,7 +36,7 @@ def test_on_parse_help_target_lacking_type_and_help(parser): def test_on_parse_shallow_print_config(parser): - parser.add_argument("--cfg", action="config") + parser.add_argument("--config", action="config") parser.add_argument("--a", type=int, default=0) parser.add_argument("--b", type=str) parser.link_arguments("a", "b") diff --git a/jsonargparse_tests/test_parsing_settings.py b/jsonargparse_tests/test_parsing_settings.py index 380464b9..ad7838cb 100644 --- a/jsonargparse_tests/test_parsing_settings.py +++ b/jsonargparse_tests/test_parsing_settings.py @@ -325,28 +325,33 @@ class UnsetListItem: value: Optional[int] -def test_unset_sentinel_dump_list_of_dataclasses(parser): +def test_unset_sentinel_list_of_dataclasses(parser): set_parsing_settings(unset_sentinel=True) parser.add_argument("--records", type=List[UnsetListItem]) - cfg = parser.parse_args(['--records=[{"name":"a"},{"name":"b","value":2}]']) - assert cfg.records[0] == Namespace(name="a", value=Unset) + with pytest.raises(ArgumentError, match="the following arguments are required: value"): + parser.parse_args(['--records=[{"name":"a"},{"name":"b","value":2}]']) + + cfg = parser.parse_args(['--records=[{"name":"a","value":null},{"name":"b","value":2}]']) + assert cfg.records[0] == Namespace(name="a", value=None) assert cfg.records[1] == Namespace(name="b", value=2) - dump_skip = parser.dump(cfg, skip_unset=True) - loaded_skip = json_or_yaml_load(dump_skip) - assert loaded_skip == {"records": [{"name": "a"}, {"name": "b", "value": 2}]} + defaults = parser.get_defaults() + assert defaults == Namespace(records=Unset) - dump_no_skip = parser.dump(cfg, skip_unset=False) - loaded_no_skip = json_or_yaml_load(dump_no_skip) - assert loaded_no_skip == {"records": [{"name": "a", "value": "==UNSET=="}, {"name": "b", "value": 2}]} + dump_skip = parser.dump(defaults, skip_unset=True) + assert json_or_yaml_load(dump_skip) == {} + + dump_no_skip = parser.dump(defaults, skip_unset=False) + assert json_or_yaml_load(dump_no_skip) == {"records": "==UNSET=="} def test_unset_sentinel_validate_skip_unset(parser): set_parsing_settings(unset_sentinel=True) parser.add_argument("--num", type=int) + cfg = parser.parse_args([]) assert cfg.num is Unset @@ -381,7 +386,7 @@ def test_unset_is_singleton(): # add_function_arguments with unset_sentinel -def test_unset_sentinel_function_arguments_no_default_is_unset(parser): +def test_unset_sentinel_function_arguments_no_default_is_required(parser): """Optional param with no default in function signature should be Unset when unset_sentinel=True.""" set_parsing_settings(unset_sentinel=True) @@ -390,12 +395,12 @@ def my_func(num: Optional[int], name: str = "hello"): parser.add_function_arguments(my_func) - cfg = parser.parse_args([]) - assert cfg.num is Unset # no default in signature → Unset - assert cfg.name == "hello" # has default → kept + with pytest.raises(ArgumentError, match="the following arguments are required: num"): + parser.parse_args([]) cfg = parser.parse_args(["--num=null"]) assert cfg.num is None # explicitly set to null → None + assert cfg.name == "hello" # has default → kept cfg = parser.parse_args(["--num=5"]) assert cfg.num == 5 @@ -459,10 +464,10 @@ def test_unset_parse_and_print_config(parser): parser.add_argument("--num", type=int) parser.add_argument("--name", type=str, default="a") - parser.add_argument("--cfg", action="config") + parser.add_argument("--config", action="config") - cfg = parser.parse_args(["--cfg={}"]) - assert cfg == Namespace(num=Unset, name="a", cfg=[None]) + cfg = parser.parse_args(["--config={}"]) + assert cfg == Namespace(num=Unset, name="a", config=[None]) out = get_parse_args_stdout(parser, ["--print_config"]) assert json_or_yaml_load(out) == {"num": "==UNSET==", "name": "a"} @@ -655,8 +660,8 @@ def test_set_instantiate_subclass_spec_in_any_failure(): set_parsing_settings(instantiate_subclass_spec_in_any="invalid") -def test_instantiate_subclass_spec_in_any_default_is_none(): - assert get_parsing_setting("instantiate_subclass_spec_in_any") is None +def test_instantiate_subclass_spec_in_any_default_is_false(): + assert get_parsing_setting("instantiate_subclass_spec_in_any") is False def test_instantiate_subclass_spec_in_any_enabled(parser): diff --git a/jsonargparse_tests/test_shtab.py b/jsonargparse_tests/test_shtab.py index aa99b6ca..17eb2237 100644 --- a/jsonargparse_tests/test_shtab.py +++ b/jsonargparse_tests/test_shtab.py @@ -18,7 +18,7 @@ import pytest -from jsonargparse import ArgumentError, ArgumentParser, set_parsing_settings +from jsonargparse import ArgumentParser, set_parsing_settings from jsonargparse._completions import get_shtab_script, norm_name from jsonargparse._optionals import pydantic_support from jsonargparse._parameter_resolvers import get_signature_parameters @@ -854,7 +854,6 @@ def test_add_print_completion_argument_env_var_enables(parser, parsing_settings_ with patch.dict("os.environ", {"JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT": "TRUE"}): help_str = get_parse_args_stdout(parser, ["--help"]) assert "--print_completion" in help_str - assert "--print_shtab" not in parser._option_string_actions def test_add_print_completion_argument_env_var_takes_precedence(parser, parsing_settings_patch): @@ -864,15 +863,6 @@ def test_add_print_completion_argument_env_var_takes_precedence(parser, parsing_ assert "--print_completion" not in help_str -def test_hidden_print_shtab_argument_shows_guidance(parser): - help_str = get_parse_args_stdout(parser, ["--help"]) - assert "--print_shtab" not in help_str - with pytest.raises(ArgumentError, match=r"Use set_parsing_settings\(add_print_completion_argument=True\)"): - parser.parse_args(["--print_shtab=bash"]) - with pytest.raises(ArgumentError, match="JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT=true"): - parser.parse_args(["--print_shtab=bash"]) - - def test_get_completion_script_invalid_completion_type(parser): with pytest.raises(ValueError, match="Unsupported completion_type"): parser.get_completion_script("unknown") diff --git a/jsonargparse_tests/test_signatures.py b/jsonargparse_tests/test_signatures.py index 0d5b472f..9c24c1b3 100644 --- a/jsonargparse_tests/test_signatures.py +++ b/jsonargparse_tests/test_signatures.py @@ -846,23 +846,27 @@ def test_add_function_fail_untyped_false(parser, logger): parser.logger = logger with capture_logs(logger) as logs: added_args = parser.add_function_arguments(func_untyped_params, fail_untyped=False) - assert Namespace(a1=None, a2=None) == parser.parse_args([]) + assert Namespace(a1="x", a2=None) == parser.parse_args(["--a1=x"]) help_str = get_parser_help(parser) + with pytest.raises(ArgumentError, match="the following arguments are required: a1"): + parser.parse_args([]) assert ["a1", "a2"] == added_args - assert f"--a1 A1 (type: {type_to_str(Union[NoneType, Untyped])}, default: null)" in help_str + assert f"--a1 A1 (required, type: {type_to_str(Untyped)})" in help_str assert f"--a2 A2 (type: {type_to_str(Union[NoneType, Untyped])}, default: null)" in help_str - assert f'"a1" from "{__name__}.func_untyped_params" does not have a type annotation. Added as ' in logs.getvalue() + assert f'"a2" from "{__name__}.func_untyped_params" does not have a type annotation. Added as ' in logs.getvalue() assert f"{type_to_str(Union[NoneType, Untyped])}, thus any value is accepted" in logs.getvalue() -def func_untyped_optional(a1: str, a2=None): +def func_untyped_optional(a1: int, a2=None): return a1 # pragma: no cover def test_add_function_fail_untyped_true_untyped_optional(parser): added_args = parser.add_function_arguments(func_untyped_optional, fail_untyped=True) assert ["a1", "a2"] == added_args - assert Namespace(a1="x", a2=None) == parser.parse_args(["--a1=x"]) + assert parser.parse_args(["--a1=123"]) == Namespace(a1=123, a2=None) + with pytest.raises(ArgumentError, match="Expected a .*int.* Got value: x"): + parser.parse_args(["--a1=x"]) def func_untyped_default(a1: str, a2=3): diff --git a/jsonargparse_tests/test_subclasses.py b/jsonargparse_tests/test_subclasses.py index 26d40ef2..9e7f50eb 100644 --- a/jsonargparse_tests/test_subclasses.py +++ b/jsonargparse_tests/test_subclasses.py @@ -41,7 +41,7 @@ lazy_instance, set_parsing_settings, ) -from jsonargparse._instantiation import _global_class_instantiators +from jsonargparse._instantiation import _class_instantiators from jsonargparse._typehints import _cached_class_parsers, implements_protocol, is_instance_or_supports_protocol from jsonargparse.typing import final from jsonargparse_tests.conftest import ( @@ -702,7 +702,7 @@ def test_custom_instantiation_prepend(parser, clear_instantiators): parser.add_argument("--cls", type=CustomInstantiationBase) add_instantiator(instantiator("first"), CustomInstantiationSub) add_instantiator(instantiator("prepended"), CustomInstantiationBase, subclasses=True, prepend=True) - assert len(_global_class_instantiators) == 2 + assert len(_class_instantiators) == 2 cfg = parser.parse_args(["--cls=CustomInstantiationSub"]) init = parser.instantiate(cfg) assert isinstance(init.cls, CustomInstantiationSub) @@ -715,8 +715,8 @@ def test_custom_instantiation_replace(parser, clear_instantiators): parser.add_argument("--cls", type=CustomInstantiationBase) add_instantiator(first_instantiator, CustomInstantiationBase) add_instantiator(second_instantiator, CustomInstantiationBase) - assert len(_global_class_instantiators) == 1 - assert list(_global_class_instantiators.values())[0] is second_instantiator + assert len(_class_instantiators) == 1 + assert list(_class_instantiators.values())[0] is second_instantiator class CustomInstantiationNested: @@ -1516,7 +1516,7 @@ def test_subclass_unresolved_parameters(parser, subtests): assert init.cls.kwargs == expected.dict_kwargs with subtests.test("print_config"): - out = get_parse_args_stdout(parser, [f"--cfg={json.dumps(config)}", "--print_config"]) + out = get_parse_args_stdout(parser, [f"--cfg={json.dumps(config)}", "--print_cfg"]) data = json_or_yaml_load(out)["cls"] assert data == expected.as_dict() diff --git a/jsonargparse_tests/test_subcommands.py b/jsonargparse_tests/test_subcommands.py index 72c23dd3..e7fbcedf 100644 --- a/jsonargparse_tests/test_subcommands.py +++ b/jsonargparse_tests/test_subcommands.py @@ -185,6 +185,12 @@ def test_subcommands_parse_args_config_explicit_subcommand_arg(subcommands_parse } +def test_subcommands_parse_string_missing_explicit_subcommand(subcommands_parser): + with pytest.raises(ValueError) as ctx: + subcommands_parser.parse_string('{"a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}') + ctx.match("Multiple subcommand settings .* without providing an explicit 'subcommand' key") + + env = { "APP_O1": "o1_env", "APP_A__AP1": "ap1_env", diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py index 34b7eb8a..b12e4ecc 100644 --- a/jsonargparse_tests/test_typehints.py +++ b/jsonargparse_tests/test_typehints.py @@ -96,6 +96,11 @@ def test_add_argument_given_type_and_null_action(parser): assert parser.get_defaults().op1 is None +def test_add_argument_nargs_zero_not_allowed(parser): + with pytest.raises(ValueError, match="does not allow nargs=0"): + parser.add_argument("--op1", type=int, nargs=0) + + @pytest.mark.parametrize("typehint", [Namespace, Optional[Namespace], Union[int, Namespace], List[Namespace]]) def test_namespace_unsupported_as_type(parser, typehint): with pytest.raises(ValueError, match="Namespace .* not supported as a type"): diff --git a/jsonargparse_tests/test_typing.py b/jsonargparse_tests/test_typing.py index 62a19125..4b7b6d0b 100644 --- a/jsonargparse_tests/test_typing.py +++ b/jsonargparse_tests/test_typing.py @@ -129,6 +129,13 @@ def test_restricted_number_already_registered(): restricted_number_type("NewName", float, [("<=", 1), (">=", 0)]) +def test_restricted_number_name_from_restrictions(): + NumberType = restricted_number_type(None, float, [(">=", 0.5), ("<", 2)], join="and") + assert NumberType.__name__ == "float_ge05_and_lt2" + assert 1.0 == NumberType(1) + pytest.raises(ValueError, lambda: NumberType(0.4)) + + def test_restricted_number_not_equal_operator(): NotTwoOrThree = restricted_number_type("NotTwoOrThree", float, [("!=", 2), ("!=", 3)]) assert 1.0 == NotTwoOrThree(1) diff --git a/jsonargparse_tests/test_yaml_comments.py b/jsonargparse_tests/test_yaml_comments.py index 05b5e0f3..b4992a6c 100644 --- a/jsonargparse_tests/test_yaml_comments.py +++ b/jsonargparse_tests/test_yaml_comments.py @@ -535,7 +535,7 @@ def test_subcommand_subclass(parser, subparser): def test_print_config_comments_subclass(parser): - parser.add_argument("--cfg", action="config") + parser.add_argument("--config", action="config") parser.add_argument("--optimizer", type=Optimizer, help="The optimizer.") out = get_parse_args_stdout(parser, [f"--optimizer={__name__}.SGD", "--print_config=comments"]) assert "# Stochastic gradient descent\n init_args:" in out diff --git a/pyproject.toml b/pyproject.toml index d5e6e6a3..51242735 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,9 +81,6 @@ argcomplete = [ ruamel = [ "ruamel.yaml>=0.18.15", ] -ruyaml = [ - "jsonargparse[ruamel]", -] omegaconf = [ "omegaconf>=2.1.1", ]