Merge branch 'master' into fix-docs-mobile

This commit is contained in:
Dan Ryan
2019-05-26 17:24:25 -04:00
committed by GitHub
497 changed files with 33769 additions and 12459 deletions
+1 -1
View File
@@ -11,5 +11,5 @@ steps:
export GIT_SSL_CAINFO=$(python -m certifi)
export LANG=C.UTF-8
python -m pip install --upgrade setuptools twine readme_renderer[md]
python setup.py sdist
python setup.py sdist bdist_wheel
twine check dist/*
+15 -2
View File
@@ -4,9 +4,22 @@ steps:
inputs:
versionSpec: '$(python.version)'
architecture: '$(python.architecture)'
addToPath: true
- powershell: |
Write-Host "##vso[task.setvariable variable=PIPENV_DEFAULT_PYTHON_VERSION]$env:PYTHON_VERSION"
env:
PYTHON_VERSION: $(python.version)
- template: ../steps/install-dependencies.yml
- template: ../steps/create-virtualenv.yml
- template: ../steps/create-virtualenv-windows.yml
- template: ../steps/run-tests.yml
- template: ../steps/run-tests-windows.yml
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFiles: '**/test-results.xml'
testRunTitle: 'Python $(python.version)'
condition: succeededOrFailed()
+5 -13
View File
@@ -4,22 +4,14 @@ steps:
inputs:
versionSpec: '$(python.version)'
architecture: '$(python.architecture)'
addToPath: true
- script: |
echo '##vso[task.setvariable variable=PIPENV_DEFAULT_PYTHON_VERSION]$(python.version)'
- template: ../steps/install-dependencies.yml
- bash: |
mkdir -p "$AGENT_HOMEDIRECTORY/.virtualenvs"
mkdir -p "$WORKON_HOME"
pip install certifi
export GIT_SSL_CAINFO="$(python -m certifi)"
export LANG="C.UTF-8"
export PIP_PROCESS_DEPENDENCY_LINKS="1"
echo "Path $PATH"
echo "Installing Pipenv…"
pip install -e "$(pwd)" --upgrade
pipenv install --deploy --dev
echo pipenv --venv && echo pipenv --py && echo pipenv run python --version
displayName: Make Virtualenv
- template: ../steps/create-virtualenv-linux.yml
- script: |
# Fix Git SSL errors
+4 -10
View File
@@ -9,15 +9,9 @@ jobs:
strategy:
maxParallel: 4
matrix:
${{ if eq(parameters.vmImage, 'vs2017-win2016') }}:
# TODO remove once vs2017-win2016 has Python 3.7
Python37:
python.version: '>= 3.7.0-b2'
python.architecture: x64
${{ if ne(parameters.vmImage, 'vs2017-win2016' )}}:
Python37:
python.version: '>= 3.7'
python.architecture: x64
Python37:
python.version: '>= 3.7'
python.architecture: x64
steps:
- task: UsePythonVersion@0
displayName: Use Python $(python.version)
@@ -33,7 +27,7 @@ jobs:
pip install certifi
export GIT_SSL_CAINFO=$(python -m certifi)
export LANG=C.UTF-8
python -m pip install --upgrade invoke requests parver bs4 vistir towncrier
python -m pip install --upgrade invoke requests parver bs4 vistir towncrier pip setuptools wheel --upgrade-strategy=eager
python -m invoke vendoring.update
- template: ./run-manifest-check.yml
+6 -28
View File
@@ -12,37 +12,15 @@ jobs:
Python27:
python.version: '2.7'
python.architecture: x64
${{ if eq(parameters.vmImage, 'vs2017-win2016') }}:
# TODO remove once vs2017-win2016 has Python 3.7
Python37:
python.version: '>= 3.7.0-b2'
python.architecture: x64
${{ if ne(parameters.vmImage, 'vs2017-win2016' )}}:
Python37:
python.version: '>= 3.7'
python.architecture: x64
steps:
- ${{ if eq(parameters.vmImage, 'vs2017-win2016') }}:
- template: ./run-tests-windows.yml
- ${{ if ne(parameters.vmImage, 'vs2017-win2016') }}:
- template: ./run-tests.yml
- job: Test_Secondary
displayName: Test python3.6
# Run after Test_Primary so we don't devour time and jobs if tests are going to fail
# dependsOn: Test_Primary
pool:
vmImage: ${{ parameters.vmImage }}
strategy:
maxParallel: 4
matrix:
Python37:
python.version: '>= 3.7'
python.architecture: x64
Python36:
python.version: '3.6'
python.version: '>= 3.6'
python.architecture: x64
steps:
- ${{ if eq(parameters.vmImage, 'vs2017-win2016') }}:
- ${{ if eq(parameters.vmImage, 'windows-2019') }}:
- template: ./run-tests-windows.yml
- ${{ if ne(parameters.vmImage, 'vs2017-win2016') }}:
- ${{ if ne(parameters.vmImage, 'windows-2019') }}:
- template: ./run-tests.yml
@@ -0,0 +1,16 @@
steps:
- bash: |
mkdir -p "$AGENT_HOMEDIRECTORY/.virtualenvs"
mkdir -p "$WORKON_HOME"
pip install certifi
export GIT_SSL_CAINFO="$(python -m certifi)"
export LANG="C.UTF-8"
export PIP_PROCESS_DEPENDENCY_LINKS="1"
echo "Path $PATH"
echo "Installing Pipenv…"
pipenv install --deploy --dev
pipenv run pip install -e "$(pwd)[tests]" --upgrade
echo pipenv --venv && echo pipenv --py && echo pipenv run python --version
displayName: Make Virtualenv
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
@@ -0,0 +1,26 @@
steps:
- script: |
echo "##vso[task.setvariable variable=LANG]C.UTF-8"
echo "##vso[task.setvariable variable=PIP_PROCESS_DEPENDENCY_LINKS]1"
displayName: Set Environment Variables
- powershell: |
pip install certifi
$env:PYTHON_PATH=$(python -c "import sys; print(sys.executable)")
$env:CERTIFI_CONTENT=$(python -m certifi)
echo "##vso[task.setvariable variable=GIT_SSL_CAINFO]$env:CERTIFI_CONTENT"
echo "##vso[task.setvariable variable=PY_EXE]$env:PYTHON_PATH"
displayName: Set Python Path
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
- script: |
echo "Python path: $(PY_EXE)"
echo "GIT_SSL_CAINFO: $(GIT_SSL_CAINFO)"
$(PY_EXE) -m pipenv install --deploy --dev
env:
PIPENV_DEFAULT_PYTHON_VERSION: '$(PIPENV_DEFAULT_PYTHON_VERSION)'
PYTHONWARNINGS: 'ignore:DEPRECATION'
PIPENV_NOSPIN: '1'
displayName: Make Virtualenv
+34 -3
View File
@@ -1,6 +1,37 @@
steps:
- script: |
virtualenv D:\.venv
D:\.venv\Scripts\pip.exe install -e . && D:\.venv\Scripts\pipenv install --dev
echo D:\.venv\Scripts\pipenv --venv && echo D:\.venv\Scripts\pipenv --py && echo D:\.venv\Scripts\pipenv run python --version
echo "##vso[task.setvariable variable=LANG]C.UTF-8"
echo "##vso[task.setvariable variable=PIP_PROCESS_DEPENDENCY_LINKS]1"
displayName: Set Environment Variables
- ${{ if eq(parameters.vmImage, 'windows-2019') }}:
- powershell: |
pip install certifi
$env:PYTHON_PATH=$(python -c "import sys; print(sys.executable)")
$env:CERTIFI_CONTENT=$(python -m certifi)
echo "##vso[task.setvariable variable=GIT_SSL_CAINFO]$env:CERTIFI_CONTENT"
echo "##vso[task.setvariable variable=PY_EXE]$env:PYTHON_PATH"
displayName: Set Python Path
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
- ${{ if ne(parameters.vmImage, 'windows-2019') }}:
- bash: |
pip install certifi
PYTHON_PATH=$(python -c 'import sys; print(sys.executable)')
CERTIFI_CONTENT=$(python -m certifi)
echo "##vso[task.setvariable variable=GIT_SSL_CAINFO]$CERTIFI_CONTENT"
echo "##vso[task.setvariable variable=PY_EXE]$PYTHON_PATH"
displayName: Set Python Path
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
- script: |
echo "Python path: $(PY_EXE)"
echo "GIT_SSL_CAINFO: $(GIT_SSL_CAINFO)"
$(PY_EXE) -m pipenv install --deploy --dev
env:
PIPENV_DEFAULT_PYTHON_VERSION: '$(PIPENV_DEFAULT_PYTHON_VERSION)'
PYTHONWARNINGS: 'ignore:DEPRECATION'
PIPENV_NOSPIN: '1'
displayName: Make Virtualenv
@@ -1,3 +1,5 @@
steps:
- script: 'python -m pip install --upgrade pip && python -m pip install -e .'
- script: 'python -m pip install --upgrade pip setuptools wheel && python -m pip install -e .[tests] --upgrade'
displayName: Upgrade Pip & Install Pipenv
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
@@ -0,0 +1,14 @@
- script: |
# Fix Git SSL errors
git submodule sync && git submodule update --init --recursive
pipenv run pytest --junitxml=test-results.xml
displayName: Run integration tests
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
PY_EXE: $(PY_EXE)
GIT_SSL_CAINFO: $(GIT_SSL_CAINFO)
LANG: $(LANG)
PIP_PROCESS_DEPENDENCY_LINKS: $(PIP_PROCESS_DEPENDENCY_LINKS)
PIPENV_DEFAULT_PYTHON_VERSION: $(PIPENV_DEFAULT_PYTHON_VERSION)
PYTHONWARNINGS: ignore:DEPRECATION
PIPENV_NOSPIN: '1'
@@ -0,0 +1,15 @@
steps:
- powershell: |
subst T: "$env:TEMP"
Write-Host "##vso[task.setvariable variable=TEMP]T:\"
Write-Host "##vso[task.setvariable variable=TMP]T:\"
displayName: Fix Temp Variable
- script: |
git submodule sync && git submodule update --init --recursive
pipenv run pytest -ra --ignore=pipenv\patched --ignore=pipenv\vendor --junitxml=test-results.xml tests
displayName: Run integration tests
env:
PIPENV_DEFAULT_PYTHON_VERSION: $(PIPENV_DEFAULT_PYTHON_VERSION)
PYTHONWARNINGS: 'ignore:DEPRECATION'
PIPENV_NOSPIN: '1'
+20 -17
View File
@@ -1,21 +1,24 @@
steps:
- powershell: |
# Fix Git SSL errors
pip install certifi
python -m certifi > cacert.txt
Write-Host "##vso[task.setvariable variable=GIT_SSL_CAINFO]$(Get-Content cacert.txt)"
$env:GIT_SSL_CAINFO="$(Get-Content cacert.txt)"
# Shorten paths to get under MAX_PATH or else integration tests will fail
# https://bugs.python.org/issue18199
subst T: "$env:TEMP"
Write-Host "##vso[task.setvariable variable=TEMP]T:\"
$env:TEMP='T:\'
Write-Host "##vso[task.setvariable variable=TMP]T:\"
$env:TMP='T:\'
git submodule sync
git submodule update --init --recursive
D:\.venv\Scripts\pipenv run pytest -ra --ignore=pipenv\patched --ignore=pipenv\vendor --junitxml=test-results.xml tests
displayName: Run integration tests
- task: UsePythonVersion@0
inputs:
versionSpec: $(python.version)
architecture: '$(python.architecture)'
addToPath: true
displayName: Use Python $(python.version)
- template: install-dependencies.yml
- script: |
echo '##vso[task.setvariable variable=PIPENV_DEFAULT_PYTHON_VERSION]$(PYTHON_VERSION)'
env:
PYTHON_VERSION: $(python.version)
- template: create-virtualenv.yml
- ${{ if eq(parameters.vmImage, 'windows-2019') }}:
- template: run-tests-windows.yml
- ${{ if ne(parameters.vmImage, 'windows-2019') }}:
- template: run-tests-linux.yml
- task: PublishTestResults@2
displayName: Publish Test Results
+1 -1
View File
@@ -21,4 +21,4 @@ trigger:
jobs:
- template: jobs/test.yml
parameters:
vmImage: vs2017-win2016
vmImage: windows-2019
+9
View File
@@ -145,6 +145,10 @@ venv.bak/
# mypy
.mypy_cache/
# Temporarily generating these with pytype locally for type safety
typeshed/
pytype.cfg
### Python Patch ###
.venv/
@@ -153,3 +157,8 @@ venv.bak/
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
.vs/slnx.sqlite
# mypy/typing section
typeshed/
.dmypy.json
mypyhtml/
+2 -2
View File
@@ -3,8 +3,8 @@
Before opening any issues or proposing any pull requests, please do the
following:
1. Read our [Contributor's Guide](https://docs.pipenv.org/dev/contributing/).
2. Understand our [development philosophy](https://docs.pipenv.org/dev/philosophy/).
1. Read our [Contributor's Guide](https://docs.pipenv.org/en/latest/dev/contributing/).
2. Understand our [development philosophy](https://docs.pipenv.org/en/latest/dev/philosophy/).
To get the greatest chance of helpful responses, please also observe the
following additional notes.
+3 -1
View File
@@ -3,9 +3,11 @@ FROM heroku/heroku:18-build
ENV DEBIAN_FRONTEND noninteractive
ENV LC_ALL C.UTF-8
ENV LANG C.UTF-8
# Python, don't write bytecode!
ENV PYTHONDONTWRITEBYTECODE 1
# -- Install Pipenv:
RUN apt update && apt upgrade -y && apt install python3.7-dev -y
RUN apt update && apt upgrade -y && apt install python3.7-dev libffi-dev -y
RUN curl --silent https://bootstrap.pypa.io/get-pip.py | python3.7
# Backwards compatility.
+3 -17
View File
@@ -1,27 +1,13 @@
[dev-packages]
pipenv = {path = ".", editable = true}
"flake8" = ">=3.3.0,<4"
pytest = "*"
mock = "*"
sphinx = "<=1.5.5"
twine = "*"
pipenv = {path = ".", editable = true, extras = ["tests", "dev"]}
sphinx-click = "*"
pytest-xdist = "*"
click = "*"
pytest-pypy = {path = "./tests/pytest-pypi", editable = true}
pytest-tap = "*"
flaky = "*"
pytest_pypi = {path = "./tests/pytest-pypi", editable = true}
stdeb = {version="*", markers="sys_platform == 'linux'"}
black = {version="*", markers="python_version >= '3.6'"}
pytz = "*"
towncrier = {git = "https://github.com/hawkowl/towncrier.git", editable = true, ref = "master"}
parver = "*"
invoke = "*"
jedi = "*"
isort = "*"
rope = "*"
passa = {editable = true, git = "https://github.com/sarugaku/passa.git"}
bs4 = "*"
passa = {git = "https://github.com/sarugaku/passa.git"}
[packages]
Generated
+334 -304
View File
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -20,7 +20,7 @@ well as adds/removes packages from your `Pipfile` as you
install/uninstall packages. It also generates the ever-important
`Pipfile.lock`, which is used to produce deterministic builds.
![image](http://media.kennethreitz.com.s3.amazonaws.com/pipenv.gif)
![image](https://s3.amazonaws.com/media.kennethreitz.com/pipenv.gif)
The problems that Pipenv seeks to solve are multi-faceted:
@@ -46,11 +46,19 @@ If you\'re on MacOS, you can install Pipenv easily with Homebrew:
$ brew install pipenv
Or, if you\'re using Debian Buster+:
$ sudo apt install pipenv
Or, if you\'re using Fedora 28:
$ sudo dnf install pipenv
Or, if you\'re using FreeBSD:
Otherwise, refer to the [documentation](https://docs.pipenv.org/install/) for instructions.
# pkg install py36-pipenv
Otherwise, refer to the [documentation](https://docs.pipenv.org/en/latest/install/#installing-pipenv) for instructions.
✨🍰✨
+69
View File
@@ -0,0 +1,69 @@
name: Pipenv Build Rules
trigger:
batch: true
branches:
include:
- master
paths:
exclude:
- docs/*
- news/*
- peeps/*
- README.md
- pipenv/*.txt
- CHANGELOG.rst
- CONTRIBUTING.md
- CODE_OF_CONDUCT.md
- .gitignore
- .gitattributes
- .editorconfig
jobs:
- job: TestLinux
pool:
vmImage: 'Ubuntu-16.04'
strategy:
matrix:
Python27:
python.version: '2.7'
Python36:
python.version: '3.6'
Python37:
python.version: '3.7'
maxParallel: 4
steps:
- template: .azure-pipelines/steps/run-tests.yml
- template: .azure-pipelines/jobs/run-vendor-scripts.yml
- job: TestWindows
pool:
vmImage: windows-2019
strategy:
matrix:
Python27:
python.version: '2.7'
python.architecture: x64
Python36:
python.version: '3.6'
python.architecture: x64
Python37:
python.version: '3.7'
python.architecture: x64
maxParallel: 4
steps:
- template: .azure-pipelines/steps/run-tests.yml
- job: TestMacOS
pool:
vmImage: macOS-10.13
strategy:
matrix:
Python27:
python.version: '2.7'
Python36:
python.version: '3.6'
Python37:
python.version: '3.7'
maxParallel: 4
steps:
- template: .azure-pipelines/steps/run-tests.yml
+21 -1
View File
@@ -8,12 +8,32 @@
<iframe src="https://ghbtns.com/github-btn.html?user=pypa&repo=pipenv&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
<style>
.algolia-autocomplete{
width: 100%;
height: 1.5em
}
.algolia-autocomplete a{
border-bottom: none !important;
}
#doc_search{
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus/>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
apiKey: '0dbb76467f0c180a1344fc46858df17b',
indexName: 'pipenv',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})" async></script>
<p>
<strong>Pipenv</strong> is a production-ready tool that aims to bring the best of all packaging worlds to the Python world. It harnesses Pipfile, pip, and virtualenv into one single command.
<p>It features very pretty terminal colors.</p>
</p>
<h3>Stay Informed</h3>
<p>Receive updates on new releases and upcoming projects.</p>
+21
View File
@@ -8,6 +8,27 @@
<iframe src="https://ghbtns.com/github-btn.html?user=pypa&repo=pipenv&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
<style>
.algolia-autocomplete{
width: 100%;
height: 1.5em
}
.algolia-autocomplete a{
border-bottom: none !important;
}
#doc_search{
width: 100%;
height: 100%;
}
</style>
<input id="doc_search" placeholder="Search the doc" autofocus/>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" onload="docsearch({
apiKey: '0dbb76467f0c180a1344fc46858df17b',
indexName: 'pipenv',
inputSelector: '#doc_search',
debug: false // Set debug to true if you want to inspect the dropdown
})" async></script>
<p>
<strong>Pipenv</strong> is a production-ready tool that aims to bring the best of all packaging worlds to the Python world. It harnesses Pipfile, pip, and virtualenv into one single command.
+3 -2
View File
@@ -127,6 +127,7 @@ Example Pipfile.lock
- Do not keep ``Pipfile.lock`` in version control if multiple versions of Python are being targeted.
- Specify your target Python version in your `Pipfile`'s ``[requires]`` section. Ideally, you should only have one target Python version, as this is a deployment tool.
- ``pipenv install`` is fully compatible with ``pip install`` syntax, for which the full documentation can be found `here <https://pip.pypa.io/en/stable/user_guide/#installing-packages>`_.
- Note that the ``Pipfile`` uses the `TOML Spec <https://github.com/toml-lang/toml#user-content-spec>`_.
@@ -203,11 +204,11 @@ To make inclusive or exclusive version comparisons you can use: ::
$ pipenv install "requests<=2.13" # will install a version equal or lower than 2.13.0
$ pipenv install "requests>2.19" # will install 2.19.1 but not 2.19.0
.. note:: The use of ``" "`` around the package and version specification is highly recommended
.. note:: The use of double quotes around the package and version specification (i.e. ``"requests>2.19"``) is highly recommended
to avoid issues with `Input and output redirection <https://robots.thoughtbot.com/input-output-redirection-in-the-shell>`_
in Unix-based operating systems.
The use of ``~=`` is preferred over the ``==`` identifier as the former prevents pipenv from updating the packages: ::
The use of ``~=`` is preferred over the ``==`` identifier as the latter prevents pipenv from updating the packages: ::
$ pipenv install "requests~=2.2" # locks the major version of the package (this is equivalent to using ==2.*)
+6 -7
View File
@@ -26,10 +26,12 @@ It automatically creates and manages a virtualenv for your projects, as well as
Pipenv is primarily meant to provide users and developers of applications with an easy method to setup a working environment. For the distinction between libraries and applications and the usage of ``setup.py`` vs ``Pipfile`` to define dependencies, see :ref:`pipfile-vs-setuppy`.
.. raw:: html
<iframe src="https://player.vimeo.com/video/233134524" width="700" height="460" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
.. image:: https://s3.amazonaws.com/media.kennethreitz.com/pipenv.gif
:height: 341px
:width: 654px
:scale: 100 %
:alt: a short animation of pipenv at work
The problems that Pipenv seeks to solve are multi-faceted:
- You no longer need to use ``pip`` and ``virtualenv`` separately. They work together.
@@ -70,9 +72,6 @@ Otherwise, refer to the :ref:`installing-pipenv` chapter for instructions.
User Testimonials
-----------------
**Jannis Leidel**, former pip maintainer—
*Pipenv is the porcelain I always wanted to build for pip. It fits my brain and mostly replaces virtualenvwrapper and manual pip calls for me. Use it.*
**David Gang**
*This package manager is really awesome. For the first time I know exactly what my dependencies are which I installed and what the transitive dependencies are. Combined with the fact that installs are deterministic, makes this package manager first class, like cargo*.
+1
View File
@@ -0,0 +1 @@
Make conservative checks of known exceptions when subprocess returns output, so user won't see the whole traceback - just the error.
+1
View File
@@ -0,0 +1 @@
Fixed a bug which caused editable package resolution to sometimes fail with an unhelpful setuptools-related error message.
+2
View File
@@ -0,0 +1,2 @@
Fixed an issue which caused errors due to reliance on the system utilities ``which`` and ``where`` which may not always exist on some systems.
- Fixed a bug which caused periodic failures in python discovery when executables named ``python`` were not present on the target ``$PATH``.
+1
View File
@@ -0,0 +1 @@
Dependency resolution now writes hashes for local and remote files to the lockfile.
+1
View File
@@ -0,0 +1 @@
Fixed a bug which prevented ``pipenv graph`` from correctly showing all dependencies when running from within ``pipenv shell``.
+1
View File
@@ -0,0 +1 @@
Fixed resolution of direct-url dependencies in ``setup.py`` files to respect ``PEP-508`` style URL dependencies.
+1
View File
@@ -0,0 +1 @@
Added support for resolution of direct-url dependencies in ``setup.py`` files to respect ``PEP-508`` style URL dependencies.
+1
View File
@@ -0,0 +1 @@
Update pytest-pypi documentation not to be pytest-httpbin documentation.
+3
View File
@@ -0,0 +1,3 @@
Fixed a bug which caused failures in warning reporting when running pipenv inside a virtualenv under some circumstances.
- Fixed a bug with package discovery when running ``pipenv clean``.
+5
View File
@@ -0,0 +1,5 @@
Added full support for resolution of all dependency types including direct URLs, zip archives, tarballs, etc.
- Improved error handling and formatting.
- Introduced improved cross platform stream wrappers for better ``stdout`` and ``stderr`` consistency.
+34
View File
@@ -0,0 +1,34 @@
Updated vendored dependencies:
- **attrs**: ``18.2.0`` => ``19.1.0``
- **certifi**: ``2018.10.15`` => ``2019.3.9``
- **cached_property**: ``1.4.3`` => ``1.5.1``
- **cerberus**: ``1.2.0`` => ``1.3.1``
- **click-completion**: ``0.5.0`` => ``0.5.1``
- **colorama**: ``0.3.9`` => ``0.4.1``
- **distlib**: ``0.2.8`` => ``0.2.9``
- **idna**: ``2.7`` => ``2.8``
- **jinja2**: ``2.10.0`` => ``2.10.1``
- **markupsafe**: ``1.0`` => ``1.1.1``
- **orderedmultidict**: ``(new)`` => ``1.0``
- **packaging**: ``18.0`` => ``19.0``
- **parse**: ``1.9.0`` => ``1.12.0``
- **pathlib2**: ``2.3.2`` => ``2.3.3``
- **pep517**: ``(new)`` => ``0.5.0``
- **pexpect**: ``4.6.0`` => ``4.7.0``
- **pipdeptree**: ``0.13.0`` => ``0.13.2``
- **pyparsing**: ``2.2.2`` => ``2.3.1``
- **python-dotenv**: ``0.9.1`` => ``0.10.2``
- **pythonfinder**: ``1.1.10`` => ``1.2.1``
- **pytoml**: ``(new)`` => ``0.1.20``
- **requests**: ``2.20.1`` => ``2.21.0``
- **requirementslib**: ``1.3.3`` => ``1.5.0``
- **scandir**: ``1.9.0`` => ``1.10.0``
- **shellingham**: ``1.2.7`` => ``1.3.1``
- **six**: ``1.11.0`` => ``1.12.0``
- **tomlkit**: ``0.5.2`` => ``0.5.3``
- **urllib3**: ``1.24`` => ``1.25.2``
- **vistir**: ``0.3.0`` => ``0.4.1``
- **yaspin**: ``0.14.0`` => ``0.14.3``
- Removed vendored dependency **cursor**.
+1
View File
@@ -0,0 +1 @@
Pipenv will now successfully recursively lock VCS sub-dependencies.
+1
View File
@@ -0,0 +1 @@
Pipenv will now discover and resolve the intrinsic dependencies of **all** VCS dependencies, whether they are editable or not, to prevent resolution conflicts.
+1
View File
@@ -0,0 +1 @@
Do not touch Pipfile early and rely on it so that one can do ``pipenv sync`` without a Pipfile.
+1
View File
@@ -0,0 +1 @@
Fixed a keyerror which could occur when locking VCS dependencies in some cases.
+1 -1
View File
@@ -1 +1 @@
Fix a bug that ``ValidationError`` is thrown when some fields are missing in source section.
Fixed a bug that ``ValidationError`` is thrown when some fields are missing in source section.
+1
View File
@@ -0,0 +1 @@
Improve the error message when one tries to initialize a Pipenv project under ``/``.
+1 -1
View File
@@ -1 +1 @@
Fix the wrong order of old and new hashes in message.
Fixed the wrong order of old and new hashes in message.
+1 -1
View File
@@ -1 +1 @@
Update the index names in lock file when source name in Pipfile is changed.
Updated the index names in lock file when source name in Pipfile is changed.
+1
View File
@@ -0,0 +1 @@
Replace (non-existant) video on docs index.rst with equivalent gif.
+1
View File
@@ -0,0 +1 @@
Fix bug causing ``[SSL: CERTIFICATE_VERIFY_FAILED]`` when Pipfile ``[[source]]`` has verify_ssl=false and url with custom port.
+1
View File
@@ -0,0 +1 @@
Clarify wording in Basic Usage example on using double quotes to escape shell redirection
+1
View File
@@ -0,0 +1 @@
Fix the issue that lock file can't be created when ``PIPENV_PIPFILE`` is not under working directory.
+1
View File
@@ -0,0 +1 @@
Added the ability for Windows users to enable emojis by setting ``PIPENV_HIDE_EMOJIS=0``.
+1
View File
@@ -0,0 +1 @@
Removed unused vendored package shutilwhich
+1
View File
@@ -0,0 +1 @@
Added a link to the TOML Spec under General Recommendations & Version Control to clarify how Pipfiles should be written.
+1
View File
@@ -0,0 +1 @@
Removed unused vendored package blindspin
+1
View File
@@ -0,0 +1 @@
Allow KeyboardInterrupt to cancel test suite checks for working internet and ssh
+1
View File
@@ -0,0 +1 @@
Cleaned up some conditional logic that would always evaluate ``True``.
+1
View File
@@ -0,0 +1 @@
Add installation instructions for Debian Buster+ in README
+1
View File
@@ -0,0 +1 @@
Update pytest configuration to support pytest 4.
+1
View File
@@ -0,0 +1 @@
Normalize the package names to lowercase when comparing used and in-Pipfile packages.
+9
View File
@@ -0,0 +1,9 @@
## PEEP-003: Subcommands
NOT YET ACCEPTED
Pipenv will automatically run commands like "pipenv deploy" if the "pipenv-deploy" binary is available on the path.
These subcommands cannot overwrite built-in commands.
These subcommands will receive environment variables with contextual information.
+65
View File
@@ -0,0 +1,65 @@
# PEEP-005: Do Not Remove Entries from the Lockfile When Using `--keep-outdated`
**PROPOSED**
This PEEP describes a change that would retain entries in the Lockfile even if they were not returned during resolution when the user passes the `--keep-outdated` flag.
The `--keep-outdated` flag is currently provided by Pipenv for the purpose of holding back outdated dependencies (i.e. dependencies that are not newly introduced). This proposal attempts to identify the reasoning behind the flag and identifies a need for a project-wide scoping. Finally, this proposal outlines the expected behavior of `--keep-outdated` under the specified circumstances, as well as the required changes to achieve full implementation.
## Retaining Outdated Dependencies
The purpose of retaining outdated dependencies is to allow the user to introduce a new package to their environment with a minimal impact on their existing environment. In an effort to achieve this, `keep_outdated` was proposed as both a flag and a Pipfile setting [in this issue](https://github.com/pypa/pipenv/issues/1255#issuecomment-354585775), originally described as follows:
> pipenv lock --keep-outdated to request a minimal update that only adjusts the lock file to account for Pipfile changes (additions, removals, and changes to version constraints)... and pipenv install --keep-outdated needed to request only the minimal changes required to satisfy the installation request
However, the current implementation always fully re-locks, rather than only locking the new dependencies. As a result, dependencies in the `Pipfile.lock` with markers for a python version different from that of the running interpreter will be removed, even if they have nothing to do with the current changeset. For instance, say you have the following dependency in your `Pipfile.lock`:
```json
{
"default": {
"backports.weakref": {
"hashes": [...],
"version": "==1.5",
"markers": "python_version<='3.4'"
}
}
}
```
If this lockfile were to be re-generated with Python 3, even with `--keep-outdated`, this entry would be removed. This makes it very difficult to maintain lockfiles which are compatible across major python versions, yet all that would be required to correct this would be a tweak to the implementation of `keep-outdated`. I believe this was the goal to begin with, but I feel this behavior should be documented and clarified before moving forward.
## Desired Behavior
1. The only changes that should occur in `Pipfile.lock` when `--keep-outdated` is passed should be changes resulting from new packages added or pin changes in the project `Pipfile`;
2. Existing packages in the project `Pipfile.lock` should remain in place, even if they are not returned during resolution;
3. New dependencies should be written to the lockfile;
4. Conflicts should be resolved as outlined below.
## Conflict Resolution
If a conflict should occur due to the presence in the `Pipfile.lock` of a dependency of a new package, the following steps should be undertaken before alerting the user:
1. Determine whether the previously locked version of the dependency meets the constraints required of the new package; if so, pin that version;
2. If the previously locked version is not present in the `Pipfile` and is not a dependency of any other dependencies (i.e. has no presence in `pipenv graph`, etc), update the lockfile with the new version;
3. If there is a new or existing dependency which has a conflict with existing entries in the lockfile, perform an intermediate resolution step by checking:
a. If the new dependency can be satisfied by existing installs;
b. Whether conflicts can be upgraded without affecting locked dependencies;
c. If locked dependencies must be upgraded, whether those dependencies ultimately have any dependencies in the `Pipfile`;
d. If a traversal up the graph lands in the `Pipfile`, create _abstract dependencies_ from the `Pipfile` entries and determine whether they will still be satisfied by the new version;
e. If a new pin is required, ensure that any subdependencies of the newly pinned dependencies are therefore also re-pinned (simply prefer the updated lockfile instead of the cached version);
4. Raise an Exception alerting the user that they either need to do a full lock or manually pin a version.
## Necessary Changes
In order to make these changes, we will need to modify the dependency resolution process. Overall, locking will require the following implementation changes:
1. The ability to restore any entries that would otherwise be removed when the `--keep-outdated` flag is passed. The process already provides a caching mechanism, so we simply need to restore missing cache keys;
2. Conflict resolution steps:
a. Check an abstract dependency/candidate against a lockfile entry;
b. Requirements mapping for each dependency in the environment to determine if a lockfile entry is a descendent of any other entries;
Author: Dan Ryan <dan@danryan.co>
+14 -10
View File
@@ -26,16 +26,6 @@ warnings.filterwarnings("ignore", category=DependencyWarning)
warnings.filterwarnings("ignore", category=ResourceWarning)
warnings.filterwarnings("ignore", category=UserWarning)
if sys.version_info >= (3, 1) and sys.version_info <= (3, 6):
if sys.stdout.isatty() and sys.stderr.isatty():
import io
import atexit
stdout_wrapper = io.TextIOWrapper(sys.stdout.buffer, encoding='utf8')
atexit.register(stdout_wrapper.close)
stderr_wrapper = io.TextIOWrapper(sys.stderr.buffer, encoding='utf8')
atexit.register(stderr_wrapper.close)
sys.stdout = stdout_wrapper
sys.stderr = stderr_wrapper
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = fs_str("1")
@@ -46,6 +36,20 @@ try:
except Exception:
pass
from pipenv.vendor.vistir.misc import get_text_stream
stdout = get_text_stream("stdout")
stderr = get_text_stream("stderr")
if os.name == "nt":
from pipenv.vendor.vistir.misc import _can_use_color, _wrap_for_color
if _can_use_color(stdout):
stdout = _wrap_for_color(stdout)
if _can_use_color(stderr):
stderr = _wrap_for_color(stderr)
sys.stdout = stdout
sys.stderr = stderr
from .cli import cli
from . import resolver
+6 -3
View File
@@ -5,9 +5,6 @@ Exposes a standard API that enables compatibility across python versions,
operating systems, etc.
"""
import functools
import importlib
import io
import os
import sys
import warnings
@@ -122,6 +119,12 @@ UNICODE_TO_ASCII_TRANSLATION_MAP = {
}
def decode_for_output(output, target=sys.stdout):
return vistir.misc.decode_for_output(
output, sys.stdout, translation_map=UNICODE_TO_ASCII_TRANSLATION_MAP
)
def decode_output(output):
if not isinstance(output, six.string_types):
return output
+10 -9
View File
@@ -12,8 +12,6 @@ import click_completion
import crayons
import delegator
from click_didyoumean import DYMCommandCollection
from ..__version__ import __version__
from .options import (
CONTEXT_SETTINGS, PipenvGroup, code_option, common_options, deploy_option,
@@ -154,7 +152,11 @@ def cli(
# There is no virtualenv yet.
if not project.virtualenv_exists:
echo(
crayons.red("No virtualenv has been created for this project yet!"),
"{}({}){}".format(
crayons.red("No virtualenv has been created for this project"),
crayons.white(project.project_directory, bold=True),
crayons.red(" yet!")
),
err=True,
)
ctx.abort()
@@ -300,6 +302,7 @@ def uninstall(
if retcode:
sys.exit(retcode)
@cli.command(short_help="Generates Pipfile.lock.", context_settings=CONTEXT_SETTINGS)
@lock_options
@pass_state
@@ -400,7 +403,6 @@ def shell(
def run(state, command, args):
"""Spawns a command installed into the virtualenv."""
from ..core import do_run
do_run(
command=command, args=args, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror
)
@@ -555,7 +557,7 @@ def run_open(state, module, *args, **kwargs):
EDITOR=atom pipenv open requests
"""
from ..core import which, ensure_project
from ..core import which, ensure_project, inline_activate_virtual_environment
# Ensure that virtualenv is available.
ensure_project(
@@ -575,6 +577,7 @@ def run_open(state, module, *args, **kwargs):
else:
p = c.out.strip().rstrip("cdo")
echo(crayons.normal("Opening {0!r} in your EDITOR.".format(p), bold=True))
inline_activate_virtual_environment()
edit(filename=p)
return 0
@@ -629,11 +632,9 @@ def sync(
def clean(ctx, state, dry_run=False, bare=False, user=False):
"""Uninstalls all packages not specified in Pipfile.lock."""
from ..core import do_clean
do_clean(ctx=ctx, three=state.three, python=state.python, dry_run=dry_run)
do_clean(ctx=ctx, three=state.three, python=state.python, dry_run=dry_run,
system=state.system)
# Only invoke the "did you mean" when an argument wasn't passed (it breaks those).
if "-" not in "".join(sys.argv) and len(sys.argv) > 1:
cli = DYMCommandCollection(sources=[cli])
if __name__ == "__main__":
cli()
+5 -3
View File
@@ -8,6 +8,7 @@ import click.types
from click import (
BadParameter, Group, Option, argument, echo, make_pass_decorator, option
)
from click_didyoumean import DYMMixin
from .. import environments
from ..utils import is_valid_url
@@ -19,7 +20,7 @@ CONTEXT_SETTINGS = {
}
class PipenvGroup(Group):
class PipenvGroup(DYMMixin, Group):
"""Custom Group class provides formatted main help"""
def get_help_option(self, ctx):
@@ -293,8 +294,9 @@ def code_option(f):
if value:
state.installstate.code = value
return value
return option("--code", "-c", nargs=1, default=False, help="Import from codebase.",
callback=callback, expose_value=False)(f)
return option("--code", "-c", nargs=1, default=False, help="Install packages "
"automatically discovered from import statements.", callback=callback,
expose_value=False)(f)
def deploy_option(f):
+1 -1
View File
@@ -10,7 +10,7 @@ class ScriptEmptyError(ValueError):
def _quote_if_contains(value, pattern):
if next(re.finditer(pattern, value), None):
if next(iter(re.finditer(pattern, value)), None):
return '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', value))
return value
+385 -166
View File
@@ -1,5 +1,5 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import json as simplejson
import logging
import os
@@ -14,13 +14,13 @@ import urllib3.util as urllib3_util
import vistir
import click_completion
import crayons
import delegator
import dotenv
import pipfile
from .patched import crayons
from . import environments, exceptions, pep508checker, progress
from ._compat import fix_utf8
from ._compat import fix_utf8, decode_for_output
from .cmdparse import Script
from .environments import (
PIPENV_CACHE_DIR, PIPENV_COLORBLIND, PIPENV_DEFAULT_PYTHON_VERSION,
@@ -34,7 +34,8 @@ from .utils import (
escape_cmd, escape_grouped_arguments, find_windows_executable,
get_canonical_names, is_pinned, is_pypi_url, is_required_version, is_star,
is_valid_url, parse_indexes, pep423_name, prepare_pip_source_args,
proper_case, python_version, venv_resolve_deps
proper_case, python_version, venv_resolve_deps, run_command,
is_python_command, find_python
)
@@ -86,16 +87,19 @@ def which(command, location=None, allow_global=False):
location = os.environ.get("VIRTUAL_ENV", None)
if not (location and os.path.exists(location)) and not allow_global:
raise RuntimeError("location not created nor specified")
version_str = "python{0}".format(".".join([str(v) for v in sys.version_info[:2]]))
is_python = command in ("python", os.path.basename(sys.executable), version_str)
if not allow_global:
if os.name == "nt":
p = find_windows_executable(os.path.join(location, "Scripts"), command)
else:
p = os.path.join(location, "bin", command)
else:
if command == "python":
if is_python:
p = sys.executable
if not os.path.exists(p):
if command == "python":
if is_python:
p = sys.executable or system_which("python")
else:
p = system_which(command)
@@ -238,7 +242,9 @@ def import_from_code(path="."):
rs = []
try:
for r in pipreqs.get_all_imports(path):
for r in pipreqs.get_all_imports(
path, encoding="utf-8", extra_ignore_dirs=[".venv"]
):
if r not in BAD_PACKAGES:
rs.append(r)
pkg_names = pipreqs.get_pkg_names(rs)
@@ -323,26 +329,16 @@ def find_a_system_python(line):
* Search for "python" and "pythonX.Y" executables in PATH to find a match.
* Nothing fits, return None.
"""
if not line:
return None
if os.path.isabs(line):
return line
from .vendor.pythonfinder import Finder
from .vendor.pythonfinder import Finder
finder = Finder(system=False, global_search=True)
if not line:
return next(iter(finder.find_all_python_versions()), None)
# Use the windows finder executable
if (line.startswith("py ") or line.startswith("py.exe ")) and os.name == "nt":
line = line.split(" ", 1)[1].lstrip("-")
elif line.startswith("py"):
python_entry = finder.which(line)
if python_entry:
return python_entry.path.as_posix()
return None
python_entry = finder.find_python_version(line)
if not python_entry:
python_entry = finder.which("python{0}".format(line))
if python_entry:
return python_entry.path.as_posix()
return None
python_entry = find_python(finder, line)
return python_entry
def ensure_python(three=None, python=None):
@@ -472,6 +468,8 @@ def ensure_virtualenv(three=None, python=None, site_packages=False, pypi_mirror=
ensure_environment()
# Ensure Python is available.
python = ensure_python(three=three, python=python)
if python is not None and not isinstance(python, six.string_types):
python = python.path.as_posix()
# Create the virtualenv.
# Abort if --system (or running in a virtualenv).
if PIPENV_USE_SYSTEM:
@@ -493,7 +491,10 @@ def ensure_virtualenv(three=None, python=None, site_packages=False, pypi_mirror=
elif (python) or (three is not None) or (site_packages is not False):
USING_DEFAULT_PYTHON = False
# Ensure python is installed before deleting existing virtual env
ensure_python(three=three, python=python)
python = ensure_python(three=three, python=python)
if python is not None and not isinstance(python, six.string_types):
python = python.path.as_posix()
click.echo(crayons.red("Virtualenv already exists!"), err=True)
# If VIRTUAL_ENV is set, there is a possibility that we are
# going to remove the active virtualenv that the user cares
@@ -540,11 +541,16 @@ def ensure_project(
# Automatically use an activated virtualenv.
if PIPENV_USE_SYSTEM:
system = True
if not project.pipfile_exists:
if deploy is True:
raise exceptions.PipfileNotFound
else:
project.touch_pipfile()
if not project.pipfile_exists and deploy:
raise exceptions.PipfileNotFound
# Fail if working under /
if not project.name:
click.echo(
"{0}: Pipenv is not intended to work under the root directory, "
"please choose another path.".format(crayons.red("ERROR")),
err=True
)
sys.exit(1)
# Skip virtualenv creation when --system was used.
if not system:
ensure_virtualenv(
@@ -607,24 +613,23 @@ def shorten_path(location, bold=False):
def do_where(virtualenv=False, bare=True):
"""Executes the where functionality."""
if not virtualenv:
location = project.pipfile_location
# Shorten the virtual display of the path to the virtualenv.
if not bare:
location = shorten_path(location)
if not location:
if not project.pipfile_exists:
click.echo(
"No Pipfile present at project home. Consider running "
"{0} first to automatically generate a Pipfile for you."
"".format(crayons.green("`pipenv install`")),
err=True,
)
elif not bare:
return
location = project.pipfile_location
# Shorten the virtual display of the path to the virtualenv.
if not bare:
location = shorten_path(location)
click.echo(
"Pipfile found at {0}.\n Considering this to be the project home."
"".format(crayons.green(location)),
err=True,
)
pass
else:
click.echo(project.project_directory)
else:
@@ -675,10 +680,10 @@ def batch_install(deps_list, procs, failed_deps_queue,
requirements_dir, no_deps=False, ignore_hashes=False,
allow_global=False, blocking=False, pypi_mirror=None,
nprocs=PIPENV_MAX_SUBPROCESS, retry=True):
from .vendor.requirementslib.models.utils import strip_extras_markers_from_requirement
failed = (not retry)
if not failed:
label = INSTALL_LABEL if os.name != "nt" else ""
label = INSTALL_LABEL if not PIPENV_HIDE_EMOJIS else ""
else:
label = INSTALL_LABEL2
@@ -690,6 +695,10 @@ def batch_install(deps_list, procs, failed_deps_queue,
trusted_hosts = []
# Install these because
for dep in deps_list_bar:
if dep.req.req:
dep.req.req = strip_extras_markers_from_requirement(dep.req.req)
if dep.markers:
dep.markers = str(strip_extras_markers_from_requirement(dep.get_markers()))
index = None
if dep.index:
index = project.find_source(dep.index)
@@ -698,10 +707,18 @@ def batch_install(deps_list, procs, failed_deps_queue,
trusted_hosts.append(urllib3_util.parse_url(index.get("url")).host)
# Install the module.
is_artifact = False
if no_deps:
link = getattr(dep.req, "link", None)
is_wheel = False
if link:
is_wheel = link.is_wheel
if dep.is_file_or_url and (dep.is_direct_url or any(
dep.req.uri.endswith(ext) for ext in ["zip", "tar.gz"]
)):
is_artifact = True
elif dep.is_vcs:
is_artifact = True
needs_deps = not no_deps if no_deps is True else is_artifact
extra_indexes = []
if not index and indexes:
@@ -714,27 +731,25 @@ def batch_install(deps_list, procs, failed_deps_queue,
os.environ["PIP_USER"] = vistir.compat.fs_str("0")
if "PYTHONHOME" in os.environ:
del os.environ["PYTHONHOME"]
if no_deps:
if not needs_deps:
link = getattr(dep.req, "link", None)
is_wheel = False
if link:
is_wheel = link.is_wheel
is_non_editable_vcs = (dep.is_vcs and not dep.editable)
no_deps = not (dep.is_file_or_url and not (is_wheel or dep.editable))
needs_deps = dep.is_file_or_url and not (is_wheel or dep.editable)
c = pip_install(
dep,
ignore_hashes=any([ignore_hashes, dep.editable, dep.is_vcs]),
allow_global=allow_global,
no_deps=no_deps,
no_deps=not needs_deps,
block=any([dep.editable, dep.is_vcs, blocking]),
index=index,
requirements_dir=requirements_dir,
pypi_mirror=pypi_mirror,
trusted_hosts=trusted_hosts,
extra_indexes=extra_indexes
extra_indexes=extra_indexes,
use_pep517=not failed,
)
if dep.is_vcs or dep.editable:
c.block()
if procs.qsize() < nprocs:
c.dep = dep
procs.put(c)
@@ -814,6 +829,7 @@ def do_install_dependencies(
else:
install_kwargs["nprocs"] = 1
# with project.environment.activated():
batch_install(
deps_list, procs, failed_deps_queue, requirements_dir, **install_kwargs
)
@@ -859,6 +875,7 @@ def convert_three_to_python(three, python):
def do_create_virtualenv(python=None, site_packages=False, pypi_mirror=None):
"""Creates a virtualenv."""
click.echo(
crayons.normal(fix_utf8("Creating a virtualenv for this project…"), bold=True), err=True
)
@@ -902,20 +919,21 @@ def do_create_virtualenv(python=None, site_packages=False, pypi_mirror=None):
pip_config = {}
# Actually create the virtualenv.
nospin = environments.PIPENV_NOSPIN
with create_spinner("Creating virtual environment...") as sp:
with create_spinner(u"Creating virtual environment...") as sp:
c = vistir.misc.run(
cmd, verbose=False, return_object=True, write_to_stdout=False,
combine_stderr=False, block=True, nospin=True, env=pip_config,
)
click.echo(crayons.blue("{0}".format(c.out)), err=True)
click.echo(crayons.blue(u"{0}".format(c.out)), err=True)
if c.returncode != 0:
sp.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format("Failed creating virtual environment"))
sp.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format(u"Failed creating virtual environment"))
error = c.err if environments.is_verbose() else exceptions.prettify_exc(c.err)
raise exceptions.VirtualenvCreationException(
extra=[crayons.blue("{0}".format(c.err)),]
extra=crayons.red("{0}".format(error))
)
else:
sp.green.ok(environments.PIPENV_SPINNER_OK_TEXT.format("Successfully created virtual environment!"))
sp.green.ok(environments.PIPENV_SPINNER_OK_TEXT.format(u"Successfully created virtual environment!"))
# Associate project directory with the environment.
# This mimics Pew's "setproject".
@@ -1016,9 +1034,12 @@ def do_lock(
dev_packages = overwrite_dev(project.packages, dev_packages)
# Resolve dev-package dependencies, with pip-tools.
for is_dev in [True, False]:
pipfile_section = "dev_packages" if is_dev else "packages"
pipfile_section = "dev-packages" if is_dev else "packages"
lockfile_section = "develop" if is_dev else "default"
packages = getattr(project, pipfile_section)
if project.pipfile_exists:
packages = project.parsed_pipfile.get(pipfile_section, {})
else:
packages = getattr(project, pipfile_section.replace("-", "_"))
if write:
# Alert the user of progress.
@@ -1031,12 +1052,9 @@ def do_lock(
err=True,
)
deps = convert_deps_to_pip(
packages, project, r=False, include_index=True
)
# Mutates the lockfile
venv_resolve_deps(
deps,
packages,
which=which,
project=project,
dev=is_dev,
@@ -1045,7 +1063,8 @@ def do_lock(
allow_global=system,
pypi_mirror=pypi_mirror,
pipfile=packages,
lockfile=lockfile
lockfile=lockfile,
keep_outdated=keep_outdated
)
# Support for --keep-outdated…
@@ -1062,6 +1081,12 @@ def do_lock(
lockfile[section_name][canonical_name] = cached_lockfile[
section_name
][canonical_name].copy()
for key in ["default", "develop"]:
packages = set(cached_lockfile[key].keys())
new_lockfile = set(lockfile[key].keys())
missing = packages - new_lockfile
for missing_pkg in missing:
lockfile[key][missing_pkg] = cached_lockfile[key][missing_pkg].copy()
# Overwrite any develop packages with default packages.
lockfile["develop"].update(overwrite_dev(lockfile.get("default", {}), lockfile["develop"]))
if write:
@@ -1140,12 +1165,19 @@ def do_init(
pypi_mirror=None,
):
"""Executes the init functionality."""
from .environments import PIPENV_VIRTUALENV
from .environments import (
PIPENV_VIRTUALENV, PIPENV_DEFAULT_PYTHON_VERSION, PIPENV_PYTHON, PIPENV_USE_SYSTEM
)
python = None
if PIPENV_PYTHON is not None:
python = PIPENV_PYTHON
elif PIPENV_DEFAULT_PYTHON_VERSION is not None:
python = PIPENV_DEFAULT_PYTHON_VERSION
if not system:
if not system and not PIPENV_USE_SYSTEM:
if not project.virtualenv_exists:
try:
do_create_virtualenv(pypi_mirror=pypi_mirror)
do_create_virtualenv(python=python, three=None, pypi_mirror=pypi_mirror)
except KeyboardInterrupt:
cleanup_virtualenv(bare=False)
sys.exit(1)
@@ -1257,12 +1289,12 @@ def pip_install(
requirements_dir=None,
extra_indexes=None,
pypi_mirror=None,
trusted_hosts=None
trusted_hosts=None,
use_pep517=True
):
from pipenv.patched.notpip._internal import logger as piplogger
from .utils import Mapping
from .vendor.vistir.compat import Mapping
from .vendor.urllib3.util import parse_url
src = []
write_to_tmpfile = False
if requirement:
@@ -1280,6 +1312,10 @@ def pip_install(
crayons.normal("Installing {0!r}".format(requirement.name), bold=True),
err=True,
)
if requirement:
ignore_hashes = True if not requirement.hashes else ignore_hashes
# Create files for hash mode.
if write_to_tmpfile:
if not requirements_dir:
@@ -1289,12 +1325,18 @@ def pip_install(
prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir,
delete=False
)
f.write(vistir.misc.to_bytes(requirement.as_line()))
line = requirement.as_line(include_hashes=not ignore_hashes)
if environments.is_verbose():
click.echo(
"Writing requirement line to temporary file: {0!r}".format(line),
err=True
)
f.write(vistir.misc.to_bytes(line))
r = f.name
f.close()
# Install dependencies when a package is a VCS dependency.
if requirement and requirement.vcs:
no_deps = False
# Install dependencies when a package is a non-editable VCS dependency.
# Don't specify a source directory when using --system.
if not allow_global and ("PIP_SRC" not in os.environ):
src.extend(["--src", "{0}".format(project.virtualenv_src_location)])
@@ -1340,25 +1382,98 @@ def pip_install(
create_mirror_source(pypi_mirror) if is_pypi_url(source["url"]) else source
for source in sources
]
line_kwargs = {"as_list": True, "include_hashes": not ignore_hashes}
# Install dependencies when a package is a VCS dependency.
if requirement and requirement.vcs:
ignore_hashes = True
# Don't specify a source directory when using --system.
src_dir = None
if "PIP_SRC" in os.environ:
src_dir = os.environ["PIP_SRC"]
src = ["--src", os.environ["PIP_SRC"]]
if not requirement.editable:
no_deps = False
if src_dir is not None:
if environments.is_verbose():
click.echo("Using source directory: {0!r}".format(src_dir))
repo = requirement.req.get_vcs_repo(src_dir=src_dir)
else:
repo = requirement.req.get_vcs_repo()
write_to_tmpfile = True
line_kwargs["include_markers"] = False
line_kwargs["include_hashes"] = False
if not requirements_dir:
requirements_dir = vistir.path.create_tracked_tempdir(prefix="pipenv",
suffix="requirements")
f = vistir.compat.NamedTemporaryFile(
prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir,
delete=False
)
line = "-e" if requirement.editable else ""
if requirement.editable or requirement.name is not None:
name = requirement.name
if requirement.extras:
name = "{0}{1}".format(name, requirement.extras_as_pip)
line = "{0}{1}#egg={2}".format(
line, vistir.path.path_to_url(repo.checkout_directory), requirement.name
)
if repo.subdirectory:
line = "{0}&subdirectory={1}".format(line, repo.subdirectory)
else:
line = requirement.as_line(**line_kwargs)
if environments.is_verbose():
click.echo(
"Writing requirement line to temporary file: {0!r}".format(line),
err=True
)
f.write(vistir.misc.to_bytes(line))
r = f.name
f.close()
# Create files for hash mode.
if write_to_tmpfile and not r:
if not requirements_dir:
requirements_dir = vistir.path.create_tracked_tempdir(
prefix="pipenv", suffix="requirements")
f = vistir.compat.NamedTemporaryFile(
prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir,
delete=False
)
ignore_hashes = True if not requirement.hashes else ignore_hashes
line = requirement.as_line(include_hashes=not ignore_hashes)
line = "{0} {1}".format(line, " ".join(src))
if environments.is_verbose():
click.echo(
"Writing requirement line to temporary file: {0!r}".format(line),
err=True
)
f.write(vistir.misc.to_bytes(line))
r = f.name
f.close()
if (requirement and requirement.editable) and not r:
line_kwargs = {"as_list": True}
if requirement.markers:
line_kwargs["include_markers"] = False
line_kwargs["include_markers"] = False
line_kwargs["include_hashes"] = False
install_reqs = requirement.as_line(**line_kwargs)
if requirement.editable and install_reqs[0].startswith("-e "):
req, install_reqs = install_reqs[0], install_reqs[1:]
possible_hashes = install_reqs[:]
editable_opt, req = req.split(" ", 1)
install_reqs = [editable_opt, req] + install_reqs
if not all(item.startswith("--hash") for item in install_reqs):
ignore_hashes = True
# hashes must be passed via a file
ignore_hashes = True
elif r:
install_reqs = ["-r", r]
with open(r) as f:
if "--hash" not in f.read():
ignore_hashes = True
else:
ignore_hashes = True if not requirement.hashes else False
install_reqs = requirement.as_line(as_list=True)
ignore_hashes = True if not requirement.hashes else ignore_hashes
install_reqs = requirement.as_line(as_list=True, include_hashes=not ignore_hashes)
if not requirement.markers:
install_reqs = [escape_cmd(r) for r in install_reqs]
elif len(install_reqs) > 1:
@@ -1379,7 +1494,11 @@ def pip_install(
pip_command.extend(prepare_pip_source_args(sources))
if not ignore_hashes:
pip_command.append("--require-hashes")
if not use_pep517:
from .vendor.packaging.version import parse as parse_version
pip_command.append("--no-build-isolation")
if project.environment.pip_version >= parse_version("19.0"):
pip_command.append("--no-use-pep517")
if environments.is_verbose():
click.echo("$ {0}".format(pip_command), err=True)
cache_dir = vistir.compat.Path(PIPENV_CACHE_DIR)
@@ -1398,6 +1517,8 @@ def pip_install(
)
cmd = Script.parse(pip_command)
pip_command = cmd.cmdify()
c = None
# with project.environment.activated():
c = delegator.run(pip_command, block=block, env=pip_config)
return c
@@ -1425,18 +1546,61 @@ def pip_download(package_name):
return c
def fallback_which(command, location=None, allow_global=False, system=False):
"""
A fallback implementation of the `which` utility command that relies exclusively on
searching the path for commands.
:param str command: The command to search for, optional
:param str location: The search location to prioritize (prepend to path), defaults to None
:param bool allow_global: Whether to search the global path, defaults to False
:param bool system: Whether to use the system python instead of pipenv's python, defaults to False
:raises ValueError: Raised if no command is provided
:raises TypeError: Raised if the command provided is not a string
:return: A path to the discovered command location
:rtype: str
"""
from .vendor.pythonfinder import Finder
if not command:
raise ValueError("fallback_which: Must provide a command to search for...")
if not isinstance(command, six.string_types):
raise TypeError("Provided command must be a string, received {0!r}".format(command))
global_search = system or allow_global
if location is None:
global_search = True
finder = Finder(system=False, global_search=global_search, path=location)
if is_python_command(command):
result = find_python(finder, command)
if result:
return result
result = finder.which(command)
if result:
return result.path.as_posix()
return ""
def which_pip(allow_global=False):
"""Returns the location of virtualenv-installed pip."""
location = None
if "VIRTUAL_ENV" in os.environ:
location = os.environ["VIRTUAL_ENV"]
if allow_global:
if "VIRTUAL_ENV" in os.environ:
return which("pip", location=os.environ["VIRTUAL_ENV"])
if location:
pip = which("pip", location=location)
if pip:
return pip
for p in ("pip", "pip3", "pip2"):
where = system_which(p)
if where:
return where
return which("pip")
pip = which("pip")
if not pip:
pip = fallback_which("pip", allow_global=allow_global, location=location)
return pip
def system_which(command, mult=False):
@@ -1446,6 +1610,7 @@ def system_which(command, mult=False):
vistir.compat.fs_str(k): vistir.compat.fs_str(val)
for k, val in os.environ.items()
}
result = None
try:
c = delegator.run("{0} {1}".format(_which, command))
try:
@@ -1460,21 +1625,20 @@ def system_which(command, mult=False):
)
assert c.return_code == 0
except AssertionError:
return None if not mult else []
result = fallback_which(command, allow_global=True)
except TypeError:
from .vendor.pythonfinder import Finder
finder = Finder()
result = finder.which(command)
if result:
return result.path.as_posix()
return
if not result:
result = fallback_which(command, allow_global=True)
else:
result = c.out.strip() or c.err.strip()
if mult:
return result.split("\n")
if not result:
result = next(iter([c.out, c.err]), "").split("\n")
result = next(iter(result)) if not mult else result
return result
if not result:
result = fallback_which(command, allow_global=True)
result = [result] if mult else result
return result
else:
return result.split("\n")[0]
def format_help(help):
@@ -1856,7 +2020,7 @@ def do_install(
# This is for if the user passed in dependencies, then we want to make sure we
else:
from .vendor.requirementslib import Requirement
from .vendor.requirementslib.models.requirements import Requirement
# make a tuple of (display_name, entry)
pkg_list = packages + ["-e {0}".format(pkg) for pkg in editable_packages]
@@ -1872,6 +2036,7 @@ def do_install(
pypi_mirror=pypi_mirror,
skip_lock=skip_lock,
)
pip_shims_module = os.environ.pop("PIP_SHIMS_BASE_MODULE", None)
for pkg_line in pkg_list:
click.echo(
crayons.normal(
@@ -1907,6 +2072,15 @@ def do_install(
pypi_mirror=pypi_mirror,
)
if not c.ok:
sp.write_err(vistir.compat.fs_str(
"{0}: {1}".format(
crayons.red("WARNING"),
"Failed installing package {0}".format(pkg_line)
),
))
sp.write_err(vistir.compat.fs_str(
"Error text: {0}".format(c.out)
))
raise RuntimeError(c.err)
except (ValueError, RuntimeError) as e:
sp.write_err(vistir.compat.fs_str(
@@ -1954,15 +2128,25 @@ def do_install(
crayons.normal(fix_utf8(""), bold=True),
)
))
# Add the package to the Pipfile.
try:
project.add_package_to_pipfile(pkg_requirement, dev)
except ValueError as e:
import traceback
sp.write_err(
"{0} {1}".format(
crayons.red("Error:", bold=True), traceback.format_exc()
)
)
sp.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format(
"Failed adding package to Pipfile"
))
sp.ok(environments.PIPENV_SPINNER_OK_TEXT.format("Installation Succeeded"))
# Add the package to the Pipfile.
try:
project.add_package_to_pipfile(pkg_requirement, dev)
except ValueError as e:
raise exceptions.PipfileException(e)
# Update project settings with pre preference.
if pre:
project.update_settings({"allow_prereleases": pre})
if pip_shims_module:
os.environ["PIP_SHIMS_BASE_MODULE"] = pip_shims_module
do_init(
dev=dev,
system=system,
@@ -2074,6 +2258,7 @@ def do_uninstall(
p for normalized, p in selected_pkg_map.items()
if normalized in (used_packages - bad_pkgs)
]
pip_path = None
for normalized, package_name in selected_pkg_map.items():
click.echo(
crayons.white(
@@ -2083,12 +2268,10 @@ def do_uninstall(
# Uninstall the package.
if package_name in packages_to_remove:
with project.environment.activated():
cmd = "{0} uninstall {1} -y".format(
escape_grouped_arguments(which_pip(allow_global=system)), package_name,
)
if environments.is_verbose():
click.echo("$ {0}".format(cmd))
c = delegator.run(cmd)
if pip_path is None:
pip_path = which_pip(allow_global=system)
cmd = [pip_path, "uninstall", package_name, "-y"]
c = run_command(cmd)
click.echo(crayons.blue(c.out))
if c.return_code != 0:
failure = True
@@ -2158,7 +2341,7 @@ def do_shell(three=None, python=False, fancy=False, shell_args=None, pypi_mirror
# Only set PIPENV_ACTIVE after finishing reading virtualenv_location
# otherwise its value will be changed
os.environ["PIPENV_ACTIVE"] = vistir.misc.fs_str("1")
os.environ.pop("PIP_SHIMS_BASE_MODULE", None)
if fancy:
@@ -2297,6 +2480,8 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None):
load_dot_env()
previous_pip_shims_module = os.environ.pop("PIP_SHIMS_BASE_MODULE", None)
# Activate virtualenv under the current interpreter's environment
inline_activate_virtual_environment()
@@ -2304,10 +2489,9 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None):
# Only set PIPENV_ACTIVE after finishing reading virtualenv_location
# such as in inline_activate_virtual_environment
# otherwise its value will be changed
previous_pipenv_active_value = os.environ.get("PIPENV_ACTIVE")
os.environ["PIPENV_ACTIVE"] = vistir.misc.fs_str("1")
os.environ.pop("PIP_SHIMS_BASE_MODULE", None)
try:
script = project.build_script(command, args)
cmd_string = ' '.join([script.command] + script.args)
@@ -2315,10 +2499,21 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None):
click.echo(crayons.normal("$ {0}".format(cmd_string)), err=True)
except ScriptEmptyError:
click.echo("Can't run script {0!r}-it's empty?", err=True)
run_args = [script]
run_kwargs = {}
if os.name == "nt":
do_run_nt(script)
run_fn = do_run_nt
else:
do_run_posix(script, command=command)
run_fn = do_run_posix
run_kwargs = {"command": command}
try:
run_fn(*run_args, **run_kwargs)
finally:
os.environ.pop("PIPENV_ACTIVE", None)
if previous_pipenv_active_value is not None:
os.environ["PIPENV_ACTIVE"] = previous_pipenv_active_value
if previous_pip_shims_module is not None:
os.environ["PIP_SHIMS_BASE_MODULE"] = previous_pip_shims_module
def do_check(
@@ -2330,6 +2525,8 @@ def do_check(
args=None,
pypi_mirror=None,
):
from .environments import is_verbose
from pipenv.vendor.vistir.compat import JSONDecodeError
if not system:
# Ensure that virtualenv is available.
ensure_project(
@@ -2342,8 +2539,8 @@ def do_check(
if not args:
args = []
if unused:
deps_required = [k for k in project.packages.keys()]
deps_needed = import_from_code(unused)
deps_required = [k.lower() for k in project.packages.keys()]
deps_needed = [k.lower() for k in import_from_code(unused)]
for dep in deps_needed:
try:
deps_required.remove(dep)
@@ -2360,18 +2557,29 @@ def do_check(
sys.exit(1)
else:
sys.exit(0)
click.echo(crayons.normal(fix_utf8("Checking PEP 508 requirements…"), bold=True))
if system:
python = system_which("python")
else:
python = which("python")
# Run the PEP 508 checker in the virtualenv.
c = delegator.run(
'"{0}" {1}'.format(
python, escape_grouped_arguments(pep508checker.__file__.rstrip("cdo"))
)
click.echo(crayons.normal(decode_for_output("Checking PEP 508 requirements…"), bold=True))
pep508checker_path = pep508checker.__file__.rstrip("cdo")
safety_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "patched", "safety.zip"
)
results = simplejson.loads(c.out)
if not system:
python = which("python")
else:
python = system_which("python")
_cmd = [vistir.compat.Path(python).as_posix()]
# Run the PEP 508 checker in the virtualenv.
cmd = _cmd + [vistir.compat.Path(pep508checker_path).as_posix()]
c = run_command(cmd)
if c.return_code is not None:
try:
results = simplejson.loads(c.out.strip())
except JSONDecodeError:
click.echo("{0}\n{1}\n{2}".format(
crayons.white(decode_for_output("Failed parsing pep508 results: "), bold=True),
c.out.strip(),
c.err.strip()
))
sys.exit(1)
# Load the pipfile.
p = pipfile.Pipfile.load(project.pipfile_location)
failed = False
@@ -2396,15 +2604,13 @@ def do_check(
sys.exit(1)
else:
click.echo(crayons.green("Passed!"))
click.echo(crayons.normal(fix_utf8("Checking installed package safety…"), bold=True))
path = pep508checker.__file__.rstrip("cdo")
path = os.sep.join(__file__.split(os.sep)[:-1] + ["patched", "safety.zip"])
if not system:
python = which("python")
else:
python = system_which("python")
click.echo(crayons.normal(
decode_for_output("Checking installed package safety…"), bold=True)
)
if ignore:
ignored = "--ignore {0}".format(" --ignore ".join(ignore))
if not isinstance(ignore, (tuple, list)):
ignore = [ignore]
ignored = [["--ignore", cve] for cve in ignore]
click.echo(
crayons.normal(
"Notice: Ignoring CVE(s) {0}".format(crayons.yellow(", ".join(ignore)))
@@ -2413,17 +2619,21 @@ def do_check(
)
else:
ignored = ""
c = delegator.run(
'"{0}" {1} check --json --key={2} {3}'.format(
python, escape_grouped_arguments(path), PIPENV_PYUP_API_KEY, ignored
)
)
key = "--key={0}".format(PIPENV_PYUP_API_KEY)
cmd = _cmd + [safety_path, "check", "--json", key]
if ignored:
for cve in ignored:
cmd += cve
c = run_command(cmd, catch_exceptions=False)
try:
results = simplejson.loads(c.out)
except ValueError:
click.echo("An error occurred:", err=True)
click.echo(c.err if len(c.err) > 0 else c.out, err=True)
sys.exit(1)
except (ValueError, JSONDecodeError):
raise exceptions.JSONParseError(c.out, c.err)
except Exception:
raise exceptions.PipenvCmdError(c.cmd, c.out, c.err, c.return_code)
if c.ok:
click.echo(crayons.green("All good!"))
sys.exit(0)
for (package, resolved, installed, description, vuln) in results:
click.echo(
"{0}: {1} {2} resolved ({3} installed)!".format(
@@ -2435,14 +2645,14 @@ def do_check(
)
click.echo("{0}".format(description))
click.echo()
if not results:
click.echo(crayons.green("All good!"))
else:
sys.exit(1)
def do_graph(bare=False, json=False, json_tree=False, reverse=False):
from pipenv.vendor.vistir.compat import JSONDecodeError
import pipdeptree
pipdeptree_path = pipdeptree.__file__.rstrip("cdo")
try:
python_path = which("python")
except AttributeError:
@@ -2457,6 +2667,9 @@ def do_graph(bare=False, json=False, json_tree=False, reverse=False):
sys.exit(1)
except RuntimeError:
pass
else:
python_path = vistir.compat.Path(python_path).as_posix()
pipdeptree_path = vistir.compat.Path(pipdeptree_path).as_posix()
if reverse and json:
click.echo(
@@ -2507,17 +2720,20 @@ def do_graph(bare=False, json=False, json_tree=False, reverse=False):
err=True,
)
sys.exit(1)
cmd = '"{0}" {1} {2} -l'.format(
python_path, escape_grouped_arguments(pipdeptree.__file__.rstrip("cdo")), flag
)
cmd_args = [python_path, pipdeptree_path, flag, "-l"]
c = run_command(cmd_args)
# Run dep-tree.
c = delegator.run(cmd)
if not bare:
if json:
data = []
for d in simplejson.loads(c.out):
if d["package"]["key"] not in BAD_PACKAGES:
data.append(d)
try:
parsed = simplejson.loads(c.out.strip())
except JSONDecodeError:
raise exceptions.JSONParseError(c.out, c.err)
else:
for d in parsed:
if d["package"]["key"] not in BAD_PACKAGES:
data.append(d)
click.echo(simplejson.dumps(data, indent=4))
sys.exit(0)
elif json_tree:
@@ -2533,12 +2749,18 @@ def do_graph(bare=False, json=False, json_tree=False, reverse=False):
obj["dependencies"] = traverse(obj["dependencies"])
return obj
data = traverse(simplejson.loads(c.out))
click.echo(simplejson.dumps(data, indent=4))
sys.exit(0)
try:
parsed = simplejson.loads(c.out.strip())
except JSONDecodeError:
raise exceptions.JSONParseError(c.out, c.err)
else:
data = traverse(parsed)
click.echo(simplejson.dumps(data, indent=4))
sys.exit(0)
else:
for line in c.out.strip().split("\n"):
# Ignore bad packages as top level.
# TODO: This should probably be a "==" in + line.partition
if line.split("==")[0] in BAD_PACKAGES and not reverse:
continue
@@ -2607,34 +2829,32 @@ def do_sync(
click.echo(crayons.green("All dependencies are now up-to-date!"))
def do_clean(ctx, three=None, python=None, dry_run=False, bare=False, pypi_mirror=None):
def do_clean(
ctx, three=None, python=None, dry_run=False, bare=False, pypi_mirror=None,
system=False
):
# Ensure that virtualenv is available.
from packaging.utils import canonicalize_name
ensure_project(three=three, python=python, validate=False, pypi_mirror=pypi_mirror)
ensure_lockfile(pypi_mirror=pypi_mirror)
# Make sure that the virtualenv's site packages are configured correctly
# otherwise we may end up removing from the global site packages directory
installed_package_names = [
canonicalize_name(pkg.project_name) for pkg
in project.environment.get_installed_packages()
]
installed_package_names = project.installed_package_names.copy()
# Remove known "bad packages" from the list.
for bad_package in BAD_PACKAGES:
if canonicalize_name(bad_package) in installed_package_names:
if environments.is_verbose():
click.echo("Ignoring {0}.".format(bad_package), err=True)
del installed_package_names[installed_package_names.index(
canonicalize_name(bad_package)
)]
installed_package_names.remove(canonicalize_name(bad_package))
# Intelligently detect if --dev should be used or not.
develop = [canonicalize_name(k) for k in project.lockfile_content["develop"].keys()]
default = [canonicalize_name(k) for k in project.lockfile_content["default"].keys()]
for used_package in set(develop + default):
locked_packages = {
canonicalize_name(pkg) for pkg in project.lockfile_package_names["combined"]
}
for used_package in locked_packages:
if used_package in installed_package_names:
del installed_package_names[installed_package_names.index(
canonicalize_name(used_package)
)]
installed_package_names.remove(used_package)
failure = False
cmd = [which_pip(allow_global=system), "uninstall", "-y", "-qq"]
for apparent_bad_package in installed_package_names:
if dry_run and not bare:
click.echo(apparent_bad_package)
@@ -2646,9 +2866,8 @@ def do_clean(ctx, three=None, python=None, dry_run=False, bare=False, pypi_mirro
)
)
# Uninstall the package.
c = delegator.run(
"{0} uninstall {1} -y".format(which_pip(), apparent_bad_package)
)
cmd = [which_pip(), "uninstall", apparent_bad_package, "-y"]
c = run_command(cmd)
if c.return_code != 0:
failure = True
sys.exit(int(failure))
+201 -55
View File
@@ -9,17 +9,18 @@ import site
import sys
from distutils.sysconfig import get_python_lib
from sysconfig import get_paths
from sysconfig import get_paths, get_python_version
import itertools
import pkg_resources
import six
import vistir
import pipenv
from cached_property import cached_property
from .vendor.cached_property import cached_property
import vistir
from .utils import normalize_path
from .utils import normalize_path, make_posix
BASE_WORKING_SET = pkg_resources.WorkingSet(sys.path)
@@ -92,19 +93,27 @@ class Environment(object):
deps |= cls.resolve_dist(dist, working_set)
return deps
def add_dist(self, dist_name):
dist = pkg_resources.get_distribution(pkg_resources.Requirement(dist_name))
def extend_dists(self, dist):
extras = self.resolve_dist(dist, self.base_working_set)
self.extra_dists.append(dist)
if extras:
self.extra_dists.extend(extras)
def add_dist(self, dist_name):
dist = pkg_resources.get_distribution(pkg_resources.Requirement(dist_name))
self.extend_dists(dist)
@cached_property
def python_version(self):
with self.activated():
from sysconfig import get_python_version
py_version = get_python_version()
sysconfig = self.safe_import("sysconfig")
py_version = sysconfig.get_python_version()
return py_version
def find_libdir(self):
libdir = self.prefix / "lib"
return next(iter(list(libdir.iterdir())), None)
@property
def python_info(self):
include_dir = self.prefix / "include"
@@ -115,6 +124,16 @@ class Environment(object):
return {"py_version_short": py_version_short, "abiflags": abiflags}
return {}
def _replace_parent_version(self, path, replace_version):
if not os.path.exists(path):
base, leaf = os.path.split(path)
base, parent = os.path.split(base)
leaf = os.path.join(parent, leaf).replace(
replace_version, self.python_info.get("py_version_short", get_python_version())
)
return os.path.join(base, leaf)
return path
@cached_property
def base_paths(self):
"""
@@ -145,50 +164,61 @@ class Environment(object):
'stdlib': '/home/hawk/.pyenv/versions/3.7.1/lib/python3.7'}
"""
prefix = self.prefix.as_posix()
prefix = make_posix(self.prefix.as_posix())
install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
paths = get_paths(install_scheme, vars={
'base': prefix,
'platbase': prefix,
})
current_version = get_python_version()
for k in list(paths.keys()):
if not os.path.exists(paths[k]):
paths[k] = self._replace_parent_version(paths[k], current_version)
if not os.path.exists(paths["purelib"]) and not os.path.exists(paths["platlib"]):
paths = self.get_paths()
paths["PATH"] = paths["scripts"] + os.pathsep + os.defpath
if "prefix" not in paths:
paths["prefix"] = prefix
purelib = get_python_lib(plat_specific=0, prefix=prefix)
platlib = get_python_lib(plat_specific=1, prefix=prefix)
purelib = paths["purelib"] = make_posix(paths["purelib"])
platlib = paths["platlib"] = make_posix(paths["platlib"])
if purelib == platlib:
lib_dirs = purelib
else:
lib_dirs = purelib + os.pathsep + platlib
paths["libdir"] = purelib
paths["purelib"] = purelib
paths["platlib"] = platlib
paths['PYTHONPATH'] = lib_dirs
paths['PYTHONPATH'] = os.pathsep.join(["", ".", lib_dirs])
paths["libdirs"] = lib_dirs
return paths
@cached_property
def script_basedir(self):
"""Path to the environment scripts dir"""
script_dir = self.base_paths["scripts"]
return script_dir
prefix = make_posix(self.prefix.as_posix())
install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
paths = get_paths(install_scheme, vars={
'base': prefix,
'platbase': prefix,
})
return paths["scripts"]
@property
def python(self):
"""Path to the environment python"""
py = vistir.compat.Path(self.base_paths["scripts"]).joinpath("python").as_posix()
py = vistir.compat.Path(self.script_basedir).joinpath("python").absolute().as_posix()
if not py:
return vistir.compat.Path(sys.executable).as_posix()
return py
@cached_property
def sys_path(self):
"""The system path inside the environment
"""
The system path inside the environment
:return: The :data:`sys.path` from the environment
:rtype: list
"""
from .vendor.vistir.compat import JSONDecodeError
current_executable = vistir.compat.Path(sys.executable).as_posix()
if not self.python or self.python == current_executable:
return sys.path
@@ -196,18 +226,46 @@ class Environment(object):
return sys.path
cmd_args = [self.python, "-c", "import json, sys; print(json.dumps(sys.path))"]
path, _ = vistir.misc.run(cmd_args, return_object=False, nospin=True, block=True, combine_stderr=False, write_to_stdout=False)
path = json.loads(path.strip())
try:
path = json.loads(path.strip())
except JSONDecodeError:
path = sys.path
return path
def get_paths(self):
"""
Get the paths for the environment by running a subcommand
:return: The python paths for the environment
:rtype: Dict[str, str]
"""
prefix = make_posix(self.prefix.as_posix())
install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
py_command = (
"import sysconfig, json, distutils.sysconfig;"
"paths = sysconfig.get_paths('{0}', vars={{'base': '{1}', 'platbase': '{1}'}}"
");paths['purelib'] = distutils.sysconfig.get_python_lib(plat_specific=0, "
"prefix='{1}');paths['platlib'] = distutils.sysconfig.get_python_lib("
"plat_specific=1, prefix='{1}');print(json.dumps(paths))"
)
vistir.misc.echo("command: {0}".format(py_command.format(install_scheme, prefix)), fg="white", style="bold", err=True)
command = [self.python, "-c", py_command.format(install_scheme, prefix)]
c = vistir.misc.run(
command, return_object=True, block=True, nospin=True, write_to_stdout=False
)
paths = json.loads(vistir.misc.to_text(c.out.strip()))
return paths
@cached_property
def sys_prefix(self):
"""The prefix run inside the context of the environment
"""
The prefix run inside the context of the environment
:return: The python prefix inside the environment
:rtype: :data:`sys.prefix`
"""
command = [self.python, "-c" "import sys; print(sys.prefix)"]
command = [self.python, "-c", "import sys; print(sys.prefix)"]
c = vistir.misc.run(command, return_object=True, block=True, nospin=True, write_to_stdout=False)
sys_prefix = vistir.compat.Path(vistir.misc.to_text(c.out).strip()).as_posix()
return sys_prefix
@@ -236,15 +294,33 @@ class Environment(object):
return "purelib", purelib
return "platlib", self.paths["platlib"]
@property
def pip_version(self):
"""
Get the pip version in the environment. Useful for knowing which args we can use
when installing.
"""
from .vendor.packaging.version import parse as parse_version
pip = next(iter(
pkg for pkg in self.get_installed_packages() if pkg.key == "pip"
), None)
if pip is not None:
return parse_version(pip.version)
return parse_version("18.0")
def get_distributions(self):
"""Retrives the distributions installed on the library path of the environment
"""
Retrives the distributions installed on the library path of the environment
:return: A set of distributions found on the library path
:rtype: iterator
"""
pkg_resources = self.safe_import("pkg_resources")
return pkg_resources.find_distributions(self.paths["PYTHONPATH"])
libdirs = self.base_paths["libdirs"].split(os.pathsep)
dists = (pkg_resources.find_distributions(libdir) for libdir in libdirs)
for dist in itertools.chain.from_iterable(dists):
yield dist
def find_egg(self, egg_dist):
"""Find an egg by name in the given environment"""
@@ -271,21 +347,28 @@ class Environment(object):
def dist_is_in_project(self, dist):
"""Determine whether the supplied distribution is in the environment."""
from .project import _normalized
prefix = _normalized(self.base_paths["prefix"])
prefixes = [
_normalized(prefix) for prefix in self.base_paths["libdirs"].split(os.pathsep)
if _normalized(prefix).startswith(_normalized(self.prefix.as_posix()))
]
location = self.locate_dist(dist)
if not location:
return False
return _normalized(location).startswith(prefix)
location = _normalized(make_posix(location))
return any(location.startswith(prefix) for prefix in prefixes)
def get_installed_packages(self):
"""Returns all of the installed packages in a given environment"""
workingset = self.get_working_set()
packages = [pkg for pkg in workingset if self.dist_is_in_project(pkg)]
packages = [
pkg for pkg in workingset
if self.dist_is_in_project(pkg) and pkg.key != "python"
]
return packages
@contextlib.contextmanager
def get_finder(self, pre=False):
from .vendor.pip_shims import Command, cmdoptions, index_group, PackageFinder
from .vendor.pip_shims.shims import Command, cmdoptions, index_group, PackageFinder
from .environments import PIPENV_CACHE_DIR
index_urls = [source.get("url") for source in self.sources]
@@ -353,33 +436,86 @@ class Environment(object):
if pkg.latest_version._version > pkg.parsed_version._version
]
def get_package_requirements(self):
from .vendor.pipdeptree import flatten, sorted_tree, build_dist_index, construct_tree
dist_index = build_dist_index(self.get_installed_packages())
tree = sorted_tree(construct_tree(dist_index))
branch_keys = set(r.key for r in flatten(tree.values()))
nodes = [p for p in tree.keys() if p.key not in branch_keys]
key_tree = dict((k.key, v) for k, v in tree.items())
@classmethod
def _get_requirements_for_package(cls, node, key_tree, parent=None, chain=None):
if chain is None:
chain = [node.project_name]
d = node.as_dict()
if parent:
d['required_version'] = node.version_spec if node.version_spec else 'Any'
else:
d['required_version'] = d['installed_version']
get_children = lambda n: key_tree.get(n.key, [])
def aux(node, parent=None, chain=None):
if chain is None:
chain = [node.project_name]
d['dependencies'] = [
cls._get_requirements_for_package(c, key_tree, parent=node,
chain=chain+[c.project_name])
for c in get_children(node)
if c.project_name not in chain
]
d = node.as_dict()
if parent:
d['required_version'] = node.version_spec if node.version_spec else 'Any'
else:
d['required_version'] = d['installed_version']
return d
d['dependencies'] = [
aux(c, parent=node, chain=chain+[c.project_name])
for c in get_children(node)
if c.project_name not in chain
]
def get_package_requirements(self, pkg=None):
from .vendor.pipdeptree import flatten, sorted_tree, build_dist_index, construct_tree
packages = self.get_installed_packages()
if pkg:
packages = [p for p in packages if p.key == pkg]
dist_index = build_dist_index(packages)
tree = sorted_tree(construct_tree(dist_index))
branch_keys = set(r.key for r in flatten(tree.values()))
if pkg is not None:
nodes = [p for p in tree.keys() if p.key == pkg]
else:
nodes = [p for p in tree.keys() if p.key not in branch_keys]
key_tree = dict((k.key, v) for k, v in tree.items())
return d
return [aux(p) for p in nodes]
return [self._get_requirements_for_package(p, key_tree) for p in nodes]
@classmethod
def reverse_dependency(cls, node):
new_node = {
"package_name": node["package_name"],
"installed_version": node["installed_version"],
"required_version": node["required_version"]
}
for dependency in node.get("dependencies", []):
for dep in cls.reverse_dependency(dependency):
new_dep = dep.copy()
new_dep["parent"] = (node["package_name"], node["installed_version"])
yield new_dep
yield new_node
def reverse_dependencies(self):
from vistir.misc import unnest, chunked
rdeps = {}
for req in self.get_package_requirements():
for d in self.reverse_dependency(req):
parents = None
name = d["package_name"]
pkg = {
name: {
"installed": d["installed_version"],
"required": d["required_version"]
}
}
parents = tuple(d.get("parent", ()))
pkg[name]["parents"] = parents
if rdeps.get(name):
if not (rdeps[name].get("required") or rdeps[name].get("installed")):
rdeps[name].update(pkg[name])
rdeps[name]["parents"] = rdeps[name].get("parents", ()) + parents
else:
rdeps[name] = pkg[name]
for k in list(rdeps.keys()):
entry = rdeps[k]
if entry.get("parents"):
rdeps[k]["parents"] = set([
p for p, version in chunked(2, unnest(entry["parents"]))
])
return rdeps
def get_working_set(self):
"""Retrieve the working set of installed packages for the environment.
@@ -475,21 +611,34 @@ class Environment(object):
vendor_dir = parent_path.joinpath("vendor").as_posix()
patched_dir = parent_path.joinpath("patched").as_posix()
parent_path = parent_path.as_posix()
self.add_dist("pip")
prefix = self.prefix.as_posix()
with vistir.contextmanagers.temp_environ(), vistir.contextmanagers.temp_path():
os.environ["PATH"] = os.pathsep.join([
vistir.compat.fs_str(self.scripts_dir),
vistir.compat.fs_str(self.script_basedir),
vistir.compat.fs_str(self.prefix.as_posix()),
os.environ.get("PATH", "")
])
os.environ["PYTHONIOENCODING"] = vistir.compat.fs_str("utf-8")
os.environ["PYTHONDONTWRITEBYTECODE"] = vistir.compat.fs_str("1")
os.environ["PYTHONPATH"] = self.base_paths["PYTHONPATH"]
from .environments import PIPENV_USE_SYSTEM
if self.is_venv:
os.environ["PYTHONPATH"] = self.base_paths["PYTHONPATH"]
os.environ["VIRTUAL_ENV"] = vistir.compat.fs_str(prefix)
else:
if not PIPENV_USE_SYSTEM and not os.environ.get("VIRTUAL_ENV"):
os.environ["PYTHONPATH"] = self.base_paths["PYTHONPATH"]
os.environ.pop("PYTHONHOME", None)
sys.path = self.sys_path
sys.prefix = self.sys_prefix
site.addsitedir(self.base_paths["purelib"])
pip = self.safe_import("pip")
pip_vendor = self.safe_import("pip._vendor")
pep517_dir = os.path.join(os.path.dirname(pip_vendor.__file__), "pep517")
site.addsitedir(pep517_dir)
os.environ["PYTHONPATH"] = os.pathsep.join([
os.environ.get("PYTHONPATH", self.base_paths["PYTHONPATH"]), pep517_dir
])
if include_extras:
site.addsitedir(parent_path)
sys.path.extend([parent_path, patched_dir, vendor_dir])
@@ -593,10 +742,7 @@ class Environment(object):
monkey_patch.activate()
pip_shims = self.safe_import("pip_shims")
pathset_base = pip_shims.UninstallPathSet
import recursive_monkey_patch
recursive_monkey_patch.monkey_patch(
PatchedUninstaller, pathset_base
)
pathset_base._permitted = PatchedUninstaller._permitted
dist = next(
iter(filter(lambda d: d.project_name == pkgname, self.get_working_set())),
None
+48 -10
View File
@@ -3,16 +3,27 @@
import os
import sys
from io import UnsupportedOperation
from appdirs import user_cache_dir
from ._compat import fix_utf8
from .vendor.vistir.misc import fs_str
from .vendor.vistir.misc import _isatty, fs_str
# HACK: avoid resolver.py uses the wrong byte code files.
# I hope I can remove this one day.
os.environ["PYTHONDONTWRITEBYTECODE"] = fs_str("1")
def _is_env_truthy(name):
"""An environment variable is truthy if it exists and isn't one of (0, false, no, off)
"""
if name not in os.environ:
return False
return os.environ.get(name).lower() not in ("0", "false", "no", "off")
PIPENV_IS_CI = bool("CI" in os.environ or "TF_BUILD" in os.environ)
# HACK: Prevent invalid shebangs with Homebrew-installed Python:
@@ -70,13 +81,15 @@ Default is to detect emulators automatically. This should be set if your
emulator, e.g. Cmder, cannot be detected correctly.
"""
PIPENV_HIDE_EMOJIS = bool(os.environ.get("PIPENV_HIDE_EMOJIS"))
PIPENV_HIDE_EMOJIS = (
os.environ.get("PIPENV_HIDE_EMOJIS") is None
and (os.name == "nt" or PIPENV_IS_CI)
or _is_env_truthy("PIPENV_HIDE_EMOJIS")
)
"""Disable emojis in output.
Default is to show emojis. This is automatically set on Windows.
"""
if os.name == "nt" or PIPENV_IS_CI:
PIPENV_HIDE_EMOJIS = True
PIPENV_IGNORE_VIRTUALENVS = bool(os.environ.get("PIPENV_IGNORE_VIRTUALENVS"))
"""If set, Pipenv will always assign a virtual environment for this project.
@@ -252,7 +265,10 @@ PIPENV_SHELL = (
)
# Internal, to tell whether the command line session is interactive.
SESSION_IS_INTERACTIVE = bool(os.isatty(sys.stdout.fileno()))
try:
SESSION_IS_INTERACTIVE = _isatty(sys.stdout.fileno())
except UnsupportedOperation:
SESSION_IS_INTERACTIVE = _isatty(sys.stdout)
# Internal, consolidated verbosity representation as an integer. The default
@@ -280,13 +296,35 @@ def is_quiet(threshold=-1):
def is_in_virtualenv():
pipenv_active = os.environ.get("PIPENV_ACTIVE")
virtual_env = os.environ.get("VIRTUAL_ENV")
return (PIPENV_USE_SYSTEM or virtual_env) and not (
pipenv_active or PIPENV_IGNORE_VIRTUALENVS
)
"""
Check virtualenv membership dynamically
:return: True or false depending on whether we are in a regular virtualenv or not
:rtype: bool
"""
pipenv_active = os.environ.get("PIPENV_ACTIVE", False)
virtual_env = None
use_system = False
ignore_virtualenvs = bool(os.environ.get("PIPENV_IGNORE_VIRTUALENVS", False))
if not pipenv_active and not ignore_virtualenvs:
virtual_env = os.environ.get("VIRTUAL_ENV")
use_system = bool(virtual_env)
return (use_system or virtual_env) and not (pipenv_active or ignore_virtualenvs)
PIPENV_SPINNER_FAIL_TEXT = fix_utf8(u"{0}") if not PIPENV_HIDE_EMOJIS else ("{0}")
PIPENV_SPINNER_OK_TEXT = fix_utf8(u"{0}") if not PIPENV_HIDE_EMOJIS else ("{0}")
def is_type_checking():
try:
from typing import TYPE_CHECKING
except ImportError:
return False
return TYPE_CHECKING
MYPY_RUNNING = is_type_checking()
+217 -53
View File
@@ -1,42 +1,67 @@
# -*- coding=utf-8 -*-
import itertools
import re
import sys
from pprint import pformat
from traceback import format_exception
from collections import namedtuple
from traceback import format_tb
import six
from . import environments
from ._compat import fix_utf8
from ._compat import decode_for_output
from .patched import crayons
from .vendor.click._compat import get_text_stderr
from .vendor.click.exceptions import (
Abort, BadOptionUsage, BadParameter, ClickException, Exit, FileError,
MissingParameter, UsageError
ClickException, FileError, UsageError
)
from .vendor.click.types import Path
from .vendor.click.utils import echo as click_echo
from .vendor.vistir.misc import echo as click_echo
import vistir
ANSI_REMOVAL_RE = re.compile(r"\033\[((?:\d|;)*)([a-zA-Z])", re.MULTILINE)
STRING_TYPES = (six.string_types, crayons.ColoredString)
if sys.version_info[:2] >= (3, 7):
KnownException = namedtuple(
'KnownException', ['exception_name', 'match_string', 'show_from_string', 'prefix'],
defaults=[None, None, None, ""]
)
else:
KnownException = namedtuple(
'KnownException', ['exception_name', 'match_string', 'show_from_string', 'prefix'],
)
KnownException.__new__.__defaults__ = (None, None, None, "")
KNOWN_EXCEPTIONS = [
KnownException("PermissionError", prefix="Permission Denied:"),
KnownException(
"VirtualenvCreationException",
match_string="do_create_virtualenv",
show_from_string=None
)
]
def handle_exception(exc_type, exception, traceback, hook=sys.excepthook):
if environments.is_verbose() or not issubclass(exc_type, ClickException):
hook(exc_type, exception, traceback)
else:
exc = format_exception(exc_type, exception, traceback)
lines = itertools.chain.from_iterable([l.splitlines() for l in exc])
lines = list(lines)[-11:-1]
tb = format_tb(traceback, limit=-6)
lines = itertools.chain.from_iterable([frame.splitlines() for frame in tb])
formatted_lines = []
for line in lines:
line = line.strip("'").strip('"').strip("\n").strip()
if not line.startswith("File"):
line = " {0}".format(line)
else:
line = " {0}".format(line)
line = "[pipenv.exceptions.{0!s}]: {1}".format(
line = "[{0!s}]: {1}".format(
exception.__class__.__name__, line
)
click_echo(fix_utf8(line), err=True)
formatted_lines.append(line)
# use new exception prettification rules to format exceptions according to
# UX rules
click_echo(decode_for_output(prettify_exc("\n".join(formatted_lines))), err=True)
exception.show()
@@ -56,16 +81,65 @@ class PipenvException(ClickException):
def show(self, file=None):
if file is None:
file = get_text_stderr()
file = vistir.misc.get_text_stderr()
if self.extra:
if isinstance(self.extra, six.string_types):
if isinstance(self.extra, STRING_TYPES):
self.extra = [self.extra,]
for extra in self.extra:
extra = "[pipenv.exceptions.{0!s}]: {1}".format(
self.__class__.__name__, extra
)
extra = decode_for_output(extra, file)
click_echo(extra, file=file)
click_echo(fix_utf8("{0}".format(self.message)), file=file)
click_echo(decode_for_output("{0}".format(self.message), file), file=file)
class PipenvCmdError(PipenvException):
def __init__(self, cmd, out="", err="", exit_code=1):
self.cmd = cmd
self.out = out
self.err = err
self.exit_code = exit_code
message = "Error running command: {0}".format(cmd)
PipenvException.__init__(self, message)
def show(self, file=None):
if file is None:
file = vistir.misc.get_text_stderr()
click_echo("{0} {1}".format(
crayons.red("Error running command: "),
crayons.white(decode_for_output("$ {0}".format(self.cmd), file), bold=True)
), err=True)
if self.out:
click_echo("{0} {1}".format(
crayons.white("OUTPUT: "),
decode_for_output(self.out, file)
), err=True)
if self.err:
click_echo("{0} {1}".format(
crayons.white("STDERR: "),
decode_for_output(self.err, file)
), err=True)
class JSONParseError(PipenvException):
def __init__(self, contents="", error_text=""):
self.error_text = error_text
PipenvException.__init__(self, contents)
def show(self, file=None):
if file is None:
file = vistir.misc.get_text_stderr()
message = "{0}\n{1}".format(
crayons.white("Failed parsing JSON results:", bold=True),
decode_for_output(self.message.strip(), file)
)
click_echo(message, err=True)
if self.error_text:
click_echo("{0} {1}".format(
crayons.white("ERROR TEXT:", bold=True),
decode_for_output(self.error_text, file)
), err=True)
class PipenvUsageError(UsageError):
@@ -78,22 +152,22 @@ class PipenvUsageError(UsageError):
message = formatted_message.format(msg_prefix, crayons.white(message, bold=True))
self.message = message
extra = kwargs.pop("extra", [])
UsageError.__init__(self, fix_utf8(message), ctx)
UsageError.__init__(self, decode_for_output(message), ctx)
self.extra = extra
def show(self, file=None):
if file is None:
file = get_text_stderr()
file = vistir.misc.get_text_stderr()
color = None
if self.ctx is not None:
color = self.ctx.color
if self.extra:
if isinstance(self.extra, six.string_types):
if isinstance(self.extra, STRING_TYPES):
self.extra = [self.extra,]
for extra in self.extra:
if color:
extra = getattr(crayons, color, "blue")(extra)
click_echo(fix_utf8(extra), file=file)
click_echo(decode_for_output(extra, file), file=file)
hint = ''
if (self.cmd is not None and
self.cmd.get_help_option(self.ctx) is not None):
@@ -117,30 +191,33 @@ class PipenvFileError(FileError):
crayons.white("{0} not found!".format(filename), bold=True),
message
)
FileError.__init__(self, filename=filename, hint=fix_utf8(message), **kwargs)
FileError.__init__(self, filename=filename, hint=decode_for_output(message), **kwargs)
self.extra = extra
def show(self, file=None):
if file is None:
file = get_text_stderr()
file = vistir.misc.get_text_stderr()
if self.extra:
if isinstance(self.extra, six.string_types):
if isinstance(self.extra, STRING_TYPES):
self.extra = [self.extra,]
for extra in self.extra:
click_echo(fix_utf8(extra), file=file)
click_echo(decode_for_output(extra, file), file=file)
click_echo(self.message, file=file)
class PipfileNotFound(PipenvFileError):
def __init__(self, filename="Pipfile", extra=None, **kwargs):
extra = kwargs.pop("extra", [])
message = ("{0} {1}".format(
message = (
"{0} {1}".format(
crayons.red("Aborting!", bold=True),
crayons.white("Please ensure that the file exists and is located in your"
" project root directory.", bold=True)
crayons.white(
"Please ensure that the file exists and is located in your"
" project root directory.", bold=True
)
)
)
super(PipfileNotFound, self).__init__(filename, message=fix_utf8(message), extra=extra, **kwargs)
super(PipfileNotFound, self).__init__(filename, message=message, extra=extra, **kwargs)
class LockfileNotFound(PipenvFileError):
@@ -151,7 +228,7 @@ class LockfileNotFound(PipenvFileError):
crayons.red("$ pipenv lock", bold=True),
crayons.white("before you can continue.", bold=True)
)
super(LockfileNotFound, self).__init__(filename, message=fix_utf8(message), extra=extra, **kwargs)
super(LockfileNotFound, self).__init__(filename, message=message, extra=extra, **kwargs)
class DeployException(PipenvUsageError):
@@ -159,13 +236,13 @@ class DeployException(PipenvUsageError):
if not message:
message = crayons.normal("Aborting deploy", bold=True)
extra = kwargs.pop("extra", [])
PipenvUsageError.__init__(self, message=fix_utf8(message), extra=extra, **kwargs)
PipenvUsageError.__init__(self, message=message, extra=extra, **kwargs)
class PipenvOptionsError(PipenvUsageError):
def __init__(self, option_name, message=None, ctx=None, **kwargs):
extra = kwargs.pop("extra", [])
PipenvUsageError.__init__(self, message=fix_utf8(message), ctx=ctx, **kwargs)
PipenvUsageError.__init__(self, message=message, ctx=ctx, **kwargs)
self.extra = extra
self.option_name = option_name
@@ -179,7 +256,8 @@ class SystemUsageError(PipenvOptionsError):
crayons.red("Warning", bold=True)
),
]
message = crayons.blue("See also: {0}".format(crayons.white("-deploy flag.")))
if message is None:
message = crayons.blue("See also: {0}".format(crayons.white("--deploy flag.")))
super(SystemUsageError, self).__init__(option_name, message=message, ctx=ctx, extra=extra, **kwargs)
@@ -191,7 +269,7 @@ class PipfileException(PipenvFileError):
hint = "{0} {1}".format(crayons.red("ERROR (PACKAGE NOT INSTALLED):"), hint)
filename = project.pipfile_location
extra = kwargs.pop("extra", [])
PipenvFileError.__init__(self, filename, fix_utf8(hint), extra=extra, **kwargs)
PipenvFileError.__init__(self, filename, hint, extra=extra, **kwargs)
class SetupException(PipenvException):
@@ -207,7 +285,7 @@ class VirtualenvException(PipenvException):
"There was an unexpected error while activating your virtualenv. "
"Continuing anyway..."
)
PipenvException.__init__(self, fix_utf8(message), **kwargs)
PipenvException.__init__(self, message, **kwargs)
class VirtualenvActivationException(VirtualenvException):
@@ -218,7 +296,7 @@ class VirtualenvActivationException(VirtualenvException):
"not activated. Continuing anyway…"
)
self.message = message
VirtualenvException.__init__(self, fix_utf8(message), **kwargs)
VirtualenvException.__init__(self, message, **kwargs)
class VirtualenvCreationException(VirtualenvException):
@@ -226,45 +304,70 @@ class VirtualenvCreationException(VirtualenvException):
if not message:
message = "Failed to create virtual environment."
self.message = message
VirtualenvException.__init__(self, fix_utf8(message), **kwargs)
extra = kwargs.pop("extra", None)
if extra is not None and isinstance(extra, STRING_TYPES):
# note we need the format interpolation because ``crayons.ColoredString``
# is not an actual string type but is only a preparation for interpolation
# so replacement or parsing requires this step
extra = ANSI_REMOVAL_RE.sub("", "{0}".format(extra))
if "KeyboardInterrupt" in extra:
extra = crayons.red("Virtualenv creation interrupted by user", bold=True)
self.extra = extra = [extra,]
VirtualenvException.__init__(self, message, extra=extra)
class UninstallError(PipenvException):
def __init__(self, package, command, return_values, return_code, **kwargs):
extra = [crayons.blue("Attempted to run command: {0}".format(
crayons.yellow("$ {0}".format(command), bold=True)
)),]
extra = [
"{0} {1}".format(
crayons.blue("Attempted to run command: "),
crayons.yellow("$ {0!r}".format(command), bold=True
)
)]
extra.extend([crayons.blue(line.strip()) for line in return_values.splitlines()])
if isinstance(package, (tuple, list)):
if isinstance(package, (tuple, list, set)):
package = " ".join(package)
message = "{0} {1}...".format(
message = "{0!s} {1!s}...".format(
crayons.normal("Failed to uninstall package(s)"),
crayons.yellow(package, bold=True)
crayons.yellow("{0}!s".format(package), bold=True)
)
self.exit_code = return_code
PipenvException.__init__(self, message=fix_utf8(message), extra=extra)
PipenvException.__init__(self, message=message, extra=extra)
self.extra = extra
class InstallError(PipenvException):
def __init__(self, package, **kwargs):
package_message = ""
if package is not None:
package_message = "Couldn't install package: {0}\n".format(
crayons.white("{0!s}".format(package), bold=True)
)
message = "{0} {1}".format(
crayons.red("ERROR:", bold=True),
"{0}".format(package_message),
crayons.yellow("Package installation failed...")
)
extra = kwargs.pop("extra", [])
PipenvException.__init__(self, message=fix_utf8(message), extra=extra, **kwargs)
PipenvException.__init__(self, message=message, extra=extra, **kwargs)
class CacheError(PipenvException):
def __init__(self, path, **kwargs):
message = "{0} {1} {2}\n{0}".format(
crayons.red("ERROR:", bold=True),
message = "{0} {1}\n{2}".format(
crayons.blue("Corrupt cache file"),
crayons.white(path),
crayons.white("{0!s}".format(path)),
crayons.white('Consider trying "pipenv lock --clear" to clear the cache.')
)
super(PipenvException, self).__init__(message=fix_utf8(message))
PipenvException.__init__(self, message=message)
class DependencyConflict(PipenvException):
def __init__(self, message):
extra = ["{0} {1}".format(
crayons.red("The operation failed...", bold=True),
crayons.red("A dependency conflict was detected and could not be resolved."),
)]
PipenvException.__init__(self, message, extra=extra)
class ResolutionFailure(PipenvException):
@@ -286,9 +389,7 @@ class ResolutionFailure(PipenvException):
)
if "no version found at all" in message:
no_version_found = True
message = "{0} {1}".format(
crayons.red("ERROR:", bold=True), crayons.yellow(message)
)
message = crayons.yellow("{0}".format(message))
if no_version_found:
message = "{0}\n{1}".format(
message,
@@ -297,4 +398,67 @@ class ResolutionFailure(PipenvException):
"See PEP440 for more information."
)
)
super(ResolutionFailure, self).__init__(fix_utf8(message), extra=extra)
PipenvException.__init__(self, message, extra=extra)
class RequirementError(PipenvException):
def __init__(self, req=None):
from .utils import VCS_LIST
keys = ("name", "path",) + VCS_LIST + ("line", "uri", "url", "relpath")
if req is not None:
possible_display_values = [getattr(req, value, None) for value in keys]
req_value = next(iter(
val for val in possible_display_values if val is not None
), None)
if not req_value:
getstate_fn = getattr(req, "__getstate__", None)
slots = getattr(req, "__slots__", None)
keys_fn = getattr(req, "keys", None)
if getstate_fn:
req_value = getstate_fn()
elif slots:
slot_vals = [
(k, getattr(req, k, None)) for k in slots
if getattr(req, k, None)
]
req_value = "\n".join([
" {0}: {1}".format(k, v) for k, v in slot_vals
])
elif keys_fn:
values = [(k, req.get(k)) for k in keys_fn() if req.get(k)]
req_value = "\n".join([
" {0}: {1}".format(k, v) for k, v in values
])
else:
req_value = getattr(req.line_instance, "line", None)
message = "{0} {1}".format(
crayons.normal(decode_for_output("Failed creating requirement instance")),
crayons.white(decode_for_output("{0!r}".format(req_value)))
)
extra = [str(req)]
PipenvException.__init__(self, message, extra=extra)
def prettify_exc(error):
"""Catch known errors and prettify them instead of showing the
entire traceback, for better UX"""
errors = []
for exc in KNOWN_EXCEPTIONS:
search_string = exc.match_string if exc.match_string else exc.exception_name
split_string = exc.show_from_string if exc.show_from_string else exc.exception_name
if search_string in error:
# for known exceptions with no display rules and no prefix
# we should simply show nothing
if not exc.show_from_string and not exc.prefix:
errors.append("")
continue
elif exc.prefix and exc.prefix in error:
_, error, info = error.rpartition(exc.prefix)
else:
_, error, info = error.rpartition(split_string)
errors.append("{0} {1}".format(error, info))
if not errors:
return "{}".format(vistir.misc.decode_for_output(error))
return "\n".join(errors)
+14
View File
@@ -0,0 +1,14 @@
Copyright (C) 2008-2011 INADA Naoki <songofacandy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+23
View File
@@ -0,0 +1,23 @@
Copyright (c) Donald Stufft and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+1 -1
View File
@@ -1 +1 @@
__version__ = "18.1"
__version__ = "19.0.3"
+128 -55
View File
@@ -4,98 +4,173 @@
import logging
import os
import sys
import textwrap
from collections import OrderedDict
from distutils.sysconfig import get_python_lib
from sysconfig import get_paths
from pipenv.patched.notpip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet
from pipenv.patched.notpip import __file__ as pip_location
from pipenv.patched.notpip._internal.utils.misc import call_subprocess
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.utils.ui import open_spinner
if MYPY_CHECK_RUNNING:
from typing import Tuple, Set, Iterable, Optional, List # noqa: F401
from pipenv.patched.notpip._internal.index import PackageFinder # noqa: F401
logger = logging.getLogger(__name__)
class _Prefix:
def __init__(self, path):
# type: (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']
# Note: prefer distutils' sysconfig to get the
# library paths so PyPy is correctly supported.
purelib = get_python_lib(plat_specific=False, prefix=path)
platlib = get_python_lib(plat_specific=True, prefix=path)
if purelib == platlib:
self.lib_dirs = [purelib]
else:
self.lib_dirs = [purelib, platlib]
class BuildEnvironment(object):
"""Creates and manages an isolated environment to install build deps
"""
def __init__(self):
# type: () -> None
self._temp_dir = TempDirectory(kind="build-env")
self._temp_dir.create()
@property
def path(self):
return self._temp_dir.path
self._prefixes = OrderedDict((
(name, _Prefix(os.path.join(self._temp_dir.path, name)))
for name in ('normal', 'overlay')
))
self._bin_dirs = [] # type: List[str]
self._lib_dirs = [] # type: List[str]
for prefix in reversed(list(self._prefixes.values())):
self._bin_dirs.append(prefix.bin_dir)
self._lib_dirs.extend(prefix.lib_dirs)
# Customize site to:
# - ensure .pth files are honored
# - prevent access to system site packages
system_sites = {
os.path.normcase(site) for site in (
get_python_lib(plat_specific=False),
get_python_lib(plat_specific=True),
)
}
self._site_dir = os.path.join(self._temp_dir.path, 'site')
if not os.path.exists(self._site_dir):
os.mkdir(self._site_dir)
with open(os.path.join(self._site_dir, 'sitecustomize.py'), 'w') as fp:
fp.write(textwrap.dedent(
'''
import os, site, sys
# First, drop system-sites related paths.
original_sys_path = sys.path[:]
known_paths = set()
for path in {system_sites!r}:
site.addsitedir(path, known_paths=known_paths)
system_paths = set(
os.path.normcase(path)
for path in sys.path[len(original_sys_path):]
)
original_sys_path = [
path for path in original_sys_path
if os.path.normcase(path) not in system_paths
]
sys.path = original_sys_path
# Second, add lib directories.
# ensuring .pth file are processed.
for path in {lib_dirs!r}:
assert not path in sys.path
site.addsitedir(path)
'''
).format(system_sites=system_sites, lib_dirs=self._lib_dirs))
def __enter__(self):
self.save_path = os.environ.get('PATH', None)
self.save_pythonpath = os.environ.get('PYTHONPATH', None)
self.save_nousersite = os.environ.get('PYTHONNOUSERSITE', None)
self._save_env = {
name: os.environ.get(name, None)
for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH')
}
install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
install_dirs = get_paths(install_scheme, vars={
'base': self.path,
'platbase': self.path,
path = self._bin_dirs[:]
old_path = self._save_env['PATH']
if old_path:
path.extend(old_path.split(os.pathsep))
pythonpath = [self._site_dir]
os.environ.update({
'PATH': os.pathsep.join(path),
'PYTHONNOUSERSITE': '1',
'PYTHONPATH': os.pathsep.join(pythonpath),
})
scripts = install_dirs['scripts']
if self.save_path:
os.environ['PATH'] = scripts + os.pathsep + self.save_path
else:
os.environ['PATH'] = scripts + os.pathsep + os.defpath
# Note: prefer distutils' sysconfig to get the
# library paths so PyPy is correctly supported.
purelib = get_python_lib(plat_specific=0, prefix=self.path)
platlib = get_python_lib(plat_specific=1, prefix=self.path)
if purelib == platlib:
lib_dirs = purelib
else:
lib_dirs = purelib + os.pathsep + platlib
if self.save_pythonpath:
os.environ['PYTHONPATH'] = lib_dirs + os.pathsep + \
self.save_pythonpath
else:
os.environ['PYTHONPATH'] = lib_dirs
os.environ['PYTHONNOUSERSITE'] = '1'
return self.path
def __exit__(self, exc_type, exc_val, exc_tb):
def restore_var(varname, old_value):
for varname, old_value in self._save_env.items():
if old_value is None:
os.environ.pop(varname, None)
else:
os.environ[varname] = old_value
restore_var('PATH', self.save_path)
restore_var('PYTHONPATH', self.save_pythonpath)
restore_var('PYTHONNOUSERSITE', self.save_nousersite)
def cleanup(self):
# type: () -> None
self._temp_dir.cleanup()
def missing_requirements(self, reqs):
"""Return a list of the requirements from reqs that are not present
def check_requirements(self, reqs):
# type: (Iterable[str]) -> Tuple[Set[Tuple[str, str]], Set[str]]
"""Return 2 sets:
- conflicting requirements: set of (installed, wanted) reqs tuples
- missing requirements: set of reqs
"""
missing = []
with self:
ws = WorkingSet(os.environ["PYTHONPATH"].split(os.pathsep))
missing = set()
conflicting = set()
if reqs:
ws = WorkingSet(self._lib_dirs)
for req in reqs:
try:
if ws.find(Requirement.parse(req)) is None:
missing.append(req)
except VersionConflict:
missing.append(req)
return missing
missing.add(req)
except VersionConflict as e:
conflicting.add((str(e.args[0].as_requirement()),
str(e.args[1])))
return conflicting, missing
def install_requirements(self, finder, requirements, message):
def install_requirements(
self,
finder, # type: PackageFinder
requirements, # type: Iterable[str]
prefix_as_string, # type: str
message # type: Optional[str]
):
# type: (...) -> None
prefix = self._prefixes[prefix_as_string]
assert not prefix.setup
prefix.setup = True
if not requirements:
return
args = [
sys.executable, '-m', 'pip', 'install', '--ignore-installed',
'--no-user', '--prefix', self.path, '--no-warn-script-location',
]
sys.executable, os.path.dirname(pip_location), 'install',
'--ignore-installed', '--no-user', '--prefix', prefix.path,
'--no-warn-script-location',
] # type: List[str]
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append('-v')
for format_control in ('no_binary', 'only_binary'):
@@ -114,8 +189,6 @@ class BuildEnvironment(object):
args.extend(['--trusted-host', host])
if finder.allow_all_prereleases:
args.append('--pre')
if finder.process_dependency_links:
args.append('--process-dependency-links')
args.append('--')
args.extend(requirements)
with open_spinner(message) as spinner:
@@ -138,5 +211,5 @@ class NoOpBuildEnvironment(BuildEnvironment):
def cleanup(self):
pass
def install_requirements(self, finder, requirements, message):
def install_requirements(self, finder, requirements, prefix, message):
raise NotImplementedError()
+22
View File
@@ -12,8 +12,13 @@ from pipenv.patched.notpip._internal.download import path_to_url
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.utils.compat import expanduser
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.wheel import InvalidWheelFilename, Wheel
if MYPY_CHECK_RUNNING:
from typing import Optional, Set, List, Any # noqa: F401
from pipenv.patched.notpip._internal.index import FormatControl # noqa: F401
logger = logging.getLogger(__name__)
@@ -29,6 +34,7 @@ class Cache(object):
"""
def __init__(self, cache_dir, format_control, allowed_formats):
# type: (str, FormatControl, Set[str]) -> None
super(Cache, self).__init__()
self.cache_dir = expanduser(cache_dir) if cache_dir else None
self.format_control = format_control
@@ -38,6 +44,7 @@ class Cache(object):
assert self.allowed_formats.union(_valid_formats) == _valid_formats
def _get_cache_path_parts(self, link):
# type: (Link) -> List[str]
"""Get parts of part that must be os.path.joined with cache_dir
"""
@@ -63,6 +70,7 @@ class Cache(object):
return parts
def _get_candidates(self, link, package_name):
# type: (Link, Optional[str]) -> List[Any]
can_not_cache = (
not self.cache_dir or
not package_name or
@@ -87,23 +95,27 @@ class Cache(object):
raise
def get_path_for_link(self, link):
# type: (Link) -> str
"""Return a directory to store cached items in for link.
"""
raise NotImplementedError()
def get(self, link, package_name):
# type: (Link, Optional[str]) -> Link
"""Returns a link to a cached item if it exists, otherwise returns the
passed link.
"""
raise NotImplementedError()
def _link_for_candidate(self, link, candidate):
# type: (Link, str) -> Link
root = self.get_path_for_link(link)
path = os.path.join(root, candidate)
return Link(path_to_url(path))
def cleanup(self):
# type: () -> None
pass
@@ -112,11 +124,13 @@ class SimpleWheelCache(Cache):
"""
def __init__(self, cache_dir, format_control):
# type: (str, FormatControl) -> None
super(SimpleWheelCache, self).__init__(
cache_dir, format_control, {"binary"}
)
def get_path_for_link(self, link):
# type: (Link) -> str
"""Return a directory to store cached wheels for link
Because there are M wheels for any one sdist, we provide a directory
@@ -137,6 +151,7 @@ class SimpleWheelCache(Cache):
return os.path.join(self.cache_dir, "wheels", *parts)
def get(self, link, package_name):
# type: (Link, Optional[str]) -> Link
candidates = []
for wheel_name in self._get_candidates(link, package_name):
@@ -160,6 +175,7 @@ class EphemWheelCache(SimpleWheelCache):
"""
def __init__(self, format_control):
# type: (FormatControl) -> None
self._temp_dir = TempDirectory(kind="ephem-wheel-cache")
self._temp_dir.create()
@@ -168,6 +184,7 @@ class EphemWheelCache(SimpleWheelCache):
)
def cleanup(self):
# type: () -> None
self._temp_dir.cleanup()
@@ -179,6 +196,7 @@ class WheelCache(Cache):
"""
def __init__(self, cache_dir, format_control):
# type: (str, FormatControl) -> None
super(WheelCache, self).__init__(
cache_dir, format_control, {'binary'}
)
@@ -186,17 +204,21 @@ class WheelCache(Cache):
self._ephem_cache = EphemWheelCache(format_control)
def get_path_for_link(self, link):
# type: (Link) -> str
return self._wheel_cache.get_path_for_link(link)
def get_ephem_path_for_link(self, link):
# type: (Link) -> str
return self._ephem_cache.get_path_for_link(link)
def get(self, link, package_name):
# type: (Link, Optional[str]) -> Link
retval = self._wheel_cache.get(link, package_name)
if retval is link:
retval = self._ephem_cache.get(link, package_name)
return retval
def cleanup(self):
# type: () -> None
self._wheel_cache.cleanup()
self._ephem_cache.cleanup()
@@ -1,11 +1,13 @@
"""Base Command class, and related routines"""
from __future__ import absolute_import
from __future__ import absolute_import, print_function
import logging
import logging.config
import optparse
import os
import platform
import sys
import traceback
from pipenv.patched.notpip._internal.cli import cmdoptions
from pipenv.patched.notpip._internal.cli.parser import (
@@ -26,13 +28,19 @@ from pipenv.patched.notpip._internal.req.constructors import (
install_req_from_editable, install_req_from_line,
)
from pipenv.patched.notpip._internal.req.req_file import parse_requirements
from pipenv.patched.notpip._internal.utils.logging import setup_logging
from pipenv.patched.notpip._internal.utils.misc import get_prog, normalize_path
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
from pipenv.patched.notpip._internal.utils.misc import (
get_prog, normalize_path, redact_password_from_url,
)
from pipenv.patched.notpip._internal.utils.outdated import pip_version_check
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Optional # noqa: F401
from typing import Optional, List, Tuple, Any # noqa: F401
from optparse import Values # noqa: F401
from pipenv.patched.notpip._internal.cache import WheelCache # noqa: F401
from pipenv.patched.notpip._internal.req.req_set import RequirementSet # noqa: F401
__all__ = ['Command']
@@ -46,6 +54,7 @@ class Command(object):
ignore_require_venv = False # type: bool
def __init__(self, isolated=False):
# type: (bool) -> None
parser_kw = {
'usage': self.usage,
'prog': '%s %s' % (get_prog(), self.name),
@@ -69,7 +78,12 @@ class Command(object):
)
self.parser.add_option_group(gen_opts)
def run(self, options, args):
# type: (Values, List[Any]) -> Any
raise NotImplementedError
def _build_session(self, options, retries=None, timeout=None):
# type: (Values, Optional[int], Optional[int]) -> PipSession
session = PipSession(
cache=(
normalize_path(os.path.join(options.cache_dir, "http"))
@@ -106,21 +120,43 @@ class Command(object):
return session
def parse_args(self, args):
# type: (List[str]) -> Tuple
# factored out for testability
return self.parser.parse_args(args)
def main(self, args):
# type: (List[str]) -> int
options, args = self.parse_args(args)
# Set verbosity so that it can be used elsewhere.
self.verbosity = options.verbose - options.quiet
setup_logging(
level_number = setup_logging(
verbosity=self.verbosity,
no_color=options.no_color,
user_log_file=options.log,
)
if sys.version_info[:2] == (3, 4):
deprecated(
"Python 3.4 support has been deprecated. pip 19.1 will be the "
"last one supporting it. Please upgrade your Python as Python "
"3.4 won't be maintained after March 2019 (cf PEP 429).",
replacement=None,
gone_in='19.2',
)
elif sys.version_info[:2] == (2, 7):
message = (
"A future version of pip will drop support for Python 2.7."
)
if platform.python_implementation() == "CPython":
message = (
"Python 2.7 will reach the end of its life on January "
"1st, 2020. Please upgrade your Python as Python 2.7 "
"won't be maintained after that date. "
) + message
deprecated(message, replacement=None, gone_in=None)
# TODO: Try to get these passing down from the command?
# without resorting to os.environ to hold these.
# This also affects isolated builds and it should.
@@ -159,6 +195,14 @@ class Command(object):
logger.critical('ERROR: %s', exc)
logger.debug('Exception information:', exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to stderr
# because stdout no longer works.
print('ERROR: Pipe to stdout was broken', file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical('Operation cancelled by user')
@@ -195,8 +239,15 @@ class Command(object):
class RequirementCommand(Command):
@staticmethod
def populate_requirement_set(requirement_set, args, options, finder,
session, name, wheel_cache):
def populate_requirement_set(requirement_set, # type: RequirementSet
args, # type: List[str]
options, # type: Values
finder, # type: PackageFinder
session, # type: PipSession
name, # type: str
wheel_cache # type: Optional[WheelCache]
):
# type: (...) -> None
"""
Marshal cmd line args into a requirement set.
"""
@@ -214,6 +265,7 @@ class RequirementCommand(Command):
for req in args:
req_to_add = install_req_from_line(
req, None, isolated=options.isolated_mode,
use_pep517=options.use_pep517,
wheel_cache=wheel_cache
)
req_to_add.is_direct = True
@@ -223,6 +275,7 @@ class RequirementCommand(Command):
req_to_add = install_req_from_editable(
req,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
wheel_cache=wheel_cache
)
req_to_add.is_direct = True
@@ -232,7 +285,8 @@ class RequirementCommand(Command):
for req_to_add in parse_requirements(
filename,
finder=finder, options=options, session=session,
wheel_cache=wheel_cache):
wheel_cache=wheel_cache,
use_pep517=options.use_pep517):
req_to_add.is_direct = True
requirement_set.add_requirement(req_to_add)
# If --require-hashes was a line in a requirements file, tell
@@ -251,15 +305,25 @@ class RequirementCommand(Command):
'You must give at least one requirement to %(name)s '
'(see "pip help %(name)s")' % opts)
def _build_package_finder(self, options, session,
platform=None, python_versions=None,
abi=None, implementation=None):
def _build_package_finder(
self,
options, # type: Values
session, # type: PipSession
platform=None, # type: Optional[str]
python_versions=None, # type: Optional[List[str]]
abi=None, # type: Optional[str]
implementation=None # type: Optional[str]
):
# type: (...) -> PackageFinder
"""
Create a package finder appropriate to this requirement command.
"""
index_urls = [options.index_url] + options.extra_index_urls
if options.no_index:
logger.debug('Ignoring indexes: %s', ','.join(index_urls))
logger.debug(
'Ignoring indexes: %s',
','.join(redact_password_from_url(url) for url in index_urls),
)
index_urls = []
return PackageFinder(
@@ -268,7 +332,6 @@ class RequirementCommand(Command):
index_urls=index_urls,
trusted_hosts=options.trusted_hosts,
allow_all_prereleases=options.pre,
process_dependency_links=options.process_dependency_links,
session=session,
platform=platform,
versions=python_versions,
+148 -53
View File
@@ -9,7 +9,9 @@ pass on state. To be consistent, all options will follow this design.
"""
from __future__ import absolute_import
import textwrap
import warnings
from distutils.util import strtobool
from functools import partial
from optparse import SUPPRESS_HELP, Option, OptionGroup
@@ -22,10 +24,27 @@ from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.utils.ui import BAR_TYPES
if MYPY_CHECK_RUNNING:
from typing import Any # noqa: F401
from typing import Any, Callable, Dict, List, Optional, Union # noqa: F401
from optparse import OptionParser, Values # noqa: F401
from pipenv.patched.notpip._internal.cli.parser import ConfigOptionParser # noqa: F401
def raise_option_error(parser, option, msg):
"""
Raise an option parsing error using parser.error().
Args:
parser: an OptionParser instance.
option: an Option instance.
msg: the error text.
"""
msg = '{} error: {}'.format(option, msg)
msg = textwrap.fill(' '.join(msg.split()))
parser.error(msg)
def make_option_group(group, parser):
# type: (Dict[str, Any], ConfigOptionParser) -> OptionGroup
"""
Return an OptionGroup object
group -- assumed to be dict with 'name' and 'options' keys
@@ -38,6 +57,7 @@ def make_option_group(group, parser):
def check_install_build_global(options, check_options=None):
# type: (Values, Optional[Values]) -> None
"""Disable wheels if per-setup.py call options are set.
:param options: The OptionParser options to update.
@@ -60,6 +80,7 @@ def check_install_build_global(options, check_options=None):
def check_dist_restriction(options, check_target=False):
# type: (Values, bool) -> None
"""Function for determining if custom platform options are allowed.
:param options: The OptionParser options.
@@ -108,7 +129,7 @@ help_ = partial(
dest='help',
action='help',
help='Show help.',
) # type: Any
) # type: Callable[..., Option]
isolated_mode = partial(
Option,
@@ -120,7 +141,7 @@ isolated_mode = partial(
"Run pip in an isolated mode, ignoring environment variables and user "
"configuration."
),
)
) # type: Callable[..., Option]
require_virtualenv = partial(
Option,
@@ -130,7 +151,7 @@ require_virtualenv = partial(
action='store_true',
default=False,
help=SUPPRESS_HELP
) # type: Any
) # type: Callable[..., Option]
verbose = partial(
Option,
@@ -139,7 +160,7 @@ verbose = partial(
action='count',
default=0,
help='Give more output. Option is additive, and can be used up to 3 times.'
)
) # type: Callable[..., Option]
no_color = partial(
Option,
@@ -148,7 +169,7 @@ no_color = partial(
action='store_true',
default=False,
help="Suppress colored output",
)
) # type: Callable[..., Option]
version = partial(
Option,
@@ -156,7 +177,7 @@ version = partial(
dest='version',
action='store_true',
help='Show version and exit.',
) # type: Any
) # type: Callable[..., Option]
quiet = partial(
Option,
@@ -169,7 +190,7 @@ quiet = partial(
' times (corresponding to WARNING, ERROR, and CRITICAL logging'
' levels).'
),
) # type: Any
) # type: Callable[..., Option]
progress_bar = partial(
Option,
@@ -182,7 +203,7 @@ progress_bar = partial(
'Specify type of progress to be displayed [' +
'|'.join(BAR_TYPES.keys()) + '] (default: %default)'
),
) # type: Any
) # type: Callable[..., Option]
log = partial(
Option,
@@ -190,7 +211,7 @@ log = partial(
dest="log",
metavar="path",
help="Path to a verbose appending log."
) # type: Any
) # type: Callable[..., Option]
no_input = partial(
Option,
@@ -200,7 +221,7 @@ no_input = partial(
action='store_true',
default=False,
help=SUPPRESS_HELP
) # type: Any
) # type: Callable[..., Option]
proxy = partial(
Option,
@@ -209,7 +230,7 @@ proxy = partial(
type='str',
default='',
help="Specify a proxy in the form [user:passwd@]proxy.server:port."
) # type: Any
) # type: Callable[..., Option]
retries = partial(
Option,
@@ -219,7 +240,7 @@ retries = partial(
default=5,
help="Maximum number of retries each connection should attempt "
"(default %default times).",
) # type: Any
) # type: Callable[..., Option]
timeout = partial(
Option,
@@ -229,7 +250,7 @@ timeout = partial(
type='float',
default=15,
help='Set the socket timeout (default %default seconds).',
) # type: Any
) # type: Callable[..., Option]
skip_requirements_regex = partial(
Option,
@@ -239,10 +260,11 @@ skip_requirements_regex = partial(
type='str',
default='',
help=SUPPRESS_HELP,
) # type: Any
) # type: Callable[..., Option]
def exists_action():
# type: () -> Option
return Option(
# Option when path already exist
'--exists-action',
@@ -264,7 +286,7 @@ cert = partial(
type='str',
metavar='path',
help="Path to alternate CA bundle.",
) # type: Any
) # type: Callable[..., Option]
client_cert = partial(
Option,
@@ -275,7 +297,7 @@ client_cert = partial(
metavar='path',
help="Path to SSL client certificate, a single file containing the "
"private key and the certificate in PEM format.",
) # type: Any
) # type: Callable[..., Option]
index_url = partial(
Option,
@@ -287,7 +309,7 @@ index_url = partial(
"This should point to a repository compliant with PEP 503 "
"(the simple repository API) or a local directory laid out "
"in the same format.",
) # type: Any
) # type: Callable[..., Option]
def extra_index_url():
@@ -310,10 +332,11 @@ no_index = partial(
action='store_true',
default=False,
help='Ignore package index (only looking at --find-links URLs instead).',
) # type: Any
) # type: Callable[..., Option]
def find_links():
# type: () -> Option
return Option(
'-f', '--find-links',
dest='find_links',
@@ -327,6 +350,7 @@ def find_links():
def trusted_host():
# type: () -> Option
return Option(
"--trusted-host",
dest="trusted_hosts",
@@ -338,18 +362,8 @@ def trusted_host():
)
# Remove after 1.5
process_dependency_links = partial(
Option,
"--process-dependency-links",
dest="process_dependency_links",
action="store_true",
default=False,
help="Enable the processing of dependency links.",
) # type: Any
def constraints():
# type: () -> Option
return Option(
'-c', '--constraint',
dest='constraints',
@@ -362,6 +376,7 @@ def constraints():
def requirements():
# type: () -> Option
return Option(
'-r', '--requirement',
dest='requirements',
@@ -374,6 +389,7 @@ def requirements():
def editable():
# type: () -> Option
return Option(
'-e', '--editable',
dest='editables',
@@ -394,15 +410,17 @@ src = partial(
help='Directory to check out editable projects into. '
'The default in a virtualenv is "<venv path>/src". '
'The default for global installs is "<current dir>/src".'
) # type: Any
) # type: Callable[..., Option]
def _get_format_control(values, option):
# type: (Values, Option) -> Any
"""Get a format_control object."""
return getattr(values, option.dest)
def _handle_no_binary(option, opt_str, value, parser):
# type: (Option, str, str, OptionParser) -> None
existing = _get_format_control(parser.values, option)
FormatControl.handle_mutual_excludes(
value, existing.no_binary, existing.only_binary,
@@ -410,6 +428,7 @@ def _handle_no_binary(option, opt_str, value, parser):
def _handle_only_binary(option, opt_str, value, parser):
# type: (Option, str, str, OptionParser) -> None
existing = _get_format_control(parser.values, option)
FormatControl.handle_mutual_excludes(
value, existing.only_binary, existing.no_binary,
@@ -417,6 +436,7 @@ def _handle_only_binary(option, opt_str, value, parser):
def no_binary():
# type: () -> Option
format_control = FormatControl(set(), set())
return Option(
"--no-binary", dest="format_control", action="callback",
@@ -432,6 +452,7 @@ def no_binary():
def only_binary():
# type: () -> Option
format_control = FormatControl(set(), set())
return Option(
"--only-binary", dest="format_control", action="callback",
@@ -454,7 +475,7 @@ platform = partial(
default=None,
help=("Only use wheels compatible with <platform>. "
"Defaults to the platform of the running system."),
)
) # type: Callable[..., Option]
python_version = partial(
@@ -469,7 +490,7 @@ python_version = partial(
"version (e.g. '2') can be specified to match all "
"minor revs of that major version. A minor version "
"(e.g. '34') can also be specified."),
)
) # type: Callable[..., Option]
implementation = partial(
@@ -483,7 +504,7 @@ implementation = partial(
" or 'ip'. If not specified, then the current "
"interpreter implementation is used. Use 'py' to force "
"implementation-agnostic wheels."),
)
) # type: Callable[..., Option]
abi = partial(
@@ -498,10 +519,11 @@ abi = partial(
"you will need to specify --implementation, "
"--platform, and --python-version when using "
"this option."),
)
) # type: Callable[..., Option]
def prefer_binary():
# type: () -> Option
return Option(
"--prefer-binary",
dest="prefer_binary",
@@ -518,15 +540,44 @@ cache_dir = partial(
default=USER_CACHE_DIR,
metavar="dir",
help="Store the cache data in <dir>."
)
) # type: Callable[..., Option]
def no_cache_dir_callback(option, opt, value, parser):
"""
Process a value provided for the --no-cache-dir option.
This is an optparse.Option callback for the --no-cache-dir option.
"""
# The value argument will be None if --no-cache-dir is passed via the
# command-line, since the option doesn't accept arguments. However,
# the value can be non-None if the option is triggered e.g. by an
# environment variable, like PIP_NO_CACHE_DIR=true.
if value is not None:
# Then parse the string value to get argument error-checking.
try:
strtobool(value)
except ValueError as exc:
raise_option_error(parser, option=option, msg=str(exc))
# Originally, setting PIP_NO_CACHE_DIR to a value that strtobool()
# converted to 0 (like "false" or "no") caused cache_dir to be disabled
# rather than enabled (logic would say the latter). Thus, we disable
# the cache directory not just on values that parse to True, but (for
# backwards compatibility reasons) also on values that parse to False.
# In other words, always set it to False if the option is provided in
# some (valid) form.
parser.values.cache_dir = False
no_cache = partial(
Option,
"--no-cache-dir",
dest="cache_dir",
action="store_false",
action="callback",
callback=no_cache_dir_callback,
help="Disable the cache.",
)
) # type: Callable[..., Option]
no_deps = partial(
Option,
@@ -535,7 +586,7 @@ no_deps = partial(
action='store_true',
default=False,
help="Don't install package dependencies.",
) # type: Any
) # type: Callable[..., Option]
build_dir = partial(
Option,
@@ -547,7 +598,7 @@ build_dir = partial(
'The location of temporary directories can be controlled by setting '
'the TMPDIR environment variable (TEMP on Windows) appropriately. '
'When passed, build directories are not cleaned in case of failures.'
) # type: Any
) # type: Callable[..., Option]
ignore_requires_python = partial(
Option,
@@ -555,7 +606,7 @@ ignore_requires_python = partial(
dest='ignore_requires_python',
action='store_true',
help='Ignore the Requires-Python information.'
) # type: Any
) # type: Callable[..., Option]
no_build_isolation = partial(
Option,
@@ -566,6 +617,50 @@ no_build_isolation = partial(
help='Disable isolation when building a modern source distribution. '
'Build dependencies specified by PEP 518 must be already installed '
'if this option is used.'
) # type: Callable[..., Option]
def no_use_pep517_callback(option, opt, value, parser):
"""
Process a value provided for the --no-use-pep517 option.
This is an optparse.Option callback for the no_use_pep517 option.
"""
# Since --no-use-pep517 doesn't accept arguments, the value argument
# will be None if --no-use-pep517 is passed via the command-line.
# However, the value can be non-None if the option is triggered e.g.
# by an environment variable, for example "PIP_NO_USE_PEP517=true".
if value is not None:
msg = """A value was passed for --no-use-pep517,
probably using either the PIP_NO_USE_PEP517 environment variable
or the "no-use-pep517" config file option. Use an appropriate value
of the PIP_USE_PEP517 environment variable or the "use-pep517"
config file option instead.
"""
raise_option_error(parser, option=option, msg=msg)
# Otherwise, --no-use-pep517 was passed via the command-line.
parser.values.use_pep517 = False
use_pep517 = partial(
Option,
'--use-pep517',
dest='use_pep517',
action='store_true',
default=None,
help='Use PEP 517 for building source distributions '
'(use --no-use-pep517 to force legacy behaviour).'
) # type: Any
no_use_pep517 = partial(
Option,
'--no-use-pep517',
dest='use_pep517',
action='callback',
callback=no_use_pep517_callback,
default=None,
help=SUPPRESS_HELP
) # type: Any
install_options = partial(
@@ -579,7 +674,7 @@ install_options = partial(
"bin\"). Use multiple --install-option options to pass multiple "
"options to setup.py install. If you are using an option with a "
"directory path, be sure to use absolute path.",
) # type: Any
) # type: Callable[..., Option]
global_options = partial(
Option,
@@ -589,7 +684,7 @@ global_options = partial(
metavar='options',
help="Extra global options to be supplied to the setup.py "
"call before the install command.",
) # type: Any
) # type: Callable[..., Option]
no_clean = partial(
Option,
@@ -597,7 +692,7 @@ no_clean = partial(
action='store_true',
default=False,
help="Don't clean up build directories."
) # type: Any
) # type: Callable[..., Option]
pre = partial(
Option,
@@ -606,7 +701,7 @@ pre = partial(
default=False,
help="Include pre-release and development versions. By default, "
"pip only finds stable versions.",
) # type: Any
) # type: Callable[..., Option]
disable_pip_version_check = partial(
Option,
@@ -616,7 +711,7 @@ disable_pip_version_check = partial(
default=False,
help="Don't periodically check PyPI to determine whether a new version "
"of pip is available for download. Implied with --no-index.",
) # type: Any
) # type: Callable[..., Option]
# Deprecated, Remove later
@@ -626,14 +721,15 @@ always_unzip = partial(
dest='always_unzip',
action='store_true',
help=SUPPRESS_HELP,
) # type: Any
) # type: Callable[..., Option]
def _merge_hash(option, opt_str, value, parser):
# type: (Option, str, str, OptionParser) -> None
"""Given a value spelled "algo:digest", append the digest to a list
pointed to in a dict by the algo name."""
if not parser.values.hashes:
parser.values.hashes = {}
parser.values.hashes = {} # type: ignore
try:
algo, digest = value.split(':', 1)
except ValueError:
@@ -657,7 +753,7 @@ hash = partial(
type='string',
help="Verify that the package's archive matches this "
'hash before installing. Example: --hash=sha256:abcdef...',
) # type: Any
) # type: Callable[..., Option]
require_hashes = partial(
@@ -669,7 +765,7 @@ require_hashes = partial(
help='Require a hash to check each requirement against, for '
'repeatable installs. This option is implied when any package in a '
'requirements file has a --hash option.',
) # type: Any
) # type: Callable[..., Option]
##########
@@ -700,7 +796,7 @@ general_group = {
disable_pip_version_check,
no_color,
]
}
} # type: Dict[str, Any]
index_group = {
'name': 'Package Index Options',
@@ -709,6 +805,5 @@ index_group = {
extra_index_url,
no_index,
find_links,
process_dependency_links,
]
}
} # type: Dict[str, Any]
@@ -14,11 +14,17 @@ from pipenv.patched.notpip._internal.commands import (
)
from pipenv.patched.notpip._internal.exceptions import CommandError
from pipenv.patched.notpip._internal.utils.misc import get_prog
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Tuple, List # noqa: F401
__all__ = ["create_main_parser", "parse_command"]
def create_main_parser():
# type: () -> ConfigOptionParser
"""Creates and returns the main parser for pip's CLI
"""
@@ -44,7 +50,8 @@ def create_main_parser():
gen_opts = cmdoptions.make_option_group(cmdoptions.general_group, parser)
parser.add_option_group(gen_opts)
parser.main = True # so the help formatter knows
# so the help formatter knows
parser.main = True # type: ignore
# create command listing for description
command_summaries = get_summaries()
@@ -55,6 +62,7 @@ def create_main_parser():
def parse_command(args):
# type: (List[str]) -> Tuple[str, List[str]]
parser = create_main_parser()
# Note: parser calls disable_interspersed_args(), so the result of this
@@ -68,7 +76,7 @@ def parse_command(args):
# --version
if general_options.version:
sys.stdout.write(parser.version)
sys.stdout.write(parser.version) # type: ignore
sys.stdout.write(os.linesep)
sys.exit()
@@ -16,7 +16,7 @@ class CheckCommand(Command):
summary = 'Verify installed packages have compatible dependencies.'
def run(self, options, args):
package_set = create_package_set_from_installed()
package_set, parsing_probs = create_package_set_from_installed()
missing, conflicting = check_package_set(package_set)
for project_name in missing:
@@ -35,7 +35,7 @@ class CheckCommand(Command):
project_name, version, req, dep_name, dep_version,
)
if missing or conflicting:
if missing or conflicting or parsing_probs:
return 1
else:
logger.info("No broken requirements found.")
@@ -58,6 +58,8 @@ class DownloadCommand(RequirementCommand):
cmd_opts.add_option(cmdoptions.require_hashes())
cmd_opts.add_option(cmdoptions.progress_bar())
cmd_opts.add_option(cmdoptions.no_build_isolation())
cmd_opts.add_option(cmdoptions.use_pep517())
cmd_opts.add_option(cmdoptions.no_use_pep517())
cmd_opts.add_option(
'-d', '--dest', '--destination-dir', '--destination-directory',
@@ -30,12 +30,6 @@ from pipenv.patched.notpip._internal.utils.misc import (
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.notpip._internal.wheel import WheelBuilder
try:
import wheel
except ImportError:
wheel = None
logger = logging.getLogger(__name__)
@@ -158,6 +152,8 @@ class InstallCommand(RequirementCommand):
cmd_opts.add_option(cmdoptions.ignore_requires_python())
cmd_opts.add_option(cmdoptions.no_build_isolation())
cmd_opts.add_option(cmdoptions.use_pep517())
cmd_opts.add_option(cmdoptions.no_use_pep517())
cmd_opts.add_option(cmdoptions.install_options())
cmd_opts.add_option(cmdoptions.global_options())
@@ -314,6 +310,7 @@ class InstallCommand(RequirementCommand):
ignore_requires_python=options.ignore_requires_python,
ignore_installed=options.ignore_installed,
isolated=options.isolated_mode,
use_pep517=options.use_pep517
)
resolver.resolve(requirement_set)
@@ -321,21 +318,51 @@ class InstallCommand(RequirementCommand):
modifying_pip=requirement_set.has_requirement("pip")
)
# If caching is disabled or wheel is not installed don't
# try to build wheels.
if wheel and options.cache_dir:
# build wheels before install.
wb = WheelBuilder(
finder, preparer, wheel_cache,
build_options=[], global_options=[],
)
# Ignore the result: a failed wheel will be
# installed from the sdist/vcs whatever.
# Consider legacy and PEP517-using requirements separately
legacy_requirements = []
pep517_requirements = []
for req in requirement_set.requirements.values():
if req.use_pep517:
pep517_requirements.append(req)
else:
legacy_requirements.append(req)
# We don't build wheels for legacy requirements if we
# don't have wheel installed or we don't have a cache dir
try:
import wheel # noqa: F401
build_legacy = bool(options.cache_dir)
except ImportError:
build_legacy = False
wb = WheelBuilder(
finder, preparer, wheel_cache,
build_options=[], global_options=[],
)
# Always build PEP 517 requirements
build_failures = wb.build(
pep517_requirements,
session=session, autobuilding=True
)
if build_legacy:
# We don't care about failures building legacy
# requirements, as we'll fall through to a direct
# install for those.
wb.build(
requirement_set.requirements.values(),
legacy_requirements,
session=session, autobuilding=True
)
# If we're using PEP 517, we cannot do a direct install
# so we fail here.
if build_failures:
raise InstallationError(
"Could not build wheels for {} which use"
" PEP 517 and cannot be installed directly".format(
", ".join(r.name for r in build_failures)))
to_install = resolver.get_installation_order(
requirement_set
)
@@ -472,7 +499,11 @@ class InstallCommand(RequirementCommand):
)
def _warn_about_conflicts(self, to_install):
package_set, _dep_info = check_install_conflicts(to_install)
try:
package_set, _dep_info = check_install_conflicts(to_install)
except Exception:
logger.error("Error checking for conflicts.", exc_info=True)
return
missing, conflicting = _dep_info
# NOTE: There is some duplication here from pipenv.patched.notpip check
@@ -118,7 +118,6 @@ class ListCommand(Command):
index_urls=index_urls,
allow_all_prereleases=options.pre,
trusted_hosts=options.trusted_hosts,
process_dependency_links=options.process_dependency_links,
session=session,
)
@@ -134,14 +133,18 @@ class ListCommand(Command):
include_editables=options.include_editable,
)
# get_not_required must be called firstly in order to find and
# filter out all dependencies correctly. Otherwise a package
# can't be identified as requirement because some parent packages
# could be filtered out before.
if options.not_required:
packages = self.get_not_required(packages, options)
if options.outdated:
packages = self.get_outdated(packages, options)
elif options.uptodate:
packages = self.get_uptodate(packages, options)
if options.not_required:
packages = self.get_not_required(packages, options)
self.output_package_listing(packages, options)
def get_outdated(self, packages, options):
@@ -168,16 +171,8 @@ class ListCommand(Command):
logger.debug('Ignoring indexes: %s', ','.join(index_urls))
index_urls = []
dependency_links = []
for dist in packages:
if dist.has_metadata('dependency_links.txt'):
dependency_links.extend(
dist.get_metadata_lines('dependency_links.txt'),
)
with self._build_session(options) as session:
finder = self._build_package_finder(options, index_urls, session)
finder.add_dependency_links(dependency_links)
for dist in packages:
typ = 'unknown'
@@ -67,6 +67,8 @@ class WheelCommand(RequirementCommand):
help="Extra arguments to be supplied to 'setup.py bdist_wheel'.",
)
cmd_opts.add_option(cmdoptions.no_build_isolation())
cmd_opts.add_option(cmdoptions.use_pep517())
cmd_opts.add_option(cmdoptions.no_use_pep517())
cmd_opts.add_option(cmdoptions.constraints())
cmd_opts.add_option(cmdoptions.editable())
cmd_opts.add_option(cmdoptions.requirements())
@@ -157,6 +159,7 @@ class WheelCommand(RequirementCommand):
ignore_requires_python=options.ignore_requires_python,
ignore_installed=True,
isolated=options.isolated_mode,
use_pep517=options.use_pep517
)
resolver.resolve(requirement_set)
@@ -167,10 +170,10 @@ class WheelCommand(RequirementCommand):
global_options=options.global_options or [],
no_clean=options.no_clean,
)
wheels_built_successfully = wb.build(
build_failures = wb.build(
requirement_set.requirements.values(), session=session,
)
if not wheels_built_successfully:
if len(build_failures) != 0:
raise CommandError(
"Failed to build one or more wheels"
)
+75 -26
View File
@@ -19,7 +19,6 @@ from pipenv.patched.notpip._vendor.lockfile import LockError
from pipenv.patched.notpip._vendor.requests.adapters import BaseAdapter, HTTPAdapter
from pipenv.patched.notpip._vendor.requests.auth import AuthBase, HTTPBasicAuth
from pipenv.patched.notpip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from pipenv.patched.notpip._vendor.requests.sessions import Session
from pipenv.patched.notpip._vendor.requests.structures import CaseInsensitiveDict
from pipenv.patched.notpip._vendor.requests.utils import get_netrc_auth
# NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is
@@ -27,7 +26,6 @@ from pipenv.patched.notpip._vendor.requests.utils import get_netrc_auth
from pipenv.patched.notpip._vendor.six.moves import xmlrpc_client # type: ignore
from pipenv.patched.notpip._vendor.six.moves.urllib import parse as urllib_parse
from pipenv.patched.notpip._vendor.six.moves.urllib import request as urllib_request
from pipenv.patched.notpip._vendor.six.moves.urllib.parse import unquote as urllib_unquote
from pipenv.patched.notpip._vendor.urllib3.util import IS_PYOPENSSL
import pipenv.patched.notpip
@@ -40,14 +38,23 @@ from pipenv.patched.notpip._internal.utils.glibc import libc_ver
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import (
ARCHIVE_EXTENSIONS, ask_path_exists, backup_dir, call_subprocess, consume,
display_path, format_size, get_installed_version, rmtree, splitext,
unpack_file,
display_path, format_size, get_installed_version, rmtree,
split_auth_from_netloc, splitext, unpack_file,
)
from pipenv.patched.notpip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.utils.ui import DownloadProgressProvider
from pipenv.patched.notpip._internal.vcs import vcs
if MYPY_CHECK_RUNNING:
from typing import ( # noqa: F401
Optional, Tuple, Dict, IO, Text, Union
)
from pipenv.patched.notpip._internal.models.link import Link # noqa: F401
from pipenv.patched.notpip._internal.utils.hashes import Hashes # noqa: F401
from pipenv.patched.notpip._internal.vcs import AuthInfo # noqa: F401
try:
import ssl # noqa
except ImportError:
@@ -137,14 +144,15 @@ def user_agent():
class MultiDomainBasicAuth(AuthBase):
def __init__(self, prompting=True):
# type: (bool) -> None
self.prompting = prompting
self.passwords = {}
self.passwords = {} # type: Dict[str, AuthInfo]
def __call__(self, req):
parsed = urllib_parse.urlparse(req.url)
# Get the netloc without any embedded credentials
netloc = parsed.netloc.rsplit("@", 1)[-1]
# Split the credentials from the netloc.
netloc, url_user_password = split_auth_from_netloc(parsed.netloc)
# Set the url of the request to the url without any credentials
req.url = urllib_parse.urlunparse(parsed[:1] + (netloc,) + parsed[2:])
@@ -152,9 +160,9 @@ class MultiDomainBasicAuth(AuthBase):
# Use any stored credentials that we have for this netloc
username, password = self.passwords.get(netloc, (None, None))
# Extract credentials embedded in the url if we have none stored
# Use the credentials embedded in the url if we have none stored
if username is None:
username, password = self.parse_credentials(parsed.netloc)
username, password = url_user_password
# Get creds from netrc if we still don't have them
if username is None and password is None:
@@ -200,6 +208,7 @@ class MultiDomainBasicAuth(AuthBase):
# Add our new username and password to the request
req = HTTPBasicAuth(username or "", password or "")(resp.request)
req.register_hook("response", self.warn_on_401)
# Send our new request
new_resp = resp.connection.send(req, **kwargs)
@@ -207,14 +216,11 @@ class MultiDomainBasicAuth(AuthBase):
return new_resp
def parse_credentials(self, netloc):
if "@" in netloc:
userinfo = netloc.rsplit("@", 1)[0]
if ":" in userinfo:
user, pwd = userinfo.split(":", 1)
return (urllib_unquote(user), urllib_unquote(pwd))
return urllib_unquote(userinfo), None
return None, None
def warn_on_401(self, resp, **kwargs):
# warn user that they provided incorrect credentials
if resp.status_code == 401:
logger.warning('401 Error, Credentials not correct for %s',
resp.request.url)
class LocalFSAdapter(BaseAdapter):
@@ -325,7 +331,7 @@ class InsecureHTTPAdapter(HTTPAdapter):
class PipSession(requests.Session):
timeout = None
timeout = None # type: Optional[int]
def __init__(self, *args, **kwargs):
retries = kwargs.pop("retries", 0)
@@ -398,6 +404,7 @@ class PipSession(requests.Session):
def get_file_content(url, comes_from=None, session=None):
# type: (str, Optional[str], Optional[PipSession]) -> Tuple[str, Text]
"""Gets the content of a file; it may be a filename, file: URL, or
http: URL. Returns (location, content). Content is unicode.
@@ -448,6 +455,7 @@ _url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I)
def is_url(name):
# type: (Union[str, Text]) -> bool
"""Returns true if the name looks like a URL"""
if ':' not in name:
return False
@@ -456,6 +464,7 @@ def is_url(name):
def url_to_path(url):
# type: (str) -> str
"""
Convert a file: URL to a path.
"""
@@ -473,6 +482,7 @@ def url_to_path(url):
def path_to_url(path):
# type: (Union[str, Text]) -> str
"""
Convert a path to a file: URL. The path will be made absolute and have
quoted path parts.
@@ -483,6 +493,7 @@ def path_to_url(path):
def is_archive_file(name):
# type: (str) -> bool
"""Return True if `name` is a considered as an archive file."""
ext = splitext(name)[1].lower()
if ext in ARCHIVE_EXTENSIONS:
@@ -503,14 +514,17 @@ def _get_used_vcs_backend(link):
def is_vcs_url(link):
# type: (Link) -> bool
return bool(_get_used_vcs_backend(link))
def is_file_url(link):
# type: (Link) -> bool
return link.url.lower().startswith('file:')
def is_dir_url(link):
# type: (Link) -> bool
"""Return whether a file:// Link points to a directory.
``link`` must not have any other scheme but file://. Call is_file_url()
@@ -525,7 +539,14 @@ def _progress_indicator(iterable, *args, **kwargs):
return iterable
def _download_url(resp, link, content_file, hashes, progress_bar):
def _download_url(
resp, # type: Response
link, # type: Link
content_file, # type: IO
hashes, # type: Hashes
progress_bar # type: str
):
# type: (...) -> None
try:
total_length = int(resp.headers['content-length'])
except (ValueError, KeyError, TypeError):
@@ -647,8 +668,15 @@ def _copy_file(filename, location, link):
logger.info('Saved %s', display_path(download_location))
def unpack_http_url(link, location, download_dir=None,
session=None, hashes=None, progress_bar="on"):
def unpack_http_url(
link, # type: Link
location, # type: str
download_dir=None, # type: Optional[str]
session=None, # type: Optional[PipSession]
hashes=None, # type: Optional[Hashes]
progress_bar="on" # type: str
):
# type: (...) -> None
if session is None:
raise TypeError(
"unpack_http_url() missing 1 required keyword argument: 'session'"
@@ -685,7 +713,13 @@ def unpack_http_url(link, location, download_dir=None,
os.unlink(from_path)
def unpack_file_url(link, location, download_dir=None, hashes=None):
def unpack_file_url(
link, # type: Link
location, # type: str
download_dir=None, # type: Optional[str]
hashes=None # type: Optional[Hashes]
):
# type: (...) -> None
"""Unpack link into location.
If download_dir is provided and link points to a file, make a copy
@@ -798,9 +832,16 @@ class PipXmlrpcTransport(xmlrpc_client.Transport):
raise
def unpack_url(link, location, download_dir=None,
only_download=False, session=None, hashes=None,
progress_bar="on"):
def unpack_url(
link, # type: Optional[Link]
location, # type: Optional[str]
download_dir=None, # type: Optional[str]
only_download=False, # type: bool
session=None, # type: Optional[PipSession]
hashes=None, # type: Optional[Hashes]
progress_bar="on" # type: str
):
# type: (...) -> None
"""Unpack link.
If link is a VCS link:
if only_download, export into download_dir and ignore location
@@ -840,7 +881,14 @@ def unpack_url(link, location, download_dir=None,
write_delete_marker_file(location)
def _download_http_url(link, session, temp_dir, hashes, progress_bar):
def _download_http_url(
link, # type: Link
session, # type: PipSession
temp_dir, # type: str
hashes, # type: Hashes
progress_bar # type: str
):
# type: (...) -> Tuple[str, str]
"""Download link url into temp_dir using provided session"""
target_url = link.url.split('#', 1)[0]
try:
@@ -900,6 +948,7 @@ def _download_http_url(link, session, temp_dir, hashes, progress_bar):
def _check_download_dir(link, download_dir, hashes):
# type: (Link, str, Hashes) -> Optional[str]
""" Check download_dir for previously downloaded file with correct hash
If a correct file is found return its path else None
"""
@@ -5,6 +5,12 @@ from itertools import chain, groupby, repeat
from pipenv.patched.notpip._vendor.six import iteritems
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Optional # noqa: F401
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement # noqa: F401
class PipError(Exception):
"""Base pip exception"""
@@ -96,7 +102,7 @@ class HashError(InstallationError):
typically available earlier.
"""
req = None
req = None # type: Optional[InstallRequirement]
head = ''
def body(self):
+277 -192
View File
@@ -16,7 +16,7 @@ from pipenv.patched.notpip._vendor.distlib.compat import unescape
from pipenv.patched.notpip._vendor.packaging import specifiers
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.packaging.version import parse as parse_version
from pipenv.patched.notpip._vendor.requests.exceptions import SSLError
from pipenv.patched.notpip._vendor.requests.exceptions import RetryError, SSLError
from pipenv.patched.notpip._vendor.six.moves.urllib import parse as urllib_parse
from pipenv.patched.notpip._vendor.six.moves.urllib import request as urllib_request
@@ -31,14 +31,29 @@ from pipenv.patched.notpip._internal.models.index import PyPI
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.pep425tags import get_supported
from pipenv.patched.notpip._internal.utils.compat import ipaddress
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import (
ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, normalize_path,
remove_auth_from_url,
ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, WHEEL_EXTENSION, normalize_path,
redact_password_from_url,
)
from pipenv.patched.notpip._internal.utils.packaging import check_requires_python
from pipenv.patched.notpip._internal.wheel import Wheel, wheel_ext
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.wheel import Wheel
if MYPY_CHECK_RUNNING:
from logging import Logger # noqa: F401
from typing import ( # noqa: F401
Tuple, Optional, Any, List, Union, Callable, Set, Sequence,
Iterable, MutableMapping
)
from pipenv.patched.notpip._vendor.packaging.version import _BaseVersion # noqa: F401
from pipenv.patched.notpip._vendor.requests import Response # noqa: F401
from pipenv.patched.notpip._internal.req import InstallRequirement # noqa: F401
from pipenv.patched.notpip._internal.download import PipSession # noqa: F401
SecureOrigin = Tuple[str, str, Optional[str]]
BuildTag = Tuple[Any, ...] # either emply tuple or Tuple[int, str]
CandidateSortingKey = Tuple[int, _BaseVersion, BuildTag, Optional[int]]
__all__ = ['FormatControl', 'PackageFinder']
@@ -53,126 +68,190 @@ SECURE_ORIGINS = [
("file", "*", None),
# ssh is always secure.
("ssh", "*", "*"),
]
] # type: List[SecureOrigin]
logger = logging.getLogger(__name__)
def _get_content_type(url, session):
"""Get the Content-Type of the given url, using a HEAD request"""
def _match_vcs_scheme(url):
# type: (str) -> Optional[str]
"""Look for VCS schemes in the URL.
Returns the matched VCS scheme, or None if there's no match.
"""
from pipenv.patched.notpip._internal.vcs import VcsSupport
for scheme in VcsSupport.schemes:
if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
return scheme
return None
def _is_url_like_archive(url):
# type: (str) -> bool
"""Return whether the URL looks like an archive.
"""
filename = Link(url).filename
for bad_ext in ARCHIVE_EXTENSIONS:
if filename.endswith(bad_ext):
return True
return False
class _NotHTML(Exception):
def __init__(self, content_type, request_desc):
# type: (str, str) -> None
super(_NotHTML, self).__init__(content_type, request_desc)
self.content_type = content_type
self.request_desc = request_desc
def _ensure_html_header(response):
# type: (Response) -> None
"""Check the Content-Type header to ensure the response contains HTML.
Raises `_NotHTML` if the content type is not text/html.
"""
content_type = response.headers.get("Content-Type", "")
if not content_type.lower().startswith("text/html"):
raise _NotHTML(content_type, response.request.method)
class _NotHTTP(Exception):
pass
def _ensure_html_response(url, session):
# type: (str, PipSession) -> None
"""Send a HEAD request to the URL, and ensure the response contains HTML.
Raises `_NotHTTP` if the URL is not available for a HEAD request, or
`_NotHTML` if the content type is not text/html.
"""
scheme, netloc, path, query, fragment = urllib_parse.urlsplit(url)
if scheme not in {'http', 'https'}:
# FIXME: some warning or something?
# assertion error?
return ''
raise _NotHTTP()
resp = session.head(url, allow_redirects=True)
resp.raise_for_status()
return resp.headers.get("Content-Type", "")
_ensure_html_header(resp)
def _handle_get_page_fail(link, reason, url, meth=None):
def _get_html_response(url, session):
# type: (str, PipSession) -> Response
"""Access an HTML page with GET, and return the response.
This consists of three parts:
1. If the URL looks suspiciously like an archive, send a HEAD first to
check the Content-Type is HTML, to avoid downloading a large file.
Raise `_NotHTTP` if the content type cannot be determined, or
`_NotHTML` if it is not HTML.
2. Actually perform the request. Raise HTTP exceptions on network failures.
3. Check the Content-Type header to make sure we got HTML, and raise
`_NotHTML` otherwise.
"""
if _is_url_like_archive(url):
_ensure_html_response(url, session=session)
logger.debug('Getting page %s', url)
resp = session.get(
url,
headers={
"Accept": "text/html",
# We don't want to blindly returned cached data for
# /simple/, because authors generally expecting that
# twine upload && pip install will function, but if
# they've done a pip install in the last ~10 minutes
# it won't. Thus by setting this to zero we will not
# blindly use any cached data, however the benefit of
# using max-age=0 instead of no-cache, is that we will
# still support conditional requests, so we will still
# minimize traffic sent in cases where the page hasn't
# changed at all, we will just always incur the round
# trip for the conditional GET now instead of only
# once per 10 minutes.
# For more information, please see pypa/pip#5670.
"Cache-Control": "max-age=0",
},
)
resp.raise_for_status()
# The check for archives above only works if the url ends with
# something that looks like an archive. However that is not a
# requirement of an url. Unless we issue a HEAD request on every
# url we cannot know ahead of time for sure if something is HTML
# or not. However we can check after we've downloaded it.
_ensure_html_header(resp)
return resp
def _handle_get_page_fail(
link, # type: Link
reason, # type: Union[str, Exception]
meth=None # type: Optional[Callable[..., None]]
):
# type: (...) -> None
if meth is None:
meth = logger.debug
meth("Could not fetch URL %s: %s - skipping", link, reason)
def _get_html_page(link, session=None):
# type: (Link, Optional[PipSession]) -> Optional[HTMLPage]
if session is None:
raise TypeError(
"_get_html_page() missing 1 required keyword argument: 'session'"
)
url = link.url
url = url.split('#', 1)[0]
url = link.url.split('#', 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
from pipenv.patched.notpip._internal.vcs import VcsSupport
for scheme in VcsSupport.schemes:
if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
logger.debug('Cannot look at %s URL %s', scheme, link)
return None
vcs_scheme = _match_vcs_scheme(url)
if vcs_scheme:
logger.debug('Cannot look at %s URL %s', vcs_scheme, link)
return None
# Tack index.html onto file:// URLs that point to directories
scheme, _, path, _, _, _ = urllib_parse.urlparse(url)
if (scheme == 'file' and os.path.isdir(urllib_request.url2pathname(path))):
# add trailing slash if not present so urljoin doesn't trim
# final segment
if not url.endswith('/'):
url += '/'
url = urllib_parse.urljoin(url, 'index.html')
logger.debug(' file: URL is directory, getting %s', url)
try:
filename = link.filename
for bad_ext in ARCHIVE_EXTENSIONS:
if filename.endswith(bad_ext):
content_type = _get_content_type(url, session=session)
if content_type.lower().startswith('text/html'):
break
else:
logger.debug(
'Skipping page %s because of Content-Type: %s',
link,
content_type,
)
return
logger.debug('Getting page %s', url)
# Tack index.html onto file:// URLs that point to directories
(scheme, netloc, path, params, query, fragment) = \
urllib_parse.urlparse(url)
if (scheme == 'file' and
os.path.isdir(urllib_request.url2pathname(path))):
# add trailing slash if not present so urljoin doesn't trim
# final segment
if not url.endswith('/'):
url += '/'
url = urllib_parse.urljoin(url, 'index.html')
logger.debug(' file: URL is directory, getting %s', url)
resp = session.get(
url,
headers={
"Accept": "text/html",
# We don't want to blindly returned cached data for
# /simple/, because authors generally expecting that
# twine upload && pip install will function, but if
# they've done a pip install in the last ~10 minutes
# it won't. Thus by setting this to zero we will not
# blindly use any cached data, however the benefit of
# using max-age=0 instead of no-cache, is that we will
# still support conditional requests, so we will still
# minimize traffic sent in cases where the page hasn't
# changed at all, we will just always incur the round
# trip for the conditional GET now instead of only
# once per 10 minutes.
# For more information, please see pypa/pip#5670.
"Cache-Control": "max-age=0",
},
resp = _get_html_response(url, session=session)
except _NotHTTP as exc:
logger.debug(
'Skipping page %s because it looks like an archive, and cannot '
'be checked by HEAD.', link,
)
except _NotHTML as exc:
logger.debug(
'Skipping page %s because the %s request got Content-Type: %s',
link, exc.request_desc, exc.content_type,
)
resp.raise_for_status()
# The check for archives above only works if the url ends with
# something that looks like an archive. However that is not a
# requirement of an url. Unless we issue a HEAD request on every
# url we cannot know ahead of time for sure if something is HTML
# or not. However we can check after we've downloaded it.
content_type = resp.headers.get('Content-Type', 'unknown')
if not content_type.lower().startswith("text/html"):
logger.debug(
'Skipping page %s because of Content-Type: %s',
link,
content_type,
)
return
inst = HTMLPage(resp.content, resp.url, resp.headers)
except requests.HTTPError as exc:
_handle_get_page_fail(link, exc, url)
_handle_get_page_fail(link, exc)
except RetryError as exc:
_handle_get_page_fail(link, exc)
except SSLError as exc:
reason = "There was a problem confirming the ssl certificate: "
reason += str(exc)
_handle_get_page_fail(link, reason, url, meth=logger.info)
_handle_get_page_fail(link, reason, meth=logger.info)
except requests.ConnectionError as exc:
_handle_get_page_fail(link, "connection error: %s" % exc, url)
_handle_get_page_fail(link, "connection error: %s" % exc)
except requests.Timeout:
_handle_get_page_fail(link, "timed out", url)
_handle_get_page_fail(link, "timed out")
else:
return inst
return HTMLPage(resp.content, resp.url, resp.headers)
return None
class PackageFinder(object):
@@ -182,11 +261,21 @@ class PackageFinder(object):
packages, by reading pages and looking for appropriate links.
"""
def __init__(self, find_links, index_urls, allow_all_prereleases=False,
trusted_hosts=None, process_dependency_links=False,
session=None, format_control=None, platform=None,
versions=None, abi=None, implementation=None,
prefer_binary=False):
def __init__(
self,
find_links, # type: List[str]
index_urls, # type: List[str]
allow_all_prereleases=False, # type: bool
trusted_hosts=None, # type: Optional[Iterable[str]]
session=None, # type: Optional[PipSession]
format_control=None, # type: Optional[FormatControl]
platform=None, # type: Optional[str]
versions=None, # type: Optional[List[str]]
abi=None, # type: Optional[str]
implementation=None, # type: Optional[str]
prefer_binary=False # type: bool
):
# type: (...) -> None
"""Create a PackageFinder.
:param format_control: A FormatControl object or None. Used to control
@@ -215,7 +304,7 @@ class PackageFinder(object):
# it and if it exists, use the normalized version.
# This is deliberately conservative - it might be fine just to
# blindly normalize anything starting with a ~...
self.find_links = []
self.find_links = [] # type: List[str]
for link in find_links:
if link.startswith('~'):
new_link = normalize_path(link)
@@ -224,10 +313,9 @@ class PackageFinder(object):
self.find_links.append(link)
self.index_urls = index_urls
self.dependency_links = []
# These are boring links that have already been logged somehow:
self.logged_links = set()
self.logged_links = set() # type: Set[Link]
self.format_control = format_control or FormatControl(set(), set())
@@ -235,14 +323,11 @@ class PackageFinder(object):
self.secure_origins = [
("*", host, "*")
for host in (trusted_hosts if trusted_hosts else [])
]
] # type: List[SecureOrigin]
# Do we want to allow _all_ pre-releases?
self.allow_all_prereleases = allow_all_prereleases
# Do we process dependency links?
self.process_dependency_links = process_dependency_links
# The Session we'll use to make requests
self.session = session
@@ -274,11 +359,12 @@ class PackageFinder(object):
break
def get_formatted_locations(self):
# type: () -> str
lines = []
if self.index_urls and self.index_urls != [PyPI.simple_url]:
lines.append(
"Looking in indexes: {}".format(", ".join(
remove_auth_from_url(url) for url in self.index_urls))
redact_password_from_url(url) for url in self.index_urls))
)
if self.find_links:
lines.append(
@@ -286,21 +372,6 @@ class PackageFinder(object):
)
return "\n".join(lines)
def add_dependency_links(self, links):
# FIXME: this shouldn't be global list this, it should only
# apply to requirements of the package that specifies the
# dependency_links value
# FIXME: also, we should track comes_from (i.e., use Link)
if self.process_dependency_links:
deprecated(
"Dependency Links processing has been deprecated and will be "
"removed in a future release.",
replacement="PEP 508 URL dependencies",
gone_in="18.2",
issue=4187,
)
self.dependency_links.extend(links)
@staticmethod
def get_extras_links(links):
requires = []
@@ -316,12 +387,11 @@ class PackageFinder(object):
extras[link[1:-1]] = current_list
else:
current_list.append(link)
return extras
@staticmethod
def _sort_locations(locations, expand_dir=False):
# type: (Sequence[str], bool) -> Tuple[List[str], List[str]]
"""
Sort locations into "files" (archives) and "urls", and return
a pair of lists (files,urls)
@@ -354,6 +424,11 @@ class PackageFinder(object):
sort_path(os.path.join(path, item))
elif is_file_url:
urls.append(url)
else:
logger.warning(
"Path '{0}' is ignored: "
"it is a directory.".format(path),
)
elif os.path.isfile(path):
sort_path(path)
else:
@@ -373,6 +448,7 @@ class PackageFinder(object):
return files, urls
def _candidate_sort_key(self, candidate, ignore_compatibility=True):
# type: (InstallationCandidate, bool) -> CandidateSortingKey
"""
Function used to generate link sort key for link tuples.
The greater the return value, the more preferred it is.
@@ -387,7 +463,7 @@ class PackageFinder(object):
with the same version, would have to be considered equal
"""
support_num = len(self.valid_tags)
build_tag = tuple()
build_tag = tuple() # type: BuildTag
binary_preference = 0
if candidate.location.is_wheel:
# can raise InvalidWheelFilename
@@ -404,7 +480,6 @@ class PackageFinder(object):
pri = -(wheel.support_index_min(tags=tags))
except TypeError:
pri = -(support_num)
if wheel.build_tag is not None:
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
build_tag_groups = match.groups()
@@ -414,6 +489,7 @@ class PackageFinder(object):
return (binary_preference, candidate.version, build_tag, pri)
def _validate_secure_origin(self, logger, location):
# type: (Logger, Link) -> bool
# Determine if this url used a secure transport mechanism
parsed = urllib_parse.urlparse(str(location))
origin = (parsed.scheme, parsed.hostname, parsed.port)
@@ -445,7 +521,9 @@ class PackageFinder(object):
network = ipaddress.ip_network(
secure_origin[1]
if isinstance(secure_origin[1], six.text_type)
else secure_origin[1].decode("utf8")
# setting secure_origin[1] to proper Union[bytes, str]
# creates problems in other places
else secure_origin[1].decode("utf8") # type: ignore
)
except ValueError:
# We don't have both a valid address or a valid network, so
@@ -485,6 +563,7 @@ class PackageFinder(object):
return False
def _get_index_urls_locations(self, project_name):
# type: (str) -> List[str]
"""Returns the locations found via self.index_urls
Checks the url_name on the main (first in the list) index and
@@ -507,9 +586,10 @@ class PackageFinder(object):
return [mkurl_pypi_url(url) for url in self.index_urls]
def find_all_candidates(self, project_name):
# type: (str) -> List[Optional[InstallationCandidate]]
"""Find all available InstallationCandidate for project_name
This checks index_urls, find_links and dependency_links.
This checks index_urls and find_links.
All versions found are returned as an InstallationCandidate list.
See _link_package_versions for details on which files are accepted
@@ -519,21 +599,18 @@ class PackageFinder(object):
fl_file_loc, fl_url_loc = self._sort_locations(
self.find_links, expand_dir=True,
)
dep_file_loc, dep_url_loc = self._sort_locations(self.dependency_links)
file_locations = (Link(url) for url in itertools.chain(
index_file_loc, fl_file_loc, dep_file_loc,
index_file_loc, fl_file_loc,
))
# We trust every url that the user has given us whether it was given
# via --index-url or --find-links
# We explicitly do not trust links that came from dependency_links
# via --index-url or --find-links.
# We want to filter out any thing which does not have a secure origin.
url_locations = [
link for link in itertools.chain(
(Link(url) for url in index_url_loc),
(Link(url) for url in fl_url_loc),
(Link(url) for url in dep_url_loc),
)
if self._validate_secure_origin(logger, link)
]
@@ -564,17 +641,6 @@ class PackageFinder(object):
self._package_versions(page.iter_links(), search)
)
dependency_versions = self._package_versions(
(Link(url) for url in self.dependency_links), search
)
if dependency_versions:
logger.debug(
'dependency_links found: %s',
', '.join([
version.location.url for version in dependency_versions
])
)
file_versions = self._package_versions(file_locations, search)
if file_versions:
file_versions.sort(reverse=True)
@@ -587,12 +653,10 @@ class PackageFinder(object):
)
# This is an intentional priority ordering
return (
file_versions + find_links_versions + page_versions +
dependency_versions
)
return file_versions + find_links_versions + page_versions
def find_requirement(self, req, upgrade, ignore_compatibility=False):
# type: (InstallRequirement, bool, bool) -> Optional[Link]
"""Try to find a Link matching req
Expects req, an InstallRequirement and upgrade, a boolean
@@ -692,20 +756,18 @@ class PackageFinder(object):
return best_candidate.location
def _get_pages(self, locations, project_name):
# type: (Iterable[Link], str) -> Iterable[HTMLPage]
"""
Yields (page, page_url) from the given locations, skipping
locations that have errors.
"""
seen = set()
seen = set() # type: Set[Link]
for location in locations:
if location in seen:
continue
seen.add(location)
try:
page = self._get_page(location)
except requests.HTTPError:
continue
page = _get_html_page(location, session=self.session)
if page is None:
continue
@@ -714,12 +776,13 @@ class PackageFinder(object):
_py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
def _sort_links(self, links):
# type: (Iterable[Link]) -> List[Link]
"""
Returns elements of links in order, non-egg links first, egg links
second, while eliminating duplicates
"""
eggs, no_eggs = [], []
seen = set()
seen = set() # type: Set[Link]
for link in links:
if link not in seen:
seen.add(link)
@@ -729,7 +792,12 @@ class PackageFinder(object):
no_eggs.append(link)
return no_eggs + eggs
def _package_versions(self, links, search):
def _package_versions(
self,
links, # type: Iterable[Link]
search # type: Search
):
# type: (...) -> List[Optional[InstallationCandidate]]
result = []
for link in self._sort_links(links):
v = self._link_package_versions(link, search)
@@ -738,11 +806,13 @@ class PackageFinder(object):
return result
def _log_skipped_link(self, link, reason):
# type: (Link, str) -> None
if link not in self.logged_links:
logger.debug('Skipping link %s; %s', link, reason)
self.logged_links.add(link)
def _link_package_versions(self, link, search, ignore_compatibility=True):
# type: (Link, Search, bool) -> Optional[InstallationCandidate]
"""Return an InstallationCandidate or None"""
version = None
if link.egg_fragment:
@@ -752,51 +822,51 @@ class PackageFinder(object):
egg_info, ext = link.splitext()
if not ext:
self._log_skipped_link(link, 'not a file')
return
return None
if ext not in SUPPORTED_EXTENSIONS:
self._log_skipped_link(
link, 'unsupported archive format: %s' % ext,
)
return
if "binary" not in search.formats and ext == wheel_ext and not ignore_compatibility:
return None
if "binary" not in search.formats and ext == WHEEL_EXTENSION and not ignore_compatibility:
self._log_skipped_link(
link, 'No binaries permitted for %s' % search.supplied,
)
return
return None
if "macosx10" in link.path and ext == '.zip' and not ignore_compatibility:
self._log_skipped_link(link, 'macosx10 one')
return
if ext == wheel_ext:
return None
if ext == WHEEL_EXTENSION:
try:
wheel = Wheel(link.filename)
except InvalidWheelFilename:
self._log_skipped_link(link, 'invalid wheel filename')
return
return None
if canonicalize_name(wheel.name) != search.canonical:
self._log_skipped_link(
link, 'wrong project name (not %s)' % search.supplied)
return
return None
if not wheel.supported(self.valid_tags) and not ignore_compatibility:
self._log_skipped_link(
link, 'it is not compatible with this Python')
return
return None
version = wheel.version
# This should be up by the search.ok_binary check, but see issue 2700.
if "source" not in search.formats and ext != wheel_ext:
if "source" not in search.formats and ext != WHEEL_EXTENSION:
self._log_skipped_link(
link, 'No sources permitted for %s' % search.supplied,
)
return
return None
if not version:
version = egg_info_matches(egg_info, search.supplied, link)
if version is None:
version = _egg_info_matches(egg_info, search.canonical)
if not version:
self._log_skipped_link(
link, 'Missing project version for %s' % search.supplied)
return
return None
match = self._py_version_re.search(version)
if match:
@@ -805,7 +875,7 @@ class PackageFinder(object):
if py_version != sys.version[:3]:
self._log_skipped_link(
link, 'Python version is incorrect')
return
return None
try:
support_this_python = check_requires_python(link.requires_python)
except specifiers.InvalidSpecifier:
@@ -814,45 +884,57 @@ class PackageFinder(object):
support_this_python = True
if not support_this_python and not ignore_compatibility:
logger.debug("The package %s is incompatible with the python"
"version in use. Acceptable python versions are:%s",
logger.debug("The package %s is incompatible with the python "
"version in use. Acceptable python versions are: %s",
link, link.requires_python)
return
return None
logger.debug('Found link %s, version: %s', link, version)
return InstallationCandidate(search.supplied, version, link, link.requires_python)
def _get_page(self, link):
return _get_html_page(link, session=self.session)
def _find_name_version_sep(egg_info, canonical_name):
# type: (str, str) -> int
"""Find the separator's index based on the package's canonical name.
`egg_info` must be an egg info string for the given package, and
`canonical_name` must be the package's canonical name.
This function is needed since the canonicalized name does not necessarily
have the same length as the egg info's name part. An example::
>>> egg_info = 'foo__bar-1.0'
>>> canonical_name = 'foo-bar'
>>> _find_name_version_sep(egg_info, canonical_name)
8
"""
# Project name and version must be separated by one single dash. Find all
# occurrences of dashes; if the string in front of it matches the canonical
# name, this is the one separating the name and version parts.
for i, c in enumerate(egg_info):
if c != "-":
continue
if canonicalize_name(egg_info[:i]) == canonical_name:
return i
raise ValueError("{} does not match {}".format(egg_info, canonical_name))
def egg_info_matches(
egg_info, search_name, link,
_egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)):
def _egg_info_matches(egg_info, canonical_name):
# type: (str, str) -> Optional[str]
"""Pull the version part out of a string.
:param egg_info: The string to parse. E.g. foo-2.1
:param search_name: The name of the package this belongs to. None to
infer the name. Note that this cannot unambiguously parse strings
like foo-2-2 which might be foo, 2-2 or foo-2, 2.
:param link: The link the string came from, for logging on failure.
:param canonical_name: The canonicalized name of the package this
belongs to.
"""
match = _egg_info_re.search(egg_info)
if not match:
logger.debug('Could not parse version from link: %s', link)
try:
version_start = _find_name_version_sep(egg_info, canonical_name) + 1
except ValueError:
return None
if search_name is None:
full_match = match.group(0)
return full_match.split('-', 1)[-1]
name = match.group(0).lower()
# To match the "safe" name that pkg_resources creates:
name = name.replace('_', '-')
# project name and version must be separated by a dash
look_for = search_name.lower() + "-"
if name.startswith(look_for):
return match.group(0)[len(look_for):]
else:
version = egg_info[version_start:]
if not version:
return None
return version
def _determine_base_url(document, page_url):
@@ -888,6 +970,7 @@ _CLEAN_LINK_RE = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I)
def _clean_link(url):
# type: (str) -> str
"""Makes sure a link is fully encoded. That is, if a ' ' shows up in
the link, it will be rewritten to %20 (while not over-quoting
% or other characters)."""
@@ -898,14 +981,16 @@ class HTMLPage(object):
"""Represents one page, along with its URL"""
def __init__(self, content, url, headers=None):
# type: (bytes, str, MutableMapping[str, str]) -> None
self.content = content
self.url = url
self.headers = headers
def __str__(self):
return self.url
return redact_password_from_url(self.url)
def iter_links(self):
# type: () -> Iterable[Link]
"""Yields all links in the page"""
document = html5lib.parse(
self.content,
+21 -4
View File
@@ -12,6 +12,11 @@ from distutils.command.install import SCHEME_KEYS # type: ignore
from pipenv.patched.notpip._internal.utils import appdirs
from pipenv.patched.notpip._internal.utils.compat import WINDOWS, expanduser
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any, Union, Dict, List, Optional # noqa: F401
# Application Directories
USER_CACHE_DIR = appdirs.user_cache_dir("pip")
@@ -28,6 +33,7 @@ PIP_DELETE_MARKER_FILENAME = 'pip-delete-this-directory.txt'
def write_delete_marker_file(directory):
# type: (str) -> None
"""
Write the pip delete marker file into this directory.
"""
@@ -37,6 +43,7 @@ def write_delete_marker_file(directory):
def running_under_virtualenv():
# type: () -> bool
"""
Return True if we're running inside a virtualenv, False otherwise.
@@ -50,6 +57,7 @@ def running_under_virtualenv():
def virtualenv_no_global():
# type: () -> bool
"""
Return True if in a venv and no system site packages.
"""
@@ -59,6 +67,8 @@ def virtualenv_no_global():
no_global_file = os.path.join(site_mod_dir, 'no-global-site-packages.txt')
if running_under_virtualenv() and os.path.isfile(no_global_file):
return True
else:
return False
if running_under_virtualenv():
@@ -80,7 +90,8 @@ src_prefix = os.path.abspath(src_prefix)
# FIXME doesn't account for venv linked to global site-packages
site_packages = sysconfig.get_path("purelib")
site_packages = sysconfig.get_path("purelib") # type: Optional[str]
# This is because of a bug in PyPy's sysconfig module, see
# https://bitbucket.org/pypy/pypy/issues/2506/sysconfig-returns-incorrect-paths
# for more information.
@@ -135,6 +146,7 @@ new_config_file = os.path.join(appdirs.user_config_dir("pip"), config_basename)
def distutils_scheme(dist_name, user=False, home=None, root=None,
isolated=False, prefix=None):
# type:(str, bool, str, str, bool, str) -> dict
"""
Return a distutils install scheme
"""
@@ -146,12 +158,15 @@ def distutils_scheme(dist_name, user=False, home=None, root=None,
extra_dist_args = {"script_args": ["--no-user-cfg"]}
else:
extra_dist_args = {}
dist_args = {'name': dist_name}
dist_args = {'name': dist_name} # type: Dict[str, Union[str, List[str]]]
dist_args.update(extra_dist_args)
d = Distribution(dist_args)
# Ignoring, typeshed issue reported python/typeshed/issues/2567
d.parse_config_files()
i = d.get_command_obj('install', create=True)
# NOTE: Ignoring type since mypy can't find attributes on 'Command'
i = d.get_command_obj('install', create=True) # type: Any
assert i is not None
# NOTE: setting user or home has the side-effect of creating the home dir
# or user base for installations during finalize_options()
# ideally, we'd prefer a scheme class that has no side-effects.
@@ -171,7 +186,9 @@ def distutils_scheme(dist_name, user=False, home=None, root=None,
# platlib). Note, i.install_lib is *always* set after
# finalize_options(); we only want to override here if the user
# has explicitly requested it hence going back to the config
if 'install_lib' in d.get_option_dict('install'):
# Ignoring, typeshed issue reported python/typeshed/issues/2567
if 'install_lib' in d.get_option_dict('install'): # type: ignore
scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib))
if running_under_virtualenv():
@@ -1,6 +1,12 @@
from pipenv.patched.notpip._vendor.packaging.version import parse as parse_version
from pipenv.patched.notpip._internal.utils.models import KeyBasedCompareMixin
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from pipenv.patched.notpip._vendor.packaging.version import _BaseVersion # noqa: F401
from pipenv.patched.notpip._internal.models.link import Link # noqa: F401
from typing import Any, Union # noqa: F401
class InstallationCandidate(KeyBasedCompareMixin):
@@ -8,8 +14,9 @@ class InstallationCandidate(KeyBasedCompareMixin):
"""
def __init__(self, project, version, location, requires_python=None):
# type: (Any, str, Link, Any) -> None
self.project = project
self.version = parse_version(version)
self.version = parse_version(version) # type: _BaseVersion
self.location = location
self.requires_python = requires_python
@@ -19,6 +26,7 @@ class InstallationCandidate(KeyBasedCompareMixin):
)
def __repr__(self):
# type: () -> str
return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
self.project, self.version, self.location,
)
@@ -1,16 +1,24 @@
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Optional, Set, FrozenSet # noqa: F401
class FormatControl(object):
"""A helper class for controlling formats from which packages are installed.
If a field is falsy, it isn't set. If it is {':all:'}, it should match all
packages except those listed in the other field. Only one field can be set
to {':all:'} at a time. The rest of the time exact package name matches
are listed, with any given package only showing up in one field at a time.
"""Helper for managing formats from which a package can be installed.
"""
def __init__(self, no_binary=None, only_binary=None):
self.no_binary = set() if no_binary is None else no_binary
self.only_binary = set() if only_binary is None else only_binary
# type: (Optional[Set], Optional[Set]) -> None
if no_binary is None:
no_binary = set()
if only_binary is None:
only_binary = set()
self.no_binary = no_binary
self.only_binary = only_binary
def __eq__(self, other):
return self.__dict__ == other.__dict__
@@ -27,6 +35,7 @@ class FormatControl(object):
@staticmethod
def handle_mutual_excludes(value, target, other):
# type: (str, Optional[Set], Optional[Set]) -> None
new = value.split(',')
while ':all:' in new:
other.clear()
@@ -45,6 +54,7 @@ class FormatControl(object):
target.add(name)
def get_allowed_formats(self, canonical_name):
# type: (str) -> FrozenSet
result = {"binary", "source"}
if canonical_name in self.only_binary:
result.discard('source')
@@ -57,6 +67,7 @@ class FormatControl(object):
return frozenset(result)
def disallow_binaries(self):
# type: () -> None
self.handle_mutual_excludes(
':all:', self.no_binary, self.only_binary,
)
@@ -6,6 +6,7 @@ class PackageIndex(object):
"""
def __init__(self, url, file_storage_domain):
# type: (str, str) -> None
super(PackageIndex, self).__init__()
self.url = url
self.netloc = urllib_parse.urlsplit(url).netloc
@@ -18,6 +19,7 @@ class PackageIndex(object):
self.file_storage_domain = file_storage_domain
def _url_for_path(self, path):
# type: (str) -> str
return urllib_parse.urljoin(self.url, path)
+27 -5
View File
@@ -4,9 +4,15 @@ import re
from pipenv.patched.notpip._vendor.six.moves.urllib import parse as urllib_parse
from pipenv.patched.notpip._internal.download import path_to_url
from pipenv.patched.notpip._internal.utils.misc import splitext
from pipenv.patched.notpip._internal.utils.misc import (
WHEEL_EXTENSION, redact_password_from_url, splitext,
)
from pipenv.patched.notpip._internal.utils.models import KeyBasedCompareMixin
from pipenv.patched.notpip._internal.wheel import wheel_ext
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Optional, Tuple, Union, Text # noqa: F401
from pipenv.patched.notpip._internal.index import HTMLPage # noqa: F401
class Link(KeyBasedCompareMixin):
@@ -14,6 +20,7 @@ class Link(KeyBasedCompareMixin):
"""
def __init__(self, url, comes_from=None, requires_python=None):
# type: (str, Optional[Union[str, HTMLPage]], Optional[str]) -> None
"""
url:
url of the resource pointed to (href of the link)
@@ -44,15 +51,17 @@ class Link(KeyBasedCompareMixin):
else:
rp = ''
if self.comes_from:
return '%s (from %s)%s' % (self.url, self.comes_from, rp)
return '%s (from %s)%s' % (redact_password_from_url(self.url),
self.comes_from, rp)
else:
return str(self.url)
return redact_password_from_url(str(self.url))
def __repr__(self):
return '<Link %s>' % self
@property
def filename(self):
# type: () -> str
_, netloc, path, _, _ = urllib_parse.urlsplit(self.url)
name = posixpath.basename(path.rstrip('/')) or netloc
name = urllib_parse.unquote(name)
@@ -61,25 +70,31 @@ class Link(KeyBasedCompareMixin):
@property
def scheme(self):
# type: () -> str
return urllib_parse.urlsplit(self.url)[0]
@property
def netloc(self):
# type: () -> str
return urllib_parse.urlsplit(self.url)[1]
@property
def path(self):
# type: () -> str
return urllib_parse.unquote(urllib_parse.urlsplit(self.url)[2])
def splitext(self):
# type: () -> Tuple[str, str]
return splitext(posixpath.basename(self.path.rstrip('/')))
@property
def ext(self):
# type: () -> str
return self.splitext()[1]
@property
def url_without_fragment(self):
# type: () -> str
scheme, netloc, path, query, fragment = urllib_parse.urlsplit(self.url)
return urllib_parse.urlunsplit((scheme, netloc, path, query, None))
@@ -87,6 +102,7 @@ class Link(KeyBasedCompareMixin):
@property
def egg_fragment(self):
# type: () -> Optional[str]
match = self._egg_fragment_re.search(self.url)
if not match:
return None
@@ -96,6 +112,7 @@ class Link(KeyBasedCompareMixin):
@property
def subdirectory_fragment(self):
# type: () -> Optional[str]
match = self._subdirectory_fragment_re.search(self.url)
if not match:
return None
@@ -107,6 +124,7 @@ class Link(KeyBasedCompareMixin):
@property
def hash(self):
# type: () -> Optional[str]
match = self._hash_re.search(self.url)
if match:
return match.group(2)
@@ -114,6 +132,7 @@ class Link(KeyBasedCompareMixin):
@property
def hash_name(self):
# type: () -> Optional[str]
match = self._hash_re.search(self.url)
if match:
return match.group(1)
@@ -121,14 +140,17 @@ class Link(KeyBasedCompareMixin):
@property
def show_url(self):
# type: () -> Optional[str]
return posixpath.basename(self.url.split('#', 1)[0].split('?', 1)[0])
@property
def is_wheel(self):
return self.ext == wheel_ext
# type: () -> bool
return self.ext == WHEEL_EXTENSION
@property
def is_artifact(self):
# type: () -> bool
"""
Determines if this points to an actual artifact (e.g. a tarball) or if
it points to an "abstract" thing like a path or a VCS location.
@@ -1,18 +1,22 @@
"""Validation of dependencies of packages
"""
import logging
from collections import namedtuple
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.pkg_resources import RequirementParseError
from pipenv.patched.notpip._internal.operations.prepare import make_abstract_dist
from pipenv.patched.notpip._internal.utils.misc import get_installed_distributions
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
logger = logging.getLogger(__name__)
if MYPY_CHECK_RUNNING:
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement # noqa: F401
from typing import ( # noqa: F401
Any, Callable, Dict, Iterator, Optional, Set, Tuple, List
Any, Callable, Dict, Optional, Set, Tuple, List
)
# Shorthands
@@ -28,7 +32,7 @@ PackageDetails = namedtuple('PackageDetails', ['version', 'requires'])
def create_package_set_from_installed(**kwargs):
# type: (**Any) -> PackageSet
# type: (**Any) -> Tuple[PackageSet, bool]
"""Converts a list of distributions into a PackageSet.
"""
# Default to using all packages installed on the system
@@ -36,10 +40,16 @@ def create_package_set_from_installed(**kwargs):
kwargs = {"local_only": False, "skip": ()}
package_set = {}
problems = False
for dist in get_installed_distributions(**kwargs):
name = canonicalize_name(dist.project_name)
package_set[name] = PackageDetails(dist.version, dist.requires())
return package_set
try:
package_set[name] = PackageDetails(dist.version, dist.requires())
except RequirementParseError as e:
# Don't crash on broken metadata
logging.warning("Error parsing requirements for %s: %s", name, e)
problems = True
return package_set, problems
def check_package_set(package_set, should_ignore=None):
@@ -95,7 +105,7 @@ def check_install_conflicts(to_install):
installing given requirements
"""
# Start from the current state
package_set = create_package_set_from_installed()
package_set, _ = create_package_set_from_installed()
# Install packages
would_be_installed = _simulate_installation_of(to_install, package_set)
@@ -110,9 +120,6 @@ def check_install_conflicts(to_install):
)
# NOTE from @pradyunsg
# This required a minor update in dependency link handling logic over at
# operations.prepare.IsSDist.dist() to get it working
def _simulate_installation_of(to_install, package_set):
# type: (List[InstallRequirement], PackageSet) -> Set[str]
"""Computes the version of packages after installing to_install.
@@ -123,7 +130,7 @@ def _simulate_installation_of(to_install, package_set):
# Modify it as installing requirement_set would (assuming no errors)
for inst_req in to_install:
dist = make_abstract_dist(inst_req).dist(finder=None)
dist = make_abstract_dist(inst_req).dist()
name = canonicalize_name(dist.key)
package_set[name] = PackageDetails(dist.version, dist.requires())
@@ -5,57 +5,61 @@ import logging
import os
import re
from pipenv.patched.notpip._vendor import pkg_resources, six
from pipenv.patched.notpip._vendor import six
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.pkg_resources import RequirementParseError
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.exceptions import BadCommand, InstallationError
from pipenv.patched.notpip._internal.req.constructors import (
install_req_from_editable, install_req_from_line,
)
from pipenv.patched.notpip._internal.req.req_file import COMMENT_RE
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.misc import (
dist_is_editable, get_installed_distributions, make_vcs_requirement_url,
dist_is_editable, get_installed_distributions,
)
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import ( # noqa: F401
Iterator, Optional, List, Container, Set, Dict, Tuple, Iterable, Union
)
from pipenv.patched.notpip._internal.cache import WheelCache # noqa: F401
from pipenv.patched.notpip._vendor.pkg_resources import ( # noqa: F401
Distribution, Requirement
)
RequirementInfo = Tuple[Optional[Union[str, Requirement]], bool, List[str]]
logger = logging.getLogger(__name__)
def freeze(
requirement=None,
find_links=None, local_only=None, user_only=None, skip_regex=None,
isolated=False,
wheel_cache=None,
exclude_editable=False,
skip=()):
requirement=None, # type: Optional[List[str]]
find_links=None, # type: Optional[List[str]]
local_only=None, # type: Optional[bool]
user_only=None, # type: Optional[bool]
skip_regex=None, # type: Optional[str]
isolated=False, # type: bool
wheel_cache=None, # type: Optional[WheelCache]
exclude_editable=False, # type: bool
skip=() # type: Container[str]
):
# type: (...) -> Iterator[str]
find_links = find_links or []
skip_match = None
if skip_regex:
skip_match = re.compile(skip_regex).search
dependency_links = []
for dist in pkg_resources.working_set:
if dist.has_metadata('dependency_links.txt'):
dependency_links.extend(
dist.get_metadata_lines('dependency_links.txt')
)
for link in find_links:
if '#egg=' in link:
dependency_links.append(link)
for link in find_links:
yield '-f %s' % link
installations = {}
installations = {} # type: Dict[str, FrozenRequirement]
for dist in get_installed_distributions(local_only=local_only,
skip=(),
user_only=user_only):
try:
req = FrozenRequirement.from_dist(
dist,
dependency_links
)
req = FrozenRequirement.from_dist(dist)
except RequirementParseError:
logger.warning(
"Could not parse requirement: %s",
@@ -71,10 +75,10 @@ def freeze(
# should only be emitted once, even if the same option is in multiple
# requirements files, so we need to keep track of what has been emitted
# so that we don't emit it again if it's seen again
emitted_options = set()
emitted_options = set() # type: Set[str]
# keep track of which files a requirement is in so that we can
# give an accurate warning if a requirement appears multiple times.
req_files = collections.defaultdict(list)
req_files = collections.defaultdict(list) # type: Dict[str, List[str]]
for req_file_path in requirement:
with open(req_file_path) as req_file:
for line in req_file:
@@ -128,10 +132,10 @@ def freeze(
# but has been processed already
if not req_files[line_req.name]:
logger.warning(
"Requirement file [%s] contains %s, but that "
"package is not installed",
"Requirement file [%s] contains %s, but "
"package %r is not installed",
req_file_path,
COMMENT_RE.sub('', line).strip(),
COMMENT_RE.sub('', line).strip(), line_req.name
)
else:
req_files[line_req.name].append(req_file_path)
@@ -157,105 +161,84 @@ def freeze(
yield str(installation).rstrip()
def get_requirement_info(dist):
# type: (Distribution) -> RequirementInfo
"""
Compute and return values (req, editable, comments) for use in
FrozenRequirement.from_dist().
"""
if not dist_is_editable(dist):
return (None, False, [])
location = os.path.normcase(os.path.abspath(dist.location))
from pipenv.patched.notpip._internal.vcs import vcs, RemoteNotFoundError
vc_type = vcs.get_backend_type(location)
if not vc_type:
req = dist.as_requirement()
logger.debug(
'No VCS found for editable requirement {!r} in: {!r}', req,
location,
)
comments = [
'# Editable install with no version control ({})'.format(req)
]
return (location, True, comments)
try:
req = vc_type.get_src_requirement(location, dist.project_name)
except RemoteNotFoundError:
req = dist.as_requirement()
comments = [
'# Editable {} install with no remote ({})'.format(
vc_type.__name__, req,
)
]
return (location, True, comments)
except BadCommand:
logger.warning(
'cannot determine version of editable source in %s '
'(%s command not found in path)',
location,
vc_type.name,
)
return (None, True, [])
except InstallationError as exc:
logger.warning(
"Error when trying to get requirement for VCS system %s, "
"falling back to uneditable format", exc
)
else:
if req is not None:
return (req, True, [])
logger.warning(
'Could not determine repository location of %s', location
)
comments = ['## !! Could not determine repository location']
return (None, False, comments)
class FrozenRequirement(object):
def __init__(self, name, req, editable, comments=()):
# type: (str, Union[str, Requirement], bool, Iterable[str]) -> None
self.name = name
self.req = req
self.editable = editable
self.comments = comments
_rev_re = re.compile(r'-r(\d+)$')
_date_re = re.compile(r'-(20\d\d\d\d\d\d)$')
@classmethod
def _init_args_from_dist(cls, dist, dependency_links):
"""
Compute and return arguments (req, editable, comments) to pass to
FrozenRequirement.__init__().
This method is for use in FrozenRequirement.from_dist().
"""
location = os.path.normcase(os.path.abspath(dist.location))
comments = []
from pipenv.patched.notpip._internal.vcs import vcs, get_src_requirement
if dist_is_editable(dist) and vcs.get_backend_name(location):
editable = True
try:
req = get_src_requirement(dist, location)
except InstallationError as exc:
logger.warning(
"Error when trying to get requirement for VCS system %s, "
"falling back to uneditable format", exc
)
req = None
if req is None:
logger.warning(
'Could not determine repository location of %s', location
)
comments.append(
'## !! Could not determine repository location'
)
req = dist.as_requirement()
editable = False
else:
editable = False
def from_dist(cls, dist):
# type: (Distribution) -> FrozenRequirement
req, editable, comments = get_requirement_info(dist)
if req is None:
req = dist.as_requirement()
specs = req.specs
assert len(specs) == 1 and specs[0][0] in ["==", "==="], \
'Expected 1 spec with == or ===; specs = %r; dist = %r' % \
(specs, dist)
version = specs[0][1]
ver_match = cls._rev_re.search(version)
date_match = cls._date_re.search(version)
if ver_match or date_match:
svn_backend = vcs.get_backend('svn')
if svn_backend:
svn_location = svn_backend().get_location(
dist,
dependency_links,
)
if not svn_location:
logger.warning(
'Warning: cannot find svn location for %s', req,
)
comments.append(
'## FIXME: could not find svn URL in dependency_links '
'for this package:'
)
else:
deprecated(
"SVN editable detection based on dependency links "
"will be dropped in the future.",
replacement=None,
gone_in="18.2",
issue=4187,
)
comments.append(
'# Installing as editable to satisfy requirement %s:' %
req
)
if ver_match:
rev = ver_match.group(1)
else:
rev = '{%s}' % date_match.group(1)
editable = True
egg_name = cls.egg_name(dist)
req = make_vcs_requirement_url(svn_location, rev, egg_name)
return (req, editable, comments)
@classmethod
def from_dist(cls, dist, dependency_links):
args = cls._init_args_from_dist(dist, dependency_links)
return cls(dist.project_name, *args)
@staticmethod
def egg_name(dist):
name = dist.egg_name()
match = re.search(r'-py\d\.\d$', name)
if match:
name = name[:match.start()]
return name
return cls(dist.project_name, req, editable, comments=comments)
def __str__(self):
req = self.req
@@ -18,12 +18,21 @@ from pipenv.patched.notpip._internal.utils.compat import expanduser
from pipenv.patched.notpip._internal.utils.hashes import MissingHashes
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import display_path, normalize_path, rmtree
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.vcs import vcs
if MYPY_CHECK_RUNNING:
from typing import Any, Optional # noqa: F401
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement # noqa: F401
from pipenv.patched.notpip._internal.index import PackageFinder # noqa: F401
from pipenv.patched.notpip._internal.download import PipSession # noqa: F401
from pipenv.patched.notpip._internal.req.req_tracker import RequirementTracker # noqa: F401
logger = logging.getLogger(__name__)
def make_abstract_dist(req):
# type: (InstallRequirement) -> DistAbstraction
"""Factory to make an abstract dist object.
Preconditions: Either an editable req with a source_dir, or satisfied_by or
@@ -59,40 +68,40 @@ class DistAbstraction(object):
"""
def __init__(self, req):
self.req = req
# type: (InstallRequirement) -> None
self.req = req # type: InstallRequirement
def dist(self, finder):
def dist(self):
# type: () -> Any
"""Return a setuptools Dist object."""
raise NotImplementedError(self.dist)
raise NotImplementedError
def prep_for_dist(self, finder, build_isolation):
# type: (PackageFinder, bool) -> Any
"""Ensure that we can get a Dist for this requirement."""
raise NotImplementedError(self.dist)
raise NotImplementedError
class IsWheel(DistAbstraction):
def dist(self, finder):
def dist(self):
# type: () -> pkg_resources.Distribution
return list(pkg_resources.find_distributions(
self.req.source_dir))[0]
def prep_for_dist(self, finder, build_isolation):
# type: (PackageFinder, bool) -> Any
# FIXME:https://github.com/pypa/pip/issues/1112
pass
class IsSDist(DistAbstraction):
def dist(self, finder):
dist = self.req.get_dist()
# FIXME: shouldn't be globally added.
if finder and dist.has_metadata('dependency_links.txt'):
finder.add_dependency_links(
dist.get_metadata_lines('dependency_links.txt')
)
return dist
def dist(self):
return self.req.get_dist()
def prep_for_dist(self, finder, build_isolation):
# type: (PackageFinder, bool) -> None
# Prepare for building. We need to:
# 1. Load pyproject.toml (if it exists)
# 2. Set up the build environment
@@ -100,43 +109,64 @@ class IsSDist(DistAbstraction):
self.req.load_pyproject_toml()
should_isolate = self.req.use_pep517 and build_isolation
def _raise_conflicts(conflicting_with, conflicting_reqs):
raise InstallationError(
"Some build dependencies for %s conflict with %s: %s." % (
self.req, conflicting_with, ', '.join(
'%s is incompatible with %s' % (installed, wanted)
for installed, wanted in sorted(conflicting))))
if should_isolate:
# Isolate in a BuildEnvironment and install the build-time
# requirements.
self.req.build_env = BuildEnvironment()
self.req.build_env.install_requirements(
finder, self.req.pyproject_requires,
finder, self.req.pyproject_requires, 'overlay',
"Installing build dependencies"
)
missing = []
if self.req.requirements_to_check:
check = self.req.requirements_to_check
missing = self.req.build_env.missing_requirements(check)
conflicting, missing = self.req.build_env.check_requirements(
self.req.requirements_to_check
)
if conflicting:
_raise_conflicts("PEP 517/518 supported requirements",
conflicting)
if missing:
logger.warning(
"Missing build requirements in pyproject.toml for %s.",
self.req,
)
logger.warning(
"The project does not specify a build backend, and pip "
"cannot fall back to setuptools without %s.",
"The project does not specify a build backend, and "
"pip cannot fall back to setuptools without %s.",
" and ".join(map(repr, sorted(missing)))
)
# Install any extra build dependencies that the backend requests.
# This must be done in a second pass, as the pyproject.toml
# dependencies must be installed before we can call the backend.
with self.req.build_env:
# We need to have the env active when calling the hook.
self.req.spin_message = "Getting requirements to build wheel"
reqs = self.req.pep517_backend.get_requires_for_build_wheel()
conflicting, missing = self.req.build_env.check_requirements(reqs)
if conflicting:
_raise_conflicts("the backend dependencies", conflicting)
self.req.build_env.install_requirements(
finder, missing, 'normal',
"Installing backend dependencies"
)
try:
self.req.run_egg_info()
except (OSError, TypeError):
self.req._correct_build_location()
self.req.run_egg_info()
self.req.prepare_metadata()
self.req.assert_source_matches_version()
class Installed(DistAbstraction):
def dist(self, finder):
def dist(self):
# type: () -> pkg_resources.Distribution
return self.req.satisfied_by
def prep_for_dist(self, finder, build_isolation):
# type: (PackageFinder, bool) -> Any
pass
@@ -144,8 +174,17 @@ class RequirementPreparer(object):
"""Prepares a Requirement
"""
def __init__(self, build_dir, download_dir, src_dir, wheel_download_dir,
progress_bar, build_isolation, req_tracker):
def __init__(
self,
build_dir, # type: str
download_dir, # type: Optional[str]
src_dir, # type: str
wheel_download_dir, # type: Optional[str]
progress_bar, # type: str
build_isolation, # type: bool
req_tracker # type: RequirementTracker
):
# type: (...) -> None
super(RequirementPreparer, self).__init__()
self.src_dir = src_dir
@@ -175,6 +214,7 @@ class RequirementPreparer(object):
@property
def _download_should_save(self):
# type: () -> bool
# TODO: Modify to reduce indentation needed
if self.download_dir:
self.download_dir = expanduser(self.download_dir)
@@ -187,8 +227,15 @@ class RequirementPreparer(object):
% display_path(self.download_dir))
return False
def prepare_linked_requirement(self, req, session, finder,
upgrade_allowed, require_hashes):
def prepare_linked_requirement(
self,
req, # type: InstallRequirement
session, # type: PipSession
finder, # type: PackageFinder
upgrade_allowed, # type: bool
require_hashes # type: bool
):
# type: (...) -> DistAbstraction
"""Prepare a requirement that would be obtained from req.link
"""
# TODO: Breakup into smaller functions
@@ -209,6 +256,7 @@ class RequirementPreparer(object):
# installation.
# FIXME: this won't upgrade when there's an existing
# package unpacked in `req.source_dir`
# package unpacked in `req.source_dir`
if os.path.exists(os.path.join(req.source_dir, 'setup.py')):
rmtree(req.source_dir)
req.populate_link(finder, upgrade_allowed, require_hashes)
@@ -298,8 +346,14 @@ class RequirementPreparer(object):
req.archive(self.download_dir)
return abstract_dist
def prepare_editable_requirement(self, req, require_hashes, use_user_site,
finder):
def prepare_editable_requirement(
self,
req, # type: InstallRequirement
require_hashes, # type: bool
use_user_site, # type: bool
finder # type: PackageFinder
):
# type: (...) -> DistAbstraction
"""Prepare an editable requirement
"""
assert req.editable, "cannot prepare a non-editable req as editable"
@@ -327,6 +381,7 @@ class RequirementPreparer(object):
return abstract_dist
def prepare_installed_requirement(self, req, require_hashes, skip_reason):
# type: (InstallRequirement, bool, Optional[str]) -> DistAbstraction
"""Prepare an already-installed requirement
"""
assert req.satisfied_by, "req should have been satisfied but isn't"
+77 -14
View File
@@ -14,8 +14,15 @@ try:
import pipenv.patched.notpip._internal.utils.glibc
except ImportError:
import pipenv.patched.notpip.utils.glibc
from pipenv.patched.notpip._internal.utils.compat import get_extension_suffixes
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import ( # noqa: F401
Tuple, Callable, List, Optional, Union, Dict
)
Pep425Tag = Tuple[str, str, str]
logger = logging.getLogger(__name__)
@@ -23,6 +30,7 @@ _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)')
def get_config_var(var):
# type: (str) -> Optional[str]
try:
return sysconfig.get_config_var(var)
except IOError as e: # Issue #1074
@@ -31,6 +39,7 @@ def get_config_var(var):
def get_abbr_impl():
# type: () -> str
"""Return abbreviated implementation name."""
if hasattr(sys, 'pypy_version_info'):
pyimpl = 'pp'
@@ -44,6 +53,7 @@ def get_abbr_impl():
def get_impl_ver():
# type: () -> str
"""Return implementation version."""
impl_ver = get_config_var("py_version_nodot")
if not impl_ver or get_abbr_impl() == 'pp':
@@ -52,17 +62,21 @@ def get_impl_ver():
def get_impl_version_info():
# type: () -> Tuple[int, ...]
"""Return sys.version_info-like tuple for use in decrementing the minor
version."""
if get_abbr_impl() == 'pp':
# as per https://github.com/pypa/pip/issues/2882
return (sys.version_info[0], sys.pypy_version_info.major,
sys.pypy_version_info.minor)
# attrs exist only on pypy
return (sys.version_info[0],
sys.pypy_version_info.major, # type: ignore
sys.pypy_version_info.minor) # type: ignore
else:
return sys.version_info[0], sys.version_info[1]
def get_impl_tag():
# type: () -> str
"""
Returns the Tag for this specific implementation.
"""
@@ -70,6 +84,7 @@ def get_impl_tag():
def get_flag(var, fallback, expected=True, warn=True):
# type: (str, Callable[..., bool], Union[bool, int], bool) -> bool
"""Use a fallback method for determining SOABI flags if the needed config
var is unset or unavailable."""
val = get_config_var(var)
@@ -82,6 +97,7 @@ def get_flag(var, fallback, expected=True, warn=True):
def get_abi_tag():
# type: () -> Optional[str]
"""Return the ABI tag based on SOABI (if available) or emulate SOABI
(CPython 2, PyPy)."""
soabi = get_config_var('SOABI')
@@ -116,10 +132,12 @@ def get_abi_tag():
def _is_running_32bit():
# type: () -> bool
return sys.maxsize == 2147483647
def get_platform():
# type: () -> str
"""Return our platform name 'win32', 'linux_x86_64'"""
if sys.platform == 'darwin':
# distutils.util.get_platform() returns the release based on the value
@@ -146,6 +164,7 @@ def get_platform():
def is_manylinux1_compatible():
# type: () -> bool
# Only Linux, and only x86-64 / i686
if get_platform() not in {"linux_x86_64", "linux_i686"}:
return False
@@ -162,13 +181,33 @@ def is_manylinux1_compatible():
return pipenv.patched.notpip._internal.utils.glibc.have_compatible_glibc(2, 5)
def is_manylinux2010_compatible():
# type: () -> bool
# Only Linux, and only x86-64 / i686
if get_platform() not in {"linux_x86_64", "linux_i686"}:
return False
# Check for presence of _manylinux module
try:
import _manylinux
return bool(_manylinux.manylinux2010_compatible)
except (ImportError, AttributeError):
# Fall through to heuristic check below
pass
# Check glibc version. CentOS 6 uses glibc 2.12.
return pipenv.patched.notpip._internal.utils.glibc.have_compatible_glibc(2, 12)
def get_darwin_arches(major, minor, machine):
# type: (int, int, str) -> List[str]
"""Return a list of supported arches (including group arches) for
the given major, minor and machine architecture of an macOS machine.
"""
arches = []
def _supports_arch(major, minor, arch):
# type: (int, int, str) -> bool
# Looking at the application support for macOS versions in the chart
# provided by https://en.wikipedia.org/wiki/OS_X#Versions it appears
# our timeline looks roughly like:
@@ -209,7 +248,7 @@ def get_darwin_arches(major, minor, machine):
("intel", ("x86_64", "i386")),
("fat64", ("x86_64", "ppc64")),
("fat32", ("x86_64", "i386", "ppc")),
])
]) # type: Dict[str, Tuple[str, ...]]
if _supports_arch(major, minor, machine):
arches.append(machine)
@@ -223,8 +262,24 @@ def get_darwin_arches(major, minor, machine):
return arches
def get_supported(versions=None, noarch=False, platform=None,
impl=None, abi=None):
def get_all_minor_versions_as_strings(version_info):
# type: (Tuple[int, ...]) -> List[str]
versions = []
major = version_info[:-1]
# Support all previous minor Python versions.
for minor in range(version_info[-1], -1, -1):
versions.append(''.join(map(str, major + (minor,))))
return versions
def get_supported(
versions=None, # type: Optional[List[str]]
noarch=False, # type: bool
platform=None, # type: Optional[str]
impl=None, # type: Optional[str]
abi=None # type: Optional[str]
):
# type: (...) -> List[Pep425Tag]
"""Return a list of supported tags for each version specified in
`versions`.
@@ -241,16 +296,12 @@ def get_supported(versions=None, noarch=False, platform=None,
# Versions must be given with respect to the preference
if versions is None:
versions = []
version_info = get_impl_version_info()
major = version_info[:-1]
# Support all previous minor Python versions.
for minor in range(version_info[-1], -1, -1):
versions.append(''.join(map(str, major + (minor,))))
versions = get_all_minor_versions_as_strings(version_info)
impl = impl or get_abbr_impl()
abis = []
abis = [] # type: List[str]
abi = abi or get_abi_tag()
if abi:
@@ -267,6 +318,7 @@ def get_supported(versions=None, noarch=False, platform=None,
if not noarch:
arch = platform or get_platform()
arch_prefix, arch_sep, arch_suffix = arch.partition('_')
if arch.startswith('macosx'):
# support macosx-10.6-intel on macosx-10.9-x86_64
match = _osx_arch_pat.match(arch)
@@ -280,8 +332,19 @@ def get_supported(versions=None, noarch=False, platform=None,
else:
# arch pattern didn't match (?!)
arches = [arch]
elif platform is None and is_manylinux1_compatible():
arches = [arch.replace('linux', 'manylinux1'), arch]
elif arch_prefix == 'manylinux2010':
# manylinux1 wheels run on most manylinux2010 systems with the
# exception of wheels depending on ncurses. PEP 571 states
# manylinux1 wheels should be considered manylinux2010 wheels:
# https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels
arches = [arch, 'manylinux1' + arch_sep + arch_suffix]
elif platform is None:
arches = []
if is_manylinux2010_compatible():
arches.append('manylinux2010' + arch_sep + arch_suffix)
if is_manylinux1_compatible():
arches.append('manylinux1' + arch_sep + arch_suffix)
arches.append(arch)
else:
arches = [arch]
+47 -20
View File
@@ -2,20 +2,43 @@ from __future__ import absolute_import
import io
import os
import sys
from pipenv.patched.notpip._vendor import pytoml, six
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any, Tuple, Optional, List # noqa: F401
def _is_list_of_str(obj):
# type: (Any) -> bool
return (
isinstance(obj, list) and
all(isinstance(item, six.string_types) for item in obj)
)
def load_pyproject_toml(use_pep517, pyproject_toml, setup_py, req_name):
def make_pyproject_path(setup_py_dir):
# type: (str) -> str
path = os.path.join(setup_py_dir, 'pyproject.toml')
# Python2 __file__ should not be unicode
if six.PY2 and isinstance(path, six.text_type):
path = path.encode(sys.getfilesystemencoding())
return path
def load_pyproject_toml(
use_pep517, # type: Optional[bool]
pyproject_toml, # type: str
setup_py, # type: str
req_name # type: str
):
# type: (...) -> Optional[Tuple[List[str], str, List[str]]]
"""Load the pyproject.toml file.
Parameters:
@@ -46,17 +69,20 @@ def load_pyproject_toml(use_pep517, pyproject_toml, setup_py, req_name):
build_system = None
# The following cases must use PEP 517
# We check for use_pep517 equalling False because that
# means the user explicitly requested --no-use-pep517
# We check for use_pep517 being non-None and falsey because that means
# the user explicitly requested --no-use-pep517. The value 0 as
# opposed to False can occur when the value is provided via an
# environment variable or config file option (due to the quirk of
# strtobool() returning an integer in pip's configuration code).
if has_pyproject and not has_setup:
if use_pep517 is False:
if use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project does not have a setup.py"
)
use_pep517 = True
elif build_system and "build-backend" in build_system:
if use_pep517 is False:
if use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project specifies a build backend of {} "
@@ -85,11 +111,13 @@ def load_pyproject_toml(use_pep517, pyproject_toml, setup_py, req_name):
# section, or the user has no pyproject.toml, but has opted in
# explicitly via --use-pep517.
# In the absence of any explicit backend specification, we
# assume the setuptools backend, and require wheel and a version
# of setuptools that supports that backend.
# assume the setuptools backend that most closely emulates the
# traditional direct setup.py execution, and require wheel and
# a version of setuptools that supports that backend.
build_system = {
"requires": ["setuptools>=38.2.5", "wheel"],
"build-backend": "setuptools.build_meta",
"requires": ["setuptools>=40.8.0", "wheel"],
"build-backend": "setuptools.build_meta:__legacy__",
}
# If we're using PEP 517, we have build system information (either
@@ -123,22 +151,21 @@ def load_pyproject_toml(use_pep517, pyproject_toml, setup_py, req_name):
))
backend = build_system.get("build-backend")
check = []
check = [] # type: List[str]
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 neede by the backend) in their requirements. So we
# (which is needed by the backend) in their requirements. So we
# make a note to check that those requirements are present once
# we have set up the environment.
# TODO: Review this - it's quite a lot of work to check for a very
# specific case. The problem is, that case is potentially quite
# common - projects that adopted PEP 518 early for the ability to
# specify requirements to execute setup.py, but never considered
# needing to mention the build tools themselves. The original PEP
# 518 code had a similar check (but implemented in a different
# way).
backend = "setuptools.build_meta"
check = ["setuptools>=38.2.5", "wheel"]
# 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
# adopted PEP 518 early for the ability to specify requirements to
# execute setup.py, but never considered needing to mention the build
# 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"]
return (requires, backend, check)
@@ -6,7 +6,10 @@ from .req_install import InstallRequirement
from .req_set import RequirementSet
from .req_file import parse_requirements
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import List, Sequence # noqa: F401
__all__ = [
"RequirementSet", "InstallRequirement",
@@ -16,8 +19,13 @@ __all__ = [
logger = logging.getLogger(__name__)
def install_given_reqs(to_install, install_options, global_options=(),
*args, **kwargs):
def install_given_reqs(
to_install, # type: List[InstallRequirement]
install_options, # type: List[str]
global_options=(), # type: Sequence[str]
*args, **kwargs
):
# type: (...) -> List[InstallRequirement]
"""
Install everything in the given list.
@@ -11,7 +11,6 @@ InstallRequirement.
import logging
import os
import re
import traceback
from pipenv.patched.notpip._vendor.packaging.markers import Marker
from pipenv.patched.notpip._vendor.packaging.requirements import InvalidRequirement, Requirement
@@ -24,11 +23,20 @@ from pipenv.patched.notpip._internal.download import (
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.models.index import PyPI, TestPyPI
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.pyproject import make_pyproject_path
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
from pipenv.patched.notpip._internal.utils.misc import is_installable_dir
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.vcs import vcs
from pipenv.patched.notpip._internal.wheel import Wheel
if MYPY_CHECK_RUNNING:
from typing import ( # noqa: F401
Optional, Tuple, Set, Any, Union, Text, Dict,
)
from pipenv.patched.notpip._internal.cache import WheelCache # noqa: F401
__all__ = [
"install_req_from_editable", "install_req_from_line",
"parse_editable"
@@ -39,6 +47,7 @@ operators = Specifier._operators.keys()
def _strip_extras(path):
# type: (str) -> Tuple[str, Optional[str]]
m = re.match(r'^(.+)(\[[^\]]+\])$', path)
extras = None
if m:
@@ -51,6 +60,7 @@ def _strip_extras(path):
def parse_editable(editable_req):
# type: (str) -> Tuple[Optional[str], str, Optional[Set[str]]]
"""Parses an editable requirement into:
- a requirement name
- an URL
@@ -68,10 +78,18 @@ def parse_editable(editable_req):
if os.path.isdir(url_no_extras):
if not os.path.exists(os.path.join(url_no_extras, 'setup.py')):
raise InstallationError(
"Directory %r is not installable. File 'setup.py' not found." %
url_no_extras
msg = (
'File "setup.py" not found. Directory cannot be installed '
'in editable mode: {}'.format(os.path.abspath(url_no_extras))
)
pyproject_path = make_pyproject_path(url_no_extras)
if os.path.isfile(pyproject_path):
msg += (
'\n(A "pyproject.toml" file was found, but editable '
'mode currently requires a setup.py based build.)'
)
raise InstallationError(msg)
# Treating it as code that has already been checked out
url_no_extras = path_to_url(url_no_extras)
@@ -116,6 +134,7 @@ def parse_editable(editable_req):
def deduce_helpful_msg(req):
# type: (str) -> str
"""Returns helpful msg in case requirements file does not exist,
or cannot be parsed.
@@ -136,7 +155,7 @@ def deduce_helpful_msg(req):
" the packages specified within it."
except RequirementParseError:
logger.debug("Cannot parse '%s' as requirements \
file" % (req), exc_info=1)
file" % (req), exc_info=True)
else:
msg += " File '%s' does not exist." % (req)
return msg
@@ -146,9 +165,15 @@ def deduce_helpful_msg(req):
def install_req_from_editable(
editable_req, comes_from=None, isolated=False, options=None,
wheel_cache=None, constraint=False
editable_req, # type: str
comes_from=None, # type: Optional[str]
use_pep517=None, # type: Optional[bool]
isolated=False, # type: bool
options=None, # type: Optional[Dict[str, Any]]
wheel_cache=None, # type: Optional[WheelCache]
constraint=False # type: bool
):
# type: (...) -> InstallRequirement
name, url, extras_override = parse_editable(editable_req)
if url.startswith('file:'):
source_dir = url_to_path(url)
@@ -167,6 +192,7 @@ def install_req_from_editable(
editable=True,
link=Link(url),
constraint=constraint,
use_pep517=use_pep517,
isolated=isolated,
options=options if options else {},
wheel_cache=wheel_cache,
@@ -175,9 +201,15 @@ def install_req_from_editable(
def install_req_from_line(
name, comes_from=None, isolated=False, options=None, wheel_cache=None,
constraint=False
name, # type: str
comes_from=None, # type: Optional[Union[str, InstallRequirement]]
use_pep517=None, # type: Optional[bool]
isolated=False, # type: bool
options=None, # type: Optional[Dict[str, Any]]
wheel_cache=None, # type: Optional[WheelCache]
constraint=False # type: bool
):
# type: (...) -> InstallRequirement
"""Creates an InstallRequirement from a name, which might be a
requirement, directory containing 'setup.py', filename, or URL.
"""
@@ -186,24 +218,24 @@ def install_req_from_line(
else:
marker_sep = ';'
if marker_sep in name:
name, markers = name.split(marker_sep, 1)
markers = markers.strip()
if not markers:
name, markers_as_string = name.split(marker_sep, 1)
markers_as_string = markers_as_string.strip()
if not markers_as_string:
markers = None
else:
markers = Marker(markers)
markers = Marker(markers_as_string)
else:
markers = None
name = name.strip()
req = None
req_as_string = None
path = os.path.normpath(os.path.abspath(name))
link = None
extras = None
extras_as_string = None
if is_url(name):
link = Link(name)
else:
p, extras = _strip_extras(path)
p, extras_as_string = _strip_extras(path)
looks_like_dir = os.path.isdir(p) and (
os.path.sep in name or
(os.path.altsep is not None and os.path.altsep in name) or
@@ -234,38 +266,41 @@ def install_req_from_line(
# wheel file
if link.is_wheel:
wheel = Wheel(link.filename) # can raise InvalidWheelFilename
req = "%s==%s" % (wheel.name, wheel.version)
req_as_string = "%s==%s" % (wheel.name, wheel.version)
else:
# set the req to the egg fragment. when it's not there, this
# will become an 'unnamed' requirement
req = link.egg_fragment
req_as_string = link.egg_fragment
# a requirement specifier
else:
req = name
req_as_string = name
if extras:
extras = Requirement("placeholder" + extras.lower()).extras
if extras_as_string:
extras = Requirement("placeholder" + extras_as_string.lower()).extras
else:
extras = ()
if req is not None:
if req_as_string is not None:
try:
req = Requirement(req)
req = Requirement(req_as_string)
except InvalidRequirement:
if os.path.sep in req:
if os.path.sep in req_as_string:
add_msg = "It looks like a path."
add_msg += deduce_helpful_msg(req)
elif '=' in req and not any(op in req for op in operators):
add_msg += deduce_helpful_msg(req_as_string)
elif ('=' in req_as_string and
not any(op in req_as_string for op in operators)):
add_msg = "= is not a valid operator. Did you mean == ?"
else:
add_msg = traceback.format_exc()
add_msg = ""
raise InstallationError(
"Invalid requirement: '%s'\n%s" % (req, add_msg)
"Invalid requirement: '%s'\n%s" % (req_as_string, add_msg)
)
else:
req = None
return InstallRequirement(
req, comes_from, link=link, markers=markers,
isolated=isolated,
use_pep517=use_pep517, isolated=isolated,
options=options if options else {},
wheel_cache=wheel_cache,
constraint=constraint,
@@ -273,11 +308,16 @@ def install_req_from_line(
)
def install_req_from_req(
req, comes_from=None, isolated=False, wheel_cache=None
def install_req_from_req_string(
req_string, # type: str
comes_from=None, # type: Optional[InstallRequirement]
isolated=False, # type: bool
wheel_cache=None, # type: Optional[WheelCache]
use_pep517=None # type: Optional[bool]
):
# type: (...) -> InstallRequirement
try:
req = Requirement(req)
req = Requirement(req_string)
except InvalidRequirement:
raise InstallationError("Invalid requirement: '%s'" % req)
@@ -294,5 +334,6 @@ def install_req_from_req(
)
return InstallRequirement(
req, comes_from, isolated=isolated, wheel_cache=wheel_cache
req, comes_from, isolated=isolated, wheel_cache=wheel_cache,
use_pep517=use_pep517
)
+64 -22
View File
@@ -19,6 +19,18 @@ from pipenv.patched.notpip._internal.exceptions import RequirementsFileParseErro
from pipenv.patched.notpip._internal.req.constructors import (
install_req_from_editable, install_req_from_line,
)
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import ( # noqa: F401
Iterator, Tuple, Optional, List, Callable, Text
)
from pipenv.patched.notpip._internal.req import InstallRequirement # noqa: F401
from pipenv.patched.notpip._internal.cache import WheelCache # noqa: F401
from pipenv.patched.notpip._internal.index import PackageFinder # noqa: F401
from pipenv.patched.notpip._internal.download import PipSession # noqa: F401
ReqFileLines = Iterator[Tuple[int, Text]]
__all__ = ['parse_requirements']
@@ -43,24 +55,32 @@ SUPPORTED_OPTIONS = [
cmdoptions.no_binary,
cmdoptions.only_binary,
cmdoptions.pre,
cmdoptions.process_dependency_links,
cmdoptions.trusted_host,
cmdoptions.require_hashes,
]
] # type: List[Callable[..., optparse.Option]]
# options to be passed to requirements
SUPPORTED_OPTIONS_REQ = [
cmdoptions.install_options,
cmdoptions.global_options,
cmdoptions.hash,
]
] # type: List[Callable[..., optparse.Option]]
# the 'dest' string values
SUPPORTED_OPTIONS_REQ_DEST = [o().dest for o in SUPPORTED_OPTIONS_REQ]
SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
def parse_requirements(filename, finder=None, comes_from=None, options=None,
session=None, constraint=False, wheel_cache=None):
def parse_requirements(
filename, # type: str
finder=None, # type: Optional[PackageFinder]
comes_from=None, # type: Optional[str]
options=None, # type: Optional[optparse.Values]
session=None, # type: Optional[PipSession]
constraint=False, # type: bool
wheel_cache=None, # type: Optional[WheelCache]
use_pep517=None # type: Optional[bool]
):
# type: (...) -> Iterator[InstallRequirement]
"""Parse a requirements file and yield InstallRequirement instances.
:param filename: Path or url of requirements file.
@@ -71,6 +91,7 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
:param constraint: If true, parsing a constraint file rather than
requirements file.
:param wheel_cache: Instance of pip.wheel.WheelCache
:param use_pep517: Value of the --use-pep517 option.
"""
if session is None:
raise TypeError(
@@ -87,18 +108,19 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
for line_number, line in lines_enum:
req_iter = process_line(line, filename, line_number, finder,
comes_from, options, session, wheel_cache,
constraint=constraint)
use_pep517=use_pep517, constraint=constraint)
for req in req_iter:
yield req
def preprocess(content, options):
# type: (Text, Optional[optparse.Values]) -> ReqFileLines
"""Split, filter, and join lines, and return a line iterator
:param content: the content of the requirements file
:param options: cli options
"""
lines_enum = enumerate(content.splitlines(), start=1)
lines_enum = enumerate(content.splitlines(), start=1) # type: ReqFileLines
lines_enum = join_lines(lines_enum)
lines_enum = ignore_comments(lines_enum)
lines_enum = skip_regex(lines_enum, options)
@@ -106,9 +128,19 @@ def preprocess(content, options):
return lines_enum
def process_line(line, filename, line_number, finder=None, comes_from=None,
options=None, session=None, wheel_cache=None,
constraint=False):
def process_line(
line, # type: Text
filename, # type: str
line_number, # type: int
finder=None, # type: Optional[PackageFinder]
comes_from=None, # type: Optional[str]
options=None, # type: Optional[optparse.Values]
session=None, # type: Optional[PipSession]
wheel_cache=None, # type: Optional[WheelCache]
use_pep517=None, # type: Optional[bool]
constraint=False # type: bool
):
# type: (...) -> Iterator[InstallRequirement]
"""Process a single requirements line; This can result in creating/yielding
requirements, or updating the finder.
@@ -130,13 +162,15 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
defaults = parser.get_default_values()
defaults.index_url = None
if finder:
# `finder.format_control` will be updated during parsing
defaults.format_control = finder.format_control
args_str, options_str = break_args_options(line)
# Prior to 2.7.3, shlex cannot deal with unicode entries
if sys.version_info < (2, 7, 3):
# Prior to 2.7.3, shlex cannot deal with unicode entries
options_str = options_str.encode('utf8')
opts, _ = parser.parse_args(shlex.split(options_str), defaults)
# https://github.com/python/mypy/issues/1174
options_str = options_str.encode('utf8') # type: ignore
# https://github.com/python/mypy/issues/1174
opts, _ = parser.parse_args(
shlex.split(options_str), defaults) # type: ignore
# preserve for the nested code path
line_comes_from = '%s %s (line %s)' % (
@@ -155,6 +189,7 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
req_options[dest] = opts.__dict__[dest]
yield install_req_from_line(
args_str, line_comes_from, constraint=constraint,
use_pep517=use_pep517,
isolated=isolated, options=req_options, wheel_cache=wheel_cache
)
@@ -163,6 +198,7 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
isolated = options.isolated_mode if options else False
yield install_req_from_editable(
opts.editables[0], comes_from=line_comes_from,
use_pep517=use_pep517,
constraint=constraint, isolated=isolated, wheel_cache=wheel_cache
)
@@ -183,11 +219,11 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
# do a join so relative paths work
req_path = os.path.join(os.path.dirname(filename), req_path)
# TODO: Why not use `comes_from='-r {} (line {})'` here as well?
parser = parse_requirements(
parsed_reqs = parse_requirements(
req_path, finder, comes_from, options, session,
constraint=nested_constraint, wheel_cache=wheel_cache
)
for req in parser:
for req in parsed_reqs:
yield req
# percolate hash-checking option upward
@@ -214,14 +250,13 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
finder.find_links.append(value)
if opts.pre:
finder.allow_all_prereleases = True
if opts.process_dependency_links:
finder.process_dependency_links = True
if opts.trusted_hosts:
finder.secure_origins.extend(
("*", host, "*") for host in opts.trusted_hosts)
def break_args_options(line):
# type: (Text) -> Tuple[str, Text]
"""Break up the line into an args and options string. We only want to shlex
(and then optparse) the options, not the args. args can contain markers
which are corrupted by shlex.
@@ -235,10 +270,11 @@ def break_args_options(line):
else:
args.append(token)
options.pop(0)
return ' '.join(args), ' '.join(options)
return ' '.join(args), ' '.join(options) # type: ignore
def build_parser(line):
# type: (Text) -> optparse.OptionParser
"""
Return a parser for parsing requirement lines
"""
@@ -255,17 +291,20 @@ def build_parser(line):
# add offending line
msg = 'Invalid requirement: %s\n%s' % (line, msg)
raise RequirementsFileParseError(msg)
parser.exit = parser_exit
# NOTE: mypy disallows assigning to a method
# https://github.com/python/mypy/issues/2427
parser.exit = parser_exit # type: ignore
return parser
def join_lines(lines_enum):
# type: (ReqFileLines) -> ReqFileLines
"""Joins a line ending in '\' with the previous line (except when following
comments). The joined line takes on the index of the first line.
"""
primary_line_number = None
new_line = []
new_line = [] # type: List[Text]
for line_number, line in lines_enum:
if not line.endswith('\\') or COMMENT_RE.match(line):
if COMMENT_RE.match(line):
@@ -290,6 +329,7 @@ def join_lines(lines_enum):
def ignore_comments(lines_enum):
# type: (ReqFileLines) -> ReqFileLines
"""
Strips comments and filter empty lines.
"""
@@ -301,6 +341,7 @@ def ignore_comments(lines_enum):
def skip_regex(lines_enum, options):
# type: (ReqFileLines, Optional[optparse.Values]) -> ReqFileLines
"""
Skip lines that match '--skip-requirements-regex' pattern
@@ -314,6 +355,7 @@ def skip_regex(lines_enum, options):
def expand_env_variables(lines_enum):
# type: (ReqFileLines) -> ReqFileLines
"""Replace all environment variables that can be retrieved via `os.getenv`.
The only allowed format for environment variables defined in the
@@ -22,7 +22,7 @@ from pipenv.patched.notpip._internal.locations import (
PIP_DELETE_MARKER_FILENAME, running_under_virtualenv,
)
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.pyproject import load_pyproject_toml
from pipenv.patched.notpip._internal.pyproject import load_pyproject_toml, make_pyproject_path
from pipenv.patched.notpip._internal.req.req_uninstall import UninstallPathSet
from pipenv.patched.notpip._internal.utils.compat import native_str
from pipenv.patched.notpip._internal.utils.hashes import Hashes
@@ -30,15 +30,28 @@ from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import (
_make_build_dir, ask_path_exists, backup_dir, call_subprocess,
display_path, dist_in_site_packages, dist_in_usersite, ensure_dir,
get_installed_version, rmtree,
get_installed_version, redact_password_from_url, rmtree,
)
from pipenv.patched.notpip._internal.utils.packaging import get_metadata
from pipenv.patched.notpip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.utils.ui import open_spinner
from pipenv.patched.notpip._internal.vcs import vcs
from pipenv.patched.notpip._internal.wheel import move_wheel_files
if MYPY_CHECK_RUNNING:
from typing import ( # noqa: F401
Optional, Iterable, List, Union, Any, Text, Sequence, Dict
)
from pipenv.patched.notpip._internal.build_env import BuildEnvironment # noqa: F401
from pipenv.patched.notpip._internal.cache import WheelCache # noqa: F401
from pipenv.patched.notpip._internal.index import PackageFinder # noqa: F401
from pipenv.patched.notpip._vendor.pkg_resources import Distribution # noqa: F401
from pipenv.patched.notpip._vendor.packaging.specifiers import SpecifierSet # noqa: F401
from pipenv.patched.notpip._vendor.packaging.markers import Marker # noqa: F401
logger = logging.getLogger(__name__)
@@ -49,10 +62,23 @@ class InstallRequirement(object):
installing the said requirement.
"""
def __init__(self, req, comes_from, source_dir=None, editable=False,
link=None, update=True, markers=None,
isolated=False, options=None, wheel_cache=None,
constraint=False, extras=()):
def __init__(
self,
req, # type: Optional[Requirement]
comes_from, # type: Optional[Union[str, InstallRequirement]]
source_dir=None, # type: Optional[str]
editable=False, # type: bool
link=None, # type: Optional[Link]
update=True, # type: bool
markers=None, # type: Optional[Marker]
use_pep517=None, # type: Optional[bool]
isolated=False, # type: bool
options=None, # type: Optional[Dict[str, Any]]
wheel_cache=None, # type: Optional[WheelCache]
constraint=False, # type: bool
extras=() # type: Iterable[str]
):
# type: (...) -> None
assert req is None or isinstance(req, Requirement), req
self.req = req
self.comes_from = comes_from
@@ -64,10 +90,10 @@ class InstallRequirement(object):
self.editable = editable
self._wheel_cache = wheel_cache
if link is not None:
self.link = self.original_link = link
else:
self.link = self.original_link = req and req.url and Link(req.url)
if link is None and req and req.url:
# PEP 508 URL requirement
link = Link(req.url)
self.link = self.original_link = link
if extras:
self.extras = extras
@@ -77,11 +103,11 @@ class InstallRequirement(object):
}
else:
self.extras = set()
if markers is not None:
self.markers = markers
else:
self.markers = req and req.marker
self._egg_info_path = None
if markers is None and req:
markers = req.marker
self.markers = markers
self._egg_info_path = None # type: Optional[str]
# This holds the pkg_resources.Distribution object if this requirement
# is already available:
self.satisfied_by = None
@@ -92,11 +118,11 @@ class InstallRequirement(object):
self._temp_build_dir = TempDirectory(kind="req-build")
# Used to store the global directory where the _temp_build_dir should
# have been created. Cf _correct_build_location method.
self._ideal_build_dir = None
self._ideal_build_dir = None # type: Optional[str]
# True if the editable should be updated:
self.update = update
# Set to True after successful installation
self.install_succeeded = None
self.install_succeeded = None # type: Optional[bool]
# UninstallPathSet of uninstalled distribution (for possible rollback)
self.uninstalled_pathset = None
self.options = options if options else {}
@@ -105,32 +131,37 @@ class InstallRequirement(object):
self.is_direct = False
self.isolated = isolated
self.build_env = NoOpBuildEnvironment()
self.build_env = NoOpBuildEnvironment() # type: BuildEnvironment
# For PEP 517, the directory where we request the project metadata
# gets stored. We need this to pass to build_wheel, so the backend
# can ensure that the wheel matches the metadata (see the PEP for
# details).
self.metadata_directory = None # type: Optional[str]
# The static build requirements (from pyproject.toml)
self.pyproject_requires = None
self.pyproject_requires = None # type: Optional[List[str]]
# Build requirements that we will check are available
# TODO: We don't do this for --no-build-isolation. Should we?
self.requirements_to_check = []
self.requirements_to_check = [] # type: List[str]
# The PEP 517 backend we should use to build the project
self.pep517_backend = None
self.pep517_backend = None # type: Optional[Pep517HookCaller]
# Are we using PEP 517 for this requirement?
# After pyproject.toml has been loaded, the only valid values are True
# and False. Before loading, None is valid (meaning "use the default").
# Setting an explicit value before loading pyproject.toml is supported,
# but after loading this flag should be treated as read only.
self.use_pep517 = None
self.use_pep517 = use_pep517
def __str__(self):
if self.req:
s = str(self.req)
if self.link:
s += ' from %s' % self.link.url
s += ' from %s' % redact_password_from_url(self.link.url)
elif self.link:
s = self.link.url
s = redact_password_from_url(self.link.url)
else:
s = '<InstallRequirement>'
if self.satisfied_by is not None:
@@ -149,6 +180,7 @@ class InstallRequirement(object):
self.__class__.__name__, str(self), self.editable)
def populate_link(self, finder, upgrade, require_hashes):
# type: (PackageFinder, bool, bool) -> None
"""Ensure that if a link can be found for this, that it is found.
Note that self.link may still be None - if Upgrade is False and the
@@ -171,16 +203,19 @@ class InstallRequirement(object):
# Things that are valid for all kinds of requirements?
@property
def name(self):
# type: () -> Optional[str]
if self.req is None:
return None
return native_str(pkg_resources.safe_name(self.req.name))
@property
def specifier(self):
# type: () -> SpecifierSet
return self.req.specifier
@property
def is_pinned(self):
# type: () -> bool
"""Return whether I am pinned to an exact version.
For example, some-package==1.2 is pinned; some-package>1.2 is not.
@@ -194,6 +229,7 @@ class InstallRequirement(object):
return get_installed_version(self.name)
def match_markers(self, extras_requested=None):
# type: (Optional[Iterable[str]]) -> bool
if not extras_requested:
# Provide an extra to safely evaluate the markers
# without matching any extra
@@ -207,6 +243,7 @@ class InstallRequirement(object):
@property
def has_hash_options(self):
# type: () -> bool
"""Return whether any known-good hashes are specified as options.
These activate --require-hashes mode; hashes specified as part of a
@@ -216,6 +253,7 @@ class InstallRequirement(object):
return bool(self.options.get('hashes', {}))
def hashes(self, trust_internet=True):
# type: (bool) -> Hashes
"""Return a hash-comparer that considers my option- and URL-based
hashes to be known-good.
@@ -237,6 +275,7 @@ class InstallRequirement(object):
return Hashes(good_hashes)
def from_path(self):
# type: () -> Optional[str]
"""Format a nice indicator to show where this "comes from"
"""
if self.req is None:
@@ -252,6 +291,7 @@ class InstallRequirement(object):
return s
def build_location(self, build_dir):
# type: (str) -> Optional[str]
assert build_dir is not None
if self._temp_build_dir.path is not None:
return self._temp_build_dir.path
@@ -279,6 +319,7 @@ class InstallRequirement(object):
return os.path.join(build_dir, name)
def _correct_build_location(self):
# type: () -> None
"""Move self._temp_build_dir to self._ideal_build_dir/self.req.name
For some requirements (e.g. a path to a directory), the name of the
@@ -292,7 +333,8 @@ class InstallRequirement(object):
return
assert self.req is not None
assert self._temp_build_dir.path
assert self._ideal_build_dir.path
assert (self._ideal_build_dir is not None and
self._ideal_build_dir.path) # type: ignore
old_location = self._temp_build_dir.path
self._temp_build_dir.path = None
@@ -311,7 +353,16 @@ class InstallRequirement(object):
self.source_dir = os.path.normpath(os.path.abspath(new_location))
self._egg_info_path = None
# Correct the metadata directory, if it exists
if self.metadata_directory:
old_meta = self.metadata_directory
rel = os.path.relpath(old_meta, start=old_location)
new_meta = os.path.join(new_location, rel)
new_meta = os.path.normpath(os.path.abspath(new_meta))
self.metadata_directory = new_meta
def remove_temporary_source(self):
# type: () -> None
"""Remove the source files from this requirement, if they are marked
for deletion"""
if self.source_dir and os.path.exists(
@@ -323,6 +374,7 @@ class InstallRequirement(object):
self.build_env.cleanup()
def check_if_exists(self, use_user_site):
# type: (bool) -> bool
"""Find an installed distribution that satisfies or conflicts
with this requirement, and set self.satisfied_by or
self.conflicts_with appropriately.
@@ -366,11 +418,22 @@ class InstallRequirement(object):
# Things valid for wheels
@property
def is_wheel(self):
return self.link and self.link.is_wheel
# type: () -> bool
if not self.link:
return False
return self.link.is_wheel
def move_wheel_files(self, wheeldir, root=None, home=None, prefix=None,
warn_script_location=True, use_user_site=False,
pycompile=True):
def move_wheel_files(
self,
wheeldir, # type: str
root=None, # type: Optional[str]
home=None, # type: Optional[str]
prefix=None, # type: Optional[str]
warn_script_location=True, # type: bool
use_user_site=False, # type: bool
pycompile=True # type: bool
):
# type: (...) -> None
move_wheel_files(
self.name, self.req, wheeldir,
user=use_user_site,
@@ -385,12 +448,14 @@ class InstallRequirement(object):
# Things valid for sdists
@property
def setup_py_dir(self):
# type: () -> str
return os.path.join(
self.source_dir,
self.link and self.link.subdirectory_fragment or '')
@property
def setup_py(self):
# type: () -> str
assert self.source_dir, "No source dir for %s" % self
setup_py = os.path.join(self.setup_py_dir, 'setup.py')
@@ -403,17 +468,13 @@ class InstallRequirement(object):
@property
def pyproject_toml(self):
# type: () -> str
assert self.source_dir, "No source dir for %s" % self
pp_toml = os.path.join(self.setup_py_dir, 'pyproject.toml')
# Python2 __file__ should not be unicode
if six.PY2 and isinstance(pp_toml, six.text_type):
pp_toml = pp_toml.encode(sys.getfilesystemencoding())
return pp_toml
return make_pyproject_path(self.setup_py_dir)
def load_pyproject_toml(self):
# type: () -> None
"""Load the pyproject.toml file.
After calling this routine, all of the attributes related to PEP 517
@@ -437,41 +498,36 @@ class InstallRequirement(object):
self.pyproject_requires = requires
self.pep517_backend = Pep517HookCaller(self.setup_py_dir, backend)
def run_egg_info(self):
# Use a custom function to call subprocesses
self.spin_message = ""
def runner(cmd, cwd=None, extra_environ=None):
with open_spinner(self.spin_message) as spinner:
call_subprocess(
cmd,
cwd=cwd,
extra_environ=extra_environ,
show_stdout=False,
spinner=spinner
)
self.spin_message = ""
self.pep517_backend._subprocess_runner = runner
def prepare_metadata(self):
# type: () -> None
"""Ensure that project metadata is available.
Under PEP 517, call the backend hook to prepare the metadata.
Under legacy processing, call setup.py egg-info.
"""
assert self.source_dir
if self.name:
logger.debug(
'Running setup.py (path:%s) egg_info for package %s',
self.setup_py, self.name,
)
else:
logger.debug(
'Running setup.py (path:%s) egg_info for package from %s',
self.setup_py, self.link,
)
with indent_log():
script = SETUPTOOLS_SHIM % self.setup_py
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
base_cmd = [sys_executable, '-c', script]
if self.isolated:
base_cmd += ["--no-user-cfg"]
egg_info_cmd = base_cmd + ['egg_info']
# We can't put the .egg-info files at the root, because then the
# source code will be mistaken for an installed egg, causing
# problems
if self.editable:
egg_base_option = []
if self.use_pep517:
self.prepare_pep517_metadata()
else:
egg_info_dir = os.path.join(self.setup_py_dir, 'pip-egg-info')
ensure_dir(egg_info_dir)
egg_base_option = ['--egg-base', 'pip-egg-info']
with self.build_env:
call_subprocess(
egg_info_cmd + egg_base_option,
cwd=self.setup_py_dir,
show_stdout=False,
command_desc='python setup.py egg_info')
self.run_egg_info()
if not self.req:
if isinstance(parse_version(self.metadata["Version"]), Version):
@@ -490,15 +546,72 @@ class InstallRequirement(object):
metadata_name = canonicalize_name(self.metadata["Name"])
if canonicalize_name(self.req.name) != metadata_name:
logger.warning(
'Running setup.py (path:%s) egg_info for package %s '
'Generating metadata for package %s '
'produced metadata for project name %s. Fix your '
'#egg=%s fragments.',
self.setup_py, self.name, metadata_name, self.name
self.name, metadata_name, self.name
)
self.req = Requirement(metadata_name)
def prepare_pep517_metadata(self):
# type: () -> None
assert self.pep517_backend is not None
metadata_dir = os.path.join(
self.setup_py_dir,
'pip-wheel-metadata'
)
ensure_dir(metadata_dir)
with self.build_env:
# Note that Pep517HookCaller implements a fallback for
# prepare_metadata_for_build_wheel, so we don't have to
# consider the possibility that this hook doesn't exist.
backend = self.pep517_backend
self.spin_message = "Preparing wheel metadata"
distinfo_dir = backend.prepare_metadata_for_build_wheel(
metadata_dir
)
self.metadata_directory = os.path.join(metadata_dir, distinfo_dir)
def run_egg_info(self):
# type: () -> None
if self.name:
logger.debug(
'Running setup.py (path:%s) egg_info for package %s',
self.setup_py, self.name,
)
else:
logger.debug(
'Running setup.py (path:%s) egg_info for package from %s',
self.setup_py, self.link,
)
script = SETUPTOOLS_SHIM % self.setup_py
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
base_cmd = [sys_executable, '-c', script]
if self.isolated:
base_cmd += ["--no-user-cfg"]
egg_info_cmd = base_cmd + ['egg_info']
# We can't put the .egg-info files at the root, because then the
# source code will be mistaken for an installed egg, causing
# problems
if self.editable:
egg_base_option = [] # type: List[str]
else:
egg_info_dir = os.path.join(self.setup_py_dir, 'pip-egg-info')
ensure_dir(egg_info_dir)
egg_base_option = ['--egg-base', 'pip-egg-info']
with self.build_env:
call_subprocess(
egg_info_cmd + egg_base_option,
cwd=self.setup_py_dir,
show_stdout=False,
command_desc='python setup.py egg_info')
@property
def egg_info_path(self):
# type: () -> str
if self._egg_info_path is None:
if self.editable:
base = self.source_dir
@@ -557,18 +670,31 @@ class InstallRequirement(object):
return self._metadata
def get_dist(self):
"""Return a pkg_resources.Distribution built from self.egg_info_path"""
egg_info = self.egg_info_path.rstrip(os.path.sep)
base_dir = os.path.dirname(egg_info)
metadata = pkg_resources.PathMetadata(base_dir, egg_info)
dist_name = os.path.splitext(os.path.basename(egg_info))[0]
return pkg_resources.Distribution(
os.path.dirname(egg_info),
# type: () -> Distribution
"""Return a pkg_resources.Distribution for this requirement"""
if self.metadata_directory:
base_dir, distinfo = os.path.split(self.metadata_directory)
metadata = pkg_resources.PathMetadata(
base_dir, self.metadata_directory
)
dist_name = os.path.splitext(distinfo)[0]
typ = pkg_resources.DistInfoDistribution
else:
egg_info = self.egg_info_path.rstrip(os.path.sep)
base_dir = os.path.dirname(egg_info)
metadata = pkg_resources.PathMetadata(base_dir, egg_info)
dist_name = os.path.splitext(os.path.basename(egg_info))[0]
# https://github.com/python/mypy/issues/1174
typ = pkg_resources.Distribution # type: ignore
return typ(
base_dir,
project_name=dist_name,
metadata=metadata,
)
def assert_source_matches_version(self):
# type: () -> None
assert self.source_dir
version = self.metadata['version']
if self.req.specifier and version not in self.req.specifier:
@@ -587,6 +713,7 @@ class InstallRequirement(object):
# For both source distributions and editables
def ensure_has_source_dir(self, parent_dir):
# type: (str) -> str
"""Ensure that a source_dir is set.
This will create a temporary build dir if the name of the requirement
@@ -601,8 +728,13 @@ class InstallRequirement(object):
return self.source_dir
# For editable installations
def install_editable(self, install_options,
global_options=(), prefix=None):
def install_editable(
self,
install_options, # type: List[str]
global_options=(), # type: Sequence[str]
prefix=None # type: Optional[str]
):
# type: (...) -> None
logger.info('Running setup.py develop for %s', self.name)
if self.isolated:
@@ -614,8 +746,8 @@ class InstallRequirement(object):
with indent_log():
# FIXME: should we do --install-headers here too?
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
with self.build_env:
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
call_subprocess(
[
sys_executable,
@@ -633,6 +765,7 @@ class InstallRequirement(object):
self.install_succeeded = True
def update_editable(self, obtain=True):
# type: (bool) -> None
if not self.link:
logger.debug(
"Cannot update repository at %s; repository location is "
@@ -664,6 +797,7 @@ class InstallRequirement(object):
# Top-level Actions
def uninstall(self, auto_confirm=False, verbose=False,
use_user_site=False):
# type: (bool, bool, bool) -> Optional[UninstallPathSet]
"""
Uninstall the distribution currently satisfying this requirement.
@@ -678,7 +812,7 @@ class InstallRequirement(object):
"""
if not self.check_if_exists(use_user_site):
logger.warning("Skipping %s as it is not installed.", self.name)
return
return None
dist = self.satisfied_by or self.conflicts_with
uninstalled_pathset = UninstallPathSet.from_dist(dist)
@@ -693,9 +827,16 @@ class InstallRequirement(object):
name = name.replace(os.path.sep, '/')
return name
def _get_archive_name(self, path, parentdir, rootdir):
# type: (str, str, str) -> str
path = os.path.join(parentdir, path)
name = self._clean_zip_name(path, rootdir)
return self.name + '/' + name
# TODO: Investigate if this should be kept in InstallRequirement
# Seems to be used only when VCS + downloads
def archive(self, build_dir):
# type: (str) -> None
assert self.source_dir
create_archive = True
archive_name = '%s-%s.zip' % (self.name, self.metadata["version"])
@@ -729,23 +870,35 @@ class InstallRequirement(object):
if 'pip-egg-info' in dirnames:
dirnames.remove('pip-egg-info')
for dirname in dirnames:
dirname = os.path.join(dirpath, dirname)
name = self._clean_zip_name(dirname, dir)
zipdir = zipfile.ZipInfo(self.name + '/' + name + '/')
dir_arcname = self._get_archive_name(dirname,
parentdir=dirpath,
rootdir=dir)
zipdir = zipfile.ZipInfo(dir_arcname + '/')
zipdir.external_attr = 0x1ED << 16 # 0o755
zip.writestr(zipdir, '')
for filename in filenames:
if filename == PIP_DELETE_MARKER_FILENAME:
continue
file_arcname = self._get_archive_name(filename,
parentdir=dirpath,
rootdir=dir)
filename = os.path.join(dirpath, filename)
name = self._clean_zip_name(filename, dir)
zip.write(filename, self.name + '/' + name)
zip.write(filename, file_arcname)
zip.close()
logger.info('Saved %s', display_path(archive_path))
def install(self, install_options, global_options=None, root=None,
home=None, prefix=None, warn_script_location=True,
use_user_site=False, pycompile=True):
def install(
self,
install_options, # type: List[str]
global_options=None, # type: Optional[Sequence[str]]
root=None, # type: Optional[str]
home=None, # type: Optional[str]
prefix=None, # type: Optional[str]
warn_script_location=True, # type: bool
use_user_site=False, # type: bool
pycompile=True # type: bool
):
# type: (...) -> None
global_options = global_options if global_options is not None else []
if self.editable:
self.install_editable(
@@ -775,7 +928,8 @@ class InstallRequirement(object):
self.options.get('install_options', [])
if self.isolated:
global_options = global_options + ["--no-user-cfg"]
# https://github.com/python/mypy/issues/1174
global_options = global_options + ["--no-user-cfg"] # type: ignore
with TempDirectory(kind="record") as temp_dir:
record_filename = os.path.join(temp_dir.path, 'install-record.txt')
@@ -834,8 +988,15 @@ class InstallRequirement(object):
with open(inst_files_path, 'w') as f:
f.write('\n'.join(new_lines) + '\n')
def get_install_args(self, global_options, record_filename, root, prefix,
pycompile):
def get_install_args(
self,
global_options, # type: Sequence[str]
record_filename, # type: str
root, # type: Optional[str]
prefix, # type: Optional[str]
pycompile # type: bool
):
# type: (...) -> List[str]
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
install_args = [sys_executable, "-u"]
install_args.append('-c')
+24 -8
View File
@@ -5,29 +5,36 @@ from collections import OrderedDict
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from pipenv.patched.notpip._internal.wheel import Wheel
if MYPY_CHECK_RUNNING:
from typing import Optional, List, Tuple, Dict, Iterable # noqa: F401
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement # noqa: F401
logger = logging.getLogger(__name__)
class RequirementSet(object):
def __init__(self, require_hashes=False, check_supported_wheels=True, ignore_compatibility=True):
# type: (bool, bool) -> None
"""Create a RequirementSet.
"""
self.requirements = OrderedDict()
self.requirements = OrderedDict() # type: Dict[str, InstallRequirement] # noqa: E501
self.require_hashes = require_hashes
self.check_supported_wheels = check_supported_wheels
if ignore_compatibility:
self.check_supported_wheels = False
self.ignore_compatibility = True if (check_supported_wheels is False or ignore_compatibility is True) else False
self.ignore_compatibility = (check_supported_wheels is False or ignore_compatibility is True)
# Mapping of alias: real_name
self.requirement_aliases = {}
self.unnamed_requirements = []
self.successfully_downloaded = []
self.reqs_to_cleanup = []
self.requirement_aliases = {} # type: Dict[str, str]
self.unnamed_requirements = [] # type: List[InstallRequirement]
self.successfully_downloaded = [] # type: List[InstallRequirement]
self.reqs_to_cleanup = [] # type: List[InstallRequirement]
def __str__(self):
reqs = [req for req in self.requirements.values()
@@ -42,8 +49,13 @@ class RequirementSet(object):
return ('<%s object; %d requirement(s): %s>'
% (self.__class__.__name__, len(reqs), reqs_str))
def add_requirement(self, install_req, parent_req_name=None,
extras_requested=None):
def add_requirement(
self,
install_req, # type: InstallRequirement
parent_req_name=None, # type: Optional[str]
extras_requested=None # type: Optional[Iterable[str]]
):
# type: (...) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]] # noqa: E501
"""Add install_req as a requirement to install.
:param parent_req_name: The name of the requirement that needed this
@@ -155,6 +167,7 @@ class RequirementSet(object):
return [existing_req], existing_req
def has_requirement(self, project_name):
# type: (str) -> bool
name = project_name.lower()
if (name in self.requirements and
not self.requirements[name].constraint or
@@ -165,10 +178,12 @@ class RequirementSet(object):
@property
def has_requirements(self):
# type: () -> List[InstallRequirement]
return list(req for req in self.requirements.values() if not
req.constraint) or self.unnamed_requirements
def get_requirement(self, project_name):
# type: (str) -> InstallRequirement
for name in project_name, project_name.lower():
if name in self.requirements:
return self.requirements[name]
@@ -177,6 +192,7 @@ class RequirementSet(object):
pass
def cleanup_files(self):
# type: () -> None
"""Clean up files, remove builds."""
logger.debug('Cleaning up...')
with indent_log():
@@ -7,6 +7,12 @@ import logging
import os
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Set, Iterator # noqa: F401
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement # noqa: F401
from pipenv.patched.notpip._internal.models.link import Link # noqa: F401
logger = logging.getLogger(__name__)
@@ -14,6 +20,7 @@ logger = logging.getLogger(__name__)
class RequirementTracker(object):
def __init__(self):
# type: () -> None
self._root = os.environ.get('PIP_REQ_TRACKER')
if self._root is None:
self._temp_dir = TempDirectory(delete=False, kind='req-tracker')
@@ -23,7 +30,7 @@ class RequirementTracker(object):
else:
self._temp_dir = None
logger.debug('Re-using requirements tracker %r', self._root)
self._entries = set()
self._entries = set() # type: Set[InstallRequirement]
def __enter__(self):
return self
@@ -32,10 +39,12 @@ class RequirementTracker(object):
self.cleanup()
def _entry_path(self, link):
# type: (Link) -> str
hashed = hashlib.sha224(link.url_without_fragment.encode()).hexdigest()
return os.path.join(self._root, hashed)
def add(self, req):
# type: (InstallRequirement) -> None
link = req.link
info = str(req)
entry_path = self._entry_path(link)
@@ -54,12 +63,14 @@ class RequirementTracker(object):
logger.debug('Added %s to build tracker %r', req, self._root)
def remove(self, req):
# type: (InstallRequirement) -> None
link = req.link
self._entries.remove(req)
os.unlink(self._entry_path(link))
logger.debug('Removed %s from build tracker %r', req, self._root)
def cleanup(self):
# type: () -> None
for req in set(self._entries):
self.remove(req)
remove = self._temp_dir is not None
@@ -71,6 +82,7 @@ class RequirementTracker(object):
@contextlib.contextmanager
def track(self, req):
# type: (InstallRequirement) -> Iterator[None]
self.add(req)
yield
self.remove(req)
@@ -15,9 +15,9 @@ from pipenv.patched.notpip._internal.utils.compat import WINDOWS, cache_from_sou
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import (
FakeFile, ask, dist_in_usersite, dist_is_local, egg_link_path, is_local,
normalize_path, renames,
normalize_path, renames, rmtree,
)
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.notpip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
logger = logging.getLogger(__name__)
@@ -86,16 +86,54 @@ def compact(paths):
sep = os.path.sep
short_paths = set()
for path in sorted(paths, key=len):
should_add = any(
should_skip = any(
path.startswith(shortpath.rstrip("*")) and
path[len(shortpath.rstrip("*").rstrip(sep))] == sep
for shortpath in short_paths
)
if not should_add:
if not should_skip:
short_paths.add(path)
return short_paths
def compress_for_rename(paths):
"""Returns a set containing the paths that need to be renamed.
This set may include directories when the original sequence of paths
included every file on disk.
"""
case_map = dict((os.path.normcase(p), p) for p in paths)
remaining = set(case_map)
unchecked = sorted(set(os.path.split(p)[0]
for p in case_map.values()), key=len)
wildcards = set()
def norm_join(*a):
return os.path.normcase(os.path.join(*a))
for root in unchecked:
if any(os.path.normcase(root).startswith(w)
for w in wildcards):
# This directory has already been handled.
continue
all_files = set()
all_subdirs = set()
for dirname, subdirs, files in os.walk(root):
all_subdirs.update(norm_join(root, dirname, d)
for d in subdirs)
all_files.update(norm_join(root, dirname, f)
for f in files)
# If all the files we found are in our remaining set of files to
# remove, then remove them from the latter set and add a wildcard
# for the directory.
if not (all_files - remaining):
remaining.difference_update(all_files)
wildcards.add(root + os.sep)
return set(map(case_map.__getitem__, remaining)) | wildcards
def compress_for_output_listing(paths):
"""Returns a tuple of 2 sets of which paths to display to user
@@ -145,6 +183,111 @@ def compress_for_output_listing(paths):
return will_remove, will_skip
class StashedUninstallPathSet(object):
"""A set of file rename operations to stash files while
tentatively uninstalling them."""
def __init__(self):
# Mapping from source file root to [Adjacent]TempDirectory
# for files under that directory.
self._save_dirs = {}
# (old path, new path) tuples for each move that may need
# to be undone.
self._moves = []
def _get_directory_stash(self, path):
"""Stashes a directory.
Directories are stashed adjacent to their original location if
possible, or else moved/copied into the user's temp dir."""
try:
save_dir = AdjacentTempDirectory(path)
save_dir.create()
except OSError:
save_dir = TempDirectory(kind="uninstall")
save_dir.create()
self._save_dirs[os.path.normcase(path)] = save_dir
return save_dir.path
def _get_file_stash(self, path):
"""Stashes a file.
If no root has been provided, one will be created for the directory
in the user's temp directory."""
path = os.path.normcase(path)
head, old_head = os.path.dirname(path), None
save_dir = None
while head != old_head:
try:
save_dir = self._save_dirs[head]
break
except KeyError:
pass
head, old_head = os.path.dirname(head), head
else:
# Did not find any suitable root
head = os.path.dirname(path)
save_dir = TempDirectory(kind='uninstall')
save_dir.create()
self._save_dirs[head] = save_dir
relpath = os.path.relpath(path, head)
if relpath and relpath != os.path.curdir:
return os.path.join(save_dir.path, relpath)
return save_dir.path
def stash(self, path):
"""Stashes the directory or file and returns its new location.
"""
if os.path.isdir(path):
new_path = self._get_directory_stash(path)
else:
new_path = self._get_file_stash(path)
self._moves.append((path, new_path))
if os.path.isdir(path) and os.path.isdir(new_path):
# If we're moving a directory, we need to
# remove the destination first or else it will be
# moved to inside the existing directory.
# We just created new_path ourselves, so it will
# be removable.
os.rmdir(new_path)
renames(path, new_path)
return new_path
def commit(self):
"""Commits the uninstall by removing stashed files."""
for _, save_dir in self._save_dirs.items():
save_dir.cleanup()
self._moves = []
self._save_dirs = {}
def rollback(self):
"""Undoes the uninstall by moving stashed files back."""
for p in self._moves:
logging.info("Moving to %s\n from %s", *p)
for new_path, path in self._moves:
try:
logger.debug('Replacing %s from %s', new_path, path)
if os.path.isfile(new_path):
os.unlink(new_path)
elif os.path.isdir(new_path):
rmtree(new_path)
renames(path, new_path)
except OSError as ex:
logger.error("Failed to restore %s", new_path)
logger.debug("Exception: %s", ex)
self.commit()
@property
def can_rollback(self):
return bool(self._moves)
class UninstallPathSet(object):
"""A set of file paths to be removed in the uninstallation of a
requirement."""
@@ -153,8 +296,7 @@ class UninstallPathSet(object):
self._refuse = set()
self.pth = {}
self.dist = dist
self.save_dir = TempDirectory(kind="uninstall")
self._moved_paths = []
self._moved_paths = StashedUninstallPathSet()
def _permitted(self, path):
"""
@@ -192,11 +334,6 @@ class UninstallPathSet(object):
else:
self._refuse.add(pth_file)
def _stash(self, path):
return os.path.join(
self.save_dir.path, os.path.splitdrive(path)[1].lstrip(os.path.sep)
)
def remove(self, auto_confirm=False, verbose=False):
"""Remove paths in ``self.paths`` with confirmation (unless
``auto_confirm`` is True)."""
@@ -215,13 +352,14 @@ class UninstallPathSet(object):
with indent_log():
if auto_confirm or self._allowed_to_proceed(verbose):
self.save_dir.create()
moved = self._moved_paths
for path in sorted(compact(self.paths)):
new_path = self._stash(path)
for_rename = compress_for_rename(self.paths)
for path in sorted(compact(for_rename)):
moved.stash(path)
logger.debug('Removing file or directory %s', path)
self._moved_paths.append(path)
renames(path, new_path)
for pth in self.pth.values():
pth.remove()
@@ -251,29 +389,27 @@ class UninstallPathSet(object):
_display('Would remove:', will_remove)
_display('Would not remove (might be manually added):', will_skip)
_display('Would not remove (outside of prefix):', self._refuse)
if verbose:
_display('Will actually move:', compress_for_rename(self.paths))
return ask('Proceed (y/n)? ', ('y', 'n')) == 'y'
def rollback(self):
"""Rollback the changes previously made by remove()."""
if self.save_dir.path is None:
if not self._moved_paths.can_rollback:
logger.error(
"Can't roll back %s; was not uninstalled",
self.dist.project_name,
)
return False
logger.info('Rolling back uninstall of %s', self.dist.project_name)
for path in self._moved_paths:
tmp_path = self._stash(path)
logger.debug('Replacing %s', path)
renames(tmp_path, path)
self._moved_paths.rollback()
for pth in self.pth.values():
pth.rollback()
def commit(self):
"""Remove temporary save dir: rollback will no longer be possible."""
self.save_dir.cleanup()
self._moved_paths = []
self._moved_paths.commit()
@classmethod
def from_dist(cls, dist):

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