From e7358a96dd3fb56cf2d2c4e49dc65db546f0bdd7 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 30 Apr 2025 14:07:41 +0100 Subject: [PATCH 1/4] Fixes #29: Improved help output for all commands --- src/manage/commands.py | 128 +++++++++++++++++++++++-------------- src/manage/list_command.py | 8 ++- src/manage/logging.py | 13 +++- tests/conftest.py | 4 ++ 4 files changed, 103 insertions(+), 50 deletions(-) diff --git a/src/manage/commands.py b/src/manage/commands.py index b9f389a..d31f499 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -33,15 +33,53 @@ WELCOME = f"""!B!Python install manager was successfully updated to {__version__}.!W! """ - -# The help text of subcommands is generated below - look for 'subcommands_list' - -GLOBAL_OPTIONS_HELP_TEXT = fr"""!G!Global options:!W! +# The 'py help' or 'pymanager help' output is constructed by these default docs, +# with individual subcommand docs added in usage_text_lines(). +# +# Descriptive text (tuple element 1) will be aligned and rewrapped across all +# commands. +# +# Where a command summary (tuple element 0) ends with a newline, it allows the +# wrapping algorithm to start the description on the following line if the +# command is too long. +PY_USAGE_DOCS = [ + (f"{EXE_NAME} !B!!W!\n", + "Launch the default runtime with specified options. " + + "This is the equivalent of the !G!python!W! command."), + (f"{EXE_NAME} -V:!B!!W!", + "Launch runtime identified by !B!!W!, which should include the " + + "company name if not !B!PythonCore!W!. Regular Python options may " + + "follow this option."), + (f"{EXE_NAME} -3!B!!W!", + r"Equivalent to -V:PythonCore\3!B!!W!. The version must begin " + + "with the digit 3, platform overrides are permitted, and regular Python " + + "options may follow. " + + "!G!py -3!W! is the equivalent of the !G!python3!W! command."), +] + + +PYMANAGER_USAGE_DOCS = [ + (f"{EXE_NAME} exec !B!!W!\n", + "Launch the default runtime with specified options, installing it if needed. " + + "This is the equivalent of the !G!python!W! command, but with auto-install."), + (f"{EXE_NAME} exec -V:!B!!W!", + "Launch runtime identified by !B!!W!, which should include the " + + "company name if not !B!PythonCore!W!. Regular Python options may " + + "follow this option. The runtime will be installed if needed."), + (f"{EXE_NAME} exec -3!B!!W!\n", + r"Equivalent to -V:PythonCore\3!B!!W!. The version must begin " + + "with a '3', platform overrides are permitted, and regular Python " + + "options may follow. The runtime will be installed if needed."), +] + + +GLOBAL_OPTIONS_HELP_TEXT = fr"""!G!Global options: !B!(options must come after a command)!W! -v, --verbose Increased output (!B!log_level={logging.INFO}!W!) -vv Further increased output (!B!log_level={logging.DEBUG}!W!) -q, --quiet Less output (!B!log_level={logging.WARN}!W!) -qq Even less output (!B!log_level={logging.ERROR}!W!) -y, --yes Always confirm prompts (!B!confirm=false!W!) + -h, -?, --help Show help for a specific command --config=!B!!W! Override configuration with JSON file """ @@ -476,35 +514,40 @@ def execute(self): @classmethod def usage_text_lines(cls): - usage_docs = [ - (f" {EXE_NAME} -V:!B!!W!", - "Launch runtime identified by !B!!W!, which should include the " + - "company name if not !B!PythonCore!W!. Regular Python options may " + - "follow this option."), - (f" {EXE_NAME} -!B!!W!", - r"Equivalent to -V:PythonCore\!B!!W!. The version must " + - "begin with the digit 3, platform overrides are permitted, " + - "and regular Python options may follow." + - (" !G!py -3!W! is the equivalent of the !G!python3!W! command." if EXE_NAME == "py" else "")), - (f" {EXE_NAME} !B!!W!", - "Run a specific command (see list below)."), - ] - - usage_ljust = max(len(logging.strip_colour(i[0])) for i in usage_docs) + if EXE_NAME.casefold() in ("py".casefold(), "pyw".casefold()): + usage_docs = PY_USAGE_DOCS + else: + usage_docs = PYMANAGER_USAGE_DOCS + + usage_docs.extend( + [ + ( + f"{EXE_NAME} " + getattr(COMMANDS[cmd], "USAGE_LINE", cmd), + getattr(COMMANDS[cmd], "HELP_LINE", "") + ) + for cmd in sorted(COMMANDS) + if cmd[:1].isalpha() + ] + ) + + usage_docs = [(" " + x.lstrip(), y) for x, y in usage_docs] + + usage_ljust = max(len(logging.strip_colour(i[0])) for i in usage_docs if not i[0].endswith("\n")) if usage_ljust % 4: usage_ljust += 4 - (usage_ljust % 4) usage_ljust = max(usage_ljust, 16) + 1 sp = " " * usage_ljust yield "!G!Usage:!W!" - if EXE_NAME.casefold() in ("py".casefold(), "pyw".casefold()): - yield f" {EXE_NAME} !B!!W!" - yield sp + "Launch the default runtime with specified options." - yield sp + "This is the equivalent of the !G!python!W! command." for k, d in usage_docs: - r = k.ljust(usage_ljust + len(k) - len(logging.strip_colour(k))) + if k.endswith("\n") and len(logging.strip_colour(k)) >= usage_ljust: + yield k.rstrip() + r = sp + else: + k = k.rstrip() + r = k.ljust(usage_ljust + len(k) - len(logging.strip_colour(k))) for b in d.split(" "): - if len(r) >= 80: + if len(r) >= logging.CONSOLE_MAX_WIDTH: yield r.rstrip() r = sp r += b + " " @@ -512,29 +555,14 @@ def usage_text_lines(cls): yield r yield "" - yield "Find additional information at !B!https://docs.python.org/using/windows.html!W!." + # TODO: Remove the 3.14 for stable release + yield "Find additional information at !B!https://docs.python.org/3.14/using/windows!W!." yield "" @classmethod def usage_text(cls): return "\n".join(cls.usage_text_lines()) - @classmethod - def subcommands_list(cls): - usage_ljust = len(EXE_NAME) + 1 + max(len(cmd) for cmd in sorted(COMMANDS) if cmd[:1].isalpha()) - if usage_ljust % 4: - usage_ljust += 4 - (usage_ljust % 4) - usage_ljust = max(usage_ljust, 16) - cmd_help = [ - " {:<{}} {}".format(f"{EXE_NAME} {cmd}", usage_ljust, getattr(COMMANDS[cmd], "HELP_LINE", "")) - for cmd in sorted(COMMANDS) - if cmd[:1].isalpha() - ] - return fr""" -!G!Commands:!W! -{'\n'.join(cmd_help)} -""".lstrip().replace("\r\n", "\n") - @classmethod def help_text(cls): return GLOBAL_OPTIONS_HELP_TEXT.replace("\r\n", "\n") @@ -542,7 +570,6 @@ def help_text(cls): def help(self): if type(self) is BaseCommand: LOGGER.print(self.usage_text()) - LOGGER.print(self.subcommands_list()) LOGGER.print(self.help_text()) try: LOGGER.print(self.HELP_TEXT.lstrip()) @@ -616,7 +643,9 @@ def get_install_to_run(self, tag=None, script=None, *, windowed=False): class ListCommand(BaseCommand): CMD = "list" - HELP_LINE = "Shows all installed Python runtimes" + HELP_LINE = ("Shows installed Python runtimes, optionally filtering by " + + "!B!!W!.") + USAGE_LINE = "list !B![]!W!" HELP_TEXT = r"""!G!List command!W! > py list !B![options] [ ...]!W! @@ -691,7 +720,9 @@ class ListPathsLegacyCommand(ListLegacyCommand): class InstallCommand(BaseCommand): CMD = "install" - HELP_LINE = "Download new Python runtimes" + HELP_LINE = ("Download new Python runtimes, or pass !B!--update!W! to " + + "update existing installs.") + USAGE_LINE = "install !B!!W!" HELP_TEXT = r"""!G!Install command!W! > py install !B![options] [] ...!W! @@ -768,7 +799,9 @@ def execute(self): class UninstallCommand(BaseCommand): CMD = "uninstall" - HELP_LINE = "Remove runtimes from your machine" + HELP_LINE = ("Remove one or more runtimes from your machine. Pass " + + "!B!--purge!W! to clean up all runtimes and cached files.") + USAGE_LINE = "uninstall !B!!W!" HELP_TEXT = r"""!G!Uninstall command!W! > py uninstall !B![options] [] ...!W! @@ -809,6 +842,7 @@ def execute(self): class HelpCommand(BaseCommand): CMD = "help" HELP_LINE = "Show help for Python installation manager commands" + USAGE_LINE = "help !B![]!W!" HELP_TEXT = r"""!G!Help command!W! > py help !B![] ...!W! @@ -824,7 +858,6 @@ def execute(self): self.show_welcome(copyright=False) if not self.args: LOGGER.print(BaseCommand.usage_text()) - LOGGER.print(BaseCommand.subcommands_list()) LOGGER.print(BaseCommand.help_text()) for a in self.args: try: @@ -846,7 +879,6 @@ def execute(self): LOGGER.print(COPYRIGHT) self.show_welcome(copyright=False) LOGGER.print(BaseCommand.usage_text()) - LOGGER.print(BaseCommand.subcommands_list()) LOGGER.print(f"The command !R!{EXE_NAME} {' '.join(self.args)}!W! was not recognized.") diff --git a/src/manage/list_command.py b/src/manage/list_command.py index e1c2869..acd9ff8 100644 --- a/src/manage/list_command.py +++ b/src/manage/list_command.py @@ -1,8 +1,10 @@ import json import sys +from . import logging from .exceptions import ArgumentError -from .logging import LOGGER + +LOGGER = logging.LOGGER def _exe_partition(n): @@ -102,6 +104,10 @@ def format_table(cmd, installs): except LookupError: pass + while sum(cwidth.values()) > logging.CONSOLE_MAX_WIDTH: + # TODO: Some kind of algorithm for reducing column widths to fit + break + LOGGER.print("!B!%s!W!", " ".join(columns[c].ljust(cwidth[c]) for c in columns), always=True) any_shown = False diff --git a/src/manage/logging.py b/src/manage/logging.py index 5d0148f..3a51641 100644 --- a/src/manage/logging.py +++ b/src/manage/logging.py @@ -1,6 +1,15 @@ import os import sys +# For convenient changing in the future. With a bit of luck, we will always +# depend on this constant dynamically, and so could update it at runtime, but +# don't assume that if you're adding that feature! +# Note that this only applies to deliberate formatting tasks. In general, we +# write entire lines of text unwrapped and let the console handle it, but some +# tasks (e.g. progress bars, tables) need to know the width. +CONSOLE_MAX_WIDTH = 80 + + DEBUG = 10 VERBOSE = 15 INFO = 20 @@ -174,8 +183,10 @@ def print(self, msg=None, *args, always=False, level=INFO, **kwargs): class ProgressPrinter: - def __init__(self, operation, maxwidth=80): + def __init__(self, operation, maxwidth=...): self.operation = operation or "Progress" + if maxwidth is ...: + maxwidth = CONSOLE_MAX_WIDTH self.width = maxwidth - 3 - len(self.operation) self._dots_shown = 0 self._started = False diff --git a/tests/conftest.py b/tests/conftest.py index 241903f..50c7ef9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,10 @@ setattr(_native, k, getattr(_native_test, k)) +import manage +manage.EXE_NAME = "pymanager-pytest" + + from manage.logging import LOGGER, DEBUG LOGGER.level = DEBUG From f45296679b9077983811c1e934caba4a9e446f09 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 30 Apr 2025 17:25:10 +0100 Subject: [PATCH 2/4] Make show_usage() function print directly and add tests --- src/manage/commands.py | 26 +++++++++++--------------- tests/conftest.py | 4 ++++ tests/test_commands.py | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/manage/commands.py b/src/manage/commands.py index efb24a8..bc24e53 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -513,7 +513,7 @@ def execute(self): raise NotImplementedError(f"'{type(self).__name__}' does not implement 'execute()'") @classmethod - def usage_text_lines(cls): + def show_usage(cls): if EXE_NAME.casefold() in ("py".casefold(), "pyw".casefold()): usage_docs = PY_USAGE_DOCS else: @@ -538,30 +538,26 @@ def usage_text_lines(cls): usage_ljust = max(usage_ljust, 16) + 1 sp = " " * usage_ljust - yield "!G!Usage:!W!" + LOGGER.print("!G!Usage:!W!") for k, d in usage_docs: if k.endswith("\n") and len(logging.strip_colour(k)) >= usage_ljust: - yield k.rstrip() + LOGGER.print(k.rstrip()) r = sp else: k = k.rstrip() r = k.ljust(usage_ljust + len(k) - len(logging.strip_colour(k))) for b in d.split(" "): if len(r) >= logging.CONSOLE_MAX_WIDTH: - yield r.rstrip() + LOGGER.print(r.rstrip()) r = sp r += b + " " if r.rstrip(): - yield r + LOGGER.print(r) - yield "" + LOGGER.print() # TODO: Remove the 3.14 for stable release - yield "Find additional information at !B!https://docs.python.org/3.14/using/windows!W!." - yield "" - - @classmethod - def usage_text(cls): - return "\n".join(cls.usage_text_lines()) + LOGGER.print("Find additional information at !B!https://docs.python.org/3.14/using/windows!W!.") + LOGGER.print() @classmethod def help_text(cls): @@ -569,7 +565,7 @@ def help_text(cls): def help(self): if type(self) is BaseCommand: - LOGGER.print(self.usage_text()) + self.show_usage() LOGGER.print(self.help_text()) try: LOGGER.print(self.HELP_TEXT.lstrip()) @@ -857,7 +853,7 @@ def execute(self): LOGGER.print(COPYRIGHT) self.show_welcome(copyright=False) if not self.args: - LOGGER.print(BaseCommand.usage_text()) + self.show_usage() LOGGER.print(BaseCommand.help_text()) for a in self.args: try: @@ -884,7 +880,7 @@ def execute(self): LOGGER.print(f"!R!Unknown command: {' '.join(args)}!W!") LOGGER.print(COPYRIGHT) self.show_welcome(copyright=False) - LOGGER.print(BaseCommand.usage_text()) + self.show_usage() LOGGER.print(f"The command !R!{' '.join(args)}!W! was not recognized.") diff --git a/tests/conftest.py b/tests/conftest.py index 49bc51e..fd844dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,10 @@ manage.EXE_NAME = "pymanager-pytest" +import manage.commands +manage.commands.WELCOME = "" + + from manage.logging import LOGGER, DEBUG LOGGER.level = DEBUG diff --git a/tests/test_commands.py b/tests/test_commands.py index 1608f94..c273f25 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -3,18 +3,44 @@ from manage import commands -def test_help_with_error_command(assert_log, monkeypatch): +def test_pymanager_help_command(assert_log): + cmd = commands.HelpCommand([commands.HelpCommand.CMD], None) + cmd.execute() + assert_log( + assert_log.skip_until(r"Python installation manager \d+\.\d+.*"), + assert_log.skip_until(".*pymanager-pytest exec -V.*"), + assert_log.skip_until(".*pymanager-pytest exec -3.*"), + assert_log.skip_until(".*pymanager-pytest install.*"), + assert_log.skip_until(".*pymanager-pytest list.*"), + assert_log.skip_until(".*pymanager-pytest uninstall.*"), + ) + + +def test_py_help_command(assert_log, monkeypatch): + monkeypatch.setattr(commands, "EXE_NAME", "py") + cmd = commands.HelpCommand([commands.HelpCommand.CMD], None) + cmd.execute() + assert_log( + assert_log.skip_until(r"Python installation manager \d+\.\d+.*"), + assert_log.skip_until(".*pymanager-pytest -V.*"), + assert_log.skip_until(".*pymanager-pytest -3.*"), + assert_log.skip_until(".*py install.*"), + assert_log.skip_until(".*py list.*"), + assert_log.skip_until(".*py uninstall.*"), + ) + + +def test_help_with_error_command(assert_log): expect = secrets.token_hex(16) cmd = commands.HelpWithErrorCommand( [commands.HelpWithErrorCommand.CMD, expect, "-v", "-q"], None ) - monkeypatch.setattr(commands, "WELCOME", "") cmd.execute() assert_log( - assert_log.skip_until(rf".*Unknown command: pymanager-pytest {expect} -v -q.*"), + assert_log.skip_until(f".*Unknown command: pymanager-pytest {expect} -v -q.*"), r"Python installation manager \d+\.\d+.*", - assert_log.skip_until(rf"The command .*?pymanager-pytest {expect} -v -q.*"), + assert_log.skip_until(f"The command .*?pymanager-pytest {expect} -v -q.*"), ) From ede6b4d37915152eb21485c44aa8197fd442aaf8 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 30 Apr 2025 17:39:17 +0100 Subject: [PATCH 3/4] Review feedback --- src/manage/commands.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/manage/commands.py b/src/manage/commands.py index bc24e53..082ad53 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -519,18 +519,16 @@ def show_usage(cls): else: usage_docs = PYMANAGER_USAGE_DOCS - usage_docs.extend( - [ - ( - f"{EXE_NAME} " + getattr(COMMANDS[cmd], "USAGE_LINE", cmd), - getattr(COMMANDS[cmd], "HELP_LINE", "") - ) - for cmd in sorted(COMMANDS) - if cmd[:1].isalpha() - ] - ) + usage_docs = usage_docs + [ + ( + f"{EXE_NAME} " + getattr(COMMANDS[cmd], "USAGE_LINE", cmd), + getattr(COMMANDS[cmd], "HELP_LINE", "") + ) + for cmd in sorted(COMMANDS) + if cmd[:1].isalpha() + ] - usage_docs = [(" " + x.lstrip(), y) for x, y in usage_docs] + usage_docs = [(f" {x.lstrip()}", y) for x, y in usage_docs] usage_ljust = max(len(logging.strip_colour(i[0])) for i in usage_docs if not i[0].endswith("\n")) if usage_ljust % 4: @@ -555,8 +553,8 @@ def show_usage(cls): LOGGER.print(r) LOGGER.print() - # TODO: Remove the 3.14 for stable release - LOGGER.print("Find additional information at !B!https://docs.python.org/3.14/using/windows!W!.") + # TODO: Remove the /dev/ for stable release + LOGGER.print("Find additional information at !B!https://docs.python.org/dev/using/windows!W!.") LOGGER.print() @classmethod From 94bf44ae12302981e42a4165f294b8973e98280f Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 30 Apr 2025 22:40:23 +0100 Subject: [PATCH 4/4] Add help entry for 'exec' --- _msbuild.py | 3 ++ src/manage/commands.py | 72 +++++++++++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/_msbuild.py b/_msbuild.py index 270e099..3ad317a 100644 --- a/_msbuild.py +++ b/_msbuild.py @@ -215,6 +215,9 @@ def get_commands(): # Check if a subclass of BaseCommand if not any(b.id in command_bases for b in cls.bases): continue + # Ignore exec command - it gets handled separately. + if cls.name == "ExecCommand": + continue command_bases.add(cls.name) for a in filter(lambda s: isinstance(s, ast.Assign), cls.body): if not any(t.id == "CMD" for t in a.targets): diff --git a/src/manage/commands.py b/src/manage/commands.py index 082ad53..083263b 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -55,6 +55,9 @@ "with the digit 3, platform overrides are permitted, and regular Python " + "options may follow. " + "!G!py -3!W! is the equivalent of the !G!python3!W! command."), + (f"{EXE_NAME} exec !B!!W!\n", + "Equivalent to any of the above launch options, and the requested runtime " + + "will be installed if needed."), ] @@ -519,14 +522,19 @@ def show_usage(cls): else: usage_docs = PYMANAGER_USAGE_DOCS - usage_docs = usage_docs + [ - ( - f"{EXE_NAME} " + getattr(COMMANDS[cmd], "USAGE_LINE", cmd), - getattr(COMMANDS[cmd], "HELP_LINE", "") - ) - for cmd in sorted(COMMANDS) - if cmd[:1].isalpha() - ] + usage_docs = list(usage_docs) + for cmd in sorted(COMMANDS): + if not cmd[:1].isalpha(): + continue + try: + usage_docs.append( + ( + f"{EXE_NAME} " + getattr(COMMANDS[cmd], "USAGE_LINE", cmd), + COMMANDS[cmd].HELP_LINE + ) + ) + except AttributeError: + pass usage_docs = [(f" {x.lstrip()}", y) for x, y in usage_docs] @@ -637,10 +645,12 @@ def get_install_to_run(self, tag=None, script=None, *, windowed=False): class ListCommand(BaseCommand): CMD = "list" - HELP_LINE = ("Shows installed Python runtimes, optionally filtering by " + + HELP_LINE = ("Show installed Python runtimes, optionally filtering by " + "!B!!W!.") USAGE_LINE = "list !B![]!W!" HELP_TEXT = r"""!G!List command!W! +Shows installed Python runtimes, optionally filtered or formatted. + > py list !B![options] [ ...]!W! !G!Options:!W! @@ -718,6 +728,8 @@ class InstallCommand(BaseCommand): "update existing installs.") USAGE_LINE = "install !B!!W!" HELP_TEXT = r"""!G!Install command!W! +Downloads new Python runtimes and sets up shortcuts and other registration. + > py install !B![options] [] ...!W! !G!Options:!W! @@ -797,12 +809,16 @@ class UninstallCommand(BaseCommand): "!B!--purge!W! to clean up all runtimes and cached files.") USAGE_LINE = "uninstall !B!!W!" HELP_TEXT = r"""!G!Uninstall command!W! +Removes one or more runtimes from your machine. + > py uninstall !B![options] [] ...!W! !G!Options:!W! - --purge Remove all runtimes, shortcuts, and cached files. Ignores tags. - --by-id Require TAG to exactly match the install ID. (For advanced use.) - !B! !W! ... One or more runtimes to uninstall (Company\Tag format) + --purge Remove all runtimes, shortcuts, and cached files. Ignores tags. + --by-id Require TAG to exactly match the install ID. (For advanced use.) + !B! !W! ... One or more runtimes to uninstall (Company\Tag format) + Each tag will only remove a single runtime, even if it matches + more than one. !B!EXAMPLE:!W! Uninstall Python 3.12 32-bit > py uninstall 3.12-32 @@ -838,6 +854,8 @@ class HelpCommand(BaseCommand): HELP_LINE = "Show help for Python installation manager commands" USAGE_LINE = "help !B![]!W!" HELP_TEXT = r"""!G!Help command!W! +Shows help for specific commands. + > py help !B![] ...!W! !G!Options:!W! @@ -882,6 +900,36 @@ def execute(self): LOGGER.print(f"The command !R!{' '.join(args)}!W! was not recognized.") +# This command exists solely to provide help. +# When it is specified, it gets handled in main.cpp +class ExecCommand(BaseCommand): + CMD = "exec" + HELP_TEXT = f"""!G!Execute command!W! +Launches the specified (or default) runtime. This command is optional when +launching through !G!py!W!, as the default behaviour is to launch a runtime. +When used explicitly, this command will automatically install the requested +runtime if it is not available. + +> {EXE_NAME} exec -V:!B!!W! ... +> {EXE_NAME} exec -3!B!!W! ... +> {EXE_NAME} exec ... +> py [ -V:!B!!W! | -3!B!!W! ] ... + +!G!Options:!W! + -V:!B!!W! Launch runtime identified by !B!!W!, which should include + the company name if not !B!PythonCore!W!. Regular Python options + may follow this option. The runtime will be installed if needed. + -3!B!!W! Equivalent to -V:PythonCore\3!B!!W!. The version must + begin with a '3', platform overrides are permitted, and regular + Python options may follow. The runtime will be installed if needed. +""" + + def __init__(self, args, root=None): + # Essentially disable argument processing for this command + super().__init__(args[:1], root) + self.args = args[1:] + + class DefaultConfig(BaseCommand): CMD = "__no_command" _create_log_file = False