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

Merge branch 'main' into per-req-config-settings

This commit is contained in:
q0w 2023-03-20 07:26:21 +03:00
commit 69cfd23c17
233 changed files with 3459 additions and 2702 deletions

30
.github/chronographer.yml vendored Normal file
View file

@ -0,0 +1,30 @@
---
action-hints:
# check-title-prefix: chng # default: `{{ branch-protection-check-name }}: `
external-docs-url: https://pip.pypa.io/how-to-changelog
inline-markdown: >
Check out https://pip.pypa.io/how-to-changelog
branch-protection-check-name: Timeline protection
enforce-name:
# suffix: .md
suffix: .rst
exclude:
bots:
- dependabot-preview
- dependabot
- patchback
humans:
- pyup-bot
labels:
skip-changelog: skip news
paths: # relative modified file paths that do or don't need changelog mention
exclude: []
include: []
...

View file

@ -11,11 +11,6 @@ on:
schedule:
- cron: 0 0 * * MON # Run every Monday at 00:00 UTC
env:
# The "FORCE_COLOR" variable, when set to 1,
# tells Nox to colorize itself.
FORCE_COLOR: "1"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

View file

@ -0,0 +1,28 @@
name: Update documentation redirects
on:
push:
branches: [main]
schedule:
- cron: 0 0 * * MON # Run every Monday at 00:00 UTC
env:
FORCE_COLOR: "1"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
update-rtd-redirects:
runs-on: ubuntu-latest
environment: RTD Deploys
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: pip install httpx pyyaml rich
- run: python tools/update-rtd-redirects.py
env:
RTD_API_TOKEN: ${{ secrets.RTD_API_TOKEN }}

View file

@ -33,7 +33,7 @@ repos:
exclude: tests/data
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
rev: 5.12.0
hooks:
- id: isort
files: \.py$
@ -47,11 +47,12 @@ repos:
additional_dependencies: [
'keyring==23.0.1',
'nox==2021.6.12',
'pytest==7.1.1',
'pytest',
'types-docutils==0.18.3',
'types-setuptools==57.4.14',
'types-freezegun==1.1.9',
'types-six==1.16.15',
'types-pyyaml==6.0.12.2',
]
- repo: https://github.com/pre-commit/pygrep-hooks

View file

@ -0,0 +1,15 @@
# This file is read by tools/update-rtd-redirects.py.
# It is related to Read the Docs, but is not a file processed by the platform.
/dev/news-entry-failure: >-
https://pip.pypa.io/en/latest/development/contributing/#news-entries
/errors/resolution-impossible: >-
https://pip.pypa.io/en/stable/topics/dependency-resolution/#dealing-with-dependency-conflicts
/surveys/backtracking: >-
https://forms.gle/LkZP95S4CfqBAU1N6
/warnings/backtracking: >-
https://pip.pypa.io/en/stable/topics/dependency-resolution/#possible-ways-to-reduce-backtracking
/warnings/enable-long-paths: >-
https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later
/warnings/venv: >-
https://docs.python.org/3/tutorial/venv.html

View file

@ -128,6 +128,7 @@ Chris Hunt
Chris Jerdonek
Chris McDonough
Chris Pawley
Chris Pryer
Chris Wolfe
Christian Clauss
Christian Heimes
@ -169,7 +170,9 @@ Daniel Jost
Daniel Katz
Daniel Shaulov
Daniele Esposti
Daniele Nicolodi
Daniele Procida
Daniil Konovalenko
Danny Hermes
Danny McClanahan
Darren Kavanagh
@ -200,6 +203,7 @@ Diego Caraballo
Diego Ramirez
DiegoCaraballo
Dimitri Merejkowsky
Dimitri Papadopoulos
Dirk Stolle
Dmitry Gladkov
Dmitry Volodin
@ -207,6 +211,7 @@ Domen Kožar
Dominic Davis-Foster
Donald Stufft
Dongweiming
doron zarhi
Douglas Thor
DrFeathers
Dustin Ingram
@ -282,6 +287,7 @@ hauntsaninja
Henrich Hartzer
Henry Schreiner
Herbert Pfennig
Holly Stotelmyer
Hsiaoming Yang
Hugo Lopes Tavares
Hugo van Kemenade
@ -305,6 +311,7 @@ Jacob Kim
Jacob Walls
Jaime Sanz
jakirkham
Jakub Kuczys
Jakub Stasiak
Jakub Vysoky
Jakub Wilk
@ -317,6 +324,7 @@ Jan Pokorný
Jannis Leidel
Jarek Potiuk
jarondl
Jason Curtis
Jason R. Coombs
Jay Graves
Jean-Christophe Fillion-Robin
@ -342,6 +350,7 @@ Jon Dufresne
Jon Parise
Jonas Nockert
Jonathan Herbert
Joonatan Partanen
Joost Molenaar
Jorge Niedbalski
Joseph Bylund
@ -350,6 +359,7 @@ Josh Bronson
Josh Hansen
Josh Schneier
Juanjo Bazán
Judah Rand
Julian Berman
Julian Gethmann
Julien Demoor
@ -455,6 +465,7 @@ Miro Hrončok
Monica Baluna
montefra
Monty Taylor
Muha Ajjan
Nadav Wexler
Nahuel Ambrosini
Nate Coraor
@ -484,6 +495,7 @@ nvdv
OBITORASU
Ofek Lev
ofrinevo
Oliver Freund
Oliver Jeeves
Oliver Mannion
Oliver Tonnhofer
@ -555,6 +567,7 @@ Riccardo Schirone
Richard Jones
Richard Si
Ricky Ng-Adam
Rishi
RobberPhex
Robert Collins
Robert McGibbon
@ -605,6 +618,7 @@ Stavros Korokithakis
Stefan Scherfke
Stefano Rivera
Stephan Erb
Stephen Rosen
stepshal
Steve (Gadget) Barnes
Steve Barnes
@ -644,6 +658,7 @@ Tom V
Tomas Hrnciar
Tomas Orsava
Tomer Chachamu
Tommi Enenkel | AnB
Tomáš Hrnčiar
Tony Beswick
Tony Narlock
@ -672,6 +687,7 @@ Wil Tan
Wilfred Hughes
William ML Leslie
William T Olson
William Woodruff
Wilson Mo
wim glenn
Winson Luk

View file

@ -2,6 +2,7 @@ include AUTHORS.txt
include LICENSE.txt
include NEWS.rst
include README.rst
include SECURITY.md
include pyproject.toml
include src/pip/_vendor/README.rst
@ -18,6 +19,7 @@ exclude .mailmap
exclude .appveyor.yml
exclude .readthedocs.yml
exclude .pre-commit-config.yaml
exclude .readthedocs-custom-redirects.yml
exclude tox.ini
exclude noxfile.py

View file

@ -9,6 +9,82 @@
.. towncrier release notes start
23.0.1 (2023-02-17)
===================
Features
--------
- Ignore PIP_REQUIRE_VIRTUALENV for ``pip index`` (`#11671 <https://github.com/pypa/pip/issues/11671>`_)
- Implement ``--break-system-packages`` to permit installing packages into
``EXTERNALLY-MANAGED`` Python installations. (`#11780 <https://github.com/pypa/pip/issues/11780>`_)
Bug Fixes
---------
- Improve handling of isolated build environments on platforms that
customize the Python's installation schemes, such as Debian and
Homebrew. (`#11740 <https://github.com/pypa/pip/issues/11740>`_)
- Do not crash in presence of misformatted hash field in ``direct_url.json``. (`#11773 <https://github.com/pypa/pip/issues/11773>`_)
23.0 (2023-01-30)
=================
Features
--------
- Change the hashes in the installation report to be a mapping. Emit the
``archive_info.hashes`` dictionary in ``direct_url.json``. (`#11312 <https://github.com/pypa/pip/issues/11312>`_)
- Implement logic to read the ``EXTERNALLY-MANAGED`` file as specified in PEP 668.
This allows a downstream Python distributor to prevent users from using pip to
modify the externally managed environment. (`#11381 <https://github.com/pypa/pip/issues/11381>`_)
- Enable the use of ``keyring`` found on ``PATH``. This allows ``keyring``
installed using ``pipx`` to be used by ``pip``. (`#11589 <https://github.com/pypa/pip/issues/11589>`_)
- The inspect and installation report formats are now declared stable, and their version
has been bumped from ``0`` to ``1``. (`#11757 <https://github.com/pypa/pip/issues/11757>`_)
Bug Fixes
---------
- Wheel cache behavior is restored to match previous versions, allowing the
cache to find existing entries. (`#11527 <https://github.com/pypa/pip/issues/11527>`_)
- Use the "venv" scheme if available to obtain prefixed lib paths. (`#11598 <https://github.com/pypa/pip/issues/11598>`_)
- Deprecated a historical ambiguity in how ``egg`` fragments in URL-style
requirements are formatted and handled. ``egg`` fragments that do not look
like PEP 508 names now produce a deprecation warning. (`#11617 <https://github.com/pypa/pip/issues/11617>`_)
- Fix scripts path in isolated build environment on Debian. (`#11623 <https://github.com/pypa/pip/issues/11623>`_)
- Make ``pip show`` show the editable location if package is editable (`#11638 <https://github.com/pypa/pip/issues/11638>`_)
- Stop checking that ``wheel`` is present when ``build-system.requires``
is provided without ``build-system.build-backend`` as ``setuptools``
(which we still check for) will inject it anyway. (`#11673 <https://github.com/pypa/pip/issues/11673>`_)
- Fix an issue when an already existing in-memory distribution would cause
exceptions in ``pip install`` (`#11704 <https://github.com/pypa/pip/issues/11704>`_)
Vendored Libraries
------------------
- Upgrade certifi to 2022.12.7
- Upgrade chardet to 5.1.0
- Upgrade colorama to 0.4.6
- Upgrade distro to 1.8.0
- Remove pep517 from vendored packages
- Upgrade platformdirs to 2.6.2
- Add pyproject-hooks 1.0.0
- Upgrade requests to 2.28.2
- Upgrade rich to 12.6.0
- Upgrade urllib3 to 1.26.14
Improved Documentation
----------------------
- Fixed the description of the option "--install-options" in the documentation (`#10265 <https://github.com/pypa/pip/issues/10265>`_)
- Remove mention that editable installs are necessary for pip freeze to report the VCS
URL. (`#11675 <https://github.com/pypa/pip/issues/11675>`_)
- Clarify that the egg URL fragment is only necessary for editable VCS installs, and
otherwise not necessary anymore. (`#11676 <https://github.com/pypa/pip/issues/11676>`_)
22.3.1 (2022-11-05)
===================

3
SECURITY.md Normal file
View file

@ -0,0 +1,3 @@
# Security and Vulnerability Reporting
If you find any security issues, please report to [security@python.org](mailto:security@python.org)

View file

@ -11,7 +11,7 @@ Usage
.. tab:: Unix/macOS
.. pip-command-usage:: install "python -m pip"
.. pip-command-usage:: install 'python -m pip'
.. tab:: Windows
@ -277,7 +277,7 @@ Examples
.. code-block:: shell
python -m pip install SomePackage # latest version
python -m pip install SomePackage==1.0.4 # specific version
python -m pip install 'SomePackage==1.0.4' # specific version
python -m pip install 'SomePackage>=1.0.4' # minimum version
.. tab:: Windows
@ -285,8 +285,8 @@ Examples
.. code-block:: shell
py -m pip install SomePackage # latest version
py -m pip install SomePackage==1.0.4 # specific version
py -m pip install 'SomePackage>=1.0.4' # minimum version
py -m pip install "SomePackage==1.0.4" # specific version
py -m pip install "SomePackage>=1.0.4" # minimum version
#. Install a list of requirements specified in a file. See the :ref:`Requirements files <Requirements Files>`.
@ -349,13 +349,13 @@ Examples
.. code-block:: shell
python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1
python -m pip install 'SomeProject@git+https://git.repo/some_pkg.git@1.3.1'
.. tab:: Windows
.. code-block:: shell
py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1
py -m pip install "SomeProject@git+https://git.repo/some_pkg.git@1.3.1"
#. Install a project from VCS in "editable" mode. See the sections on :doc:`../topics/vcs-support` and :ref:`Editable Installs <editable-installs>`.
@ -364,20 +364,20 @@ Examples
.. code-block:: shell
python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git
python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial
python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn
python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch
python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory
python -m pip install -e 'git+https://git.repo/some_pkg.git#egg=SomePackage' # from git
python -m pip install -e 'hg+https://hg.repo/some_pkg.git#egg=SomePackage' # from mercurial
python -m pip install -e 'svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage' # from svn
python -m pip install -e 'git+https://git.repo/some_pkg.git@feature#egg=SomePackage' # from 'feature' branch
python -m pip install -e 'git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path' # install a python package from a repo subdirectory
.. tab:: Windows
.. code-block:: shell
py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git
py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial
py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn
py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch
py -m pip install -e "git+https://git.repo/some_pkg.git#egg=SomePackage" # from git
py -m pip install -e "hg+https://hg.repo/some_pkg.git#egg=SomePackage" # from mercurial
py -m pip install -e "svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage" # from svn
py -m pip install -e "git+https://git.repo/some_pkg.git@feature#egg=SomePackage" # from 'feature' branch
py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory
#. Install a package with `extras`_.
@ -386,21 +386,21 @@ Examples
.. code-block:: shell
python -m pip install SomePackage[PDF]
python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@main#subdirectory=subdir_path"
python -m pip install .[PDF] # project in current directory
python -m pip install SomePackage[PDF]==3.0
python -m pip install SomePackage[PDF,EPUB] # multiple extras
python -m pip install 'SomePackage[PDF]'
python -m pip install 'SomePackage[PDF] @ git+https://git.repo/SomePackage@main#subdirectory=subdir_path'
python -m pip install '.[PDF]' # project in current directory
python -m pip install 'SomePackage[PDF]==3.0'
python -m pip install 'SomePackage[PDF,EPUB]' # multiple extras
.. tab:: Windows
.. code-block:: shell
py -m pip install SomePackage[PDF]
py -m pip install "SomePackage[PDF]"
py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@main#subdirectory=subdir_path"
py -m pip install .[PDF] # project in current directory
py -m pip install SomePackage[PDF]==3.0
py -m pip install SomePackage[PDF,EPUB] # multiple extras
py -m pip install ".[PDF]" # project in current directory
py -m pip install "SomePackage[PDF]==3.0"
py -m pip install "SomePackage[PDF,EPUB]" # multiple extras
#. Install a particular source archive file.
@ -408,15 +408,15 @@ Examples
.. code-block:: shell
python -m pip install ./downloads/SomePackage-1.0.4.tar.gz
python -m pip install http://my.package.repo/SomePackage-1.0.4.zip
python -m pip install './downloads/SomePackage-1.0.4.tar.gz'
python -m pip install 'http://my.package.repo/SomePackage-1.0.4.zip'
.. tab:: Windows
.. code-block:: shell
py -m pip install ./downloads/SomePackage-1.0.4.tar.gz
py -m pip install http://my.package.repo/SomePackage-1.0.4.zip
py -m pip install "./downloads/SomePackage-1.0.4.tar.gz"
py -m pip install "http://my.package.repo/SomePackage-1.0.4.zip"
#. Install a particular source archive file following :pep:`440` direct references.
@ -424,17 +424,17 @@ Examples
.. code-block:: shell
python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl
python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl"
python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz
python -m pip install 'SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl'
python -m pip install 'SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl'
python -m pip install 'SomeProject@http://my.package.repo/1.2.3.tar.gz'
.. tab:: Windows
.. code-block:: shell
py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl
py -m pip install "SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl"
py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl"
py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz
py -m pip install "SomeProject@http://my.package.repo/1.2.3.tar.gz"
#. Install from alternative package repositories.

