Compare commits

...

4 Commits

Author SHA1 Message Date
bbb075a1de
Expand single character iteration variable 2024-08-20 14:06:51 -04:00
198287a633
Standardize import structure
Standardize on "import module" format rather than "from module import foo" format
Remove _poetry stub module since we directly depend on the poetry package now
Fix conflicts between modules and parameters both named 'poetry'
Fixes #92
2024-08-20 14:06:51 -04:00
4b38b00f81
Update mypy to 1.11
Fix typing errors
2024-08-20 13:14:22 -04:00
863f88d63c
Fix linting errors for pylint 3 2024-08-20 13:02:12 -04:00
11 changed files with 189 additions and 209 deletions

View File

@ -11,7 +11,6 @@
# --disable=W" # --disable=W"
disable=logging-fstring-interpolation disable=logging-fstring-interpolation
,logging-format-interpolation ,logging-format-interpolation
,bad-continuation
,line-too-long ,line-too-long
,ungrouped-imports ,ungrouped-imports
,typecheck ,typecheck

81
poetry.lock generated
View File

@ -164,13 +164,13 @@ redis = ["redis (>=2.10.5)"]
[[package]] [[package]]
name = "cachetools" name = "cachetools"
version = "5.4.0" version = "5.5.0"
description = "Extensible memoizing collections and decorators" description = "Extensible memoizing collections and decorators"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
{file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
] ]
[[package]] [[package]]
@ -780,13 +780,13 @@ files = [
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "8.2.0" version = "8.3.0"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, {file = "importlib_metadata-8.3.0-py3-none-any.whl", hash = "sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67"},
{file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, {file = "importlib_metadata-8.3.0.tar.gz", hash = "sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364"},
] ]
[package.dependencies] [package.dependencies]
@ -799,13 +799,13 @@ test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "p
[[package]] [[package]]
name = "importlib-resources" name = "importlib-resources"
version = "6.4.2" version = "6.4.3"
description = "Read resources from Python packages" description = "Read resources from Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "importlib_resources-6.4.2-py3-none-any.whl", hash = "sha256:8bba8c54a8a3afaa1419910845fa26ebd706dc716dd208d9b158b4b6966f5c5c"}, {file = "importlib_resources-6.4.3-py3-none-any.whl", hash = "sha256:2d6dfe3b9e055f72495c2085890837fc8c758984e209115c8792bddcb762cd93"},
{file = "importlib_resources-6.4.2.tar.gz", hash = "sha256:6cbfbefc449cc6e2095dd184691b7a12a04f40bc75dd4c55d31c34f174cdf57a"}, {file = "importlib_resources-6.4.3.tar.gz", hash = "sha256:4a202b9b9d38563b46da59221d77bb73862ab5d79d461307bcb826d725448b98"},
] ]
[package.dependencies] [package.dependencies]
@ -1192,41 +1192,50 @@ files = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "0.930" version = "1.11.1"
description = "Optional static typing for Python" description = "Optional static typing for Python"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.8"
files = [ files = [
{file = "mypy-0.930-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221cc94dc6a801ccc2be7c0c9fd791c5e08d1fa2c5e1c12dec4eab15b2469871"}, {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
{file = "mypy-0.930-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db3a87376a1380f396d465bed462e76ea89f838f4c5e967d68ff6ee34b785c31"}, {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
{file = "mypy-0.930-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d2296f35aae9802eeb1327058b550371ee382d71374b3e7d2804035ef0b830b"}, {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
{file = "mypy-0.930-cp310-cp310-win_amd64.whl", hash = "sha256:959319b9a3cafc33a8185f440a433ba520239c72e733bf91f9efd67b0a8e9b30"}, {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
{file = "mypy-0.930-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:45a4dc21c789cfd09b8ccafe114d6de66f0b341ad761338de717192f19397a8c"}, {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
{file = "mypy-0.930-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1e689e92cdebd87607a041585f1dc7339aa2e8a9f9bad9ba7e6ece619431b20c"}, {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
{file = "mypy-0.930-cp36-cp36m-win_amd64.whl", hash = "sha256:ed4e0ea066bb12f56b2812a15ff223c57c0a44eca817ceb96b214bb055c7051f"}, {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
{file = "mypy-0.930-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a9d8dffefba634b27d650e0de2564379a1a367e2e08d6617d8f89261a3bf63b2"}, {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
{file = "mypy-0.930-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b419e9721260161e70d054a15abbd50603c16f159860cfd0daeab647d828fc29"}, {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
{file = "mypy-0.930-cp37-cp37m-win_amd64.whl", hash = "sha256:601f46593f627f8a9b944f74fd387c9b5f4266b39abad77471947069c2fc7651"}, {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
{file = "mypy-0.930-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ea7199780c1d7940b82dbc0a4e37722b4e3851264dbba81e01abecc9052d8a7"}, {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
{file = "mypy-0.930-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70b197dd8c78fc5d2daf84bd093e8466a2b2e007eedaa85e792e513a820adbf7"}, {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
{file = "mypy-0.930-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5feb56f8bb280468fe5fc8e6f56f48f99aa0df9eed3c507a11505ee4657b5380"}, {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
{file = "mypy-0.930-cp38-cp38-win_amd64.whl", hash = "sha256:2e9c5409e9cb81049bb03fa1009b573dea87976713e3898561567a86c4eaee01"}, {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
{file = "mypy-0.930-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:554873e45c1ca20f31ddf873deb67fa5d2e87b76b97db50669f0468ccded8fae"}, {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
{file = "mypy-0.930-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0feb82e9fa849affca7edd24713dbe809dce780ced9f3feca5ed3d80e40b777f"}, {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
{file = "mypy-0.930-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc1a0607ea03c30225347334af66b0af12eefba018a89a88c209e02b7065ea95"}, {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
{file = "mypy-0.930-cp39-cp39-win_amd64.whl", hash = "sha256:f9f665d69034b1fcfdbcd4197480d26298bbfb5d2dfe206245b6498addb34999"}, {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
{file = "mypy-0.930-py3-none-any.whl", hash = "sha256:bf4a44e03040206f7c058d1f5ba02ef2d1820720c88bc4285c7d9a4269f54173"}, {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
{file = "mypy-0.930.tar.gz", hash = "sha256:51426262ae4714cc7dd5439814676e0992b55bcc0f6514eccb4cf8e0678962c2"}, {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
] ]
[package.dependencies] [package.dependencies]
mypy-extensions = ">=0.4.3" mypy-extensions = ">=1.0.0"
tomli = ">=1.1.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10" typing-extensions = ">=4.6.0"
[package.extras] [package.extras]
dmypy = ["psutil (>=4.0)"] dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"] install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
@ -2270,4 +2279,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "19b76bbf7fcd66676f2d95fd8b7969bc5abfec4ed70b6237f8f4606089b74d5c" content-hash = "5db0c01a710f54d0e31cbfd93f1b7ed2c445581c214c9dcff12370db3e47df3d"

View File

@ -49,7 +49,7 @@ ipython = {version = "^8.10.1", python = "^3.10"}
isort = {version = "^5.13.2", python = "^3.10"} isort = {version = "^5.13.2", python = "^3.10"}
mdformat = {version = "^0.7", python = "^3.10"} mdformat = {version = "^0.7", python = "^3.10"}
mdformat-gfm = {version = "^0.3", python = "^3.10"} mdformat-gfm = {version = "^0.3", python = "^3.10"}
mypy = {version = "^0.930", python = "^3.10"} mypy = {version = "^1.11.1", python = "^3.10"}
pre-commit = {version = "^3.8.0", python = "^3.10"} pre-commit = {version = "^3.8.0", python = "^3.10"}
pre-commit-hooks = {version = "^4.6.0", python = "^3.10"} pre-commit-hooks = {version = "^4.6.0", python = "^3.10"}
pylint = {version = "^3.2.6", python = "^3.10"} pylint = {version = "^3.2.6", python = "^3.10"}

View File

@ -5,10 +5,10 @@ from typing import List
import poetry.factory import poetry.factory
import poetry.installation.executor import poetry.installation.executor
import poetry.installation.operations.operation
import poetry.utils.env import poetry.utils.env
import pytest import pytest
import tox.tox_env.python.virtual_env.runner import tox.tox_env.python.virtual_env.runner
from poetry.installation.operations.operation import Operation
import tox_poetry_installer.hooks._tox_on_install_helpers import tox_poetry_installer.hooks._tox_on_install_helpers
@ -40,7 +40,9 @@ class MockExecutor:
def __init__(self, env: MockVirtualEnv, **kwargs): def __init__(self, env: MockVirtualEnv, **kwargs):
self.env = env self.env = env
def execute(self, operations: List[Operation]): def execute(
self, operations: List[poetry.installation.operations.operation.Operation]
):
self.env.installed.extend([operation.package for operation in operations]) self.env.installed.extend([operation.package for operation in operations])
time.sleep(1) time.sleep(1)
@ -61,9 +63,9 @@ def mock_venv(monkeypatch):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mock_poetry_factory(monkeypatch): def mock_poetry_factory(monkeypatch):
pypoetry = poetry.factory.Factory().create_poetry(cwd=TEST_PROJECT_PATH) project = poetry.factory.Factory().create_poetry(cwd=TEST_PROJECT_PATH)
def mock_factory(*args, **kwargs): def mock_factory(*args, **kwargs):
return pypoetry return project
monkeypatch.setattr(poetry.factory.Factory, "create_poetry", mock_factory) monkeypatch.setattr(poetry.factory.Factory, "create_poetry", mock_factory)

View File

@ -2,9 +2,10 @@
import time import time
from unittest import mock from unittest import mock
import poetry.factory
import poetry.installation.executor
import pytest import pytest
import tox.tox_env.python.virtual_env.runner import tox.tox_env.python.virtual_env.runner
from poetry.factory import Factory
import tox_poetry_installer.hooks._tox_on_install_helpers import tox_poetry_installer.hooks._tox_on_install_helpers
@ -14,16 +15,16 @@ from .fixtures import mock_venv
def test_deduplication(mock_venv, mock_poetry_factory): def test_deduplication(mock_venv, mock_poetry_factory):
"""Test that the installer does not install duplicate dependencies""" """Test that the installer does not install duplicate dependencies"""
poetry = Factory().create_poetry(None) project = poetry.factory.Factory().create_poetry(None)
packages: tox_poetry_installer.hooks._tox_on_install_helpers.PackageMap = { packages: tox_poetry_installer.hooks._tox_on_install_helpers.PackageMap = {
item.name: item for item in poetry.locker.locked_repository().packages item.name: item for item in project.locker.locked_repository().packages
} }
venv = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner() venv = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner()
to_install = [packages["toml"], packages["toml"]] to_install = [packages["toml"], packages["toml"]]
tox_poetry_installer.hooks._tox_on_install_helpers.install_package( tox_poetry_installer.hooks._tox_on_install_helpers.install_package(
poetry, venv, to_install project, venv, to_install
) )
assert len(set(to_install)) == len(venv.installed) # pylint: disable=no-member assert len(set(to_install)) == len(venv.installed) # pylint: disable=no-member
@ -31,9 +32,9 @@ def test_deduplication(mock_venv, mock_poetry_factory):
def test_parallelization(mock_venv, mock_poetry_factory): def test_parallelization(mock_venv, mock_poetry_factory):
"""Test that behavior is consistent between parallel and non-parallel usage""" """Test that behavior is consistent between parallel and non-parallel usage"""
poetry = Factory().create_poetry(None) project = poetry.factory.Factory().create_poetry(None)
packages: tox_poetry_installer.hooks._tox_on_install_helpers.PackageMap = { packages: tox_poetry_installer.hooks._tox_on_install_helpers.PackageMap = {
item.name: item for item in poetry.locker.locked_repository().packages item.name: item for item in project.locker.locked_repository().packages
} }
to_install = [ to_install = [
@ -48,14 +49,14 @@ def test_parallelization(mock_venv, mock_poetry_factory):
venv_sequential = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner() venv_sequential = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner()
start_sequential = time.time() start_sequential = time.time()
tox_poetry_installer.hooks._tox_on_install_helpers.install_package( tox_poetry_installer.hooks._tox_on_install_helpers.install_package(
poetry, venv_sequential, to_install, 0 project, venv_sequential, to_install, 0
) )
sequential = time.time() - start_sequential sequential = time.time() - start_sequential
venv_parallel = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner() venv_parallel = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner()
start_parallel = time.time() start_parallel = time.time()
tox_poetry_installer.hooks._tox_on_install_helpers.install_package( tox_poetry_installer.hooks._tox_on_install_helpers.install_package(
poetry, venv_parallel, to_install, 5 project, venv_parallel, to_install, 5
) )
parallel = time.time() - start_parallel parallel = time.time() - start_parallel
@ -75,24 +76,22 @@ def test_propagates_exceptions_during_installation(
Regression test for https://github.com/enpaul/tox-poetry-installer/issues/86 Regression test for https://github.com/enpaul/tox-poetry-installer/issues/86
""" """
from tox_poetry_installer import _poetry # pylint: disable=import-outside-toplevel project = poetry.factory.Factory().create_poetry(None)
poetry = Factory().create_poetry(None)
packages: tox_poetry_installer.hooks._tox_on_install_helpers.PackageMap = { packages: tox_poetry_installer.hooks._tox_on_install_helpers.PackageMap = {
item.name: item for item in poetry.locker.locked_repository().packages item.name: item for item in project.locker.locked_repository().packages
} }
to_install = [packages["toml"]] to_install = [packages["toml"]]
venv = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner() venv = tox.tox_env.python.virtual_env.runner.VirtualEnvRunner()
fake_exception = ValueError("my testing exception") fake_exception = ValueError("my testing exception")
with mock.patch.object( with mock.patch.object(
_poetry, poetry.installation.executor,
"Executor", "Executor",
**{"return_value.execute.side_effect": fake_exception}, **{"return_value.execute.side_effect": fake_exception},
): ):
with pytest.raises(ValueError) as exc_info: with pytest.raises(ValueError) as exc_info:
tox_poetry_installer.hooks._tox_on_install_helpers.install_package( tox_poetry_installer.hooks._tox_on_install_helpers.install_package(
poetry, venv, to_install, num_threads project, venv, to_install, num_threads
) )
assert exc_info.value is fake_exception assert exc_info.value is fake_exception

View File

@ -48,9 +48,9 @@ def test_functional(mock_poetry_factory, mock_venv):
Trivially test that it resolves dependencies properly and that the parent package Trivially test that it resolves dependencies properly and that the parent package
is always the last in the returned list. is always the last in the returned list.
""" """
pypoetry = poetry.factory.Factory().create_poetry(None) project = poetry.factory.Factory().create_poetry(None)
packages = tox_poetry_installer.hooks._tox_on_install_helpers.build_package_map( packages = tox_poetry_installer.hooks._tox_on_install_helpers.build_package_map(
pypoetry project
) )
venv = poetry.utils.env.VirtualEnv() # pylint: disable=no-value-for-parameter venv = poetry.utils.env.VirtualEnv() # pylint: disable=no-value-for-parameter

View File

@ -1,44 +0,0 @@
"""You've heard of vendoirization, now get ready for internal namespace shadowing
Poetry is an optional dependency of this package explicitly to support the use case of having the
plugin and the `poetry` package installed to the same python environment; this is most common in
containers and/or CI. In this case there are two potential problems that can arise in this case:
* The installation of the plugin overwrites the installed version of Poetry resulting in
compatibility issues.
* Running `poetry install --no-dev`, when this plugin is in the dev-deps, results in poetry being
uninstalled from the environment.
To support these edge cases, and more broadly to support not messing with a system package manager,
the `poetry` package dependency is listed as optional dependency. This allows the plugin to be
installed to the same environment as Poetry and import that same Poetry installation here.
However, simply importing Poetry on the assumption that it is installed breaks another valid use
case: having this plugin installed alongside Tox when not using a Poetry-based project. To account
for this the imports in this module are isolated and the resultant import error that would result
is converted to an internal error that can be caught by callers. Rather than importing this module
at the module scope it is imported into function scope wherever Poetry components are needed. This
moves import errors from load time to runtime which allows the plugin to be skipped if Poetry isn't
installed and/or a more helpful error be raised within the Tox framework.
"""
import sys
from tox_poetry_installer import exceptions
try:
# pylint: disable=import-outside-toplevel,unused-import
from cleo.io.null_io import NullIO
from poetry.config.config import Config
from poetry.core.packages.dependency import Dependency as PoetryDependency
from poetry.core.packages.package import Package as PoetryPackage
from poetry.factory import Factory
from poetry.installation.executor import Executor
from poetry.installation.operations.install import Install
from poetry.poetry import Poetry
from poetry.utils.env import VirtualEnv
except ImportError as err:
raise exceptions.PoetryNotInstalledError(
f"Failed to import a supported version of Poetry under the current environment '{sys.executable}': {err}"
) from None

View File

@ -3,36 +3,36 @@
import collections import collections
import concurrent.futures import concurrent.futures
import contextlib import contextlib
import typing import datetime
from datetime import datetime import pathlib
from pathlib import Path
from typing import Collection from typing import Collection
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from poetry.core.packages.dependency import Dependency as PoetryDependency import cleo.io.null_io
from poetry.core.packages.package import Package as PoetryPackage import packaging.utils
from tox.tox_env.api import ToxEnv as ToxVirtualEnv import poetry.config.config
from tox.tox_env.package import PackageToxEnv import poetry.core.packages.dependency
import poetry.core.packages.package
import poetry.factory
import poetry.installation.executor
import poetry.installation.operations.install
import poetry.poetry
import poetry.utils.env
import tox.tox_env.api
import tox.tox_env.package
from tox_poetry_installer import constants from tox_poetry_installer import constants
from tox_poetry_installer import exceptions from tox_poetry_installer import exceptions
from tox_poetry_installer import logger from tox_poetry_installer import logger
if typing.TYPE_CHECKING: PackageMap = Dict[str, List[poetry.core.packages.package.Package]]
from tox_poetry_installer import _poetry
# This is globally disabled to support the usage of the _poetry shadow module
# pylint: disable=import-outside-toplevel
PackageMap = Dict[str, List[PoetryPackage]] def check_preconditions(venv: tox.tox_env.api.ToxEnv) -> poetry.poetry.Poetry:
def check_preconditions(venv: ToxVirtualEnv) -> "_poetry.Poetry":
"""Check that the local project environment meets expectations""" """Check that the local project environment meets expectations"""
# Skip running the plugin for the provisioning environment. The provisioned environment, # Skip running the plugin for the provisioning environment. The provisioned environment,
@ -40,13 +40,11 @@ def check_preconditions(venv: ToxVirtualEnv) -> "_poetry.Poetry":
# handled by Tox and is out of scope for this plugin. Since one of the ways to install this # handled by Tox and is out of scope for this plugin. Since one of the ways to install this
# plugin in the first place is via the Tox provisioning environment, it quickly becomes a # plugin in the first place is via the Tox provisioning environment, it quickly becomes a
# chicken-and-egg problem. # chicken-and-egg problem.
if isinstance(venv, PackageToxEnv): if isinstance(venv, tox.tox_env.package.PackageToxEnv):
raise exceptions.SkipEnvironment(f"Skipping Tox provisioning env '{venv.name}'") raise exceptions.SkipEnvironment(f"Skipping Tox provisioning env '{venv.name}'")
from tox_poetry_installer import _poetry
try: try:
return _poetry.Factory().create_poetry(venv.core["tox_root"]) return poetry.factory.Factory().create_poetry(venv.core["tox_root"])
# Support running the plugin when the current tox project does not use Poetry for its # Support running the plugin when the current tox project does not use Poetry for its
# environment/dependency management. # environment/dependency management.
# #
@ -61,9 +59,9 @@ def check_preconditions(venv: ToxVirtualEnv) -> "_poetry.Poetry":
def identify_transients( def identify_transients(
dep_name: str, dep_name: str,
packages: PackageMap, packages: PackageMap,
venv: "_poetry.VirtualEnv", venv: poetry.utils.env.VirtualEnv,
allow_missing: Sequence[str] = (), allow_missing: Sequence[str] = (),
) -> List[PoetryPackage]: ) -> List[poetry.core.packages.package.Package]:
"""Using a pool of packages, identify all transient dependencies of a given package name """Using a pool of packages, identify all transient dependencies of a given package name
:param dep_name: Either the Poetry dependency or the dependency's bare package name to recursively :param dep_name: Either the Poetry dependency or the dependency's bare package name to recursively
@ -80,10 +78,12 @@ def identify_transients(
""" """
searched: Set[str] = set() searched: Set[str] = set()
def _transients(transient: PoetryDependency) -> List[PoetryPackage]: def _transients(
transient: poetry.core.packages.dependency.Dependency,
) -> List[poetry.core.packages.package.Package]:
searched.add(transient.name) searched.add(transient.name)
results: List[PoetryPackage] = [] results: List[poetry.core.packages.package.Package] = []
for option in packages[transient.name]: for option in packages[transient.name]:
if venv.is_valid_for_marker(option.to_dependency().marker): if venv.is_valid_for_marker(option.to_dependency().marker):
for requirement in option.requires: for requirement in option.requires:
@ -132,38 +132,43 @@ def identify_transients(
def find_project_deps( def find_project_deps(
packages: PackageMap, packages: PackageMap,
venv: "_poetry.VirtualEnv", venv: poetry.utils.env.VirtualEnv,
poetry: "_poetry.Poetry", project: poetry.poetry.Poetry,
extras: Sequence[str] = (), extras: Sequence[str] = (),
) -> List[PoetryPackage]: ) -> List[poetry.core.packages.package.Package]:
"""Find the root project dependencies """Find the root project dependencies
Recursively identify the dependencies of the root project package Recursively identify the dependencies of the root project package
:param packages: Mapping of all locked package names to their corresponding package object :param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks :param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project :param project: Poetry object for the current project
:param extras: Sequence of extra names to include the dependencies of :param extras: Sequence of extra names to include the dependencies of
""" """
required_dep_names = [ required_dep_names = [
item.name for item in poetry.package.requires if not item.is_optional() item.name for item in project.package.requires if not item.is_optional()
] ]
extra_dep_names: List[str] = [] extra_dep_names: List[str] = []
for extra in extras: for extra in extras:
logger.info(f"Processing project extra '{extra}'") logger.info(f"Processing project extra '{extra}'")
try: try:
extra_dep_names += [item.name for item in poetry.package.extras[extra]] extra_dep_names += [
item.name
for item in project.package.extras[
packaging.utils.NormalizedName(extra)
]
]
except KeyError: except KeyError:
raise exceptions.ExtraNotFoundError( raise exceptions.ExtraNotFoundError(
f"Environment specifies project extra '{extra}' which was not found in the lockfile" f"Environment specifies project extra '{extra}' which was not found in the lockfile"
) from None ) from None
dependencies: List[PoetryPackage] = [] dependencies: List[poetry.core.packages.package.Package] = []
for dep_name in required_dep_names + extra_dep_names: for dep_name in required_dep_names + extra_dep_names:
dependencies += identify_transients( dependencies += identify_transients(
dep_name.lower(), packages, venv, allow_missing=[poetry.package.name] dep_name.lower(), packages, venv, allow_missing=[project.package.name]
) )
return dedupe_packages(dependencies) return dedupe_packages(dependencies)
@ -171,24 +176,24 @@ def find_project_deps(
def find_additional_deps( def find_additional_deps(
packages: PackageMap, packages: PackageMap,
venv: "_poetry.VirtualEnv", venv: poetry.utils.env.VirtualEnv,
poetry: "_poetry.Poetry", project: poetry.poetry.Poetry,
dep_names: Sequence[str], dep_names: Sequence[str],
) -> List[PoetryPackage]: ) -> List[poetry.core.packages.package.Package]:
"""Find additional dependencies """Find additional dependencies
Recursively identify the dependencies of an arbitrary list of package names Recursively identify the dependencies of an arbitrary list of package names
:param packages: Mapping of all locked package names to their corresponding package object :param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks :param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project :param project: Poetry object for the current project
:param dep_names: Sequence of additional dependency names to recursively find the transient :param dep_names: Sequence of additional dependency names to recursively find the transient
dependencies for dependencies for
""" """
dependencies: List[PoetryPackage] = [] dependencies: List[poetry.core.packages.package.Package] = []
for dep_name in dep_names: for dep_name in dep_names:
dependencies += identify_transients( dependencies += identify_transients(
dep_name.lower(), packages, venv, allow_missing=[poetry.package.name] dep_name.lower(), packages, venv, allow_missing=[project.package.name]
) )
return dedupe_packages(dependencies) return dedupe_packages(dependencies)
@ -197,9 +202,9 @@ def find_additional_deps(
def find_group_deps( def find_group_deps(
group: str, group: str,
packages: PackageMap, packages: PackageMap,
venv: "_poetry.VirtualEnv", venv: poetry.utils.env.VirtualEnv,
poetry: "_poetry.Poetry", project: poetry.poetry.Poetry,
) -> List[PoetryPackage]: ) -> List[poetry.core.packages.package.Package]:
"""Find the dependencies belonging to a dependency group """Find the dependencies belonging to a dependency group
Recursively identify the Poetry dev dependencies Recursively identify the Poetry dev dependencies
@ -207,13 +212,17 @@ def find_group_deps(
:param group: Name of the dependency group from the project's ``pyproject.toml`` :param group: Name of the dependency group from the project's ``pyproject.toml``
:param packages: Mapping of all locked package names to their corresponding package object :param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks :param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project :param project: Poetry object for the current project
""" """
return find_additional_deps( return find_additional_deps(
packages, packages,
venv, venv,
poetry, project,
poetry.pyproject.data["tool"]["poetry"] # the type ignore here is due to the difficulties around getting nested data
# from the inherrently unstructured toml structure (which necessarily is flexibly
# typed) but in a situation where there is a meta-structure applied to it (i.e. a
# pyproject.toml structure).
project.pyproject.data["tool"]["poetry"] # type: ignore
.get("group", {}) .get("group", {})
.get(group, {}) .get(group, {})
.get("dependencies", {}) .get("dependencies", {})
@ -222,81 +231,87 @@ def find_group_deps(
def find_dev_deps( def find_dev_deps(
packages: PackageMap, venv: "_poetry.VirtualEnv", poetry: "_poetry.Poetry" packages: PackageMap,
) -> List[PoetryPackage]: venv: poetry.utils.env.VirtualEnv,
project: poetry.poetry.Poetry,
) -> List[poetry.core.packages.package.Package]:
"""Find the dev dependencies """Find the dev dependencies
Recursively identify the Poetry dev dependencies Recursively identify the Poetry dev dependencies
:param packages: Mapping of all locked package names to their corresponding package object :param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks :param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project :param project: Poetry object for the current project
""" """
dev_group_deps = find_group_deps("dev", packages, venv, poetry) dev_group_deps = find_group_deps("dev", packages, venv, project)
# Legacy pyproject.toml poetry format: # Legacy pyproject.toml poetry format:
legacy_dev_group_deps = find_additional_deps( legacy_dev_group_deps = find_additional_deps(
packages, packages,
venv, venv,
poetry, project,
poetry.pyproject.data["tool"]["poetry"].get("dev-dependencies", {}).keys(), # the type ignore here is due to the difficulties around getting nested data
# from the inherrently unstructured toml structure (which necessarily is flexibly
# typed) but in a situation where there is a meta-structure applied to it (i.e. a
# pyproject.toml structure).
project.pyproject.data["tool"]["poetry"].get("dev-dependencies", {}).keys(), # type: ignore
) )
# Poetry 1.2 unions these two toml sections. # Poetry 1.2 unions these two toml sections.
return dedupe_packages(dev_group_deps + legacy_dev_group_deps) return dedupe_packages(dev_group_deps + legacy_dev_group_deps)
@contextlib.contextmanager
def _optional_parallelize(parallels: int):
"""A bit of cheat, really
A context manager that exposes a common interface for the caller that optionally
enables/disables the usage of the parallel thread pooler depending on the value of
the ``parallels`` parameter.
"""
if parallels > 0:
with concurrent.futures.ThreadPoolExecutor(max_workers=parallels) as executor:
yield executor.submit
else:
yield lambda func, arg: func(arg)
def install_package( def install_package(
poetry: "_poetry.Poetry", project: poetry.poetry.Poetry,
venv: ToxVirtualEnv, venv: tox.tox_env.api.ToxEnv,
packages: Collection["_poetry.PoetryPackage"], packages: Collection[poetry.core.packages.package.Package],
parallels: int = 0, parallels: int = 0,
): ):
"""Install a bunch of packages to a virtualenv """Install a bunch of packages to a virtualenv
:param poetry: Poetry object the packages were sourced from :param project: Poetry object the packages were sourced from
:param venv: Tox virtual environment to install the packages to :param venv: Tox virtual environment to install the packages to
:param packages: List of packages to install to the virtual environment :param packages: List of packages to install to the virtual environment
:param parallels: Number of parallel processes to use for installing dependency packages, or :param parallels: Number of parallel processes to use for installing dependency packages, or
``None`` to disable parallelization. ``None`` to disable parallelization.
""" """
from tox_poetry_installer import _poetry
logger.info(f"Installing {len(packages)} packages to environment at {venv.env_dir}") logger.info(f"Installing {len(packages)} packages to environment at {venv.env_dir}")
install_executor = _poetry.Executor( install_executor = poetry.installation.executor.Executor(
env=convert_virtualenv(venv), env=convert_virtualenv(venv),
io=_poetry.NullIO(), io=cleo.io.null_io.NullIO(),
pool=poetry.pool, pool=project.pool,
config=_poetry.Config(), config=poetry.config.config.Config(),
) )
installed: Set[_poetry.PoetryPackage] = set() installed: Set[poetry.core.packages.package.Package] = set()
def logged_install(dependency: _poetry.PoetryPackage) -> None: def logged_install(dependency: poetry.core.packages.package.Package) -> None:
start = datetime.now() start = datetime.datetime.now()
logger.debug(f"Installing {dependency}") logger.debug(f"Installing {dependency}")
install_executor.execute([_poetry.Install(package=dependency)]) install_executor.execute(
end = datetime.now() [poetry.installation.operations.install.Install(package=dependency)]
)
end = datetime.datetime.now()
logger.debug(f"Finished installing {dependency} in {end - start}") logger.debug(f"Finished installing {dependency} in {end - start}")
@contextlib.contextmanager with _optional_parallelize(parallels) as executor:
def _optional_parallelize():
"""A bit of cheat, really
A context manager that exposes a common interface for the caller that optionally
enables/disables the usage of the parallel thread pooler depending on the value of
the ``parallels`` parameter.
"""
if parallels > 0:
with concurrent.futures.ThreadPoolExecutor(
max_workers=parallels
) as executor:
yield executor.submit
else:
yield lambda func, arg: func(arg)
with _optional_parallelize() as executor:
futures = [] futures = []
for dependency in packages: for dependency in packages:
if dependency not in installed: if dependency not in installed:
@ -316,36 +331,36 @@ def install_package(
future.result() future.result()
def dedupe_packages(packages: Sequence[PoetryPackage]) -> List[PoetryPackage]: def dedupe_packages(
"""Deduplicates a sequence of PoetryPackages while preserving ordering packages: Sequence[poetry.core.packages.package.Package],
) -> List[poetry.core.packages.package.Package]:
"""Deduplicates a sequence of Packages while preserving ordering
Adapted from StackOverflow: https://stackoverflow.com/a/480227 Adapted from StackOverflow: https://stackoverflow.com/a/480227
""" """
seen: Set[PoetryPackage] = set() seen: Set[poetry.core.packages.package.Package] = set()
# Make this faster, avoid method lookup below # Make this faster, avoid method lookup below
seen_add = seen.add seen_add = seen.add
return [p for p in packages if not (p in seen or seen_add(p))] return [item for item in packages if not (item in seen or seen_add(item))]
def convert_virtualenv(venv: ToxVirtualEnv) -> "_poetry.VirtualEnv": def convert_virtualenv(venv: tox.tox_env.api.ToxEnv) -> poetry.utils.env.VirtualEnv:
"""Convert a Tox venv to a Poetry venv """Convert a Tox venv to a Poetry venv
:param venv: Tox ``VirtualEnv`` object representing a tox virtual environment :param venv: Tox ``VirtualEnv`` object representing a tox virtual environment
:returns: Poetry ``VirtualEnv`` object representing a poetry virtual environment :returns: Poetry ``VirtualEnv`` object representing a poetry virtual environment
""" """
from tox_poetry_installer import _poetry return poetry.utils.env.VirtualEnv(path=pathlib.Path(venv.env_dir))
return _poetry.VirtualEnv(path=Path(venv.env_dir))
def build_package_map(poetry: "_poetry.Poetry") -> PackageMap: def build_package_map(project: poetry.poetry.Poetry) -> PackageMap:
"""Build the mapping of package names to objects """Build the mapping of package names to objects
:param poetry: Populated poetry object to load locked packages from :param project: Populated poetry object to load locked packages from
:returns: Mapping of package names to Poetry package objects :returns: Mapping of package names to Poetry package objects
""" """
packages = collections.defaultdict(list) packages = collections.defaultdict(list)
for package in poetry.locker.locked_repository().packages: for package in project.locker.locked_repository().packages:
packages[package.name].append(package) packages[str(package.name)].append(package)
return packages return packages

View File

@ -2,14 +2,14 @@
from typing import List from typing import List
from tox.config.sets import EnvConfigSet import tox.config.sets
from tox.plugin import impl import tox.plugin
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
@impl @tox.plugin.impl
def tox_add_env_config( def tox_add_env_config(
env_conf: EnvConfigSet, env_conf: tox.config.sets.EnvConfigSet,
): ):
env_conf.add_config( env_conf.add_config(
"poetry_dep_groups", "poetry_dep_groups",

View File

@ -1,14 +1,14 @@
"""Add additional command line arguments to tox to configure plugin behavior""" """Add additional command line arguments to tox to configure plugin behavior"""
from tox.config.cli.parser import ToxParser import tox.config.cli.parser
from tox.plugin import impl import tox.plugin
from tox_poetry_installer import constants from tox_poetry_installer import constants
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
@impl @tox.plugin.impl
def tox_add_option(parser: ToxParser): def tox_add_option(parser: tox.config.cli.parser.ToxParser):
parser.add_argument( parser.add_argument(
"--parallel-install-threads", "--parallel-install-threads",
type=int, type=int,

View File

@ -5,10 +5,10 @@ specified by the Tox environment. Finally these dependencies are installed into
environment using the Poetry ``PipInstaller`` backend. environment using the Poetry ``PipInstaller`` backend.
""" """
from itertools import chain import itertools
from tox.plugin import impl import tox.plugin
from tox.tox_env.api import ToxEnv as ToxVirtualEnv import tox.tox_env.api
from tox_poetry_installer import exceptions from tox_poetry_installer import exceptions
from tox_poetry_installer import logger from tox_poetry_installer import logger
@ -23,8 +23,8 @@ from tox_poetry_installer.hooks._tox_on_install_helpers import install_package
# pylint: disable=missing-function-docstring,unused-argument # pylint: disable=missing-function-docstring,unused-argument
@impl @tox.plugin.impl
def tox_on_install(tox_env: ToxVirtualEnv, *args) -> None: def tox_on_install(tox_env: tox.tox_env.api.ToxEnv, *args) -> None:
try: try:
poetry = check_preconditions(tox_env) poetry = check_preconditions(tox_env)
except exceptions.SkipEnvironment as err: except exceptions.SkipEnvironment as err:
@ -62,7 +62,7 @@ def tox_on_install(tox_env: ToxVirtualEnv, *args) -> None:
group_deps = dedupe_packages( group_deps = dedupe_packages(
list( list(
chain( itertools.chain(
*[ *[
find_group_deps(group, packages, virtualenv, poetry) find_group_deps(group, packages, virtualenv, poetry)
for group in tox_env.conf["poetry_dep_groups"] for group in tox_env.conf["poetry_dep_groups"]