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-10-01 16:44:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cyclopts (Old)
 and      /work/SRC/openSUSE:Factory/.python-cyclopts.new.1253 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-cyclopts"

Thu Oct  1 16:44:40 2026 rev:20 rq:1381676 version:5.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cyclopts/python-cyclopts.changes  
2026-09-28 10:40:04.002898812 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-cyclopts.new.1253/python-cyclopts.changes    
    2026-10-01 16:45:27.196037657 +0200
@@ -1,0 +2,20 @@
+Wed Sep 30 08:08:13 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to version 5.1.0:
+  * Support bool union'd with other types: for hints like
+    "bool | list[str]", giving --x no value acts as a flag with
+    implicit value True, including negative --no-x forms
+  * Bad values on such unions now report the value member's
+    specific error instead of a generic one
+  * Support bool alongside an enum.Flag in a union (e.g.
+    "bool | MyFlag"); --x.<member> options can select a non-Flag
+    member
+  * Repeated flags whose implicit value is the whole value raise
+    RepeatArgumentError instead of mixing tokens
+  * Public App/decorator API and typing surface unchanged
+- Spec: require python-rich-rst >= 2.0.1, upstream's declared
+  minimum (stale at 1.3.1 since the 5.0.0 packaging); drop
+  conditional tomli/typing_extensions requires (unused upstream
+  since 5.0.0, requires-python >= 3.11)
+
+-------------------------------------------------------------------

Old:
----
  cyclopts-5.0.0.tar.gz

New:
----
  cyclopts-5.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-cyclopts.spec ++++++
--- /var/tmp/diff_new_pack.z4ukpL/_old  2026-10-01 16:45:28.167078363 +0200
+++ /var/tmp/diff_new_pack.z4ukpL/_new  2026-10-01 16:45:28.169078447 +0200
@@ -18,7 +18,7 @@
 
 %bcond_without libalternatives
 Name:           python-cyclopts
-Version:        5.0.0
+Version:        5.1.0
 Release:        0
 Summary:        Intuitive, easy CLIs based on python type hints
 License:        Apache-2.0
@@ -30,7 +30,7 @@
 BuildRequires:  %{python_module hatchling}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module rich >= 13.6.0}
-BuildRequires:  %{python_module rich-rst >= 1.3.1}
+BuildRequires:  %{python_module rich-rst >= 2.0.1}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  alts
 BuildRequires:  fdupes
@@ -39,12 +39,8 @@
 Requires:       python-attrs >= 23.1.0
 Requires:       python-docstring-parser >= 0.15
 Requires:       python-rich >= 13.6.0
-Requires:       python-rich-rst >= 1.3.1
+Requires:       python-rich-rst >= 2.0.1
 BuildArch:      noarch
-%if %{python_version_nodots} < 311
-Requires:       python-tomli >= 2.0.0
-Requires:       python-typing_extensions >= 4.8.0
-%endif
 %python_subpackages
 
 %description

++++++ cyclopts-5.0.0.tar.gz -> cyclopts-5.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cyclopts-5.0.0/PKG-INFO new/cyclopts-5.1.0/PKG-INFO
--- old/cyclopts-5.0.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/cyclopts-5.1.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: cyclopts
-Version: 5.0.0
+Version: 5.1.0
 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-5.0.0/cyclopts/_convert.py 
new/cyclopts-5.1.0/cyclopts/_convert.py
--- old/cyclopts-5.0.0/cyclopts/_convert.py     2020-02-02 01:00:00.000000000 
+0100
+++ new/cyclopts-5.1.0/cyclopts/_convert.py     2020-02-02 01:00:00.000000000 
+0100
@@ -660,8 +660,8 @@
     elif TypeAliasType is not None and isinstance(type_, TypeAliasType):
         out = convert(type_.__value__, token)
     elif is_union(origin_type):
-        non_none_types = [t for t in inner_types if not is_nonetype(t)]
-        last_error = None
+        value_member = _sole_value_member(inner_types)
+        value_member_error = None
         for t in inner_types:
             try:
                 # When token is a sequence (e.g., list of Token objects), we 
may need
@@ -683,13 +683,13 @@
                 # Propagate immediately since the input matched this type's 
