Skip to content

Commit 321206d

Browse files
authored
Keep parameters as Unvalidated<...> instead of skipping when type not supported (#948)
1 parent 5b1885d commit 321206d

9 files changed

Lines changed: 501 additions & 110 deletions

File tree

CHANGELOG.rst

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,43 @@ Fixed
5959
<https://github.com/mauvilsa/jsonargparse/pull/939>`__).
6060
- ``shtab`` bash completion scripts not escaping choices and type messages, such
6161
that a value containing a single quote, e.g. a ``Literal`` type, produced a
62-
script with invalid syntax (`#497
63-
<https://github.com/mauvilsa/jsonargparse/pull/497>`__).
62+
script with invalid syntax (`#947
63+
<https://github.com/mauvilsa/jsonargparse/pull/947>`__).
6464
- Tests for ``shtab`` completions failing with ``shtab>=1.9.1`` due to a change
6565
in how it quotes the elements of the generated bash arrays. The completion
66-
scripts themselves were not affected (`#497
67-
<https://github.com/mauvilsa/jsonargparse/pull/497>`__).
66+
scripts themselves were not affected (`#947
67+
<https://github.com/mauvilsa/jsonargparse/pull/947>`__).
68+
- ``fail_untyped=True`` failing for mandatory parameters that do have a type,
69+
with an error that says the parameter "does not specify a type". This happened
70+
for any type that jsonargparse can't validate, since the parameter was skipped,
71+
making it indistinguishable from an untyped one. Now ``fail_untyped`` only fails
72+
for parameters that have no type at all (`#948
73+
<https://github.com/mauvilsa/jsonargparse/pull/948>`__).
74+
- Signature parameters with a pydantic type nested in a container, e.g.
75+
``list[HttpUrl]``, being skipped. Only pydantic types given as the entire type
76+
of a parameter were registered for validation (`#948
77+
<https://github.com/mauvilsa/jsonargparse/pull/948>`__).
78+
- ``dump``, and thus ``--print_config``, failing when the value of an ``Any``
79+
typed argument is a class instance that the config format can't represent, e.g.
80+
a default that is an arbitrary object. Now these values are serialized the same
81+
as the instances given for a subclass type, i.e. as an import path when the
82+
value can be imported back, otherwise as a message that says that it was not
83+
serializable (`#948 <https://github.com/mauvilsa/jsonargparse/pull/948>`__).
6884

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

94119

95120
v4.50.0 (2026-07-22)

DOCUMENTATION.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,52 @@ Some notes about this support are:
612612
(python 3.12+) and aliases created with ``typing_extensions.TypeAliasType``.
613613

614614

615+
.. _unvalidated-types:
616+
617+
Unvalidated types
618+
-----------------
619+
620+
When arguments are added from a signature, i.e. :meth:`add_function_arguments
621+
<.ArgumentParser.add_function_arguments>`, :meth:`add_method_arguments
622+
<.ArgumentParser.add_method_arguments>`, :meth:`add_class_arguments
623+
<.ArgumentParser.add_class_arguments>` or a parameter of a :ref:`subclass type
624+
<sub-classes>`, there can be parameters with a type that jsonargparse can't
625+
validate. Instead of skipping these parameters, which would make it impossible
626+
to give them in the command line or a config file, the parameter is added with
627+
only the parts of the type that can't be validated replaced by a type that
628+
accepts any value. In the help these parts are shown as ``Unvalidated<...>``,
629+
keeping the name that the source code has. For example, a class with an ``items:
630+
list[SomeType] = []`` parameter for which ``SomeType`` can't be validated, is
631+
shown in the help as:
632+
633+
.. code-block:: text
634+
635+
--myclass.items ITEMS (type: list[Unvalidated<SomeType>], default: [])
636+
637+
A type or a part of it can't be validated when:
638+
639+
- It failed to resolve, e.g. a missing import or a typo in a postponed
640+
annotation.
641+
- It is not a type that jsonargparse supports.
642+
643+
To know which of the two it is for a given parameter, enable debut level
644+
logging, see :ref:`logging`. The debug log states the reason for each of the
645+
parts of the type that can't be validated.
646+
647+
Note that only these parts accept any value. In the example above, the value
648+
must still be a list, though its items are not validated. Likewise, in a
649+
``Union`` only the subtypes that can't be validated accept any value, the others
650+
are still validated as usual.
651+
652+
Since there is no type to serialize with, a value of one of these parameters
653+
that a config format can't represent, e.g. a default that is an arbitrary
654+
object, is serialized in :meth:`dump <.ArgumentParser.dump>` and
655+
``--print_config`` the same as the instances given for a :ref:`subclass type
656+
<sub-classes>`. That is, as an import path when the value can be imported back,
657+
and otherwise as a message that says that it was not serializable, in which case
658+
a warning is also raised. The same applies to arguments typed as ``Any``.
659+
660+
615661
.. _restricted-numbers:
616662

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

1772+
This section is about parameters whose *name* the resolvers can't determine. For
1773+
parameters that are resolved but have a type that can't be validated, see
1774+
:ref:`unvalidated-types`.
1775+
17261776
Take for example the following parsing and instantiation:
17271777

17281778
.. testsetup:: unresolved

jsonargparse/_signatures.py

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@
3232
is_optional,
3333
is_subclass_container_typehint,
3434
not_required_types,
35-
replace_unresolved_forward_refs,
35+
replace_unvalidatable_typehints,
3636
sequence_origin_types,
3737
strip_required_typehint,
3838
)
3939
from ._util import NoneType, get_import_path, get_private_kwargs, get_typehint_origin, iter_to_set_str
40-
from .typing import _LazyInitBaseClass, register_pydantic_type
40+
from .typing import _LazyInitBaseClass, register_pydantic_types
4141

