22
33import contextlib
44import dataclasses
5- import inspect
65import sys
76from collections .abc import Callable
87from typing import (
2625from cappa .arg import Arg , FinalArg , Group
2726from cappa .class_inspect import fields as get_fields
2827from cappa .class_inspect import get_command , get_command_capable_object
29- from cappa .default import Default
3028from cappa .docstring import ClassHelpText
3129from cappa .help import HelpFormattable , HelpFormatter
3230from cappa .invoke .types import Resolved
@@ -115,8 +113,8 @@ class Command(Generic[T]):
115113 """
116114
117115 cmd_cls : type [T ]
118- arguments : Sequence [Arg [Any ] | Subcommand ] = dataclasses . field (
119- default_factory = lambda : []
116+ arguments : Sequence [Arg [Any ] | Subcommand | FinalDestructure [ Any ]] = (
117+ dataclasses . field ( default_factory = lambda : [])
120118 )
121119 propagated_arguments : list [FinalArg [Any ]] = dataclasses .field (
122120 default_factory = lambda : []
@@ -192,7 +190,7 @@ def collect(
192190
193191 propagated_arguments = propagated_arguments or []
194192
195- arguments : list [FinalArg [Any ]] = []
193+ arguments : list [FinalArg [Any ] | FinalDestructure [ Any ] ] = []
196194 raw_subcommands : list [tuple [Subcommand , TypeView [Any ] | None , str | None ]] = []
197195 if self .arguments :
198196 param_by_name = {p .name : p for p in function_view .parameters }
@@ -213,6 +211,8 @@ def collect(
213211 state = state ,
214212 )
215213 )
214+ elif isinstance (arg , FinalDestructure ):
215+ arguments .append (arg )
216216 else :
217217 raw_subcommands .append ((arg , None , None ))
218218
@@ -233,7 +233,7 @@ def collect(
233233 )
234234 )
235235 else :
236- arg_defs : list [ FinalArg [ Any ]] = Arg .collect (
236+ arg_defs = Arg .collect (
237237 field ,
238238 param_view .type_view ,
239239 fallback_help = arg_help ,
@@ -245,7 +245,7 @@ def collect(
245245
246246 propagating_arguments = [
247247 * propagated_arguments ,
248- * (arg for arg in arguments if arg .propagate ),
248+ * (arg for arg in arguments if isinstance ( arg , FinalArg ) and arg .propagate ),
249249 ]
250250 subcommands = [
251251 subcommand .normalize (
@@ -258,8 +258,10 @@ def collect(
258258 for subcommand , type_view , field_name in raw_subcommands
259259 ]
260260
261- check_group_identity (arguments )
262- final_arguments : list [FinalArg [Any ] | FinalSubcommand ] = [
261+ check_group_identity ([a for a in arguments if isinstance (a , FinalArg )])
262+ final_arguments : list [
263+ FinalArg [Any ] | FinalSubcommand | FinalDestructure [Any ]
264+ ] = [
263265 * arguments ,
264266 * subcommands ,
265267 ]
@@ -290,8 +292,8 @@ class FinalCommand(Command[T]):
290292 Produced exclusively by :meth:`Command.collect`.
291293 """
292294
293- arguments : Sequence [FinalArg [Any ] | FinalSubcommand ] = dataclasses . field ( # pyright: ignore
294- default_factory = list
295+ arguments : Sequence [FinalArg [Any ] | FinalSubcommand | FinalDestructure [ Any ]] = ( # pyright: ignore
296+ dataclasses . field ( default_factory = list )
295297 )
296298 propagated_arguments : list [FinalArg [Any ]] = dataclasses .field ( # pyright: ignore
297299 default_factory = list
@@ -310,10 +312,17 @@ def value_arguments(self) -> Iterable[FinalArg[Any]]:
310312 if isinstance (arg , FinalArg ) and arg .has_value :
311313 yield arg
312314
315+ @property
316+ def destructured_arguments (self ) -> Iterable [FinalDestructure [Any ]]:
317+ for arg in self .arguments :
318+ if isinstance (arg , FinalDestructure ):
319+ yield arg
320+
313321 @property
314322 def all_arguments (self ) -> Iterable [FinalArg [Any ] | FinalSubcommand ]:
315323 for arg in self .arguments :
316- yield arg
324+ if not isinstance (arg , FinalDestructure ):
325+ yield arg
317326 for arg in self .propagated_arguments :
318327 yield arg
319328
@@ -329,10 +338,7 @@ def positional_arguments(
329338 ) -> Iterable [FinalArg [Any ] | FinalSubcommand ]:
330339 for arg in self .arguments :
331340 if (
332- isinstance (arg , FinalArg )
333- and not arg .short
334- and not arg .long
335- and not arg .destructure
341+ isinstance (arg , FinalArg ) and not arg .short and not arg .long
336342 ) or isinstance (arg , FinalSubcommand ):
337343 yield arg
338344
@@ -379,18 +385,18 @@ def map_result(
379385
380386 kwargs : dict [str , Any ] = {}
381387 for arg in self .value_arguments :
382- field_name = arg .field_name
383-
384- if field_name in parsed_args :
385- is_parsed , value = (False , parsed_args [field_name ])
386- else :
387- assert isinstance (arg .default , Default ), arg
388- is_parsed , value = arg .default (state = state , input = input )
389-
390- handler = parse_handler (arg , prog , value )
391- kwargs [field_name ] = Resolved (handler , args = (value , is_parsed ))
388+ kwargs [arg .field_name ] = arg .map_result (
389+ prog , parsed_args , state = state , input = input
390+ )
392391
393392 subcommand_deps : dict [Hashable , Any ] = {}
393+ for destructure in self .destructured_arguments :
394+ fd_parsed = parsed_args .get (destructure .field_name , {})
395+ fd_resolved = destructure .map_result (
396+ prog , fd_parsed , output , state = state , input = input
397+ )
398+ kwargs [destructure .field_name ] = fd_resolved
399+
394400 subcommand = self .subcommand
395401 if subcommand :
396402 field_name = subcommand .field_name
@@ -464,62 +470,6 @@ def check_group_identity(args: list[FinalArg[Any]]):
464470 group_identity [arg .group .id ] = arg .group
465471
466472
467- @contextlib .contextmanager
468- def parse_value (
469- arg : FinalArg [T ], prog : str , value : Any , is_parsed : bool
470- ) -> Generator [T , None , None ]:
471- if not is_parsed :
472- try :
473- value = arg .parse (value )
474- except Exception as e :
475- exception_reason = str (e )
476- raise Exit (
477- f"Invalid value for '{ arg .names_str ()} ': { exception_reason } " ,
478- code = 2 ,
479- prog = prog ,
480- )
481-
482- yield value
483-
484-
485- def parse_handler (
486- arg : FinalArg [T ], prog : str , value : Any
487- ) -> Callable [[Any , bool ], Any ]:
488- is_async_value = inspect .iscoroutine (value )
489- is_async_parse = inspect .iscoroutinefunction (
490- arg .parse
491- ) or inspect .isasyncgenfunction (arg .parse )
492-
493- if is_async_value or is_async_parse :
494-
495- async def async_parse (raw_value : Any , is_parsed : bool ) -> T :
496- # Await the value if it's a coroutine
497- if inspect .iscoroutine (raw_value ):
498- raw_value = await raw_value
499-
500- with parse_value (arg , prog , raw_value , is_parsed ) as parsed :
501- # Check if the parse result is a coroutine and await it
502- if inspect .iscoroutine (parsed ):
503- try :
504- return await parsed
505- except Exception as e :
506- exception_reason = str (e )
507- raise Exit (
508- f"Invalid value for '{ arg .names_str ()} ': { exception_reason } " ,
509- code = 2 ,
510- prog = prog ,
511- )
512- return parsed
513-
514- return async_parse
515-
516- def sync_parse (raw_value : Any , is_parsed : bool ) -> T :
517- with parse_value (arg , prog , raw_value , is_parsed ) as parsed :
518- return parsed
519-
520- return sync_parse
521-
522-
523473@contextlib .contextmanager
524474def graceful_exit (
525475 command : FinalCommand [T ], prog : str , output : Output
@@ -544,3 +494,6 @@ def graceful_exit(
544494 raise
545495
546496 raise
497+
498+
499+ from cappa .destructure import FinalDestructure # noqa: E402
0 commit comments