mirror of
https://github.com/enpaul/tox-poetry-installer.git
synced 2025-10-28 07:00:43 +00:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 429992cff9 | |||
| 216ee8b095 | |||
| e96f4fb5e5 | |||
| eebd16383d | |||
| add4ec62eb | |||
| 0c25ca965e | |||
| 6a5e955fce | |||
| d910b6ee8d | |||
| e37c166a8b | |||
| 3d6d39eca8 | |||
| b9b0eba90f | |||
| df8312f5ee | |||
| 1f102b16cb | |||
| a7cde7a9ab | |||
| b0bee11272 | |||
| accb4c3278 | |||
| ea183553c4 | |||
| 5c5536581b | |||
| b6ef671e67 | |||
| 99c10482fc | |||
| b32a212e82 | |||
| b6415888d9 | |||
| eba268a127 | |||
| d8d483e849 | |||
| c14313b7b0 | |||
| d1f161e0fa | |||
| 2961b55c9a | |||
| d4fb7046d8 | |||
| 8c4e596316 | |||
| 50f6e3d151 | |||
| 106d1bf6cf | |||
| b57b78d4e2 | |||
| 33e81f742a | |||
| 3a262d718c | |||
| 5979ec7a8a | |||
| c7bb3d35ea | |||
| 961e6f6acd | |||
| ed039de674 | |||
| db0cf6ce0c | |||
| e8d3f4fcac | |||
| 653622fd35 | |||
| de4c3515ec | |||
| 01bfdd74bd | |||
| 3e98ec81eb | |||
| b6534f86d0 | |||
| 74e3ed01c0 | |||
| f944843278 | |||
| 7c761073b3 | |||
| 6075ea6a3e | |||
| fdee2d9a8d | |||
| 699fb347da | |||
| a3bfd2687a | |||
| 0bf3b16091 | |||
| 979fa58618 | |||
| 462cc166a9 | |||
| 30160985c1 | |||
| 5d87ffac72 | |||
| 6b92189e50 | |||
| 0fbc77c2c4 | |||
| 7867a7c98b | |||
| 7a34c47168 | |||
| 4a1dc52755 | |||
| 1e2156ecdb | |||
| 707a73c922 | |||
| 640dbfe102 | |||
| 05c5a26cc4 | |||
| ee6f939b6a | |||
| 82678e81e8 | |||
| 5411025612 |
43
.github/workflows/ci.yaml
vendored
Normal file
43
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: ["opened", "synchronize"]
|
||||||
|
push:
|
||||||
|
branches: ["devel"]
|
||||||
|
jobs:
|
||||||
|
Test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python:
|
||||||
|
- version: 3.6
|
||||||
|
toxenv: py36
|
||||||
|
- version: 3.7
|
||||||
|
toxenv: py37
|
||||||
|
- version: 3.8
|
||||||
|
toxenv: py38
|
||||||
|
- version: 3.9
|
||||||
|
toxenv: py39
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python.version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python.version }}
|
||||||
|
- name: Install project
|
||||||
|
run: pip install .
|
||||||
|
- name: Run tests via ${{ matrix.python.toxenv }}
|
||||||
|
run: tox -e ${{ matrix.python.toxenv }}
|
||||||
|
Check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python 3.8
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
- name: Install project
|
||||||
|
run: pip install .
|
||||||
|
- name: Run meta checks
|
||||||
|
run: tox -e static -e static-tests -e security
|
||||||
@@ -1,28 +1,48 @@
|
|||||||
---
|
---
|
||||||
|
# All of the pre-commit hooks here actually use the `pytyhon` pre-commit language
|
||||||
|
# setting. However, for the python language setting, pre-commit will create and manage
|
||||||
|
# a cached virtual environment for each hook ID and do a bare `pip install <repo>` into
|
||||||
|
# the venv to setup the hook. This can result in conflicting dependency versions between
|
||||||
|
# the version installed to the pre-commit venv and the version installed to the Poetry
|
||||||
|
# venv specified in the lockfile.
|
||||||
|
#
|
||||||
|
# The solution is to specify `language: system` for all hooks and then install the
|
||||||
|
# required dependencies to the Poetry venv. The `system` language skips the isolated
|
||||||
|
# venv creation and looks for the entrypoint specified by the hook in the global
|
||||||
|
# environment which, if running in the Poetry venv, will find the entrypoint provided
|
||||||
|
# by the Poetry-managed dependency.
|
||||||
|
#
|
||||||
repos:
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v3.3.0
|
||||||
|
hooks:
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
language: system
|
||||||
|
- id: fix-encoding-pragma
|
||||||
|
args:
|
||||||
|
- "--remove"
|
||||||
|
language: system
|
||||||
|
- id: trailing-whitespace
|
||||||
|
language: system
|
||||||
|
- id: check-merge-conflict
|
||||||
|
language: system
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: 20.8b1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3.7
|
language: system
|
||||||
|
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
- repo: https://github.com/asottile/blacken-docs
|
||||||
rev: v0.5.0
|
rev: v1.8.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
additional_dependencies: [black==20.8b1]
|
language: system
|
||||||
language_version: python3.7
|
|
||||||
|
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
rev: v2.3.5
|
rev: v2.3.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
language_version: python3
|
args:
|
||||||
|
- "--unclassifiable-application-module=tox_poetry_installer"
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
language: system
|
||||||
rev: v2.0.0
|
|
||||||
hooks:
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: fix-encoding-pragma
|
|
||||||
args: [--remove]
|
|
||||||
- id: trailing-whitespace
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -33,4 +33,4 @@ test: ## Run the project testsuite(s)
|
|||||||
poetry run tox --recreate
|
poetry run tox --recreate
|
||||||
|
|
||||||
publish: wheel source ## Build and upload to pypi (requires $PYPI_API_KEY be set)
|
publish: wheel source ## Build and upload to pypi (requires $PYPI_API_KEY be set)
|
||||||
poetry publish --username __token__ --password $(PYPI_API_KEY)
|
@poetry publish --username __token__ --password $(PYPI_API_KEY)
|
||||||
|
|||||||
386
README.md
386
README.md
@@ -1,20 +1,24 @@
|
|||||||
# tox-poetry-installer
|
# tox-poetry-installer
|
||||||
|
|
||||||
A plugin for [Tox](https://tox.readthedocs.io/en/latest/) that allows test environment
|
A plugin for [Tox](https://tox.readthedocs.io/en/latest/) that allows test environment
|
||||||
dependencies to be installed using [Poetry](https://python-poetry.org/) using its lockfile.
|
dependencies to be installed using [Poetry](https://python-poetry.org/) from its lockfile.
|
||||||
|
|
||||||
⚠️ **This project is alpha software and should not be used in a production capacity** ⚠️
|
⚠️ **This project is alpha software and should not be used in production environments** ⚠️
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://github.com/enpaul/tox-poetry-installer/actions)
|
||||||
[](https://pypi.org/project/tox-poetry-installer/)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.python.org)
|
[](https://pypi.org/project/tox-poetry-installer/)
|
||||||
|
[](https://www.python.org)
|
||||||
[](https://github.com/psf/black)
|
[](https://github.com/psf/black)
|
||||||
|
|
||||||
**Documentation**
|
**Documentation**
|
||||||
|
|
||||||
* [Installation](#installation)
|
* [Installation](#installation)
|
||||||
* [Quick Start](#quick-start)
|
* [Quick Start](#quick-start)
|
||||||
* [Usage Examples](#usage-examples)
|
* [Reference and Usage](#reference-and-usage)
|
||||||
|
* [Config Option Reference](#config-option-reference)
|
||||||
|
* [Error Reference](#error-reference)
|
||||||
|
* [Example Config](#example-config)
|
||||||
* [Known Drawbacks and Problems](#known-drawbacks-and-problems)
|
* [Known Drawbacks and Problems](#known-drawbacks-and-problems)
|
||||||
* [Why would I use this?](#why-would-i-use-this) (What problems does this solve?)
|
* [Why would I use this?](#why-would-i-use-this) (What problems does this solve?)
|
||||||
* [Developing](#developing)
|
* [Developing](#developing)
|
||||||
@@ -26,12 +30,14 @@ dependencies to be installed using [Poetry](https://python-poetry.org/) using it
|
|||||||
Related resources:
|
Related resources:
|
||||||
* [Poetry Python Project Manager](https://python-poetry.org/)
|
* [Poetry Python Project Manager](https://python-poetry.org/)
|
||||||
* [Tox Automation Project](https://tox.readthedocs.io/en/latest/)
|
* [Tox Automation Project](https://tox.readthedocs.io/en/latest/)
|
||||||
* [Tox plugins](https://tox.readthedocs.io/en/latest/plugins.html)
|
* [Poetry Dev-Dependencies Tox Plugin](https://github.com/sinoroc/tox-poetry-dev-dependencies)
|
||||||
|
* [Poetry Tox Plugin](https://github.com/tkukushkin/tox-poetry)
|
||||||
|
* [Other Tox plugins](https://tox.readthedocs.io/en/latest/plugins.html)
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Add the plugin as a development dependency a project using Poetry:
|
Add the plugin as a development dependency of a Poetry project:
|
||||||
|
|
||||||
```
|
```
|
||||||
~ $: poetry add tox-poetry-installer --dev
|
~ $: poetry add tox-poetry-installer --dev
|
||||||
@@ -43,11 +49,10 @@ Confirm that the plugin is installed, and Tox recognizes it, by checking the Tox
|
|||||||
~ $: poetry run tox --version
|
~ $: poetry run tox --version
|
||||||
3.20.0 imported from .venv/lib64/python3.8/site-packages/tox/__init__.py
|
3.20.0 imported from .venv/lib64/python3.8/site-packages/tox/__init__.py
|
||||||
registered plugins:
|
registered plugins:
|
||||||
tox-poetry-installer-0.2.0 at .venv/lib64/python3.8/site-packages/tox_poetry_installer.py
|
tox-poetry-installer-0.5.0 at .venv/lib64/python3.8/site-packages/tox_poetry_installer.py
|
||||||
```
|
```
|
||||||
|
|
||||||
If using in a CI/automation environment using Pip, ensure that the plugin is installed to the
|
If using Pip, ensure that the plugin is installed to the same environment as Tox:
|
||||||
same environment as Tox:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# Calling the virtualenv's 'pip' binary directly will cause pip to install to that virtualenv
|
# Calling the virtualenv's 'pip' binary directly will cause pip to install to that virtualenv
|
||||||
@@ -55,186 +60,140 @@ same environment as Tox:
|
|||||||
~ $: /path/to/my/automation/virtualenv/bin/pip install tox-poetry-installer
|
~ $: /path/to/my/automation/virtualenv/bin/pip install tox-poetry-installer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** While it is possible to install this plugin using Tox's
|
||||||
|
[`requires`](https://tox.readthedocs.io/en/latest/config.html#conf-requires)
|
||||||
|
configuration option, it is not recommended. Dependencies from the `requires` option are
|
||||||
|
installed using the default Tox installation backend which opens up the
|
||||||
|
[possibility of transient dependency problems](#why-would-i-use-this) in your automation
|
||||||
|
environment.
|
||||||
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
To require a Tox environment install all it's dependencies from the Poetry lockfile, add the
|
To add dependencies from the lockfile to a Tox environment, add the option `locked_deps`
|
||||||
`require_locked_deps = true` option to the environment configuration and remove all version
|
to the environment configuration and list names of dependencies (with no version
|
||||||
specifiers from the dependency list. The versions to install will be taken from the lockfile
|
specifier) under it:
|
||||||
directly:
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[testenv]
|
[testenv]
|
||||||
description = Run the tests
|
description = Some very cool tests
|
||||||
require_locked_deps = true
|
locked_deps =
|
||||||
deps =
|
|
||||||
pytest
|
|
||||||
pytest-cov
|
|
||||||
black
|
black
|
||||||
pylint
|
pylint
|
||||||
mypy
|
mypy
|
||||||
commands = ...
|
commands = ...
|
||||||
```
|
```
|
||||||
|
|
||||||
To require specific dependencies be installed from the Poetry lockfile, and let the rest be
|
The standard `deps` option can be used in parallel with the `locked_deps` option to
|
||||||
installed using the default Tox installation method, add the suffix `@poetry` to the dependencies.
|
install unlocked dependencies (dependencies not in the lockfile) alongside locked
|
||||||
In the example below the `pytest`, `pytest-cov`, and `black` dependencies will be installed using
|
dependencies:
|
||||||
the lockfile while `pylint` and `mypy` will be installed using the versions specified here:
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[testenv]
|
[testenv]
|
||||||
description = Run the tests
|
description = Some very cool tests
|
||||||
require_locked_deps = true
|
locked_deps =
|
||||||
|
black
|
||||||
|
pylint
|
||||||
|
mypy
|
||||||
deps =
|
deps =
|
||||||
pytest@poetry
|
pytest == 6.1.1
|
||||||
pytest-cov@poetry
|
pytest-cov >= 2.10, <2.11
|
||||||
black@poetry
|
|
||||||
pylint >=2.5.0
|
|
||||||
mypy == 0.770
|
|
||||||
commands = ...
|
commands = ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Alternatively, to quickly install all Poetry dev-dependencies to a Tox environment, add the
|
||||||
|
`install_dev_deps = true` option to the environment configuration.
|
||||||
|
|
||||||
**Note:** Regardless of the settings outlined above, all dependencies of the project package (the
|
**Note:** Regardless of the settings outlined above, all dependencies of the project package (the
|
||||||
one Tox is testing) will always be installed from the lockfile.
|
one Tox is testing) will always be installed from the lockfile.
|
||||||
|
|
||||||
|
|
||||||
## Usage Examples
|
## Reference and Usage
|
||||||
|
|
||||||
After installing the plugin to a project, your Tox automation is already benefiting from the
|
### Config Option Reference
|
||||||
lockfile: when Tox installs your project package to one of your environments, all the dependencies
|
|
||||||
of your project package will be installed using the versions specified in the lockfile. This
|
|
||||||
happens automatically and requires no configuration changes.
|
|
||||||
|
|
||||||
But what about the rest of your Tox environment dependencies?
|
All options listed below are Tox environment options and can be applied to one or more
|
||||||
|
environment sections of the `tox.ini` file. They cannot be applied to the global Tox
|
||||||
|
configuration section.
|
||||||
|
|
||||||
Let's use an example `tox.ini` file, below, that defines two environments: the main `testenv` for
|
**NOTE:** Environment settings applied to the main `testenv` environment will be
|
||||||
running the project tests and `testenv:check` for running some other helpful checks:
|
inherited by child environments (i.e. `testenv:foo`) unless they are explicitly
|
||||||
|
overridden by the child environment's configuration.
|
||||||
|
|
||||||
|
| Option | Type | Default | Usage |
|
||||||
|
|:----------------------|:----------------|:--------|:-----------------------------------------------|
|
||||||
|
| `locked_deps` | Multi-line list | `[]` | Names of packages in the Poetry lockfile to install to the Tox environment. All dependencies specified here (and their dependencies) will be installed to the Tox environment using the version the Poetry lockfile specifies for them. |
|
||||||
|
| `require_locked_deps` | Bool | `false` | Indicates whether the environment should allow unlocked dependencies (dependencies not in the Poetry lockfile) to be installed alongside locked dependencies. If `true` then installation of unlocked dependencies will be blocked and an error will be raised if the `deps` option specifies any values. |
|
||||||
|
| `install_dev_deps` | Bool | `false` | Indicates whether all Poetry development dependencies should be installed to the environment. Provides a quick and easy way to install all dev-dependencies without needing to specify them individually. |
|
||||||
|
|
||||||
|
### Error Reference
|
||||||
|
|
||||||
|
* `LockedDepVersionConflictError` - Indicates that a locked dependency included a PEP-508 version
|
||||||
|
specifier (i.e. `pytest >=6.0, <6.1`). Locked dependencies always take their version from the
|
||||||
|
Poetry lockfile so specifying a specific version for a locked dependency is not supported.
|
||||||
|
* `LockedDepNotFoundError` - Indicates that a locked dependency could not be found in the Poetry
|
||||||
|
lockfile. This can be solved by [adding the dependency using Poetry](https://python-poetry.org/docs/cli/#add).
|
||||||
|
* `ExtraNotFoundError` - Indicates that the Tox `extras` option specified a project extra that
|
||||||
|
Poetry does not know about. This may be due to a misconfigured `pyproject.toml` or out of date
|
||||||
|
lockfile.
|
||||||
|
* `LockedDepsRequiredError` - Indicates that an environment with `require_locked_deps = true` also
|
||||||
|
specified unlocked dependencies using Tox's `deps` option. This can be solved by either setting
|
||||||
|
`require_locked_deps = false` (the default) or removing the `deps` option from the environment
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
### Example Config
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py37, static
|
envlist = py, foo, bar, baz
|
||||||
isolated_build = true
|
isolated_build = true
|
||||||
|
|
||||||
|
# The base testenv will always use locked dependencies and only ever installs the project package
|
||||||
|
# (and its dependencies) and the two pytest dependencies listed below
|
||||||
[testenv]
|
[testenv]
|
||||||
description = Run the tests
|
description = Some very cool tests
|
||||||
deps =
|
|
||||||
pytest == 5.3.0
|
|
||||||
commands = ...
|
|
||||||
|
|
||||||
[testenv:check]
|
|
||||||
description = Static formatting and quality enforcement
|
|
||||||
deps =
|
|
||||||
pylint >=2.4.4,<2.6.0
|
|
||||||
mypy == 0.770
|
|
||||||
black --pre
|
|
||||||
commands = ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's focus on the `testenv:check` environment first. In this project there's no reason that any
|
|
||||||
of these tools should be a different version than what a human developer is using when installing
|
|
||||||
from the lockfile. We can require that these dependencies be installed from the lockfile by adding
|
|
||||||
the option `require_locked_deps = true` to the environment config, but this will cause an error:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[testenv:check]
|
|
||||||
description = Static formatting and quality enforcement
|
|
||||||
require_locked_deps = true
|
require_locked_deps = true
|
||||||
deps =
|
locked_deps =
|
||||||
pylint >=2.4.4,<2.6.0
|
pytest
|
||||||
mypy == 0.770
|
pytest-cov
|
||||||
black --pre
|
|
||||||
commands = ...
|
commands = ...
|
||||||
```
|
|
||||||
|
|
||||||
Running Tox using this config gives us this error:
|
# This environment also requires locked dependencies, but the "skip_install" setting means that
|
||||||
|
# the project dependencies will not be installed to the environment from the lockfile
|
||||||
```
|
[testenv:foo]
|
||||||
tox_poetry_installer.LockedDepVersionConflictError: Locked dependency 'pylint >=2.4.4,<2.6.0' cannot include version specifier
|
description = FOObarbaz
|
||||||
```
|
skip_install = true
|
||||||
|
|
||||||
This is because we told the Tox environment to require all dependencies to be locked, but then also
|
|
||||||
specified a specific version constraint for Pylint. With the `require_locked_deps = true` setting
|
|
||||||
Tox expects all dependencies to take their version from the lockfile, so when it got conflicting
|
|
||||||
information it errors. We can fix this by simply removing all version specifiers from the
|
|
||||||
environment dependency list:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[testenv:check]
|
|
||||||
description = Static formatting and quality enforcement
|
|
||||||
require_locked_deps = true
|
require_locked_deps = true
|
||||||
deps =
|
locked_deps =
|
||||||
pylint
|
requests
|
||||||
|
toml
|
||||||
|
ruamel.yaml
|
||||||
|
commands = ...
|
||||||
|
|
||||||
|
# This environment allows unlocked dependencies to be installed ad-hoc. Below, the "mypy" and
|
||||||
|
# "pylint" dependencies (and their dependencies) will be installed from the Poetry lockfile but the
|
||||||
|
# "black" dependency will be installed using the default Tox backend. Note, this environment does
|
||||||
|
# not specify "require_locked_deps = true" to allow the unlocked "black" dependency without raising
|
||||||
|
# an error.
|
||||||
|
[testenv:bar]
|
||||||
|
description = fooBARbaz
|
||||||
|
locked_deps =
|
||||||
mypy
|
mypy
|
||||||
|
pylint
|
||||||
|
deps =
|
||||||
black
|
black
|
||||||
commands = ...
|
commands = ...
|
||||||
```
|
|
||||||
|
|
||||||
Now all the dependencies will be installed from the lockfile. If Poetry updates the lockfile with
|
# This environment requires locked dependencies but does not specify any. Instead it specifies the
|
||||||
a new version then that updated version will be automatically installed when the Tox environment is
|
# "install_dev_deps = true" option which will cause all of the Poetry dev-dependencies to be
|
||||||
recreated.
|
# installed from the lockfile.
|
||||||
|
[testenv:baz]
|
||||||
Now let's look at the `testenv` environment. Let's make the same changes to the `testenv`
|
description = foobarBAZ
|
||||||
environment that we made to `testenv:check` above; remove the PyTest version and add
|
install_dev_deps = true
|
||||||
`require_locked_deps = true`. Then imagine that we want to add a new (made up) tool the test
|
|
||||||
environment called `crash_override` to the environment: we can add `crash-override` as a dependency
|
|
||||||
of the test environment, but this will cause an error:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[testenv]
|
|
||||||
description = Run the tests
|
|
||||||
require_locked_deps = true
|
require_locked_deps = true
|
||||||
deps =
|
|
||||||
pytest
|
|
||||||
crash-override
|
|
||||||
commands = ...
|
commands = ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Running Tox with this config gives us this error:
|
|
||||||
|
|
||||||
```
|
|
||||||
tox_poetry_installer.LockedDepNotFoundError: No version of locked dependency 'crash-override' found in the project lockfile
|
|
||||||
```
|
|
||||||
|
|
||||||
This is because `crash-override` is not in our lockfile. Tox will refuse to install a dependency
|
|
||||||
that isn't in the lockfile to an an environment that specifies `require_locked_deps = true`. We
|
|
||||||
could fix this (if `crash-override` was a real package) by running
|
|
||||||
`poetry add crash-override --dev` to add it to the lockfile.
|
|
||||||
|
|
||||||
Now let's combine dependencies from the lockfile ("locked dependencies") with dependencies that are
|
|
||||||
specified inline in the environment configuration ("unlocked dependencies").
|
|
||||||
[This isn't generally recommended of course](#why-would-i-use-this), but it's a valid use case and
|
|
||||||
fully supported by this plugin. Let's modify the `testenv` configuration to install PyTest from the
|
|
||||||
lockfile but then install an older version of the
|
|
||||||
[Requests](https://requests.readthedocs.io/en/master/) library.
|
|
||||||
|
|
||||||
The first thing to do is remove the `require_locked_deps = true` setting so that we can install
|
|
||||||
Requests as an unlocked dependency. Then we can add our version of requests to the dependency list:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[testenv]
|
|
||||||
description = Run the tests
|
|
||||||
deps =
|
|
||||||
pytest
|
|
||||||
requests >=2.2.0,<2.10.0
|
|
||||||
commands = ...
|
|
||||||
```
|
|
||||||
|
|
||||||
However we still want `pytest` to be installed from the lockfile, so the final step is to tell Tox
|
|
||||||
to install it from the lockfile by adding the suffix `@poetry` to it:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[testenv]
|
|
||||||
description = Run the tests
|
|
||||||
deps =
|
|
||||||
pytest@poetry
|
|
||||||
requests >=2.2.0,<2.10.0
|
|
||||||
commands = ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Now when the `testenv` environment is created it will install PyTest (and all of its dependencies)
|
|
||||||
from the lockfile while it will install Requests (and all of its dependencies) using the default
|
|
||||||
Tox installation backend using Pip.
|
|
||||||
|
|
||||||
|
|
||||||
## Known Drawbacks and Problems
|
## Known Drawbacks and Problems
|
||||||
|
|
||||||
@@ -247,18 +206,11 @@ Tox installation backend using Pip.
|
|||||||
* [`indexserver`](https://tox.readthedocs.io/en/latest/config.html#conf-indexserver)
|
* [`indexserver`](https://tox.readthedocs.io/en/latest/config.html#conf-indexserver)
|
||||||
* [`usedevelop`](https://tox.readthedocs.io/en/latest/config.html#conf-indexserver)
|
* [`usedevelop`](https://tox.readthedocs.io/en/latest/config.html#conf-indexserver)
|
||||||
|
|
||||||
* The [`extras`](https://tox.readthedocs.io/en/latest/config.html#conf-extras) setting in `tox.ini`
|
* Tox will not automatically detect changes to the locked dependencies and so
|
||||||
does not work. Optional dependencies of the project package will not be installed to Tox
|
environments will not be automatically rebuilt when locked dependencies are changed.
|
||||||
environments. (See the [road map](#roadmap))
|
When changing the locked dependencies (or their versions) the environments will need to
|
||||||
|
be manually rebuilt using either the `-r`/`--recreate` CLI option or the
|
||||||
* The plugin currently depends on `poetry<1.1.0`. This can be a different version than Poetry being
|
`recreate = true` option in `tox.ini`.
|
||||||
used for actual project development. (See the [road map](#roadmap))
|
|
||||||
|
|
||||||
* Tox environments automatically inherit their settings from the main `testenv` environment. This
|
|
||||||
means that if the `require_locked_deps = true` is specified for the `testenv` environment then
|
|
||||||
all environments will also require locked dependencies. This can be overridden by explicitly
|
|
||||||
specifying `require_locked_deps = false` on child environments where unlocked dependencies are
|
|
||||||
needed.
|
|
||||||
|
|
||||||
* There are a handful of packages that cannot be installed from the lockfile, whether as specific
|
* There are a handful of packages that cannot be installed from the lockfile, whether as specific
|
||||||
dependencies or as transient dependencies (dependencies of dependencies). This is due to
|
dependencies or as transient dependencies (dependencies of dependencies). This is due to
|
||||||
@@ -272,20 +224,20 @@ Tox installation backend using Pip.
|
|||||||
|
|
||||||
**Introduction**
|
**Introduction**
|
||||||
|
|
||||||
The lockfile is a file generated by a package manager for a project that lists what
|
The lockfile is a file generated by a package manager for a project that records what
|
||||||
dependencies are installed, the versions of those dependencies, and additional metadata that
|
dependencies are installed, the versions of those dependencies, and any additional metadata that
|
||||||
the package manager can use to recreate the local project environment. This allows developers
|
the package manager needs to recreate the local project environment. This allows developers
|
||||||
to have confidence that a bug they are encountering that may be caused by one of their
|
to have confidence that a bug they are encountering that may be caused by one of their
|
||||||
dependencies will be reproducible on another device. In addition, installing a project
|
dependencies will be reproducible on another device. In addition, installing a project
|
||||||
environment from a lockfile gives confidence that automated systems running tests or performing
|
environment from a lockfile gives confidence that automated systems running tests or performing
|
||||||
builds are using the same environment that a developer is.
|
builds are using the same environment as a developer.
|
||||||
|
|
||||||
[Poetry](https://python-poetry.org/) is a project dependency manager for Python projects, and
|
[Poetry](https://python-poetry.org/) is a project dependency manager for Python projects, and
|
||||||
as such it creates and manages a lockfile so that its users can benefit from all the features
|
so it creates and manages a lockfile so that its users can benefit from all the features
|
||||||
described above. [Tox](https://tox.readthedocs.io/en/latest/#what-is-tox) is an automation tool
|
described above. [Tox](https://tox.readthedocs.io/en/latest/#what-is-tox) is an automation tool
|
||||||
that allows Python developers to run tests suites, perform builds, and automate tasks within
|
that allows Python developers to run tests suites, perform builds, and automate tasks within
|
||||||
self contained [Python virtual environments](https://docs.python.org/3/tutorial/venv.html).
|
self-contained [Python virtual environments](https://docs.python.org/3/tutorial/venv.html).
|
||||||
To make these environments useful, Tox supports installing per-environment dependencies.
|
To make these environments useful Tox supports installing dependencies in each environment.
|
||||||
However, since these environments are created on the fly and Tox does not maintain a lockfile,
|
However, since these environments are created on the fly and Tox does not maintain a lockfile,
|
||||||
there can be subtle differences between the dependencies a developer is using and the
|
there can be subtle differences between the dependencies a developer is using and the
|
||||||
dependencies Tox uses.
|
dependencies Tox uses.
|
||||||
@@ -293,19 +245,17 @@ dependencies Tox uses.
|
|||||||
This is where this plugin comes into play.
|
This is where this plugin comes into play.
|
||||||
|
|
||||||
By default Tox uses [Pip](https://docs.python.org/3/tutorial/venv.html) to install the
|
By default Tox uses [Pip](https://docs.python.org/3/tutorial/venv.html) to install the
|
||||||
PEP-508 compliant dependencies to a test environment. A more robust way to do this is to
|
PEP-508 compliant dependencies to a test environment. This plugin extends the default
|
||||||
install dependencies directly from the lockfile so that the version installed to the Tox
|
Tox dependency installation behavior to support installing dependencies using a Poetry-based
|
||||||
environment always matches the version Poetry specifies. This plugin overwrites the default
|
installation method that makes use of the dependency metadata from Poetry's lockfile.
|
||||||
Tox dependency installation behavior and replaces it with a Poetry-based installation using
|
|
||||||
the dependency metadata from the lockfile.
|
|
||||||
|
|
||||||
**The Problem**
|
**The Problem**
|
||||||
|
|
||||||
Environment dependencies for a Tox environment are usually done in PEP-508 format like the
|
Environment dependencies for a Tox environment are usually specified in PEP-508 format, like
|
||||||
below example
|
the below example:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
# tox.ini
|
# from tox.ini
|
||||||
...
|
...
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
@@ -318,31 +268,35 @@ deps =
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Perhaps these dependencies are also useful during development, so they can be added to the
|
Let's assume these dependencies are also useful during development, so they can be added to the
|
||||||
Poetry environment using this command:
|
Poetry environment using this command:
|
||||||
|
|
||||||
```
|
```
|
||||||
poetry add foo==1.2.3 bar>=1.3,<2.0 baz --dev
|
poetry add --dev \
|
||||||
|
foo==1.2.3 \
|
||||||
|
bar>=1.3,<2.0 \
|
||||||
|
baz
|
||||||
```
|
```
|
||||||
|
|
||||||
However there are three potential problems that could arise from each of these environment
|
However there is a potential problem that could arise from each of these environment
|
||||||
dependencies that would _only_ appear in the Tox environment and not in the Poetry
|
dependencies that would _only_ appear in the Tox environment and not in the Poetry
|
||||||
environment:
|
environment in use by a developer:
|
||||||
|
|
||||||
* **The `foo` dependency is pinned to a specific version:** let's imagine a security
|
* **The `foo` dependency is pinned to a specific version:** let's imagine a security
|
||||||
vulnerability is discovered in `foo` and the maintainers release version `1.2.4` to fix
|
vulnerability is discovered in `foo` and the maintainers release version `1.2.4` to fix
|
||||||
it. A developer can run `poetry remove foo && poetry add foo^1.2` to get the new version,
|
it. A developer can run `poetry remove foo` and then `poetry add foo^1.2` to get the new
|
||||||
but the Tox environment is left unchanged. The developer environment specified by the
|
version, but the Tox environment is left unchanged. The development environment, as defined by
|
||||||
lockfile is now patched against the vulnerability, but the Tox environment is not.
|
the lockfile, is now patched against the vulnerability but the Tox environment is not.
|
||||||
|
|
||||||
* **The `bar` dependency specifies a dynamic range:** a dynamic range allows a range of
|
* **The `bar` dependency specifies a dynamic range:** a dynamic range allows a range of
|
||||||
versions to be installed, but the lockfile will have an exact version specified so that
|
versions to be installed, but the lockfile will have an exact version specified so that
|
||||||
the Poetry environment is reproducible; this allows versions to be updated with
|
the Poetry environment is reproducible; this allows versions to be updated with
|
||||||
`poetry update` rather than with the `remove` and `add` used above. If the maintainers of
|
`poetry update` rather than with the `remove` and `add` commands used above. If the
|
||||||
`bar` release version `1.6.0` then the Tox environment will install it because it is valid
|
maintainers of `bar` release version `1.6.0` then the Tox environment will install it
|
||||||
for the specified version range, meanwhile the Poetry environment will continue to install
|
because it is valid for the specified version range. Meanwhile the Poetry environment will
|
||||||
the version from the lockfile until `poetry update bar` explicitly updates it. The
|
continue to install the version from the lockfile until `poetry update bar` explicitly
|
||||||
development environment is now has a different version of `bar` than the Tox environment.
|
updates it. The development environment is now has a different version of `bar` than the Tox
|
||||||
|
environment.
|
||||||
|
|
||||||
* **The `baz` dependency is unpinned:** unpinned dependencies are
|
* **The `baz` dependency is unpinned:** unpinned dependencies are
|
||||||
[generally a bad idea](https://python-poetry.org/docs/faq/#why-are-unbound-version-constraints-a-bad-idea),
|
[generally a bad idea](https://python-poetry.org/docs/faq/#why-are-unbound-version-constraints-a-bad-idea),
|
||||||
@@ -351,28 +305,27 @@ Poetry environment using this command:
|
|||||||
but Pip (via Tox) will interpret it as a wildcard. If the latest version of `baz` is `1.0.0`
|
but Pip (via Tox) will interpret it as a wildcard. If the latest version of `baz` is `1.0.0`
|
||||||
then `poetry add baz` will result in a constraint of `baz>=1.0.0,<2.0.0` while the Tox
|
then `poetry add baz` will result in a constraint of `baz>=1.0.0,<2.0.0` while the Tox
|
||||||
environment will have a constraint of `baz==*`. The Tox environment can now install an
|
environment will have a constraint of `baz==*`. The Tox environment can now install an
|
||||||
incompatible version of `baz` that cannot be easily caught using `poetry update`.
|
incompatible version of `baz` and any errors that causes cannot be replicated using `poetry update`.
|
||||||
|
|
||||||
All of these problems can apply not only to the dependencies specified for a Tox environment,
|
All of these problems can apply not only to the dependencies specified for a Tox environment,
|
||||||
but also to the dependencies of those dependencies, and so on.
|
but also to the dependencies of those dependencies, those dependencies' dependencies, and so on.
|
||||||
|
|
||||||
**The Solution**
|
**The Solution**
|
||||||
|
|
||||||
This plugin requires that all dependencies specified for all Tox environments be unbound
|
This plugin allows dependencies specified in Tox environment take their version directly from
|
||||||
with no version constraint specified at all. This seems counter-intuitive given the problems
|
the Poetry lockfile without needing an independent version to be specified in the Tox
|
||||||
outlined above, but what it allows the plugin to do is offload all version management to
|
environment configuration. The modified version of the example environment given below appears
|
||||||
Poetry.
|
less stable than the one presented above because it does not specify any versions for its
|
||||||
|
dependencies:
|
||||||
On initial inspection, the environment below appears less stable than the one presented above
|
|
||||||
because it does not specify any versions for its dependencies:
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
# tox.ini
|
# from tox.ini
|
||||||
...
|
...
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
description = Some very cool tests
|
description = Some very cool tests
|
||||||
deps =
|
require_locked_deps = true
|
||||||
|
locked_deps =
|
||||||
foo
|
foo
|
||||||
bar
|
bar
|
||||||
baz
|
baz
|
||||||
@@ -380,19 +333,17 @@ deps =
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
However with the `tox-poetry-installer` plugin installed this instructs Tox to install these
|
However with the `tox-poetry-installer` plugin installed the `require_locked_deps = true`
|
||||||
dependencies using the Poetry lockfile so that the version installed to the Tox environment
|
setting means that Tox will install these dependencies from the Poetry lockfile so that the
|
||||||
exactly matches the version Poetry is managing. When `poetry update` updates the lockfile
|
version installed to the Tox environment exactly matches the version Poetry is managing. When
|
||||||
with new dependency versions, Tox will automatically install these new versions without needing
|
`poetry update` updates the lockfile with new versions of these dependencies, Tox will
|
||||||
any changes to the configuration.
|
automatically install these new versions without needing any changes to the configuration.
|
||||||
|
|
||||||
All dependencies are specified in one place (the lockfile) and dependency version management is
|
|
||||||
handled by a tool dedicated to that task (Poetry).
|
|
||||||
|
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
This project requires Poetry-1.0+, see the [installation instructions here](https://python-poetry.org/docs/#installation).
|
This project requires a developer to have Poetry version 1.0+ installed on their workstation, see
|
||||||
|
the [installation instructions here](https://python-poetry.org/docs/#installation).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository...
|
# Clone the repository...
|
||||||
@@ -435,7 +386,7 @@ releases on PyPI.
|
|||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
This project is under active development and is classified as alpha software, not yet ready
|
This project is under active development and is classified as alpha software, not yet ready
|
||||||
usage in production systems.
|
for usage in production environments.
|
||||||
|
|
||||||
* Beta classification will be assigned when the initial feature set is finalized
|
* Beta classification will be assigned when the initial feature set is finalized
|
||||||
* Stable classification will be assigned when the test suite covers an acceptable number of
|
* Stable classification will be assigned when the test suite covers an acceptable number of
|
||||||
@@ -445,20 +396,21 @@ usage in production systems.
|
|||||||
|
|
||||||
- [X] Verify that primary package dependencies (from the `.package` env) are installed
|
- [X] Verify that primary package dependencies (from the `.package` env) are installed
|
||||||
correctly using the Poetry backend.
|
correctly using the Poetry backend.
|
||||||
- [ ] Support the [`extras`](https://tox.readthedocs.io/en/latest/config.html#conf-extras)
|
- [X] Support the [`extras`](https://tox.readthedocs.io/en/latest/config.html#conf-extras)
|
||||||
Tox configuration option
|
Tox configuration option ([#4](https://github.com/enpaul/tox-poetry-installer/issues/4))
|
||||||
- [X] Add per-environment Tox configuration option to fall back to default installation
|
- [X] Add per-environment Tox configuration option to fall back to default installation
|
||||||
backend.
|
backend.
|
||||||
- [ ] Add detection of a changed lockfile to automatically trigger a rebuild of Tox
|
|
||||||
environments when necessary.
|
|
||||||
- [ ] Add warnings when an unsupported Tox configuration option is detected while using the
|
- [ ] Add warnings when an unsupported Tox configuration option is detected while using the
|
||||||
Poetry backend.
|
Poetry backend. ([#5](https://github.com/enpaul/tox-poetry-installer/issues/5))
|
||||||
- [X] Add trivial tests to ensure the project metadata is consistent between the pyproject.toml
|
- [X] Add trivial tests to ensure the project metadata is consistent between the pyproject.toml
|
||||||
and the module constants.
|
and the module constants.
|
||||||
- [ ] Update to use [poetry-core](https://github.com/python-poetry/poetry-core)
|
- [X] Update to use [poetry-core](https://github.com/python-poetry/poetry-core) and
|
||||||
Tox configuration option) and improve robustness of the Tox and Poetry module imports
|
improve robustness of the Tox and Poetry module imports
|
||||||
to avoid potentially breaking API changes in upstream packages.
|
to avoid potentially breaking API changes in upstream packages. ([#2](https://github.com/enpaul/tox-poetry-installer/issues/2))
|
||||||
- [ ] Find and implement a way to mitigate the [Poetry UNSAFE_DEPENDENCIES bug](https://github.com/python-poetry/poetry/issues/1584).
|
- [ ] Find and implement a way to mitigate the [UNSAFE_DEPENDENCIES issue](https://github.com/python-poetry/poetry/issues/1584) in Poetry.
|
||||||
|
([#6](https://github.com/enpaul/tox-poetry-installer/issues/6))
|
||||||
|
- [ ] Fix logging to make proper use of Tox's logging reporter infrastructure ([#3](https://github.com/enpaul/tox-poetry-installer/issues/3))
|
||||||
|
- [X] Add configuration option for installing all dev-dependencies to a testenv ([#14](https://github.com/enpaul/tox-poetry-installer/issues/14))
|
||||||
|
|
||||||
### Path to Stable
|
### Path to Stable
|
||||||
|
|
||||||
@@ -466,6 +418,6 @@ Everything in Beta plus...
|
|||||||
|
|
||||||
- [ ] Add tests for each feature version of Tox between 2.3 and 3.20
|
- [ ] Add tests for each feature version of Tox between 2.3 and 3.20
|
||||||
- [ ] Add tests for Python-3.6, 3.7, and 3.8
|
- [ ] Add tests for Python-3.6, 3.7, and 3.8
|
||||||
- [ ] Add Github Actions based CI
|
- [X] Add Github Actions based CI
|
||||||
- [ ] Add CI for CPython, PyPy, and Conda
|
- [ ] Add CI for CPython, PyPy, and Conda
|
||||||
- [ ] Add CI for Linux and Windows
|
- [ ] Add CI for Linux and Windows
|
||||||
|
|||||||
1336
poetry.lock
generated
1336
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "tox-poetry-installer"
|
name = "tox-poetry-installer"
|
||||||
version = "0.2.2"
|
version = "0.5.1"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
|
authors = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
|
||||||
description = "Tox plugin to install Tox environment dependencies using the Poetry backend and lockfile"
|
description = "Tox plugin to install Tox environment dependencies using the Poetry backend and lockfile"
|
||||||
repository = "https://github.com/enpaul/tox-poetry-installer/"
|
repository = "https://github.com/enpaul/tox-poetry-installer/"
|
||||||
packages = [{include = "tox_poetry_installer.py"}]
|
packages = [
|
||||||
|
{include = "tox_poetry_installer"},
|
||||||
|
{include = "tests/*.py", format = "sdist"}
|
||||||
|
]
|
||||||
|
include = [
|
||||||
|
"tox_poetry_installer/py.typed"
|
||||||
|
]
|
||||||
keywords = ["tox", "poetry", "plugin"]
|
keywords = ["tox", "poetry", "plugin"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -19,6 +25,7 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -26,24 +33,27 @@ classifiers = [
|
|||||||
poetry_installer = "tox_poetry_installer"
|
poetry_installer = "tox_poetry_installer"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.6"
|
python = "^3.6.1"
|
||||||
poetry = ">=1.0.0, <1.1.0"
|
poetry = "^1.0.0"
|
||||||
|
poetry-core = "^1.0.0"
|
||||||
tox = "^2.3.0 || ^3.0.0"
|
tox = "^2.3.0 || ^3.0.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
bandit = "^1.6.2"
|
bandit = "^1.6.2"
|
||||||
black = {version = "^20.8b1", allow-prereleases = true}
|
black = { version = "^20.8b1", allow-prereleases = true }
|
||||||
ipython = {version = "^7.18.1", python = "^3.7"}
|
blacken-docs = "^1.8.0"
|
||||||
|
ipython = { version = "^7.18.1", python = "^3.7" }
|
||||||
mypy = "^0.782"
|
mypy = "^0.782"
|
||||||
pre-commit = {version = "^2.7.1", python = "^3.6.1"}
|
pre-commit = "^2.7.1"
|
||||||
|
pre-commit-hooks = "^3.3.0"
|
||||||
pylint = "^2.4.4"
|
pylint = "^2.4.4"
|
||||||
pytest = "^6.0.2"
|
pytest = "^6.0.2"
|
||||||
pytest-cov = "^2.10.1"
|
pytest-cov = "^2.10.1"
|
||||||
reorder-python-imports = {version = "^2.3.5", python = "^3.6.1"}
|
reorder-python-imports = "^2.3.5"
|
||||||
safety = "^1.9.0"
|
safety = "^1.9.0"
|
||||||
toml = "^0.10.1"
|
toml = "^0.10.1"
|
||||||
tox = "^3.20.0"
|
tox = "^3.20.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import toml
|
import toml
|
||||||
|
|
||||||
import tox_poetry_installer
|
from tox_poetry_installer import __about__
|
||||||
|
|
||||||
|
|
||||||
def test_metadata():
|
def test_metadata():
|
||||||
@@ -16,16 +16,14 @@ def test_metadata():
|
|||||||
with (Path(__file__).resolve().parent / ".." / "pyproject.toml").open() as infile:
|
with (Path(__file__).resolve().parent / ".." / "pyproject.toml").open() as infile:
|
||||||
pyproject = toml.load(infile, _dict=dict)
|
pyproject = toml.load(infile, _dict=dict)
|
||||||
|
|
||||||
assert pyproject["tool"]["poetry"]["name"] == tox_poetry_installer.__title__
|
assert pyproject["tool"]["poetry"]["name"] == __about__.__title__
|
||||||
assert pyproject["tool"]["poetry"]["version"] == tox_poetry_installer.__version__
|
assert pyproject["tool"]["poetry"]["version"] == __about__.__version__
|
||||||
assert pyproject["tool"]["poetry"]["license"] == tox_poetry_installer.__license__
|
assert pyproject["tool"]["poetry"]["license"] == __about__.__license__
|
||||||
assert (
|
assert pyproject["tool"]["poetry"]["description"] == __about__.__summary__
|
||||||
pyproject["tool"]["poetry"]["description"] == tox_poetry_installer.__summary__
|
assert pyproject["tool"]["poetry"]["repository"] == __about__.__url__
|
||||||
)
|
|
||||||
assert pyproject["tool"]["poetry"]["repository"] == tox_poetry_installer.__url__
|
|
||||||
assert (
|
assert (
|
||||||
all(
|
all(
|
||||||
item in tox_poetry_installer.__authors__
|
item in __about__.__authors__
|
||||||
for item in pyproject["tool"]["poetry"]["authors"]
|
for item in pyproject["tool"]["poetry"]["authors"]
|
||||||
)
|
)
|
||||||
is True
|
is True
|
||||||
@@ -33,7 +31,7 @@ def test_metadata():
|
|||||||
assert (
|
assert (
|
||||||
all(
|
all(
|
||||||
item in pyproject["tool"]["poetry"]["authors"]
|
item in pyproject["tool"]["poetry"]["authors"]
|
||||||
for item in tox_poetry_installer.__authors__
|
for item in __about__.__authors__
|
||||||
)
|
)
|
||||||
is True
|
is True
|
||||||
)
|
)
|
||||||
|
|||||||
54
tox.ini
54
tox.ini
@@ -1,66 +1,62 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py36, py37, py38, static, static-tests, security
|
envlist = py36, py37, py38, py39, static, static-tests, security
|
||||||
isolated_build = true
|
isolated_build = true
|
||||||
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
description = Run the tests
|
description = Run the tests
|
||||||
require_locked_deps = true
|
require_locked_deps = true
|
||||||
deps =
|
locked_deps =
|
||||||
pytest
|
pytest
|
||||||
pytest-cov
|
pytest-cov
|
||||||
toml
|
toml
|
||||||
commands =
|
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]
|
[testenv:static]
|
||||||
description = Static formatting and quality enforcement
|
description = Static formatting and quality enforcement
|
||||||
require_locked_deps = true
|
|
||||||
basepython = python3.8
|
basepython = python3.8
|
||||||
|
platform = linux
|
||||||
ignore_errors = true
|
ignore_errors = true
|
||||||
deps =
|
require_locked_deps = true
|
||||||
|
locked_deps =
|
||||||
|
pre-commit
|
||||||
|
pre-commit-hooks
|
||||||
|
black
|
||||||
|
blacken-docs
|
||||||
|
reorder-python-imports
|
||||||
pylint
|
pylint
|
||||||
mypy
|
mypy
|
||||||
black
|
|
||||||
reorder-python-imports
|
|
||||||
pre-commit
|
|
||||||
commands =
|
commands =
|
||||||
black {toxinidir}/tox_poetry_installer.py
|
|
||||||
reorder-python-imports {toxinidir}/tox_poetry_installer.py
|
|
||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tox_poetry_installer.py
|
pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tox_poetry_installer/
|
||||||
mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tox_poetry_installer.py
|
mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tox_poetry_installer/
|
||||||
|
|
||||||
[testenv:static-tests]
|
[testenv:static-tests]
|
||||||
description = Static formatting and quality enforcement for the tests
|
description = Static formatting and quality enforcement for the tests
|
||||||
require_locked_deps = true
|
|
||||||
basepython = python3.8
|
basepython = python3.8
|
||||||
|
platform = linux
|
||||||
ingore_errors = true
|
ingore_errors = true
|
||||||
deps =
|
require_locked_deps = true
|
||||||
|
locked_deps =
|
||||||
pylint
|
pylint
|
||||||
mypy
|
mypy
|
||||||
black
|
|
||||||
reorder-python-imports
|
|
||||||
allowlist_externals =
|
|
||||||
bash
|
|
||||||
commands =
|
commands =
|
||||||
black {toxinidir}/tests/
|
|
||||||
bash -c "reorder-python-imports {toxinidir}/tests/*.py --unclassifiable-application-module tox_poetry_installer"
|
|
||||||
pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tests/
|
pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tests/
|
||||||
mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tests/
|
mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tests/
|
||||||
|
|
||||||
[testenv:security]
|
[testenv:security]
|
||||||
description = Security checks
|
description = Security checks
|
||||||
require_locked_deps = true
|
|
||||||
basepython = python3.8
|
basepython = python3.8
|
||||||
ignore_errors = true
|
platform = linux
|
||||||
skip_install = true
|
ingore_errors = true
|
||||||
deps =
|
require_locked_deps = true
|
||||||
|
locked_deps =
|
||||||
bandit
|
bandit
|
||||||
safety
|
safety
|
||||||
poetry
|
poetry
|
||||||
allowlist_externals =
|
|
||||||
bash
|
|
||||||
commands =
|
commands =
|
||||||
bandit --quiet {toxinidir}/tox_poetry_installer.py
|
bandit --recursive --quiet {toxinidir}/tox_poetry_installer/
|
||||||
bash -c "bandit --quiet --skip B101 {toxinidir}/tests/*.py"
|
bandit --recursive --quiet --skip B101 {toxinidir}/tests/
|
||||||
bash -c "poetry export --format requirements.txt --without-hashes --dev | safety check --stdin --bare"
|
poetry export --format requirements.txt --output {envtmpdir}/requirements.txt --without-hashes --dev
|
||||||
|
safety check --bare --file {envtmpdir}/requirements.txt
|
||||||
|
|||||||
@@ -1,306 +0,0 @@
|
|||||||
"""Tox plugin for installing environments using Poetry
|
|
||||||
|
|
||||||
This plugin makes use of the ``tox_testenv_install_deps`` Tox plugin hook to replace 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.
|
|
||||||
"""
|
|
||||||
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.factory import Factory as PoetryFactory
|
|
||||||
from poetry.factory import Poetry
|
|
||||||
from poetry.installation.pip_installer import PipInstaller as PoetryPipInstaller
|
|
||||||
from poetry.io.null_io import NullIO as PoetryNullIO
|
|
||||||
from poetry.packages import Package as PoetryPackage
|
|
||||||
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.2.2"
|
|
||||||
__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"
|
|
||||||
|
|
||||||
|
|
||||||
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"""
|
|
||||||
|
|
||||||
|
|
||||||
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 for installation from poetry lockfile: {[item.name for item in locked_deps]}"
|
|
||||||
)
|
|
||||||
reporter.verbosity1(
|
|
||||||
f"{_REPORTER_PREFIX} identified {len(unlocked_deps)} unlocked env dependencies for installation using default backend: {[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
|
|
||||||
"""
|
|
||||||
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(poetry: Poetry, 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.
|
|
||||||
"""
|
|
||||||
packages: Dict[str, PoetryPackage] = {
|
|
||||||
package.name: package
|
|
||||||
for package in poetry.locker.locked_repository(True).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 '{name}' using Poetry is not supported; skipping"
|
|
||||||
)
|
|
||||||
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):
|
|
||||||
env_deps = _sort_env_deps(venv)
|
|
||||||
|
|
||||||
dependencies: List[PoetryPackage] = []
|
|
||||||
for dep in env_deps.locked_deps:
|
|
||||||
try:
|
|
||||||
dependencies += _find_transients(poetry, dep.name)
|
|
||||||
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)} actual dependencies from {len(venv.envconfig.deps)} specified 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_package_dependencies(venv: ToxVirtualEnv, poetry: Poetry):
|
|
||||||
reporter.verbosity1(
|
|
||||||
f"{_REPORTER_PREFIX} performing installation of project dependencies"
|
|
||||||
)
|
|
||||||
|
|
||||||
primary_dependencies = poetry.locker.locked_repository(False).packages
|
|
||||||
|
|
||||||
reporter.verbosity1(
|
|
||||||
f"{_REPORTER_PREFIX} identified {len(primary_dependencies)} dependencies of project '{poetry.package.name}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
reporter.verbosity0(
|
|
||||||
f"{_REPORTER_PREFIX} ({venv.name}) installing {len(primary_dependencies)} project dependencies from lockfile"
|
|
||||||
)
|
|
||||||
_install_to_venv(poetry, venv, primary_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="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 as err:
|
|
||||||
# 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. This check of the error
|
|
||||||
# content, while crude and potentially fragile, will hopefully prevent ``RuntimeError``s
|
|
||||||
# not caused by this specific condition to be re-raised as genuine errors. This may need
|
|
||||||
# tuning in the future.
|
|
||||||
if "[tool.poetry] section not found" in str(err):
|
|
||||||
reporter.verbosity1(
|
|
||||||
f"{_REPORTER_PREFIX} project does not use Poetry for env management, skipping installation of locked dependencies"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
raise err
|
|
||||||
|
|
||||||
reporter.verbosity1(
|
|
||||||
f"{_REPORTER_PREFIX} loaded project pyproject.toml from {poetry.file}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handle the installation of any locked env dependencies from the lockfile
|
|
||||||
_install_env_dependencies(venv, poetry)
|
|
||||||
|
|
||||||
# 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 not venv.envconfig.skip_install and not venv.envconfig.config.skipsdist:
|
|
||||||
_install_package_dependencies(venv, poetry)
|
|
||||||
else:
|
|
||||||
if venv.envconfig.skip_install:
|
|
||||||
reporter.verbosity1(
|
|
||||||
f"{_REPORTER_PREFIX} env specifies 'skip_install = true', skipping installation of project package"
|
|
||||||
)
|
|
||||||
elif venv.envconfig.config.skipsdist:
|
|
||||||
reporter.verbosity1(
|
|
||||||
f"{_REPORTER_PREFIX} config specifies 'skipsdist = true', skipping installation of project package"
|
|
||||||
)
|
|
||||||
7
tox_poetry_installer/__about__.py
Normal file
7
tox_poetry_installer/__about__.py
Normal file
@@ -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.5.1"
|
||||||
|
__url__ = "https://github.com/enpaul/tox-poetry-installer/"
|
||||||
|
__license__ = "MIT"
|
||||||
|
__authors__ = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
|
||||||
3
tox_poetry_installer/__init__.py
Normal file
3
tox_poetry_installer/__init__.py
Normal file
@@ -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
|
||||||
19
tox_poetry_installer/constants.py
Normal file
19
tox_poetry_installer/constants.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"""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__}]:"
|
||||||
8
tox_poetry_installer/datatypes.py
Normal file
8
tox_poetry_installer/datatypes.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""Definitions for typehints/containers used by the plugin"""
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from poetry.core.packages import Package as PoetryPackage
|
||||||
|
|
||||||
|
|
||||||
|
# Map of package names to the package object
|
||||||
|
PackageMap = Dict[str, PoetryPackage]
|
||||||
38
tox_poetry_installer/exceptions.py
Normal file
38
tox_poetry_installer/exceptions.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""Custom plugin exceptions
|
||||||
|
|
||||||
|
All exceptions should inherit from the common base exception :exc:`ToxPoetryInstallerException`.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
ToxPoetryInstallerException
|
||||||
|
+-- SkipEnvironment
|
||||||
|
+-- LockedDepVersionConflictError
|
||||||
|
+-- LockedDepNotFoundError
|
||||||
|
+-- ExtraNotFoundError
|
||||||
|
+-- LockedDepsRequiredError
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ToxPoetryInstallerException(Exception):
|
||||||
|
"""Error while installing locked dependencies to the test environment"""
|
||||||
|
|
||||||
|
|
||||||
|
class SkipEnvironment(ToxPoetryInstallerException):
|
||||||
|
"""Current environment does not meet preconditions and should be skipped by the plugin"""
|
||||||
|
|
||||||
|
|
||||||
|
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"""
|
||||||
|
|
||||||
|
|
||||||
|
class LockedDepsRequiredError(ToxPoetryInstallerException):
|
||||||
|
"""Environment cannot specify unlocked dependencies when locked dependencies are required"""
|
||||||
165
tox_poetry_installer/hooks.py
Normal file
165
tox_poetry_installer/hooks.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
"""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 typing import Optional
|
||||||
|
|
||||||
|
from poetry.core.packages import Package as PoetryPackage
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_testenv_attribute(
|
||||||
|
name="locked_deps",
|
||||||
|
type="line-list",
|
||||||
|
help="List of locked dependencies to install to the environment using the Poetry lockfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction) -> Optional[bool]:
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
poetry = utilities.check_preconditions(venv, action)
|
||||||
|
except exceptions.SkipEnvironment as err:
|
||||||
|
reporter.verbosity1(str(err))
|
||||||
|
return None
|
||||||
|
|
||||||
|
reporter.verbosity1(
|
||||||
|
f"{constants.REPORTER_PREFIX} Loaded project pyproject.toml from {poetry.file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if venv.envconfig.require_locked_deps and venv.envconfig.deps:
|
||||||
|
raise exceptions.LockedDepsRequiredError(
|
||||||
|
f"Unlocked dependencies '{venv.envconfig.deps}' specified for environment '{venv.name}' which requires locked dependencies"
|
||||||
|
)
|
||||||
|
|
||||||
|
package_map: PackageMap = {
|
||||||
|
package.name: package
|
||||||
|
for package in poetry.locker.locked_repository(True).packages
|
||||||
|
}
|
||||||
|
|
||||||
|
if venv.envconfig.install_dev_deps:
|
||||||
|
dev_deps: List[PoetryPackage] = [
|
||||||
|
dep
|
||||||
|
for dep in package_map.values()
|
||||||
|
if dep not in poetry.locker.locked_repository(False).packages
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
dev_deps = []
|
||||||
|
|
||||||
|
reporter.verbosity1(
|
||||||
|
f"{constants.REPORTER_PREFIX} Identified {len(dev_deps)} development dependencies to install to env"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
env_deps: List[PoetryPackage] = []
|
||||||
|
for dep in venv.envconfig.locked_deps:
|
||||||
|
env_deps += utilities.find_transients(package_map, dep.lower())
|
||||||
|
reporter.verbosity1(
|
||||||
|
f"{constants.REPORTER_PREFIX} Identified {len(env_deps)} environment dependencies to install to env"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not venv.envconfig.skip_install and not venv.envconfig.config.skipsdist:
|
||||||
|
project_deps: List[PoetryPackage] = _find_project_dependencies(
|
||||||
|
venv, poetry, package_map
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
project_deps = []
|
||||||
|
reporter.verbosity1(
|
||||||
|
f"{constants.REPORTER_PREFIX} Skipping installation of project dependencies, env does not install project package"
|
||||||
|
)
|
||||||
|
reporter.verbosity1(
|
||||||
|
f"{constants.REPORTER_PREFIX} Identified {len(project_deps)} project dependencies to install to env"
|
||||||
|
)
|
||||||
|
except exceptions.ToxPoetryInstallerException as err:
|
||||||
|
venv.status = "lockfile installation failed"
|
||||||
|
reporter.error(f"{constants.REPORTER_PREFIX} {err}")
|
||||||
|
raise err
|
||||||
|
|
||||||
|
dependencies = list(set(dev_deps + env_deps + project_deps))
|
||||||
|
reporter.verbosity0(
|
||||||
|
f"{constants.REPORTER_PREFIX} Installing {len(dependencies)} dependencies to env '{action.name}'"
|
||||||
|
)
|
||||||
|
utilities.install_to_venv(poetry, venv, dependencies)
|
||||||
|
|
||||||
|
return venv.envconfig.require_locked_deps or None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_dependencies(
|
||||||
|
venv: ToxVirtualEnv, poetry: Poetry, packages: PackageMap
|
||||||
|
) -> List[PoetryPackage]:
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
dependencies += utilities.find_transients(packages, dep.name.lower())
|
||||||
|
|
||||||
|
return dependencies
|
||||||
0
tox_poetry_installer/py.typed
Normal file
0
tox_poetry_installer/py.typed
Normal file
110
tox_poetry_installer/utilities.py
Normal file
110
tox_poetry_installer/utilities.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"""Helper utility functions, usually bridging Tox and Poetry functionality"""
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
|
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 reporter
|
||||||
|
from tox.action import Action as ToxAction
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
def find_deps_of_deps(name: str, transients: PackageMap):
|
||||||
|
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}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
transients[name] = packages[name]
|
||||||
|
for dep in packages[name].requires:
|
||||||
|
if dep.name not in transients.keys():
|
||||||
|
find_deps_of_deps(dep.name, transients)
|
||||||
|
|
||||||
|
transients: PackageMap = {}
|
||||||
|
find_deps_of_deps(packages[dependency_name].name, transients)
|
||||||
|
|
||||||
|
return set(transients.values())
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def check_preconditions(venv: ToxVirtualEnv, action: ToxAction) -> Poetry:
|
||||||
|
"""Check that the local project environment meets expectations"""
|
||||||
|
# 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.
|
||||||
|
if action.name == venv.envconfig.config.isolated_build_env:
|
||||||
|
raise exceptions.SkipEnvironment(
|
||||||
|
f"Skipping isolated packaging build env '{action.name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return PoetryFactory().create_poetry(venv.envconfig.config.toxinidir)
|
||||||
|
# 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.
|
||||||
|
except RuntimeError:
|
||||||
|
raise exceptions.SkipEnvironment(
|
||||||
|
"Project does not use Poetry for env management, skipping installation of locked dependencies"
|
||||||
|
) from None
|
||||||
Reference in New Issue
Block a user