Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-cyclopts for openSUSE:Factory
checked in at 2026-09-21 12:24:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cyclopts (Old)
and /work/SRC/openSUSE:Factory/.python-cyclopts.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cyclopts"
Mon Sep 21 12:24:33 2026 rev:18 rq:1379421 version:4.25.3
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cyclopts/python-cyclopts.changes
2026-09-10 11:52:05.377347827 +0200
+++
/work/SRC/openSUSE:Factory/.python-cyclopts.new.383539/python-cyclopts.changes
2026-09-21 12:24:37.611821422 +0200
@@ -1,0 +2,11 @@
+Mon Sep 21 08:30:49 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 4.25.3:
+ * Fix nested JSON object parsing by routing JSON object tokens
+ through the Argument layer
+ * Recognise a JSON object token for types whose fields consume
+ all tokens
+ * Explain unhashable element types when converting to set or
+ frozenset
+
+-------------------------------------------------------------------
Old:
----
cyclopts-4.25.2.tar.gz
New:
----
cyclopts-4.25.3.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-cyclopts.spec ++++++
--- /var/tmp/diff_new_pack.GNzaMB/_old 2026-09-21 12:24:38.292849885 +0200
+++ /var/tmp/diff_new_pack.GNzaMB/_new 2026-09-21 12:24:38.294849969 +0200
@@ -18,7 +18,7 @@
%bcond_without libalternatives
Name: python-cyclopts
-Version: 4.25.2
+Version: 4.25.3
Release: 0
Summary: Intuitive, easy CLIs based on python type hints
License: Apache-2.0
++++++ cyclopts-4.25.2.tar.gz -> cyclopts-4.25.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-4.25.2/PKG-INFO new/cyclopts-4.25.3/PKG-INFO
--- old/cyclopts-4.25.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/cyclopts-4.25.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.5
Name: cyclopts
-Version: 4.25.2
+Version: 4.25.3
Summary: Intuitive, easy CLIs based on type hints.
Project-URL: Homepage, https://github.com/BrianPugh/cyclopts
Project-URL: Repository, https://github.com/BrianPugh/cyclopts
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-4.25.2/cyclopts/_convert.py
new/cyclopts-4.25.3/cyclopts/_convert.py
--- old/cyclopts-4.25.2/cyclopts/_convert.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-4.25.3/cyclopts/_convert.py 2020-02-02 01:00:00.000000000
+0100
@@ -28,6 +28,7 @@
from cyclopts.annotations import (
ITERABLE_TYPES,
get_annotated_discriminator,
+ get_hint_name,
is_annotated,
is_enum_flag,
is_nonetype,
@@ -374,52 +375,48 @@
)
-def _convert_json(
- type_: Any,
- data: dict,
- field_infos: dict,
- converter: Callable | None,
- name_transform: Callable[[str], str],
-):
- """Convert JSON dict to dataclass with proper type conversion for fields.
+def _convert_json_dict(type_: Any, token: "Token", name_transform:
Callable[[str], str]):
+ """Convert a JSON-object token into ``type_`` via the Argument machinery.
+
+ Building a throwaway :class:`ArgumentCollection` for ``type_`` reuses the
same
+ key-matching, alias handling, nested-collection expansion, and pydantic
+ validation that a top-level structured parameter gets, so a model bound
from a
+ JSON token behaves identically to one bound from ``--key.subkey`` options.
Parameters
----------
type_ : Type
- The dataclass type to create.
- data : dict
- The JSON dictionary containing field values.
- field_infos : dict
- Field information from the dataclass.
- converter : Callable | None
- Optional converter function.
+ The structured type to create.
+ token : Token
+ Token whose value is a JSON object.
name_transform : Callable[[str], str]
Function to transform field names.
Returns
-------
- Instance of type_ with properly converted field values.
+ Instance of type_.
"""
- from cyclopts.token import Token
-
- _validate_json_extra_keys(data, type_)
-
- converted_data = {}
- for field_name, field_info in field_infos.items():
- if field_name in data:
- value = data[field_name]
- # None and str-typed fields pass through unchanged; every other
field is
- # round-tripped through convert() via a Token, re-serializing
dict/list back
- # to JSON so the recursive call receives parseable JSON (scalars
are stringified).
- if value is not None and not
is_class_and_subclass(field_info.hint, str):
- token = Token(value=json.dumps(value) if isinstance(value,
dict | list) else str(value))
- # Always attempt conversion, let errors propagate for
consistency
- converted_value = convert(field_info.hint, [token], converter,
name_transform)
- else:
- converted_value = value
- converted_data[field_name] = converted_value
+ from cyclopts.argument import ArgumentCollection
+ from cyclopts.group import Group
+ from cyclopts.parameter import Parameter
- return type_(**converted_data)
+ # Reuse the originating option name so nested error messages read
``--outer.field``.
+ name = token.keyword if token.keyword and token.keyword.startswith("-")
else "--json"
+ field_info = FieldInfo(names=("json",),
kind=FieldInfo.POSITIONAL_OR_KEYWORD, annotation=type_, required=True)
+ collection = ArgumentCollection._from_type(
+ field_info,
+ (),
+ Parameter(name=name, name_transform=name_transform),
+ group_lookup={},
+ group_arguments=Group.create_default_arguments(),
+ group_parameters=Group.create_default_parameters(),
+ parse_docstring=False,
+ _resolve_groups=False,
+ )
+ argument = collection[0]
+ assert not argument.keys
+ argument.append(token.evolve(keys=()))
+ return argument.convert_and_validate()
def _create_json_decode_error_message(
@@ -716,7 +713,18 @@
gen = zip(*[iter(token)] * count, strict=False)
else:
gen = token
- out = origin_type(convert(inner_types[0], e) for e in gen)
+ try:
+ out = origin_type(convert(inner_types[0], e) for e in gen)
+ except TypeError as e:
+ # Python 3.14 rewords this to "cannot use 'X' as a set element
(unhashable type: 'X')".
+ if origin_type in (set, frozenset) and "unhashable type" in str(e):
+ raise CoercionError(
+ msg=f"{get_hint_name(type_)} requires hashable elements,
but {get_hint_name(inner_type)} is not "
+ "hashable. Use a list instead, or make the element type
hashable (e.g. a frozen dataclass).",
+ token=token[0] if token and isinstance(token[0], Token)
else None,
+ target_type=type_,
+ ) from e
+ raise
elif is_class_and_subclass(type_, Flag):
# TODO: this might never execute since enum.Flag is now handled in
``convert``.
out = convert_enum_flag(type_, token if isinstance(token, Sequence)
else [token], name_transform)
@@ -754,24 +762,18 @@
raise CoercionError(token=token, target_type=type_) from None
else:
# Convert it into a user-supplied class.
- # First check if we have a single token that's a JSON string
+ # A type whose fields consume all tokens (e.g. a sole ``list``
field) arrives as a
+ # one-element sequence; unwrap it so a JSON-object token is still
recognized.
+ if isinstance(token, Sequence) and len(token) == 1 and
isinstance(token[0], Token):
+ token = token[0]
if isinstance(token, Token) and
token.value.strip().startswith("{") and type_ is not str:
try:
data = json.loads(token.value)
- if not isinstance(data, dict):
- # JSON was valid but didn't produce a dict (e.g., it
was an array or scalar)
- raise TypeError # noqa: TRY301
- # Convert dict to dataclass with proper type conversion
- out = _convert_json(type_, data, field_infos, converter,
name_transform)
except json.JSONDecodeError as e:
- # Create helpful error message for invalid JSON
msg = _create_json_decode_error_message(token, type_, e)
raise CoercionError(msg=msg, token=token,
target_type=type_) from e
- except TypeError:
- # Fall back to positional argument parsing
- if not isinstance(token, Sequence):
- token = [token]
- out = _convert_structured_type(type_, token, field_infos,
convert)
+ assert isinstance(data, dict)
+ out = _convert_json_dict(type_, token, name_transform)
else:
# Standard positional argument parsing
if not isinstance(token, Sequence):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-4.25.2/cyclopts/_version.py
new/cyclopts-4.25.3/cyclopts/_version.py
--- old/cyclopts-4.25.2/cyclopts/_version.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-4.25.3/cyclopts/_version.py 2020-02-02 01:00:00.000000000
+0100
@@ -18,7 +18,7 @@
commit_id: str | None
__commit_id__: str | None
-__version__ = version = '4.25.2'
-__version_tuple__ = version_tuple = (4, 25, 2)
+__version__ = version = '4.25.3'
+__version_tuple__ = version_tuple = (4, 25, 3)
__commit_id__ = commit_id = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-4.25.2/cyclopts/argument/_argument.py
new/cyclopts-4.25.3/cyclopts/argument/_argument.py
--- old/cyclopts-4.25.2/cyclopts/argument/_argument.py 2020-02-02
01:00:00.000000000 +0100
+++ new/cyclopts-4.25.3/cyclopts/argument/_argument.py 2020-02-02
01:00:00.000000000 +0100
@@ -366,6 +366,16 @@
return self.parameter.show_default
@property
+ def _explicit_none(self) -> bool:
+ """A lone keyless ``None`` token (a JSON/config ``null``) with no
populated children."""
+ return (
+ len(self.tokens) == 1
+ and not self.tokens[0].keys
+ and self.tokens[0].implicit_value is None
+ and not any(child.has_tokens for child in self.children)
+ )
+
+ @property
def _use_pydantic_type_adapter(self) -> bool:
return bool(
is_pydantic(self.hint)
@@ -734,6 +744,8 @@
out = UNSET
elif self.parameter.count:
out = sum(token.implicit_value for token in self.tokens if
token.implicit_value is not UNSET)
+ elif self._explicit_none:
+ return None
elif not self.children:
positional: list[Token] = []
keyword = {}
@@ -1284,7 +1296,9 @@
if not child.has_tokens:
continue
keys = child.keys[len(self.keys) :]
- if child._accepts_keywords:
+ if child._explicit_none:
+ out[keys[0]] = None
+ elif child._accepts_keywords:
result = child._json()
if result:
out[keys[0]] = result