diff --git a/.gitignore b/.gitignore index b858aca..741f1b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,114 @@ -.env -.venv -__pycache__ - /build /dist /docs/_build /pack +htmlcov/ +.pytest_cache/ +.vscode/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so -*.egg-info +# Distribution / packaging +.Python +develop-eggs/ +downloads/ +eggs/ +.eggs/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -*.py[co] +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ .pytest_cache/ -.vscode/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json diff --git a/.travis.yml b/.travis.yml index 8e38f96..3acd3a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,8 @@ matrix: fast_finish: true install: - - "python -m pip install --upgrade pip pytest-timeout" - - "python -m pip install --upgrade -e .[tests]" + - "python -m pip install --upgrade pip setuptools pytest-timeout" + - "python -m pip install --upgrade -e .[tests,virtualenv]" script: - "python -m pytest -v -n 8 tests/" @@ -24,7 +24,7 @@ jobs: - stage: packaging python: "3.6" install: - - "python -m pip install --upgrade pip" + - "python -m pip install --upgrade pip setuptools" - "python -m pip install --upgrade check-manifest readme-renderer" script: - "python setup.py check -m -r -s" @@ -38,8 +38,7 @@ jobs: - stage: coverage python: "3.6" install: - - "python -m pip install --upgrade pip" - - "python -m pip install --upgrade -e .[tests]" - - "python -m pip install --upgrade pytest-timeout pytest-xdist pytest-cov" + - "python -m pip install --upgrade pip setuptools pytest-timeout pytest-cov pytest-xdist" + - "python -m pip install --upgrade -e .[tests,virtualenv]" script: - "pytest -n auto --timeout 300 --cov=passa --cov-report=term-missing --cov-report=xml --cov-report=html tests" diff --git a/Pipfile b/Pipfile index d81fba9..6356718 100644 --- a/Pipfile +++ b/Pipfile @@ -1,5 +1,5 @@ [packages] -passa = { editable = true, path = '.' } +passa = {editable = true,path = '.',extras = ['virtualenv']} # Override sdist-only dependency via TOMLkit to fix build. (sarugaku/passa#61) [packages.functools32] @@ -12,12 +12,13 @@ markers = "python_version < '3.0'" black = '*' invoke = '*' parver = '*' -passa = { editable = true, path = '.', extras = ['tests'] } +passa = {editable = true,path = '.',extras = ['tests']} sphinx = '*' sphinx-rtd-theme = '*' towncrier = '*' twine = '*' wheel = '*' +coverage = "<5.0" [scripts] passa-add = 'python -m passa.cli.add' @@ -25,7 +26,6 @@ passa-remove = 'python -m passa.cli.remove' passa-upgrade = 'python -m passa.cli.upgrade' passa-lock = 'python -m passa.cli.lock' passa-freeze = 'python -m passa.cli.freeze' - black = 'black src/passa/ --exclude "/(\.git|\.hg|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist)/"' build = 'inv build' changelog = 'towncrier' @@ -34,3 +34,6 @@ draft = 'towncrier --draft' release = 'inv release' tests = "pytest -v tests" upload = 'inv upload' + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index 624bd15..6dca024 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "98ad4ec51e7ea8861ce2e31ac6d39a134251d84a2a7b47f2053e905900312639" + "sha256": "2c60c86f5fbcfab3b4b94e06a793479341cd462c3868f40ca1efffd795ad9a20" }, "pipfile-spec": 6, "requires": {}, @@ -19,7 +19,6 @@ "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '3.6'", "version": "==1.4.3" }, "attrs": { @@ -27,7 +26,6 @@ "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '3.6'", "version": "==18.2.0" }, "backports-functools-lru-cache": { @@ -51,11 +49,17 @@ "markers": "python_version < '3.3' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.0.post1" }, + "cached-property": { + "hashes": [ + "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", + "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" + ], + "version": "==1.5.1" + }, "cerberus": { "hashes": [ "sha256:f5c2e048fb15ecb3c088d192164316093fcfa602a74b3386eefb2983aa7e800a" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2" }, "certifi": { @@ -63,7 +67,6 @@ "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2018.11.29" }, "chardet": { @@ -71,7 +74,6 @@ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "colorama": { @@ -79,21 +81,18 @@ "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' and sys_platform == 'win32' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and sys_platform == 'win32'", "version": "==0.4.1" }, "cursor": { "hashes": [ "sha256:8ee9fe5b925e1001f6ae6c017e93682583d2b4d1ef7130a26cfcdf1651c0032c" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "distlib": { "hashes": [ "sha256:57977cd7d9ea27986ec62f425630e4ddb42efe651ff80bc58ed8dbc3c7c21f19" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.8" }, "enum34": { @@ -111,39 +110,57 @@ "sha256:3bb3de3582cb27071cfb514f00ed784dc444b7f96dc21e140de65fe00585c95e", "sha256:41d5b64e70507d0c3ca742d68010a76060eea8a3d863e9b5130ab11a4a91aa0e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.0.1" }, "functools32": { "file": "https://github.com/sarugaku/functools32/releases/download/3.2.3-2/functools32-3.2.3.post2-py2.py3-none-any.whl", - "markers": "python_version < '3.0' or python_version < '3.0' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version < '3.0' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'" + "hashes": [ + "sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0", + "sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d" + ], + "markers": "python_version < '3.0'", + "version": "==3.2.3.post2" }, "idna": { "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "installer": { + "hashes": [ + "sha256:1ba23de573e9b95a8dcbd04fd026c40a64b77db0aadc48f28a844b4cb87479fe", + "sha256:f4f195c9b17ea7d2b631a758451485c6b080975349b4adebe45ef4bb022db069" + ], + "version": "==0.1.1" + }, + "mork": { + "hashes": [ + "sha256:13772edb4724915cf0cfa30d31426e0565487a3b2d7883b8468718eaed8ecfc2", + "sha256:b1b41bc31603eef1b50e42e75ae2d74d7a0d9ab46ea4d0dd1ba387a451870873" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7" + "version": "==0.1.4" }, - "importlib": { + "packagebuilder": { "hashes": [ - "sha256:b6ee7066fea66e35f8d0acee24d98006de1a0a8a94a8ce6efe73a9a23c8d9826" + "sha256:1e85c4e0e994322996b93cd6685c12834d30f3558889154f8e3de8fb1f3fd1e7", + "sha256:dc525d06ecd102db23ab421b879d7d27021d784ff933e33e8c411a53af5c9dbe" ], - "markers": "python_version < '2.7' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.4" + "version": "==0.1.0" }, "packaging": { "hashes": [ "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==18.0" }, "passa": { "editable": true, - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "extras": [ + "virtualenv" + ], "path": "." }, "pathlib2": { @@ -159,7 +176,6 @@ "sha256:cc663a438fdfe2e88d8d3c5ef2203ac858de34e31b6609b1fc505d611490a926", "sha256:f79bb08fb064dfc5b141204bfeb56a4141a6d504677fab4723036a464fc25cc1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3" }, "pip-shims": { @@ -167,7 +183,6 @@ "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" }, "plette": { @@ -178,7 +193,6 @@ "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" }, "pyparsing": { @@ -186,30 +200,33 @@ "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.0" }, "pytoml": { "hashes": [ "sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.1.20" }, + "recursive-monkey-patch": { + "hashes": [ + "sha256:546739fea5be2ea9f98b5ec44fafeb697b5cf9fdcda64a03422582ab03ee24c4", + "sha256:98922554e77f2e2c85a4f5d873a0f52efdc1b553f32444bd6c788b2ff583bf3e" + ], + "version": "==0.4.0" + }, "requests": { "hashes": [ - "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", - "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20.1" + "version": "==2.21.0" }, "requirementslib": { "hashes": [ "sha256:c2c00c7bd3bd4984c97d10cd4d143efbe33b5ed9e55961bea30ca7a9a4927289", "sha256:dc6b692e8dee03d6e90c29db1e337b0bf8152cce84a57f0fb4765e596afde4e0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.3.3" }, "resolvelib": { @@ -217,7 +234,6 @@ "sha256:6c4c6690b0bdd78bcc002e1a5d1b6abbde58c694a6ea1838f165b20d2c943db7", "sha256:8734e53271ef98f38a2c99324d5e7905bc00c97dc3fc5bb7d83c82a979e71c04" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.2" }, "scandir": { @@ -239,17 +255,16 @@ }, "six": { "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==1.11.0" + "version": "==1.12.0" }, "tomlkit": { "hashes": [ "sha256:d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2", "sha256:f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.3" }, "typing": { @@ -258,7 +273,7 @@ "sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", "sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a" ], - "markers": "python_version < '3.5' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version < '3.5' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "(python_version >= '2.7' and python_version < '2.8') or (python_version >= '3.4' and python_version < '3.5') and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or (python_version >= '2.7' and python_version < '2.8') or (python_version >= '3.4' and python_version < '3.5') and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.6.6" }, "urllib3": { @@ -266,9 +281,15 @@ "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" ], - "markers": "python_version < '4' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version < '4' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.24.1" }, + "virtualenv": { + "hashes": [ + "sha256:686176c23a538ecc56d27ed9d5217abd34644823d6391cbeb232f42bf722baad", + "sha256:f899fafcd92e1150f40c8215328be38ff24b519cd95357fa6e78e006c7638208" + ], + "version": "==16.1.0" + }, "vistir": { "extras": [ "spinner" @@ -277,7 +298,6 @@ "sha256:3a1020fb7be000b268af96641ced9ead844b1f75840c41e20e473647688fc630", "sha256:6d2005ad670f77bd9c9b5415c4e2a4a20dce5b0cf0e0d11598eb463b2e0ebe44" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.5" }, "wheel": { @@ -285,7 +305,6 @@ "sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6", "sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.32.3" }, "yaspin": { @@ -293,7 +312,6 @@ "sha256:36fdccc5e0637b5baa8892fe2c3d927782df7d504e9020f40eb2c1502518aa5a", "sha256:8e52bf8079a48e2a53f3dfeec9e04addb900c101d1591c85df69cf677d3237e7" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.14.0" } }, @@ -303,7 +321,6 @@ "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.7.12" }, "apipkg": { @@ -311,7 +328,6 @@ "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": { @@ -319,7 +335,6 @@ "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '3.6'", "version": "==1.4.3" }, "arpeggio": { @@ -327,7 +342,6 @@ "sha256:a5258b84f76661d558492fa87e42db634df143685a0e51802d59cae7daad8732", "sha256:dc5c0541e7cc2c6033dc0338133436abfac53655624784736e9bc8bd35e56583" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.9.0" }, "atomicwrites": { @@ -335,7 +349,6 @@ "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.1" }, "attrs": { @@ -343,7 +356,6 @@ "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '3.6'", "version": "==18.2.0" }, "babel": { @@ -351,7 +363,6 @@ "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": { @@ -388,14 +399,19 @@ "sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718", "sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.2" }, + "cached-property": { + "hashes": [ + "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", + "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" + ], + "version": "==1.5.1" + }, "cerberus": { "hashes": [ "sha256:f5c2e048fb15ecb3c088d192164316093fcfa602a74b3386eefb2983aa7e800a" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2" }, "certifi": { @@ -403,7 +419,6 @@ "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2018.11.29" }, "chardet": { @@ -411,7 +426,6 @@ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "click": { @@ -419,15 +433,46 @@ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "markers": "python_version >= '2.7' and python_version >= '3.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==7.0" }, + "cmarkgfm": { + "hashes": [ + "sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061", + "sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c", + "sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d", + "sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de", + "sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2", + "sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63", + "sha256:61a72def110eed903cd1848245897bcb80d295cd9d13944d4f9f30cba5b76655", + "sha256:64186fb75d973a06df0e6ea12879533b71f6e7ba1ab01ffee7fc3e7534758889", + "sha256:665303d34d7f14f10d7b0651082f25ebf7107f29ef3d699490cac16cdc0fc8ce", + "sha256:70b18f843aec58e4e64aadce48a897fe7c50426718b7753aaee399e72df64190", + "sha256:761ee7b04d1caee2931344ac6bfebf37102ffb203b136b676b0a71a3f0ea3c87", + "sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957", + "sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5", + "sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7", + "sha256:a3d17ee4ae739fe16f7501a52255c2e287ac817cfd88565b9859f70520afffea", + "sha256:ba5b5488719c0f2ced0aa1986376f7baff1a1653a8eb5fdfcf3f84c7ce46ef8d", + "sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f", + "sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1", + "sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034", + "sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65", + "sha256:cf5014eb214d814a83a7a47407272d5db10b719dbeaf4d3cfe5969309d0fcf4b", + "sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697", + "sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34", + "sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9", + "sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061", + "sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a", + "sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09", + "sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c" + ], + "version": "==0.4.2" + }, "colorama": { "hashes": [ "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' and sys_platform == 'win32' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and sys_platform == 'win32'", "version": "==0.4.1" }, "coverage": { @@ -464,21 +509,18 @@ "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" ], - "markers": "python_version < '4' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.5.2" }, "cursor": { "hashes": [ "sha256:8ee9fe5b925e1001f6ae6c017e93682583d2b4d1ef7130a26cfcdf1651c0032c" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "distlib": { "hashes": [ "sha256:57977cd7d9ea27986ec62f425630e4ddb42efe651ff80bc58ed8dbc3c7c21f19" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.8" }, "docutils": { @@ -504,7 +546,6 @@ "sha256:a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a", "sha256:fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.5.0" }, "first": { @@ -512,7 +553,6 @@ "sha256:3bb3de3582cb27071cfb514f00ed784dc444b7f96dc21e140de65fe00585c95e", "sha256:41d5b64e70507d0c3ca742d68010a76060eea8a3d863e9b5130ab11a4a91aa0e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.0.1" }, "funcsigs": { @@ -523,33 +563,38 @@ "markers": "python_version < '3.0' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.0.2" }, + "future": { + "hashes": [ + "sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb" + ], + "version": "==0.16.0" + }, + "html5lib": { + "hashes": [ + "sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", + "sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.1" + }, "functools32": { "file": "https://github.com/sarugaku/functools32/releases/download/3.2.3-2/functools32-3.2.3.post2-py2.py3-none-any.whl", "markers": "python_version < '3.0' or python_version < '3.0' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version < '3.0' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'" }, "idna": { "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7" + "version": "==2.8" }, "imagesize": { "hashes": [ "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" }, - "importlib": { - "hashes": [ - "sha256:b6ee7066fea66e35f8d0acee24d98006de1a0a8a94a8ce6efe73a9a23c8d9826" - ], - "markers": "python_version < '2.7' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.4" - }, "incremental": { "hashes": [ "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", @@ -557,6 +602,13 @@ ], "version": "==17.5.0" }, + "installer": { + "hashes": [ + "sha256:1ba23de573e9b95a8dcbd04fd026c40a64b77db0aadc48f28a844b4cb87479fe", + "sha256:f4f195c9b17ea7d2b631a758451485c6b080975349b4adebe45ef4bb022db069" + ], + "version": "==0.1.1" + }, "invoke": { "hashes": [ "sha256:4f4de934b15c2276caa4fbc5a3b8a61c0eb0b234f2be1780d2b793321995c2d6", @@ -603,7 +655,6 @@ "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "more-itertools": { @@ -612,15 +663,20 @@ "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.3.0" }, + "packagebuilder": { + "hashes": [ + "sha256:1e85c4e0e994322996b93cd6685c12834d30f3558889154f8e3de8fb1f3fd1e7", + "sha256:dc525d06ecd102db23ab421b879d7d27021d784ff933e33e8c411a53af5c9dbe" + ], + "version": "==0.1.0" + }, "packaging": { "hashes": [ "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==18.0" }, "parver": { @@ -628,15 +684,13 @@ "sha256:b8b2976fd8a73a0515465b2a265fd9b20cc25a6dc88bc1154fd5f60f10dad4db", "sha256:d9ae08a2629105fdb83e4971ae8a04f1de5a3803d1dd928f6e181aeadb398180" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.0" }, "passa": { "editable": true, "extras": [ - "tests" + "virtualenv" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "path": "." }, "pathlib2": { @@ -652,7 +706,6 @@ "sha256:cc663a438fdfe2e88d8d3c5ef2203ac858de34e31b6609b1fc505d611490a926", "sha256:f79bb08fb064dfc5b141204bfeb56a4141a6d504677fab4723036a464fc25cc1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3" }, "pip-shims": { @@ -660,7 +713,6 @@ "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" }, "pkginfo": { @@ -678,7 +730,6 @@ "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": { @@ -686,7 +737,6 @@ "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.8.0" }, "py": { @@ -694,9 +744,15 @@ "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.7.0" }, + "pycparser": { + "hashes": [ + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.19" + }, "pygments": { "hashes": [ "sha256:6301ecb0997a52d2d31385e62d0a4a4cf18d2f2da7054a5ddad5c366cd39cee7", @@ -709,7 +765,6 @@ "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.0" }, "pytest": { @@ -717,7 +772,6 @@ "sha256:1d131cc532be0023ef8ae265e2a779938d0619bb6c2510f52987ffcba7fa1ee4", "sha256:ca4761407f1acc85ffd1609f464ca20bb71a767803505bd4127d0e45c5a50e23" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.0.1" }, "pytest-cov": { @@ -725,7 +779,6 @@ "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pytest-forked": { @@ -733,7 +786,6 @@ "sha256:e4500cd0509ec4a26535f7d4112a8cc0f17d3a41c29ffd4eab479d2a55b30805", "sha256:f275cb48a73fc61a6710726348e1da6d68a978f0ec0c54ece5a5fae5977e5a08" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2" }, "pytest-timeout": { @@ -741,7 +793,6 @@ "sha256:4a30ba76837a32c7b7cd5c84ee9933fde4b9022b0cd20ea7d4a577c2a1649fb1", "sha256:d49f618c6448c14168773b6cdda022764c63ea80d42274e3156787e8088d04c6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.3.3" }, "pytest-xdist": { @@ -749,14 +800,12 @@ "sha256:5e8b68466c057f0f37e36909612f8838e518ce703c8da31f85e47c7dea8acc93", "sha256:909bb938bdb21e68a28a8d58c16a112b30da088407b678633efb01067e3923de" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.24.1" }, "pytoml": { "hashes": [ "sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.1.20" }, "pytz": { @@ -764,7 +813,6 @@ "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2018.7" }, "readme-renderer": { @@ -774,13 +822,19 @@ ], "version": "==24.0" }, + "recursive-monkey-patch": { + "hashes": [ + "sha256:546739fea5be2ea9f98b5ec44fafeb697b5cf9fdcda64a03422582ab03ee24c4", + "sha256:98922554e77f2e2c85a4f5d873a0f52efdc1b553f32444bd6c788b2ff583bf3e" + ], + "version": "==0.4.0" + }, "requests": { "hashes": [ - "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", - "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20.1" + "version": "==2.21.0" }, "requests-toolbelt": { "hashes": [ @@ -794,7 +848,6 @@ "sha256:c2c00c7bd3bd4984c97d10cd4d143efbe33b5ed9e55961bea30ca7a9a4927289", "sha256:dc6b692e8dee03d6e90c29db1e337b0bf8152cce84a57f0fb4765e596afde4e0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.3.3" }, "resolvelib": { @@ -802,39 +855,20 @@ "sha256:6c4c6690b0bdd78bcc002e1a5d1b6abbde58c694a6ea1838f165b20d2c943db7", "sha256:8734e53271ef98f38a2c99324d5e7905bc00c97dc3fc5bb7d83c82a979e71c04" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.2" }, - "scandir": { - "hashes": [ - "sha256:04b8adb105f2ed313a7c2ef0f1cf7aff4871aa7a1883fa4d8c44b5551ab052d6", - "sha256:1444134990356c81d12f30e4b311379acfbbcd03e0bab591de2696a3b126d58e", - "sha256:1b5c314e39f596875e5a95dd81af03730b338c277c54a454226978d5ba95dbb6", - "sha256:346619f72eb0ddc4cf355ceffd225fa52506c92a2ff05318cfabd02a144e7c4e", - "sha256:44975e209c4827fc18a3486f257154d34ec6eaec0f90fef0cca1caa482db7064", - "sha256:61859fd7e40b8c71e609c202db5b0c1dbec0d5c7f1449dec2245575bdc866792", - "sha256:a5e232a0bf188362fa00123cc0bb842d363a292de7126126df5527b6a369586a", - "sha256:c14701409f311e7a9b7ec8e337f0815baf7ac95776cc78b419a1e6d49889a383", - "sha256:c7708f29d843fc2764310732e41f0ce27feadde453261859ec0fca7865dfc41b", - "sha256:c9009c527929f6e25604aec39b0a43c3f831d2947d89d6caaab22f057b7055c8", - "sha256:f5c71e29b4e2af7ccdc03a020c626ede51da471173b4a6ad1e904f2b2e04b4bd" - ], - "markers": "python_version < '3.5' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version < '3.5' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.9.0" - }, "six": { "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==1.11.0" + "version": "==1.12.0" }, "snowballstemmer": { "hashes": [ "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.1" }, "sphinx": { @@ -842,7 +876,6 @@ "sha256:120732cbddb1b2364471c3d9f8bfd4b0c5b550862f99a65736c77f970b142aea", "sha256:b348790776490894e0424101af9c8413f2a86831524bd55c5f379d3e3e12ca64" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.8.2" }, "sphinx-rtd-theme": { @@ -857,7 +890,6 @@ "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "toml": { @@ -872,7 +904,6 @@ "sha256:d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2", "sha256:f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.3" }, "towncrier": { @@ -887,7 +918,6 @@ "sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392", "sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1'", "version": "==4.28.1" }, "twine": { @@ -903,7 +933,7 @@ "sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", "sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a" ], - "markers": "python_version < '3.5' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version < '3.5' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "(python_version >= '2.7' and python_version < '2.8') or (python_version >= '3.4' and python_version < '3.5') and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or (python_version >= '2.7' and python_version < '2.8') or (python_version >= '3.4' and python_version < '3.5') and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.6.6" }, "urllib3": { @@ -911,7 +941,6 @@ "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" ], - "markers": "python_version < '4' and python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version < '4' and python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.24.1" }, "vistir": { @@ -922,7 +951,6 @@ "sha256:3a1020fb7be000b268af96641ced9ead844b1f75840c41e20e473647688fc630", "sha256:6d2005ad670f77bd9c9b5415c4e2a4a20dce5b0cf0e0d11598eb463b2e0ebe44" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.5" }, "webencodings": { @@ -930,7 +958,6 @@ "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.1" }, "wheel": { @@ -938,7 +965,6 @@ "sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6", "sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' or python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.32.3" }, "yaspin": { @@ -946,7 +972,6 @@ "sha256:36fdccc5e0637b5baa8892fe2c3d927782df7d504e9020f40eb2c1502518aa5a", "sha256:8e52bf8079a48e2a53f3dfeec9e04addb900c101d1591c85df69cf677d3237e7" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.14.0" } } diff --git a/appveyor.yml b/appveyor.yml index 3216d26..a9ecca6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,8 +5,8 @@ branches: install: - "SET PATH=C:\\Python36-x64;%PATH%" - "python --version" - - "python -m pip install --upgrade pip" - - "python -m pip install --upgrade -e .[pack,tests]" + - "python -m pip install --upgrade pip setuptools pytest-timeout pytest-xdist" + - "python -m pip install --upgrade -e .[pack,tests,virtualenv]" build_script: - "python -m invoke pack" diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..8213302 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx +sphinx_rtd_theme diff --git a/setup.cfg b/setup.cfg index 98ea198..fbd97bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,13 +37,17 @@ python_requires = >=2.7,!=3.0,!=3.1,!=3.2,!=3.3 setup_requires = setuptools>=36.2.2 install_requires = appdirs - distlib + cached-property + distlib>=0.2.8 + installer + packagebuilder packaging - pip-shims>=0.1.2 + pip-shims>=0.3.1 plette[validation]>=0.2.2 + recursive-monkey-patch requests + requirementslib>=1.1.7 resolvelib>=0.2.1,!=1.0.0.dev0 - requirementslib>=1.1.1 six vistir[spinner]>=0.1.4 @@ -51,6 +55,8 @@ install_requires = pack = invoke parver +virtualenv = + mork tests = pytest-xdist pytest-timeout @@ -89,13 +95,21 @@ ignore = # E231: missing whitespace after ',' # E402: module level import not at top of file # E501: line too long - E127,E128,E129,E222,E231,E402,E501 + E231,E402,E501 [tool:pytest] strict = true addopts = -ra testpaths = tests/ norecursedirs = .* build dist news tasks docs +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning [build-system] requires = ["setuptools", "wheel"] + +[mypy] +ignore_missing_imports=true +follow_imports=skip +python_version=2.7 diff --git a/src/passa/actions/add.py b/src/passa/actions/add.py index 6338466..19471de 100644 --- a/src/passa/actions/add.py +++ b/src/passa/actions/add.py @@ -47,8 +47,7 @@ def add_packages(packages=[], editables=[], project=None, dev=False, sync=False, develop = any(lockfile_diff.develop) syncer = Synchronizer( - project, default=default, develop=develop, - clean_unneeded=clean, + project, default=default, develop=develop, clean_unneeded=clean ) success = sync(syncer) if not success: diff --git a/src/passa/actions/clean.py b/src/passa/actions/clean.py index 3570e4d..9006f22 100644 --- a/src/passa/actions/clean.py +++ b/src/passa/actions/clean.py @@ -3,14 +3,15 @@ from __future__ import absolute_import, print_function, unicode_literals -def clean(project, dev=False): +def clean(project, default=True, dev=False, sync=True): from passa.models.synchronizers import Cleaner from passa.operations.sync import clean - cleaner = Cleaner(project, default=True, develop=dev) + cleaner = Cleaner(project, default=default, develop=dev, sync=sync) success = clean(cleaner) if not success: return 1 - print("Cleaned project at", project.root) + if sync: + print("Cleaned project at", project.root) diff --git a/src/passa/actions/init.py b/src/passa/actions/init.py index 1d9f592..bbab009 100644 --- a/src/passa/actions/init.py +++ b/src/passa/actions/init.py @@ -42,7 +42,7 @@ def init_project(root=None, python_version=None): index_urls = [parsed.index_url] + parsed.extra_index_urls sources = get_sources(index_urls, parsed.trusted_hosts) data = { - "sources": sources, + "source": sources, "packages": {}, "dev-packages": {}, } diff --git a/src/passa/actions/install.py b/src/passa/actions/install.py index 1728dae..9872584 100644 --- a/src/passa/actions/install.py +++ b/src/passa/actions/install.py @@ -30,3 +30,4 @@ def install(project=None, check=True, dev=False, clean=True): return 1 print("Synchronized project at", project.root) + return 0 diff --git a/src/passa/actions/lock.py b/src/passa/actions/lock.py index 7c09469..f661c82 100644 --- a/src/passa/actions/lock.py +++ b/src/passa/actions/lock.py @@ -11,7 +11,8 @@ def lock(project=None): locker = BasicLocker(project) success = lock(locker) if not success: - return + return 1 project._l.write() print("Written to project at", project.root) + return 0 diff --git a/src/passa/actions/remove.py b/src/passa/actions/remove.py index 158f5e6..92f6168 100644 --- a/src/passa/actions/remove.py +++ b/src/passa/actions/remove.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, print_function, unicode_literals -def remove(project=None, only="default", packages=[], clean=True): +def remove(project=None, only="default", packages=[], clean=True, sync=False): from passa.models.lockers import PinReuseLocker from passa.operations.lock import lock @@ -36,3 +36,4 @@ def remove(project=None, only="default", packages=[], clean=True): return 1 print("Cleaned project at", project.root) + return 0 diff --git a/src/passa/cli/add.py b/src/passa/cli/add.py index d5596cd..2635b98 100644 --- a/src/passa/cli/add.py +++ b/src/passa/cli/add.py @@ -4,14 +4,14 @@ from ..actions.add import add_packages from ._base import BaseCommand -from .options import package_group +from .options import package_group, clean_group class Command(BaseCommand): name = "add" description = "Add packages to project." - arguments = [package_group] + arguments = [package_group, clean_group] def run(self, options): if not options.editables and not options.packages: @@ -20,7 +20,8 @@ def run(self, options): packages=options.packages, editables=options.editables, project=options.project, - dev=options.dev + dev=options.dev, + clean=options.clean ) diff --git a/src/passa/cli/clean.py b/src/passa/cli/clean.py index e23d5ee..a74d814 100644 --- a/src/passa/cli/clean.py +++ b/src/passa/cli/clean.py @@ -4,17 +4,20 @@ from ..actions.clean import clean from ._base import BaseCommand -from .options import dev, no_default +from .options import dev, no_default, sync_group class Command(BaseCommand): name = "clean" description = "Uninstall unlisted packages from the environment." - arguments = [dev, no_default] + arguments = [dev, no_default, sync_group] def run(self, options): - return clean(project=options.project, default=options.default, dev=options.dev) + return clean( + project=options.project, default=options.default, dev=options.dev, + sync=options.sync + ) if __name__ == "__main__": diff --git a/src/passa/cli/options.py b/src/passa/cli/options.py index f20b612..bc75db6 100644 --- a/src/passa/cli/options.py +++ b/src/passa/cli/options.py @@ -2,14 +2,18 @@ from __future__ import absolute_import import argparse +import inspect import os import sys +import six import tomlkit.exceptions import passa.models.projects import vistir +from ..models.environments import Environment + PYTHON_VERSION = ".".join(str(v) for v in sys.version_info[:2]) @@ -18,25 +22,69 @@ class Project(passa.models.projects.Project): def __init__(self, root, *args, **kwargs): root = vistir.compat.Path(root).absolute() pipfile = root.joinpath("Pipfile") + environment = kwargs.pop("environment", self.get_env()) if not pipfile.is_file(): raise argparse.ArgumentError( "project", "{0!r} is not a Pipfile project".format(root), ) try: - super(Project, self).__init__(root.as_posix(), *args, **kwargs) + super(Project, self).__init__(root.as_posix(), environment=environment, + *args, **kwargs) except tomlkit.exceptions.ParseError as e: raise argparse.ArgumentError( "project", "failed to parse Pipfile: {0!r}".format(str(e)), ) + def get_env(self): + if 'VIRTUAL_ENV' in os.environ: + return Environment(prefix=os.environ['VIRTUAL_ENV'], is_venv=True) + return Environment() + def __name__(self): return "Project Root" +class OptionMeta(type): + + @property + def action_map(self): + action_map = getattr(self, '_action_map', None) + if not action_map: + self.action_map = { + name.strip("_").replace("Action", ""): obj + for name, obj in inspect.getmembers(argparse) + if name.startswith('_') and name.endswith('Action') + } + return self._action_map + + @action_map.setter + def action_map(self, action_map): + self._action_map = action_map + + +@six.add_metaclass(OptionMeta) class Option(object): def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs + self.args = list(args) + self.kwargs = kwargs.copy() + if "dest" not in kwargs and not args[0].startswith("-"): + dest = list(args).pop(0) + else: + dest = kwargs.pop("dest", args[0].lstrip("-").replace("-", "_")) + action = kwargs.pop("action", None) + if not action: + if 'const' in kwargs: + action = 'store_const' + else: + action = 'store' + self.action = self.get_option(action, args, dest, **kwargs) + + @classmethod + def get_option(cls, action, option_strings, dest, *args, **kwargs): + if action: + action = action.title().replace("_", "") + return cls.action_map[action](list(option_strings), dest, *args, **kwargs) + return def add_to_parser(self, parser): parser.add_argument(*self.args, **self.kwargs) @@ -68,6 +116,9 @@ def add_to_parser(self, parser): self.argument_group = group self.parser = parser + def add_to_group(self, group): + self.add_to_parser(group) + project = Option( "--project", metavar="project", default=os.getcwd(), type=Project, @@ -80,7 +131,7 @@ def add_to_parser(self, parser): ) python_version = Option( - "--py-version", "--python-version", "--requires-python", metavar="python-version", + "--py-version", "--python-version", "--requires-python", metavar="python_version", dest="python_version", default=PYTHON_VERSION, type=str, help="required minor python version for the project" ) @@ -105,6 +156,11 @@ def add_to_parser(self, parser): help="do not synchronize the environment", ) +sync = Option( + "--sync", dest="sync", action="store_true", help="synchronize the environment", + default=False +) + target = Option( "-t", "--target", default=None, help="file to export into (default is to print to stdout)" @@ -135,6 +191,10 @@ def add_to_parser(self, parser): help="do not remove packages not specified in Pipfile.lock", ) +clean = Option( + "--clean", dest="clean", action="store_true", default=False, + help="remove packages not specified in Pipfile.lock", +) dev_only = Option( "--dev", dest="only", action="store_const", const="dev", help="only try to modify [dev-packages]", @@ -152,5 +212,7 @@ def add_to_parser(self, parser): include_hashes_group = ArgumentGroup("include_hashes", is_mutually_exclusive=True, options=[include_hashes, no_include_hashes]) dev_group = ArgumentGroup("dev", is_mutually_exclusive="True", options=[dev_only, default_only]) -package_group = ArgumentGroup("packages", options=[packages, editable, dev, no_sync]) new_project_group = ArgumentGroup("new-project", options=[new_project, python_version]) +clean_group = ArgumentGroup("clean", is_mutually_exclusive=True, options=[clean, no_clean]) +sync_group = ArgumentGroup("sync", is_mutually_exclusive=True, options=[sync, no_sync]) +package_group = ArgumentGroup("packages", options=[packages, editable, dev, sync_group]) diff --git a/src/passa/cli/remove.py b/src/passa/cli/remove.py index 538acbf..041c195 100644 --- a/src/passa/cli/remove.py +++ b/src/passa/cli/remove.py @@ -4,18 +4,18 @@ from ..actions.remove import remove from ._base import BaseCommand -from .options import dev_group, no_clean, packages +from .options import dev_group, clean_group, sync_group, packages class Command(BaseCommand): name = "remove" description = "Remove packages from project." - arguments = [dev_group, no_clean, packages] + arguments = [dev_group, clean_group, sync_group, packages] def run(self, options): return remove(project=options.project, only=options.only, - packages=options.packages, clean=options.clean) + packages=options.packages, clean=options.clean, sync=options.sync) if __name__ == "__main__": diff --git a/src/passa/cli/sync.py b/src/passa/cli/sync.py index a09b784..9a31fe1 100644 --- a/src/passa/cli/sync.py +++ b/src/passa/cli/sync.py @@ -4,14 +4,14 @@ from ..actions.sync import sync from ._base import BaseCommand -from .options import dev, no_clean +from .options import dev, clean_group class Command(BaseCommand): name = "sync" description = "Install Pipfile.lock into the environment." - arguments = [dev, no_clean] + arguments = [dev, clean_group] def run(self, options): return sync(project=options.project, dev=options.dev, clean=options.clean) diff --git a/src/passa/cli/upgrade.py b/src/passa/cli/upgrade.py index cf7f502..c7696c2 100644 --- a/src/passa/cli/upgrade.py +++ b/src/passa/cli/upgrade.py @@ -3,14 +3,14 @@ from ..actions.upgrade import upgrade from ._base import BaseCommand -from .options import no_clean, no_sync, packages, strategy +from .options import clean_group, sync_group, packages, strategy class Command(BaseCommand): name = "upgrade" description = "Upgrade packages in project." - arguments = [packages, strategy, no_clean, no_sync] + arguments = [packages, strategy, clean_group, sync_group] def run(self, options): return upgrade(project=options.project, strategy=options.strategy, diff --git a/src/passa/internals/_pip.py b/src/passa/internals/_pip.py index 2aa143a..7b1a067 100644 --- a/src/passa/internals/_pip.py +++ b/src/passa/internals/_pip.py @@ -7,18 +7,24 @@ import itertools import distutils.log import os +import re import distlib.database +import distlib.metadata import distlib.scripts import distlib.wheel import packaging.utils import pip_shims -import setuptools.dist import six +import sys +import sysconfig import vistir from ..models.caches import CACHE_DIR -from ._pip_shims import VCS_SUPPORT, build_wheel as _build_wheel, unpack_url +from ..models.environments import Environment +from ._pip_shims import ( + SETUPTOOLS_SHIM, VCS_SUPPORT, build_wheel as _build_wheel, unpack_url +) from .utils import filter_sources @@ -87,21 +93,21 @@ def _get_pip_session(trusted_hosts): options, _ = cmd.parser.parse_args([]) options.cache_dir = CACHE_DIR options.trusted_hosts = trusted_hosts - session = cmd._build_session(options) - return session + return cmd._build_session(options) +@contextlib.contextmanager def _get_finder(sources): index_urls, trusted_hosts = _get_pip_index_urls(sources) - session = _get_pip_session(trusted_hosts) - finder = pip_shims.PackageFinder( - find_links=[], - index_urls=index_urls, - trusted_hosts=trusted_hosts, - allow_all_prereleases=True, - session=session, - ) - return finder + with contextlib.closing(_get_pip_session(trusted_hosts)) as session: + finder = pip_shims.PackageFinder( + find_links=[], + index_urls=index_urls, + trusted_hosts=trusted_hosts, + allow_all_prereleases=True, + session=session, + ) + yield finder def _get_wheel_cache(): @@ -134,7 +140,7 @@ class WheelBuildError(RuntimeError): pass -def build_wheel(ireq, sources, hashes=None): +def build_wheel(ireq, sources, finder, hashes=None): """Build a wheel file for the InstallRequirement object. An artifact is downloaded (or read from cache). If the artifact is not a @@ -148,7 +154,6 @@ def build_wheel(ireq, sources, hashes=None): `RuntimeError` subclass) if the wheel cannot be built. """ kwargs = _prepare_wheel_building_kwargs(ireq) - finder = _get_finder(sources) # Not for upgrade, hash not required. Hashes are not required here even # when we provide them, because pip skips local wheel cache if we set it @@ -196,29 +201,15 @@ def build_wheel(ireq, sources, hashes=None): return distlib.wheel.Wheel(wheel_path) -def _obtrain_ref(vcs_obj, src_dir, name, rev=None): - target_dir = os.path.join(src_dir, name) - target_rev = vcs_obj.make_rev_options(rev) - if not os.path.exists(target_dir): - vcs_obj.obtain(target_dir) - if (not vcs_obj.is_commit_id_equal(target_dir, rev) and - not vcs_obj.is_commit_id_equal(target_dir, target_rev)): - vcs_obj.update(target_dir, target_rev) - return vcs_obj.get_revision(target_dir) - - def get_vcs_ref(requirement): - backend = VCS_SUPPORT.get_backend(requirement.vcs) - vcs = backend(url=requirement.req.vcs_uri) - src = _get_src_dir() - name = requirement.normalized_name - ref = _obtrain_ref(vcs, src, name, rev=requirement.req.ref) - return ref + return requirement.commit_hash def find_installation_candidates(ireq, sources): - finder = _get_finder(sources) - return finder.find_all_candidates(ireq.name) + candidates = [] + with _get_finder(sources) as finder: + candidates = finder.find_all_candidates(ireq.name) + return candidates class RequirementUninstaller(object): @@ -227,17 +218,24 @@ class RequirementUninstaller(object): This uses `UninstallPathSet` to control the workflow. If the inner block exits correctly, the uninstallation is committed, otherwise rolled back. """ - def __init__(self, ireq, auto_confirm, verbose): + def __init__(self, ireq, auto_confirm, verbose, env=None): self.ireq = ireq self.pathset = None self.auto_confirm = auto_confirm self.verbose = verbose + self.env = env if env else Environment() + + def check_permitted(self, pathset, path): + if self.env.is_venv and self.env.is_installed(self.ireq.name): + return True + return pathset._permitted(path) def __enter__(self): self.pathset = self.ireq.uninstall( auto_confirm=self.auto_confirm, verbose=self.verbose, ) + self.pathset._permitted = self.check_permitted return self.pathset def __exit__(self, exc_type, exc_value, traceback): @@ -282,6 +280,7 @@ class NoopInstaller(object): arguments, and should be called in that order to prepare an installation operation, and to actually install things. """ + def prepare(self): pass @@ -289,39 +288,172 @@ def install(self): pass -class EditableInstaller(NoopInstaller): - """Installer to handle editable. - """ - def __init__(self, requirement): - ireq = requirement.as_ireq() - self.working_directory = ireq.setup_py_dir - self.setup_py = ireq.setup_py - - def install(self): - with vistir.cd(self.working_directory), _suppress_distutils_logs(): - # Access from Setuptools to ensure things are patched correctly. - setuptools.dist.distutils.core.run_setup( - self.setup_py, ["develop", "--no-deps"], - ) - +dist_info_re = re.compile(r"""^(?P(?P.+?)(-(?P.+?))?) + \.dist-info$""", re.VERBOSE) -class WheelInstaller(NoopInstaller): - """Installer by building a wheel. - The wheel is built during `prepare()`, and installed in `install()`. +def root_is_purelib(name, wheeldir): """ - def __init__(self, requirement, sources, paths): + Return True if the extracted wheel in wheeldir should go into purelib. + """ + name_folded = name.replace("-", "_") + for item in os.listdir(wheeldir): + match = dist_info_re.match(item) + if match and match.group('name') == name_folded: + with open(os.path.join(wheeldir, item, 'WHEEL')) as wheel: + for line in wheel: + line = line.lower().rstrip() + if line == "root-is-purelib: true": + return True + return False + + +def get_entrypoints(filename): + import pkg_resources + if not os.path.exists(filename): + return {}, {} + + # This is done because you can pass a string to entry_points wrappers which + # means that they may or may not be valid INI files. The attempt here is to + # strip leading and trailing whitespace in order to make them valid INI + # files. + with open(filename) as fp: + data = io.StringIO() + for line in fp: + data.write(line.strip()) + data.write("\n") + data.seek(0) + + # get the entry points and then the script names + entry_points = pkg_resources.EntryPoint.parse_map(data) + console = entry_points.get('console_scripts', {}) + gui = entry_points.get('gui_scripts', {}) + + def _split_ep(s): + """get the string representation of EntryPoint, remove space and split + on '='""" + return str(s).replace(" ", "").split("=") + + # convert the EntryPoint objects into strings with module:function + console = dict(_split_ep(v) for v in console.values()) + gui = dict(_split_ep(v) for v in gui.values()) + return console, gui + + +class BaseInstaller(NoopInstaller): + """Virtualenv-capable installer""" + + def __init__(self, requirement, sources=None, environment=None): self.ireq = requirement.as_ireq() self.sources = filter_sources(requirement, sources) self.hashes = requirement.hashes or None - self.paths = paths - self.wheel = None + self.environment = environment if environment else Environment() + self.built = None + self.metadata = None + self.is_wheel = False + + @property + def src_dir(self): + build_dir = os.environ.get("PASSA_BUILD_DIR", None) + if not build_dir: + build_dir = vistir.path.create_tracked_tempdir("passa-build-dir") + return build_dir + + @property + def setup_dir(self): + if not self.built: + return self.ireq.setup_py_dir + return vistir.compat.Path(self.built.path).parent + + @property + def installation_args(self): + install_arg = "install" if not self.ireq.editable else "develop" + setup_path = self.setup_dir.joinpath("setup.py") + install_keys = ["headers", "purelib", "platlib", "scripts", "data"] + install_args = [ + self.environment.python, "-u", "-c", SETUPTOOLS_SHIM % setup_path.as_posix(), + install_arg, "--single-version-externally-managed", "--no-deps", + "--prefix={0}".format(self.environment.paths["prefix"]) + ] + for key in install_keys: + install_args.append( + "--install-{0}={1}".format(key, self.environment.paths[key]) + ) + return install_args + + def build_wheel(self): + with _get_finder(self.sources) as finder: + self.built = build_wheel(self.ireq, self.sources, finder, self.hashes) + self.metadata = self.built.metadata + self.is_wheel = True + + def build_sdist(self): + with _get_finder(self.sources) as finder: + self.ireq.populate_link(finder, False, False) + self.ireq.ensure_has_source_dir(self.src_dir) + self.built = get_sdist(self.ireq) + self.metadata = read_sdist_metadata(self.ireq) + + def install_wheel(self): + scripts = distlib.scripts.ScriptMaker(None, None) + self.built.install(self.environment.paths, scripts) + + def install_sdist(self): + with vistir.cd(self.setup_dir.as_posix()), _suppress_distutils_logs(): + c = self.environment.run( + self.installation_args, return_object=True, block=True, nospin=True, + combine_stderr=False, write_to_stdout=False + ) + if c.returncode != 0: + err_text = "{0!r}: {1!r}".format(c.err, c.out) + raise RuntimeError("Failed to install package: {0!r}".format(err_text)) + return def prepare(self): - self.wheel = build_wheel(self.ireq, self.sources, self.hashes) + pass def install(self): - self.wheel.install(self.paths, distlib.scripts.ScriptMaker(None, None)) + with self.environment.activated(): + self._install() + + +class SdistInstaller(BaseInstaller): + """Installer for SDists""" + def __init__(self, *args, **kwargs): + super(SdistInstaller, self).__init__(*args, **kwargs) + + def prepare(self): + try: + self.build_wheel() + except (WheelBuildError, distlib.metadata.MetadataConflictError): + self.build_sdist() + if not self.built or not self.metadata: + raise + + def _install(self): + if self.is_wheel: + self.install_wheel() + else: + self.install_sdist() + + +class Installer(SdistInstaller): + """Installer to handle editable. + """ + def __init__(self, *args, **kwargs): + super(Installer, self).__init__(*args, **kwargs) + + @property + def src_dir(self): + build_dir = os.environ.get("PIP_SRC", None) + venv = os.environ.get("VIRTUAL_ENV", None) + if venv: + src_dir = os.path.join(venv, "src") + if os.path.exists(src_dir): + build_dir = src_dir + if not build_dir: + build_dir = vistir.path.create_tracked_tempdir("passa-build-dir") + return build_dir def _iter_egg_info_directories(root, name): @@ -389,9 +521,15 @@ def _find_egg_info(ireq): return top_egg_info -def read_sdist_metadata(ireq): +def get_sdist(ireq): egg_info_dir = _find_egg_info(ireq) if not egg_info_dir: return None - distribution = distlib.database.EggInfoDistribution(egg_info_dir) - return distribution.metadata + return distlib.database.EggInfoDistribution(egg_info_dir) + + +def read_sdist_metadata(ireq): + sdist = get_sdist(ireq) + if not sdist: + return None + return sdist.metadata diff --git a/src/passa/internals/_pip_shims.py b/src/passa/internals/_pip_shims.py index b2c7b6e..87d0904 100644 --- a/src/passa/internals/_pip_shims.py +++ b/src/passa/internals/_pip_shims.py @@ -11,7 +11,19 @@ from __future__ import absolute_import, unicode_literals +import distlib.metadata +import importlib import pip_shims +import recursive_monkey_patch + + +class LegacyMetadata(object): + def set_metadata_version(self): + metadata_version = self._fields.get("Metadata-Version") + if metadata_version == "2.1": + self._fields["Metadata-Version"] = metadata_version + else: + self._fields['Metadata-Version'] = distlib.metadata._best_version(self._fields) def _build_wheel_pre10(ireq, output_dir, finder, wheel_cache, kwargs): @@ -47,8 +59,8 @@ def _unpack_url_pre10(*args, **kwargs): return pip_shims.unpack_url(*args, **kwargs) -PIP_VERSION = pip_shims.utils._parse(pip_shims.pip_version) -VERSION_10 = pip_shims.utils._parse("10") +PIP_VERSION = pip_shims._parse(pip_shims.pip_version) +VERSION_10 = pip_shims._parse("10") VCS_SUPPORT = pip_shims.VcsSupport() @@ -59,3 +71,15 @@ def _unpack_url_pre10(*args, **kwargs): if PIP_VERSION < VERSION_10: build_wheel = _build_wheel_pre10 unpack_url = _unpack_url_pre10 + + +SETUPTOOLS_SHIM = ( + "import setuptools, tokenize;__file__=%r;" + "f=getattr(tokenize, 'open', open)(__file__);" + "code=f.read().replace('\\r\\n', '\\n');" + "f.close();" + "exec(compile(code, __file__, 'exec'))" +) + + +recursive_monkey_patch.monkey_patch(LegacyMetadata, distlib.metadata.LegacyMetadata) diff --git a/src/passa/internals/dependencies.py b/src/passa/internals/dependencies.py index 410a5e6..6c74b47 100644 --- a/src/passa/internals/dependencies.py +++ b/src/passa/internals/dependencies.py @@ -10,11 +10,12 @@ import packaging.utils import packaging.version import requests -import requirementslib import six +import requirementslib + from ..models.caches import DependencyCache, RequiresPythonCache -from ._pip import WheelBuildError, build_wheel, read_sdist_metadata +from ._pip import WheelBuildError, build_wheel, get_sdist, read_sdist_metadata from .markers import contains_extra, get_contained_extras, get_without_extra from .utils import get_pinned_version, is_pinned @@ -140,20 +141,20 @@ def _get_dependencies_from_json(ireq, sources): if proc_url.endswith("/simple") ] - session = requests.session() - - for prefix in url_prefixes: - url = "{prefix}/pypi/{name}/{version}/json".format( - prefix=prefix, - name=packaging.utils.canonicalize_name(ireq.name), - version=version, - ) - try: - dependencies = _get_dependencies_from_json_url(url, session) - if dependencies is not None: - return dependencies - except Exception as e: - print("unable to read dependencies via {0} ({1})".format(url, e)) + with requests.session() as session: + + for prefix in url_prefixes: + url = "{prefix}/pypi/{name}/{version}/json".format( + prefix=prefix, + name=packaging.utils.canonicalize_name(ireq.name), + version=version, + ) + try: + dependencies = _get_dependencies_from_json_url(url, session) + if dependencies is not None: + return dependencies + except Exception as e: + print("unable to read dependencies via {0} ({1})".format(url, e)) return @@ -225,17 +226,18 @@ def _get_dependencies_from_pip(ireq, sources): """ extras = ireq.extras or () try: - wheel = build_wheel(ireq, sources) + built = build_wheel(ireq, sources) except WheelBuildError: # XXX: This depends on a side effect of `build_wheel`. This block is # reached when it fails to build an sdist, where the sdist would have # been downloaded, extracted into `ireq.source_dir`, and partially # built (hopefully containing .egg-info). + built = get_sdist(ireq) metadata = read_sdist_metadata(ireq) if not metadata: raise else: - metadata = wheel.metadata + metadata = built.metadata requirements = _read_requirements(metadata, extras) requires_python = _read_requires_python(metadata) return requirements, requires_python diff --git a/src/passa/internals/utils.py b/src/passa/internals/utils.py index 8f8e6fd..d9d6c0d 100644 --- a/src/passa/internals/utils.py +++ b/src/passa/internals/utils.py @@ -1,9 +1,33 @@ # -*- coding=utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, unicode_literals, print_function + +import atexit +import os + +import requests + + +def is_type_checking(): + # type: () -> bool + try: + from typing import TYPE_CHECKING + except ImportError: + return False + return TYPE_CHECKING + + +MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking()) + + +if MYPY_RUNNING: + from typing import Any, List, Dict # noqa + from requirementslib.models.requirements import Requirement # noqa + from pip_shims.shims import InstallRequirement # noqa def identify_requirment(r): + # type: (Requirement) -> str """Produce an identifier for a requirement to use in the resolver. Note that we are treating the same package with different extras as @@ -18,6 +42,7 @@ def identify_requirment(r): def get_pinned_version(ireq): + # type: (InstallRequirement) -> str """Get the pinned version of an InstallRequirement. An InstallRequirement is considered pinned if: @@ -60,6 +85,7 @@ def get_pinned_version(ireq): def is_pinned(ireq): + # type: (InstallRequirement) -> bool """Returns whether an InstallRequirement is a "pinned" requirement. An InstallRequirement is considered pinned if: @@ -83,6 +109,7 @@ def is_pinned(ireq): def filter_sources(requirement, sources): + # type: (Requirement, List[Dict[str, Union[bool, str]]]) -> List[Dict[str, Union[bool, str]]] """Returns a filtered list of sources for this requirement. This considers the index specified by the requirement, and returns only @@ -98,11 +125,13 @@ def filter_sources(requirement, sources): def get_allow_prereleases(requirement, global_setting): + # type: (Requirement, bool) -> bool # TODO: Implement per-package prereleases flag. (pypa/pipenv#1696) return global_setting def are_requirements_equal(this, that): + # type: (Requirement, Requirement) -> bool return ( this.as_line(include_hashes=False) == that.as_line(include_hashes=False) @@ -110,9 +139,22 @@ def are_requirements_equal(this, that): def strip_extras(requirement): + # type: (Requirement) -> Requirement """Returns a new requirement object with extras removed. """ line = requirement.as_line() new = type(requirement).from_line(line) new.extras = None return new + + +REQUESTS_SESSIONS = [] # type: List[requests.Session] + + +def get_tracked_session(): + # type: () -> requests.Session + """Build a requests session and register it to be closed with interpreter exit""" + + session = requests.Session() + atexit.register(session.close) + return session diff --git a/src/passa/models/caches.py b/src/passa/models/caches.py index c6d29b5..ed8c92f 100644 --- a/src/passa/models/caches.py +++ b/src/passa/models/caches.py @@ -10,25 +10,33 @@ import appdirs import pip_shims -import requests import vistir from ..internals._pip_shims import VCS_SUPPORT -from ..internals.utils import get_pinned_version +from ..internals.utils import get_pinned_version, get_tracked_session, MYPY_RUNNING -CACHE_DIR = os.environ.get("PASSA_CACHE_DIR", appdirs.user_cache_dir("passa")) +if MYPY_RUNNING: + import requests # noqa + from typing import Optional # noqa + + +CACHE_DIR = os.environ.get("PASSA_CACHE_DIR", appdirs.user_cache_dir("passa")) # type: ignore class HashCache(pip_shims.SafeFileCache): + """Caches hashes of PyPI artifacts so we do not need to re-download them. Hashes are only cached when the URL appears to contain a hash in it and the cache key includes the hash value returned from the server). This ought to avoid ssues where the location on the server changes. """ + def __init__(self, *args, **kwargs): - session = kwargs.pop('session', requests.session()) + session = kwargs.pop('session', None) # type: requests.Session + if session is None: + session = get_tracked_session() self.session = session kwargs.setdefault('directory', os.path.join(CACHE_DIR, 'hash-cache')) super(HashCache, self).__init__(*args, **kwargs) @@ -76,6 +84,7 @@ def __str__(self): def _key_from_req(req): """Get an all-lowercase version of the requirement's name.""" + if hasattr(req, 'key'): # from pkg_resources, such as installed dists for pip-sync key = req.key @@ -100,6 +109,7 @@ def _read_cache_file(cache_file_path): class _JSONCache(object): + """A persistent cache backed by a JSON file. The cache file is written to the appropriate user cache dir for the @@ -109,7 +119,8 @@ class _JSONCache(object): Where X.Y indicates the Python version. """ - filename_format = None + + filename_format = None # type: Optional[str] def __init__(self, cache_dir=CACHE_DIR): vistir.mkdir_p(cache_dir) @@ -122,10 +133,12 @@ def __init__(self, cache_dir=CACHE_DIR): @property def cache(self): - """The dictionary that is the actual in-memory cache. + """ + The dictionary that is the actual in-memory cache. This property lazily loads the cache from disk. """ + if self._cache is None: self.read_cache() return self._cache @@ -144,6 +157,7 @@ def as_cache_key(self, ireq): ("ipython", "2.1.0[nbconvert,notebook]") """ + extras = tuple(sorted(ireq.extras)) if not extras: extras_string = "" @@ -154,16 +168,16 @@ def as_cache_key(self, ireq): return name, "{}{}".format(version, extras_string) def read_cache(self): - """Reads the cached contents into memory. - """ + """Reads the cached contents into memory.""" + if os.path.exists(self._cache_file): self._cache = _read_cache_file(self._cache_file) else: self._cache = {} def write_cache(self): - """Writes the cache to disk as JSON. - """ + """Writes the cache to disk as JSON.""" + doc = { '__format__': 1, 'dependencies': self._cache, @@ -203,12 +217,18 @@ def get(self, ireq, default=None): class DependencyCache(_JSONCache): - """Cache the dependency of cancidates. + """ + Cache the dependency of cancidates. + """ + filename_format = "depcache-py{python_version}.json" class RequiresPythonCache(_JSONCache): - """Cache a candidate's Requires-Python information. + """ + Cache a candidate's Requires-Python information. + """ + filename_format = "pyreqcache-py{python_version}.json" diff --git a/src/passa/models/environments.py b/src/passa/models/environments.py new file mode 100644 index 0000000..bb52d13 --- /dev/null +++ b/src/passa/models/environments.py @@ -0,0 +1,452 @@ +# -*- coding=utf-8 -*- + +import contextlib +import importlib +import json +import os +import site +import sys + +from distutils.sysconfig import get_python_lib +from functools import partial +from sysconfig import get_paths + +import pkg_resources +import six + +from cached_property import cached_property + +import vistir + + +BASE_WORKING_SET = pkg_resources.WorkingSet(sys.path) +run = partial(vistir.misc.run, write_to_stdout=False, nospin=True, block=True) + + +class Environment(object): + def __init__(self, prefix=None, is_venv=False, base_working_set=None): + self.base_working_set = base_working_set if base_working_set else BASE_WORKING_SET + self._modules = {'pkg_resources': pkg_resources} + self.extra_dists = [] + prefix = prefix if prefix else sys.prefix + prefix = vistir.path.normalize_path(prefix) + sys_prefix = vistir.path.normalize_path(sys.prefix) + prefix = prefix if prefix else sys_prefix + self.is_venv = is_venv or prefix != sys_prefix + self.prefix = vistir.compat.Path(prefix) + self.sys_paths = get_paths() + super(Environment, self).__init__() + + def safe_import(self, name): + """Helper utility for reimporting previously imported modules while inside the env""" + module = None + if name not in self._modules: + self._modules[name] = importlib.import_module(name) + module = self._modules[name] + if not module: + dist = next(iter( + dist for dist in self.base_working_set if dist.project_name == name + ), None) + if dist: + dist.activate() + module = importlib.import_module(name) + if name in sys.modules: + try: + six.moves.reload_module(module) + six.moves.reload_module(sys.modules[name]) + except TypeError: + del sys.modules[name] + sys.modules[name] = self._modules[name] + return module + + @classmethod + def resolve_dist(cls, dist, working_set): + """Given a local distribution and a working set, returns all dependencies from the set. + + :param dist: A single distribution to find the dependencies of + :type dist: :class:`pkg_resources.Distribution` + :param working_set: A working set to search for all packages + :type working_set: :class:`pkg_resources.WorkingSet` + :return: A set of distributions which the package depends on, including the package + :rtype: set(:class:`pkg_resources.Distribution`) + """ + + deps = set() + deps.add(dist) + try: + reqs = dist.requires() + except (AttributeError, OSError, IOError): # The METADATA file can't be found + return deps + for req in reqs: + dist = working_set.find(req) + deps |= cls.resolve_dist(dist, working_set) + return deps + + def add_dist(self, dist_name): + dist = pkg_resources.get_distribution(pkg_resources.Requirement(dist_name)) + extras = self.resolve_dist(dist, self.base_working_set) + if extras: + self.extra_dists.extend(extras) + + @cached_property + def python_version(self): + with self.activated(): + sysconfig = self.safe_import("sysconfig") + py_version = sysconfig.get_python_version() + return py_version + + @property + def python_info(self): + include_dir = self.prefix / "include" + python_path = next(iter(list(include_dir.iterdir())), None) + if python_path and python_path.name.startswith("python"): + python_version = python_path.name.replace("python", "") + py_version_short, abiflags = python_version[:3], python_version[3:] + return {"py_version_short": py_version_short, "abiflags": abiflags} + return {} + + @cached_property + def base_paths(self): + """ + Returns the context appropriate paths for the environment. + + :return: A dictionary of environment specific paths to be used for installation operations + :rtype: dict + + .. note:: The implementation of this is borrowed from a combination of pip and + virtualenv and is likely to change at some point in the future. + + >>> from pipenv.core import project + >>> from pipenv.environment import Environment + >>> env = Environment(prefix=project.virtualenv_location, is_venv=True, sources=project.sources) + >>> import pprint + >>> pprint.pprint(env.base_paths) + {'PATH': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/bin::/bin:/usr/bin', + 'PYTHONPATH': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/lib/python3.7/site-packages', + 'data': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW', + 'include': '/home/hawk/.pyenv/versions/3.7.1/include/python3.7m', + 'libdir': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/lib/python3.7/site-packages', + 'platinclude': '/home/hawk/.pyenv/versions/3.7.1/include/python3.7m', + 'platlib': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/lib/python3.7/site-packages', + 'platstdlib': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/lib/python3.7', + 'prefix': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW', + 'purelib': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/lib/python3.7/site-packages', + 'scripts': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/bin', + 'stdlib': '/home/hawk/.pyenv/versions/3.7.1/lib/python3.7'} + """ + + prefix = self.prefix.as_posix() + install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix' + paths = get_paths(install_scheme, vars={ + 'base': prefix, + 'platbase': prefix, + }) + paths["PATH"] = paths["scripts"] + os.pathsep + os.defpath + if "prefix" not in paths: + paths["prefix"] = prefix + purelib = get_python_lib(plat_specific=0, prefix=prefix) + platlib = get_python_lib(plat_specific=1, prefix=prefix) + if purelib == platlib: + lib_dirs = purelib + else: + lib_dirs = purelib + os.pathsep + platlib + paths["libdir"] = purelib + paths["purelib"] = purelib + paths["platlib"] = platlib + paths['PYTHONPATH'] = lib_dirs + paths["libdirs"] = lib_dirs + return paths + + @cached_property + def script_basedir(self): + """Path to the environment scripts dir""" + script_dir = self.base_paths["scripts"] + return script_dir + + @property + def python(self): + """Path to the environment python""" + py = vistir.compat.Path(self.base_paths["scripts"]).joinpath("python").as_posix() + if not py: + return vistir.compat.Path(sys.executable).as_posix() + return py + + @cached_property + def sys_path(self): + """ + The system path inside the environment + + :return: The :data:`sys.path` from the environment + :rtype: list + """ + + current_executable = vistir.compat.Path(sys.executable).as_posix() + if not self.python or self.python == current_executable: + return sys.path + elif any([sys.prefix == self.prefix, not self.is_venv]): + return sys.path + cmd_args = [self.python, "-c", "import json, sys; print(json.dumps(sys.path))"] + path, _ = run(cmd_args, return_object=False, combine_stderr=False) + path = json.loads(path.strip()) + return path + + @cached_property + def sys_prefix(self): + """ + The prefix run inside the context of the environment + + :return: The python prefix inside the environment + :rtype: :data:`sys.prefix` + """ + + command = [self.python, "-c" "import sys; print(sys.prefix)"] + c = run(command, return_object=True) + sys_prefix = vistir.compat.Path(vistir.misc.to_text(c.out).strip()).as_posix() + return sys_prefix + + @property + def scripts_dir(self): + return self.paths["scripts"] + + @property + def libdir(self): + purelib = self.paths.get("purelib", None) + if purelib and os.path.exists(purelib): + return "purelib", purelib + return "platlib", self.paths["platlib"] + + @cached_property + def paths(self): + paths = {} + with vistir.contextmanagers.temp_environ(), vistir.contextmanagers.temp_path(): + os.environ["PYTHONIOENCODING"] = vistir.compat.fs_str("utf-8") + os.environ["PYTHONDONTWRITEBYTECODE"] = vistir.compat.fs_str("1") + paths = self.base_paths + os.environ["PATH"] = paths["PATH"] + os.environ["PYTHONPATH"] = paths["PYTHONPATH"] + if "headers" not in paths: + paths["headers"] = paths["include"] + return paths + + def get_distributions(self): + """Retrives the distributions installed on the library path of the environment + + :return: A set of distributions found on the library path + :rtype: iterator + """ + + pkg_resources = self.safe_import("pkg_resources") + return pkg_resources.find_distributions(self.paths["PYTHONPATH"], only=True) + + def find_egg(self, egg_dist): + """Find an egg by name in the given environment""" + + site_packages = self.libdir[1] + search_filename = "{0}.egg-link".format(egg_dist.project_name) + try: + user_site = site.getusersitepackages() + except AttributeError: + user_site = site.USER_SITE + search_locations = [site_packages, user_site] + for site_directory in search_locations: + egg = os.path.join(site_directory, search_filename) + if os.path.isfile(egg): + return egg + + def locate_dist(self, dist): + """ + Given a distribution, try to find a corresponding egg link first. + + If the egg - link doesn 't exist, return the supplied distribution.""" + + location = self.find_egg(dist) + return location or dist.location + + def dist_is_in_project(self, dist): + """Determine whether the supplied distribution is in the environment.""" + + prefix = vistir.path.normalize_path(self.base_paths["prefix"]) + location = self.locate_dist(dist) + if not location: + return False + return vistir.path.normalize_path(location).startswith(prefix) + + def get_installed_packages(self): + """ + Returns all of the installed packages in a given environment""" + + workingset = self.get_working_set() + packages = [pkg for pkg in workingset if self.dist_is_in_project(pkg)] + return packages + + def get_working_set(self): + """Retrieve the working set of installed packages for the environment. + + :return: The working set for the environment + :rtype: :class:`pkg_resources.WorkingSet` + """ + + working_set = None + import pkg_resources + working_set = pkg_resources.WorkingSet(self.sys_path) + return working_set + + def is_installed(self, pkgname): + """Given a package name, returns whether it is installed in the environment + + :param str pkgname: The name of a package + :return: Whether the supplied package is installed in the environment + :rtype: bool + """ + + return any(d for d in self.get_distributions() if d.project_name == pkgname) + + def run(self, cmd, cwd=os.curdir): + """Run a command with :class:`~subprocess.Popen` in the context of the environment + + :param cmd: A command to run in the environment + :type cmd: str or list + :param str cwd: The working directory in which to execute the command, defaults to :data:`os.curdir` + :return: A finished command object + :rtype: :class:`~subprocess.Popen` + """ + + c = None + with self.activated(): + script = vistir.cmdparse.Script.parse(cmd) + c = run(script._parts, return_object=True, cwd=cwd) + return c + + def run_py(self, cmd, cwd=os.curdir): + """Run a python command in the enviornment context. + + :param cmd: A command to run in the environment - runs with `python -c` + :type cmd: str or list + :param str cwd: The working directory in which to execute the command, defaults to :data:`os.curdir` + :return: A finished command object + :rtype: :class:`~subprocess.Popen` + """ + + c = None + if isinstance(cmd, six.string_types): + script = vistir.cmdparse.Script.parse("{0} -c {1}".format(self.python, cmd)) + else: + script = vistir.cmdparse.Script.parse([self.python, "-c"] + list(cmd)) + with self.activated(): + c = run(script._parts, return_object=True, cwd=cwd) + return c + + def run_activate_this(self): + """Runs the environment's inline activation script""" + if self.is_venv: + activate_this = os.path.join(self.scripts_dir, "activate_this.py") + if not os.path.isfile(activate_this): + raise OSError("No such file: {0!s}".format(activate_this)) + with open(activate_this, "r") as f: + code = compile(f.read(), activate_this, "exec") + exec(code, dict(__file__=activate_this)) + + @contextlib.contextmanager + def activated(self, include_extras=True, extra_dists=None): + """Helper context manager to activate the environment. + + This context manager will set the following variables for the duration + of its activation: + + * sys.prefix + * sys.path + * os.environ["VIRTUAL_ENV"] + * os.environ["PATH"] + + In addition, it will make any distributions passed into `extra_dists` available + on `sys.path` while inside the context manager, as well as making `passa` itself + available. + + The environment's `prefix` as well as `scripts_dir` properties are both prepended + to `os.environ["PATH"]` to ensure that calls to `~Environment.run()` use the + environment's path preferentially. + """ + + if not extra_dists: + extra_dists = [] + original_prefix = sys.prefix + parent_path = vistir.compat.Path(__file__).absolute().parent.parent.as_posix() + 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.prefix.as_posix()), + os.environ.get("PATH", os.defpath) + ]) + os.environ["PYTHONIOENCODING"] = vistir.compat.fs_str("utf-8") + os.environ["PYTHONDONTWRITEBYTECODE"] = vistir.compat.fs_str("1") + os.environ["PYTHONPATH"] = self.base_paths["PYTHONPATH"] + if self.is_venv: + os.environ["VIRTUAL_ENV"] = vistir.compat.fs_str(prefix) + sys.path = self.sys_path + sys.prefix = self.sys_prefix + pkg_resources = self.safe_import("pkg_resources") + site = self.safe_import("site") + site.addsitedir(self.libdir[1]) + if include_extras: + site.addsitedir(parent_path) + extra_dists = list(self.extra_dists) + extra_dists + for extra_dist in extra_dists: + if extra_dist not in self.get_working_set(): + extra_dist.activate(self.sys_path) + try: + yield + finally: + sys.prefix = original_prefix + six.moves.reload_module(pkg_resources) + + @contextlib.contextmanager + def uninstall(self, pkgname, *args, **kwargs): + """A context manager which allows uninstallation of packages from the environment + + :param str pkgname: The name of a package to uninstall + + >>> env = Environment("/path/to/env/root") + >>> with env.uninstall("pytz", auto_confirm=True, verbose=False) as uninstaller: + cleaned = uninstaller.paths + >>> if cleaned: + print("uninstalled packages: %s" % cleaned) + """ + + auto_confirm = kwargs.pop("auto_confirm", True) + verbose = kwargs.pop("verbose", False) + with self.activated(): + monkey_patch = next(iter( + dist for dist in self.base_working_set + if dist.project_name == "recursive-monkey-patch" + ), None) + if monkey_patch: + monkey_patch.activate() + pip_shims = self.safe_import("pip_shims") + pathset_base = pip_shims.UninstallPathSet + import recursive_monkey_patch + recursive_monkey_patch.monkey_patch( + PatchedUninstaller, pathset_base + ) + dist = next( + iter(filter(lambda d: d.project_name == pkgname, self.get_working_set())), + None + ) + pathset = pathset_base.from_dist(dist) + if pathset is not None: + pathset.remove(auto_confirm=auto_confirm, verbose=verbose) + try: + yield pathset + except Exception as e: + if pathset is not None: + pathset.rollback() + else: + if pathset is not None: + pathset.commit() + if pathset is None: + return + + +class PatchedUninstaller(object): + def _permitted(self, path): + return True diff --git a/src/passa/models/projects.py b/src/passa/models/projects.py index f6e037d..5c8d027 100644 --- a/src/passa/models/projects.py +++ b/src/passa/models/projects.py @@ -14,6 +14,8 @@ import six import tomlkit +from .environments import Environment + SectionDifference = collections.namedtuple("SectionDifference", [ "inthis", "inthat", @@ -84,6 +86,7 @@ def dumps(self): class Project(object): root = attr.ib() + environment = attr.ib(default=attr.Factory(Environment)) _p = attr.ib(init=False) _l = attr.ib(init=False) diff --git a/src/passa/models/synchronizers.py b/src/passa/models/synchronizers.py index bad4905..1ba8fa6 100644 --- a/src/passa/models/synchronizers.py +++ b/src/passa/models/synchronizers.py @@ -1,6 +1,6 @@ # -*- coding=utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, unicode_literals, print_function import collections import contextlib @@ -8,21 +8,24 @@ import sys import sysconfig +import distlib.wheel import pkg_resources import packaging.markers import packaging.version import requirementslib -from ..internals._pip import uninstall, EditableInstaller, WheelInstaller +from ..internals._pip import uninstall, Installer -def _is_installation_local(name): +def _is_installation_local(name, environment=None): """Check whether the distribution is in the current Python installation. - This is used to distinguish packages seen by a virtual environment. A venv + This is used to distinguish packages seen by a virtual environment. A environment may be able to see global packages, but we don't want to mess with them. """ + if environment: + return environment.is_installed(name) loc = os.path.normcase(pkg_resources.working_set.by_key[name].location) pre = os.path.normcase(sys.prefix) return os.path.commonprefix([loc, pre]) == pre @@ -38,12 +41,14 @@ def _is_up_to_date(distro, version): ]) -def _group_installed_names(packages): +def _group_installed_names(packages, environment=None): """Group locally installed packages based on given specifications. `packages` is a name-package mapping that are used as baseline to determine how the installed package should be grouped. + `environment` is the virtual environment object of the virtualenv being installed into. + Returns a 3-tuple of disjoint sets, all containing names of installed packages: @@ -54,8 +59,13 @@ def _group_installed_names(packages): """ groupcoll = GroupCollection(set(), set(), set(), set()) - for distro in pkg_resources.working_set: - name = distro.key + if environment: + working_set = environment.get_working_set() + else: + working_set = pkg_resources.working_set + + for dist in working_set: + name = dist.key try: package = packages[name] except KeyError: @@ -66,7 +76,7 @@ def _group_installed_names(packages): if not r.is_named: # Always mark non-named. I think pip does something similar? groupcoll.outdated.add(name) - elif not _is_up_to_date(distro, r.get_version()): + elif not _is_up_to_date(dist, r.get_version()): groupcoll.outdated.add(name) else: groupcoll.uptodate.add(name) @@ -75,11 +85,14 @@ def _group_installed_names(packages): @contextlib.contextmanager -def _remove_package(name): - if name is None or not _is_installation_local(name): +def _remove_package(name, environment=None): + if name is None or not _is_installation_local(name, environment=environment): yield None return - with uninstall(name, auto_confirm=True, verbose=False) as uninstaller: + _uninstall = uninstall + if environment: + _uninstall = environment.uninstall + with _uninstall(name, auto_confirm=True, verbose=False) as uninstaller: yield uninstaller @@ -88,19 +101,22 @@ def _get_packages(lockfile, default, develop): # Extras don't matter because they only affect dependencies, and we # don't install dependencies anyway! packages = {} - if default: - packages.update(lockfile.default._data) if develop: packages.update(lockfile.develop._data) + if default: + packages.update(lockfile.default._data) return packages -def _build_paths(): +def _build_paths(environment=None): """Prepare paths for distlib.wheel.Wheel to install into. """ - paths = sysconfig.get_paths() + if environment: + paths = environment.paths + else: + paths = sysconfig.get_paths() return { - "prefix": sys.prefix, + "prefix": sys.prefix if not environment else environment.prefix.as_posix(), "data": paths["data"], "scripts": paths["scripts"], "headers": paths["include"], @@ -112,12 +128,12 @@ def _build_paths(): PROTECTED_FROM_CLEAN = {"setuptools", "pip", "wheel"} -def _clean(names): +def _clean(names, environment=None): cleaned = set() for name in names: if name in PROTECTED_FROM_CLEAN: continue - with _remove_package(name) as uninst: + with _remove_package(name, environment=environment) as uninst: if uninst: cleaned.add(name) return cleaned @@ -126,18 +142,36 @@ def _clean(names): class Synchronizer(object): """Helper class to install packages from a project's lock file. """ - def __init__(self, project, default, develop, clean_unneeded): + def __init__(self, project, default, develop, clean_unneeded, environment=None): self._root = project.root # Only for repr. + self.project = project self.packages = _get_packages(project.lockfile, default, develop) self.sources = project.lockfile.meta.sources._data - self.paths = _build_paths() self.clean_unneeded = clean_unneeded + if not environment: + self._environment = getattr(project, "environment", None) + else: + self._environment = environment + super(Synchronizer, self).__init__() + self.paths = _build_paths(environment=self.environment) + + @property + def environment(self): + if self._environment: + return self._environment + return self.project.environment def __repr__(self): return "<{0} @ {1!r}>".format(type(self).__name__, self._root) def sync(self): - groupcoll = _group_installed_names(self.packages) + if not self.environment: + return self._sync() + with self.environment.activated(): + return self._sync() + + def _sync(self): + groupcoll = _group_installed_names(self.packages, environment=self.environment) installed = set() updated = set() @@ -146,7 +180,7 @@ def sync(self): # TODO: Show a prompt to confirm cleaning. We will need to implement a # reporter pattern for this as well. if self.clean_unneeded: - names = _clean(groupcoll.unneeded) + names = _clean(groupcoll.unneeded, environment=self.environment) cleaned.update(names) # TODO: Specify installation order? (pypa/pipenv#2274) @@ -160,10 +194,7 @@ def sync(self): if markers and not packaging.markers.Marker(markers).evaluate(): continue r.markers = None - if r.editable: - installer = EditableInstaller(r) - else: - installer = WheelInstaller(r, self.sources, self.paths) + installer = Installer(r, sources=self.sources, environment=self.environment) try: installer.prepare() except Exception as e: @@ -181,7 +212,7 @@ def sync(self): else: name_to_remove = None try: - with _remove_package(name_to_remove): + with _remove_package(name_to_remove, environment=self.environment): installer.install() except Exception as e: if os.environ.get("PASSA_NO_SUPPRESS_EXCEPTIONS"): @@ -201,14 +232,27 @@ def sync(self): class Cleaner(object): """Helper class to clean packages not in a project's lock file. """ - def __init__(self, project, default, develop): + def __init__(self, project, default, develop, sync=True, verbose=False): self._root = project.root # Only for repr. self.packages = _get_packages(project.lockfile, default, develop) + self.sync = sync + self.project = project def __repr__(self): return "<{0} @ {1!r}>".format(type(self).__name__, self._root) + def print(self, packages): + if not self.sync: + message = "Would clean: {0}" + else: + message = "Cleaned: {0}" + print(message.format(", ".join(sorted(set(packages))))) + def clean(self): - groupcoll = _group_installed_names(self.packages) - cleaned = _clean(groupcoll.unneeded) + groupcoll = _group_installed_names(self.packages, environment=self.project.environment) + cleaned = set() + if self.sync: + cleaned = _clean(groupcoll.unneeded, environment=self.project.environment) + else: + return groupcoll.unneeded return cleaned diff --git a/src/passa/operations/sync.py b/src/passa/operations/sync.py index 3014e8d..45502a4 100644 --- a/src/passa/operations/sync.py +++ b/src/passa/operations/sync.py @@ -16,8 +16,8 @@ def sync(syncer): def clean(cleaner): - print("Cleaning") + print("Cleaning...") cleaned = cleaner.clean() if cleaned: - print("Uninstalled: {}".format(", ".join(sorted(cleaned)))) + cleaner.print(cleaned) return True diff --git a/tasks/admin.py b/tasks/admin.py index 2fabe99..fa1be74 100644 --- a/tasks/admin.py +++ b/tasks/admin.py @@ -14,6 +14,14 @@ INIT_PY = ROOT.joinpath('src', PACKAGE_NAME, '__init__.py') +@invoke.task() +def typecheck(ctx): + src_dir = ROOT / "src" / PACKAGE_NAME + src_dir = src_dir.as_posix() + env = {"MYPYPATH": src_dir} + ctx.run(f"mypy {src_dir}", env=env) + + @invoke.task() def clean(ctx): """Clean previously built package artifacts. diff --git a/tasks/package.py b/tasks/package.py index f229e3a..bba878b 100644 --- a/tasks/package.py +++ b/tasks/package.py @@ -22,7 +22,6 @@ 'importlib', # We only support 2.7 so this is not needed. 'modutil', # This breaks <3.7. - 'toml', # Why is requirementslib still not dropping it? 'typing', # This breaks 2.7. We'll provide a special stub for it. } diff --git a/tests/actions/__init__.py b/tests/actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/actions/test_add.py b/tests/actions/test_add.py new file mode 100644 index 0000000..e7cd883 --- /dev/null +++ b/tests/actions/test_add.py @@ -0,0 +1,20 @@ +# -*- coding=utf-8 -*- +import passa.actions.init +import passa.actions.add +import passa.cli.options +import passa.models.projects + + +def test_add_one(project_directory): + project = passa.cli.options.Project(project_directory.strpath) + retcode = passa.actions.add.add_packages(["pytz"], project=project) + assert not retcode + assert 'pytz' in project.lockfile.default + + +def test_add_one_with_deps(project_directory): + project = passa.cli.options.Project(project_directory.strpath) + retcode = passa.actions.add.add_packages(["requests"], project=project) + assert not retcode + assert 'requests' in project.lockfile.default + assert 'idna' in project.lockfile.default diff --git a/tests/actions/test_clean.py b/tests/actions/test_clean.py new file mode 100644 index 0000000..68562f9 --- /dev/null +++ b/tests/actions/test_clean.py @@ -0,0 +1,20 @@ +# -*- coding=utf-8 -*- +import passa.actions.add +import passa.actions.clean + + +def test_clean(project): + retcode = passa.actions.add.add_packages(["requests"], project=project) + assert not retcode + packages = ["requests", "chardet", "certifi", "idna"] + c = project.env.run("pip install pytz") + assert c.returncode == 0 + assert project.env.is_installed("pytz") + c = project.env.run("python -c 'import pytz'") + assert c.returncode == 0 + clean_retcode = passa.actions.clean.clean(project=project) + assert not clean_retcode + assert not project.env.is_installed("pytz") + c = project.env.run("python -c 'import pytz'") + assert c.returncode != 0 + assert all(pkg in project.lockfile.default for pkg in packages) diff --git a/tests/actions/test_freeze.py b/tests/actions/test_freeze.py new file mode 100644 index 0000000..53d34f9 --- /dev/null +++ b/tests/actions/test_freeze.py @@ -0,0 +1,21 @@ +# -*- coding=utf-8 -*- +import passa.actions.add +import passa.actions.freeze +import passa.cli.options +import passa.models.projects + + +def test_freeze(project_directory): + project = passa.cli.options.Project(project_directory.strpath) + retcode = passa.actions.add.add_packages(["requests"], project=project) + assert not retcode + packages = ["requests", "chardet", "certifi", "idna"] + assert all(pkg in project.lockfile.default for pkg in packages) + freeze_file = project_directory.join("requirements.txt") + freeze_retcode = passa.actions.freeze.freeze( + project=project, include_hashes=False, target=freeze_file.strpath + ) + assert not freeze_retcode + lines = [line.strip() for line in freeze_file.readlines() if line.strip() != ''] + for pkg in packages: + assert any(line.startswith(pkg) for line in lines) diff --git a/tests/actions/test_init.py b/tests/actions/test_init.py new file mode 100644 index 0000000..2f12530 --- /dev/null +++ b/tests/actions/test_init.py @@ -0,0 +1,19 @@ +# -*- coding=utf-8 -*- + +import pytest + +import passa.actions.init +import passa.cli.options + + +def test_init(tmpdir): + init_retcode = passa.actions.init.init_project(root=tmpdir.strpath) + assert init_retcode == 0 + project = passa.cli.options.Project(tmpdir.strpath) + assert project.pipfile.packages._data == {} + assert project.pipfile.dev_packages._data == {} + + +def test_init_exists(project_directory): + with pytest.raises(RuntimeError, match=r'.* is already a Pipfile project'): + passa.actions.init.init_project(root=project_directory.strpath) diff --git a/tests/actions/test_install.py b/tests/actions/test_install.py new file mode 100644 index 0000000..45b44c0 --- /dev/null +++ b/tests/actions/test_install.py @@ -0,0 +1,96 @@ +# -*- coding=utf-8 -*- + +import passa.actions.install +import passa.actions.add +import passa.cli.options +import passa.models.projects +import pytest + + +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_install_one(project, is_dev): + add_kwargs = { + "project": project, + "packages": ["pytz",], + "editables": [], + "dev": is_dev, + "sync": False, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert 'pytz' in project.lockfile._data[lockfile_section].keys() + install = passa.actions.install.install(project=project, check=True, dev=is_dev, clean=False) + assert install == 0 + assert project.env.is_installed("pytz") or project.is_installed("pytz") + + +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_install_one_with_deps(project, is_dev): + add_kwargs = { + "project": project, + "packages": ["requests",], + "editables": [], + "dev": is_dev, + "sync": False, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert 'requests' in project.lockfile._data[lockfile_section].keys() + assert 'idna' in project.lockfile._data[lockfile_section].keys() + install = passa.actions.install.install(project=project, check=True, dev=is_dev, clean=False) + assert install == 0 + assert project.env.is_installed("requests") or project.is_installed("requests") + assert project.env.is_installed("idna") or project.is_installed("idna") + + +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_install_editable(project, is_dev): + add_kwargs = { + "project": project, + "packages": [], + "editables": ["git+https://github.com/sarugaku/shellingham.git@1.2.1#egg=shellingham",], + "dev": is_dev, + "sync": False, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert 'shellingham' in project.lockfile._data[lockfile_section].keys() + install = passa.actions.install.install(project=project, check=True, dev=is_dev, clean=False) + assert install == 0 + project.reload() + assert (project.env.is_installed("shellingham") or + project.is_installed("shellingham")), list([dist.project_name for dist in project.env.get_distributions()]) + + +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_install_sdist(project, is_dev): + add_kwargs = { + "project": project, + "packages": ["arrow",], + "editables": [], + "dev": is_dev, + "sync": False, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert 'arrow' in project.lockfile._data[lockfile_section].keys() + install = passa.actions.install.install(project=project, check=True, dev=is_dev, clean=False) + assert install == 0 + project.reload() + assert project.env.is_installed("arrow") or project.is_installed("arrow") diff --git a/tests/actions/test_lock.py b/tests/actions/test_lock.py new file mode 100644 index 0000000..962493e --- /dev/null +++ b/tests/actions/test_lock.py @@ -0,0 +1,53 @@ +# -*- coding=utf-8 -*- + +import passa.actions.lock +import passa.actions.install +import passa.cli.options +import passa.models.projects +import pytest + + +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_lock_one(project, is_dev): + line = "pytz" + project.add_line_to_pipfile(line, develop=is_dev) + retcode = passa.actions.lock.lock(project=project) + project.reload() + assert retcode == 0 + lockfile_section = "default" if not is_dev else "develop" + assert 'pytz' in project.lockfile._data[lockfile_section].keys() + install = passa.actions.install.install(project=project, check=True, dev=is_dev, clean=False) + assert install == 0 + + +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_lock_one_with_deps(project, is_dev): + line = "requests" + project.add_line_to_pipfile(line, develop=is_dev) + retcode = passa.actions.lock.lock(project=project) + project.reload() + assert retcode == 0 + lockfile_section = "default" if not is_dev else "develop" + assert 'requests' in project.lockfile._data[lockfile_section].keys() + assert 'idna' in project.lockfile._data[lockfile_section].keys() + install = passa.actions.install.install(project=project, check=True, dev=is_dev, clean=False) + assert install == 0 + + +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_lock_editable(project, is_dev): + line = "-e git+https://github.com/sarugaku/shellingham.git@1.2.1#egg=shellingham" + project.add_line_to_pipfile(line, develop=is_dev) + retcode = passa.actions.lock.lock(project=project) + project.reload() + assert retcode == 0 + lockfile_section = "default" if not is_dev else "develop" + assert 'shellingham' in project.lockfile._data[lockfile_section].keys(), project.lockfile._data + install = passa.actions.install.install(project=project, check=True, dev=is_dev, clean=False) + assert install == 0 diff --git a/tests/actions/test_remove_and_sync.py b/tests/actions/test_remove_and_sync.py new file mode 100644 index 0000000..f428a94 --- /dev/null +++ b/tests/actions/test_remove_and_sync.py @@ -0,0 +1,142 @@ +# -*- coding=utf-8 -*- + +import passa.actions.add +import passa.actions.remove +import passa.cli.options +import passa.models.projects +import pytest +import vistir + + +@pytest.mark.parametrize( + 'sync', (True, False) +) +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_remove_one(project, sync, is_dev): + pkg = "xlrd" + add_kwargs = { + "project": project, + "packages": [pkg,], + "editables": [], + "dev": is_dev, + "sync": sync, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert pkg in project.lockfile._data[lockfile_section].keys() + if sync: + assert project.env.is_installed(pkg) or project.is_installed(pkg) + remove = "default" if not is_dev else "dev" + retcode = passa.actions.remove.remove(project=project, packages=[pkg,], sync=sync, only=remove) + assert not retcode + project.reload() + assert pkg not in project.lockfile._data[lockfile_section].keys() + if sync: + assert not project.env.is_installed(pkg) + + +@pytest.mark.parametrize( + 'sync', (True, False), +) +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_remove_one_with_deps(project, sync, is_dev): + add_kwargs = { + "project": project, + "packages": ["requests",], + "editables": [], + "dev": is_dev, + "sync": sync, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert 'requests' in project.lockfile._data[lockfile_section].keys() + assert 'idna' in project.lockfile._data[lockfile_section].keys() + if sync: + c = vistir.misc.run(["{0}".format(project.env.python), "-c", "import requests"], + nospin=True, block=True, return_object=True) + assert c.returncode == 0, (c.out, c.err) + assert project.env.is_installed("requests") or project.is_installed("requests") + assert project.env.is_installed("idna") or project.is_installed("idna") + remove = "default" if not is_dev else "dev" + retcode = passa.actions.remove.remove(project=project, packages=["requests",], sync=sync, only=remove) + assert not retcode + project.reload() + assert "requests" not in project.lockfile._data[lockfile_section].keys() + assert "idna" not in project.lockfile._data[lockfile_section].keys() + if sync: + assert not project.env.is_installed("requests") + assert not project.env.is_installed("idna") + + +@pytest.mark.parametrize( + 'sync', (True, False), +) +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_remove_editable(project, sync, is_dev): + add_kwargs = { + "project": project, + "packages": [], + "editables": ["git+https://github.com/sarugaku/shellingham.git@1.2.1#egg=shellingham",], + "dev": is_dev, + "sync": sync, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert 'shellingham' in project.lockfile._data[lockfile_section].keys() + if sync: + c = vistir.misc.run(["{0}".format(project.env.python), "-c", "import shellingham"], + nospin=True, block=True, return_object=True) + assert c.returncode == 0, (c.out, c.err) + assert project.env.is_installed("shellingham") or project.is_installed("shellingham") + remove = "default" if not is_dev else "dev" + retcode = passa.actions.remove.remove(project=project, packages=["shellingham",], sync=sync, only=remove) + assert not retcode + project.reload() + assert "shellingham" not in project.lockfile._data[lockfile_section].keys() + if sync: + assert not project.env.is_installed("shellingham") + + +@pytest.mark.parametrize( + 'sync', (True, False), +) +@pytest.mark.parametrize( + 'is_dev', (True, False) +) +def test_remove_sdist(project, is_dev, sync): + add_kwargs = { + "project": project, + "packages": ["arrow"], + "editables": [], + "dev": is_dev, + "sync": sync, + "clean": False + } + retcode = passa.actions.add.add_packages(**add_kwargs) + assert not retcode + lockfile_section = "default" if not is_dev else "develop" + assert 'arrow' in project.lockfile._data[lockfile_section].keys() + if sync: + c = vistir.misc.run(["{0}".format(project.env.python), "-c", "import arrow"], + nospin=True, block=True, return_object=True) + assert c.returncode == 0, (c.out, c.err) + assert project.env.is_installed("arrow") or project.is_installed("arrow") + remove = "default" if not is_dev else "dev" + retcode = passa.actions.remove.remove(project=project, packages=["arrow",], sync=sync, only=remove) + assert not retcode + project.reload() + assert "arrow" not in project.lockfile._data[lockfile_section].keys() + if sync: + assert not project.env.is_installed("arrow") diff --git a/tests/actions/test_upgrade.py b/tests/actions/test_upgrade.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/__init__.py b/tests/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_add.py b/tests/cli/test_add.py new file mode 100644 index 0000000..a6bcbda --- /dev/null +++ b/tests/cli/test_add.py @@ -0,0 +1 @@ +# -*- coding=utf-8 -*- diff --git a/tests/cli/test_clean.py b/tests/cli/test_clean.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_freeze.py b/tests/cli/test_freeze.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_init.py b/tests/cli/test_init.py new file mode 100644 index 0000000..a6bcbda --- /dev/null +++ b/tests/cli/test_init.py @@ -0,0 +1 @@ +# -*- coding=utf-8 -*- diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_lock.py b/tests/cli/test_lock.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_remove.py b/tests/cli/test_remove.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_sync.py b/tests/cli/test_sync.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_upgrade.py b/tests/cli/test_upgrade.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1ac4319 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,107 @@ +# -*- coding=utf-8 -*- +import os +import pytest +import passa +import passa.models.projects +import passa.cli.options +# import mork +import passa.models.environments +import pkg_resources +import plette +import sys +import vistir + +from collections import deque + + +DEFAULT_PIPFILE_CONTENTS = """ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] + +[dev-packages] +""".strip() + + +@pytest.fixture(scope="session") +def working_set_extension(): + dists = set() + passa_dist = pkg_resources.get_distribution(pkg_resources.Requirement('passa')) + dists.add(passa_dist) + requirements = deque(passa_dist.requires(extras=('tests', 'virtualenv'))) + while requirements: + req = requirements.popleft() + dist = pkg_resources.working_set.find(req) + dists.add(dist) + requirements.extend(dist.requires()) + return dists + + +@pytest.fixture(scope="function") +def virtualenv(tmpdir_factory): + venv_dir = tmpdir_factory.mktemp("passa-testenv") + print("Creating virtualenv {0!r}".format(venv_dir.strpath)) + c = vistir.misc.run([sys.executable, "-m", "virtualenv", venv_dir.strpath], + return_object=True, block=True, nospin=True) + if c.returncode == 0: + print("Virtualenv created...") + return venv_dir + raise RuntimeError("Failed creating virtualenv for testing...{0!r}".format(c.err.strip())) + + +class _Project(passa.cli.options.Project): + def __init__(self, root, environment=None, working_set_extension=[]): + self.path = root + self.working_set_extension = working_set_extension + self.env = environment + super(_Project, self).__init__(self.path, environment=environment) + self.pipfile_instance = vistir.compat.Path(self.pipfile_location) + self.lockfile_instance = vistir.compat.Path(self.lockfile_location) + + def reload(self): + self._p = passa.models.projects.ProjectFile.read( + os.path.join(self.path, "Pipfile"), + plette.Pipfile, + ) + self._l = passa.models.projects.ProjectFile.read( + os.path.join(self.path, "Pipfile.lock"), + plette.Lockfile, + invalid_ok=True, + ) + + +@pytest.fixture(scope="function") +def project_directory(tmpdir_factory): + project_dir = tmpdir_factory.mktemp("passa-project") + project_dir.join("Pipfile").write(DEFAULT_PIPFILE_CONTENTS) + with vistir.contextmanagers.cd(project_dir.strpath): + yield project_dir + + +@pytest.fixture +def tmpvenv(virtualenv, tmpdir): + venv_srcdir = virtualenv.join("src").mkdir() + # venv = mork.virtualenv.VirtualEnv(virtualenv.strpath) + workingset = pkg_resources.WorkingSet(sys.path) + venv = passa.models.environments.Environment(prefix=virtualenv.strpath, is_venv=True, + base_working_set=workingset) + venv.add_dist("passa") + venv.run(["pip", "install", "--upgrade", "mork", "setuptools"]) + with vistir.contextmanagers.temp_environ(): + os.environ["PACKAGEBUILDER_CACHE_DIR"] = tmpdir.strpath + os.environ["PIP_QUIET"] = "1" + os.environ["PIP_SRC"] = venv_srcdir.strpath + venv.is_installed = lambda x: any(d for d in venv.get_distributions() if d.project_name == x) + yield venv + + +@pytest.fixture(scope="function") +def project(project_directory, working_set_extension, tmpvenv): + # resolved = tmpvenv.resolve_dist(passa_dist, tmpvenv.base_working_set) + with tmpvenv.activated(extra_dists=list(working_set_extension)): + project = _Project(project_directory.strpath, environment=tmpvenv, working_set_extension=working_set_extension) + project.is_installed = lambda x: any(d for d in tmpvenv.get_working_set() if d.project_name == x) + yield project diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini index 2bc8e1d..5c0a6dd 100644 --- a/tox.ini +++ b/tox.ini @@ -8,9 +8,13 @@ setenv = LC_ALL = en_US.UTF-8 deps = coverage - -e .[tests] + setuptools + pytest + pytest-timeout + pytest-sugar + -e .[tests,virtualenv] commands = coverage run --parallel -m pytest --timeout 300 [] -install_command = python -m pip install {opts} {packages} +install_command = python -m pip install --upgrade {opts} {packages} usedevelop = True [testenv:coverage-report] @@ -23,15 +27,15 @@ commands = [testenv:docs] deps = -r{toxinidir}/docs/requirements.txt - -e .[tests] commands = sphinx-build -d {envtmpdir}/doctrees -b html docs docs/build/html sphinx-build -d {envtmpdir}/doctrees -b man docs docs/build/man [testenv:packaging] deps = - check-manifest - readme_renderer + setuptools + twine + readme_renderer[md] commands = - check-manifest - python setup.py check -m -r -s + python setup.py sdist bdist_wheel + twine check dist/*