structure.
                 raise
             except Exception as e:
-                if not is_nonetype(t):
-                    last_error = e
+                if t is value_member:
+                    value_member_error = e
         else:
-            if len(non_none_types) == 1 and isinstance(last_error, 
CycloptsError):
-                # ``T | None`` only has one real branch; surface its specific 
error
-                # (e.g. a nested missing field) instead of a generic one.
-                raise last_error
+            # A union with a single value member (``T | None``, ``bool | T``) 
surfaces
+            # that member's specific error (e.g. a nested missing field) 
instead of a generic one.
+            if isinstance(value_member_error, CycloptsError):
+                raise value_member_error
             if isinstance(token, Sequence):
                 raise ValueError  # noqa: TRY004
             raise CoercionError(token=token, target_type=type_)
@@ -1044,6 +1044,62 @@
     return converter, name_transform
 
 
+def _sole_value_member(union_args: tuple[Any, ...]) -> Any:
+    """The union's only member that takes a value, ignoring ``None`` and a 
``bool`` flag; else ``None``.
+
+    For ``bool | list[str]``, ``bool`` is the flag form, so a value that no 
member
+    converts was meant for ``list[str]``. ``bool | None`` has no flag form, so 
its value member is ``bool``.
+    """
+    members = [t for t in union_args if not is_nonetype(t)]
+    if len(members) > 1:
+        members = [t for t in members if resolve(t) is not bool]
+    return members[0] if len(members) == 1 else None
+
+
+def _takes_first_token(type_: Any, token: "Token") -> bool:
+    """Whether ``type_`` accepts ``token`` as its first token.
+
+    Descends through container elements and structured-class fields to the 
single-token
+    type that would consume it, e.g. ``int`` for ``list[tuple[int, int]]``.
+    """
+    while token_count(type_) != (1, False):
+        resolved = resolve(type_)
+        if args := get_args(resolved):
+            type_ = args[0]
+        elif field_infos := get_field_infos(resolved):
+            type_ = next(iter(field_infos.values())).hint
+        else:
+            return False
+    try:
+        _convert(type_, token, converter=None, 
name_transform=default_name_transform)
+    except ValidationError:
+        return True
+    except Exception:
+        return False
+    return True
+
+
+def _unmatched_union_token_count(union_args: tuple[Any, ...], upcoming_tokens: 
"Sequence[Token]") -> tuple[int, bool]:
+    """Token count for a union that no member could convert 
``upcoming_tokens`` into.
+
+    A leading bool word (``--x true pos``) is the ``bool`` flag's value, 
unless the value
+    member takes it as its first token (``"1"`` for ``bool | tuple[int, 
int]``); otherwise
+    the value member's count reports the bad or missing value like the plain 
type does.
+    """
+    value_member = _sole_value_member(union_args)
+    if value_member is None:
+        return 1, False
+    token = upcoming_tokens[0]
+    try:
+        _bool(token.value)
+    except CoercionError:
+        pass
+    else:
+        if not _takes_first_token(value_member, token):
+            return 1, False
+    return token_count(value_member)
+
+
 def _union_conversion(
     union_args: tuple[Any, ...],
     upcoming_tokens: "Sequence[Token]",
@@ -1170,7 +1226,7 @@
             non_none_types = [t for t in args if not is_nonetype(t)]
             if len(non_none_types) != 1:
                 # Not an Optional - it's a real union with multiple non-None 
types
-                return 1, False
+                return _unmatched_union_token_count(args, upcoming_tokens)
             # Optional type - fall through to structural analysis of the 
non-None type
 
     # Check for explicit n_tokens in Parameter annotation before resolving
@@ -1258,7 +1314,7 @@
                 non_none_types = [t for t in args if not is_nonetype(t)]
                 if len(non_none_types) != 1:
                     # Not an Optional - it's a real union with multiple 
non-None types
-                    return 1, False
+                    return _unmatched_union_token_count(args, upcoming_tokens)
                 # Optional type - fall through to structural analysis
 
         # Fallback: use structural analysis.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cyclopts-5.0.0/cyclopts/_version.py 
new/cyclopts-5.1.0/cyclopts/_version.py
--- old/cyclopts-5.0.0/cyclopts/_version.py     2020-02-02 01:00:00.000000000 
+0100
+++ new/cyclopts-5.1.0/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 = '5.0.0'
-__version_tuple__ = version_tuple = (5, 0, 0)
+__version__ = version = '5.1.0'
+__version_tuple__ = version_tuple = (5, 1, 0)
 
 __commit_id__ = commit_id = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cyclopts-5.0.0/cyclopts/argument/_argument.py 
new/cyclopts-5.1.0/cyclopts/argument/_argument.py
--- old/cyclopts-5.0.0/cyclopts/argument/_argument.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/cyclopts-5.1.0/cyclopts/argument/_argument.py   2020-02-02 
01:00:00.000000000 +0100
@@ -666,11 +666,19 @@
             if self.parameter.allow_repeating is False:
                 raise RepeatArgumentError(token=token)
             _, consume_all = self.token_count(token.keys)
+            is_flag_repeat = not token.keys and any(
+                self._is_whole_implicit_value(x.implicit_value)
+                for x in (token, *self.tokens)
+                if x.address == token.address
+            )
             if self.parameter.allow_repeating is True:
-                if not consume_all:
+                if is_flag_repeat:
+                    # A flag replaces the whole value, including every element 
of a multi-token occurrence.
+                    self.tokens = [x for x in self.tokens if x.keys != 
token.keys]
+                elif not consume_all:
                     # "last wins" for scalar types — remove old tokens with 
same address
                     self.tokens = [x for x in self.tokens if x.address != 
token.address]
-            elif not consume_all and not self.parameter.count:
+            elif (not consume_all or is_flag_repeat) and not 
self.parameter.count:
                 raise RepeatArgumentError(token=token)
 
         if self.tokens:
@@ -678,6 +686,26 @@
                 raise MixedArgumentError(argument=self)
         self.tokens.append(token)
 
+    def _is_whole_implicit_value(self, value: Any) -> bool:
+        """Whether a flag's implicit value is this argument's complete value.
+
+        ``False`` for an element of an iterable, like each ``--flag`` of a 
``list[bool]``.
+        """
+        if value is UNSET:
+            return False
+        hint = self.resolved_hint
+        for member in get_args(hint) if is_union(hint) else (hint,):
+            member = resolve_annotated(member)
+            origin = get_origin(member) or member
+            if not isinstance(origin, type):
+                continue
+            try:
+                if isinstance(value, origin):
+                    return True
+            except TypeError:  # e.g. TypedDict and non-runtime Protocols 
reject isinstance.
+                continue
+        return False
+
     @property
     def has_tokens(self) -> bool:
         """This argument, or a child argument, has at least 1 parsed token.""" 
 # noqa: D404
@@ -762,9 +790,7 @@
             if self.parameter.choices:
                 expanded_tokens = self._validate_choices(expanded_tokens)
             for token in expanded_tokens:
-                if token.implicit_value is not UNSET and isinstance(
-                    token.implicit_value, get_origin(self.resolved_hint) or 
self.resolved_hint
-                ):
+                if self._is_whole_implicit_value(token.implicit_value):
                     assert len(expanded_tokens) == 1
                     return token.implicit_value
 
@@ -816,6 +842,20 @@
             # ``--x={}`` on the cli) means "instantiate from defaults", not 
"missing".
             explicit_empty_mapping = False
 
+            if (
+                self._enum_flag_type
+                and is_union(self.resolved_hint)
+                and not any(token.keys for token in self.tokens)
+                and not any(child.has_tokens for child in self.children)
+            ):
+                # e.g. ``bool | MyFlag``; the other members need a chance at 
the tokens.
+                # With ``--x.<key>`` tokens too, the Flag path below combines 
or rejects them.
+                positional_tokens = self.tokens
+                if len(positional_tokens) == 1 and 
self._is_whole_implicit_value(positional_tokens[0].implicit_value):
+                    return positional_tokens[0].implicit_value
+                if positional_tokens:
+                    return safe_converter(self.hint, tuple(positional_tokens))
+
             if self._enum_flag_type:
                 out = self._enum_flag_type(0)
 
@@ -854,6 +894,8 @@
                     positional_tokens = [
                         token for token in positional_tokens if not 
isinstance(token.implicit_value, dict)
                     ]
+                if len(positional_tokens) == 1 and 
self._is_whole_implicit_value(positional_tokens[0].implicit_value):
+                    return positional_tokens[0].implicit_value
                 if positional_tokens:
                     return safe_converter(self.hint, tuple(positional_tokens))
 
@@ -884,26 +926,27 @@
 
             self._run_missing_keys_checker(data)
 
-            if self._enum_flag_type:
+            member = None
+            if self._union_branches and data:
+                # ``instantiate_from_dict`` cannot build a bare ``Union``; 
pick the branch
+                # whose fields accept the supplied data.
+                member = self._resolve_union_member(set(data))
+                if member is None or (member is not self._enum_flag_type and 
out):
+                    # Supplied fields span multiple branches / match no single 
one, or
+                    # sibling-member fields were mixed with enum.Flag values.
+                    raise CoercionError(
+                        msg=f"Cannot determine which 
{get_hint_name(self.hint)} variant the supplied fields belong to.",
+                        argument=self,
+                        target_type=self.hint,
+                    )
+
+            if self._enum_flag_type and member in (None, self._enum_flag_type):
                 out |= enum_flag_from_dict(self._enum_flag_type, data, 
self.parameter.name_transform)
                 if not out:
                     out = UNSET
             elif data or explicit_empty_mapping:
                 # Use resolved_hint to get the actual class type (Optional 
stripped)
-                target_hint = self.resolved_hint
-                if self._union_branches:
-                    # ``instantiate_from_dict`` cannot build a bare ``Union``; 
pick the branch
-                    # whose fields accept the supplied data.
-                    member = self._resolve_union_member(set(data))
-                    if member is None:
-                        # Supplied fields span multiple branches / match no 
single one.
-                        raise CoercionError(
-                            msg=f"Cannot determine which 
{get_hint_name(self.hint)} variant the supplied fields belong to.",
-                            argument=self,
-                            target_type=self.hint,
-                        )
-                    target_hint = member
-                out = instantiate_from_dict(target_hint, data)
+                out = instantiate_from_dict(member or self.resolved_hint, data)
             elif self.required:
                 raise MissingArgumentError(argument=self)  # pragma: no cover
             else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cyclopts-5.0.0/cyclopts/bind.py 
