Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ The semantic versioning only considers the public API as described in
paths are considered internals and can change in minor and patch releases.


v4.30.0 (2024-06-??)
--------------------

Added
^^^^^
- Allow adding config argument with ``action="config"`` avoiding need to import
action class.


v4.29.0 (2024-05-24)
--------------------

Expand Down
28 changes: 14 additions & 14 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ Depending on the parse method used (see :class:`.ArgumentParser`) and how the
parser was built, some of the options above might not apply. Parsing of
environment variables must be explicitly enabled, except if using
:py:meth:`.ArgumentParser.parse_env`. If the parser does not have an
:class:`.ActionConfigFile` argument, then there is no parsing of a full config
`action="config"` argument, then there is no parsing of a full config
environment variable or a way to provide a config file from command line.


Expand Down Expand Up @@ -651,11 +651,11 @@ If you import ``from jsonargparse import set_config_read_mode`` and then run
``set_config_read_mode(fsspec_enabled=True)``, the following functions and
classes will also support loading from URLs:
:py:meth:`.ArgumentParser.parse_path`, :py:meth:`.ArgumentParser.get_defaults`
(``default_config_files`` argument), :class:`.ActionConfigFile`,
(``default_config_files`` argument), `action="config"`,
:class:`.ActionJsonSchema`, :class:`.ActionJsonnet` and :class:`.ActionParser`.
This means that a tool that can receive a configuration file via
:class:`.ActionConfigFile` is able to get the content from a URL, thus something
like the following would work:
`action="config"` is able to get the content from a URL, thus something like the
following would work:

.. code-block:: bash

Expand Down Expand Up @@ -1285,11 +1285,11 @@ the following would be observed:

.. doctest:: config

>>> from jsonargparse import ArgumentParser, ActionConfigFile
>>> from jsonargparse import ArgumentParser
>>> parser = ArgumentParser()
>>> parser.add_argument("--lev1.opt1", default="from default 1") # doctest: +IGNORE_RESULT
>>> parser.add_argument("--lev1.opt2", default="from default 2") # doctest: +IGNORE_RESULT
>>> parser.add_argument("--config", action=ActionConfigFile) # doctest: +IGNORE_RESULT
>>> parser.add_argument("--config", action="config") # doctest: +IGNORE_RESULT
>>> cfg = parser.parse_args(["--lev1.opt1", "from arg 1", "--config", "example.yaml", "--lev1.opt2", "from arg 2"])
>>> cfg.lev1.opt1
'from yaml 1'
Expand Down Expand Up @@ -1318,7 +1318,7 @@ string respectively.
Serialization
-------------

Parsers that have an :class:`.ActionConfigFile` argument also include a
Parsers that have an `action="config"` argument also include a
``--print_config`` option. This is useful particularly for command line tools
with a large set of options to create an initial config file including all
default values. If the `ruyaml <https://ruyaml.readthedocs.io>`__ package is
Expand Down Expand Up @@ -2243,7 +2243,7 @@ This yaml could be parsed as follows:
>>> parser = ArgumentParser(parser_mode="omegaconf")
>>> parser.add_argument("--server", type=ServerOptions) # doctest: +IGNORE_RESULT
>>> parser.add_argument("--client", type=ClientOptions) # doctest: +IGNORE_RESULT
>>> parser.add_argument("--config", action=ActionConfigFile) # doctest: +IGNORE_RESULT
>>> parser.add_argument("--config", action="config") # doctest: +IGNORE_RESULT

>>> cfg = parser.parse_args(["--config=example.yaml"])
>>> cfg.client.url
Expand Down Expand Up @@ -2303,9 +2303,9 @@ There is also the :py:meth:`.ArgumentParser.parse_env` function to only parse
environment variables, which might be useful for some use cases in which there
is no command line call involved.

If a parser includes an :class:`.ActionConfigFile` argument, then the
environment variable for this config file will be parsed before all the other
environment variables.
If a parser includes an `action="config"` argument, then the environment
variable for this config file will be parsed before all the other environment
variables.


