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

Reply via email to