@@ -39,8 +39,7 @@ def do_paint(
3939
4040- ``str`` -- default string argument
4141- ``int``, ``float`` -- sets ``type=`` for argparse
42- - ``bool`` with default ``False`` -- ``--flag`` with ``store_true``
43- - ``bool`` with default ``True`` -- ``--no-flag`` with ``store_false``
42+ - ``bool`` with default -- ``--flag / --no-flag`` via ``BooleanOptionalAction``
4443- positional ``bool`` -- parsed from ``true/false``, ``yes/no``, ``on/off``, ``1/0``
4544- ``pathlib.Path`` -- sets ``type=Path``
4645- ``enum.Enum`` subclass -- ``type=converter``, ``choices`` from member values
@@ -278,20 +277,15 @@ def _resolve_bool(
278277 _args : tuple [Any , ...],
279278 * ,
280279 is_positional : bool ,
281- has_default : bool ,
282- default : Any ,
283280 metadata : ArgMetadata ,
281+ ** _ctx : Any ,
284282) -> dict [str , Any ]:
285283 """Resolve bool -- flag or positional depending on context."""
286284 if not is_positional :
287285 action_str = getattr (metadata , 'action' , None ) if metadata else None
288286 if action_str :
289- action = action_str
290- elif has_default and default is True :
291- action = 'store_false'
292- else :
293- action = 'store_true'
294- return {'action' : action , 'is_bool_flag' : True }
287+ return {'action' : action_str , 'is_bool_flag' : True }
288+ return {'action' : argparse .BooleanOptionalAction , 'is_bool_flag' : True }
295289 return {'type' : _parse_bool }
296290
297291
@@ -307,8 +301,18 @@ def _make_collection_resolver(collection_type: type) -> Callable[..., dict[str,
307301 """Create a resolver for single-arg collections (list[T], set[T])."""
308302
309303 def _resolve (_tp : Any , args : tuple [Any , ...], * , has_default : bool = False , ** _ctx : Any ) -> dict [str , Any ]:
304+ if len (args ) == 0 :
305+ # Bare list/tuple without type args -- treat as list[str]/set[str]
306+ nargs = '*' if has_default else '+'
307+ return {
308+ 'is_collection' : True ,
309+ 'nargs' : nargs ,
310+ 'base_type' : str ,
311+ 'action' : _CollectionCastingAction ,
312+ 'container_factory' : collection_type ,
313+ }
310314 if len (args ) != 1 :
311- return {}
315+ return {} # pragma: no cover
312316 element_type , inner = _resolve_element (args [0 ])
313317 nargs = '*' if has_default else '+'
314318 return {
@@ -327,12 +331,17 @@ def _resolve_tuple(_tp: Any, args: tuple[Any, ...], *, has_default: bool = False
327331 """Resolve tuple[T, ...] and tuple[T1, T2, ...]."""
328332 cast_kwargs = {'action' : _CollectionCastingAction , 'container_factory' : tuple }
329333
334+ if not args :
335+ # Bare tuple without type args -- treat as tuple[str, ...]
336+ nargs = '*' if has_default else '+'
337+ return {'is_collection' : True , 'nargs' : nargs , 'base_type' : str , ** cast_kwargs }
338+
330339 if len (args ) == 2 and args [1 ] is Ellipsis :
331340 element_type , inner = _resolve_element (args [0 ])
332341 nargs = '*' if has_default else '+'
333342 return {** inner , 'is_collection' : True , 'nargs' : nargs , 'base_type' : element_type , ** cast_kwargs }
334343
335- if args and Ellipsis not in args :
344+ if Ellipsis not in args :
336345 first = args [0 ]
337346 if not all (a == first for a in args [1 :]):
338347 raise TypeError (
@@ -344,7 +353,7 @@ def _resolve_tuple(_tp: Any, args: tuple[Any, ...], *, has_default: bool = False
344353 _ , inner = _resolve_element (first )
345354 return {** inner , 'is_collection' : True , 'nargs' : len (args ), 'base_type' : first , ** cast_kwargs }
346355
347- return {}
356+ return {} # pragma: no cover
348357
349358
350359def _resolve_literal (_tp : Any , args : tuple [Any , ...], ** _ctx : Any ) -> dict [str , Any ]:
@@ -425,8 +434,8 @@ def _resolve_annotation(
425434 * ,
426435 has_default : bool = False ,
427436 default : Any = None ,
428- ) -> tuple [type , dict [str , Any ], ArgMetadata , bool ]:
429- """Decompose a type annotation into ``(base_type, type_kwargs, metadata, is_positional)``.
437+ ) -> tuple [dict [str , Any ], ArgMetadata , bool , bool ]:
438+ """Decompose a type annotation into ``(type_kwargs, metadata, is_positional, is_bool_flag )``.
430439
431440 Peels in order: Annotated → Optional → type resolution.
432441 """
@@ -450,7 +459,9 @@ def _resolve_annotation(
450459 if len (args ) == 1 :
451460 tp = args [0 ]
452461 is_optional = True
453- elif len (args ) > 1 :
462+ else :
463+ # len > 1: ambiguous union (e.g. str | int)
464+ # len == 0: all-None union -- unreachable via normal typing but handle defensively
454465 type_names = ' | ' .join (a .__name__ if hasattr (a , '__name__' ) else str (a ) for a in args )
455466 raise TypeError (f"Union type { type_names } is ambiguous for auto-resolution. " )
456467
@@ -490,6 +501,11 @@ def _resolve_annotation(
490501 return type_kwargs , metadata , is_positional , is_bool_flag
491502
492503
504+ # Parameter names that conflict with argparse internals and cannot be used
505+ # as annotated parameter names.
506+ _RESERVED_PARAM_NAMES = frozenset ({'dest' })
507+
508+
493509# ---------------------------------------------------------------------------
494510# Signature → Parser conversion
495511# ---------------------------------------------------------------------------
@@ -513,18 +529,26 @@ def build_parser_from_function(func: Callable[..., Any]) -> argparse.ArgumentPar
513529 sig = inspect .signature (func )
514530 try :
515531 hints = get_type_hints (func , include_extras = True )
516- except (NameError , AttributeError , TypeError ):
517- hints = {}
532+ except (NameError , AttributeError , TypeError ) as exc :
533+ raise TypeError (
534+ f"Failed to resolve type hints for { func .__qualname__ } . Ensure all annotations use valid, importable types."
535+ ) from exc
518536
519537 for name , param in sig .parameters .items ():
520538 if name == 'self' :
521539 continue
522540
541+ if name in _RESERVED_PARAM_NAMES :
542+ raise ValueError (
543+ f"Parameter name { name !r} in { func .__qualname__ } is reserved by argparse "
544+ f"and cannot be used as an annotated parameter name."
545+ )
546+
523547 annotation = hints .get (name , param .annotation )
524548 has_default = param .default is not inspect .Parameter .empty
525549 default = param .default if has_default else None
526550
527- kwargs , metadata , positional , is_bool_flag = _resolve_annotation (
551+ kwargs , metadata , positional , _is_bool_flag = _resolve_annotation (
528552 annotation ,
529553 has_default = has_default ,
530554 default = default ,
@@ -533,12 +557,7 @@ def build_parser_from_function(func: Callable[..., Any]) -> argparse.ArgumentPar
533557 if positional :
534558 parser .add_argument (name , ** kwargs )
535559 else :
536- if isinstance (metadata , Option ) and metadata .names :
537- flags = list (metadata .names )
538- elif is_bool_flag and has_default and default is True :
539- flags = [f'--no-{ name } ' ]
540- else :
541- flags = [f'--{ name } ' ]
560+ flags = list (metadata .names ) if isinstance (metadata , Option ) and metadata .names else [f'--{ name } ' ]
542561 if isinstance (metadata , Option ) and metadata .required :
543562 kwargs ['required' ] = True
544563 kwargs ['dest' ] = name
0 commit comments