Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address #6692: Only import a Command class when needed #6694

Merged
merged 2 commits into from
Jul 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/pip_sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from docutils.statemachine import ViewList

from pip._internal.cli import cmdoptions
from pip._internal.commands import commands_dict as commands
from pip._internal.commands import create_command


class PipCommandUsage(rst.Directive):
required_arguments = 1

def run(self):
cmd = commands[self.arguments[0]]
cmd = create_command(self.arguments[0])
usage = dedent(
cmd.usage.replace('%prog', 'pip {}'.format(cmd.name))
).strip()
Expand All @@ -31,7 +31,8 @@ def run(self):
node = nodes.paragraph()
node.document = self.state.document
desc = ViewList()
description = dedent(commands[self.arguments[0]].__doc__)
cmd = create_command(self.arguments[0])
description = dedent(cmd.__doc__)
for line in description.split('\n'):
desc.append(line, "")
self.state.nested_parse(desc, 0, node)
Expand Down Expand Up @@ -95,7 +96,7 @@ class PipCommandOptions(PipOptions):
required_arguments = 1

def process_options(self):
cmd = commands[self.arguments[0]]()
cmd = create_command(self.arguments[0])
self._format_options(
cmd.parser.option_groups[0].option_list,
cmd_name=cmd.name,
Expand Down
5 changes: 3 additions & 2 deletions src/pip/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

from pip._internal.cli.autocompletion import autocomplete
from pip._internal.cli.main_parser import parse_command
from pip._internal.commands import commands_dict
from pip._internal.commands import create_command
from pip._internal.exceptions import PipError
from pip._internal.utils import deprecation
from pip._vendor.urllib3.exceptions import InsecureRequestWarning
Expand Down Expand Up @@ -73,5 +73,6 @@ def main(args=None):
except locale.Error as e:
# setlocale can apparently crash if locale are uninitialized
logger.debug("Ignoring error %s when setting locale", e)
command = commands_dict[cmd_name](isolated=("--isolated" in cmd_args))
command = create_command(cmd_name, isolated=("--isolated" in cmd_args))

return command.main(cmd_args)
6 changes: 3 additions & 3 deletions src/pip/_internal/cli/autocompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys

from pip._internal.cli.main_parser import create_main_parser
from pip._internal.commands import commands_dict, get_summaries
from pip._internal.commands import commands_dict, create_command
from pip._internal.utils.misc import get_installed_distributions


Expand All @@ -23,7 +23,7 @@ def autocomplete():
except IndexError:
current = ''

subcommands = [cmd for cmd, summary in get_summaries()]
subcommands = list(commands_dict)
options = []
# subcommand
try:
Expand Down Expand Up @@ -54,7 +54,7 @@ def autocomplete():
print(dist)
sys.exit(1)

subcommand = commands_dict[subcommand_name]()
subcommand = create_command(subcommand_name)

for opt in subcommand.parser.option_list_all:
if opt.help != optparse.SUPPRESS_HELP:
Expand Down
8 changes: 5 additions & 3 deletions src/pip/_internal/cli/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,20 @@ class Command(object):
usage = None # type: Optional[str]
ignore_require_venv = False # type: bool

def __init__(self, isolated=False):
# type: (bool) -> None
def __init__(self, name, summary, isolated=False):
# type: (str, str, bool) -> None
parser_kw = {
'usage': self.usage,
'prog': '%s %s' % (get_prog(), self.name),
'formatter': UpdatingDefaultsHelpFormatter(),
'add_help_option': False,
'name': self.name,
'name': name,
'description': self.__doc__,
'isolated': isolated,
}

self.name = name
self.summary = summary
self.parser = ConfigOptionParser(**parser_kw)

# Commands should add options to this option group
Expand Down
12 changes: 5 additions & 7 deletions src/pip/_internal/cli/main_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
ConfigOptionParser,
UpdatingDefaultsHelpFormatter,
)
from pip._internal.commands import (
commands_dict,
get_similar_commands,
get_summaries,
)
from pip._internal.commands import commands_dict, get_similar_commands
from pip._internal.exceptions import CommandError
from pip._internal.utils.misc import get_pip_version, get_prog
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
Expand Down Expand Up @@ -51,8 +47,10 @@ def create_main_parser():
parser.main = True # type: ignore

