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
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ Added
- Support for methods and ``__init__`` defined with ``functools.partialmethod``,
both for adding their parameters and as import paths of callables (`#665
<https://github.com/mauvilsa/jsonargparse/pull/665>`__).
- Parsing now keeps track of where each value came from. Errors due to a given
value say so, e.g. ``Source: config file config.yaml:3`` followed by that line
of the file, and the new ``provenance`` flag of the print config argument adds
it as comments (`#975 <https://github.com/mauvilsa/jsonargparse/pull/975>`__).

Fixed
^^^^^
- Parsing a list took time quadratic in its number of items (`#975
<https://github.com/mauvilsa/jsonargparse/pull/975>`__).

Changed
^^^^^^^
Expand Down Expand Up @@ -63,6 +72,9 @@ Changed
``tomli`` for reading in python 3.10, instead of the unmaintained ``toml``
package. Dumped toml arrays are now multi-line (`#973
<https://github.com/mauvilsa/jsonargparse/pull/973>`__).
- The ``comments`` flag of the print config argument is now always accepted and
listed in the help, and fails with an informative error when ``ruamel.yaml``
is not installed (`#975 <https://github.com/mauvilsa/jsonargparse/pull/975>`__).

Removed
^^^^^^^
Expand Down
23 changes: 15 additions & 8 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ arguments is given to it, instead of taking them from the command line:
(2.3, <class 'float'>)

If parsing fails, by default the usage is printed and the program exits. With
``exit_on_error=False`` an :class:`.ArgumentError` is raised instead.
``exit_on_error=False`` an :class:`.ArgumentError` is raised instead. When the
failure is due to a given value, the error message says where it came from, e.g.
``Source: config file config.yaml:3`` followed by that line of the file.


Override order
Expand Down Expand Up @@ -1409,13 +1411,18 @@ Serialization
Parsers that have an ``action="config"`` argument also get a ``--print_config``
option. It is useful for tools with many options, to create an initial config
file with all default values. The option accepts one or more flags separated by
comma, e.g. ``--print_config=comments,skip_default``:

- ``comments``: add the help descriptions as YAML comments. Requires the
`ruamel.yaml <https://pypi.org/project/ruamel.yaml>`__ package. The comments
are the descriptions of the groups and arguments of the parser and, for values
that correspond to a class, e.g. the ``init_args`` of a subclass or the fields
of a dataclass, the descriptions from that class.
comma, e.g. ``--print_config=comments,skip_default``. The ``comments`` and
``provenance`` flags require the `ruamel.yaml
<https://pypi.org/project/ruamel.yaml>`__ package:

- ``comments``: add the help descriptions as YAML comments. The comments are the
descriptions of the groups and arguments of the parser and, for values that
correspond to a class, e.g. the ``init_args`` of a subclass or the fields of a
dataclass, the descriptions from that class.
- ``provenance``: add to each value a YAML comment saying where it came from,
i.e. a default, a default config file, a config file, a config string, an
environment variable or a command line argument. For config files parsed as
YAML, the comment includes the line number, e.g. ``# config file config.yaml:3``.
- ``skip_default``: skip entries whose value is the same as the default.
- ``skip_unset``: skip entries that were not given a value, see
:ref:`unset-values`.
Expand Down
24 changes: 16 additions & 8 deletions jsonargparse/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
parser_context,
)
from ._loaders_dumpers import get_loader_exceptions, load_value
from ._namespace import Namespace
from ._namespace import Namespace, ValueSource, copy_provenance, value_source_context
from ._optionals import _get_config_read_mode, ruamel_support
from ._paths import change_to_path_dir
from ._type_checking import ArgumentParser
Expand Down Expand Up @@ -140,6 +140,7 @@ def apply_config(parser, cfg, dest, value) -> None:
cfg_file = parser.parse_path(value, **kwargs)
cfg_merged = merge_config(parser, cfg_file, cfg)
cfg.__dict__.update(cfg_merged.__dict__)
copy_provenance(cfg_merged, cfg)
if cfg.get(dest) is get_parsing_setting("unset_sentinel"):
cfg[dest] = []
cfg[dest].append(cfg_path)
Expand Down Expand Up @@ -182,20 +183,26 @@ def __init__(
help=(
"Print the configuration after applying all other arguments and exit. The optional "
"flags customizes the output and are one or more keywords separated by comma. The "
"supported flags are:%s skip_default, skip_unset."
)
% (" comments," if ruamel_support else ""),
"supported flags are: comments, provenance, skip_default, skip_unset."
),
)

