mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 22:50:18 +00:00
Merge branch 'master' into fix-docs-mobile
This commit is contained in:
@@ -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/*
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -21,4 +21,4 @@ trigger:
|
||||
jobs:
|
||||
- template: jobs/test.yml
|
||||
parameters:
|
||||
vmImage: vs2017-win2016
|
||||
vmImage: windows-2019
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
✨🍰✨
|
||||
|
||||
|
||||
@@ -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
|
||||
Vendored
+21
-1
@@ -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>
|
||||
|
||||
|
||||
Vendored
+21
@@ -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
@@ -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
@@ -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*.
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Make conservative checks of known exceptions when subprocess returns output, so user won't see the whole traceback - just the error.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed a bug which caused editable package resolution to sometimes fail with an unhelpful setuptools-related error message.
|
||||
@@ -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``.
|
||||
@@ -0,0 +1 @@
|
||||
Dependency resolution now writes hashes for local and remote files to the lockfile.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed a bug which prevented ``pipenv graph`` from correctly showing all dependencies when running from within ``pipenv shell``.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed resolution of direct-url dependencies in ``setup.py`` files to respect ``PEP-508`` style URL dependencies.
|
||||
@@ -0,0 +1 @@
|
||||
Added support for resolution of direct-url dependencies in ``setup.py`` files to respect ``PEP-508`` style URL dependencies.
|
||||
@@ -0,0 +1 @@
|
||||
Update pytest-pypi documentation not to be pytest-httpbin documentation.
|
||||
@@ -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``.
|
||||
@@ -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.
|
||||
@@ -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**.
|
||||
@@ -0,0 +1 @@
|
||||
Pipenv will now successfully recursively lock VCS sub-dependencies.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Do not touch Pipfile early and rely on it so that one can do ``pipenv sync`` without a Pipfile.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed a keyerror which could occur when locking VCS dependencies in some cases.
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Improve the error message when one tries to initialize a Pipenv project under ``/``.
|
||||
@@ -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 @@
|
||||
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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Replace (non-existant) video on docs index.rst with equivalent gif.
|
||||
@@ -0,0 +1 @@
|
||||
Fix bug causing ``[SSL: CERTIFICATE_VERIFY_FAILED]`` when Pipfile ``[[source]]`` has verify_ssl=false and url with custom port.
|
||||
@@ -0,0 +1 @@
|
||||
Clarify wording in Basic Usage example on using double quotes to escape shell redirection
|
||||
@@ -0,0 +1 @@
|
||||
Fix the issue that lock file can't be created when ``PIPENV_PIPFILE`` is not under working directory.
|
||||
@@ -0,0 +1 @@
|
||||
Added the ability for Windows users to enable emojis by setting ``PIPENV_HIDE_EMOJIS=0``.
|
||||
@@ -0,0 +1 @@
|
||||
Removed unused vendored package shutilwhich
|
||||
@@ -0,0 +1 @@
|
||||
Added a link to the TOML Spec under General Recommendations & Version Control to clarify how Pipfiles should be written.
|
||||
@@ -0,0 +1 @@
|
||||
Removed unused vendored package blindspin
|
||||
@@ -0,0 +1 @@
|
||||
Allow KeyboardInterrupt to cancel test suite checks for working internet and ssh
|
||||
@@ -0,0 +1 @@
|
||||
Cleaned up some conditional logic that would always evaluate ``True``.
|
||||
@@ -0,0 +1 @@
|
||||
Add installation instructions for Debian Buster+ in README
|
||||
@@ -0,0 +1 @@
|
||||
Update pytest configuration to support pytest 4.
|
||||
@@ -0,0 +1 @@
|
||||
Normalize the package names to lowercase when comparing used and in-Pipfile packages.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 @@
|
||||
__version__ = "18.1"
|
||||
__version__ = "19.0.3"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user