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