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-10 14:39:22
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cyclopts (Old)
and /work/SRC/openSUSE:Factory/.python-cyclopts.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cyclopts"
Tue Nov 10 14:39:22 2026 rev:27 rq:1396273 version:5.4.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cyclopts/python-cyclopts.changes
2026-11-04 19:49:56.696668856 +0100
+++
/work/SRC/openSUSE:Factory/.python-cyclopts.new.3759290/python-cyclopts.changes
2026-11-10 14:40:01.323565082 +0100
@@ -1,0 +2,16 @@
+Mon Nov 9 08:52:13 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 5.4.0 (PyPI release 2026-11-08):
+ * New App(argfile_prefix="@"): read CLI args from files,
+ one per line; files may reference other files
+ * Meta apps: unknown combined short flag reported whole;
+ meta option in combined token takes all values;
+ combined short flags with = no longer misbind
+ * App run as python -m named after module in help/usage
+ * Parse pydantic URL and other custom-schema types as
+ a single value instead of recursing
+ * CoercionError from custom converters printable
+ instead of AssertionError
+ * Shell completion skips Parameter(parse=False) args
+
+-------------------------------------------------------------------
Old:
----
cyclopts-5.3.3.tar.gz
New:
----
cyclopts-5.4.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-cyclopts.spec ++++++
--- /var/tmp/diff_new_pack.7K778Q/_old 2026-11-10 14:40:02.278605052 +0100
+++ /var/tmp/diff_new_pack.7K778Q/_new 2026-11-10 14:40:02.279605094 +0100
@@ -18,7 +18,7 @@
%bcond_without libalternatives
Name: python-cyclopts
-Version: 5.3.3
+Version: 5.4.0
Release: 0
Summary: Intuitive, easy CLIs based on python type hints
License: Apache-2.0
++++++ cyclopts-5.3.3.tar.gz -> cyclopts-5.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/PKG-INFO new/cyclopts-5.4.0/PKG-INFO
--- old/cyclopts-5.3.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/cyclopts-5.4.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.5
Name: cyclopts
-Version: 5.3.3
+Version: 5.4.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.3.3/cyclopts/_convert.py
new/cyclopts-5.4.0/cyclopts/_convert.py
--- old/cyclopts-5.3.3/cyclopts/_convert.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.4.0/cyclopts/_convert.py 2020-02-02 01:00:00.000000000
+0100
@@ -27,6 +27,7 @@
is_annotated,
is_enum_flag,
is_nonetype,
+ is_pydantic_leaf,
is_union,
resolve,
resolve_new_type,
@@ -820,6 +821,16 @@
try:
if token.implicit_value is not UNSET:
out = token.implicit_value
+ elif converter is None and is_pydantic_leaf(type_) and
"pydantic" in sys.modules:
+ # Validate like pydantic would; the constructor may not
take the string form.
+ # Non-pydantic libraries (e.g. ``whenever``) may define
the hook without pydantic installed.
+ import pydantic
+
+ try:
+ out =
pydantic.TypeAdapter(type_).validate_python(token.value)
+ except pydantic.ValidationError as e:
+ # ``CoercionError`` appends the pydantic reason from
``__cause__``.
+ raise CoercionError(token=token, target_type=type_)
from e
elif converter is None:
out = _converters.get(type_, type_)(token.value) #
pyright: ignore[reportOptionalCall]
elif converter_needs_token:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/_version.py
new/cyclopts-5.4.0/cyclopts/_version.py
--- old/cyclopts-5.3.3/cyclopts/_version.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.4.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.3.3'
-__version_tuple__ = version_tuple = (5, 3, 3)
+__version__ = version = '5.4.0'
+__version_tuple__ = version_tuple = (5, 4, 0)
__commit_id__ = commit_id = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/annotations.py
new/cyclopts-5.4.0/cyclopts/annotations.py
--- old/cyclopts-5.3.3/cyclopts/annotations.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.4.0/cyclopts/annotations.py 2020-02-02 01:00:00.000000000
+0100
@@ -77,6 +77,14 @@
return hasattr(hint, "__pydantic_core_schema__")
+def is_pydantic_leaf(hint) -> bool:
+ """Check if a type is a non-model Pydantic custom-schema type (``AnyUrl``,
``ByteSize``, etc.).
+
+ Pydantic validates these from a single value, so their ``__init__`` isn't
a set of fields.
+ """
+ return hasattr(hint, "__get_pydantic_core_schema__") and not
is_pydantic(hint)
+
+
def is_pydantic_secret(hint) -> bool:
"""Check if a type is a Pydantic secret type (SecretStr, SecretBytes,
Secret, etc.)."""
return (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/bind.py
new/cyclopts-5.4.0/cyclopts/bind.py
--- old/cyclopts-5.3.3/cyclopts/bind.py 2020-02-02 01:00:00.000000000 +0100
+++ new/cyclopts-5.4.0/cyclopts/bind.py 2020-02-02 01:00:00.000000000 +0100
@@ -6,6 +6,7 @@
from collections.abc import Callable, Iterable, Iterator, Sequence
from contextlib import suppress
from functools import partial
+from pathlib import Path
from typing import TYPE_CHECKING, Any, NamedTuple, get_args
from cyclopts._convert import _bool, _none, create_empty_instance
@@ -63,6 +64,42 @@
return tokens
+def expand_argfiles(tokens: list[str], prefix: str | None,
end_of_options_delimiter: str) -> list[str]:
+ """Replace each ``<prefix><path>`` token with the lines of the file at
``path``.
+
+ Mirrors :mod:`argparse`'s ``fromfile_prefix_chars``: one argument per
line, paths
+ relative to the current directory, and argument files may reference other
argument files.
+ Tokens after ``end_of_options_delimiter`` are not expanded.
+ """
+ if not prefix:
+ return tokens
+
+ expanded: list[str] = []
+ end_of_options = False
+
+ def expand(tokens: Iterable[str], parents: tuple[Path, ...]) -> None:
+ nonlocal end_of_options
+ for token in tokens:
+ if end_of_options or not token.startswith(prefix) or token ==
prefix:
+ if end_of_options_delimiter and token ==
end_of_options_delimiter:
+ end_of_options = True
+ expanded.append(token)
+ continue
+
+ path = Path(token[len(prefix) :])
+ resolved = path.resolve()
+ if resolved in parents:
+ raise TokenizationError(msg=f"Argument file {str(path)!r} is
referenced recursively.")
+ try:
+ lines = path.read_text(encoding="utf-8").splitlines()
+ except OSError as e:
+ raise TokenizationError(msg=f"Cannot read argument file
{str(path)!r}: {e.strerror}.") from e
+ expand(lines, (*parents, resolved))
+
+ expand(tokens, ())
+ return expanded
+
+
class _ProbeResult(NamedTuple):
"""Result of :func:`_probe_unclaimed`."""
@@ -148,9 +185,10 @@
meta_part: str | None
"""Reconstructed token for the meta's keyword stream, or ``None``."""
- meta_awaits_value: bool
- """A meta value-taking option ended the token with no attached value; the
- next stream token is its value."""
+ meta_value_option: str | None
+ """The meta value-taking option that ended the token, with its attached
+ value (e.g. ``-uroot``), or ``None``. Bare (``-u``) means its values come
+ from the following stream tokens."""
def _is_combined_short_candidate(
@@ -160,21 +198,20 @@
"""Whether ``token`` should be resolved by the merged combined-short scan.
Mirrors the gate in :func:`_parse_kw_and_flags`'s combined-short branch:
- single-dash, multi-character, non-numeric, no ``=``, and not an exact match
- of any collection (exact matches are ordinary options, handled by the
- normal probing paths).
+ single-dash, multi-character, non-numeric, and neither the token nor its
+ ``=``-prefix an exact match of any collection (exact matches are ordinary
+ options, handled by the normal probing paths).
"""
- if "=" in token:
- return False
if not (token.startswith("-") and not token.startswith("--") and
len(token) > 2):
return False
if not is_option_like(token, allow_numbers=False):
return False
+ option = token.split("=", 1)[0]
for collection in collections:
if collection is None:
continue
try:
- collection.match(token)
+ collection.match(option)
except ValueError:
continue
return False
@@ -185,17 +222,22 @@
token: str,
child_collection: "ArgumentCollection | None",
meta_collection: ArgumentCollection,
+ intermediate_collections: Sequence[ArgumentCollection] = (),
) -> _MergedScanResult:
"""Resolve a combined short-option token against the MERGED flag namespace.
Fallthrough mode presents the user with one flat flag namespace; this scan
makes that literal: a single GNU left-to-right pass where each character is
matched against the child first (child wins name collisions after its
- command), then the meta. Standard rules apply to the merged table:
+ command), then the meta, then any ``intermediate_collections`` (meta levels
+ between this meta and the child, which receive their characters via the
+ child's stream). The meta outranks intermediates, as it does for
+ standalone options. Standard rules apply to the merged table:
- a flag/count match consumes its character;
- the first value-taking match absorbs the rest of the token as an attached
- value (or, with no remainder, the next stream token) and ends the scan;
+ value and ends the scan (any further values come from the following
+ stream tokens, claimed by the caller);
- an unknown character is routed to the child's stream, which reports it.
Characters are routed to per-owner reconstructed tokens in scan order, so
@@ -204,12 +246,16 @@
chars = token.lstrip("-")
child_chars: list[str] = []
meta_chars: list[str] = []
- meta_awaits_value = False
+ meta_value_option: str | None = None
position = 0
while position < len(chars):
char = chars[position]
matched = None
- for owner_chars, collection in ((child_chars, child_collection),
(meta_chars, meta_collection)):
+ for owner_chars, collection in (
+ (child_chars, child_collection),
+ (meta_chars, meta_collection),
+ *((child_chars, collection) for collection in
intermediate_collections),
+ ):
if collection is None:
continue
try:
@@ -219,6 +265,10 @@
matched = (owner_chars, argument, implicit)
break
if matched is None:
+ if position == 0:
+ # Not a combined-short group (GNU groups must begin with a
known
+ # option); the child keeps the token intact. See issue #932.
+ return _MergedScanResult(token, None, None)
child_chars.append(char) # Unknown flag; the child's parse
reports it.
position += 1
continue
@@ -229,15 +279,14 @@
continue
# Value-taking option: the rest of the token is its attached value.
remainder = chars[position + 1 :]
- if remainder:
- owner_chars.extend(remainder)
- elif owner_chars is meta_chars:
- meta_awaits_value = True
+ owner_chars.extend(remainder)
+ if owner_chars is meta_chars:
+ meta_value_option = f"-{char}{remainder}"
break
return _MergedScanResult(
"-" + "".join(child_chars) if child_chars else None,
"-" + "".join(meta_chars) if meta_chars else None,
- meta_awaits_value,
+ meta_value_option,
)
@@ -254,6 +303,7 @@
*,
parse_mode: str,
end_of_options_delimiter: str = "--",
+ intermediate_collections: Sequence[ArgumentCollection] = (),
) -> tuple[list[str], list[str], int | None]:
"""Scope tokens between a meta-app level and the subcommand it forwards to.
@@ -284,6 +334,10 @@
``"fallthrough"`` or ``"strict"``.
end_of_options_delimiter: str
Token that marks the end of options.
+ intermediate_collections: Sequence[ArgumentCollection]
+ Collections of the meta levels between this meta and the child (a meta
+ of a meta). Their short flags count as known in the combined-short
+ scan; their characters stay in the child's stream.
Returns
-------
@@ -408,10 +462,10 @@
synthetics = child_unclaimed_by_position[position]
partially_claimed = not (len(synthetics) == 1 and synthetics[0] ==
original)
if not partially_claimed and not _is_combined_short_candidate(
- original, (child_collection, meta_collection)
+ original, (child_collection, *intermediate_collections,
meta_collection)
):
continue
- scan = _merged_combined_short_scan(original, child_collection,
meta_collection)
+ scan = _merged_combined_short_scan(original, child_collection,
meta_collection, intermediate_collections)
if scan.meta_part is None:
if partially_claimed:
# Nothing for the meta; keep the child's own reading and
@@ -421,22 +475,45 @@
scanned_positions.add(position)
newly_replaced[position] = scan.child_part
bubbled.append(scan.meta_part)
- if scan.meta_awaits_value:
- # The meta's value-taking option ended the token; the NEXT
- # stream token is its value — if the child didn't claim it.
- value_position = position + 1
- value_synthetics =
child_unclaimed_by_position.get(value_position)
- if (
- value_position < delimiter_pos
- and value_synthetics is not None
- and len(value_synthetics) == 1
- and value_synthetics[0] == stream_tokens[value_position]
- ):
- scanned_positions.add(value_position)
- newly_replaced[value_position] = None
- bubbled.append(stream_tokens[value_position])
- # Otherwise the meta's real parse raises the proper
- # missing-value error.
+ if scan.meta_value_option is None:
+ continue
+ # The meta's value-taking option may consume following stream
+ # tokens the child left wholly unclaimed (as many as its token
+ # count, or more with ``consume_multiple``). Probe that option
+ # alone, so no other meta option claims a follower the child's
+ # flags should win.
+ follower_positions: list[int] = []
+ follower_position = position + 1
+ while follower_position < delimiter_pos and
child_unclaimed_by_position.get(follower_position) == [
+ stream_tokens[follower_position]
+ ]:
+ follower_positions.append(follower_position)
+ follower_position += 1
+ if not follower_positions:
+ continue
+ value_argument =
meta_collection.match(scan.meta_value_option[:2])[0]
+ follower_probe = _probe_unclaimed(
+ ArgumentCollection([value_argument]),
+ [scan.meta_value_option, *(stream_tokens[p] for p in
follower_positions)],
+ end_of_options_delimiter=end_of_options_delimiter,
+ )
+ if follower_probe is None:
+ # Malformed for the meta (e.g. too few values); hand it the
+ # next token so its real parse raises the proper error.
+ claimed_followers = follower_positions[:1] if
len(scan.meta_value_option) == 2 else []
+ else:
+ claimed_followers = []
+ for follower_position in follower_positions:
+ if follower_probe.kw_claims.get(follower_position -
position) != (
+ stream_tokens[follower_position],
+ None,
+ ):
+ break
+ claimed_followers.append(follower_position)
+ for follower_position in claimed_followers:
+ scanned_positions.add(follower_position)
+ newly_replaced[follower_position] = None
+ bubbled.append(stream_tokens[follower_position])
candidates = [
(position, token)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/completion/_base.py
new/cyclopts-5.4.0/cyclopts/completion/_base.py
--- old/cyclopts-5.3.3/cyclopts/completion/_base.py 2020-02-02
01:00:00.000000000 +0100
+++ new/cyclopts-5.4.0/cyclopts/completion/_base.py 2020-02-02
01:00:00.000000000 +0100
@@ -145,8 +145,13 @@
# represent it; its ``--[KEYWORD]`` placeholder name is
invalid shell
# syntax (zsh's ``_arguments`` aborts on it, breaking
completion for the
# entire command).
+ # Unparsed arguments (``Parameter(parse=False)``) are never
read from the
+ # command line, so they are never completed; inspecting their
type hint
+ # (e.g. via ``is_flag()``) may also fail for values injected
at runtime.
app_arguments = ArgumentCollection(
- argument for argument in app_arguments if
argument.field_info.kind is not VAR_KEYWORD
+ argument
+ for argument in app_arguments
+ if argument.field_info.kind is not VAR_KEYWORD and
argument.parse
)
arguments.extend(app_arguments)
if not any(subapp is a for a in current):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/core.py
new/cyclopts-5.4.0/cyclopts/core.py
--- old/cyclopts-5.3.3/cyclopts/core.py 2020-02-02 01:00:00.000000000 +0100
+++ new/cyclopts-5.4.0/cyclopts/core.py 2020-02-02 01:00:00.000000000 +0100
@@ -34,6 +34,7 @@
_parse_kw_and_flags,
_scope_tokens_for_meta,
create_bound_arguments,
+ expand_argfiles,
is_option_like,
normalize_tokens,
)
@@ -130,6 +131,28 @@
raise _CannotDeriveCallingModuleNameError # pragma: no cover
+def _default_app_name() -> str:
+ """Name of the running program, for an :class:`App` without an explicit
``name``.
+
+ Like :mod:`argparse`'s default ``prog``: ``python -m <module>`` when the
program
+ was started with ``python -m``, otherwise the script's file name.
+ """
+ try:
+ spec = sys.modules["__main__"].__spec__
+ except (KeyError, AttributeError):
+ spec = None
+ # Wrappers like ``python -m cProfile script.py`` point ``sys.argv[0]`` at
the script.
+ if spec is not None and spec.name != "__main__" and sys.argv[0] ==
spec.origin:
+ # Started with ``python -m``; a package's ``__main__`` is run as the
package.
+ module_name = spec.name.removesuffix(".__main__")
+ return f"{Path(sys.executable).name} -m {module_name}"
+
+ name = Path(sys.argv[0]).name
+ if name == "__main__.py":
+ name = _get_root_module_name()
+ return name
+
+
def _validate_default_command(x: Callable[..., Any] | None) -> Callable[...,
Any] | None:
if isinstance(x, App):
raise TypeError("Cannot register a sub-App to default.")
@@ -506,6 +529,8 @@
end_of_options_delimiter: str | None = field(default=None, kw_only=True)
+ argfile_prefix: str | None = field(default=None, kw_only=True)
+
parse_mode: Literal["fallthrough", "strict"] | None = field(
default=None,
kw_only=True,
@@ -656,10 +681,7 @@
if self._name:
return self._name + self.alias # pyright: ignore
elif self.default_command is None:
- name = Path(sys.argv[0]).name
- if name == "__main__.py":
- name = _get_root_module_name()
- return (name,) + self.alias # pyright: ignore
+ return (_default_app_name(),) + self.alias # pyright: ignore
else:
try:
func_name = self.default_command.__name__
@@ -1713,7 +1735,11 @@
"_error_console": error_console,
"end_of_options_delimiter": end_of_options_delimiter,
}
+ if tokens is None:
+ _log_framework_warning(_detect_test_framework())
+
with self.app_stack([], overrides=overrides):
+ tokens = self._expand_argfiles(normalize_tokens(tokens))
command, bound, unused_tokens, ignored, _ =
self._parse_known_args(tokens)
return command, bound, unused_tokens, ignored
@@ -1912,6 +1938,14 @@
# forwarded tokens reach the child's own parse.
pass
else:
+ intermediate_collections:
list[ArgumentCollection] = []
+ intermediate = command_app._meta_parent
+ while intermediate is not None and
intermediate._meta_parent is not None:
+ if intermediate.default_command and (
+ collection :=
_safe_assemble_argument_collection(intermediate)
+ ):
+
intermediate_collections.append(collection)
+ intermediate = intermediate._meta_parent
scoped = _scope_tokens_for_meta(
argument_collection,
child_argument_collection,
@@ -1919,6 +1953,7 @@
hier_command_indices,
parse_mode=parse_mode,
end_of_options_delimiter=end_of_options_delimiter,
+
intermediate_collections=intermediate_collections,
)
# A meta level's captured tokens are forwarded to
@@ -2103,10 +2138,22 @@
Must be called inside an :attr:`app_stack` context so error settings
resolve.
"""
try:
- return normalize_tokens(tokens)
+ return self._expand_argfiles(normalize_tokens(tokens))
except CycloptsError as e:
self._handle_parse_error(e, [])
+ def _expand_argfiles(self, tokens: list[str]) -> list[str]:
+ """Expand :attr:`argfile_prefix` tokens. Must be called inside an
:attr:`app_stack` context."""
+ if tokens and tokens[0] == _COMPLETE_COMMAND:
+ # The shell's words may hold a half-typed ``@path``; reading it
would abort completion.
+ return tokens
+ # Files are expanded before any command is resolved, so resolve from
the entry app alone,
+ # even when an enclosing ``app_stack`` already holds the invoked
command chain.
+ with self.app_stack([self]):
+ prefix = self.app_stack.resolve("argfile_prefix")
+ delimiter = self.app_stack.resolve("end_of_options_delimiter",
fallback="--")
+ return expand_argfiles(tokens, prefix, delimiter)
+
def _handle_parse_error(self, e: CycloptsError, tokens: list[str]) ->
NoReturn:
"""Print ``e`` according to the resolved error settings, then exit or
re-raise it."""
print_error = self.app_stack.resolve("print_error")
@@ -2210,6 +2257,7 @@
"verbose": verbose,
"backend": backend,
"result_action": _result_action_converter(result_action),
+ "end_of_options_delimiter": end_of_options_delimiter,
"error_formatter": error_formatter,
}.items()
if v is not None
@@ -2340,6 +2388,7 @@
"verbose": verbose,
"backend": backend,
"result_action": _result_action_converter(result_action),
+ "end_of_options_delimiter": end_of_options_delimiter,
"error_formatter": error_formatter,
}.items()
if v is not None
@@ -3126,6 +3175,8 @@
overrides["_console"] = console
if error_console is not None:
overrides["_error_console"] = error_console
+ if kwargs.get("end_of_options_delimiter") is not None:
+ overrides["end_of_options_delimiter"] =
kwargs["end_of_options_delimiter"]
overrides["exit_on_error"] = exit_on_error
overrides["remap_flags"] = remap_flags
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/exceptions.py
new/cyclopts-5.4.0/cyclopts/exceptions.py
--- old/cyclopts-5.3.3/cyclopts/exceptions.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.4.0/cyclopts/exceptions.py 2020-02-02 01:00:00.000000000
+0100
@@ -1,4 +1,5 @@
import inspect
+import sys
from collections.abc import Callable, Iterator, Sequence
from enum import Enum
from itertools import chain
@@ -383,14 +384,17 @@
yield f"Invalid value for {self.token.keyword}:
{verbosified}", ""
return
- assert self.argument is not None
assert self.target_type is not None
yield from super()._segments()
+ # ``argument`` is filled in only once the error reaches the binding
+ # layer. Raised by ``convert`` directly, e.g. inside a custom
+ # converter, it is still None, so the message must not depend on it.
+ argument = self.argument
choice_strs: list[str] | None = None
plain_choices: list[str] | None = None
- nt = self.argument.parameter.name_transform
+ nt = argument.parameter.name_transform if argument is not None else
cyclopts.utils.default_name_transform
if get_origin(self.target_type) is Literal:
args = [nt(x.name) if isinstance(x, Enum) and x.name else x for x
in get_args(self.target_type)]
choice_strs = [f'"{x}"' if isinstance(x, str) else repr(x) for x
in args]
@@ -402,11 +406,19 @@
# Branch 3: Literal/Enum with a token -- "Choose from" + suggestion.
if choice_strs is not None and self.token is not None:
- name = self.token.keyword if self.token.keyword else
self.argument.name.lstrip("-").upper()
+ if self.token.keyword:
+ name = self.token.keyword
+ elif argument is not None:
+ name = argument.name.lstrip("-").upper()
+ else:
+ name = None
yield 'Invalid value "', ""
yield self.token.value, STYLE_OFFENDING_VALUE
- yield '" for ', ""
- yield name, STYLE_NAME
+ if name is None:
+ yield '"', ""
+ else:
+ yield '" for ', ""
+ yield name, STYLE_NAME
if self.token.source not in ("", "cli"):
yield f" from {self.token.source}", STYLE_SOURCE
yield ". Choose from: ", ""
@@ -431,23 +443,47 @@
)
if not self.token:
+ if argument is None:
+ yield f"Unable to convert value to {target_type_name}.", ""
+ return
yield "Invalid value for ", ""
- yield self.argument.name, STYLE_NAME
+ yield argument.name, STYLE_NAME
yield f": unable to convert value to {target_type_name}.", ""
return
- if self.token.keyword is None:
- display_name = self.argument.name.lstrip("-").upper()
- else:
+ if self.token.keyword is not None:
display_name = self.token.keyword
+ elif argument is not None:
+ display_name = argument.name.lstrip("-").upper()
+ else:
+ display_name = None
- yield "Invalid value for ", ""
- yield display_name, STYLE_NAME
- if self.token.source not in ("", "cli"):
- yield f" from {self.token.source}", STYLE_SOURCE
- yield ': unable to convert "', ""
- yield self.token.value, STYLE_OFFENDING_VALUE
- yield f'" into {target_type_name}.', ""
+ if display_name is None:
+ yield 'Unable to convert "', ""
+ yield self.token.value, STYLE_OFFENDING_VALUE
+ yield '"', ""
+ if self.token.source not in ("", "cli"):
+ yield f" from {self.token.source}", STYLE_SOURCE
+ yield f" into {target_type_name}.", ""
+ else:
+ yield "Invalid value for ", ""
+ yield display_name, STYLE_NAME
+ if self.token.source not in ("", "cli"):
+ yield f" from {self.token.source}", STYLE_SOURCE
+ yield ': unable to convert "', ""
+ yield self.token.value, STYLE_OFFENDING_VALUE
+ yield f'" into {target_type_name}.', ""
+
+ if "pydantic" in sys.modules:
+ import pydantic
+
+ if isinstance(self.__cause__, pydantic.ValidationError):
+ error = self.__cause__.errors()[0]
+ reason = error["msg"]
+ if error["type"] == "value_error":
+ reason = reason.removeprefix("Value error, ")
+ reason = reason.rstrip(".")
+ yield f" {reason[:1].upper()}{reason[1:]}.", ""
class UnknownCommandError(CycloptsError):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/field_info.py
new/cyclopts-5.4.0/cyclopts/field_info.py
--- old/cyclopts-5.3.3/cyclopts/field_info.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.4.0/cyclopts/field_info.py 2020-02-02 01:00:00.000000000
+0100
@@ -24,6 +24,7 @@
is_enum_flag,
is_namedtuple,
is_pydantic,
+ is_pydantic_leaf,
is_pydantic_secret,
is_typeddict,
is_union,
@@ -384,6 +385,9 @@
# Plain (non-Flag) Enums are leaf choice-sets converted by member name;
# a user-defined ``__init__`` must not cause decomposition into
children.
return {}
+ elif is_pydantic_leaf(hint):
+ # ``AnyUrl``'s ``__init__`` references itself; decomposing it would
recurse forever.
+ return {}
else:
return _generic_class_field_infos(hint)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/cyclopts-5.3.3/cyclopts/help/help.py
new/cyclopts-5.4.0/cyclopts/help/help.py
--- old/cyclopts-5.3.3/cyclopts/help/help.py 2020-02-02 01:00:00.000000000
+0100
+++ new/cyclopts-5.4.0/cyclopts/help/help.py 2020-02-02 01:00:00.000000000
+0100
@@ -1,9 +1,7 @@
import inspect
-import sys
from collections.abc import Iterable, Sequence
from enum import Enum
from functools import lru_cache, partial
-from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
@@ -27,7 +25,7 @@
resolve_optional,
)
from cyclopts.argument.utils import is_short_flag
-from cyclopts.core import _get_root_module_name,
_iter_resolution_argument_collections
+from cyclopts.core import _default_app_name,
_iter_resolution_argument_collections
from cyclopts.field_info import get_field_infos
from cyclopts.group import Group
from cyclopts.help.inline_text import InlineText
@@ -351,13 +349,10 @@
usage = []
# If we're at the root level (no command chain), the app has a
default_command,
- # and no explicit name was set, derive a better name from sys.argv[0]
+ # and no explicit name was set, derive the name of the running program
if not command_chain and app.default_command and not app._name:
# Use the same logic as in App.name property for apps without
default_command
- name = Path(sys.argv[0]).name
- if name == "__main__.py":
- name = _get_root_module_name()
- app_name = name
+ app_name = _default_app_name()
else:
app_name = app.name[0]