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
47 changes: 36 additions & 11 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,43 @@ Fixed
<https://github.com/mauvilsa/jsonargparse/pull/939>`__).
- ``shtab`` bash completion scripts not escaping choices and type messages, such
that a value containing a single quote, e.g. a ``Literal`` type, produced a
script with invalid syntax (`#497
<https://github.com/mauvilsa/jsonargparse/pull/497>`__).
script with invalid syntax (`#947
<https://github.com/mauvilsa/jsonargparse/pull/947>`__).
- Tests for ``shtab`` completions failing with ``shtab>=1.9.1`` due to a change
in how it quotes the elements of the generated bash arrays. The completion
scripts themselves were not affected (`#497
<https://github.com/mauvilsa/jsonargparse/pull/497>`__).
scripts themselves were not affected (`#947
<https://github.com/mauvilsa/jsonargparse/pull/947>`__).
- ``fail_untyped=True`` failing for mandatory parameters that do have a type,
with an error that says the parameter "does not specify a type". This happened
for any type that jsonargparse can't validate, since the parameter was skipped,
making it indistinguishable from an untyped one. Now ``fail_untyped`` only fails
for parameters that have no type at all (`#948
<https://github.com/mauvilsa/jsonargparse/pull/948>`__).
- Signature parameters with a pydantic type nested in a container, e.g.
``list[HttpUrl]``, being skipped. Only pydantic types given as the entire type
of a parameter were registered for validation (`#948
<https://github.com/mauvilsa/jsonargparse/pull/948>`__).
- ``dump``, and thus ``--print_config``, failing when the value of an ``Any``
typed argument is a class instance that the config format can't represent, e.g.
a default that is an arbitrary object. Now these values are serialized the same
as the instances given for a subclass type, i.e. as an import path when the
value can be imported back, otherwise as a message that says that it was not
serializable (`#948 <https://github.com/mauvilsa/jsonargparse/pull/948>`__).

Changed
^^^^^^^
- Signature parameters with a type hint that fails to resolve, e.g. a missing
import or a typo in a postponed annotation, are now accepted instead of the
parameter being skipped or, when mandatory and ``fail_untyped=True``, raising
a ``ValueError``. The unresolved parts accept any value without validation and
are shown in the help as ``Unresolved<...>``, making evident which type failed
to resolve (`#936 <https://github.com/mauvilsa/jsonargparse/pull/936>`__,
`#944 <https://github.com/mauvilsa/jsonargparse/pull/944>`__).
- Signature parameters with a type that jsonargparse can't validate are now
accepted instead of skipped. A type can't be validated when it fails to
resolve, e.g. a missing import or a typo in a postponed annotation, or when it
is not a supported type. Only the parts of the type that can't be validated
accept any value, e.g. a ``list[SomeType]`` still requires a list, and the
subtypes of a ``Union`` that can't be validated are no longer silently
discarded. These parts are shown in the help as ``Unvalidated<...>``, making
evident which type is not validated, and a debug log states the reason. See
the new documentation section :ref:`unvalidated-types` (`#936
<https://github.com/mauvilsa/jsonargparse/pull/936>`__, `#944
<https://github.com/mauvilsa/jsonargparse/pull/944>`__, `#948
<https://github.com/mauvilsa/jsonargparse/pull/948>`__).
- ``Required`` and ``NotRequired`` given as the type of an argument are no
longer shown in the help. Now they must agree with whether the argument is
required, otherwise adding the argument fails (`#937
Expand All @@ -90,6 +111,10 @@ Changed
<https://github.com/mauvilsa/jsonargparse/pull/941>`__).
- The default print config argument name will remain as ``--print_config`` in
v5.0.0, no longer changing as described in the deprecated section of v4.35.0.
- A signature parameter typed as ``jsonargparse.Namespace`` now raises a
``ValueError`` when adding the arguments, instead of the parameter being
silently skipped. ``Namespace`` is only intended for parsing results (`#948
<https://github.com/mauvilsa/jsonargparse/pull/948>`__).


v4.50.0 (2026-07-22)
Expand Down
50 changes: 50 additions & 0 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,52 @@ Some notes about this support are:
(python 3.12+) and aliases created with ``typing_extensions.TypeAliasType``.


.. _unvalidated-types:

Unvalidated types
-----------------

When arguments are added from a signature, i.e. :meth:`add_function_arguments
<.ArgumentParser.add_function_arguments>`, :meth:`add_method_arguments
<.ArgumentParser.add_method_arguments>`, :meth:`add_class_arguments
<.ArgumentParser.add_class_arguments>` or a parameter of a :ref:`subclass type
<sub-classes>`, there can be parameters with a type that jsonargparse can't
validate. Instead of skipping these parameters, which would make it impossible
to give them in the command line or a config file, the parameter is added with
only the parts of the type that can't be validated replaced by a type that
accepts any value. In the help these parts are shown as ``Unvalidated<...>``,
keeping the name that the source code has. For example, a class with an ``items:
list[SomeType] = []`` parameter for which ``SomeType`` can't be validated, is
shown in the help as:

.. code-block:: text

--myclass.items ITEMS (type: list[Unvalidated<SomeType>], default: [])

A type or a part of it can't be validated when:

- It failed to resolve, e.g. a missing import or a typo in a postponed
annotation.
- It is not a type that jsonargparse supports.

To know which of the two it is for a given parameter, enable debut level
logging, see :ref:`logging`. The debug log states the reason for each of the
parts of the type that can't be validated.

