3333WELCOME = f"""!B!Python install manager was successfully updated to { __version__ } .!W!
3434"""
3535
36-
37- # The help text of subcommands is generated below - look for 'subcommands_list'
38-
39- GLOBAL_OPTIONS_HELP_TEXT = fr"""!G!Global options:!W!
36+ # The 'py help' or 'pymanager help' output is constructed by these default docs,
37+ # with individual subcommand docs added in usage_text_lines().
38+ #
39+ # Descriptive text (tuple element 1) will be aligned and rewrapped across all
40+ # commands.
41+ #
42+ # Where a command summary (tuple element 0) ends with a newline, it allows the
43+ # wrapping algorithm to start the description on the following line if the
44+ # command is too long.
45+ PY_USAGE_DOCS = [
46+ (f"{ EXE_NAME } !B!<regular Python options>!W!\n " ,
47+ "Launch the default runtime with specified options. " +
48+ "This is the equivalent of the !G!python!W! command." ),
49+ (f"{ EXE_NAME } -V:!B!<TAG>!W!" ,
50+ "Launch runtime identified by !B!<TAG>!W!, which should include the " +
51+ "company name if not !B!PythonCore!W!. Regular Python options may " +
52+ "follow this option." ),
53+ (f"{ EXE_NAME } -3!B!<VERSION>!W!" ,
54+ r"Equivalent to -V:PythonCore\3!B!<VERSION>!W!. The version must begin " +
55+ "with the digit 3, platform overrides are permitted, and regular Python " +
56+ "options may follow. " +
57+ "!G!py -3!W! is the equivalent of the !G!python3!W! command." ),
58+ (f"{ EXE_NAME } exec !B!<any of the above>!W!\n " ,
59+ "Equivalent to any of the above launch options, and the requested runtime " +
60+ "will be installed if needed." ),
61+ ]
62+
63+
64+ PYMANAGER_USAGE_DOCS = [
65+ (f"{ EXE_NAME } exec !B!<regular Python options>!W!\n " ,
66+ "Launch the default runtime with specified options, installing it if needed. " +
67+ "This is the equivalent of the !G!python!W! command, but with auto-install." ),
68+ (f"{ EXE_NAME } exec -V:!B!<TAG>!W!" ,
69+ "Launch runtime identified by !B!<TAG>!W!, which should include the " +
70+ "company name if not !B!PythonCore!W!. Regular Python options may " +
71+ "follow this option. The runtime will be installed if needed." ),
72+ (f"{ EXE_NAME } exec -3!B!<VERSION>!W!\n " ,
73+ r"Equivalent to -V:PythonCore\3!B!<VERSION>!W!. The version must begin " +
74+ "with a '3', platform overrides are permitted, and regular Python " +
75+ "options may follow. The runtime will be installed if needed." ),
76+ ]
77+
78+
79+ GLOBAL_OPTIONS_HELP_TEXT = fr"""!G!Global options: !B!(options must come after a command)!W!
4080 -v, --verbose Increased output (!B!log_level={ logging .INFO } !W!)
4181 -vv Further increased output (!B!log_level={ logging .DEBUG } !W!)
4282 -q, --quiet Less output (!B!log_level={ logging .WARN } !W!)
4383 -qq Even less output (!B!log_level={ logging .ERROR } !W!)
4484 -y, --yes Always confirm prompts (!B!confirm=false!W!)
85+ -h, -?, --help Show help for a specific command
4586 --config=!B!<PATH>!W! Override configuration with JSON file
4687"""
4788
@@ -475,74 +516,62 @@ def execute(self):
475516 raise NotImplementedError (f"'{ type (self ).__name__ } ' does not implement 'execute()'" )
476517
477518 @classmethod
478- def usage_text_lines (cls ):
479- usage_docs = [
480- (f" { EXE_NAME } -V:!B!<TAG>!W!" ,
481- "Launch runtime identified by !B!<TAG>!W!, which should include the " +
482- "company name if not !B!PythonCore!W!. Regular Python options may " +
483- "follow this option." ),
484- (f" { EXE_NAME } -!B!<VERSION>!W!" ,
485- r"Equivalent to -V:PythonCore\!B!<VERSION>!W!. The version must " +
486- "begin with the digit 3, platform overrides are permitted, " +
487- "and regular Python options may follow." +
488- (" !G!py -3!W! is the equivalent of the !G!python3!W! command." if EXE_NAME == "py" else "" )),
489- (f" { EXE_NAME } !B!<COMMAND>!W!" ,
490- "Run a specific command (see list below)." ),
491- ]
492-
493- usage_ljust = max (len (logging .strip_colour (i [0 ])) for i in usage_docs )
519+ def show_usage (cls ):
520+ if EXE_NAME .casefold () in ("py" .casefold (), "pyw" .casefold ()):
521+ usage_docs = PY_USAGE_DOCS
522+ else :
523+ usage_docs = PYMANAGER_USAGE_DOCS
524+
525+ usage_docs = list (usage_docs )
526+ for cmd in sorted (COMMANDS ):
527+ if not cmd [:1 ].isalpha ():
528+ continue
529+ try :
530+ usage_docs .append (
531+ (
532+ f"{ EXE_NAME } " + getattr (COMMANDS [cmd ], "USAGE_LINE" , cmd ),
533+ COMMANDS [cmd ].HELP_LINE
534+ )
535+ )
536+ except AttributeError :
537+ pass
538+
539+ usage_docs = [(f" { x .lstrip ()} " , y ) for x , y in usage_docs ]
540+
541+ usage_ljust = max (len (logging .strip_colour (i [0 ])) for i in usage_docs if not i [0 ].endswith ("\n " ))
494542 if usage_ljust % 4 :
495543 usage_ljust += 4 - (usage_ljust % 4 )
496544 usage_ljust = max (usage_ljust , 16 ) + 1
497545 sp = " " * usage_ljust
498546
499- yield "!G!Usage:!W!"
500- if EXE_NAME .casefold () in ("py" .casefold (), "pyw" .casefold ()):
501- yield f" { EXE_NAME } !B!<regular Python options>!W!"
502- yield sp + "Launch the default runtime with specified options."
503- yield sp + "This is the equivalent of the !G!python!W! command."
547+ LOGGER .print ("!G!Usage:!W!" )
504548 for k , d in usage_docs :
505- r = k .ljust (usage_ljust + len (k ) - len (logging .strip_colour (k )))
549+ if k .endswith ("\n " ) and len (logging .strip_colour (k )) >= usage_ljust :
550+ LOGGER .print (k .rstrip ())
551+ r = sp
552+ else :
553+ k = k .rstrip ()
554+ r = k .ljust (usage_ljust + len (k ) - len (logging .strip_colour (k )))
506555 for b in d .split (" " ):
507- if len (r ) >= 80 :
508- yield r .rstrip ()
556+ if len (r ) >= logging . CONSOLE_MAX_WIDTH :
557+ LOGGER . print ( r .rstrip () )
509558 r = sp
510559 r += b + " "
511560 if r .rstrip ():
512- yield r
561+ LOGGER . print ( r )
513562
514- yield ""
515- yield "Find additional information at !B!https://docs.python.org/using/windows.html!W!."
516- yield ""
517-
518- @classmethod
519- def usage_text (cls ):
520- return "\n " .join (cls .usage_text_lines ())
521-
522- @classmethod
523- def subcommands_list (cls ):
524- usage_ljust = len (EXE_NAME ) + 1 + max (len (cmd ) for cmd in sorted (COMMANDS ) if cmd [:1 ].isalpha ())
525- if usage_ljust % 4 :
526- usage_ljust += 4 - (usage_ljust % 4 )
527- usage_ljust = max (usage_ljust , 16 )
528- cmd_help = [
529- " {:<{}} {}" .format (f"{ EXE_NAME } { cmd } " , usage_ljust , getattr (COMMANDS [cmd ], "HELP_LINE" , "" ))
530- for cmd in sorted (COMMANDS )
531- if cmd [:1 ].isalpha ()
532- ]
533- return fr"""
534- !G!Commands:!W!
535- { '\n ' .join (cmd_help )}
536- """ .lstrip ().replace ("\r \n " , "\n " )
563+ LOGGER .print ()
564+ # TODO: Remove the /dev/ for stable release
565+ LOGGER .print ("Find additional information at !B!https://docs.python.org/dev/using/windows!W!." )
566+ LOGGER .print ()
537567
538568 @classmethod
539569 def help_text (cls ):
540570 return GLOBAL_OPTIONS_HELP_TEXT .replace ("\r \n " , "\n " )
541571
542572 def help (self ):
543573 if type (self ) is BaseCommand :
544- LOGGER .print (self .usage_text ())
545- LOGGER .print (self .subcommands_list ())
574+ self .show_usage ()
546575 LOGGER .print (self .help_text ())
547576 try :
548577 LOGGER .print (self .HELP_TEXT .lstrip ())
@@ -616,8 +645,12 @@ def get_install_to_run(self, tag=None, script=None, *, windowed=False):
616645
617646class ListCommand (BaseCommand ):
618647 CMD = "list"
619- HELP_LINE = "Shows all installed Python runtimes"
648+ HELP_LINE = ("Show installed Python runtimes, optionally filtering by " +
649+ "!B!<FILTER>!W!." )
650+ USAGE_LINE = "list !B![<FILTER>]!W!"
620651 HELP_TEXT = r"""!G!List command!W!
652+ Shows installed Python runtimes, optionally filtered or formatted.
653+
621654> py list !B![options] [<FILTER> ...]!W!
622655
623656!G!Options:!W!
@@ -691,8 +724,12 @@ class ListPathsLegacyCommand(ListLegacyCommand):
691724
692725class InstallCommand (BaseCommand ):
693726 CMD = "install"
694- HELP_LINE = "Download new Python runtimes"
727+ HELP_LINE = ("Download new Python runtimes, or pass !B!--update!W! to " +
728+ "update existing installs." )
729+ USAGE_LINE = "install !B!<TAG>!W!"
695730 HELP_TEXT = r"""!G!Install command!W!
731+ Downloads new Python runtimes and sets up shortcuts and other registration.
732+
696733> py install !B![options] <TAG> [<TAG>] ...!W!
697734
698735!G!Options:!W!
@@ -768,14 +805,20 @@ def execute(self):
768805
769806class UninstallCommand (BaseCommand ):
770807 CMD = "uninstall"
771- HELP_LINE = "Remove runtimes from your machine"
808+ HELP_LINE = ("Remove one or more runtimes from your machine. Pass " +
809+ "!B!--purge!W! to clean up all runtimes and cached files." )
810+ USAGE_LINE = "uninstall !B!<TAG>!W!"
772811 HELP_TEXT = r"""!G!Uninstall command!W!
812+ Removes one or more runtimes from your machine.
813+
773814> py uninstall !B![options] <TAG> [<TAG>] ...!W!
774815
775816!G!Options:!W!
776- --purge Remove all runtimes, shortcuts, and cached files. Ignores tags.
777- --by-id Require TAG to exactly match the install ID. (For advanced use.)
778- !B!<TAG> <TAG>!W! ... One or more runtimes to uninstall (Company\Tag format)
817+ --purge Remove all runtimes, shortcuts, and cached files. Ignores tags.
818+ --by-id Require TAG to exactly match the install ID. (For advanced use.)
819+ !B!<TAG> <TAG>!W! ... One or more runtimes to uninstall (Company\Tag format)
820+ Each tag will only remove a single runtime, even if it matches
821+ more than one.
779822
780823!B!EXAMPLE:!W! Uninstall Python 3.12 32-bit
781824> py uninstall 3.12-32
@@ -809,7 +852,10 @@ def execute(self):
809852class HelpCommand (BaseCommand ):
810853 CMD = "help"
811854 HELP_LINE = "Show help for Python installation manager commands"
855+ USAGE_LINE = "help !B![<CMD>]!W!"
812856 HELP_TEXT = r"""!G!Help command!W!
857+ Shows help for specific commands.
858+
813859> py help !B![<CMD>] ...!W!
814860
815861!G!Options:!W!
@@ -823,8 +869,7 @@ def execute(self):
823869 LOGGER .print (COPYRIGHT )
824870 self .show_welcome (copyright = False )
825871 if not self .args :
826- LOGGER .print (BaseCommand .usage_text ())
827- LOGGER .print (BaseCommand .subcommands_list ())
872+ self .show_usage ()
828873 LOGGER .print (BaseCommand .help_text ())
829874 for a in self .args :
830875 try :
@@ -851,11 +896,40 @@ def execute(self):
851896 LOGGER .print (f"!R!Unknown command: { ' ' .join (args )} !W!" )
852897 LOGGER .print (COPYRIGHT )
853898 self .show_welcome (copyright = False )
854- LOGGER .print (BaseCommand .usage_text ())
855- LOGGER .print (BaseCommand .subcommands_list ())
899+ self .show_usage ()
856900 LOGGER .print (f"The command !R!{ ' ' .join (args )} !W! was not recognized." )
857901
858902
903+ # This command exists solely to provide help.
904+ # When it is specified, it gets handled in main.cpp
905+ class ExecCommand (BaseCommand ):
906+ CMD = "exec"
907+ HELP_TEXT = f"""!G!Execute command!W!
908+ Launches the specified (or default) runtime. This command is optional when
909+ launching through !G!py!W!, as the default behaviour is to launch a runtime.
910+ When used explicitly, this command will automatically install the requested
911+ runtime if it is not available.
912+
913+ > { EXE_NAME } exec -V:!B!<TAG>!W! ...
914+ > { EXE_NAME } exec -3!B!<VERSION>!W! ...
915+ > { EXE_NAME } exec ...
916+ > py [ -V:!B!<TAG>!W! | -3!B!<VERSION>!W! ] ...
917+
918+ !G!Options:!W!
919+ -V:!B!<TAG>!W! Launch runtime identified by !B!<TAG>!W!, which should include
920+ the company name if not !B!PythonCore!W!. Regular Python options
921+ may follow this option. The runtime will be installed if needed.
922+ -3!B!<VERSION>!W! Equivalent to -V:PythonCore\3 !B!<VERSION>!W!. The version must
923+ begin with a '3', platform overrides are permitted, and regular
924+ Python options may follow. The runtime will be installed if needed.
925+ """
926+
927+ def __init__ (self , args , root = None ):
928+ # Essentially disable argument processing for this command
929+ super ().__init__ (args [:1 ], root )
930+ self .args = args [1 :]
931+
932+
859933class DefaultConfig (BaseCommand ):
860934 CMD = "__no_command"
861935 _create_log_file = False
0 commit comments