diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7ce634abb4..019eba65b90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,6 @@ jobs: tests: # Anything that's touching testable stuff - ".github/workflows/ci.yml" - - "tools/requirements/tests.txt" - "src/**" - "tests/**" if: github.event_name == 'pull_request' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c00d67a621..8882f6bbe8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,11 +22,9 @@ repos: - id: black exclude: | (?x) - ^src/pip/_internal/commands| ^src/pip/_internal/index| ^src/pip/_internal/models| ^src/pip/_internal/operations| - ^src/pip/_internal/req| ^src/pip/_internal/vcs| ^src/pip/_internal/\w+\.py$| # Tests diff --git a/.readthedocs.yml b/.readthedocs.yml index bd4cb9cd3cd..7d62011a6e3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,4 +7,4 @@ sphinx: python: version: 3.8 install: - - requirements: tools/requirements/docs.txt + - requirements: docs/requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index 9148af0b652..f9b15403e84 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,7 @@ recursive-include src/pip/_vendor *LICENSE* recursive-include src/pip/_vendor *COPYING* include docs/docutils.conf +include docs/requirements.txt exclude .coveragerc exclude .mailmap diff --git a/docs/html/cli/pip.rst b/docs/html/cli/pip.rst index 1f52630f69f..6ac4caaec15 100644 --- a/docs/html/cli/pip.rst +++ b/docs/html/cli/pip.rst @@ -47,7 +47,7 @@ verbosity log will be kept. This option is empty by default. This log appends to previous logging. Like all pip options, ``--log`` can also be set as an environment variable, or -placed into the pip config file. See the :ref:`Configuration` section. +placed into the pip config file. See the :doc:`topics/configuration` section. .. _`exists-action`: diff --git a/docs/html/development/configuration.rst b/docs/html/development/configuration.rst index 8615065aa6b..b099e327eb3 100644 --- a/docs/html/development/configuration.rst +++ b/docs/html/development/configuration.rst @@ -4,4 +4,4 @@ Configuration ============= -This content is now covered in the :ref:`Configuration` section of the :doc:`User Guide `. +This content is now covered in the :doc:`topics/configuration` section of the :doc:`User Guide `. diff --git a/tools/requirements/docs.txt b/docs/requirements.txt similarity index 93% rename from tools/requirements/docs.txt rename to docs/requirements.txt index 1ab19c9b9ab..e311562e669 100644 --- a/tools/requirements/docs.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx == 3.2.1 +sphinx ~= 4.1.0 towncrier furo myst_parser diff --git a/news/10128.removal.rst b/news/10128.removal.rst new file mode 100644 index 00000000000..850b645642f --- /dev/null +++ b/news/10128.removal.rst @@ -0,0 +1 @@ +Improve deprecation warning regarding the copying of source trees when installing from a local directory. diff --git a/news/10165.trivial.rst b/news/10165.trivial.rst new file mode 100644 index 00000000000..45d40200c5b --- /dev/null +++ b/news/10165.trivial.rst @@ -0,0 +1,4 @@ +Add a ``feature_flag`` optional kwarg to the ``deprecated()`` function +``pip._internal.utils.deprecation:deprecated``. Also formulate a corresponding canned +message which suggests using the ``--use-feature={feature_flag}`` to test upcoming +behavior. diff --git a/news/10233.bugfix.rst b/news/10233.bugfix.rst new file mode 100644 index 00000000000..8578a7dd071 --- /dev/null +++ b/news/10233.bugfix.rst @@ -0,0 +1,3 @@ +New resolver: When a package is specified with extras in constraints, and with +extras in non-constraint requirements, the resolver now correctly identifies the +constraint's existence and avoids backtracking. diff --git a/news/10252.bugfix.rst b/news/10252.bugfix.rst new file mode 100644 index 00000000000..ebeeefd25a8 --- /dev/null +++ b/news/10252.bugfix.rst @@ -0,0 +1,2 @@ +Modify the ``sysconfig.get_preferred_scheme`` function check to be +compatible with CPython 3.10’s alphareleases. diff --git a/news/2A2E5A1E-F014-40E9-B0EF-0D9C4686358F.trivial.rst b/news/2A2E5A1E-F014-40E9-B0EF-0D9C4686358F.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/51d70b65-78e9-426c-80a6-47efcaad9628.trivial.rst b/news/51d70b65-78e9-426c-80a6-47efcaad9628.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/noxfile.py b/noxfile.py index becd0c30480..dae585738e3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -24,9 +24,9 @@ "protected-pip": "tools/tox_pip.py", } REQUIREMENTS = { - "docs": "tools/requirements/docs.txt", - "tests": "tools/requirements/tests.txt", - "common-wheels": "tools/requirements/tests-common_wheels.txt", + "docs": "docs/requirements.txt", + "tests": "tests/requirements.txt", + "common-wheels": "tests/requirements-common_wheels.txt", } AUTHORS_FILE = "AUTHORS.txt" diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 8e94b38f70f..c72f24f30e2 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -3,87 +3,102 @@ """ import importlib -from collections import OrderedDict, namedtuple +from collections import namedtuple from typing import Any, Dict, Optional from pip._internal.cli.base_command import Command -CommandInfo = namedtuple('CommandInfo', 'module_path, class_name, 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: Dict[str, CommandInfo] = 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.', - )), - ('cache', CommandInfo( - 'pip._internal.commands.cache', 'CacheCommand', +# This dictionary does a bunch of heavy lifting for help output: +# - Enables avoiding additional (costly) imports for presenting `--help`. +# - The ordering matters for help display. +# +# Even though the module path starts with the same "pip._internal.commands" +# prefix, the full path makes testing easier (specifically when modifying +# `commands_dict` in test setup / teardown). +commands_dict: Dict[str, CommandInfo] = { + "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.", + ), + "cache": CommandInfo( + "pip._internal.commands.cache", + "CacheCommand", "Inspect and manage pip's wheel cache.", - )), - ('index', CommandInfo( - 'pip._internal.commands.index', 'IndexCommand', + ), + "index": CommandInfo( + "pip._internal.commands.index", + "IndexCommand", "Inspect information available from package indexes.", - )), - ('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.', - )), -]) + ), + "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.", + ), +} def create_command(name: str, **kwargs: Any) -> Command: diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 3a5bb9c885f..dc307ef202d 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -39,12 +39,12 @@ class CacheCommand(Command): def add_options(self) -> None: self.cmd_opts.add_option( - '--format', - action='store', - dest='list_format', + "--format", + action="store", + dest="list_format", default="human", - choices=('human', 'abspath'), - help="Select the output format among: human (default) or abspath" + choices=("human", "abspath"), + help="Select the output format among: human (default) or abspath", ) self.parser.insert_option_group(0, self.cmd_opts) @@ -59,8 +59,7 @@ def run(self, options: Values, args: List[Any]) -> int: } if not options.cache_dir: - logger.error("pip cache commands can not " - "function since cache is disabled.") + logger.error("pip cache commands can not function since cache is disabled.") return ERROR # Determine action @@ -84,69 +83,73 @@ def run(self, options: Values, args: List[Any]) -> int: def get_cache_dir(self, options: Values, args: List[Any]) -> None: if args: - raise CommandError('Too many arguments') + raise CommandError("Too many arguments") logger.info(options.cache_dir) def get_cache_info(self, options: Values, args: List[Any]) -> None: if args: - raise CommandError('Too many arguments') + raise CommandError("Too many arguments") num_http_files = len(self._find_http_files(options)) - num_packages = len(self._find_wheels(options, '*')) + num_packages = len(self._find_wheels(options, "*")) - http_cache_location = self._cache_dir(options, 'http') - wheels_cache_location = self._cache_dir(options, 'wheels') + http_cache_location = self._cache_dir(options, "http") + wheels_cache_location = self._cache_dir(options, "wheels") http_cache_size = filesystem.format_directory_size(http_cache_location) - wheels_cache_size = filesystem.format_directory_size( - wheels_cache_location + wheels_cache_size = filesystem.format_directory_size(wheels_cache_location) + + message = ( + textwrap.dedent( + """ + Package index page cache location: {http_cache_location} + Package index page cache size: {http_cache_size} + Number of HTTP files: {num_http_files} + Wheels location: {wheels_cache_location} + Wheels size: {wheels_cache_size} + Number of wheels: {package_count} + """ + ) + .format( + http_cache_location=http_cache_location, + http_cache_size=http_cache_size, + num_http_files=num_http_files, + wheels_cache_location=wheels_cache_location, + package_count=num_packages, + wheels_cache_size=wheels_cache_size, + ) + .strip() ) - message = textwrap.dedent(""" - Package index page cache location: {http_cache_location} - Package index page cache size: {http_cache_size} - Number of HTTP files: {num_http_files} - Wheels location: {wheels_cache_location} - Wheels size: {wheels_cache_size} - Number of wheels: {package_count} - """).format( - http_cache_location=http_cache_location, - http_cache_size=http_cache_size, - num_http_files=num_http_files, - wheels_cache_location=wheels_cache_location, - package_count=num_packages, - wheels_cache_size=wheels_cache_size, - ).strip() - logger.info(message) def list_cache_items(self, options: Values, args: List[Any]) -> None: if len(args) > 1: - raise CommandError('Too many arguments') + raise CommandError("Too many arguments") if args: pattern = args[0] else: - pattern = '*' + pattern = "*" files = self._find_wheels(options, pattern) - if options.list_format == 'human': + if options.list_format == "human": self.format_for_human(files) else: self.format_for_abspath(files) def format_for_human(self, files: List[str]) -> None: if not files: - logger.info('Nothing cached.') + logger.info("Nothing cached.") return results = [] for filename in files: wheel = os.path.basename(filename) size = filesystem.format_file_size(filename) - results.append(f' - {wheel} ({size})') - logger.info('Cache contents:\n') - logger.info('\n'.join(sorted(results))) + results.append(f" - {wheel} ({size})") + logger.info("Cache contents:\n") + logger.info("\n".join(sorted(results))) def format_for_abspath(self, files: List[str]) -> None: if not files: @@ -156,23 +159,23 @@ def format_for_abspath(self, files: List[str]) -> None: for filename in files: results.append(filename) - logger.info('\n'.join(sorted(results))) + logger.info("\n".join(sorted(results))) def remove_cache_items(self, options: Values, args: List[Any]) -> None: if len(args) > 1: - raise CommandError('Too many arguments') + raise CommandError("Too many arguments") if not args: - raise CommandError('Please provide a pattern') + raise CommandError("Please provide a pattern") files = self._find_wheels(options, args[0]) # Only fetch http files if no specific pattern given - if args[0] == '*': + if args[0] == "*": files += self._find_http_files(options) if not files: - raise CommandError('No matching packages') + raise CommandError("No matching packages") for filename in files: os.unlink(filename) @@ -181,19 +184,19 @@ def remove_cache_items(self, options: Values, args: List[Any]) -> None: def purge_cache(self, options: Values, args: List[Any]) -> None: if args: - raise CommandError('Too many arguments') + raise CommandError("Too many arguments") - return self.remove_cache_items(options, ['*']) + return self.remove_cache_items(options, ["*"]) def _cache_dir(self, options: Values, subdir: str) -> str: return os.path.join(options.cache_dir, subdir) def _find_http_files(self, options: Values) -> List[str]: - http_dir = self._cache_dir(options, 'http') - return filesystem.find_files(http_dir, '*') + http_dir = self._cache_dir(options, "http") + return filesystem.find_files(http_dir, "*") def _find_wheels(self, options: Values, pattern: str) -> List[str]: - wheel_dir = self._cache_dir(options, 'wheels') + wheel_dir = self._cache_dir(options, "wheels") # The wheel filename format, as specified in PEP 427, is: # {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index f9412a7a9cb..ec08928a993 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -29,7 +29,9 @@ def run(self, options: Values, args: List[Any]) -> int: for dependency in missing[project_name]: write_output( "%s %s requires %s, which is not installed.", - project_name, version, dependency[0], + project_name, + version, + dependency[0], ) for project_name in conflicting: @@ -37,7 +39,11 @@ def run(self, options: Values, args: List[Any]) -> int: for dep_name, dep_version, req in conflicting[project_name]: write_output( "%s %s has requirement %s, but you have %s %s.", - project_name, version, req, dep_name, dep_version, + project_name, + version, + req, + dep_name, + dep_version, ) if missing or conflicting or parsing_probs: diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 9ce7888ef28..c0fb4caf8b2 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -12,7 +12,7 @@ """ COMPLETION_SCRIPTS = { - 'bash': """ + "bash": """ _pip_completion() {{ COMPREPLY=( $( COMP_WORDS="${{COMP_WORDS[*]}}" \\ @@ -21,7 +21,7 @@ }} complete -o default -F _pip_completion {prog} """, - 'zsh': """ + "zsh": """ function _pip_completion {{ local words cword read -Ac words @@ -32,7 +32,7 @@ }} compctl -K _pip_completion {prog} """, - 'fish': """ + "fish": """ function __fish_complete_pip set -lx COMP_WORDS (commandline -o) "" set -lx COMP_CWORD ( \\ @@ -53,39 +53,44 @@ class CompletionCommand(Command): def add_options(self) -> None: self.cmd_opts.add_option( - '--bash', '-b', - action='store_const', - const='bash', - dest='shell', - help='Emit completion code for bash') + "--bash", + "-b", + action="store_const", + const="bash", + dest="shell", + help="Emit completion code for bash", + ) self.cmd_opts.add_option( - '--zsh', '-z', - action='store_const', - const='zsh', - dest='shell', - help='Emit completion code for zsh') + "--zsh", + "-z", + action="store_const", + const="zsh", + dest="shell", + help="Emit completion code for zsh", + ) self.cmd_opts.add_option( - '--fish', '-f', - action='store_const', - const='fish', - dest='shell', - help='Emit completion code for fish') + "--fish", + "-f", + action="store_const", + const="fish", + dest="shell", + help="Emit completion code for fish", + ) self.parser.insert_option_group(0, self.cmd_opts) def run(self, options: Values, args: List[str]) -> int: """Prints the completion code of the given shell""" shells = COMPLETION_SCRIPTS.keys() - shell_options = ['--' + shell for shell in sorted(shells)] + shell_options = ["--" + shell for shell in sorted(shells)] if options.shell in shells: script = textwrap.dedent( - COMPLETION_SCRIPTS.get(options.shell, '').format( - prog=get_prog()) + COMPLETION_SCRIPTS.get(options.shell, "").format(prog=get_prog()) ) print(BASE_COMPLETION.format(script=script, shell=options.shell)) return SUCCESS else: sys.stderr.write( - 'ERROR: You must pass {}\n' .format(' or '.join(shell_options)) + "ERROR: You must pass {}\n".format(" or ".join(shell_options)) ) return SUCCESS diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 6e47b8732d1..c6c74ed50ba 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -34,7 +34,7 @@ class ConfigurationCommand(Command): If none of --user, --global and --site are passed, a virtual environment configuration file is used if one is active and the file - exists. Otherwise, all modifications happen on the to the user file by + exists. Otherwise, all modifications happen to the user file by default. """ @@ -51,38 +51,38 @@ class ConfigurationCommand(Command): def add_options(self) -> None: self.cmd_opts.add_option( - '--editor', - dest='editor', - action='store', + "--editor", + dest="editor", + action="store", default=None, help=( - 'Editor to use to edit the file. Uses VISUAL or EDITOR ' - 'environment variables if not provided.' - ) + "Editor to use to edit the file. Uses VISUAL or EDITOR " + "environment variables if not provided." + ), ) self.cmd_opts.add_option( - '--global', - dest='global_file', - action='store_true', + "--global", + dest="global_file", + action="store_true", default=False, - help='Use the system-wide configuration file only' + help="Use the system-wide configuration file only", ) self.cmd_opts.add_option( - '--user', - dest='user_file', - action='store_true', + "--user", + dest="user_file", + action="store_true", default=False, - help='Use the user configuration file only' + help="Use the user configuration file only", ) self.cmd_opts.add_option( - '--site', - dest='site_file', - action='store_true', + "--site", + dest="site_file", + action="store_true", default=False, - help='Use the current environment configuration file only' + help="Use the current environment configuration file only", ) self.parser.insert_option_group(0, self.cmd_opts) @@ -133,11 +133,15 @@ def run(self, options: Values, args: List[str]) -> int: return SUCCESS def _determine_file(self, options: Values, need_value: bool) -> Optional[Kind]: - file_options = [key for key, value in ( - (kinds.USER, options.user_file), - (kinds.GLOBAL, options.global_file), - (kinds.SITE, options.site_file), - ) if value] + file_options = [ + key + for key, value in ( + (kinds.USER, options.user_file), + (kinds.GLOBAL, options.global_file), + (kinds.SITE, options.site_file), + ) + if value + ] if not file_options: if not need_value: @@ -194,24 +198,22 @@ def list_config_values(self, options: Values, args: List[str]) -> None: for fname in files: with indent_log(): file_exists = os.path.exists(fname) - write_output("%s, exists: %r", - fname, file_exists) + write_output("%s, exists: %r", fname, file_exists) if file_exists: self.print_config_file_values(variant) def print_config_file_values(self, variant: Kind) -> None: """Get key-value pairs from the file of a variant""" - for name, value in self.configuration.\ - get_values_in_config(variant).items(): + for name, value in self.configuration.get_values_in_config(variant).items(): with indent_log(): write_output("%s: %s", name, value) def print_env_var_values(self) -> None: """Get key-values pairs present as environment variables""" - write_output("%s:", 'env_var') + write_output("%s:", "env_var") with indent_log(): for key, value in sorted(self.configuration.get_environ_vars()): - env_var = f'PIP_{key.upper()}' + env_var = f"PIP_{key.upper()}" write_output("%s=%r", env_var, value) def open_in_editor(self, options: Values, args: List[str]) -> None: @@ -225,16 +227,14 @@ def open_in_editor(self, options: Values, args: List[str]) -> None: subprocess.check_call([editor, fname]) except subprocess.CalledProcessError as e: raise PipError( - "Editor Subprocess exited with exit code {}" - .format(e.returncode) + "Editor Subprocess exited with exit code {}".format(e.returncode) ) def _get_n_args(self, args: List[str], example: str, n: int) -> Any: - """Helper to make sure the command got the right number of arguments - """ + """Helper to make sure the command got the right number of arguments""" if len(args) != n: msg = ( - 'Got unexpected number of arguments, expected {}. ' + "Got unexpected number of arguments, expected {}. " '(example: "{} config {}")' ).format(n, get_prog(), example) raise PipError(msg) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index b316b67bd2c..d3f1f28de4c 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -24,52 +24,46 @@ def show_value(name: str, value: Any) -> None: - logger.info('%s: %s', name, value) + logger.info("%s: %s", name, value) def show_sys_implementation() -> None: - logger.info('sys.implementation:') + logger.info("sys.implementation:") implementation_name = sys.implementation.name with indent_log(): - show_value('name', implementation_name) + show_value("name", implementation_name) def create_vendor_txt_map() -> Dict[str, str]: vendor_txt_path = os.path.join( - os.path.dirname(pip_location), - '_vendor', - 'vendor.txt' + os.path.dirname(pip_location), "_vendor", "vendor.txt" ) with open(vendor_txt_path) as f: # Purge non version specifying lines. # Also, remove any space prefix or suffixes (including comments). - lines = [line.strip().split(' ', 1)[0] - for line in f.readlines() if '==' in line] + lines = [ + line.strip().split(" ", 1)[0] for line in f.readlines() if "==" in line + ] # Transform into "module" -> version dict. - return dict(line.split('==', 1) for line in lines) # type: ignore + return dict(line.split("==", 1) for line in lines) # type: ignore def get_module_from_module_name(module_name: str) -> ModuleType: # Module name can be uppercase in vendor.txt for some reason... module_name = module_name.lower() # PATCH: setuptools is actually only pkg_resources. - if module_name == 'setuptools': - module_name = 'pkg_resources' - - __import__( - f'pip._vendor.{module_name}', - globals(), - locals(), - level=0 - ) + if module_name == "setuptools": + module_name = "pkg_resources" + + __import__(f"pip._vendor.{module_name}", globals(), locals(), level=0) return getattr(pip._vendor, module_name) def get_vendor_version_from_module(module_name: str) -> Optional[str]: module = get_module_from_module_name(module_name) - version = getattr(module, '__version__', None) + version = getattr(module, "__version__", None) if not version: # Try to find version in debundled module info. @@ -86,20 +80,24 @@ def show_actual_vendor_versions(vendor_txt_versions: Dict[str, str]) -> None: a conflict or if the actual version could not be imported. """ for module_name, expected_version in vendor_txt_versions.items(): - extra_message = '' + extra_message = "" actual_version = get_vendor_version_from_module(module_name) if not actual_version: - extra_message = ' (Unable to locate actual module version, using'\ - ' vendor.txt specified version)' + extra_message = ( + " (Unable to locate actual module version, using" + " vendor.txt specified version)" + ) actual_version = expected_version elif parse_version(actual_version) != parse_version(expected_version): - extra_message = ' (CONFLICT: vendor.txt suggests version should'\ - ' be {})'.format(expected_version) - logger.info('%s==%s%s', module_name, actual_version, extra_message) + extra_message = ( + " (CONFLICT: vendor.txt suggests version should" + " be {})".format(expected_version) + ) + logger.info("%s==%s%s", module_name, actual_version, extra_message) def show_vendor_versions() -> None: - logger.info('vendored library versions:') + logger.info("vendored library versions:") vendor_txt_versions = create_vendor_txt_map() with indent_log(): @@ -114,11 +112,11 @@ def show_tags(options: Values) -> None: # Display the target options that were explicitly provided. formatted_target = target_python.format_given() - suffix = '' + suffix = "" if formatted_target: - suffix = f' (target: {formatted_target})' + suffix = f" (target: {formatted_target})" - msg = 'Compatible tags: {}{}'.format(len(tags), suffix) + msg = "Compatible tags: {}{}".format(len(tags), suffix) logger.info(msg) if options.verbose < 1 and len(tags) > tag_limit: @@ -133,8 +131,7 @@ def show_tags(options: Values) -> None: if tags_limited: msg = ( - '...\n' - '[First {tag_limit} tags shown. Pass --verbose to show all.]' + "...\n[First {tag_limit} tags shown. Pass --verbose to show all.]" ).format(tag_limit=tag_limit) logger.info(msg) @@ -142,20 +139,20 @@ def show_tags(options: Values) -> None: def ca_bundle_info(config: Configuration) -> str: levels = set() for key, _ in config.items(): - levels.add(key.split('.')[0]) + levels.add(key.split(".")[0]) if not levels: return "Not specified" - levels_that_override_global = ['install', 'wheel', 'download'] + levels_that_override_global = ["install", "wheel", "download"] global_overriding_level = [ level for level in levels if level in levels_that_override_global ] if not global_overriding_level: - return 'global' + return "global" - if 'global' in levels: - levels.remove('global') + if "global" in levels: + levels.remove("global") return ", ".join(levels) @@ -180,20 +177,21 @@ def run(self, options: Values, args: List[str]) -> int: "details, since the output and options of this command may " "change without notice." ) - show_value('pip version', get_pip_version()) - show_value('sys.version', sys.version) - show_value('sys.executable', sys.executable) - show_value('sys.getdefaultencoding', sys.getdefaultencoding()) - show_value('sys.getfilesystemencoding', sys.getfilesystemencoding()) + show_value("pip version", get_pip_version()) + show_value("sys.version", sys.version) + show_value("sys.executable", sys.executable) + show_value("sys.getdefaultencoding", sys.getdefaultencoding()) + show_value("sys.getfilesystemencoding", sys.getfilesystemencoding()) show_value( - 'locale.getpreferredencoding', locale.getpreferredencoding(), + "locale.getpreferredencoding", + locale.getpreferredencoding(), ) - show_value('sys.platform', sys.platform) + show_value("sys.platform", sys.platform) show_sys_implementation() show_value("'cert' config value", ca_bundle_info(self.parser.config)) - show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE')) - show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE')) + show_value("REQUESTS_CA_BUNDLE", os.environ.get("REQUESTS_CA_BUNDLE")) + show_value("CURL_CA_BUNDLE", os.environ.get("CURL_CA_BUNDLE")) show_value("pip._vendor.certifi.where()", where()) show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 230264591f0..2f6aac29e83 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -53,11 +53,14 @@ def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) self.cmd_opts.add_option( - '-d', '--dest', '--destination-dir', '--destination-directory', - dest='download_dir', - metavar='dir', + "-d", + "--dest", + "--destination-dir", + "--destination-directory", + dest="download_dir", + metavar="dir", default=os.curdir, - help=("Download packages into ."), + help="Download packages into .", ) cmdoptions.add_target_python_options(self.cmd_opts) @@ -123,9 +126,7 @@ def run(self, options: Values, args: List[str]) -> int: self.trace_basic_info(finder) - requirement_set = resolver.resolve( - reqs, check_supported_wheels=True - ) + requirement_set = resolver.resolve(reqs, check_supported_wheels=True) downloaded: List[str] = [] for req in requirement_set.requirements.values(): @@ -134,6 +135,6 @@ def run(self, options: Values, args: List[str]) -> int: preparer.save_linked_requirement(req) downloaded.append(req.name) if downloaded: - write_output('Successfully downloaded %s', ' '.join(downloaded)) + write_output("Successfully downloaded %s", " ".join(downloaded)) return SUCCESS diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 1ccc8752507..5fa6d39b2c7 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -8,7 +8,7 @@ from pip._internal.operations.freeze import freeze from pip._internal.utils.compat import stdlib_pkgs -DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'} +DEV_PKGS = {"pip", "setuptools", "distribute", "wheel"} class FreezeCommand(Command): @@ -24,39 +24,52 @@ class FreezeCommand(Command): def add_options(self) -> None: self.cmd_opts.add_option( - '-r', '--requirement', - dest='requirements', - action='append', + "-r", + "--requirement", + dest="requirements", + action="append", default=[], - metavar='file', - help="Use the order in the given requirements file and its " - "comments when generating output. This option can be " - "used multiple times.") + metavar="file", + help=( + "Use the order in the given requirements file and its " + "comments when generating output. This option can be " + "used multiple times." + ), + ) self.cmd_opts.add_option( - '-l', '--local', - dest='local', - action='store_true', + "-l", + "--local", + dest="local", + action="store_true", default=False, - help='If in a virtualenv that has global access, do not output ' - 'globally-installed packages.') + help=( + "If in a virtualenv that has global access, do not output " + "globally-installed packages." + ), + ) self.cmd_opts.add_option( - '--user', - dest='user', - action='store_true', + "--user", + dest="user", + action="store_true", default=False, - help='Only output packages installed in user-site.') + help="Only output packages installed in user-site.", + ) self.cmd_opts.add_option(cmdoptions.list_path()) self.cmd_opts.add_option( - '--all', - dest='freeze_all', - action='store_true', - help='Do not skip these packages in the output:' - ' {}'.format(', '.join(DEV_PKGS))) + "--all", + dest="freeze_all", + action="store_true", + help=( + "Do not skip these packages in the output:" + " {}".format(", ".join(DEV_PKGS)) + ), + ) self.cmd_opts.add_option( - '--exclude-editable', - dest='exclude_editable', - action='store_true', - help='Exclude editable package from output.') + "--exclude-editable", + dest="exclude_editable", + action="store_true", + help="Exclude editable package from output.", + ) self.cmd_opts.add_option(cmdoptions.list_exclude()) self.parser.insert_option_group(0, self.cmd_opts) @@ -80,5 +93,5 @@ def run(self, options: Values, args: List[str]) -> int: skip=skip, exclude_editable=options.exclude_editable, ): - sys.stdout.write(line + '\n') + sys.stdout.write(line + "\n") return SUCCESS diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 3e4c32f35ac..042dac813e7 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -20,18 +20,21 @@ class HashCommand(Command): installs. """ - usage = '%prog [options] ...' + usage = "%prog [options] ..." ignore_require_venv = True def add_options(self) -> None: self.cmd_opts.add_option( - '-a', '--algorithm', - dest='algorithm', + "-a", + "--algorithm", + dest="algorithm", choices=STRONG_HASHES, - action='store', + action="store", default=FAVORITE_HASH, - help='The hash algorithm to use: one of {}'.format( - ', '.join(STRONG_HASHES))) + help="The hash algorithm to use: one of {}".format( + ", ".join(STRONG_HASHES) + ), + ) self.parser.insert_option_group(0, self.cmd_opts) def run(self, options: Values, args: List[str]) -> int: @@ -41,14 +44,15 @@ def run(self, options: Values, args: List[str]) -> int: algorithm = options.algorithm for path in args: - write_output('%s:\n--hash=%s:%s', - path, algorithm, _hash_of_file(path, algorithm)) + write_output( + "%s:\n--hash=%s:%s", path, algorithm, _hash_of_file(path, algorithm) + ) return SUCCESS def _hash_of_file(path: str, algorithm: str) -> str: """Return the hash digest of a file.""" - with open(path, 'rb') as archive: + with open(path, "rb") as archive: hash = hashlib.new(algorithm) for chunk in read_chunks(archive): hash.update(chunk) diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 811ce89d566..62066318b74 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -33,7 +33,7 @@ def run(self, options: Values, args: List[str]) -> int: if guess: msg.append(f'maybe you meant "{guess}"') - raise CommandError(' - '.join(msg)) + raise CommandError(" - ".join(msg)) command = create_command(cmd_name) command.parser.print_help() diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index c505464f635..508171e99cd 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -101,7 +101,7 @@ def _build_package_finder( def get_available_package_versions(self, options: Values, args: List[Any]) -> None: if len(args) != 1: - raise CommandError('You need to specify exactly one argument') + raise CommandError("You need to specify exactly one argument") target_python = cmdoptions.make_target_python(options) query = args[0] @@ -115,25 +115,24 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No ) versions: Iterable[Union[LegacyVersion, Version]] = ( - candidate.version - for candidate in finder.find_all_candidates(query) + candidate.version for candidate in finder.find_all_candidates(query) ) if not options.pre: # Remove prereleases - versions = (version for version in versions - if not version.is_prerelease) + versions = ( + version for version in versions if not version.is_prerelease + ) versions = set(versions) if not versions: raise DistributionNotFound( - 'No matching distribution found for {}'.format(query)) + "No matching distribution found for {}".format(query) + ) - formatted_versions = [str(ver) for ver in sorted( - versions, reverse=True)] + formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)] latest = formatted_versions[0] - write_output('{} ({})'.format(query, latest)) - write_output('Available versions: {}'.format( - ', '.join(formatted_versions))) + write_output("{} ({})".format(query, latest)) + write_output("Available versions: {}".format(", ".join(formatted_versions))) print_dist_installation_info(query, latest) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 02da0777a39..a427c6c5929 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -86,87 +86,105 @@ def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.editable()) self.cmd_opts.add_option( - '-t', '--target', - dest='target_dir', - metavar='dir', + "-t", + "--target", + dest="target_dir", + metavar="dir", default=None, - help='Install packages into . ' - 'By default this will not replace existing files/folders in ' - '. Use --upgrade to replace existing packages in ' - 'with new versions.' + help=( + "Install packages into . " + "By default this will not replace existing files/folders in " + ". Use --upgrade to replace existing packages in " + "with new versions." + ), ) cmdoptions.add_target_python_options(self.cmd_opts) self.cmd_opts.add_option( - '--user', - dest='use_user_site', - action='store_true', - help="Install to the Python user install directory for your " - "platform. Typically ~/.local/, or %APPDATA%\\Python on " - "Windows. (See the Python documentation for site.USER_BASE " - "for full details.)") + "--user", + dest="use_user_site", + action="store_true", + help=( + "Install to the Python user install directory for your " + "platform. Typically ~/.local/, or %APPDATA%\\Python on " + "Windows. (See the Python documentation for site.USER_BASE " + "for full details.)" + ), + ) self.cmd_opts.add_option( - '--no-user', - dest='use_user_site', - action='store_false', - help=SUPPRESS_HELP) + "--no-user", + dest="use_user_site", + action="store_false", + help=SUPPRESS_HELP, + ) self.cmd_opts.add_option( - '--root', - dest='root_path', - metavar='dir', + "--root", + dest="root_path", + metavar="dir", default=None, - help="Install everything relative to this alternate root " - "directory.") + help="Install everything relative to this alternate root directory.", + ) self.cmd_opts.add_option( - '--prefix', - dest='prefix_path', - metavar='dir', + "--prefix", + dest="prefix_path", + metavar="dir", default=None, - help="Installation prefix where lib, bin and other top-level " - "folders are placed") + help=( + "Installation prefix where lib, bin and other top-level " + "folders are placed" + ), + ) self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.src()) self.cmd_opts.add_option( - '-U', '--upgrade', - dest='upgrade', - action='store_true', - help='Upgrade all specified packages to the newest available ' - 'version. The handling of dependencies depends on the ' - 'upgrade-strategy used.' + "-U", + "--upgrade", + dest="upgrade", + action="store_true", + help=( + "Upgrade all specified packages to the newest available " + "version. The handling of dependencies depends on the " + "upgrade-strategy used." + ), ) self.cmd_opts.add_option( - '--upgrade-strategy', - dest='upgrade_strategy', - default='only-if-needed', - choices=['only-if-needed', 'eager'], - help='Determines how dependency upgrading should be handled ' - '[default: %default]. ' - '"eager" - dependencies are upgraded regardless of ' - 'whether the currently installed version satisfies the ' - 'requirements of the upgraded package(s). ' - '"only-if-needed" - are upgraded only when they do not ' - 'satisfy the requirements of the upgraded package(s).' + "--upgrade-strategy", + dest="upgrade_strategy", + default="only-if-needed", + choices=["only-if-needed", "eager"], + help=( + "Determines how dependency upgrading should be handled " + "[default: %default]. " + '"eager" - dependencies are upgraded regardless of ' + "whether the currently installed version satisfies the " + "requirements of the upgraded package(s). " + '"only-if-needed" - are upgraded only when they do not ' + "satisfy the requirements of the upgraded package(s)." + ), ) self.cmd_opts.add_option( - '--force-reinstall', - dest='force_reinstall', - action='store_true', - help='Reinstall all packages even if they are already ' - 'up-to-date.') + "--force-reinstall", + dest="force_reinstall", + action="store_true", + help="Reinstall all packages even if they are already up-to-date.", + ) self.cmd_opts.add_option( - '-I', '--ignore-installed', - dest='ignore_installed', - action='store_true', - help='Ignore the installed packages, overwriting them. ' - 'This can break your system if the existing package ' - 'is of a different version or was installed ' - 'with a different package manager!' + "-I", + "--ignore-installed", + dest="ignore_installed", + action="store_true", + help=( + "Ignore the installed packages, overwriting them. " + "This can break your system if the existing package " + "is of a different version or was installed " + "with a different package manager!" + ), ) self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) @@ -249,11 +267,14 @@ def run(self, options: Values, args: List[str]) -> int: if options.target_dir: options.ignore_installed = True options.target_dir = os.path.abspath(options.target_dir) - if (os.path.exists(options.target_dir) and not - os.path.isdir(options.target_dir)): + if ( + # fmt: off + os.path.exists(options.target_dir) and + not os.path.isdir(options.target_dir) + # fmt: on + ): raise CommandError( - "Target path exists but is not a directory, will not " - "continue." + "Target path exists but is not a directory, will not continue." ) # Create a target directory for using with the target option @@ -285,9 +306,7 @@ def run(self, options: Values, args: List[str]) -> int: try: reqs = self.get_requirements(args, options, finder, session) - reject_location_related_install_options( - reqs, options.install_options - ) + reject_location_related_install_options(reqs, options.install_options) preparer = self.make_requirement_preparer( temp_build_dir=directory, @@ -324,19 +343,14 @@ def run(self, options: Values, args: List[str]) -> int: # If we're not replacing an already installed pip, # we're not modifying it. modifying_pip = pip_req.satisfied_by is None - protect_pip_from_modification_on_windows( - modifying_pip=modifying_pip - ) + protect_pip_from_modification_on_windows(modifying_pip=modifying_pip) - check_binary_allowed = get_check_binary_allowed( - finder.format_control - ) + check_binary_allowed = get_check_binary_allowed(finder.format_control) reqs_to_build = [ - r for r in requirement_set.requirements.values() - if should_build_for_install_command( - r, check_binary_allowed - ) + r + for r in requirement_set.requirements.values() + if should_build_for_install_command(r, check_binary_allowed) ] _, build_failures = build( @@ -350,8 +364,7 @@ def run(self, options: Values, args: List[str]) -> int: # If we're using PEP 517, we cannot do a direct install # so we fail here. pep517_build_failure_names: List[str] = [ - r.name # type: ignore - for r in build_failures if r.use_pep517 + r.name for r in build_failures if r.use_pep517 # type: ignore ] if pep517_build_failure_names: raise InstallationError( @@ -368,15 +381,12 @@ def run(self, options: Values, args: List[str]) -> int: if not r.use_pep517: r.legacy_install_reason = 8368 - to_install = resolver.get_installation_order( - requirement_set - ) + to_install = resolver.get_installation_order(requirement_set) # Check for conflicts in the package set we're installing. conflicts: Optional[ConflictDetails] = None should_warn_about_conflicts = ( - not options.ignore_dependencies and - options.warn_about_conflicts + not options.ignore_dependencies and options.warn_about_conflicts ) if should_warn_about_conflicts: conflicts = self._determine_conflicts(to_install) @@ -408,7 +418,7 @@ def run(self, options: Values, args: List[str]) -> int: ) env = get_environment(lib_locations) - installed.sort(key=operator.attrgetter('name')) + installed.sort(key=operator.attrgetter("name")) items = [] for result in installed: item = result.name @@ -426,16 +436,19 @@ def run(self, options: Values, args: List[str]) -> int: resolver_variant=self.determine_resolver_variant(options), ) - installed_desc = ' '.join(items) + installed_desc = " ".join(items) if installed_desc: write_output( - 'Successfully installed %s', installed_desc, + "Successfully installed %s", + installed_desc, ) except OSError as error: - show_traceback = (self.verbosity >= 1) + show_traceback = self.verbosity >= 1 message = create_os_error_message( - error, show_traceback, options.use_user_site, + error, + show_traceback, + options.use_user_site, ) logger.error(message, exc_info=show_traceback) # noqa @@ -461,7 +474,7 @@ def _handle_target_dir( # Checking both purelib and platlib directories for installed # packages to be moved to target directory - scheme = get_scheme('', home=target_temp_dir.path) + scheme = get_scheme("", home=target_temp_dir.path) purelib_dir = scheme.purelib platlib_dir = scheme.platlib data_dir = scheme.data @@ -483,18 +496,18 @@ def _handle_target_dir( if os.path.exists(target_item_dir): if not upgrade: logger.warning( - 'Target directory %s already exists. Specify ' - '--upgrade to force replacement.', - target_item_dir + "Target directory %s already exists. Specify " + "--upgrade to force replacement.", + target_item_dir, ) continue if os.path.islink(target_item_dir): logger.warning( - 'Target directory %s already exists and is ' - 'a link. pip will not automatically replace ' - 'links, please remove if replacement is ' - 'desired.', - target_item_dir + "Target directory %s already exists and is " + "a link. pip will not automatically replace " + "links, please remove if replacement is " + "desired.", + target_item_dir, ) continue if os.path.isdir(target_item_dir): @@ -502,10 +515,7 @@ def _handle_target_dir( else: os.remove(target_item_dir) - shutil.move( - os.path.join(lib_dir, item), - target_item_dir - ) + shutil.move(os.path.join(lib_dir, item), target_item_dir) def _determine_conflicts( self, to_install: List[InstallRequirement] @@ -567,7 +577,7 @@ def _warn_about_conflicts( requirement=req, dep_name=dep_name, dep_version=dep_version, - you=("you" if resolver_variant == "2020-resolver" else "you'll") + you=("you" if resolver_variant == "2020-resolver" else "you'll"), ) parts.append(message) @@ -575,14 +585,14 @@ def _warn_about_conflicts( def get_lib_location_guesses( - user: bool = False, - home: Optional[str] = None, - root: Optional[str] = None, - isolated: bool = False, - prefix: Optional[str] = None + user: bool = False, + home: Optional[str] = None, + root: Optional[str] = None, + isolated: bool = False, + prefix: Optional[str] = None, ) -> List[str]: scheme = get_scheme( - '', + "", user=user, home=home, root=root, @@ -594,8 +604,8 @@ def get_lib_location_guesses( def site_packages_writable(root: Optional[str], isolated: bool) -> bool: return all( - test_writable_dir(d) for d in set( - get_lib_location_guesses(root=root, isolated=isolated)) + test_writable_dir(d) + for d in set(get_lib_location_guesses(root=root, isolated=isolated)) ) @@ -653,8 +663,10 @@ def decide_user_install( logger.debug("Non-user install because site-packages writeable") return False - logger.info("Defaulting to user installation because normal site-packages " - "is not writeable") + logger.info( + "Defaulting to user installation because normal site-packages " + "is not writeable" + ) return True @@ -664,6 +676,7 @@ def reject_location_related_install_options( """If any location-changing --install-option arguments were passed for requirements or on the command-line, then show a deprecation warning. """ + def format_options(option_names: Iterable[str]) -> List[str]: return ["--{}".format(name.replace("_", "-")) for name in option_names] @@ -683,9 +696,7 @@ def format_options(option_names: Iterable[str]) -> List[str]: location_options = parse_distutils_args(options) if location_options: offenders.append( - "{!r} from command line".format( - format_options(location_options.keys()) - ) + "{!r} from command line".format(format_options(location_options.keys())) ) if not offenders: @@ -694,9 +705,7 @@ def format_options(option_names: Iterable[str]) -> List[str]: raise CommandError( "Location-changing options found in --install-option: {}." " This is unsupported, use pip-level options like --user," - " --prefix, --root, and --target instead.".format( - "; ".join(offenders) - ) + " --prefix, --root, and --target instead.".format("; ".join(offenders)) ) @@ -727,18 +736,25 @@ def create_os_error_message( permissions_part = "Check the permissions" if not running_under_virtualenv() and not using_user_site: - parts.extend([ - user_option_part, " or ", - permissions_part.lower(), - ]) + parts.extend( + [ + user_option_part, + " or ", + permissions_part.lower(), + ] + ) else: parts.append(permissions_part) parts.append(".\n") # Suggest the user to enable Long Paths if path length is # more than 260 - if (WINDOWS and error.errno == errno.ENOENT and error.filename and - len(error.filename) > 260): + if ( + WINDOWS + and error.errno == errno.ENOENT + and error.filename + and len(error.filename) > 260 + ): parts.append( "HINT: This error might have occurred since " "this system does not have Windows Long Path " diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 828889f49e6..abe6ef2fc80 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -26,6 +26,7 @@ class _DistWithLatestInfo(BaseDistribution): These will be populated during ``get_outdated()``. This is dirty but makes the rest of the code much cleaner. """ + latest_version: DistributionVersion latest_filetype: str @@ -48,77 +49,85 @@ class ListCommand(IndexGroupCommand): def add_options(self) -> None: self.cmd_opts.add_option( - '-o', '--outdated', - action='store_true', + "-o", + "--outdated", + action="store_true", default=False, - help='List outdated packages') + help="List outdated packages", + ) self.cmd_opts.add_option( - '-u', '--uptodate', - action='store_true', + "-u", + "--uptodate", + action="store_true", default=False, - help='List uptodate packages') + help="List uptodate packages", + ) self.cmd_opts.add_option( - '-e', '--editable', - action='store_true', + "-e", + "--editable", + action="store_true", default=False, - help='List editable projects.') + help="List editable projects.", + ) self.cmd_opts.add_option( - '-l', '--local', - action='store_true', + "-l", + "--local", + action="store_true", default=False, - help=('If in a virtualenv that has global access, do not list ' - 'globally-installed packages.'), + help=( + "If in a virtualenv that has global access, do not list " + "globally-installed packages." + ), ) self.cmd_opts.add_option( - '--user', - dest='user', - action='store_true', + "--user", + dest="user", + action="store_true", default=False, - help='Only output packages installed in user-site.') + help="Only output packages installed in user-site.", + ) self.cmd_opts.add_option(cmdoptions.list_path()) self.cmd_opts.add_option( - '--pre', - action='store_true', + "--pre", + action="store_true", default=False, - help=("Include pre-release and development versions. By default, " - "pip only finds stable versions."), + help=( + "Include pre-release and development versions. By default, " + "pip only finds stable versions." + ), ) self.cmd_opts.add_option( - '--format', - action='store', - dest='list_format', + "--format", + action="store", + dest="list_format", default="columns", - choices=('columns', 'freeze', 'json'), - help="Select the output format among: columns (default), freeze, " - "or json", + choices=("columns", "freeze", "json"), + help="Select the output format among: columns (default), freeze, or json", ) self.cmd_opts.add_option( - '--not-required', - action='store_true', - dest='not_required', - help="List packages that are not dependencies of " - "installed packages.", + "--not-required", + action="store_true", + dest="not_required", + help="List packages that are not dependencies of installed packages.", ) self.cmd_opts.add_option( - '--exclude-editable', - action='store_false', - dest='include_editable', - help='Exclude editable package from output.', + "--exclude-editable", + action="store_false", + dest="include_editable", + help="Exclude editable package from output.", ) self.cmd_opts.add_option( - '--include-editable', - action='store_true', - dest='include_editable', - help='Include editable package from output.', + "--include-editable", + action="store_true", + dest="include_editable", + help="Include editable package from output.", default=True, ) self.cmd_opts.add_option(cmdoptions.list_exclude()) - index_opts = cmdoptions.make_option_group( - cmdoptions.index_group, self.parser - ) + index_opts = cmdoptions.make_option_group(cmdoptions.index_group, self.parser) self.parser.insert_option_group(0, index_opts) self.parser.insert_option_group(0, self.cmd_opts) @@ -144,8 +153,7 @@ def _build_package_finder( def run(self, options: Values, args: List[str]) -> int: if options.outdated and options.uptodate: - raise CommandError( - "Options --outdated and --uptodate cannot be combined.") + raise CommandError("Options --outdated and --uptodate cannot be combined.") cmdoptions.check_list_path_option(options) @@ -183,7 +191,8 @@ def get_outdated( self, packages: "_ProcessedDists", options: Values ) -> "_ProcessedDists": return [ - dist for dist in self.iter_packages_latest_infos(packages, options) + dist + for dist in self.iter_packages_latest_infos(packages, options) if dist.latest_version > dist.version ] @@ -191,7 +200,8 @@ def get_uptodate( self, packages: "_ProcessedDists", options: Values ) -> "_ProcessedDists": return [ - dist for dist in self.iter_packages_latest_infos(packages, options) + dist + for dist in self.iter_packages_latest_infos(packages, options) if dist.latest_version == dist.version ] @@ -216,13 +226,16 @@ def iter_packages_latest_infos( finder = self._build_package_finder(options, session) def latest_info( - dist: "_DistWithLatestInfo" + dist: "_DistWithLatestInfo", ) -> Optional["_DistWithLatestInfo"]: all_candidates = finder.find_all_candidates(dist.canonical_name) if not options.pre: # Remove prereleases - all_candidates = [candidate for candidate in all_candidates - if not candidate.version.is_prerelease] + all_candidates = [ + candidate + for candidate in all_candidates + if not candidate.version.is_prerelease + ] evaluator = finder.make_candidate_evaluator( project_name=dist.canonical_name, @@ -233,9 +246,9 @@ def latest_info( remote_version = best_candidate.version if best_candidate.link.is_wheel: - typ = 'wheel' + typ = "wheel" else: - typ = 'sdist' + typ = "sdist" dist.latest_version = remote_version dist.latest_filetype = typ return dist @@ -251,17 +264,18 @@ def output_package_listing( packages, key=lambda dist: dist.canonical_name, ) - if options.list_format == 'columns' and packages: + if options.list_format == "columns" and packages: data, header = format_for_columns(packages, options) self.output_package_listing_columns(data, header) - elif options.list_format == 'freeze': + elif options.list_format == "freeze": for dist in packages: if options.verbose >= 1: - write_output("%s==%s (%s)", dist.raw_name, - dist.version, dist.location) + write_output( + "%s==%s (%s)", dist.raw_name, dist.version, dist.location + ) else: write_output("%s==%s", dist.raw_name, dist.version) - elif options.list_format == 'json': + elif options.list_format == "json": write_output(format_for_json(packages, options)) def output_package_listing_columns( @@ -275,7 +289,7 @@ def output_package_listing_columns( # Create and add a separator. if len(data) > 0: - pkg_strings.insert(1, " ".join(map(lambda x: '-' * x, sizes))) + pkg_strings.insert(1, " ".join(map(lambda x: "-" * x, sizes))) for val in pkg_strings: write_output(val) @@ -324,14 +338,14 @@ def format_for_json(packages: "_ProcessedDists", options: Values) -> str: data = [] for dist in packages: info = { - 'name': dist.raw_name, - 'version': str(dist.version), + "name": dist.raw_name, + "version": str(dist.version), } if options.verbose >= 1: - info['location'] = dist.location or "" - info['installer'] = dist.installer + info["location"] = dist.location or "" + info["installer"] = dist.installer if options.outdated: - info['latest_version'] = str(dist.latest_version) - info['latest_filetype'] = dist.latest_filetype + info["latest_version"] = str(dist.latest_version) + info["latest_filetype"] = dist.latest_filetype data.append(info) return json.dumps(data) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 7a20ba1e46f..03ed925b246 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -27,6 +27,7 @@ class TransformedHit(TypedDict): summary: str versions: List[str] + logger = logging.getLogger(__name__) @@ -39,17 +40,19 @@ class SearchCommand(Command, SessionCommandMixin): def add_options(self) -> None: self.cmd_opts.add_option( - '-i', '--index', - dest='index', - metavar='URL', + "-i", + "--index", + dest="index", + metavar="URL", default=PyPI.pypi_url, - help='Base URL of Python Package Index (default %default)') + help="Base URL of Python Package Index (default %default)", + ) self.parser.insert_option_group(0, self.cmd_opts) def run(self, options: Values, args: List[str]) -> int: if not args: - raise CommandError('Missing required argument (search query).') + raise CommandError("Missing required argument (search query).") query = args pypi_hits = self.search(query, options) hits = transform_hits(pypi_hits) @@ -71,7 +74,7 @@ def search(self, query: List[str], options: Values) -> List[Dict[str, str]]: transport = PipXmlrpcTransport(index_url, session) pypi = xmlrpc.client.ServerProxy(index_url, transport) try: - hits = pypi.search({'name': query, 'summary': query}, 'or') + hits = pypi.search({"name": query, "summary": query}, "or") except xmlrpc.client.Fault as fault: message = "XMLRPC request failed [code: {code}]\n{string}".format( code=fault.faultCode, @@ -90,22 +93,22 @@ def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]: """ packages: Dict[str, "TransformedHit"] = OrderedDict() for hit in hits: - name = hit['name'] - summary = hit['summary'] - version = hit['version'] + name = hit["name"] + summary = hit["summary"] + version = hit["version"] if name not in packages.keys(): packages[name] = { - 'name': name, - 'summary': summary, - 'versions': [version], + "name": name, + "summary": summary, + "versions": [version], } else: - packages[name]['versions'].append(version) + packages[name]["versions"].append(version) # if this is the highest version, replace summary and score - if version == highest_version(packages[name]['versions']): - packages[name]['summary'] = summary + if version == highest_version(packages[name]["versions"]): + packages[name]["summary"] = summary return list(packages.values()) @@ -116,14 +119,17 @@ def print_dist_installation_info(name: str, latest: str) -> None: if dist is not None: with indent_log(): if dist.version == latest: - write_output('INSTALLED: %s (latest)', dist.version) + write_output("INSTALLED: %s (latest)", dist.version) else: - write_output('INSTALLED: %s', dist.version) + write_output("INSTALLED: %s", dist.version) if parse_version(latest).pre: - write_output('LATEST: %s (pre-release; install' - ' with "pip install --pre")', latest) + write_output( + "LATEST: %s (pre-release; install" + " with `pip install --pre`)", + latest, + ) else: - write_output('LATEST: %s', latest) + write_output("LATEST: %s", latest) def print_results( @@ -134,25 +140,29 @@ def print_results( if not hits: return if name_column_width is None: - name_column_width = max([ - len(hit['name']) + len(highest_version(hit.get('versions', ['-']))) - for hit in hits - ]) + 4 + name_column_width = ( + max( + [ + len(hit["name"]) + len(highest_version(hit.get("versions", ["-"]))) + for hit in hits + ] + ) + + 4 + ) for hit in hits: - name = hit['name'] - summary = hit['summary'] or '' - latest = highest_version(hit.get('versions', ['-'])) + name = hit["name"] + summary = hit["summary"] or "" + latest = highest_version(hit.get("versions", ["-"])) if terminal_width is not None: target_width = terminal_width - name_column_width - 5 if target_width > 10: # wrap and indent summary to fit terminal summary_lines = textwrap.wrap(summary, target_width) - summary = ('\n' + ' ' * (name_column_width + 3)).join( - summary_lines) + summary = ("\n" + " " * (name_column_width + 3)).join(summary_lines) - name_latest = f'{name} ({latest})' - line = f'{name_latest:{name_column_width}} - {summary}' + name_latest = f"{name} ({latest})" + line = f"{name_latest:{name_column_width}} - {summary}" try: write_output(line) print_dist_installation_info(name, latest) diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 5b2de39e5fb..0c763a30849 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -27,23 +27,26 @@ class ShowCommand(Command): def add_options(self) -> None: self.cmd_opts.add_option( - '-f', '--files', - dest='files', - action='store_true', + "-f", + "--files", + dest="files", + action="store_true", default=False, - help='Show the full list of installed files for each package.') + help="Show the full list of installed files for each package.", + ) self.parser.insert_option_group(0, self.cmd_opts) def run(self, options: Values, args: List[str]) -> int: if not args: - logger.warning('ERROR: Please provide a package name or names.') + logger.warning("ERROR: Please provide a package name or names.") return ERROR query = args results = search_packages_info(query) if not print_results( - results, list_files=options.files, verbose=options.verbose): + results, list_files=options.files, verbose=options.verbose + ): return ERROR return SUCCESS @@ -102,29 +105,25 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]: """ env = get_default_environment() - installed = { - dist.canonical_name: dist - for dist in env.iter_distributions() - } + installed = {dist.canonical_name: dist for dist in env.iter_distributions()} query_names = [canonicalize_name(name) for name in query] missing = sorted( [name for name, pkg in zip(query, query_names) if pkg not in installed] ) if missing: - logger.warning('Package(s) not found: %s', ', '.join(missing)) + logger.warning("Package(s) not found: %s", ", ".join(missing)) def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]: return [ dist.metadata["Name"] or "UNKNOWN" for dist in installed.values() - if current_dist.canonical_name in { - canonicalize_name(d.name) for d in dist.iter_dependencies() - } + if current_dist.canonical_name + in {canonicalize_name(d.name) for d in dist.iter_dependencies()} ] def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]: try: - text = dist.read_text('RECORD') + text = dist.read_text("RECORD") except FileNotFoundError: return None # This extra Path-str cast normalizes entries. @@ -132,7 +131,7 @@ def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]: def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]: try: - text = dist.read_text('installed-files.txt') + text = dist.read_text("installed-files.txt") except FileNotFoundError: return None paths = (p for p in text.splitlines(keepends=False) if p) @@ -147,8 +146,7 @@ def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]: if not info_rel.parts: # info *is* root. return paths return ( - _covert_legacy_entry(pathlib.Path(p).parts, info_rel.parts) - for p in paths + _covert_legacy_entry(pathlib.Path(p).parts, info_rel.parts) for p in paths ) for query_name in query_names: @@ -158,7 +156,7 @@ def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]: continue try: - entry_points_text = dist.read_text('entry_points.txt') + entry_points_text = dist.read_text("entry_points.txt") entry_points = entry_points_text.splitlines(keepends=False) except FileNotFoundError: entry_points = [] @@ -212,8 +210,8 @@ def print_results( write_output("Author-email: %s", dist.author_email) write_output("License: %s", dist.license) write_output("Location: %s", dist.location) - write_output("Requires: %s", ', '.join(dist.requires)) - write_output("Required-by: %s", ', '.join(dist.required_by)) + write_output("Requires: %s", ", ".join(dist.requires)) + write_output("Required-by: %s", ", ".join(dist.required_by)) if verbose: write_output("Metadata-Version: %s", dist.metadata_version) diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index c590627eaa0..bb9e8e6a380 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -35,19 +35,24 @@ class UninstallCommand(Command, SessionCommandMixin): def add_options(self) -> None: self.cmd_opts.add_option( - '-r', '--requirement', - dest='requirements', - action='append', + "-r", + "--requirement", + dest="requirements", + action="append", default=[], - metavar='file', - help='Uninstall all the packages listed in the given requirements ' - 'file. This option can be used multiple times.', + metavar="file", + help=( + "Uninstall all the packages listed in the given requirements " + "file. This option can be used multiple times." + ), ) self.cmd_opts.add_option( - '-y', '--yes', - dest='yes', - action='store_true', - help="Don't ask for confirmation of uninstall deletions.") + "-y", + "--yes", + dest="yes", + action="store_true", + help="Don't ask for confirmation of uninstall deletions.", + ) self.parser.insert_option_group(0, self.cmd_opts) @@ -57,7 +62,8 @@ def run(self, options: Values, args: List[str]) -> int: reqs_to_uninstall = {} for name in args: req = install_req_from_line( - name, isolated=options.isolated_mode, + name, + isolated=options.isolated_mode, ) if req.name: reqs_to_uninstall[canonicalize_name(req.name)] = req @@ -70,18 +76,16 @@ def run(self, options: Values, args: List[str]) -> int: ) for filename in options.requirements: for parsed_req in parse_requirements( - filename, - options=options, - session=session): + filename, options=options, session=session + ): req = install_req_from_parsed_requirement( - parsed_req, - isolated=options.isolated_mode + parsed_req, isolated=options.isolated_mode ) if req.name: reqs_to_uninstall[canonicalize_name(req.name)] = req if not reqs_to_uninstall: raise InstallationError( - f'You must give at least one requirement to {self.name} (see ' + f"You must give at least one requirement to {self.name} (see " f'"pip help {self.name}")' ) @@ -91,7 +95,8 @@ def run(self, options: Values, args: List[str]) -> int: for req in reqs_to_uninstall.values(): uninstall_pathset = req.uninstall( - auto_confirm=options.yes, verbose=self.verbosity > 0, + auto_confirm=options.yes, + verbose=self.verbosity > 0, ) if uninstall_pathset: uninstall_pathset.commit() diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index c8bf4e25d94..1fb5007a705 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -43,12 +43,15 @@ class WheelCommand(RequirementCommand): def add_options(self) -> None: self.cmd_opts.add_option( - '-w', '--wheel-dir', - dest='wheel_dir', - metavar='dir', + "-w", + "--wheel-dir", + dest="wheel_dir", + metavar="dir", default=os.curdir, - help=("Build wheels into , where the default is the " - "current working directory."), + help=( + "Build wheels into , where the default is the " + "current working directory." + ), ) self.cmd_opts.add_option(cmdoptions.no_binary()) self.cmd_opts.add_option(cmdoptions.only_binary()) @@ -66,9 +69,9 @@ def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.progress_bar()) self.cmd_opts.add_option( - '--no-verify', - dest='no_verify', - action='store_true', + "--no-verify", + dest="no_verify", + action="store_true", default=False, help="Don't verify if built wheel is valid.", ) @@ -77,11 +80,13 @@ def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.global_options()) self.cmd_opts.add_option( - '--pre', - action='store_true', + "--pre", + action="store_true", default=False, - help=("Include pre-release and development versions. By default, " - "pip only finds stable versions."), + help=( + "Include pre-release and development versions. By default, " + "pip only finds stable versions." + ), ) self.cmd_opts.add_option(cmdoptions.require_hashes()) @@ -137,9 +142,7 @@ def run(self, options: Values, args: List[str]) -> int: self.trace_basic_info(finder) - requirement_set = resolver.resolve( - reqs, check_supported_wheels=True - ) + requirement_set = resolver.resolve(reqs, check_supported_wheels=True) reqs_to_build: List[InstallRequirement] = [] for req in requirement_set.requirements.values(): @@ -165,12 +168,11 @@ def run(self, options: Values, args: List[str]) -> int: except OSError as e: logger.warning( "Building wheel for %s failed: %s", - req.name, e, + req.name, + e, ) build_failures.append(req) if len(build_failures) != 0: - raise CommandError( - "Failed to build one or more wheels" - ) + raise CommandError("Failed to build one or more wheels") return SUCCESS diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 8aacf812014..ee0b1da2c5a 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -189,13 +189,9 @@ def __str__(self): return '\n'.join(lines) return '' - def __nonzero__(self): - # type: () -> bool - return bool(self.errors) - def __bool__(self): # type: () -> bool - return self.__nonzero__() + return bool(self.errors) class HashError(InstallationError): diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 247e63fc86c..8bb5a6843f5 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -216,14 +216,16 @@ def unpack_url( # be removed. if link.is_existing_dir(): deprecated( - "A future pip version will change local packages to be built " - "in-place without first copying to a temporary directory. " - "We recommend you use --use-feature=in-tree-build to test " - "your packages with this new behavior before it becomes the " - "default.\n", + reason=( + "pip copied the source tree into a temporary directory " + "before building it. This is changing so that packages " + "are built in-place " + 'within the original source tree ("in-tree build").' + ), replacement=None, gone_in="21.3", - issue=7555 + feature_flag="in-tree-build", + issue=7555, ) if os.path.isdir(location): rmtree(location) diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index aaea748dc5a..70dea27a6a8 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -9,8 +9,10 @@ from .req_set import RequirementSet __all__ = [ - "RequirementSet", "InstallRequirement", - "parse_requirements", "install_given_reqs", + "RequirementSet", + "InstallRequirement", + "parse_requirements", + "install_given_reqs", ] logger = logging.getLogger(__name__) @@ -52,8 +54,8 @@ def install_given_reqs( if to_install: logger.info( - 'Installing collected packages: %s', - ', '.join(to_install.keys()), + "Installing collected packages: %s", + ", ".join(to_install.keys()), ) installed = [] @@ -61,11 +63,9 @@ def install_given_reqs( with indent_log(): for req_name, requirement in to_install.items(): if requirement.should_reinstall: - logger.info('Attempting uninstall: %s', req_name) + logger.info("Attempting uninstall: %s", req_name) with indent_log(): - uninstalled_pathset = requirement.uninstall( - auto_confirm=True - ) + uninstalled_pathset = requirement.uninstall(auto_confirm=True) else: uninstalled_pathset = None diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index d0f5b424890..b9b18139aaf 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -31,8 +31,9 @@ from pip._internal.vcs import is_url, vcs __all__ = [ - "install_req_from_editable", "install_req_from_line", - "parse_editable" + "install_req_from_editable", + "install_req_from_line", + "parse_editable", ] logger = logging.getLogger(__name__) @@ -40,7 +41,7 @@ def _strip_extras(path: str) -> Tuple[str, Optional[str]]: - m = re.match(r'^(.+)(\[[^\]]+\])$', path) + m = re.match(r"^(.+)(\[[^\]]+\])$", path) extras = None if m: path_no_extras = m.group(1) @@ -74,26 +75,25 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]: url_no_extras, extras = _strip_extras(url) if os.path.isdir(url_no_extras): - setup_py = os.path.join(url_no_extras, 'setup.py') - setup_cfg = os.path.join(url_no_extras, 'setup.cfg') + setup_py = os.path.join(url_no_extras, "setup.py") + setup_cfg = os.path.join(url_no_extras, "setup.cfg") if not os.path.exists(setup_py) and not os.path.exists(setup_cfg): msg = ( 'File "setup.py" or "setup.cfg" not found. Directory cannot be ' - 'installed in editable mode: {}' - .format(os.path.abspath(url_no_extras)) + "installed in editable mode: {}".format(os.path.abspath(url_no_extras)) ) pyproject_path = make_pyproject_path(url_no_extras) if os.path.isfile(pyproject_path): msg += ( '\n(A "pyproject.toml" file was found, but editable ' - 'mode currently requires a setuptools-based build.)' + "mode currently requires a setuptools-based build.)" ) raise InstallationError(msg) # Treating it as code that has already been checked out url_no_extras = path_to_url(url_no_extras) - if url_no_extras.lower().startswith('file:'): + if url_no_extras.lower().startswith("file:"): package_name = Link(url_no_extras).egg_fragment if extras: return ( @@ -105,8 +105,8 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]: return package_name, url_no_extras, set() for version_control in vcs: - if url.lower().startswith(f'{version_control}:'): - url = f'{version_control}+{url}' + if url.lower().startswith(f"{version_control}:"): + url = f"{version_control}+{url}" break link = Link(url) @@ -114,9 +114,9 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]: if not link.is_vcs: backends = ", ".join(vcs.all_schemes) raise InstallationError( - f'{editable_req} is not a valid editable requirement. ' - f'It should either be a path to a local project or a VCS URL ' - f'(beginning with {backends}).' + f"{editable_req} is not a valid editable requirement. " + f"It should either be a path to a local project or a VCS URL " + f"(beginning with {backends})." ) package_name = link.egg_fragment @@ -150,9 +150,7 @@ def deduce_helpful_msg(req: str) -> str: " the packages specified within it." ).format(req) except RequirementParseError: - logger.debug( - "Cannot parse '%s' as requirements file", req, exc_info=True - ) + logger.debug("Cannot parse '%s' as requirements file", req, exc_info=True) else: msg += f" File '{req}' does not exist." return msg @@ -160,11 +158,11 @@ def deduce_helpful_msg(req: str) -> str: class RequirementParts: def __init__( - self, - requirement: Optional[Requirement], - link: Optional[Link], - markers: Optional[Marker], - extras: Set[str], + self, + requirement: Optional[Requirement], + link: Optional[Link], + markers: Optional[Marker], + extras: Set[str], ): self.requirement = requirement self.link = link @@ -258,24 +256,23 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]: return None if os.path.isfile(path): return path_to_url(path) - urlreq_parts = name.split('@', 1) + urlreq_parts = name.split("@", 1) if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]): # If the path contains '@' and the part before it does not look # like a path, try to treat it as a PEP 440 URL req instead. return None logger.warning( - 'Requirement %r looks like a filename, but the ' - 'file does not exist', - name + "Requirement %r looks like a filename, but the file does not exist", + name, ) return path_to_url(path) def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts: if is_url(name): - marker_sep = '; ' + marker_sep = "; " else: - marker_sep = ';' + marker_sep = ";" if marker_sep in name: name, markers_as_string = name.split(marker_sep, 1) markers_as_string = markers_as_string.strip() @@ -302,9 +299,8 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar # it's a local file, dir, or url if link: # Handle relative file URLs - if link.scheme == 'file' and re.search(r'\.\./', link.url): - link = Link( - path_to_url(os.path.normpath(os.path.abspath(link.path)))) + if link.scheme == "file" and re.search(r"\.\./", link.url): + link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path)))) # wheel file if link.is_wheel: wheel = Wheel(link.filename) # can raise InvalidWheelFilename @@ -323,7 +319,7 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar def with_source(text: str) -> str: if not line_source: return text - return f'{text} (from {line_source})' + return f"{text} (from {line_source})" def _parse_req_string(req_as_string: str) -> Requirement: try: @@ -332,16 +328,15 @@ def _parse_req_string(req_as_string: str) -> Requirement: if os.path.sep in req_as_string: add_msg = "It looks like a path." add_msg += deduce_helpful_msg(req_as_string) - elif ('=' in req_as_string and - not any(op in req_as_string for op in operators)): + elif "=" in req_as_string and not any( + op in req_as_string for op in operators + ): add_msg = "= is not a valid operator. Did you mean == ?" else: - add_msg = '' - msg = with_source( - f'Invalid requirement: {req_as_string!r}' - ) + add_msg = "" + msg = with_source(f"Invalid requirement: {req_as_string!r}") if add_msg: - msg += f'\nHint: {add_msg}' + msg += f"\nHint: {add_msg}" raise InstallationError(msg) else: # Deprecate extras after specifiers: "name>=1.0[extras]" @@ -350,7 +345,7 @@ def _parse_req_string(req_as_string: str) -> Requirement: # RequirementParts for spec in req.specifier: spec_str = str(spec) - if spec_str.endswith(']'): + if spec_str.endswith("]"): msg = f"Extras after version '{spec_str}'." raise InstallationError(msg) return req @@ -382,8 +377,12 @@ def install_req_from_line( parts = parse_req_from_line(name, line_source) return InstallRequirement( - parts.requirement, comes_from, link=parts.link, markers=parts.markers, - use_pep517=use_pep517, isolated=isolated, + parts.requirement, + comes_from, + link=parts.link, + markers=parts.markers, + use_pep517=use_pep517, + isolated=isolated, install_options=options.get("install_options", []) if options else [], global_options=options.get("global_options", []) if options else [], hash_options=options.get("hashes", {}) if options else {}, @@ -409,8 +408,12 @@ def install_req_from_req_string( PyPI.file_storage_domain, TestPyPI.file_storage_domain, ] - if (req.url and comes_from and comes_from.link and - comes_from.link.netloc in domains_not_allowed): + if ( + req.url + and comes_from + and comes_from.link + and comes_from.link.netloc in domains_not_allowed + ): # Explicitly disallow pypi packages that depend on external urls raise InstallationError( "Packages installed from PyPI cannot depend on packages " diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 01c6cf679f9..b392989bf8d 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -25,20 +25,20 @@ from pip._internal.index.package_finder import PackageFinder -__all__ = ['parse_requirements'] +__all__ = ["parse_requirements"] ReqFileLines = Iterator[Tuple[int, str]] LineParser = Callable[[str], Tuple[str, Values]] -SCHEME_RE = re.compile(r'^(http|https|file):', re.I) -COMMENT_RE = re.compile(r'(^|\s+)#.*$') +SCHEME_RE = re.compile(r"^(http|https|file):", re.I) +COMMENT_RE = re.compile(r"(^|\s+)#.*$") # Matches environment variable-style values in '${MY_VARIABLE_1}' with the # variable name consisting of only uppercase letters, digits or the '_' # (underscore). This follows the POSIX standard defined in IEEE Std 1003.1, # 2013 Edition. -ENV_VAR_RE = re.compile(r'(?P\$\{(?P[A-Z0-9_]+)\})') +ENV_VAR_RE = re.compile(r"(?P\$\{(?P[A-Z0-9_]+)\})") SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [ cmdoptions.index_url, @@ -134,10 +134,7 @@ def parse_requirements( for parsed_line in parser.parse(filename, constraint): parsed_req = handle_line( - parsed_line, - options=options, - finder=finder, - session=session + parsed_line, options=options, finder=finder, session=session ) if parsed_req is not None: yield parsed_req @@ -161,8 +158,10 @@ def handle_requirement_line( ) -> ParsedRequirement: # preserve for the nested code path - line_comes_from = '{} {} (line {})'.format( - '-c' if line.constraint else '-r', line.filename, line.lineno, + line_comes_from = "{} {} (line {})".format( + "-c" if line.constraint else "-r", + line.filename, + line.lineno, ) assert line.is_requirement @@ -187,7 +186,7 @@ def handle_requirement_line( if dest in line.opts.__dict__ and line.opts.__dict__[dest]: req_options[dest] = line.opts.__dict__[dest] - line_source = f'line {line.lineno} of {line.filename}' + line_source = f"line {line.lineno} of {line.filename}" return ParsedRequirement( requirement=line.requirement, is_editable=line.is_editable, @@ -213,8 +212,7 @@ def handle_option_line( options.require_hashes = opts.require_hashes if opts.features_enabled: options.features_enabled.extend( - f for f in opts.features_enabled - if f not in options.features_enabled + f for f in opts.features_enabled if f not in options.features_enabled ) # set finder options @@ -256,7 +254,7 @@ def handle_option_line( if session: for host in opts.trusted_hosts or []: - source = f'line {lineno} of {filename}' + source = f"line {lineno} of {filename}" session.add_trusted_host(host, source=source) @@ -314,17 +312,15 @@ def __init__( self._line_parser = line_parser def parse(self, filename: str, constraint: bool) -> Iterator[ParsedLine]: - """Parse a given file, yielding parsed lines. - """ + """Parse a given file, yielding parsed lines.""" yield from self._parse_and_recurse(filename, constraint) def _parse_and_recurse( self, filename: str, constraint: bool ) -> Iterator[ParsedLine]: for line in self._parse_file(filename, constraint): - if ( - not line.is_requirement and - (line.opts.requirements or line.opts.constraints) + if not line.is_requirement and ( + line.opts.requirements or line.opts.constraints ): # parse a nested requirements file if line.opts.requirements: @@ -342,7 +338,8 @@ def _parse_and_recurse( elif not SCHEME_RE.search(req_path): # do a join so relative paths work req_path = os.path.join( - os.path.dirname(filename), req_path, + os.path.dirname(filename), + req_path, ) yield from self._parse_and_recurse(req_path, nested_constraint) @@ -359,7 +356,7 @@ def _parse_file(self, filename: str, constraint: bool) -> Iterator[ParsedLine]: args_str, opts = self._line_parser(line) except OptionParsingError as e: # add offending line - msg = f'Invalid requirement: {line}\n{e.msg}' + msg = f"Invalid requirement: {line}\n{e.msg}" raise RequirementsFileParseError(msg) yield ParsedLine( @@ -395,16 +392,16 @@ def break_args_options(line: str) -> Tuple[str, str]: (and then optparse) the options, not the args. args can contain markers which are corrupted by shlex. """ - tokens = line.split(' ') + tokens = line.split(" ") args = [] options = tokens[:] for token in tokens: - if token.startswith('-') or token.startswith('--'): + if token.startswith("-") or token.startswith("--"): break else: args.append(token) options.pop(0) - return ' '.join(args), ' '.join(options) + return " ".join(args), " ".join(options) class OptionParsingError(Exception): @@ -427,6 +424,7 @@ def build_parser() -> optparse.OptionParser: # that in our own exception. def parser_exit(self: Any, msg: str) -> "NoReturn": raise OptionParsingError(msg) + # NOTE: mypy disallows assigning to a method # https://github.com/python/mypy/issues/2427 parser.exit = parser_exit # type: ignore @@ -441,26 +439,26 @@ def join_lines(lines_enum: ReqFileLines) -> ReqFileLines: primary_line_number = None new_line: List[str] = [] for line_number, line in lines_enum: - if not line.endswith('\\') or COMMENT_RE.match(line): + if not line.endswith("\\") or COMMENT_RE.match(line): if COMMENT_RE.match(line): # this ensures comments are always matched later - line = ' ' + line + line = " " + line if new_line: new_line.append(line) assert primary_line_number is not None - yield primary_line_number, ''.join(new_line) + yield primary_line_number, "".join(new_line) new_line = [] else: yield line_number, line else: if not new_line: primary_line_number = line_number - new_line.append(line.strip('\\')) + new_line.append(line.strip("\\")) # last line contains \ if new_line: assert primary_line_number is not None - yield primary_line_number, ''.join(new_line) + yield primary_line_number, "".join(new_line) # TODO: handle space after '\'. @@ -470,7 +468,7 @@ def ignore_comments(lines_enum: ReqFileLines) -> ReqFileLines: Strips comments and filter empty lines. """ for line_number, line in lines_enum: - line = COMMENT_RE.sub('', line) + line = COMMENT_RE.sub("", line) line = line.strip() if line: yield line_number, line @@ -514,15 +512,15 @@ def get_file_content(url: str, session: PipSession) -> Tuple[str, str]: scheme = get_url_scheme(url) # Pip has special support for file:// URLs (LocalFSAdapter). - if scheme in ['http', 'https', 'file']: + if scheme in ["http", "https", "file"]: resp = session.get(url) raise_for_status(resp) return resp.url, resp.text # Assume this is a bare path. try: - with open(url, 'rb') as f: + with open(url, "rb") as f: content = auto_decode(f.read()) except OSError as exc: - raise InstallationError(f'Could not open requirements file: {exc}') + raise InstallationError(f"Could not open requirements file: {exc}") return url, content diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4c58cdbdbe7..615535f24b6 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -122,9 +122,7 @@ def __init__( if self.editable: assert link if link.is_file: - self.source_dir = os.path.normpath( - os.path.abspath(link.file_path) - ) + self.source_dir = os.path.normpath(os.path.abspath(link.file_path)) if link is None and req and req.url: # PEP 508 URL requirement @@ -140,9 +138,7 @@ def __init__( if extras: self.extras = extras elif req: - self.extras = { - pkg_resources.safe_extra(extra) for extra in req.extras - } + self.extras = {pkg_resources.safe_extra(extra) for extra in req.extras} else: self.extras = set() if markers is None and req: @@ -202,36 +198,34 @@ def __str__(self) -> str: if self.req: s = str(self.req) if self.link: - s += ' from {}'.format(redact_auth_from_url(self.link.url)) + s += " from {}".format(redact_auth_from_url(self.link.url)) elif self.link: s = redact_auth_from_url(self.link.url) else: - s = '' + s = "" if self.satisfied_by is not None: - s += ' in {}'.format(display_path(self.satisfied_by.location)) + s += " in {}".format(display_path(self.satisfied_by.location)) if self.comes_from: if isinstance(self.comes_from, str): comes_from: Optional[str] = self.comes_from else: comes_from = self.comes_from.from_path() if comes_from: - s += f' (from {comes_from})' + s += f" (from {comes_from})" return s def __repr__(self) -> str: - return '<{} object: {} editable={!r}>'.format( - self.__class__.__name__, str(self), self.editable) + return "<{} object: {} editable={!r}>".format( + self.__class__.__name__, str(self), self.editable + ) def format_debug(self) -> str: - """An un-tested helper for getting state, for debugging. - """ + """An un-tested helper for getting state, for debugging.""" attributes = vars(self) names = sorted(attributes) - state = ( - "{}={!r}".format(attr, attributes[attr]) for attr in sorted(names) - ) - return '<{name} object: {{{state}}}>'.format( + state = ("{}={!r}".format(attr, attributes[attr]) for attr in sorted(names)) + return "<{name} object: {{{state}}}>".format( name=self.__class__.__name__, state=", ".join(state), ) @@ -254,18 +248,17 @@ def is_pinned(self) -> bool: For example, some-package==1.2 is pinned; some-package>1.2 is not. """ specifiers = self.specifier - return (len(specifiers) == 1 and - next(iter(specifiers)).operator in {'==', '==='}) + return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="} def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool: if not extras_requested: # Provide an extra to safely evaluate the markers # without matching any extra - extras_requested = ('',) + extras_requested = ("",) if self.markers is not None: return any( - self.markers.evaluate({'extra': extra}) - for extra in extras_requested) + self.markers.evaluate({"extra": extra}) for extra in extras_requested + ) else: return True @@ -301,8 +294,7 @@ def hashes(self, trust_internet: bool = True) -> Hashes: return Hashes(good_hashes) def from_path(self) -> Optional[str]: - """Format a nice indicator to show where this "comes from" - """ + """Format a nice indicator to show where this "comes from" """ if self.req is None: return None s = str(self.req) @@ -312,7 +304,7 @@ def from_path(self) -> Optional[str]: else: comes_from = self.comes_from.from_path() if comes_from: - s += '->' + comes_from + s += "->" + comes_from return s def ensure_build_location( @@ -345,7 +337,7 @@ def ensure_build_location( # FIXME: Is there a better place to create the build_dir? (hg and bzr # need this) if not os.path.exists(build_dir): - logger.debug('Creating directory %s', build_dir) + logger.debug("Creating directory %s", build_dir) os.makedirs(build_dir) actual_build_dir = os.path.join(build_dir, dir_name) # `None` indicates that we respect the globally-configured deletion @@ -359,8 +351,7 @@ def ensure_build_location( ).path def _set_requirement(self) -> None: - """Set requirement after generating metadata. - """ + """Set requirement after generating metadata.""" assert self.req is None assert self.metadata is not None assert self.source_dir is not None @@ -372,11 +363,13 @@ def _set_requirement(self) -> None: op = "===" self.req = Requirement( - "".join([ - self.metadata["Name"], - op, - self.metadata["Version"], - ]) + "".join( + [ + self.metadata["Name"], + op, + self.metadata["Version"], + ] + ) ) def warn_on_mismatching_name(self) -> None: @@ -387,10 +380,12 @@ def warn_on_mismatching_name(self) -> None: # If we're here, there's a mismatch. Log a warning about it. logger.warning( - 'Generating metadata for package %s ' - 'produced metadata for project name %s. Fix your ' - '#egg=%s fragments.', - self.name, metadata_name, self.name + "Generating metadata for package %s " + "produced metadata for project name %s. Fix your " + "#egg=%s fragments.", + self.name, + metadata_name, + self.name, ) self.req = Requirement(metadata_name) @@ -411,20 +406,22 @@ def check_if_exists(self, use_user_site: bool) -> None: # parses the version instead. existing_version = existing_dist.version version_compatible = ( - existing_version is not None and - self.req.specifier.contains(existing_version, prereleases=True) + existing_version is not None + and self.req.specifier.contains(existing_version, prereleases=True) ) if not version_compatible: self.satisfied_by = None if use_user_site: if dist_in_usersite(existing_dist): self.should_reinstall = True - elif (running_under_virtualenv() and - dist_in_site_packages(existing_dist)): + elif running_under_virtualenv() and dist_in_site_packages( + existing_dist + ): raise InstallationError( "Will not install to the user site because it will " "lack sys.path precedence to {} in {}".format( - existing_dist.project_name, existing_dist.location) + existing_dist.project_name, existing_dist.location + ) ) else: self.should_reinstall = True @@ -448,13 +445,13 @@ def is_wheel(self) -> bool: @property def unpacked_source_directory(self) -> str: return os.path.join( - self.source_dir, - self.link and self.link.subdirectory_fragment or '') + self.source_dir, self.link and self.link.subdirectory_fragment or "" + ) @property def setup_py_path(self) -> str: assert self.source_dir, f"No source dir for {self}" - setup_py = os.path.join(self.unpacked_source_directory, 'setup.py') + setup_py = os.path.join(self.unpacked_source_directory, "setup.py") return setup_py @@ -472,10 +469,7 @@ def load_pyproject_toml(self) -> None: follow the PEP 517 or legacy (setup.py) code path. """ pyproject_toml_data = load_pyproject_toml( - self.use_pep517, - self.pyproject_toml_path, - self.setup_py_path, - str(self) + self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self) ) if pyproject_toml_data is None: @@ -487,12 +481,13 @@ def load_pyproject_toml(self) -> None: self.requirements_to_check = check self.pyproject_requires = requires self.pep517_backend = Pep517HookCaller( - self.unpacked_source_directory, backend, backend_path=backend_path, + self.unpacked_source_directory, + backend, + backend_path=backend_path, ) def _generate_metadata(self) -> str: - """Invokes metadata generator functions, with the required arguments. - """ + """Invokes metadata generator functions, with the required arguments.""" if not self.use_pep517: assert self.unpacked_source_directory @@ -506,7 +501,7 @@ def _generate_metadata(self) -> str: setup_py_path=self.setup_py_path, source_dir=self.unpacked_source_directory, isolated=self.isolated, - details=self.name or f"from {self.link}" + details=self.name or f"from {self.link}", ) assert self.pep517_backend is not None @@ -537,7 +532,7 @@ def prepare_metadata(self) -> None: @property def metadata(self) -> Any: - if not hasattr(self, '_metadata'): + if not hasattr(self, "_metadata"): self._metadata = get_metadata(self.get_dist()) return self._metadata @@ -547,16 +542,16 @@ def get_dist(self) -> Distribution: def assert_source_matches_version(self) -> None: assert self.source_dir - version = self.metadata['version'] + version = self.metadata["version"] if self.req.specifier and version not in self.req.specifier: logger.warning( - 'Requested %s, but installing version %s', + "Requested %s, but installing version %s", self, version, ) else: logger.debug( - 'Source in %s has version %s, which satisfies requirement %s', + "Source in %s has version %s, which satisfies requirement %s", display_path(self.source_dir), version, self, @@ -589,14 +584,13 @@ def ensure_has_source_dir( def update_editable(self) -> None: if not self.link: logger.debug( - "Cannot update repository at %s; repository location is " - "unknown", + "Cannot update repository at %s; repository location is unknown", self.source_dir, ) return assert self.editable assert self.source_dir - if self.link.scheme == 'file': + if self.link.scheme == "file": # Static paths don't get updated return vcs_backend = vcs.get_backend_for_scheme(self.link.scheme) @@ -627,25 +621,24 @@ def uninstall( if not dist: logger.warning("Skipping %s as it is not installed.", self.name) return None - logger.info('Found existing installation: %s', dist) + logger.info("Found existing installation: %s", dist) uninstalled_pathset = UninstallPathSet.from_dist(dist) uninstalled_pathset.remove(auto_confirm, verbose) return uninstalled_pathset def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str: - def _clean_zip_name(name: str, prefix: str) -> str: - assert name.startswith(prefix + os.path.sep), ( - f"name {name!r} doesn't start with prefix {prefix!r}" - ) - name = name[len(prefix) + 1:] - name = name.replace(os.path.sep, '/') + assert name.startswith( + prefix + os.path.sep + ), f"name {name!r} doesn't start with prefix {prefix!r}" + name = name[len(prefix) + 1 :] + name = name.replace(os.path.sep, "/") return name path = os.path.join(parentdir, path) name = _clean_zip_name(path, rootdir) - return self.name + '/' + name + return self.name + "/" + name def archive(self, build_dir: Optional[str]) -> None: """Saves archive to provided build_dir. @@ -657,57 +650,62 @@ def archive(self, build_dir: Optional[str]) -> None: return create_archive = True - archive_name = '{}-{}.zip'.format(self.name, self.metadata["version"]) + archive_name = "{}-{}.zip".format(self.name, self.metadata["version"]) archive_path = os.path.join(build_dir, archive_name) if os.path.exists(archive_path): response = ask_path_exists( - 'The file {} exists. (i)gnore, (w)ipe, ' - '(b)ackup, (a)bort '.format( - display_path(archive_path)), - ('i', 'w', 'b', 'a')) - if response == 'i': + "The file {} exists. (i)gnore, (w)ipe, " + "(b)ackup, (a)bort ".format(display_path(archive_path)), + ("i", "w", "b", "a"), + ) + if response == "i": create_archive = False - elif response == 'w': - logger.warning('Deleting %s', display_path(archive_path)) + elif response == "w": + logger.warning("Deleting %s", display_path(archive_path)) os.remove(archive_path) - elif response == 'b': + elif response == "b": dest_file = backup_dir(archive_path) logger.warning( - 'Backing up %s to %s', + "Backing up %s to %s", display_path(archive_path), display_path(dest_file), ) shutil.move(archive_path, dest_file) - elif response == 'a': + elif response == "a": sys.exit(-1) if not create_archive: return zip_output = zipfile.ZipFile( - archive_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True, + archive_path, + "w", + zipfile.ZIP_DEFLATED, + allowZip64=True, ) with zip_output: - dir = os.path.normcase( - os.path.abspath(self.unpacked_source_directory) - ) + dir = os.path.normcase(os.path.abspath(self.unpacked_source_directory)) for dirpath, dirnames, filenames in os.walk(dir): for dirname in dirnames: dir_arcname = self._get_archive_name( - dirname, parentdir=dirpath, rootdir=dir, + dirname, + parentdir=dirpath, + rootdir=dir, ) - zipdir = zipfile.ZipInfo(dir_arcname + '/') + zipdir = zipfile.ZipInfo(dir_arcname + "/") zipdir.external_attr = 0x1ED << 16 # 0o755 - zip_output.writestr(zipdir, '') + zip_output.writestr(zipdir, "") for filename in filenames: file_arcname = self._get_archive_name( - filename, parentdir=dirpath, rootdir=dir, + filename, + parentdir=dirpath, + rootdir=dir, ) filename = os.path.join(dirpath, filename) zip_output.write(filename, file_arcname) - logger.info('Saved %s', display_path(archive_path)) + logger.info("Saved %s", display_path(archive_path)) def install( self, @@ -718,7 +716,7 @@ def install( prefix: Optional[str] = None, warn_script_location: bool = True, use_user_site: bool = False, - pycompile: bool = True + pycompile: bool = True, ) -> None: scheme = get_scheme( self.name, @@ -808,8 +806,9 @@ def install( deprecated( reason=( "{} was installed using the legacy 'setup.py install' " - "method, because a wheel could not be built for it.". - format(self.name) + "method, because a wheel could not be built for it.".format( + self.name + ) ), replacement="to fix the wheel build issue reported above", gone_in=None, diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 39a2b01cd2f..6626c37e2e1 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -13,10 +13,8 @@ class RequirementSet: - def __init__(self, check_supported_wheels: bool = True) -> None: - """Create a RequirementSet. - """ + """Create a RequirementSet.""" self.requirements: Dict[str, InstallRequirement] = OrderedDict() self.check_supported_wheels = check_supported_wheels @@ -28,7 +26,7 @@ def __str__(self) -> str: (req for req in self.requirements.values() if not req.comes_from), key=lambda req: canonicalize_name(req.name or ""), ) - return ' '.join(str(req.req) for req in requirements) + return " ".join(str(req.req) for req in requirements) def __repr__(self) -> str: requirements = sorted( @@ -36,11 +34,11 @@ def __repr__(self) -> str: key=lambda req: canonicalize_name(req.name or ""), ) - format_string = '<{classname} object; {count} requirement(s): {reqs}>' + format_string = "<{classname} object; {count} requirement(s): {reqs}>" return format_string.format( classname=self.__class__.__name__, count=len(requirements), - reqs=', '.join(str(req.req) for req in requirements), + reqs=", ".join(str(req.req) for req in requirements), ) def add_unnamed_requirement(self, install_req: InstallRequirement) -> None: @@ -57,7 +55,7 @@ def add_requirement( self, install_req: InstallRequirement, parent_req_name: Optional[str] = None, - extras_requested: Optional[Iterable[str]] = None + extras_requested: Optional[Iterable[str]] = None, ) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]: """Add install_req as a requirement to install. @@ -77,7 +75,8 @@ def add_requirement( if not install_req.match_markers(extras_requested): logger.info( "Ignoring %s: markers '%s' don't match your environment", - install_req.name, install_req.markers, + install_req.name, + install_req.markers, ) return [], None @@ -88,16 +87,17 @@ def add_requirement( if install_req.link and install_req.link.is_wheel: wheel = Wheel(install_req.link.filename) tags = compatibility_tags.get_supported() - if (self.check_supported_wheels and not wheel.supported(tags)): + if self.check_supported_wheels and not wheel.supported(tags): raise InstallationError( "{} is not a supported wheel on this platform.".format( - wheel.filename) + wheel.filename + ) ) # This next bit is really a sanity check. - assert not install_req.user_supplied or parent_req_name is None, ( - "a user supplied req shouldn't have a parent" - ) + assert ( + not install_req.user_supplied or parent_req_name is None + ), "a user supplied req shouldn't have a parent" # Unnamed requirements are scanned again and the requirement won't be # added as a dependency until after scanning. @@ -107,23 +107,25 @@ def add_requirement( try: existing_req: Optional[InstallRequirement] = self.get_requirement( - install_req.name) + install_req.name + ) except KeyError: existing_req = None has_conflicting_requirement = ( - parent_req_name is None and - existing_req and - not existing_req.constraint and - existing_req.extras == install_req.extras and - existing_req.req and - install_req.req and - existing_req.req.specifier != install_req.req.specifier + parent_req_name is None + and existing_req + and not existing_req.constraint + and existing_req.extras == install_req.extras + and existing_req.req + and install_req.req + and existing_req.req.specifier != install_req.req.specifier ) if has_conflicting_requirement: raise InstallationError( - "Double requirement given: {} (already in {}, name={!r})" - .format(install_req, existing_req, install_req.name) + "Double requirement given: {} (already in {}, name={!r})".format( + install_req, existing_req, install_req.name + ) ) # When no existing requirement exists, add the requirement as a @@ -138,12 +140,8 @@ def add_requirement( if install_req.constraint or not existing_req.constraint: return [], existing_req - does_not_satisfy_constraint = ( - install_req.link and - not ( - existing_req.link and - install_req.link.path == existing_req.link.path - ) + does_not_satisfy_constraint = install_req.link and not ( + existing_req.link and install_req.link.path == existing_req.link.path ) if does_not_satisfy_constraint: raise InstallationError( @@ -158,12 +156,13 @@ def add_requirement( # mark the existing object as such. if install_req.user_supplied: existing_req.user_supplied = True - existing_req.extras = tuple(sorted( - set(existing_req.extras) | set(install_req.extras) - )) + existing_req.extras = tuple( + sorted(set(existing_req.extras) | set(install_req.extras)) + ) logger.debug( "Setting %s extras to: %s", - existing_req, existing_req.extras, + existing_req, + existing_req.extras, ) # Return the existing requirement for addition to the parent and # scanning again. @@ -173,8 +172,8 @@ def has_requirement(self, name: str) -> bool: project_name = canonicalize_name(name) return ( - project_name in self.requirements and - not self.requirements[project_name].constraint + project_name in self.requirements + and not self.requirements[project_name].constraint ) def get_requirement(self, name: str) -> InstallRequirement: diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 27c6baf433f..24d3c530335 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -40,12 +40,10 @@ def update_env_context_manager(**changes: str) -> Iterator[None]: @contextlib.contextmanager def get_requirement_tracker() -> Iterator["RequirementTracker"]: - root = os.environ.get('PIP_REQ_TRACKER') + root = os.environ.get("PIP_REQ_TRACKER") with contextlib.ExitStack() as ctx: if root is None: - root = ctx.enter_context( - TempDirectory(kind='req-tracker') - ).path + root = ctx.enter_context(TempDirectory(kind="req-tracker")).path ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root)) logger.debug("Initialized build tracking at %s", root) @@ -54,7 +52,6 @@ def get_requirement_tracker() -> Iterator["RequirementTracker"]: class RequirementTracker: - def __init__(self, root: str) -> None: self._root = root self._entries: Set[InstallRequirement] = set() @@ -68,7 +65,7 @@ def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType] + exc_tb: Optional[TracebackType], ) -> None: self.cleanup() @@ -77,8 +74,7 @@ def _entry_path(self, link: Link) -> str: return os.path.join(self._root, hashed) def add(self, req: InstallRequirement) -> None: - """Add an InstallRequirement to build tracking. - """ + """Add an InstallRequirement to build tracking.""" assert req.link # Get the file to write information about this requirement. @@ -92,30 +88,28 @@ def add(self, req: InstallRequirement) -> None: except FileNotFoundError: pass else: - message = '{} is already being built: {}'.format( - req.link, contents) + message = "{} is already being built: {}".format(req.link, contents) raise LookupError(message) # If we're here, req should really not be building already. assert req not in self._entries # Start tracking this requirement. - with open(entry_path, 'w', encoding="utf-8") as fp: + with open(entry_path, "w", encoding="utf-8") as fp: fp.write(str(req)) self._entries.add(req) - logger.debug('Added %s to build tracker %r', req, self._root) + logger.debug("Added %s to build tracker %r", req, self._root) def remove(self, req: InstallRequirement) -> None: - """Remove an InstallRequirement from build tracking. - """ + """Remove an InstallRequirement from build tracking.""" assert req.link # Delete the created file and the corresponding entries. os.unlink(self._entry_path(req.link)) self._entries.remove(req) - logger.debug('Removed %s from build tracker %r', req, self._root) + logger.debug("Removed %s from build tracker %r", req, self._root) def cleanup(self) -> None: for req in set(self._entries): diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 0c51c846722..ef7352f7ba3 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -40,12 +40,12 @@ def _script_names(dist: Distribution, script_name: str, is_gui: bool) -> List[st exe_name = os.path.join(bin_dir, script_name) paths_to_remove = [exe_name] if WINDOWS: - paths_to_remove.append(exe_name + '.exe') - paths_to_remove.append(exe_name + '.exe.manifest') + paths_to_remove.append(exe_name + ".exe") + paths_to_remove.append(exe_name + ".exe.manifest") if is_gui: - paths_to_remove.append(exe_name + '-script.pyw') + paths_to_remove.append(exe_name + "-script.pyw") else: - paths_to_remove.append(exe_name + '-script.py') + paths_to_remove.append(exe_name + "-script.py") return paths_to_remove @@ -57,6 +57,7 @@ def unique(*args: Any, **kw: Any) -> Iterator[Any]: if item not in seen: seen.add(item) yield item + return unique @@ -76,29 +77,31 @@ def uninstallation_paths(dist: Distribution) -> Iterator[str]: https://packaging.python.org/specifications/recording-installed-packages/ """ try: - r = csv.reader(dist.get_metadata_lines('RECORD')) + r = csv.reader(dist.get_metadata_lines("RECORD")) except FileNotFoundError as missing_record_exception: - msg = 'Cannot uninstall {dist}, RECORD file not found.'.format(dist=dist) + msg = "Cannot uninstall {dist}, RECORD file not found.".format(dist=dist) try: - installer = next(dist.get_metadata_lines('INSTALLER')) - if not installer or installer == 'pip': + installer = next(dist.get_metadata_lines("INSTALLER")) + if not installer or installer == "pip": raise ValueError() except (OSError, StopIteration, ValueError): - dep = '{}=={}'.format(dist.project_name, dist.version) - msg += (" You might be able to recover from this via: " - "'pip install --force-reinstall --no-deps {}'.".format(dep)) + dep = "{}=={}".format(dist.project_name, dist.version) + msg += ( + " You might be able to recover from this via: " + "'pip install --force-reinstall --no-deps {}'.".format(dep) + ) else: - msg += ' Hint: The package was installed by {}.'.format(installer) + msg += " Hint: The package was installed by {}.".format(installer) raise UninstallationError(msg) from missing_record_exception for row in r: path = os.path.join(dist.location, row[0]) yield path - if path.endswith('.py'): + if path.endswith(".py"): dn, fn = os.path.split(path) base = fn[:-3] - path = os.path.join(dn, base + '.pyc') + path = os.path.join(dn, base + ".pyc") yield path - path = os.path.join(dn, base + '.pyo') + path = os.path.join(dn, base + ".pyo") yield path @@ -112,8 +115,8 @@ def compact(paths: Iterable[str]) -> Set[str]: short_paths: Set[str] = set() for path in sorted(paths, key=len): should_skip = any( - path.startswith(shortpath.rstrip("*")) and - path[len(shortpath.rstrip("*").rstrip(sep))] == sep + path.startswith(shortpath.rstrip("*")) + and path[len(shortpath.rstrip("*").rstrip(sep))] == sep for shortpath in short_paths ) if not should_skip: @@ -136,18 +139,15 @@ def norm_join(*a: str) -> str: return os.path.normcase(os.path.join(*a)) for root in unchecked: - if any(os.path.normcase(root).startswith(w) - for w in wildcards): + if any(os.path.normcase(root).startswith(w) for w in wildcards): # This directory has already been handled. continue all_files: Set[str] = set() all_subdirs: Set[str] = set() for dirname, subdirs, files in os.walk(root): - all_subdirs.update(norm_join(root, dirname, d) - for d in subdirs) - all_files.update(norm_join(root, dirname, f) - for f in files) + all_subdirs.update(norm_join(root, dirname, d) for d in subdirs) + all_files.update(norm_join(root, dirname, f) for f in files) # If all the files we found are in our remaining set of files to # remove, then remove them from the latter set and add a wildcard # for the directory. @@ -196,14 +196,14 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str continue file_ = os.path.join(dirpath, fname) - if (os.path.isfile(file_) and - os.path.normcase(file_) not in _normcased_files): + if ( + os.path.isfile(file_) + and os.path.normcase(file_) not in _normcased_files + ): # We are skipping this file. Add it to the set. will_skip.add(file_) - will_remove = files | { - os.path.join(folder, "*") for folder in folders - } + will_remove = files | {os.path.join(folder, "*") for folder in folders} return will_remove, will_skip @@ -211,6 +211,7 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str class StashedUninstallPathSet: """A set of file rename operations to stash files while tentatively uninstalling them.""" + def __init__(self) -> None: # Mapping from source file root to [Adjacent]TempDirectory # for files under that directory. @@ -252,7 +253,7 @@ def _get_file_stash(self, path: str) -> str: else: # Did not find any suitable root head = os.path.dirname(path) - save_dir = TempDirectory(kind='uninstall') + save_dir = TempDirectory(kind="uninstall") self._save_dirs[head] = save_dir relpath = os.path.relpath(path, head) @@ -271,7 +272,7 @@ def stash(self, path: str) -> str: new_path = self._get_file_stash(path) self._moves.append((path, new_path)) - if (path_is_dir and os.path.isdir(new_path)): + if path_is_dir and os.path.isdir(new_path): # If we're moving a directory, we need to # remove the destination first or else it will be # moved to inside the existing directory. @@ -295,7 +296,7 @@ def rollback(self) -> None: for new_path, path in self._moves: try: - logger.debug('Replacing %s from %s', new_path, path) + logger.debug("Replacing %s from %s", new_path, path) if os.path.isfile(new_path) or os.path.islink(new_path): os.unlink(new_path) elif os.path.isdir(new_path): @@ -315,6 +316,7 @@ def can_rollback(self) -> bool: class UninstallPathSet: """A set of file paths to be removed in the uninstallation of a requirement.""" + def __init__(self, dist: Distribution) -> None: self.paths: Set[str] = set() self._refuse: Set[str] = set() @@ -346,7 +348,7 @@ def add(self, path: str) -> None: # __pycache__ files can show up after 'installed-files.txt' is created, # due to imports - if os.path.splitext(path)[1] == '.py': + if os.path.splitext(path)[1] == ".py": self.add(cache_from_source(path)) def add_pth(self, pth_file: str, entry: str) -> None: @@ -369,10 +371,8 @@ def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None: ) return - dist_name_version = ( - self.dist.project_name + "-" + self.dist.version - ) - logger.info('Uninstalling %s:', dist_name_version) + dist_name_version = self.dist.project_name + "-" + self.dist.version + logger.info("Uninstalling %s:", dist_name_version) with indent_log(): if auto_confirm or self._allowed_to_proceed(verbose): @@ -382,16 +382,15 @@ def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None: for path in sorted(compact(for_rename)): moved.stash(path) - logger.verbose('Removing file or directory %s', path) + logger.verbose("Removing file or directory %s", path) for pth in self.pth.values(): pth.remove() - logger.info('Successfully uninstalled %s', dist_name_version) + logger.info("Successfully uninstalled %s", dist_name_version) def _allowed_to_proceed(self, verbose: bool) -> bool: - """Display which files would be deleted and prompt for confirmation - """ + """Display which files would be deleted and prompt for confirmation""" def _display(msg: str, paths: Iterable[str]) -> None: if not paths: @@ -410,13 +409,13 @@ def _display(msg: str, paths: Iterable[str]) -> None: will_remove = set(self.paths) will_skip = set() - _display('Would remove:', will_remove) - _display('Would not remove (might be manually added):', will_skip) - _display('Would not remove (outside of prefix):', self._refuse) + _display("Would remove:", will_remove) + _display("Would not remove (might be manually added):", will_skip) + _display("Would not remove (outside of prefix):", self._refuse) if verbose: - _display('Will actually move:', compress_for_rename(self.paths)) + _display("Will actually move:", compress_for_rename(self.paths)) - return ask('Proceed (Y/n)? ', ('y', 'n', '')) != 'n' + return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n" def rollback(self) -> None: """Rollback the changes previously made by remove().""" @@ -426,7 +425,7 @@ def rollback(self) -> None: self.dist.project_name, ) return - logger.info('Rolling back uninstall of %s', self.dist.project_name) + logger.info("Rolling back uninstall of %s", self.dist.project_name) self._moved_paths.rollback() for pth in self.pth.values(): pth.rollback() @@ -447,9 +446,11 @@ def from_dist(cls, dist: Distribution) -> "UninstallPathSet": ) return cls(dist) - if dist_path in {p for p in {sysconfig.get_path("stdlib"), - sysconfig.get_path("platstdlib")} - if p}: + if dist_path in { + p + for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")} + if p + }: logger.info( "Not uninstalling %s at %s, as it is in the standard library.", dist.key, @@ -459,43 +460,47 @@ def from_dist(cls, dist: Distribution) -> "UninstallPathSet": paths_to_remove = cls(dist) develop_egg_link = egg_link_path(dist) - develop_egg_link_egg_info = '{}.egg-info'.format( - pkg_resources.to_filename(dist.project_name)) + develop_egg_link_egg_info = "{}.egg-info".format( + pkg_resources.to_filename(dist.project_name) + ) egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info) # Special case for distutils installed package - distutils_egg_info = getattr(dist._provider, 'path', None) + distutils_egg_info = getattr(dist._provider, "path", None) # Uninstall cases order do matter as in the case of 2 installs of the # same package, pip needs to uninstall the currently detected version - if (egg_info_exists and dist.egg_info.endswith('.egg-info') and - not dist.egg_info.endswith(develop_egg_link_egg_info)): + if ( + egg_info_exists + and dist.egg_info.endswith(".egg-info") + and not dist.egg_info.endswith(develop_egg_link_egg_info) + ): # if dist.egg_info.endswith(develop_egg_link_egg_info), we # are in fact in the develop_egg_link case paths_to_remove.add(dist.egg_info) - if dist.has_metadata('installed-files.txt'): + if dist.has_metadata("installed-files.txt"): for installed_file in dist.get_metadata( - 'installed-files.txt').splitlines(): - path = os.path.normpath( - os.path.join(dist.egg_info, installed_file) - ) + "installed-files.txt" + ).splitlines(): + path = os.path.normpath(os.path.join(dist.egg_info, installed_file)) paths_to_remove.add(path) # FIXME: need a test for this elif block # occurs with --single-version-externally-managed/--record outside # of pip - elif dist.has_metadata('top_level.txt'): - if dist.has_metadata('namespace_packages.txt'): - namespaces = dist.get_metadata('namespace_packages.txt') + elif dist.has_metadata("top_level.txt"): + if dist.has_metadata("namespace_packages.txt"): + namespaces = dist.get_metadata("namespace_packages.txt") else: namespaces = [] for top_level_pkg in [ - p for p - in dist.get_metadata('top_level.txt').splitlines() - if p and p not in namespaces]: + p + for p in dist.get_metadata("top_level.txt").splitlines() + if p and p not in namespaces + ]: path = os.path.join(dist.location, top_level_pkg) paths_to_remove.add(path) - paths_to_remove.add(path + '.py') - paths_to_remove.add(path + '.pyc') - paths_to_remove.add(path + '.pyo') + paths_to_remove.add(path + ".py") + paths_to_remove.add(path + ".pyc") + paths_to_remove.add(path + ".pyo") elif distutils_egg_info: raise UninstallationError( @@ -506,17 +511,18 @@ def from_dist(cls, dist: Distribution) -> "UninstallPathSet": ) ) - elif dist.location.endswith('.egg'): + elif dist.location.endswith(".egg"): # package installed by easy_install # We cannot match on dist.egg_name because it can slightly vary # i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg paths_to_remove.add(dist.location) easy_install_egg = os.path.split(dist.location)[1] - easy_install_pth = os.path.join(os.path.dirname(dist.location), - 'easy-install.pth') - paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg) + easy_install_pth = os.path.join( + os.path.dirname(dist.location), "easy-install.pth" + ) + paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg) - elif egg_info_exists and dist.egg_info.endswith('.dist-info'): + elif egg_info_exists and dist.egg_info.endswith(".dist-info"): for path in uninstallation_paths(dist): paths_to_remove.add(path) @@ -524,40 +530,42 @@ def from_dist(cls, dist: Distribution) -> "UninstallPathSet": # develop egg with open(develop_egg_link) as fh: link_pointer = os.path.normcase(fh.readline().strip()) - assert (link_pointer == dist.location), ( - 'Egg-link {} does not match installed location of {} ' - '(at {})'.format( - link_pointer, dist.project_name, dist.location) + assert ( + link_pointer == dist.location + ), "Egg-link {} does not match installed location of {} (at {})".format( + link_pointer, dist.project_name, dist.location ) paths_to_remove.add(develop_egg_link) - easy_install_pth = os.path.join(os.path.dirname(develop_egg_link), - 'easy-install.pth') + easy_install_pth = os.path.join( + os.path.dirname(develop_egg_link), "easy-install.pth" + ) paths_to_remove.add_pth(easy_install_pth, dist.location) else: logger.debug( - 'Not sure how to uninstall: %s - Check: %s', - dist, dist.location, + "Not sure how to uninstall: %s - Check: %s", + dist, + dist.location, ) # find distutils scripts= scripts - if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'): - for script in dist.metadata_listdir('scripts'): + if dist.has_metadata("scripts") and dist.metadata_isdir("scripts"): + for script in dist.metadata_listdir("scripts"): if dist_in_usersite(dist): bin_dir = get_bin_user() else: bin_dir = get_bin_prefix() paths_to_remove.add(os.path.join(bin_dir, script)) if WINDOWS: - paths_to_remove.add(os.path.join(bin_dir, script) + '.bat') + paths_to_remove.add(os.path.join(bin_dir, script) + ".bat") # find console_scripts _scripts_to_remove = [] - console_scripts = dist.get_entry_map(group='console_scripts') + console_scripts = dist.get_entry_map(group="console_scripts") for name in console_scripts.keys(): _scripts_to_remove.extend(_script_names(dist, name, False)) # find gui_scripts - gui_scripts = dist.get_entry_map(group='gui_scripts') + gui_scripts = dist.get_entry_map(group="gui_scripts") for name in gui_scripts.keys(): _scripts_to_remove.extend(_script_names(dist, name, True)) @@ -585,45 +593,41 @@ def add(self, entry: str) -> None: # have more than "\\sever\share". Valid examples: "\\server\share\" or # "\\server\share\folder". if WINDOWS and not os.path.splitdrive(entry)[0]: - entry = entry.replace('\\', '/') + entry = entry.replace("\\", "/") self.entries.add(entry) def remove(self) -> None: - logger.verbose('Removing pth entries from %s:', self.file) + logger.verbose("Removing pth entries from %s:", self.file) # If the file doesn't exist, log a warning and return if not os.path.isfile(self.file): - logger.warning( - "Cannot remove entries from nonexistent file %s", self.file - ) + logger.warning("Cannot remove entries from nonexistent file %s", self.file) return - with open(self.file, 'rb') as fh: + with open(self.file, "rb") as fh: # windows uses '\r\n' with py3k, but uses '\n' with py2.x lines = fh.readlines() self._saved_lines = lines - if any(b'\r\n' in line for line in lines): - endline = '\r\n' + if any(b"\r\n" in line for line in lines): + endline = "\r\n" else: - endline = '\n' + endline = "\n" # handle missing trailing newline if lines and not lines[-1].endswith(endline.encode("utf-8")): lines[-1] = lines[-1] + endline.encode("utf-8") for entry in self.entries: try: - logger.verbose('Removing entry: %s', entry) + logger.verbose("Removing entry: %s", entry) lines.remove((entry + endline).encode("utf-8")) except ValueError: pass - with open(self.file, 'wb') as fh: + with open(self.file, "wb") as fh: fh.writelines(lines) def rollback(self) -> bool: if self._saved_lines is None: - logger.error( - 'Cannot roll back changes to %s, none were made', self.file - ) + logger.error("Cannot roll back changes to %s, none were made", self.file) return False - logger.debug('Rolling %s back to previous state', self.file) - with open(self.file, 'wb') as fh: + logger.debug("Rolling %s back to previous state", self.file) + with open(self.file, "wb") as fh: fh.writelines(self._saved_lines) return True diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 7f258c57481..b206692a0a9 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -36,11 +36,8 @@ def from_ireq(cls, ireq: InstallRequirement) -> "Constraint": links = frozenset([ireq.link]) if ireq.link else frozenset() return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links) - def __nonzero__(self) -> bool: - return bool(self.specifier) or bool(self.hashes) or bool(self.links) - def __bool__(self) -> bool: - return self.__nonzero__() + return bool(self.specifier) or bool(self.hashes) or bool(self.links) def __and__(self, other: InstallRequirement) -> "Constraint": if not isinstance(other, InstallRequirement): diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index d2fa5ef5591..f28df6481f3 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -138,5 +138,3 @@ def __bool__(self) -> bool: if self._prefers_installed and self._installed: return True return any(self) - - __nonzero__ = __bool__ # XXX: Python 2. diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index 57dbdbdca4e..a163395c4e5 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -11,6 +11,15 @@ from pip import __version__ as current_version DEPRECATION_MSG_PREFIX = "DEPRECATION: " +DEPRECATION_MESSAGE = DEPRECATION_MSG_PREFIX + "{reason}" +GONE_IN_MESSAGE_FUTURE = "pip {gone_in} will enforce this behavior change." +GONE_IN_MESSAGE_PAST = "This behavior change has been enforced since pip {gone_in}." +REPLACEMENT_MESSAGE = "A possible replacement is {replacement}." +FEATURE_FLAG_MESSAGE = ( + "You can temporarily use the flag --use-feature={feature_flag} " + "to test the upcoming behavior." +) +ISSUE_MESSAGE = "Discussion can be found at https://github.com/pypa/pip/issues/{issue}." class PipDeprecationWarning(Warning): @@ -56,20 +65,24 @@ def deprecated( reason: str, replacement: Optional[str], gone_in: Optional[str], + feature_flag: Optional[str] = None, issue: Optional[int] = None, ) -> None: """Helper to deprecate existing functionality. reason: Textual reason shown to the user about why this functionality has - been deprecated. + been deprecated. Should be a complete sentence. replacement: Textual suggestion shown to the user about what alternative functionality they can use. gone_in: The version of pip does this functionality should get removed in. - Raises errors if pip's current version is greater than or equal to + Raises an error if pip's current version is greater than or equal to this. + feature_flag: + Command-line flag of the form --use-feature={feature_flag} for testing + upcoming functionality. issue: Issue number on the tracker that would serve as a useful place for users to find related discussion and provide feedback. @@ -77,28 +90,38 @@ def deprecated( Always pass replacement, gone_in and issue as keyword arguments for clarity at the call site. """ - + # Determine whether or not the feature is already gone in this version. + is_gone = gone_in is not None and parse(current_version) >= parse(gone_in) + # Allow variable substitutions within the "reason" variable. + formatted_reason = reason.format(gone_in=gone_in) # Construct a nice message. # This is eagerly formatted as we want it to get logged as if someone # typed this entire message out. + formatted_deprecation_message = DEPRECATION_MESSAGE.format(reason=formatted_reason) + gone_in_message = GONE_IN_MESSAGE_PAST if is_gone else GONE_IN_MESSAGE_FUTURE + formatted_gone_in_message = ( + gone_in_message.format(gone_in=gone_in) if gone_in else None + ) + formatted_replacement_message = ( + REPLACEMENT_MESSAGE.format(replacement=replacement) if replacement else None + ) + formatted_feature_flag_message = ( + None + if is_gone or not feature_flag + else FEATURE_FLAG_MESSAGE.format(feature_flag=feature_flag) + ) + formatted_issue_message = ISSUE_MESSAGE.format(issue=issue) if issue else None sentences = [ - (reason, DEPRECATION_MSG_PREFIX + "{}"), - (gone_in, "pip {} will remove support for this functionality."), - (replacement, "A possible replacement is {}."), - ( - issue, - ( - "You can find discussion regarding this at " - "https://github.com/pypa/pip/issues/{}." - ), - ), + formatted_deprecation_message, + formatted_gone_in_message, + formatted_replacement_message, + formatted_feature_flag_message, + formatted_issue_message, ] - message = " ".join( - template.format(val) for val, template in sentences if val is not None - ) + message = " ".join(sentence for sentence in sentences if sentence) - # Raise as an error if it has to be removed. - if gone_in is not None and parse(current_version) >= parse(gone_in): + # Raise as an error if the functionality is gone. + if is_gone: raise PipDeprecationWarning(message) - - warnings.warn(message, category=PipDeprecationWarning, stacklevel=2) + else: + warnings.warn(message, category=PipDeprecationWarning, stacklevel=2) diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 3d20b8d02cd..f011f659140 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -117,15 +117,11 @@ def check_against_path(self, path): with open(path, "rb") as file: return self.check_against_file(file) - def __nonzero__(self): + def __bool__(self): # type: () -> bool """Return whether I know any known-good hashes.""" return bool(self._allowed) - def __bool__(self): - # type: () -> bool - return self.__nonzero__() - def __eq__(self, other): # type: (object) -> bool if not isinstance(other, Hashes): diff --git a/tools/requirements/tests-common_wheels.txt b/tests/requirements-common_wheels.txt similarity index 100% rename from tools/requirements/tests-common_wheels.txt rename to tests/requirements-common_wheels.txt diff --git a/tools/requirements/tests.txt b/tests/requirements.txt similarity index 100% rename from tools/requirements/tests.txt rename to tests/requirements.txt diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ebccd666011..4e5bba2e1e4 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -934,9 +934,9 @@ def test_deprecated_message_reads_well(): assert message == ( "DEPRECATION: Stop doing this! " - "pip 1.0 will remove support for this functionality. " + "This behavior change has been enforced since pip 1.0. " "A possible replacement is to be nicer. " - "You can find discussion regarding this at " + "Discussion can be found at " "https://github.com/pypa/pip/issues/100000." ) diff --git a/tox.ini b/tox.ini index 60f133cea91..5000f115ebd 100644 --- a/tox.ini +++ b/tox.ini @@ -23,10 +23,10 @@ setenv = # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. LC_CTYPE = en_US.UTF-8 -deps = -r{toxinidir}/tools/requirements/tests.txt +deps = -r{toxinidir}/tests/requirements.txt commands_pre = python -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' {toxinidir}/tests/data/common_wheels - {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tools/requirements/tests-common_wheels.txt + {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tests/requirements-common_wheels.txt commands = pytest [] install_command = {[helpers]pip} install {opts} {packages} list_dependencies_command = {[helpers]pip} freeze --all @@ -48,7 +48,7 @@ setenv = [testenv:docs] # Don't skip install here since pip_sphinxext uses pip's internals. -deps = -r{toxinidir}/tools/requirements/docs.txt +deps = -r{toxinidir}/docs/requirements.txt basepython = python3 commands = sphinx-build -W -j auto -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html