View file

@ -24,9 +24,9 @@ Things pip does:
backwards compatibility reasons. But thing with setuptools:
has a ``setup.py`` file that it invokes to …… get info?
2. Decides where to install stuff. Once the package is built, resulting
artifact is then installed into system in appropriate place. :pep:`517`
defines interface between build backend & installer.
2. Decides where to install stuff. Once the package is built, the resulting
artifact is then installed to the system in its appropriate place. :pep:`517`
defines the interface between the build backend & installer.
Broad overview of flow
======================
@ -111,24 +111,24 @@ The package index gives pip a list of files for that package (via the existing P
pip chooses from the list a single file to download.
It may go back and choose another file to download
It may go back and choose another file to download.
When pip looks at the package index, the place where it looks has
basically a link. The links text is the name of the file
basically a link. The links text is the name of the file.
This is the `PyPI Simple API`_ (PyPI has several APIs, some are being
deprecated). pip looks at Simple API, documented initially at :pep:`503` --
packaging.python.org has PyPA specifications with more details for
Simple Repository API
Simple Repository API.
For this package name -- this is the list of files available
For this package name -- this is the list of files available.
Looks there for:
* The list of filenames
* Other info
Once it has those, selects one file, downloads it
Once it has those, it selects one file and downloads it.
(Question: If I want to ``pip install flask``, I think the whole list of filenames
cannot….should not be …. ? I want only the Flask …. Why am I getting the

View file

@ -90,7 +90,7 @@ distro community, cloud provider support channels, etc).
## Upgrading `pip`
Upgrading your `pip` by running:
Upgrade your `pip` by running:
```{pip-cli}
$ pip install --upgrade pip

View file

@ -3,6 +3,10 @@
```{versionadded} 22.2
```
```{versionchanged} 23.0
``version`` has been bumped to ``1`` and the format declared stable.
```
The `pip inspect` command produces a detailed JSON report of the Python
environment, including installed distributions.
@ -10,10 +14,7 @@ environment, including installed distributions.
The report is a JSON object with the following properties:
- `version`: the string `0`, denoting that the inspect command is an experimental
feature. This value will change to `1`, when the feature is deemed stable after
gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes
may be introduced in version `1` without notice. After that, it will change only if
- `version`: the string `1`. It will change only if
and when backward incompatible changes are introduced, such as removing mandatory
fields or changing the semantics or data type of existing fields. The introduction of
backward incompatible changes will follow the usual pip processes such as the
@ -72,7 +73,7 @@ this (metadata abriged for brevity):
```json
{
"version": "0",
"version": "1",
"pip_version": "22.2.dev0",
"installed": [
{

View file

@ -3,6 +3,10 @@
```{versionadded} 22.2
```
```{versionchanged} 23.0
``version`` has been bumped to ``1`` and the format declared stable.
```
The `--report` option of the pip install command produces a detailed JSON report of what
it did install (or what it would have installed, if used with the `--dry-run` option).
@ -23,10 +27,7 @@ When considering use cases, please bear in mind that
The report is a JSON object with the following properties:
- `version`: the string `0`, denoting that the installation report is an experimental
feature. This value will change to `1`, when the feature is deemed stable after
gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes
may be introduced in version `1` without notice. After that, it will change only if
- `version`: the string `1`. It will change only if
and when backward incompatible changes are introduced, such as removing mandatory
fields or changing the semantics or data type of existing fields. The introduction of
backward incompatible changes will follow the usual pip processes such as the
@ -64,7 +65,7 @@ package with the following properties:
`--find-links`.
```{note}
For source archives, `download_info.archive_info.hash` may
For source archives, `download_info.archive_info.hashes` may
be absent when the requirement was installed from the wheel cache
and the cache entry was populated by an older pip version that did not
record the origin URL of the downloaded artifact.
@ -92,14 +93,16 @@ will produce an output similar to this (metadata abriged for brevity):
```json
{
"version": "0",
"version": "1",
"pip_version": "22.2",
"install": [
{
"download_info": {
"url": "https://files.pythonhosted.org/packages/a4/0c/fbaa7319dcb5eecd3484686eb5a5c5702a6445adb566f01aee6de3369bc4/pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"archive_info": {
"hash": "sha256=18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"
"hashes": {
"sha256": "18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"
}
}
},
"is_direct": false,
@ -144,7 +147,9 @@ will produce an output similar to this (metadata abriged for brevity):
"download_info": {
"url": "https://files.pythonhosted.org/packages/6c/10/a7d0fa5baea8fe7b50f448ab742f26f52b80bfca85ac2be9d35cdd9a3246/pyparsing-3.0.9-py3-none-any.whl",
"archive_info": {
"hash": "sha256=5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
"hashes": {
"sha256": "5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
}
}
},
"is_direct": false,
@ -163,7 +168,9 @@ will produce an output similar to this (metadata abriged for brevity):
"download_info": {
"url": "https://files.pythonhosted.org/packages/75/e1/932e06004039dd670c9d5e1df0cd606bf46e29a28e65d5bb28e894ea29c9/typing_extensions-4.2.0-py3-none-any.whl",
"archive_info": {
"hash": "sha256=6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"
"hashes": {
"sha256": "6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"
}
}
},
"is_direct": false,

View file

@ -109,7 +109,6 @@ and two {ref}`--find-links <install_--find-links>` locations:
The options which can be applied to individual requirements are:
- {ref}`--install-option <install_--install-option>`
- {ref}`--global-option <install_--global-option>`
- {ref}`--config-settings <install_--config-settings>`
- `--hash` (for {ref}`Hash-checking mode`)
@ -161,7 +160,7 @@ This disables the use of wheels (cached or otherwise). This could mean that buil
This mechanism is only preserved for backwards compatibility and should be considered deprecated. A future release of pip may drop these options.
```
The `--global-option` and `--install-option` options are used to pass options to `setup.py`.
The `--global-option` option is used to pass options to `setup.py`.
```{attention}
These options are highly coupled with how pip invokes setuptools using the {doc}`../reference/build-system/setup-py` build system interface. It is not compatible with newer {doc}`../reference/build-system/pyproject-toml` build system interface.
@ -171,15 +170,10 @@ This is will not work with other build-backends or newer setup.cfg-only projects
If you have a declaration like:
FooProject >= 1.2 --global-option="--no-user-cfg" \
--install-option="--prefix='/usr/local'" \
--install-option="--no-compile"
FooProject >= 1.2 --global-option="--no-user-cfg"
The above translates roughly into running FooProject's `setup.py` script as:
python setup.py --no-user-cfg install --prefix='/usr/local' --no-compile
python setup.py --no-user-cfg install
Note that the only way of giving more than one option to `setup.py` is through multiple `--global-option` and `--install-option` options, as shown in the example above. The value of each option is passed as a single argument to the `setup.py` script. Therefore, a line such as the following is invalid and would result in an installation error.
# Invalid. Please use '--install-option' twice as shown above.
FooProject >= 1.2 --install-option="--prefix=/usr/local --no-compile"
Note that the only way of giving more than one option to `setup.py` is through multiple `--global-option` options.

View file

@ -66,12 +66,120 @@ man pages][netrc-docs].
## Keyring Support
pip supports loading credentials stored in your keyring using the
{pypi}`keyring` library.
{pypi}`keyring` library, which can be enabled py passing `--keyring-provider`
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
is used; otherwise it tries the `import`, `subprocess`, and `disabled`
providers (in this order) and uses the first one that works.
### Configuring pip's keyring usage
Since the keyring configuration is likely system-wide, a more common way to
configure its usage would be to use a configuration instead:
```{seealso}
{doc}`./configuration` describes how pip configuration works.
```
```bash
$ pip install keyring # install keyring from PyPI
$ pip config set --global global.keyring-provider subprocess
# A different user on the same system which has PYTHONPATH configured and and
# wanting to use keyring installed that way could then run
$ pip config set --user global.keyring-provider import
# For a specific virtual environment you might want to use disable it again
# because you will only be using PyPI and the private repo (and mirror)
# requires 2FA with a keycard and a pincode
$ pip config set --site global.index https://pypi.org/simple
$ pip config set --site global.keyring-provider disabled
# configuring it via environment variable is also possible
$ export PIP_KEYRING_PROVIDER=disabled
```
### Using keyring's Python module
Setting `keyring-provider` to `import` makes pip communicate with `keyring` via
its Python interface.
```bash
# install keyring from PyPI
$ pip install keyring --index-url https://pypi.org/simple
$ echo "your-password" | keyring set pypi.company.com your-username
$ pip install your-package --index-url https://pypi.company.com/
$ pip install your-package --keyring-provider import --index-url https://pypi.company.com/
```
### Using keyring as a command line application
Setting `keyring-provider` to `subprocess` makes pip look for and use the
`keyring` command found on `PATH`.
For this use case, a username *must* be included in the URL, since it is
required by `keyring`'s command line interface. See the example below or the
basic HTTP authentication section at the top of this page.
```bash
# Install keyring from PyPI using pipx, which we assume is installed properly
# you can also create a venv somewhere and add it to the PATH yourself instead
$ pipx install keyring --index-url https://pypi.org/simple
# For Azure DevOps, also install its keyring backend.
$ pipx inject keyring artifacts-keyring --index-url https://pypi.org/simple
# For Google Artifact Registry, also install and initialize its keyring backend.
$ pipx inject keyring keyrings.google-artifactregistry-auth --index-url https://pypi.org/simple
$ gcloud auth login
# Note that a username is required in the index URL.
$ pip install your-package --keyring-provider subprocess --index-url https://username@pypi.example.com/
```
### Here be dragons
The `auto` provider is conservative and does not query keyring at all when
`--no-input` is used because the keyring might require user interaction such as
prompting the user on the console. Third party tools frequently call Pip for
you and do indeed pass `--no-input` as they are well-behaved and don't have
much information to work with. (Keyring does have an api to request a backend
that does not require user input.) You have more information about your system,
however!
You can force keyring usage by requesting a keyring provider other than `auto`
(or `disabled`). Leaving `import` and `subprocess`. You do this by passing
`--keyring-provider import` or one of the following methods:
```bash
# via config file, possibly with --user, --global or --site
$ pip config set global.keyring-provider subprocess
# or via environment variable
$ export PIP_KEYRING_PROVIDER=import
```
```{warning}
Be careful when doing this since it could cause tools such as pipx and Pipenv
to appear to hang. They show their own progress indicator while hiding output
from the subprocess in which they run Pip. You won't know whether the keyring
backend is waiting the user input or not in such situations.
```
pip is conservative and does not query keyring at all when `--no-input` is used
because the keyring might require user interaction such as prompting the user
on the console. You can force keyring usage by passing `--force-keyring` or one
of the following:
```bash
# possibly with --user, --global or --site
$ pip config set global.force-keyring true
# or
$ export PIP_FORCE_KEYRING=1
```
```{warning}
Be careful when doing this since it could cause tools such as pipx and Pipenv
to appear to hang. They show their own progress indicator while hiding output
from the subprocess in which they run Pip. You won't know whether the keyring
backend is waiting the user input or not in such situations.
```
Note that `keyring` (the Python package) needs to be installed separately from
@ -79,5 +187,4 @@ pip. This can create a bootstrapping issue if you need the credentials stored in
the keyring to download and install keyring.
It is, thus, expected that users that wish to use pip's keyring support have
some mechanism for downloading and installing {pypi}`keyring` in their Python
environment.
some mechanism for downloading and installing {pypi}`keyring`.

View file

@ -67,7 +67,7 @@ You can use `pip cache dir` to get the cache directory that pip is currently con
### Default paths
````{tab} Unix
````{tab} Linux
```
~/.cache/pip
```

View file

@ -19,13 +19,14 @@ and how they are related to pip's various command line options.
## Configuration Files
Configuration files can change the default values for command line option.
They are written using a standard INI style configuration files.
Configuration files can change the default values for command line options.
They are written using standard INI style configuration files.
pip has 3 "levels" of configuration files:
pip has 4 "levels" of configuration files:
- `global`: system-wide configuration file, shared across users.
- `user`: per-user configuration file.
- `global`: system-wide configuration file, shared across all users.
- `user`: per-user configuration file, shared across all environments.
- `base` : per-base environment configuration file, shared across all virtualenvs with the same base. (available since pip 23.0)
- `site`: per-environment configuration file; i.e. per-virtualenv.
### Location
@ -47,6 +48,9 @@ User
The legacy "per-user" configuration file is also loaded, if it exists: {file}`$HOME/.pip/pip.conf`.
Base
: {file}`\{sys.base_prefix\}/pip.conf`
Site
: {file}`$VIRTUAL_ENV/pip.conf`
```
@ -63,6 +67,9 @@ User
The legacy "per-user" configuration file is also loaded, if it exists: {file}`$HOME/.pip/pip.conf`.
Base
: {file}`\{sys.base_prefix\}/pip.conf`
Site
: {file}`$VIRTUAL_ENV/pip.conf`
```
@ -81,6 +88,9 @@ User
The legacy "per-user" configuration file is also loaded, if it exists: {file}`%HOME%\\pip\\pip.ini`
Base
: {file}`\{sys.base_prefix\}\\pip.ini`
Site
: {file}`%VIRTUAL_ENV%\\pip.ini`
```
@ -102,6 +112,7 @@ order:
- `PIP_CONFIG_FILE`, if given.
- Global
- User
- Base
- Site
Each file read overrides any values read from previous files, so if the

View file

@ -34,7 +34,7 @@ Editable installs allow you to install your project without copying any files. I
With an editable install, you only need to perform a re-installation if you change the project metadata (eg: version, what scripts need to be generated etc). You will still need to run build commands when you need to perform a compilation for non-Python code in the project (eg: C extensions).
```{caution}
It is possible to see behaviour differences between regular installs vs editable installs. In case you distribute the project as a "distribution package", users will see the behaviour of regular installs -- thus, it is important to ensure that regular installs work correctly.
It is possible to see behaviour differences between regular installs vs editable installs. These differences depend on the build-backend, and you should check the build-backend documentation for the details. In case you distribute the project as a "distribution package", users will see the behaviour of regular installs -- thus, it is important to ensure that regular installs work correctly.
```
```{note}

View file

@ -18,7 +18,7 @@ The supported schemes are `git+file`, `git+https`, `git+ssh`, `git+http`,
`git+git` and `git`. Here are some of the supported forms:
```none
MyProject @ git+ssh://git.example.com/MyProject
MyProject @ git+ssh://git@git.example.com/MyProject
MyProject @ git+file:///home/user/projects/MyProject
MyProject @ git+https://git.example.com/MyProject
```
@ -130,18 +130,19 @@ VCS source will not overwrite it without an `--upgrade` flag. Further, pip
looks at the package version, at the target revision to determine what action to
take on the VCS requirement (not the commit itself).
The {ref}`pip freeze` subcommand will record the VCS requirement specifier
(referencing a specific commit) only if the install is done with the editable
option.
## URL fragments
pip looks at 2 fragments for VCS URLs:
pip looks at the `subdirectory` fragments of VCS URLs for specifying the path to the
Python package, when it is not in the root of the VCS directory. eg: `pkg_dir`.
- `egg`: For specifying the "project name" for use in pip's dependency
resolution logic. eg: `egg=project_name`
- `subdirectory`: For specifying the path to the Python package, when it is not
in the root of the VCS directory. eg: `pkg_dir`
pip also looks at the `egg` fragment specifying the "project name". In practice the
`egg` fragment is only required to help pip determine the VCS clone location in editable
mode. In all other circumstances, the `egg` fragment is not necessary and its use is
discouraged.
The `egg` fragment **should** be a bare
[PEP 508](https://peps.python.org/pep-0508/) project name. Anything else
is not guaranteed to work.
````{admonition} Example
If your repository layout is:
@ -157,6 +158,12 @@ some_other_file
Then, to install from this repository, the syntax would be:
```{pip-cli}
$ pip install "pkg @ vcs+protocol://repo_url/#subdirectory=pkg_dir"
```
or:
```{pip-cli}
$ pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir"
```

View file

@ -800,7 +800,7 @@ As noted previously, pip is a command line program. While it is implemented in
Python, and so is available from your Python code via ``import pip``, you must
not use pip's internal APIs in this way. There are a number of reasons for this:
#. The pip code assumes that is in sole control of the global state of the
#. The pip code assumes that it is in sole control of the global state of the
program.
pip manages things like the logging system configuration, or the values of
the standard IO streams, without considering the possibility that user code

View file

@ -1 +0,0 @@
Fixed the description of the option "--install-options" in the documentation

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

@ -0,0 +1 @@
Display dependency chain on each Collecting/Processing log line.

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

@ -0,0 +1 @@
Remove support for the deprecated ``--install-options``.

2
news/11451.removal.rst Normal file
View file

@ -0,0 +1,2 @@
``--no-binary`` does not imply ``setup.py install`` anymore. Instead a wheel will be
built locally and installed.

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

@ -0,0 +1 @@
Fix grammar by changing "A new release of pip available:" to "A new release of pip is available:" in the notice used for indicating that.

View file

@ -1,3 +0,0 @@
Fix entry point generation of ``pip.X``, ``pipX.Y``, and ``easy_install-X.Y``
to correctly account for multi-digit Python version segments (e.g. the "11"
part of 3.11).

View file

@ -1,2 +0,0 @@
Enable the use of ``keyring`` found on ``PATH``. This allows ``keyring``
installed using ``pipx`` to be used by ``pip``.

View file

@ -1 +0,0 @@
Use the "venv" scheme if available to obtain prefixed lib paths.

4
news/11681.feature.rst Normal file
View file

@ -0,0 +1,4 @@
The ``--config-settings``/``-C`` option now supports using the same key multiple
times. When the same key is specified multiple times, all values are passed to
the build backend as a list, as opposed to the previous behavior, where pip would
only pass the last value if the same key was used multiple times.

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

@ -0,0 +1 @@
Correct the way to decide if keyring is available.

2
news/11775.doc.rst Normal file
View file

@ -0,0 +1,2 @@
Cross-reference the ``--python`` flag from the ``--prefix`` flag,
and mention limitations of ``--prefix`` regarding script installation.

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

@ -0,0 +1 @@
Add SECURITY.md to make the policy offical.

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

@ -0,0 +1 @@
More consistent resolution backtracking by removing legacy hack related to setuptools resolution

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

@ -0,0 +1 @@
Add username to Git over SSH example.

2
news/11842.doc.rst Normal file
View file

@ -0,0 +1,2 @@
Quote extras in the pip install docs to guard shells with default glob
qualifiers, like zsh.

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

@ -0,0 +1 @@
Add ``--keyring-provider`` flag. See the Authentication page in the documentation for more info.

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

@ -0,0 +1 @@
In the case of virtual environments, configuration files are now also included from the base installation.

View file

@ -1 +0,0 @@
Upgrade colorama to 0.4.6

View file

@ -1 +0,0 @@
Upgrade distro to 1.8.0

View file

@ -0,0 +1 @@
Patch pkg_resources to remove dependency on ``jaraco.text``.

View file

@ -1 +0,0 @@
Upgrade platformdirs to 2.5.3

View file

@ -0,0 +1 @@
Upgrade resolvelib to 0.9.0

View file

@ -0,0 +1 @@
Update pkg_resources (via setuptools) to 65.6.3

View file

@ -0,0 +1 @@
Add ``-C`` as a short version of the ``--config-settings`` option.

View file

@ -1,6 +1,7 @@
"""Automation using nox.
"""
import argparse
import glob
import os
import shutil
@ -183,7 +184,13 @@ def lint(session: nox.Session) -> None:
def vendoring(session: nox.Session) -> None:
session.install("vendoring~=1.2.0")
if "--upgrade" not in session.posargs:
parser = argparse.ArgumentParser(prog="nox -s vendoring")
parser.add_argument("--upgrade-all", action="store_true")
parser.add_argument("--upgrade", action="append", default=[])
parser.add_argument("--skip", action="append", default=[])
args = parser.parse_args(session.posargs)
if not (args.upgrade or args.upgrade_all):
session.run("vendoring", "sync", "-v")
return
@ -199,7 +206,9 @@ def vendoring(session: nox.Session) -> None:
vendor_txt = Path("src/pip/_vendor/vendor.txt")
for name, old_version in pinned_requirements(vendor_txt):
if name == "setuptools":
if name in args.skip:
continue
if args.upgrade and name not in args.upgrade:
continue
# update requirements.txt

View file

@ -50,6 +50,8 @@ drop = [
"easy_install.py",
"setuptools",
"pkg_resources/_vendor/",
"_distutils_hack",
"distutils-precedence.pth",
"pkg_resources/extern/",
# trim vendored pygments styles and lexers
"pygments/styles/[!_]*.py",

View file

@ -42,6 +42,9 @@ disallow_any_generics = True
warn_unused_ignores = True
no_implicit_optional = True
[mypy-pip._internal.utils._jaraco_text]
ignore_errors = True
[mypy-pip._vendor.*]
ignore_errors = True

View file

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

View file

@ -8,9 +8,8 @@ import site
import sys
import textwrap
from collections import OrderedDict
from sysconfig import get_paths
from types import TracebackType
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
from pip._vendor.certifi import where
from pip._vendor.packaging.requirements import Requirement
@ -18,11 +17,7 @@ from pip._vendor.packaging.version import Version
from pip import __file__ as pip_location
from pip._internal.cli.spinners import open_spinner
from pip._internal.locations import (
get_isolated_environment_lib_paths,
get_platlib,
get_purelib,
)
from pip._internal.locations import get_platlib, get_purelib, get_scheme
from pip._internal.metadata import get_default_environment, get_environment
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
@ -33,15 +28,17 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]:
return (a, b) if a != b else (a,)
class _Prefix:
def __init__(self, path: str) -> None:
self.path = path
self.setup = False
self.bin_dir = get_paths(
"nt" if os.name == "nt" else "posix_prefix",
vars={"base": path, "platbase": path},
)["scripts"]
self.lib_dirs = get_isolated_environment_lib_paths(path)
scheme = get_scheme("", prefix=path)
self.bin_dir = scheme.scripts
self.lib_dirs = _dedup(scheme.purelib, scheme.platlib)
def get_runnable_pip() -> str:

View file

@ -164,6 +164,14 @@ require_virtualenv: Callable[..., Option] = partial(
),
)
override_externally_managed: Callable[..., Option] = partial(
Option,
"--break-system-packages",
dest="override_externally_managed",
action="store_true",
help="Allow pip to modify an EXTERNALLY-MANAGED Python installation",
)
python: Callable[..., Option] = partial(
Option,
"--python",
@ -244,6 +252,19 @@ no_input: Callable[..., Option] = partial(
help="Disable prompting for input.",
)
keyring_provider: Callable[..., Option] = partial(
Option,
"--keyring-provider",
dest="keyring_provider",
choices=["auto", "disabled", "import", "subprocess"],
default="disabled",
help=(
"Enable the credential lookup via the keyring library if user input is allowed."
" Specify which mechanism to use [disabled, import, subprocess]."
" (default: disabled)"
),
)
proxy: Callable[..., Option] = partial(
Option,
"--proxy",
@ -803,11 +824,18 @@ def _handle_config_settings(
if dest is None:
dest = {}
setattr(parser.values, option.dest, dest)
dest[key] = val
if key in dest:
if isinstance(dest[key], list):
dest[key].append(val)
else:
dest[key] = [dest[key], val]
else:
dest[key] = val
config_settings: Callable[..., Option] = partial(
Option,
"-C",
"--config-settings",
dest="config_settings",
type=str,
@ -819,17 +847,6 @@ config_settings: Callable[..., Option] = partial(
"to pass multiple keys to the backend.",
)
install_options: Callable[..., Option] = partial(
Option,
"--install-option",
dest="install_options",
action="append",
metavar="options",
help="This option is deprecated. Using this option with location-changing "
"options may cause unexpected behavior. "
"Use pip-level options like --user, --prefix, --root, and --target.",
)
build_options: Callable[..., Option] = partial(
Option,
"--build-option",
@ -1019,6 +1036,7 @@ general_group: Dict[str, Any] = {
quiet,
log,
no_input,
keyring_provider,
proxy,
retries,
timeout,

View file

@ -151,6 +151,7 @@ class SessionCommandMixin(CommandContextMixIn):
# Determine if we can prompt the user for authentication or not
session.auth.prompting = not options.no_input
session.auth.keyring_provider = options.keyring_provider
return session

View file

@ -48,7 +48,7 @@ def create_vendor_txt_map() -> Dict[str, str]:
def get_module_from_module_name(module_name: str) -> ModuleType:
# Module name can be uppercase in vendor.txt for some reason...
module_name = module_name.lower()
module_name = module_name.lower().replace("-", "_")
# PATCH: setuptools is actually only pkg_resources.
if module_name == "setuptools":
module_name = "pkg_resources"

View file

@ -8,10 +8,7 @@ from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.req.req_install import (
LegacySetupPyOptionsCheckMode,
check_legacy_setup_py_options,
)
from pip._internal.req.req_install import check_legacy_setup_py_options
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
from pip._internal.utils.temp_dir import TempDirectory
@ -109,9 +106,7 @@ class DownloadCommand(RequirementCommand):
)
reqs = self.get_requirements(args, options, finder, session)
check_legacy_setup_py_options(
options, reqs, LegacySetupPyOptionsCheckMode.DOWNLOAD
)
check_legacy_setup_py_options(options, reqs)
preparer = self.make_requirement_preparer(
temp_build_dir=directory,

View file

@ -24,6 +24,7 @@ class IndexCommand(IndexGroupCommand):
Inspect information available from package indexes.
"""
ignore_require_venv = True
usage = """
%prog versions <package>
"""

View file

@ -46,11 +46,6 @@ class InspectCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
logger.warning(
"pip inspect is currently an experimental command. "
"The output format may change in a future release without prior warning."
)
cmdoptions.check_list_path_option(options)
dists = get_environment(options.path).iter_installed_distributions(
local_only=options.local,
@ -58,7 +53,7 @@ class InspectCommand(Command):
skip=set(stdlib_pkgs),
)
output = {
"version": "0",
"version": "1",
"pip_version": __version__,
"installed": [self._dist_to_dict(dist) for dist in dists],
"environment": default_environment(),

View file

@ -5,9 +5,8 @@ import os
import shutil
import site
from optparse import SUPPRESS_HELP, Values
from typing import Iterable, List, Optional
from typing import List, Optional
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.rich import print_json
from pip._internal.cache import WheelCache
@ -22,14 +21,12 @@ from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.exceptions import CommandError, InstallationError
from pip._internal.locations import get_scheme
from pip._internal.metadata import get_environment
from pip._internal.models.format_control import FormatControl
from pip._internal.models.installation_report import InstallationReport
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.operations.check import ConflictDetails, check_install_conflicts
from pip._internal.req import install_given_reqs
from pip._internal.req.req_install import (
InstallRequirement,
LegacySetupPyOptionsCheckMode,
check_legacy_setup_py_options,
)
from pip._internal.utils.compat import WINDOWS
@ -37,10 +34,10 @@ from pip._internal.utils.deprecation import (
LegacyInstallReasonFailedBdistWheel,
deprecated,
)
from pip._internal.utils.distutils_args import parse_distutils_args
from pip._internal.utils.filesystem import test_writable_dir
from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import (
check_externally_managed,
ensure_dir,
get_pip_version,
protect_pip_from_modification_on_windows,
@ -51,26 +48,11 @@ from pip._internal.utils.virtualenv import (
running_under_virtualenv,
virtualenv_no_global,
)
from pip._internal.wheel_builder import (
BdistWheelAllowedPredicate,
build,
should_build_for_install_command,
)
from pip._internal.wheel_builder import build, should_build_for_install_command
logger = getLogger(__name__)
def get_check_bdist_wheel_allowed(
format_control: FormatControl,
) -> BdistWheelAllowedPredicate:
def check_binary_allowed(req: InstallRequirement) -> bool:
canonical_name = canonicalize_name(req.name or "")
allowed_formats = format_control.get_allowed_formats(canonical_name)
return "binary" in allowed_formats
return check_binary_allowed
class InstallCommand(RequirementCommand):
"""
Install packages from:
@ -155,7 +137,12 @@ class InstallCommand(RequirementCommand):
default=None,
help=(
"Installation prefix where lib, bin and other top-level "
"folders are placed"
"folders are placed. Note that the resulting installation may "
"contain scripts and other resources which reference the "
"Python interpreter of pip, and not that of ``--prefix``. "
"See also the ``--python`` option if the intention is to "
"install packages into another (possibly pip-free) "
"environment."
),
)
@ -214,9 +201,9 @@ class InstallCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.override_externally_managed())
self.cmd_opts.add_option(cmdoptions.config_settings())
self.cmd_opts.add_option(cmdoptions.install_options())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
@ -284,14 +271,29 @@ class InstallCommand(RequirementCommand):
if options.use_user_site and options.target_dir is not None:
raise CommandError("Can not combine '--user' and '--target'")
# Check whether the environment we're installing into is externally
# managed, as specified in PEP 668. Specifying --root, --target, or
# --prefix disables the check, since there's no reliable way to locate
# the EXTERNALLY-MANAGED file for those cases. An exception is also
# made specifically for "--dry-run --report" for convenience.
installing_into_current_environment = (
not (options.dry_run and options.json_report_file)
and options.root_path is None
and options.target_dir is None
and options.prefix_path is None
)
if (
installing_into_current_environment
and not options.override_externally_managed
):
check_externally_managed()
upgrade_strategy = "to-satisfy-only"
if options.upgrade:
upgrade_strategy = options.upgrade_strategy
cmdoptions.check_dist_restriction(options, check_target=True)
install_options = options.install_options or []
logger.verbose("Using %s", get_pip_version())
options.use_user_site = decide_user_install(
options.use_user_site,
@ -342,9 +344,7 @@ class InstallCommand(RequirementCommand):
try:
reqs = self.get_requirements(args, options, finder, session)
check_legacy_setup_py_options(
options, reqs, LegacySetupPyOptionsCheckMode.INSTALL
)
check_legacy_setup_py_options(options, reqs)
if "no-binary-enable-wheel-cache" in options.features_enabled:
# TODO: remove format_control from WheelCache when the deprecation cycle
@ -371,8 +371,6 @@ class InstallCommand(RequirementCommand):
for req in reqs:
req.permit_editable_wheels = True
reject_location_related_install_options(reqs, options.install_options)
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
options=options,
@ -402,12 +400,6 @@ class InstallCommand(RequirementCommand):
)
if options.json_report_file:
logger.warning(
"--report is currently an experimental option. "
"The output format may change in a future release "
"without prior warning."
)
report = InstallationReport(requirement_set.requirements_to_install)
if options.json_report_file == "-":
print_json(data=report.to_dict())
@ -437,14 +429,10 @@ class InstallCommand(RequirementCommand):
modifying_pip = pip_req.satisfied_by is None
protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
check_bdist_wheel_allowed = get_check_bdist_wheel_allowed(
finder.format_control
)
reqs_to_build = [
r
for r in requirement_set.requirements.values()
if should_build_for_install_command(r, check_bdist_wheel_allowed)
if should_build_for_install_command(r)
]
_, build_failures = build(
@ -493,7 +481,6 @@ class InstallCommand(RequirementCommand):
installed = install_given_reqs(
to_install,
install_options,
global_options,
root=options.root_path,
home=target_temp_dir_path,
@ -764,45 +751,6 @@ def decide_user_install(
return True
def reject_location_related_install_options(
requirements: List[InstallRequirement], options: Optional[List[str]]
) -> None:
"""If any location-changing --install-option arguments were passed for
requirements or on the command-line, then show a deprecation warning.
"""
def format_options(option_names: Iterable[str]) -> List[str]:
return ["--{}".format(name.replace("_", "-")) for name in option_names]
offenders = []
for requirement in requirements:
install_options = requirement.install_options
location_options = parse_distutils_args(install_options)
if location_options:
offenders.append(
"{!r} from {}".format(
format_options(location_options.keys()), requirement
)
)
if options:
location_options = parse_distutils_args(options)
if location_options:
offenders.append(
"{!r} from command line".format(format_options(location_options.keys()))
)
if not offenders:
return
raise CommandError(
"Location-changing options found in --install-option: {}."
" This is unsupported, use pip-level options like --user,"
" --prefix, --root, and --target instead.".format("; ".join(offenders))
)
def create_os_error_message(
error: OSError, show_traceback: bool, using_user_site: bool
) -> str:

View file

@ -53,6 +53,7 @@ class _PackageInfo(NamedTuple):
name: str
version: str
location: str
editable_project_location: Optional[str]
requires: List[str]
required_by: List[str]
installer: str
@ -120,6 +121,7 @@ def search_packages_info(query: List[str]) -> Generator[_PackageInfo, None, None
name=dist.raw_name,
version=str(dist.version),
location=dist.location or "",
editable_project_location=dist.editable_project_location,
requires=requires,
required_by=required_by,
installer=dist.installer,
@ -158,6 +160,10 @@ def print_results(
write_output("Author-email: %s", dist.author_email)
write_output("License: %s", dist.license)
write_output("Location: %s", dist.location)
if dist.editable_project_location is not None:
write_output(
"Editable project location: %s", dist.editable_project_location
)
write_output("Requires: %s", ", ".join(dist.requires))
write_output("Required-by: %s", ", ".join(dist.required_by))

View file

@ -14,7 +14,10 @@ from pip._internal.req.constructors import (
install_req_from_line,
install_req_from_parsed_requirement,
)
from pip._internal.utils.misc import protect_pip_from_modification_on_windows
from pip._internal.utils.misc import (
check_externally_managed,
protect_pip_from_modification_on_windows,
)
logger = logging.getLogger(__name__)
@ -55,6 +58,7 @@ class UninstallCommand(Command, SessionCommandMixin):
help="Don't ask for confirmation of uninstall deletions.",
)
self.cmd_opts.add_option(cmdoptions.root_user_action())
self.cmd_opts.add_option(cmdoptions.override_externally_managed())
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
@ -90,6 +94,9 @@ class UninstallCommand(Command, SessionCommandMixin):
f'"pip help {self.name}")'
)
if not options.override_externally_managed:
check_externally_managed()
protect_pip_from_modification_on_windows(
modifying_pip="pip" in reqs_to_uninstall
)

View file

@ -12,7 +12,6 @@ from pip._internal.exceptions import CommandError
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.req.req_install import (
InstallRequirement,
LegacySetupPyOptionsCheckMode,
check_legacy_setup_py_options,
)
from pip._internal.utils.deprecation import deprecated
@ -122,9 +121,7 @@ class WheelCommand(RequirementCommand):
)
reqs = self.get_requirements(args, options, finder, session)
check_legacy_setup_py_options(
options, reqs, LegacySetupPyOptionsCheckMode.WHEEL
)
check_legacy_setup_py_options(options, reqs)
if "no-binary-enable-wheel-cache" in options.features_enabled:
# TODO: remove format_control from WheelCache when the deprecation cycle

View file

@ -36,12 +36,20 @@ ENV_NAMES_IGNORED = "version", "help"
kinds = enum(
USER="user", # User Specific
GLOBAL="global", # System Wide
SITE="site", # [Virtual] Environment Specific
BASE="base", # Base environment specific (e.g. for all venvs with the same base)
SITE="site", # Environment Specific (e.g. per venv)
ENV="env", # from PIP_CONFIG_FILE
ENV_VAR="env-var", # from Environment Variables
)
OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE
OVERRIDE_ORDER = (
kinds.GLOBAL,
kinds.USER,
kinds.BASE,
kinds.SITE,
kinds.ENV,
kinds.ENV_VAR,
)
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.BASE, kinds.SITE
logger = getLogger(__name__)
@ -70,6 +78,7 @@ def get_configuration_files() -> Dict[Kind, List[str]]:
os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
]
base_config_file = os.path.join(sys.base_prefix, CONFIG_BASENAME)
site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME)
legacy_config_file = os.path.join(
os.path.expanduser("~"),
@ -78,6 +87,7 @@ def get_configuration_files() -> Dict[Kind, List[str]]:
)
new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME)
return {
kinds.BASE: [base_config_file],
kinds.GLOBAL: global_config_files,
kinds.SITE: [site_config_file],
kinds.USER: [legacy_config_file, new_config_file],
@ -344,6 +354,8 @@ class Configuration:
# The legacy config file is overridden by the new config file
yield kinds.USER, config_files[kinds.USER]
yield kinds.BASE, config_files[kinds.BASE]
# finally virtualenv configuration first trumping others
yield kinds.SITE, config_files[kinds.SITE]

View file

@ -6,9 +6,14 @@ subpackage and, thus, should not depend on them.
"""
import configparser
import contextlib
import locale
import logging
import pathlib
import re
import sys
from itertools import chain, groupby, repeat
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union
from pip._vendor.requests.models import Request, Response
from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
@ -22,6 +27,8 @@ if TYPE_CHECKING:
from pip._internal.metadata import BaseDistribution
from pip._internal.req.req_install import InstallRequirement
logger = logging.getLogger(__name__)
#
# Scaffolding
@ -658,3 +665,83 @@ class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
assert self.error is not None
message_part = f".\n{self.error}\n"
return f"Configuration file {self.reason}{message_part}"
_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\
The Python environment under {sys.prefix} is managed externally, and may not be
manipulated by the user. Please use specific tooling from the distributor of
the Python installation to interact with this environment instead.
"""
class ExternallyManagedEnvironment(DiagnosticPipError):
"""The current environment is externally managed.
This is raised when the current environment is externally managed, as
defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked
and displayed when the error is bubbled up to the user.
:param error: The error message read from ``EXTERNALLY-MANAGED``.
"""
reference = "externally-managed-environment"
def __init__(self, error: Optional[str]) -> None:
if error is None:
context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR)
else:
context = Text(error)
super().__init__(
message="This environment is externally managed",
context=context,
note_stmt=(
"If you believe this is a mistake, please contact your "
"Python installation or OS distribution provider. "
"You can override this, at the risk of breaking your Python "
"installation or OS, by passing --break-system-packages."
),
hint_stmt=Text("See PEP 668 for the detailed specification."),
)
@staticmethod
def _iter_externally_managed_error_keys() -> Iterator[str]:
# LC_MESSAGES is in POSIX, but not the C standard. The most common
# platform that does not implement this category is Windows, where
# using other categories for console message localization is equally
# unreliable, so we fall back to the locale-less vendor message. This
# can always be re-evaluated when a vendor proposes a new alternative.
try:
category = locale.LC_MESSAGES
except AttributeError:
lang: Optional[str] = None
else:
lang, _ = locale.getlocale(category)
if lang is not None:
yield f"Error-{lang}"
for sep in ("-", "_"):
before, found, _ = lang.partition(sep)
if not found:
continue
yield f"Error-{before}"
yield "Error"
@classmethod
def from_config(
cls,
config: Union[pathlib.Path, str],
) -> "ExternallyManagedEnvironment":
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read(config, encoding="utf-8")
section = parser["externally-managed"]
for key in cls._iter_externally_managed_error_keys():
with contextlib.suppress(KeyError):
return cls(section[key])
except KeyError:
pass
except (OSError, UnicodeDecodeError, configparser.ParsingError):
from pip._internal.utils._log import VERBOSE
exc_info = logger.isEnabledFor(VERBOSE)
logger.warning("Failed to read %s", config, exc_info=exc_info)
return cls(None)

View file

@ -1,14 +1,11 @@
"""Routines related to PyPI, indexes"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import enum
import functools
import itertools
import logging
import re
from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Set, Tuple, Union
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.tags import Tag
@ -39,6 +36,9 @@ from pip._internal.utils.misc import build_netloc
from pip._internal.utils.packaging import check_requires_python
from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
if TYPE_CHECKING:
from pip._vendor.typing_extensions import TypeGuard
__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
@ -251,7 +251,7 @@ class LinkEvaluator:
def filter_unallowed_hashes(
candidates: List[InstallationCandidate],
hashes: Hashes,
hashes: Optional[Hashes],
project_name: str,
) -> List[InstallationCandidate]:
"""
@ -540,6 +540,7 @@ class CandidateEvaluator:
binary_preference = 1
if wheel.build_tag is not None:
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
assert match is not None, "guaranteed by filename validation"
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
@ -942,43 +943,46 @@ class PackageFinder:
"No matching distribution found for {}".format(req)
)
best_installed = False
if installed_version and (
best_candidate is None or best_candidate.version <= installed_version
):
best_installed = True
def _should_install_candidate(
candidate: Optional[InstallationCandidate],
) -> "TypeGuard[InstallationCandidate]":
if installed_version is None:
return True
if best_candidate is None:
return False
return best_candidate.version > installed_version
if not upgrade and installed_version is not None:
if best_installed:
logger.debug(
"Existing installed version (%s) is most up-to-date and "
"satisfies requirement",
installed_version,
)
else:
if _should_install_candidate(best_candidate):
logger.debug(
"Existing installed version (%s) satisfies requirement "
"(most up-to-date version is %s)",
installed_version,
best_candidate.version,
)
else:
logger.debug(
"Existing installed version (%s) is most up-to-date and "
"satisfies requirement",
installed_version,
)
return None
if best_installed:
# We have an existing version, and its the best version
if _should_install_candidate(best_candidate):
logger.debug(
"Installed version (%s) is most up-to-date (past versions: %s)",
installed_version,
"Using version %s (newest of versions: %s)",
best_candidate.version,
_format_versions(best_candidate_result.iter_applicable()),
)
raise BestVersionAlreadyInstalled
return best_candidate
# We have an existing version, and its the best version
logger.debug(
"Using version %s (newest of versions: %s)",
best_candidate.version,
"Installed version (%s) is most up-to-date (past versions: %s)",
installed_version,
_format_versions(best_candidate_result.iter_applicable()),
)
return best_candidate
raise BestVersionAlreadyInstalled
def _find_name_version_sep(fragment: str, canonical_name: str) -> int:

View file

@ -4,7 +4,7 @@ import os
import pathlib
import sys
import sysconfig
from typing import Any, Dict, Generator, List, Optional, Tuple
from typing import Any, Dict, Generator, Optional, Tuple
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
from pip._internal.utils.compat import WINDOWS
@ -27,7 +27,6 @@ __all__ = [
"get_bin_user",
"get_major_minor_version",
"get_platlib",
"get_isolated_environment_lib_paths",
"get_purelib",
"get_scheme",
"get_src_prefix",
@ -466,63 +465,3 @@ def get_platlib() -> str:
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"):
_log_context()
return old
def _deduplicated(v1: str, v2: str) -> List[str]:
"""Deduplicate values from a list."""
if v1 == v2:
return [v1]
return [v1, v2]
def _looks_like_apple_library(path: str) -> bool:
"""Apple patches sysconfig to *always* look under */Library/Python*."""
if sys.platform[:6] != "darwin":
return False
return path == f"/Library/Python/{get_major_minor_version()}/site-packages"
def get_isolated_environment_lib_paths(prefix: str) -> List[str]:
"""Return the lib locations under ``prefix``."""
new_pure, new_plat = _sysconfig.get_isolated_environment_lib_paths(prefix)
if _USE_SYSCONFIG:
return _deduplicated(new_pure, new_plat)
old_pure, old_plat = _distutils.get_isolated_environment_lib_paths(prefix)
old_lib_paths = _deduplicated(old_pure, old_plat)
# Apple's Python (shipped with Xcode and Command Line Tools) hard-code
# platlib and purelib to '/Library/Python/X.Y/site-packages'. This will
# cause serious build isolation bugs when Apple starts shipping 3.10 because
# pip will install build backends to the wrong location. This tells users
# who is at fault so Apple may notice it and fix the issue in time.
if all(_looks_like_apple_library(p) for p in old_lib_paths):
deprecated(
reason=(
"Python distributed by Apple's Command Line Tools incorrectly "
"patches sysconfig to always point to '/Library/Python'. This "
"will cause build isolation to operate incorrectly on Python "
"3.10 or later. Please help report this to Apple so they can "
"fix this. https://developer.apple.com/bug-reporting/"
),
replacement=None,
gone_in=None,
)
return old_lib_paths
warned = [
_warn_if_mismatch(
pathlib.Path(old_pure),
pathlib.Path(new_pure),
key="prefixed-purelib",
),
_warn_if_mismatch(
pathlib.Path(old_plat),
pathlib.Path(new_plat),
key="prefixed-platlib",
),
]
if any(warned):
_log_context(prefix=prefix)
return old_lib_paths

View file

@ -21,7 +21,7 @@ from distutils.cmd import Command as DistutilsCommand
from distutils.command.install import SCHEME_KEYS
from distutils.command.install import install as distutils_install_command
from distutils.sysconfig import get_python_lib
from typing import Dict, List, Optional, Tuple, Union, cast
from typing import Dict, List, Optional, Union, cast
from pip._internal.models.scheme import Scheme
from pip._internal.utils.compat import WINDOWS
@ -171,10 +171,3 @@ def get_purelib() -> str:
def get_platlib() -> str:
return get_python_lib(plat_specific=True)
def get_isolated_environment_lib_paths(prefix: str) -> Tuple[str, str]:
return (
get_python_lib(plat_specific=False, prefix=prefix),
get_python_lib(plat_specific=True, prefix=prefix),
)

View file

@ -211,12 +211,3 @@ def get_purelib() -> str:
def get_platlib() -> str:
return sysconfig.get_paths()["platlib"]
def get_isolated_environment_lib_paths(prefix: str) -> typing.Tuple[str, str]:
vars = {"base": prefix, "platbase": prefix}
if "venv" in sysconfig.get_scheme_names():
paths = sysconfig.get_paths(vars=vars, scheme="venv")
else:
paths = sysconfig.get_paths(vars=vars)
return (paths["purelib"], paths["platlib"])

View file

@ -13,7 +13,7 @@ from pip._internal.utils.virtualenv import running_under_virtualenv
USER_CACHE_DIR = appdirs.user_cache_dir("pip")
# FIXME doesn't account for venv linked to global site-packages
site_packages: typing.Optional[str] = sysconfig.get_path("purelib")
site_packages: str = sysconfig.get_path("purelib")
def get_major_minor_version() -> str:

View file

@ -103,17 +103,33 @@ class ArchiveInfo:
def __init__(
self,
hash: Optional[str] = None,
hashes: Optional[Dict[str, str]] = None,
) -> None:
if hash is not None:
# Auto-populate the hashes key to upgrade to the new format automatically.
# We don't back-populate the legacy hash key.
try:
hash_name, hash_value = hash.split("=", 1)
except ValueError:
raise DirectUrlValidationError(
f"invalid archive_info.hash format: {hash!r}"
)
if hashes is None:
hashes = {hash_name: hash_value}
elif hash_name not in hash:
hashes = hashes.copy()
hashes[hash_name] = hash_value
self.hash = hash
self.hashes = hashes
@classmethod
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]:
if d is None:
return None
return cls(hash=_get(d, str, "hash"))
return cls(hash=_get(d, str, "hash"), hashes=_get(d, dict, "hashes"))
def _to_dict(self) -> Dict[str, Any]:
return _filter_none(hash=self.hash)
return _filter_none(hash=self.hash, hashes=self.hashes)
class DirInfo:

View file

@ -38,7 +38,7 @@ class InstallationReport:
def to_dict(self) -> Dict[str, Any]:
return {
"version": "0",
"version": "1",
"pip_version": __version__,
"install": [
self._install_req_to_dict(ireq) for ireq in self._install_requirements

View file

@ -18,6 +18,7 @@ from typing import (
Union,
)
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.filetypes import WHEEL_EXTENSION
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
@ -78,6 +79,9 @@ class LinkHash:
name, value = match.groups()
return cls(name=name, value=value)
def as_dict(self) -> Dict[str, str]:
return {self.name: self.value}
def as_hashes(self) -> Hashes:
"""Return a Hashes instance which checks only for the current hash."""
return Hashes({self.name: [self.value]})
@ -164,8 +168,8 @@ class Link(KeyBasedCompareMixin):
"requires_python",
"yanked_reason",
"dist_info_metadata",
"link_hash",
"cache_link_parsing",
"egg_fragment",
]
def __init__(
@ -175,7 +179,6 @@ class Link(KeyBasedCompareMixin):
requires_python: Optional[str] = None,
yanked_reason: Optional[str] = None,
dist_info_metadata: Optional[str] = None,
link_hash: Optional[LinkHash] = None,
cache_link_parsing: bool = True,
hashes: Optional[Mapping[str, str]] = None,
) -> None:
@ -198,16 +201,11 @@ class Link(KeyBasedCompareMixin):
attribute, if present, in a simple repository HTML link. This may be parsed
into its own `Link` by `self.metadata_link()`. See PEP 658 for more
information and the specification.
:param link_hash: a checksum for the content the link points to. If not
provided, this will be extracted from the link URL, if the URL has
any checksum.
:param cache_link_parsing: A flag that is used elsewhere to determine
whether resources retrieved from this link
should be cached. PyPI index urls should
generally have this set to False, for
example.
whether resources retrieved from this link should be cached. PyPI
URLs should generally have this set to False, for example.
:param hashes: A mapping of hash names to digests to allow us to
determine the validity of a download.
determine the validity of a download.
"""
# url can be a UNC windows share
@ -218,17 +216,23 @@ class Link(KeyBasedCompareMixin):
# Store the url as a private attribute to prevent accidentally
# trying to set a new value.
self._url = url
self._hashes = hashes if hashes is not None else {}
link_hash = LinkHash.split_hash_name_and_value(url)
hashes_from_link = {} if link_hash is None else link_hash.as_dict()
if hashes is None:
self._hashes = hashes_from_link
else:
self._hashes = {**hashes, **hashes_from_link}
self.comes_from = comes_from
self.requires_python = requires_python if requires_python else None
self.yanked_reason = yanked_reason
self.dist_info_metadata = dist_info_metadata
self.link_hash = link_hash or LinkHash.split_hash_name_and_value(self._url)
super().__init__(key=url, defining_class=Link)
self.cache_link_parsing = cache_link_parsing
self.egg_fragment = self._egg_fragment()
@classmethod
def from_json(
@ -358,12 +362,28 @@ class Link(KeyBasedCompareMixin):
_egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
@property
def egg_fragment(self) -> Optional[str]:
# Per PEP 508.
_project_name_re = re.compile(
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
)
def _egg_fragment(self) -> Optional[str]:
match = self._egg_fragment_re.search(self._url)
if not match:
return None
return match.group(1)
# An egg fragment looks like a PEP 508 project name, along with
# an optional extras specifier. Anything else is invalid.
project_name = match.group(1)
if not self._project_name_re.match(project_name):
deprecated(
reason=f"{self} contains an egg fragment with a non-PEP 508 name",
replacement="to use the req @ url syntax, and remove the egg fragment",
gone_in="25.0",
issue=11617,
)
return project_name
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
@ -382,29 +402,26 @@ class Link(KeyBasedCompareMixin):
if self.dist_info_metadata is None:
return None
metadata_url = f"{self.url_without_fragment}.metadata"
link_hash: Optional[LinkHash] = None
# If data-dist-info-metadata="true" is set, then the metadata file exists,
# but there is no information about its checksum or anything else.
if self.dist_info_metadata != "true":
link_hash = LinkHash.split_hash_name_and_value(self.dist_info_metadata)
return Link(metadata_url, link_hash=link_hash)
else:
link_hash = None
if link_hash is None:
return Link(metadata_url)
return Link(metadata_url, hashes=link_hash.as_dict())
def as_hashes(self) -> Optional[Hashes]:
if self.link_hash is not None:
return self.link_hash.as_hashes()
return None
def as_hashes(self) -> Hashes:
return Hashes({k: [v] for k, v in self._hashes.items()})
@property
def hash(self) -> Optional[str]:
if self.link_hash is not None:
return self.link_hash.value
return None
return next(iter(self._hashes.values()), None)
@property
def hash_name(self) -> Optional[str]:
if self.link_hash is not None:
return self.link_hash.name
return None
return next(iter(self._hashes), None)
@property
def show_url(self) -> str:
@ -433,15 +450,15 @@ class Link(KeyBasedCompareMixin):
@property
def has_hash(self) -> bool:
return self.link_hash is not None
return bool(self._hashes)
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
"""
Return True if the link has a hash and it is allowed by `hashes`.
"""
if self.link_hash is None:
if hashes is None:
return False
return self.link_hash.is_hash_allowed(hashes)
return any(hashes.is_hash_allowed(k, v) for k, v in self._hashes.items())
class _CleanResult(NamedTuple):

View file

@ -3,12 +3,17 @@
Contains interface (MultiDomainBasicAuth) and associated glue code for
providing credentials in the context of network requests.
"""
import logging
import os
import shutil
import subprocess
import sysconfig
import typing
import urllib.parse
from abc import ABC, abstractmethod
from functools import lru_cache
from os.path import commonprefix
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
@ -39,6 +44,8 @@ class Credentials(NamedTuple):
class KeyRingBaseProvider(ABC):
"""Keyring base provider interface"""
has_keyring: bool
@abstractmethod
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
...
@ -51,6 +58,8 @@ class KeyRingBaseProvider(ABC):
class KeyRingNullProvider(KeyRingBaseProvider):
"""Keyring null provider"""
has_keyring = False
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
return None
@ -61,6 +70,8 @@ class KeyRingNullProvider(KeyRingBaseProvider):
class KeyRingPythonProvider(KeyRingBaseProvider):
"""Keyring interface which uses locally imported `keyring`"""
has_keyring = True
def __init__(self) -> None:
import keyring
@ -97,6 +108,8 @@ class KeyRingCliProvider(KeyRingBaseProvider):
PATH.
"""
has_keyring = True
def __init__(self, cmd: str) -> None:
self.keyring = cmd
@ -123,77 +136,100 @@ class KeyRingCliProvider(KeyRingBaseProvider):
res = subprocess.run(
cmd,
stdin=subprocess.DEVNULL,
capture_output=True,
stdout=subprocess.PIPE,
env=env,
)
if res.returncode:
return None
return res.stdout.decode("utf-8").strip("\n")
return res.stdout.decode("utf-8").strip(os.linesep)
def _set_password(self, service_name: str, username: str, password: str) -> None:
"""Mirror the implementation of keyring.set_password using cli"""
if self.keyring is None:
return None
cmd = [self.keyring, "set", service_name, username]
input_ = password.encode("utf-8") + b"\n"
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
res = subprocess.run(cmd, input=input_, env=env)
res.check_returncode()
subprocess.run(
[self.keyring, "set", service_name, username],
input=f"{password}{os.linesep}".encode("utf-8"),
env=env,
check=True,
)
return None
def get_keyring_provider() -> KeyRingBaseProvider:
@lru_cache(maxsize=None)
def get_keyring_provider(provider: str) -> KeyRingBaseProvider:
logger.verbose("Keyring provider requested: %s", provider)
# keyring has previously failed and been disabled
if not KEYRING_DISABLED:
# Default to trying to use Python provider
if KEYRING_DISABLED:
provider = "disabled"
if provider in ["import", "auto"]:
try:
return KeyRingPythonProvider()
impl = KeyRingPythonProvider()
logger.verbose("Keyring provider set: import")
return impl
except ImportError:
pass
except Exception as exc:
# In the event of an unexpected exception
# we should warn the user
logger.warning(
"Installed copy of keyring fails with exception %s, "
"trying to find a keyring executable as a fallback",
str(exc),
)
# Fallback to Cli Provider if `keyring` isn't installed
msg = "Installed copy of keyring fails with exception %s"
if provider == "auto":
msg = msg + ", trying to find a keyring executable as a fallback"
logger.warning(msg, exc, exc_info=logger.isEnabledFor(logging.DEBUG))
if provider in ["subprocess", "auto"]:
cli = shutil.which("keyring")
if cli and cli.startswith(sysconfig.get_path("scripts")):
# all code within this function is stolen from shutil.which implementation
@typing.no_type_check
def PATH_as_shutil_which_determines_it() -> str:
path = os.environ.get("PATH", None)
if path is None:
try:
path = os.confstr("CS_PATH")
except (AttributeError, ValueError):
# os.confstr() or CS_PATH is not available
path = os.defpath
# bpo-35755: Don't use os.defpath if the PATH environment variable is
# set to an empty string
return path
scripts = Path(sysconfig.get_path("scripts"))
paths = []
for path in PATH_as_shutil_which_determines_it().split(os.pathsep):
p = Path(path)
try:
if not p.samefile(scripts):
paths.append(path)
except FileNotFoundError:
pass
path = os.pathsep.join(paths)
cli = shutil.which("keyring", path=path)
if cli:
logger.verbose("Keyring provider set: subprocess with executable %s", cli)
return KeyRingCliProvider(cli)
logger.verbose("Keyring provider set: disabled")
return KeyRingNullProvider()
def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[AuthInfo]:
"""Return the tuple auth for a given url from keyring."""
# Do nothing if no url was provided
if not url:
return None
keyring = get_keyring_provider()
try:
return keyring.get_auth_info(url, username)
except Exception as exc:
logger.warning(
"Keyring is skipped due to an exception: %s",
str(exc),
)
global KEYRING_DISABLED
KEYRING_DISABLED = True
return None
class MultiDomainBasicAuth(AuthBase):
def __init__(
self, prompting: bool = True, index_urls: Optional[List[str]] = None
self,
prompting: bool = True,
index_urls: Optional[List[str]] = None,
keyring_provider: str = "auto",
) -> None:
self.prompting = prompting
self.index_urls = index_urls
self.keyring_provider = keyring_provider # type: ignore[assignment]
self.passwords: Dict[str, AuthInfo] = {}
# When the user is prompted to enter credentials and keyring is
# available, we will offer to save them. If the user accepts,
@ -202,6 +238,47 @@ class MultiDomainBasicAuth(AuthBase):
# ``save_credentials`` to save these.
self._credentials_to_save: Optional[Credentials] = None
@property
def keyring_provider(self) -> KeyRingBaseProvider:
return get_keyring_provider(self._keyring_provider)
@keyring_provider.setter
def keyring_provider(self, provider: str) -> None:
# The free function get_keyring_provider has been decorated with
# functools.cache. If an exception occurs in get_keyring_auth that
# cache will be cleared and keyring disabled, take that into account
# if you want to remove this indirection.
self._keyring_provider = provider
@property
def use_keyring(self) -> bool:
# We won't use keyring when --no-input is passed unless
# a specific provider is requested because it might require
# user interaction
return self.prompting or self._keyring_provider not in ["auto", "disabled"]
def _get_keyring_auth(
self,
url: Optional[str],
username: Optional[str],
) -> Optional[AuthInfo]:
"""Return the tuple auth for a given url from keyring."""
# Do nothing if no url was provided
if not url:
return None
try:
return self.keyring_provider.get_auth_info(url, username)
except Exception as exc:
logger.warning(
"Keyring is skipped due to an exception: %s",
str(exc),
)
global KEYRING_DISABLED
KEYRING_DISABLED = True
get_keyring_provider.cache_clear()
return None
def _get_index_url(self, url: str) -> Optional[str]:
"""Return the original index URL matching the requested URL.
@ -218,15 +295,42 @@ class MultiDomainBasicAuth(AuthBase):
if not url or not self.index_urls:
return None
for u in self.index_urls:
prefix = remove_auth_from_url(u).rstrip("/") + "/"
if url.startswith(prefix):
return u
return None
url = remove_auth_from_url(url).rstrip("/") + "/"
parsed_url = urllib.parse.urlsplit(url)
candidates = []
for index in self.index_urls:
index = index.rstrip("/") + "/"
parsed_index = urllib.parse.urlsplit(remove_auth_from_url(index))
if parsed_url == parsed_index:
return index
if parsed_url.netloc != parsed_index.netloc:
continue
candidate = urllib.parse.urlsplit(index)
candidates.append(candidate)
if not candidates:
return None
candidates.sort(
reverse=True,
key=lambda candidate: commonprefix(
[
parsed_url.path,
candidate.path,
]
).rfind("/"),
)
return urllib.parse.urlunsplit(candidates[0])
def _get_new_credentials(
self,
original_url: str,
*,
allow_netrc: bool = True,
allow_keyring: bool = False,
) -> AuthInfo:
@ -270,8 +374,8 @@ class MultiDomainBasicAuth(AuthBase):
# The index url is more specific than the netloc, so try it first
# fmt: off
kr_auth = (
get_keyring_auth(index_url, username) or
get_keyring_auth(netloc, username)
self._get_keyring_auth(index_url, username) or
self._get_keyring_auth(netloc, username)
)
# fmt: on
if kr_auth:
@ -348,18 +452,23 @@ class MultiDomainBasicAuth(AuthBase):
def _prompt_for_password(
self, netloc: str
) -> Tuple[Optional[str], Optional[str], bool]:
username = ask_input(f"User for {netloc}: ")
username = ask_input(f"User for {netloc}: ") if self.prompting else None
if not username:
return None, None, False
auth = get_keyring_auth(netloc, username)
if auth and auth[0] is not None and auth[1] is not None:
return auth[0], auth[1], False
if self.use_keyring:
auth = self._get_keyring_auth(netloc, username)
if auth and auth[0] is not None and auth[1] is not None:
return auth[0], auth[1], False
password = ask_password("Password: ")
return username, password, True
# Factored out to allow for easy patching in tests
def _should_save_password_to_keyring(self) -> bool:
if get_keyring_provider() is None:
if (
not self.prompting
or not self.use_keyring
or not self.keyring_provider.has_keyring
):
return False
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
@ -369,19 +478,22 @@ class MultiDomainBasicAuth(AuthBase):
if resp.status_code != 401:
return resp
username, password = None, None
# Query the keyring for credentials:
if self.use_keyring:
username, password = self._get_new_credentials(
resp.url,
allow_netrc=False,
allow_keyring=True,
)
# We are not able to prompt the user so simply return the response
if not self.prompting:
if not self.prompting and not username and not password:
return resp
parsed = urllib.parse.urlparse(resp.url)
# Query the keyring for credentials:
username, password = self._get_new_credentials(
resp.url,
allow_netrc=False,
allow_keyring=True,
)
# Prompt the user for a new username and password
save = False
if not username and not password:
@ -431,9 +543,8 @@ class MultiDomainBasicAuth(AuthBase):
def save_credentials(self, resp: Response, **kwargs: Any) -> None:
"""Response callback to save credentials on success."""
keyring = get_keyring_provider()
assert not isinstance(
keyring, KeyRingNullProvider
assert (
self.keyring_provider.has_keyring
), "should never reach here without keyring"
creds = self._credentials_to_save
@ -441,6 +552,8 @@ class MultiDomainBasicAuth(AuthBase):
if creds and resp.status_code < 400:
try:
logger.info("Saving credentials to keyring")
keyring.save_auth_info(creds.url, creds.username, creds.password)
self.keyring_provider.save_auth_info(
creds.url, creds.username, creds.password
)
except Exception:
logger.exception("Failed to save credentials")

View file

@ -6,7 +6,7 @@ from bisect import bisect_left, bisect_right
from contextlib import contextmanager
from tempfile import NamedTemporaryFile
from typing import Any, Dict, Generator, List, Optional, Tuple
from zipfile import BadZipfile, ZipFile
from zipfile import BadZipFile, ZipFile
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
@ -160,7 +160,7 @@ class LazyZipOverHTTP:
# For read-only ZIP files, ZipFile only needs
# methods read, seek, seekable and tell.
ZipFile(self) # type: ignore
except BadZipfile:
except BadZipFile:
pass
else:
break

View file

@ -3,7 +3,7 @@
import os
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.build_env import BuildEnvironment
from pip._internal.exceptions import (
@ -15,7 +15,7 @@ from pip._internal.utils.temp_dir import TempDirectory
def generate_metadata(
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
build_env: BuildEnvironment, backend: BuildBackendHookCaller, details: str
) -> str:
"""Generate metadata using mechanisms described in PEP 517.
@ -26,7 +26,7 @@ def generate_metadata(
metadata_dir = metadata_tmpdir.path
with build_env:
# Note that Pep517HookCaller implements a fallback for
# Note that BuildBackendHookCaller implements a fallback for
# prepare_metadata_for_build_wheel, so we don't have to
# consider the possibility that this hook doesn't exist.
runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)")

View file

@ -3,7 +3,7 @@
import os
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.build_env import BuildEnvironment
from pip._internal.exceptions import (
@ -15,7 +15,7 @@ from pip._internal.utils.temp_dir import TempDirectory
def generate_editable_metadata(
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
build_env: BuildEnvironment, backend: BuildBackendHookCaller, details: str
) -> str:
"""Generate metadata using mechanisms described in PEP 660.
@ -26,7 +26,7 @@ def generate_editable_metadata(
metadata_dir = metadata_tmpdir.path
with build_env:
# Note that Pep517HookCaller implements a fallback for
# Note that BuildBackendHookCaller implements a fallback for
# prepare_metadata_for_build_wheel/editable, so we don't have to
# consider the possibility that this hook doesn't exist.
runner = runner_with_spinner_message(

View file

@ -2,7 +2,7 @@ import logging
import os
from typing import Optional
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.utils.subprocess import runner_with_spinner_message
@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
def build_wheel_pep517(
name: str,
backend: Pep517HookCaller,
backend: BuildBackendHookCaller,
metadata_directory: str,
tempd: str,
) -> Optional[str]:

View file

@ -2,7 +2,7 @@ import logging
import os
from typing import Optional
from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller
from pip._vendor.pyproject_hooks import BuildBackendHookCaller, HookMissing
from pip._internal.utils.subprocess import runner_with_spinner_message
@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
def build_wheel_editable(
name: str,
backend: Pep517HookCaller,
backend: BuildBackendHookCaller,
metadata_directory: str,
tempd: str,
) -> Optional[str]:

View file

@ -75,7 +75,7 @@ def check_package_set(
if name not in package_set:
missed = True
if req.marker is not None:
missed = req.marker.evaluate()
missed = req.marker.evaluate({"extra": ""})
if missed:
missing_deps.add((name, req))
continue

View file

@ -1,7 +1,7 @@
"""Legacy editable installation process, i.e. `setup.py develop`.
"""
import logging
from typing import List, Optional, Sequence
from typing import Optional, Sequence
from pip._internal.build_env import BuildEnvironment
from pip._internal.utils.logging import indent_log
@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
def install_editable(
install_options: List[str],
*,
global_options: Sequence[str],
prefix: Optional[str],
home: Optional[str],
@ -31,7 +31,6 @@ def install_editable(
args = make_setuptools_develop_args(
setup_py_path,
global_options=global_options,
install_options=install_options,
no_user_config=isolated,
prefix=prefix,
home=home,

View file

@ -55,7 +55,6 @@ def write_installed_files_from_setuptools_record(
def install(
install_options: List[str],
global_options: Sequence[str],
root: Optional[str],
home: Optional[str],
@ -79,7 +78,6 @@ def install(
install_args = make_setuptools_install_args(
setup_py_path,
global_options=global_options,
install_options=install_options,
record_filename=record_filename,
root=root,
prefix=prefix,

View file

@ -270,6 +270,16 @@ class RequirementPreparer:
message = "Collecting %s"
information = str(req.req or req)
# If we used req.req, inject requirement source if available (this
# would already be included if we used req directly)
if req.req and req.comes_from:
if isinstance(req.comes_from, str):
comes_from: Optional[str] = req.comes_from
else:
comes_from = req.comes_from.from_path()
if comes_from:
information += f" (from {comes_from})"
if (message, information) != self._previous_requirement_header:
self._previous_requirement_header = (message, information)
logger.info(message, information)

View file

@ -159,9 +159,8 @@ def load_pyproject_toml(
if backend is None:
# If the user didn't specify a backend, we assume they want to use
# the setuptools backend. But we can't be sure they have included
# a version of setuptools which supplies the backend, or wheel
# (which is needed by the backend) in their requirements. So we
# make a note to check that those requirements are present once
# a version of setuptools which supplies the backend. So we
# make a note to check that this requirement is present once
# we have set up the environment.
# This is quite a lot of work to check for a very specific case. But
# the problem is, that case is potentially quite common - projects that
@ -170,6 +169,6 @@ def load_pyproject_toml(
# tools themselves. The original PEP 518 code had a similar check (but
# implemented in a different way).
backend = "setuptools.build_meta:__legacy__"
check = ["setuptools>=40.8.0", "wheel"]
check = ["setuptools>=40.8.0"]
return BuildSystemDetails(requires, backend, check, backend_path)

View file

@ -36,7 +36,6 @@ def _validate_requirements(
def install_given_reqs(
requirements: List[InstallRequirement],
install_options: List[str],
global_options: Sequence[str],
root: Optional[str],
home: Optional[str],
@ -71,7 +70,6 @@ def install_given_reqs(
try:
requirement.install(
install_options,
global_options,
root=root,
home=home,

View file

@ -222,7 +222,6 @@ def install_req_from_editable(
constraint=constraint,
use_pep517=use_pep517,
isolated=isolated,
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
config_settings=config_settings,
@ -399,7 +398,6 @@ def install_req_from_line(
markers=parts.markers,
use_pep517=use_pep517,
isolated=isolated,
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
config_settings=config_settings,
@ -493,7 +491,6 @@ def install_req_from_link_and_ireq(
markers=ireq.markers,
use_pep517=ireq.use_pep517,
isolated=ireq.isolated,
install_options=ireq.install_options,
global_options=ireq.global_options,
hash_options=ireq.hash_options,
config_settings=ireq.config_settings,

View file

@ -69,7 +69,6 @@ SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
# options to be passed to requirements
SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
cmdoptions.install_options,
cmdoptions.global_options,
cmdoptions.hash,
cmdoptions.config_settings,

View file

@ -8,7 +8,6 @@ import shutil
import sys
import uuid
import zipfile
from enum import Enum
from optparse import Values
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
@ -18,7 +17,7 @@ from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
from pip._internal.exceptions import InstallationError, LegacyInstallFailure
@ -51,7 +50,7 @@ from pip._internal.utils.direct_url_helpers import (
)
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
ConfiguredPep517HookCaller,
ConfiguredBuildBackendHookCaller,
ask_path_exists,
backup_dir,
display_path,
@ -83,7 +82,7 @@ class InstallRequirement:
markers: Optional[Marker] = None,
use_pep517: Optional[bool] = None,
isolated: bool = False,
install_options: Optional[List[str]] = None,
*,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
config_settings: Optional[Dict[str, str]] = None,
@ -146,7 +145,6 @@ class InstallRequirement:
# Set to True after successful installation
self.install_succeeded: Optional[bool] = None
# Supplied options
self.install_options = install_options if install_options else []
self.global_options = global_options if global_options else []
self.hash_options = hash_options if hash_options else {}
self.config_settings = config_settings
@ -182,7 +180,7 @@ class InstallRequirement:
self.requirements_to_check: List[str] = []
# The PEP 517 backend we should use to build the project
self.pep517_backend: Optional[Pep517HookCaller] = None
self.pep517_backend: Optional[BuildBackendHookCaller] = None
# Are we using PEP 517 for this requirement?
# After pyproject.toml has been loaded, the only valid values are True
@ -204,7 +202,11 @@ class InstallRequirement:
else:
s = "<InstallRequirement>"
if self.satisfied_by is not None:
s += " in {}".format(display_path(self.satisfied_by.location))
if self.satisfied_by.location is not None:
location = display_path(self.satisfied_by.location)
else:
location = "<memory>"
s += f" in {location}"
if self.comes_from:
if isinstance(self.comes_from, str):
comes_from: Optional[str] = self.comes_from
@ -491,7 +493,7 @@ class InstallRequirement:
requires, backend, check, backend_path = pyproject_toml_data
self.requirements_to_check = check
self.pyproject_requires = requires
self.pep517_backend = ConfiguredPep517HookCaller(
self.pep517_backend = ConfiguredBuildBackendHookCaller(
self,
self.unpacked_source_directory,
backend,
@ -751,7 +753,6 @@ class InstallRequirement:
def install(
self,
install_options: List[str],
global_options: Optional[Sequence[str]] = None,
root: Optional[str] = None,
home: Optional[str] = None,
@ -772,8 +773,7 @@ class InstallRequirement:
global_options = global_options if global_options is not None else []
if self.editable and not self.is_wheel:
install_editable_legacy(
install_options,
global_options,
global_options=global_options,
prefix=prefix,
home=home,
use_user_site=use_user_site,
@ -813,13 +813,12 @@ class InstallRequirement:
# TODO: Why don't we do this for editable installs?
# Extend the list of global and install options passed on to
# Extend the list of global options passed on to
# the setup.py call with the ones from the requirements file.
# Options specified in requirements file override those
# specified on the command line, since the last option given
# to setup.py is the one that is used.
global_options = list(global_options) + self.global_options
install_options = list(install_options) + self.install_options
try:
if (
@ -828,7 +827,6 @@ class InstallRequirement:
):
self.legacy_install_reason.emit_deprecation(self.name)
success = install_legacy(
install_options=install_options,
global_options=global_options,
root=root,
home=home,
@ -898,54 +896,16 @@ def _has_option(options: Values, reqs: List[InstallRequirement], option: str) ->
return False
def _install_option_ignored(
install_options: List[str], reqs: List[InstallRequirement]
) -> bool:
for req in reqs:
if (install_options or req.install_options) and not req.use_pep517:
return False
return True
class LegacySetupPyOptionsCheckMode(Enum):
INSTALL = 1
WHEEL = 2
DOWNLOAD = 3
def check_legacy_setup_py_options(
options: Values,
reqs: List[InstallRequirement],
mode: LegacySetupPyOptionsCheckMode,
) -> None:
has_install_options = _has_option(options, reqs, "install_options")
has_build_options = _has_option(options, reqs, "build_options")
has_global_options = _has_option(options, reqs, "global_options")
legacy_setup_py_options_present = (
has_install_options or has_build_options or has_global_options
)
if not legacy_setup_py_options_present:
return
options.format_control.disallow_binaries()
logger.warning(
"Implying --no-binary=:all: due to the presence of "
"--build-option / --global-option / --install-option. "
"Consider using --config-settings for more flexibility.",
)
if mode == LegacySetupPyOptionsCheckMode.INSTALL and has_install_options:
if _install_option_ignored(options.install_options, reqs):
logger.warning(
"Ignoring --install-option when building using PEP 517",
)
else:
deprecated(
reason=(
"--install-option is deprecated because "
"it forces pip to use the 'setup.py install' "
"command which is itself deprecated."
),
issue=11358,
replacement="to use --config-settings",
gone_in="23.1",
)
if has_build_options or has_global_options:
logger.warning(
"Implying --no-binary=:all: due to the presence of "
"--build-option / --global-option. "
"Consider using --config-settings for more flexibility.",
)
options.format_control.disallow_binaries()

View file

@ -66,7 +66,6 @@ def make_install_req_from_link(
isolated=template.isolated,
constraint=template.constraint,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
),
@ -90,7 +89,6 @@ def make_install_req_from_editable(
constraint=template.constraint,
permit_editable_wheels=template.permit_editable_wheels,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
),
@ -115,7 +113,6 @@ def _make_install_req_from_dist(
isolated=template.isolated,
constraint=template.constraint,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
),

View file

@ -104,7 +104,7 @@ class PipProvider(_ProviderBase):
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
return requirement_or_candidate.name
def get_preference( # type: ignore
def get_preference(
self,
identifier: str,
resolutions: Mapping[str, Candidate],
@ -124,14 +124,29 @@ class PipProvider(_ProviderBase):
* If equal, prefer if any requirement is "pinned", i.e. contains
operator ``===`` or ``==``.
* If equal, calculate an approximate "depth" and resolve requirements
closer to the user-specified requirements first.
closer to the user-specified requirements first. If the depth cannot
by determined (eg: due to no matching parents), it is considered
infinite.
* Order user-specified requirements by the order they are specified.
* If equal, prefers "non-free" requirements, i.e. contains at least one
operator, such as ``>=`` or ``<``.
* If equal, order alphabetically for consistency (helps debuggability).
"""
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
candidate, ireqs = zip(*lookups)
try:
next(iter(information[identifier]))
except StopIteration:
# There is no information for this identifier, so there's no known
# candidates.
has_information = False
else:
has_information = True
if has_information:
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
candidate, ireqs = zip(*lookups)
else:
candidate, ireqs = None, ()
operators = [
specifier.operator
for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
@ -146,11 +161,14 @@ class PipProvider(_ProviderBase):
requested_order: Union[int, float] = self._user_requested[identifier]
except KeyError:
requested_order = math.inf
parent_depths = (
self._known_depths[parent.name] if parent is not None else 0.0
for _, parent in information[identifier]
)
inferred_depth = min(d for d in parent_depths) + 1.0
if has_information:
parent_depths = (
self._known_depths[parent.name] if parent is not None else 0.0
for _, parent in information[identifier]
)
inferred_depth = min(d for d in parent_depths) + 1.0
else:
inferred_depth = math.inf
else:
inferred_depth = 1.0
self._known_depths[identifier] = inferred_depth
@ -161,16 +179,6 @@ class PipProvider(_ProviderBase):
# free, so we always do it first to avoid needless work if it fails.
requires_python = identifier == REQUIRES_PYTHON_IDENTIFIER
# HACK: Setuptools have a very long and solid backward compatibility
# track record, and extremely few projects would request a narrow,
# non-recent version range of it since that would break a lot things.
# (Most projects specify it only to request for an installer feature,
# which does not work, but that's another topic.) Intentionally
# delaying Setuptools helps reduce branches the resolver has to check.
# This serves as a temporary fix for issues like "apache-airflow[all]"
# while we work on "proper" branch pruning techniques.
delay_this = identifier == "setuptools"
# Prefer the causes of backtracking on the assumption that the problem
# resolving the dependency tree is related to the failures that caused
# the backtracking
@ -178,7 +186,6 @@ class PipProvider(_ProviderBase):
return (
not requires_python,
delay_this,
not direct,
not pinned,
not backtrack_cause,

View file

@ -11,9 +11,9 @@ logger = getLogger(__name__)
class PipReporter(BaseReporter):
def __init__(self) -> None:
self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int)
self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int)
self._messages_at_backtrack = {
self._messages_at_reject_count = {
1: (
"pip is looking at multiple versions of {package_name} to "
"determine which version is compatible with other "
@ -32,14 +32,14 @@ class PipReporter(BaseReporter):
),
}
def backtracking(self, candidate: Candidate) -> None:
self.backtracks_by_package[candidate.name] += 1
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
self.reject_count_by_package[candidate.name] += 1
count = self.backtracks_by_package[candidate.name]
if count not in self._messages_at_backtrack:
count = self.reject_count_by_package[candidate.name]
if count not in self._messages_at_reject_count:
return
message = self._messages_at_backtrack[count]
message = self._messages_at_reject_count[count]
logger.info("INFO: %s", message.format(package_name=candidate.name))
@ -61,8 +61,8 @@ class PipDebuggingReporter(BaseReporter):
def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None:
logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
def backtracking(self, candidate: Candidate) -> None:
logger.info("Reporter.backtracking(%r)", candidate)
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
logger.info("Reporter.rejecting_candidate(%r, %r)", criterion, candidate)
def pinning(self, candidate: Candidate) -> None:
logger.info("Reporter.pinning(%r)", candidate)

View file

@ -133,7 +133,7 @@ class UpgradePrompt:
return Group(
Text(),
Text.from_markup(
f"{notice} A new release of pip available: "
f"{notice} A new release of pip is available: "
f"[red]{self.old}[reset] -> [green]{self.new}[reset]"
),
Text.from_markup(
@ -155,7 +155,7 @@ def was_installed_by_pip(pkg: str) -> bool:
def _get_current_remote_pip_version(
session: PipSession, options: optparse.Values
) -> str:
) -> Optional[str]:
# Lets use PackageFinder to see what the latest pip version is
link_collector = LinkCollector.create(
session,
@ -176,7 +176,7 @@ def _get_current_remote_pip_version(
)
best_candidate = finder.find_best_candidate("pip").best_candidate
if best_candidate is None:
return
return None
return str(best_candidate.version)
@ -186,11 +186,14 @@ def _self_version_check_logic(
state: SelfCheckState,
current_time: datetime.datetime,
local_version: DistributionVersion,
get_remote_version: Callable[[], str],
get_remote_version: Callable[[], Optional[str]],
) -> Optional[UpgradePrompt]:
remote_version_str = state.get(current_time)
if remote_version_str is None:
remote_version_str = get_remote_version()
if remote_version_str is None:
logger.debug("No remote pip version found")
return None
state.set(remote_version_str, current_time)
remote_version = parse_version(remote_version_str)

View file

@ -0,0 +1,109 @@
"""Functions brought over from jaraco.text.
These functions are not supposed to be used within `pip._internal`. These are
helper functions brought over from `jaraco.text` to enable vendoring newer
copies of `pkg_resources` without having to vendor `jaraco.text` and its entire
dependency cone; something that our vendoring setup is not currently capable of
handling.
License reproduced from original source below:
Copyright Jason R. Coombs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
import functools
import itertools
def _nonblank(str):
return str and not str.startswith("#")
@functools.singledispatch
def yield_lines(iterable):
r"""
Yield valid lines of a string or iterable.
>>> list(yield_lines(''))
[]
>>> list(yield_lines(['foo', 'bar']))
['foo', 'bar']
>>> list(yield_lines('foo\nbar'))
['foo', 'bar']
>>> list(yield_lines('\nfoo\n#bar\nbaz #comment'))
['foo', 'baz #comment']
>>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n']))
['foo', 'bar', 'baz', 'bing']
"""
return itertools.chain.from_iterable(map(yield_lines, iterable))
@yield_lines.register(str)
def _(text):
return filter(_nonblank, map(str.strip, text.splitlines()))
def drop_comment(line):
"""
Drop comments.
>>> drop_comment('foo # bar')
'foo'
A hash without a space may be in a URL.
>>> drop_comment('http://example.com/foo#bar')
'http://example.com/foo#bar'
"""
return line.partition(" #")[0]
def join_continuation(lines):
r"""
Join lines continued by a trailing backslash.
>>> list(join_continuation(['foo \\', 'bar', 'baz']))
['foobar', 'baz']
>>> list(join_continuation(['foo \\', 'bar', 'baz']))
['foobar', 'baz']
>>> list(join_continuation(['foo \\', 'bar \\', 'baz']))
['foobarbaz']
Not sure why, but...
The character preceeding the backslash is also elided.
>>> list(join_continuation(['goo\\', 'dly']))
['godly']
A terrible idea, but...
If no line is available to continue, suppress the lines.
>>> list(join_continuation(['foo', 'bar\\', 'baz\\']))
['foo']
"""
lines = iter(lines)
for item in lines:
while item.endswith("\\"):
try:
item = item[:-2].strip() + next(lines)
except StopIteration:
return
yield item

View file

@ -173,16 +173,3 @@ LegacyInstallReasonMissingWheelPackage = LegacyInstallReason(
issue=8559,
emit_before_install=True,
)
LegacyInstallReasonNoBinaryForcesSetuptoolsInstall = LegacyInstallReason(
reason=(
"{name} is being installed using the legacy "
"'setup.py install' method, because the '--no-binary' option was enabled "
"for it and this currently disables local wheel building for projects that "
"don't have a 'pyproject.toml' file."
),
replacement="to enable the '--use-pep517' option",
gone_in="23.1",
issue=11451,
emit_before_install=True,
)

View file

@ -1,43 +0,0 @@
from getopt import GetoptError, getopt
from typing import Dict, List
_options = [
"exec-prefix=",
"home=",
"install-base=",
"install-data=",
"install-headers=",
"install-lib=",
"install-platlib=",
"install-purelib=",
"install-scripts=",
"prefix=",
"root=",
"user",
]
def parse_distutils_args(args: List[str]) -> Dict[str, str]:
"""Parse provided arguments, returning an object that has the matched arguments.
Any unknown arguments are ignored.
"""
result = {}
for arg in args:
try:
parsed_opt, _ = getopt(args=[arg], shortopts="", longopts=_options)
except GetoptError:
# We don't care about any other options, which here may be
# considered unrecognized since our option list is not
# exhaustive.
continue
if not parsed_opt:
continue
option = parsed_opt[0]
name_from_parsed = option[0][2:].replace("-", "_")
value_from_parsed = option[1] or "true"
result[name_from_parsed] = value_from_parsed
return result

View file

@ -1,10 +1,7 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import os
import re
import sys
from typing import Optional
from typing import List, Optional
from pip._internal.locations import site_packages, user_site
from pip._internal.utils.virtualenv import (
@ -57,7 +54,7 @@ def egg_link_path_from_location(raw_name: str) -> Optional[str]:
This method will just return the first one found.
"""
sites = []
sites: List[str] = []
if running_under_virtualenv():
sites.append(site_packages)
if not virtualenv_no_global() and user_site:

View file

@ -12,6 +12,7 @@ import posixpath
import shutil
import stat
import sys
import sysconfig
import urllib.parse
from io import StringIO
from itertools import filterfalse, tee, zip_longest
@ -34,11 +35,11 @@ from typing import (
cast,
)
from pip._vendor.pep517 import Pep517HookCaller
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed
from pip import __version__
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import CommandError, ExternallyManagedEnvironment
from pip._internal.locations import get_major_minor_version
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.virtualenv import running_under_virtualenv
@ -57,10 +58,10 @@ __all__ = [
"captured_stdout",
"ensure_dir",
"remove_auth_from_url",
"ConfiguredPep517HookCaller",
"check_externally_managed",
"ConfiguredBuildBackendHookCaller",
]
logger = logging.getLogger(__name__)
T = TypeVar("T")
@ -581,6 +582,21 @@ def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None:
)
def check_externally_managed() -> None:
"""Check whether the current environment is externally managed.
If the ``EXTERNALLY-MANAGED`` config file is found, the current environment
is considered externally managed, and an ExternallyManagedEnvironment is
raised.
"""
if running_under_virtualenv():
return
marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")
if not os.path.isfile(marker):
return
raise ExternallyManagedEnvironment.from_config(marker)
def is_console_interactive() -> bool:
"""Is this console interactive?"""
return sys.stdin is not None and sys.stdin.isatty()
@ -635,7 +651,7 @@ def partition(
return filterfalse(pred, t1), filter(pred, t2)
class ConfiguredPep517HookCaller(Pep517HookCaller):
class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
def __init__(
self,
config_holder: Any,

View file

@ -103,8 +103,8 @@ def make_setuptools_clean_args(
def make_setuptools_develop_args(
setup_py_path: str,
*,
global_options: Sequence[str],
install_options: Sequence[str],
no_user_config: bool,
prefix: Optional[str],
home: Optional[str],
@ -120,8 +120,6 @@ def make_setuptools_develop_args(
args += ["develop", "--no-deps"]
args += install_options
if prefix:
args += ["--prefix", prefix]
if home is not None:
@ -150,8 +148,8 @@ def make_setuptools_egg_info_args(
def make_setuptools_install_args(
setup_py_path: str,
*,
global_options: Sequence[str],
install_options: Sequence[str],
record_filename: str,
root: Optional[str],
prefix: Optional[str],
@ -190,6 +188,4 @@ def make_setuptools_install_args(
if header_dir:
args += ["--install-headers", header_dir]
args += install_options
return args

View file

@ -239,8 +239,8 @@ def call_subprocess(
def runner_with_spinner_message(message: str) -> Callable[..., None]:
"""Provide a subprocess_runner that shows a spinner message.
Intended for use with for pep517's Pep517HookCaller. Thus, the runner has
an API that matches what's expected by Pep517HookCaller.subprocess_runner.
Intended for use with for BuildBackendHookCaller. Thus, the runner has
an API that matches what's expected by BuildBackendHookCaller.subprocess_runner.
"""
def runner(

View file

@ -5,7 +5,7 @@ import logging
import os.path
import re
import shutil
from typing import Callable, Iterable, List, Optional, Tuple
from typing import Iterable, List, Optional, Tuple
from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
from pip._vendor.packaging.version import InvalidVersion, Version
@ -19,10 +19,7 @@ from pip._internal.operations.build.wheel import build_wheel_pep517
from pip._internal.operations.build.wheel_editable import build_wheel_editable
from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.deprecation import (
LegacyInstallReasonMissingWheelPackage,
LegacyInstallReasonNoBinaryForcesSetuptoolsInstall,
)
from pip._internal.utils.deprecation import LegacyInstallReasonMissingWheelPackage
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed
from pip._internal.utils.setuptools_build import make_setuptools_clean_args
@ -35,7 +32,6 @@ logger = logging.getLogger(__name__)
_egg_info_re = re.compile(r"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re.IGNORECASE)
BdistWheelAllowedPredicate = Callable[[InstallRequirement], bool]
BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
@ -50,7 +46,6 @@ def _contains_egg_info(s: str) -> bool:
def _should_build(
req: InstallRequirement,
need_wheel: bool,
check_bdist_wheel: Optional[BdistWheelAllowedPredicate] = None,
) -> bool:
"""Return whether an InstallRequirement should be built into a wheel."""
if req.constraint:
@ -81,16 +76,6 @@ def _should_build(
if req.use_pep517:
return True
assert check_bdist_wheel is not None
if not check_bdist_wheel(req):
# /!\ When we change this to unconditionally return True, we must also remove
# support for `--install-option`. Indeed, `--install-option` implies
# `--no-binary` so we can return False here and run `setup.py install`.
# `--global-option` and `--build-option` can remain until we drop support for
# building with `setup.py bdist_wheel`.
req.legacy_install_reason = LegacyInstallReasonNoBinaryForcesSetuptoolsInstall
return False
if not is_wheel_installed():
# we don't build legacy requirements if wheel is not installed
req.legacy_install_reason = LegacyInstallReasonMissingWheelPackage
@ -107,11 +92,8 @@ def should_build_for_wheel_command(
def should_build_for_install_command(
req: InstallRequirement,
check_bdist_wheel_allowed: BdistWheelAllowedPredicate,
) -> bool:
return _should_build(
req, need_wheel=False, check_bdist_wheel=check_bdist_wheel_allowed
)
return _should_build(req, need_wheel=False)
def _should_cache(

View file

@ -1,4 +1,4 @@
from .core import contents, where
__all__ = ["contents", "where"]
__version__ = "2022.09.24"
__version__ = "2022.12.07"

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