new/cyclopts-5.1.0/cyclopts/bind.py
--- old/cyclopts-5.0.0/cyclopts/bind.py 2020-02-02 01:00:00.000000000 +0100
+++ new/cyclopts-5.1.0/cyclopts/bind.py 2020-02-02 01:00:00.000000000 +0100
@@ -6,9 +6,10 @@
 from collections.abc import Callable, Iterable, Iterator, Sequence
 from contextlib import suppress
 from functools import partial
-from typing import TYPE_CHECKING, Any, NamedTuple
+from typing import TYPE_CHECKING, Any, NamedTuple, get_args
 
 from cyclopts._convert import _bool, create_empty_instance
+from cyclopts.annotations import is_union
 from cyclopts.argument import Argument, ArgumentCollection
 from cyclopts.exceptions import (
     ArgumentOrderError,
@@ -794,6 +795,18 @@
                             pass
                 else:
                     
match.argument.append(CliToken(keyword=match.matched_token, 
implicit_value=match.implicit_value))
+            elif (
+                not cli_values
+                and not match.keys
+                and is_union(match.argument.resolved_hint)
+                and bool in get_args(match.argument.resolved_hint)
+                and (
+                    i + 1 >= len(tokens)
+                    or (not match.argument.parameter.allow_leading_hyphen and 
is_option_like(tokens[i + 1]))
+                )
+            ):
+                # A union containing ``bool`` (e.g. ``bool | list[str]``) 
given no value acts as a flag.
+                match.argument.append(CliToken(keyword=match.matched_token, 
implicit_value=True))
             else:
                 # This is a value-taking option (not a flag or counting 
parameter)
                 # Error only if we're trying to combine multiple value-taking 
options without values

Reply via email to