@@ -61,7 +61,8 @@ def do_paint(
6161- ``list[T]`` / ``set[T]`` / ``tuple[T, ...]`` -- ``nargs='+'`` (or ``'*'`` if has a default or is ``| None``)
6262- ``tuple[T, T]`` (fixed arity, same type) -- ``nargs=N`` with ``type=T``
6363- ``*args: T`` -- variadic positional with ``nargs='*'`` (zero or more), collected into a tuple.
64- ``T`` is the type of each value (a scalar), not the collected tuple
64+ ``T`` is the type of each value (a scalar), not the collected tuple.
65+ ``Annotated[T, Argument(...)]`` metadata (help text, metavar, choices) is honored
6566- ``T | None`` (no default) -- positional with ``nargs='?'`` (accepts 0-or-1 tokens)
6667- ``T | None = None`` -- ``--flag`` option with ``default=None``
6768
@@ -87,6 +88,12 @@ def do_paint(
8788- ``*args: tuple[T, ...]`` (or ``*args: list[T]`` / any collection element) -- on ``*args``
8889 the annotation is the type of each value, so a collection element would mean a
8990 tuple-of-collections. Annotate the element type instead, e.g. ``*args: str``
91+ - ``*args: Annotated[T, Option(...)]`` -- ``*args`` is always positional, so ``Option()``
92+ metadata is rejected; use ``Argument()`` instead
93+ - ``*args: Annotated[T, Argument(nargs=N)]`` -- ``*args`` arity is fixed to ``nargs='*'``
94+ and cannot be overridden
95+ - a keyword-only parameter (after ``*``) annotated with ``Argument()`` metadata -- ``Argument()``
96+ marks a positional, which contradicts keyword-only; use ``Option()`` or omit the metadata
9097- ``Annotated[T, Argument(nargs=N)]`` where ``N`` produces a list (``'*'``, ``'+'``,
9198 or integer ``>= 1``) and ``T`` is not a collection type. Use ``list[T]`` or
9299 ``tuple[T, ...]`` to match the runtime shape.
@@ -550,7 +557,7 @@ def _nargs_yields_list(nargs: Any) -> bool:
550557
551558
552559def _resolve_type (
553- tp : type ,
560+ tp : Any ,
554561 * ,
555562 is_positional : bool = False ,
556563 is_optional : bool = False ,
@@ -837,10 +844,24 @@ def _resolve_parameters(
837844 if param .kind == inspect .Parameter .VAR_POSITIONAL :
838845 # ``*args: T`` is a variadic positional: zero or more values (nargs='*')
839846 # collected into a tuple. The hint gives the element type T (the type of
840- # each value), so annotating *args with a collection -- e.g.
841- # ``*args: tuple[str, ...]`` -- would mean each value is itself a tuple
842- # (a tuple-of-tuples), which cannot be mapped onto a flat command line.
843- element = hints .get (name , str )
847+ # each value). Peel any Annotated/Optional so we see the real element
848+ # type and any Argument() metadata (help text, metavar, choices, ...).
849+ element , metadata , _element_optional = _normalize_annotation (hints .get (name , str ))
850+
851+ if isinstance (metadata , Option ):
852+ raise TypeError (
853+ f"Parameter '*{ name } ' in { func .__qualname__ } uses Option() metadata, but *args is "
854+ f"always a positional argument. Use Argument() metadata instead."
855+ )
856+ if isinstance (metadata , Argument ) and metadata .nargs is not None :
857+ raise TypeError (
858+ f"Parameter '*{ name } ' in { func .__qualname__ } sets nargs={ metadata .nargs !r} via Argument(), "
859+ f"but *args always accepts zero or more values (nargs='*') and its arity cannot be overridden."
860+ )
861+
862+ # Annotating *args with a collection -- e.g. ``*args: tuple[str, ...]`` -- would
863+ # mean each value is itself a tuple (a tuple-of-collections), which cannot be
864+ # mapped onto a flat command line.
844865 _ , element_kwargs = _resolve_type (element , is_positional = True )
845866 if element_kwargs .get ("is_collection" ):
846867 # Show the parametrized form (e.g. ``tuple[str, ...]``), not the bare origin.
@@ -852,8 +873,15 @@ def _resolve_parameters(
852873 f"'{ element_display } '. Annotate the element type instead "
853874 f"(e.g. '*{ name } : str'); values are always collected into a tuple."
854875 )
855- variadic_annotation = types .GenericAlias (tuple , (element , ...))
856- kwargs , metadata , positional = _resolve_annotation (variadic_annotation , is_variadic = True )
876+
877+ # Each value has type ``element``; values are collected into a tuple (nargs='*').
878+ # ``is_optional=True`` selects nargs='*' (zero or more); any Argument() metadata
879+ # (help text, metavar, choices) is applied to the variadic positional.
880+ variadic_tuple = types .GenericAlias (tuple , (element , ...))
881+ _ , kwargs = _resolve_type (variadic_tuple , is_positional = True , is_optional = True , metadata = metadata )
882+ kwargs .pop ("is_collection" , None )
883+ kwargs .pop ("base_type" , None )
884+ positional = True
857885 else :
858886 annotation = hints .get (name , param .annotation )
859887 has_default = param .default is not inspect .Parameter .empty
@@ -867,6 +895,13 @@ def _resolve_parameters(
867895 is_kw_only = is_kw_only ,
868896 )
869897
898+ if is_kw_only and isinstance (metadata , Argument ):
899+ raise TypeError (
900+ f"Parameter '{ name } ' in { func .__qualname__ } is keyword-only but uses Argument() metadata, "
901+ f"which marks it as a positional argument. Keyword-only parameters always become options; "
902+ f"use Option() metadata (or omit the metadata) instead."
903+ )
904+
870905 if positional :
871906 flags : list [str ] = []
872907 else :
0 commit comments