2929 LiveStatsCollector = None
3030
3131
32+ class CustomFormatter (
33+ argparse .ArgumentDefaultsHelpFormatter ,
34+ argparse .RawDescriptionHelpFormatter ,
35+ ):
36+ """Custom formatter that combines default values display with raw description formatting."""
37+ pass
38+
39+
3240_HELP_DESCRIPTION = """Sample a process's stack frames and generate profiling data.
3341
3442Commands:
3543 run Run and profile a script or module
3644 attach Attach to and profile a running process
37- live Interactive TUI profiler (top-like interface)
3845
3946Examples:
40- # Run and profile a script (default: pstats to stdout)
47+ # Run and profile a script
4148 python -m profiling.sampling run script.py arg1 arg2
4249
43- # Run and profile a module
44- python -m profiling.sampling run -m mymodule arg1 arg2
45-
4650 # Attach to a running process
4751 python -m profiling.sampling attach 1234
4852
49- # Live interactive mode for a process
50- python -m profiling.sampling live 1234
51-
52- # Live mode for a script
53- python -m profiling.sampling live script.py
54-
55- # Generate flamegraph from a script
56- python -m profiling.sampling run --flamegraph -o output.html script.py
53+ # Live interactive mode for a script
54+ python -m profiling.sampling run --live script.py
5755
58- # Profile with custom interval and duration
59- python -m profiling.sampling run -i 50 -d 30 script.py
60-
61- # Profile all threads, sort by total time
62- python -m profiling.sampling attach -a --sort tottime 1234
63-
64- # Save collapsed stacks to file
65- python -m profiling.sampling run --collapsed -o stacks.txt script.py
56+ # Live interactive mode for a running process
57+ python -m profiling.sampling attach --live 1234
6658
6759Use 'python -m profiling.sampling <command> --help' for command-specific help."""
6860
@@ -167,14 +159,16 @@ def _add_sampling_options(parser):
167159 "--interval" ,
168160 type = int ,
169161 default = 100 ,
170- help = "Sampling interval in microseconds (default: 100)" ,
162+ metavar = "MICROSECONDS" ,
163+ help = "Sampling interval" ,
171164 )
172165 sampling_group .add_argument (
173166 "-d" ,
174167 "--duration" ,
175168 type = int ,
176169 default = 10 ,
177- help = "Sampling duration in seconds (default: 10)" ,
170+ metavar = "SECONDS" ,
171+ help = "Sampling duration" ,
178172 )
179173 sampling_group .add_argument (
180174 "-a" ,
@@ -208,7 +202,7 @@ def _add_mode_options(parser):
208202 choices = ["wall" , "cpu" , "gil" ],
209203 default = "wall" ,
210204 help = "Sampling mode: wall (all samples), cpu (only samples when thread is on CPU), "
211- "gil (only samples when thread holds the GIL) (default: wall) " ,
205+ "gil (only samples when thread holds the GIL)" ,
212206 )
213207
214208
@@ -270,14 +264,14 @@ def _add_pstats_options(parser):
270264 "name" ,
271265 ],
272266 default = "nsamples" ,
273- help = "Sort order for pstats output (default: nsamples) " ,
267+ help = "Sort order for pstats output" ,
274268 )
275269 pstats_group .add_argument (
276270 "-l" ,
277271 "--limit" ,
278272 type = int ,
279273 default = 15 ,
280- help = "Limit the number of rows in the output (default: 15) " ,
274+ help = "Limit the number of rows in the output" ,
281275 )
282276 pstats_group .add_argument (
283277 "--no-summary" ,
@@ -368,13 +362,18 @@ def _validate_args(args, parser):
368362 parser: ArgumentParser instance for error reporting
369363 """
370364 # Check if live mode is available
371- if args . command == " live" and LiveStatsCollector is None :
365+ if hasattr ( args , 'live' ) and args . live and LiveStatsCollector is None :
372366 parser .error (
373367 "Live mode requires the curses module, which is not available."
374368 )
375369
376- # Only validate format options for run/attach commands (live doesn't have format option)
377- if args .command not in ("run" , "attach" ):
370+ # Live mode is incompatible with format options
371+ if hasattr (args , 'live' ) and args .live :
372+ if args .format != "pstats" :
373+ format_flag = f"--{ args .format } "
374+ parser .error (
375+ f"--live is incompatible with { format_flag } . Live mode uses a TUI interface."
376+ )
378377 return
379378
380379 # Validate gecko mode doesn't use non-wall mode
@@ -406,7 +405,7 @@ def main():
406405 # Create the main parser
407406 parser = argparse .ArgumentParser (
408407 description = _HELP_DESCRIPTION ,
409- formatter_class = argparse . RawDescriptionHelpFormatter ,
408+ formatter_class = CustomFormatter ,
410409 )
411410
412411 # Create subparsers for commands
@@ -418,8 +417,24 @@ def main():
418417 run_parser = subparsers .add_parser (
419418 "run" ,
420419 help = "Run and profile a script or module" ,
421- formatter_class = argparse .RawDescriptionHelpFormatter ,
422- description = "Run and profile a Python script or module" ,
420+ formatter_class = CustomFormatter ,
421+ description = """Run and profile a Python script or module
422+
423+ Examples:
424+ # Run and profile a module
425+ python -m profiling.sampling run -m mymodule arg1 arg2
426+
427+ # Generate flamegraph from a script
428+ python -m profiling.sampling run --flamegraph -o output.html script.py
429+
430+ # Profile with custom interval and duration
431+ python -m profiling.sampling run -i 50 -d 30 script.py
432+
433+ # Save collapsed stacks to file
434+ python -m profiling.sampling run --collapsed -o stacks.txt script.py
435+
436+ # Live interactive mode for a script
437+ python -m profiling.sampling run --live script.py""" ,
423438 )
424439 run_parser .add_argument (
425440 "-m" ,
@@ -436,6 +451,11 @@ def main():
436451 nargs = argparse .REMAINDER ,
437452 help = "Arguments to pass to the script or module" ,
438453 )
454+ run_parser .add_argument (
455+ "--live" ,
456+ action = "store_true" ,
457+ help = "Interactive TUI profiler (top-like interface, press 'q' to quit, 's' to cycle sort)" ,
458+ )
439459 _add_sampling_options (run_parser )
440460 _add_mode_options (run_parser )
441461 _add_format_options (run_parser )
@@ -445,44 +465,31 @@ def main():
445465 attach_parser = subparsers .add_parser (
446466 "attach" ,
447467 help = "Attach to and profile a running process" ,
448- formatter_class = argparse .RawDescriptionHelpFormatter ,
449- description = "Attach to a running process and profile it" ,
468+ formatter_class = CustomFormatter ,
469+ description = """Attach to a running process and profile it
470+
471+ Examples:
472+ # Profile all threads, sort by total time
473+ python -m profiling.sampling attach -a --sort tottime 1234
474+
475+ # Live interactive mode for a running process
476+ python -m profiling.sampling attach --live 1234""" ,
450477 )
451478 attach_parser .add_argument (
452479 "pid" ,
453480 type = int ,
454481 help = "Process ID to attach to" ,
455482 )
483+ attach_parser .add_argument (
484+ "--live" ,
485+ action = "store_true" ,
486+ help = "Interactive TUI profiler (top-like interface, press 'q' to quit, 's' to cycle sort)" ,
487+ )
456488 _add_sampling_options (attach_parser )
457489 _add_mode_options (attach_parser )
458490 _add_format_options (attach_parser )
459491 _add_pstats_options (attach_parser )
460492
461- # === LIVE COMMAND ===
462- live_parser = subparsers .add_parser (
463- "live" ,
464- help = "Interactive TUI profiler (top-like interface)" ,
465- formatter_class = argparse .RawDescriptionHelpFormatter ,
466- description = "Interactive live profiling with a terminal UI (press 'q' to quit, 's' to cycle sort)" ,
467- )
468- live_parser .add_argument (
469- "-m" ,
470- "--module" ,
471- action = "store_true" ,
472- help = "Run target as a module (like python -m)" ,
473- )
474- live_parser .add_argument (
475- "target" ,
476- help = "Process ID, script file, or module name to profile" ,
477- )
478- live_parser .add_argument (
479- "args" ,
480- nargs = argparse .REMAINDER ,
481- help = "Arguments to pass to the script or module (if not a PID)" ,
482- )
483- _add_sampling_options (live_parser )
484- _add_mode_options (live_parser )
485-
486493 # Parse arguments
487494 args = parser .parse_args ()
488495
@@ -493,7 +500,6 @@ def main():
493500 command_handlers = {
494501 "run" : _handle_run ,
495502 "attach" : _handle_attach ,
496- "live" : _handle_live ,
497503 }
498504
499505 # Execute the appropriate command
@@ -506,6 +512,11 @@ def main():
506512
507513def _handle_attach (args ):
508514 """Handle the 'attach' command."""
515+ # Check if live mode is requested
516+ if args .live :
517+ _handle_live_attach (args , args .pid )
518+ return
519+
509520 # Use PROFILING_MODE_ALL for gecko format
510521 mode = (
511522 PROFILING_MODE_ALL
@@ -539,6 +550,11 @@ def _handle_attach(args):
539550
540551def _handle_run (args ):
541552 """Handle the 'run' command."""
553+ # Check if live mode is requested
554+ if args .live :
555+ _handle_live_run (args )
556+ return
557+
542558 # Build the command to run
543559 if args .module :
544560 cmd = (sys .executable , "-m" , args .target , * args .args )
@@ -589,26 +605,6 @@ def _handle_run(args):
589605 process .wait ()
590606
591607
592- def _handle_live (args ):
593- """Handle the 'live' command."""
594- # Determine if target is a PID or a script/module
595- try :
596- # Try to parse as PID
597- pid = int (args .target )
598- is_pid = True
599- except ValueError :
600- # It's a script or module name
601- is_pid = False
602- pid = None
603-
604- if is_pid :
605- # Attach to existing process in live mode
606- _handle_live_attach (args , pid )
607- else :
608- # Run script/module in live mode
609- _handle_live_run (args )
610-
611-
612608def _handle_live_attach (args , pid ):
613609 """Handle live mode for an existing process."""
614610 mode = _parse_mode (args .mode )
0 commit comments