.. _sub-commands:
Expand Down Expand Up @@ -2450,10 +2450,10 @@ config files to be in jsonnet format instead. Example:

.. testcode:: jsonnet

from jsonargparse import ArgumentParser, ActionConfigFile
from jsonargparse import ArgumentParser

parser = ArgumentParser(parser_mode="jsonnet")
parser.add_argument("--config", action=ActionConfigFile)
parser.add_argument("--config", action="config")
cfg = parser.parse_args(["--config", "example.jsonnet"])

Jsonnet files are commonly parametrized, thus requiring external variables for
Expand Down Expand Up @@ -2511,7 +2511,7 @@ following:
When using the :class:`.ActionParser` class, the value of the node in a config
file can be either the complex node itself, or the path to a file which will be
loaded and parsed with the corresponding inner parser. Naturally using
:class:`.ActionConfigFile` to parse a complete config file will parse the inner
`action="config"` to parse a complete config file will parse the inner
nodes correctly.

Note that when adding ``inner_parser`` a title was given. In the help, the added
Expand Down
6 changes: 4 additions & 2 deletions jsonargparse/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ def set_default_error():

@staticmethod
def _ensure_single_config_argument(container, action):
if is_subclass(action, ActionConfigFile) and any(isinstance(a, ActionConfigFile) for a in container._actions):
raise ValueError("A parser is only allowed to have a single ActionConfigFile argument.")
if (action == "config" or is_subclass(action, ActionConfigFile)) and any(
isinstance(a, ActionConfigFile) for a in container._actions
):
raise ValueError("A parser is only allowed to have a single 'config' argument.")

@staticmethod
def _add_print_config_argument(container, action):
Expand Down
1 change: 1 addition & 0 deletions jsonargparse/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.register("type", None, identity)
self.register("action", "parsers", _ActionSubCommands)
self.register("action", "config", ActionConfigFile)

def add_argument(self, *args, enable_path: bool = False, **kwargs):
"""Adds an argument to the parser or argument group.
Expand Down
13 changes: 6 additions & 7 deletions jsonargparse_tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import pytest

from jsonargparse import (
ActionConfigFile,
ActionParser,
ActionYesNo,
ArgumentError,
Expand All @@ -24,7 +23,7 @@ def test_action_config_file(parser, tmp_cwd):
abs_yaml_file.parent.mkdir()
abs_yaml_file.write_text("val: yaml\n")

parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
parser.add_argument("--val")

cfg = parser.parse_args([f"--cfg={abs_yaml_file}", f"--cfg={rel_yaml_file}", "--cfg", "val: arg"])
Expand All @@ -36,26 +35,26 @@ def test_action_config_file(parser, tmp_cwd):


def test_action_config_file_set_defaults_error(parser):
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
with pytest.raises(ValueError) as ctx:
parser.set_defaults(cfg="config.yaml")
ctx.match("does not accept a default, use default_config_files")


def test_action_config_file_add_argument_default_error(parser):
with pytest.raises(ValueError) as ctx:
parser.add_argument("--cfg", default="config.yaml", action=ActionConfigFile)
parser.add_argument("--cfg", default="config.yaml", action="config")
ctx.match("does not accept a default, use default_config_files")


def test_action_config_file_nested_error(parser):
with pytest.raises(ValueError) as ctx:
parser.add_argument("--nested.cfg", action=ActionConfigFile)
parser.add_argument("--nested.cfg", action="config")
ctx.match("ActionConfigFile must be a top level option")


def test_action_config_file_argument_errors(parser, tmp_cwd):
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
pytest.raises(ArgumentError, lambda: parser.parse_args(["--cfg", '"""']))
pytest.raises(ArgumentError, lambda: parser.parse_args(["--cfg=not-exist"]))
pytest.raises(ArgumentError, lambda: parser.parse_args(["--cfg", '{"k":"v"}']))
Expand Down Expand Up @@ -251,7 +250,7 @@ def test_action_parser_parse_args_subconfig_string(composed_parsers):

def test_action_parser_parse_args_global_config(composed_parsers):
parser, yaml_main = composed_parsers[:2]
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")