4242
kinds = inspect._ParameterKind
4343
inspect_empty = inspect._empty
@@ -341,15 +341,17 @@ def _add_signature_parameter(
341341
name = param.name
342342
kind = param.kind
343343
annotation = param.annotation
344-
unresolved_replaced = replace_unresolved_forward_refs(annotation)
345-
if unresolved_replaced is not annotation:
344+
register_pydantic_types(annotation) # before the check of what can be validated
345+
unvalidated: list = []
346+
unvalidatable_replaced = replace_unvalidatable_typehints(annotation, unvalidated)
347+
if unvalidated:
348+
reasons = " ".join(f"{u.name}: {u.reason}." for u in unvalidated)
346349
self.logger.debug(
347-
f'Unable to resolve the type of parameter "{name}" from '
348-
f'"{get_parameter_origins(param.component, param.parent)}": {annotation}. '
349-
"The unresolved parts are shown in the help as Unresolved<...> and accept "
350-
"any value, so the parameter is accepted but its value is not validated."
350+
f'Parameter "{name}" from "{get_parameter_origins(param.component, param.parent)}" has '
351+
f"a type that can't be fully validated: {annotation}. {reasons} These parts are shown "
352+
"in the help as Unvalidated<...> and accept any value without validation."
351353
)
352-
annotation = unresolved_replaced
354+
annotation = unvalidatable_replaced
353355
if default == inspect_empty:
354356
default = param.default
355357
if default == inspect_empty:
@@ -437,35 +439,31 @@ def _add_signature_parameter(
437439
)
438440
if annotation in {str, int, float, bool} or is_subclass(annotation, (str, int, float)) or subclasses_disabled:
439441
kwargs["type"] = annotation
440-
register_pydantic_type(annotation)
441442
elif annotation != inspect_empty:
442-
try:
443-
is_subclass_typehint = ActionTypeHint.is_subclass_typehint(annotation, all_subtypes=False)
444-
is_return_subclass_typehint = ActionTypeHint.is_return_subclass_typehint(annotation)
445-
kwargs["type"] = annotation
446-
sub_add_kwargs: dict = {"fail_untyped": fail_untyped, "sub_configs": sub_configs}
447-
if is_subclass_typehint or is_return_subclass_typehint:
448-
prefix = f"{name}.init_args."
449-
nested_skip = {s[len(prefix) :] for s in skip or [] if s.startswith(prefix)}
450-
sub_add_kwargs["skip"] = nested_skip
451-
else:
452-
register_pydantic_type(annotation)
453-
enable_path = sub_configs and (
454-
is_subclass_typehint
455-
or is_return_subclass_typehint
456-
or is_list_pathlike(annotation)
457-
or is_subclass_container_typehint(annotation)
458-
)
459-
args = ActionTypeHint.prepare_add_argument(
460-
args=args,
461-
kwargs=kwargs,
462-
enable_path=enable_path,
463-
container=container,
464-
logger=self.logger,
465-
sub_add_kwargs=sub_add_kwargs,
466-
)
467-
except ValueError as ex:
468-
self.logger.debug(skip_message + str(ex))
443+
# No need to handle unsupported types here, since replace_unvalidatable_typehints
444+
# already replaced them by a type that accepts any value without validation.
445+
is_subclass_typehint = ActionTypeHint.is_subclass_typehint(annotation, all_subtypes=False)
446+
is_return_subclass_typehint = ActionTypeHint.is_return_subclass_typehint(annotation)
447+
kwargs["type"] = annotation
448+
sub_add_kwargs: dict = {"fail_untyped": fail_untyped, "sub_configs": sub_configs}
449+
if is_subclass_typehint or is_return_subclass_typehint:
450+
prefix = f"{name}.init_args."
451+
nested_skip = {s[len(prefix) :] for s in skip or [] if s.startswith(prefix)}
452+
sub_add_kwargs["skip"] = nested_skip
453+
enable_path = sub_configs and (
454+
is_subclass_typehint
455+
or is_return_subclass_typehint
456+
or is_list_pathlike(annotation)
457+
or is_subclass_container_typehint(annotation)
458+
)
459+
args = ActionTypeHint.prepare_add_argument(
460+
args=args,
461+
kwargs=kwargs,
462+
enable_path=enable_path,
463+
container=container,
464+
logger=self.logger,
465+
sub_add_kwargs=sub_add_kwargs,
466+
)
469467
if "type" in kwargs or "action" in kwargs:
470468
sub_add_kwargs = {
471469
"fail_untyped": fail_untyped,

0 commit comments

Comments
 (0)