Skip to content

Commit

Permalink
Merge pull request #6694 from cjerdonek/remove-command-imports
Browse files Browse the repository at this point in the history
Address #6692: Only import a Command class when needed
  • Loading branch information
cjerdonek authored Jul 28, 2019
2 parents 2e51624 + 4cd8258 commit 2cd26a2
Show file tree
Hide file tree
Showing 28 changed files with 164 additions and 137 deletions.
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

0 comments on commit 2cd26a2

Please sign in to comment.