1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Merge branch 'main' into no-import-from-conftest

This commit is contained in:
Tzu-ping Chung 2023-10-03 16:11:30 +08:00
commit 2fad07e6e2
250 changed files with 7964 additions and 3168 deletions

36
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,36 @@
917b41d6d73535c090fc312668dff353cdaef906 # Blacken docs/html/conf.py
ed383dd8afa8fe0250dcf9b8962927ada0e21c89 # Blacken docs/pip_sphinxext.py
228405e62451abe8a66233573035007df4be575f # Blacken noxfile.py
f477a9f490e978177b71c9dbaa5465c51ea21129 # Blacken setup.py
e59ba23468390217479465019f8d78e724a23550 # Blacken src/pip/__main__.py
d7013db084e9a52242354ee5754dc5d19ccf062e # Blacken src/pip/_internal/build_env.py
30e9ffacae75378fc3e3df48f754dabad037edb9 # Blacken src/pip/_internal/cache.py
8341d56b46776a805286218ac5fb0e7850fd9341 # Blacken src/pip/_internal/cli/autocompletion.py
3d3461ed65208656358b3595e25d8c31c5c89470 # Blacken src/pip/_internal/cli/base_command.py
d489b0f1b104bc936b0fb17e6c33633664ebdc0e # Blacken src/pip/_internal/cli/cmdoptions.py
591fe4841aefe9befa0530f2a54f820c4ecbb392 # Blacken src/pip/_internal/cli/command_context.py
9265b28ef7248ae1847a80384dbeeb8119c3e2f5 # Blacken src/pip/_internal/cli/main.py
847a369364878c38d210c90beed2737bb6fb3a85 # Blacken src/pip/_internal/cli/main_parser.py
ec97119067041ae58b963935ff5f0e5d9fead80c # Blacken src/pip/_internal/cli/parser.py
6e3b8de22fa39fa3073599ecf9db61367f4b3b32 # Blacken src/pip/_internal/cli/progress_bars.py
55405227de983c5bd5bf0858ea12dbe537d3e490 # Blacken src/pip/_internal/cli/req_command.py
d5ca5c850cae9a0c64882a8f49d3a318699a7e2e # Blacken src/pip/_internal/cli/spinners.py
9747cb48f8430a7a91b36fe697dd18dbddb319f0 # Blacken src/pip/_internal/commands/__init__.py
1c09fd6f124df08ca36bed68085ad68e89bb1957 # Blacken src/pip/_internal/commands/cache.py
315e93d7eb87cd476afcc4eaf0f01a7b56a5037f # Blacken src/pip/_internal/commands/check.py
8ae3b96ed7d24fd24024ccce4840da0dcf635f26 # Blacken src/pip/_internal/commands/completion.py
42ca4792202f26a293ee48380718743a80bbee37 # Blacken src/pip/_internal/commands/configuration.py
790ad78fcd43d41a5bef9dca34a3c128d05eb02c # Blacken src/pip/_internal/commands/debug.py
a6fcc8f045afe257ce321f4012fc8fcb4be01eb3 # Blacken src/pip/_internal/commands/download.py
920e735dfc60109351fbe2f4c483c2f6ede9e52d # Blacken src/pip/_internal/commands/freeze.py
053004e0fcf0851238b1064fbce13aea87b24e9c # Blacken src/pip/_internal/commands/hash.py
a6b6ae487e52c2242045b64cb8962e0a992cfd76 # Blacken src/pip/_internal/commands/help.py
2495cf95a6c7eb61ccf1f9f0e8b8d736af914e53 # Blacken __main__.py
c7ee560e00b85f7486b452c14ff49e4737996eda # Blacken tools/
8e2e1964a4f0a060f7299a96a911c9e116b2283d # Blacken src/pip/_internal/commands/
1bc0eef05679e87f45540ab0a294667cb3c6a88e # Blacken src/pip/_internal/network/
069b01932a7d64a81c708c6254cc93e1f89e6783 # Blacken src/pip/_internal/req
1897784d59e0d5fcda2dd75fea54ddd8be3d502a # Blacken src/pip/_internal/index
94999255d5ede440c37137d210666fdf64302e75 # Reformat the codebase, with black
585037a80a1177f1fa92e159a7079855782e543e # Cleanup implicit string concatenation
8a6f6ac19b80a6dc35900a47016c851d9fcd2ee2 # Blacken src/pip/_internal/resolution directory

View file

@ -91,7 +91,7 @@ jobs:
- run: git diff --exit-code - run: git diff --exit-code
tests-unix: tests-unix:
name: tests / ${{ matrix.python }} / ${{ matrix.os }} name: tests / ${{ matrix.python.key || matrix.python }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest runs-on: ${{ matrix.os }}-latest
needs: [packaging, determine-changes] needs: [packaging, determine-changes]
@ -109,12 +109,14 @@ jobs:
- "3.9" - "3.9"
- "3.10" - "3.10"
- "3.11" - "3.11"
- "3.12"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Install Ubuntu dependencies - name: Install Ubuntu dependencies
if: matrix.os == 'Ubuntu' if: matrix.os == 'Ubuntu'
@ -129,12 +131,12 @@ jobs:
# Main check # Main check
- name: Run unit tests - name: Run unit tests
run: >- run: >-
nox -s test-${{ matrix.python }} -- nox -s test-${{ matrix.python.key || matrix.python }} --
-m unit -m unit
--verbose --numprocesses auto --showlocals --verbose --numprocesses auto --showlocals
- name: Run integration tests - name: Run integration tests
run: >- run: >-
nox -s test-${{ matrix.python }} -- nox -s test-${{ matrix.python.key || matrix.python }} --
-m integration -m integration
--verbose --numprocesses auto --showlocals --verbose --numprocesses auto --showlocals
--durations=5 --durations=5
@ -167,24 +169,13 @@ jobs:
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
# We use a RAMDisk on Windows, since filesystem IO is a big slowdown # We use C:\Temp (which is already available on the worker)
# for our tests. # as a temporary directory for all of the tests because the
- name: Create a RAMDisk # default value (under the user dir) is more deeply nested
run: ./tools/ci/New-RAMDisk.ps1 -Drive R -Size 1GB # and causes tests to fail with "path too long" errors.
- name: Setup RAMDisk permissions
run: |
mkdir R:\Temp
$acl = Get-Acl "R:\Temp"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
)
$acl.AddAccessRule($rule)
Set-Acl "R:\Temp" $acl
- run: pip install nox - run: pip install nox
env: env:
TEMP: "R:\\Temp" TEMP: "C:\\Temp"
# Main check # Main check
- name: Run unit tests - name: Run unit tests
@ -194,7 +185,7 @@ jobs:
-m unit -m unit
--verbose --numprocesses auto --showlocals --verbose --numprocesses auto --showlocals
env: env:
TEMP: "R:\\Temp" TEMP: "C:\\Temp"
- name: Run integration tests (group 1) - name: Run integration tests (group 1)
if: matrix.group == 1 if: matrix.group == 1
@ -203,7 +194,7 @@ jobs:
-m integration -k "not test_install" -m integration -k "not test_install"
--verbose --numprocesses auto --showlocals --verbose --numprocesses auto --showlocals
env: env:
TEMP: "R:\\Temp" TEMP: "C:\\Temp"
- name: Run integration tests (group 2) - name: Run integration tests (group 2)
if: matrix.group == 2 if: matrix.group == 2
@ -212,7 +203,7 @@ jobs:
-m integration -k "test_install" -m integration -k "test_install"
--verbose --numprocesses auto --showlocals --verbose --numprocesses auto --showlocals
env: env:
TEMP: "R:\\Temp" TEMP: "C:\\Temp"
tests-zipapp: tests-zipapp:
name: tests / zipapp name: tests / zipapp

View file

@ -1,19 +0,0 @@
name: No Response
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on:
issue_comment:
types: [created]
schedule:
# Schedule for five minutes after the hour, every hour
- cron: '5 * * * *'
jobs:
noResponse:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
responseRequiredLabel: "S: awaiting response"

View file

@ -17,26 +17,15 @@ repos:
exclude: .patch exclude: .patch
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.1.0 rev: 23.7.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 6.0.0 # Ruff version.
rev: v0.0.287
hooks: hooks:
- id: flake8 - id: ruff
additional_dependencies: [
'flake8-bugbear',
'flake8-logging-format',
'flake8-implicit-str-concat',
]
exclude: tests/data
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
files: \.py$
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.961 rev: v0.961

View file

@ -6,7 +6,7 @@ build:
python: "3.11" python: "3.11"
sphinx: sphinx:
builder: htmldir builder: dirhtml
configuration: docs/html/conf.py configuration: docs/html/conf.py
python: python:

View file

@ -71,6 +71,7 @@ atse
Atsushi Odagiri Atsushi Odagiri
Avinash Karhana Avinash Karhana
Avner Cohen Avner Cohen
Awit (Ah-Wit) Ghirmai
Baptiste Mispelon Baptiste Mispelon
Barney Gale Barney Gale
barneygale barneygale
@ -126,6 +127,7 @@ Chih-Hsuan Yen
Chris Brinker Chris Brinker
Chris Hunt Chris Hunt
Chris Jerdonek Chris Jerdonek
Chris Kuehl
Chris McDonough Chris McDonough
Chris Pawley Chris Pawley
Chris Pryer Chris Pryer
@ -330,6 +332,8 @@ Jarek Potiuk
jarondl jarondl
Jason Curtis Jason Curtis
Jason R. Coombs Jason R. Coombs
JasonMo
JasonMo1
Jay Graves Jay Graves
Jean-Christophe Fillion-Robin Jean-Christophe Fillion-Robin
Jeff Barber Jeff Barber
@ -344,6 +348,7 @@ Jim Fisher
Jim Garrison Jim Garrison
Jiun Bae Jiun Bae
Jivan Amara Jivan Amara
Joe Bylund
Joe Michelini Joe Michelini
John Paton John Paton
John T. Wodder II John T. Wodder II
@ -441,6 +446,7 @@ Matthew Einhorn
Matthew Feickert Matthew Feickert
Matthew Gilliard Matthew Gilliard
Matthew Iversen Matthew Iversen
Matthew Treinish
Matthew Trumbell Matthew Trumbell
Matthew Willson Matthew Willson
Matthias Bussonnier Matthias Bussonnier
@ -582,6 +588,7 @@ Rishi
RobberPhex RobberPhex
Robert Collins Robert Collins
Robert McGibbon Robert McGibbon
Robert Pollak
Robert T. McGibbon Robert T. McGibbon
robin elisha robinson robin elisha robinson
Roey Berman Roey Berman
@ -614,6 +621,7 @@ SeongSoo Cho
Sergey Vasilyev Sergey Vasilyev
Seth Michael Larson Seth Michael Larson
Seth Woodworth Seth Woodworth
Shantanu
shireenrao shireenrao
Shivansh-007 Shivansh-007
Shlomi Fish Shlomi Fish
@ -638,6 +646,7 @@ Steve Barnes
Steve Dower Steve Dower
Steve Kowalik Steve Kowalik
Steven Myint Steven Myint
Steven Silvester
stonebig stonebig
Stéphane Bidoul Stéphane Bidoul
Stéphane Bidoul (ACSONE) Stéphane Bidoul (ACSONE)
@ -707,6 +716,7 @@ Wilson Mo
wim glenn wim glenn
Winson Luk Winson Luk
Wolfgang Maier Wolfgang Maier
Wu Zhenyu
XAMES3 XAMES3
Xavier Fernandez Xavier Fernandez
xoviat xoviat

View file

@ -14,6 +14,7 @@ recursive-include src/pip/_vendor *COPYING*
include docs/docutils.conf include docs/docutils.conf
include docs/requirements.txt include docs/requirements.txt
exclude .git-blame-ignore-revs
exclude .coveragerc exclude .coveragerc
exclude .mailmap exclude .mailmap
exclude .appveyor.yml exclude .appveyor.yml

View file

@ -9,6 +9,69 @@
.. towncrier release notes start .. towncrier release notes start
23.2.1 (2023-07-22)
===================
Bug Fixes
---------
- Disable PEP 658 metadata fetching with the legacy resolver. (`#12156 <https://github.com/pypa/pip/issues/12156>`_)
23.2 (2023-07-15)
=================
Process
-------
- Deprecate support for eggs for Python 3.11 or later, when the new ``importlib.metadata`` backend is used to load distribution metadata. This only affects the egg *distribution format* (with the ``.egg`` extension); distributions using the ``.egg-info`` *metadata format* (but are not actually eggs) are not affected. For more information about eggs, see `relevant section in the setuptools documentation <https://setuptools.pypa.io/en/stable/deprecated/python_eggs.html>`__.
Deprecations and Removals
-------------------------
- Deprecate legacy version and version specifiers that don't conform to `PEP 440
<https://peps.python.org/pep-0440/>`_ (`#12063 <https://github.com/pypa/pip/issues/12063>`_)
- ``freeze`` no longer excludes the ``setuptools``, ``distribute``, and ``wheel``
from the output when running on Python 3.12 or later, where they are not
included in a virtual environment by default. Use ``--exclude`` if you wish to
exclude any of these packages. (`#4256 <https://github.com/pypa/pip/issues/4256>`_)
Features
--------
- make rejection messages slightly different between 1 and 8, so the user can make the difference. (`#12040 <https://github.com/pypa/pip/issues/12040>`_)
Bug Fixes
---------
- Fix ``pip completion --zsh``. (`#11417 <https://github.com/pypa/pip/issues/11417>`_)
- Prevent downloading files twice when PEP 658 metadata is present (`#11847 <https://github.com/pypa/pip/issues/11847>`_)
- Add permission check before configuration (`#11920 <https://github.com/pypa/pip/issues/11920>`_)
- Fix deprecation warnings in Python 3.12 for usage of shutil.rmtree (`#11957 <https://github.com/pypa/pip/issues/11957>`_)
- Ignore invalid or unreadable ``origin.json`` files in the cache of locally built wheels. (`#11985 <https://github.com/pypa/pip/issues/11985>`_)
- Fix installation of packages with PEP658 metadata using non-canonicalized names (`#12038 <https://github.com/pypa/pip/issues/12038>`_)
- Correctly parse ``dist-info-metadata`` values from JSON-format index data. (`#12042 <https://github.com/pypa/pip/issues/12042>`_)
- Fail with an error if the ``--python`` option is specified after the subcommand name. (`#12067 <https://github.com/pypa/pip/issues/12067>`_)
- Fix slowness when using ``importlib.metadata`` (the default way for pip to read metadata in Python 3.11+) and there is a large overlap between already installed and to-be-installed packages. (`#12079 <https://github.com/pypa/pip/issues/12079>`_)
- Pass the ``-r`` flag to mercurial to be explicit that a revision is passed and protect
against ``hg`` options injection as part of VCS URLs. Users that do not have control on
VCS URLs passed to pip are advised to upgrade. (`#12119 <https://github.com/pypa/pip/issues/12119>`_)
Vendored Libraries
------------------
- Upgrade certifi to 2023.5.7
- Upgrade platformdirs to 3.8.1
- Upgrade pygments to 2.15.1
- Upgrade pyparsing to 3.1.0
- Upgrade Requests to 2.31.0
- Upgrade rich to 13.4.2
- Upgrade setuptools to 68.0.0
- Updated typing_extensions to 4.6.0
- Upgrade typing_extensions to 4.7.1
- Upgrade urllib3 to 1.26.16
23.1.2 (2023-04-26) 23.1.2 (2023-04-26)
=================== ===================
@ -53,7 +116,7 @@ Deprecations and Removals
``--config-settings``. (`#11859 <https://github.com/pypa/pip/issues/11859>`_) ``--config-settings``. (`#11859 <https://github.com/pypa/pip/issues/11859>`_)
- Using ``--config-settings`` with projects that don't have a ``pyproject.toml`` now prints - Using ``--config-settings`` with projects that don't have a ``pyproject.toml`` now prints
a deprecation warning. In the future the presence of config settings will automatically a deprecation warning. In the future the presence of config settings will automatically
enable the default build backend for legacy projects and pass the setttings to it. (`#11915 <https://github.com/pypa/pip/issues/11915>`_) enable the default build backend for legacy projects and pass the settings to it. (`#11915 <https://github.com/pypa/pip/issues/11915>`_)
- Remove ``setup.py install`` fallback when building a wheel failed for projects without - Remove ``setup.py install`` fallback when building a wheel failed for projects without
``pyproject.toml``. (`#8368 <https://github.com/pypa/pip/issues/8368>`_) ``pyproject.toml``. (`#8368 <https://github.com/pypa/pip/issues/8368>`_)
- When the ``wheel`` package is not installed, pip now uses the default build backend - When the ``wheel`` package is not installed, pip now uses the default build backend

