diff options
70 files changed, 893 insertions, 617 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 508153d8d..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ -* pip version: -* Python version: -* Operating system: diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index dd12e3da8..000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 30 -# Issues and pull requests with these labels will not be locked. -exemptLabels: [] -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: "S: auto-locked" -# Comment to post before locking. Set to `false` to disable -lockComment: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f3e41746..51f7b8f9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,16 @@ on: - cron: 0 0 * * MON # Run every Monday at 00:00 UTC jobs: + docs: + name: docs + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install nox + - run: nox -s docs + determine-changes: runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/lock-threads.yml b/.github/workflows/lock-threads.yml new file mode 100644 index 000000000..985060c2e --- /dev/null +++ b/.github/workflows/lock-threads.yml @@ -0,0 +1,22 @@ +name: 'Lock Closed Threads' + +on: + schedule: + - cron: '0 7 * * *' # 7am UTC, daily + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2 + with: + issue-lock-inactive-days: '30' + pr-lock-inactive-days: '15' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b68022f94..a2a147be0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,7 @@ repos: additional_dependencies: [ 'keyring==23.0.1', 'nox==2021.6.12', + 'pytest==6.2.5', 'types-docutils==0.1.8', 'types-setuptools==57.0.2', 'types-six==0.1.9', diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index 1362b490b..6b4d3b1a2 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -149,120 +149,10 @@ profile: ``setup_requires``. -.. _`Requirements File Format`: - Requirements File Format ------------------------ -Each line of the requirements file indicates something to be installed, -and like arguments to :ref:`pip install`, the following forms are supported:: - - [[--option]...] - <requirement specifier> [; markers] [[--option]...] - <archive url/path> - [-e] <local project path> - [-e] <vcs project url> - -For details on requirement specifiers, see :ref:`Requirement Specifiers`. - -See the :ref:`pip install Examples<pip install Examples>` for examples of all these forms. - -A line that begins with ``#`` is treated as a comment and ignored. Whitespace -followed by a ``#`` causes the ``#`` and the remainder of the line to be -treated as a comment. - -A line ending in an unescaped ``\`` is treated as a line continuation -and the newline following it is effectively ignored. - -Comments are stripped *after* line continuations are processed. - -To interpret the requirements file in UTF-8 format add a comment -``# -*- coding: utf-8 -*-`` to the first or second line of the file. - -The following options are supported: - -.. pip-requirements-file-options-ref-list:: - -Please note that the above options are global options, and should be specified on their individual lines. -The options which can be applied to individual requirements are -:ref:`--install-option <install_--install-option>`, :ref:`--global-option <install_--global-option>` and ``--hash``. - -For example, to specify :ref:`--pre <install_--pre>`, :ref:`--no-index <install_--no-index>` and two -:ref:`--find-links <install_--find-links>` locations: - -:: - ---pre ---no-index ---find-links /my/local/archives ---find-links http://some.archives.com/archives - - -If you wish, you can refer to other requirements files, like this:: - - -r more_requirements.txt - -You can also refer to :ref:`constraints files <Constraints Files>`, like this:: - - -c some_constraints.txt - -.. _`Using Environment Variables`: - -Using Environment Variables -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Since version 10, pip supports the use of environment variables inside the -requirements file. You can now store sensitive data (tokens, keys, etc.) in -environment variables and only specify the variable name for your requirements, -letting pip lookup the value at runtime. This approach aligns with the commonly -used `12-factor configuration pattern <https://12factor.net/config>`_. - -You have to use the POSIX format for variable names including brackets around -the uppercase name as shown in this example: ``${API_TOKEN}``. pip will attempt -to find the corresponding environment variable defined on the host system at -runtime. - -.. note:: - - There is no support for other variable expansion syntaxes such as - ``$VARIABLE`` and ``%VARIABLE%``. - - -.. _`Example Requirements File`: - -Example Requirements File -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use ``pip install -r example-requirements.txt`` to install:: - - # - ####### example-requirements.txt ####### - # - ###### Requirements without Version Specifiers ###### - nose - nose-cov - beautifulsoup4 - # - ###### Requirements with Version Specifiers ###### - # See https://www.python.org/dev/peps/pep-0440/#version-specifiers - docopt == 0.6.1 # Version Matching. Must be version 0.6.1 - keyring >= 4.1.1 # Minimum version 4.1.1 - coverage != 3.5 # Version Exclusion. Anything except version 3.5 - Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* - # - ###### Refer to other requirements files ###### - -r other-requirements.txt - # - # - ###### A particular file ###### - ./downloads/numpy-1.9.2-cp34-none-win32.whl - http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl - # - ###### Additional Requirements without Version Specifiers ###### - # Same as 1st section, just here to show that you can put things in any order. - rejected - green - # +This section has been moved to :doc:`../reference/requirements-file-format`. .. _`Requirement Specifiers`: diff --git a/docs/html/cli/pip_list.rst b/docs/html/cli/pip_list.rst index cb40de67d..c84cb8c09 100644 --- a/docs/html/cli/pip_list.rst +++ b/docs/html/cli/pip_list.rst @@ -139,3 +139,93 @@ Examples docopt==0.6.2 idlex==1.13 jedi==0.9.0 + +#. List packages installed in editable mode + +When some packages are installed in editable mode, ``pip list`` outputs an +additional column that shows the directory where the editable project is +located (i.e. the directory that contains the ``pyproject.toml`` or +``setup.py`` file). + + .. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip list + Package Version Editable project location + ---------------- -------- ------------------------------------- + pip 21.2.4 + pip-test-package 0.1.1 /home/you/.venv/src/pip-test-package + setuptools 57.4.0 + wheel 0.36.2 + + + .. tab:: Windows + + .. code-block:: console + + C:\> py -m pip list + Package Version Editable project location + ---------------- -------- ---------------------------------------- + pip 21.2.4 + pip-test-package 0.1.1 C:\Users\You\.venv\src\pip-test-package + setuptools 57.4.0 + wheel 0.36.2 + +The json format outputs an additional ``editable_project_location`` field. + + .. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip list --format=json | python -m json.tool + [ + { + "name": "pip", + "version": "21.2.4", + }, + { + "name": "pip-test-package", + "version": "0.1.1", + "editable_project_location": "/home/you/.venv/src/pip-test-package" + }, + { + "name": "setuptools", + "version": "57.4.0" + }, + { + "name": "wheel", + "version": "0.36.2" + } + ] + + .. tab:: Windows + + .. code-block:: console + + C:\> py -m pip list --format=json | py -m json.tool + [ + { + "name": "pip", + "version": "21.2.4", + }, + { + "name": "pip-test-package", + "version": "0.1.1", + "editable_project_location": "C:\Users\You\.venv\src\pip-test-package" + }, + { + "name": "setuptools", + "version": "57.4.0" + }, + { + "name": "wheel", + "version": "0.36.2" + } + ] + +.. note:: + + Contrary to the ``freeze`` comand, ``pip list --format=freeze`` will not + report editable install information, but the version of the package at the + time it was installed. diff --git a/docs/html/index.md b/docs/html/index.md index 4b565b9a3..34a017449 100644 --- a/docs/html/index.md +++ b/docs/html/index.md @@ -14,6 +14,7 @@ getting-started installation user_guide topics/index +reference/index cli/index ``` diff --git a/docs/html/reference/index.md b/docs/html/reference/index.md new file mode 100644 index 000000000..13e57b2a4 --- /dev/null +++ b/docs/html/reference/index.md @@ -0,0 +1,10 @@ +# Reference + +Reference provides information about various file formats, interfaces and +interoperability standards that pip utilises/implements. + +```{toctree} +:titlesonly: + +requirements-file-format +``` diff --git a/docs/html/reference/index.rst b/docs/html/reference/index.rst deleted file mode 100644 index 5e81105c9..000000000 --- a/docs/html/reference/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -:orphan: - -.. meta:: - - :http-equiv=refresh: 3; url=../cli/ - -This page has moved -=================== - -You should be redirected automatically in 3 seconds. If that didn't -work, here's a link: :doc:`../cli/index` diff --git a/docs/html/reference/requirements-file-format.md b/docs/html/reference/requirements-file-format.md new file mode 100644 index 000000000..d05f1acca --- /dev/null +++ b/docs/html/reference/requirements-file-format.md @@ -0,0 +1,147 @@ +(requirements-file-format)= + +# Requirements File Format + +Requirements files serve as a list of items to be installed by pip, when +using {ref}`pip install`. Files that use this format are often called +"pip requirements.txt files", since `requirements.txt` is usually what +these files are named (although, that is not a requirement). + +```{note} +The requirements file format is closely tied to a number of internal details of +pip (e.g., pip's command line options). The basic format is relatively stable +and portable but the full syntax, as described here, is only intended for +consumption by pip, and other tools should take that into account before using +it for their own purposes. +``` + +## Structure + +Each line of the requirements file indicates something to be installed, +or arguments to {ref}`pip install`. The following forms are supported: + +- `[[--option]...]` +- `<requirement specifier> [; markers] [[--option]...]` +- `<archive url/path>` +- `[-e] <local project path>` +- `[-e] <vcs project url>` + +For details on requirement specifiers, see {ref}`Requirement Specifiers`. For +examples of all these forms, see {ref}`pip install Examples`. + +### Encoding + +Requirements files are `utf-8` encoding by default and also support +{pep}`263` style comments to change the encoding (i.e. +`# -*- coding: <encoding name> -*-`). + +### Line continuations + +A line ending in an unescaped `\` is treated as a line continuation +and the newline following it is effectively ignored. + +### Comments + +A line that begins with `#` is treated as a comment and ignored. Whitespace +followed by a `#` causes the `#` and the remainder of the line to be +treated as a comment. + +Comments are stripped _after_ line continuations are processed. + +## Supported options + +Requirements files only supports certain pip install options, which are listed +below. + +### Global options + +The following options have an effect on the _entire_ `pip install` run, and +must be specified on their individual lines. + +```{eval-rst} +.. pip-requirements-file-options-ref-list:: +``` + +````{admonition} Example +To specify {ref}`--pre <install_--pre>`, {ref}`--no-index <install_--no-index>` +and two {ref}`--find-links <install_--find-links>` locations: + +``` +--pre +--no-index +--find-links /my/local/archives +--find-links http://some.archives.com/archives +``` +```` + +### Per-requirement options + +The options which can be applied to individual requirements are: + +- {ref}`--install-option <install_--install-option>` +- {ref}`--global-option <install_--global-option>` +- `--hash` (for {ref}`Hash-Checking mode`) + +If you wish, you can refer to other requirements files, like this: + +``` +-r more_requirements.txt +``` + +You can also refer to {ref}`constraints files <Constraints Files>`, like this: + +``` +-c some_constraints.txt +``` + +## Using environment variables + +```{versionadded} 10.0 + +``` + +pip supports the use of environment variables inside the +requirements file. + +You have to use the POSIX format for variable names including brackets around +the uppercase name as shown in this example: `${API_TOKEN}`. pip will attempt +to find the corresponding environment variable defined on the host system at +runtime. + +```{note} +There is no support for other variable expansion syntaxes such as `$VARIABLE` +and `%VARIABLE%`. +``` + +You can now store sensitive data (tokens, keys, etc.) in environment variables +and only specify the variable name for your requirements, letting pip lookup +the value at runtime. This approach aligns with the commonly used +[12-factor configuration pattern](https://12factor.net/config). + +## Example + +``` +###### Requirements without Version Specifiers ###### +pytest +pytest-cov +beautifulsoup4 + +###### Requirements with Version Specifiers ###### +# See https://www.python.org/dev/peps/pep-0440/#version-specifiers +docopt == 0.6.1 # Version Matching. Must be version 0.6.1 +keyring >= 4.1.1 # Minimum version 4.1.1 +coverage != 3.5 # Version Exclusion. Anything except version 3.5 +Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* + +###### Refer to other requirements files ###### +-r other-requirements.txt + +###### A particular file ###### +./downloads/numpy-1.9.2-cp34-none-win32.whl +http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl + +###### Additional Requirements without Version Specifiers ###### +# Same as 1st section, just here to show that you can put things in any order. +rejected +green +``` diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 2d0597092..059fd7cdc 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -113,7 +113,7 @@ installed using :ref:`pip install` like so: py -m pip install -r requirements.txt -Details on the format of the files are here: :ref:`Requirements File Format`. +Details on the format of the files are here: :ref:`requirements-file-format`. Logically, a Requirements file is just a list of :ref:`pip install` arguments placed in a file. Note that you should not rely on the items in the file being @@ -185,7 +185,7 @@ not by discovering ``requirements.txt`` files embedded in projects. See also: -* :ref:`Requirements File Format` +* :ref:`requirements-file-format` * :ref:`pip freeze` * `"setup.py vs requirements.txt" (an article by Donald Stufft) <https://caremad.io/2013/07/setup-vs-requirement/>`_ diff --git a/news/0b3269eb-560c-4b8e-8553-9fe205dc62ec.trivial.rst b/news/0b3269eb-560c-4b8e-8553-9fe205dc62ec.trivial.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/news/0b3269eb-560c-4b8e-8553-9fe205dc62ec.trivial.rst diff --git a/news/10249.feature.rst b/news/10249.feature.rst new file mode 100644 index 000000000..baf8395b1 --- /dev/null +++ b/news/10249.feature.rst @@ -0,0 +1,4 @@ +Support `PEP 610 <https://www.python.org/dev/peps/pep-0610/>`_ to detect +editable installs in ``pip freeze`` and ``pip list``. The ``pip list`` column output +has a new ``Editable project location`` column, and the JSON output has a new +``editable_project_location`` field. diff --git a/news/10269.bugfix.rst b/news/10269.bugfix.rst new file mode 100644 index 000000000..45aee70d8 --- /dev/null +++ b/news/10269.bugfix.rst @@ -0,0 +1,3 @@ +Fix the auth credential cache to allow for the case in which +the index url contains the username, but the password comes +from an external source, such as keyring. diff --git a/news/10361.trivial.rst b/news/10361.trivial.rst new file mode 100644 index 000000000..05b3cb10f --- /dev/null +++ b/news/10361.trivial.rst @@ -0,0 +1 @@ +Added an explicit warning when pip is unable to parse git version. diff --git a/news/10410.feature.rst b/news/10410.feature.rst new file mode 100644 index 000000000..e3bfdc83b --- /dev/null +++ b/news/10410.feature.rst @@ -0,0 +1,3 @@ +``pip freeze`` will now always fallback to reporting the editable project +location when it encounters a VCS error while analyzing an editable +requirement. Before, it sometimes reported the requirement as non-editable. diff --git a/news/10418.trivial.rst b/news/10418.trivial.rst new file mode 100644 index 000000000..d427a75f8 --- /dev/null +++ b/news/10418.trivial.rst @@ -0,0 +1 @@ +Make _load_file log become verbose instead of debug. diff --git a/news/10422.feature.rst b/news/10422.feature.rst new file mode 100644 index 000000000..d4d6d824c --- /dev/null +++ b/news/10422.feature.rst @@ -0,0 +1 @@ +``pip show`` now sorts ``Requires`` and ``Required-By`` alphabetically. diff --git a/news/118e193d-d2f2-4e70-9767-ba5dddf7d263.trivial.rst b/news/118e193d-d2f2-4e70-9767-ba5dddf7d263.trivial.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/news/118e193d-d2f2-4e70-9767-ba5dddf7d263.trivial.rst diff --git a/news/3332a3c1-fd62-4d5a-8bc6-459fd677ca70.trivial.rst b/news/3332a3c1-fd62-4d5a-8bc6-459fd677ca70.trivial.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/news/3332a3c1-fd62-4d5a-8bc6-459fd677ca70.trivial.rst diff --git a/news/52ff07b1-69ed-4bbe-a3be-913e0e247cee.trivial.rst b/news/52ff07b1-69ed-4bbe-a3be-913e0e247cee.trivial.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/news/52ff07b1-69ed-4bbe-a3be-913e0e247cee.trivial.rst diff --git a/news/569d0d85-be83-40ab-9229-1a6327060a3f.trivial.rst b/news/569d0d85-be83-40ab-9229-1a6327060a3f.trivial.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/news/569d0d85-be83-40ab-9229-1a6327060a3f.trivial.rst diff --git a/news/6a2297c8-25c3-4c79-b856-03f0184af36f.trivial.rst b/news/6a2297c8-25c3-4c79-b856-03f0184af36f.trivial.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/news/6a2297c8-25c3-4c79-b856-03f0184af36f.trivial.rst diff --git a/news/77150b20-02ed-411a-ad49-90afdc0a4b53.trivial.rst b/news/77150b20-02ed-411a-ad49-90afdc0a4b53.trivial.rst new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/news/77150b20-02ed-411a-ad49-90afdc0a4b53.trivial.rst diff --git a/news/9349.feature.rst b/news/9349.feature.rst new file mode 100644 index 000000000..7d8744bc6 --- /dev/null +++ b/news/9349.feature.rst @@ -0,0 +1 @@ +Add a ``--debug`` flag, to enable a mode that doesn't log errors and propagates them to the top level instead. This is primarily to aid with debugging pip's crashes. diff --git a/news/9498.feature.rst b/news/9498.feature.rst new file mode 100644 index 000000000..0682915e4 --- /dev/null +++ b/news/9498.feature.rst @@ -0,0 +1 @@ +If a host is explicitly specified as trusted by the user (via the --trusted-host option), cache HTTP responses from it in addition to HTTPS ones. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 16a8faca8..5932eedc2 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -1,5 +1,6 @@ """Base Command class, and related routines""" +import functools import logging import logging.config import optparse @@ -7,7 +8,7 @@ import os import sys import traceback from optparse import Values -from typing import List, Optional, Tuple +from typing import Any, Callable, List, Optional, Tuple from pip._internal.cli import cmdoptions from pip._internal.cli.command_context import CommandContextMixIn @@ -169,46 +170,60 @@ class Command(CommandContextMixIn): "This will become an error in pip 21.0." ) + def intercepts_unhandled_exc( + run_func: Callable[..., int] + ) -> Callable[..., int]: + @functools.wraps(run_func) + def exc_logging_wrapper(*args: Any) -> int: + try: + status = run_func(*args) + assert isinstance(status, int) + return status + except PreviousBuildDirError as exc: + logger.critical(str(exc)) + logger.debug("Exception information:", exc_info=True) + + return PREVIOUS_BUILD_DIR_ERROR + except ( + InstallationError, + UninstallationError, + BadCommand, + NetworkConnectionError, + ) as exc: + logger.critical(str(exc)) + logger.debug("Exception information:", exc_info=True) + + return ERROR + except CommandError as exc: + logger.critical("%s", exc) + logger.debug("Exception information:", exc_info=True) + + return ERROR + except BrokenStdoutLoggingError: + # Bypass our logger and write any remaining messages to + # stderr because stdout no longer works. + print("ERROR: Pipe to stdout was broken", file=sys.stderr) + if level_number <= logging.DEBUG: + traceback.print_exc(file=sys.stderr) + + return ERROR + except KeyboardInterrupt: + logger.critical("Operation cancelled by user") + logger.debug("Exception information:", exc_info=True) + + return ERROR + except BaseException: + logger.critical("Exception:", exc_info=True) + + return UNKNOWN_ERROR + + return exc_logging_wrapper + try: - status = self.run(options, args) - assert isinstance(status, int) - return status - except PreviousBuildDirError as exc: - logger.critical(str(exc)) - logger.debug("Exception information:", exc_info=True) - - return PREVIOUS_BUILD_DIR_ERROR - except ( - InstallationError, - UninstallationError, - BadCommand, - NetworkConnectionError, - ) as exc: - logger.critical(str(exc)) - logger.debug("Exception information:", exc_info=True) - - return ERROR - except CommandError as exc: - logger.critical("%s", exc) - logger.debug("Exception information:", exc_info=True) - - return ERROR - except BrokenStdoutLoggingError: - # Bypass our logger and write any remaining messages to stderr - # because stdout no longer works. - print("ERROR: Pipe to stdout was broken", file=sys.stderr) - if level_number <= logging.DEBUG: - traceback.print_exc(file=sys.stderr) - - return ERROR - except KeyboardInterrupt: - logger.critical("Operation cancelled by user") - logger.debug("Exception information:", exc_info=True) - - return ERROR - except BaseException: - logger.critical("Exception:", exc_info=True) - - return UNKNOWN_ERROR + if not options.debug_mode: + run = intercepts_unhandled_exc(self.run) + else: + run = self.run + return run(options, args) finally: self.handle_pip_version_check(options) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index b4f0f83c6..e24037800 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -151,6 +151,18 @@ help_: Callable[..., Option] = partial( help="Show help.", ) +debug_mode: Callable[..., Option] = partial( + Option, + "--debug", + dest="debug_mode", + action="store_true", + default=False, + help=( + "Let unhandled exceptions propagate outside the main subroutine, " + "instead of logging them to stderr." + ), +) + isolated_mode: Callable[..., Option] = partial( Option, "--isolated", @@ -974,6 +986,7 @@ general_group: Dict[str, Any] = { "name": "General Options", "options": [ help_, + debug_mode, isolated_mode, require_virtualenv, verbose, diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index abe6ef2fc..75d8dd465 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -14,7 +14,8 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.metadata import BaseDistribution, get_environment from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.session import PipSession -from pip._internal.utils.misc import stdlib_pkgs, tabulate, write_output +from pip._internal.utils.compat import stdlib_pkgs +from pip._internal.utils.misc import tabulate, write_output from pip._internal.utils.parallel import map_multithread if TYPE_CHECKING: @@ -302,19 +303,22 @@ def format_for_columns( Convert the package data into something usable by output_package_listing_columns. """ + header = ["Package", "Version"] + running_outdated = options.outdated - # Adjust the header for the `pip list --outdated` case. if running_outdated: - header = ["Package", "Version", "Latest", "Type"] - else: - header = ["Package", "Version"] + header.extend(["Latest", "Type"]) - data = [] - if options.verbose >= 1 or any(x.editable for x in pkgs): + has_editables = any(x.editable for x in pkgs) + if has_editables: + header.append("Editable project location") + + if options.verbose >= 1: header.append("Location") if options.verbose >= 1: header.append("Installer") + data = [] for proj in pkgs: # if we're working on the 'outdated' list, separate out the # latest_version and type @@ -324,7 +328,10 @@ def format_for_columns( row.append(str(proj.latest_version)) row.append(proj.latest_filetype) - if options.verbose >= 1 or proj.editable: + if has_editables: + row.append(proj.editable_project_location or "") + + if options.verbose >= 1: row.append(proj.location or "") if options.verbose >= 1: row.append(proj.installer) @@ -347,5 +354,8 @@ def format_for_json(packages: "_ProcessedDists", options: Values) -> str: if options.outdated: info["latest_version"] = str(dist.latest_version) info["latest_filetype"] = dist.latest_filetype + editable_project_location = dist.editable_project_location + if editable_project_location: + info["editable_project_location"] = editable_project_location data.append(info) return json.dumps(data) diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 0bbe1209a..872292a2b 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -113,13 +113,13 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]: if missing: logger.warning("Package(s) not found: %s", ", ".join(missing)) - def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]: - return [ + def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[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()} - ] + ) def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]: try: @@ -155,6 +155,9 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]: except KeyError: continue + requires = sorted((req.name for req in dist.iter_dependencies()), key=str.lower) + required_by = sorted(_get_requiring_packages(dist), key=str.lower) + try: entry_points_text = dist.read_text("entry_points.txt") entry_points = entry_points_text.splitlines(keepends=False) @@ -173,8 +176,8 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]: name=dist.raw_name, version=str(dist.version), location=dist.location or "", - requires=[req.name for req in dist.iter_dependencies()], - required_by=_get_requiring_packages(dist), + requires=requires, + required_by=required_by, installer=dist.installer, metadata_version=dist.metadata_version or "", classifiers=metadata.get_all("Classifier", []), diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 3b54c0d69..4c3a362fd 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -13,7 +13,6 @@ Some terminology: import configparser import locale -import logging import os import sys from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple @@ -24,6 +23,7 @@ from pip._internal.exceptions import ( ) from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS +from pip._internal.utils.logging import getLogger from pip._internal.utils.misc import ensure_dir, enum RawConfigParser = configparser.RawConfigParser # Shorthand @@ -43,7 +43,7 @@ kinds = enum( OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE -logger = logging.getLogger(__name__) +logger = getLogger(__name__) # NOTE: Maybe use the optionx attribute to normalize keynames. @@ -250,7 +250,7 @@ class Configuration: self._parsers[variant].append((fname, parser)) def _load_file(self, variant: Kind, fname: str) -> RawConfigParser: - logger.debug("For variant '%s', will try loading '%s'", variant, fname) + logger.verbose("For variant '%s', will try loading '%s'", variant, fname) parser = self._construct_parser(fname) for section in parser.sections(): diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index e1b229d1f..7eacd00e2 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -24,7 +24,9 @@ from pip._internal.models.direct_url import ( DirectUrl, DirectUrlValidationError, ) -from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. +from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here. +from pip._internal.utils.egg_link import egg_link_path_from_sys_path +from pip._internal.utils.urls import url_to_path if TYPE_CHECKING: from typing import Protocol @@ -74,6 +76,28 @@ class BaseDistribution(Protocol): raise NotImplementedError() @property + def editable_project_location(self) -> Optional[str]: + """The project location for editable distributions. + + This is the directory where pyproject.toml or setup.py is located. + None if the distribution is not installed in editable mode. + """ + # TODO: this property is relatively costly to compute, memoize it ? + direct_url = self.direct_url + if direct_url: + if direct_url.is_local_editable(): + return url_to_path(direct_url.url) + else: + # Search for an .egg-link file by walking sys.path, as it was + # done before by dist_is_editable(). + egg_link_path = egg_link_path_from_sys_path(self.raw_name) + if egg_link_path: + # TODO: get project location from second line of egg_link file + # (https://github.com/pypa/pip/issues/10243) + return self.location + return None + + @property def info_directory(self) -> Optional[str]: """Location of the .[egg|dist]-info directory. @@ -129,7 +153,7 @@ class BaseDistribution(Protocol): @property def editable(self) -> bool: - raise NotImplementedError() + return bool(self.editable_project_location) @property def local(self) -> bool: diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 75fd3518f..35e63f2c5 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -70,10 +70,6 @@ class Distribution(BaseDistribution): return get_installer(self._dist) @property - def editable(self) -> bool: - return misc.dist_is_editable(self._dist) - - @property def local(self) -> bool: return misc.dist_is_local(self._dist) diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index d652ce153..92060d45d 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -215,3 +215,6 @@ class DirectUrl: def to_json(self) -> str: return json.dumps(self.to_dict(), sort_keys=True) + + def is_local_editable(self) -> bool: + return isinstance(self.info, DirInfo) and self.info.editable diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index ee27fb67a..ca42798bd 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -179,9 +179,16 @@ class MultiDomainBasicAuth(AuthBase): # Try to get credentials from original url username, password = self._get_new_credentials(original_url) - # If credentials not found, use any stored credentials for this netloc - if username is None and password is None: - username, password = self.passwords.get(netloc, (None, None)) + # If credentials not found, use any stored credentials for this netloc. + # Do this if either the username or the password is missing. + # This accounts for the situation in which the user has specified + # the username in the index url, but the password comes from keyring. + if (username is None or password is None) and netloc in self.passwords: + un, pw = self.passwords[netloc] + # It is possible that the cached credentials are for a different username, + # in which case the cache should be ignored. + if username is None or username == un: + username, password = un, pw if username is not None or password is not None: # Convert the username and password if they're None, so that diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index fe1cf717e..e2916ca81 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -358,8 +358,15 @@ class PipSession(requests.Session): if host_port not in self.pip_trusted_origins: self.pip_trusted_origins.append(host_port) + self.mount( + build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter + ) self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter) if not host_port[1]: + self.mount( + build_url_from_netloc(host, scheme="http") + ":", + self._trusted_host_adapter, + ) # Mount wildcard ports for the same host. self.mount(build_url_from_netloc(host) + ":", self._trusted_host_adapter) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 518e95c8b..456554085 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -1,19 +1,8 @@ import collections import logging import os -from typing import ( - Container, - Dict, - Iterable, - Iterator, - List, - NamedTuple, - Optional, - Set, - Union, -) +from typing import Container, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set -from pip._vendor.packaging.requirements import Requirement from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version @@ -30,8 +19,7 @@ logger = logging.getLogger(__name__) class _EditableInfo(NamedTuple): - requirement: Optional[str] - editable: bool + requirement: str comments: List[str] @@ -164,21 +152,12 @@ def _format_as_name_version(dist: BaseDistribution) -> str: def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: """ - Compute and return values (req, editable, comments) for use in + Compute and return values (req, comments) for use in FrozenRequirement.from_dist(). """ - if not dist.editable: - return _EditableInfo(requirement=None, editable=False, comments=[]) - if dist.location is None: - display = _format_as_name_version(dist) - logger.warning("Editable requirement not found on disk: %s", display) - return _EditableInfo( - requirement=None, - editable=True, - comments=[f"# Editable install not found ({display})"], - ) - - location = os.path.normcase(os.path.abspath(dist.location)) + editable_project_location = dist.editable_project_location + assert editable_project_location + location = os.path.normcase(os.path.abspath(editable_project_location)) from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs @@ -193,7 +172,6 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: ) return _EditableInfo( requirement=location, - editable=True, comments=[f"# Editable install with no version control ({display})"], ) @@ -205,21 +183,18 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: display = _format_as_name_version(dist) return _EditableInfo( requirement=location, - editable=True, comments=[f"# Editable {vcs_name} install with no remote ({display})"], ) except RemoteNotValidError as ex: display = _format_as_name_version(dist) return _EditableInfo( requirement=location, - editable=True, comments=[ f"# Editable {vcs_name} install ({display}) with either a deleted " f"local remote or invalid URI:", f"# '{ex.url}'", ], ) - except BadCommand: logger.warning( "cannot determine version of editable source in %s " @@ -227,22 +202,16 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: location, vcs_backend.name, ) - return _EditableInfo(requirement=None, editable=True, comments=[]) - + return _EditableInfo(requirement=location, comments=[]) except InstallationError as exc: - logger.warning( - "Error when trying to get requirement for VCS system %s, " - "falling back to uneditable format", - exc, - ) + logger.warning("Error when trying to get requirement for VCS system %s", exc) else: - return _EditableInfo(requirement=req, editable=True, comments=[]) + return _EditableInfo(requirement=req, comments=[]) logger.warning("Could not determine repository location of %s", location) return _EditableInfo( - requirement=None, - editable=False, + requirement=location, comments=["## !! Could not determine repository location"], ) @@ -251,7 +220,7 @@ class FrozenRequirement: def __init__( self, name: str, - req: Union[str, Requirement], + req: str, editable: bool, comments: Iterable[str] = (), ) -> None: @@ -263,19 +232,18 @@ class FrozenRequirement: @classmethod def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement": - # TODO `get_requirement_info` is taking care of editable requirements. - # TODO This should be refactored when we will add detection of - # editable that provide .dist-info metadata. - req, editable, comments = _get_editable_info(dist) - if req is None and not editable: - # if PEP 610 metadata is present, attempt to use it + editable = dist.editable + if editable: + req, comments = _get_editable_info(dist) + else: + comments = [] direct_url = dist.direct_url if direct_url: + # if PEP 610 metadata is present, use it req = direct_url_as_pep440_direct_reference(direct_url, dist.raw_name) - comments = [] - if req is None: - # name==version requirement - req = _format_as_name_version(dist) + else: + # name==version requirement + req = _format_as_name_version(dist) return cls(dist.raw_name, req, editable, comments=comments) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 075317622..e191b1343 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -38,7 +38,6 @@ from zipfile import ZipFile, ZipInfo from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.six import ensure_str, ensure_text, reraise from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -220,8 +219,7 @@ def _normalized_outrows( # For additional background, see-- # https://github.com/pypa/pip/issues/5868 return sorted( - (ensure_str(record_path, encoding="utf-8"), hash_, str(size)) - for record_path, hash_, size in outrows + (record_path, hash_, str(size)) for record_path, hash_, size in outrows ) @@ -242,11 +240,6 @@ def _fs_to_record_path(path: str, relative_to: Optional[str] = None) -> RecordPa return cast("RecordPath", path) -def _parse_record_path(record_column: str) -> RecordPath: - p = ensure_text(record_column, encoding="utf-8") - return cast("RecordPath", p) - - def get_csv_rows_for_installed( old_csv_rows: List[List[str]], installed: Dict[RecordPath, RecordPath], @@ -262,7 +255,7 @@ def get_csv_rows_for_installed( for row in old_csv_rows: if len(row) > 3: logger.warning("RECORD line has more than three elements: %s", row) - old_record_path = _parse_record_path(row[0]) + old_record_path = cast("RecordPath", row[0]) new_record_path = installed.pop(old_record_path, old_record_path) if new_record_path in changed: digest, length = rehash(_record_to_fs_path(new_record_path)) @@ -483,14 +476,6 @@ def _install_wheel( if modified: changed.add(_fs_to_record_path(destfile)) - def all_paths() -> Iterable[RecordPath]: - names = wheel_zip.namelist() - # If a flag is set, names may be unicode in Python 2. We convert to - # text explicitly so these are valid for lookup in RECORD. - decoded_names = map(ensure_text, names) - for name in decoded_names: - yield cast("RecordPath", name) - def is_dir_path(path: RecordPath) -> bool: return path.endswith("/") @@ -518,12 +503,7 @@ def _install_wheel( def data_scheme_file_maker( zip_file: ZipFile, scheme: Scheme ) -> Callable[[RecordPath], "File"]: - scheme_paths = {} - for key in SCHEME_KEYS: - encoded_key = ensure_text(key) - scheme_paths[encoded_key] = ensure_text( - getattr(scheme, key), encoding=sys.getfilesystemencoding() - ) + scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS} def make_data_scheme_file(record_path: RecordPath) -> "File": normed_path = os.path.normpath(record_path) @@ -556,14 +536,11 @@ def _install_wheel( def is_data_scheme_path(path: RecordPath) -> bool: return path.split("/", 1)[0].endswith(".data") - paths = all_paths() + paths = cast(List[RecordPath], wheel_zip.namelist()) file_paths = filterfalse(is_dir_path, paths) root_scheme_paths, data_scheme_paths = partition(is_data_scheme_path, file_paths) - make_root_scheme_file = root_scheme_file_maker( - wheel_zip, - ensure_text(lib_dir, encoding=sys.getfilesystemencoding()), - ) + make_root_scheme_file = root_scheme_file_maker(wheel_zip, lib_dir) files: Iterator[File] = map(make_root_scheme_file, root_scheme_paths) def is_script_scheme_path(path: RecordPath) -> bool: @@ -722,9 +699,8 @@ def _install_wheel( record_path = os.path.join(dest_info_dir, "RECORD") with _generate_file(record_path, **csv_io_kwargs("w")) as record_file: - # The type mypy infers for record_file is different for Python 3 - # (typing.IO[Any]) and Python 2 (typing.BinaryIO). We explicitly - # cast to typing.IO[str] as a workaround. + # Explicitly cast to typing.IO[str] as a workaround for the mypy error: + # "writer" has incompatible type "BinaryIO"; expected "_Writer" writer = csv.writer(cast("IO[str]", record_file)) writer.writerows(_normalized_outrows(rows)) @@ -735,7 +711,7 @@ def req_error_context(req_description: str) -> Iterator[None]: yield except InstallationError as e: message = "For req: {}. {}".format(req_description, e.args[0]) - reraise(InstallationError, InstallationError(message), sys.exc_info()[2]) + raise InstallationError(message) from e def install_wheel( diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index ef7352f7b..779e93b44 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -12,12 +12,12 @@ from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import UninstallationError from pip._internal.locations import get_bin_prefix, get_bin_user from pip._internal.utils.compat import WINDOWS +from pip._internal.utils.egg_link import egg_link_path_from_location from pip._internal.utils.logging import getLogger, indent_log from pip._internal.utils.misc import ( ask, dist_in_usersite, dist_is_local, - egg_link_path, is_local, normalize_path, renames, @@ -459,7 +459,7 @@ class UninstallPathSet: return cls(dist) paths_to_remove = cls(dist) - develop_egg_link = egg_link_path(dist) + develop_egg_link = egg_link_path_from_location(dist.project_name) develop_egg_link_egg_info = "{}.egg-info".format( pkg_resources.to_filename(dist.project_name) ) diff --git a/src/pip/_internal/utils/egg_link.py b/src/pip/_internal/utils/egg_link.py new file mode 100644 index 000000000..9e0da8d2d --- /dev/null +++ b/src/pip/_internal/utils/egg_link.py @@ -0,0 +1,75 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + +import os +import re +import sys +from typing import Optional + +from pip._internal.locations import site_packages, user_site +from pip._internal.utils.virtualenv import ( + running_under_virtualenv, + virtualenv_no_global, +) + +__all__ = [ + "egg_link_path_from_sys_path", + "egg_link_path_from_location", +] + + +def _egg_link_name(raw_name: str) -> str: + """ + Convert a Name metadata value to a .egg-link name, by applying + the same substitution as pkg_resources's safe_name function. + Note: we cannot use canonicalize_name because it has a different logic. + """ + return re.sub("[^A-Za-z0-9.]+", "-", raw_name) + ".egg-link" + + +def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]: + """ + Look for a .egg-link file for project name, by walking sys.path. + """ + egg_link_name = _egg_link_name(raw_name) + for path_item in sys.path: + egg_link = os.path.join(path_item, egg_link_name) + if os.path.isfile(egg_link): + return egg_link + return None + + +def egg_link_path_from_location(raw_name: str) -> Optional[str]: + """ + Return the path for the .egg-link file if it exists, otherwise, None. + + There's 3 scenarios: + 1) not in a virtualenv + try to find in site.USER_SITE, then site_packages + 2) in a no-global virtualenv + try to find in site_packages + 3) in a yes-global virtualenv + try to find in site_packages, then site.USER_SITE + (don't look in global location) + + For #1 and #3, there could be odd cases, where there's an egg-link in 2 + locations. + + This method will just return the first one found. + """ + sites = [] + if running_under_virtualenv(): + sites.append(site_packages) + if not virtualenv_no_global() and user_site: + sites.append(user_site) + else: + if user_site: + sites.append(user_site) + sites.append(site_packages) + + egg_link_name = _egg_link_name(raw_name) + for site in sites: + egglink = os.path.join(site, egg_link_name) + if os.path.isfile(egglink): + return egglink + return None diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 8b1081f29..d3e9053ef 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -20,7 +20,6 @@ from typing import ( Any, BinaryIO, Callable, - Container, ContextManager, Iterable, Iterator, @@ -39,11 +38,9 @@ from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed from pip import __version__ from pip._internal.exceptions import CommandError from pip._internal.locations import get_major_minor_version, site_packages, user_site -from pip._internal.utils.compat import WINDOWS, stdlib_pkgs -from pip._internal.utils.virtualenv import ( - running_under_virtualenv, - virtualenv_no_global, -) +from pip._internal.utils.compat import WINDOWS +from pip._internal.utils.egg_link import egg_link_path_from_location +from pip._internal.utils.virtualenv import running_under_virtualenv __all__ = [ "rmtree", @@ -357,46 +354,6 @@ def dist_in_site_packages(dist: Distribution) -> bool: return dist_location(dist).startswith(normalize_path(site_packages)) -def dist_is_editable(dist: Distribution) -> bool: - """ - Return True if given Distribution is an editable install. - """ - for path_item in sys.path: - egg_link = os.path.join(path_item, dist.project_name + ".egg-link") - if os.path.isfile(egg_link): - return True - return False - - -def get_installed_distributions( - local_only: bool = True, - skip: Container[str] = stdlib_pkgs, - include_editables: bool = True, - editables_only: bool = False, - user_only: bool = False, - paths: Optional[List[str]] = None, -) -> List[Distribution]: - """Return a list of installed Distribution objects. - - Left for compatibility until direct pkg_resources uses are refactored out. - """ - from pip._internal.metadata import get_default_environment, get_environment - from pip._internal.metadata.pkg_resources import Distribution as _Dist - - if paths is None: - env = get_default_environment() - else: - env = get_environment(paths) - dists = env.iter_installed_distributions( - local_only=local_only, - skip=skip, - include_editables=include_editables, - editables_only=editables_only, - user_only=user_only, - ) - return [cast(_Dist, dist)._dist for dist in dists] - - def get_distribution(req_name: str) -> Optional[Distribution]: """Given a requirement name, return the installed Distribution object. @@ -414,41 +371,6 @@ def get_distribution(req_name: str) -> Optional[Distribution]: return cast(_Dist, dist)._dist -def egg_link_path(dist: Distribution) -> Optional[str]: - """ - Return the path for the .egg-link file if it exists, otherwise, None. - - There's 3 scenarios: - 1) not in a virtualenv - try to find in site.USER_SITE, then site_packages - 2) in a no-global virtualenv - try to find in site_packages - 3) in a yes-global virtualenv - try to find in site_packages, then site.USER_SITE - (don't look in global location) - - For #1 and #3, there could be odd cases, where there's an egg-link in 2 - locations. - - This method will just return the first one found. - """ - sites = [] - if running_under_virtualenv(): - sites.append(site_packages) - if not virtualenv_no_global() and user_site: - sites.append(user_site) - else: - if user_site: - sites.append(user_site) - sites.append(site_packages) - - for site in sites: - egglink = os.path.join(site, dist.project_name) + ".egg-link" - if os.path.isfile(egglink): - return egglink - return None - - def dist_location(dist: Distribution) -> str: """ Get the site-packages location of this distribution. Generally @@ -458,7 +380,7 @@ def dist_location(dist: Distribution) -> str: The returned location is normalized (in particular, with symlinks removed). """ - egg_link = egg_link_path(dist) + egg_link = egg_link_path_from_location(dist.project_name) if egg_link: return normalize_path(egg_link) return normalize_path(dist.location) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 3186553e7..7a78ad12d 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -94,6 +94,7 @@ class Git(VersionControl): version = self.run_command(["version"], show_stdout=False, stdout_only=True) match = GIT_VERSION_REGEX.match(version) if not match: + logger.warning("Can't parse git version: %s", version) return () return tuple(int(c) for c in match.groups()) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 1938a109a..d41ec7f49 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -622,11 +622,8 @@ def make_wheel_with_python_requires(script, package_name, python_requires): return package_dir / "dist" / file_name -def test_download__python_version_used_for_python_requires( - script, - data, - with_wheel, -): +@pytest.mark.usefixtures("with_wheel") +def test_download__python_version_used_for_python_requires(script, data): """ Test that --python-version is used for the Requires-Python check. """ @@ -664,10 +661,8 @@ def test_download__python_version_used_for_python_requires( script.pip(*args) # no exception -def test_download_ignore_requires_python_dont_fail_with_wrong_python( - script, - with_wheel, -): +@pytest.mark.usefixtures("with_wheel") +def test_download_ignore_requires_python_dont_fail_with_wrong_python(script): """ Test that --ignore-requires-python ignores Requires-Python check. """ diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index d361bc540..6756944ff 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -7,6 +7,7 @@ from doctest import ELLIPSIS, OutputChecker import pytest from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.models.direct_url import DirectUrl from tests.lib import ( _create_test_package, _create_test_package_with_srcdir, @@ -19,6 +20,7 @@ from tests.lib import ( path_to_url, wheel, ) +from tests.lib.direct_url import get_created_direct_url_path distribute_re = re.compile("^distribute==[0-9.]+\n", re.MULTILINE) @@ -99,7 +101,8 @@ def test_exclude_and_normalization(script, tmpdir): assert "Normalizable_Name" not in result.stdout -def test_freeze_multiple_exclude_with_all(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_freeze_multiple_exclude_with_all(script): result = script.pip("freeze", "--all") assert "pip==" in result.stdout assert "wheel==" in result.stdout @@ -164,7 +167,7 @@ def test_freeze_with_invalid_names(script): @pytest.mark.git -def test_freeze_editable_not_vcs(script, tmpdir): +def test_freeze_editable_not_vcs(script): """ Test an editable install that is not version controlled. """ @@ -189,7 +192,7 @@ def test_freeze_editable_not_vcs(script, tmpdir): @pytest.mark.git -def test_freeze_editable_git_with_no_remote(script, tmpdir, deprecated_python): +def test_freeze_editable_git_with_no_remote(script, deprecated_python): """ Test an editable Git install with no remote url. """ @@ -214,7 +217,7 @@ def test_freeze_editable_git_with_no_remote(script, tmpdir, deprecated_python): @need_svn -def test_freeze_svn(script, tmpdir): +def test_freeze_svn(script): """Test freezing a svn checkout""" checkout_path = _create_test_package(script, vcs="svn") @@ -237,7 +240,7 @@ def test_freeze_svn(script, tmpdir): run=True, strict=True, ) -def test_freeze_exclude_editable(script, tmpdir): +def test_freeze_exclude_editable(script): """ Test excluding editable from freezing list. """ @@ -270,7 +273,7 @@ def test_freeze_exclude_editable(script, tmpdir): @pytest.mark.git -def test_freeze_git_clone(script, tmpdir): +def test_freeze_git_clone(script): """ Test freezing a Git clone. """ @@ -328,7 +331,7 @@ def test_freeze_git_clone(script, tmpdir): @pytest.mark.git -def test_freeze_git_clone_srcdir(script, tmpdir): +def test_freeze_git_clone_srcdir(script): """ Test freezing a Git clone where setup.py is in a subdirectory relative the repo root and the source code is in a subdirectory @@ -363,7 +366,7 @@ def test_freeze_git_clone_srcdir(script, tmpdir): @need_mercurial -def test_freeze_mercurial_clone_srcdir(script, tmpdir): +def test_freeze_mercurial_clone_srcdir(script): """ Test freezing a Mercurial clone where setup.py is in a subdirectory relative to the repo root and the source code is in a subdirectory @@ -386,7 +389,7 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): @pytest.mark.git -def test_freeze_git_remote(script, tmpdir): +def test_freeze_git_remote(script): """ Test freezing a Git clone. """ @@ -469,7 +472,7 @@ def test_freeze_git_remote(script, tmpdir): @need_mercurial -def test_freeze_mercurial_clone(script, tmpdir): +def test_freeze_mercurial_clone(script): """ Test freezing a Mercurial clone. @@ -503,7 +506,7 @@ def test_freeze_mercurial_clone(script, tmpdir): @need_bzr -def test_freeze_bazaar_clone(script, tmpdir): +def test_freeze_bazaar_clone(script): """ Test freezing a Bazaar clone. @@ -936,7 +939,8 @@ def test_freeze_path_multiple(tmpdir, script, data): _check_output(result.stdout, expected) -def test_freeze_direct_url_archive(script, shared_data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_freeze_direct_url_archive(script, shared_data): req = "simple @ " + path_to_url(shared_data.packages / "simple-2.0.tar.gz") assert req.startswith("simple @ file://") script.pip("install", req) @@ -975,3 +979,22 @@ def test_freeze_include_work_dir_pkg(script): # when package directory is in PYTHONPATH result = script.pip("freeze", cwd=pkg_path) assert "simple==1.0" in result.stdout + + +def test_freeze_pep610_editable(script, with_wheel): + """ + Test that a package installed with a direct_url.json with editable=true + is correctly frozeon as editable. + """ + pkg_path = _create_test_package(script, name="testpkg") + result = script.pip("install", pkg_path) + direct_url_path = get_created_direct_url_path(result, "testpkg") + assert direct_url_path + # patch direct_url.json to simulate an editable install + with open(direct_url_path) as f: + direct_url = DirectUrl.from_json(f.read()) + direct_url.info.editable = True + with open(direct_url_path, "w") as f: + f.write(direct_url.to_json()) + result = script.pip("freeze") + assert "# Editable Git install with no remote (testpkg==0.1)" in result.stdout diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 32f8eda8d..3af75cb56 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -222,8 +222,9 @@ def test_pep518_forkbombs(script, data, common_wheels, command, package): @pytest.mark.network +@pytest.mark.usefixtures("with_wheel") def test_pip_second_command_line_interface_works( - script, pip_src, data, common_wheels, deprecated_python, with_wheel + script, pip_src, data, common_wheels, deprecated_python ): """ Check if ``pip<PYVERSION>`` commands behaves equally @@ -258,7 +259,8 @@ def test_install_exit_status_code_when_blank_requirements_file(script): @pytest.mark.network -def test_basic_install_from_pypi(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_basic_install_from_pypi(script): """ Test installing a package from PyPI. """ @@ -300,7 +302,7 @@ def test_basic_install_editable_from_svn(script): result.assert_installed("version-pkg", with_files=[".svn"]) -def _test_install_editable_from_git(script, tmpdir): +def _test_install_editable_from_git(script): """Test cloning from Git.""" pkg_path = _create_test_package(script, name="testpackage", vcs="git") args = [ @@ -312,12 +314,13 @@ def _test_install_editable_from_git(script, tmpdir): result.assert_installed("testpackage", with_files=[".git"]) -def test_basic_install_editable_from_git(script, tmpdir): - _test_install_editable_from_git(script, tmpdir) +def test_basic_install_editable_from_git(script): + _test_install_editable_from_git(script) -def test_install_editable_from_git_autobuild_wheel(script, tmpdir, with_wheel): - _test_install_editable_from_git(script, tmpdir) +@pytest.mark.usefixtures("with_wheel") +def test_install_editable_from_git_autobuild_wheel(script): + _test_install_editable_from_git(script) @pytest.mark.network @@ -375,7 +378,7 @@ def test_install_editable_uninstalls_existing_from_path(script, data): @need_mercurial -def test_basic_install_editable_from_hg(script, tmpdir): +def test_basic_install_editable_from_hg(script): """Test cloning and hg+file install from Mercurial.""" pkg_path = _create_test_package(script, name="testpackage", vcs="hg") url = "hg+{}#egg=testpackage".format(path_to_url(pkg_path)) @@ -386,7 +389,7 @@ def test_basic_install_editable_from_hg(script, tmpdir): @need_mercurial -def test_vcs_url_final_slash_normalization(script, tmpdir): +def test_vcs_url_final_slash_normalization(script): """ Test that presence or absence of final slash in VCS URL is normalized. """ @@ -401,7 +404,7 @@ def test_vcs_url_final_slash_normalization(script, tmpdir): @need_bzr -def test_install_editable_from_bazaar(script, tmpdir): +def test_install_editable_from_bazaar(script): """Test checking out from Bazaar.""" pkg_path = _create_test_package(script, name="testpackage", vcs="bazaar") args = [ @@ -434,7 +437,8 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): @pytest.mark.parametrize("resolver", ["", "--use-deprecated=legacy-resolver"]) -def test_basic_install_from_local_directory(script, data, resolver, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_basic_install_from_local_directory(script, data, resolver): """ Test installing from a local directory. """ @@ -461,9 +465,8 @@ def test_basic_install_from_local_directory(script, data, resolver, with_wheel): ("embedded_rel_path", True), ], ) -def test_basic_install_relative_directory( - script, data, test_type, editable, with_wheel -): +@pytest.mark.usefixtures("with_wheel") +def test_basic_install_relative_directory(script, data, test_type, editable): """ Test installing a requirement using a relative path. """ @@ -578,9 +581,8 @@ def test_hashed_install_failure_later_flag(script, tmpdir): ) -def test_install_from_local_directory_with_symlinks_to_directories( - script, data, with_wheel -): +@pytest.mark.usefixtures("with_wheel") +def test_install_from_local_directory_with_symlinks_to_directories(script, data): """ Test installing from a local directory containing symlinks to directories. """ @@ -592,7 +594,8 @@ def test_install_from_local_directory_with_symlinks_to_directories( result.did_create(dist_info_folder) -def test_install_from_local_directory_with_in_tree_build(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_from_local_directory_with_in_tree_build(script, data): """ Test installing from a local directory with --use-feature=in-tree-build. """ @@ -610,9 +613,8 @@ def test_install_from_local_directory_with_in_tree_build(script, data, with_whee @pytest.mark.skipif("sys.platform == 'win32'") -def test_install_from_local_directory_with_socket_file( - script, data, tmpdir, with_wheel -): +@pytest.mark.usefixtures("with_wheel") +def test_install_from_local_directory_with_socket_file(script, data, tmpdir): """ Test installing from a local directory containing a socket file. """ @@ -689,7 +691,8 @@ def test_upgrade_argparse_shadowed(script): assert "Not uninstalling argparse" not in result.stdout -def test_install_curdir(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_curdir(script, data): """ Test installing current directory ('.'). """ @@ -705,7 +708,8 @@ def test_install_curdir(script, data, with_wheel): result.did_create(dist_info_folder) -def test_install_pardir(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_pardir(script, data): """ Test installing parent directory ('..'). """ @@ -780,7 +784,8 @@ def test_install_global_option_using_editable(script, tmpdir): @pytest.mark.network -def test_install_package_with_same_name_in_curdir(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_package_with_same_name_in_curdir(script): """ Test installing a package with the same name of a local folder """ @@ -798,7 +803,8 @@ mock100_setup_py = textwrap.dedent( ) -def test_install_folder_using_dot_slash(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_folder_using_dot_slash(script): """ Test installing a folder using pip install ./foldername """ @@ -810,7 +816,8 @@ def test_install_folder_using_dot_slash(script, with_wheel): result.did_create(dist_info_folder) -def test_install_folder_using_slash_in_the_end(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_folder_using_slash_in_the_end(script): r""" Test installing a folder using pip install foldername/ or foldername\ """ @@ -822,7 +829,8 @@ def test_install_folder_using_slash_in_the_end(script, with_wheel): result.did_create(dist_info_folder) -def test_install_folder_using_relative_path(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_folder_using_relative_path(script): """ Test installing a folder using pip install folder1/folder2 """ @@ -836,7 +844,8 @@ def test_install_folder_using_relative_path(script, with_wheel): @pytest.mark.network -def test_install_package_which_contains_dev_in_name(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_package_which_contains_dev_in_name(script): """ Test installing package from PyPI which contains 'dev' in name """ @@ -847,7 +856,8 @@ def test_install_package_which_contains_dev_in_name(script, with_wheel): result.did_create(dist_info_folder) -def test_install_package_with_target(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_package_with_target(script): """ Test installing a package using pip install --target """ @@ -975,7 +985,8 @@ def test_install_nonlocal_compatible_wheel_path( @pytest.mark.parametrize("opt", ("--target", "--prefix")) -def test_install_with_target_or_prefix_and_scripts_no_warning(opt, script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_with_target_or_prefix_and_scripts_no_warning(opt, script): """ Test that installing with --target does not trigger the "script not in PATH" warning (issue #5201) @@ -1011,7 +1022,8 @@ def test_install_with_target_or_prefix_and_scripts_no_warning(opt, script, with_ assert "--no-warn-script-location" not in result.stderr, str(result) -def test_install_package_with_root(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_package_with_root(script, data): """ Test installing a package using pip install --root """ @@ -1194,7 +1206,8 @@ def test_install_package_with_latin1_setup(script, data): script.pip("install", to_install) -def test_url_req_case_mismatch_no_index(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_url_req_case_mismatch_no_index(script, data): """ tar ball url requirements (with no egg fragment), that happen to have upper case project names, should be considered equal to later requirements that @@ -1215,7 +1228,8 @@ def test_url_req_case_mismatch_no_index(script, data, with_wheel): result.did_not_create(dist_info_folder) -def test_url_req_case_mismatch_file_index(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_url_req_case_mismatch_file_index(script, data): """ tar ball url requirements (with no egg fragment), that happen to have upper case project names, should be considered equal to later requirements that @@ -1242,7 +1256,8 @@ def test_url_req_case_mismatch_file_index(script, data, with_wheel): result.did_not_create(dist_info_folder) -def test_url_incorrect_case_no_index(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_url_incorrect_case_no_index(script, data): """ Same as test_url_req_case_mismatch_no_index, except testing for the case where the incorrect case is given in the name of the package to install @@ -1263,7 +1278,8 @@ def test_url_incorrect_case_no_index(script, data, with_wheel): result.did_create(dist_info_folder) -def test_url_incorrect_case_file_index(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_url_incorrect_case_file_index(script, data): """ Same as test_url_req_case_mismatch_file_index, except testing for the case where the incorrect case is given in the name of the package to install @@ -1408,12 +1424,14 @@ def test_install_topological_sort(script, data): assert order1 in res or order2 in res, res -def test_install_wheel_broken(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_wheel_broken(script): res = script.pip_install_local("wheelbroken", expect_stderr=True) assert "Successfully installed wheelbroken-0.1" in str(res), str(res) -def test_cleanup_after_failed_wheel(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_cleanup_after_failed_wheel(script): res = script.pip_install_local("wheelbrokenafter", expect_stderr=True) # One of the effects of not cleaning up is broken scripts: script_py = script.bin_path / "script.py" @@ -1426,7 +1444,8 @@ def test_cleanup_after_failed_wheel(script, with_wheel): assert "Running setup.py clean for wheelbrokenafter" in str(res), str(res) -def test_install_builds_wheels(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_builds_wheels(script, data): # We need to use a subprocess to get the right value on Windows. res = script.run( "python", @@ -1473,7 +1492,8 @@ def test_install_builds_wheels(script, data, with_wheel): ] -def test_install_no_binary_disables_building_wheels(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_no_binary_disables_building_wheels(script, data): to_install = data.packages.joinpath("requires_wheelbroken_upper") res = script.pip( "install", @@ -1504,7 +1524,8 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel): @pytest.mark.network -def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_no_binary_builds_pep_517_wheel(script, data): to_install = data.packages.joinpath("pep517_setup_and_pyproject") res = script.pip("install", "--no-binary=:all:", "-f", data.find_links, to_install) expected = "Successfully installed pep517-setup-and-pyproject" @@ -1516,7 +1537,8 @@ def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): @pytest.mark.network -def test_install_no_binary_uses_local_backend(script, data, with_wheel, tmpdir): +@pytest.mark.usefixtures("with_wheel") +def test_install_no_binary_uses_local_backend(script, data, tmpdir): to_install = data.packages.joinpath("pep517_wrapper_buildsys") script.environ["PIP_TEST_MARKER_FILE"] = marker = str(tmpdir / "marker") res = script.pip("install", "--no-binary=:all:", "-f", data.find_links, to_install) @@ -1527,7 +1549,8 @@ def test_install_no_binary_uses_local_backend(script, data, with_wheel, tmpdir): assert os.path.isfile(marker), "Local PEP 517 backend not used" -def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_no_binary_disables_cached_wheels(script, data): # Seed the cache script.pip("install", "--no-index", "-f", data.find_links, "upper") script.pip("uninstall", "upper", "-y") @@ -1659,7 +1682,8 @@ def test_install_incompatible_python_requires_editable(script): assert _get_expected_error_text() in result.stderr, str(result) -def test_install_incompatible_python_requires_wheel(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_incompatible_python_requires_wheel(script): script.scratch_path.joinpath("pkga").mkdir() pkga_path = script.scratch_path / "pkga" pkga_path.joinpath("setup.py").write_text( diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index a11a901c5..02b2a1967 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -27,7 +27,8 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): @pytest.mark.network -def test_pep517_no_legacy_cleanup(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_pep517_no_legacy_cleanup(script, data): """Test a PEP 517 failed build does not attempt a legacy cleanup""" to_install = data.packages.joinpath("pep517_wrapper_buildsys") script.environ["PIP_TEST_FAIL_BUILD_WHEEL"] = "1" diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index b9348d271..20fdc62bc 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -222,7 +222,8 @@ def test_options_from_venv_config(script, virtualenv): assert msg.lower() in result.stdout.lower(), str(result) -def test_install_no_binary_via_config_disables_cached_wheels(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_no_binary_via_config_disables_cached_wheels(script, data): config_file = tempfile.NamedTemporaryFile(mode="wt", delete=False) try: script.environ["PIP_CONFIG_FILE"] = config_file.name diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py index baa5a3f2c..5dd7482cf 100644 --- a/tests/functional/test_install_direct_url.py +++ b/tests/functional/test_install_direct_url.py @@ -4,12 +4,14 @@ from tests.lib import _create_test_package, path_to_url from tests.lib.direct_url import get_created_direct_url -def test_install_find_links_no_direct_url(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_find_links_no_direct_url(script): result = script.pip_install_local("simple") assert not get_created_direct_url(result, "simple") -def test_install_vcs_editable_no_direct_url(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_vcs_editable_no_direct_url(script): pkg_path = _create_test_package(script, name="testpkg") args = ["install", "-e", "git+%s#egg=testpkg" % path_to_url(pkg_path)] result = script.pip(*args) @@ -18,7 +20,8 @@ def test_install_vcs_editable_no_direct_url(script, with_wheel): assert not get_created_direct_url(result, "testpkg") -def test_install_vcs_non_editable_direct_url(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_vcs_non_editable_direct_url(script): pkg_path = _create_test_package(script, name="testpkg") url = path_to_url(pkg_path) args = ["install", f"git+{url}#egg=testpkg"] @@ -29,7 +32,8 @@ def test_install_vcs_non_editable_direct_url(script, with_wheel): assert direct_url.info.vcs == "git" -def test_install_archive_direct_url(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_archive_direct_url(script, data): req = "simple @ " + path_to_url(data.packages / "simple-2.0.tar.gz") assert req.startswith("simple @ file://") result = script.pip("install", req) @@ -37,7 +41,8 @@ def test_install_archive_direct_url(script, data, with_wheel): @pytest.mark.network -def test_install_vcs_constraint_direct_url(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_vcs_constraint_direct_url(script): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text( "git+https://github.com/pypa/pip-test-package" @@ -48,7 +53,8 @@ def test_install_vcs_constraint_direct_url(script, with_wheel): assert get_created_direct_url(result, "pip_test_package") -def test_install_vcs_constraint_direct_file_url(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_vcs_constraint_direct_file_url(script): pkg_path = _create_test_package(script, name="testpkg") url = path_to_url(pkg_path) constraints_file = script.scratch_path / "constraints.txt" diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py index 3b8e36c79..962a64008 100644 --- a/tests/functional/test_install_index.py +++ b/tests/functional/test_install_index.py @@ -2,8 +2,11 @@ import os import textwrap import urllib.parse +import pytest -def test_find_links_relative_path(script, data, with_wheel): + +@pytest.mark.usefixtures("with_wheel") +def test_find_links_relative_path(script, data): """Test find-links as a relative path.""" result = script.pip( "install", @@ -19,7 +22,8 @@ def test_find_links_relative_path(script, data, with_wheel): result.did_create(initools_folder) -def test_find_links_requirements_file_relative_path(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_find_links_requirements_file_relative_path(script, data): """Test find-links as a relative path to a reqs file.""" script.scratch_path.joinpath("test-req.txt").write_text( textwrap.dedent( @@ -44,7 +48,8 @@ def test_find_links_requirements_file_relative_path(script, data, with_wheel): result.did_create(initools_folder) -def test_install_from_file_index_hash_link(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_from_file_index_hash_link(script, data): """ Test that a pkg can be installed from a file:// index using a link with a hash @@ -54,7 +59,8 @@ def test_install_from_file_index_hash_link(script, data, with_wheel): result.did_create(dist_info_folder) -def test_file_index_url_quoting(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_file_index_url_quoting(script, data): """ Test url quoting of file index url with a space """ diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index b8f8925ce..049cd7ab5 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -57,7 +57,8 @@ def arg_recording_sdist_maker(script): @pytest.mark.network -def test_requirements_file(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_requirements_file(script): """ Test installing from a requirements file. @@ -107,7 +108,8 @@ def test_schema_check_in_requirements_file(script): ("embedded_rel_path", True), ], ) -def test_relative_requirements_file(script, data, test_type, editable, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_relative_requirements_file(script, data, test_type, editable): """ Test installing from a requirements file with a relative path. For path URLs, use an egg= definition. @@ -152,7 +154,8 @@ def test_relative_requirements_file(script, data, test_type, editable, with_whee @pytest.mark.xfail @pytest.mark.network @need_svn -def test_multiple_requirements_files(script, tmpdir, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_multiple_requirements_files(script, tmpdir): """ Test installing from multiple nested requirements files. @@ -290,7 +293,8 @@ def test_install_local_with_subdirectory(script): @pytest.mark.incompatible_with_test_venv -def test_wheel_user_with_prefix_in_pydistutils_cfg(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_wheel_user_with_prefix_in_pydistutils_cfg(script, data): if os.name == "posix": user_filename = ".pydistutils.cfg" else: @@ -482,7 +486,8 @@ def test_constrained_to_url_install_same_url(script, data): assert "Running setup.py install for singlemodule" in result.stdout, str(result) -def test_double_install_spurious_hash_mismatch(script, tmpdir, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_double_install_spurious_hash_mismatch(script, tmpdir, data): """Make sure installing the same hashed sdist twice doesn't throw hash mismatch errors. diff --git a/tests/functional/test_install_requested.py b/tests/functional/test_install_requested.py index 1f3977078..d7a6ea110 100644 --- a/tests/functional/test_install_requested.py +++ b/tests/functional/test_install_requested.py @@ -1,3 +1,6 @@ +import pytest + + def _assert_requested_present(script, result, name, version): dist_info = script.site_packages / name + "-" + version + ".dist-info" requested = dist_info / "REQUESTED" @@ -12,7 +15,8 @@ def _assert_requested_absent(script, result, name, version): assert requested not in result.files_created -def test_install_requested_basic(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_requested_basic(script, data): result = script.pip( "install", "--no-index", "-f", data.find_links, "require_simple" ) @@ -21,7 +25,8 @@ def test_install_requested_basic(script, data, with_wheel): _assert_requested_absent(script, result, "simple", "3.0") -def test_install_requested_requirements(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_requested_requirements(script, data): script.scratch_path.joinpath("requirements.txt").write_text("require_simple\n") result = script.pip( "install", @@ -35,7 +40,8 @@ def test_install_requested_requirements(script, data, with_wheel): _assert_requested_absent(script, result, "simple", "3.0") -def test_install_requested_dep_in_requirements(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_requested_dep_in_requirements(script, data): script.scratch_path.joinpath("requirements.txt").write_text( "require_simple\nsimple<3\n" ) @@ -52,7 +58,8 @@ def test_install_requested_dep_in_requirements(script, data, with_wheel): _assert_requested_present(script, result, "simple", "2.0") -def test_install_requested_reqs_and_constraints(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_requested_reqs_and_constraints(script, data): script.scratch_path.joinpath("requirements.txt").write_text("require_simple\n") script.scratch_path.joinpath("constraints.txt").write_text("simple<3\n") result = script.pip( @@ -70,7 +77,8 @@ def test_install_requested_reqs_and_constraints(script, data, with_wheel): _assert_requested_absent(script, result, "simple", "2.0") -def test_install_requested_in_reqs_and_constraints(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_requested_in_reqs_and_constraints(script, data): script.scratch_path.joinpath("requirements.txt").write_text( "require_simple\nsimple\n" ) diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index edeebb0b9..5d55a0d76 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -36,9 +36,8 @@ def test_invalid_upgrade_strategy_causes_error(script): assert "invalid choice" in result.stderr -def test_only_if_needed_does_not_upgrade_deps_when_satisfied( - script, resolver_variant, with_wheel -): +@pytest.mark.usefixtures("with_wheel") +def test_only_if_needed_does_not_upgrade_deps_when_satisfied(script, resolver_variant): """ It doesn't upgrade a dependency if it already satisfies the requirements. @@ -63,7 +62,8 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied( ), "did not print correct message for not-upgraded requirement" -def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script): """ It does upgrade a dependency if it no longer satisfies the requirements. @@ -82,7 +82,8 @@ def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script, with_ assert expected in result.files_deleted, "should have uninstalled simple==1.0" -def test_eager_does_upgrade_dependecies_when_currently_satisfied(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_eager_does_upgrade_dependecies_when_currently_satisfied(script): """ It does upgrade a dependency even if it already satisfies the requirements. @@ -100,7 +101,8 @@ def test_eager_does_upgrade_dependecies_when_currently_satisfied(script, with_wh ) in result.files_deleted, "should have uninstalled simple==2.0" -def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script): """ It does upgrade a dependency if it no longer satisfies the requirements. @@ -123,7 +125,8 @@ def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script, with_wh @pytest.mark.network -def test_upgrade_to_specific_version(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_upgrade_to_specific_version(script): """ It does upgrade to specific version requested. @@ -136,7 +139,8 @@ def test_upgrade_to_specific_version(script, with_wheel): @pytest.mark.network -def test_upgrade_if_requested(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_upgrade_if_requested(script): """ And it does upgrade if requested. @@ -296,7 +300,8 @@ def test_uninstall_rollback(script, data): @pytest.mark.network -def test_should_not_install_always_from_cache(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_should_not_install_always_from_cache(script): """ If there is an old cached package, pip should download the newer version Related to issue #175 @@ -309,7 +314,8 @@ def test_should_not_install_always_from_cache(script, with_wheel): @pytest.mark.network -def test_install_with_ignoreinstalled_requested(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_with_ignoreinstalled_requested(script): """ Test old conflicting package is completely ignored """ diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index f01754d4a..3cf8b1e29 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -69,9 +69,8 @@ class Tests_UserSite: result.assert_installed("INITools", use_user_site=True) @pytest.mark.incompatible_with_test_venv - def test_install_from_current_directory_into_usersite( - self, script, data, with_wheel - ): + @pytest.mark.usefixtures("with_wheel") + def test_install_from_current_directory_into_usersite(self, script, data): """ Test installing current directory ('.') into usersite """ diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index ceb136fa1..b7a288d70 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -158,7 +158,8 @@ def test_install_editable_from_git_with_https(script, tmpdir): @pytest.mark.network -def test_install_noneditable_git(script, tmpdir, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_noneditable_git(script): """ Test installing from a non-editable git URL with a given tag. """ @@ -516,7 +517,8 @@ def test_check_submodule_addition(script): update_result.did_create(script.venv / "src/version-pkg/testpkg/static/testfile2") -def test_install_git_branch_not_cached(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_git_branch_not_cached(script): """ Installing git urls with a branch revision does not cause wheel caching. """ @@ -531,7 +533,8 @@ def test_install_git_branch_not_cached(script, with_wheel): assert f"Successfully built {PKG}" in result.stdout, result.stdout -def test_install_git_sha_cached(script, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_git_sha_cached(script): """ Installing git urls with a sha revision does cause wheel caching. """ diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index ae046eb38..a87fe2933 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -172,7 +172,8 @@ def test_install_from_wheel_with_headers(script): assert header_path.read_text() == header_text -def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir): +@pytest.mark.usefixtures("with_wheel") +def test_install_wheel_with_target(script, shared_data, tmpdir): """ Test installing a wheel using pip install --target """ @@ -190,7 +191,8 @@ def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir): result.did_create(Path("scratch") / "target" / "simpledist") -def test_install_wheel_with_target_and_data_files(script, data, with_wheel): +@pytest.mark.usefixtures("with_wheel") +def test_install_wheel_with_target_and_data_files(script, data): """ Test for issue #4092. It will be checked that a data_files specification in setup.py is handled correctly when a wheel is installed with the --target @@ -326,7 +328,8 @@ def test_wheel_record_lines_have_hash_for_data_files(script): @pytest.mark.incompatible_with_test_venv -def test_install_user_wheel(script, shared_data, with_wheel, tmpdir): +@pytest.mark.usefixtures("with_wheel") +def test_install_user_wheel(script, shared_data, tmpdir): """ Test user install from wheel (that has a script) """ @@ -580,7 +583,7 @@ def test_wheel_compile_syntax_error(script, data): assert "SyntaxError: " not in result.stdout -def test_wheel_install_with_no_cache_dir(script, tmpdir, data): +def test_wheel_install_with_no_cache_dir(script, data): """Check wheel installations work, even with no cache.""" package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") result = script.pip("install", "--no-cache-dir", "--no-index", package) diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 5598fa941..80c72471d 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -3,7 +3,9 @@ import os import pytest -from tests.lib import create_test_package_with_setup, wheel +from pip._internal.models.direct_url import DirectUrl +from tests.lib import _create_test_package, create_test_package_with_setup, wheel +from tests.lib.direct_url import get_created_direct_url_path from tests.lib.path import Path @@ -172,13 +174,17 @@ def test_uptodate_flag(script, data): "--uptodate", "--format=json", ) - assert {"name": "simple", "version": "1.0"} not in json.loads( - result.stdout - ) # 3.0 is latest - assert {"name": "pip-test-package", "version": "0.1.1"} in json.loads( - result.stdout - ) # editables included - assert {"name": "simple2", "version": "3.0"} in json.loads(result.stdout) + json_output = json.loads(result.stdout) + for item in json_output: + if "editable_project_location" in item: + item["editable_project_location"] = "<location>" + assert {"name": "simple", "version": "1.0"} not in json_output # 3.0 is latest + assert { + "name": "pip-test-package", + "version": "0.1.1", + "editable_project_location": "<location>", + } in json_output # editables included + assert {"name": "simple2", "version": "3.0"} in json_output @pytest.mark.network @@ -210,7 +216,7 @@ def test_uptodate_columns_flag(script, data): ) assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout # editables included + assert "Editable project location" in result.stdout # editables included assert "pip-test-package (0.1.1," not in result.stdout assert "pip-test-package 0.1.1" in result.stdout, str(result) assert "simple2 3.0" in result.stdout, str(result) @@ -244,25 +250,36 @@ def test_outdated_flag(script, data): "--outdated", "--format=json", ) + json_output = json.loads(result.stdout) + for item in json_output: + if "editable_project_location" in item: + item["editable_project_location"] = "<location>" assert { "name": "simple", "version": "1.0", "latest_version": "3.0", "latest_filetype": "sdist", - } in json.loads(result.stdout) - assert dict( - name="simplewheel", version="1.0", latest_version="2.0", latest_filetype="wheel" - ) in json.loads(result.stdout) + } in json_output + assert ( + dict( + name="simplewheel", + version="1.0", + latest_version="2.0", + latest_filetype="wheel", + ) + in json_output + ) assert ( dict( name="pip-test-package", version="0.1", latest_version="0.1.1", latest_filetype="sdist", + editable_project_location="<location>", ) - in json.loads(result.stdout) + in json_output ) - assert "simple2" not in {p["name"] for p in json.loads(result.stdout)} + assert "simple2" not in {p["name"] for p in json_output} @pytest.mark.network @@ -346,7 +363,7 @@ def test_editables_columns_flag(pip_test_package_script): result = pip_test_package_script.pip("list", "--editable", "--format=columns") assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout + assert "Editable project location" in result.stdout assert os.path.join("src", "pip-test-package") in result.stdout, str(result) @@ -384,7 +401,7 @@ def test_uptodate_editables_columns_flag(pip_test_package_script, data): ) assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout + assert "Editable project location" in result.stdout assert os.path.join("src", "pip-test-package") in result.stdout, str(result) @@ -433,7 +450,7 @@ def test_outdated_editables_columns_flag(script, data): ) assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout + assert "Editable project location" in result.stdout assert os.path.join("src", "pip-test-package") in result.stdout, str(result) @@ -684,3 +701,27 @@ def test_list_include_work_dir_pkg(script): result = script.pip("list", "--format=json", cwd=pkg_path) json_result = json.loads(result.stdout) assert {"name": "simple", "version": "1.0"} in json_result + + +def test_list_pep610_editable(script, with_wheel): + """ + Test that a package installed with a direct_url.json with editable=true + is correctly listed as editable. + """ + pkg_path = _create_test_package(script, name="testpkg") + result = script.pip("install", pkg_path) + direct_url_path = get_created_direct_url_path(result, "testpkg") + assert direct_url_path + # patch direct_url.json to simulate an editable install + with open(direct_url_path) as f: + direct_url = DirectUrl.from_json(f.read()) + direct_url.info.editable = True + with open(direct_url_path, "w") as f: + f.write(direct_url.to_json()) + result = script.pip("list", "--format=json") + for item in json.loads(result.stdout): + if item["name"] == "testpkg": + assert item["editable_project_location"] + break + else: + assert False, "package 'testpkg' not found in pip list result" diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index d0f5a3c86..052c00179 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -944,12 +944,7 @@ class TestExtraMerge: _wheel_from_index, ], ) - def test_new_resolver_extra_merge_in_package( - self, - monkeypatch, - script, - pkg_builder, - ): + def test_new_resolver_extra_merge_in_package(self, script, pkg_builder): create_basic_wheel_for_package(script, "depdev", "1.0.0") create_basic_wheel_for_package( script, diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py index 9e2b32720..72b876138 100644 --- a/tests/functional/test_requests.py +++ b/tests/functional/test_requests.py @@ -1,18 +1,18 @@ import pytest -@pytest.mark.skipif +@pytest.mark.network def test_timeout(script): result = script.pip( "--timeout", - "0.01", + "0.001", "install", "-vvv", "INITools", expect_error=True, ) assert ( - "Could not fetch URL https://pypi.org/simple/INITools/: " - "timed out" in result.stdout - ) - assert "Could not fetch URL https://pypi.org/simple/: timed out" in result.stdout + "Could not fetch URL https://pypi.org/simple/initools/: " + "connection error: HTTPSConnectionPool(host='pypi.org', port=443): " + "Max retries exceeded with url: /simple/initools/ " + ) in result.stdout diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 1cb83a86a..7b3932af0 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -9,10 +9,7 @@ import pytest from pip._internal.cli.status_codes import ERROR from tests.lib import pyversion # noqa: F401 - -@pytest.fixture(autouse=True) -def auto_with_wheel(with_wheel): - pass +pytestmark = pytest.mark.usefixtures("with_wheel") def add_files_to_dist_directory(folder): @@ -364,7 +361,7 @@ def test_pip_wheel_ext_module_with_tmpdir_inside(script, data, common_wheels): @pytest.mark.network -def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data): +def test_pep517_wheels_are_not_confused_with_other_files(script, data): """Check correct wheels are copied. (#6196)""" pkg_to_wheel = data.src / "withpyproject" add_files_to_dist_directory(pkg_to_wheel) @@ -377,7 +374,7 @@ def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data): result.did_create(wheel_file_path) -def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): +def test_legacy_wheels_are_not_confused_with_other_files(script, data): """Check correct wheels are copied. (#6196)""" pkg_to_wheel = data.src / "simplewheel-1.0" add_files_to_dist_directory(pkg_to_wheel) diff --git a/tests/lib/direct_url.py b/tests/lib/direct_url.py index 7f1dee8bd..ec0a32b4d 100644 --- a/tests/lib/direct_url.py +++ b/tests/lib/direct_url.py @@ -3,15 +3,22 @@ from typing import Optional from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from tests.lib import TestPipResult +from tests.lib.path import Path -def get_created_direct_url(result: TestPipResult, pkg: str) -> Optional[DirectUrl]: +def get_created_direct_url_path(result: TestPipResult, pkg: str) -> Optional[Path]: direct_url_metadata_re = re.compile( pkg + r"-[\d\.]+\.dist-info." + DIRECT_URL_METADATA_NAME + r"$" ) for filename in result.files_created: if direct_url_metadata_re.search(filename): - direct_url_path = result.test_env.base_path / filename - with open(direct_url_path) as f: - return DirectUrl.from_json(f.read()) + return result.test_env.base_path / filename + return None + + +def get_created_direct_url(result: TestPipResult, pkg: str) -> Optional[DirectUrl]: + direct_url_path = get_created_direct_url_path(result, pkg) + if direct_url_path: + with open(direct_url_path) as f: + return DirectUrl.from_json(f.read()) return None diff --git a/tests/lib/filesystem.py b/tests/lib/filesystem.py index 84a1c81bd..8563783e7 100644 --- a/tests/lib/filesystem.py +++ b/tests/lib/filesystem.py @@ -27,8 +27,7 @@ def make_unreadable_file(path: str) -> None: Path(path).touch() os.chmod(path, 0o000) if sys.platform == "win32": - # Once we drop PY2 we can use `os.getlogin()` instead. - username = os.environ["USERNAME"] + username = os.getlogin() # Remove "Read Data/List Directory" permission for current user, but # leave everything else. args = ["icacls", path, "/deny", username + ":(RD)"] diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py index f1b9bf65e..753afa9b9 100644 --- a/tests/unit/test_direct_url_helpers.py +++ b/tests/unit/test_direct_url_helpers.py @@ -122,7 +122,7 @@ def test_from_link_vcs_with_source_dir_obtains_commit_id(script, tmpdir): assert direct_url.info.commit_id == commit_id -def test_from_link_vcs_without_source_dir(script, tmpdir): +def test_from_link_vcs_without_source_dir(script): direct_url = direct_url_from_link( Link("git+https://g.c/u/p.git@1"), link_is_in_wheel_cache=True ) diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py index 300c953b0..7be8941fe 100644 --- a/tests/unit/test_network_auth.py +++ b/tests/unit/test_network_auth.py @@ -78,6 +78,15 @@ def test_get_credentials_uses_cached_credentials(): assert got == expected +def test_get_credentials_uses_cached_credentials_only_username(): + auth = MultiDomainBasicAuth() + auth.passwords["example.com"] = ("user", "pass") + + got = auth._get_url_and_credentials("http://user@example.com/path") + expected = ("http://example.com/path", "user", "pass") + assert got == expected + + def test_get_index_url_credentials(): auth = MultiDomainBasicAuth(index_urls=["http://foo:bar@example.com/path"]) get = functools.partial( diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index dd99bc758..1f333fedf 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -82,6 +82,7 @@ class TestPipSession: # Check that the "port wildcard" is present. assert "https://example.com:" in session.adapters # Check that the cache is enabled. + assert hasattr(session.adapters["http://example.com/"], "cache") assert hasattr(session.adapters["https://example.com/"], "cache") def test_add_trusted_host(self): @@ -93,12 +94,20 @@ class TestPipSession: prefix3 = "https://host3/" prefix3_wildcard = "https://host3:" + prefix2_http = "http://host2/" + prefix3_http = "http://host3/" + prefix3_wildcard_http = "http://host3:" + # Confirm some initial conditions as a baseline. assert session.pip_trusted_origins == [("host1", None), ("host3", None)] assert session.adapters[prefix3] is trusted_host_adapter assert session.adapters[prefix3_wildcard] is trusted_host_adapter + assert session.adapters[prefix3_http] is trusted_host_adapter + assert session.adapters[prefix3_wildcard_http] is trusted_host_adapter + assert prefix2 not in session.adapters + assert prefix2_http not in session.adapters # Test adding a new host. session.add_trusted_host("host2") @@ -110,6 +119,7 @@ class TestPipSession: # Check that prefix3 is still present. assert session.adapters[prefix3] is trusted_host_adapter assert session.adapters[prefix2] is trusted_host_adapter + assert session.adapters[prefix2_http] is trusted_host_adapter # Test that adding the same host doesn't create a duplicate. session.add_trusted_host("host3") @@ -121,6 +131,7 @@ class TestPipSession: session.add_trusted_host("host4:8080") prefix4 = "https://host4:8080/" + prefix4_http = "http://host4:8080/" assert session.pip_trusted_origins == [ ("host1", None), ("host3", None), @@ -128,6 +139,7 @@ class TestPipSession: ("host4", 8080), ] assert session.adapters[prefix4] is trusted_host_adapter + assert session.adapters[prefix4_http] is trusted_host_adapter def test_add_trusted_host__logging(self, caplog): """ diff --git a/tests/unit/test_network_utils.py b/tests/unit/test_network_utils.py index 8105e1600..96a17808e 100644 --- a/tests/unit/test_network_utils.py +++ b/tests/unit/test_network_utils.py @@ -18,12 +18,12 @@ def test_raise_for_status_raises_exception(status_code, error_type): resp.status_code = status_code resp.url = "http://www.example.com/whatever.tgz" resp.reason = "Network Error" - with pytest.raises(NetworkConnectionError) as exc: + with pytest.raises(NetworkConnectionError) as excinfo: raise_for_status(resp) - assert str(exc.info) == ( - "{} {}: Network Error for url:" - " http://www.example.com/whatever.tgz".format(status_code, error_type) - ) + assert str(excinfo.value) == ( + "{} {}: Network Error for url:" + " http://www.example.com/whatever.tgz".format(status_code, error_type) + ) def test_raise_for_status_does_not_raises_exception(): diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index ede9a1001..b48def4d8 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -66,7 +66,7 @@ def parse_reqfile( yield install_req_from_parsed_requirement(parsed_req, isolated=isolated) -def test_read_file_url(tmp_path): +def test_read_file_url(tmp_path, session): reqs = tmp_path.joinpath("requirements.txt") reqs.write_text("foo") result = list(parse_requirements(reqs.as_posix(), session)) @@ -302,7 +302,7 @@ class TestProcessLine: assert repr(found_req) == repr(req) assert found_req.constraint is True - def test_nested_constraints_file(self, monkeypatch, tmpdir): + def test_nested_constraints_file(self, monkeypatch, tmpdir, session): req_name = "hello" req_file = tmpdir / "parent" / "req_file.txt" req_file.parent.mkdir() diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 15b4367df..182a13ea0 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -17,6 +17,7 @@ import pytest from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated +from pip._internal.utils.egg_link import egg_link_path_from_location from pip._internal.utils.encoding import BOMS, auto_decode from pip._internal.utils.glibc import ( glibc_version_string, @@ -28,10 +29,8 @@ from pip._internal.utils.misc import ( HiddenText, build_netloc, build_url_from_netloc, - egg_link_path, format_size, get_distribution, - get_installed_distributions, get_prog, hide_url, hide_value, @@ -52,7 +51,7 @@ from pip._internal.utils.setuptools_build import make_setuptools_shim_args class Tests_EgglinkPath: - "util.egg_link_path() tests" + "util.egg_link_path_from_location() tests" def setup(self): @@ -68,7 +67,7 @@ class Tests_EgglinkPath: ) # patches - from pip._internal.utils import misc as utils + from pip._internal.utils import egg_link as utils self.old_site_packages = utils.site_packages self.mock_site_packages = utils.site_packages = "SITE_PACKAGES" @@ -107,19 +106,25 @@ class Tests_EgglinkPath: self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.side_effect = self.eggLinkInUserSite - assert egg_link_path(self.mock_dist) == self.user_site_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.user_site_egglink + ) def test_egglink_in_usersite_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInUserSite - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None def test_egglink_in_usersite_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInUserSite - assert egg_link_path(self.mock_dist) == self.user_site_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.user_site_egglink + ) # ####################### # # # egglink in sitepkgs # # @@ -128,19 +133,28 @@ class Tests_EgglinkPath: self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.side_effect = self.eggLinkInSitePackages - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) def test_egglink_in_sitepkgs_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInSitePackages - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) def test_egglink_in_sitepkgs_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInSitePackages - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) # ################################## # # # egglink in usersite & sitepkgs # # @@ -149,19 +163,28 @@ class Tests_EgglinkPath: self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.return_value = True - assert egg_link_path(self.mock_dist) == self.user_site_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.user_site_egglink + ) def test_egglink_in_both_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = True - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) def test_egglink_in_both_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = True - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) # ############## # # # no egglink # # @@ -170,26 +193,25 @@ class Tests_EgglinkPath: self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.return_value = False - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None def test_noegglink_in_sitepkgs_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = False - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None def test_noegglink_in_sitepkgs_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = False - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None @patch("pip._internal.utils.misc.dist_in_usersite") @patch("pip._internal.utils.misc.dist_is_local") -@patch("pip._internal.utils.misc.dist_is_editable") class TestsGetDistributions: - """Test get_installed_distributions() and get_distribution().""" + """Test get_distribution().""" class MockWorkingSet(List[Mock]): def require(self, name): @@ -219,78 +241,12 @@ class TestsGetDistributions: ) ) - def dist_is_editable(self, dist): - return dist.test_name == "editable" - def dist_is_local(self, dist): return dist.test_name != "global" and dist.test_name != "user" def dist_in_usersite(self, dist): return dist.test_name == "user" - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_editables_only( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(editables_only=True) - assert len(dists) == 1, dists - assert dists[0].test_name == "editable" - - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_exclude_editables( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(include_editables=False) - assert len(dists) == 1 - assert dists[0].test_name == "normal" - - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_include_globals( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(local_only=False) - assert len(dists) == 4 - - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_user_only( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(local_only=False, user_only=True) - assert len(dists) == 1 - assert dists[0].test_name == "user" - - @patch("pip._vendor.pkg_resources.working_set", workingset_stdlib) - def test_gte_py27_excludes( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions() - assert len(dists) == 0 - - @patch("pip._vendor.pkg_resources.working_set", workingset_freeze) - def test_freeze_excludes( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(skip=("setuptools", "pip", "distribute")) - assert len(dists) == 0 - @pytest.mark.parametrize( "working_set, req_name", itertools.chain( @@ -306,14 +262,12 @@ class TestsGetDistributions: ) def test_get_distribution( self, - mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite, working_set, req_name, ): """Ensure get_distribution() finds all kinds of distributions.""" - mock_dist_is_editable.side_effect = self.dist_is_editable mock_dist_is_local.side_effect = self.dist_is_local mock_dist_in_usersite.side_effect = self.dist_in_usersite with patch("pip._vendor.pkg_resources.working_set", working_set): @@ -324,11 +278,9 @@ class TestsGetDistributions: @patch("pip._vendor.pkg_resources.working_set", workingset) def test_get_distribution_nonexist( self, - mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite, ): - mock_dist_is_editable.side_effect = self.dist_is_editable mock_dist_is_local.side_effect = self.dist_is_local mock_dist_in_usersite.side_effect = self.dist_in_usersite dist = get_distribution("non-exist") @@ -405,7 +357,7 @@ class Failer: raise OSError("Failed") -def test_rmtree_retries(tmpdir, monkeypatch): +def test_rmtree_retries(monkeypatch): """ Test pip._internal.utils.rmtree will retry failures """ @@ -413,7 +365,7 @@ def test_rmtree_retries(tmpdir, monkeypatch): rmtree("foo") -def test_rmtree_retries_for_3sec(tmpdir, monkeypatch): +def test_rmtree_retries_for_3sec(monkeypatch): """ Test pip._internal.utils.rmtree will retry failures for no more than 3 sec """ diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index 03721b63f..89409ae82 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -67,7 +67,7 @@ def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_dir): assert "does not start with 'simple'" in str(e.value) -def test_wheel_version_ok(tmpdir, data): +def test_wheel_version_ok(data): assert wheel.wheel_version(message_from_string("Wheel-Version: 1.9")) == (1, 9) diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index a9c7489d7..3ddd9e66a 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -150,7 +150,7 @@ def test_should_cache(req, expected): assert wheel_builder._should_cache(req) is expected -def test_should_cache_git_sha(script, tmpdir): +def test_should_cache_git_sha(script): repo_path = _create_test_package(script, name="mypkg") commit = script.run("git", "rev-parse", "HEAD", cwd=repo_path).stdout.strip() |