# create command listing for description
command_summaries = get_summaries()
description = [''] + ['%-27s %s' % (i, j) for i, j in command_summaries]
description = [''] + [
'%-27s %s' % (name, command_info.summary)
for name, command_info in commands_dict.items()
]
parser.description = '\n'.join(description)

return parser
Expand Down
139 changes: 84 additions & 55 deletions src/pip/_internal/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,97 @@
"""
from __future__ import absolute_import

from pip._internal.commands.completion import CompletionCommand
from pip._internal.commands.configuration import ConfigurationCommand
from pip._internal.commands.debug import DebugCommand
from pip._internal.commands.download import DownloadCommand
from pip._internal.commands.freeze import FreezeCommand
from pip._internal.commands.hash import HashCommand
from pip._internal.commands.help import HelpCommand
from pip._internal.commands.list import ListCommand
from pip._internal.commands.check import CheckCommand
from pip._internal.commands.search import SearchCommand
from pip._internal.commands.show import ShowCommand
from pip._internal.commands.install import InstallCommand
from pip._internal.commands.uninstall import UninstallCommand
from pip._internal.commands.wheel import WheelCommand
import importlib
from collections import namedtuple, OrderedDict

from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
from typing import List, Type
from typing import Any
from pip._internal.cli.base_command import Command

commands_order = [
InstallCommand,
DownloadCommand,
UninstallCommand,
FreezeCommand,
ListCommand,
ShowCommand,
CheckCommand,
ConfigurationCommand,
SearchCommand,
WheelCommand,
HashCommand,
CompletionCommand,
DebugCommand,
HelpCommand,
] # type: List[Type[Command]]

commands_dict = {c.name: c for c in commands_order}


def get_summaries(ordered=True):
"""Yields sorted (command name, command summary) tuples."""

if ordered:
cmditems = _sort_commands(commands_dict, commands_order)
else:
cmditems = commands_dict.items()

for name, command_class in cmditems:
yield (name, command_class.summary)
CommandInfo = namedtuple('CommandInfo', 'module_path, class_name, summary')

# The ordering matters for help display.
# Also, even though the module path starts with the same
# "pip._internal.commands" prefix in each case, we include the full path
# because it makes testing easier (specifically when modifying commands_dict
# in test setup / teardown by adding info for a FakeCommand class defined
# in a test-related module).
# Finally, we need to pass an iterable of pairs here rather than a dict
# so that the ordering won't be lost when using Python 2.7.
commands_dict = OrderedDict([
('install', CommandInfo(
'pip._internal.commands.install', 'InstallCommand',
'Install packages.',
)),
('download', CommandInfo(
'pip._internal.commands.download', 'DownloadCommand',
'Download packages.',
)),
('uninstall', CommandInfo(
'pip._internal.commands.uninstall', 'UninstallCommand',
'Uninstall packages.',
)),
('freeze', CommandInfo(
'pip._internal.commands.freeze', 'FreezeCommand',
'Output installed packages in requirements format.',
)),
('list', CommandInfo(
'pip._internal.commands.list', 'ListCommand',
'List installed packages.',
)),
('show', CommandInfo(
'pip._internal.commands.show', 'ShowCommand',
'Show information about installed packages.',
)),
('check', CommandInfo(
'pip._internal.commands.check', 'CheckCommand',
'Verify installed packages have compatible dependencies.',
)),
('config', CommandInfo(
'pip._internal.commands.configuration', 'ConfigurationCommand',
'Manage local and global configuration.',
)),
('search', CommandInfo(
'pip._internal.commands.search', 'SearchCommand',
'Search PyPI for packages.',
)),
('wheel', CommandInfo(
'pip._internal.commands.wheel', 'WheelCommand',
'Build wheels from your requirements.',
)),
('hash', CommandInfo(
'pip._internal.commands.hash', 'HashCommand',
'Compute hashes of package archives.',
)),
('completion', CommandInfo(
'pip._internal.commands.completion', 'CompletionCommand',
'A helper command used for command completion.',
)),
('debug', CommandInfo(
'pip._internal.commands.debug', 'DebugCommand',
'Show information useful for debugging.',
)),
('help', CommandInfo(
'pip._internal.commands.help', 'HelpCommand',
'Show help for commands.',
)),
]) # type: OrderedDict[str, CommandInfo]


def create_command(name, **kwargs):
# type: (str, **Any) -> Command
"""
Create an instance of the Command class with the given name.
"""
module_path, class_name, summary = commands_dict[name]
module = importlib.import_module(module_path)
command_class = getattr(module, class_name)
command = command_class(name=name, summary=summary, **kwargs)

return command


def get_similar_commands(name):
Expand All @@ -68,14 +108,3 @@ def get_similar_commands(name):
return close_commands[0]
else:
return False


def _sort_commands(cmddict, order):
def keyfn(key):
try:
return order.index(key[1])
except ValueError:
# unordered items should come last
return 0xff

return sorted(cmddict.items(), key=keyfn)
3 changes: 1 addition & 2 deletions src/pip/_internal/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@

class CheckCommand(Command):
"""Verify installed packages have compatible dependencies."""
name = 'check'

usage = """
%prog [options]"""
summary = 'Verify installed packages have compatible dependencies.'

def run(self, options, args):
package_set, parsing_probs = create_package_set_from_installed()
Expand Down
3 changes: 1 addition & 2 deletions src/pip/_internal/commands/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@

class CompletionCommand(Command):
"""A helper command to be used for command completion."""
name = 'completion'
summary = 'A helper command used for command completion.'

ignore_require_venv = True

def __init__(self, *args, **kw):
Expand Down
3 changes: 0 additions & 3 deletions src/pip/_internal/commands/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ class ConfigurationCommand(Command):
default.
"""