View file

@ -3,9 +3,15 @@ pip - The Python Package Installer
.. image:: https://img.shields.io/pypi/v/pip.svg .. image:: https://img.shields.io/pypi/v/pip.svg
:target: https://pypi.org/project/pip/ :target: https://pypi.org/project/pip/
:alt: PyPI
.. image:: https://img.shields.io/pypi/pyversions/pip
:target: https://pypi.org/project/pip
:alt: PyPI - Python Version
.. image:: https://readthedocs.org/projects/pip/badge/?version=latest .. image:: https://readthedocs.org/projects/pip/badge/?version=latest
:target: https://pip.pypa.io/en/latest :target: https://pip.pypa.io/en/latest
:alt: Documentation
pip is the `package installer`_ for Python. You can use pip to install packages from the `Python Package Index`_ and other indexes. pip is the `package installer`_ for Python. You can use pip to install packages from the `Python Package Index`_ and other indexes.
@ -19,10 +25,6 @@ We release updates regularly, with a new version every 3 months. Find more detai
* `Release notes`_ * `Release notes`_
* `Release process`_ * `Release process`_
In pip 20.3, we've `made a big improvement to the heart of pip`_; `learn more`_. We want your input, so `sign up for our user experience research studies`_ to help us do it right.
**Note**: pip 21.0, in January 2021, removed Python 2 support, per pip's `Python 2 support policy`_. Please migrate to Python 3.
If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms:
* `Issue tracking`_ * `Issue tracking`_
@ -49,10 +51,6 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
.. _Release process: https://pip.pypa.io/en/latest/development/release-process/ .. _Release process: https://pip.pypa.io/en/latest/development/release-process/
.. _GitHub page: https://github.com/pypa/pip .. _GitHub page: https://github.com/pypa/pip
.. _Development documentation: https://pip.pypa.io/en/latest/development .. _Development documentation: https://pip.pypa.io/en/latest/development
.. _made a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/11/pip-20-3-new-resolver.html
.. _learn more: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-3-2020
.. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html
.. _Python 2 support policy: https://pip.pypa.io/en/latest/development/release-process/#python-2-support
.. _Issue tracking: https://github.com/pypa/pip/issues .. _Issue tracking: https://github.com/pypa/pip/issues
.. _Discourse channel: https://discuss.python.org/c/packaging .. _Discourse channel: https://discuss.python.org/c/packaging
.. _User IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa .. _User IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa

View file

@ -1,3 +1,10 @@
# Security and Vulnerability Reporting # Security Policy
If you find any security issues, please report to [security@python.org](mailto:security@python.org) ## Reporting a Vulnerability
Please read the guidelines on reporting security issues [on the
official website](https://www.python.org/dev/security/) for
instructions on how to report a security-related problem to
the Python Security Response Team responsibly.
To reach the response team, email `security at python dot org`.

View file

@ -21,6 +21,12 @@ Usage
Description Description
=========== ===========
.. attention::
PyPI no longer supports ``pip search`` (or XML-RPC search). Please use https://pypi.org/search (via a browser)
instead. See https://warehouse.pypa.io/api-reference/xml-rpc.html#deprecated-methods for more information.
However, XML-RPC search (and this command) may still be supported by indexes other than PyPI.
.. pip-command-description:: search .. pip-command-description:: search

View file

@ -103,7 +103,7 @@ $ pip install --upgrade pip
The current version of pip works on: The current version of pip works on:
- Windows, Linux and MacOS. - Windows, Linux and MacOS.
- CPython 3.7, 3.8, 3.9, 3.10 and latest PyPy3. - CPython 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and latest PyPy3.
pip is tested to work on the latest patch version of the Python interpreter, pip is tested to work on the latest patch version of the Python interpreter,
for each of the minor versions listed above. Previous patch versions are for each of the minor versions listed above. Previous patch versions are

View file

@ -56,6 +56,9 @@ package with the following properties:
URL reference. `false` if the requirements was provided as a name and version URL reference. `false` if the requirements was provided as a name and version
specifier. specifier.
- `is_yanked`: `true` if the requirement was yanked from the index, but was still
selected by pip conform to [PEP 592](https://peps.python.org/pep-0592/#installers).
- `download_info`: Information about the artifact (to be) downloaded for installation, - `download_info`: Information about the artifact (to be) downloaded for installation,
using the [direct URL data using the [direct URL data
structure](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/). structure](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/).
@ -106,6 +109,7 @@ will produce an output similar to this (metadata abriged for brevity):
} }
}, },
"is_direct": false, "is_direct": false,
"is_yanked": false,
"requested": true, "requested": true,
"metadata": { "metadata": {
"name": "pydantic", "name": "pydantic",
@ -133,6 +137,7 @@ will produce an output similar to this (metadata abriged for brevity):
} }
}, },
"is_direct": true, "is_direct": true,
"is_yanked": false,
"requested": true, "requested": true,
"metadata": { "metadata": {
"name": "packaging", "name": "packaging",

View file

@ -68,7 +68,7 @@ man pages][netrc-docs].
pip supports loading credentials stored in your keyring using the pip supports loading credentials stored in your keyring using the
{pypi}`keyring` library, which can be enabled py passing `--keyring-provider` {pypi}`keyring` library, which can be enabled py passing `--keyring-provider`
with a value of `auto`, `disabled`, `import`, or `subprocess`. The default with a value of `auto`, `disabled`, `import`, or `subprocess`. The default
value `auto` respects `--no-input` and not query keyring at all if the option value `auto` respects `--no-input` and does not query keyring at all if the option
is used; otherwise it tries the `import`, `subprocess`, and `disabled` is used; otherwise it tries the `import`, `subprocess`, and `disabled`
providers (in this order) and uses the first one that works. providers (in this order) and uses the first one that works.

View file

@ -27,6 +27,13 @@ While this cache attempts to minimize network activity, it does not prevent
network access altogether. If you want a local install solution that network access altogether. If you want a local install solution that
circumvents accessing PyPI, see {ref}`Installing from local packages`. circumvents accessing PyPI, see {ref}`Installing from local packages`.
```{versionchanged} 23.3
A new cache format is now used, stored in a directory called `http-v2` (see
below for this directory's location). Previously this cache was stored in a
directory called `http` in the main cache directory. If you have completely
switched to newer versions of `pip`, you may wish to delete the old directory.
```
(wheel-caching)= (wheel-caching)=
### Locally built wheels ### Locally built wheels
@ -124,11 +131,11 @@ The {ref}`pip cache` command can be used to manage pip's cache.
### Removing a single package ### Removing a single package
`pip cache remove setuptools` removes all wheel files related to setuptools from pip's cache. `pip cache remove setuptools` removes all wheel files related to setuptools from pip's cache. HTTP cache files are not removed at this time.
### Removing the cache ### Removing the cache
`pip cache purge` will clear all wheel files from pip's cache. `pip cache purge` will clear all files from pip's wheel and HTTP caches.
### Listing cached files ### Listing cached files

View file

@ -28,19 +28,9 @@ It is possible to use the system trust store, instead of the bundled certifi
certificates for verifying HTTPS certificates. This approach will typically certificates for verifying HTTPS certificates. This approach will typically
support corporate proxy certificates without additional configuration. support corporate proxy certificates without additional configuration.
In order to use system trust stores, you need to: In order to use system trust stores, you need to use Python 3.10 or newer.
- Use Python 3.10 or newer.
- Install the {pypi}`truststore` package, in the Python environment you're
running pip in.
This is typically done by installing this package using a system package
manager or by using pip in {ref}`Hash-checking mode` for this package and
trusting the network using the `--trusted-host` flag.
```{pip-cli} ```{pip-cli}
$ python -m pip install truststore
[...]
$ python -m pip install SomePackage --use-feature=truststore $ python -m pip install SomePackage --use-feature=truststore
[...] [...]
Successfully installed SomePackage Successfully installed SomePackage

View file

@ -8,7 +8,7 @@ and this article is intended to help readers understand what is happening
```{note} ```{note}
This document is a work in progress. The details included are accurate (at the This document is a work in progress. The details included are accurate (at the
time of writing), but there is additional information, in particular around time of writing), but there is additional information, in particular around
pip's interface with resolvelib, which have not yet been included. pip's interface with resolvelib, which has not yet been included.
Contributions to improve this document are welcome. Contributions to improve this document are welcome.
``` ```
@ -26,7 +26,7 @@ The practical implication of that is that there will always be some situations
where pip cannot determine what to install in a reasonable length of time. We where pip cannot determine what to install in a reasonable length of time. We
make every effort to ensure that such situations happen rarely, but eliminating make every effort to ensure that such situations happen rarely, but eliminating
them altogether isn't even theoretically possible. We'll discuss what options them altogether isn't even theoretically possible. We'll discuss what options
yopu have if you hit a problem situation like this a little later. you have if you hit a problem situation like this a little later.
## Python specific issues ## Python specific issues
@ -136,7 +136,7 @@ operations:
that satisfy them. This is essentially where the finder interacts with the that satisfy them. This is essentially where the finder interacts with the
resolver. resolver.
* `is_satisfied_by` - checks if a candidate satisfies a requirement. This is * `is_satisfied_by` - checks if a candidate satisfies a requirement. This is
basically the implementation of what a requirement meams. basically the implementation of what a requirement means.
* `get_dependencies` - get the dependency metadata for a candidate. This is * `get_dependencies` - get the dependency metadata for a candidate. This is
the implementation of the process of getting and reading package metadata. the implementation of the process of getting and reading package metadata.

View file

@ -1,4 +1,4 @@
sphinx ~= 6.0 sphinx ~= 7.0
towncrier towncrier
furo furo
myst_parser myst_parser

1
news/11394.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Ignore errors in temporary directory cleanup (show a warning instead).

5
news/11649.bugfix.rst Normal file
View file

@ -0,0 +1,5 @@
Normalize extras according to :pep:`685` from package metadata in the resolver
for comparison. This ensures extras are correctly compared and merged as long
as the package providing the extra(s) is built with values normalized according
to the standard. Note, however, that this *does not* solve cases where the
package itself contains unnormalized extra values in the metadata.

1
news/12005.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Removed uses of ``datetime.datetime.utcnow`` from non-vendored code.

1
news/12059.doc.rst Normal file
View file

@ -0,0 +1 @@
Document that ``pip search`` support has been removed from PyPI

1
news/12122.doc.rst Normal file
View file

@ -0,0 +1 @@
Clarify --prefer-binary in CLI and docs

6
news/12155.process.rst Normal file
View file

@ -0,0 +1,6 @@
The metadata-fetching log message is moved to the VERBOSE level and now hidden
by default. The more significant information in this message to most users are
already available in surrounding logs (the package name and version of the
metadata being fetched), while the URL to the exact metadata file is generally
too long and clutters the output. The message can be brought back with
``--verbose``.

1
news/12175.removal.rst Normal file
View file

@ -0,0 +1 @@
Drop a fallback to using SecureTransport on macOS. It was useful when pip detected OpenSSL older than 1.0.1, but the current pip does not support any Python version supporting such old OpenSSL versions.

1
news/12183.trivial.rst Normal file
View file

@ -0,0 +1 @@
Add test cases for some behaviors of ``install --dry-run`` and ``--use-feature=fast-deps``.

1
news/12187.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Fix improper handling of the new onexc argument of ``shutil.rmtree()`` in Python 3.12.

1
news/12191.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Prevent downloading sdists twice when PEP 658 metadata is present.

1
news/12194.trivial.rst Normal file
View file

@ -0,0 +1 @@
Add lots of comments to the ``BuildTracker``.

1
news/12204.feature.rst Normal file
View file

@ -0,0 +1 @@
Improve use of datastructures to make candidate selection 1.6x faster

1
news/12215.feature.rst Normal file
View file

@ -0,0 +1 @@
Allow ``pip install --dry-run`` to use platform and ABI overriding options similar to ``--target``.

1
news/12224.feature.rst Normal file
View file

@ -0,0 +1 @@
Add ``is_yanked`` boolean entry to the installation report (``--report``) to indicate whether the requirement was yanked from the index, but was still selected by pip conform to PEP 592.

1
news/12225.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Filter out yanked links from the available versions error message: "(from versions: 1.0, 2.0, 3.0)" will not contain yanked versions conform PEP 592. The yanked versions (if any) will be mentioned in a separate error message.

0
news/12252.trivial.rst Normal file
View file

1
news/12254.process.rst Normal file
View file

@ -0,0 +1 @@
Added reference to `vulnerability reporting guidelines <https://www.python.org/dev/security/>`_ to pip's security policy.

0
news/12261.trivial.rst Normal file
View file

1
news/12280.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Fix crash when the git version number contains something else than digits and dots.

1
news/12306.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Use ``-r=...`` instead of ``-r ...`` to specify references with Mercurial.

1
news/2984.bugfix.rst Normal file
View file

@ -0,0 +1 @@
pip uses less memory when caching large packages. As a result, there is a new on-disk cache format stored in a new directory ($PIP_CACHE_DIR/http-v2).

View file

@ -0,0 +1 @@
Add ruff rules ASYNC,C4,C90,PERF,PLE,PLR for minor optimizations and to set upper limits on code complexity.

1
news/certifi.vendor.rst Normal file
View file

@ -0,0 +1 @@
Upgrade certifi to 2023.7.22

View file

@ -1,2 +0,0 @@
Added seperate instructions for installing ``nox`` in the ``docs/development/getting-started.rst`` doc. and slight update
to the below ``Running pip From Source Tree`` section.

View file

@ -0,0 +1 @@
Add truststore 0.8.0

View file

View file

@ -67,7 +67,7 @@ def should_update_common_wheels() -> bool:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Development Commands # Development Commands
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "pypy3"]) @nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3"])
def test(session: nox.Session) -> None: def test(session: nox.Session) -> None:
# Get the common wheels. # Get the common wheels.
if should_update_common_wheels(): if should_update_common_wheels():
@ -89,6 +89,7 @@ def test(session: nox.Session) -> None:
shutil.rmtree(sdist_dir, ignore_errors=True) shutil.rmtree(sdist_dir, ignore_errors=True)
# fmt: off # fmt: off
session.install("setuptools")
session.run( session.run(
"python", "setup.py", "sdist", "--formats=zip", "--dist-dir", sdist_dir, "python", "setup.py", "sdist", "--formats=zip", "--dist-dir", sdist_dir,
silent=True, silent=True,
@ -183,6 +184,12 @@ def lint(session: nox.Session) -> None:
# git reset --hard origin/main # git reset --hard origin/main
@nox.session @nox.session
def vendoring(session: nox.Session) -> None: def vendoring(session: nox.Session) -> None:
# Ensure that the session Python is running 3.10+
# so that truststore can be installed correctly.
session.run(
"python", "-c", "import sys; sys.exit(1 if sys.version_info < (3, 10) else 0)"
)
session.install("vendoring~=1.2.0") session.install("vendoring~=1.2.0")
parser = argparse.ArgumentParser(prog="nox -s vendoring") parser = argparse.ArgumentParser(prog="nox -s vendoring")
@ -219,7 +226,7 @@ def vendoring(session: nox.Session) -> None:
new_version = old_version new_version = old_version
for inner_name, inner_version in pinned_requirements(vendor_txt): for inner_name, inner_version in pinned_requirements(vendor_txt):
if inner_name == name: if inner_name == name:
# this is a dedicated assignment, to make flake8 happy # this is a dedicated assignment, to make lint happy
new_version = inner_version new_version = inner_version
break break
else: else:
@ -351,6 +358,7 @@ def build_dists(session: nox.Session) -> List[str]:
) )
session.log("# Build distributions") session.log("# Build distributions")
session.install("setuptools", "wheel")
session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True)
produced_dists = glob.glob("dist/*") produced_dists = glob.glob("dist/*")