Note that only these parts accept any value. In the example above, the value
must still be a list, though its items are not validated. Likewise, in a
``Union`` only the subtypes that can't be validated accept any value, the others
are still validated as usual.

Since there is no type to serialize with, a value of one of these parameters
that a config format can't represent, e.g. a default that is an arbitrary
object, is serialized in :meth:`dump <.ArgumentParser.dump>` and
``--print_config`` the same as the instances given for a :ref:`subclass type
<sub-classes>`. That is, as an import path when the value can be imported back,
and otherwise as a message that says that it was not serializable, in which case
a warning is also raised. The same applies to arguments typed as ``Any``.


.. _restricted-numbers:

Restricted numbers
Expand Down Expand Up @@ -1723,6 +1769,10 @@ used for class instantiation. It is called ``dict_kwargs`` because there are use
cases in which ``**kwargs`` is used just as a dict, thus it also serves that
purpose.

This section is about parameters whose *name* the resolvers can't determine. For
parameters that are resolved but have a type that can't be validated, see
:ref:`unvalidated-types`.

Take for example the following parsing and instantiation:

.. testsetup:: unresolved
Expand Down
72 changes: 35 additions & 37 deletions jsonargparse/_signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@
is_optional,
is_subclass_container_typehint,
not_required_types,
replace_unresolved_forward_refs,
replace_unvalidatable_typehints,
sequence_origin_types,
strip_required_typehint,
)
from ._util import NoneType, get_import_path, get_private_kwargs, get_typehint_origin, iter_to_set_str
from .typing import _LazyInitBaseClass, register_pydantic_type
from .typing import _LazyInitBaseClass, register_pydantic_types

kinds = inspect._ParameterKind
inspect_empty = inspect._empty
Expand Down Expand Up @@ -341,15 +341,17 @@ def _add_signature_parameter(
name = param.name
kind = param.kind
annotation = param.annotation
unresolved_replaced = replace_unresolved_forward_refs(annotation)
if unresolved_replaced is not annotation:
register_pydantic_types(annotation) # before the check of what can be validated
unvalidated: list = []
unvalidatable_replaced = replace_unvalidatable_typehints(annotation, unvalidated)
if unvalidated:
reasons = " ".join(f"{u.name}: {u.reason}." for u in unvalidated)
self.logger.debug(
f'Unable to resolve the type of parameter "{name}" from '
f'"{get_parameter_origins(param.component, param.parent)}": {annotation}. '
"The unresolved parts are shown in the help as Unresolved<...> and accept "
"any value, so the parameter is accepted but its value is not validated."
f'Parameter "{name}" from "{get_parameter_origins(param.component, param.parent)}" has '
f"a type that can't be fully validated: {annotation}. {reasons} These parts are shown "
"in the help as Unvalidated<...> and accept any value without validation."
)
annotation = unresolved_replaced
annotation = unvalidatable_replaced
if default == inspect_empty:
default = param.default
if default == inspect_empty:
Expand Down Expand Up @@ -437,35 +439,31 @@ def _add_signature_parameter(
)
if annotation in {str, int, float, bool} or is_subclass(annotation, (str, int, float)) or subclasses_disabled:
kwargs["type"] = annotation
register_pydantic_type(annotation)
elif annotation != inspect_empty:
try:
is_subclass_typehint = ActionTypeHint.is_subclass_typehint(annotation, all_subtypes=False)
is_return_subclass_typehint = ActionTypeHint.is_return_subclass_typehint(annotation)
kwargs["type"] = annotation
sub_add_kwargs: dict = {"fail_untyped": fail_untyped, "sub_configs": sub_configs}
if is_subclass_typehint or is_return_subclass_typehint:
prefix = f"{name}.init_args."
nested_skip = {s[len(prefix) :] for s in skip or [] if s.startswith(prefix)}
sub_add_kwargs["skip"] = nested_skip
else:
register_pydantic_type(annotation)
enable_path = sub_configs and (
is_subclass_typehint
or is_return_subclass_typehint
or is_list_pathlike(annotation)
or is_subclass_container_typehint(annotation)
)
args = ActionTypeHint.prepare_add_argument(
args=args,
kwargs=kwargs,
enable_path=enable_path,
container=container,
logger=self.logger,
sub_add_kwargs=sub_add_kwargs,
)
except ValueError as ex:
self.logger.debug(skip_message + str(ex))
# No need to handle unsupported types here, since replace_unvalidatable_typehints
# already replaced them by a type that accepts any value without validation.
is_subclass_typehint = ActionTypeHint.is_subclass_typehint(annotation, all_subtypes=False)
is_return_subclass_typehint = ActionTypeHint.is_return_subclass_typehint(annotation)
kwargs["type"] = annotation
sub_add_kwargs: dict = {"fail_untyped": fail_untyped, "sub_configs": sub_configs}
if is_subclass_typehint or is_return_subclass_typehint:
prefix = f"{name}.init_args."
nested_skip = {s[len(prefix) :] for s in skip or [] if s.startswith(prefix)}
sub_add_kwargs["skip"] = nested_skip
enable_path = sub_configs and (
is_subclass_typehint
or is_return_subclass_typehint
or is_list_pathlike(annotation)
or is_subclass_container_typehint(annotation)
)
args = ActionTypeHint.prepare_add_argument(
args=args,
kwargs=kwargs,
enable_path=enable_path,
container=container,
logger=self.logger,
sub_add_kwargs=sub_add_kwargs,
)
if "type" in kwargs or "action" in kwargs:
sub_add_kwargs = {
"fail_untyped": fail_untyped,
Expand Down
Loading