def __call__(self, parser, namespace, value, option_string=None):
kwargs = {"subparser": parser, "key": None, "skip_unset": False, "skip_validation": False}
valid_flags = {"": None, "skip_default": "skip_default", "skip_unset": "skip_unset"}
if ruamel_support:
valid_flags["comments"] = "with_comments"
valid_flags = {
"": None,
"comments": "with_comments",
"provenance": "_with_provenance",
"skip_default": "skip_default",
"skip_unset": "skip_unset",
}
flags = value[0].split(",")
invalid_flags = [f for f in flags if f not in valid_flags]
if len(invalid_flags) > 0:
raise argument_error(f'Invalid option "{invalid_flags[0]}" for {option_string}')
ruamel_flags = [f for f in flags if f in {"comments", "provenance"}]
if ruamel_flags and not ruamel_support:
raise argument_error(f'{option_string} flag "{ruamel_flags[0]}" requires the ruamel.yaml package')
for flag in [f for f in flags if f != ""]:
kwargs[valid_flags[flag]] = True
while hasattr(parser, "parent_parser"):
Expand Down Expand Up @@ -308,7 +315,8 @@ def _load_config(self, value, parser):
cfg = self.resolve_subclass_spec(cfg)
if not isinstance(cfg, (dict, Namespace)):
raise TypeError(f'Parser key "{self.dest}": Unable to load config "{value}"')
with load_config_path_context(cfg_path), change_to_path_dir(cfg_path):
source = None if cfg_path is None else ValueSource("config file", cfg_path, parser.parser_mode)
with load_config_path_context(cfg_path), change_to_path_dir(cfg_path), value_source_context(source):
cfg = parser._apply_actions(cfg, parent_key=self.dest)
return cfg
except (SubclassesDisabledError, ImportDenied) as ex:
Expand Down
18 changes: 16 additions & 2 deletions jsonargparse/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
_GenericAlias,
)

from ._namespace import Namespace
from ._namespace import Namespace, ValueSource, value_source
from ._optionals import (
_set_config_read_mode,
_set_docstring_parse_options,
Expand Down Expand Up @@ -79,6 +79,7 @@ def __bool__(self):
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)
command_line_option: ContextVar[str | None] = ContextVar("command_line_option", default=None)


parser_context_vars = {
Expand All @@ -93,6 +94,7 @@ def __bool__(self):
"nested_links": nested_links,
"applied_instantiation_links": applied_instantiation_links,
"path_dump_preserve_relative": path_dump_preserve_relative,
"command_line_option": command_line_option,
}


Expand All @@ -110,6 +112,14 @@ def parser_context(**kwargs):
context_var.reset(token)


def command_line_source(action: argparse.Action, option_string: str | None = None) -> ValueSource:
"""Returns the source of a value given as a command line argument, named as the option that was used."""
option = command_line_option.get() or option_string # a nested parse refers to the option being parsed
if option is None:
option = max(action.option_strings, key=len) if action.option_strings else action.dest
return ValueSource("command line argument", option)


class ImportDenied(ImportError, ValueError):
"""Raised when an import path given as a value is not allowed.

Expand Down Expand Up @@ -763,7 +773,11 @@ def _check_type_(self, value, **kwargs):
if not hasattr(self, "_check_type_kwargs"):
self._check_type_kwargs = set(inspect.signature(self._check_type).parameters)
kwargs = {k: v for k, v in kwargs.items() if k in self._check_type_kwargs}
return self._check_type(value, **kwargs)
token = value_source.set(None) # adapting a value does not change where it came from
try:
return self._check_type(value, **kwargs)
finally:
value_source.reset(token)


class NonParsingAction(Action):
Expand Down
2 changes: 1 addition & 1 deletion jsonargparse/_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def parse_known_args(self, args=None, namespace=None):


def get_argcomplete_namespace(parser, namespace):
namespace.__class__ = __import__("jsonargparse").Namespace
namespace = __import__("jsonargparse").Namespace(namespace)
return merge_config(parser, parser.get_defaults(skip_validation=True), namespace).as_flat()


Expand Down
Loading