View file

@ -71,3 +71,56 @@ setuptools = "pkg_resources"
CacheControl = "https://raw.githubusercontent.com/ionrock/cachecontrol/v0.12.6/LICENSE.txt" CacheControl = "https://raw.githubusercontent.com/ionrock/cachecontrol/v0.12.6/LICENSE.txt"
distlib = "https://bitbucket.org/pypa/distlib/raw/master/LICENSE.txt" distlib = "https://bitbucket.org/pypa/distlib/raw/master/LICENSE.txt"
webencodings = "https://github.com/SimonSapin/python-webencodings/raw/master/LICENSE" webencodings = "https://github.com/SimonSapin/python-webencodings/raw/master/LICENSE"
[tool.ruff]
extend-exclude = [
"_vendor",
"./build",
".scratch",
"data",
]
ignore = [
"B019",
"B020",
"B904", # Ruff enables opinionated warnings by default
"B905", # Ruff enables opinionated warnings by default
"G202",
]
line-length = 88
select = [
"ASYNC",
"B",
"C4",
"C90",
"E",
"F",
"G",
"I",
"ISC",
"PERF",
"PLE",
"PLR0",
"W",
"RUF100",
]
[tool.ruff.isort]
# We need to explicitly make pip "first party" as it's imported by code in
# the docs and tests directories.
known-first-party = ["pip"]
known-third-party = ["pip._vendor"]
[tool.ruff.mccabe]
max-complexity = 33 # default is 10
[tool.ruff.per-file-ignores]
"noxfile.py" = ["G"]
"src/pip/_internal/*" = ["PERF203"]
"tests/*" = ["B011"]
"tests/unit/test_finder.py" = ["C414"]
[tool.ruff.pylint]
max-args = 15 # default is 5
max-branches = 28 # default is 12
max-returns = 13 # default is 6
max-statements = 134 # default is 50

View file