expected = {"opt1": "opt1_yaml", "inner2": {"opt2": "opt2_yaml", "inner3": {"opt3": "opt3_yaml"}}}
cfg = parser.parse_args([f"--cfg={yaml_main}"], with_meta=False)
Expand Down
4 changes: 2 additions & 2 deletions jsonargparse_tests/test_argcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import pytest

from jsonargparse import ActionConfigFile, ActionJsonSchema, ActionYesNo
from jsonargparse import ActionJsonSchema, ActionYesNo
from jsonargparse._common import parser_context
from jsonargparse.typing import Email, Path_fr, PositiveFloat, PositiveInt
from jsonargparse_tests.conftest import (
Expand Down Expand Up @@ -102,7 +102,7 @@ def test_stderr_instruction_simple_types(parser, value, expected):

@skip_if_not_posix
def test_action_config_file(parser, tmp_cwd):
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
Path("file1").touch()
Path("config.yaml").touch()

Expand Down
29 changes: 18 additions & 11 deletions jsonargparse_tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def test_parse_args_positional_nargs_plus(parser):
def test_parse_args_positional_config(parser):
parser.add_argument("pos1")
parser.add_argument("pos2", nargs="+")
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
cfg = parser.parse_args(["--cfg", '{"pos2": ["v2", "v3"]}', "v1"])
assert cfg == Namespace(cfg=[None], pos1="v1", pos2=["v2", "v3"])

Expand All @@ -147,7 +147,7 @@ def test_parse_args_choices(parser):


def test_parse_args_choices_config(parser):
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
parser.add_argument("--ch1", choices="ABC")
parser.add_argument("--ch2", type=str, choices=["v1", "v2"])
assert parser.parse_args(["--cfg=ch1: B"]).ch1 == "B"
Expand All @@ -158,7 +158,7 @@ def test_parse_args_choices_config(parser):

def test_parse_args_non_hashable_choice(parser):
choices = {"A": 1, "B": 2}
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
parser.add_argument("--ch1", choices=choices.keys())
with pytest.raises(ArgumentError) as ctx:
parser.parse_args(["--cfg=ch1: [1,2]"])
Expand Down Expand Up @@ -209,7 +209,7 @@ def test_parse_env_nested():

def test_parse_env_config(parser):
parser.env_prefix = "app"
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
parser.add_argument("--l1.num", type=int)
cfg = parser.parse_env({"APP_CFG": '{"l1": {"num": 1}}'})
assert cfg.cfg == [None]
Expand Down Expand Up @@ -344,7 +344,7 @@ def test_precedence_of_sources(tmp_cwd, subtests):
parser = ArgumentParser(prog="app", default_env=True, default_config_files=[default_config_file])
parser.add_argument("--op1", default="from parser default")
parser.add_argument("--op2")
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")

input1_config_file.write_text("op1: from input config file")
input2_config_file.write_text("op2: unused")
Expand Down Expand Up @@ -435,7 +435,7 @@ def test_non_positional_required(parser, subtests):
pytest.raises(ArgumentError, lambda: parser.parse_args(["--req1", "val1"]))

with subtests.test("parse_args config"):
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
cfg = parser.parse_args(["--cfg", '{"req1":"val1","lev1":{"req2":"val2"}}'])
assert cfg == Namespace(cfg=[None], lev1=Namespace(req2="val2"), req1="val1")

Expand Down Expand Up @@ -507,7 +507,7 @@ def test_dump_order(parser, subtests):

@pytest.fixture
def parser_schema_jsonnet(parser, example_parser):
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
parser.add_argument("--subparser", action=ActionParser(parser=example_parser))
if jsonschema_support:
schema = {
Expand Down Expand Up @@ -696,7 +696,7 @@ def test_save_fsspec(example_parser):
@pytest.fixture
def print_parser(parser, subparser):
parser.description = "cli tool"
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", 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")
Expand Down Expand Up @@ -885,10 +885,17 @@ def test_set_get_defaults_multiple(parser, subparser, subtests):
pytest.raises(KeyError, lambda: parser.get_default("v4"))


def test_add_multiple_config_arguments_error(parser):
parser.add_argument("--cfg1", action=ActionConfigFile)
def test_add_config_action_class(parser):
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--op", type=int)
assert parser.parse_args(['--cfg={"op": 2}']).op == 2


@pytest.mark.parametrize("action", [ActionConfigFile, "config"])
def test_add_multiple_config_arguments_error(parser, action):
parser.add_argument("--cfg1", action=action)
with pytest.raises(ValueError) as ctx:
parser.add_argument("--cfg2", action=ActionConfigFile)
parser.add_argument("--cfg2", action=action)
ctx.match("only allowed to have a single")


Expand Down
3 changes: 1 addition & 2 deletions jsonargparse_tests/test_dataclass_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import yaml

from jsonargparse import (
ActionConfigFile,
ArgumentError,
ArgumentParser,
Namespace,
Expand Down Expand Up @@ -404,7 +403,7 @@ class SingleParamChange:


def test_optional_dataclass_single_param_change(parser):
parser.add_argument("--config", action=ActionConfigFile)
parser.add_argument("--config", action="config")
parser.add_argument("--data", type=Optional[SingleParamChange])
config = {"data": {"p1": 1}}
cfg = parser.parse_args([f"--config={config}", "--data.p2=2"])
Expand Down
3 changes: 1 addition & 2 deletions jsonargparse_tests/test_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

from jsonargparse import (
CLI,
ActionConfigFile,
ActionJsonnet,
ArgumentError,
ArgumentParser,
Expand Down Expand Up @@ -340,7 +339,7 @@ def test_ActionPath(tmp_cwd):
output_file.write("file: " + rel_yaml_file + "\ndir: " + str(tmp_cwd) + "\n")

parser = ArgumentParser(exit_on_error=False)
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
with catch_warnings(record=True) as w:
parser.add_argument("--file", action=ActionPath(mode="fr"))
assert_deprecation_warn(
Expand Down
4 changes: 2 additions & 2 deletions jsonargparse_tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from jsonargparse import ActionConfigFile, ActionParser, ActionYesNo, ArgumentParser
from jsonargparse import ActionParser, ActionYesNo, ArgumentParser
from jsonargparse_tests.conftest import get_parser_help


Expand All @@ -23,7 +23,7 @@ def test_help_basics(parser):


def test_help_action_config_file(parser):
parser.add_argument("-c", "--cfg", help="Config in yaml/json.", action=ActionConfigFile)
parser.add_argument("-c", "--cfg", 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
Expand Down
7 changes: 3 additions & 4 deletions jsonargparse_tests/test_jsonnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import yaml

from jsonargparse import (
ActionConfigFile,
ActionJsonnet,
ActionJsonSchema,
ArgumentError,
Expand Down Expand Up @@ -77,7 +76,7 @@ def skip_if_jsonnet_unavailable():
@skip_if_jsonschema_unavailable
def test_parser_mode_jsonnet(tmp_path):
parser = ArgumentParser(parser_mode="jsonnet", exit_on_error=False)
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
parser.add_argument("--param", type=int)
parser.add_argument("--records", action=ActionJsonSchema(schema=records_schema))

Expand All @@ -95,7 +94,7 @@ def test_parser_mode_jsonnet(tmp_path):

def test_parser_mode_jsonnet_import_libsonnet(parser, tmp_cwd):
parser.parser_mode = "jsonnet"
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")
parser.add_argument("--name", type=str, default="Lucky")
parser.add_argument("--prize", type=int, default=100)

Expand Down Expand Up @@ -158,7 +157,7 @@ def test_action_jsonnet(parser):
def test_action_jsonnet_save_config_metadata(parser, tmp_path):
parser.add_argument("--ext_vars", type=dict)
parser.add_argument("--jsonnet", action=ActionJsonnet(ext_vars="ext_vars"))
parser.add_argument("--cfg", action=ActionConfigFile)
parser.add_argument("--cfg", action="config")

jsonnet_file = tmp_path / "example.jsonnet"
jsonnet_file.write_text(example_2_jsonnet)
Expand Down
Loading