name = 'config'
usage = """
%prog [<file-option>] list
%prog [<file-option>] [--editor <editor-path>] edit
Expand All @@ -44,8 +43,6 @@ class ConfigurationCommand(Command):
%prog [<file-option>] unset name
"""

summary = "Manage local and global configuration."

def __init__(self, *args, **kwargs):
super(ConfigurationCommand, self).__init__(*args, **kwargs)

Expand Down
2 changes: 0 additions & 2 deletions src/pip/_internal/commands/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,8 @@ class DebugCommand(Command):
Display debug information.
"""

name = 'debug'
usage = """
%prog <options>"""
summary = 'Show information useful for debugging.'
ignore_require_venv = True

def __init__(self, *args, **kw):
Expand Down
3 changes: 0 additions & 3 deletions src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class DownloadCommand(RequirementCommand):
pip also supports downloading from "requirements files", which provide
an easy way to specify a whole environment to be downloaded.
"""
name = 'download'

usage = """
%prog [options] <requirement specifier> [package-index-options] ...
Expand All @@ -38,8 +37,6 @@ class DownloadCommand(RequirementCommand):
%prog [options] <local project path> ...
%prog [options] <archive url/path> ..."""

summary = 'Download packages.'

def __init__(self, *args, **kw):
super(DownloadCommand, self).__init__(*args, **kw)

Expand Down
3 changes: 1 addition & 2 deletions src/pip/_internal/commands/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ class FreezeCommand(Command):
packages are listed in a case-insensitive sorted order.
"""
name = 'freeze'

usage = """
%prog [options]"""
summary = 'Output installed packages in requirements format.'
log_streams = ("ext://sys.stderr", "ext://sys.stderr")

def __init__(self, *args, **kw):
Expand Down
4 changes: 1 addition & 3 deletions src/pip/_internal/commands/hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ class HashCommand(Command):

These can be used with --hash in a requirements file to do repeatable
installs.

"""
name = 'hash'

usage = '%prog [options] <file> ...'
summary = 'Compute hashes of package archives.'
ignore_require_venv = True

def __init__(self, *args, **kw):
Expand Down
Loading