@ -1,46 +1,13 @@
[isort]
profile = black
skip =
./build,
.nox,
.tox,
.scratch,
_vendor,
data
known_third_party =
pip._vendor
[flake8]
max-line-length = 88
exclude =
./build,
.nox,
.tox,
.scratch,
_vendor,
data
enable-extensions = G
extend-ignore =
G200, G202,
# black adds spaces around ':'
E203,
# using a cache
B019,
# reassigning variables in a loop
B020,
per-file-ignores =
# G: The plugin logging-format treats every .log and .error as logging.
noxfile.py: G
# B011: Do not call assert False since python -O removes these calls
tests/*: B011
[mypy] [mypy]
mypy_path = $MYPY_CONFIG_FILE_DIR/src mypy_path = $MYPY_CONFIG_FILE_DIR/src
strict = True
no_implicit_reexport = False
allow_subclassing_any = True
allow_untyped_calls = True
warn_return_any = False
ignore_missing_imports = True ignore_missing_imports = True
disallow_untyped_defs = True
disallow_any_generics = True
warn_unused_ignores = True
no_implicit_optional = True
[mypy-pip._internal.utils._jaraco_text] [mypy-pip._internal.utils._jaraco_text]
ignore_errors = True ignore_errors = True
@ -51,12 +18,8 @@ ignore_errors = True
# These vendored libraries use runtime magic to populate things and don't sit # These vendored libraries use runtime magic to populate things and don't sit
# well with static typing out of the box. Eventually we should provide correct # well with static typing out of the box. Eventually we should provide correct
# typing information for their public interface and remove these configs. # typing information for their public interface and remove these configs.
[mypy-pip._vendor.colorama]
follow_imports = skip
[mypy-pip._vendor.pkg_resources] [mypy-pip._vendor.pkg_resources]
follow_imports = skip follow_imports = skip
[mypy-pip._vendor.progress.*]
follow_imports = skip
[mypy-pip._vendor.requests.*] [mypy-pip._vendor.requests.*]
follow_imports = skip follow_imports = skip

View file

@ -1,6 +1,6 @@
from typing import List, Optional from typing import List, Optional
__version__ = "23.2.dev0" __version__ = "23.3.dev0"
def main(args: Optional[List[str]] = None) -> int: def main(args: Optional[List[str]] = None) -> int:

View file

@ -1,6 +1,5 @@
import os import os
import sys import sys
import warnings
# Remove '' and current working directory from the first entry # Remove '' and current working directory from the first entry
# of sys.path, if present to avoid using current directory # of sys.path, if present to avoid using current directory
@ -20,12 +19,6 @@ if __package__ == "":
sys.path.insert(0, path) sys.path.insert(0, path)
if __name__ == "__main__": if __name__ == "__main__":
# Work around the error reported in #9540, pending a proper fix.
# Note: It is essential the warning filter is set *before* importing
# pip, as the deprecation happens at import time, not runtime.
warnings.filterwarnings(
"ignore", category=DeprecationWarning, module=".*packaging\\.version"
)
from pip._internal.cli.main import main as _main from pip._internal.cli.main import main as _main
sys.exit(_main()) sys.exit(_main())

View file

@ -1,6 +1,5 @@
from typing import List, Optional from typing import List, Optional
import pip._internal.utils.inject_securetransport # noqa
from pip._internal.utils import _log from pip._internal.utils import _log
# init_logging() must be called before any call to logging.getLogger() # init_logging() must be called before any call to logging.getLogger()

View file

@ -78,12 +78,10 @@ class Cache:
if can_not_cache: if can_not_cache:
return [] return []
candidates = []
path = self.get_path_for_link(link) path = self.get_path_for_link(link)
if os.path.isdir(path): if os.path.isdir(path):
for candidate in os.listdir(path): return [(candidate, path) for candidate in os.listdir(path)]
candidates.append((candidate, path)) return []
return candidates
def get_path_for_link(self, link: Link) -> str: def get_path_for_link(self, link: Link) -> str:
"""Return a directory to store cached items in for link.""" """Return a directory to store cached items in for link."""
@ -194,7 +192,17 @@ class CacheEntry:
self.origin: Optional[DirectUrl] = None self.origin: Optional[DirectUrl] = None
origin_direct_url_path = Path(self.link.file_path).parent / ORIGIN_JSON_NAME origin_direct_url_path = Path(self.link.file_path).parent / ORIGIN_JSON_NAME
if origin_direct_url_path.exists(): if origin_direct_url_path.exists():
self.origin = DirectUrl.from_json(origin_direct_url_path.read_text()) try:
self.origin = DirectUrl.from_json(
origin_direct_url_path.read_text(encoding="utf-8")
)
except Exception as e:
logger.warning(
"Ignoring invalid cache entry origin file %s for %s (%s)",
origin_direct_url_path,
link.filename,
e,
)
class WheelCache(Cache): class WheelCache(Cache):
@ -257,16 +265,26 @@ class WheelCache(Cache):
@staticmethod @staticmethod
def record_download_origin(cache_dir: str, download_info: DirectUrl) -> None: def record_download_origin(cache_dir: str, download_info: DirectUrl) -> None:
origin_path = Path(cache_dir) / ORIGIN_JSON_NAME origin_path = Path(cache_dir) / ORIGIN_JSON_NAME
if origin_path.is_file(): if origin_path.exists():
origin = DirectUrl.from_json(origin_path.read_text()) try:
# TODO: use DirectUrl.equivalent when https://github.com/pypa/pip/pull/10564 origin = DirectUrl.from_json(origin_path.read_text(encoding="utf-8"))
# is merged. except Exception as e:
if origin.url != download_info.url:
logger.warning( logger.warning(
"Origin URL %s in cache entry %s does not match download URL %s. " "Could not read origin file %s in cache entry (%s). "
"This is likely a pip bug or a cache corruption issue.", "Will attempt to overwrite it.",
origin.url, origin_path,
cache_dir, e,
download_info.url,
) )
else:
# TODO: use DirectUrl.equivalent when
# https://github.com/pypa/pip/pull/10564 is merged.
if origin.url != download_info.url:
logger.warning(
"Origin URL %s in cache entry %s does not match download URL "
"%s. This is likely a pip bug or a cache corruption issue. "
"Will overwrite it with the new value.",
origin.url,
cache_dir,
download_info.url,
)
origin_path.write_text(download_info.to_json(), encoding="utf-8") origin_path.write_text(download_info.to_json(), encoding="utf-8")

View file

@ -71,8 +71,9 @@ def autocomplete() -> None:
for opt in subcommand.parser.option_list_all: for opt in subcommand.parser.option_list_all:
if opt.help != optparse.SUPPRESS_HELP: if opt.help != optparse.SUPPRESS_HELP:
for opt_str in opt._long_opts + opt._short_opts: options += [
options.append((opt_str, opt.nargs)) (opt_str, opt.nargs) for opt_str in opt._long_opts + opt._short_opts
]
# filter out previously specified options from available options # filter out previously specified options from available options
prev_opts = [x.split("=")[0] for x in cwords[1 : cword - 1]] prev_opts = [x.split("=")[0] for x in cwords[1 : cword - 1]]

View file

@ -131,6 +131,17 @@ class Command(CommandContextMixIn):
", ".join(sorted(always_enabled_features)), ", ".join(sorted(always_enabled_features)),
) )
# Make sure that the --python argument isn't specified after the
# subcommand. We can tell, because if --python was specified,
# we should only reach this point if we're running in the created
# subprocess, which has the _PIP_RUNNING_IN_SUBPROCESS environment
# variable set.
if options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
logger.critical(
"The --python option must be placed before the pip subcommand name"
)
sys.exit(ERROR)
# TODO: Try to get these passing down from the command? # TODO: Try to get these passing down from the command?
# without resorting to os.environ to hold these. # without resorting to os.environ to hold these.
# This also affects isolated builds and it should. # This also affects isolated builds and it should.
@ -170,7 +181,7 @@ class Command(CommandContextMixIn):
assert isinstance(status, int) assert isinstance(status, int)
return status return status
except DiagnosticPipError as exc: except DiagnosticPipError as exc:
logger.error("[present-rich] %s", exc) logger.error("%s", exc, extra={"rich": True})
logger.debug("Exception information:", exc_info=True) logger.debug("Exception information:", exc_info=True)
return ERROR return ERROR

View file

@ -92,10 +92,10 @@ def check_dist_restriction(options: Values, check_target: bool = False) -> None:
) )
if check_target: if check_target:
if dist_restriction_set and not options.target_dir: if not options.dry_run and dist_restriction_set and not options.target_dir:
raise CommandError( raise CommandError(
"Can not use any platform or abi specific options unless " "Can not use any platform or abi specific options unless "
"installing via '--target'" "installing via '--target' or using '--dry-run'"
) )
@ -670,7 +670,10 @@ def prefer_binary() -> Option:
dest="prefer_binary", dest="prefer_binary",
action="store_true", action="store_true",
default=False, default=False,
help="Prefer older binary packages over newer source packages.", help=(
"Prefer binary packages over source packages, even if the "
"source packages are newer."
),
) )
@ -823,7 +826,7 @@ def _handle_config_settings(
) -> None: ) -> None:
key, sep, val = value.partition("=") key, sep, val = value.partition("=")
if sep != "=": if sep != "=":
parser.error(f"Arguments to {opt_str} must be of the form KEY=VAL") # noqa parser.error(f"Arguments to {opt_str} must be of the form KEY=VAL")
dest = getattr(parser.values, option.dest) dest = getattr(parser.values, option.dest)
if dest is None: if dest is None:
dest = {} dest = {}
@ -918,13 +921,13 @@ def _handle_merge_hash(
algo, digest = value.split(":", 1) algo, digest = value.split(":", 1)
except ValueError: except ValueError:
parser.error( parser.error(
"Arguments to {} must be a hash name " # noqa "Arguments to {} must be a hash name "
"followed by a value, like --hash=sha256:" "followed by a value, like --hash=sha256:"
"abcde...".format(opt_str) "abcde...".format(opt_str)
) )
if algo not in STRONG_HASHES: if algo not in STRONG_HASHES:
parser.error( parser.error(
"Allowed hash algorithms for {} are {}.".format( # noqa "Allowed hash algorithms for {} are {}.".format(
opt_str, ", ".join(STRONG_HASHES) opt_str, ", ".join(STRONG_HASHES)
) )
) )

View file

@ -229,7 +229,7 @@ class ConfigOptionParser(CustomOptionParser):
val = strtobool(val) val = strtobool(val)
except ValueError: except ValueError:
self.error( self.error(
"{} is not a valid value for {} option, " # noqa "{} is not a valid value for {} option, "
"please specify a boolean value like yes/no, " "please specify a boolean value like yes/no, "
"true/false or 1/0 instead.".format(val, key) "true/false or 1/0 instead.".format(val, key)
) )
@ -240,7 +240,7 @@ class ConfigOptionParser(CustomOptionParser):
val = int(val) val = int(val)
if not isinstance(val, int) or val < 0: if not isinstance(val, int) or val < 0:
self.error( self.error(
"{} is not a valid value for {} option, " # noqa "{} is not a valid value for {} option, "
"please instead specify either a non-negative integer " "please instead specify either a non-negative integer "
"or a boolean value like yes/no or false/true " "or a boolean value like yes/no or false/true "
"which is equivalent to 1/0.".format(val, key) "which is equivalent to 1/0.".format(val, key)

View file

@ -58,12 +58,9 @@ def _create_truststore_ssl_context() -> Optional["SSLContext"]:
return None return None
try: try:
import truststore from pip._vendor import truststore
except ImportError: except ImportError as e:
raise CommandError( raise CommandError(f"The truststore feature is unavailable: {e}")
"To use the truststore feature, 'truststore' must be installed into "
"pip's current environment."
)
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@ -123,7 +120,7 @@ class SessionCommandMixin(CommandContextMixIn):
ssl_context = None ssl_context = None
session = PipSession( session = PipSession(
cache=os.path.join(cache_dir, "http") if cache_dir else None, cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
retries=retries if retries is not None else options.retries, retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts, trusted_hosts=options.trusted_hosts,
index_urls=self._get_index_urls(options), index_urls=self._get_index_urls(options),
@ -268,7 +265,7 @@ class RequirementCommand(IndexGroupCommand):
if "legacy-resolver" in options.deprecated_features_enabled: if "legacy-resolver" in options.deprecated_features_enabled:
return "legacy" return "legacy"
return "2020-resolver" return "resolvelib"
@classmethod @classmethod
def make_requirement_preparer( def make_requirement_preparer(
@ -287,9 +284,10 @@ class RequirementCommand(IndexGroupCommand):
""" """
temp_build_dir_path = temp_build_dir.path temp_build_dir_path = temp_build_dir.path
assert temp_build_dir_path is not None assert temp_build_dir_path is not None
legacy_resolver = False
resolver_variant = cls.determine_resolver_variant(options) resolver_variant = cls.determine_resolver_variant(options)
if resolver_variant == "2020-resolver": if resolver_variant == "resolvelib":
lazy_wheel = "fast-deps" in options.features_enabled lazy_wheel = "fast-deps" in options.features_enabled
if lazy_wheel: if lazy_wheel:
logger.warning( logger.warning(
@ -300,6 +298,7 @@ class RequirementCommand(IndexGroupCommand):
"production." "production."
) )
else: else:
legacy_resolver = True
lazy_wheel = False lazy_wheel = False
if "fast-deps" in options.features_enabled: if "fast-deps" in options.features_enabled:
logger.warning( logger.warning(
@ -320,6 +319,7 @@ class RequirementCommand(IndexGroupCommand):
use_user_site=use_user_site, use_user_site=use_user_site,
lazy_wheel=lazy_wheel, lazy_wheel=lazy_wheel,
verbosity=verbosity, verbosity=verbosity,
legacy_resolver=legacy_resolver,
) )
@classmethod @classmethod
@ -349,7 +349,7 @@ class RequirementCommand(IndexGroupCommand):
# The long import name and duplicated invocation is needed to convince # The long import name and duplicated invocation is needed to convince
# Mypy into correctly typechecking. Otherwise it would complain the # Mypy into correctly typechecking. Otherwise it would complain the
# "Resolver" class being redefined. # "Resolver" class being redefined.
if resolver_variant == "2020-resolver": if resolver_variant == "resolvelib":
import pip._internal.resolution.resolvelib.resolver import pip._internal.resolution.resolvelib.resolver
return pip._internal.resolution.resolvelib.resolver.Resolver( return pip._internal.resolution.resolvelib.resolver.Resolver(

View file

@ -3,10 +3,10 @@ import textwrap
from optparse import Values from optparse import Values
from typing import Any, List from typing import Any, List
import pip._internal.utils.filesystem as filesystem
from pip._internal.cli.base_command import Command from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.exceptions import CommandError, PipError from pip._internal.exceptions import CommandError, PipError
from pip._internal.utils import filesystem
from pip._internal.utils.logging import getLogger from pip._internal.utils.logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
@ -93,24 +93,30 @@ class CacheCommand(Command):
num_http_files = len(self._find_http_files(options)) num_http_files = len(self._find_http_files(options))
num_packages = len(self._find_wheels(options, "*")) num_packages = len(self._find_wheels(options, "*"))
http_cache_location = self._cache_dir(options, "http") http_cache_location = self._cache_dir(options, "http-v2")
old_http_cache_location = self._cache_dir(options, "http")
wheels_cache_location = self._cache_dir(options, "wheels") wheels_cache_location = self._cache_dir(options, "wheels")
http_cache_size = filesystem.format_directory_size(http_cache_location) http_cache_size = filesystem.format_size(
filesystem.directory_size(http_cache_location)
+ filesystem.directory_size(old_http_cache_location)
)
wheels_cache_size = filesystem.format_directory_size(wheels_cache_location) wheels_cache_size = filesystem.format_directory_size(wheels_cache_location)
message = ( message = (
textwrap.dedent( textwrap.dedent(
""" """
Package index page cache location: {http_cache_location} Package index page cache location (pip v23.3+): {http_cache_location}
Package index page cache location (older pips): {old_http_cache_location}
Package index page cache size: {http_cache_size} Package index page cache size: {http_cache_size}
Number of HTTP files: {num_http_files} Number of HTTP files: {num_http_files}
Locally built wheels location: {wheels_cache_location} Locally built wheels location: {wheels_cache_location}
Locally built wheels size: {wheels_cache_size} Locally built wheels size: {wheels_cache_size}
Number of locally built wheels: {package_count} Number of locally built wheels: {package_count}
""" """ # noqa: E501
) )
.format( .format(
http_cache_location=http_cache_location, http_cache_location=http_cache_location,
old_http_cache_location=old_http_cache_location,
http_cache_size=http_cache_size, http_cache_size=http_cache_size,
num_http_files=num_http_files, num_http_files=num_http_files,
wheels_cache_location=wheels_cache_location, wheels_cache_location=wheels_cache_location,
@ -151,14 +157,8 @@ class CacheCommand(Command):
logger.info("\n".join(sorted(results))) logger.info("\n".join(sorted(results)))
def format_for_abspath(self, files: List[str]) -> None: def format_for_abspath(self, files: List[str]) -> None:
if not files: if files:
return logger.info("\n".join(sorted(files)))
results = []
for filename in files:
results.append(filename)
logger.info("\n".join(sorted(results)))
def remove_cache_items(self, options: Values, args: List[Any]) -> None: def remove_cache_items(self, options: Values, args: List[Any]) -> None:
if len(args) > 1: if len(args) > 1:
@ -195,8 +195,11 @@ class CacheCommand(Command):
return os.path.join(options.cache_dir, subdir) return os.path.join(options.cache_dir, subdir)
def _find_http_files(self, options: Values) -> List[str]: def _find_http_files(self, options: Values) -> List[str]:
http_dir = self._cache_dir(options, "http") old_http_dir = self._cache_dir(options, "http")
return filesystem.find_files(http_dir, "*") new_http_dir = self._cache_dir(options, "http-v2")
return filesystem.find_files(old_http_dir, "*") + filesystem.find_files(
new_http_dir, "*"
)
def _find_wheels(self, options: Values, pattern: str) -> List[str]: def _find_wheels(self, options: Values, pattern: str) -> List[str]:
wheel_dir = self._cache_dir(options, "wheels") wheel_dir = self._cache_dir(options, "wheels")

View file

@ -7,6 +7,7 @@ from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.operations.check import ( from pip._internal.operations.check import (
check_package_set, check_package_set,
create_package_set_from_installed, create_package_set_from_installed,
warn_legacy_versions_and_specifiers,
) )
from pip._internal.utils.misc import write_output from pip._internal.utils.misc import write_output
@ -21,6 +22,7 @@ class CheckCommand(Command):
def run(self, options: Values, args: List[str]) -> int: def run(self, options: Values, args: List[str]) -> int:
package_set, parsing_probs = create_package_set_from_installed() package_set, parsing_probs = create_package_set_from_installed()
warn_legacy_versions_and_specifiers(package_set)
missing, conflicting = check_package_set(package_set) missing, conflicting = check_package_set(package_set)
for project_name in missing: for project_name in missing:

View file

@ -22,15 +22,10 @@ COMPLETION_SCRIPTS = {
complete -o default -F _pip_completion {prog} complete -o default -F _pip_completion {prog}
""", """,
"zsh": """ "zsh": """
function _pip_completion {{ #compdef -P pip[0-9.]#
local words cword compadd $( COMP_WORDS="$words[*]" \\
read -Ac words COMP_CWORD=$((CURRENT-1)) \\
read -cn cword PIP_AUTO_COMPLETE=1 $words[1] 2>/dev/null )
reply=( $( COMP_WORDS="$words[*]" \\
COMP_CWORD=$(( cword-1 )) \\
PIP_AUTO_COMPLETE=1 $words[1] 2>/dev/null ))
}}
compctl -K _pip_completion {prog}
""", """,
"fish": """ "fish": """
function __fish_complete_pip function __fish_complete_pip

View file

@ -46,22 +46,29 @@ def create_vendor_txt_map() -> Dict[str, str]:
return dict(line.split("==", 1) for line in lines) return dict(line.split("==", 1) for line in lines)
def get_module_from_module_name(module_name: str) -> ModuleType: def get_module_from_module_name(module_name: str) -> Optional[ModuleType]:
# Module name can be uppercase in vendor.txt for some reason... # Module name can be uppercase in vendor.txt for some reason...
module_name = module_name.lower().replace("-", "_") module_name = module_name.lower().replace("-", "_")
# PATCH: setuptools is actually only pkg_resources. # PATCH: setuptools is actually only pkg_resources.
if module_name == "setuptools": if module_name == "setuptools":
module_name = "pkg_resources" module_name = "pkg_resources"
__import__(f"pip._vendor.{module_name}", globals(), locals(), level=0) try:
return getattr(pip._vendor, module_name) __import__(f"pip._vendor.{module_name}", globals(), locals(), level=0)
return getattr(pip._vendor, module_name)
except ImportError:
# We allow 'truststore' to fail to import due
# to being unavailable on Python 3.9 and earlier.
if module_name == "truststore" and sys.version_info < (3, 10):
return None
raise
def get_vendor_version_from_module(module_name: str) -> Optional[str]: def get_vendor_version_from_module(module_name: str) -> Optional[str]:
module = get_module_from_module_name(module_name) module = get_module_from_module_name(module_name)
version = getattr(module, "__version__", None) version = getattr(module, "__version__", None)
if not version: if module and not version:
# Try to find version in debundled module info. # Try to find version in debundled module info.
assert module.__file__ is not None assert module.__file__ is not None
env = get_environment([os.path.dirname(module.__file__)]) env = get_environment([os.path.dirname(module.__file__)])
@ -105,7 +112,7 @@ def show_tags(options: Values) -> None:
tag_limit = 10 tag_limit = 10
target_python = make_target_python(options) target_python = make_target_python(options)
tags = target_python.get_tags() tags = target_python.get_sorted_tags()
# Display the target options that were explicitly provided. # Display the target options that were explicitly provided.
formatted_target = target_python.format_given() formatted_target = target_python.format_given()
@ -134,10 +141,7 @@ def show_tags(options: Values) -> None:
def ca_bundle_info(config: Configuration) -> str: def ca_bundle_info(config: Configuration) -> str:
levels = set() levels = {key.split(".", 1)[0] for key, _ in config.items()}
for key, _ in config.items():
levels.add(key.split(".")[0])
if not levels: if not levels:
return "Not specified" return "Not specified"

View file

@ -137,6 +137,10 @@ class DownloadCommand(RequirementCommand):
assert req.name is not None assert req.name is not None
preparer.save_linked_requirement(req) preparer.save_linked_requirement(req)
downloaded.append(req.name) downloaded.append(req.name)
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
requirement_set.warn_legacy_versions_and_specifiers()
if downloaded: if downloaded:
write_output("Successfully downloaded %s", " ".join(downloaded)) write_output("Successfully downloaded %s", " ".join(downloaded))

View file

@ -1,6 +1,6 @@
import sys import sys
from optparse import Values from optparse import Values
from typing import List from typing import AbstractSet, List
from pip._internal.cli import cmdoptions from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command from pip._internal.cli.base_command import Command
@ -8,7 +8,18 @@ from pip._internal.cli.status_codes import SUCCESS
from pip._internal.operations.freeze import freeze from pip._internal.operations.freeze import freeze
from pip._internal.utils.compat import stdlib_pkgs from pip._internal.utils.compat import stdlib_pkgs
DEV_PKGS = {"pip", "setuptools", "distribute", "wheel"}
def _should_suppress_build_backends() -> bool:
return sys.version_info < (3, 12)
def _dev_pkgs() -> AbstractSet[str]:
pkgs = {"pip"}
if _should_suppress_build_backends():
pkgs |= {"setuptools", "distribute", "wheel"}
return pkgs
class FreezeCommand(Command): class FreezeCommand(Command):
@ -61,7 +72,7 @@ class FreezeCommand(Command):
action="store_true", action="store_true",
help=( help=(
"Do not skip these packages in the output:" "Do not skip these packages in the output:"
" {}".format(", ".join(DEV_PKGS)) " {}".format(", ".join(_dev_pkgs()))
), ),
) )
self.cmd_opts.add_option( self.cmd_opts.add_option(
@ -77,7 +88,7 @@ class FreezeCommand(Command):
def run(self, options: Values, args: List[str]) -> int: def run(self, options: Values, args: List[str]) -> int:
skip = set(stdlib_pkgs) skip = set(stdlib_pkgs)
if not options.freeze_all: if not options.freeze_all:
skip.update(DEV_PKGS) skip.update(_dev_pkgs())
if options.excludes: if options.excludes:
skip.update(options.excludes) skip.update(options.excludes)

View file

@ -387,6 +387,9 @@ class InstallCommand(RequirementCommand):
json.dump(report.to_dict(), f, indent=2, ensure_ascii=False) json.dump(report.to_dict(), f, indent=2, ensure_ascii=False)
if options.dry_run: if options.dry_run:
# In non dry-run mode, the legacy versions and specifiers check
# will be done as part of conflict detection.
requirement_set.warn_legacy_versions_and_specifiers()
would_install_items = sorted( would_install_items = sorted(
(r.metadata["name"], r.metadata["version"]) (r.metadata["name"], r.metadata["version"])
for r in requirement_set.requirements_to_install for r in requirement_set.requirements_to_install
@ -498,7 +501,7 @@ class InstallCommand(RequirementCommand):
show_traceback, show_traceback,
options.use_user_site, options.use_user_site,
) )
logger.error(message, exc_info=show_traceback) # noqa logger.error(message, exc_info=show_traceback)
return ERROR return ERROR
@ -592,7 +595,7 @@ class InstallCommand(RequirementCommand):
"source of the following dependency conflicts." "source of the following dependency conflicts."
) )
else: else:
assert resolver_variant == "2020-resolver" assert resolver_variant == "resolvelib"
parts.append( parts.append(
"pip's dependency resolver does not currently take into account " "pip's dependency resolver does not currently take into account "
"all the packages that are installed. This behaviour is the " "all the packages that are installed. This behaviour is the "
@ -625,7 +628,7 @@ class InstallCommand(RequirementCommand):
requirement=req, requirement=req,
dep_name=dep_name, dep_name=dep_name,
dep_version=dep_version, dep_version=dep_version,
you=("you" if resolver_variant == "2020-resolver" else "you'll"), you=("you" if resolver_variant == "resolvelib" else "you'll"),
) )
parts.append(message) parts.append(message)

View file

@ -103,7 +103,10 @@ class ListCommand(IndexGroupCommand):
dest="list_format", dest="list_format",
default="columns", default="columns",
choices=("columns", "freeze", "json"), choices=("columns", "freeze", "json"),
help="Select the output format among: columns (default), freeze, or json", help=(
"Select the output format among: columns (default), freeze, or json. "
"The 'freeze' format cannot be used with the --outdated option."
),
) )
self.cmd_opts.add_option( self.cmd_opts.add_option(
@ -157,7 +160,7 @@ class ListCommand(IndexGroupCommand):
if options.outdated and options.list_format == "freeze": if options.outdated and options.list_format == "freeze":
raise CommandError( raise CommandError(
"List format 'freeze' can not be used with the --outdated option." "List format 'freeze' cannot be used with the --outdated option."
) )
cmdoptions.check_list_path_option(options) cmdoptions.check_list_path_option(options)
@ -294,7 +297,7 @@ class ListCommand(IndexGroupCommand):
# Create and add a separator. # Create and add a separator.
if len(data) > 0: if len(data) > 0:
pkg_strings.insert(1, " ".join(map(lambda x: "-" * x, sizes))) pkg_strings.insert(1, " ".join("-" * x for x in sizes))
for val in pkg_strings: for val in pkg_strings:
write_output(val) write_output(val)

View file

@ -153,6 +153,9 @@ class WheelCommand(RequirementCommand):
elif should_build_for_wheel_command(req): elif should_build_for_wheel_command(req):
reqs_to_build.append(req) reqs_to_build.append(req)
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
requirement_set.warn_legacy_versions_and_specifiers()
# build wheels # build wheels
build_successes, build_failures = build( build_successes, build_failures = build(
reqs_to_build, reqs_to_build,

View file

@ -210,8 +210,15 @@ class Configuration:
# Ensure directory exists. # Ensure directory exists.
ensure_dir(os.path.dirname(fname)) ensure_dir(os.path.dirname(fname))
with open(fname, "w") as f: # Ensure directory's permission(need to be writeable)
parser.write(f) try:
with open(fname, "w") as f:
parser.write(f)
except OSError as error:
raise ConfigurationError(
f"An error occurred while writing to the configuration file "
f"{fname}: {error}"
)
# #
# Private routines # Private routines

View file

@ -1,4 +1,5 @@
import abc import abc
from typing import Optional
from pip._internal.index.package_finder import PackageFinder from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata.base import BaseDistribution from pip._internal.metadata.base import BaseDistribution
@ -19,12 +20,23 @@ class AbstractDistribution(metaclass=abc.ABCMeta):
- we must be able to create a Distribution object exposing the - we must be able to create a Distribution object exposing the
above metadata. above metadata.
- if we need to do work in the build tracker, we must be able to generate a unique
string to identify the requirement in the build tracker.
""" """
def __init__(self, req: InstallRequirement) -> None: def __init__(self, req: InstallRequirement) -> None:
super().__init__() super().__init__()
self.req = req self.req = req
@abc.abstractproperty
def build_tracker_id(self) -> Optional[str]:
"""A string that uniquely identifies this requirement to the build tracker.
If None, then this dist has no work to do in the build tracker, and
``.prepare_distribution_metadata()`` will not be called."""
raise NotImplementedError()
@abc.abstractmethod @abc.abstractmethod
def get_metadata_distribution(self) -> BaseDistribution: def get_metadata_distribution(self) -> BaseDistribution:
raise NotImplementedError() raise NotImplementedError()

View file

@ -1,3 +1,5 @@
from typing import Optional
from pip._internal.distributions.base import AbstractDistribution from pip._internal.distributions.base import AbstractDistribution
from pip._internal.index.package_finder import PackageFinder from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution from pip._internal.metadata import BaseDistribution
@ -10,6 +12,10 @@ class InstalledDistribution(AbstractDistribution):
been computed. been computed.
""" """
@property
def build_tracker_id(self) -> Optional[str]:
return None
def get_metadata_distribution(self) -> BaseDistribution: def get_metadata_distribution(self) -> BaseDistribution:
assert self.req.satisfied_by is not None, "not actually installed" assert self.req.satisfied_by is not None, "not actually installed"
return self.req.satisfied_by return self.req.satisfied_by

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import Iterable, Set, Tuple from typing import Iterable, Optional, Set, Tuple
from pip._internal.build_env import BuildEnvironment from pip._internal.build_env import BuildEnvironment
from pip._internal.distributions.base import AbstractDistribution from pip._internal.distributions.base import AbstractDistribution
@ -18,6 +18,12 @@ class SourceDistribution(AbstractDistribution):
generated, either using PEP 517 or using the legacy `setup.py egg_info`. generated, either using PEP 517 or using the legacy `setup.py egg_info`.
""" """
@property
def build_tracker_id(self) -> Optional[str]:
"""Identify this requirement uniquely by its link."""
assert self.req.link
return self.req.link.url_without_fragment
def get_metadata_distribution(self) -> BaseDistribution: def get_metadata_distribution(self) -> BaseDistribution:
return self.req.get_dist() return self.req.get_dist()

View file

@ -1,3 +1,5 @@
from typing import Optional
from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.distributions.base import AbstractDistribution from pip._internal.distributions.base import AbstractDistribution
@ -15,6 +17,10 @@ class WheelDistribution(AbstractDistribution):
This does not need any preparation as wheels can be directly unpacked. This does not need any preparation as wheels can be directly unpacked.
""" """
@property
def build_tracker_id(self) -> Optional[str]:
return None
def get_metadata_distribution(self) -> BaseDistribution: def get_metadata_distribution(self) -> BaseDistribution:
"""Loads the metadata from the wheel file into memory and returns a """Loads the metadata from the wheel file into memory and returns a
Distribution that uses it, not relying on the wheel file or Distribution that uses it, not relying on the wheel file or

View file

@ -544,7 +544,7 @@ class HashMissing(HashError):
# so the output can be directly copied into the requirements file. # so the output can be directly copied into the requirements file.
package = ( package = (
self.req.original_link self.req.original_link
if self.req.original_link if self.req.is_direct
# In case someone feeds something downright stupid # In case someone feeds something downright stupid
# to InstallRequirement's constructor. # to InstallRequirement's constructor.
else getattr(self.req, "req", None) else getattr(self.req, "req", None)

View file

@ -198,7 +198,7 @@ class LinkEvaluator:
reason = f"wrong project name (not {self.project_name})" reason = f"wrong project name (not {self.project_name})"
return (LinkType.different_project, reason) return (LinkType.different_project, reason)
supported_tags = self._target_python.get_tags() supported_tags = self._target_python.get_unsorted_tags()
if not wheel.supported(supported_tags): if not wheel.supported(supported_tags):
# Include the wheel's tags in the reason string to # Include the wheel's tags in the reason string to
# simplify troubleshooting compatibility issues. # simplify troubleshooting compatibility issues.
@ -414,7 +414,7 @@ class CandidateEvaluator:
if specifier is None: if specifier is None:
specifier = specifiers.SpecifierSet() specifier = specifiers.SpecifierSet()
supported_tags = target_python.get_tags() supported_tags = target_python.get_sorted_tags()
return cls( return cls(
project_name=project_name, project_name=project_name,

View file

@ -89,7 +89,7 @@ def distutils_scheme(
# finalize_options(); we only want to override here if the user # finalize_options(); we only want to override here if the user
# has explicitly requested it hence going back to the config # has explicitly requested it hence going back to the config
if "install_lib" in d.get_option_dict("install"): if "install_lib" in d.get_option_dict("install"):
scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib)) scheme.update({"purelib": i.install_lib, "platlib": i.install_lib})
if running_under_virtualenv(): if running_under_virtualenv():
if home: if home:

View file

@ -9,7 +9,7 @@ from pip._internal.utils.misc import strtobool
from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Protocol from typing import Literal, Protocol
else: else:
Protocol = object Protocol = object
@ -50,6 +50,7 @@ def _should_use_importlib_metadata() -> bool:
class Backend(Protocol): class Backend(Protocol):
NAME: 'Literal["importlib", "pkg_resources"]'
Distribution: Type[BaseDistribution] Distribution: Type[BaseDistribution]
Environment: Type[BaseEnvironment] Environment: Type[BaseEnvironment]

View file

@ -24,7 +24,7 @@ from typing import (
from pip._vendor.packaging.requirements import Requirement from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.utils import NormalizedName from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import LegacyVersion, Version from pip._vendor.packaging.version import LegacyVersion, Version
from pip._internal.exceptions import NoneMetadataError from pip._internal.exceptions import NoneMetadataError
@ -37,7 +37,6 @@ from pip._internal.models.direct_url import (
from pip._internal.utils.compat 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.egg_link import egg_link_path_from_sys_path
from pip._internal.utils.misc import is_local, normalize_path from pip._internal.utils.misc import is_local, normalize_path
from pip._internal.utils.packaging import safe_extra
from pip._internal.utils.urls import url_to_path from pip._internal.utils.urls import url_to_path
from ._json import msg_to_json from ._json import msg_to_json
@ -460,6 +459,19 @@ class BaseDistribution(Protocol):
For modern .dist-info distributions, this is the collection of For modern .dist-info distributions, this is the collection of
"Provides-Extra:" entries in distribution metadata. "Provides-Extra:" entries in distribution metadata.
The return value of this function is not particularly useful other than
display purposes due to backward compatibility issues and the extra
names being poorly normalized prior to PEP 685. If you want to perform
logic operations on extras, use :func:`is_extra_provided` instead.
"""
raise NotImplementedError()
def is_extra_provided(self, extra: str) -> bool:
"""Check whether an extra is provided by this distribution.
This is needed mostly for compatibility issues with pkg_resources not
following the extra normalization rules defined in PEP 685.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -537,10 +549,11 @@ class BaseDistribution(Protocol):
"""Get extras from the egg-info directory.""" """Get extras from the egg-info directory."""
known_extras = {""} known_extras = {""}
for entry in self._iter_requires_txt_entries(): for entry in self._iter_requires_txt_entries():
if entry.extra in known_extras: extra = canonicalize_name(entry.extra)
if extra in known_extras:
continue continue
known_extras.add(entry.extra) known_extras.add(extra)
yield entry.extra yield extra
def _iter_egg_info_dependencies(self) -> Iterable[str]: def _iter_egg_info_dependencies(self) -> Iterable[str]:
"""Get distribution dependencies from the egg-info directory. """Get distribution dependencies from the egg-info directory.
@ -556,10 +569,11 @@ class BaseDistribution(Protocol):
all currently available PEP 517 backends, although not standardized. all currently available PEP 517 backends, although not standardized.
""" """
for entry in self._iter_requires_txt_entries(): for entry in self._iter_requires_txt_entries():
if entry.extra and entry.marker: extra = canonicalize_name(entry.extra)
marker = f'({entry.marker}) and extra == "{safe_extra(entry.extra)}"' if extra and entry.marker:
elif entry.extra: marker = f'({entry.marker}) and extra == "{extra}"'
marker = f'extra == "{safe_extra(entry.extra)}"' elif extra:
marker = f'extra == "{extra}"'
elif entry.marker: elif entry.marker:
marker = entry.marker marker = entry.marker
else: else:

View file

@ -1,4 +1,6 @@
from ._dists import Distribution from ._dists import Distribution
from ._envs import Environment from ._envs import Environment
__all__ = ["Distribution", "Environment"] __all__ = ["NAME", "Distribution", "Environment"]
NAME = "importlib"

View file

@ -27,7 +27,6 @@ from pip._internal.metadata.base import (
Wheel, Wheel,
) )
from pip._internal.utils.misc import normalize_path from pip._internal.utils.misc import normalize_path
from pip._internal.utils.packaging import safe_extra
from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
@ -208,12 +207,16 @@ class Distribution(BaseDistribution):
return cast(email.message.Message, self._dist.metadata) return cast(email.message.Message, self._dist.metadata)
def iter_provided_extras(self) -> Iterable[str]: def iter_provided_extras(self) -> Iterable[str]:
return ( return self.metadata.get_all("Provides-Extra", [])
safe_extra(extra) for extra in self.metadata.get_all("Provides-Extra", [])
def is_extra_provided(self, extra: str) -> bool:
return any(
canonicalize_name(provided_extra) == canonicalize_name(extra)
for provided_extra in self.metadata.get_all("Provides-Extra", [])
) )
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]: def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
contexts: Sequence[Dict[str, str]] = [{"extra": safe_extra(e)} for e in extras] contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]
for req_string in self.metadata.get_all("Requires-Dist", []): for req_string in self.metadata.get_all("Requires-Dist", []):
req = Requirement(req_string) req = Requirement(req_string)
if not req.marker: if not req.marker:

View file

@ -151,7 +151,7 @@ def _emit_egg_deprecation(location: Optional[str]) -> None:
deprecated( deprecated(
reason=f"Loading egg at {location} is deprecated.", reason=f"Loading egg at {location} is deprecated.",
replacement="to use pip for package installation.", replacement="to use pip for package installation.",
gone_in=None, gone_in="23.3",
) )
@ -174,7 +174,7 @@ class Environment(BaseEnvironment):
for location in self._paths: for location in self._paths:
yield from finder.find(location) yield from finder.find(location)
for dist in finder.find_eggs(location): for dist in finder.find_eggs(location):
# _emit_egg_deprecation(dist.location) # TODO: Enable this. _emit_egg_deprecation(dist.location)
yield dist yield dist
# This must go last because that's how pkg_resources tie-breaks. # This must go last because that's how pkg_resources tie-breaks.
yield from finder.find_linked(location) yield from finder.find_linked(location)

View file

@ -24,8 +24,12 @@ from .base import (
Wheel, Wheel,
) )
__all__ = ["NAME", "Distribution", "Environment"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
NAME = "pkg_resources"
class EntryPoint(NamedTuple): class EntryPoint(NamedTuple):
name: str name: str
@ -212,12 +216,16 @@ class Distribution(BaseDistribution):
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]: def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
if extras: # pkg_resources raises on invalid extras, so we sanitize. if extras: # pkg_resources raises on invalid extras, so we sanitize.
extras = frozenset(extras).intersection(self._dist.extras) extras = frozenset(pkg_resources.safe_extra(e) for e in extras)
extras = extras.intersection(self._dist.extras)
return self._dist.requires(extras) return self._dist.requires(extras)
def iter_provided_extras(self) -> Iterable[str]: def iter_provided_extras(self) -> Iterable[str]:
return self._dist.extras return self._dist.extras
def is_extra_provided(self, extra: str) -> bool:
return pkg_resources.safe_extra(extra) in self._dist.extras
class Environment(BaseEnvironment): class Environment(BaseEnvironment):
def __init__(self, ws: pkg_resources.WorkingSet) -> None: def __init__(self, ws: pkg_resources.WorkingSet) -> None:

View file

@ -22,7 +22,10 @@ class InstallationReport:
# is_direct is true if the requirement was a direct URL reference (which # is_direct is true if the requirement was a direct URL reference (which
# includes editable requirements), and false if the requirement was # includes editable requirements), and false if the requirement was
# downloaded from a PEP 503 index or --find-links. # downloaded from a PEP 503 index or --find-links.
"is_direct": bool(ireq.original_link), "is_direct": ireq.is_direct,
# is_yanked is true if the requirement was yanked from the index, but
# was still selected by pip to conform to PEP 592.
"is_yanked": ireq.link.is_yanked if ireq.link else False,
# requested is true if the requirement was specified by the user (aka # requested is true if the requirement was specified by the user (aka
# top level requirement), and false if it was installed as a dependency of a # top level requirement), and false if it was installed as a dependency of a
# requirement. https://peps.python.org/pep-0376/#requested # requirement. https://peps.python.org/pep-0376/#requested
@ -33,7 +36,7 @@ class InstallationReport:
} }
if ireq.user_supplied and ireq.extras: if ireq.user_supplied and ireq.extras:
# For top level requirements, the list of requested extras, if any. # For top level requirements, the list of requested extras, if any.
res["requested_extras"] = list(sorted(ireq.extras)) res["requested_extras"] = sorted(ireq.extras)
return res return res
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:

View file

@ -69,18 +69,6 @@ class LinkHash:
def __post_init__(self) -> None: def __post_init__(self) -> None:
assert self.name in _SUPPORTED_HASHES assert self.name in _SUPPORTED_HASHES
@classmethod
def parse_pep658_hash(cls, dist_info_metadata: str) -> Optional["LinkHash"]:
"""Parse a PEP 658 data-dist-info-metadata hash."""
if dist_info_metadata == "true":
return None
name, sep, value = dist_info_metadata.partition("=")
if not sep:
return None
if name not in _SUPPORTED_HASHES:
return None
return cls(name=name, value=value)
@classmethod @classmethod
@functools.lru_cache(maxsize=None) @functools.lru_cache(maxsize=None)
def find_hash_url_fragment(cls, url: str) -> Optional["LinkHash"]: def find_hash_url_fragment(cls, url: str) -> Optional["LinkHash"]:
@ -107,6 +95,28 @@ class LinkHash:
return hashes.is_hash_allowed(self.name, hex_digest=self.value) return hashes.is_hash_allowed(self.name, hex_digest=self.value)
@dataclass(frozen=True)
class MetadataFile:
"""Information about a core metadata file associated with a distribution."""
hashes: Optional[Dict[str, str]]
def __post_init__(self) -> None:
if self.hashes is not None:
assert all(name in _SUPPORTED_HASHES for name in self.hashes)
def supported_hashes(hashes: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
# Remove any unsupported hash types from the mapping. If this leaves no
# supported hashes, return None
if hashes is None:
return None
hashes = {n: v for n, v in hashes.items() if n in _SUPPORTED_HASHES}
if not hashes:
return None
return hashes
def _clean_url_path_part(part: str) -> str: def _clean_url_path_part(part: str) -> str:
""" """
Clean a "part" of a URL path (i.e. after splitting on "@" characters). Clean a "part" of a URL path (i.e. after splitting on "@" characters).
@ -179,7 +189,7 @@ class Link(KeyBasedCompareMixin):
"comes_from", "comes_from",
"requires_python", "requires_python",
"yanked_reason", "yanked_reason",
"dist_info_metadata", "metadata_file_data",
"cache_link_parsing", "cache_link_parsing",
"egg_fragment", "egg_fragment",
] ]
@ -190,7 +200,7 @@ class Link(KeyBasedCompareMixin):
comes_from: Optional[Union[str, "IndexContent"]] = None, comes_from: Optional[Union[str, "IndexContent"]] = None,
requires_python: Optional[str] = None, requires_python: Optional[str] = None,
yanked_reason: Optional[str] = None, yanked_reason: Optional[str] = None,
dist_info_metadata: Optional[str] = None, metadata_file_data: Optional[MetadataFile] = None,
cache_link_parsing: bool = True, cache_link_parsing: bool = True,
hashes: Optional[Mapping[str, str]] = None, hashes: Optional[Mapping[str, str]] = None,
) -> None: ) -> None:
@ -208,11 +218,10 @@ class Link(KeyBasedCompareMixin):
a simple repository HTML link. If the file has been yanked but a simple repository HTML link. If the file has been yanked but
no reason was provided, this should be the empty string. See no reason was provided, this should be the empty string. See
PEP 592 for more information and the specification. PEP 592 for more information and the specification.
:param dist_info_metadata: the metadata attached to the file, or None if no such :param metadata_file_data: the metadata attached to the file, or None if
metadata is provided. This is the value of the "data-dist-info-metadata" no such metadata is provided. This argument, if not None, indicates
attribute, if present, in a simple repository HTML link. This may be parsed that a separate metadata file exists, and also optionally supplies
into its own `Link` by `self.metadata_link()`. See PEP 658 for more hashes for that file.
information and the specification.
:param cache_link_parsing: A flag that is used elsewhere to determine :param cache_link_parsing: A flag that is used elsewhere to determine
whether resources retrieved from this link should be cached. PyPI whether resources retrieved from this link should be cached. PyPI
URLs should generally have this set to False, for example. URLs should generally have this set to False, for example.
@ -220,6 +229,10 @@ class Link(KeyBasedCompareMixin):
determine the validity of a download. determine the validity of a download.
""" """
# The comes_from, requires_python, and metadata_file_data arguments are
# only used by classmethods of this class, and are not used in client
# code directly.
# url can be a UNC windows share # url can be a UNC windows share
if url.startswith("\\\\"): if url.startswith("\\\\"):
url = path_to_url(url) url = path_to_url(url)
@ -239,7 +252,7 @@ class Link(KeyBasedCompareMixin):
self.comes_from = comes_from self.comes_from = comes_from
self.requires_python = requires_python if requires_python else None self.requires_python = requires_python if requires_python else None
self.yanked_reason = yanked_reason self.yanked_reason = yanked_reason
self.dist_info_metadata = dist_info_metadata self.metadata_file_data = metadata_file_data
super().__init__(key=url, defining_class=Link) super().__init__(key=url, defining_class=Link)
@ -262,9 +275,25 @@ class Link(KeyBasedCompareMixin):
url = _ensure_quoted_url(urllib.parse.urljoin(page_url, file_url)) url = _ensure_quoted_url(urllib.parse.urljoin(page_url, file_url))
pyrequire = file_data.get("requires-python") pyrequire = file_data.get("requires-python")
yanked_reason = file_data.get("yanked") yanked_reason = file_data.get("yanked")
dist_info_metadata = file_data.get("dist-info-metadata")
hashes = file_data.get("hashes", {}) hashes = file_data.get("hashes", {})
# PEP 714: Indexes must use the name core-metadata, but
# clients should support the old name as a fallback for compatibility.
metadata_info = file_data.get("core-metadata")
if metadata_info is None:
metadata_info = file_data.get("dist-info-metadata")
# The metadata info value may be a boolean, or a dict of hashes.
if isinstance(metadata_info, dict):
# The file exists, and hashes have been supplied
metadata_file_data = MetadataFile(supported_hashes(metadata_info))
elif metadata_info:
# The file exists, but there are no hashes
metadata_file_data = MetadataFile(None)
else:
# False or not present: the file does not exist
metadata_file_data = None
# The Link.yanked_reason expects an empty string instead of a boolean. # The Link.yanked_reason expects an empty string instead of a boolean.
if yanked_reason and not isinstance(yanked_reason, str): if yanked_reason and not isinstance(yanked_reason, str):
yanked_reason = "" yanked_reason = ""
@ -278,7 +307,7 @@ class Link(KeyBasedCompareMixin):
requires_python=pyrequire, requires_python=pyrequire,
yanked_reason=yanked_reason, yanked_reason=yanked_reason,
hashes=hashes, hashes=hashes,
dist_info_metadata=dist_info_metadata, metadata_file_data=metadata_file_data,
) )
@classmethod @classmethod
@ -298,14 +327,39 @@ class Link(KeyBasedCompareMixin):
url = _ensure_quoted_url(urllib.parse.urljoin(base_url, href)) url = _ensure_quoted_url(urllib.parse.urljoin(base_url, href))
pyrequire = anchor_attribs.get("data-requires-python") pyrequire = anchor_attribs.get("data-requires-python")
yanked_reason = anchor_attribs.get("data-yanked") yanked_reason = anchor_attribs.get("data-yanked")
dist_info_metadata = anchor_attribs.get("data-dist-info-metadata")
# PEP 714: Indexes must use the name data-core-metadata, but
# clients should support the old name as a fallback for compatibility.
metadata_info = anchor_attribs.get("data-core-metadata")
if metadata_info is None:
metadata_info = anchor_attribs.get("data-dist-info-metadata")
# The metadata info value may be the string "true", or a string of
# the form "hashname=hashval"
if metadata_info == "true":
# The file exists, but there are no hashes
metadata_file_data = MetadataFile(None)
elif metadata_info is None:
# The file does not exist
metadata_file_data = None
else:
# The file exists, and hashes have been supplied
hashname, sep, hashval = metadata_info.partition("=")
if sep == "=":
metadata_file_data = MetadataFile(supported_hashes({hashname: hashval}))
else:
# Error - data is wrong. Treat as no hashes supplied.
logger.debug(
"Index returned invalid data-dist-info-metadata value: %s",
metadata_info,
)
metadata_file_data = MetadataFile(None)
return cls( return cls(
url, url,
comes_from=page_url, comes_from=page_url,
requires_python=pyrequire, requires_python=pyrequire,
yanked_reason=yanked_reason, yanked_reason=yanked_reason,
dist_info_metadata=dist_info_metadata, metadata_file_data=metadata_file_data,
) )
def __str__(self) -> str: def __str__(self) -> str:
@ -407,17 +461,13 @@ class Link(KeyBasedCompareMixin):
return match.group(1) return match.group(1)
def metadata_link(self) -> Optional["Link"]: def metadata_link(self) -> Optional["Link"]:
"""Implementation of PEP 658 parsing.""" """Return a link to the associated core metadata file (if any)."""
# Note that Link.from_element() parsing the "data-dist-info-metadata" attribute if self.metadata_file_data is None:
# from an HTML anchor tag is typically how the Link.dist_info_metadata attribute
# gets set.
if self.dist_info_metadata is None:
return None return None
metadata_url = f"{self.url_without_fragment}.metadata" metadata_url = f"{self.url_without_fragment}.metadata"
metadata_link_hash = LinkHash.parse_pep658_hash(self.dist_info_metadata) if self.metadata_file_data.hashes is None:
if metadata_link_hash is None:
return Link(metadata_url) return Link(metadata_url)
return Link(metadata_url, hashes=metadata_link_hash.as_dict()) return Link(metadata_url, hashes=self.metadata_file_data.hashes)
def as_hashes(self) -> Hashes: def as_hashes(self) -> Hashes:
return Hashes({k: [v] for k, v in self._hashes.items()}) return Hashes({k: [v] for k, v in self._hashes.items()})

View file

@ -1,5 +1,5 @@
import sys import sys
from typing import List, Optional, Tuple from typing import List, Optional, Set, Tuple
from pip._vendor.packaging.tags import Tag from pip._vendor.packaging.tags import Tag
@ -22,6 +22,7 @@ class TargetPython:
"py_version", "py_version",
"py_version_info", "py_version_info",
"_valid_tags", "_valid_tags",
"_valid_tags_set",
] ]
def __init__( def __init__(
@ -61,8 +62,9 @@ class TargetPython:
self.py_version = py_version self.py_version = py_version
self.py_version_info = py_version_info self.py_version_info = py_version_info
# This is used to cache the return value of get_tags(). # This is used to cache the return value of get_(un)sorted_tags.
self._valid_tags: Optional[List[Tag]] = None self._valid_tags: Optional[List[Tag]] = None
self._valid_tags_set: Optional[Set[Tag]] = None
def format_given(self) -> str: def format_given(self) -> str:
""" """
@ -84,7 +86,7 @@ class TargetPython:
f"{key}={value!r}" for key, value in key_values if value is not None f"{key}={value!r}" for key, value in key_values if value is not None
) )
def get_tags(self) -> List[Tag]: def get_sorted_tags(self) -> List[Tag]:
""" """
Return the supported PEP 425 tags to check wheel candidates against. Return the supported PEP 425 tags to check wheel candidates against.
@ -108,3 +110,13 @@ class TargetPython:
self._valid_tags = tags self._valid_tags = tags
return self._valid_tags return self._valid_tags
def get_unsorted_tags(self) -> Set[Tag]:
"""Exactly the same as get_sorted_tags, but returns a set.
This is important for performance.
"""
if self._valid_tags_set is None:
self._valid_tags_set = set(self.get_sorted_tags())
return self._valid_tags_set

View file

@ -514,7 +514,9 @@ class MultiDomainBasicAuth(AuthBase):
# Consume content and release the original connection to allow our new # Consume content and release the original connection to allow our new
# request to reuse the same one. # request to reuse the same one.
resp.content # The result of the assignment isn't used, it's just needed to consume
# the content.
_ = resp.content
resp.raw.release_conn() resp.raw.release_conn()
# Add our new username and password to the request # Add our new username and password to the request

View file

@ -3,10 +3,11 @@
import os import os
from contextlib import contextmanager from contextlib import contextmanager
from typing import Generator, Optional from datetime import datetime
from typing import BinaryIO, Generator, Optional, Union
from pip._vendor.cachecontrol.cache import BaseCache from pip._vendor.cachecontrol.cache import SeparateBodyBaseCache
from pip._vendor.cachecontrol.caches import FileCache from pip._vendor.cachecontrol.caches import SeparateBodyFileCache
from pip._vendor.requests.models import Response from pip._vendor.requests.models import Response
from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.filesystem import adjacent_tmp_file, replace
@ -28,7 +29,7 @@ def suppressed_cache_errors() -> Generator[None, None, None]:
pass pass
class SafeFileCache(BaseCache): class SafeFileCache(SeparateBodyBaseCache):
""" """
A file based cache which is safe to use even when the target directory may A file based cache which is safe to use even when the target directory may
not be accessible or writable. not be accessible or writable.
@ -43,7 +44,7 @@ class SafeFileCache(BaseCache):
# From cachecontrol.caches.file_cache.FileCache._fn, brought into our # From cachecontrol.caches.file_cache.FileCache._fn, brought into our
# class for backwards-compatibility and to avoid using a non-public # class for backwards-compatibility and to avoid using a non-public
# method. # method.
hashed = FileCache.encode(name) hashed = SeparateBodyFileCache.encode(name)
parts = list(hashed[:5]) + [hashed] parts = list(hashed[:5]) + [hashed]
return os.path.join(self.directory, *parts) return os.path.join(self.directory, *parts)
@ -53,17 +54,33 @@ class SafeFileCache(BaseCache):
with open(path, "rb") as f: with open(path, "rb") as f:
return f.read() return f.read()
def set(self, key: str, value: bytes, expires: Optional[int] = None) -> None: def _write(self, path: str, data: bytes) -> None:
path = self._get_cache_path(key)
with suppressed_cache_errors(): with suppressed_cache_errors():
ensure_dir(os.path.dirname(path)) ensure_dir(os.path.dirname(path))
with adjacent_tmp_file(path) as f: with adjacent_tmp_file(path) as f:
f.write(value) f.write(data)
replace(f.name, path) replace(f.name, path)
def set(
self, key: str, value: bytes, expires: Union[int, datetime, None] = None
) -> None:
path = self._get_cache_path(key)
self._write(path, value)
def delete(self, key: str) -> None: def delete(self, key: str) -> None:
path = self._get_cache_path(key) path = self._get_cache_path(key)
with suppressed_cache_errors(): with suppressed_cache_errors():
os.remove(path) os.remove(path)
with suppressed_cache_errors():
os.remove(path + ".body")
def get_body(self, key: str) -> Optional[BinaryIO]:
path = self._get_cache_path(key) + ".body"
with suppressed_cache_errors():
return open(path, "rb")
def set_body(self, key: str, body: bytes) -> None:
path = self._get_cache_path(key) + ".body"
self._write(path, body)

View file

@ -419,15 +419,17 @@ class PipSession(requests.Session):
msg += f" (from {source})" msg += f" (from {source})"
logger.info(msg) logger.info(msg)
host_port = parse_netloc(host) parsed_host, parsed_port = parse_netloc(host)
if host_port not in self.pip_trusted_origins: if parsed_host is None:
self.pip_trusted_origins.append(host_port) raise ValueError(f"Trusted host URL must include a host part: {host!r}")
if (parsed_host, parsed_port) not in self.pip_trusted_origins:
self.pip_trusted_origins.append((parsed_host, parsed_port))
self.mount( self.mount(
build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter
) )
self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter) self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter)
if not host_port[1]: if not parsed_port:
self.mount( self.mount(
build_url_from_netloc(host, scheme="http") + ":", build_url_from_netloc(host, scheme="http") + ":",
self._trusted_host_adapter, self._trusted_host_adapter,

View file

@ -51,10 +51,22 @@ def get_build_tracker() -> Generator["BuildTracker", None, None]:
yield tracker yield tracker
class TrackerId(str):
"""Uniquely identifying string provided to the build tracker."""
class BuildTracker: class BuildTracker:
"""Ensure that an sdist cannot request itself as a setup requirement.
When an sdist is prepared, it identifies its setup requirements in the
context of ``BuildTracker.track()``. If a requirement shows up recursively, this
raises an exception.
This stops fork bombs embedded in malicious packages."""
def __init__(self, root: str) -> None: def __init__(self, root: str) -> None:
self._root = root self._root = root
self._entries: Set[InstallRequirement] = set() self._entries: Dict[TrackerId, InstallRequirement] = {}
logger.debug("Created build tracker: %s", self._root) logger.debug("Created build tracker: %s", self._root)
def __enter__(self) -> "BuildTracker": def __enter__(self) -> "BuildTracker":
@ -69,16 +81,15 @@ class BuildTracker:
) -> None: ) -> None:
self.cleanup() self.cleanup()
def _entry_path(self, link: Link) -> str: def _entry_path(self, key: TrackerId) -> str:
hashed = hashlib.sha224(link.url_without_fragment.encode()).hexdigest() hashed = hashlib.sha224(key.encode()).hexdigest()
return os.path.join(self._root, hashed) return os.path.join(self._root, hashed)
def add(self, req: InstallRequirement) -> None: def add(self, req: InstallRequirement, key: TrackerId) -> None:
"""Add an InstallRequirement to build tracking.""" """Add an InstallRequirement to build tracking."""
assert req.link
# Get the file to write information about this requirement. # Get the file to write information about this requirement.
entry_path = self._entry_path(req.link) entry_path = self._entry_path(key)
# Try reading from the file. If it exists and can be read from, a build # Try reading from the file. If it exists and can be read from, a build
# is already in progress, so a LookupError is raised. # is already in progress, so a LookupError is raised.
@ -92,33 +103,37 @@ class BuildTracker:
raise LookupError(message) raise LookupError(message)
# If we're here, req should really not be building already. # If we're here, req should really not be building already.
assert req not in self._entries assert key not in self._entries
# Start tracking this requirement. # Start tracking this requirement.
with open(entry_path, "w", encoding="utf-8") as fp: with open(entry_path, "w", encoding="utf-8") as fp:
fp.write(str(req)) fp.write(str(req))
self._entries.add(req) self._entries[key] = req
logger.debug("Added %s to build tracker %r", req, self._root) logger.debug("Added %s to build tracker %r", req, self._root)
def remove(self, req: InstallRequirement) -> None: def remove(self, req: InstallRequirement, key: TrackerId) -> None:
"""Remove an InstallRequirement from build tracking.""" """Remove an InstallRequirement from build tracking."""
assert req.link # Delete the created file and the corresponding entry.
# Delete the created file and the corresponding entries. os.unlink(self._entry_path(key))
os.unlink(self._entry_path(req.link)) del self._entries[key]
self._entries.remove(req)
logger.debug("Removed %s from build tracker %r", req, self._root) logger.debug("Removed %s from build tracker %r", req, self._root)
def cleanup(self) -> None: def cleanup(self) -> None:
for req in set(self._entries): for key, req in list(self._entries.items()):
self.remove(req) self.remove(req, key)
logger.debug("Removed build tracker: %r", self._root) logger.debug("Removed build tracker: %r", self._root)
@contextlib.contextmanager @contextlib.contextmanager
def track(self, req: InstallRequirement) -> Generator[None, None, None]: def track(self, req: InstallRequirement, key: str) -> Generator[None, None, None]:
self.add(req) """Ensure that `key` cannot install itself as a setup requirement.
:raises LookupError: If `key` was already provided in a parent invocation of
the context introduced by this method."""
tracker_id = TrackerId(key)
self.add(req, tracker_id)
yield yield
self.remove(req) self.remove(req, tracker_id)

View file

@ -5,12 +5,15 @@ import logging
from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple
from pip._vendor.packaging.requirements import Requirement from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import LegacySpecifier
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import LegacyVersion
from pip._internal.distributions import make_distribution_for_install_requirement from pip._internal.distributions import make_distribution_for_install_requirement
from pip._internal.metadata import get_default_environment from pip._internal.metadata import get_default_environment
from pip._internal.metadata.base import DistributionVersion from pip._internal.metadata.base import DistributionVersion
from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.deprecation import deprecated
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,6 +60,8 @@ def check_package_set(
package name and returns a boolean. package name and returns a boolean.
""" """
warn_legacy_versions_and_specifiers(package_set)
missing = {} missing = {}
conflicting = {} conflicting = {}
@ -147,3 +152,36 @@ def _create_whitelist(
break break
return packages_affected return packages_affected
def warn_legacy_versions_and_specifiers(package_set: PackageSet) -> None:
for project_name, package_details in package_set.items():
if isinstance(package_details.version, LegacyVersion):
deprecated(
reason=(
f"{project_name} {package_details.version} "
f"has a non-standard version number."
),
replacement=(
f"to upgrade to a newer version of {project_name} "
f"or contact the author to suggest that they "
f"release a version with a conforming version number"
),
issue=12063,
gone_in="23.3",
)
for dep in package_details.dependencies:
if any(isinstance(spec, LegacySpecifier) for spec in dep.specifier):
deprecated(
reason=(
f"{project_name} {package_details.version} "
f"has a non-standard dependency specifier {dep}."
),
replacement=(
f"to upgrade to a newer version of {project_name} "
f"or contact the author to suggest that they "
f"release a version with a conforming dependency specifiers"
),
issue=12063,
gone_in="23.3",
)

View file

@ -267,9 +267,9 @@ def get_csv_rows_for_installed(
path = _fs_to_record_path(f, lib_dir) path = _fs_to_record_path(f, lib_dir)
digest, length = rehash(f) digest, length = rehash(f)
installed_rows.append((path, digest, length)) installed_rows.append((path, digest, length))
for installed_record_path in installed.values(): return installed_rows + [
installed_rows.append((installed_record_path, "", "")) (installed_record_path, "", "") for installed_record_path in installed.values()
return installed_rows ]
def get_console_script_specs(console: Dict[str, str]) -> List[str]: def get_console_script_specs(console: Dict[str, str]) -> List[str]:

View file

@ -4,10 +4,10 @@
# The following comment should be removed at some point in the future. # The following comment should be removed at some point in the future.
# mypy: strict-optional=False # mypy: strict-optional=False
import logging
import mimetypes import mimetypes
import os import os
import shutil import shutil
from pathlib import Path
from typing import Dict, Iterable, List, Optional from typing import Dict, Iterable, List, Optional
from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.utils import canonicalize_name
@ -21,7 +21,6 @@ from pip._internal.exceptions import (
InstallationError, InstallationError,
MetadataInconsistent, MetadataInconsistent,
NetworkConnectionError, NetworkConnectionError,
PreviousBuildDirError,
VcsHashUnsupported, VcsHashUnsupported,
) )
from pip._internal.index.package_finder import PackageFinder from pip._internal.index.package_finder import PackageFinder
@ -37,6 +36,7 @@ from pip._internal.network.lazy_wheel import (
from pip._internal.network.session import PipSession from pip._internal.network.session import PipSession
from pip._internal.operations.build.build_tracker import BuildTracker from pip._internal.operations.build.build_tracker import BuildTracker
from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils._log import getLogger
from pip._internal.utils.direct_url_helpers import ( from pip._internal.utils.direct_url_helpers import (
direct_url_for_editable, direct_url_for_editable,
direct_url_from_link, direct_url_from_link,
@ -47,13 +47,12 @@ from pip._internal.utils.misc import (
display_path, display_path,
hash_file, hash_file,
hide_url, hide_url,
is_installable_dir,
) )
from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.unpacking import unpack_file from pip._internal.utils.unpacking import unpack_file
from pip._internal.vcs import vcs from pip._internal.vcs import vcs
logger = logging.getLogger(__name__) logger = getLogger(__name__)
def _get_prepared_distribution( def _get_prepared_distribution(
@ -65,10 +64,12 @@ def _get_prepared_distribution(
) -> BaseDistribution: ) -> BaseDistribution:
"""Prepare a distribution for installation.""" """Prepare a distribution for installation."""
abstract_dist = make_distribution_for_install_requirement(req) abstract_dist = make_distribution_for_install_requirement(req)
with build_tracker.track(req): tracker_id = abstract_dist.build_tracker_id
abstract_dist.prepare_distribution_metadata( if tracker_id is not None:
finder, build_isolation, check_build_deps with build_tracker.track(req, tracker_id):
) abstract_dist.prepare_distribution_metadata(
finder, build_isolation, check_build_deps
)
return abstract_dist.get_metadata_distribution() return abstract_dist.get_metadata_distribution()
@ -226,6 +227,7 @@ class RequirementPreparer:
use_user_site: bool, use_user_site: bool,
lazy_wheel: bool, lazy_wheel: bool,
verbosity: int, verbosity: int,
legacy_resolver: bool,
) -> None: ) -> None:
super().__init__() super().__init__()
@ -259,6 +261,9 @@ class RequirementPreparer:
# How verbose should underlying tooling be? # How verbose should underlying tooling be?
self.verbosity = verbosity self.verbosity = verbosity
# Are we using the legacy resolver?
self.legacy_resolver = legacy_resolver
# Memoized downloaded files, as mapping of url: path. # Memoized downloaded files, as mapping of url: path.
self._downloaded: Dict[str, str] = {} self._downloaded: Dict[str, str] = {}
@ -313,21 +318,7 @@ class RequirementPreparer:
autodelete=True, autodelete=True,
parallel_builds=parallel_builds, parallel_builds=parallel_builds,
) )
req.ensure_pristine_source_checkout()
# If a checkout exists, it's unwise to keep going. version
# inconsistencies are logged later, but do not fail the
# installation.
# FIXME: this won't upgrade when there's an existing
# package unpacked in `req.source_dir`
# TODO: this check is now probably dead code
if is_installable_dir(req.source_dir):
raise PreviousBuildDirError(
"pip can't proceed with requirements '{}' due to a"
"pre-existing build directory ({}). This is likely "
"due to a previous installation that failed . pip is "
"being responsible and not assuming it can delete this. "
"Please delete it and try again.".format(req, req.source_dir)
)
def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes: def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
# By the time this is called, the requirement's link should have # By the time this is called, the requirement's link should have
@ -352,7 +343,7 @@ class RequirementPreparer:
# a surprising hash mismatch in the future. # a surprising hash mismatch in the future.
# file:/// URLs aren't pinnable, so don't complain about them # file:/// URLs aren't pinnable, so don't complain about them
# not being pinned. # not being pinned.
if req.original_link is None and not req.is_pinned: if not req.is_direct and not req.is_pinned:
raise HashUnpinned() raise HashUnpinned()
# If known-good hashes are missing for this requirement, # If known-good hashes are missing for this requirement,
@ -365,6 +356,11 @@ class RequirementPreparer:
self, self,
req: InstallRequirement, req: InstallRequirement,
) -> Optional[BaseDistribution]: ) -> Optional[BaseDistribution]:
if self.legacy_resolver:
logger.debug(
"Metadata-only fetching is not used in the legacy resolver",
)
return None
if self.require_hashes: if self.require_hashes:
logger.debug( logger.debug(
"Metadata-only fetching is not used as hash checking is required", "Metadata-only fetching is not used as hash checking is required",
@ -385,7 +381,7 @@ class RequirementPreparer:
if metadata_link is None: if metadata_link is None:
return None return None
assert req.req is not None assert req.req is not None
logger.info( logger.verbose(
"Obtaining dependency information for %s from %s", "Obtaining dependency information for %s from %s",
req.req, req.req,
metadata_link, metadata_link,
@ -410,7 +406,7 @@ class RequirementPreparer:
# NB: raw_name will fall back to the name from the install requirement if # NB: raw_name will fall back to the name from the install requirement if
# the Name: field is not present, but it's noted in the raw_name docstring # the Name: field is not present, but it's noted in the raw_name docstring
# that that should NEVER happen anyway. # that that should NEVER happen anyway.
if metadata_dist.raw_name != req.req.name: if canonicalize_name(metadata_dist.raw_name) != canonicalize_name(req.req.name):
raise MetadataInconsistent( raise MetadataInconsistent(
req, "Name", req.req.name, metadata_dist.raw_name req, "Name", req.req.name, metadata_dist.raw_name
) )
@ -470,7 +466,19 @@ class RequirementPreparer:
for link, (filepath, _) in batch_download: for link, (filepath, _) in batch_download:
logger.debug("Downloading link %s to %s", link, filepath) logger.debug("Downloading link %s to %s", link, filepath)
req = links_to_fully_download[link] req = links_to_fully_download[link]
# Record the downloaded file path so wheel reqs can extract a Distribution
# in .get_dist().
req.local_file_path = filepath req.local_file_path = filepath
# Record that the file is downloaded so we don't do it again in
# _prepare_linked_requirement().
self._downloaded[req.link.url] = filepath
# If this is an sdist, we need to unpack it after downloading, but the
# .source_dir won't be set up until we are in _prepare_linked_requirement().
# Add the downloaded archive to the install requirement to unpack after
# preparing the source dir.
if not req.is_wheel:
req.needs_unpacked_archive(Path(filepath))
# This step is necessary to ensure all lazy wheels are processed # This step is necessary to ensure all lazy wheels are processed
# successfully by the 'download', 'wheel', and 'install' commands. # successfully by the 'download', 'wheel', and 'install' commands.

View file

@ -1,6 +1,3 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import functools import functools
import logging import logging
import os import os
@ -9,6 +6,7 @@ import sys
import uuid import uuid
import zipfile import zipfile
from optparse import Values from optparse import Values
from pathlib import Path
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.markers import Marker
@ -20,7 +18,7 @@ from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pyproject_hooks import BuildBackendHookCaller from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
from pip._internal.exceptions import InstallationError from pip._internal.exceptions import InstallationError, PreviousBuildDirError
from pip._internal.locations import get_scheme from pip._internal.locations import get_scheme
from pip._internal.metadata import ( from pip._internal.metadata import (
BaseDistribution, BaseDistribution,
@ -50,11 +48,13 @@ from pip._internal.utils.misc import (
backup_dir, backup_dir,
display_path, display_path,
hide_url, hide_url,
is_installable_dir,
redact_auth_from_url, redact_auth_from_url,
) )
from pip._internal.utils.packaging import safe_extra from pip._internal.utils.packaging import safe_extra
from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
from pip._internal.utils.unpacking import unpack_file
from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._internal.vcs import vcs from pip._internal.vcs import vcs
@ -104,6 +104,8 @@ class InstallRequirement:
if link.is_file: if link.is_file:
self.source_dir = os.path.normpath(os.path.abspath(link.file_path)) self.source_dir = os.path.normpath(os.path.abspath(link.file_path))
# original_link is the direct URL that was provided by the user for the
# requirement, either directly or via a constraints file.
if link is None and req and req.url: if link is None and req and req.url:
# PEP 508 URL requirement # PEP 508 URL requirement
link = Link(req.url) link = Link(req.url)
@ -126,7 +128,7 @@ class InstallRequirement:
if extras: if extras:
self.extras = extras self.extras = extras
elif req: elif req:
self.extras = {safe_extra(extra) for extra in req.extras} self.extras = req.extras
else: else:
self.extras = set() self.extras = set()
if markers is None and req: if markers is None and req:
@ -181,6 +183,9 @@ class InstallRequirement:
# This requirement needs more preparation before it can be built # This requirement needs more preparation before it can be built
self.needs_more_preparation = False self.needs_more_preparation = False
# This requirement needs to be unpacked before it can be installed.
self._archive_source: Optional[Path] = None
def __str__(self) -> str: def __str__(self) -> str:
if self.req: if self.req:
s = str(self.req) s = str(self.req)
@ -242,15 +247,22 @@ class InstallRequirement:
@property @property
def specifier(self) -> SpecifierSet: def specifier(self) -> SpecifierSet:
assert self.req is not None
return self.req.specifier return self.req.specifier
@property
def is_direct(self) -> bool:
"""Whether this requirement was specified as a direct URL."""
return self.original_link is not None
@property @property
def is_pinned(self) -> bool: def is_pinned(self) -> bool:
"""Return whether I am pinned to an exact version. """Return whether I am pinned to an exact version.
For example, some-package==1.2 is pinned; some-package>1.2 is not. For example, some-package==1.2 is pinned; some-package>1.2 is not.
""" """
specifiers = self.specifier assert self.req is not None
specifiers = self.req.specifier
return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="} return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool: def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
@ -260,7 +272,12 @@ class InstallRequirement:
extras_requested = ("",) extras_requested = ("",)
if self.markers is not None: if self.markers is not None:
return any( return any(
self.markers.evaluate({"extra": extra}) for extra in extras_requested self.markers.evaluate({"extra": extra})
# TODO: Remove these two variants when packaging is upgraded to
# support the marker comparison logic specified in PEP 685.
or self.markers.evaluate({"extra": safe_extra(extra)})
or self.markers.evaluate({"extra": canonicalize_name(extra)})
for extra in extras_requested
) )
else: else:
return True return True
@ -293,11 +310,12 @@ class InstallRequirement:
good_hashes = self.hash_options.copy() good_hashes = self.hash_options.copy()
if trust_internet: if trust_internet:
link = self.link link = self.link
elif self.original_link and self.user_supplied: elif self.is_direct and self.user_supplied:
link = self.original_link link = self.original_link
else: else:
link = None link = None
if link and link.hash: if link and link.hash:
assert link.hash_name is not None
good_hashes.setdefault(link.hash_name, []).append(link.hash) good_hashes.setdefault(link.hash_name, []).append(link.hash)
return Hashes(good_hashes) return Hashes(good_hashes)
@ -307,6 +325,7 @@ class InstallRequirement:
return None return None
s = str(self.req) s = str(self.req)
if self.comes_from: if self.comes_from:
comes_from: Optional[str]
if isinstance(self.comes_from, str): if isinstance(self.comes_from, str):
comes_from = self.comes_from comes_from = self.comes_from
else: else:
@ -338,7 +357,7 @@ class InstallRequirement:
# When parallel builds are enabled, add a UUID to the build directory # When parallel builds are enabled, add a UUID to the build directory
# name so multiple builds do not interfere with each other. # name so multiple builds do not interfere with each other.
dir_name: str = canonicalize_name(self.name) dir_name: str = canonicalize_name(self.req.name)
if parallel_builds: if parallel_builds:
dir_name = f"{dir_name}_{uuid.uuid4().hex}" dir_name = f"{dir_name}_{uuid.uuid4().hex}"
@ -381,6 +400,7 @@ class InstallRequirement:
) )
def warn_on_mismatching_name(self) -> None: def warn_on_mismatching_name(self) -> None:
assert self.req is not None
metadata_name = canonicalize_name(self.metadata["Name"]) metadata_name = canonicalize_name(self.metadata["Name"])
if canonicalize_name(self.req.name) == metadata_name: if canonicalize_name(self.req.name) == metadata_name:
# Everything is fine. # Everything is fine.
@ -450,6 +470,7 @@ class InstallRequirement:
# Things valid for sdists # Things valid for sdists
@property @property
def unpacked_source_directory(self) -> str: def unpacked_source_directory(self) -> str:
assert self.source_dir, f"No source dir for {self}"
return os.path.join( return os.path.join(
self.source_dir, self.link and self.link.subdirectory_fragment or "" self.source_dir, self.link and self.link.subdirectory_fragment or ""
) )
@ -493,7 +514,7 @@ class InstallRequirement:
"to use --use-pep517 or add a " "to use --use-pep517 or add a "
"pyproject.toml file to the project" "pyproject.toml file to the project"
), ),
gone_in="23.3", gone_in="24.0",
) )
self.use_pep517 = False self.use_pep517 = False
return return
@ -536,7 +557,7 @@ class InstallRequirement:
Under PEP 517 and PEP 660, call the backend hook to prepare the metadata. Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
Under legacy processing, call setup.py egg-info. Under legacy processing, call setup.py egg-info.
""" """
assert self.source_dir assert self.source_dir, f"No source dir for {self}"
details = self.name or f"from {self.link}" details = self.name or f"from {self.link}"
if self.use_pep517: if self.use_pep517:
@ -585,8 +606,10 @@ class InstallRequirement:
if self.metadata_directory: if self.metadata_directory:
return get_directory_distribution(self.metadata_directory) return get_directory_distribution(self.metadata_directory)
elif self.local_file_path and self.is_wheel: elif self.local_file_path and self.is_wheel:
assert self.req is not None
return get_wheel_distribution( return get_wheel_distribution(
FilesystemWheel(self.local_file_path), canonicalize_name(self.name) FilesystemWheel(self.local_file_path),
canonicalize_name(self.req.name),
) )
raise AssertionError( raise AssertionError(
f"InstallRequirement {self} has no metadata directory and no wheel: " f"InstallRequirement {self} has no metadata directory and no wheel: "
@ -594,9 +617,9 @@ class InstallRequirement:
) )
def assert_source_matches_version(self) -> None: def assert_source_matches_version(self) -> None:
assert self.source_dir assert self.source_dir, f"No source dir for {self}"
version = self.metadata["version"] version = self.metadata["version"]
if self.req.specifier and version not in self.req.specifier: if self.req and self.req.specifier and version not in self.req.specifier:
logger.warning( logger.warning(
"Requested %s, but installing version %s", "Requested %s, but installing version %s",
self, self,
@ -633,6 +656,27 @@ class InstallRequirement:
parallel_builds=parallel_builds, parallel_builds=parallel_builds,
) )
def needs_unpacked_archive(self, archive_source: Path) -> None:
assert self._archive_source is None
self._archive_source = archive_source
def ensure_pristine_source_checkout(self) -> None:
"""Ensure the source directory has not yet been built in."""
assert self.source_dir is not None
if self._archive_source is not None:
unpack_file(str(self._archive_source), self.source_dir)
elif is_installable_dir(self.source_dir):
# If a checkout exists, it's unwise to keep going.
# version inconsistencies are logged later, but do not fail
# the installation.
raise PreviousBuildDirError(
f"pip can't proceed with requirements '{self}' due to a "
f"pre-existing build directory ({self.source_dir}). This is likely "
"due to a previous installation that failed . pip is "
"being responsible and not assuming it can delete this. "
"Please delete it and try again."
)
# For editable installations # For editable installations
def update_editable(self) -> None: def update_editable(self) -> None:
if not self.link: if not self.link:
@ -689,9 +733,10 @@ class InstallRequirement:
name = name.replace(os.path.sep, "/") name = name.replace(os.path.sep, "/")
return name return name
assert self.req is not None
path = os.path.join(parentdir, path) path = os.path.join(parentdir, path)
name = _clean_zip_name(path, rootdir) name = _clean_zip_name(path, rootdir)
return self.name + "/" + name return self.req.name + "/" + name
def archive(self, build_dir: Optional[str]) -> None: def archive(self, build_dir: Optional[str]) -> None:
"""Saves archive to provided build_dir. """Saves archive to provided build_dir.
@ -770,8 +815,9 @@ class InstallRequirement:
use_user_site: bool = False, use_user_site: bool = False,
pycompile: bool = True, pycompile: bool = True,
) -> None: ) -> None:
assert self.req is not None
scheme = get_scheme( scheme = get_scheme(
self.name, self.req.name,
user=use_user_site, user=use_user_site,
home=home, home=home,
root=root, root=root,
@ -785,7 +831,7 @@ class InstallRequirement:
prefix=prefix, prefix=prefix,
home=home, home=home,
use_user_site=use_user_site, use_user_site=use_user_site,
name=self.name, name=self.req.name,
setup_py_path=self.setup_py_path, setup_py_path=self.setup_py_path,
isolated=self.isolated, isolated=self.isolated,
build_env=self.build_env, build_env=self.build_env,
@ -798,13 +844,13 @@ class InstallRequirement:
assert self.local_file_path assert self.local_file_path
install_wheel( install_wheel(
self.name, self.req.name,
self.local_file_path, self.local_file_path,
scheme=scheme, scheme=scheme,
req_description=str(self.req), req_description=str(self.req),
pycompile=pycompile, pycompile=pycompile,
warn_script_location=warn_script_location, warn_script_location=warn_script_location,
direct_url=self.download_info if self.original_link else None, direct_url=self.download_info if self.is_direct else None,
requested=self.user_supplied, requested=self.user_supplied,
) )
self.install_succeeded = True self.install_succeeded = True
@ -858,7 +904,7 @@ def check_legacy_setup_py_options(
reason="--build-option and --global-option are deprecated.", reason="--build-option and --global-option are deprecated.",
issue=11859, issue=11859,
replacement="to use --config-settings", replacement="to use --config-settings",
gone_in="23.3", gone_in="24.0",
) )
logger.warning( logger.warning(
"Implying --no-binary=:all: due to the presence of " "Implying --no-binary=:all: due to the presence of "

View file

@ -2,9 +2,12 @@ import logging
from collections import OrderedDict from collections import OrderedDict
from typing import Dict, List from typing import Dict, List
from pip._vendor.packaging.specifiers import LegacySpecifier
from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import LegacyVersion
from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.deprecation import deprecated
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -80,3 +83,37 @@ class RequirementSet:
for install_req in self.all_requirements for install_req in self.all_requirements
if not install_req.constraint and not install_req.satisfied_by if not install_req.constraint and not install_req.satisfied_by
] ]
def warn_legacy_versions_and_specifiers(self) -> None:
for req in self.requirements_to_install:
version = req.get_dist().version
if isinstance(version, LegacyVersion):
deprecated(
reason=(
f"pip has selected the non standard version {version} "
f"of {req}. In the future this version will be "
f"ignored as it isn't standard compliant."
),
replacement=(
"set or update constraints to select another version "
"or contact the package author to fix the version number"
),
issue=12063,
gone_in="23.3",
)
for dep in req.get_dist().iter_dependencies():
if any(isinstance(spec, LegacySpecifier) for spec in dep.specifier):
deprecated(
reason=(
f"pip has selected {req} {version} which has non "
f"standard dependency specifier {dep}. "
f"In the future this version of {req} will be "
f"ignored as it isn't standard compliant."
),
replacement=(
"set or update constraints to select another version "
"or contact the package author to fix the version number"
),
issue=12063,
gone_in="23.3",
)

View file

@ -274,7 +274,7 @@ class StashedUninstallPathSet:
def commit(self) -> None: def commit(self) -> None:
"""Commits the uninstall by removing stashed files.""" """Commits the uninstall by removing stashed files."""
for _, save_dir in self._save_dirs.items(): for save_dir in self._save_dirs.values():
save_dir.cleanup() save_dir.cleanup()
self._moves = [] self._moves = []
self._save_dirs = {} self._save_dirs = {}

View file

@ -1,7 +1,7 @@
from typing import FrozenSet, Iterable, Optional, Tuple, Union from typing import FrozenSet, Iterable, Optional, Tuple, Union
from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import LegacyVersion, Version from pip._vendor.packaging.version import LegacyVersion, Version
from pip._internal.models.link import Link, links_equivalent from pip._internal.models.link import Link, links_equivalent
@ -12,11 +12,11 @@ CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]]
CandidateVersion = Union[LegacyVersion, Version] CandidateVersion = Union[LegacyVersion, Version]
def format_name(project: str, extras: FrozenSet[str]) -> str: def format_name(project: NormalizedName, extras: FrozenSet[NormalizedName]) -> str:
if not extras: if not extras:
return project return project
canonical_extras = sorted(canonicalize_name(e) for e in extras) extras_expr = ",".join(sorted(extras))
return "{}[{}]".format(project, ",".join(canonical_extras)) return f"{project}[{extras_expr}]"
class Constraint: class Constraint:

View file

@ -341,6 +341,7 @@ class AlreadyInstalledCandidate(Candidate):
self.dist = dist self.dist = dist
self._ireq = _make_install_req_from_dist(dist, template) self._ireq = _make_install_req_from_dist(dist, template)
self._factory = factory self._factory = factory
self._version = None
# This is just logging some messages, so we can do it eagerly. # This is just logging some messages, so we can do it eagerly.
# The returned dist would be exactly the same as self.dist because we # The returned dist would be exactly the same as self.dist because we
@ -376,7 +377,9 @@ class AlreadyInstalledCandidate(Candidate):
@property @property
def version(self) -> CandidateVersion: def version(self) -> CandidateVersion:
return self.dist.version if self._version is None:
self._version = self.dist.version
return self._version
@property @property
def is_editable(self) -> bool: def is_editable(self) -> bool:
@ -426,7 +429,15 @@ class ExtrasCandidate(Candidate):
extras: FrozenSet[str], extras: FrozenSet[str],
) -> None: ) -> None:
self.base = base self.base = base
self.extras = extras self.extras = frozenset(canonicalize_name(e) for e in extras)
# If any extras are requested in their non-normalized forms, keep track
# of their raw values. This is needed when we look up dependencies
# since PEP 685 has not been implemented for marker-matching, and using
# the non-normalized extra for lookup ensures the user can select a
# non-normalized extra in a package with its non-normalized form.
# TODO: Remove this attribute when packaging is upgraded to support the
# marker comparison logic specified in PEP 685.
self._unnormalized_extras = extras.difference(self.extras)
def __str__(self) -> str: def __str__(self) -> str:
name, rest = str(self.base).split(" ", 1) name, rest = str(self.base).split(" ", 1)
@ -477,6 +488,50 @@ class ExtrasCandidate(Candidate):
def source_link(self) -> Optional[Link]: def source_link(self) -> Optional[Link]:
return self.base.source_link return self.base.source_link
def _warn_invalid_extras(
self,
requested: FrozenSet[str],
valid: FrozenSet[str],
) -> None:
"""Emit warnings for invalid extras being requested.
This emits a warning for each requested extra that is not in the
candidate's ``Provides-Extra`` list.
"""
invalid_extras_to_warn = frozenset(
extra
for extra in requested
if extra not in valid
# If an extra is requested in an unnormalized form, skip warning
# about the normalized form being missing.
and extra in self.extras
)
if not invalid_extras_to_warn:
return
for extra in sorted(invalid_extras_to_warn):
logger.warning(
"%s %s does not provide the extra '%s'",
self.base.name,
self.version,
extra,
)
def _calculate_valid_requested_extras(self) -> FrozenSet[str]:
"""Get a list of valid extras requested by this candidate.
The user (or upstream dependant) may have specified extras that the
candidate doesn't support. Any unsupported extras are dropped, and each
cause a warning to be logged here.
"""
requested_extras = self.extras.union(self._unnormalized_extras)
valid_extras = frozenset(
extra
for extra in requested_extras
if self.base.dist.is_extra_provided(extra)
)
self._warn_invalid_extras(requested_extras, valid_extras)
return valid_extras
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]: def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
factory = self.base._factory factory = self.base._factory
@ -486,18 +541,7 @@ class ExtrasCandidate(Candidate):
if not with_requires: if not with_requires:
return return
# The user may have specified extras that the candidate doesn't valid_extras = self._calculate_valid_requested_extras()
# support. We ignore any unsupported extras here.
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
for extra in sorted(invalid_extras):
logger.warning(
"%s %s does not provide the extra '%s'",
self.base.name,
self.version,
extra,
)
for r in self.base.dist.iter_dependencies(valid_extras): for r in self.base.dist.iter_dependencies(valid_extras):
requirement = factory.make_requirement_from_spec( requirement = factory.make_requirement_from_spec(
str(r), self.base._ireq, valid_extras str(r), self.base._ireq, valid_extras

Some files were not shown because too many files have changed in this diff Show more