20

I am switching a project that currently uses pipenv to poetry as a test to see what the differences are. The project is a simple, redistributable, Django app. It supports Python 3.6-8, and Django 2.2 and 3.0. I have a tox.ini file that covers all combinations of Python and Django thus:

[tox]
envlist = py{36,37,38}-django{22,30}

[testenv]
whitelist_externals = poetry
skip_install = true

deps =
    django22: Django==2.2
    django30: Django==3.0

commands =
    poetry install -vvv
    poetry run pytest --cov=my_app tests/
    poetry run coverage report -m

The problem that I am having (which does not exist in the pipenv world) is that the poetry install statement will always overwrite whatever is in the deps section with whatever is in the poetry.lock file (which will be auto-generated if it does not exist). This means that the test matrix will never test against Django 2.2 - as each tox virtualenv gets Django 3.0 installed by default.

I don't understand how this is supposed to work - should installing dependencies using poetry respect the existing environment into which it is being installed, or not?

So - my question is - how do I set up a multi-version tox (or travis) test matrix, with poetry as the dependency manager?

My pyproject.toml defines Python / Django versions as:

[tool.poetry.dependencies]
python = "^3.6"
django = "^2.2 || ^3.0"

The generated poetry.lock file (not committed) has this Django version information:

[[package]]
category = "main"
description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
name = "django"
optional = false
python-versions = ">=3.6"
version = "3.0"

UPDATE: include clean tox output

This is the result when I delete the lock file, and recreate the tox environment. As you can see, tox installs Django==2.2 as a dependency in the virtualenv, but poetry then updates this to 3.0 when it installs.

I need a solution that runs the poetry install, respecting existing package installs. i.e. if pyproject.toml states Django = "^2.2 || ^3.0", and 2.2 is already installed, then pin to that version - don't attempt to upgrade.

my-app$ tox -r -e py36-django22
py36-django22 recreate: .tox/py36-django22
py36-django22 installdeps: Django==2.2
py36-django22 installed: Django==2.2,my-app==0.1.0,pytz==2019.3,sqlparse==0.3.0
py36-django22 run-test: commands[0] | poetry install -vvv
Using virtualenv: .tox/py36-django22
Updating dependencies
Resolving dependencies...
   1: derived: django (^2.2 || ^3.0)
   ...
PyPI: 10 packages found for django >=2.2,<4.0
   ...
   1: Version solving took 3.330 seconds.
   1: Tried 1 solutions.

Writing lock file

Package operations: 52 installs, 1 update, 0 removals, 3 skipped

  - ...
  - Updating django (2.2 -> 3.0)
  - ...

UPDATE 2

Following instructions from sinoroc below - I have updated the tox file to remove skip_dist and include isolated_build. This works, sort of. tox builds the package, and installs it - but only the non-dev version, which does not include pytest, coverage and a host of linting tools that I'd like to include at a later point. i.e. the tools I want to run through tox are specified as dev-dependencies in poetry. There is a solution here, to include all of these inside the tox file - but that seems self-defeating - as then I have poetry and tox both declaring dependencies.

[tool.poetry.dependencies]
python = "^3.6"
django = "^2.2 || ^3.0"

[tool.poetry.dev-dependencies]
pytest = "^3.0"
pytest-cov = "^2.8"
pytest-django = "^3.7"
coverage = "^4.5"
pylint = "^2.4"
pylint-django = "^2.0"
flake8 = "^3.7"
flake8-bandit = "^2.1"
flake8-docstrings = "^1.5"
isort = "^4.3"
mypy = "^0.750.0"
pre-commit = "^1.20"
black = "=19.3b0"

UPDATE 3: solution

[tox]
isolated_build = True
envlist = lint, mypy, py{36,37,38}-django{22,30}

[travis]
python =
    3.6: lint, mypy, py36
    3.7: lint, mypy, py37
    3.8: lint, mypy, py38

[testenv]
deps =
    pytest
    pytest-cov
    pytest-django
    coverage
    django22: Django==2.2
    django30: Django==3.0

commands =
    django-admin --version
    pytest --cov=my_app tests/

[testenv:lint]
deps =
    pylint
    pylint-django
    flake8
    flake8-bandit
    flake8-docstrings
    isort
    black

commands =
    isort --recursive my_app
    black my_app
    pylint my_app
    flake8 my_app

[testenv:mypy]
deps =
    mypy

commands =
    mypy my_app
Hugo Rodger-Brown
  • 11,054
  • 11
  • 52
  • 78
  • Not a _poetry_ user myself, so not 100% sure as I can't test it, but... I would try following [this advice](https://tox.readthedocs.io/en/latest/example/package.html#poetry), as well as removing `skip_install` and `whitelist_externals`, and use the commands directly without `poetry install` or `poetry run` in `commands`. – sinoroc Dec 29 '19 at 18:49
  • 2
    I think it would be more clear to move the solution into its own answer :) – Jacob Pavlock Aug 03 '20 at 13:40
  • Also, such solution should mention it accepts the tradeoff to repeat dev requirements and does not benefit from any poetry locking – N1ngu Oct 19 '21 at 15:50

1 Answers1

14

Haven't thoroughly tested it, but I believe something like this should work:

[tox]
envlist = py{36,37,38}-django{22,30}
isolated_build = True

[testenv]
deps =
    django22: Django==2.2
    django30: Django==3.0
    # plus the dev dependencies
    pytest
    coverage

commands =
    pytest --cov=my_app tests/
    coverage report -m

See the "poetry" section in the "packaging" chapter of the tox documentation.


In order to avoid the repetition of the dev dependencies, one could try the following variation based on the extras feature:

tox.ini

[tox]
# ...

[testenv]
# ...
deps =
    django22: Django==2.2
    django30: Django==3.0
extras =
    test

pyproject.toml

[tool.poetry]
# ...

[tool.poetry.dependencies]
python = "^3.6"
django = "^2.2 || ^3.0"
#
pytest = { version = "^5.2", optional = true }

[tool.poetry.extras]
test = ["pytest"]

[build-system]
# ...

Nowadays there are tox plug-ins that try to make for a better integration with poetry-based projects:

sinoroc
  • 18,409
  • 2
  • 39
  • 70
  • 1
    This is very close - so thank you for that (`isolated_build` is the key) - however it's installing the non-dev dependencies only, which means that it cannot find `pytest` to actually run the tests. Ideally, all the dependencies for running tests (`pytest`, `coverage`, etc.) would be in dev-dependencies and not packaged with the app. – Hugo Rodger-Brown Dec 31 '19 at 09:58
  • 1
    I think it used the dev dependencies in my quick test, but I don't remember clearly, I didn't spend much time on it. But yes, I get the point that the dev dependencies are now both in poetry and tox. With setuptools I set the dev dependencies as extras and tell tox to install the extras. – sinoroc Dec 31 '19 at 18:03
  • @Hugo I updated the answer to show a suggestion of how to avoid repeating the _dev_ dependencies. It works perfectly for my use cases, but I can see how it has its drawbacks for other use cases though... – sinoroc Jan 08 '20 at 08:59