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
23 changes: 23 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ Added
<https://github.com/mauvilsa/jsonargparse/pull/1002>`__).
- New ``json_or_yaml`` parser mode, which parses as JSON and if that fails as
YAML (`#1004 <https://github.com/mauvilsa/jsonargparse/pull/1004>`__).
- Support for ``Counter``, ``ChainMap`` and ``Iterator`` (a list until
instantiation) types, and for fields of dataclass-like types typed
``Final[X]`` or ``InitVar[X]``, parsed as ``X``. Previously these were not
validated (`#1007 <https://github.com/mauvilsa/jsonargparse/pull/1007>`__).

Fixed
^^^^^
Expand Down Expand Up @@ -154,6 +158,20 @@ Fixed
- ``jsonnet`` parser mode hiding Jsonnet evaluation errors when the config also
parses as YAML, e.g. ``{"a": error "boom"}`` giving ``a`` the string ``error
"boom"`` (`#1004 <https://github.com/mauvilsa/jsonargparse/pull/1004>`__).
- Variadic tuples, e.g. ``tuple[int, *Ts]`` or ``tuple[int, *tuple[str, ...]]``,
and ``*args: *Ts`` failing with ``The code should never reach here`` (python
3.10: ``Unable to add parameter``) or validated as having a fixed number of
elements. Now they are unvalidated (`#1007
<https://github.com/mauvilsa/jsonargparse/pull/1007>`__).
- Signature parameters with an unhashable type, e.g. ``P.args`` of a
``ParamSpec``, failing with ``unhashable type`` instead of being unvalidated
(`#1007 <https://github.com/mauvilsa/jsonargparse/pull/1007>`__).
- A subclass spec with ``init_args`` whose ``class_path`` is the import path of
an instance of the type giving that instance and silently ignoring the
``init_args`` (`#1007
<https://github.com/mauvilsa/jsonargparse/pull/1007>`__).
- ``dict`` arguments with a ``Counter`` default parsed as an empty ``Counter``
(`#1007 <https://github.com/mauvilsa/jsonargparse/pull/1007>`__).

Changed
^^^^^^^
Expand Down Expand Up @@ -238,6 +256,11 @@ Changed
without the ``yaml`` extra makes YAML configs fail. Load errors now say which
format failed to parse (`#1004
<https://github.com/mauvilsa/jsonargparse/pull/1004>`__).
- ``Hashable`` and ``Sized`` are now value types instead of subclass types: a
value is accepted if it is hashable or has a length, the config schema
describes the accepted values, a list given for ``Hashable`` becomes a tuple,
and ``Sized`` rejects subclass specs (`#1007
<https://github.com/mauvilsa/jsonargparse/pull/1007>`__).

Removed
^^^^^^^
Expand Down
24 changes: 14 additions & 10 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -453,18 +453,20 @@ Types can be nested with any complexity. Notes about the support:
- Fully supported types are: ``str``/``LiteralString``, ``bool`` (see
:ref:`boolean-arguments`), ``int``, ``float``, ``Decimal``, ``complex``,
``bytes``/``bytearray`` (Base64 encoding), ``range``, ``list`` (see
:ref:`list-append`), ``Deque``, ``Iterable``, ``Sequence``,
``MutableSequence``, ``Collection``, ``Container``, ``Reversible``,
``Any``/``object``, ``Union``/``Optional`` (see :ref:`union-types`),
``Literal``, ``Type``, ``Enum``, ``PathLike``, ``UUID``, ``Fraction``,
``re.Pattern`` (str only), ``datetime``/``date``/``time`` (ISO 8601),
``timedelta``, the restricted types of :ref:`restricted-numbers` and
:ref:`list-append`), ``Deque``, ``Iterable``, ``Iterator`` (a list until
instantiation), ``Sequence``, ``MutableSequence``, ``Collection``,
``Container``, ``Reversible``, ``Hashable`` (lists become tuples),
``Sized`` (no subclass specs, for an object use a link, see
:ref:`argument-linking`), ``Any``/``object``, ``Union``/``Optional`` (see
:ref:`union-types`), ``Literal``, ``Type``, ``Enum``, ``PathLike``, ``UUID``,
``Fraction``, ``re.Pattern`` (str only), ``datetime``/``date``/``time`` (ISO
8601), ``timedelta``, the restricted types of :ref:`restricted-numbers` and
:ref:`restricted-strings`, and the path and URL types of :ref:`parsing-paths`
and :ref:`parsing-urls`.

