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-11-04 19:46:12
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cyclopts (Old)
and /work/SRC/openSUSE:Factory/.python-cyclopts.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cyclopts"
Wed Nov 4 19:46:12 2026 rev:26 rq:1394850 version:5.3.3
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cyclopts/python-cyclopts.changes
2026-10-28 17:47:35.695693232 +0100
+++
/work/SRC/openSUSE:Factory/.python-cyclopts.new.3759290/python-cyclopts.changes
2026-11-04 19:49:56.696668856 +0100
@@ -1,0 +2,10 @@
+Tue Nov 3 18:29:53 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 5.3.3 (PyPI release 2026-11-02):
+ * Fix NewType nested in containers and unions not
+ converting to its underlying type, including enum
+ choices display
+ * Complete default-command positionals alongside
+ subcommands in bash/zsh/fish
+
+-------------------------------------------------------------------
Old:
----
cyclopts-5.3.2.tar.gz
New:
----
cyclopts-5.3.3.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-cyclopts.spec ++++++
--- /var/tmp/diff_new_pack.1Y0nrq/_old 2026-11-04 19:49:57.474701300 +0100
+++ /var/tmp/diff_new_pack.1Y0nrq/_new 2026-11-04 19:49:57.478701467 +0100
@@ -18,7 +18,7 @@
%bcond_without libalternatives
Name: python-cyclopts
-Version: 5.3.2
+Version: 5.3.3
Release: 0
Summary: Intuitive, easy CLIs based on python type hints
License: Apache-2.0
@@ -68,7 +68,7 @@
%pre
%python_libalternatives_reset_alternative cyclopts
-%files %{python_files}
+%files %python_files
%doc README.md
%license LICENSE
%python_alternative %{_bindir}/cyclopts
++++++ cyclopts-5.3.2.tar.gz -> cyclopts-5.3.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.2/PKG-INFO new/cyclopts-5.3.3/PKG-INFO
--- old/cyclopts-5.3.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/cyclopts-5.3.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.5
Name: cyclopts
-Version: 5.3.2
+Version: 5.3.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-5.3.2/cyclopts/_convert.py
new/cyclopts-5.3.3/cyclopts/_convert.py
--- old/cyclopts-5.3.2/cyclopts/_convert.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.3.3/cyclopts/_convert.py 2020-02-02 01:00:00.000000000
+0100
@@ -29,6 +29,7 @@
is_nonetype,
is_union,
resolve,
+ resolve_new_type,
resolve_optional,
)
from cyclopts.exceptions import CoercionError, CycloptsError, ValidationError
@@ -600,7 +601,7 @@
def _convert(
- type_,
+ type_: Any,
token: Union["Token", Sequence["Token"]],
*,
converter: Callable[[Any, str], Any] | None,
@@ -620,6 +621,9 @@
# The flag travels with the converter so recursive calls (e.g. through a
preserved
# ``Optional``/``Union`` member) keep passing the full ``Token`` rather
than ``token.value``.
converter_needs_token = getattr(converter, "_cyclopts_needs_token", False)
+ # A ``NewType`` is an identity function at runtime; convert to its
underlying type instead.
+ # The top-level type is already resolved, but a nested one (e.g.
``list[UserId]``) is not.
+ type_ = resolve_new_type(type_)
if is_annotated(type_):
from cyclopts.parameter import Parameter
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.2/cyclopts/_version.py
new/cyclopts-5.3.3/cyclopts/_version.py
--- old/cyclopts-5.3.2/cyclopts/_version.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.3.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 = '5.3.2'
-__version_tuple__ = version_tuple = (5, 3, 2)
+__version__ = version = '5.3.3'
+__version_tuple__ = version_tuple = (5, 3, 3)
__commit_id__ = commit_id = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.2/cyclopts/annotations.py
new/cyclopts-5.3.3/cyclopts/annotations.py
--- old/cyclopts-5.3.2/cyclopts/annotations.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.3.3/cyclopts/annotations.py 2020-02-02 01:00:00.000000000
+0100
@@ -336,7 +336,7 @@
def contains_enum(hint) -> bool:
"""Whether an ``Enum`` appears anywhere in ``hint`` (through
``Annotated``, unions, iterables, aliases)."""
- hint = resolve_type_alias(hint)
+ hint = resolve(hint, annotated=False, optional=False, required=False)
return is_enum(hint) or any(contains_enum(arg) for arg in get_args(hint)
if arg is not Ellipsis)
@@ -359,6 +359,7 @@
"""
get_choices = partial(get_choices_from_hint, name_transform=name_transform)
choices = []
+ type_ = resolve_new_type(type_)
_origin = get_origin(type_)
if is_enum(type_):
choices.extend(name_transform(x) for x in type_.__members__)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.2/cyclopts/completion/bash.py
new/cyclopts-5.3.3/cyclopts/completion/bash.py
--- old/cyclopts-5.3.2/cyclopts/completion/bash.py 2020-02-02
01:00:00.000000000 +0100
+++ new/cyclopts-5.3.3/cyclopts/completion/bash.py 2020-02-02
01:00:00.000000000 +0100
@@ -5,6 +5,7 @@
"""
import re
+from collections.abc import Sequence
from typing import TYPE_CHECKING
from cyclopts.annotations import is_iterable_type
@@ -122,7 +123,7 @@
]
-def _emit_choice_completion(choices: list[str], indent: str) -> list[str]:
+def _emit_choice_completion(choices: list[str], indent: str, append: bool =
False) -> list[str]:
"""Emit bash that prefix-matches ``$cur`` against an array of choices.
Avoids ``compgen -W`` whitespace tokenization, which mangles choices
@@ -139,6 +140,8 @@
Raw choice strings (already cleaned via ``clean_choice_text``).
indent : str
Indentation prefix.
+ append : bool
+ Add to the existing ``COMPREPLY`` instead of replacing it.
Returns
-------
@@ -150,7 +153,7 @@
return [
f"{indent}local -a _c=({array_body})",
f"{indent}local _q",
- f"{indent}COMPREPLY=()",
+ *([] if append else [f"{indent}COMPREPLY=()"]),
f'{indent}for _x in "${{_c[@]}}"; do',
f'{indent} [[ "$_x" == "${{cur}}"* ]] && {{ printf -v _q \'%q\'
"$_x"; COMPREPLY+=("$_q"); }}',
f"{indent}done",
@@ -473,6 +476,9 @@
local_arguments = data.launcher_arguments + data.own_arguments
positional_args = [arg for arg in local_arguments if arg.index is not None
and arg.show]
positional_args.sort(key=lambda a: a.index if a.index is not None else 0)
+ if commands and any(arg.index is not None for arg in
data.launcher_arguments):
+ # Launcher positionals shift the subcommand's slot, which bash doesn't
model.
+ positional_args = []
lines.append(f"{indent}if [[ ${{cur}} == -* ]]; then")
@@ -490,10 +496,10 @@
data.arguments, commands, positional_args, f"{indent} ", prog_name
)
lines.extend(value_completion_lines)
+ elif positional_args:
+ lines.extend(_generate_positional_completion(positional_args,
f"{indent} ", prog_name, commands))
elif commands:
lines.extend(_emit_choice_completion(commands, f"{indent} "))
- elif positional_args:
- lines.extend(_generate_positional_completion(positional_args,
f"{indent} ", prog_name))
else:
lines.append(f"{indent} COMPREPLY=()")
@@ -502,7 +508,9 @@
return lines
-def _generate_positional_completion(positional_args, indent: str, prog_name:
str) -> list[str]:
+def _generate_positional_completion(
+ positional_args, indent: str, prog_name: str, commands: Sequence[str] = ()
+) -> list[str]:
"""Generate position-aware positional argument completion.
Parameters
@@ -513,6 +521,9 @@
Indentation string.
prog_name : str
Program name (fallback command for dynamic completer callbacks).
+ commands : Sequence[str]
+ Subcommand names, offered alongside the first positional slot (a word
+ there that isn't a subcommand goes to the default command).
Returns
-------
@@ -521,17 +532,19 @@
"""
lines = []
- def _emit_one(argument, body_indent: str) -> list[str]:
+ def _emit_one(argument, body_indent: str, commands: Sequence[str] = ()) ->
list[str]:
if argument.parameter.completer is not None:
- return _emit_completer_completion(body_indent, prog_name)
- choices = argument.get_choices(force=True)
- if choices:
+ out = _emit_completer_completion(body_indent, prog_name)
+ elif choices := argument.get_choices(force=True):
cleaned = [clean_choice_text(c) for c in choices]
- return _emit_choice_completion(cleaned, body_indent)
- compgen_flag =
_map_completion_action_to_bash(get_completion_action(argument.hint))
- if compgen_flag:
- return [f'{body_indent}COMPREPLY=( $(compgen {compgen_flag} --
"${{cur}}") )']
- return [f"{body_indent}COMPREPLY=()"]
+ return _emit_choice_completion([*commands, *cleaned], body_indent)
+ elif compgen_flag :=
_map_completion_action_to_bash(get_completion_action(argument.hint)):
+ out = [f'{body_indent}COMPREPLY=( $(compgen {compgen_flag} --
"${{cur}}") )']
+ else:
+ out = [f"{body_indent}COMPREPLY=()"]
+ if commands:
+ out += _emit_choice_completion(list(commands), body_indent,
append=True)
+ return out
# An iterable positional (``list[X]``, ``set[X]``, or ``*args``) greedily
# consumes all remaining positions starting at its index. The args that
@@ -554,11 +567,15 @@
head = positional_args if rest_idx is None else positional_args[:rest_idx]
rest_owner = None if rest_idx is None else positional_args[rest_idx]
+ if commands and not head and rest_owner is not None:
+ # The rest-owner shares only slot 0 with the subcommands.
+ head = [rest_owner]
+
if not head and rest_owner is not None:
# Rest-owner at index 0 — no case statement needed; the iterable
# answers every position.
lines.extend(_emit_one(rest_owner, indent))
- elif len(head) == 1 and rest_owner is None and head[0].parameter.completer
is None:
+ elif len(head) == 1 and rest_owner is None and head[0].parameter.completer
is None and not commands:
# Single non-iterable positional — simple case. Completer-backed args
# take the case statement below so an already-filled slot doesn't spawn
# a useless ``__complete`` subprocess on every TAB.
@@ -567,7 +584,7 @@
lines.append(f"{indent}case ${{positional_count}} in")
for idx, argument in enumerate(head):
lines.append(f"{indent} {idx})")
- lines.extend(_emit_one(argument, f"{indent} "))
+ lines.extend(_emit_one(argument, f"{indent} ", commands if idx
== 0 else ()))
lines.append(f"{indent} ;;")
lines.append(f"{indent} *)")
if rest_owner is not None:
@@ -668,20 +685,20 @@
if has_cases:
lines.append(f"{indent} *)")
- if commands:
+ if positional_args:
+ lines.extend(_generate_positional_completion(positional_args,
f"{indent} ", prog_name, commands))
+ elif commands:
lines.extend(_emit_choice_completion(commands, f"{indent} "))
- elif positional_args:
- lines.extend(_generate_positional_completion(positional_args,
f"{indent} ", prog_name))
else:
lines.append(f"{indent} COMPREPLY=()")
lines.append(f"{indent} ;;")
lines.append(f"{indent}esac")
else:
lines = []
- if commands:
+ if positional_args:
+ lines.extend(_generate_positional_completion(positional_args,
indent, prog_name, commands))
+ elif commands:
lines.extend(_emit_choice_completion(commands, indent))
- elif positional_args:
- lines.extend(_generate_positional_completion(positional_args,
indent, prog_name))
else:
lines.append(f"{indent}COMPREPLY=()")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.2/cyclopts/completion/fish.py
new/cyclopts-5.3.3/cyclopts/completion/fish.py
--- old/cyclopts-5.3.2/cyclopts/completion/fish.py 2020-02-02
01:00:00.000000000 +0100
+++ new/cyclopts-5.3.3/cyclopts/completion/fish.py 2020-02-02
01:00:00.000000000 +0100
@@ -108,11 +108,14 @@
]
has_nested_commands = any(len(path) > 0 for path in completion_data.keys())
- if has_nested_commands:
+ has_positional_choices = _any_positional_choices(completion_data)
+ if has_nested_commands or has_positional_choices:
lines.extend(_generate_helper_functions(prog_name, completion_data))
lines.append("")
- if _any_nested_positional_choices(completion_data):
+ if has_positional_choices or any(
+ _subcommands_share_first_slot(path, data) for path, data in
completion_data.items()
+ ):
lines.extend(_generate_positional_index_helper(prog_name,
completion_data))
lines.append("")
@@ -128,17 +131,15 @@
return "\n".join(lines) + "\n"
-def _any_nested_positional_choices(completion_data: dict[tuple[str, ...],
CompletionData]) -> bool:
- """Whether any nested command path has a positional argument with choices
or a completer.
+def _any_positional_choices(completion_data: dict[tuple[str, ...],
CompletionData]) -> bool:
+ """Whether any command path has a positional argument with choices or a
completer.
The positional-index helper is only needed when there is at least one
- nested positional that emits a per-slot rule (a choice list or a dynamic
+ positional that emits a per-slot rule (a choice list or a dynamic
completer) — without either, fish's default file fallback already produces
sensible completions.
"""
- for path, data in completion_data.items():
- if not path:
- continue
+ for data in completion_data.values():
for argument in data.arguments:
if argument.index is None or not argument.show:
continue
@@ -147,6 +148,20 @@
return False
+def _subcommands_share_first_slot(command_path: tuple[str, ...], data:
CompletionData) -> bool:
+ """Whether a nested path's subcommands compete with its default command's
positionals.
+
+ Subcommand names are then only valid at the first positional slot. Paths
with
+ meta-launcher positionals are left alone: those shift the subcommand's
slot.
+ """
+ return (
+ bool(command_path)
+ and any(not name.startswith("-") for rc in data.commands for name in
rc.names)
+ and any(arg.index is not None for arg in data.own_arguments)
+ and not any(arg.index is not None for arg in data.launcher_arguments)
+ )
+
+
def _escape_fish_string(text: str) -> str:
r"""Escape single quotes for fish strings."""
return text.replace("'", r"'\''")
@@ -227,8 +242,8 @@
[
" set -l skip_next 0",
" # Extract command words (only real subcommand names) from
command line",
- " for i in (seq 2 (count $cmd))",
- " set -l word $cmd[$i]",
+ " set -e cmd[1]",
+ " for word in $cmd",
" if test $skip_next -eq 1",
" set skip_next 0",
" continue",
@@ -249,6 +264,8 @@
" if test (count $subcommands) -ne (count $argv)",
" return 1",
" end",
+ " # macOS ``seq 1 0`` counts down instead of yielding nothing.",
+ " test (count $argv) -eq 0; and return 0",
" for i in (seq 1 (count $argv))",
" if test $subcommands[$i] != $argv[$i]",
" return 1",
@@ -300,8 +317,8 @@
" set -l skip_next 0",
" set -l consumed_path 0",
" set -l count 0",
- " for i in (seq 2 (count $cmd))",
- " set -l word $cmd[$i]",
+ " set -e cmd[1]",
+ " for word in $cmd",
" if test $skip_next -eq 1",
" set skip_next 0",
" continue",
@@ -421,7 +438,12 @@
lines = []
condition = _get_condition_for_path(command_path, prog_name)
- lines.extend(_generate_subcommand_completions(data, command_path,
prog_name, condition))
+ subcommand_condition = condition
+ if _subcommands_share_first_slot(command_path, data):
+ subcommand_condition = _get_condition_for_path(
+ command_path, prog_name, extra=f"test
(__fish_{prog_name}_positional_index {len(command_path)}) = 0"
+ )
+ lines.extend(_generate_subcommand_completions(data, command_path,
prog_name, subcommand_condition))
keyword_args = [arg for arg in data.arguments if not
arg.is_positional_only() and arg.show]
if keyword_args or help_flags or version_flags:
@@ -440,13 +462,7 @@
command_path: tuple[str, ...],
prog_name: str,
) -> list[str]:
- """Emit per-position choice rules for positional args at a nested path.
-
- Only nested paths (``len(command_path) > 0``) are handled. At root, the
- natural fish gate ``__fish_use_subcommand`` flips false as soon as the
- first positional is typed, so a position-N rule for N>0 can't be
- expressed cleanly there; root-level positional values (rare in practice)
- fall back to fish's default behavior.
+ """Emit per-position choice rules for the positional args at a command
path.
Positionals without choices (e.g. ``Path``) emit nothing — fish's
default file completion kicks in automatically at positions where no
@@ -458,9 +474,6 @@
the first iterable contributes a rest rule; later iterables remain
reachable via their ``--name`` keyword forms.
"""
- if not command_path:
- return []
-
# Exclude inherited (ancestor-meta) positionals: they were consumed before
# this path's command name, so they must not claim a slot here. (This PR
# does not attempt the full #759-style subcommand shifting for fish.)
@@ -488,8 +501,7 @@
helper_fn = f"__fish_{prog_name}_positional_index"
using_cmd_fn = f"__fish_{prog_name}_using_command"
path_len = len(command_path)
- escaped_commands = " ".join(_escape_fish_string(cmd) for cmd in
command_path)
- base_predicate = f"{using_cmd_fn} {escaped_commands}"
+ base_predicate = " ".join([using_cmd_fn, *(_escape_fish_string(cmd) for
cmd in command_path)])
lines: list[str] = []
header_emitted = False
@@ -497,7 +509,7 @@
def _ensure_header() -> None:
nonlocal header_emitted
if not header_emitted:
- lines.append(f"# Positionals for: {' '.join(command_path)}")
+ lines.append(f"# Positionals for: {' '.join(command_path) or
'(root)'}")
header_emitted = True
# Fish evaluates ``-a`` command substitutions even while the user is typing
@@ -779,7 +791,7 @@
return lines
-def _get_condition_for_path(command_path: tuple[str, ...], prog_name: str) ->
str:
+def _get_condition_for_path(command_path: tuple[str, ...], prog_name: str,
extra: str = "") -> str:
"""Generate fish condition string for a command path.
Parameters
@@ -788,6 +800,8 @@
Command path (empty for root).
prog_name : str
Program name.
+ extra : str
+ Additional fish predicate joined onto a nested path's condition with
``and``.
Returns
-------
@@ -799,4 +813,6 @@
func_name = f"__fish_{prog_name}_using_command"
escaped_commands = " ".join(_escape_fish_string(cmd) for cmd in
command_path)
+ if extra:
+ return f"-n '{func_name} {escaped_commands}; and {extra}'"
return f"-n '{func_name} {escaped_commands}'"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.2/cyclopts/completion/zsh.py
new/cyclopts-5.3.3/cyclopts/completion/zsh.py
--- old/cyclopts-5.3.2/cyclopts/completion/zsh.py 2020-02-02
01:00:00.000000000 +0100
+++ new/cyclopts-5.3.3/cyclopts/completion/zsh.py 2020-02-02
01:00:00.000000000 +0100
@@ -310,6 +310,7 @@
argument: "Argument",
help_format: str,
indent_str: str,
+ prog_name: str = "",
) -> list[str]:
"""Generate _describe-based completion for a single positional argument.
@@ -321,6 +322,8 @@
Help text format.
indent_str : str
Indentation string.
+ prog_name : str
+ Program name; enables the runtime helper for a completer-backed
argument.
Returns
-------
@@ -332,7 +335,9 @@
# Check for choices (Literal/Enum types)
choices = argument.get_choices(force=True)
- if choices:
+ if argument.parameter.completer is not None and prog_name:
+ lines.append(f"{indent_str}{_completer_action_fn(prog_name)}")
+ elif choices:
# Generate choices array with descriptions
escaped_choices = [_escape_completion_choice(clean_choice_text(c)) for
c in choices]
lines.append(f"{indent_str}local -a choices")
@@ -520,6 +525,14 @@
# Positional specs and the subcommand's word slot.
command_position = 1
+ # A default command's positionals compete with the subcommands for the
first
+ # word; a word there that isn't a subcommand goes to the default command.
+ shares_first_slot = (
+ has_non_flag_commands
+ and not launcher_positionals
+ and bool(own_positionals)
+ and (own_positionals[0].index == 0 or _is_variadic(own_positionals[0]))
+ )
if has_non_flag_commands:
# The subcommand slot shifts past the fixed launcher positionals only
# when every one of them is required (occupies a definite word) and
@@ -562,6 +575,7 @@
# Add positionals BEFORE options to prioritize them in completion
args_specs = positional_specs + args_specs
+ keyword_specs = list(args_specs)
if has_non_flag_commands:
args_specs.append(f"'{command_position}: :->cmds'")
args_specs.append("'*::arg:->args'")
@@ -604,6 +618,10 @@
lines.append(f"{indent_str} {cmd}")
lines.append(f"{indent_str} )")
lines.append(f"{indent_str} _describe -t commands 'command'
commands")
+ if shares_first_slot:
+ lines.extend(
+ _generate_describe_completion(own_positionals[0],
data.help_format, f"{indent_str} ", prog_name)
+ )
lines.append(f"{indent_str} ;;")
lines.append(f"{indent_str} args)")
@@ -634,6 +652,25 @@
lines.extend(sub_lines)
lines.append(f"{indent_str} ;;")
+ if shares_first_slot:
+ # ``$words[1]`` is the default command's first positional here, so
+ # ``_arguments`` slot N is its positional index N.
+ later_specs = _generate_nested_positional_specs(
+ [arg for arg in own_positionals if _is_variadic(arg) or
(arg.index or 0) >= 1],
+ data.help_format,
+ offset=-1,
+ prog_name=prog_name,
+ )
+ fallback_specs = later_specs + keyword_specs
+ lines.append(f"{indent_str} *)")
+ lines.extend(_generate_eq_form_prepass(keyword_args,
f"{indent_str} ", prog_name=prog_name))
+ if fallback_specs:
+ lines.append(f"{indent_str} _arguments \\")
+ for spec in fallback_specs[:-1]:
+ lines.append(f"{indent_str} {spec} \\")
+ lines.append(f"{indent_str} {fallback_specs[-1]}")
+ lines.append(f"{indent_str} ;;")
+
lines.append(f"{indent_str} esac")
lines.append(f"{indent_str} ;;")
lines.append(f"{indent_str}esac")