5151from typer import Option , Typer , rich_utils
5252from typer .core import TyperCommand , TyperGroup
5353from typer .models import OptionInfo
54- from typing_extensions import ParamSpec
54+ from typing_extensions import NotRequired , ParamSpec , TypedDict
5555
5656from nemo_run .cli import devspace as devspace_cli
5757from nemo_run .cli import experiment as experiment_cli
7777NEMORUN_SKIP_CONFIRMATION : Optional [bool ] = None
7878
7979INCLUDE_WORKSPACE_FILE = os .environ .get ("INCLUDE_WORKSPACE_FILE" , "true" ).lower () == "true"
80+ NEMORUN_PRETTY_EXCEPTIONS = os .environ .get ("NEMORUN_PRETTY_EXCEPTIONS" , "false" ).lower () == "true"
8081
8182logger = logging .getLogger (__name__ )
8283MAIN_ENTRYPOINT = None
@@ -211,11 +212,28 @@ def wrapper(f: F) -> F:
211212 return wrapper (fn )
212213
213214
215+ class CommandDefaults (TypedDict , total = False ):
216+ direct : NotRequired [bool ]
217+ dryrun : NotRequired [bool ]
218+ load : NotRequired [str ]
219+ yaml : NotRequired [str ]
220+ repl : NotRequired [bool ]
221+ detach : NotRequired [bool ]
222+ skip_confirmation : NotRequired [bool ]
223+ tail_logs : NotRequired [bool ]
224+ rich_exceptions : NotRequired [bool ]
225+ rich_traceback : NotRequired [bool ]
226+ rich_locals : NotRequired [bool ]
227+ rich_theme : NotRequired [str ]
228+ verbose : NotRequired [bool ]
229+
230+
214231def main (
215232 fn : F ,
216233 default_factory : Optional [Callable ] = None ,
217234 default_executor : Optional [Config [Executor ]] = None ,
218235 default_plugins : Optional [List [Config [Plugin ]] | Config [Plugin ]] = None ,
236+ cmd_defaults : Optional [CommandDefaults ] = None ,
219237 ** kwargs ,
220238):
221239 """
@@ -280,7 +298,7 @@ def my_cli_function():
280298 MAIN_ENTRYPOINT = fn .cli_entrypoint
281299 return
282300
283- fn .cli_entrypoint .main ()
301+ fn .cli_entrypoint .main (cmd_defaults )
284302
285303 fn .cli_entrypoint .default_factory = _original_default_factory
286304 fn .cli_entrypoint .default_executor = _original_default_executor
@@ -507,7 +525,7 @@ def list_factories(type_or_namespace: Type | str) -> list[Callable]:
507525
508526
509527def create_cli (
510- add_verbose_callback : bool = True ,
528+ add_verbose_callback : bool = False ,
511529 nested_entrypoints_creation : bool = True ,
512530) -> Typer :
513531 app : Typer = Typer (pretty_exceptions_enable = False )
@@ -558,7 +576,7 @@ def create_cli(
558576 )
559577
560578 if add_verbose_callback :
561- app . callback ()( global_options )
579+ add_global_options ( app )
562580
563581 return app
564582
@@ -577,9 +595,53 @@ def wrapped(self) -> Callable: ...
577595 def __factory__ (self ) -> bool : ...
578596
579597
580- def global_options (verbose : bool = Option (False , "-v" , "--verbose" )):
598+ def add_global_options (app : Typer ):
599+ @app .callback ()
600+ def global_options (
601+ verbose : bool = Option (False , "-v" , "--verbose" ),
602+ rich_exceptions : bool = typer .Option (
603+ False , "--rich-exceptions/--no-rich-exceptions" , help = "Enable rich exception formatting"
604+ ),
605+ rich_traceback : bool = typer .Option (
606+ False ,
607+ "--rich-traceback-short/--rich-traceback-full" ,
608+ help = "Control traceback verbosity" ,
609+ ),
610+ rich_locals : bool = typer .Option (
611+ True ,
612+ "--rich-show-locals/--rich-hide-locals" ,
613+ help = "Toggle local variables in exceptions" ,
614+ ),
615+ rich_theme : Optional [str ] = typer .Option (
616+ None , "--rich-theme" , help = "Color theme (dark/light/monochrome)"
617+ ),
618+ ):
619+ _configure_global_options (
620+ app , rich_exceptions , rich_traceback , rich_locals , rich_theme , verbose
621+ )
622+
623+ return global_options
624+
625+
626+ def _configure_global_options (
627+ app : Typer ,
628+ rich_exceptions = False ,
629+ rich_traceback = True ,
630+ rich_locals = True ,
631+ rich_theme = None ,
632+ verbose = False ,
633+ ):
581634 configure_logging (verbose )
582635
636+ app .pretty_exceptions_enable = rich_exceptions
637+ app .pretty_exceptions_short = False if rich_exceptions else rich_traceback
638+ app .pretty_exceptions_show_locals = True if rich_exceptions else rich_locals
639+
640+ if rich_theme :
641+ from rich .traceback import Traceback
642+
643+ Traceback .theme = rich_theme
644+
583645
584646def configure_logging (verbose : bool ):
585647 handler = RichHandler (
@@ -682,7 +744,10 @@ def _get_return_type(fn: Callable) -> Type:
682744def _load_entrypoints ():
683745 entrypoints = metadata .entry_points ().select (group = "nemo_run.cli" )
684746 for ep in entrypoints :
685- ep .load ()
747+ try :
748+ ep .load ()
749+ except Exception as e :
750+ print (f"Couldn't load entrypoint { ep .name } : { e } " )
686751
687752
688753def _search_workspace_file () -> str | None :
@@ -775,6 +840,7 @@ def cli_command(
775840 type : Literal ["task" , "experiment" ] = "task" ,
776841 command_kwargs : Dict [str , Any ] = {},
777842 is_main : bool = False ,
843+ cmd_defaults : Optional [Dict [str , Any ]] = None ,
778844 ):
779845 """
780846 Create a CLI command for the given function.
@@ -820,19 +886,50 @@ def command(
820886 tail_logs : bool = typer .Option (
821887 False , "--tail-logs/--no-tail-logs" , help = "Tail logs after execution"
822888 ),
889+ verbose : bool = Option (False , "-v" , "--verbose" , help = "Enable verbose logging" ),
890+ rich_exceptions : bool = typer .Option (
891+ False ,
892+ "--rich-exceptions/--no-rich-exceptions" ,
893+ help = "Enable rich exception formatting" ,
894+ ),
895+ rich_traceback : bool = typer .Option (
896+ False ,
897+ "--rich-traceback-short/--rich-traceback-full" ,
898+ help = "Control traceback verbosity" ,
899+ ),
900+ rich_locals : bool = typer .Option (
901+ True ,
902+ "--rich-show-locals/--rich-hide-locals" ,
903+ help = "Toggle local variables in exceptions" ,
904+ ),
905+ rich_theme : Optional [str ] = typer .Option (
906+ None , "--rich-theme" , help = "Color theme (dark/light/monochrome)"
907+ ),
823908 ctx : typer .Context = typer .Context ,
824909 ):
910+ _cmd_defaults = cmd_defaults or {}
825911 self = cls (
826912 name = run_name or name ,
827- direct = direct ,
828- dryrun = dryrun ,
913+ direct = direct or _cmd_defaults . get ( "direct" , False ) ,
914+ dryrun = dryrun or _cmd_defaults . get ( "dryrun" , False ) ,
829915 factory = factory or default_factory ,
830- load = load ,
831- yaml = yaml ,
832- repl = repl ,
833- detach = detach ,
834- skip_confirmation = skip_confirmation ,
835- tail_logs = tail_logs ,
916+ load = load or _cmd_defaults .get ("load" , None ),
917+ yaml = yaml or _cmd_defaults .get ("yaml" , None ),
918+ repl = repl or _cmd_defaults .get ("repl" , False ),
919+ detach = detach or _cmd_defaults .get ("detach" , False ),
920+ skip_confirmation = skip_confirmation
921+ or _cmd_defaults .get ("skip_confirmation" , False ),
922+ tail_logs = tail_logs or _cmd_defaults .get ("tail_logs" , False ),
923+ )
924+
925+ print ("Configuring global options" )
926+ _configure_global_options (
927+ parent ,
928+ rich_exceptions or _cmd_defaults .get ("rich_exceptions" , False ),
929+ rich_traceback or _cmd_defaults .get ("rich_traceback" , True ),
930+ rich_locals or _cmd_defaults .get ("rich_locals" , True ),
931+ rich_theme or _cmd_defaults .get ("rich_theme" , None ),
932+ verbose or _cmd_defaults .get ("verbose" , False ),
836933 )
837934
838935 if default_executor :
@@ -851,8 +948,15 @@ def command(
851948 _load_workspace ()
852949 self .cli_execute (fn , ctx .args , type )
853950 except RunContextError as e :
854- typer .echo (f"Error: { str (e )} " , err = True , color = True )
855- raise typer .Exit (code = 1 )
951+ if not verbose :
952+ typer .echo (f"Error: { str (e )} " , err = True , color = True )
953+ raise typer .Exit (code = 1 )
954+ raise # Re-raise the exception for verbose mode
955+ except Exception as e :
956+ if not verbose :
957+ typer .echo (f"Unexpected error: { str (e )} " , err = True , color = True )
958+ raise typer .Exit (code = 1 )
959+ raise # Re-raise the exception for verbose mode
856960
857961 return command
858962
@@ -1285,9 +1389,14 @@ def parse_partial(self, args: List[str], **default_args) -> Partial[T]:
12851389 def cli (self , parent : typer .Typer ):
12861390 self ._add_command (parent )
12871391
1288- def _add_command (self , typer_instance : typer .Typer , is_main : bool = False ):
1392+ def _add_command (
1393+ self ,
1394+ typer_instance : typer .Typer ,
1395+ is_main : bool = False ,
1396+ cmd_defaults : Optional [Dict [str , Any ]] = None ,
1397+ ):
12891398 if self .enable_executor :
1290- self ._add_executor_command (typer_instance , is_main = is_main )
1399+ self ._add_executor_command (typer_instance , is_main = is_main , cmd_defaults = cmd_defaults )
12911400 else :
12921401 self ._add_simple_command (typer_instance , is_main = is_main )
12931402
@@ -1307,7 +1416,12 @@ def cmd_cli(ctx: typer.Context):
13071416 console .print (f"[bold red]Error: { str (e )} [/bold red]" )
13081417 sys .exit (1 )
13091418
1310- def _add_executor_command (self , parent : typer .Typer , is_main : bool = False ):
1419+ def _add_executor_command (
1420+ self ,
1421+ parent : typer .Typer ,
1422+ is_main : bool = False ,
1423+ cmd_defaults : Optional [Dict [str , Any ]] = None ,
1424+ ):
13111425 help = self .help_str
13121426 colored_help = None
13131427 if help :
@@ -1329,6 +1443,7 @@ class CLITaskCommand(EntrypointCommand):
13291443 cls = CLITaskCommand ,
13301444 ),
13311445 is_main = is_main ,
1446+ cmd_defaults = cmd_defaults ,
13321447 )
13331448
13341449 def _add_options_to_command (self , command : Callable ):
@@ -1345,9 +1460,9 @@ def _execute_simple(self, args: List[str], console: Console):
13451460 fn .func .__io__ = config
13461461 fn ()
13471462
1348- def main (self ):
1349- app = typer .Typer (help = self .help_str , pretty_exceptions_enable = False )
1350- self ._add_command (app , is_main = True )
1463+ def main (self , cmd_defaults : Optional [ Dict [ str , Any ]] = None ):
1464+ app = typer .Typer (help = self .help_str , pretty_exceptions_enable = NEMORUN_PRETTY_EXCEPTIONS )
1465+ self ._add_command (app , is_main = True , cmd_defaults = cmd_defaults )
13511466 app (standalone_mode = False )
13521467
13531468 def help (self , console = Console (), with_docs : bool = True ):
0 commit comments