diff --git a/poetry.lock b/poetry.lock index 53fbe8a..4ed3fcf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -241,7 +241,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "cryptography" -version = "3.1.1" +version = "3.2.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -252,11 +252,11 @@ cffi = ">=1.8,<1.11.3 || >1.11.3" six = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] +test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "dataclasses" @@ -466,7 +466,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" parso = ">=0.7.0,<0.8.0" [package.extras] -qa = ["flake8 (3.7.9)"] +qa = ["flake8 (==3.7.9)"] testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] @@ -496,7 +496,7 @@ SecretStorage = {version = ">=3", markers = "sys_platform == \"linux\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black (>=0.3.7)", "pytest-cov", "pytest-mypy"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black (>=0.3.7)", "pytest-cov", "pytest-mypy"] [[package]] name = "lazy-object-proxy" @@ -803,7 +803,7 @@ py = ">=1.8.2" toml = "*" [package.extras] -checkqa_mypy = ["mypy (0.780)"] +checkqa_mypy = ["mypy (==0.780)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -819,7 +819,7 @@ coverage = ">=4.4" pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] [[package]] name = "pywin32-ctypes" @@ -872,7 +872,7 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] name = "requests-toolbelt" @@ -1027,7 +1027,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] brotli = ["brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" @@ -1083,7 +1083,7 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" @@ -1245,28 +1245,28 @@ crashtest = [ {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"}, ] cryptography = [ - {file = "cryptography-3.1.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719"}, - {file = "cryptography-3.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe"}, - {file = "cryptography-3.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3"}, - {file = "cryptography-3.1.1-cp27-cp27m-win32.whl", hash = "sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba"}, - {file = "cryptography-3.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118"}, - {file = "cryptography-3.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db"}, - {file = "cryptography-3.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396"}, - {file = "cryptography-3.1.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc"}, - {file = "cryptography-3.1.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7"}, - {file = "cryptography-3.1.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6"}, - {file = "cryptography-3.1.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536"}, - {file = "cryptography-3.1.1-cp35-cp35m-win32.whl", hash = "sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f"}, - {file = "cryptography-3.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154"}, - {file = "cryptography-3.1.1-cp36-abi3-win32.whl", hash = "sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70"}, - {file = "cryptography-3.1.1-cp36-abi3-win_amd64.whl", hash = "sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8"}, - {file = "cryptography-3.1.1-cp36-cp36m-win32.whl", hash = "sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499"}, - {file = "cryptography-3.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49"}, - {file = "cryptography-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921"}, - {file = "cryptography-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2"}, - {file = "cryptography-3.1.1-cp38-cp38-win32.whl", hash = "sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490"}, - {file = "cryptography-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba"}, - {file = "cryptography-3.1.1.tar.gz", hash = "sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d"}, + {file = "cryptography-3.2.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7"}, + {file = "cryptography-3.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d"}, + {file = "cryptography-3.2.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33"}, + {file = "cryptography-3.2.1-cp27-cp27m-win32.whl", hash = "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297"}, + {file = "cryptography-3.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4"}, + {file = "cryptography-3.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8"}, + {file = "cryptography-3.2.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e"}, + {file = "cryptography-3.2.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b"}, + {file = "cryptography-3.2.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df"}, + {file = "cryptography-3.2.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77"}, + {file = "cryptography-3.2.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7"}, + {file = "cryptography-3.2.1-cp35-cp35m-win32.whl", hash = "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b"}, + {file = "cryptography-3.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7"}, + {file = "cryptography-3.2.1-cp36-abi3-win32.whl", hash = "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f"}, + {file = "cryptography-3.2.1-cp36-abi3-win_amd64.whl", hash = "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538"}, + {file = "cryptography-3.2.1-cp36-cp36m-win32.whl", hash = "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b"}, + {file = "cryptography-3.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13"}, + {file = "cryptography-3.2.1-cp37-cp37m-win32.whl", hash = "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e"}, + {file = "cryptography-3.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb"}, + {file = "cryptography-3.2.1-cp38-cp38-win32.whl", hash = "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b"}, + {file = "cryptography-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851"}, + {file = "cryptography-3.2.1.tar.gz", hash = "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3"}, ] dataclasses = [ {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, @@ -1613,19 +1613,28 @@ typed-ast = [ {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, + {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, + {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ diff --git a/pyproject.toml b/pyproject.toml index 6c44955..a5388fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,12 @@ authors = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"] description = "Tox plugin to install Tox environment dependencies using the Poetry backend and lockfile" repository = "https://github.com/enpaul/tox-poetry-installer/" packages = [ - {include = "tox_poetry_installer.py"}, + {include = "tox_poetry_installer"}, {include = "tests/*.py", format = "sdist"} ] +include = [ + "tox_poetry_installer/py.typed" +] keywords = ["tox", "poetry", "plugin"] readme = "README.md" classifiers = [ diff --git a/tests/test_metadata.py b/tests/test_metadata.py index eaa4bf1..c28e9da 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -7,7 +7,7 @@ from pathlib import Path import toml -import tox_poetry_installer +from tox_poetry_installer import __about__ def test_metadata(): @@ -16,16 +16,14 @@ def test_metadata(): with (Path(__file__).resolve().parent / ".." / "pyproject.toml").open() as infile: pyproject = toml.load(infile, _dict=dict) - assert pyproject["tool"]["poetry"]["name"] == tox_poetry_installer.__title__ - assert pyproject["tool"]["poetry"]["version"] == tox_poetry_installer.__version__ - assert pyproject["tool"]["poetry"]["license"] == tox_poetry_installer.__license__ - assert ( - pyproject["tool"]["poetry"]["description"] == tox_poetry_installer.__summary__ - ) - assert pyproject["tool"]["poetry"]["repository"] == tox_poetry_installer.__url__ + assert pyproject["tool"]["poetry"]["name"] == __about__.__title__ + assert pyproject["tool"]["poetry"]["version"] == __about__.__version__ + assert pyproject["tool"]["poetry"]["license"] == __about__.__license__ + assert pyproject["tool"]["poetry"]["description"] == __about__.__summary__ + assert pyproject["tool"]["poetry"]["repository"] == __about__.__url__ assert ( all( - item in tox_poetry_installer.__authors__ + item in __about__.__authors__ for item in pyproject["tool"]["poetry"]["authors"] ) is True @@ -33,7 +31,7 @@ def test_metadata(): assert ( all( item in pyproject["tool"]["poetry"]["authors"] - for item in tox_poetry_installer.__authors__ + for item in __about__.__authors__ ) is True ) diff --git a/tox.ini b/tox.ini index b89285d..f262d81 100644 --- a/tox.ini +++ b/tox.ini @@ -10,31 +10,42 @@ deps = pytest-cov toml commands = - pytest --cov tox_poetry_installer --cov-config {toxinidir}/.coveragerc tests/ --cov-report term-missing + pytest --cov {envsitepackagesdir}/tox_poetry_installer --cov-config {toxinidir}/.coveragerc --cov-report term-missing tests/ [testenv:static] description = Static formatting and quality enforcement -require_locked_deps = true basepython = python3.8 +platform = linux ignore_errors = true +require_locked_deps = true deps = pylint mypy black reorder-python-imports pre-commit +allowlist_externals = + bash commands = - black {toxinidir}/tox_poetry_installer.py - reorder-python-imports {toxinidir}/tox_poetry_installer.py + black {toxinidir}/tox_poetry_installer/ + # Oh man this is a doozy. If submodules are ever added to this plugin this will break, but I'm + # frustrated enough at this point that I'll need to take another look at it later to fix that. + # reorder-python-imports doesn't support handling directories on the CLI + # (https://github.com/asottile/reorder_python_imports/pull/76) and because the command is + # invoked directly (see comment below) we need file globbing to work around it. + # The "--unclassifiable-application-module" is a work around for reorder-python-imports not + # properly detecting the top-level module when run in a bash-wrapped command like this. + bash -c "reorder-python-imports {toxinidir}/tox_poetry_installer/*.py --unclassifiable-application-module tox_poetry_installer" pre-commit run --all-files - pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tox_poetry_installer.py - mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tox_poetry_installer.py + pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tox_poetry_installer/ + mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tox_poetry_installer/ [testenv:static-tests] description = Static formatting and quality enforcement for the tests -require_locked_deps = true basepython = python3.8 +platform = linux ingore_errors = true +require_locked_deps = true deps = pylint mypy @@ -44,23 +55,26 @@ allowlist_externals = bash commands = black {toxinidir}/tests/ + # These bash-wrapped commands hurt my face, but these tools expect directories to be valid + # python modules, which the "tests/" directory is not. Since tox calls all commands directly + # (which is good) file globbing doesn't work. To make file globbing work they need to be wrapped + # in a bash call (which is bad). bash -c "reorder-python-imports {toxinidir}/tests/*.py --unclassifiable-application-module tox_poetry_installer" bash -c "pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tests/*.py" bash -c "mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tests/*.py" [testenv:security] description = Security checks -require_locked_deps = true basepython = python3.8 -ignore_errors = true -skip_install = true +platform = linux +ingore_errors = true +require_locked_deps = true deps = bandit safety poetry -allowlist_externals = - bash commands = - bandit --quiet {toxinidir}/tox_poetry_installer.py - bash -c "bandit --quiet --skip B101 {toxinidir}/tests/*.py" - bash -c "poetry export --format requirements.txt --without-hashes --dev | safety check --stdin --bare" + bandit --recursive --quiet {toxinidir}/tox_poetry_installer/ + bandit --recursive --quiet --skip B101 {toxinidir}/tests/ + poetry export --format requirements.txt --output {envtmpdir}/requirements.txt --without-hashes --dev + safety check --bare --file {envtmpdir}/requirements.txt diff --git a/tox_poetry_installer.py b/tox_poetry_installer.py deleted file mode 100644 index f8b6fae..0000000 --- a/tox_poetry_installer.py +++ /dev/null @@ -1,401 +0,0 @@ -"""Tox plugin for installing environments using Poetry - -This plugin makes use of the ``tox_testenv_install_deps`` Tox plugin hook to augment the default -installation functionality to install dependencies from the Poetry lockfile for the project. It -does this by using ``poetry`` to read in the lockfile, identify necessary dependencies, and then -use Poetry's ``PipInstaller`` class to install those packages into the Tox environment. - -Quick definition of terminology: - -* "project package" - the package that Tox is testing, usually the one the current project is - is developing; definitionally, this is the package that is built by Tox in the ``.package`` env. -* "project package dependency" or "project dependency" - a dependency required by the project - package for installation; i.e. a package that would be installed when running - ``pip install ``. -* "environment dependency" - a dependency specified for a given testenv in the Tox configuration. -* "locked dependency" - a package that is present in the Poetry lockfile and will be installed - according to the metadata in the lockfile. -* "unlocked dependency" - a package that is either not present in the Poetry lockfile or is not - specified to be installed according to the metadata in the lockfile. -* "transiety dependency" - a package not explicitly specified for installation, but required by a - package that is explicitly specified. -""" -from pathlib import Path -from typing import Dict -from typing import List -from typing import NamedTuple -from typing import Sequence -from typing import Set -from typing import Tuple - -from poetry.core.packages import Package as PoetryPackage -from poetry.factory import Factory as PoetryFactory -from poetry.installation.pip_installer import PipInstaller as PoetryPipInstaller -from poetry.io.null_io import NullIO as PoetryNullIO -from poetry.poetry import Poetry -from poetry.puzzle.provider import Provider as PoetryProvider -from poetry.utils.env import VirtualEnv as PoetryVirtualEnv -from tox import hookimpl -from tox import reporter -from tox.action import Action as ToxAction -from tox.config import DepConfig as ToxDepConfig -from tox.config import Parser as ToxParser -from tox.venv import VirtualEnv as ToxVirtualEnv - - -__title__ = "tox-poetry-installer" -__summary__ = "Tox plugin to install Tox environment dependencies using the Poetry backend and lockfile" -__version__ = "0.4.0" -__url__ = "https://github.com/enpaul/tox-poetry-installer/" -__license__ = "MIT" -__authors__ = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"] - - -# Valid PEP508 version delimiters. These are used to test whether a given string (specifically a -# dependency name) is just a package name or also includes a version identifier. -_PEP508_VERSION_DELIMITERS: Tuple[str, ...] = ("~=", "==", "!=", ">", "<") - -# Prefix all reporter messages should include to indicate that they came from this module in the -# console output. -_REPORTER_PREFIX = f"[{__title__}]:" - -# Suffix that indicates an env dependency should be treated as a locked dependency and thus be -# installed from the lockfile. Will be automatically stripped off of a dependency name during -# sorting so that the resulting string is just the valid package name. This becomes optional when -# the "require_locked_deps" option is true for an environment; in that case a bare dependency like -# 'foo' is treated the same as an explicitly locked dependency like 'foo@poetry' -_MAGIC_SUFFIX_MARKER = "@poetry" - - -# Map of package names to the package object -PackageMap = Dict[str, PoetryPackage] - - -class _SortedEnvDeps(NamedTuple): - unlocked_deps: List[ToxDepConfig] - locked_deps: List[ToxDepConfig] - - -class ToxPoetryInstallerException(Exception): - """Error while installing locked dependencies to the test environment""" - - -class LockedDepVersionConflictError(ToxPoetryInstallerException): - """Locked dependencies cannot specify an alternate version for installation""" - - -class LockedDepNotFoundError(ToxPoetryInstallerException): - """Locked dependency was not found in the lockfile""" - - -class ExtraNotFoundError(ToxPoetryInstallerException): - """Project package extra not defined in project's pyproject.toml""" - - -def _sort_env_deps(venv: ToxVirtualEnv) -> _SortedEnvDeps: - """Sorts the environment dependencies by lock status - - Lock status determines whether a given environment dependency will be installed from the - lockfile using the Poetry backend, or whether this plugin will skip it and allow it to be - installed using the default pip-based backend (an unlocked dependency). - - .. note:: A locked dependency must follow a required format. To avoid reinventing the wheel - (no pun intended) this module does not have any infrastructure for parsing PEP-508 - version specifiers, and so requires locked dependencies to be specified with no - version (the installed version being taken from the lockfile). If a dependency is - specified as locked and its name is also a PEP-508 string then an error will be - raised. - """ - - reporter.verbosity1( - f"{_REPORTER_PREFIX} sorting {len(venv.envconfig.deps)} env dependencies by lock requirement" - ) - unlocked_deps = [] - locked_deps = [] - - for dep in venv.envconfig.deps: - if venv.envconfig.require_locked_deps: - reporter.verbosity1( - f"{_REPORTER_PREFIX} lock required for env, treating '{dep.name}' as locked env dependency" - ) - dep.name = dep.name.replace(_MAGIC_SUFFIX_MARKER, "") - locked_deps.append(dep) - else: - if dep.name.endswith(_MAGIC_SUFFIX_MARKER): - reporter.verbosity1( - f"{_REPORTER_PREFIX} specification includes marker '{_MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as locked env dependency" - ) - dep.name = dep.name.replace(_MAGIC_SUFFIX_MARKER, "") - locked_deps.append(dep) - else: - reporter.verbosity1( - f"{_REPORTER_PREFIX} specification does not include marker '{_MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as unlocked env dependency" - ) - unlocked_deps.append(dep) - - reporter.verbosity1( - f"{_REPORTER_PREFIX} identified {len(locked_deps)} locked env dependencies: {[item.name for item in locked_deps]}" - ) - reporter.verbosity1( - f"{_REPORTER_PREFIX} identified {len(unlocked_deps)} unlocked env dependencies: {[item.name for item in unlocked_deps]}" - ) - - return _SortedEnvDeps(locked_deps=locked_deps, unlocked_deps=unlocked_deps) - - -def _install_to_venv( - poetry: Poetry, venv: ToxVirtualEnv, packages: Sequence[PoetryPackage] -): - """Install a bunch of packages to a virtualenv - - :param poetry: Poetry object the packages were sourced from - :param venv: Tox virtual environment to install the packages to - :param packages: List of packages to install to the virtual environment - """ - - reporter.verbosity1( - f"{_REPORTER_PREFIX} Installing {len(packages)} packages to environment at {venv.envconfig.envdir}" - ) - - installer = PoetryPipInstaller( - env=PoetryVirtualEnv(path=Path(venv.envconfig.envdir)), - io=PoetryNullIO(), - pool=poetry.pool, - ) - - for dependency in packages: - reporter.verbosity1(f"{_REPORTER_PREFIX} installing {dependency}") - installer.install(dependency) - - -def _find_transients(packages: PackageMap, dependency_name: str) -> Set[PoetryPackage]: - """Using a poetry object identify all dependencies of a specific dependency - - :param poetry: Populated poetry object which can be used to build a populated locked - repository object. - :param dependency_name: Bare name (without version) of the dependency to fetch the transient - dependencies of. - :returns: List of packages that need to be installed for the requested dependency. - - .. note:: The package corresponding to the dependency named by ``dependency_name`` is included - in the list of returned packages. - """ - - try: - top_level = packages[dependency_name] - - def find_deps_of_deps(name: str) -> List[PoetryPackage]: - if name in PoetryProvider.UNSAFE_PACKAGES: - reporter.warning( - f"{_REPORTER_PREFIX} installing package '{name}' using Poetry is not supported; skipping installation of package '{name}'" - ) - return [] - transients = [packages[name]] - for dep in packages[name].requires: - transients += find_deps_of_deps(dep.name) - return transients - - return set(find_deps_of_deps(top_level.name)) - - except KeyError: - if any( - delimiter in dependency_name for delimiter in _PEP508_VERSION_DELIMITERS - ): - raise LockedDepVersionConflictError( - f"Locked dependency '{dependency_name}' cannot include version specifier" - ) from None - raise LockedDepNotFoundError( - f"No version of locked dependency '{dependency_name}' found in the project lockfile" - ) from None - - -def _install_env_dependencies( - venv: ToxVirtualEnv, poetry: Poetry, packages: PackageMap -): - """Install the packages for a specified testenv - - Processes the tox environment config, identifies any locked environment dependencies, pulls - them from the lockfile, and installs them to the virtual environment. - - :param venv: Tox virtual environment to install the packages to - :param poetry: Poetry object the packages were sourced from - :param packages: Mapping of package names to the corresponding package object - """ - env_deps = _sort_env_deps(venv) - - dependencies: List[PoetryPackage] = [] - for dep in env_deps.locked_deps: - try: - dependencies += _find_transients(packages, dep.name.lower()) - except ToxPoetryInstallerException as err: - venv.status = "lockfile installation failed" - reporter.error(f"{_REPORTER_PREFIX} {err}") - raise err - - if venv.envconfig.install_dev_deps: - reporter.verbosity1( - f"{_REPORTER_PREFIX} env specifies 'install_env_deps = true', including Poetry dev dependencies" - ) - - dev_dependencies = [ - dep - for dep in poetry.locker.locked_repository(True).packages - if dep not in poetry.locker.locked_repository(False).packages - ] - - reporter.verbosity1( - f"{_REPORTER_PREFIX} identified {len(dev_dependencies)} Poetry dev dependencies" - ) - - dependencies = list(set(dev_dependencies + dependencies)) - - reporter.verbosity1( - f"{_REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(env_deps.locked_deps)} locked env dependencies" - ) - - reporter.verbosity1( - f"{_REPORTER_PREFIX} updating env config with {len(env_deps.unlocked_deps)} unlocked env dependencies for installation using the default backend" - ) - venv.envconfig.deps = env_deps.unlocked_deps - - reporter.verbosity0( - f"{_REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} env dependencies from lockfile" - ) - _install_to_venv(poetry, venv, dependencies) - - -def _install_project_dependencies( - venv: ToxVirtualEnv, poetry: Poetry, packages: PackageMap -): - """Install the dependencies of the project package - - Install all primary dependencies of the project package. - - :param venv: Tox virtual environment to install the packages to - :param poetry: Poetry object the packages were sourced from - :param packages: Mapping of package names to the corresponding package object - """ - reporter.verbosity1( - f"{_REPORTER_PREFIX} performing installation of project dependencies" - ) - - base_dependencies: List[PoetryPackage] = [ - packages[item.name] - for item in poetry.package.requires - if not item.is_optional() - ] - - extra_dependencies: List[PoetryPackage] = [] - for extra in venv.envconfig.extras: - try: - extra_dependencies += [ - packages[item.name] for item in poetry.package.extras[extra] - ] - except KeyError: - raise ExtraNotFoundError( - f"Environment '{venv.name}' specifies project extra '{extra}' which was not found in the lockfile" - ) from None - - dependencies: List[PoetryPackage] = [] - for dep in base_dependencies + extra_dependencies: - try: - dependencies += _find_transients(packages, dep.name.lower()) - except ToxPoetryInstallerException as err: - venv.status = "lockfile installation failed" - reporter.error(f"{_REPORTER_PREFIX} {err}") - raise err - - reporter.verbosity1( - f"{_REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(poetry.package.requires)} project dependencies" - ) - - reporter.verbosity0( - f"{_REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} project dependencies from lockfile" - ) - _install_to_venv(poetry, venv, dependencies) - - -@hookimpl -def tox_addoption(parser: ToxParser): - """Add required configuration options to the tox INI file - - Adds the ``require_locked_deps`` configuration option to the venv to check whether all - dependencies should be treated as locked or not. - """ - - parser.add_testenv_attribute( - name="install_dev_deps", - type="bool", - default=False, - help="Automatically install all Poetry development dependencies to the environment", - ) - - parser.add_testenv_attribute( - name="require_locked_deps", - type="bool", - default=False, - help="Require all dependencies in the environment be installed using the Poetry lockfile", - ) - - -@hookimpl -def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction): - """Install the dependencies for the current environment - - Loads the local Poetry environment and the corresponding lockfile then pulls the dependencies - specified by the Tox environment. Finally these dependencies are installed into the Tox - environment using the Poetry ``PipInstaller`` backend. - - :param venv: Tox virtual environment object with configuration for the local Tox environment. - :param action: Tox action object - """ - - if action.name == venv.envconfig.config.isolated_build_env: - # Skip running the plugin for the packaging environment. PEP-517 front ends can handle - # that better than we can, so let them do their thing. More to the point: if you're having - # problems in the packaging env that this plugin would solve, god help you. - reporter.verbosity1( - f"{_REPORTER_PREFIX} skipping isolated build env '{action.name}'" - ) - return - - try: - poetry = PoetryFactory().create_poetry(venv.envconfig.config.toxinidir) - except RuntimeError: - # Support running the plugin when the current tox project does not use Poetry for its - # environment/dependency management. - # - # ``RuntimeError`` is dangerous to blindly catch because it can be (and in Poetry's case, - # is) raised in many different places for different purposes. - reporter.verbosity1( - f"{_REPORTER_PREFIX} project does not use Poetry for env management, skipping installation of locked dependencies" - ) - return - - reporter.verbosity1( - f"{_REPORTER_PREFIX} loaded project pyproject.toml from {poetry.file}" - ) - - package_map: PackageMap = { - package.name: package - for package in poetry.locker.locked_repository(True).packages - } - - # Handle the installation of any locked env dependencies from the lockfile - _install_env_dependencies(venv, poetry, package_map) - - # Handle the installation of the package dependencies from the lockfile if the package is - # being installed to this venv; otherwise skip installing the package dependencies - if venv.envconfig.skip_install: - reporter.verbosity1( - f"{_REPORTER_PREFIX} env specifies 'skip_install = true', skipping installation of project package" - ) - return - - if venv.envconfig.config.skipsdist: - reporter.verbosity1( - f"{_REPORTER_PREFIX} config specifies 'skipsdist = true', skipping installation of project package" - ) - return - - _install_project_dependencies(venv, poetry, package_map) diff --git a/tox_poetry_installer/__about__.py b/tox_poetry_installer/__about__.py new file mode 100644 index 0000000..3211a6c --- /dev/null +++ b/tox_poetry_installer/__about__.py @@ -0,0 +1,7 @@ +# pylint: disable=missing-docstring +__title__ = "tox-poetry-installer" +__summary__ = "Tox plugin to install Tox environment dependencies using the Poetry backend and lockfile" +__version__ = "0.4.0" +__url__ = "https://github.com/enpaul/tox-poetry-installer/" +__license__ = "MIT" +__authors__ = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"] diff --git a/tox_poetry_installer/__init__.py b/tox_poetry_installer/__init__.py new file mode 100644 index 0000000..7e22471 --- /dev/null +++ b/tox_poetry_installer/__init__.py @@ -0,0 +1,3 @@ +# pylint: disable=missing-docstring +from tox_poetry_installer.hooks import tox_addoption +from tox_poetry_installer.hooks import tox_testenv_install_deps diff --git a/tox_poetry_installer/constants.py b/tox_poetry_installer/constants.py new file mode 100644 index 0000000..a4e7933 --- /dev/null +++ b/tox_poetry_installer/constants.py @@ -0,0 +1,26 @@ +"""Static constants for reference + +Rule of thumb: if it's an arbitrary value that will never be changed at runtime, it should go +in this module. + +All constants should be type hinted. +""" +from typing import Tuple + +from tox_poetry_installer import __about__ + + +# Valid PEP508 version delimiters. These are used to test whether a given string (specifically a +# dependency name) is just a package name or also includes a version identifier. +PEP508_VERSION_DELIMITERS: Tuple[str, ...] = ("~=", "==", "!=", ">", "<") + +# Prefix all reporter messages should include to indicate that they came from this module in the +# console output. +REPORTER_PREFIX = f"[{__about__.__title__}]:" + +# Suffix that indicates an env dependency should be treated as a locked dependency and thus be +# installed from the lockfile. Will be automatically stripped off of a dependency name during +# sorting so that the resulting string is just the valid package name. This becomes optional when +# the "require_locked_deps" option is true for an environment; in that case a bare dependency like +# 'foo' is treated the same as an explicitly locked dependency like 'foo@poetry' +MAGIC_SUFFIX_MARKER = "@poetry" diff --git a/tox_poetry_installer/datatypes.py b/tox_poetry_installer/datatypes.py new file mode 100644 index 0000000..b042024 --- /dev/null +++ b/tox_poetry_installer/datatypes.py @@ -0,0 +1,18 @@ +"""Definitions for typehints/containers used by the plugin""" +from typing import Dict +from typing import List +from typing import NamedTuple + +from poetry.core.packages import Package as PoetryPackage +from tox.config import DepConfig as ToxDepConfig + + +# Map of package names to the package object +PackageMap = Dict[str, PoetryPackage] + + +class SortedEnvDeps(NamedTuple): + """Container for the two types of environment dependencies""" + + unlocked_deps: List[ToxDepConfig] + locked_deps: List[ToxDepConfig] diff --git a/tox_poetry_installer/exceptions.py b/tox_poetry_installer/exceptions.py new file mode 100644 index 0000000..969bf13 --- /dev/null +++ b/tox_poetry_installer/exceptions.py @@ -0,0 +1,28 @@ +"""Custom plugin exceptions + +All exceptions should inherit from the common base exception :exc:`ToxPoetryInstallerException`. + +:: + + ToxPoetryInstallerException + +-- LockedDepVersionConflictError + +-- LockedDepNotFoundError + +-- ExtraNotFoundError + +""" + + +class ToxPoetryInstallerException(Exception): + """Error while installing locked dependencies to the test environment""" + + +class LockedDepVersionConflictError(ToxPoetryInstallerException): + """Locked dependencies cannot specify an alternate version for installation""" + + +class LockedDepNotFoundError(ToxPoetryInstallerException): + """Locked dependency was not found in the lockfile""" + + +class ExtraNotFoundError(ToxPoetryInstallerException): + """Project package extra not defined in project's pyproject.toml""" diff --git a/tox_poetry_installer/hooks.py b/tox_poetry_installer/hooks.py new file mode 100644 index 0000000..d9b80c9 --- /dev/null +++ b/tox_poetry_installer/hooks.py @@ -0,0 +1,213 @@ +"""Main hook definition module + +All implementations of tox hooks are defined here, as well as any single-use helper functions +specifically related to implementing the hooks (to keep the size/readability of the hook functions +themselves manageable). +""" +from typing import List + +from poetry.core.packages import Package as PoetryPackage +from poetry.factory import Factory as PoetryFactory +from poetry.poetry import Poetry +from tox import hookimpl +from tox import reporter +from tox.action import Action as ToxAction +from tox.config import Parser as ToxParser +from tox.venv import VirtualEnv as ToxVirtualEnv + +from tox_poetry_installer import constants +from tox_poetry_installer import exceptions +from tox_poetry_installer import utilities +from tox_poetry_installer.datatypes import PackageMap + + +@hookimpl +def tox_addoption(parser: ToxParser): + """Add required configuration options to the tox INI file + + Adds the ``require_locked_deps`` configuration option to the venv to check whether all + dependencies should be treated as locked or not. + """ + + parser.add_testenv_attribute( + name="install_dev_deps", + type="bool", + default=False, + help="Automatically install all Poetry development dependencies to the environment", + ) + + parser.add_testenv_attribute( + name="require_locked_deps", + type="bool", + default=False, + help="Require all dependencies in the environment be installed using the Poetry lockfile", + ) + + +@hookimpl +def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction): + """Install the dependencies for the current environment + + Loads the local Poetry environment and the corresponding lockfile then pulls the dependencies + specified by the Tox environment. Finally these dependencies are installed into the Tox + environment using the Poetry ``PipInstaller`` backend. + + :param venv: Tox virtual environment object with configuration for the local Tox environment. + :param action: Tox action object + """ + + if action.name == venv.envconfig.config.isolated_build_env: + # Skip running the plugin for the packaging environment. PEP-517 front ends can handle + # that better than we can, so let them do their thing. More to the point: if you're having + # problems in the packaging env that this plugin would solve, god help you. + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} skipping isolated build env '{action.name}'" + ) + return + + try: + poetry = PoetryFactory().create_poetry(venv.envconfig.config.toxinidir) + except RuntimeError: + # Support running the plugin when the current tox project does not use Poetry for its + # environment/dependency management. + # + # ``RuntimeError`` is dangerous to blindly catch because it can be (and in Poetry's case, + # is) raised in many different places for different purposes. + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} project does not use Poetry for env management, skipping installation of locked dependencies" + ) + return + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} loaded project pyproject.toml from {poetry.file}" + ) + + package_map: PackageMap = { + package.name: package + for package in poetry.locker.locked_repository(True).packages + } + + # Handle the installation of any locked env dependencies from the lockfile + _install_env_dependencies(venv, poetry, package_map) + + # Handle the installation of the package dependencies from the lockfile if the package is + # being installed to this venv; otherwise skip installing the package dependencies + if venv.envconfig.skip_install: + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} env specifies 'skip_install = true', skipping installation of project package" + ) + return + + if venv.envconfig.config.skipsdist: + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} config specifies 'skipsdist = true', skipping installation of project package" + ) + return + + _install_project_dependencies(venv, poetry, package_map) + + +def _install_env_dependencies( + venv: ToxVirtualEnv, poetry: Poetry, packages: PackageMap +): + """Install the packages for a specified testenv + + Processes the tox environment config, identifies any locked environment dependencies, pulls + them from the lockfile, and installs them to the virtual environment. + + :param venv: Tox virtual environment to install the packages to + :param poetry: Poetry object the packages were sourced from + :param packages: Mapping of package names to the corresponding package object + """ + env_deps = utilities.sort_env_deps(venv) + + dependencies: List[PoetryPackage] = [] + for dep in env_deps.locked_deps: + try: + dependencies += utilities.find_transients(packages, dep.name.lower()) + except exceptions.ToxPoetryInstallerException as err: + venv.status = "lockfile installation failed" + reporter.error(f"{constants.REPORTER_PREFIX} {err}") + raise err + + if venv.envconfig.install_dev_deps: + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} env specifies 'install_env_deps = true', including Poetry dev dependencies" + ) + + dev_dependencies = [ + dep + for dep in poetry.locker.locked_repository(True).packages + if dep not in poetry.locker.locked_repository(False).packages + ] + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} identified {len(dev_dependencies)} Poetry dev dependencies" + ) + + dependencies = list(set(dev_dependencies + dependencies)) + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(env_deps.locked_deps)} locked env dependencies" + ) + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} updating env config with {len(env_deps.unlocked_deps)} unlocked env dependencies for installation using the default backend" + ) + venv.envconfig.deps = env_deps.unlocked_deps + + reporter.verbosity0( + f"{constants.REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} env dependencies from lockfile" + ) + utilities.install_to_venv(poetry, venv, dependencies) + + +def _install_project_dependencies( + venv: ToxVirtualEnv, poetry: Poetry, packages: PackageMap +): + """Install the dependencies of the project package + + Install all primary dependencies of the project package. + + :param venv: Tox virtual environment to install the packages to + :param poetry: Poetry object the packages were sourced from + :param packages: Mapping of package names to the corresponding package object + """ + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} performing installation of project dependencies" + ) + + base_dependencies: List[PoetryPackage] = [ + packages[item.name] + for item in poetry.package.requires + if not item.is_optional() + ] + + extra_dependencies: List[PoetryPackage] = [] + for extra in venv.envconfig.extras: + try: + extra_dependencies += [ + packages[item.name] for item in poetry.package.extras[extra] + ] + except KeyError: + raise exceptions.ExtraNotFoundError( + f"Environment '{venv.name}' specifies project extra '{extra}' which was not found in the lockfile" + ) from None + + dependencies: List[PoetryPackage] = [] + for dep in base_dependencies + extra_dependencies: + try: + dependencies += utilities.find_transients(packages, dep.name.lower()) + except exceptions.ToxPoetryInstallerException as err: + venv.status = "lockfile installation failed" + reporter.error(f"{constants.REPORTER_PREFIX} {err}") + raise err + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(poetry.package.requires)} project dependencies" + ) + + reporter.verbosity0( + f"{constants.REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} project dependencies from lockfile" + ) + utilities.install_to_venv(poetry, venv, dependencies) diff --git a/tox_poetry_installer/py.typed b/tox_poetry_installer/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tox_poetry_installer/utilities.py b/tox_poetry_installer/utilities.py new file mode 100644 index 0000000..5ce41d0 --- /dev/null +++ b/tox_poetry_installer/utilities.py @@ -0,0 +1,137 @@ +"""Helper utility functions, usually bridging Tox and Poetry functionality""" +from pathlib import Path +from typing import List +from typing import Sequence +from typing import Set + +from poetry.core.packages import Package as PoetryPackage +from poetry.installation.pip_installer import PipInstaller as PoetryPipInstaller +from poetry.io.null_io import NullIO as PoetryNullIO +from poetry.poetry import Poetry +from poetry.puzzle.provider import Provider as PoetryProvider +from poetry.utils.env import VirtualEnv as PoetryVirtualEnv +from tox import reporter +from tox.venv import VirtualEnv as ToxVirtualEnv + +from tox_poetry_installer import constants +from tox_poetry_installer import exceptions +from tox_poetry_installer.datatypes import PackageMap +from tox_poetry_installer.datatypes import SortedEnvDeps + + +def sort_env_deps(venv: ToxVirtualEnv) -> SortedEnvDeps: + """Sorts the environment dependencies by lock status + + Lock status determines whether a given environment dependency will be installed from the + lockfile using the Poetry backend, or whether this plugin will skip it and allow it to be + installed using the default pip-based backend (an unlocked dependency). + + .. note:: A locked dependency must follow a required format. To avoid reinventing the wheel + (no pun intended) this module does not have any infrastructure for parsing PEP-508 + version specifiers, and so requires locked dependencies to be specified with no + version (the installed version being taken from the lockfile). If a dependency is + specified as locked and its name is also a PEP-508 string then an error will be + raised. + """ + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} sorting {len(venv.envconfig.deps)} env dependencies by lock requirement" + ) + unlocked_deps = [] + locked_deps = [] + + for dep in venv.envconfig.deps: + if venv.envconfig.require_locked_deps: + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} lock required for env, treating '{dep.name}' as locked env dependency" + ) + dep.name = dep.name.replace(constants.MAGIC_SUFFIX_MARKER, "") + locked_deps.append(dep) + else: + if dep.name.endswith(constants.MAGIC_SUFFIX_MARKER): + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} specification includes marker '{constants.MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as locked env dependency" + ) + dep.name = dep.name.replace(constants.MAGIC_SUFFIX_MARKER, "") + locked_deps.append(dep) + else: + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} specification does not include marker '{constants.MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as unlocked env dependency" + ) + unlocked_deps.append(dep) + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} identified {len(locked_deps)} locked env dependencies: {[item.name for item in locked_deps]}" + ) + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} identified {len(unlocked_deps)} unlocked env dependencies: {[item.name for item in unlocked_deps]}" + ) + + return SortedEnvDeps(locked_deps=locked_deps, unlocked_deps=unlocked_deps) + + +def install_to_venv( + poetry: Poetry, venv: ToxVirtualEnv, packages: Sequence[PoetryPackage] +): + """Install a bunch of packages to a virtualenv + + :param poetry: Poetry object the packages were sourced from + :param venv: Tox virtual environment to install the packages to + :param packages: List of packages to install to the virtual environment + """ + + reporter.verbosity1( + f"{constants.REPORTER_PREFIX} Installing {len(packages)} packages to environment at {venv.envconfig.envdir}" + ) + + installer = PoetryPipInstaller( + env=PoetryVirtualEnv(path=Path(venv.envconfig.envdir)), + io=PoetryNullIO(), + pool=poetry.pool, + ) + + for dependency in packages: + reporter.verbosity1(f"{constants.REPORTER_PREFIX} installing {dependency}") + installer.install(dependency) + + +def find_transients(packages: PackageMap, dependency_name: str) -> Set[PoetryPackage]: + """Using a poetry object identify all dependencies of a specific dependency + + :param poetry: Populated poetry object which can be used to build a populated locked + repository object. + :param dependency_name: Bare name (without version) of the dependency to fetch the transient + dependencies of. + :returns: List of packages that need to be installed for the requested dependency. + + .. note:: The package corresponding to the dependency named by ``dependency_name`` is included + in the list of returned packages. + """ + + try: + top_level = packages[dependency_name] + + def find_deps_of_deps(name: str) -> List[PoetryPackage]: + if name in PoetryProvider.UNSAFE_PACKAGES: + reporter.warning( + f"{constants.REPORTER_PREFIX} installing package '{name}' using Poetry is not supported; skipping installation of package '{name}'" + ) + return [] + transients = [packages[name]] + for dep in packages[name].requires: + transients += find_deps_of_deps(dep.name) + return transients + + return set(find_deps_of_deps(top_level.name)) + + except KeyError: + if any( + delimiter in dependency_name + for delimiter in constants.PEP508_VERSION_DELIMITERS + ): + raise exceptions.LockedDepVersionConflictError( + f"Locked dependency '{dependency_name}' cannot include version specifier" + ) from None + raise exceptions.LockedDepNotFoundError( + f"No version of locked dependency '{dependency_name}' found in the project lockfile" + ) from None