Merge branch 'master' into unused-vendors

This commit is contained in:
Dan Ryan
2019-05-19 22:01:02 -04:00
committed by GitHub
175 changed files with 10769 additions and 3668 deletions
+1 -1
View File
@@ -11,5 +11,5 @@ steps:
export GIT_SSL_CAINFO=$(python -m certifi)
export LANG=C.UTF-8
python -m pip install --upgrade setuptools twine readme_renderer[md]
python setup.py sdist
python setup.py sdist bdist_wheel
twine check dist/*
+9 -2
View File
@@ -13,6 +13,13 @@ steps:
- 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()
+3 -8
View File
@@ -9,14 +9,9 @@ jobs:
strategy:
maxParallel: 4
matrix:
${{ if eq(parameters.vmImage, 'vs2017-win2016') }}:
Python37:
python.version: '>= 3.7.2'
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)
+6 -28
View File
@@ -12,37 +12,15 @@ jobs:
Python27:
python.version: '2.7'
python.architecture: x64
${{ if eq(parameters.vmImage, 'vs2017-win2016') }}:
# TODO remove once vs2017-win2016 has Python 3.7
Python37:
python.version: '>= 3.7.2'
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
@@ -9,6 +9,8 @@ steps:
echo "Path $PATH"
echo "Installing Pipenv…"
pipenv install --deploy --dev
pipenv run pip install -e "$(pwd)[test]" --upgrade
pipenv run pip install -e "$(pwd)[tests]" --upgrade
echo pipenv --venv && echo pipenv --py && echo pipenv run python --version
displayName: Make Virtualenv
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
@@ -0,0 +1,26 @@
steps:
- script: |
echo "##vso[task.setvariable variable=LANG]C.UTF-8"
echo "##vso[task.setvariable variable=PIP_PROCESS_DEPENDENCY_LINKS]1"
displayName: Set Environment Variables
- powershell: |
pip install certifi
$env:PYTHON_PATH=$(python -c "import sys; print(sys.executable)")
$env:CERTIFI_CONTENT=$(python -m certifi)
echo "##vso[task.setvariable variable=GIT_SSL_CAINFO]$env:CERTIFI_CONTENT"
echo "##vso[task.setvariable variable=PY_EXE]$env:PYTHON_PATH"
displayName: Set Python Path
env:
PYTHONWARNINGS: 'ignore:DEPRECATION'
- script: |
echo "Python path: $(PY_EXE)"
echo "GIT_SSL_CAINFO: $(GIT_SSL_CAINFO)"
$(PY_EXE) -m pipenv install --deploy --dev
env:
PIPENV_DEFAULT_PYTHON_VERSION: '$(PIPENV_DEFAULT_PYTHON_VERSION)'
PYTHONWARNINGS: 'ignore:DEPRECATION'
PIPENV_NOSPIN: '1'
displayName: Make Virtualenv
+34 -28
View File
@@ -1,31 +1,37 @@
steps:
- powershell: |
$env:PY_EXE=$(python -c "import sys; print(sys.executable)")
if (!$env:PY_EXE) {
$env:PY_EXE="python"
}
Write-Host "##vso[task.setvariable variable=PY_EXE]$env:PY_EXE"
Write-Host "Found Python: $env:PY_EXE"
Invoke-Expression "& '$env:PY_EXE' -m virtualenv D:\.venv"
Write-Host "##vso[task.setvariable variable=VIRTUAL_ENV]D:\.venv"
Invoke-Expression "& 'D:\.venv\Scripts\activate.ps1'"
$env:VIRTUAL_ENV="D:\.venv"
Write-Host "Installing local package..."
Invoke-Expression "& '$env:PY_EXE' -m pip install -e .[test] --upgrade"
Write-Host "upgrading local package in virtual env"
$venv_scripts = Join-Path -path D:\.venv -childpath Scripts
$venv_py = Join-Path -path $venv_scripts -childpath python.exe
Write-Host "##vso[task.setvariable variable=VIRTUAL_ENV_PY]$venv_py"
Invoke-Expression "& '$venv_py' -m pip install -e .[test] --upgrade"
Write-Host "Installing pipenv development packages"
Invoke-Expression "& '$venv_py' -m pipenv install --dev"
Write-Host "Installing local package in pipenv environment"
Invoke-Expression "& '$venv_py' -m pipenv run pip install -e .[test]"
Write-Host "Printing metadata"
Write-Host $(Invoke-Expression "& '$venv_py' -m pipenv --venv")
Write-Host $(Invoke-Expression "& '$venv_py' -m pipenv --py")
Write-Host $(Invoke-Expression "& '$venv_py' -m pipenv run python --version")
displayName: Make Virtualenv
- script: |
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)
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 setuptools wheel && python -m pip install -e .[test] --upgrade'
- 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'
+19 -19
View File
@@ -1,24 +1,24 @@
steps:
- powershell: |
# Fix Git SSL errors
Invoke-Expression "& '$env:VIRTUAL_ENV_PY' -m pip install certifi"
Invoke-Expression "& '$env:VIRTUAL_ENV_PY' -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
Invoke-Expression "& '$env:VIRTUAL_ENV_PY' -m 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:
VIRTUAL_ENV: $(VIRTUAL_ENV)
VIRTUAL_ENV_PY: $(VIRTUAL_ENV_PY)
PYTHON_VERSION: $(python.version)
- template: create-virtualenv.yml
- ${{ if eq(parameters.vmImage, 'windows-2019') }}:
- template: run-tests-windows.yml
- ${{ if ne(parameters.vmImage, 'windows-2019') }}:
- template: run-tests-linux.yml
- task: PublishTestResults@2
displayName: Publish Test Results
+1 -1
View File
@@ -21,4 +21,4 @@ trigger:
jobs:
- template: jobs/test.yml
parameters:
vmImage: vs2017-win2016
vmImage: windows-2019
+2 -11
View File
@@ -1,22 +1,13 @@
[dev-packages]
pipenv = {path = ".", editable = true, extras = ["test"]}
"flake8" = ">=3.3.0,<4"
sphinx = "<=1.5.5"
twine = "*"
pipenv = {path = ".", editable = true, extras = ["tests", "dev"]}
sphinx-click = "*"
click = "*"
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
+176 -138
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "0cdfbd95f33a9edb69dede4bc868f24f7b770ba6b06ce73f6de5f175544e996d"
"sha256": "f4d89c0aab5c4e865f8c96ba24613fb1e66bae803a3ceaeadb6abf0061898091"
},
"pipfile-spec": 6,
"requires": {},
@@ -27,6 +27,7 @@
"sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6",
"sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.5"
},
"appdirs": {
@@ -55,6 +56,7 @@
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==19.1.0"
},
"babel": {
@@ -62,6 +64,7 @@
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.6.0"
},
"backports.functools-lru-cache": {
@@ -69,7 +72,7 @@
"sha256:9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a",
"sha256:f0b0e4eba956de51238e17573b7087e852dfe9854afd2e9c873f73fc0ca0a6dd"
],
"markers": "python_version < '3'",
"markers": "python_version == '2.7'",
"version": "==1.5"
},
"backports.shutil-get-terminal-size": {
@@ -77,7 +80,7 @@
"sha256:0975ba55054c15e346944b38956a4c9cbee9009391e41b86c68990effb8c1f64",
"sha256:713e7a8228ae80341c70586d1cc0a8caa5207346927e23d09dcbcaf18eadec80"
],
"markers": "python_version < '3.3'",
"markers": "python_version == '2.7'",
"version": "==1.0.0"
},
"backports.weakref": {
@@ -85,7 +88,7 @@
"sha256:81bc9b51c0abc58edc76aefbbc68c62a787918ffe943a37947e162c3f8e19e82",
"sha256:bc4170a29915f8b22c9e7c4939701859650f2eb84184aee80da329ac0b9825c2"
],
"markers": "python_version < '3.3'",
"markers": "python_version == '2.7'",
"version": "==1.0.post1"
},
"beautifulsoup4": {
@@ -98,25 +101,24 @@
},
"black": {
"hashes": [
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==18.9b0"
"version": "==19.3b0"
},
"bleach": {
"hashes": [
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.1.0"
},
"bs4": {
"hashes": [
"sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"
],
"index": "pypi",
"version": "==0.0.1"
},
"cached-property": {
@@ -128,16 +130,16 @@
},
"cerberus": {
"hashes": [
"sha256:f5c2e048fb15ecb3c088d192164316093fcfa602a74b3386eefb2983aa7e800a"
"sha256:0be48fc0dc84f83202a5309c0aa17cd5393e70731a1698a50d118b762fbe6875"
],
"version": "==1.2"
"version": "==1.3.1"
},
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
],
"version": "==2018.11.29"
"version": "==2019.3.9"
},
"chardet": {
"hashes": [
@@ -163,24 +165,17 @@
},
"configparser": {
"hashes": [
"sha256:5bd5fa2a491dc3cfe920a3f2a107510d65eceae10e9c6e547b90261a4710df32",
"sha256:c114ff90ee2e762db972fa205f02491b1f5cf3ff950decd8542c62970c9bedac",
"sha256:df28e045fbff307a28795b18df6ac8662be3219435560ddb068c283afab1ea7a"
"sha256:8be81d89d6e7b4c0d4e44bcc525845f6da25821de80cb5e06e7e0238a2899e32",
"sha256:da60d0014fd8c55eb48c1c5354352e363e2d30bbf7057e5e171a468390184c75"
],
"markers": "python_version < '3.2'",
"version": "==3.7.1"
},
"cursor": {
"hashes": [
"sha256:33f279a17789c04efd27a92501a0dad62bb011f8a4cdff93867c798d26508940"
],
"version": "==1.3.4"
"markers": "python_version == '2.7'",
"version": "==3.7.4"
},
"distlib": {
"hashes": [
"sha256:57977cd7d9ea27986ec62f425630e4ddb42efe651ff80bc58ed8dbc3c7c21f19"
"sha256:ecb3d0e4f71d0fa7f38db6bcc276c7c9a1c6638a516d726495934a553eb3fbe0"
],
"version": "==0.2.8"
"version": "==0.2.9.post0"
},
"docutils": {
"hashes": [
@@ -195,6 +190,7 @@
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"markers": "python_version >= '2.7'",
"version": "==0.3"
},
"enum34": {
@@ -209,31 +205,33 @@
},
"execnet": {
"hashes": [
"sha256:a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a",
"sha256:fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83"
"sha256:027ee5d961afa01e97b90d6ccc34b4ed976702bc58e7f092b3c513ea288cb6d2",
"sha256:752a3786f17416d491f833a29217dda3ea4a471fc5269c492eebcee8cc4772d3"
],
"version": "==1.5.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.6.0"
},
"first": {
"hashes": [
"sha256:3bb3de3582cb27071cfb514f00ed784dc444b7f96dc21e140de65fe00585c95e",
"sha256:41d5b64e70507d0c3ca742d68010a76060eea8a3d863e9b5130ab11a4a91aa0e"
"sha256:8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86",
"sha256:ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"
],
"version": "==2.0.1"
"version": "==2.0.2"
},
"flake8": {
"hashes": [
"sha256:c3ba1e130c813191db95c431a18cb4d20a468e98af7a77e2181b68574481ad36",
"sha256:fd9ddf503110bf3d8b1d270e8c673aab29ccb3dd6abf29bae1f54e5116ab4a91"
"sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661",
"sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"
],
"index": "pypi",
"version": "==3.7.5"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.7.7"
},
"flaky": {
"hashes": [
"sha256:12bd5e41f372b2190e8d754b6e5829c2f11dbc764e10b30f57e59f829c9ca1da",
"sha256:a94931c46a33469ec26f09b652bc88f55a8f5cc77807b90ca7bbafef1108fd7d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.5.3"
},
"flask": {
@@ -248,7 +246,7 @@
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
],
"markers": "python_version < '3.0'",
"markers": "python_version < '3.3'",
"version": "==1.0.2"
},
"functools32": {
@@ -256,7 +254,7 @@
"sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0",
"sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d"
],
"markers": "python_version >= '2.7' and python_version < '2.8'",
"markers": "python_version < '3.2'",
"version": "==3.2.3.post2"
},
"futures": {
@@ -264,7 +262,7 @@
"sha256:9ec02aa7d674acb8618afb127e27fde7fc68994c0437ad759fa094a574adb265",
"sha256:ec0a6cb848cc212002b9828c3e34c675e0c9ff6741dc445cab6fdd4e1085d1f1"
],
"markers": "python_version < '3.0'",
"markers": "python_version < '3.2'",
"version": "==3.2.0"
},
"idna": {
@@ -279,6 +277,7 @@
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"incremental": {
@@ -294,39 +293,38 @@
"sha256:dc492f8f17a0746e92081aec3f86ae0b4750bf41607ea2ad87e5a7b5705121b7",
"sha256:eb6f9262d4d25b40330fb21d1e99bf0f85011ccc3526980f8a3eaedd4b43892e"
],
"index": "pypi",
"version": "==1.2.0"
},
"isort": {
"hashes": [
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
"sha256:1349c6f7c2a0f7539f5f2ace51a9a8e4a37086ce4de6f78f5f53fb041d0a3cd5",
"sha256:f09911f6eb114e5592abe635aded8bf3d2c3144ebcfcaf81ee32e7af7b7d1870"
],
"index": "pypi",
"version": "==4.3.4"
"version": "==4.3.18"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"jedi": {
"hashes": [
"sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd",
"sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191"
"sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b",
"sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"
],
"index": "pypi",
"version": "==0.13.2"
"version": "==0.13.3"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
],
"version": "==2.10"
"version": "==2.10.1"
},
"markupsafe": {
"hashes": [
@@ -359,6 +357,7 @@
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
},
"mccabe": {
@@ -370,10 +369,11 @@
},
"mock": {
"hashes": [
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
"sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3",
"sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"
],
"version": "==2.0.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.5"
},
"more-itertools": {
"hashes": [
@@ -395,25 +395,25 @@
"sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
"sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==19.0"
},
"parso": {
"hashes": [
"sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9",
"sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"
"sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33",
"sha256:2e9574cb12e7112a87253e14e2c380ce312060269d04bd018478a3c92ea9a376"
],
"version": "==0.3.4"
"version": "==0.4.0"
},
"parver": {
"hashes": [
"sha256:1b37a691af145a3a193eff269d53ba5b2ab16dfbb65d47d85360755919f5fe4b",
"sha256:72d056b8f8883ac90eef5554a9c8a47fac39d3b66479f3d2c8d5bc21b849cdba"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.2.1"
},
"passa": {
"editable": true,
"git": "https://github.com/sarugaku/passa.git",
"ref": "a2ba0b30c86339cae5ef3a03046fc9c583452c40",
"version": "==0.3.1.dev0"
@@ -428,10 +428,10 @@
},
"pbr": {
"hashes": [
"sha256:8257baf496c8522437e8a6cfe0f15e00aedc6c0e0e7c9d55eeeeab31e0853843",
"sha256:8c361cc353d988e4f5b998555c88098b9d5964c2e11acf7b0d21925a66bb5824"
"sha256:6901995b9b686cb90cceba67a0f6d4d14ae003cd59bc12beb61549bdfbe3bc89",
"sha256:d950c64aeea5456bbd147468382a5bb77fe692c13c9f00f0219814ce5b642755"
],
"version": "==5.1.3"
"version": "==5.2.0"
},
"pep517": {
"hashes": [
@@ -445,12 +445,16 @@
"sha256:3bc24ec050a6b9eea35419467237e4f47eaf806dadc9999bf887355c377edea7",
"sha256:edb4cf3c509eab2f36b55c1ac1a59a4c485ccd537cc87934d74950880f641256"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.3.2"
},
"pipenv": {
"editable": true,
"path": ".",
"version": "==2018.11.27.dev0"
"extras": [
"dev",
"tests"
],
"path": "."
},
"pkginfo": {
"hashes": [
@@ -467,20 +471,23 @@
"sha256:c0e3553c1e581d8423daccbd825789c6e7f29b7d9e00e5331b12e1642a1a26d3",
"sha256:dde5d525cf5f0cbad4d938c83b93db17887918daf63c13eafed257c4f61b07b4"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.2.2"
},
"pluggy": {
"hashes": [
"sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f",
"sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"
"sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180",
"sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a"
],
"version": "==0.9.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.11.0"
},
"py": {
"hashes": [
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.8.0"
},
"pycodestyle": {
@@ -488,6 +495,7 @@
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.5.0"
},
"pyflakes": {
@@ -495,6 +503,7 @@
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.1.1"
},
"pygments": {
@@ -506,17 +515,19 @@
},
"pyparsing": {
"hashes": [
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
"sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
"sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
],
"version": "==2.3.1"
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.0"
},
"pytest": {
"hashes": [
"sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec",
"sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"
"sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24",
"sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6"
],
"version": "==3.10.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==4.5.0"
},
"pytest-forked": {
"hashes": [
@@ -527,8 +538,7 @@
},
"pytest-pypi": {
"editable": true,
"path": "./tests/pytest-pypi",
"version": "==0.1.1"
"path": "./tests/pytest-pypi"
},
"pytest-tap": {
"hashes": [
@@ -539,10 +549,11 @@
},
"pytest-xdist": {
"hashes": [
"sha256:4a201bb3ee60f5dd6bb40c5209d4e491cecc4d5bafd656cfb10f86178786e568",
"sha256:d03d1ff1b008458ed04fa73e642d840ac69b4107c168e06b71037c62d7813dd4"
"sha256:b0bb4b0293ee8657b9eb3ff334a3b6aac4db74fd4a86b81e1982c879237a47eb",
"sha256:f83a485293e81fd57c8a5a85a3f12473a532c5ca7dec518857cbb72766bb526c"
],
"version": "==1.26.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.28.0"
},
"pytoml": {
"hashes": [
@@ -552,11 +563,10 @@
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
"sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
"sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
],
"index": "pypi",
"version": "==2018.9"
"version": "==2019.1"
},
"readme-renderer": {
"hashes": [
@@ -570,6 +580,7 @@
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.21.0"
},
"requests-toolbelt": {
@@ -581,10 +592,11 @@
},
"requirementslib": {
"hashes": [
"sha256:04c19bfe6f2c4dbfe3cc5115744c2079b0f3ce52c61c9d130a1ccf2b9896f812",
"sha256:fdfc75d0ce418e80fa3b573ff078a5732ec1f11415afc2d4280809a1b5b2575d"
"sha256:a5bcff2861eea9358e90d6a4234f0bf4ad0ba730f86a4ea4680f53365b7b4735",
"sha256:ae0c2fce1b33c9c7b171895ab11472bd7be9c45f6214aad97ceaf83511d78d93"
],
"version": "==1.4.2"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.5.0"
},
"resolvelib": {
"hashes": [
@@ -595,33 +607,36 @@
},
"rope": {
"hashes": [
"sha256:031eb54b3eeec89f4304ede816995ed2b93a21e6fba16bd02aff10a0d6c257b7"
"sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969",
"sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf",
"sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"
],
"index": "pypi",
"version": "==0.12.0"
"version": "==0.14.0"
},
"scandir": {
"hashes": [
"sha256:04b8adb105f2ed313a7c2ef0f1cf7aff4871aa7a1883fa4d8c44b5551ab052d6",
"sha256:1444134990356c81d12f30e4b311379acfbbcd03e0bab591de2696a3b126d58e",
"sha256:1b5c314e39f596875e5a95dd81af03730b338c277c54a454226978d5ba95dbb6",
"sha256:346619f72eb0ddc4cf355ceffd225fa52506c92a2ff05318cfabd02a144e7c4e",
"sha256:44975e209c4827fc18a3486f257154d34ec6eaec0f90fef0cca1caa482db7064",
"sha256:61859fd7e40b8c71e609c202db5b0c1dbec0d5c7f1449dec2245575bdc866792",
"sha256:a5e232a0bf188362fa00123cc0bb842d363a292de7126126df5527b6a369586a",
"sha256:c14701409f311e7a9b7ec8e337f0815baf7ac95776cc78b419a1e6d49889a383",
"sha256:c7708f29d843fc2764310732e41f0ce27feadde453261859ec0fca7865dfc41b",
"sha256:c9009c527929f6e25604aec39b0a43c3f831d2947d89d6caaab22f057b7055c8",
"sha256:f5c71e29b4e2af7ccdc03a020c626ede51da471173b4a6ad1e904f2b2e04b4bd"
"sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e",
"sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022",
"sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f",
"sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f",
"sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae",
"sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173",
"sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4",
"sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32",
"sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188",
"sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d",
"sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"
],
"markers": "python_version < '3.5'",
"version": "==1.9.0"
"version": "==1.10.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.12.0"
},
"snowballstemmer": {
@@ -633,26 +648,34 @@
},
"soupsieve": {
"hashes": [
"sha256:afa56bf14907bb09403e5d15fbed6275caa4174d36b975226e3b67a3bb6e2c4b",
"sha256:eaed742b48b1f3e2d45ba6f79401b2ed5dc33b2123dfe216adb90d4bfa0ade26"
"sha256:6898e82ecb03772a0d82bd0d0a10c0d6dcc342f77e0701d0ec4a8271be465ece",
"sha256:b20eff5e564529711544066d7dc0f7661df41232ae263619dede5059799cdfca"
],
"version": "==1.8"
"version": "==1.9.1"
},
"sphinx": {
"hashes": [
"sha256:11f271e7a9398385ed730e90f0bb41dc3815294bdcd395b46ed2d033bc2e7d87",
"sha256:4064ea6c56feeb268838cb8fbbee507d0c3d5d92fa63a7df935a916b52c9e2f5"
"sha256:9f3e17c64b34afc653d7c5ec95766e03043cc6d80b0de224f59b6b6e19d37c3c",
"sha256:c7658aab75c920288a8cf6f09f244c6cfdae30d82d803ac1634d9f223a80ca08"
],
"index": "pypi",
"version": "==1.5.5"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.8.5"
},
"sphinx-click": {
"hashes": [
"sha256:926da1a7c677ae1b35cf255269ff84fec65d0f92e4863acfa77b92cf8ae32275",
"sha256:f0c03d6ea0e4258c9c09646b6f745090ea8dd13e7e045903e4b789dfc02f7846"
"sha256:9ceb0ee39e9734f39f593f99499e2a2f8e62929ac0b5f040b1d0411c02efaab7",
"sha256:c9c45127bbbf720f1d24476cb3e57c806dd270662ae63b53a4b23be6c334491e"
],
"index": "pypi",
"version": "==2.0.1"
"version": "==2.1.0"
},
"sphinxcontrib-websupport": {
"hashes": [
"sha256:4044751a075b6560f155c96f9fec6bc5198cd5307e5db9f77c7b1c5247ac9a09",
"sha256:c1b918b1b41cde045cdb9755941086b4ce4ebbfd7bff41d10ffb6d325779cbf9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1.dev20190321"
},
"stdeb": {
"hashes": [
@@ -682,27 +705,30 @@
"sha256:d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2",
"sha256:f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.5.3"
},
"towncrier": {
"editable": true,
"git": "https://github.com/hawkowl/towncrier.git",
"ref": "ecd438c9c0ef132a92aba2eecc4dc672ccf9ec63",
"hashes": [
"sha256:48251a1ae66d2cf7e6fa5552016386831b3e12bb3b2d08eb70374508c17a8196",
"sha256:de19da8b8cb44f18ea7ed3a3823087d2af8fcf497151bb9fd1e1b092ff56ed8d"
],
"version": "==19.2.0"
},
"tqdm": {
"hashes": [
"sha256:d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021",
"sha256:e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05"
"sha256:0a860bf2683fdbb4812fe539a6c22ea3f1777843ea985cb8c3807db448a0f7ab",
"sha256:e288416eecd4df19d12407d0c913cbf77aa8009d7fddb18f632aded3bdbdda6b"
],
"version": "==4.31.1"
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==4.32.1"
},
"twine": {
"hashes": [
"sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446",
"sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.13.0"
},
"typing": {
@@ -716,35 +742,45 @@
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
"sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4",
"sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb"
],
"version": "==1.24.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'",
"version": "==1.24.3"
},
"virtualenv": {
"hashes": [
"sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417",
"sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39"
"sha256:99acaf1e35c7ccf9763db9ba2accbca2f4254d61d1912c5ee364f9cc4a8942a0",
"sha256:fe51cdbf04e5d8152af06c075404745a7419de27495a83f0d72518ad50be3ce8"
],
"version": "==16.4.3"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==16.6.0"
},
"virtualenv-clone": {
"hashes": [
"sha256:217bd3f0880c9f85672c0bcc9ad9e0354ab7dfa89c2f117e63aa878b4279f5bf",
"sha256:316c8a05432a7adb5e461709759aca18c51433ffc2c33e2e80c9e51c452d339f",
"sha256:f2a07ed255f3abaceef8c8442512d8cdb2ba9f867e212d8a51680c7790a85033"
"sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5",
"sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557"
],
"version": "==0.5.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.5.3"
},
"vistir": {
"extras": [
"spinner"
],
"hashes": [
"sha256:1a3d16d541de7ff098037260506a9efc5f6967176137988bd2cbfdd13b240ba0",
"sha256:68896b279f64ff078e06ffd41f77181ef7cdedbeaa5f453cae3cfdd97d41dbcf"
"sha256:00af96b75157b299616f47657ed34368e92e01d039100368c9dcd94897e3c109",
"sha256:bbe040ce656f1de9b5f75c953abe49af4d1ba6fdf8f1f4b8db3e63cfd2dad24a"
],
"version": "==0.3.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.4.1"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
],
"version": "==0.1.7"
},
"webencodings": {
"hashes": [
@@ -755,24 +791,26 @@
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
"sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
"sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
],
"version": "==0.14.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.15.4"
},
"wheel": {
"hashes": [
"sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d",
"sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668"
"sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08",
"sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565"
],
"version": "==0.33.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.33.4"
},
"yaspin": {
"hashes": [
"sha256:441f8a6761e347652d04614899fd0a9cfda7439e2d5682e664bd31230c656176",
"sha256:d3ebcf8162e0ef8bb5484b8751d5b6d2fbf0720112c81f64614c308576a03b1d"
"sha256:8e6d2e2b207ba18510f190e04a25273a32d1f192af9c9a77ebe46deaca799dfa",
"sha256:94b7602f0dc59d26a15e63cefff6aaf644c58dd77fc4e1ef675d3ba2c302ed06"
],
"version": "==0.14.1"
"version": "==0.14.3"
}
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ Or, if you\'re using FreeBSD:
# pkg install py36-pipenv
Otherwise, refer to the [documentation](https://docs.pipenv.org/install/) for instructions.
Otherwise, refer to the [documentation](https://docs.pipenv.org/en/latest/install/#installing-pipenv) for instructions.
✨🍰✨
+69
View File
@@ -0,0 +1,69 @@
name: Pipenv Build Rules
trigger:
batch: true
branches:
include:
- master
paths:
exclude:
- docs/*
- news/*
- peeps/*
- README.md
- pipenv/*.txt
- CHANGELOG.rst
- CONTRIBUTING.md
- CODE_OF_CONDUCT.md
- .gitignore
- .gitattributes
- .editorconfig
jobs:
- job: TestLinux
pool:
vmImage: 'Ubuntu-16.04'
strategy:
matrix:
Python27:
python.version: '2.7'
Python36:
python.version: '3.6'
Python37:
python.version: '3.7'
maxParallel: 4
steps:
- template: .azure-pipelines/steps/run-tests.yml
- template: .azure-pipelines/jobs/run-vendor-scripts.yml
- job: TestWindows
pool:
vmImage: windows-2019
strategy:
matrix:
Python27:
python.version: '2.7'
python.architecture: x64
Python36:
python.version: '3.6'
python.architecture: x64
Python37:
python.version: '3.7'
python.architecture: x64
maxParallel: 4
steps:
- template: .azure-pipelines/steps/run-tests.yml
- job: TestMacOS
pool:
vmImage: macOS-10.13
strategy:
matrix:
Python27:
python.version: '2.7'
Python36:
python.version: '3.6'
Python37:
python.version: '3.7'
maxParallel: 4
steps:
- template: .azure-pipelines/steps/run-tests.yml
+15 -9
View File
@@ -1,28 +1,34 @@
Updated vendored dependencies:
- **attrs**: ``18.2.0`` => ``19.1.0``
- **certifi**: ``2018.10.15`` => ``2018.11.29``
- **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.11.1``
- **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.1``
- **pythonfinder**: ``1.1.10`` => ``1.2.0``
- **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.4.2``
- **shellingham**: ``1.2.7`` => ``1.2.8``
- **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.24.1``
- **vistir**: ``0.3.0`` => ``0.3.1``
- **yaspin**: ``0.14.0`` => ``0.14.1``
- **urllib3**: ``1.24`` => ``1.25.2``
- **vistir**: ``0.3.0`` => ``0.4.1``
- **yaspin**: ``0.14.0`` => ``0.14.3``
- Removed vendored dependency **cursor**.
+1
View File
@@ -0,0 +1 @@
Update pytest configuration to support pytest 4.
+11 -15
View File
@@ -36,23 +36,19 @@ try:
except Exception:
pass
from .vendor.vistir.misc import get_wrapped_stream
if sys.version_info >= (3, 0):
stdout = sys.stdout.buffer
stderr = sys.stderr.buffer
else:
stdout = sys.stdout
stderr = sys.stderr
from pipenv.vendor.vistir.misc import get_text_stream
stdout = get_text_stream("stdout")
stderr = get_text_stream("stderr")
sys.stderr = get_wrapped_stream(stderr)
sys.stdout = get_wrapped_stream(stdout)
from .vendor.colorama import AnsiToWin32
if os.name == "nt":
stderr_wrapper = AnsiToWin32(sys.stderr, autoreset=False, convert=None, strip=None)
stdout_wrapper = AnsiToWin32(sys.stdout, autoreset=False, convert=None, strip=None)
sys.stderr = stderr_wrapper.stream
sys.stdout = stdout_wrapper.stream
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
+19 -16
View File
@@ -14,11 +14,11 @@ 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, decode_for_output
from .cmdparse import Script
@@ -746,7 +746,7 @@ def batch_install(deps_list, procs, failed_deps_queue,
pypi_mirror=pypi_mirror,
trusted_hosts=trusted_hosts,
extra_indexes=extra_indexes,
use_pep517=not retry,
use_pep517=not failed,
)
if procs.qsize() < nprocs:
c.dep = dep
@@ -917,18 +917,17 @@ 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.red("{0}".format(error)),]
extra=crayons.red("{0}".format(error))
)
else:
@@ -1416,15 +1415,18 @@ def pip_install(
name = requirement.name
if requirement.extras:
name = "{0}{1}".format(name, requirement.extras_as_pip)
line = "-e {0}#egg={1}".format(vistir.path.path_to_url(repo.checkout_directory), requirement.name)
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)
click.echo(
"Writing requirement line to temporary file: {0!r}".format(line),
err=True
)
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()
@@ -1441,10 +1443,11 @@ def pip_install(
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))
click.echo(
"Writing requirement line to temporary file: {0!r}".format(line),
err=True
)
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()
+66 -17
View File
@@ -9,7 +9,7 @@ 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
@@ -106,10 +106,14 @@ class Environment(object):
@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"
@@ -120,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):
"""
@@ -156,18 +170,22 @@ class Environment(object):
'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 = make_posix(get_python_lib(plat_specific=0, prefix=prefix))
platlib = make_posix(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'] = os.pathsep.join(["", ".", lib_dirs])
paths["libdirs"] = lib_dirs
return paths
@@ -175,13 +193,18 @@ class Environment(object):
@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").absolute().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
@@ -209,6 +232,30 @@ class Environment(object):
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):
"""
@@ -218,7 +265,7 @@ class Environment(object):
: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
@@ -258,7 +305,7 @@ class Environment(object):
pkg for pkg in self.get_installed_packages() if pkg.key == "pip"
), None)
if pip is not None:
pip_version = parse_version(pip.version)
return parse_version(pip.version)
return parse_version("18.0")
def get_distributions(self):
@@ -442,7 +489,7 @@ class Environment(object):
yield new_node
def reverse_dependencies(self):
from vistir.misc import unnest
from vistir.misc import unnest, chunked
rdeps = {}
for req in self.get_package_requirements():
for d in self.reverse_dependency(req):
@@ -454,18 +501,20 @@ class Environment(object):
"required": d["required_version"]
}
}
parents = set(d.get("parent", []))
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", set()) | parents
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 in unnest(entry["parents"])])
rdeps[k]["parents"] = set([
p for p, version in chunked(2, unnest(entry["parents"]))
])
return rdeps
def get_working_set(self):
@@ -566,7 +615,7 @@ class Environment(object):
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", "")
])
+7 -2
View File
@@ -3,10 +3,12 @@
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.
@@ -263,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
+107 -66
View File
@@ -1,36 +1,54 @@
# -*- coding=utf-8 -*-
import itertools
import re
import sys
from pprint import pformat
from traceback import format_exception, format_tb
from collections import namedtuple
from traceback import format_tb
import six
from . import environments
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
KNOWN_EXCEPTIONS = {
"PermissionError": "Permission denied:",
}
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)
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"):
@@ -40,7 +58,10 @@ def handle_exception(exc_type, exception, traceback, hook=sys.excepthook):
line = "[{0!s}]: {1}".format(
exception.__class__.__name__, line
)
click_echo(decode_for_output(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()
@@ -60,9 +81,9 @@ 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(
@@ -84,7 +105,7 @@ class PipenvCmdError(PipenvException):
def show(self, file=None):
if file is None:
file = get_text_stderr()
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)
@@ -108,12 +129,12 @@ class JSONParseError(PipenvException):
def show(self, file=None):
if file is None:
file = get_text_stderr()
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(self.message, err=True)
click_echo(message, err=True)
if self.error_text:
click_echo("{0} {1}".format(
crayons.white("ERROR TEXT:", bold=True),
@@ -136,12 +157,12 @@ class PipenvUsageError(UsageError):
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:
@@ -175,9 +196,9 @@ class PipenvFileError(FileError):
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(decode_for_output(extra, file), file=file)
@@ -187,13 +208,16 @@ class PipenvFileError(FileError):
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=decode_for_output(message), extra=extra, **kwargs)
super(PipfileNotFound, self).__init__(filename, message=message, extra=extra, **kwargs)
class LockfileNotFound(PipenvFileError):
@@ -204,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=decode_for_output(message), extra=extra, **kwargs)
super(LockfileNotFound, self).__init__(filename, message=message, extra=extra, **kwargs)
class DeployException(PipenvUsageError):
@@ -212,13 +236,13 @@ class DeployException(PipenvUsageError):
if not message:
message = crayons.normal("Aborting deploy", bold=True)
extra = kwargs.pop("extra", [])
PipenvUsageError.__init__(self, message=decode_for_output(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=decode_for_output(message), ctx=ctx, **kwargs)
PipenvUsageError.__init__(self, message=message, ctx=ctx, **kwargs)
self.extra = extra
self.option_name = option_name
@@ -245,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, decode_for_output(hint), extra=extra, **kwargs)
PipenvFileError.__init__(self, filename, hint, extra=extra, **kwargs)
class SetupException(PipenvException):
@@ -261,7 +285,7 @@ class VirtualenvException(PipenvException):
"There was an unexpected error while activating your virtualenv. "
"Continuing anyway..."
)
PipenvException.__init__(self, decode_for_output(message), **kwargs)
PipenvException.__init__(self, message, **kwargs)
class VirtualenvActivationException(VirtualenvException):
@@ -272,7 +296,7 @@ class VirtualenvActivationException(VirtualenvException):
"not activated. Continuing anyway…"
)
self.message = message
VirtualenvException.__init__(self, decode_for_output(message), **kwargs)
VirtualenvException.__init__(self, message, **kwargs)
class VirtualenvCreationException(VirtualenvException):
@@ -280,23 +304,35 @@ class VirtualenvCreationException(VirtualenvException):
if not message:
message = "Failed to create virtual environment."
self.message = message
VirtualenvException.__init__(self, decode_for_output(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!r}".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, set)):
package = " ".join(package)
message = "{0!s} {1!s}...".format(
crayons.normal("Failed to uninstall package(s)"),
crayons.yellow(str(package), bold=True)
crayons.yellow("{0}!s".format(package), bold=True)
)
self.exit_code = return_code
PipenvException.__init__(self, message=decode_for_output(message), extra=extra)
PipenvException.__init__(self, message=message, extra=extra)
self.extra = extra
@@ -304,36 +340,34 @@ class InstallError(PipenvException):
def __init__(self, package, **kwargs):
package_message = ""
if package is not None:
package_message = crayons.normal("Couldn't install package {0}\n".format(
crayons.white(package, bold=True)
))
message = "{0} {1} {2}".format(
crayons.red("ERROR:", bold=True),
package_message,
package_message = "Couldn't install package: {0}\n".format(
crayons.white("{0!s}".format(package), bold=True)
)
message = "{0} {1}".format(
"{0}".format(package_message),
crayons.yellow("Package installation failed...")
)
extra = kwargs.pop("extra", [])
PipenvException.__init__(self, message=decode_for_output(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.')
)
PipenvException.__init__(self, message=decode_for_output(message))
PipenvException.__init__(self, message=message)
class DependencyConflict(PipenvException):
def __init__(self, message):
extra = [decode_for_output("{0} {1}".format(
crayons.red("ERROR:", bold=True),
crayons.white("A dependency conflict was detected and could not be resolved.", bold=True),
)),]
super(DependencyConflict, self).__init__(decode_for_output(message), extra=extra)
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):
@@ -355,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,
@@ -366,7 +398,7 @@ class ResolutionFailure(PipenvException):
"See PEP440 for more information."
)
)
super(ResolutionFailure, self).__init__(decode_for_output(message), extra=extra)
PipenvException.__init__(self, message, extra=extra)
class RequirementError(PipenvException):
@@ -404,20 +436,29 @@ class RequirementError(PipenvException):
crayons.normal(decode_for_output("Failed creating requirement instance")),
crayons.white(decode_for_output("{0!r}".format(req_value)))
)
extra = [crayons.normal(decode_for_output(str(req)))]
super(RequirementError, self).__init__(message, extra=extra)
super(ResolutionFailure, self).__init__(fix_utf8(message), extra=extra)
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"""
matched_exceptions = [k for k in KNOWN_EXCEPTIONS.keys() if k in error]
if not matched_exceptions:
return "{}".format(vistir.misc.decode_for_output(error))
errors = []
for match in matched_exceptions:
_, error, info = error.rpartition(KNOWN_EXCEPTIONS[match])
errors.append("{} {}".format(error, info))
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)
+1 -1
View File
@@ -19,8 +19,8 @@ from pipenv.patched.notpip._internal.exceptions import (
UnsupportedPythonVersion,
)
from pipenv.patched.notpip._internal.req.constructors import install_req_from_req_string
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import dist_in_usersite, ensure_dir
from pipenv.patched.notpip._internal.utils.packaging import check_dist_requires_python
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
@@ -25,4 +25,3 @@ 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.
@@ -4,7 +4,7 @@ Copyright 2018 Kenneth Reitz
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
https://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,
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2010-2017 Benjamin Peterson
Copyright (c) 2010-2018 Benjamin Peterson
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
@@ -1,19 +1,21 @@
This is the MIT license: http://www.opensource.org/licenses/mit-license.php
MIT License
Copyright 2008-2016 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+18 -18
View File
@@ -1,23 +1,23 @@
appdirs==1.4.3
distlib==0.2.7
distro==1.3.0
html5lib==1.0.1
six==1.11.0
colorama==0.3.9
CacheControl==0.12.5
msgpack-python==0.5.6
lockfile==0.12.2
progress==1.4
colorama==0.4.1
distlib==0.2.8
distro==1.4.0
html5lib==1.0.1
ipaddress==1.0.22 # Only needed on 2.6 and 2.7
packaging==18.0
pep517==0.2
pyparsing==2.2.1
pytoml==0.1.19
retrying==1.3.3
requests==2.19.1
lockfile==0.12.2
msgpack==0.5.6
packaging==19.0
pep517==0.5.0
progress==1.5
pyparsing==2.4.0
pytoml==0.1.20
requests==2.21.0
certifi==2019.3.9
chardet==3.0.4
idna==2.7
urllib3==1.23
certifi==2018.8.24
setuptools==40.4.3
idna==2.8
urllib3==1.25.2
retrying==1.3.3
setuptools==41.0.1
six==1.12.0
webencodings==0.5.1
+6 -5
View File
@@ -14,8 +14,8 @@ from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet, Specifier
os.environ["PIP_SHIMS_BASE_MODULE"] = str("pipenv.patched.notpip")
from pip_shims.shims import VcsSupport, WheelCache, InstallationError, pip_version
from pip_shims.shims import Resolver as PipResolver
import pip_shims
from pip_shims.shims import VcsSupport, WheelCache, InstallationError
from .._compat import (
@@ -112,7 +112,7 @@ class PyPIRepository(BaseRepository):
}
# pip 19.0 has removed process_dependency_links from the PackageFinder constructor
if pkg_resources.parse_version(pip_version) < pkg_resources.parse_version('19.0'):
if pkg_resources.parse_version(pip_shims.shims.pip_version) < pkg_resources.parse_version('19.0'):
finder_kwargs["process_dependency_links"] = pip_options.process_dependency_links
self.finder = PackageFinder(**finder_kwargs)
@@ -279,7 +279,7 @@ class PyPIRepository(BaseRepository):
'finder': self.finder,
'session': self.session,
'upgrade_strategy': "to-satisfy-only",
'force_reinstall': True,
'force_reinstall': False,
'ignore_dependencies': False,
'ignore_requires_python': True,
'ignore_installed': True,
@@ -299,7 +299,7 @@ class PyPIRepository(BaseRepository):
reqset = RequirementSet()
ireq.is_direct = True
# reqset.add_requirement(ireq)
resolver = PipResolver(**resolver_kwargs)
resolver = pip_shims.shims.Resolver(**resolver_kwargs)
resolver.require_hashes = False
results = resolver._resolve_one(reqset, ireq)
@@ -309,6 +309,7 @@ class PyPIRepository(BaseRepository):
cleanup_fn()
except OSError:
pass
results = set(results) if results else set()
return results, ireq
+177 -59
View File
@@ -133,12 +133,104 @@ class Entry(object):
del entry_dict["name"]
return entry_dict
def get_cleaned_dict(self):
if self.is_updated:
@classmethod
def parse_pyparsing_exprs(cls, expr_iterable):
from pipenv.vendor.pyparsing import Literal, MatchFirst
keys = []
expr_list = []
expr = expr_iterable.copy()
if isinstance(expr, Literal) or (
expr.__class__.__name__ == Literal.__name__
):
keys.append(expr.match)
elif isinstance(expr, MatchFirst) or (
expr.__class__.__name__ == MatchFirst.__name__
):
expr_list = expr.exprs
elif isinstance(expr, list):
expr_list = expr
if expr_list:
for part in expr_list:
keys.extend(cls.parse_pyparsing_exprs(part))
return keys
@classmethod
def get_markers_from_dict(cls, entry_dict):
from pipenv.vendor.packaging import markers as packaging_markers
from pipenv.vendor.requirementslib.models.markers import normalize_marker_str
marker_keys = cls.parse_pyparsing_exprs(packaging_markers.VARIABLE)
markers = set()
keys_in_dict = [k for k in marker_keys if k in entry_dict]
markers = {
normalize_marker_str("{k} {v}".format(k=k, v=entry_dict.pop(k)))
for k in keys_in_dict
}
if "markers" in entry_dict:
markers.add(normalize_marker_str(entry_dict["markers"]))
if None in markers:
markers.remove(None)
if markers:
entry_dict["markers"] = " and ".join(list(markers))
else:
markers = None
return markers, entry_dict
@property
def markers(self):
self._markers, self.entry_dict = self.get_markers_from_dict(self.entry_dict)
return self._markers
@markers.setter
def markers(self, markers):
if not markers:
marker_str = self.marker_to_str(markers)
if marker_str:
self._entry = self.entry.merge_markers(marker_str)
self._markers = self.marker_to_str(self._entry.markers)
entry_dict = self.entry_dict.copy()
entry_dict["markers"] = self.marker_to_str(self._entry.markers)
self.entry_dict = entry_dict
@property
def original_markers(self):
original_markers, lockfile_dict = self.get_markers_from_dict(
self.lockfile_dict
)
self.lockfile_dict = lockfile_dict
self._original_markers = self.marker_to_str(original_markers)
return self._original_markers
@staticmethod
def marker_to_str(marker):
from pipenv.vendor.requirementslib.models.markers import normalize_marker_str
if not marker:
return None
from pipenv.vendor import six
from pipenv.vendor.vistir.compat import Mapping
marker_str = None
if isinstance(marker, Mapping):
marker_dict, _ = Entry.get_markers_from_dict(marker)
if marker_dict:
marker_str = "{0}".format(marker_dict.popitem()[1])
elif isinstance(marker, (list, set, tuple)):
marker_str = " and ".join([normalize_marker_str(m) for m in marker if m])
elif isinstance(marker, six.string_types):
marker_str = "{0}".format(normalize_marker_str(marker))
if isinstance(marker_str, six.string_types):
return marker_str
return None
def get_cleaned_dict(self, keep_outdated=False):
if keep_outdated and self.is_updated:
self.validate_constraints()
self.ensure_least_updates_possible()
elif not keep_outdated:
self.validate_constraints()
if self.entry.extras != self.lockfile_entry.extras:
self._entry.req.extras.extend(self.lockfile_entry.req.extras)
entry_extras = list(self.entry.extras)
if self.lockfile_entry.extras:
entry_extras.extend(list(self.lockfile_entry.extras))
self._entry.req.extras = entry_extras
self.entry_dict["extras"] = self.entry.extras
entry_hashes = set(self.entry.hashes)
locked_hashes = set(self.lockfile_entry.hashes)
@@ -146,6 +238,7 @@ class Entry(object):
self.entry_dict["hashes"] = list(entry_hashes | locked_hashes)
self.entry_dict["name"] = self.name
self.entry_dict["version"] = self.strip_version(self.entry_dict["version"])
_, self.entry_dict = self.get_markers_from_dict(self.entry_dict)
return self.entry_dict
@property
@@ -202,10 +295,10 @@ class Entry(object):
def clean_specifier(specifier):
from pipenv.vendor.packaging.specifiers import Specifier
if not any(specifier.startswith(k) for k in Specifier._operators.keys()):
if specifier.strip().lower() in ["any", "*"]:
if specifier.strip().lower() in ["any", "<any>", "*"]:
return "*"
specifier = "=={0}".format(specifier)
elif specifier.startswith("==") and specifier.count("=") > 2:
elif specifier.startswith("==") and specifier.count("=") > 3:
specifier = "=={0}".format(specifier.lstrip("="))
return specifier
@@ -255,7 +348,7 @@ class Entry(object):
if not self._requires:
self._requires = next(iter(
self.project.environment.get_package_requirements(self.name)
), None)
), {})
return self._requires
@property
@@ -284,21 +377,25 @@ class Entry(object):
return True
def get_dependency(self, name):
return next(iter(
dep for dep in self.requirements.get("dependencies", [])
if dep.get("package_name", "") == name
), {})
if self.requirements:
return next(iter(
dep for dep in self.requirements.get("dependencies", [])
if dep and dep.get("package_name", "") == name
), {})
return {}
def get_parent_deps(self, unnest=False):
from pipenv.vendor.packaging.specifiers import Specifier
parents = []
for spec in self.reverse_deps.get(self.normalized_name, {}).get("parents", set()):
spec_index = next(iter(c for c in Specifier._operators if c in spec), None)
spec_match = next(iter(c for c in Specifier._operators if c in spec), None)
name = spec
parent = None
if spec_index is not None:
specifier = self.clean_specifier(spec[spec_index:])
name = spec[:spec_index]
if spec_match is not None:
spec_index = spec.index(spec_match)
specifier = self.clean_specifier(spec[spec_index:len(spec_match)]).strip()
name_start = spec_index + len(spec_match)
name = spec[name_start:].strip()
parent = self.create_parent(name, specifier)
else:
name = spec
@@ -373,7 +470,7 @@ class Entry(object):
if c and c.name == self.entry.name
}
pipfile_constraint = self.get_pipfile_constraint()
if pipfile_constraint:
if pipfile_constraint and not (self.pipfile_entry.editable or pipfile_constraint.editable):
constraints.add(pipfile_constraint)
return constraints
@@ -415,10 +512,16 @@ class Entry(object):
required = self.clean_specifier(required)
parent_requires = self.make_requirement(self.name, required)
parent_dependencies.add("{0} => {1} ({2})".format(p.name, self.name, required))
if not parent_requires.requirement.specifier.contains(self.original_version):
# use pre=True here or else prereleases dont satisfy constraints
if parent_requires.requirement.specifier and (
not parent_requires.requirement.specifier.contains(self.original_version, prereleases=True)
):
can_use_original = False
if not parent_requires.requirement.specifier.contains(self.updated_version):
has_mismatch = True
if parent_requires.requirement.specifier and (
not parent_requires.requirement.specifier.contains(self.updated_version, prereleases=True)
):
if not self.entry.editable and self.updated_version != self.original_version:
has_mismatch = True
if has_mismatch and not can_use_original:
from pipenv.exceptions import DependencyConflict
msg = (
@@ -500,6 +603,24 @@ class Entry(object):
return super(Entry, self).__getattribute__(key)
def clean_results(results, resolver, project, dev=False):
from pipenv.utils import translate_markers
if not project.lockfile_exists:
return results
lockfile = project.lockfile_content
section = "develop" if dev else "default"
pipfile_section = "dev-packages" if dev else "packages"
reverse_deps = project.environment.reverse_dependencies()
new_results = [r for r in results if r["name"] not in lockfile[section]]
for result in results:
name = result.get("name")
entry_dict = result.copy()
entry = Entry(name, entry_dict, project, resolver, reverse_deps=reverse_deps, dev=dev)
entry_dict = translate_markers(entry.get_cleaned_dict(keep_outdated=False))
new_results.append(entry_dict)
return new_results
def clean_outdated(results, resolver, project, dev=False):
from pipenv.vendor.requirementslib.models.requirements import Requirement
if not project.lockfile_exists:
@@ -520,24 +641,29 @@ def clean_outdated(results, resolver, project, dev=False):
# TODO: Should this be the case for all locking?
if entry.was_editable and not entry.is_editable:
continue
# if the entry has not changed versions since the previous lock,
# don't introduce new markers since that is more restrictive
if entry.has_markers and not entry.had_markers and not entry.is_updated:
del entry.entry_dict["markers"]
entry._entry.req.req.marker = None
entry._entry.markers = ""
# do make sure we retain the original markers for entries that are not changed
elif entry.had_markers and not entry.has_markers and not entry.is_updated:
if entry._entry and entry._entry.req and entry._entry.req.req and (
entry.lockfile_entry and entry.lockfile_entry.req and
entry.lockfile_entry.req.req and entry.lockfile_entry.req.req.marker
):
entry._entry.req.req.marker = entry.lockfile_entry.req.req.marker
if entry.lockfile_entry and entry.lockfile_entry.markers:
entry._entry.markers = entry.lockfile_entry.markers
if entry.lockfile_dict and "markers" in entry.lockfile_dict:
entry.entry_dict["markers"] = entry.lockfile_dict["markers"]
entry_dict = entry.get_cleaned_dict()
lockfile_entry = lockfile[section].get(name, None)
if not lockfile_entry:
alternate_section = "develop" if not dev else "default"
if name in lockfile[alternate_section]:
lockfile_entry = lockfile[alternate_section][name]
if lockfile_entry and not entry.is_updated:
old_markers = next(iter(m for m in (
entry.lockfile_entry.markers, lockfile_entry.get("markers", None)
) if m is not None), None)
new_markers = entry_dict.get("markers", None)
if old_markers:
old_markers = Entry.marker_to_str(old_markers)
if old_markers and not new_markers:
entry.markers = old_markers
elif new_markers and not old_markers:
del entry.entry_dict["markers"]
entry._entry.req.req.marker = None
entry._entry.markers = None
# if the entry has not changed versions since the previous lock,
# don't introduce new markers since that is more restrictive
# if entry.has_markers and not entry.had_markers and not entry.is_updated:
# do make sure we retain the original markers for entries that are not changed
entry_dict = entry.get_cleaned_dict(keep_outdated=True)
new_results.append(entry_dict)
return new_results
@@ -557,7 +683,6 @@ def parse_packages(packages, pre, clear, system, requirements_dir=None):
sys.path.insert(0, req.req.setup_info.base_dir)
req.req._setup_info.get_info()
req.update_name_from_path(req.req.setup_info.base_dir)
print(os.listdir(req.req.setup_info.base_dir))
try:
name, entry = req.pipfile_entry
except Exception:
@@ -582,6 +707,8 @@ def resolve_packages(pre, clear, verbose, system, write, requirements_dir, packa
)
def resolve(packages, pre, project, sources, clear, system, requirements_dir=None):
from pipenv.patched.piptools import logging as piptools_logging
piptools_logging.log.verbosity = 1 if verbose else 0
return resolve_deps(
packages,
which,
@@ -611,6 +738,8 @@ def resolve_packages(pre, clear, verbose, system, write, requirements_dir, packa
)
if keep_outdated:
results = clean_outdated(results, resolver, project)
else:
results = clean_results(results, resolver, project)
if write:
with open(write, "w") as fh:
if not results:
@@ -646,29 +775,18 @@ def main():
_patch_path(pipenv_site=parsed.pipenv_site)
import warnings
from pipenv.vendor.vistir.compat import ResourceWarning
from pipenv.vendor.vistir.misc import get_wrapped_stream
from pipenv.vendor.vistir.misc import replace_with_text_stream
warnings.simplefilter("ignore", category=ResourceWarning)
import six
if six.PY3:
stdout = sys.stdout.buffer
stderr = sys.stderr.buffer
else:
stdout = sys.stdout
stderr = sys.stderr
sys.stderr = get_wrapped_stream(stderr)
sys.stdout = get_wrapped_stream(stdout)
from pipenv.vendor import colorama
if os.name == "nt" and (
all(getattr(stream, method, None) for stream in [sys.stdout, sys.stderr] for method in ["write", "isatty"]) and
all(stream.isatty() for stream in [sys.stdout, sys.stderr])
):
stderr_wrapper = colorama.AnsiToWin32(sys.stderr, autoreset=False, convert=None, strip=None)
stdout_wrapper = colorama.AnsiToWin32(sys.stdout, autoreset=False, convert=None, strip=None)
sys.stderr = stderr_wrapper.stream
sys.stdout = stdout_wrapper.stream
colorama.init(wrap=False)
elif os.name != "nt":
colorama.init()
replace_with_text_stream("stdout")
replace_with_text_stream("stderr")
# from pipenv.vendor import colorama
# if os.name == "nt" and (
# all(getattr(stream, method, None) for stream in [sys.stdout, sys.stderr] for method in ["write", "isatty"]) and
# all(stream.isatty() for stream in [sys.stdout, sys.stderr])
# ):
# colorama.init(wrap=False)
# elif os.name != "nt":
# colorama.init()
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = str("1")
os.environ["PYTHONIOENCODING"] = str("utf-8")
os.environ["PYTHONUNBUFFERED"] = str("1")
+267 -115
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import contextlib
import errno
import logging
@@ -38,6 +39,8 @@ from .vendor.urllib3 import util as urllib3_util
if environments.MYPY_RUNNING:
from typing import Tuple, Dict, Any, List, Union, Optional, Text
from .vendor.requirementslib.models.requirements import Requirement, Line
from .vendor.packaging.markers import Marker
from .vendor.packaging.specifiers import Specifier
from .project import Project
@@ -292,11 +295,14 @@ def get_pipenv_sitedir():
class Resolver(object):
def __init__(self, constraints, req_dir, project, sources, clear=False, pre=False):
def __init__(
self, constraints, req_dir, project, sources, index_lookup=None,
markers_lookup=None, skipped=None, clear=False, pre=False
):
from pipenv.patched.piptools import logging as piptools_logging
if environments.is_verbose():
logging.log.verbose = True
piptools_logging.log.verbose = True
piptools_logging.log.verbosity = environments.PIPENV_VERBOSITY
self.initial_constraints = constraints
self.req_dir = req_dir
self.project = project
@@ -306,6 +312,11 @@ class Resolver(object):
self.clear = clear
self.pre = pre
self.results = None
self.markers_lookup = markers_lookup if markers_lookup is not None else {}
self.index_lookup = index_lookup if index_lookup is not None else {}
self.skipped = skipped if skipped is not None else {}
self.markers = {}
self.requires_python_markers = {}
self._pip_args = None
self._constraints = None
self._parsed_constraints = None
@@ -343,7 +354,10 @@ class Resolver(object):
index_lookup, # type: Dict[str, str]
markers_lookup, # type: Dict[str, str]
project, # type: Project
sources # type: Dict[str, str]
sources, # type: Dict[str, str]
req_dir=None, # type: Optional[str]
pre=False, # type: bool
clear=False, # type: bool
):
# type: (...) -> Tuple[Set[str], Dict[str, Dict[str, Union[str, bool, List[str]]]], Dict[str, str], Dict[str, str]]
constraints = set() # type: Set[str]
@@ -352,6 +366,13 @@ class Resolver(object):
index_lookup = {}
if markers_lookup is None:
markers_lookup = {}
if not req_dir:
from .vendor.vistir.path import create_tracked_tempdir
req_dir = create_tracked_tempdir(prefix="pipenv-", suffix="-reqdir")
transient_resolver = cls(
[], req_dir, project, sources, index_lookup=index_lookup,
markers_lookup=markers_lookup, clear=clear, pre=pre
)
for dep in deps:
if not dep:
continue
@@ -360,7 +381,9 @@ class Resolver(object):
)
index_lookup.update(req_idx)
markers_lookup.update(markers_idx)
constraint_update, lockfile_update = cls.get_deps_from_req(req)
constraint_update, lockfile_update = cls.get_deps_from_req(
req, resolver=transient_resolver
)
constraints |= constraint_update
skipped.update(lockfile_update)
return constraints, skipped, index_lookup, markers_lookup
@@ -417,12 +440,13 @@ class Resolver(object):
return cls.get_deps_from_req(req)
@classmethod
def get_deps_from_req(cls, req):
# type: (Requirement) -> Tuple[Set[str], Dict[str, Dict[str, Union[str, bool, List[str]]]]]
from requirementslib.models.utils import _requirement_to_str_lowercase_name
def get_deps_from_req(cls, req, resolver=None):
# type: (Requirement, Optional["Resolver"]) -> Tuple[Set[str], Dict[str, Dict[str, Union[str, bool, List[str]]]]]
from .vendor.requirementslib.models.utils import _requirement_to_str_lowercase_name
from .vendor.requirementslib.models.requirements import Requirement
constraints = set() # type: Set[str]
locked_deps = dict() # type: Dict[str, Dict[str, Union[str, bool, List[str]]]]
if req.is_file_or_url or req.is_vcs and not req.is_wheel:
if (req.is_file_or_url or req.is_vcs) and not req.is_wheel:
# for local packages with setup.py files and potential direct url deps:
if req.is_vcs:
req_list, lockfile = get_vcs_deps(reqs=[req])
@@ -437,6 +461,7 @@ class Resolver(object):
except TypeError:
raise RequirementError(req=req)
setup_info = req.req.setup_info
setup_info.get_info()
locked_deps[pep423_name(name)] = entry
requirements = [v for v in getattr(setup_info, "requires", {}).values()]
for r in requirements:
@@ -446,20 +471,22 @@ class Resolver(object):
continue
line = _requirement_to_str_lowercase_name(r)
new_req, _, _ = cls.parse_line(line)
if r.marker and not r.marker.evaluate():
new_constraints = {}
_, new_entry = req.pipfile_entry
new_lock = {
pep423_name(new_req.normalized_name): new_entry
}
else:
new_constraints, new_lock = cls.get_deps_from_req(new_req)
locked_deps.update(new_lock)
constraints |= new_constraints
else:
if r is not None:
line = _requirement_to_str_lowercase_name(r)
constraints.add(line)
if r.marker and not r.marker.evaluate():
new_constraints = {}
_, new_entry = req.pipfile_entry
new_lock = {
pep423_name(new_req.normalized_name): new_entry
}
else:
new_constraints, new_lock = cls.get_deps_from_req(
new_req, resolver
)
locked_deps.update(new_lock)
constraints |= new_constraints
# if there is no marker or there is a valid marker, add the constraint line
elif r and (not r.marker or (r.marker and r.marker.evaluate())):
line = _requirement_to_str_lowercase_name(r)
constraints.add(line)
# ensure the top level entry remains as provided
# note that we shouldn't pin versions for editable vcs deps
if (not req.is_vcs or (req.is_vcs and not req.editable)):
@@ -477,10 +504,58 @@ class Resolver(object):
req.req.setup_path is not None and os.path.exists(req.req.setup_path)):
constraints.add(req.constraint_line)
else:
# if the dependency isn't installable, don't add it to constraints
# and instead add it directly to the lock
if req and req.requirement and (
req.requirement.marker and not req.requirement.marker.evaluate()
):
pypi = resolver.repository if resolver else None
best_match = pypi.find_best_match(req.ireq) if pypi else None
if best_match:
hashes = resolver.collect_hashes(best_match) if resolver else []
new_req = Requirement.from_ireq(best_match)
new_req = new_req.add_hashes(hashes)
name, entry = new_req.pipfile_entry
locked_deps[pep423_name(name)] = translate_markers(entry)
return constraints, locked_deps
constraints.add(req.constraint_line)
return constraints, locked_deps
return constraints, locked_deps
@classmethod
def create(
cls,
deps, # type: List[str]
index_lookup=None, # type: Dict[str, str]
markers_lookup=None, # type: Dict[str, str]
project=None, # type: Project
sources=None, # type: List[str]
req_dir=None, # type: str
clear=False, # type: bool
pre=False # type: bool
):
# type: (...) -> "Resolver"
from pipenv.vendor.vistir.path import create_tracked_tempdir
if not req_dir:
req_dir = create_tracked_tempdir(suffix="-requirements", prefix="pipenv-")
if index_lookup is None:
index_lookup = {}
if markers_lookup is None:
markers_lookup = {}
if project is None:
from pipenv.core import project
project = project
if sources is None:
sources = project.sources
constraints, skipped, index_lookup, markers_lookup = cls.get_metadata(
deps, index_lookup, markers_lookup, project, sources, req_dir=req_dir,
pre=pre, clear=clear
)
return Resolver(
constraints, req_dir, project, sources, index_lookup=index_lookup,
markers_lookup=markers_lookup, skipped=skipped, clear=clear, pre=pre
)
@property
def pip_command(self):
if self._pip_command is None:
@@ -529,16 +604,16 @@ class Resolver(object):
pip_options, _ = self.pip_command.parser.parse_args(self.pip_args)
pip_options.cache_dir = environments.PIPENV_CACHE_DIR
self._pip_options = pip_options
if environments.is_verbose():
click_echo(
crayons.blue("Using pip: {0}".format(" ".join(self.pip_args))), err=True
)
return self._pip_options
@property
def session(self):
if self._session is None:
self._session = self.pip_command._build_session(self.pip_options)
# if environments.is_verbose():
# click_echo(
# crayons.blue("Using pip: {0}".format(" ".join(self.pip_args))), err=True
# )
return self._session
@property
@@ -602,6 +677,35 @@ class Resolver(object):
self.resolved_tree.update(results)
return self.resolved_tree
@lru_cache(maxsize=1024)
def fetch_candidate(self, ireq):
candidates = self.repository.find_all_candidates(ireq.name)
matched_version = next(iter(sorted(
ireq.specifier.filter((c.version for c in candidates), True), reverse=True)
), None)
if matched_version:
matched_candidate = next(iter(
c for c in candidates if c.version == matched_version
))
return matched_candidate
return None
def resolve_constraints(self):
new_tree = set()
for result in self.resolved_tree:
if result.markers:
self.markers[result.name] = result.markers
else:
candidate = self.fetch_candidate(result)
if getattr(candidate, "requires_python", None):
marker = make_marker_from_specifier(candidate.requires_python)
self.markers[result.name] = marker
result.markers = marker
if result.req:
result.req.marker = marker
new_tree.add(result)
self.resolved_tree = new_tree
@classmethod
def prepend_hash_types(cls, checksums):
cleaned_checksums = []
@@ -720,6 +824,89 @@ class Resolver(object):
self.hashes[ireq] = self.get_hash(ireq, ireq_hashes=ireq_hashes)
return self.hashes
def _clean_skipped_result(self, req, value):
ref = None
if req.is_vcs:
ref = req.commit_hash
ireq = req.as_ireq()
entry = value.copy()
entry["name"] = req.name
if entry.get("editable", False) and entry.get("version"):
del entry["version"]
ref = ref if ref is not None else entry.get("ref")
if ref:
entry["ref"] = ref
if self._should_include_hash(ireq):
collected_hashes = self.collect_hashes(ireq)
if collected_hashes:
entry["hashes"] = sorted(set(collected_hashes))
return req.name, entry
def clean_results(self):
from pipenv.vendor.requirementslib.models.requirements import Requirement
reqs = [(Requirement.from_ireq(ireq), ireq) for ireq in self.resolved_tree]
results = {}
for req, ireq in reqs:
if (req.vcs and req.editable and not req.is_direct_url):
continue
collected_hashes = self.collect_hashes(ireq)
req = req.add_hashes(collected_hashes)
if not collected_hashes and self._should_include_hash(ireq):
discovered_hashes = self.hashes.get(ireq, set()) | self.get_hash(ireq)
if discovered_hashes:
req = req.add_hashes(discovered_hashes)
self.hashes[ireq] = collected_hashes = discovered_hashes
if collected_hashes:
collected_hashes = sorted(set(collected_hashes))
name, entry = format_requirement_for_lockfile(
req, self.markers_lookup, self.index_lookup, collected_hashes
)
entry = translate_markers(entry)
if name in results:
results[name].update(entry)
else:
results[name] = entry
for k in list(self.skipped.keys()):
req = Requirement.from_pipfile(k, self.skipped[k])
name, entry = self._clean_skipped_result(req, self.skipped[k])
entry = translate_markers(entry)
if name in results:
results[name].update(entry)
else:
results[name] = entry
results = list(results.values())
return results
def format_requirement_for_lockfile(req, markers_lookup, index_lookup, hashes=None):
if req.specifiers:
version = str(req.get_version())
else:
version = None
index = index_lookup.get(req.normalized_name)
markers = markers_lookup.get(req.normalized_name)
req.index = index
name, pf_entry = req.pipfile_entry
name = pep423_name(req.name)
entry = {}
if isinstance(pf_entry, six.string_types):
entry["version"] = pf_entry.lstrip("=")
else:
entry.update(pf_entry)
if version is not None:
entry["version"] = version
if req.line_instance.is_direct_url:
entry["file"] = req.req.uri
if hashes:
entry["hashes"] = sorted(set(hashes))
entry["name"] = name
if index: # and index != next(iter(project.sources), {}).get("name"):
entry.update({"index": index})
if markers:
entry.update({"markers": markers})
entry = translate_markers(entry)
return name, entry
def _show_warning(message, category, filename, lineno, line):
warnings.showwarning(message=message, category=category, filename=filename,
@@ -738,87 +925,23 @@ def actually_resolve_deps(
req_dir=None,
):
from pipenv.vendor.vistir.path import create_tracked_tempdir
from pipenv.vendor.requirementslib.models.requirements import Requirement
if not req_dir:
req_dir = create_tracked_tempdir(suffix="-requirements", prefix="pipenv-")
warning_list = []
with warnings.catch_warnings(record=True) as warning_list:
constraints, skipped, index_lookup, markers_lookup = Resolver.get_metadata(
deps, index_lookup, markers_lookup, project, sources,
resolver = Resolver.create(
deps, index_lookup, markers_lookup, project, sources, req_dir, clear, pre
)
resolver = Resolver(constraints, req_dir, project, sources, clear=clear, pre=pre)
resolved_tree = resolver.resolve()
resolver.resolve()
hashes = resolver.resolve_hashes()
reqs = [(Requirement.from_ireq(ireq), ireq) for ireq in resolved_tree]
results = {}
for req, ireq in reqs:
if (req.vcs and req.editable and not req.is_direct_url):
continue
collected_hashes = resolver.collect_hashes(ireq)
if collected_hashes:
req = req.add_hashes(collected_hashes)
elif resolver._should_include_hash(ireq):
existing_hashes = hashes.get(ireq, set())
discovered_hashes = existing_hashes | resolver.get_hash(ireq)
if discovered_hashes:
req = req.add_hashes(discovered_hashes)
resolver.hashes[ireq] = discovered_hashes
if req.specifiers:
version = str(req.get_version())
else:
version = None
index = index_lookup.get(req.normalized_name)
markers = markers_lookup.get(req.normalized_name)
req.index = index
name, pf_entry = req.pipfile_entry
name = pep423_name(req.name)
entry = {}
if isinstance(pf_entry, six.string_types):
entry["version"] = pf_entry.lstrip("=")
else:
entry.update(pf_entry)
if version is not None:
entry["version"] = version
if req.line_instance.is_direct_url:
entry["file"] = req.req.uri
if collected_hashes:
entry["hashes"] = sorted(set(collected_hashes))
entry["name"] = name
if index: # and index != next(iter(project.sources), {}).get("name"):
entry.update({"index": index})
if markers:
entry.update({"markers": markers})
entry = translate_markers(entry)
if name in results:
results[name].update(entry)
else:
results[name] = entry
for k in list(skipped.keys()):
req = Requirement.from_pipfile(k, skipped[k])
ref = None
if req.is_vcs:
ref = req.commit_hash
ireq = req.as_ireq()
entry = skipped[k].copy()
entry["name"] = req.name
ref = ref if ref is not None else entry.get("ref")
if ref:
entry["ref"] = ref
if resolver._should_include_hash(ireq):
collected_hashes = resolver.collect_hashes(ireq)
if collected_hashes:
entry["hashes"] = sorted(set(collected_hashes))
if k in results:
results[k].update(entry)
else:
results[k] = entry
results = list(results.values())
resolver.resolve_constraints()
results = resolver.clean_results()
for warning in warning_list:
_show_warning(warning.message, warning.category, warning.filename, warning.lineno,
warning.line)
return (results, hashes, markers_lookup, resolver, skipped)
return (results, hashes, resolver.markers_lookup, resolver, resolver.skipped)
@contextlib.contextmanager
@@ -842,33 +965,40 @@ def resolve(cmd, sp):
from .cmdparse import Script
from .vendor.pexpect.exceptions import EOF, TIMEOUT
from .vendor.vistir.compat import to_native_string
from .vendor.vistir.misc import echo
EOF.__module__ = "pexpect.exceptions"
from ._compat import decode_output
c = delegator.run(Script.parse(cmd).cmdify(), block=False, env=os.environ.copy())
if environments.is_verbose():
c.subprocess.logfile = sys.stderr
_out = decode_output("")
result = None
out = to_native_string("")
while True:
result = None
try:
result = c.expect(u"\n", timeout=environments.PIPENV_INSTALL_TIMEOUT)
except (EOF, TIMEOUT):
pass
_out = c.subprocess.before
if _out is not None:
_out = decode_output("{0}".format(_out))
if _out:
_out = decode_output("{0}\n".format(_out))
out += _out
sp.text = to_native_string("{0}".format(_out[:100]))
if environments.is_verbose():
sp.hide_and_write(_out.rstrip())
if result is None:
# if environments.is_verbose():
# sp.hide_and_write(_out.rstrip())
_out = to_native_string("")
if not result and not _out:
break
c.block()
if c.return_code != 0:
sp.red.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format(
"Locking Failed!"
))
click_echo(c.out.strip(), err=True)
click_echo(c.err.strip(), err=True)
echo(c.out.strip(), err=True)
if not environments.is_verbose():
echo(out, err=True)
echo(c.err.strip(), err=True)
sys.exit(c.return_code)
return c
@@ -922,6 +1052,7 @@ def prepare_lockfile(results, pipfile, lockfile):
lockfile[name] = lockfile_entry[name]
else:
lockfile[name].update(lockfile_entry[name])
lockfile[name] = translate_markers(lockfile[name])
else:
lockfile[name] = lockfile_entry[name]
return lockfile
@@ -1038,9 +1169,6 @@ def venv_resolve_deps(
raise RuntimeError("There was a problem with locking.")
if os.path.exists(target_file.name):
os.unlink(target_file.name)
if environments.is_verbose():
click_echo(results, err=True)
if lockfile_section not in lockfile:
lockfile[lockfile_section] = {}
prepare_lockfile(results, pipfile, lockfile[lockfile_section])
@@ -1607,23 +1735,24 @@ def translate_markers(pipfile_entry):
"""
if not isinstance(pipfile_entry, Mapping):
raise TypeError("Entry is not a pipfile formatted mapping.")
from .vendor.distlib.markers import DEFAULT_CONTEXT as marker_context
from .vendor.packaging.markers import Marker
from .vendor.packaging.markers import Marker, default_environment
from .vendor.vistir.misc import dedup
allowed_marker_keys = ["markers"] + [k for k in marker_context.keys()]
allowed_marker_keys = ["markers"] + list(default_environment().keys())
provided_keys = list(pipfile_entry.keys()) if hasattr(pipfile_entry, "keys") else []
pipfile_markers = [k for k in provided_keys if k in allowed_marker_keys]
pipfile_markers = set(provided_keys) & set(allowed_marker_keys)
new_pipfile = dict(pipfile_entry).copy()
marker_set = set()
if "markers" in new_pipfile:
marker = str(Marker(new_pipfile.pop("markers")))
if 'extra' not in marker:
marker_set.add(marker)
marker_str = new_pipfile.pop("markers")
if marker_str is not None:
marker = str(Marker(marker_str))
if 'extra' not in marker:
marker_set.add(marker)
for m in pipfile_markers:
entry = "{0}".format(pipfile_entry[m])
if m != "markers":
marker_set.add(str(Marker("{0}{1}".format(m, entry))))
marker_set.add(str(Marker("{0} {1}".format(m, entry))))
new_pipfile.pop(m)
if marker_set:
new_pipfile["markers"] = str(Marker(" or ".join(
@@ -1639,7 +1768,7 @@ def clean_resolved_dep(dep, is_top_level=False, pipfile_entry=None):
lockfile = {}
# We use this to determine if there are any markers on top level packages
# So we can make sure those win out during resolution if the packages reoccur
if "version" in dep:
if "version" in dep and dep["version"] and not dep.get("editable", False):
version = "{0}".format(dep["version"])
if not version.startswith("=="):
version = "=={0}".format(version)
@@ -1939,3 +2068,26 @@ def is_python_command(line):
if line.startswith("py"):
return True
return False
def make_marker_from_specifier(spec):
# type: (str) -> Optional[Marker]
"""Given a python version specifier, create a marker
:param spec: A specifier
:type spec: str
:return: A new marker
:rtype: Optional[:class:`packaging.marker.Marker`]
"""
from .vendor.packaging.specifiers import SpecifierSet, Specifier
from .vendor.packaging.markers import Marker
from .vendor.requirementslib.models.markers import cleanup_pyspecs, format_pyversion
if not any(spec.startswith(k) for k in Specifier._operators.keys()):
if spec.strip().lower() in ["any", "<any>", "*"]:
return None
spec = "=={0}".format(spec)
elif spec.startswith("==") and spec.count("=") > 3:
spec = "=={0}".format(spec.lstrip("="))
specset = cleanup_pyspecs(SpecifierSet(spec))
marker_str = " and ".join([format_pyversion(pv) for pv in specset])
return Marker(marker_str)
+1 -1
View File
@@ -1,5 +1,5 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
from . import weakref
from . import enum
from . import shutil_get_terminal_size
from . import enum
from . import functools_lru_cache
+9 -6
View File
@@ -10,20 +10,23 @@
from __future__ import absolute_import
from pkg_resources import get_distribution, DistributionNotFound
from cerberus.validator import DocumentError, Validator
from cerberus.schema import (rules_set_registry, schema_registry, Registry,
SchemaError)
from cerberus.schema import rules_set_registry, schema_registry, SchemaError
from cerberus.utils import TypeDefinition
__version__ = "1.2"
try:
__version__ = get_distribution("Cerberus").version
except DistributionNotFound:
__version__ = "unknown"
__all__ = [
DocumentError.__name__,
Registry.__name__,
SchemaError.__name__,
TypeDefinition.__name__,
Validator.__name__,
'schema_registry',
'rules_set_registry'
"schema_registry",
"rules_set_registry",
]
+105 -97
View File
@@ -3,12 +3,12 @@
from __future__ import absolute_import
from collections import defaultdict, namedtuple, MutableMapping
from collections import defaultdict, namedtuple
from copy import copy, deepcopy
from functools import wraps
from pprint import pformat
from cerberus.platform import PYTHON_VERSION
from cerberus.platform import PYTHON_VERSION, MutableMapping
from cerberus.utils import compare_paths_lt, quote_string
@@ -54,6 +54,7 @@ UNALLOWED_VALUE = ErrorDefinition(0x44, 'allowed')
UNALLOWED_VALUES = ErrorDefinition(0x45, 'allowed')
FORBIDDEN_VALUE = ErrorDefinition(0x46, 'forbidden')
FORBIDDEN_VALUES = ErrorDefinition(0x47, 'forbidden')
MISSING_MEMBERS = ErrorDefinition(0x48, 'contains')
# other
NORMALIZATION = ErrorDefinition(0x60, None)
@@ -66,9 +67,10 @@ SETTING_DEFAULT_FAILED = ErrorDefinition(0x64, 'default_setter')
ERROR_GROUP = ErrorDefinition(0x80, None)
MAPPING_SCHEMA = ErrorDefinition(0x81, 'schema')
SEQUENCE_SCHEMA = ErrorDefinition(0x82, 'schema')
KEYSCHEMA = ErrorDefinition(0x83, 'keyschema')
VALUESCHEMA = ErrorDefinition(0x84, 'valueschema')
BAD_ITEMS = ErrorDefinition(0x8f, 'items')
# TODO remove KEYSCHEMA AND VALUESCHEMA with next major release
KEYSRULES = KEYSCHEMA = ErrorDefinition(0x83, 'keysrules')
VALUESRULES = VALUESCHEMA = ErrorDefinition(0x84, 'valuesrules')
BAD_ITEMS = ErrorDefinition(0x8F, 'items')
LOGICAL = ErrorDefinition(0x90, None)
NONEOF = ErrorDefinition(0x91, 'noneof')
@@ -79,8 +81,7 @@ ALLOF = ErrorDefinition(0x94, 'allof')
""" SchemaError messages """
SCHEMA_ERROR_DEFINITION_TYPE = \
"schema definition for field '{0}' must be a dict"
SCHEMA_ERROR_DEFINITION_TYPE = "schema definition for field '{0}' must be a dict"
SCHEMA_ERROR_MISSING = "validation schema missing"
@@ -89,8 +90,8 @@ SCHEMA_ERROR_MISSING = "validation schema missing"
class ValidationError(object):
""" A simple class to store and query basic error information. """
def __init__(self, document_path, schema_path, code, rule, constraint,
value, info):
def __init__(self, document_path, schema_path, code, rule, constraint, value, info):
self.document_path = document_path
""" The path to the field within the document that caused the error.
Type: :class:`tuple` """
@@ -115,8 +116,7 @@ class ValidationError(object):
def __hash__(self):
""" Expects that all other properties are transitively determined. """
return hash(self.document_path) ^ hash(self.schema_path) \
^ hash(self.code)
return hash(self.document_path) ^ hash(self.schema_path) ^ hash(self.code)
def __lt__(self, other):
if self.document_path != other.document_path:
@@ -125,20 +125,24 @@ class ValidationError(object):
return compare_paths_lt(self.schema_path, other.schema_path)
def __repr__(self):
return "{class_name} @ {memptr} ( " \
"document_path={document_path}," \
"schema_path={schema_path}," \
"code={code}," \
"constraint={constraint}," \
"value={value}," \
"info={info} )"\
.format(class_name=self.__class__.__name__, memptr=hex(id(self)), # noqa: E501
document_path=self.document_path,
schema_path=self.schema_path,
code=hex(self.code),
constraint=quote_string(self.constraint),
value=quote_string(self.value),
info=self.info)
return (
"{class_name} @ {memptr} ( "
"document_path={document_path},"
"schema_path={schema_path},"
"code={code},"
"constraint={constraint},"
"value={value},"
"info={info} )".format(
class_name=self.__class__.__name__,
memptr=hex(id(self)), # noqa: E501
document_path=self.document_path,
schema_path=self.schema_path,
code=hex(self.code),
constraint=quote_string(self.constraint),
value=quote_string(self.value),
info=self.info,
)
)
@property
def child_errors(self):
@@ -190,11 +194,13 @@ class ErrorList(list):
""" A list for :class:`~cerberus.errors.ValidationError` instances that
can be queried with the ``in`` keyword for a particular
:class:`~cerberus.errors.ErrorDefinition`. """
def __contains__(self, error_definition):
for code in (x.code for x in self):
if code == error_definition.code:
return True
return False
if not isinstance(error_definition, ErrorDefinition):
raise TypeError
wanted_code = error_definition.code
return any(x.code == wanted_code for x in self)
class ErrorTreeNode(MutableMapping):
@@ -203,14 +209,10 @@ class ErrorTreeNode(MutableMapping):
def __init__(self, path, parent_node):
self.parent_node = parent_node
self.tree_root = self.parent_node.tree_root
self.path = path[:self.parent_node.depth + 1]
self.path = path[: self.parent_node.depth + 1]
self.errors = ErrorList()
self.descendants = {}
def __add__(self, error):
self.add(error)
return self
def __contains__(self, item):
if isinstance(item, ErrorDefinition):
return item in self.errors
@@ -228,6 +230,7 @@ class ErrorTreeNode(MutableMapping):
for error in self.errors:
if item.code == error.code:
return error
return None
else:
return self.descendants.get(item)
@@ -258,14 +261,16 @@ class ErrorTreeNode(MutableMapping):
if key not in self.descendants:
self[key] = ErrorTreeNode(error_path, self)
node = self[key]
if len(error_path) == self.depth + 1:
self[key].errors.append(error)
self[key].errors.sort()
node.errors.append(error)
node.errors.sort()
if error.is_group_error:
for child_error in error.child_errors:
self.tree_root += child_error
self.tree_root.add(child_error)
else:
self[key] += error
node.add(error)
def _path_of_(self, error):
return getattr(error, self.tree_type + '_path')
@@ -274,14 +279,15 @@ class ErrorTreeNode(MutableMapping):
class ErrorTree(ErrorTreeNode):
""" Base class for :class:`~cerberus.errors.DocumentErrorTree` and
:class:`~cerberus.errors.SchemaErrorTree`. """
def __init__(self, errors=[]):
def __init__(self, errors=()):
self.parent_node = None
self.tree_root = self
self.path = ()
self.errors = ErrorList()
self.descendants = {}
for error in errors:
self += error
self.add(error)
def add(self, error):
""" Add an error to the tree.
@@ -323,18 +329,21 @@ class ErrorTree(ErrorTreeNode):
class DocumentErrorTree(ErrorTree):
""" Implements a dict-like class to query errors by indexes following the
structure of a validated document. """
tree_type = 'document'
class SchemaErrorTree(ErrorTree):
""" Implements a dict-like class to query errors by indexes following the
structure of the used schema. """
tree_type = 'schema'
class BaseErrorHandler(object):
""" Base class for all error handlers.
Subclasses are identified as error-handlers with an instance-test. """
def __init__(self, *args, **kwargs):
""" Optionally initialize a new instance. """
pass
@@ -411,9 +420,9 @@ def encode_unicode(f):
This decorator ensures that if legacy Python is used unicode
strings are encoded before passing to a function.
"""
@wraps(f)
def wrapped(obj, error):
def _encode(value):
"""Helper encoding unicode strings into binary utf-8"""
if isinstance(value, unicode): # noqa: F821
@@ -436,56 +445,52 @@ class BasicErrorHandler(BaseErrorHandler):
through :class:`str` a pretty-formatted representation of that
tree is returned.
"""
messages = {0x00: "{0}",
0x01: "document is missing",
0x02: "required field",
0x03: "unknown field",
0x04: "field '{0}' is required",
0x05: "depends on these values: {constraint}",
0x06: "{0} must not be present with '{field}'",
0x21: "'{0}' is not a document, must be a dict",
0x22: "empty values not allowed",
0x23: "null value not allowed",
0x24: "must be of {constraint} type",
0x25: "must be of dict type",
0x26: "length of list should be {constraint}, it is {0}",
0x27: "min length is {constraint}",
0x28: "max length is {constraint}",
0x41: "value does not match regex '{constraint}'",
0x42: "min value is {constraint}",
0x43: "max value is {constraint}",
0x44: "unallowed value {value}",
0x45: "unallowed values {0}",
0x46: "unallowed value {value}",
0x47: "unallowed values {0}",
0x61: "field '{field}' cannot be coerced: {0}",
0x62: "field '{field}' cannot be renamed: {0}",
0x63: "field is read-only",
0x64: "default value for '{field}' cannot be set: {0}",
0x81: "mapping doesn't validate subschema: {0}",
0x82: "one or more sequence-items don't validate: {0}",
0x83: "one or more keys of a mapping don't validate: {0}",
0x84: "one or more values in a mapping don't validate: {0}",
0x85: "one or more sequence-items don't validate: {0}",
0x91: "one or more definitions validate",
0x92: "none or more than one rule validate",
0x93: "no definitions validate",
0x94: "one or more definitions don't validate"
}
messages = {
0x00: "{0}",
0x01: "document is missing",
0x02: "required field",
0x03: "unknown field",
0x04: "field '{0}' is required",
0x05: "depends on these values: {constraint}",
0x06: "{0} must not be present with '{field}'",
0x21: "'{0}' is not a document, must be a dict",
0x22: "empty values not allowed",
0x23: "null value not allowed",
0x24: "must be of {constraint} type",
0x25: "must be of dict type",
0x26: "length of list should be {constraint}, it is {0}",
0x27: "min length is {constraint}",
0x28: "max length is {constraint}",
0x41: "value does not match regex '{constraint}'",
0x42: "min value is {constraint}",
0x43: "max value is {constraint}",
0x44: "unallowed value {value}",
0x45: "unallowed values {0}",
0x46: "unallowed value {value}",
0x47: "unallowed values {0}",
0x48: "missing members {0}",
0x61: "field '{field}' cannot be coerced: {0}",
0x62: "field '{field}' cannot be renamed: {0}",
0x63: "field is read-only",
0x64: "default value for '{field}' cannot be set: {0}",
0x81: "mapping doesn't validate subschema: {0}",
0x82: "one or more sequence-items don't validate: {0}",
0x83: "one or more keys of a mapping don't validate: {0}",
0x84: "one or more values in a mapping don't validate: {0}",
0x85: "one or more sequence-items don't validate: {0}",
0x91: "one or more definitions validate",
0x92: "none or more than one rule validate",
0x93: "no definitions validate",
0x94: "one or more definitions don't validate",
}
def __init__(self, tree=None):
self.tree = {} if tree is None else tree
def __call__(self, errors=None):
if errors is not None:
self.clear()
self.extend(errors)
def __call__(self, errors):
self.clear()
self.extend(errors)
return self.pretty_tree
def __str__(self):
@@ -511,8 +516,9 @@ class BasicErrorHandler(BaseErrorHandler):
elif error.is_group_error:
self._insert_group_error(error)
elif error.code in self.messages:
self._insert_error(error.document_path,
self._format_message(error.field, error))
self._insert_error(
error.document_path, self._format_message(error.field, error)
)
def clear(self):
self.tree = {}
@@ -522,8 +528,8 @@ class BasicErrorHandler(BaseErrorHandler):
def _format_message(self, field, error):
return self.messages[error.code].format(
*error.info, constraint=error.constraint,
field=field, value=error.value)
*error.info, constraint=error.constraint, field=field, value=error.value
)
def _insert_error(self, path, node):
""" Adds an error or sub-tree to :attr:tree.
@@ -559,14 +565,14 @@ class BasicErrorHandler(BaseErrorHandler):
elif child_error.is_group_error:
self._insert_group_error(child_error)
else:
self._insert_error(child_error.document_path,
self._format_message(child_error.field,
child_error))
self._insert_error(
child_error.document_path,
self._format_message(child_error.field, child_error),
)
def _insert_logic_error(self, error):
field = error.field
self._insert_error(error.document_path,
self._format_message(field, error))
self._insert_error(error.document_path, self._format_message(field, error))
for definition_errors in error.definitions_errors.values():
for child_error in definition_errors:
@@ -575,8 +581,10 @@ class BasicErrorHandler(BaseErrorHandler):
elif child_error.is_group_error:
self._insert_group_error(child_error)
else:
self._insert_error(child_error.document_path,
self._format_message(field, child_error))
self._insert_error(
child_error.document_path,
self._format_message(field, child_error),
)
def _purge_empty_dicts(self, error_list):
subtree = error_list[-1]
+26
View File
@@ -12,3 +12,29 @@ if PYTHON_VERSION < 3:
else:
_str_type = str
_int_types = (int,)
if PYTHON_VERSION < 3.3:
from collections import ( # noqa: F401
Callable,
Container,
Hashable,
Iterable,
Mapping,
MutableMapping,
Sequence,
Set,
Sized,
)
else:
from collections.abc import ( # noqa: F401
Callable,
Container,
Hashable,
Iterable,
Mapping,
MutableMapping,
Sequence,
Set,
Sized,
)
+176 -125
View File
@@ -1,13 +1,23 @@
from __future__ import absolute_import
from collections import (Callable, Hashable, Iterable, Mapping,
MutableMapping, Sequence)
from copy import copy
from warnings import warn
from cerberus import errors
from cerberus.platform import _str_type
from cerberus.utils import (get_Validator_class, validator_factory,
mapping_hash, TypeDefinition)
from cerberus.platform import (
_str_type,
Callable,
Hashable,
Mapping,
MutableMapping,
Sequence,
)
from cerberus.utils import (
get_Validator_class,
validator_factory,
mapping_hash,
TypeDefinition,
)
class _Abort(Exception):
@@ -17,6 +27,7 @@ class _Abort(Exception):
class SchemaError(Exception):
""" Raised when the validation schema is missing, has the wrong format or
contains errors. """
pass
@@ -26,18 +37,19 @@ class DefinitionSchema(MutableMapping):
def __new__(cls, *args, **kwargs):
if 'SchemaValidator' not in globals():
global SchemaValidator
SchemaValidator = validator_factory('SchemaValidator',
SchemaValidatorMixin)
SchemaValidator = validator_factory('SchemaValidator', SchemaValidatorMixin)
types_mapping = SchemaValidator.types_mapping.copy()
types_mapping.update({
'callable': TypeDefinition('callable', (Callable,), ()),
'hashable': TypeDefinition('hashable', (Hashable,), ())
})
types_mapping.update(
{
'callable': TypeDefinition('callable', (Callable,), ()),
'hashable': TypeDefinition('hashable', (Hashable,), ()),
}
)
SchemaValidator.types_mapping = types_mapping
return super(DefinitionSchema, cls).__new__(cls)
def __init__(self, validator, schema={}):
def __init__(self, validator, schema):
"""
:param validator: An instance of Validator-(sub-)class that uses this
schema.
@@ -45,8 +57,7 @@ class DefinitionSchema(MutableMapping):
one.
"""
if not isinstance(validator, get_Validator_class()):
raise RuntimeError('validator argument must be a Validator-'
'instance.')
raise RuntimeError('validator argument must be a Validator-' 'instance.')
self.validator = validator
if isinstance(schema, _str_type):
@@ -56,14 +67,16 @@ class DefinitionSchema(MutableMapping):
try:
schema = dict(schema)
except Exception:
raise SchemaError(
errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema))
raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema))
self.validation_schema = SchemaValidationSchema(validator)
self.schema_validator = SchemaValidator(
None, allow_unknown=self.validation_schema,
None,
allow_unknown=self.validation_schema,
error_handler=errors.SchemaErrorHandler,
target_schema=schema, target_validator=validator)
target_schema=schema,
target_validator=validator,
)
schema = self.expand(schema)
self.validate(schema)
@@ -110,6 +123,10 @@ class DefinitionSchema(MutableMapping):
schema = cls._expand_subschemas(schema)
except Exception:
pass
# TODO remove this with the next major release
schema = cls._rename_deprecated_rulenames(schema)
return schema
@classmethod
@@ -119,13 +136,15 @@ class DefinitionSchema(MutableMapping):
:param schema: The schema-definition to expand.
:return: The expanded schema-definition.
"""
def is_of_rule(x):
return isinstance(x, _str_type) and \
x.startswith(('allof_', 'anyof_', 'noneof_', 'oneof_'))
return isinstance(x, _str_type) and x.startswith(
('allof_', 'anyof_', 'noneof_', 'oneof_')
)
for field in schema:
for of_rule in (x for x in schema[field] if is_of_rule(x)):
operator, rule = of_rule.split('_')
operator, rule = of_rule.split('_', 1)
schema[field].update({operator: []})
for value in schema[field][of_rule]:
schema[field][operator].append({rule: value})
@@ -135,15 +154,15 @@ class DefinitionSchema(MutableMapping):
@classmethod
def _expand_subschemas(cls, schema):
def has_schema_rule():
return isinstance(schema[field], Mapping) and \
'schema' in schema[field]
return isinstance(schema[field], Mapping) and 'schema' in schema[field]
def has_mapping_schema():
""" Tries to determine heuristically if the schema-constraints are
aimed to mappings. """
try:
return all(isinstance(x, Mapping) for x
in schema[field]['schema'].values())
return all(
isinstance(x, Mapping) for x in schema[field]['schema'].values()
)
except TypeError:
return False
@@ -153,13 +172,12 @@ class DefinitionSchema(MutableMapping):
elif has_mapping_schema():
schema[field]['schema'] = cls.expand(schema[field]['schema'])
else: # assumes schema-constraints for a sequence
schema[field]['schema'] = \
cls.expand({0: schema[field]['schema']})[0]
schema[field]['schema'] = cls.expand({0: schema[field]['schema']})[0]
for rule in ('keyschema', 'valueschema'):
# TODO remove the last two values in the tuple with the next major release
for rule in ('keysrules', 'valuesrules', 'keyschema', 'valueschema'):
if rule in schema[field]:
schema[field][rule] = \
cls.expand({0: schema[field][rule]})[0]
schema[field][rule] = cls.expand({0: schema[field][rule]})[0]
for rule in ('allof', 'anyof', 'items', 'noneof', 'oneof'):
if rule in schema[field]:
@@ -171,6 +189,12 @@ class DefinitionSchema(MutableMapping):
schema[field][rule] = new_rules_definition
return schema
def get(self, item, default=None):
return self.schema.get(item, default)
def items(self):
return self.schema.items()
def update(self, schema):
try:
schema = self.expand(schema)
@@ -178,31 +202,64 @@ class DefinitionSchema(MutableMapping):
_new_schema.update(schema)
self.validate(_new_schema)
except ValueError:
raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE
.format(schema))
raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema))
except Exception as e:
raise e
else:
self.schema = _new_schema
# TODO remove with next major release
@staticmethod
def _rename_deprecated_rulenames(schema):
for field, rules in schema.items():
if isinstance(rules, str): # registry reference
continue
for old, new in (
('keyschema', 'keysrules'),
('validator', 'check_with'),
('valueschema', 'valuesrules'),
):
if old not in rules:
continue
if new in rules:
raise RuntimeError(
"The rule '{new}' is also present with its old "
"name '{old}' in the same set of rules."
)
warn(
"The rule '{old}' was renamed to '{new}'. The old name will "
"not be available in the next major release of "
"Cerberus.".format(old=old, new=new),
DeprecationWarning,
)
schema[field][new] = schema[field][old]
schema[field].pop(old)
return schema
def regenerate_validation_schema(self):
self.validation_schema = SchemaValidationSchema(self.validator)
def validate(self, schema=None):
""" Validates a schema that defines rules against supported rules.
:param schema: The schema to be validated as a legal cerberus schema
according to the rules of the assigned Validator object.
Raises a :class:`~cerberus.base.SchemaError` when an invalid
schema is encountered. """
if schema is None:
schema = self.schema
_hash = (mapping_hash(schema),
mapping_hash(self.validator.types_mapping))
_hash = (mapping_hash(schema), mapping_hash(self.validator.types_mapping))
if _hash not in self.validator._valid_schemas:
self._validate(schema)
self.validator._valid_schemas.add(_hash)
def _validate(self, schema):
""" Validates a schema that defines rules against supported rules.
:param schema: The schema to be validated as a legal cerberus schema
according to the rules of this Validator object.
"""
if isinstance(schema, _str_type):
schema = self.validator.schema_registry.get(schema, schema)
@@ -212,8 +269,7 @@ class DefinitionSchema(MutableMapping):
schema = copy(schema)
for field in schema:
if isinstance(schema[field], _str_type):
schema[field] = rules_set_registry.get(schema[field],
schema[field])
schema[field] = rules_set_registry.get(schema[field], schema[field])
if not self.schema_validator(schema, normalize=False):
raise SchemaError(self.schema_validator.errors)
@@ -236,31 +292,31 @@ class UnvalidatedSchema(DefinitionSchema):
class SchemaValidationSchema(UnvalidatedSchema):
def __init__(self, validator):
self.schema = {'allow_unknown': False,
'schema': validator.rules,
'type': 'dict'}
self.schema = {
'allow_unknown': False,
'schema': validator.rules,
'type': 'dict',
}
class SchemaValidatorMixin(object):
""" This validator is extended to validate schemas passed to a Cerberus
""" This validator mixin provides mechanics to validate schemas passed to a Cerberus
validator. """
def __init__(self, *args, **kwargs):
kwargs.setdefault('known_rules_set_refs', set())
kwargs.setdefault('known_schema_refs', set())
super(SchemaValidatorMixin, self).__init__(*args, **kwargs)
@property
def known_rules_set_refs(self):
""" The encountered references to rules set registry items. """
return self._config.get('known_rules_set_refs', ())
@known_rules_set_refs.setter
def known_rules_set_refs(self, value):
self._config['known_rules_set_refs'] = value
return self._config['known_rules_set_refs']
@property
def known_schema_refs(self):
""" The encountered references to schema registry items. """
return self._config.get('known_schema_refs', ())
@known_schema_refs.setter
def known_schema_refs(self, value):
self._config['known_schema_refs'] = value
return self._config['known_schema_refs']
@property
def target_schema(self):
@@ -272,35 +328,13 @@ class SchemaValidatorMixin(object):
""" The validator whose schema is being validated. """
return self._config['target_validator']
def _validate_logical(self, rule, field, value):
""" {'allowed': ('allof', 'anyof', 'noneof', 'oneof')} """
if not isinstance(value, Sequence):
self._error(field, errors.BAD_TYPE)
return
validator = self._get_child_validator(
document_crumb=rule, allow_unknown=False,
schema=self.target_validator.validation_rules)
for constraints in value:
_hash = (mapping_hash({'turing': constraints}),
mapping_hash(self.target_validator.types_mapping))
if _hash in self.target_validator._valid_schemas:
continue
validator(constraints, normalize=False)
if validator._errors:
self._error(validator._errors)
else:
self.target_validator._valid_schemas.add(_hash)
def _validator_bulk_schema(self, field, value):
def _check_with_bulk_schema(self, field, value):
# resolve schema registry reference
if isinstance(value, _str_type):
if value in self.known_rules_set_refs:
return
else:
self.known_rules_set_refs += (value,)
self.known_rules_set_refs.add(value)
definition = self.target_validator.rules_set_registry.get(value)
if definition is None:
self._error(field, 'Rules set definition %s not found.' % value)
@@ -308,28 +342,32 @@ class SchemaValidatorMixin(object):
else:
value = definition
_hash = (mapping_hash({'turing': value}),
mapping_hash(self.target_validator.types_mapping))
_hash = (
mapping_hash({'turing': value}),
mapping_hash(self.target_validator.types_mapping),
)
if _hash in self.target_validator._valid_schemas:
return
validator = self._get_child_validator(
document_crumb=field, allow_unknown=False,
schema=self.target_validator.rules)
document_crumb=field,
allow_unknown=False,
schema=self.target_validator.rules,
)
validator(value, normalize=False)
if validator._errors:
self._error(validator._errors)
else:
self.target_validator._valid_schemas.add(_hash)
def _validator_dependencies(self, field, value):
def _check_with_dependencies(self, field, value):
if isinstance(value, _str_type):
pass
elif isinstance(value, Mapping):
validator = self._get_child_validator(
document_crumb=field,
schema={'valueschema': {'type': 'list'}},
allow_unknown=True
schema={'valuesrules': {'type': 'list'}},
allow_unknown=True,
)
if not validator(value, normalize=False):
self._error(validator._errors)
@@ -338,54 +376,36 @@ class SchemaValidatorMixin(object):
path = self.document_path + (field,)
self._error(path, 'All dependencies must be a hashable type.')
def _validator_handler(self, field, value):
if isinstance(value, Callable):
return
if isinstance(value, _str_type):
if value not in self.target_validator.validators + \
self.target_validator.coercers:
self._error(field, '%s is no valid coercer' % value)
elif isinstance(value, Iterable):
for handler in value:
self._validator_handler(field, handler)
def _validator_items(self, field, value):
def _check_with_items(self, field, value):
for i, schema in enumerate(value):
self._validator_bulk_schema((field, i), schema)
self._check_with_bulk_schema((field, i), schema)
def _validator_schema(self, field, value):
def _check_with_schema(self, field, value):
try:
value = self._handle_schema_reference_for_validator(field, value)
except _Abort:
return
_hash = (mapping_hash(value),
mapping_hash(self.target_validator.types_mapping))
_hash = (mapping_hash(value), mapping_hash(self.target_validator.types_mapping))
if _hash in self.target_validator._valid_schemas:
return
validator = self._get_child_validator(
document_crumb=field,
schema=None, allow_unknown=self.root_allow_unknown)
document_crumb=field, schema=None, allow_unknown=self.root_allow_unknown
)
validator(self._expand_rules_set_refs(value), normalize=False)
if validator._errors:
self._error(validator._errors)
else:
self.target_validator._valid_schemas.add(_hash)
def _handle_schema_reference_for_validator(self, field, value):
if not isinstance(value, _str_type):
return value
if value in self.known_schema_refs:
raise _Abort
self.known_schema_refs += (value,)
definition = self.target_validator.schema_registry.get(value)
if definition is None:
path = self.document_path + (field,)
self._error(path, 'Schema definition {} not found.'.format(value))
raise _Abort
return definition
def _check_with_type(self, field, value):
value = set((value,)) if isinstance(value, _str_type) else set(value)
invalid_constraints = value - set(self.target_validator.types)
if invalid_constraints:
self._error(
field, 'Unsupported types: {}'.format(', '.join(invalid_constraints))
)
def _expand_rules_set_refs(self, schema):
result = {}
@@ -396,15 +416,46 @@ class SchemaValidatorMixin(object):
result[k] = v
return result
def _validator_type(self, field, value):
value = (value,) if isinstance(value, _str_type) else value
invalid_constraints = ()
for constraint in value:
if constraint not in self.target_validator.types:
invalid_constraints += (constraint,)
if invalid_constraints:
def _handle_schema_reference_for_validator(self, field, value):
if not isinstance(value, _str_type):
return value
if value in self.known_schema_refs:
raise _Abort
self.known_schema_refs.add(value)
definition = self.target_validator.schema_registry.get(value)
if definition is None:
path = self.document_path + (field,)
self._error(path, 'Unsupported types: %s' % invalid_constraints)
self._error(path, 'Schema definition {} not found.'.format(value))
raise _Abort
return definition
def _validate_logical(self, rule, field, value):
""" {'allowed': ('allof', 'anyof', 'noneof', 'oneof')} """
if not isinstance(value, Sequence):
self._error(field, errors.BAD_TYPE)
return
validator = self._get_child_validator(
document_crumb=rule,
allow_unknown=False,
schema=self.target_validator.validation_rules,
)
for constraints in value:
_hash = (
mapping_hash({'turing': constraints}),
mapping_hash(self.target_validator.types_mapping),
)
if _hash in self.target_validator._valid_schemas:
continue
validator(constraints, normalize=False)
if validator._errors:
self._error(validator._errors)
else:
self.target_validator._valid_schemas.add(_hash)
####
+29 -14
View File
@@ -1,22 +1,23 @@
# -*- coding: utf-8 -*-
import re
import pytest
from cerberus import errors, Validator, SchemaError, DocumentError
from cerberus.tests.conftest import sample_schema
def assert_exception(exception, document={}, schema=None, validator=None,
msg=None):
def assert_exception(exception, document={}, schema=None, validator=None, msg=None):
""" Tests whether a specific exception is raised. Optionally also tests
whether the exception message is as expected. """
if validator is None:
validator = Validator()
if msg is None:
with pytest.raises(exception) as excinfo:
with pytest.raises(exception):
validator(document, schema)
else:
with pytest.raises(exception, message=msg) as excinfo: # noqa: F841
with pytest.raises(exception, match=re.escape(msg)):
validator(document, schema)
@@ -32,8 +33,15 @@ def assert_document_error(*args):
assert_exception(DocumentError, *args)
def assert_fail(document, schema=None, validator=None, update=False,
error=None, errors=None, child_errors=None):
def assert_fail(
document,
schema=None,
validator=None,
update=False,
error=None,
errors=None,
child_errors=None,
):
""" Tests whether a validation fails. """
if validator is None:
validator = Validator(sample_schema)
@@ -45,8 +53,7 @@ def assert_fail(document, schema=None, validator=None, update=False,
assert not (error is not None and errors is not None)
assert not (errors is not None and child_errors is not None), (
'child_errors can only be tested in '
'conjunction with the error parameter'
'child_errors can only be tested in ' 'conjunction with the error parameter'
)
assert not (child_errors is not None and error is None)
if error is not None:
@@ -99,7 +106,8 @@ def assert_has_error(_errors, d_path, s_path, error_def, constraint, info=()):
else:
break
else:
raise AssertionError("""
raise AssertionError(
"""
Error with properties:
document_path={doc_path}
schema_path={schema_path}
@@ -108,9 +116,15 @@ def assert_has_error(_errors, d_path, s_path, error_def, constraint, info=()):
info={info}
not found in errors:
{errors}
""".format(doc_path=d_path, schema_path=s_path,
code=hex(error.code), info=info,
constraint=constraint, errors=_errors))
""".format(
doc_path=d_path,
schema_path=s_path,
code=hex(error.code),
info=info,
constraint=constraint,
errors=_errors,
)
)
return i
@@ -133,8 +147,9 @@ def assert_not_has_error(_errors, *args, **kwargs):
def assert_bad_type(field, data_type, value):
assert_fail({field: value},
error=(field, (field, 'type'), errors.BAD_TYPE, data_type))
assert_fail(
{field: value}, error=(field, (field, 'type'), errors.BAD_TYPE, data_type)
)
def assert_normalized(document, expected, schema=None, validator=None):
+23 -76
View File
@@ -23,67 +23,27 @@ def validator():
sample_schema = {
'a_string': {
'type': 'string',
'minlength': 2,
'maxlength': 10
},
'a_binary': {
'type': 'binary',
'minlength': 2,
'maxlength': 10
},
'a_nullable_integer': {
'type': 'integer',
'nullable': True
},
'an_integer': {
'type': 'integer',
'min': 1,
'max': 100,
},
'a_restricted_integer': {
'type': 'integer',
'allowed': [-1, 0, 1],
},
'a_boolean': {
'type': 'boolean',
},
'a_datetime': {
'type': 'datetime',
},
'a_float': {
'type': 'float',
'min': 1,
'max': 100,
},
'a_number': {
'type': 'number',
'min': 1,
'max': 100,
},
'a_set': {
'type': 'set',
},
'one_or_more_strings': {
'type': ['string', 'list'],
'schema': {'type': 'string'}
},
'a_string': {'type': 'string', 'minlength': 2, 'maxlength': 10},
'a_binary': {'type': 'binary', 'minlength': 2, 'maxlength': 10},
'a_nullable_integer': {'type': 'integer', 'nullable': True},
'an_integer': {'type': 'integer', 'min': 1, 'max': 100},
'a_restricted_integer': {'type': 'integer', 'allowed': [-1, 0, 1]},
'a_boolean': {'type': 'boolean', 'meta': 'can haz two distinct states'},
'a_datetime': {'type': 'datetime', 'meta': {'format': '%a, %d. %b %Y'}},
'a_float': {'type': 'float', 'min': 1, 'max': 100},
'a_number': {'type': 'number', 'min': 1, 'max': 100},
'a_set': {'type': 'set'},
'one_or_more_strings': {'type': ['string', 'list'], 'schema': {'type': 'string'}},
'a_regex_email': {
'type': 'string',
'regex': '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
'regex': r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$',
},
'a_readonly_string': {
'type': 'string',
'readonly': True,
},
'a_restricted_string': {
'type': 'string',
'allowed': ["agent", "client", "vendor"],
},
'an_array': {
'a_readonly_string': {'type': 'string', 'readonly': True},
'a_restricted_string': {'type': 'string', 'allowed': ['agent', 'client', 'vendor']},
'an_array': {'type': 'list', 'allowed': ['agent', 'client', 'vendor']},
'an_array_from_set': {
'type': 'list',
'allowed': ["agent", "client", "vendor"],
'allowed': set(['agent', 'client', 'vendor']),
},
'a_list_of_dicts': {
'type': 'list',
@@ -97,38 +57,25 @@ sample_schema = {
},
'a_list_of_values': {
'type': 'list',
'items': [{'type': 'string'}, {'type': 'integer'}, ]
},
'a_list_of_integers': {
'type': 'list',
'schema': {'type': 'integer'},
'items': [{'type': 'string'}, {'type': 'integer'}],
},
'a_list_of_integers': {'type': 'list', 'schema': {'type': 'integer'}},
'a_dict': {
'type': 'dict',
'schema': {
'address': {'type': 'string'},
'city': {'type': 'string', 'required': True}
'city': {'type': 'string', 'required': True},
},
},
'a_dict_with_valueschema': {
'type': 'dict',
'valueschema': {'type': 'integer'}
},
'a_dict_with_keyschema': {
'type': 'dict',
'keyschema': {'type': 'string', 'regex': '[a-z]+'}
},
'a_dict_with_valuesrules': {'type': 'dict', 'valuesrules': {'type': 'integer'}},
'a_list_length': {
'type': 'list',
'schema': {'type': 'integer'},
'minlength': 2,
'maxlength': 5,
},
'a_nullable_field_without_type': {
'nullable': True
},
'a_not_nullable_field_without_type': {
},
'a_nullable_field_without_type': {'nullable': True},
'a_not_nullable_field_without_type': {},
}
sample_document = {'name': 'john doe'}
+49 -14
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from decimal import Decimal
from pkg_resources import Distribution, DistributionNotFound
from pytest import mark
@@ -8,6 +9,37 @@ from cerberus import TypeDefinition, Validator
from cerberus.tests import assert_fail, assert_success
from cerberus.utils import validator_factory
from cerberus.validator import BareValidator
from cerberus.platform import PYTHON_VERSION
if PYTHON_VERSION > 3 and PYTHON_VERSION < 3.4:
from imp import reload
elif PYTHON_VERSION >= 3.4:
from importlib import reload
else:
pass # Python 2.x
def test_pkgresources_version(monkeypatch):
def create_fake_distribution(name):
return Distribution(project_name="cerberus", version="1.2.3")
with monkeypatch.context() as m:
cerberus = __import__("cerberus")
m.setattr("pkg_resources.get_distribution", create_fake_distribution)
reload(cerberus)
assert cerberus.__version__ == "1.2.3"
def test_version_not_found(monkeypatch):
def raise_distribution_not_found(name):
raise DistributionNotFound("pkg_resources cannot get distribution")
with monkeypatch.context() as m:
cerberus = __import__("cerberus")
m.setattr("pkg_resources.get_distribution", raise_distribution_not_found)
reload(cerberus)
assert cerberus.__version__ == "unknown"
def test_clear_cache(validator):
@@ -23,8 +55,11 @@ def test_docstring(validator):
# Test that testing with the sample schema works as expected
# as there might be rules with side-effects in it
@mark.parametrize('test,document', ((assert_fail, {'an_integer': 60}),
(assert_success, {'an_integer': 110})))
@mark.parametrize(
"test,document",
((assert_fail, {"an_integer": 60}), (assert_success, {"an_integer": 110})),
)
def test_that_test_fails(test, document):
try:
test(document)
@@ -35,42 +70,42 @@ def test_that_test_fails(test, document):
def test_dynamic_types():
decimal_type = TypeDefinition('decimal', (Decimal,), ())
document = {'measurement': Decimal(0)}
schema = {'measurement': {'type': 'decimal'}}
decimal_type = TypeDefinition("decimal", (Decimal,), ())
document = {"measurement": Decimal(0)}
schema = {"measurement": {"type": "decimal"}}
validator = Validator()
validator.types_mapping['decimal'] = decimal_type
validator.types_mapping["decimal"] = decimal_type
assert_success(document, schema, validator)
class MyValidator(Validator):
types_mapping = Validator.types_mapping.copy()
types_mapping['decimal'] = decimal_type
types_mapping["decimal"] = decimal_type
validator = MyValidator()
assert_success(document, schema, validator)
def test_mro():
assert Validator.__mro__ == (Validator, BareValidator, object), \
Validator.__mro__
assert Validator.__mro__ == (Validator, BareValidator, object), Validator.__mro__
def test_mixin_init():
class Mixin(object):
def __init__(self, *args, **kwargs):
kwargs['test'] = True
kwargs["test"] = True
super(Mixin, self).__init__(*args, **kwargs)
MyValidator = validator_factory('MyValidator', Mixin)
MyValidator = validator_factory("MyValidator", Mixin)
validator = MyValidator()
assert validator._config['test']
assert validator._config["test"]
def test_sub_init():
class MyValidator(Validator):
def __init__(self, *args, **kwargs):
kwargs['test'] = True
kwargs["test"] = True
super(MyValidator, self).__init__(*args, **kwargs)
validator = MyValidator()
assert validator._config['test']
assert validator._config["test"]
+33 -10
View File
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
from pytest import mark
import cerberus
from cerberus.tests import assert_fail, assert_success
from cerberus.tests.conftest import sample_schema
def test_contextual_data_preservation():
class InheritedValidator(cerberus.Validator):
def __init__(self, *args, **kwargs):
if 'working_dir' in kwargs:
@@ -18,9 +19,9 @@ def test_contextual_data_preservation():
return True
assert 'test' in InheritedValidator.types
v = InheritedValidator({'test': {'type': 'list',
'schema': {'type': 'test'}}},
working_dir='/tmp')
v = InheritedValidator(
{'test': {'type': 'list', 'schema': {'type': 'test'}}}, working_dir='/tmp'
)
assert_success({'test': ['foo']}, validator=v)
@@ -42,25 +43,47 @@ def test_docstring_parsing():
assert 'bar' in CustomValidator.validation_rules
def test_issue_265():
# TODO remove 'validator' as rule parameter with the next major release
@mark.parametrize('rule', ('check_with', 'validator'))
def test_check_with_method(rule):
# https://github.com/pyeve/cerberus/issues/265
class MyValidator(cerberus.Validator):
def _check_with_oddity(self, field, value):
if not value & 1:
self._error(field, "Must be an odd number")
v = MyValidator(schema={'amount': {rule: 'oddity'}})
assert_success(document={'amount': 1}, validator=v)
assert_fail(
document={'amount': 2},
validator=v,
error=('amount', (), cerberus.errors.CUSTOM, None, ('Must be an odd number',)),
)
# TODO remove test with the next major release
@mark.parametrize('rule', ('check_with', 'validator'))
def test_validator_method(rule):
class MyValidator(cerberus.Validator):
def _validator_oddity(self, field, value):
if not value & 1:
self._error(field, "Must be an odd number")
v = MyValidator(schema={'amount': {'validator': 'oddity'}})
v = MyValidator(schema={'amount': {rule: 'oddity'}})
assert_success(document={'amount': 1}, validator=v)
assert_fail(document={'amount': 2}, validator=v,
error=('amount', (), cerberus.errors.CUSTOM, None,
('Must be an odd number',)))
assert_fail(
document={'amount': 2},
validator=v,
error=('amount', (), cerberus.errors.CUSTOM, None, ('Must be an odd number',)),
)
def test_schema_validation_can_be_disabled_in_schema_setter():
class NonvalidatingValidator(cerberus.Validator):
"""
Skips schema validation to speed up initialization
"""
@cerberus.Validator.schema.setter
def schema(self, schema):
if schema is None:
+135 -72
View File
@@ -24,14 +24,14 @@ def test__error_1():
def test__error_2():
v = Validator(schema={'foo': {'keyschema': {'type': 'integer'}}})
v = Validator(schema={'foo': {'keysrules': {'type': 'integer'}}})
v.document = {'foo': {'0': 'bar'}}
v._error('foo', errors.KEYSCHEMA, ())
v._error('foo', errors.KEYSRULES, ())
error = v._errors[0]
assert error.document_path == ('foo',)
assert error.schema_path == ('foo', 'keyschema')
assert error.schema_path == ('foo', 'keysrules')
assert error.code == 0x83
assert error.rule == 'keyschema'
assert error.rule == 'keysrules'
assert error.constraint == {'type': 'integer'}
assert error.value == {'0': 'bar'}
assert error.info == ((),)
@@ -40,8 +40,10 @@ def test__error_2():
def test__error_3():
valids = [{'type': 'string', 'regex': '0x[0-9a-f]{2}'},
{'type': 'integer', 'min': 0, 'max': 255}]
valids = [
{'type': 'string', 'regex': '0x[0-9a-f]{2}'},
{'type': 'integer', 'min': 0, 'max': 255},
]
v = Validator(schema={'foo': {'oneof': valids}})
v.document = {'foo': '0x100'}
v._error('foo', errors.ONEOF, (), 0, 2)
@@ -77,8 +79,9 @@ def test_error_tree_from_subschema(validator):
assert 'bar' in s_error_tree['foo']['schema']
assert 'type' in s_error_tree['foo']['schema']['bar']
assert s_error_tree['foo']['schema']['bar']['type'].errors[0].value == 0
assert s_error_tree.fetch_errors_from(
('foo', 'schema', 'bar', 'type'))[0].value == 0
assert (
s_error_tree.fetch_errors_from(('foo', 'schema', 'bar', 'type'))[0].value == 0
)
def test_error_tree_from_anyof(validator):
@@ -98,12 +101,17 @@ def test_error_tree_from_anyof(validator):
def test_nested_error_paths(validator):
schema = {'a_dict': {'keyschema': {'type': 'integer'},
'valueschema': {'regex': '[a-z]*'}},
'a_list': {'schema': {'type': 'string',
'oneof_regex': ['[a-z]*$', '[A-Z]*']}}}
document = {'a_dict': {0: 'abc', 'one': 'abc', 2: 'aBc', 'three': 'abC'},
'a_list': [0, 'abc', 'abC']}
schema = {
'a_dict': {
'keysrules': {'type': 'integer'},
'valuesrules': {'regex': '[a-z]*'},
},
'a_list': {'schema': {'type': 'string', 'oneof_regex': ['[a-z]*$', '[A-Z]*']}},
}
document = {
'a_dict': {0: 'abc', 'one': 'abc', 2: 'aBc', 'three': 'abC'},
'a_list': [0, 'abc', 'abC'],
}
assert_fail(document, schema, validator=validator)
_det = validator.document_error_tree
@@ -120,35 +128,59 @@ def test_nested_error_paths(validator):
assert len(_det['a_dict'][2].errors) == 1
assert len(_det['a_dict']['three'].errors) == 2
assert len(_set['a_dict']['keyschema'].errors) == 1
assert len(_set['a_dict']['valueschema'].errors) == 1
assert len(_set['a_dict']['keysrules'].errors) == 1
assert len(_set['a_dict']['valuesrules'].errors) == 1
assert len(_set['a_dict']['keyschema']['type'].errors) == 2
assert len(_set['a_dict']['valueschema']['regex'].errors) == 2
assert len(_set['a_dict']['keysrules']['type'].errors) == 2
assert len(_set['a_dict']['valuesrules']['regex'].errors) == 2
_ref_err = ValidationError(
('a_dict', 'one'), ('a_dict', 'keyschema', 'type'),
errors.BAD_TYPE.code, 'type', 'integer', 'one', ())
('a_dict', 'one'),
('a_dict', 'keysrules', 'type'),
errors.BAD_TYPE.code,
'type',
'integer',
'one',
(),
)
assert _det['a_dict']['one'].errors[0] == _ref_err
assert _set['a_dict']['keyschema']['type'].errors[0] == _ref_err
assert _set['a_dict']['keysrules']['type'].errors[0] == _ref_err
_ref_err = ValidationError(
('a_dict', 2), ('a_dict', 'valueschema', 'regex'),
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'aBc', ())
('a_dict', 2),
('a_dict', 'valuesrules', 'regex'),
errors.REGEX_MISMATCH.code,
'regex',
'[a-z]*$',
'aBc',
(),
)
assert _det['a_dict'][2].errors[0] == _ref_err
assert _set['a_dict']['valueschema']['regex'].errors[0] == _ref_err
assert _set['a_dict']['valuesrules']['regex'].errors[0] == _ref_err
_ref_err = ValidationError(
('a_dict', 'three'), ('a_dict', 'keyschema', 'type'),
errors.BAD_TYPE.code, 'type', 'integer', 'three', ())
('a_dict', 'three'),
('a_dict', 'keysrules', 'type'),
errors.BAD_TYPE.code,
'type',
'integer',
'three',
(),
)
assert _det['a_dict']['three'].errors[0] == _ref_err
assert _set['a_dict']['keyschema']['type'].errors[1] == _ref_err
assert _set['a_dict']['keysrules']['type'].errors[1] == _ref_err
_ref_err = ValidationError(
('a_dict', 'three'), ('a_dict', 'valueschema', 'regex'),
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ())
('a_dict', 'three'),
('a_dict', 'valuesrules', 'regex'),
errors.REGEX_MISMATCH.code,
'regex',
'[a-z]*$',
'abC',
(),
)
assert _det['a_dict']['three'].errors[1] == _ref_err
assert _set['a_dict']['valueschema']['regex'].errors[1] == _ref_err
assert _set['a_dict']['valuesrules']['regex'].errors[1] == _ref_err
assert len(_det['a_list'].errors) == 1
assert len(_det['a_list'][0].errors) == 1
@@ -161,34 +193,56 @@ def test_nested_error_paths(validator):
assert len(_set['a_list']['schema']['oneof'][1]['regex'].errors) == 1
_ref_err = ValidationError(
('a_list', 0), ('a_list', 'schema', 'type'), errors.BAD_TYPE.code,
'type', 'string', 0, ())
('a_list', 0),
('a_list', 'schema', 'type'),
errors.BAD_TYPE.code,
'type',
'string',
0,
(),
)
assert _det['a_list'][0].errors[0] == _ref_err
assert _set['a_list']['schema']['type'].errors[0] == _ref_err
_ref_err = ValidationError(
('a_list', 2), ('a_list', 'schema', 'oneof'), errors.ONEOF.code,
'oneof', 'irrelevant_at_this_point', 'abC', ())
('a_list', 2),
('a_list', 'schema', 'oneof'),
errors.ONEOF.code,
'oneof',
'irrelevant_at_this_point',
'abC',
(),
)
assert _det['a_list'][2].errors[0] == _ref_err
assert _set['a_list']['schema']['oneof'].errors[0] == _ref_err
_ref_err = ValidationError(
('a_list', 2), ('a_list', 'schema', 'oneof', 0, 'regex'),
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ())
('a_list', 2),
('a_list', 'schema', 'oneof', 0, 'regex'),
errors.REGEX_MISMATCH.code,
'regex',
'[a-z]*$',
'abC',
(),
)
assert _det['a_list'][2].errors[1] == _ref_err
assert _set['a_list']['schema']['oneof'][0]['regex'].errors[0] == _ref_err
_ref_err = ValidationError(
('a_list', 2), ('a_list', 'schema', 'oneof', 1, 'regex'),
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ())
('a_list', 2),
('a_list', 'schema', 'oneof', 1, 'regex'),
errors.REGEX_MISMATCH.code,
'regex',
'[a-z]*$',
'abC',
(),
)
assert _det['a_list'][2].errors[2] == _ref_err
assert _set['a_list']['schema']['oneof'][1]['regex'].errors[0] == _ref_err
def test_queries():
schema = {'foo': {'type': 'dict',
'schema':
{'bar': {'type': 'number'}}}}
schema = {'foo': {'type': 'dict', 'schema': {'bar': {'type': 'number'}}}}
document = {'foo': {'bar': 'zero'}}
validator = Validator(schema)
validator(document)
@@ -202,59 +256,68 @@ def test_queries():
assert errors.MAPPING_SCHEMA in validator.document_error_tree['foo']
assert errors.BAD_TYPE in validator.document_error_tree['foo']['bar']
assert errors.MAPPING_SCHEMA in validator.schema_error_tree['foo']['schema']
assert errors.BAD_TYPE in \
validator.schema_error_tree['foo']['schema']['bar']['type']
assert (
errors.BAD_TYPE in validator.schema_error_tree['foo']['schema']['bar']['type']
)
assert (validator.document_error_tree['foo'][errors.MAPPING_SCHEMA]
.child_errors[0].code == errors.BAD_TYPE.code)
assert (
validator.document_error_tree['foo'][errors.MAPPING_SCHEMA].child_errors[0].code
== errors.BAD_TYPE.code
)
def test_basic_error_handler():
handler = errors.BasicErrorHandler()
_errors, ref = [], {}
_errors.append(ValidationError(
['foo'], ['foo'], 0x63, 'readonly', True, None, ()))
_errors.append(ValidationError(['foo'], ['foo'], 0x63, 'readonly', True, None, ()))
ref.update({'foo': [handler.messages[0x63]]})
assert handler(_errors) == ref
_errors.append(ValidationError(
['bar'], ['foo'], 0x42, 'min', 1, 2, ()))
_errors.append(ValidationError(['bar'], ['foo'], 0x42, 'min', 1, 2, ()))
ref.update({'bar': [handler.messages[0x42].format(constraint=1)]})
assert handler(_errors) == ref
_errors.append(ValidationError(
['zap', 'foo'], ['zap', 'schema', 'foo'], 0x24, 'type', 'string',
True, ()))
ref.update({'zap': [{'foo': [handler.messages[0x24].format(
constraint='string')]}]})
_errors.append(
ValidationError(
['zap', 'foo'], ['zap', 'schema', 'foo'], 0x24, 'type', 'string', True, ()
)
)
ref.update({'zap': [{'foo': [handler.messages[0x24].format(constraint='string')]}]})
assert handler(_errors) == ref
_errors.append(ValidationError(
['zap', 'foo'], ['zap', 'schema', 'foo'], 0x41, 'regex',
'^p[äe]ng$', 'boom', ()))
ref['zap'][0]['foo'].append(
handler.messages[0x41].format(constraint='^p[äe]ng$'))
_errors.append(
ValidationError(
['zap', 'foo'],
['zap', 'schema', 'foo'],
0x41,
'regex',
'^p[äe]ng$',
'boom',
(),
)
)
ref['zap'][0]['foo'].append(handler.messages[0x41].format(constraint='^p[äe]ng$'))
assert handler(_errors) == ref
def test_basic_error_of_errors(validator):
schema = {'foo': {'oneof': [
{'type': 'integer'},
{'type': 'string'}
]}}
schema = {'foo': {'oneof': [{'type': 'integer'}, {'type': 'string'}]}}
document = {'foo': 23.42}
error = ('foo', ('foo', 'oneof'), errors.ONEOF,
schema['foo']['oneof'], ())
error = ('foo', ('foo', 'oneof'), errors.ONEOF, schema['foo']['oneof'], ())
child_errors = [
(error[0], error[1] + (0, 'type'), errors.BAD_TYPE, 'integer'),
(error[0], error[1] + (1, 'type'), errors.BAD_TYPE, 'string')
(error[0], error[1] + (1, 'type'), errors.BAD_TYPE, 'string'),
]
assert_fail(document, schema, validator=validator,
error=error, child_errors=child_errors)
assert_fail(
document, schema, validator=validator, error=error, child_errors=child_errors
)
assert validator.errors == {
'foo': [errors.BasicErrorHandler.messages[0x92],
{'oneof definition 0': ['must be of integer type'],
'oneof definition 1': ['must be of string type']}
]
'foo': [
errors.BasicErrorHandler.messages[0x92],
{
'oneof definition 0': ['must be of integer type'],
'oneof definition 1': ['must be of string type'],
},
]
}
+213 -155
View File
@@ -1,10 +1,21 @@
# -*- coding: utf-8 -*-
from copy import deepcopy
from tempfile import NamedTemporaryFile
from pytest import mark
from cerberus import Validator, errors
from cerberus.tests import (assert_fail, assert_has_error, assert_normalized,
assert_success)
from cerberus.tests import (
assert_fail,
assert_has_error,
assert_normalized,
assert_success,
)
def must_not_be_called(*args, **kwargs):
raise RuntimeError('This shall not be called.')
def test_coerce():
@@ -15,21 +26,31 @@ def test_coerce():
def test_coerce_in_dictschema():
schema = {'thing': {'type': 'dict',
'schema': {'amount': {'coerce': int}}}}
schema = {'thing': {'type': 'dict', 'schema': {'amount': {'coerce': int}}}}
document = {'thing': {'amount': '2'}}
expected = {'thing': {'amount': 2}}
assert_normalized(document, expected, schema)
def test_coerce_in_listschema():
schema = {'things': {'type': 'list',
'schema': {'coerce': int}}}
schema = {'things': {'type': 'list', 'schema': {'coerce': int}}}
document = {'things': ['1', '2', '3']}
expected = {'things': [1, 2, 3]}
assert_normalized(document, expected, schema)
def test_coerce_in_listitems():
schema = {'things': {'type': 'list', 'items': [{'coerce': int}, {'coerce': str}]}}
document = {'things': ['1', 2]}
expected = {'things': [1, '2']}
assert_normalized(document, expected, schema)
validator = Validator(schema)
document['things'].append(3)
assert not validator(document)
assert validator.document['things'] == document['things']
def test_coerce_in_dictschema_in_listschema():
item_schema = {'type': 'dict', 'schema': {'amount': {'coerce': int}}}
schema = {'things': {'type': 'list', 'schema': item_schema}}
@@ -39,9 +60,7 @@ def test_coerce_in_dictschema_in_listschema():
def test_coerce_not_destructive():
schema = {
'amount': {'coerce': int}
}
schema = {'amount': {'coerce': int}}
v = Validator(schema)
doc = {'amount': '1'}
v.validate(doc)
@@ -52,16 +71,48 @@ def test_coerce_catches_ValueError():
schema = {'amount': {'coerce': int}}
_errors = assert_fail({'amount': 'not_a_number'}, schema)
_errors[0].info = () # ignore exception message here
assert_has_error(_errors, 'amount', ('amount', 'coerce'),
errors.COERCION_FAILED, int)
assert_has_error(
_errors, 'amount', ('amount', 'coerce'), errors.COERCION_FAILED, int
)
def test_coerce_in_listitems_catches_ValueError():
schema = {'things': {'type': 'list', 'items': [{'coerce': int}, {'coerce': str}]}}
document = {'things': ['not_a_number', 2]}
_errors = assert_fail(document, schema)
_errors[0].info = () # ignore exception message here
assert_has_error(
_errors,
('things', 0),
('things', 'items', 'coerce'),
errors.COERCION_FAILED,
int,
)
def test_coerce_catches_TypeError():
schema = {'name': {'coerce': str.lower}}
_errors = assert_fail({'name': 1234}, schema)
_errors[0].info = () # ignore exception message here
assert_has_error(_errors, 'name', ('name', 'coerce'),
errors.COERCION_FAILED, str.lower)
assert_has_error(
_errors, 'name', ('name', 'coerce'), errors.COERCION_FAILED, str.lower
)
def test_coerce_in_listitems_catches_TypeError():
schema = {
'things': {'type': 'list', 'items': [{'coerce': int}, {'coerce': str.lower}]}
}
document = {'things': ['1', 2]}
_errors = assert_fail(document, schema)
_errors[0].info = () # ignore exception message here
assert_has_error(
_errors,
('things', 1),
('things', 'items', 'coerce'),
errors.COERCION_FAILED,
str.lower,
)
def test_coerce_unknown():
@@ -88,16 +139,16 @@ def test_custom_coerce_and_rename():
def test_coerce_chain():
drop_prefix = lambda x: x[2:]
upper = lambda x: x.upper()
drop_prefix = lambda x: x[2:] # noqa: E731
upper = lambda x: x.upper() # noqa: E731
schema = {'foo': {'coerce': [hex, drop_prefix, upper]}}
assert_normalized({'foo': 15}, {'foo': 'F'}, schema)
def test_coerce_chain_aborts(validator):
def dont_do_me(value):
raise AssertionError('The coercion chain did not abort after an '
'error.')
raise AssertionError('The coercion chain did not abort after an ' 'error.')
schema = {'foo': {'coerce': [hex, dont_do_me]}}
validator({'foo': '0'}, schema)
assert errors.COERCION_FAILED in validator._errors
@@ -105,12 +156,12 @@ def test_coerce_chain_aborts(validator):
def test_coerce_non_digit_in_sequence(validator):
# https://github.com/pyeve/cerberus/issues/211
schema = {'data': {'type': 'list',
'schema': {'type': 'integer', 'coerce': int}}}
schema = {'data': {'type': 'list', 'schema': {'type': 'integer', 'coerce': int}}}
document = {'data': ['q']}
assert validator.validated(document, schema) is None
assert (validator.validated(document, schema, always_return_document=True)
== document) # noqa: W503
assert (
validator.validated(document, schema, always_return_document=True) == document
) # noqa: W503
def test_nullables_dont_fail_coerce():
@@ -119,6 +170,18 @@ def test_nullables_dont_fail_coerce():
assert_normalized(document, document, schema)
def test_nullables_fail_coerce_on_non_null_values(validator):
def failing_coercion(value):
raise Exception("expected to fail")
schema = {'foo': {'coerce': failing_coercion, 'nullable': True, 'type': 'integer'}}
document = {'foo': None}
assert_normalized(document, document, schema)
validator({'foo': 2}, schema)
assert errors.COERCION_FAILED in validator._errors
def test_normalized():
schema = {'amount': {'coerce': int}}
document = {'amount': '2'}
@@ -154,9 +217,13 @@ def test_purge_unknown():
def test_purge_unknown_in_subschema():
schema = {'foo': {'type': 'dict',
'schema': {'foo': {'type': 'string'}},
'purge_unknown': True}}
schema = {
'foo': {
'type': 'dict',
'schema': {'foo': {'type': 'string'}},
'purge_unknown': True,
}
}
document = {'foo': {'bar': ''}}
expected = {'foo': {}}
assert_normalized(document, expected, schema)
@@ -175,8 +242,7 @@ def test_issue_147_complex():
def test_issue_147_nested_dict():
schema = {'thing': {'type': 'dict',
'schema': {'amount': {'coerce': int}}}}
schema = {'thing': {'type': 'dict', 'schema': {'amount': {'coerce': int}}}}
ref_obj = '2'
document = {'thing': {'amount': ref_obj}}
normalized = Validator(schema).normalized(document)
@@ -186,20 +252,21 @@ def test_issue_147_nested_dict():
assert document['thing']['amount'] is ref_obj
def test_coerce_in_valueschema():
def test_coerce_in_valuesrules():
# https://github.com/pyeve/cerberus/issues/155
schema = {'thing': {'type': 'dict',
'valueschema': {'coerce': int,
'type': 'integer'}}}
schema = {
'thing': {'type': 'dict', 'valuesrules': {'coerce': int, 'type': 'integer'}}
}
document = {'thing': {'amount': '2'}}
expected = {'thing': {'amount': 2}}
assert_normalized(document, expected, schema)
def test_coerce_in_keyschema():
def test_coerce_in_keysrules():
# https://github.com/pyeve/cerberus/issues/155
schema = {'thing': {'type': 'dict',
'keyschema': {'coerce': int, 'type': 'integer'}}}
schema = {
'thing': {'type': 'dict', 'keysrules': {'coerce': int, 'type': 'integer'}}
}
document = {'thing': {'5': 'foo'}}
expected = {'thing': {5: 'foo'}}
assert_normalized(document, expected, schema)
@@ -207,8 +274,7 @@ def test_coerce_in_keyschema():
def test_coercion_of_sequence_items(validator):
# https://github.com/pyeve/cerberus/issues/161
schema = {'a_list': {'type': 'list', 'schema': {'type': 'float',
'coerce': float}}}
schema = {'a_list': {'type': 'list', 'schema': {'type': 'float', 'coerce': float}}}
document = {'a_list': [3, 4, 5]}
expected = {'a_list': [3.0, 4.0, 5.0]}
assert_normalized(document, expected, schema, validator)
@@ -216,110 +282,76 @@ def test_coercion_of_sequence_items(validator):
assert isinstance(x, float)
def test_default_missing():
_test_default_missing({'default': 'bar_value'})
def test_default_setter_missing():
_test_default_missing({'default_setter': lambda doc: 'bar_value'})
def _test_default_missing(default):
@mark.parametrize(
'default', ({'default': 'bar_value'}, {'default_setter': lambda doc: 'bar_value'})
)
def test_default_missing(default):
bar_schema = {'type': 'string'}
bar_schema.update(default)
schema = {'foo': {'type': 'string'},
'bar': bar_schema}
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
document = {'foo': 'foo_value'}
expected = {'foo': 'foo_value', 'bar': 'bar_value'}
assert_normalized(document, expected, schema)
def test_default_existent():
_test_default_existent({'default': 'bar_value'})
def test_default_setter_existent():
def raise_error(doc):
raise RuntimeError('should not be called')
_test_default_existent({'default_setter': raise_error})
def _test_default_existent(default):
@mark.parametrize(
'default', ({'default': 'bar_value'}, {'default_setter': must_not_be_called})
)
def test_default_existent(default):
bar_schema = {'type': 'string'}
bar_schema.update(default)
schema = {'foo': {'type': 'string'},
'bar': bar_schema}
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
document = {'foo': 'foo_value', 'bar': 'non_default'}
assert_normalized(document, document.copy(), schema)
def test_default_none_nullable():
_test_default_none_nullable({'default': 'bar_value'})
def test_default_setter_none_nullable():
def raise_error(doc):
raise RuntimeError('should not be called')
_test_default_none_nullable({'default_setter': raise_error})
def _test_default_none_nullable(default):
bar_schema = {'type': 'string',
'nullable': True}
@mark.parametrize(
'default', ({'default': 'bar_value'}, {'default_setter': must_not_be_called})
)
def test_default_none_nullable(default):
bar_schema = {'type': 'string', 'nullable': True}
bar_schema.update(default)
schema = {'foo': {'type': 'string'},
'bar': bar_schema}
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
document = {'foo': 'foo_value', 'bar': None}
assert_normalized(document, document.copy(), schema)
def test_default_none_nonnullable():
_test_default_none_nullable({'default': 'bar_value'})
def test_default_setter_none_nonnullable():
_test_default_none_nullable(
{'default_setter': lambda doc: 'bar_value'})
def _test_default_none_nonnullable(default):
bar_schema = {'type': 'string',
'nullable': False}
@mark.parametrize(
'default', ({'default': 'bar_value'}, {'default_setter': lambda doc: 'bar_value'})
)
def test_default_none_nonnullable(default):
bar_schema = {'type': 'string', 'nullable': False}
bar_schema.update(default)
schema = {'foo': {'type': 'string'},
'bar': bar_schema}
document = {'foo': 'foo_value', 'bar': 'bar_value'}
assert_normalized(document, document.copy(), schema)
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
document = {'foo': 'foo_value', 'bar': None}
expected = {'foo': 'foo_value', 'bar': 'bar_value'}
assert_normalized(document, expected, schema)
def test_default_none_default_value():
schema = {'foo': {'type': 'string'},
'bar': {'type': 'string',
'nullable': True,
'default': None}}
schema = {
'foo': {'type': 'string'},
'bar': {'type': 'string', 'nullable': True, 'default': None},
}
document = {'foo': 'foo_value'}
expected = {'foo': 'foo_value', 'bar': None}
assert_normalized(document, expected, schema)
def test_default_missing_in_subschema():
_test_default_missing_in_subschema({'default': 'bar_value'})
def test_default_setter_missing_in_subschema():
_test_default_missing_in_subschema(
{'default_setter': lambda doc: 'bar_value'})
def _test_default_missing_in_subschema(default):
@mark.parametrize(
'default', ({'default': 'bar_value'}, {'default_setter': lambda doc: 'bar_value'})
)
def test_default_missing_in_subschema(default):
bar_schema = {'type': 'string'}
bar_schema.update(default)
schema = {'thing': {'type': 'dict',
'schema': {'foo': {'type': 'string'},
'bar': bar_schema}}}
schema = {
'thing': {
'type': 'dict',
'schema': {'foo': {'type': 'string'}, 'bar': bar_schema},
}
}
document = {'thing': {'foo': 'foo_value'}}
expected = {'thing': {'foo': 'foo_value',
'bar': 'bar_value'}}
expected = {'thing': {'foo': 'foo_value', 'bar': 'bar_value'}}
assert_normalized(document, expected, schema)
@@ -328,8 +360,7 @@ def test_depending_default_setters():
'a': {'type': 'integer'},
'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1},
'c': {'type': 'integer', 'default_setter': lambda d: d['b'] * 2},
'd': {'type': 'integer',
'default_setter': lambda d: d['b'] + d['c']}
'd': {'type': 'integer', 'default_setter': lambda d: d['b'] + d['c']},
}
document = {'a': 1}
expected = {'a': 1, 'b': 2, 'c': 4, 'd': 6}
@@ -339,7 +370,7 @@ def test_depending_default_setters():
def test_circular_depending_default_setters(validator):
schema = {
'a': {'type': 'integer', 'default_setter': lambda d: d['b'] + 1},
'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1}
'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1},
}
validator({}, schema)
assert errors.SETTING_DEFAULT_FAILED in validator._errors
@@ -353,14 +384,16 @@ def test_issue_250():
'schema': {
'type': 'dict',
'allow_unknown': True,
'schema': {'a': {'type': 'string'}}
}
'schema': {'a': {'type': 'string'}},
},
}
}
document = {'list': {'is_a': 'mapping'}}
assert_fail(document, schema,
error=('list', ('list', 'type'), errors.BAD_TYPE,
schema['list']['type']))
assert_fail(
document,
schema,
error=('list', ('list', 'type'), errors.BAD_TYPE, schema['list']['type']),
)
def test_issue_250_no_type_pass_on_list():
@@ -370,7 +403,7 @@ def test_issue_250_no_type_pass_on_list():
'schema': {
'allow_unknown': True,
'type': 'dict',
'schema': {'a': {'type': 'string'}}
'schema': {'a': {'type': 'string'}},
}
}
}
@@ -381,28 +414,25 @@ def test_issue_250_no_type_pass_on_list():
def test_issue_250_no_type_fail_on_dict():
# https://github.com/pyeve/cerberus/issues/250
schema = {
'list': {
'schema': {
'allow_unknown': True,
'schema': {'a': {'type': 'string'}}
}
}
'list': {'schema': {'allow_unknown': True, 'schema': {'a': {'type': 'string'}}}}
}
document = {'list': {'a': {'a': 'known'}}}
assert_fail(document, schema,
error=('list', ('list', 'schema'), errors.BAD_TYPE_FOR_SCHEMA,
schema['list']['schema']))
assert_fail(
document,
schema,
error=(
'list',
('list', 'schema'),
errors.BAD_TYPE_FOR_SCHEMA,
schema['list']['schema'],
),
)
def test_issue_250_no_type_fail_pass_on_other():
# https://github.com/pyeve/cerberus/issues/250
schema = {
'list': {
'schema': {
'allow_unknown': True,
'schema': {'a': {'type': 'string'}}
}
}
'list': {'schema': {'allow_unknown': True, 'schema': {'a': {'type': 'string'}}}}
}
document = {'list': 1}
assert_normalized(document, document, schema)
@@ -416,21 +446,20 @@ def test_allow_unknown_with_of_rules():
{
'type': 'dict',
'allow_unknown': True,
'schema': {'known': {'type': 'string'}}
},
{
'type': 'dict',
'schema': {'known': {'type': 'string'}}
'schema': {'known': {'type': 'string'}},
},
{'type': 'dict', 'schema': {'known': {'type': 'string'}}},
]
}
}
# check regression and that allow unknown does not cause any different
# than expected behaviour for one-of.
document = {'test': {'known': 's'}}
assert_fail(document, schema,
error=('test', ('test', 'oneof'),
errors.ONEOF, schema['test']['oneof']))
assert_fail(
document,
schema,
error=('test', ('test', 'oneof'), errors.ONEOF, schema['test']['oneof']),
)
# check that allow_unknown is actually applied
document = {'test': {'known': 's', 'unknown': 'asd'}}
assert_success(document, schema)
@@ -439,18 +468,20 @@ def test_allow_unknown_with_of_rules():
def test_271_normalising_tuples():
# https://github.com/pyeve/cerberus/issues/271
schema = {
'my_field': {
'type': 'list',
'schema': {'type': ('string', 'number', 'dict')}
}
'my_field': {'type': 'list', 'schema': {'type': ('string', 'number', 'dict')}}
}
document = {'my_field': ('foo', 'bar', 42, 'albert',
'kandinsky', {'items': 23})}
document = {'my_field': ('foo', 'bar', 42, 'albert', 'kandinsky', {'items': 23})}
assert_success(document, schema)
normalized = Validator(schema).normalized(document)
assert normalized['my_field'] == ('foo', 'bar', 42, 'albert',
'kandinsky', {'items': 23})
assert normalized['my_field'] == (
'foo',
'bar',
42,
'albert',
'kandinsky',
{'items': 23},
)
def test_allow_unknown_wo_schema():
@@ -472,14 +503,41 @@ def test_allow_unknown_with_purge_unknown_subdocument():
schema = {
'foo': {
'type': 'dict',
'schema': {
'bar': {
'type': 'string'
}
},
'allow_unknown': True
'schema': {'bar': {'type': 'string'}},
'allow_unknown': True,
}
}
document = {'foo': {'bar': 'baz', 'corge': False}, 'thud': 'xyzzy'}
expected = {'foo': {'bar': 'baz', 'corge': False}}
assert_normalized(document, expected, schema, validator)
def test_purge_readonly():
schema = {
'description': {'type': 'string', 'maxlength': 500},
'last_updated': {'readonly': True},
}
validator = Validator(schema=schema, purge_readonly=True)
document = {'description': 'it is a thing'}
expected = deepcopy(document)
document['last_updated'] = 'future'
assert_normalized(document, expected, validator=validator)
def test_defaults_in_allow_unknown_schema():
schema = {'meta': {'type': 'dict'}, 'version': {'type': 'string'}}
allow_unknown = {
'type': 'dict',
'schema': {
'cfg_path': {'type': 'string', 'default': 'cfg.yaml'},
'package': {'type': 'string'},
},
}
validator = Validator(schema=schema, allow_unknown=allow_unknown)
document = {'version': '1.2.3', 'plugin_foo': {'package': 'foo'}}
expected = {
'version': '1.2.3',
'plugin_foo': {'package': 'foo', 'cfg_path': 'cfg.yaml'},
}
assert_normalized(document, expected, schema, validator)
+13 -11
View File
@@ -1,14 +1,17 @@
# -*- coding: utf-8 -*-
from cerberus import schema_registry, rules_set_registry, Validator
from cerberus.tests import (assert_fail, assert_normalized,
assert_schema_error, assert_success)
from cerberus.tests import (
assert_fail,
assert_normalized,
assert_schema_error,
assert_success,
)
def test_schema_registry_simple():
schema_registry.add('foo', {'bar': {'type': 'string'}})
schema = {'a': {'schema': 'foo'},
'b': {'schema': 'foo'}}
schema = {'a': {'schema': 'foo'}, 'b': {'schema': 'foo'}}
document = {'a': {'bar': 'a'}, 'b': {'bar': 'b'}}
assert_success(document, schema)
@@ -33,23 +36,22 @@ def test_allow_unknown_as_reference():
def test_recursion():
rules_set_registry.add('self',
{'type': 'dict', 'allow_unknown': 'self'})
rules_set_registry.add('self', {'type': 'dict', 'allow_unknown': 'self'})
v = Validator(allow_unknown='self')
assert_success({0: {1: {2: {}}}}, {}, v)
def test_references_remain_unresolved(validator):
rules_set_registry.extend((('boolean', {'type': 'boolean'}),
('booleans', {'valueschema': 'boolean'})))
rules_set_registry.extend(
(('boolean', {'type': 'boolean'}), ('booleans', {'valuesrules': 'boolean'}))
)
validator.schema = {'foo': 'booleans'}
assert 'booleans' == validator.schema['foo']
assert 'boolean' == rules_set_registry._storage['booleans']['valueschema']
assert 'boolean' == rules_set_registry._storage['booleans']['valuesrules']
def test_rules_registry_with_anyof_type():
rules_set_registry.add('string_or_integer',
{'anyof_type': ['string', 'integer']})
rules_set_registry.add('string_or_integer', {'anyof_type': ['string', 'integer']})
schema = {'soi': 'string_or_integer'}
assert_success({'soi': 'hello'}, schema)
+90 -27
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
import re
import pytest
from cerberus import Validator, errors, SchemaError
@@ -9,14 +11,14 @@ from cerberus.tests import assert_schema_error
def test_empty_schema():
validator = Validator()
with pytest.raises(SchemaError, message=errors.SCHEMA_ERROR_MISSING):
with pytest.raises(SchemaError, match=errors.SCHEMA_ERROR_MISSING):
validator({}, schema=None)
def test_bad_schema_type(validator):
schema = "this string should really be dict"
exp_msg = errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema)
with pytest.raises(SchemaError, message=exp_msg):
msg = errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema)
with pytest.raises(SchemaError, match=msg):
validator.schema = schema
@@ -28,23 +30,21 @@ def test_bad_schema_type_field(validator):
def test_unknown_rule(validator):
message = "{'foo': [{'unknown': ['unknown rule']}]}"
with pytest.raises(SchemaError, message=message):
msg = "{'foo': [{'unknown': ['unknown rule']}]}"
with pytest.raises(SchemaError, match=re.escape(msg)):
validator.schema = {'foo': {'unknown': 'rule'}}
def test_unknown_type(validator):
field = 'name'
value = 'catch_me'
message = str({field: [{'type': ['unallowed value %s' % value]}]})
with pytest.raises(SchemaError, message=message):
validator.schema = {'foo': {'unknown': 'rule'}}
msg = str({'foo': [{'type': ['Unsupported types: unknown']}]})
with pytest.raises(SchemaError, match=re.escape(msg)):
validator.schema = {'foo': {'type': 'unknown'}}
def test_bad_schema_definition(validator):
field = 'name'
message = str({field: ['must be of dict type']})
with pytest.raises(SchemaError, message=message):
msg = str({field: ['must be of dict type']})
with pytest.raises(SchemaError, match=re.escape(msg)):
validator.schema = {field: 'this should really be a dict'}
@@ -61,14 +61,14 @@ def test_normalization_rules_are_invalid_in_of_rules():
def test_anyof_allof_schema_validate():
# make sure schema with 'anyof' and 'allof' constraints are checked
# correctly
schema = {'doc': {'type': 'dict',
'anyof': [
{'schema': [{'param': {'type': 'number'}}]}]}}
schema = {
'doc': {'type': 'dict', 'anyof': [{'schema': [{'param': {'type': 'number'}}]}]}
}
assert_schema_error({'doc': 'this is my document'}, schema)
schema = {'doc': {'type': 'dict',
'allof': [
{'schema': [{'param': {'type': 'number'}}]}]}}
schema = {
'doc': {'type': 'dict', 'allof': [{'schema': [{'param': {'type': 'number'}}]}]}
}
assert_schema_error({'doc': 'this is my document'}, schema)
@@ -88,24 +88,87 @@ def test_validated_schema_cache():
v = Validator({'foozifix': {'coerce': int}})
assert len(v._valid_schemas) == cache_size
max_cache_size = 147
assert cache_size <= max_cache_size, \
"There's an unexpected high amount (%s) of cached valid " \
"definition schemas. Unless you added further tests, " \
"there are good chances that something is wrong. " \
"If you added tests with new schemas, you can try to " \
"adjust the variable `max_cache_size` according to " \
max_cache_size = 160
assert cache_size <= max_cache_size, (
"There's an unexpected high amount (%s) of cached valid "
"definition schemas. Unless you added further tests, "
"there are good chances that something is wrong. "
"If you added tests with new schemas, you can try to "
"adjust the variable `max_cache_size` according to "
"the added schemas." % cache_size
)
def test_expansion_in_nested_schema():
schema = {'detroit': {'schema': {'anyof_regex': ['^Aladdin', 'Sane$']}}}
v = Validator(schema)
assert (v.schema['detroit']['schema'] ==
{'anyof': [{'regex': '^Aladdin'}, {'regex': 'Sane$'}]})
assert v.schema['detroit']['schema'] == {
'anyof': [{'regex': '^Aladdin'}, {'regex': 'Sane$'}]
}
def test_unvalidated_schema_can_be_copied():
schema = UnvalidatedSchema()
schema_copy = schema.copy()
assert schema_copy == schema
# TODO remove with next major release
def test_deprecated_rule_names_in_valueschema():
def check_with(field, value, error):
pass
schema = {
"field_1": {
"type": "dict",
"valueschema": {
"type": "dict",
"keyschema": {"type": "string"},
"valueschema": {"type": "string"},
},
},
"field_2": {
"type": "list",
"items": [
{"keyschema": {}},
{"validator": check_with},
{"valueschema": {}},
],
},
}
validator = Validator(schema)
assert validator.schema == {
"field_1": {
"type": "dict",
"valuesrules": {
"type": "dict",
"keysrules": {"type": "string"},
"valuesrules": {"type": "string"},
},
},
"field_2": {
"type": "list",
"items": [
{"keysrules": {}},
{"check_with": check_with},
{"valuesrules": {}},
],
},
}
def test_anyof_check_with():
def foo(field, value, error):
pass
def bar(field, value, error):
pass
schema = {'field': {'anyof_check_with': [foo, bar]}}
validator = Validator(schema)
assert validator.schema == {
'field': {'anyof': [{'check_with': foo}, {'check_with': bar}]}
}
+11
View File
@@ -0,0 +1,11 @@
from cerberus.utils import compare_paths_lt
def test_compare_paths():
lesser = ('a_dict', 'keysrules')
greater = ('a_dict', 'valuesrules')
assert compare_paths_lt(lesser, greater)
lesser += ('type',)
greater += ('regex',)
assert compare_paths_lt(lesser, greater)
File diff suppressed because it is too large Load Diff
+35 -24
View File
@@ -1,12 +1,11 @@
from __future__ import absolute_import
from collections import Mapping, namedtuple, Sequence
from collections import namedtuple
from cerberus.platform import _int_types, _str_type
from cerberus.platform import _int_types, _str_type, Mapping, Sequence, Set
TypeDefinition = namedtuple('TypeDefinition',
'name,included_types,excluded_types')
TypeDefinition = namedtuple('TypeDefinition', 'name,included_types,excluded_types')
"""
This class is used to define types that can be used as value in the
:attr:`~cerberus.Validator.types_mapping` property.
@@ -19,19 +18,33 @@ contained in ``excluded_types``.
def compare_paths_lt(x, y):
for i in range(min(len(x), len(y))):
if isinstance(x[i], type(y[i])):
if x[i] != y[i]:
return x[i] < y[i]
elif isinstance(x[i], _int_types):
min_length = min(len(x), len(y))
if x[:min_length] == y[:min_length]:
return len(x) == min_length
for i in range(min_length):
a, b = x[i], y[i]
for _type in (_int_types, _str_type, tuple):
if isinstance(a, _type):
if isinstance(b, _type):
break
else:
return True
if a == b:
continue
elif a < b:
return True
elif isinstance(y[i], _int_types):
else:
return False
return len(x) < len(y)
raise RuntimeError
def drop_item_from_tuple(t, i):
return t[:i] + t[i + 1:]
return t[:i] + t[i + 1 :]
def get_Validator_class():
@@ -50,26 +63,24 @@ def mapping_to_frozenset(mapping):
equal. As it is used to identify equality of schemas, this can be
considered okay as definitions are semantically equal regardless the
container type. """
mapping = mapping.copy()
aggregation = {}
for key, value in mapping.items():
if isinstance(value, Mapping):
mapping[key] = mapping_to_frozenset(value)
aggregation[key] = mapping_to_frozenset(value)
elif isinstance(value, Sequence):
value = list(value)
for i, item in enumerate(value):
if isinstance(item, Mapping):
value[i] = mapping_to_frozenset(item)
mapping[key] = tuple(value)
return frozenset(mapping.items())
aggregation[key] = tuple(value)
elif isinstance(value, Set):
aggregation[key] = frozenset(value)
else:
aggregation[key] = value
def isclass(obj):
try:
issubclass(obj, object)
except TypeError:
return False
else:
return True
return frozenset(aggregation.items())
def quote_string(value):
+507 -299
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,3 +1,3 @@
from .core import where
__version__ = "2018.11.29"
__version__ = "2019.03.09"
+146
View File
@@ -4510,3 +4510,149 @@ Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh
jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw
3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=
-----END CERTIFICATE-----
# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI
# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI
# Label: "emSign Root CA - G1"
# Serial: 235931866688319308814040
# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac
# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c
# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67
-----BEGIN CERTIFICATE-----
MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD
VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU
ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH
MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO
MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv
Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz
f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO
8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq
d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM
tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt
Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB
o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD
AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x
PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM
wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d
GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH
6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby
RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
iN66zB+Afko=
-----END CERTIFICATE-----
# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI
# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI
# Label: "emSign ECC Root CA - G3"
# Serial: 287880440101571086945156
# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40
# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1
# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b
-----BEGIN CERTIFICATE-----
MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
+JbNR6iC8hZVdyR+EhCVBCyj
-----END CERTIFICATE-----
# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI
# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI
# Label: "emSign Root CA - C1"
# Serial: 825510296613316004955058
# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68
# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01
# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f
-----BEGIN CERTIFICATE-----
MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG
A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg
SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw
MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v
dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ
BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ
HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH
3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH
GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c
xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1
aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq
TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87
/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4
kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG
YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT
+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo
WXzhriKi4gp6D/piq1JM4fHfyr6DDUI=
-----END CERTIFICATE-----
# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI
# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI
# Label: "emSign ECC Root CA - C3"
# Serial: 582948710642506000014504
# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5
# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66
# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3
-----BEGIN CERTIFICATE-----
MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG
EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx
IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw
MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND
IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci
MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti
sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O
BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c
3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J
0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==
-----END CERTIFICATE-----
# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post
# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post
# Label: "Hongkong Post Root CA 3"
# Serial: 46170865288971385588281144162979347873371282084
# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0
# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02
# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n
a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5
NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT
CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u
Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO
dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI
VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV
9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY
2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY
vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt
bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb
x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+
l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK
TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj
Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e
i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw
DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG
7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk
MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr
gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk
GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS
3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm
Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+
l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c
JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP
L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
mpv0
-----END CERTIFICATE-----
-5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@@ -14,7 +13,3 @@ def where():
f = os.path.dirname(__file__)
return os.path.join(f, 'cacert.pem')
if __name__ == '__main__':
print(where())
+1 -1
View File
@@ -19,7 +19,7 @@ from click_completion.core import completion_configuration, get_code, install, s
from click_completion.lib import get_auto_shell
from click_completion.patch import patch as _patch
__version__ = '0.5.0'
__version__ = '0.5.1'
_initialized = False
+8 -12
View File
@@ -131,8 +131,9 @@ def get_choices(cli, prog_name, args, incomplete):
choices.append((opt, None))
if isinstance(ctx.command, MultiCommand):
for name in ctx.command.list_commands(ctx):
if match(name, incomplete):
choices.append((name, ctx.command.get_command_short_help(ctx, name)))
command = ctx.command.get_command(ctx, name)
if match(name, incomplete) and not command.hidden:
choices.append((name, command.get_short_help_str()))
for item, help in choices:
yield (item, help)
@@ -201,7 +202,7 @@ def do_fish_complete(cli, prog_name):
for item, help in get_choices(cli, prog_name, args, incomplete):
if help:
echo("%s\t%s" % (item, re.sub('\s', ' ', help)))
echo("%s\t%s" % (item, re.sub(r'\s', ' ', help)))
else:
echo(item)
@@ -232,11 +233,11 @@ def do_zsh_complete(cli, prog_name):
incomplete = ''
def escape(s):
return s.replace('"', '""').replace("'", "''").replace('$', '\\$')
return s.replace('"', '""').replace("'", "''").replace('$', '\\$').replace('`', '\\`')
res = []
for item, help in get_choices(cli, prog_name, args, incomplete):
if help:
res.append('"%s"\:"%s"' % (escape(item), escape(help)))
res.append(r'"%s"\:"%s"' % (escape(item), escape(help)))
else:
res.append('"%s"' % escape(item))
if res:
@@ -349,13 +350,8 @@ def install(shell=None, prog_name=None, env_name=None, path=None, append=None, e
path = path or os.path.expanduser('~') + '/.bash_completion'
mode = mode or 'a'
elif shell == 'zsh':
ohmyzsh = os.path.expanduser('~') + '/.oh-my-zsh'
if os.path.exists(ohmyzsh):
path = path or ohmyzsh + '/completions/_%s' % prog_name
mode = mode or 'w'
else:
path = path or os.path.expanduser('~') + '/.zshrc'
mode = mode or 'a'
path = path or os.path.expanduser('~') + '/.zshrc'
mode = mode or 'a'
elif shell == 'powershell':
subprocess.check_call(['powershell', 'Set-ExecutionPolicy Unrestricted -Scope CurrentUser'])
path = path or subprocess.check_output(['powershell', '-NoProfile', 'echo $profile']).strip() if install else ''
-1
View File
@@ -3,6 +3,5 @@ _{{prog_name}}() {
eval $(env COMMANDLINE="${words[1,$CURRENT]}" {{complete_var}}=complete-zsh {% for k, v in extra_env.items() %} {{k}}={{v}}{% endfor %} {{prog_name}})
}
if [[ "$(basename -- ${(%):-%x})" != "_{{prog_name}}" ]]; then
autoload -U compinit && compinit
compdef _{{prog_name}} {{prog_name}}
fi
+1 -1
View File
@@ -6,7 +6,7 @@
#
import logging
__version__ = '0.2.8'
__version__ = '0.2.9'
class DistlibException(Exception):
pass
+1 -1
View File
@@ -22,7 +22,7 @@ from .util import cached_property, zip_dir, ServerProxy
logger = logging.getLogger(__name__)
DEFAULT_INDEX = 'https://pypi.python.org/pypi'
DEFAULT_INDEX = 'https://pypi.org/pypi'
DEFAULT_REALM = 'pypi'
class PackageIndex(object):
+3 -3
View File
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
HASHER_HASH = re.compile(r'^(\w+)=([a-f0-9]+)')
CHARSET = re.compile(r';\s*charset\s*=\s*(.*)\s*$', re.I)
HTML_CONTENT_TYPE = re.compile('text/html|application/x(ht)?ml')
DEFAULT_INDEX = 'https://pypi.python.org/pypi'
DEFAULT_INDEX = 'https://pypi.org/pypi'
def get_all_distribution_names(url=None):
"""
@@ -197,7 +197,7 @@ class Locator(object):
is_downloadable = basename.endswith(self.downloadable_extensions)
if is_wheel:
compatible = is_compatible(Wheel(basename), self.wheel_tags)
return (t.scheme == 'https', 'pypi.python.org' in t.netloc,
return (t.scheme == 'https', 'pypi.org' in t.netloc,
is_downloadable, is_wheel, compatible, basename)
def prefer_url(self, url1, url2):
@@ -1049,7 +1049,7 @@ class AggregatingLocator(Locator):
# versions which don't conform to PEP 426 / PEP 440.
default_locator = AggregatingLocator(
JSONLocator(),
SimpleScrapingLocator('https://pypi.python.org/simple/',
SimpleScrapingLocator('https://pypi.org/simple/',
timeout=3.0),
scheme='legacy')
+5 -3
View File
@@ -91,9 +91,11 @@ _426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
_426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
'Setup-Requires-Dist', 'Extension')
# See issue #106: Sometimes 'Requires' occurs wrongly in the metadata. Include
# it in the tuple literal below to allow it (for now)
_566_FIELDS = _426_FIELDS + ('Description-Content-Type', 'Requires')
# See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in
# the metadata. Include them in the tuple literal below to allow them
# (for now).
_566_FIELDS = _426_FIELDS + ('Description-Content-Type',
'Requires', 'Provides')
_566_MARKERS = ('Description-Content-Type',)
+6 -20
View File
@@ -39,27 +39,12 @@ _DEFAULT_MANIFEST = '''
# check if Python is called on the first line with this expression
FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*-
import re
import sys
from %(module)s import %(import_name)s
if __name__ == '__main__':
import sys, re
def _resolve(module, func):
__import__(module)
mod = sys.modules[module]
parts = func.split('.')
result = getattr(mod, parts.pop(0))
for p in parts:
result = getattr(result, p)
return result
try:
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
func = _resolve('%(module)s', '%(func)s')
rc = func() # None interpreted as 0
except Exception as e: # only supporting Python >= 2.6
sys.stderr.write('%%s\n' %% e)
rc = 1
sys.exit(rc)
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(%(func)s())
'''
@@ -225,6 +210,7 @@ class ScriptMaker(object):
def _get_script_text(self, entry):
return self.script_template % dict(module=entry.prefix,
import_name=entry.suffix.split('.')[0],
func=entry.suffix)
manifest = _DEFAULT_MANIFEST
+5 -1
View File
@@ -804,11 +804,15 @@ def ensure_slash(s):
def parse_credentials(netloc):
username = password = None
if '@' in netloc:
prefix, netloc = netloc.split('@', 1)
prefix, netloc = netloc.rsplit('@', 1)
if ':' not in prefix:
username = prefix
else:
username, password = prefix.split(':', 1)
if username:
username = unquote(username)
if password:
password = unquote(password)
return username, password, netloc
+23 -7
View File
@@ -433,6 +433,22 @@ class Wheel(object):
self.build_zip(pathname, archive_paths)
return pathname
def skip_entry(self, arcname):
"""
Determine whether an archive entry should be skipped when verifying
or installing.
"""
# The signature file won't be in RECORD,
# and we don't currently don't do anything with it
# We also skip directories, as they won't be in RECORD
# either. See:
#
# https://github.com/pypa/wheel/issues/294
# https://github.com/pypa/wheel/issues/287
# https://github.com/pypa/wheel/pull/289
#
return arcname.endswith(('/', '/RECORD.jws'))
def install(self, paths, maker, **kwargs):
"""
Install a wheel to the specified paths. If kwarg ``warner`` is
@@ -514,9 +530,7 @@ class Wheel(object):
u_arcname = arcname
else:
u_arcname = arcname.decode('utf-8')
# The signature file won't be in RECORD,
# and we don't currently don't do anything with it
if u_arcname.endswith('/RECORD.jws'):
if self.skip_entry(u_arcname):
continue
row = records[u_arcname]
if row[2] and str(zinfo.file_size) != row[2]:
@@ -786,13 +800,15 @@ class Wheel(object):
u_arcname = arcname
else:
u_arcname = arcname.decode('utf-8')
if '..' in u_arcname:
# See issue #115: some wheels have .. in their entries, but
# in the filename ... e.g. __main__..py ! So the check is
# updated to look for .. in the directory portions
p = u_arcname.split('/')
if '..' in p:
raise DistlibException('invalid entry in '
'wheel: %r' % u_arcname)
# The signature file won't be in RECORD,
# and we don't currently don't do anything with it
if u_arcname.endswith('/RECORD.jws'):
if self.skip_entry(u_arcname):
continue
row = records[u_arcname]
if row[2] and str(zinfo.file_size) != row[2]:
+6
View File
@@ -1,12 +1,18 @@
from .compat import IS_TYPE_CHECKING
from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values
if IS_TYPE_CHECKING:
from typing import Any, Optional
def load_ipython_extension(ipython):
# type: (Any) -> None
from .ipython import load_ipython_extension
load_ipython_extension(ipython)
def get_cli_string(path=None, action=None, key=None, value=None, quote=None):
# type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str
"""Returns a string suitable for running as a shell script.
Useful for converting a arguments passed to a fabric task
+11 -1
View File
@@ -8,9 +8,13 @@ except ImportError:
'Run pip install "python-dotenv[cli]" to fix this.')
sys.exit(1)
from .compat import IS_TYPE_CHECKING
from .main import dotenv_values, get_key, set_key, unset_key, run_command
from .version import __version__
if IS_TYPE_CHECKING:
from typing import Any, List
@click.group()
@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
@@ -22,6 +26,7 @@ from .version import __version__
@click.version_option(version=__version__)
@click.pass_context
def cli(ctx, file, quote):
# type: (click.Context, Any, Any) -> None
'''This script is used to set, get or unset values from a .env file.'''
ctx.obj = {}
ctx.obj['FILE'] = file
@@ -31,6 +36,7 @@ def cli(ctx, file, quote):
@cli.command()
@click.pass_context
def list(ctx):
# type: (click.Context) -> None
'''Display all the stored key/value.'''
file = ctx.obj['FILE']
dotenv_as_dict = dotenv_values(file)
@@ -43,6 +49,7 @@ def list(ctx):
@click.argument('key', required=True)
@click.argument('value', required=True)
def set(ctx, key, value):
# type: (click.Context, Any, Any) -> None
'''Store the given key/value.'''
file = ctx.obj['FILE']
quote = ctx.obj['QUOTE']
@@ -57,6 +64,7 @@ def set(ctx, key, value):
@click.pass_context
@click.argument('key', required=True)
def get(ctx, key):
# type: (click.Context, Any) -> None
'''Retrieve the value for the given key.'''
file = ctx.obj['FILE']
stored_value = get_key(file, key)
@@ -70,6 +78,7 @@ def get(ctx, key):
@click.pass_context
@click.argument('key', required=True)
def unset(ctx, key):
# type: (click.Context, Any) -> None
'''Removes the given key.'''
file = ctx.obj['FILE']
quote = ctx.obj['QUOTE']
@@ -84,13 +93,14 @@ def unset(ctx, key):
@click.pass_context
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
def run(ctx, commandline):
# type: (click.Context, List[str]) -> None
"""Run command with environment variables present."""
file = ctx.obj['FILE']
dotenv_as_dict = dotenv_values(file)
if not commandline:
click.echo('No command given.')
exit(1)
ret = run_command(commandline, dotenv_as_dict)
ret = run_command(commandline, dotenv_as_dict) # type: ignore
exit(ret)
+19 -7
View File
@@ -1,9 +1,21 @@
import os
import sys
try:
from StringIO import StringIO # noqa
except ImportError:
from io import StringIO # noqa
PY2 = sys.version_info[0] == 2
WIN = sys.platform.startswith('win')
text_type = unicode if PY2 else str # noqa
if sys.version_info >= (3, 0):
from io import StringIO # noqa
else:
from StringIO import StringIO # noqa
PY2 = sys.version_info[0] == 2 # type: bool
def is_type_checking():
# type: () -> bool
try:
from typing import TYPE_CHECKING
except ImportError:
return False
return TYPE_CHECKING
IS_TYPE_CHECKING = os.environ.get("MYPY_RUNNING", is_type_checking())
-54
View File
@@ -1,54 +0,0 @@
import os
class UndefinedValueError(Exception):
pass
class Undefined(object):
"""Class to represent undefined type. """
pass
# Reference instance to represent undefined values
undefined = Undefined()
def _cast_boolean(value):
"""
Helper to convert config values to boolean as ConfigParser do.
"""
_BOOLEANS = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False, '': False}
value = str(value)
if value.lower() not in _BOOLEANS:
raise ValueError('Not a boolean: %s' % value)
return _BOOLEANS[value.lower()]
def getenv(option, default=undefined, cast=undefined):
"""
Return the value for option or default if defined.
"""
# We can't avoid __contains__ because value may be empty.
if option in os.environ:
value = os.environ[option]
else:
if isinstance(default, Undefined):
raise UndefinedValueError('{} not found. Declare it as envvar or define a default value.'.format(option))
value = default
if isinstance(cast, Undefined):
return value
if cast is bool:
value = _cast_boolean(value)
elif cast is list:
value = [x for x in value.split(',') if x]
else:
value = cast(value)
return value
+3 -3
View File
@@ -1,8 +1,8 @@
from __future__ import print_function
from IPython.core.magic import Magics, line_magic, magics_class
from IPython.core.magic_arguments import (argument, magic_arguments,
parse_argstring)
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
parse_argstring) # type: ignore
from .main import find_dotenv, load_dotenv
+91 -31
View File
@@ -10,12 +10,27 @@ import sys
from subprocess import Popen
import tempfile
import warnings
from collections import OrderedDict, namedtuple
from collections import OrderedDict
from contextlib import contextmanager
from .compat import StringIO, PY2, WIN, text_type
from .compat import StringIO, PY2, IS_TYPE_CHECKING
__posix_variable = re.compile(r'\$\{[^\}]*\}')
if IS_TYPE_CHECKING: # pragma: no cover
from typing import (
Dict, Iterator, List, Match, Optional, Pattern, Union,
Text, IO, Tuple
)
if sys.version_info >= (3, 6):
_PathLike = os.PathLike
else:
_PathLike = Text
if sys.version_info >= (3, 0):
_StringIO = StringIO
else:
_StringIO = StringIO[Text]
__posix_variable = re.compile(r'\$\{[^\}]*\}') # type: Pattern[Text]
_binding = re.compile(
r"""
@@ -42,22 +57,31 @@ _binding = re.compile(
)
""".format(r'[^\S\r\n]'),
re.MULTILINE | re.VERBOSE,
)
) # type: Pattern[Text]
_escape_sequence = re.compile(r"\\[\\'\"abfnrtv]")
_escape_sequence = re.compile(r"\\[\\'\"abfnrtv]") # type: Pattern[Text]
Binding = namedtuple('Binding', 'key value original')
try:
from typing import NamedTuple, Optional, Text
Binding = NamedTuple("Binding", [("key", Optional[Text]),
("value", Optional[Text]),
("original", Text)])
except ImportError:
from collections import namedtuple
Binding = namedtuple("Binding", ["key", "value", "original"])
def decode_escapes(string):
# type: (Text) -> Text
def decode_match(match):
return codecs.decode(match.group(0), 'unicode-escape')
# type: (Match[Text]) -> Text
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
return _escape_sequence.sub(decode_match, string)
def is_surrounded_by(string, char):
# type: (Text, Text) -> bool
return (
len(string) > 1
and string[0] == string[-1] == char
@@ -65,7 +89,9 @@ def is_surrounded_by(string, char):
def parse_binding(string, position):
# type: (Text, int) -> Tuple[Binding, int]
match = _binding.match(string, position)
assert match is not None
(matched, key, value) = match.groups()
if key is None or value is None:
key = None
@@ -80,6 +106,7 @@ def parse_binding(string, position):
def parse_stream(stream):
# type:(IO[Text]) -> Iterator[Binding]
string = stream.read()
position = 0
length = len(string)
@@ -88,26 +115,41 @@ def parse_stream(stream):
yield binding
def to_env(text):
# type: (Text) -> str
"""
Encode a string the same way whether it comes from the environment or a `.env` file.
"""
if PY2:
return text.encode(sys.getfilesystemencoding() or "utf-8")
else:
return text
class DotEnv():
def __init__(self, dotenv_path, verbose=False):
self.dotenv_path = dotenv_path
self._dict = None
self.verbose = verbose
def __init__(self, dotenv_path, verbose=False, encoding=None):
# type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text]) -> None
self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO]
self._dict = None # type: Optional[Dict[Text, Text]]
self.verbose = verbose # type: bool
self.encoding = encoding # type: Union[None, Text]
@contextmanager
def _get_stream(self):
# type: () -> Iterator[IO[Text]]
if isinstance(self.dotenv_path, StringIO):
yield self.dotenv_path
elif os.path.isfile(self.dotenv_path):
with io.open(self.dotenv_path) as stream:
with io.open(self.dotenv_path, encoding=self.encoding) as stream:
yield stream
else:
if self.verbose:
warnings.warn("File doesn't exist {}".format(self.dotenv_path))
warnings.warn("File doesn't exist {}".format(self.dotenv_path)) # type: ignore
yield StringIO('')
def dict(self):
# type: () -> Dict[Text, Text]
"""Return dotenv as dict"""
if self._dict:
return self._dict
@@ -117,29 +159,26 @@ class DotEnv():
return self._dict
def parse(self):
# type: () -> Iterator[Tuple[Text, Text]]
with self._get_stream() as stream:
for mapping in parse_stream(stream):
if mapping.key is not None and mapping.value is not None:
yield mapping.key, mapping.value
def set_as_environment_variables(self, override=False):
# type: (bool) -> bool
"""
Load the current dotenv as system environemt variable.
"""
for k, v in self.dict().items():
if k in os.environ and not override:
continue
# With Python2 on Windows, force environment variables to str to avoid
# "TypeError: environment can only contain strings" in Python's subprocess.py.
if PY2 and WIN:
if isinstance(k, text_type) or isinstance(v, text_type):
k = k.encode('ascii')
v = v.encode('ascii')
os.environ[k] = v
os.environ[to_env(k)] = to_env(v)
return True
def get(self, key):
# type: (Text) -> Optional[Text]
"""
"""
data = self.dict()
@@ -148,10 +187,13 @@ class DotEnv():
return data[key]
if self.verbose:
warnings.warn("key %s not found in %s." % (key, self.dotenv_path))
warnings.warn("key %s not found in %s." % (key, self.dotenv_path)) # type: ignore
return None
def get_key(dotenv_path, key_to_get):
# type: (Union[Text, _PathLike], Text) -> Optional[Text]
"""
Gets the value of a given key from the given .env
@@ -162,10 +204,11 @@ def get_key(dotenv_path, key_to_get):
@contextmanager
def rewrite(path):
# type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
try:
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
with io.open(path) as source:
yield (source, dest)
yield (source, dest) # type: ignore
except BaseException:
if os.path.isfile(dest.name):
os.unlink(dest.name)
@@ -175,6 +218,7 @@ def rewrite(path):
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
# type: (_PathLike, Text, Text, Text) -> Tuple[Optional[bool], Text, Text]
"""
Adds or Updates a key/value to the given .env
@@ -183,7 +227,7 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
"""
value_to_set = value_to_set.strip("'").strip('"')
if not os.path.exists(dotenv_path):
warnings.warn("can't write to %s - it doesn't exist." % dotenv_path)
warnings.warn("can't write to %s - it doesn't exist." % dotenv_path) # type: ignore
return None, key_to_set, value_to_set
if " " in value_to_set:
@@ -207,6 +251,7 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
# type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text]
"""
Removes a given key from the given .env
@@ -214,7 +259,7 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
If the given key doesn't exist in the .env, fails
"""
if not os.path.exists(dotenv_path):
warnings.warn("can't delete from %s - it doesn't exist." % dotenv_path)
warnings.warn("can't delete from %s - it doesn't exist." % dotenv_path) # type: ignore
return None, key_to_unset
removed = False
@@ -226,14 +271,16 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
dest.write(mapping.original)
if not removed:
warnings.warn("key %s not removed from %s - key doesn't exist." % (key_to_unset, dotenv_path))
warnings.warn("key %s not removed from %s - key doesn't exist." % (key_to_unset, dotenv_path)) # type: ignore
return None, key_to_unset
return removed, key_to_unset
def resolve_nested_variables(values):
# type: (Dict[Text, Text]) -> Dict[Text, Text]
def _replacement(name):
# type: (Text) -> Text
"""
get appropriate value for a variable name.
first search in environ, if not found,
@@ -243,6 +290,7 @@ def resolve_nested_variables(values):
return ret
def _re_sub_callback(match_object):
# type: (Match[Text]) -> Text
"""
From a match object gets the variable name and returns
the correct replacement
@@ -258,6 +306,7 @@ def resolve_nested_variables(values):
def _walk_to_root(path):
# type: (Text) -> Iterator[Text]
"""
Yield directories starting from the given directory up to the root
"""
@@ -276,6 +325,7 @@ def _walk_to_root(path):
def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
# type: (Text, bool, bool) -> Text
"""
Search in increasingly higher folders for the given file
@@ -288,7 +338,14 @@ def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
# will work for .py files
frame = sys._getframe()
# find first frame that is outside of this file
while frame.f_code.co_filename == __file__:
if PY2 and not __file__.endswith('.py'):
# in Python2 __file__ extension could be .pyc or .pyo (this doesn't account
# for edge case of Python compiled for non-standard extension)
current_file = __file__.rsplit('.', 1)[0] + '.py'
else:
current_file = __file__
while frame.f_code.co_filename == current_file:
frame = frame.f_back
frame_filename = frame.f_code.co_filename
path = os.path.dirname(os.path.abspath(frame_filename))
@@ -304,17 +361,20 @@ def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
return ''
def load_dotenv(dotenv_path=None, stream=None, verbose=False, override=False):
def load_dotenv(dotenv_path=None, stream=None, verbose=False, override=False, **kwargs):
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Union[None, Text]) -> bool
f = dotenv_path or stream or find_dotenv()
return DotEnv(f, verbose=verbose).set_as_environment_variables(override=override)
return DotEnv(f, verbose=verbose, **kwargs).set_as_environment_variables(override=override)
def dotenv_values(dotenv_path=None, stream=None, verbose=False):
def dotenv_values(dotenv_path=None, stream=None, verbose=False, **kwargs):
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, Union[None, Text]) -> Dict[Text, Text]
f = dotenv_path or stream or find_dotenv()
return DotEnv(f, verbose=verbose).dict()
return DotEnv(f, verbose=verbose, **kwargs).dict()
def run_command(command, env):
# type: (List[str], Dict[str, str]) -> int
"""Run command in sub process.
Runs the command in a sub process with the variables from `env`
+1
View File
@@ -0,0 +1 @@
# Marker file for PEP 561
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.10.1"
__version__ = "0.10.2"
+4 -3
View File
@@ -345,6 +345,7 @@ the pattern, the actual match represents the shortest successful match for
**Version history (in brief)**:
- 1.12.0 Do not assume closing brace when an opening one is found (thanks @mattsep)
- 1.11.1 Revert having unicode char in docstring, it breaks Bamboo builds(?!)
- 1.11.0 Implement `__contains__` for Result instances.
- 1.10.0 Introduce a "letters" matcher, since "w" matches numbers
@@ -415,7 +416,7 @@ See the end of the source file for the license of use.
'''
from __future__ import absolute_import
__version__ = '1.11.1'
__version__ = '1.12.0'
# yes, I now have two problems
import re
@@ -431,7 +432,7 @@ log = logging.getLogger(__name__)
def with_pattern(pattern, regex_group_count=None):
"""Attach a regular expression pattern matcher to a custom type converter
r"""Attach a regular expression pattern matcher to a custom type converter
function.
This annotates the type converter with the :attr:`pattern` attribute.
@@ -885,7 +886,7 @@ class Parser(object):
e.append(r'\{')
elif part == '}}':
e.append(r'\}')
elif part[0] == '{':
elif part[0] == '{' and part[-1] == '}':
# this will be a braces-delimited field to handle
e.append(self._handle_field(part))
else:
+1 -1
View File
@@ -75,7 +75,7 @@ if sys.platform != 'win32':
from .pty_spawn import spawn, spawnu
from .run import run, runu
__version__ = '4.6.0'
__version__ = '4.7.0'
__revision__ = ''
__all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'spawnu', 'run', 'runu',
'which', 'split_command_line', '__version__', '__revision__']
+22 -4
View File
@@ -1,5 +1,6 @@
import asyncio
import errno
import signal
from pexpect import EOF
@@ -29,6 +30,23 @@ def expect_async(expecter, timeout=None):
transport.pause_reading()
return expecter.timeout(e)
@asyncio.coroutine
def repl_run_command_async(repl, cmdlines, timeout=-1):
res = []
repl.child.sendline(cmdlines[0])
for line in cmdlines[1:]:
yield from repl._expect_prompt(timeout=timeout, async_=True)
res.append(repl.child.before)
repl.child.sendline(line)
# Command was fully submitted, now wait for the next prompt
prompt_idx = yield from repl._expect_prompt(timeout=timeout, async_=True)
if prompt_idx == 1:
# We got the continuation prompt - command was incomplete
repl.child.kill(signal.SIGINT)
yield from repl._expect_prompt(timeout=1, async_=True)
raise ValueError("Continuation prompt found - input was incomplete:")
return u''.join(res + [repl.child.before])
class PatternWaiter(asyncio.Protocol):
transport = None
@@ -41,7 +59,7 @@ class PatternWaiter(asyncio.Protocol):
if not self.fut.done():
self.fut.set_result(result)
self.transport.pause_reading()
def error(self, exc):
if not self.fut.done():
self.fut.set_exception(exc)
@@ -49,7 +67,7 @@ class PatternWaiter(asyncio.Protocol):
def connection_made(self, transport):
self.transport = transport
def data_received(self, data):
spawn = self.expecter.spawn
s = spawn._decoder.decode(data)
@@ -67,7 +85,7 @@ class PatternWaiter(asyncio.Protocol):
except Exception as e:
self.expecter.errored()
self.error(e)
def eof_received(self):
# N.B. If this gets called, async will close the pipe (the spawn object)
# for us
@@ -78,7 +96,7 @@ class PatternWaiter(asyncio.Protocol):
self.error(e)
else:
self.found(index)
def connection_lost(self, exc):
if isinstance(exc, OSError) and exc.errno == errno.EIO:
# We may get here without eof_received being called, e.g on Linux
+1 -1
View File
@@ -244,7 +244,7 @@ class searcher_re(object):
self.eof_index = -1
self.timeout_index = -1
self._searches = []
for n, s in zip(list(range(len(patterns))), patterns):
for n, s in enumerate(patterns):
if s is EOF:
self.eof_index = n
continue
+62 -40
View File
@@ -430,61 +430,83 @@ class spawn(SpawnBase):
available right away then one character will be returned immediately.
It will not wait for 30 seconds for another 99 characters to come in.
This is a wrapper around os.read(). It uses select.select() to
implement the timeout. '''
On the other hand, if there are bytes available to read immediately,
all those bytes will be read (up to the buffer size). So, if the
buffer size is 1 megabyte and there is 1 megabyte of data available
to read, the buffer will be filled, regardless of timeout.
This is a wrapper around os.read(). It uses select.select() or
select.poll() to implement the timeout. '''
if self.closed:
raise ValueError('I/O operation on closed file.')
if self.use_poll:
def select(timeout):
return poll_ignore_interrupts([self.child_fd], timeout)
else:
def select(timeout):
return select_ignore_interrupts([self.child_fd], [], [], timeout)[0]
# If there is data available to read right now, read as much as
# we can. We do this to increase performance if there are a lot
# of bytes to be read. This also avoids calling isalive() too
# often. See also:
# * https://github.com/pexpect/pexpect/pull/304
# * http://trac.sagemath.org/ticket/10295
if select(0):
try:
incoming = super(spawn, self).read_nonblocking(size)
except EOF:
# Maybe the child is dead: update some attributes in that case
self.isalive()
raise
while len(incoming) < size and select(0):
try:
incoming += super(spawn, self).read_nonblocking(size - len(incoming))
except EOF:
# Maybe the child is dead: update some attributes in that case
self.isalive()
# Don't raise EOF, just return what we read so far.
return incoming
return incoming
if timeout == -1:
timeout = self.timeout
# Note that some systems such as Solaris do not give an EOF when
# the child dies. In fact, you can still try to read
# from the child_fd -- it will block forever or until TIMEOUT.
# For this case, I test isalive() before doing any reading.
# If isalive() is false, then I pretend that this is the same as EOF.
if not self.isalive():
# timeout of 0 means "poll"
if self.use_poll:
r = poll_ignore_interrupts([self.child_fd], timeout)
else:
r, w, e = select_ignore_interrupts([self.child_fd], [], [], 0)
if not r:
self.flag_eof = True
raise EOF('End Of File (EOF). Braindead platform.')
# The process is dead, but there may or may not be data
# available to read. Note that some systems such as Solaris
# do not give an EOF when the child dies. In fact, you can
# still try to read from the child_fd -- it will block
# forever or until TIMEOUT. For that reason, it's important
# to do this check before calling select() with timeout.
if select(0):
return super(spawn, self).read_nonblocking(size)
self.flag_eof = True
raise EOF('End Of File (EOF). Braindead platform.')
elif self.__irix_hack:
# Irix takes a long time before it realizes a child was terminated.
# Make sure that the timeout is at least 2 seconds.
# FIXME So does this mean Irix systems are forced to always have
# FIXME a 2 second delay when calling read_nonblocking? That sucks.
if self.use_poll:
r = poll_ignore_interrupts([self.child_fd], timeout)
else:
r, w, e = select_ignore_interrupts([self.child_fd], [], [], 2)
if not r and not self.isalive():
self.flag_eof = True
raise EOF('End Of File (EOF). Slow platform.')
if self.use_poll:
r = poll_ignore_interrupts([self.child_fd], timeout)
else:
r, w, e = select_ignore_interrupts(
[self.child_fd], [], [], timeout
)
if timeout is not None and timeout < 2:
timeout = 2
if not r:
if not self.isalive():
# Some platforms, such as Irix, will claim that their
# processes are alive; timeout on the select; and
# then finally admit that they are not alive.
self.flag_eof = True
raise EOF('End of File (EOF). Very slow platform.')
else:
raise TIMEOUT('Timeout exceeded.')
if self.child_fd in r:
# Because of the select(0) check above, we know that no data
# is available right now. But if a non-zero timeout is given
# (possibly timeout=None), we call select() with a timeout.
if (timeout != 0) and select(timeout):
return super(spawn, self).read_nonblocking(size)
raise ExceptionPexpect('Reached an unexpected state.') # pragma: no cover
if not self.isalive():
# Some platforms, such as Irix, will claim that their
# processes are alive; timeout on the select; and
# then finally admit that they are not alive.
self.flag_eof = True
raise EOF('End of File (EOF). Very slow platform.')
else:
raise TIMEOUT('Timeout exceeded.')
def write(self, s):
'''This is similar to send() except that there is no return value.
+58 -20
View File
@@ -109,7 +109,7 @@ class pxssh (spawn):
username = raw_input('username: ')
password = getpass.getpass('password: ')
s.login (hostname, username, password)
`debug_command_string` is only for the test suite to confirm that the string
generated for SSH is correct, using this will not allow you to do
anything other than get a string back from `pxssh.pxssh.login()`.
@@ -118,12 +118,12 @@ class pxssh (spawn):
def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None,
logfile=None, cwd=None, env=None, ignore_sighup=True, echo=True,
options={}, encoding=None, codec_errors='strict',
debug_command_string=False):
debug_command_string=False, use_poll=False):
spawn.__init__(self, None, timeout=timeout, maxread=maxread,
searchwindowsize=searchwindowsize, logfile=logfile,
cwd=cwd, env=env, ignore_sighup=ignore_sighup, echo=echo,
encoding=encoding, codec_errors=codec_errors)
encoding=encoding, codec_errors=codec_errors, use_poll=use_poll)
self.name = '<pxssh>'
@@ -154,7 +154,7 @@ class pxssh (spawn):
# Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying!
#self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'"
self.force_password = False
self.debug_command_string = debug_command_string
# User defined SSH options, eg,
@@ -220,7 +220,7 @@ class pxssh (spawn):
can take 12 seconds. Low latency connections are more likely to fail
with a low sync_multiplier. Best case sync time gets worse with a
high sync multiplier (500 ms with default). '''
# All of these timing pace values are magic.
# I came up with these based on what seemed reliable for
# connecting to a heavily loaded machine I have.
@@ -253,20 +253,19 @@ class pxssh (spawn):
### TODO: This is getting messy and I'm pretty sure this isn't perfect.
### TODO: I need to draw a flow chart for this.
### TODO: Unit tests for SSH tunnels, remote SSH command exec, disabling original prompt sync
def login (self, server, username, password='', terminal_type='ansi',
def login (self, server, username=None, password='', terminal_type='ansi',
original_prompt=r"[#$]", login_timeout=10, port=None,
auto_prompt_reset=True, ssh_key=None, quiet=True,
sync_multiplier=1, check_local_ip=True,
password_regex=r'(?i)(?:password:)|(?:passphrase for key)',
ssh_tunnels={}, spawn_local_ssh=True,
sync_original_prompt=True, ssh_config=None):
sync_original_prompt=True, ssh_config=None, cmd='ssh'):
'''This logs the user into the given server.
It uses
'original_prompt' to try to find the prompt right after login. When it
finds the prompt it immediately tries to reset the prompt to something
more easily matched. The default 'original_prompt' is very optimistic
and is easily fooled. It's more reliable to try to match the original
It uses 'original_prompt' to try to find the prompt right after login.
When it finds the prompt it immediately tries to reset the prompt to
something more easily matched. The default 'original_prompt' is very
optimistic and is easily fooled. It's more reliable to try to match the original
prompt as exactly as possible to prevent false matches by server
strings such as the "Message Of The Day". On many systems you can
disable the MOTD on the remote server by creating a zero-length file
@@ -284,27 +283,31 @@ class pxssh (spawn):
uses a unique prompt in the :meth:`prompt` method. If the original prompt is
not reset then this will disable the :meth:`prompt` method unless you
manually set the :attr:`PROMPT` attribute.
Set ``password_regex`` if there is a MOTD message with `password` in it.
Changing this is like playing in traffic, don't (p)expect it to match straight
away.
If you require to connect to another SSH server from the your original SSH
connection set ``spawn_local_ssh`` to `False` and this will use your current
session to do so. Setting this option to `False` and not having an active session
will trigger an error.
Set ``ssh_key`` to a file path to an SSH private key to use that SSH key
for the session authentication.
Set ``ssh_key`` to `True` to force passing the current SSH authentication socket
to the desired ``hostname``.
Set ``ssh_config`` to a file path string of an SSH client config file to pass that
file to the client to handle itself. You may set any options you wish in here, however
doing so will require you to post extra information that you may not want to if you
run into issues.
Alter the ``cmd`` to change the ssh client used, or to prepend it with network
namespaces. For example ```cmd="ip netns exec vlan2 ssh"``` to execute the ssh in
network namespace named ```vlan```.
'''
session_regex_array = ["(?i)are you sure you want to continue connecting", original_prompt, password_regex, "(?i)permission denied", "(?i)terminal type", TIMEOUT]
session_init_regex_array = []
session_init_regex_array.extend(session_regex_array)
@@ -320,7 +323,7 @@ class pxssh (spawn):
if ssh_config is not None:
if spawn_local_ssh and not os.path.isfile(ssh_config):
raise ExceptionPxssh('SSH config does not exist or is not a file.')
ssh_options = ssh_options + '-F ' + ssh_config
ssh_options = ssh_options + ' -F ' + ssh_config
if port is not None:
ssh_options = ssh_options + ' -p %s'%(str(port))
if ssh_key is not None:
@@ -331,7 +334,7 @@ class pxssh (spawn):
if spawn_local_ssh and not os.path.isfile(ssh_key):
raise ExceptionPxssh('private ssh key does not exist or is not a file.')
ssh_options = ssh_options + ' -i %s' % (ssh_key)
# SSH tunnels, make sure you know what you're putting into the lists
# under each heading. Do not expect these to open 100% of the time,
# The port you're requesting might be bound.
@@ -354,7 +357,42 @@ class pxssh (spawn):
if spawn_local_ssh==False:
tunnel = quote(str(tunnel))
ssh_options = ssh_options + ' -' + cmd_type + ' ' + str(tunnel)
cmd = "ssh %s -l %s %s" % (ssh_options, username, server)
if username is not None:
ssh_options = ssh_options + ' -l ' + username
elif ssh_config is None:
raise TypeError('login() needs either a username or an ssh_config')
else: # make sure ssh_config has an entry for the server with a username
with open(ssh_config, 'rt') as f:
lines = [l.strip() for l in f.readlines()]
server_regex = r'^Host\s+%s\s*$' % server
user_regex = r'^User\s+\w+\s*$'
config_has_server = False
server_has_username = False
for line in lines:
if not config_has_server and re.match(server_regex, line, re.IGNORECASE):
config_has_server = True
elif config_has_server and 'hostname' in line.lower():
pass
elif config_has_server and 'host' in line.lower():
server_has_username = False # insurance
break # we have left the relevant section
elif config_has_server and re.match(user_regex, line, re.IGNORECASE):
server_has_username = True
break
if lines:
del line
del lines
if not config_has_server:
raise TypeError('login() ssh_config has no Host entry for %s' % server)
elif not server_has_username:
raise TypeError('login() ssh_config has no user entry for %s' % server)
cmd += " %s %s" % (ssh_options, server)
if self.debug_command_string:
return(cmd)
+11 -3
View File
@@ -61,11 +61,11 @@ class REPLWrapper(object):
self.child.expect(orig_prompt)
self.child.sendline(prompt_change)
def _expect_prompt(self, timeout=-1):
def _expect_prompt(self, timeout=-1, async_=False):
return self.child.expect_exact([self.prompt, self.continuation_prompt],
timeout=timeout)
timeout=timeout, async_=async_)
def run_command(self, command, timeout=-1):
def run_command(self, command, timeout=-1, async_=False):
"""Send a command to the REPL, wait for and return output.
:param str command: The command to send. Trailing newlines are not needed.
@@ -75,6 +75,10 @@ class REPLWrapper(object):
:param int timeout: How long to wait for the next prompt. -1 means the
default from the :class:`pexpect.spawn` object (default 30 seconds).
None means to wait indefinitely.
:param bool async_: On Python 3.4, or Python 3.3 with asyncio
installed, passing ``async_=True`` will make this return an
:mod:`asyncio` Future, which you can yield from to get the same
result that this method would normally give directly.
"""
# Split up multiline commands and feed them in bit-by-bit
cmdlines = command.splitlines()
@@ -84,6 +88,10 @@ class REPLWrapper(object):
if not cmdlines:
raise ValueError("No command was given")
if async_:
from ._async import repl_run_command_async
return repl_run_command_async(self, cmdlines, timeout)
res = []
self.child.sendline(cmdlines[0])
for line in cmdlines[1:]:
+1 -1
View File
@@ -10,7 +10,7 @@ from .exceptions import InvalidPythonVersion
from .models import SystemPath, WindowsFinder
from .pythonfinder import Finder
__version__ = "1.2.0"
__version__ = "1.2.1"
logger = logging.getLogger(__name__)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Steve Dower
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -172,7 +172,7 @@ class RegistryAccessor(object):
items = info.items()
else:
raise TypeError('info must be a dictionary')
self._set_all_values(self._root, self.subkey, items, errors)
if len(errors) == 1:
raise ValueError(errors[0])
+1 -1
View File
@@ -1 +1 @@
-e git+https://github.com/zooba/pep514tools.git@320e48745660b696e2dcaee888fc2e516b435e48#egg=pep514tools
git+https://github.com/zooba/pep514tools.git@master#egg=pep514tools
+5
View File
@@ -35,6 +35,11 @@ else:
IGNORE_UNSUPPORTED = bool(os.environ.get("PYTHONFINDER_IGNORE_UNSUPPORTED", False))
MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
SUBPROCESS_TIMEOUT = os.environ.get("PYTHONFINDER_SUBPROCESS_TIMEOUT", 5)
"""The default subprocess timeout for determining python versions
Set to **5** by default.
"""
def get_shim_paths():
+5 -4
View File
@@ -89,6 +89,9 @@ class BasePath(object):
self._children = {}
for key in list(self._pythons.keys()):
del self._pythons[key]
self._pythons = None
self._py_version = None
self.path = None
@property
def children(self):
@@ -315,10 +318,8 @@ class BaseFinder(object):
raise NotImplementedError
@classmethod
def create(
cls, *args, **kwargs # type: Type[BaseFinderType] # type: Any # type: Any
):
# type: (...) -> BaseFinderType
def create(cls, *args, **kwargs):
# type: (Any, Any) -> BaseFinderType
raise NotImplementedError
@property
+183 -83
View File
@@ -12,6 +12,7 @@ import attr
import six
from cached_property import cached_property
from vistir.compat import Path, fs_str
from vistir.misc import dedup
from .mixins import BaseFinder, BasePath
from .python import PythonVersion
@@ -22,6 +23,7 @@ from ..environment import (
PYENV_INSTALLED,
PYENV_ROOT,
SHIM_PATHS,
get_shim_paths,
)
from ..exceptions import InvalidPythonVersion
from ..utils import (
@@ -37,6 +39,7 @@ from ..utils import (
parse_asdf_version_order,
parse_pyenv_version_order,
path_is_known_executable,
split_version_and_name,
unnest,
)
@@ -76,10 +79,9 @@ class SystemPath(object):
path_order = attr.ib(default=attr.Factory(list)) # type: List[str]
python_version_dict = attr.ib() # type: DefaultDict[Tuple, List[PythonVersion]]
only_python = attr.ib(default=False, type=bool)
pyenv_finder = attr.ib(
default=None, validator=optional_instance_of("PythonFinder")
) # type: Optional[PythonFinder]
pyenv_finder = attr.ib(default=None) # type: Optional[PythonFinder]
asdf_finder = attr.ib(default=None) # type: Optional[PythonFinder]
windows_finder = attr.ib(default=None) # type: Optional[WindowsFinder]
system = attr.ib(default=False, type=bool)
_version_dict = attr.ib(
default=attr.Factory(defaultdict)
@@ -91,31 +93,63 @@ class SystemPath(object):
) # type: Dict[str, Union[WindowsFinder, PythonFinder]]
def _register_finder(self, finder_name, finder):
# type: (str, Union[WindowsFinder, PythonFinder]) -> None
# type: (str, Union[WindowsFinder, PythonFinder]) -> "SystemPath"
if finder_name not in self.__finders:
self.__finders[finder_name] = finder
return self
def clear_caches(self):
for key in ["executables", "python_executables", "version_dict", "path_entries"]:
if key in self.__dict__:
del self.__dict__[key]
self._executables = []
self._python_executables = {}
self.python_version_dict = defaultdict(list)
self._version_dict = defaultdict(list)
for finder in list(self.__finders.keys()):
del self.__finders[finder]
self.__finders = {}
return attr.evolve(
self,
executables=[],
python_executables={},
python_version_dict=defaultdict(list),
version_dict=defaultdict(list),
pyenv_finder=None,
windows_finder=None,
asdf_finder=None,
path_order=[],
paths=defaultdict(PathEntry),
)
def __del__(self):
self.clear_caches()
for key in ["executables", "python_executables", "version_dict", "path_entries"]:
try:
del self.__dict__[key]
except KeyError:
pass
for finder in list(self.__finders.keys()):
del self.__finders[finder]
self.__finders = {}
self._python_executables = {}
self._executables = []
self.python_version_dict = defaultdict(list)
self.version_dict = defaultdict(list)
self.path_order = []
self.pyenv_finder = None
self.asdf_finder = None
self.paths = defaultdict(PathEntry)
self.__finders = {}
@property
def finders(self):
# type: () -> List[str]
return [k for k in self.__finders.keys()]
@staticmethod
def check_for_pyenv():
return PYENV_INSTALLED or os.path.exists(normalize_path(PYENV_ROOT))
@staticmethod
def check_for_asdf():
return ASDF_INSTALLED or os.path.exists(normalize_path(ASDF_DATA_DIR))
@python_version_dict.default
def create_python_version_dict(self):
# type: () -> DefaultDict[Tuple, List[PythonVersion]]
@@ -168,35 +202,69 @@ class SystemPath(object):
self._version_dict[version].append(entry)
return self._version_dict
def __attrs_post_init__(self):
# type: () -> None
#: slice in pyenv
def _run_setup(self):
# type: () -> "SystemPath"
if not self.__class__ == SystemPath:
return
if os.name == "nt":
self._setup_windows()
if PYENV_INSTALLED:
self._setup_pyenv()
if ASDF_INSTALLED:
self._setup_asdf()
return self
new_instance = self
path_order = new_instance.path_order[:]
path_entries = self.paths.copy()
if self.global_search and "PATH" in os.environ:
path_order = path_order + os.environ["PATH"].split(os.pathsep)
path_order = list(dedup(path_order))
path_instances = [
ensure_path(p.strip('"'))
for p in path_order
if not any(
is_in_path(normalize_path(str(p)), normalize_path(shim))
for shim in SHIM_PATHS
)
]
path_entries.update(
{
p.as_posix(): PathEntry.create(
path=p.absolute(), is_root=True, only_python=self.only_python
)
for p in path_instances
}
)
new_instance = attr.evolve(
new_instance,
path_order=[p.as_posix() for p in path_instances],
paths=path_entries,
)
if os.name == "nt" and "windows" not in self.finders:
new_instance = new_instance._setup_windows()
#: slice in pyenv
if self.check_for_pyenv() and "pyenv" not in self.finders:
new_instance = new_instance._setup_pyenv()
#: slice in asdf
if self.check_for_asdf() and "asdf" not in self.finders:
new_instance = new_instance._setup_asdf()
venv = os.environ.get("VIRTUAL_ENV")
if os.name == "nt":
bin_dir = "Scripts"
else:
bin_dir = "bin"
if venv and (self.system or self.global_search):
if venv and (new_instance.system or new_instance.global_search):
p = ensure_path(venv)
self.path_order = [(p / bin_dir).as_posix()] + self.path_order
self.paths[p] = self.get_path(p.joinpath(bin_dir))
if self.system:
path_order = [(p / bin_dir).as_posix()] + new_instance.path_order
new_instance = attr.evolve(new_instance, path_order=path_order)
paths = new_instance.paths.copy()
paths[p] = new_instance.get_path(p.joinpath(bin_dir))
new_instance = attr.evolve(new_instance, paths=paths)
if new_instance.system:
syspath = Path(sys.executable)
syspath_bin = syspath.parent
if syspath_bin.name != bin_dir and syspath_bin.joinpath(bin_dir).exists():
syspath_bin = syspath_bin / bin_dir
self.path_order = [syspath_bin.as_posix()] + self.path_order
self.paths[syspath_bin] = PathEntry.create(
path_order = [syspath_bin.as_posix()] + new_instance.path_order
paths = new_instance.paths.copy()
paths[syspath_bin] = PathEntry.create(
path=syspath_bin, is_root=True, only_python=False
)
new_instance = attr.evolve(new_instance, path_order=path_order, paths=paths)
return new_instance
def _get_last_instance(self, path):
# type: (str) -> int
@@ -210,7 +278,7 @@ class SystemPath(object):
return path_index
def _slice_in_paths(self, start_idx, paths):
# type: (int, List[Path]) -> None
# type: (int, List[Path]) -> "SystemPath"
before_path = [] # type: List[str]
after_path = [] # type: List[str]
if start_idx == 0:
@@ -220,29 +288,35 @@ class SystemPath(object):
else:
before_path = self.path_order[: start_idx + 1]
after_path = self.path_order[start_idx + 2 :]
self.path_order = before_path + [p.as_posix() for p in paths] + after_path
path_order = before_path + [p.as_posix() for p in paths] + after_path
if path_order == self.path_order:
return self
return attr.evolve(self, path_order=path_order)
def _remove_path(self, path):
# type: (str) -> None
# type: (str) -> "SystemPath"
path_copy = [p for p in reversed(self.path_order[:])]
new_order = []
target = normalize_path(path)
path_map = {normalize_path(pth): pth for pth in self.paths.keys()}
new_paths = self.paths.copy()
if target in path_map:
del self.paths[path_map[target]]
del new_paths[path_map[target]]
for current_path in path_copy:
normalized = normalize_path(current_path)
if normalized != target:
new_order.append(normalized)
new_order = [p for p in reversed(new_order)]
self.path_order = new_order
return attr.evolve(self, path_order=new_order, paths=new_paths)
def _setup_asdf(self):
# type: () -> None
# type: () -> "SystemPath"
if "asdf" in self.finders and self.asdf_finder is not None:
return self
from .python import PythonFinder
os_path = os.environ["PATH"].split(os.pathsep)
self.asdf_finder = PythonFinder.create(
asdf_finder = PythonFinder.create(
root=ASDF_DATA_DIR,
ignore_unsupported=True,
sort_function=parse_asdf_version_order,
@@ -252,20 +326,24 @@ class SystemPath(object):
try:
asdf_index = self._get_last_instance(ASDF_DATA_DIR)
except ValueError:
pyenv_index = 0 if is_in_path(next(iter(os_path), ""), PYENV_ROOT) else -1
asdf_index = 0 if is_in_path(next(iter(os_path), ""), ASDF_DATA_DIR) else -1
if asdf_index is None:
# we are in a virtualenv without global pyenv on the path, so we should
# not write pyenv to the path here
return
root_paths = [p for p in self.asdf_finder.roots]
self._slice_in_paths(asdf_index, [self.asdf_finder.root])
self.paths[self.asdf_finder.root] = self.asdf_finder
self.paths.update(self.asdf_finder.roots)
self._remove_path(normalize_path(os.path.join(ASDF_DATA_DIR, "shims")))
self._register_finder("asdf", self.asdf_finder)
return self
root_paths = [p for p in asdf_finder.roots]
new_instance = self._slice_in_paths(asdf_index, [asdf_finder.root])
paths = self.paths.copy()
paths[asdf_finder.root] = asdf_finder
paths.update(asdf_finder.roots)
return (
attr.evolve(new_instance, paths=paths, asdf_finder=asdf_finder)
._remove_path(normalize_path(os.path.join(ASDF_DATA_DIR, "shims")))
._register_finder("asdf", asdf_finder)
)
def reload_finder(self, finder_name):
# type: (str) -> None
# type: (str) -> "SystemPath"
if finder_name is None:
raise TypeError("Must pass a string as the name of the target finder")
finder_attr = "{0}_finder".format(finder_name)
@@ -286,19 +364,21 @@ class SystemPath(object):
finder_name == "asdf" and not ASDF_INSTALLED
):
# Don't allow loading of finders that aren't explicitly 'installed' as it were
pass
return self
setattr(self, finder_attr, None)
if finder_name in self.__finders:
del self.__finders[finder_name]
setup_fn()
return setup_fn()
def _setup_pyenv(self):
# type: () -> None
# type: () -> "SystemPath"
if "pyenv" in self.finders and self.pyenv_finder is not None:
return self
from .python import PythonFinder
os_path = os.environ["PATH"].split(os.pathsep)
self.pyenv_finder = PythonFinder.create(
pyenv_finder = PythonFinder.create(
root=PYENV_ROOT,
sort_function=parse_pyenv_version_order,
version_glob_path="versions/*",
@@ -312,25 +392,37 @@ class SystemPath(object):
if pyenv_index is None:
# we are in a virtualenv without global pyenv on the path, so we should
# not write pyenv to the path here
return
return self
root_paths = [p for p in self.pyenv_finder.roots]
self._slice_in_paths(pyenv_index, [self.pyenv_finder.root])
self.paths[self.pyenv_finder.root] = self.pyenv_finder
self.paths.update(self.pyenv_finder.roots)
self._remove_path(os.path.join(PYENV_ROOT, "shims"))
self._register_finder("pyenv", self.pyenv_finder)
root_paths = [p for p in pyenv_finder.roots]
new_instance = self._slice_in_paths(pyenv_index, [pyenv_finder.root])
paths = new_instance.paths.copy()
paths[pyenv_finder.root] = pyenv_finder
paths.update(pyenv_finder.roots)
return (
attr.evolve(new_instance, paths=paths, pyenv_finder=pyenv_finder)
._remove_path(os.path.join(PYENV_ROOT, "shims"))
._register_finder("pyenv", pyenv_finder)
)
def _setup_windows(self):
# type: () -> None
# type: () -> "SystemPath"
if "windows" in self.finders and self.windows_finder is not None:
return self
from .windows import WindowsFinder
self.windows_finder = WindowsFinder.create()
root_paths = (p for p in self.windows_finder.paths if p.is_root)
windows_finder = WindowsFinder.create()
root_paths = (p for p in windows_finder.paths if p.is_root)
path_addition = [p.path.as_posix() for p in root_paths]
self.path_order = self.path_order[:] + path_addition
self.paths.update({p.path: p for p in root_paths})
self._register_finder("windows", self.windows_finder)
new_path_order = self.path_order[:] + path_addition
new_paths = self.paths.copy()
new_paths.update({p.path: p for p in root_paths})
return attr.evolve(
self,
windows_finder=windows_finder,
path_order=new_path_order,
paths=new_paths,
)._register_finder("windows", windows_finder)
def get_path(self, path):
# type: (Union[str, Path]) -> PathType
@@ -350,7 +442,7 @@ class SystemPath(object):
return _path
def _get_paths(self):
# type: () -> Iterator
# type: () -> Generator[Union[PathType, WindowsFinder], None, None]
for path in self.path_order:
try:
entry = self.get_path(path)
@@ -361,7 +453,7 @@ class SystemPath(object):
@cached_property
def path_entries(self):
# type: () -> List[Union[PathEntry, FinderType]]
# type: () -> List[Union[PathType, WindowsFinder]]
paths = list(self._get_paths())
return paths
@@ -469,6 +561,7 @@ class SystemPath(object):
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
sort_by_path=False, # type: bool
):
# type: (...) -> PathEntry
"""Search for a specific python version on the path.
@@ -481,29 +574,12 @@ class SystemPath(object):
:param bool dev: Search for devreleases (default None) - prioritize releases if None
:param str arch: Architecture to include, e.g. '64bit', defaults to None
:param str name: The name of a python version, e.g. ``anaconda3-5.3.0``
:param bool sort_by_path: Whether to sort by path -- default sort is by version(default: False)
:return: A :class:`~pythonfinder.models.PathEntry` instance matching the version requested.
:rtype: :class:`~pythonfinder.models.PathEntry`
"""
if isinstance(major, six.string_types) and not minor and not patch:
# Only proceed if this is in the format "x.y.z" or similar
if major.isdigit() or (major.count(".") > 0 and major[0].isdigit()):
version = major.split(".", 2)
if isinstance(version, (tuple, list)):
if len(version) > 3:
major, minor, patch, rest = version
elif len(version) == 3:
major, minor, patch = version
elif len(version) == 2:
major, minor = version
else:
major = major[0]
else:
major = major
name = None
else:
name = "{0!s}".format(major)
major = None
major, minor, patch, name = split_version_and_name(major, minor, patch, name)
sub_finder = operator.methodcaller(
"find_python_version", major, minor, patch, pre, dev, arch, name
)
@@ -521,6 +597,18 @@ class SystemPath(object):
windows_finder_version = sub_finder(self.windows_finder)
if windows_finder_version:
return windows_finder_version
if sort_by_path:
paths = [self.get_path(k) for k in self.path_order]
for path in paths:
found_version = sub_finder(path)
if found_version:
return found_version
if alternate_sub_finder:
for path in paths:
found_version = alternate_sub_finder(path)
if found_version:
return found_version
ver = next(iter(self.get_pythons(sub_finder)), None)
if not ver and alternate_sub_finder is not None:
ver = next(iter(self.get_pythons(alternate_sub_finder)), None)
@@ -561,7 +649,19 @@ class SystemPath(object):
if global_search:
if "PATH" in os.environ:
paths = os.environ["PATH"].split(os.pathsep)
path_order = []
if path:
path_order = [path]
path_instance = ensure_path(path)
path_entries.update(
{
path_instance.as_posix(): PathEntry.create(
path=path_instance.absolute(),
is_root=True,
only_python=only_python,
)
}
)
paths = [path] + paths
paths = [p for p in paths if not any(is_in_path(p, shim) for shim in SHIM_PATHS)]
_path_objects = [ensure_path(p.strip('"')) for p in paths]
@@ -574,14 +674,16 @@ class SystemPath(object):
for p in _path_objects
}
)
return cls(
instance = cls(
paths=path_entries,
path_order=paths,
path_order=path_order,
only_python=only_python,
system=system,
global_search=global_search,
ignore_unsupported=ignore_unsupported,
)
instance = instance._run_setup()
return instance
@attr.s(slots=True)
@@ -603,8 +705,6 @@ class PathEntry(BasePath):
def _gen_children(self):
# type: () -> Iterator
from ..environment import get_shim_paths
shim_paths = get_shim_paths()
pass_name = self.name != self.path.name
pass_args = {"is_root": False, "only_python": self.only_python}
+4 -4
View File
@@ -229,10 +229,8 @@ class PythonFinder(BaseFinder, BasePath):
return self.pythons
@classmethod
def create(
cls, root, sort_function, version_glob_path=None, ignore_unsupported=True
): # type: ignore
# type: (Type[PythonFinder], str, Callable, Optional[str], bool) -> PythonFinder
def create(cls, root, sort_function, version_glob_path=None, ignore_unsupported=True):
# type: (str, Callable, Optional[str], bool) -> PythonFinder
root = ensure_path(root)
if not version_glob_path:
version_glob_path = "versions/*"
@@ -593,6 +591,8 @@ class PythonVersion(object):
raise TypeError("Must pass a valid path to parse.")
if not isinstance(path, six.string_types):
path = path.as_posix()
# if not looks_like_python(path):
# raise ValueError("Path %r does not look like a valid python path" % path)
try:
result_version = get_python_version(path)
except Exception:
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Steve Dower
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+129 -64
View File
@@ -1,6 +1,7 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import importlib
import operator
import os
@@ -10,15 +11,16 @@ from vistir.compat import lru_cache
from . import environment
from .exceptions import InvalidPythonVersion
from .models import path as pyfinder_path
from .utils import Iterable, filter_pythons, version_re
if environment.MYPY_RUNNING:
from typing import Optional, Dict, Any, Union, List, Iterator
from typing import Optional, Dict, Any, Union, List, Iterator, Text
from .models.path import Path, PathEntry
from .models.windows import WindowsFinder
from .models.path import SystemPath
STRING_TYPE = Union[str, Text, bytes]
class Finder(object):
@@ -33,9 +35,14 @@ class Finder(object):
"""
def __init__(
self, path=None, system=False, global_search=True, ignore_unsupported=True
self,
path=None,
system=False,
global_search=True,
ignore_unsupported=True,
sort_by_path=False,
):
# type: (Optional[str], bool, bool, bool) -> None
# type: (Optional[str], bool, bool, bool, bool) -> None
"""Create a new :class:`~pythonfinder.pythonfinder.Finder` instance.
:param path: A bin-directory search location, defaults to None
@@ -46,12 +53,14 @@ class Finder(object):
:param global_search: bool, optional
:param ignore_unsupported: Whether to ignore unsupported python versions, if False, an error is raised, defaults to True
:param ignore_unsupported: bool, optional
:param bool sort_by_path: Whether to always sort by path
:returns: a :class:`~pythonfinder.pythonfinder.Finder` object.
"""
self.path_prepend = path # type: Optional[str]
self.global_search = global_search # type: bool
self.system = system # type: bool
self.sort_by_path = sort_by_path # type: bool
self.ignore_unsupported = ignore_unsupported # type: bool
self._system_path = None # type: Optional[SystemPath]
self._windows_finder = None # type: Optional[WindowsFinder]
@@ -68,6 +77,7 @@ class Finder(object):
def create_system_path(self):
# type: () -> SystemPath
pyfinder_path = importlib.import_module("pythonfinder.models.path")
return pyfinder_path.SystemPath.create(
path=self.path_prepend,
system=self.system,
@@ -84,19 +94,23 @@ class Finder(object):
"""
if self._system_path is not None:
self._system_path.clear_caches()
self._system_path = None
self._system_path = self._system_path.clear_caches()
self._system_path = None
pyfinder_path = importlib.import_module("pythonfinder.models.path")
six.moves.reload_module(pyfinder_path)
self._system_path = self.create_system_path()
def rehash(self):
# type: () -> None
# type: () -> "Finder"
if not self._system_path:
self._system_path = self.create_system_path()
self.find_all_python_versions.cache_clear()
self.find_python_version.cache_clear()
self.reload_system_path()
if self._windows_finder is not None:
self._windows_finder = None
filter_pythons.cache_clear()
self.reload_system_path()
return self
@property
def system_path(self):
@@ -118,11 +132,92 @@ class Finder(object):
# type: (str) -> Optional[PathEntry]
return self.system_path.which(exe)
@classmethod
def parse_major(cls, major, minor=None, patch=None, pre=None, dev=None, arch=None):
# type: (Optional[str], Optional[int], Optional[int], Optional[bool], Optional[bool], Optional[str]) -> Dict[str, Union[int, str, bool, None]]
from .models import PythonVersion
major_is_str = major and isinstance(major, six.string_types)
is_num = (
major
and major_is_str
and all(part.isdigit() for part in major.split(".")[:2])
)
major_has_arch = (
arch is None
and major
and major_is_str
and "-" in major
and major[0].isdigit()
)
name = None
if major and major_has_arch:
orig_string = "{0!s}".format(major)
major, _, arch = major.rpartition("-")
if arch:
arch = arch.lower().lstrip("x").replace("bit", "")
if not (arch.isdigit() and (int(arch) & int(arch) - 1) == 0):
major = orig_string
arch = None
else:
arch = "{0}bit".format(arch)
try:
version_dict = PythonVersion.parse(major)
except (ValueError, InvalidPythonVersion):
if name is None:
name = "{0!s}".format(major)
major = None
version_dict = {}
elif major and major[0].isalpha():
return {"major": None, "name": major, "arch": arch}
elif major and is_num:
match = version_re.match(major)
version_dict = match.groupdict() if match else {} # type: ignore
version_dict.update(
{
"is_prerelease": bool(version_dict.get("prerel", False)),
"is_devrelease": bool(version_dict.get("dev", False)),
}
)
else:
version_dict = {
"major": major,
"minor": minor,
"patch": patch,
"pre": pre,
"dev": dev,
"arch": arch,
}
if not version_dict.get("arch") and arch:
version_dict["arch"] = arch
version_dict["minor"] = (
int(version_dict["minor"]) if version_dict.get("minor") is not None else minor
)
version_dict["patch"] = (
int(version_dict["patch"]) if version_dict.get("patch") is not None else patch
)
version_dict["major"] = (
int(version_dict["major"]) if version_dict.get("major") is not None else major
)
if not (version_dict["major"] or version_dict.get("name")):
version_dict["major"] = major
if name:
version_dict["name"] = name
return version_dict
@lru_cache(maxsize=1024)
def find_python_version(
self, major=None, minor=None, patch=None, pre=None, dev=None, arch=None, name=None
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
sort_by_path=False, # type: bool
):
# type: (Optional[Union[str, int]], Optional[int], Optional[int], Optional[bool], Optional[bool], Optional[str], Optional[str]) -> PathEntry
# type: (...) -> Optional[PathEntry]
"""
Find the python version which corresponds most closely to the version requested.
@@ -133,18 +228,19 @@ class Finder(object):
:param Optional[bool] dev: If provided, whether to search dev-releases.
:param Optional[str] arch: If provided, which architecture to search.
:param Optional[str] name: *Name* of the target python, e.g. ``anaconda3-5.3.0``
:param bool sort_by_path: Whether to sort by path -- default sort is by version(default: False)
:return: A new *PathEntry* pointer at a matching python version, if one can be located.
:rtype: :class:`pythonfinder.models.path.PathEntry`
"""
from .models import PythonVersion
minor = int(minor) if minor is not None else minor
patch = int(patch) if patch is not None else patch
version_dict = {
"minor": minor,
"patch": patch,
"name": name,
"arch": arch,
} # type: Dict[str, Union[str, int, Any]]
if (
@@ -154,60 +250,22 @@ class Finder(object):
and dev is None
and patch is None
):
if arch is None and "-" in major and major[0].isdigit():
orig_string = "{0!s}".format(major)
major, _, arch = major.rpartition("-")
if arch.startswith("x"):
arch = arch.lstrip("x")
if arch.lower().endswith("bit"):
arch = arch.lower().replace("bit", "")
if not (arch.isdigit() and (int(arch) & int(arch) - 1) == 0):
major = orig_string
arch = None
else:
arch = "{0}bit".format(arch)
try:
version_dict = PythonVersion.parse(major)
except (ValueError, InvalidPythonVersion):
if name is None:
name = "{0!s}".format(major)
major = None
version_dict = {}
elif major[0].isalpha():
name = "%s" % major
major = None
else:
if "." in major and all(part.isdigit() for part in major.split(".")[:2]):
match = version_re.match(major)
version_dict = match.groupdict()
version_dict["is_prerelease"] = bool(
version_dict.get("prerel", False)
)
version_dict["is_devrelease"] = bool(version_dict.get("dev", False))
else:
version_dict = {
"major": major,
"minor": minor,
"patch": patch,
"pre": pre,
"dev": dev,
"arch": arch,
}
if version_dict.get("minor") is not None:
minor = int(version_dict["minor"])
if version_dict.get("patch") is not None:
patch = int(version_dict["patch"])
if version_dict.get("major") is not None:
major = int(version_dict["major"])
version_dict = self.parse_major(major, minor=minor, patch=patch, arch=arch)
major = version_dict["major"]
minor = version_dict.get("minor", minor) # type: ignore
patch = version_dict.get("patch", patch) # type: ignore
arch = version_dict.get("arch", arch) # type: ignore
name = version_dict.get("name", name) # type: ignore
_pre = version_dict.get("is_prerelease", pre)
pre = bool(_pre) if _pre is not None else pre
_dev = version_dict.get("is_devrelease", dev)
dev = bool(_dev) if _dev is not None else dev
arch = (
version_dict.get("architecture", None) if arch is None else arch
) # type: ignore
if "architecture" in version_dict and isinstance(
version_dict["architecture"], six.string_types
):
arch = version_dict["architecture"] # type: ignore
if os.name == "nt" and self.windows_finder is not None:
match = self.windows_finder.find_python_version(
found = self.windows_finder.find_python_version(
major=major,
minor=minor,
patch=patch,
@@ -216,10 +274,17 @@ class Finder(object):
arch=arch,
name=name,
)
if match:
return match
if found:
return found
return self.system_path.find_python_version(
major=major, minor=minor, patch=patch, pre=pre, dev=dev, arch=arch, name=name
major=major,
minor=minor,
patch=patch,
pre=pre,
dev=dev,
arch=arch,
name=name,
sort_by_path=self.sort_by_path,
)
@lru_cache(maxsize=1024)
+49 -12
View File
@@ -6,13 +6,14 @@ import itertools
import os
import re
from fnmatch import fnmatch
from threading import Timer
import attr
import six
import vistir
from packaging.version import LegacyVersion, Version
from .environment import MYPY_RUNNING, PYENV_ROOT
from .environment import MYPY_RUNNING, PYENV_ROOT, SUBPROCESS_TIMEOUT
from .exceptions import InvalidPythonVersion
six.add_move(
@@ -37,11 +38,12 @@ if MYPY_RUNNING:
from .models.path import PathEntry
version_re = re.compile(
version_re_str = (
r"(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>(?<=\.)[0-9]+))?\.?"
r"(?:(?P<prerel>[abc]|rc|dev)(?:(?P<prerelversion>\d+(?:\.\d+)*))?)"
r"?(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?"
)
version_re = re.compile(version_re_str)
PYTHON_IMPLEMENTATIONS = (
@@ -53,13 +55,19 @@ PYTHON_IMPLEMENTATIONS = (
"miniconda",
"stackless",
"activepython",
"pyston",
"micropython",
)
RE_MATCHER = re.compile(
r"(({0})(?:\d?(?:\.\d[cpm]{{0,3}}))?(?:-?[\d\.]+)*[^z])".format(
"|".join(PYTHON_IMPLEMENTATIONS)
)
KNOWN_EXTS = {"exe", "py", "fish", "sh", ""}
KNOWN_EXTS = KNOWN_EXTS | set(
filter(None, os.environ.get("PATHEXT", "").split(os.pathsep))
)
PY_MATCH_STR = r"((?P<implementation>{0})(?:\d?(?:\.\d[cpm]{{0,3}}))?(?:-?[\d\.]+)*[^z])".format(
"|".join(PYTHON_IMPLEMENTATIONS)
)
EXE_MATCH_STR = r"{0}(?:\.(?P<ext>{1}))?".format(PY_MATCH_STR, "|".join(KNOWN_EXTS))
RE_MATCHER = re.compile(r"({0}|{1})".format(version_re_str, PY_MATCH_STR))
EXE_MATCHER = re.compile(EXE_MATCH_STR)
RULES_BASE = [
"*{0}",
"*{0}?",
@@ -71,11 +79,6 @@ RULES_BASE = [
]
RULES = [rule.format(impl) for impl in PYTHON_IMPLEMENTATIONS for rule in RULES_BASE]
KNOWN_EXTS = {"exe", "py", "fish", "sh", ""}
KNOWN_EXTS = KNOWN_EXTS | set(
filter(None, os.environ.get("PATHEXT", "").split(os.pathsep))
)
MATCH_RULES = []
for rule in RULES:
MATCH_RULES.extend(
@@ -87,7 +90,11 @@ for rule in RULES:
def get_python_version(path):
# type: (str) -> str
"""Get python version string using subprocess from a given path."""
version_cmd = [path, "-c", "import sys; print(sys.version.split()[0])"]
version_cmd = [
path,
"-c",
"import sys; print('.'.join([str(i) for i in sys.version_info[:3]]))",
]
try:
c = vistir.misc.run(
version_cmd,
@@ -97,6 +104,7 @@ def get_python_version(path):
combine_stderr=False,
write_to_stdout=False,
)
timer = Timer(SUBPROCESS_TIMEOUT, c.kill)
except OSError:
raise InvalidPythonVersion("%s is not a valid python path" % path)
if not c.out:
@@ -326,6 +334,35 @@ def parse_asdf_version_order(filename=".tool-versions"):
return []
def split_version_and_name(
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[Union[str, int]]
patch=None, # type: Optional[Union[str, int]]
name=None, # type: Optional[str]
):
# type: (...) -> Tuple[Optional[Union[str, int]], Optional[Union[str, int]], Optional[Union[str, int]], Optional[str]]
if isinstance(major, six.string_types) and not minor and not patch:
# Only proceed if this is in the format "x.y.z" or similar
if major.isdigit() or (major.count(".") > 0 and major[0].isdigit()):
version = major.split(".", 2)
if isinstance(version, (tuple, list)):
if len(version) > 3:
major, minor, patch, _ = version
elif len(version) == 3:
major, minor, patch = version
elif len(version) == 2:
major, minor = version
else:
major = major[0]
else:
major = major
name = None
else:
name = "{0!s}".format(major)
major = None
return (major, minor, patch, name)
# TODO: Reimplement in vistir
def is_in_path(path, parent):
return normalize_path(str(path)).startswith(normalize_path(str(parent)))
+2 -2
View File
@@ -57,10 +57,10 @@ def check_compatibility(urllib3_version, chardet_version):
# Check urllib3 for compatibility.
major, minor, patch = urllib3_version # noqa: F811
major, minor, patch = int(major), int(minor), int(patch)
# urllib3 >= 1.21.1, <= 1.24
# urllib3 >= 1.21.1, <= 1.25
assert major == 1
assert minor >= 21
assert minor <= 24
assert minor <= 25
# Check chardet for compatibility.
major, minor, patch = chardet_version.split('.')[:3]
+3 -3
View File
@@ -5,10 +5,10 @@
__title__ = 'requests'
__description__ = 'Python HTTP for Humans.'
__url__ = 'http://python-requests.org'
__version__ = '2.21.0'
__build__ = 0x022100
__version__ = '2.22.0'
__build__ = 0x022200
__author__ = 'Kenneth Reitz'
__author_email__ = 'me@kennethreitz.org'
__license__ = 'Apache 2.0'
__copyright__ = 'Copyright 2018 Kenneth Reitz'
__copyright__ = 'Copyright 2019 Kenneth Reitz'
__cake__ = u'\u2728 \U0001f370 \u2728'
+2 -2
View File
@@ -19,7 +19,7 @@ def request(method, url, **kwargs):
:param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary, list of tuples or bytes to send
in the body of the :class:`Request`.
in the query string for the :class:`Request`.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
@@ -65,7 +65,7 @@ def get(url, params=None, **kwargs):
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary, list of tuples or bytes to send
in the body of the :class:`Request`.
in the query string for the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
+1 -1
View File
@@ -10,7 +10,7 @@ from .models.lockfile import Lockfile
from .models.pipfile import Pipfile
from .models.requirements import Requirement
__version__ = "1.4.2"
__version__ = "1.5.1"
logger = logging.getLogger(__name__)
+525 -5
View File
@@ -1,19 +1,42 @@
# -*- coding: utf-8 -*-
import itertools
import operator
import attr
import distlib.markers
import packaging.version
import six
from packaging.markers import InvalidMarker, Marker
from packaging.specifiers import Specifier, SpecifierSet
from vistir.compat import Mapping, Set, lru_cache
from vistir.misc import dedup
from ..exceptions import RequirementError
from .utils import filter_none, validate_markers
from ..environment import MYPY_RUNNING
from ..exceptions import RequirementError
from six.moves import reduce # isort:skip
if MYPY_RUNNING:
from typing import Optional, List, Type, Any
MAX_VERSIONS = {2: 7, 3: 10}
def is_instance(item, cls):
# type: (Any, Type) -> bool
if isinstance(item, cls) or item.__class__.__name__ == cls.__name__:
return True
return False
@attr.s
class PipenvMarkers(object):
"""System-level requirements - see PEP508 for more detail"""
os_name = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
os_name = attr.ib(default=None, validator=attr.validators.optional(validate_markers))
sys_platform = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
@@ -92,3 +115,500 @@ class PipenvMarkers(object):
pass
else:
return combined_marker
@lru_cache(maxsize=128)
def _tuplize_version(version):
return tuple(int(x) for x in filter(lambda i: i != "*", version.split(".")))
@lru_cache(maxsize=128)
def _format_version(version):
if not isinstance(version, six.string_types):
return ".".join(str(i) for i in version)
return version
# Prefer [x,y) ranges.
REPLACE_RANGES = {">": ">=", "<=": "<"}
@lru_cache(maxsize=128)
def _format_pyspec(specifier):
if isinstance(specifier, str):
if not any(op in specifier for op in Specifier._operators.keys()):
specifier = "=={0}".format(specifier)
specifier = Specifier(specifier)
version = getattr(specifier, "version", specifier).rstrip()
if version and version.endswith("*"):
if version.endswith(".*"):
version = version.rstrip(".*")
else:
version = version.rstrip("*")
specifier = Specifier("{0}{1}".format(specifier.operator, version))
try:
op = REPLACE_RANGES[specifier.operator]
except KeyError:
return specifier
curr_tuple = _tuplize_version(version)
try:
next_tuple = (curr_tuple[0], curr_tuple[1] + 1)
except IndexError:
next_tuple = (curr_tuple[0], 1)
if not next_tuple[1] <= MAX_VERSIONS[next_tuple[0]]:
if specifier.operator == "<" and curr_tuple[1] <= MAX_VERSIONS[next_tuple[0]]:
op = "<="
next_tuple = (next_tuple[0], curr_tuple[1])
else:
return specifier
specifier = Specifier("{0}{1}".format(op, _format_version(next_tuple)))
return specifier
@lru_cache(maxsize=128)
def _get_specs(specset):
if specset is None:
return
if is_instance(specset, Specifier):
new_specset = SpecifierSet()
specs = set()
specs.add(specset)
new_specset._specs = frozenset(specs)
specset = new_specset
if isinstance(specset, str):
specset = SpecifierSet(specset)
result = []
for spec in set(specset):
version = spec.version
op = spec.operator
if op in ("in", "not in"):
versions = version.split(",")
op = "==" if op == "in" else "!="
for ver in versions:
result.append((op, _tuplize_version(ver.strip())))
else:
result.append((spec.operator, _tuplize_version(spec.version)))
return sorted(result, key=operator.itemgetter(1))
@lru_cache(maxsize=128)
def _group_by_op(specs):
specs = [_get_specs(x) for x in list(specs)]
flattened = [(op, version) for spec in specs for op, version in spec]
specs = sorted(flattened)
grouping = itertools.groupby(specs, key=operator.itemgetter(0))
return grouping
@lru_cache(maxsize=128)
def cleanup_pyspecs(specs, joiner="or"):
if isinstance(specs, six.string_types):
specs = set([_format_pyspec(specs)])
else:
specs = {_format_pyspec(spec) for spec in specs}
# for != operator we want to group by version
# if all are consecutive, join as a list
results = set()
for op, versions in _group_by_op(tuple(specs)):
versions = [version[1] for version in versions]
versions = sorted(dedup(versions))
# if we are doing an or operation, we need to use the min for >=
# this way OR(>=2.6, >=2.7, >=3.6) picks >=2.6
# if we do an AND operation we need to use MAX to be more selective
if op in (">", ">="):
if joiner == "or":
results.add((op, _format_version(min(versions))))
else:
results.add((op, _format_version(max(versions))))
# we use inverse logic here so we will take the max value if we are
# using OR but the min value if we are using AND
elif op in ("<=", "<"):
if joiner == "or":
results.add((op, _format_version(max(versions))))
else:
results.add((op, _format_version(min(versions))))
# leave these the same no matter what operator we use
elif op in ("!=", "==", "~="):
version_list = sorted(
"{0}".format(_format_version(version)) for version in versions
)
version = ", ".join(version_list)
if len(version_list) == 1:
results.add((op, version))
elif op == "!=":
results.add(("not in", version))
elif op == "==":
results.add(("in", version))
else:
specifier = SpecifierSet(
",".join(sorted("{0}{1}".format(op, v) for v in version_list))
)._specs
for s in specifier:
results.add((s._spec[0], s._spec[1]))
else:
if len(version) == 1:
results.add((op, version))
else:
specifier = SpecifierSet("{0}".format(version))._specs
for s in specifier:
results.add((s._spec[0], s._spec[1]))
return sorted(results, key=operator.itemgetter(1))
def fix_version_tuple(version_tuple):
op, version = version_tuple
max_major = max(MAX_VERSIONS.keys())
if version[0] > max_major:
return (op, (max_major, MAX_VERSIONS[max_major]))
max_allowed = MAX_VERSIONS[version[0]]
if op == "<" and version[1] > max_allowed and version[1] - 1 <= max_allowed:
op = "<="
version = (version[0], version[1] - 1)
return (op, version)
@lru_cache(maxsize=128)
def get_versions(specset, group_by_operator=True):
specs = [_get_specs(x) for x in list(tuple(specset))]
initial_sort_key = lambda k: (k[0], k[1])
initial_grouping_key = operator.itemgetter(0)
if not group_by_operator:
initial_grouping_key = operator.itemgetter(1)
initial_sort_key = operator.itemgetter(1)
version_tuples = sorted(
set((op, version) for spec in specs for op, version in spec), key=initial_sort_key
)
version_tuples = [fix_version_tuple(t) for t in version_tuples]
op_groups = [
(grp, list(map(operator.itemgetter(1), keys)))
for grp, keys in itertools.groupby(version_tuples, key=initial_grouping_key)
]
versions = [
(op, packaging.version.parse(".".join(str(v) for v in val)))
for op, vals in op_groups
for val in vals
]
return sorted(versions, key=operator.itemgetter(1))
def _ensure_marker(marker):
if not is_instance(marker, Marker):
return Marker(str(marker))
return marker
def gen_marker(mkr):
m = Marker("python_version == '1'")
m._markers.pop()
m._markers.append(mkr)
return m
def _strip_extra(elements):
"""Remove the "extra == ..." operands from the list."""
return _strip_marker_elem("extra", elements)
def _strip_pyversion(elements):
return _strip_marker_elem("python_version", elements)
def _strip_marker_elem(elem_name, elements):
"""Remove the supplied element from the marker.
This is not a comprehensive implementation, but relies on an important
characteristic of metadata generation: The element's operand is always
associated with an "and" operator. This means that we can simply remove the
operand and the "and" operator associated with it.
"""
extra_indexes = []
preceding_operators = ["and"] if elem_name == "extra" else ["and", "or"]
for i, element in enumerate(elements):
if isinstance(element, list):
cancelled = _strip_marker_elem(elem_name, element)
if cancelled:
extra_indexes.append(i)
elif isinstance(element, tuple) and element[0].value == elem_name:
extra_indexes.append(i)
for i in reversed(extra_indexes):
del elements[i]
if i > 0 and elements[i - 1] in preceding_operators:
# Remove the "and" before it.
del elements[i - 1]
elif elements:
# This shouldn't ever happen, but is included for completeness.
# If there is not an "and" before this element, try to remove the
# operator after it.
del elements[0]
return not elements
def _get_stripped_marker(marker, strip_func):
"""Build a new marker which is cleaned according to `strip_func`"""
if not marker:
return None
marker = _ensure_marker(marker)
elements = marker._markers
strip_func(elements)
if elements:
return marker
return None
def get_without_extra(marker):
"""Build a new marker without the `extra == ...` part.
The implementation relies very deep into packaging's internals, but I don't
have a better way now (except implementing the whole thing myself).
This could return `None` if the `extra == ...` part is the only one in the
input marker.
"""
return _get_stripped_marker(marker, _strip_extra)
def get_without_pyversion(marker):
"""Built a new marker without the `python_version` part.
This could return `None` if the `python_version` section is the only section in the
marker.
"""
return _get_stripped_marker(marker, _strip_pyversion)
def _markers_collect_extras(markers, collection):
# Optimization: the marker element is usually appended at the end.
for el in reversed(markers):
if isinstance(el, tuple) and el[0].value == "extra" and el[1].value == "==":
collection.add(el[2].value)
elif isinstance(el, list):
_markers_collect_extras(el, collection)
def _markers_collect_pyversions(markers, collection):
local_collection = []
marker_format_str = "{0}"
for i, el in enumerate(reversed(markers)):
if isinstance(el, tuple) and el[0].value == "python_version":
new_marker = str(gen_marker(el))
local_collection.append(marker_format_str.format(new_marker))
elif isinstance(el, list):
_markers_collect_pyversions(el, local_collection)
if local_collection:
# local_collection = "{0}".format(" ".join(local_collection))
collection.extend(local_collection)
def _markers_contains_extra(markers):
# Optimization: the marker element is usually appended at the end.
return _markers_contains_key(markers, "extra")
def _markers_contains_pyversion(markers):
return _markers_contains_key(markers, "python_version")
def _markers_contains_key(markers, key):
for element in reversed(markers):
if isinstance(element, tuple) and element[0].value == key:
return True
elif isinstance(element, list):
if _markers_contains_key(element, key):
return True
return False
@lru_cache(maxsize=128)
def get_contained_extras(marker):
"""Collect "extra == ..." operands from a marker.
Returns a list of str. Each str is a speficied extra in this marker.
"""
if not marker:
return set()
extras = set()
marker = _ensure_marker(marker)
_markers_collect_extras(marker._markers, extras)
return extras
def get_contained_pyversions(marker):
"""Collect all `python_version` operands from a marker.
"""
collection = []
if not marker:
return set()
marker = _ensure_marker(marker)
# Collect the (Variable, Op, Value) tuples and string joiners from the marker
_markers_collect_pyversions(marker._markers, collection)
marker_str = " and ".join(sorted(collection))
if not marker_str:
return set()
# Use the distlib dictionary parser to create a dictionary 'trie' which is a bit
# easier to reason about
marker_dict = distlib.markers.parse_marker(marker_str)[0]
version_set = set()
pyversions, _ = parse_marker_dict(marker_dict)
if isinstance(pyversions, set):
version_set.update(pyversions)
elif pyversions is not None:
version_set.add(pyversions)
# Each distinct element in the set was separated by an "and" operator in the marker
# So we will need to reduce them with an intersection here rather than a union
# in order to find the boundaries
versions = set()
if version_set:
versions = reduce(lambda x, y: x & y, version_set)
return versions
@lru_cache(maxsize=128)
def contains_extra(marker):
"""Check whehter a marker contains an "extra == ..." operand.
"""
if not marker:
return False
marker = _ensure_marker(marker)
return _markers_contains_extra(marker._markers)
@lru_cache(maxsize=128)
def contains_pyversion(marker):
"""Check whether a marker contains a python_version operand.
"""
if not marker:
return False
marker = _ensure_marker(marker)
return _markers_contains_pyversion(marker._markers)
def get_specset(marker_list):
# type: (List) -> Optional[SpecifierSet]
specset = set()
_last_str = "and"
for marker_parts in marker_list:
if isinstance(marker_parts, tuple):
variable, op, value = marker_parts
if variable.value != "python_version":
continue
if op.value == "in":
values = [v.strip() for v in value.value.split(",")]
specset.update(Specifier("=={0}".format(v)) for v in values)
elif op.value == "not in":
values = [v.strip() for v in value.value.split(",")]
bad_versions = ["3.0", "3.1", "3.2", "3.3"]
if len(values) >= 2 and any(v in values for v in bad_versions):
values = bad_versions
specset.update(
Specifier("!={0}".format(v.strip())) for v in sorted(bad_versions)
)
else:
specset.add(Specifier("{0}{1}".format(op.value, value.value)))
elif isinstance(marker_parts, list):
parts = get_specset(marker_parts)
if parts:
specset.update(parts)
elif isinstance(marker_parts, str):
_last_str = marker_parts
specifiers = SpecifierSet()
specifiers._specs = frozenset(specset)
return specifiers
def parse_marker_dict(marker_dict):
op = marker_dict["op"]
lhs = marker_dict["lhs"]
rhs = marker_dict["rhs"]
# This is where the spec sets for each side land if we have an "or" operator
side_spec_list = []
side_markers_list = []
finalized_marker = ""
# And if we hit the end of the parse tree we use this format string to make a marker
format_string = "{lhs} {op} {rhs}"
specset = SpecifierSet()
specs = set()
# Essentially we will iterate over each side of the parsed marker if either one is
# A mapping instance (i.e. a dictionary) and recursively parse and reduce the specset
# Union the "and" specs, intersect the "or"s to find the most appropriate range
if any(issubclass(type(side), Mapping) for side in (lhs, rhs)):
for side in (lhs, rhs):
side_specs = set()
side_markers = set()
if issubclass(type(side), Mapping):
merged_side_specs, merged_side_markers = parse_marker_dict(side)
side_specs.update(merged_side_specs)
side_markers.update(merged_side_markers)
else:
marker = _ensure_marker(side)
marker_parts = getattr(marker, "_markers", [])
if marker_parts[0][0].value == "python_version":
side_specs |= set(get_specset(marker_parts))
else:
side_markers.add(str(marker))
side_spec_list.append(side_specs)
side_markers_list.append(side_markers)
if op == "and":
# When we are "and"-ing things together, it probably makes the most sense
# to reduce them here into a single PySpec instance
specs = reduce(lambda x, y: set(x) | set(y), side_spec_list)
markers = reduce(lambda x, y: set(x) | set(y), side_markers_list)
if not specs and not markers:
return specset, finalized_marker
if markers and isinstance(markers, (tuple, list, Set)):
finalized_marker = Marker(" and ".join([m for m in markers if m]))
elif markers:
finalized_marker = str(markers)
specset._specs = frozenset(specs)
return specset, finalized_marker
# Actually when we "or" things as well we can also just turn them into a reduced
# set using this logic now
sides = reduce(lambda x, y: set(x) & set(y), side_spec_list)
finalized_marker = " or ".join(
[normalize_marker_str(m) for m in side_markers_list]
)
specset._specs = frozenset(sorted(sides))
return specset, finalized_marker
else:
# At the tip of the tree we are dealing with strings all around and they just need
# to be smashed together
specs = set()
if lhs == "python_version":
format_string = "{lhs}{op}{rhs}"
marker = Marker(format_string.format(**marker_dict))
marker_parts = getattr(marker, "_markers", [])
_set = get_specset(marker_parts)
if _set:
specs |= set(_set)
specset._specs = frozenset(specs)
return specset, finalized_marker
def format_pyversion(parts):
op, val = parts
return "python_version {0} '{1}'".format(op, val)
def normalize_marker_str(marker):
marker_str = ""
if not marker:
return None
if not is_instance(marker, Marker):
marker = _ensure_marker(marker)
pyversion = get_contained_pyversions(marker)
marker = get_without_pyversion(marker)
if pyversion:
parts = cleanup_pyspecs(pyversion)
marker_str = " and ".join([format_pyversion(pv) for pv in parts])
if marker:
if marker_str:
marker_str = "{0!s} and {1!s}".format(marker_str, marker)
else:
marker_str = "{0!s}".format(marker)
return marker_str.replace('"', "'")
+42 -26
View File
@@ -7,22 +7,21 @@ import os
import sys
import attr
import tomlkit
import plette.models.base
import plette.pipfiles
import tomlkit
from vistir.compat import FileNotFoundError, Path
from ..exceptions import RequirementError
from ..utils import is_editable, is_vcs, merge_items
from .project import ProjectFile
from .requirements import Requirement
from .utils import optional_instance_of, get_url_name
from .utils import get_url_name, optional_instance_of, tomlkit_value_to_python
from ..environment import MYPY_RUNNING
from ..exceptions import RequirementError
from ..utils import is_editable, is_vcs, merge_items
if MYPY_RUNNING:
from typing import Union, Any, Dict, Iterable, Mapping, List, Text
package_type = Dict[Text, Dict[Text, Union[List[Text], Text]]]
source_type = Dict[Text, Union[Text, bool]]
sources_type = Iterable[source_type]
@@ -46,7 +45,7 @@ def patch_plette():
def validate(cls, data):
# type: (Any, Dict[Text, Any]) -> None
if not cerberus: # Skip validation if Cerberus is not available.
if not cerberus: # Skip validation if Cerberus is not available.
return
schema = cls.__SCHEMA__
key = id(schema)
@@ -156,10 +155,12 @@ class Pipfile(object):
path = attr.ib(validator=is_path, type=Path)
projectfile = attr.ib(validator=is_projectfile, type=ProjectFile)
_pipfile = attr.ib(type=PipfileLoader)
_pyproject = attr.ib(default=attr.Factory(tomlkit.document), type=tomlkit.toml_document.TOMLDocument)
_pyproject = attr.ib(
default=attr.Factory(tomlkit.document), type=tomlkit.toml_document.TOMLDocument
)
build_system = attr.ib(default=attr.Factory(dict), type=dict)
requirements = attr.ib(default=attr.Factory(list), type=list)
dev_requirements = attr.ib(default=attr.Factory(list), type=list)
_requirements = attr.ib(default=attr.Factory(list), type=list)
_dev_requirements = attr.ib(default=attr.Factory(list), type=list)
@path.default
def _get_path(self):
@@ -188,7 +189,9 @@ class Pipfile(object):
deps.update(self.pipfile._data["dev-packages"])
if only:
return deps
return merge_items([deps, self.pipfile._data["packages"]])
return tomlkit_value_to_python(
merge_items([deps, self.pipfile._data["packages"]])
)
def get(self, k):
# type: (Text) -> Any
@@ -213,6 +216,7 @@ class Pipfile(object):
if "-" in k:
section, _, pkg_type = k.rpartition("-")
vals = getattr(pipfile.get(section, {}), "_data", {})
vals = tomlkit_value_to_python(vals)
if pkg_type == "vcs":
retval = {k: v for k, v in vals.items() if is_vcs(v)}
elif pkg_type == "editable":
@@ -254,11 +258,7 @@ class Pipfile(object):
:return: A project file with the model and location for interaction
:rtype: :class:`~requirementslib.models.project.ProjectFile`
"""
pf = ProjectFile.read(
path,
PipfileLoader,
invalid_ok=True
)
pf = ProjectFile.read(path, PipfileLoader, invalid_ok=True)
return pf
@classmethod
@@ -303,18 +303,10 @@ class Pipfile(object):
projectfile = cls.load_projectfile(path, create=create)
pipfile = projectfile.model
dev_requirements = [
Requirement.from_pipfile(k, getattr(v, "_data", v)) for k, v in pipfile.get("dev-packages", {}).items()
]
requirements = [
Requirement.from_pipfile(k, getattr(v, "_data", v)) for k, v in pipfile.get("packages", {}).items()
]
creation_args = {
"projectfile": projectfile,
"pipfile": pipfile,
"dev_requirements": dev_requirements,
"requirements": requirements,
"path": Path(projectfile.location)
"path": Path(projectfile.location),
}
return cls(**creation_args)
@@ -333,6 +325,30 @@ class Pipfile(object):
# type: () -> List[Requirement]
return self.requirements
@property
def dev_requirements(self):
# type: () -> List[Requirement]
if not self._dev_requirements:
packages = tomlkit_value_to_python(self.pipfile.get("dev-packages", {}))
self._dev_requirements = [
Requirement.from_pipfile(k, v)
for k, v in packages.items()
if v is not None
]
return self._dev_requirements
@property
def requirements(self):
# type: () -> List[Requirement]
if not self._requirements:
packages = tomlkit_value_to_python(self.pipfile.get("packages", {}))
self._requirements = [
Requirement.from_pipfile(k, v)
for k, v in packages.items()
if v is not None
]
return self._requirements
def _read_pyproject(self):
# type: () -> None
pyproject = self.path.parent.joinpath("pyproject.toml")
+138 -264
View File
@@ -4,7 +4,6 @@ from __future__ import absolute_import, print_function
import collections
import copy
import hashlib
import os
import sys
from contextlib import contextmanager
@@ -40,7 +39,20 @@ from vistir.path import (
normalize_path,
)
from .setup_info import SetupInfo, _prepare_wheel_building_kwargs
from .markers import (
cleanup_pyspecs,
contains_pyversion,
format_pyversion,
get_contained_pyversions,
normalize_marker_str,
)
from .setup_info import (
SetupInfo,
_prepare_wheel_building_kwargs,
ast_parse_setup_py,
get_metadata,
parse_setup_cfg,
)
from .url import URI
from .utils import (
DIRECT_URL_RE,
@@ -74,6 +86,7 @@ from ..utils import (
VCS_LIST,
add_ssh_scheme_to_git_uri,
get_setup_paths,
is_installable_dir,
is_installable_file,
is_vcs,
strip_ssh_from_git_uri,
@@ -195,6 +208,18 @@ class Line(object):
except Exception:
return "<Line {0}>".format(self.__dict__.values())
@property
def name_and_specifier(self):
name_str, spec_str = "", ""
if self.name:
name_str = "{0}".format(self.name.lower())
extras_str = extras_to_string(self.extras)
if extras_str:
name_str = "{0}{1}".format(name_str, extras_str)
if self.specifier:
spec_str = "{0}".format(self.specifier)
return "{0}{1}".format(name_str, spec_str)
@classmethod
def split_hashes(cls, line):
# type: (S) -> Tuple[S, List[S]]
@@ -216,6 +241,8 @@ class Line(object):
def line_with_prefix(self):
# type: () -> STRING_TYPE
line = self.line
if self.is_named:
return self.name_and_specifier
extras_str = extras_to_string(self.extras)
if self.is_direct_url:
line = self.link.url
@@ -224,7 +251,7 @@ class Line(object):
line = self.link.url
if "git+file:/" in line and "git+file:///" not in line:
line = line.replace("git+file:/", "git+file:///")
else:
elif extras_str not in line:
line = "{0}{1}".format(line, extras_str)
if self.editable:
return "-e {0}".format(line)
@@ -487,10 +514,10 @@ class Line(object):
:returns: Nothing
:rtype: None
"""
line, hashes = self.split_hashes(self.line)
self.hashes = hashes
self.line = line
return self
def parse_extras(self):
# type: () -> None
@@ -499,7 +526,6 @@ class Line(object):
:returns: Nothing
:rtype: None
"""
extras = None
if "@" in self.line or self.is_vcs or self.is_url:
line = "{0}".format(self.line)
@@ -525,11 +551,11 @@ class Line(object):
extras_set |= name_extras
if extras_set is not None:
self.extras = tuple(sorted(extras_set))
return self
def get_url(self):
# type: () -> STRING_TYPE
"""Sets ``self.name`` if given a **PEP-508** style URL"""
line = self.line
try:
parsed = URI.parse(line)
@@ -569,6 +595,10 @@ class Line(object):
if self._name is None and not self.is_named and not self.is_wheel:
if self.setup_info:
self._name = self.setup_info.name
elif self.is_wheel:
self._name = self._parse_wheel()
if not self._name:
self._name = self.ireq.name
return self._name
@name.setter
@@ -781,6 +811,29 @@ class Line(object):
self._vcsrepo = self._get_vcsrepo()
return self._vcsrepo
@cached_property
def metadata(self):
# type: () -> Dict[Any, Any]
if self.is_local and is_installable_dir(self.path):
return get_metadata(self.path)
return {}
@cached_property
def parsed_setup_cfg(self):
# type: () -> Dict[Any, Any]
if self.is_local and is_installable_dir(self.path):
if self.setup_cfg:
return parse_setup_cfg(self.setup_cfg)
return {}
@cached_property
def parsed_setup_py(self):
# type: () -> Dict[Any, Any]
if self.is_local and is_installable_dir(self.path):
if self.setup_py:
return ast_parse_setup_py(self.setup_py)
return {}
@vcsrepo.setter
def vcsrepo(self, repo):
# type (VCSRepository) -> None
@@ -843,7 +896,6 @@ class Line(object):
def _parse_name_from_link(self):
# type: () -> Optional[STRING_TYPE]
if self.link is None:
return None
if getattr(self.link, "egg_fragment", None):
@@ -881,8 +933,29 @@ class Line(object):
self._specifier = "{0}{1}".format(specifier, version)
return name
def _parse_name_from_path(self):
# type: () -> Optional[S]
if self.path and self.is_local and is_installable_dir(self.path):
metadata = get_metadata(self.path)
if metadata:
name = metadata.get("name", "")
if name and name != "wheel":
return name
parsed_setup_cfg = self.parsed_setup_cfg
if parsed_setup_cfg:
name = parsed_setup_cfg.get("name", "")
if name:
return name
parsed_setup_py = self.parsed_setup_py
if parsed_setup_py:
name = parsed_setup_py.get("name", "")
if name:
return name
return None
def parse_name(self):
# type: () -> None
# type: () -> "Line"
if self._name is None:
name = None
if self.link is not None:
@@ -895,13 +968,17 @@ class Line(object):
if "&" in name:
# subdirectory fragments might also be in here
name, _, _ = name.partition("&")
if self.is_named:
if name is None and self.is_named:
name = self._parse_name_from_line()
elif name is None and self.is_file or self.is_url or self.is_path:
if self.is_local:
name = self._parse_name_from_path()
if name is not None:
name, extras = pip_shims.shims._strip_extras(name)
if extras is not None and not self.extras:
self.extras = tuple(sorted(set(parse_extras(extras))))
self._name = name
return self
def _parse_requirement_from_vcs(self):
# type: () -> Optional[PackagingRequirement]
@@ -939,7 +1016,7 @@ class Line(object):
return self._requirement
def parse_requirement(self):
# type: () -> None
# type: () -> "Line"
if self._name is None:
self.parse_name()
if not self._name and not self.is_vcs and not self.is_named:
@@ -982,9 +1059,10 @@ class Line(object):
"dependencies. Please install remote dependency "
"in the form {0}#egg=<package-name>.".format(url)
)
return self
def parse_link(self):
# type: () -> None
# type: () -> "Line"
parsed_url = None # type: Optional[URI]
if not is_valid_url(self.line) and (
self.line.startswith("./")
@@ -1033,6 +1111,7 @@ class Line(object):
self._link = parsed_link
else:
self._link = link
return self
def parse_markers(self):
# type: () -> None
@@ -1110,8 +1189,7 @@ class Line(object):
def parse(self):
# type: () -> None
self.parse_hashes()
self.line, self.markers = split_markers_from_line(self.line)
self.line, self.markers = split_markers_from_line(self.parse_hashes().line)
self.parse_extras()
self.line = self.line.strip('"').strip("'").strip()
if self.line.startswith("git+file:/") and not self.line.startswith(
@@ -1184,8 +1262,8 @@ class NamedRequirement(object):
return cls(**creation_kwargs)
@classmethod
def from_pipfile(cls, name, pipfile): # type: S # type: TPIPFILE
# type: (...) -> NamedRequirement
def from_pipfile(cls, name, pipfile):
# type: (S, TPIPFILE) -> NamedRequirement
creation_args = {} # type: TPIPFILE
if hasattr(pipfile, "keys"):
attr_fields = [field.name for field in attr.fields(cls)]
@@ -1471,84 +1549,12 @@ class FileRequirement(object):
@name.default
def get_name(self):
# type: () -> STRING_TYPE
loc = self.path or self.uri
if loc and not self._uri_scheme:
self._uri_scheme = "path" if self.path else "file"
name = None # type: Optional[STRING_TYPE]
hashed_loc = None # type: Optional[STRING_TYPE]
hashed_name = None # type: Optional[STRING_TYPE]
if loc:
hashed_loc = hashlib.sha256(loc.encode("utf-8")).hexdigest()
hashed_name = hashed_loc[-7:]
if (
getattr(self, "req", None)
and self.req is not None
and getattr(self.req, "name")
and self.req.name is not None
):
if self.is_direct_url and self.req.name != hashed_name:
return self.req.name
if self.link and self.link.egg_fragment and self.link.egg_fragment != hashed_name:
if self.parsed_line and self.parsed_line.name:
return self.parsed_line.name
elif self.link and self.link.egg_fragment:
return self.link.egg_fragment
elif self.link and self.link.is_wheel:
from pip_shims import Wheel
self._has_hashed_name = False
return Wheel(self.link.filename).name
elif self.link and (
(self.link.scheme == "file" or self.editable)
or (self.path and self.setup_path and os.path.isfile(str(self.setup_path)))
):
_ireq = None # type: Optional[InstallRequirement]
target_path = "" # type: STRING_TYPE
if self.setup_py_dir:
target_path = Path(self.setup_py_dir).as_posix()
elif self.path:
target_path = Path(os.path.abspath(self.path)).as_posix()
if self.editable:
line = pip_shims.shims.path_to_url(target_path)
if self.extras:
line = "{0}[{1}]".format(line, ",".join(self.extras))
_ireq = pip_shims.shims.install_req_from_editable(line)
else:
line = target_path
if self.extras:
line = "{0}[{1}]".format(line, ",".join(self.extras))
_ireq = pip_shims.shims.install_req_from_line(line)
if getattr(self, "req", None) is not None:
_ireq.req = copy.deepcopy(self.req)
if self.extras and _ireq and not _ireq.extras:
_ireq.extras = set(self.extras)
from .setup_info import SetupInfo
subdir = getattr(self, "subdirectory", None)
if self.setup_info is not None:
setupinfo = self.setup_info
else:
setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir)
if setupinfo:
self._setup_info = setupinfo
self._setup_info.get_info()
setupinfo_dict = setupinfo.as_dict()
setup_name = setupinfo_dict.get("name", None)
if setup_name:
name = setup_name
self._has_hashed_name = False
build_requires = setupinfo_dict.get("build_requires")
build_backend = setupinfo_dict.get("build_backend")
if build_requires and not self.pyproject_requires:
self.pyproject_requires = tuple(build_requires)
if build_backend and not self.pyproject_backend:
self.pyproject_backend = build_backend
if not name or name.lower() == "unknown":
self._has_hashed_name = True
name = hashed_name
name_in_link = getattr(self.link, "egg_fragment", "") if self.link else ""
if not self._has_hashed_name and name_in_link != name and self.link is not None:
self.link = create_link("{0}#egg={1}".format(self.link.url, name))
if name is not None:
return name
return ""
elif self.setup_info and self.setup_info.name:
return self.setup_info.name
@link.default
def get_link(self):
@@ -1581,34 +1587,6 @@ class FileRequirement(object):
if req:
return req
req = init_requirement(normalize_name(self.name))
if req is None:
raise ValueError(
"Failed to generate a requirement: missing name for {0!r}".format(self)
)
req.editable = False
if self.link is not None:
req.line = self.link.url_without_fragment
elif self.uri is not None:
req.line = self.uri
else:
req.line = self.name
if self.path and self.link and self.link.scheme.startswith("file"):
req.local_file = True
req.path = self.path
if self.editable:
req.url = None
else:
req.url = self.link.url_without_fragment
else:
req.local_file = False
req.path = None
req.url = self.link.url_without_fragment
if self.editable:
req.editable = True
req.link = self.link
return req
@property
def parsed_line(self):
# type: () -> Optional[Line]
@@ -1639,11 +1617,9 @@ class FileRequirement(object):
if self.link is None:
return False
return (
any(
self.link.scheme.startswith(scheme)
for scheme in ("http", "https", "ftp", "ftps", "uri")
)
and (self.link.is_artifact or self.link.is_wheel)
self._parsed_line
and not self._parsed_line.is_local
and (self._parsed_line.is_artifact or self._parsed_line.is_wheel)
and not self.editable
)
@@ -1833,53 +1809,8 @@ class FileRequirement(object):
@classmethod
def from_line(cls, line, editable=None, extras=None, parsed_line=None):
# type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr, ...]], Optional[Line]) -> F
line = line.strip('"').strip("'")
link = None
path = None
editable = line.startswith("-e ")
line = line.split(" ", 1)[1] if editable else line
setup_path = None
name = None
req = None
if not extras:
extras = ()
else:
extras = tuple(extras)
if not any([is_installable_file(line), is_valid_url(line), is_file_url(line)]):
try:
req = init_requirement(line)
except Exception:
raise RequirementError(
"Supplied requirement is not installable: {0!r}".format(line)
)
else:
name = getattr(req, "name", None)
line = getattr(req, "url", None)
vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line)
arg_dict = {
"path": relpath if relpath else path,
"uri": unquote(link.url_without_fragment),
"link": link,
"editable": editable,
"setup_path": setup_path,
"uri_scheme": prefer,
"line": line,
"extras": extras,
# "name": name,
}
if req is not None:
arg_dict["req"] = req
if parsed_line is not None:
arg_dict["parsed_line"] = parsed_line
if link and link.is_wheel:
from pip_shims import Wheel
arg_dict["name"] = Wheel(link.filename).name
elif name:
arg_dict["name"] = name
elif link.egg_fragment:
arg_dict["name"] = link.egg_fragment
return cls.create(**arg_dict)
parsed_line = Line(line)
file_req_from_parsed_line(parsed_line)
@classmethod
def from_pipfile(cls, name, pipfile):
@@ -1964,8 +1895,9 @@ class FileRequirement(object):
line = "{0}&subdirectory={1}".format(line, pipfile["subdirectory"])
if editable:
line = "-e {0}".format(line)
arg_dict["line"] = line
return cls.create(**arg_dict) # type: ignore
arg_dict["parsed_line"] = Line(line)
arg_dict["setup_info"] = arg_dict["parsed_line"].setup_info
return cls(**arg_dict) # type: ignore
@property
def line_part(self):
@@ -2344,7 +2276,7 @@ class VCSRequirement(FileRequirement):
)
if self.parsed_line and self._parsed_line:
self._parsed_line.vcsrepo = vcsrepo
if self.req:
if self.req and not self.editable:
self.req.specifier = SpecifierSet("=={0}".format(self.setup_info.version))
try:
yield self._repo
@@ -2407,83 +2339,8 @@ class VCSRequirement(FileRequirement):
@classmethod
def from_line(cls, line, editable=None, extras=None, parsed_line=None):
# type: (AnyStr, Optional[bool], Optional[Tuple[AnyStr, ...]], Optional[Line]) -> F
relpath = None
if parsed_line is None:
parsed_line = Line(line)
if editable:
parsed_line.editable = editable
if extras:
parsed_line.extras = extras
if line.startswith("-e "):
editable = True
line = line.split(" ", 1)[1]
if "@" in line:
parsed = urllib_parse.urlparse(add_ssh_scheme_to_git_uri(line))
if not parsed.scheme:
possible_name, _, line = line.partition("@")
possible_name = possible_name.strip()
line = line.strip()
possible_name, extras = pip_shims.shims._strip_extras(possible_name)
name = possible_name
line = "{0}#egg={1}".format(line, name)
vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line)
if not extras and link.egg_fragment:
name, extras = pip_shims.shims._strip_extras(link.egg_fragment)
else:
name, _ = pip_shims.shims._strip_extras(link.egg_fragment)
parsed_extras = None # type: Optional[List[STRING_TYPE]]
extras_tuple = None # type: Optional[Tuple[STRING_TYPE, ...]]
if not extras:
line, extras = pip_shims.shims._strip_extras(line)
if extras:
if isinstance(extras, six.string_types):
parsed_extras = parse_extras(extras)
if parsed_extras:
extras_tuple = tuple(parsed_extras)
subdirectory = link.subdirectory_fragment
ref = None
if uri:
uri, ref = split_ref_from_uri(uri)
if path is not None and "@" in path:
path, _ref = split_ref_from_uri(path)
if ref is None:
ref = _ref
if relpath and "@" in relpath:
relpath, ref = split_ref_from_uri(relpath)
creation_args = {
"name": name if name else parsed_line.name,
"path": relpath or path,
"editable": editable,
"extras": extras_tuple,
"link": link,
"vcs_type": vcs_type,
"line": line,
"uri": uri,
"uri_scheme": prefer,
"parsed_line": parsed_line,
}
if relpath:
creation_args["relpath"] = relpath
# return cls.create(**creation_args)
cls_inst = cls(
name=name,
ref=ref,
vcs=vcs_type,
subdirectory=subdirectory,
link=link,
path=relpath or path,
editable=editable,
uri=uri,
extras=extras_tuple if extras_tuple else tuple(),
base_line=line,
parsed_line=parsed_line,
)
if cls_inst.req and (
cls_inst._parsed_line.ireq and not cls_inst.parsed_line.ireq.req
):
cls_inst._parsed_line._ireq.req = cls_inst.req
return cls_inst
parsed_line = Line(line)
return vcs_req_from_parsed_line(parsed_line)
@property
def line_part(self):
@@ -2695,12 +2552,12 @@ class Requirement(object):
return ""
def update_name_from_path(self, path):
from .setup_info import get_metadata
metadata = get_metadata(path)
name = self.name
if metadata is not None:
name = metadata.get("name")
metadata_name = metadata.get("name")
if metadata_name and metadata_name != "wheel":
name = metadata_name
if name is not None:
if self.req.name is None:
self.req.name = name
@@ -3238,15 +3095,32 @@ class Requirement(object):
# type: (Union[AnyStr, Marker]) -> None
if not isinstance(markers, Marker):
markers = Marker(markers)
_markers = set() # type: Set[Marker]
if self.ireq and self.ireq.markers:
_markers.add(Marker(self.ireq.markers))
_markers.add(markers)
new_markers = Marker(" or ".join([str(m) for m in sorted(_markers)]))
self.markers = str(new_markers)
if self.req and self.req.req:
self.req.req.marker = new_markers
return
_markers = [] # type: List[Marker]
ireq = self.as_ireq()
if ireq and ireq.markers:
ireq_marker = ireq.markers
_markers.append(str(ireq_marker))
_markers.append(str(markers))
marker_str = " and ".join([normalize_marker_str(m) for m in _markers if m])
new_marker = Marker(marker_str)
line = copy.deepcopy(self._line_instance)
line.markers = marker_str
line.parsed_marker = new_marker
if getattr(line, "_requirement", None) is not None:
line._requirement.marker = new_marker
if getattr(line, "_ireq", None) is not None and line._ireq.req:
line._ireq.req.marker = new_marker
new_ireq = getattr(self, "ireq", None)
if new_ireq and new_ireq.req:
new_ireq.req.marker = new_marker
req = self.req
if req.req:
req_requirement = req.req
req_requirement.marker = new_marker
req = attr.evolve(req, req=req_requirement, parsed_line=line)
return attr.evolve(
self, markers=str(new_marker), ireq=new_ireq, req=req, line_instance=line
)
def file_req_from_parsed_line(parsed_line):
File diff suppressed because it is too large Load Diff
+6
View File
@@ -203,6 +203,12 @@ class URI(object):
fragment = ""
if parsed_dict["fragment"] is not None:
fragment = "{0}".format(parsed_dict["fragment"])
if fragment.startswith("egg="):
name, extras = pip_shims.shims._strip_extras(name_with_extras)
fragment_name, fragment_extras = pip_shims.shims._strip_extras(fragment)
if fragment_extras and not extras:
name_with_extras = "{0}{1}".format(name, fragment_extras)
fragment = ""
elif "&subdirectory" in parsed_dict["path"]:
path, fragment = cls.parse_subdirectory(parsed_dict["path"])
parsed_dict["path"] = path
+102 -22
View File
@@ -17,14 +17,17 @@ from first import first
from packaging.markers import InvalidMarker, Marker, Op, Value, Variable
from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
from packaging.version import parse as parse_version
from plette.models import Package, PackageCollection
from six.moves.urllib import parse as urllib_parse
from tomlkit.container import Container
from tomlkit.items import AoT, Array, Bool, InlineTable, Item, String, Table
from urllib3 import util as urllib3_util
from vistir.compat import lru_cache
from vistir.misc import dedup
from vistir.path import is_valid_url
from ..environment import MYPY_RUNNING
from ..utils import SCHEME_LIST, VCS_LIST, add_ssh_scheme_to_git_uri, is_star
from ..utils import SCHEME_LIST, VCS_LIST, is_star
if MYPY_RUNNING:
from typing import (
@@ -40,9 +43,9 @@ if MYPY_RUNNING:
Text,
AnyStr,
Match,
Iterable,
Iterable, # noqa
)
from attr import _ValidatorType
from attr import _ValidatorType # noqa
from packaging.requirements import Requirement as PackagingRequirement
from pkg_resources import Requirement as PkgResourcesRequirement
from pkg_resources.extern.packaging.markers import (
@@ -62,9 +65,13 @@ if MYPY_RUNNING:
MarkerTuple = Tuple[TVariable, TOp, TValue]
TRequirement = Union[PackagingRequirement, PkgResourcesRequirement]
STRING_TYPE = Union[bytes, str, Text]
TOML_DICT_TYPES = Union[Container, Package, PackageCollection, Table, InlineTable]
S = TypeVar("S", bytes, str, Text)
TOML_DICT_OBJECTS = (Container, Package, Table, InlineTable, PackageCollection)
TOML_DICT_NAMES = [o.__class__.__name__ for o in TOML_DICT_OBJECTS]
HASH_STRING = " --hash={0}"
ALPHA_NUMERIC = r"[{0}{1}]".format(string.ascii_letters, string.digits)
@@ -111,6 +118,60 @@ def create_link(link):
return Link(link)
def tomlkit_value_to_python(toml_value):
# type: (Union[Array, AoT, TOML_DICT_TYPES, Item]) -> Union[List, Dict]
value_type = type(toml_value).__name__
if (
isinstance(toml_value, TOML_DICT_OBJECTS + (dict,))
or value_type in TOML_DICT_NAMES
):
return tomlkit_dict_to_python(toml_value)
elif isinstance(toml_value, AoT) or value_type == "AoT":
return [tomlkit_value_to_python(val) for val in toml_value._body]
elif isinstance(toml_value, Array) or value_type == "Array":
return [tomlkit_value_to_python(val) for val in list(toml_value)]
elif isinstance(toml_value, String) or value_type == "String":
return "{0!s}".format(toml_value)
elif isinstance(toml_value, Bool) or value_type == "Bool":
return toml_value.value
elif isinstance(toml_value, Item):
return toml_value.value
return toml_value
def tomlkit_dict_to_python(toml_dict):
# type: (TOML_DICT_TYPES) -> Dict
value_type = type(toml_dict).__name__
if toml_dict is None:
raise TypeError("Invalid type NoneType when converting toml dict to python")
converted = None # type: Optional[Dict]
if isinstance(toml_dict, (InlineTable, Table)) or value_type in (
"InlineTable",
"Table",
):
converted = toml_dict.value
elif isinstance(toml_dict, (Package, PackageCollection)) or value_type in (
"Package, PackageCollection"
):
converted = toml_dict._data
if isinstance(converted, Container) or type(converted).__name__ == "Container":
converted = converted.value
elif isinstance(toml_dict, Container) or value_type == "Container":
converted = toml_dict.value
elif isinstance(toml_dict, dict):
converted = toml_dict.copy()
else:
raise TypeError(
"Invalid type for conversion: expected Container, Dict, or Table, "
"got {0!r}".format(toml_dict)
)
if isinstance(converted, dict):
return {k: tomlkit_value_to_python(v) for k, v in converted.items()}
elif isinstance(converted, (TOML_DICT_OBJECTS)) or value_type in TOML_DICT_NAMES:
return tomlkit_dict_to_python(converted)
return converted
def get_url_name(url):
# type: (AnyStr) -> AnyStr
"""
@@ -142,7 +203,12 @@ def init_requirement(name):
def extras_to_string(extras):
# type: (Iterable[S]) -> S
"""Turn a list of extras into a string"""
"""Turn a list of extras into a string
:param List[str]] extras: a list of extras to format
:return: A string of extras
:rtype: str
"""
if isinstance(extras, six.string_types):
if extras.startswith("["):
return extras
@@ -155,8 +221,11 @@ def extras_to_string(extras):
def parse_extras(extras_str):
# type: (AnyStr) -> List[AnyStr]
"""
Turn a string of extras into a parsed extras list
"""Turn a string of extras into a parsed extras list
:param str extras_str: An extras string
:return: A sorted list of extras
:rtype: List[str]
"""
from pkg_resources import Requirement
@@ -167,8 +236,11 @@ def parse_extras(extras_str):
def specs_to_string(specs):
# type: (List[Union[STRING_TYPE, Specifier]]) -> AnyStr
"""
Turn a list of specifier tuples into a string
"""Turn a list of specifier tuples into a string
:param List[Union[Specifier, str]] specs: a list of specifiers to format
:return: A string of specifiers
:rtype: str
"""
if specs:
@@ -212,7 +284,8 @@ def build_vcs_uri(
def convert_direct_url_to_url(direct_url):
# type: (AnyStr) -> AnyStr
"""
"""Converts direct URLs to standard, link-style URLs
Given a direct url as defined by *PEP 508*, convert to a :class:`~pip_shims.shims.Link`
compatible URL by moving the name and extras into an **egg_fragment**.
@@ -253,6 +326,8 @@ def convert_direct_url_to_url(direct_url):
def convert_url_to_direct_url(url, name=None):
# type: (AnyStr, Optional[AnyStr]) -> AnyStr
"""
Converts normal link-style URLs to direct urls.
Given a :class:`~pip_shims.shims.Link` compatible URL, convert to a direct url as
defined by *PEP 508* by extracting the name and extras from the **egg_fragment**.
@@ -303,7 +378,7 @@ def get_version(pipfile_entry):
if str(pipfile_entry) == "{}" or is_star(pipfile_entry):
return ""
elif hasattr(pipfile_entry, "keys") and "version" in pipfile_entry:
if hasattr(pipfile_entry, "keys") and "version" in pipfile_entry:
if is_star(pipfile_entry.get("version")):
return ""
return pipfile_entry.get("version", "").strip().lstrip("(").rstrip(")")
@@ -316,6 +391,8 @@ def get_version(pipfile_entry):
def strip_extras_markers_from_requirement(req):
# type: (TRequirement) -> TRequirement
"""
Strips extras markers from requirement instances.
Given a :class:`~packaging.requirements.Requirement` instance with markers defining
*extra == 'name'*, strip out the extras from the markers and return the cleaned
requirement
@@ -389,7 +466,6 @@ def get_pyproject(path):
:return: A 2 tuple of build requirements and the build backend
:rtype: Optional[Tuple[List[AnyStr], AnyStr]]
"""
if not path:
return
from vistir.compat import Path
@@ -519,8 +595,7 @@ def key_from_req(req):
def _requirement_to_str_lowercase_name(requirement):
"""
Formats a packaging.requirements.Requirement with a lowercase name.
"""Formats a packaging.requirements.Requirement with a lowercase name.
This is simply a copy of
https://github.com/pypa/packaging/blob/16.8/packaging/requirements.py#L109-L124
@@ -531,7 +606,6 @@ def _requirement_to_str_lowercase_name(requirement):
important stuff that should not be lower-cased (such as the marker). See
this issue for more information: https://github.com/pypa/pipenv/issues/2113.
"""
parts = [requirement.name.lower()]
if requirement.extras:
@@ -550,11 +624,15 @@ def _requirement_to_str_lowercase_name(requirement):
def format_requirement(ireq):
"""
"""Formats an `InstallRequirement` instance as a string.
Generic formatter for pretty printing InstallRequirements to the terminal
in a less verbose way than using its `__str__` method.
"""
:param :class:`InstallRequirement` ireq: A pip **InstallRequirement** instance.
:return: A formatted string for prettyprinting
:rtype: str
"""
if ireq.editable:
line = "-e {}".format(ireq.link)
else:
@@ -572,9 +650,13 @@ def format_requirement(ireq):
def format_specifier(ireq):
"""
Generic formatter for pretty printing the specifier part of
InstallRequirements to the terminal.
"""Generic formatter for pretty printing specifiers.
Pretty-prints specifiers from InstallRequirements for output to terminal.
:param :class:`InstallRequirement` ireq: A pip **InstallRequirement** instance.
:return: A string of specifiers in the given install requirement or <any>
:rtype: str
"""
# TODO: Ideally, this is carried over to the pip library itself
specs = ireq.specifier._specs if ireq.req is not None else []
@@ -583,8 +665,7 @@ def format_specifier(ireq):
def get_pinned_version(ireq):
"""
Get the pinned version of an InstallRequirement.
"""Get the pinned version of an InstallRequirement.
An InstallRequirement is considered pinned if:
@@ -602,7 +683,6 @@ def get_pinned_version(ireq):
Raises `TypeError` if the input is not a valid InstallRequirement, or
`ValueError` if the InstallRequirement is not pinned.
"""
try:
specifier = ireq.specifier
except AttributeError:
+15 -58
View File
@@ -1,51 +1,40 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import contextlib
import logging
import os
import sys
import pip_shims.shims
import six
import six.moves
import tomlkit
import vistir
from six.moves.urllib.parse import urlparse, urlsplit, urlunparse
from vistir.compat import Path
from vistir.path import create_tracked_tempdir, ensure_mkdir_p, is_valid_url
from vistir.path import ensure_mkdir_p, is_valid_url
from .environment import MYPY_RUNNING
# fmt: off
six.add_move(
six.MovedAttribute("Mapping", "collections", "collections.abc")
) # type: ignore # noqa # isort:skip
six.add_move(
six.MovedAttribute("Sequence", "collections", "collections.abc")
) # type: ignore # noqa # isort:skip
six.add_move(
six.MovedAttribute("Set", "collections", "collections.abc")
) # type: ignore # noqa # isort:skip
six.add_move(
six.MovedAttribute("ItemsView", "collections", "collections.abc")
) # type: ignore # noqa
six.add_move( # type: ignore
six.MovedAttribute("Mapping", "collections", "collections.abc") # type: ignore
) # noqa # isort:skip
six.add_move( # type: ignore
six.MovedAttribute("Sequence", "collections", "collections.abc") # type: ignore
) # noqa # isort:skip
six.add_move( # type: ignore
six.MovedAttribute("Set", "collections", "collections.abc") # type: ignore
) # noqa # isort:skip
six.add_move( # type: ignore
six.MovedAttribute("ItemsView", "collections", "collections.abc") # type: ignore
) # noqa
from six.moves import ItemsView, Mapping, Sequence, Set # type: ignore # noqa # isort:skip
# fmt: on
if MYPY_RUNNING:
from typing import (
Dict,
Any,
Optional,
Union,
Tuple,
List,
Iterable,
Generator,
Text,
TypeVar,
)
from typing import Dict, Any, Optional, Union, Tuple, List, Iterable, Text, TypeVar
STRING_TYPE = Union[bytes, str, Text]
S = TypeVar("S", bytes, str, Text)
@@ -169,14 +158,6 @@ def is_editable(pipfile_entry):
return False
def multi_split(s, split):
# type: (S, Iterable[S]) -> List[S]
"""Splits on multiple given separators."""
for r in split:
s = s.replace(r, "|")
return [i for i in s.split("|") if len(i) > 0]
def is_star(val):
# type: (PipfileType) -> bool
return (isinstance(val, six.string_types) and val == "*") or (
@@ -318,30 +299,6 @@ def _ensure_dir(path):
return path
@contextlib.contextmanager
def ensure_setup_py(base):
# type: (STRING_TYPE) -> Generator[None, None, None]
if not base:
base = create_tracked_tempdir(prefix="requirementslib-setup")
base_dir = Path(base)
if base_dir.exists() and base_dir.name == "setup.py":
base_dir = base_dir.parent
elif not (base_dir.exists() and base_dir.is_dir()):
base_dir = base_dir.parent
if not (base_dir.exists() and base_dir.is_dir()):
base_dir = base_dir.parent
setup_py = base_dir.joinpath("setup.py")
is_new = False if setup_py.exists() else True
if not setup_py.exists():
setup_py.write_text(u"")
try:
yield
finally:
if is_new:
setup_py.unlink()
_UNSET = object()
_REMAP_EXIT = object()
+2 -2
View File
@@ -37,7 +37,7 @@ if _scandir is None and ctypes is None:
warnings.warn("scandir can't find the compiled _scandir C module "
"or ctypes, using slow generic fallback")
__version__ = '1.9.0'
__version__ = '1.10.0'
__all__ = ['scandir', 'walk']
# Windows FILE_ATTRIBUTE constants for interpreting the
@@ -583,7 +583,7 @@ elif sys.platform.startswith(('linux', 'darwin', 'sunos5')) or 'bsd' in sys.plat
if _scandir is not None:
scandir = scandir_c
DirEntry = DirEntry_c
elif ctypes is not None:
elif ctypes is not None and have_dirent_d_type:
scandir = scandir_python
DirEntry = PosixDirEntry
else:

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