- ``dict``, ``Mapping``, ``MutableMapping``, ``MappingProxyType``,
``OrderedDict`` and ``TypedDict`` are supported, but only with ``str`` or
``int`` keys, see :ref:`dict-items`.
``OrderedDict``, ``Counter``, ``ChainMap`` and ``TypedDict`` are supported,
but only with ``str`` or ``int`` keys, see :ref:`dict-items`.

- ``TypedDict`` accepts ``Required`` and ``NotRequired`` to mark single keys as
required or optional, ``ReadOnly`` (PEP `705
Expand Down Expand Up @@ -537,7 +539,8 @@ Types can be nested with any complexity. Notes about the support:
and pydantic's ``BaseModel`` are supported, even when nested. By default they
don't accept subclasses, see :ref:`subclasses-disabled` and
:ref:`enable-disable-subclasses`. A dataclass that also inherits from a normal
class does accept subclasses by default. A pydantic model configured with
class does accept subclasses by default. Fields typed ``Final[X]`` or
``InitVar[X]`` are parsed as ``X``. A pydantic model configured with
``extra`` as ``"allow"`` or ``"ignore"`` accepts keys not in its signature,
which are forwarded to the model on instantiation.

Expand Down Expand Up @@ -689,7 +692,8 @@ 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, e.g. a ``TypeVar`` that stands
for nothing, see :ref:`generic-types`.
for nothing (see :ref:`generic-types`) or a variadic tuple like
``tuple[int, *Ts]``.

The debug log gives the reason for each part, see :ref:`logging`. A parameter
without a type annotation is shown as ``Untyped`` and behaves the same, see
Expand Down
18 changes: 17 additions & 1 deletion jsonargparse/_completions_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import operator
import re
import uuid
from collections import Counter, abc
from enum import Enum
from types import ModuleType
from typing import Any, Callable, Optional, Tuple, Union
Expand Down Expand Up @@ -81,7 +82,7 @@
"present in the config, or can be given as a command line argument."
)

basic_type_schemas = {
basic_type_schemas: dict[Any, dict] = {
bool: {"type": "boolean"},
int: {"type": "integer"},
float: {"type": "number"},
Expand All @@ -90,6 +91,8 @@
dict: {"type": "object"},
list: {"type": "array"},
ModuleType: {"type": "string"},
# in json, numbers, booleans and null are not sized, and subclass specs are rejected
abc.Sized: {"type": ["string", "array", "object"], "not": {"type": "object", "required": ["class_path"]}},
}

uuid_schema = {
Expand Down Expand Up @@ -490,6 +493,8 @@ def typehint_schema(self, typehint, action, key: str) -> dict:
return dict(uuid_schema)
if is_sentinel(typehint):
return {"const": get_import_path(typehint)}
if root is abc.Hashable:
return self.hashable_ref(key)
if typehint in basic_type_schemas:
return dict(basic_type_schemas[typehint])
registered = get_registered_type(typehint)
Expand Down Expand Up @@ -517,6 +522,8 @@ def typehint_schema(self, typehint, action, key: str) -> dict:
return self.items_schema(typehint, action, key, {"type": "array"})
if root in mapping_origin_types:
args: tuple = getattr(typehint, "__args__", ())
if root is Counter:
args = (args[0] if args else Any, int) # a Counter is subscripted only with the type of its keys
if len(args) == 2:
values_schema = self.typehint_schema(args[1], action, key)
if values_schema:
Expand All @@ -528,6 +535,15 @@ def typehint_schema(self, typehint, action, key: str) -> dict:
return self.class_ref(typehint, action, key, subclass=False)
return {}

def hashable_ref(self, key: str) -> dict:
"""References the definition of Hashable, which in json is anything but objects, including nested in arrays."""

def build_def(kwargs: dict) -> dict:
json_types = ["null", "boolean", "integer", "number", "string", "array"]
return {"type": json_types, "items": self.hashable_ref(key)}

return self.def_ref(("hashable",), self.def_name(abc.Hashable), key, {}, build_def)

def alias_schema(self, alias, action, key: str) -> dict:
"""Describes a type alias, which when recursive is a definition that has variants like the ones of classes.

Expand Down
2 changes: 1 addition & 1 deletion jsonargparse/_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def _recreate_branches(data, skip_keys):
new_data = data
if isinstance(data, (Namespace, dict)) and not isinstance(data, OrderedDict):
new_data = type(data)()
for key, val in getattr(data, "__dict__", data).items():
for key, val in (data.__dict__ if isinstance(data, Namespace) else data).items():
if skip_keys is None or key not in skip_keys:
new_data[key] = _recreate_branches(val, skip_keys)
provenance = getattr(data, provenance_key, None)
Expand Down
13 changes: 12 additions & 1 deletion jsonargparse/_parameter_resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from functools import partial, partialmethod
from importlib import import_module
from types import MethodType
from typing import Any, Literal, Union
from typing import Any, Final, Literal, Union

from ._common import (
LoggerProperty,
Expand Down Expand Up @@ -322,10 +322,20 @@ def get_signature_parameters_and_indexes(component, parent, logger):
)
evaluate_postponed_annotations(params, signature_source, parent, logger)
stubs = get_stub_types(params, signature_source, parent, logger)
strip_field_qualifiers(params)
replace_generic_type_vars(params, parent)
return params, args_idx, kwargs_idx, doc_params, stubs


def strip_field_qualifiers(params: ParamList) -> None:
"""Removes a top level Final or InitVar, which qualify a field of a dataclass-like type but not its value."""
for param in params:
if get_typehint_origin(param.annotation) is Final:
param.annotation = param.annotation.__args__[0]
elif isinstance(param.annotation, dataclasses.InitVar):
param.annotation = param.annotation.type


def replace_generic_type_vars(params: ParamList, parent) -> None:
if is_generic_class(parent) and parent.__args__ and getattr(parent.__origin__, "__parameters__", None):
from ._typehints import rebuild_typehint_args
Expand Down Expand Up @@ -1256,6 +1266,7 @@ def get_parameters_from_pydantic_or_attrs(
field_names.append(get_field_names(field, name, function_or_class))
# after the attribute names have been used to resolve the annotations
evaluate_postponed_annotations(params, function_or_class, None, logger)
strip_field_qualifiers(params)
set_param_names_and_aliases(params, field_names)

return params
Expand Down
6 changes: 4 additions & 2 deletions jsonargparse/_postponed_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from types import UnionType
from typing import Any, ForwardRef, TypeAlias, TypeVar, Union, get_type_hints

from ._common import get_generic_origin
from ._typehints import literal_types, mapping_origin_types, sequence_origin_types, tuple_set_origin_types
from ._util import get_typehint_origin

Expand Down Expand Up @@ -403,8 +404,9 @@ def evaluate_postponed_annotations(params, component, parent, logger):
if not (params and any(type_requires_eval(p.annotation) for p in params)):
return
try:
if is_generated_dataclass_init(component, parent):
types = get_types(parent, logger)
dataclass = get_generic_origin(parent) # a subscripted generic dataclass is resolved from its origin
if is_generated_dataclass_init(component, dataclass):
types = get_types(dataclass, logger)
else:
types = get_types(component, logger, parent)
except Exception as ex:
Expand Down
Loading
Loading