2 Commits
0.3.1 ... 0.1.0

Author SHA1 Message Date
62cd9934a2 Add automation config files
Add coveragerc for upcoming pytest coverage
Add pre-commit config
Add makefile for automating common processes
2020-09-23 18:42:21 -04:00
50dc91c211 Add project dependencies and update pyproject meta
Add classifiers and keywords
Add dev dependencies
Add repo link
2020-09-23 18:40:31 -04:00
11 changed files with 938 additions and 1968 deletions

View File

@@ -1,41 +0,0 @@
---
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
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 tox
run: pip install "tox>=3.20.0,<3.21.0" --upgrade
- 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 tox requirements
run: pip install "tox>=3.20.0,<3.21.0" --upgrade
- name: Run meta checks
run: tox -e static -e static-tests -e security

View File

@@ -4,14 +4,14 @@ repos:
rev: 20.8b1
hooks:
- id: black
language_version: python3
language_version: python3.7
- repo: https://github.com/asottile/blacken-docs
rev: v0.5.0
hooks:
- id: blacken-docs
additional_dependencies: [black==20.8b1]
language_version: python3
language_version: python3.7
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.3.5

427
.pylintrc
View File

@@ -1,427 +0,0 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint.
jobs=1
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=logging-fstring-interpolation, logging-format-interpolation, bad-continuation, line-too-long
#print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[BASIC]
# Naming hint for argument names
argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct argument names
argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Naming hint for attribute names
attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct attribute names
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming hint for function names
function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct function names
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Good variable names which should always be accepted, separated by a comma
good-names=_,ip,ap
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for method names
method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct method names
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Naming hint for variable names
variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct variable names
variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
#notes=FIXME,XXX,TODO
notes=XXX
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=yes
# Minimum lines number of a similarity.
min-similarity-lines=10
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_|^fxt_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,TERMIOS,Bastion,rexec
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[DESIGN]
# Maximum number of arguments for function / method
max-args=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of statements in function / method body
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View File

@@ -1,129 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,5 +1,8 @@
# tox-poetry-installer makefile
# You can set these variables from the command line
PROJECT = tox_poetry_installer
.PHONY: help
# Put it first so that "make" without argument is like "make help"
# Adapted from:
@@ -8,20 +11,21 @@ help: ## List Makefile targets
$(info Makefile documentation)
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-10s\033[0m %s\n", $$1, $$2}'
tox: clean
tox
clean-tox:
rm --recursive --force ./.mypy_cache
rm --recursive --force ./.tox
rm --recursive --force tests/__pycache__/
rm --recursive --force .pytest_cache/
rm --force .coverage
rm -rf ./.mypy_cache
rm -rf ./.tox
rm -f .coverage
clean-py:
rm --recursive --force ./dist
rm --recursive --force ./build
rm --recursive --force ./*.egg-info
rm --recursive --force __pycache__/
rm -rf ./dist
rm -rf ./build
rm -rf ./*.egg-info
rm -rf __pycache__/
clean: clean-tox clean-py; ## Clean temp build/cache files and directories
clean: clean-tox clean-py clean-docs; ## Clean temp build/cache files and directories
wheel: ## Build Python binary distribution wheel package
poetry build --format wheel
@@ -30,7 +34,7 @@ source: ## Build Python source distribution package
poetry build --format sdist
test: ## Run the project testsuite(s)
poetry run tox --recreate
poetry run tox -r
publish: wheel source ## Build and upload to pypi (requires $PYPI_API_KEY be set)
poetry publish --username __token__ --password $(PYPI_API_KEY)
docs: ## Build the documentation using Sphinx
poetry run tox -e docs

480
README.md
View File

@@ -1,466 +1,62 @@
# tox-poetry-installer
A plugin for [Tox](https://tox.readthedocs.io/en/latest/) that allows test environment
dependencies to be installed using [Poetry](https://python-poetry.org/) from its lockfile.
A [Tox](https://tox.readthedocs.io/en/latest/) plugin for installing Tox environment
dependencies using [Poetry](https://python-poetry.org/) from the Poetry lockfile.
⚠️ **This project is alpha software and should not be used in production environments** ⚠️
[![ci-status](https://github.com/enpaul/tox-poetry-installer/workflows/CI/badge.svg?event=push)](https://github.com/enpaul/tox-poetry-installer/actions)
[![license](https://img.shields.io/pypi/l/tox-poetry-installer)](https://opensource.org/licenses/MIT)
[![pypi-version](https://img.shields.io/pypi/v/tox-poetry-installer)](https://pypi.org/project/tox-poetry-installer/)
[![python-versions](https://img.shields.io/pypi/pyversions/tox-poetry-installer)](https://www.python.org)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
**Documentation**
* [Installation](#installation)
* [Quick Start](#quick-start)
* [Usage Examples](#usage-examples)
* [Known Drawbacks and Problems](#known-drawbacks-and-problems)
* [Why would I use this?](#why-would-i-use-this) (What problems does this solve?)
* [Developing](#developing)
* [Contributing](#contributing)
* [Roadmap](#roadmap)
* [Path to Beta](#path-to-beta)
* [Path to Stable](#path-to-stable)
Related resources:
* [Poetry Python Project Manager](https://python-poetry.org/)
* [Tox Automation Project](https://tox.readthedocs.io/en/latest/)
* [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
Add the plugin as a development dependency of a Poetry project:
```
~ $: poetry add tox-poetry-installer --dev
```
Confirm that the plugin is installed, and Tox recognizes it, by checking the Tox version:
```
~ $: poetry run tox --version
3.20.0 imported from .venv/lib64/python3.8/site-packages/tox/__init__.py
registered plugins:
tox-poetry-installer-0.2.2 at .venv/lib64/python3.8/site-packages/tox_poetry_installer.py
```
If using Pip, ensure that the plugin is installed to the same environment as Tox:
```
# Calling the virtualenv's 'pip' binary directly will cause pip to install to that virtualenv
~ $: /path/to/my/automation/virtualenv/bin/pip install tox
~ $: /path/to/my/automation/virtualenv/bin/pip install tox-poetry-installer
```
## Quick Start
To require a Tox environment install all it's dependencies from the Poetry lockfile, add the
`require_locked_deps = true` option to the environment configuration and remove all version
specifiers from the dependency list. The versions to install will be taken from the lockfile
directly:
```ini
[testenv]
description = Run the tests
require_locked_deps = true
deps =
pytest
pytest-cov
black
pylint
mypy
commands = ...
```
To require specific dependencies be installed from the Poetry lockfile, and let the rest be
installed using the default Tox installation backend, add the suffix `@poetry` to the dependencies.
In the example below the `pytest`, `pytest-cov`, and `black` dependencies will be installed from
the lockfile while `pylint` and `mypy` will be installed using the versions specified here:
```ini
[testenv]
description = Run the tests
require_locked_deps = true
deps =
pytest@poetry
pytest-cov@poetry
black@poetry
pylint >=2.5.0
mypy == 0.770
commands = ...
```
**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.
## Usage Examples
After installing the plugin to a project your Tox automation is already benefiting from the
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?
Let's use an example `tox.ini` file, below, that defines two environments: the main `testenv` for
running the project tests and `testenv:check` for running some other helpful tools:
```ini
[tox]
envlist = py37, static
isolated_build = true
[testenv]
description = Run the 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
deps =
pylint >=2.4.4,<2.6.0
mypy == 0.770
black --pre
commands = ...
```
Running Tox using this config gives us this error:
```
tox_poetry_installer.LockedDepVersionConflictError: Locked dependency 'pylint >=2.4.4,<2.6.0' cannot include version specifier
```
This is because we told the Tox environment to require all dependencies 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 gets 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
deps =
pylint
mypy
black
commands = ...
```
Now all the dependencies will be installed from the lockfile. If Poetry updates the lockfile with
a new version then that updated version will be automatically installed when the Tox environment is
recreated.
Now let's look at the `testenv` environment. Let's make the same changes to the `testenv`
environment that we made to `testenv:check` above; remove the PyTest version and add
`require_locked_deps = true`. Then imagine that we want to add the
[Requests](https://requests.readthedocs.io/en/master/) library to the test environment: we
can add `requests` as a dependency of the test environment, but this will cause an error:
```ini
[testenv]
description = Run the tests
require_locked_deps = true
deps =
pytest
requests
commands = ...
```
Running Tox with this config gives us this error:
```
tox_poetry_installer.LockedDepNotFoundError: No version of locked dependency 'requests' found in the project lockfile
```
This is because `requests` is not in our lockfile yet. Tox will refuse to install a dependency
that isn't in the lockfile to an an environment that specifies `require_locked_deps = true`. We
can fix this by running `poetry add requests --dev` to add it to the lockfile.
Now let's combine dependencies from the lockfile with dependencies that are
specified in-line in the Tox environment configuration.
[This isn't generally recommended](#why-would-i-use-this), but it is 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 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 specifier to the `requests`
entry in 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 the `pytest` entry in the
dependency list:
```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.
## Known Drawbacks and Problems
* The following `tox.ini` configuration options have no effect on the dependencies installed from
the Poetry lockfile (note that they will still affect unlocked dependencies):
* [`install_command`](https://tox.readthedocs.io/en/latest/config.html#conf-install_command)
* [`pip_pre`](https://tox.readthedocs.io/en/latest/config.html#conf-pip_pre)
* [`downloadcache`](https://tox.readthedocs.io/en/latest/config.html#conf-downloadcache) (deprecated)
* [`download`](https://tox.readthedocs.io/en/latest/config.html#conf-download)
* [`indexserver`](https://tox.readthedocs.io/en/latest/config.html#conf-indexserver)
* [`usedevelop`](https://tox.readthedocs.io/en/latest/config.html#conf-indexserver)
* 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 overwritten 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
dependencies or as transient dependencies (dependencies of dependencies). This is due to
[an ongoing discussion in the Poetry project](https://github.com/python-poetry/poetry/issues/1584);
the list of dependencies that cannot be installed from the lockfile can be found
[here](https://github.com/python-poetry/poetry/blob/cc8f59a31567f806be868aba880ae0642d49b74e/poetry/puzzle/provider.py#L55).
This plugin will skip these dependencies entirely, but log a warning when they are encountered.
⚠️ **This project is a very, very early prototype and should not be used in any production
capacity.**
## Why would I use this?
**Introduction**
[The point of using a lockfile is to create reproducable builds](https://docs.gradle.org/current/userguide/dependency_locking.html). One of the main points of Tox is to [allow a Python
package to be built and tested in multiple environments](https://tox.readthedocs.io/en/latest/#what-is-tox). However, in the Tox configuration file the dependencies are specified with
standard dynamic ranges and passed directly to Pip. This means that the reproducability
a lockfile brings to a project is circumvented when running the tests.
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 any additional metadata that
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
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
builds are using the same environment as a developer.
The obvious solution to this problem is to add the dependencies required for testing to the
lockfile as development dependencies so that they are locked along with the primary dependencies
of the project. The only remaining question however, is how to install the dev-dependencies from
the lockfile into the Tox environment when Tox sets it up. [For very good reason](https://dev.to/elabftw/stop-using-sudo-pip-install-52mn) Tox uses independent
[virtual environments](https://docs.python.org/3/tutorial/venv.html) for each environment a
project defines, so there needs to be a way to install a locked dependency into a Tox
environment.
[Poetry](https://python-poetry.org/) is a project dependency manager for Python projects, and
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
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).
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,
there can be subtle differences between the dependencies a developer is using and the
dependencies Tox uses.
This is where this plugin comes in.
This is where this plugin comes into play.
By default Tox uses [Pip](https://docs.python.org/3/tutorial/venv.html) to install the
PEP-508 compliant dependencies to a test environment. This plugin extends the default
Tox dependency installation behavior to support installing dependencies using a Poetry-based
installation method that makes use of the dependency metadata from Poetry's lockfile.
**The Problem**
Environment dependencies for a Tox environment are usually specified in PEP-508 format, like
the below example:
Traditionally Tox environments specify dependencies and their corresponding versions inline in
[PEP-440](https://www.python.org/dev/peps/pep-0440/) format like below:
```ini
# from tox.ini
...
[testenv]
description = Some very cool tests
description = Run the tests
deps =
foo == 1.2.3
bar >=1.3,<2.0
baz
...
```
Let's assume these dependencies are also useful during development, so they can be added to the
Poetry environment using this command:
This runs into the problem outlined above: many different versions of the `bar` dependency
could be installed depending on what the latest version is that matches the defined range. The
`baz` dependency is entirely unpinned making it a true wildcard, and even the seemingly static
`foo` dependency could result in subtly different files being downloaded depending on what's
available in the upstream mirrors.
```
poetry add --dev \
foo==1.2.3 \
bar>=1.3,<2.0 \
baz
```
However these same versions, specified in the [pyproject.toml](https://snarky.ca/what-the-heck-is-pyproject-toml/) file, result in reproducible
installations when using `poetry install` because they each have a specific version and file
hash specified in the lockfile. The versions specified in the lockfile are updated only when
`poetry update` is run.
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
environment in use by a developer:
* **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
it. A developer can run `poetry remove foo` and then `poetry add foo^1.2` to get the new
version, but the Tox environment is left unchanged. The development environment, as defined by
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
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
`poetry update` rather than with the `remove` and `add` commands used above. If the
maintainers of `bar` release version `1.6.0` then the Tox environment will install it
because it is valid for the specified version range. Meanwhile the Poetry environment will
continue to install the version from the lockfile until `poetry update bar` explicitly
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
[generally a bad idea](https://python-poetry.org/docs/faq/#why-are-unbound-version-constraints-a-bad-idea),
but here it can cause real problems. Poetry will interpret an unbound dependency using
[the carrot requirement](https://python-poetry.org/docs/dependency-specification/#caret-requirements)
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
environment will have a constraint of `baz==*`. The Tox environment can now install an
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,
but also to the dependencies of those dependencies, those dependencies' dependencies, and so on.
**The Solution**
This plugin allows dependencies specified in Tox environment take their version directly from
the Poetry lockfile without needing an independent version to be specified in the Tox
environment configuration. The modified version of the example environment given below appears
less stable than the one presented above because it does not specify any versions for its
dependencies:
```ini
# from tox.ini
...
[testenv]
description = Some very cool tests
require_locked_deps = true
deps =
foo
bar
baz
...
```
However with the `tox-poetry-installer` plugin installed the `require_locked_deps = true`
setting means that Tox will install these dependencies from the Poetry lockfile so that the
version installed to the Tox environment exactly matches the version Poetry is managing. When
`poetry update` updates the lockfile with new versions of these dependencies, Tox will
automatically install these new versions without needing any changes to the configuration.
This plugin allows environment dependencies to be specified in the [tox.ini](https://tox.readthedocs.io/en/latest/config.html) configuration file
just by name. The package is automatically retrieved from the lockfile and the Poetry backend
is used to install the singular locked package version to the Tox environment. When the
lockfile is updated, the Tox environment will automatically install the newly locked package
as well. All dependency requirements are specified in one place (pyproject.toml), all
dependencies have a locked version, and everything is installed from that source of truth.
## Developing
## Planned features
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
# Clone the repository...
# ...over HTTPS
git clone https://github.com/enpaul/tox-poetry-installer.git
# ...over SSH
git clone git@github.com:enpaul/tox-poetry-installer.git
# Create a the local project virtual environment and install dependencies
cd tox-poetry-installer
poetry install
# Install pre-commit hooks
poetry run pre-commit install
# Run tests and static analysis
poetry run tox
```
## Contributing
All project contributors and participants are expected to adhere to the
[Contributor Covenant Code of Conduct, Version 2](CODE_OF_CONDUCT.md).
The `devel` branch has the latest (potentially unstable) changes. The
[tagged versions](https://github.com/enpaul/tox-poetry-installer/releases) correspond to the
releases on PyPI.
* To report a bug, request a feature, or ask for assistance, please
[open an issue on the Github repository](https://github.com/enpaul/tox-poetry-installer/issues/new).
* To report a security concern or code of conduct violation, please contact the project author
directly at **ethan dot paul at enp dot one**.
* To submit an update, please
[fork the repository](https://docs.github.com/en/enterprise/2.20/user/github/getting-started-with-github/fork-a-repo)
and
[open a pull request](https://github.com/enpaul/tox-poetry-installer/compare).
## Roadmap
This project is under active development and is classified as alpha software, not yet ready
for usage in production environments.
* 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
use cases
### Path to Beta
- [X] Verify that primary package dependencies (from the `.package` env) are installed
correctly using the Poetry backend.
- [X] Support the [`extras`](https://tox.readthedocs.io/en/latest/config.html#conf-extras)
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
backend.
- [ ] Add warnings when an unsupported Tox configuration option is detected while using the
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
and the module constants.
- [X] Update to use [poetry-core](https://github.com/python-poetry/poetry-core) and
improve robustness of the Tox and Poetry module imports
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 [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))
- [ ] Add configuration option for installing all dev-dependencies to a testenv ([#14](https://github.com/enpaul/tox-poetry-installer/issues/14))
### Path to Stable
Everything in Beta plus...
- [ ] 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
- [X] Add Github Actions based CI
- [ ] Add CI for CPython, PyPy, and Conda
- [ ] Add CI for Linux and Windows
* Per-environment disabling (i.e. fallback to the default Tox installation backend)
* Detection of lockfile changes that trigger Tox environment recreation
* Tests

1255
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,12 @@
[tool.poetry]
name = "tox-poetry-installer"
version = "0.3.1"
version = "0.1.0"
license = "MIT"
authors = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
authors = ["Ethan Paul <e@enp.one>"]
description = "Tox plugin to install Tox environment dependencies using the Poetry backend and lockfile"
repository = "https://github.com/enpaul/tox-poetry-installer/"
packages = [
{include = "tox_poetry_installer.py"},
{include = "tests/*.py", format = "sdist"}
]
packages = [{include = "tox_poetry_installer.py"}]
keywords = ["tox", "poetry", "plugin"]
readme = "README.md"
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Plugins",
@@ -31,22 +27,23 @@ poetry_installer = "tox_poetry_installer"
[tool.poetry.dependencies]
python = "^3.6"
poetry = "^1.0.0"
poetry-core = "^1.0.0"
tox = "^2.3.0 || ^3.0.0"
[tool.poetry.dev-dependencies]
bandit = "^1.6.2"
black = {version = "^20.8b1", allow-prereleases = true}
ipython = {version = "^7.18.1", python = "^3.7"}
mypy = "^0.782"
pre-commit = {version = "^2.7.1", python = "^3.6.1"}
pylint = "^2.4.4"
pytest = "^6.0.2"
pytest-cov = "^2.10.1"
pytest = "^5.2.0"
pytest-cov = "^2.8.0"
reorder-python-imports = {version = "^2.3.5", python = "^3.6.1"}
safety = "^1.9.0"
sphinx = "^3.0.4"
sphinx-autodoc-typehints = "^1.8.0"
toml = "^0.10.1"
tox = "^3.20.0"
black = {version = "^20.8b1", allow-prereleases = true}
[build-system]
requires = ["poetry>=1.0.0"]

View File

@@ -1,39 +0,0 @@
"""Ensure that the pyproject and module metadata never drift out of sync
The next best thing to having one source of truth is having a way to ensure all of your
sources of truth agree with each other.
"""
from pathlib import Path
import toml
import tox_poetry_installer
def test_metadata():
"""Test that module metadata matches pyproject poetry metadata"""
with (Path(__file__).resolve().parent / ".." / "pyproject.toml").open() as infile:
pyproject = toml.load(infile, _dict=dict)
assert pyproject["tool"]["poetry"]["name"] == tox_poetry_installer.__title__
assert pyproject["tool"]["poetry"]["version"] == tox_poetry_installer.__version__
assert pyproject["tool"]["poetry"]["license"] == tox_poetry_installer.__license__
assert (
pyproject["tool"]["poetry"]["description"] == tox_poetry_installer.__summary__
)
assert pyproject["tool"]["poetry"]["repository"] == tox_poetry_installer.__url__
assert (
all(
item in tox_poetry_installer.__authors__
for item in pyproject["tool"]["poetry"]["authors"]
)
is True
)
assert (
all(
item in pyproject["tool"]["poetry"]["authors"]
for item in tox_poetry_installer.__authors__
)
is True
)

63
tox.ini
View File

@@ -1,66 +1,11 @@
[tox]
envlist = py36, py37, py38, static, static-tests, security
envlist = py38
isolated_build = true
[testenv]
description = Run the tests
require_locked_deps = true
description = Run the tests (pytest)
deps =
pytest
pytest-cov
toml
commands =
pytest --cov tox_poetry_installer --cov-config {toxinidir}/.coveragerc tests/ --cov-report term-missing
[testenv:static]
description = Static formatting and quality enforcement
require_locked_deps = true
basepython = python3.8
ignore_errors = true
deps =
pylint
mypy
black
reorder-python-imports
pre-commit
commands =
black {toxinidir}/tox_poetry_installer.py
reorder-python-imports {toxinidir}/tox_poetry_installer.py
pre-commit run --all-files
pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tox_poetry_installer.py
mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tox_poetry_installer.py
[testenv:static-tests]
description = Static formatting and quality enforcement for the tests
require_locked_deps = true
basepython = python3.8
ingore_errors = true
deps =
pylint
mypy
black
reorder-python-imports
allowlist_externals =
bash
commands =
black {toxinidir}/tests/
bash -c "reorder-python-imports {toxinidir}/tests/*.py --unclassifiable-application-module tox_poetry_installer"
bash -c "pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/tests/*.py"
bash -c "mypy --ignore-missing-imports --no-strict-optional {toxinidir}/tests/*.py"
[testenv:security]
description = Security checks
require_locked_deps = true
basepython = python3.8
ignore_errors = true
requests
skip_install = true
deps =
bandit
safety
poetry
allowlist_externals =
bash
commands =
bandit --quiet {toxinidir}/tox_poetry_installer.py
bash -c "bandit --quiet --skip B101 {toxinidir}/tests/*.py"
bash -c "poetry export --format requirements.txt --without-hashes --dev | safety check --stdin --bare"
pip freeze

View File

@@ -1,377 +1,78 @@
"""Tox plugin for installing environments using Poetry
This plugin makes use of the ``tox_testenv_install_deps`` Tox plugin hook to augment the default
installation functionality to install dependencies from the Poetry lockfile for the project. It
does this by using ``poetry`` to read in the lockfile, identify necessary dependencies, and then
use Poetry's ``PipInstaller`` class to install those packages into the Tox environment.
Quick definition of terminology:
* "project package" - the package that Tox is testing, usually the one the current project is
is developing; definitionally, this is the package that is built by Tox in the ``.package`` env.
* "project package dependency" or "project dependency" - a dependency required by the project
package for installation; i.e. a package that would be installed when running
``pip install <project package>``.
* "environment dependency" - a dependency specified for a given testenv in the Tox configuration.
* "locked dependency" - a package that is present in the Poetry lockfile and will be installed
according to the metadata in the lockfile.
* "unlocked dependency" - a package that is either not present in the Poetry lockfile or is not
specified to be installed according to the metadata in the lockfile.
* "transiety dependency" - a package not explicitly specified for installation, but required by a
package that is explicitly specified.
"""
from pathlib import Path
from typing import Dict
from typing import List
from typing import NamedTuple
from typing import Sequence
from typing import Set
from typing import Tuple
import logging
from typing import Dict, List
from poetry.factory import Factory
from poetry.factory import Poetry
from poetry.packages import Package
from poetry.installation.pip_installer import PipInstaller
from poetry.io.null_io import NullIO
from poetry.utils.env import VirtualEnv
from poetry.core.packages import Package as PoetryPackage
from poetry.factory import Factory as PoetryFactory
from poetry.installation.pip_installer import PipInstaller as PoetryPipInstaller
from poetry.io.null_io import NullIO as PoetryNullIO
from poetry.poetry import Poetry
from poetry.puzzle.provider import Provider as PoetryProvider
from poetry.utils.env import VirtualEnv as PoetryVirtualEnv
from tox import hookimpl
from tox import reporter
from tox.action import Action as ToxAction
from tox.config import DepConfig as ToxDepConfig
from tox.config import Parser as ToxParser
from tox.venv import VirtualEnv as ToxVirtualEnv
from tox import hookimpl
__title__ = "tox-poetry-installer"
__summary__ = "Tox plugin to install Tox environment dependencies using the Poetry backend and lockfile"
__version__ = "0.3.1"
__version__ = "0.1.0"
__url__ = "https://github.com/enpaul/tox-poetry-installer/"
__license__ = "MIT"
__authors__ = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
__authors__ = ["Ethan Paul <e@enp.one>"]
# 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, ...] = ("~=", "==", "!=", ">", "<")
def _make_poetry(venv: ToxVirtualEnv) -> Poetry:
return Factory().create_poetry(venv.envconfig.config.toxinidir)
# Prefix all reporter messages should include to indicate that they came from this module in the
# console output.
_REPORTER_PREFIX = f"[{__title__}]:"
# Suffix that indicates an env dependency should be treated as a locked dependency and thus be
# installed from the lockfile. Will be automatically stripped off of a dependency name during
# sorting so that the resulting string is just the valid package name. This becomes optional when
# the "require_locked_deps" option is true for an environment; in that case a bare dependency like
# 'foo' is treated the same as an explicitly locked dependency like 'foo@poetry'
_MAGIC_SUFFIX_MARKER = "@poetry"
# Map of package names to the package object
PackageMap = Dict[str, PoetryPackage]
class _SortedEnvDeps(NamedTuple):
unlocked_deps: List[ToxDepConfig]
locked_deps: List[ToxDepConfig]
class ToxPoetryInstallerException(Exception):
"""Error while installing locked dependencies to the test environment"""
class LockedDepVersionConflictError(ToxPoetryInstallerException):
"""Locked dependencies cannot specify an alternate version for installation"""
class LockedDepNotFoundError(ToxPoetryInstallerException):
"""Locked dependency was not found in the lockfile"""
class ExtraNotFoundError(ToxPoetryInstallerException):
"""Project package extra not defined in project's pyproject.toml"""
def _sort_env_deps(venv: ToxVirtualEnv) -> _SortedEnvDeps:
"""Sorts the environment dependencies by lock status
Lock status determines whether a given environment dependency will be installed from the
lockfile using the Poetry backend, or whether this plugin will skip it and allow it to be
installed using the default pip-based backend (an unlocked dependency).
.. note:: A locked dependency must follow a required format. To avoid reinventing the wheel
(no pun intended) this module does not have any infrastructure for parsing PEP-508
version specifiers, and so requires locked dependencies to be specified with no
version (the installed version being taken from the lockfile). If a dependency is
specified as locked and its name is also a PEP-508 string then an error will be
raised.
"""
reporter.verbosity1(
f"{_REPORTER_PREFIX} sorting {len(venv.envconfig.deps)} env dependencies by lock requirement"
)
unlocked_deps = []
locked_deps = []
for dep in venv.envconfig.deps:
if venv.envconfig.require_locked_deps:
reporter.verbosity1(
f"{_REPORTER_PREFIX} lock required for env, treating '{dep.name}' as locked env dependency"
)
dep.name = dep.name.replace(_MAGIC_SUFFIX_MARKER, "")
locked_deps.append(dep)
else:
if dep.name.endswith(_MAGIC_SUFFIX_MARKER):
reporter.verbosity1(
f"{_REPORTER_PREFIX} specification includes marker '{_MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as locked env dependency"
)
dep.name = dep.name.replace(_MAGIC_SUFFIX_MARKER, "")
locked_deps.append(dep)
else:
reporter.verbosity1(
f"{_REPORTER_PREFIX} specification does not include marker '{_MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as unlocked env dependency"
)
unlocked_deps.append(dep)
reporter.verbosity1(
f"{_REPORTER_PREFIX} identified {len(locked_deps)} locked env dependencies: {[item.name for item in locked_deps]}"
)
reporter.verbosity1(
f"{_REPORTER_PREFIX} identified {len(unlocked_deps)} unlocked env dependencies: {[item.name for item in unlocked_deps]}"
)
return _SortedEnvDeps(locked_deps=locked_deps, unlocked_deps=unlocked_deps)
def _install_to_venv(
poetry: Poetry, venv: ToxVirtualEnv, packages: Sequence[PoetryPackage]
):
"""Install a bunch of packages to a virtualenv
:param poetry: Poetry object the packages were sourced from
:param venv: Tox virtual environment to install the packages to
:param packages: List of packages to install to the virtual environment
"""
reporter.verbosity1(
f"{_REPORTER_PREFIX} Installing {len(packages)} packages to environment at {venv.envconfig.envdir}"
)
installer = PoetryPipInstaller(
env=PoetryVirtualEnv(path=Path(venv.envconfig.envdir)),
io=PoetryNullIO(),
pool=poetry.pool,
)
for dependency in packages:
reporter.verbosity1(f"{_REPORTER_PREFIX} installing {dependency}")
installer.install(dependency)
def _find_transients(packages: PackageMap, dependency_name: str) -> Set[PoetryPackage]:
"""Using a poetry object identify all dependencies of a specific dependency
:param poetry: Populated poetry object which can be used to build a populated locked
repository object.
:param dependency_name: Bare name (without version) of the dependency to fetch the transient
dependencies of.
:returns: List of packages that need to be installed for the requested dependency.
.. note:: The package corresponding to the dependency named by ``dependency_name`` is included
in the list of returned packages.
"""
try:
top_level = packages[dependency_name]
def find_deps_of_deps(name: str) -> List[PoetryPackage]:
if name in PoetryProvider.UNSAFE_PACKAGES:
reporter.warning(
f"{_REPORTER_PREFIX} installing package '{name}' using Poetry is not supported; skipping installation of package '{name}'"
)
return []
transients = [packages[name]]
for dep in packages[name].requires:
transients += find_deps_of_deps(dep.name)
return transients
return set(find_deps_of_deps(top_level.name))
except KeyError:
if any(
delimiter in dependency_name for delimiter in _PEP508_VERSION_DELIMITERS
):
raise LockedDepVersionConflictError(
f"Locked dependency '{dependency_name}' cannot include version specifier"
) from None
raise LockedDepNotFoundError(
f"No version of locked dependency '{dependency_name}' found in the project lockfile"
) from None
def _install_env_dependencies(
venv: ToxVirtualEnv, poetry: Poetry, packages: PackageMap
):
"""Install the packages for a specified testenv
Processes the tox environment config, identifies any locked environment dependencies, pulls
them from the lockfile, and installs them to the virtual environment.
:param venv: Tox virtual environment to install the packages to
:param poetry: Poetry object the packages were sourced from
:param packages: Mapping of package names to the corresponding package object
"""
env_deps = _sort_env_deps(venv)
dependencies: List[PoetryPackage] = []
for dep in env_deps.locked_deps:
try:
dependencies += _find_transients(packages, dep.name.lower())
except ToxPoetryInstallerException as err:
venv.status = "lockfile installation failed"
reporter.error(f"{_REPORTER_PREFIX} {err}")
raise err
reporter.verbosity1(
f"{_REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(env_deps.locked_deps)} locked env dependencies"
)
reporter.verbosity1(
f"{_REPORTER_PREFIX} updating env config with {len(env_deps.unlocked_deps)} unlocked env dependencies for installation using the default backend"
)
venv.envconfig.deps = env_deps.unlocked_deps
reporter.verbosity0(
f"{_REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} env dependencies from lockfile"
)
_install_to_venv(poetry, venv, dependencies)
def _install_project_dependencies(
venv: ToxVirtualEnv, poetry: Poetry, packages: PackageMap
):
"""Install the dependencies of the project package
Install all primary dependencies of the project package.
:param venv: Tox virtual environment to install the packages to
:param poetry: Poetry object the packages were sourced from
:param packages: Mapping of package names to the corresponding package object
"""
reporter.verbosity1(
f"{_REPORTER_PREFIX} performing installation of project dependencies"
)
base_dependencies: List[PoetryPackage] = [
packages[item.name]
for item in poetry.package.requires
if not item.is_optional()
]
extra_dependencies: List[PoetryPackage] = []
for extra in venv.envconfig.extras:
try:
extra_dependencies += [
packages[item.name] for item in poetry.package.extras[extra]
]
except KeyError:
raise ExtraNotFoundError(
f"Environment '{venv.name}' specifies project extra '{extra}' which was not found in the lockfile"
) from None
dependencies: List[PoetryPackage] = []
for dep in base_dependencies + extra_dependencies:
try:
dependencies += _find_transients(packages, dep.name.lower())
except ToxPoetryInstallerException as err:
venv.status = "lockfile installation failed"
reporter.error(f"{_REPORTER_PREFIX} {err}")
raise err
reporter.verbosity1(
f"{_REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(poetry.package.requires)} project dependencies"
)
reporter.verbosity0(
f"{_REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} project dependencies from lockfile"
)
_install_to_venv(poetry, venv, dependencies)
@hookimpl
def tox_addoption(parser: ToxParser):
"""Add required configuration options to the tox INI file
Adds the ``require_locked_deps`` configuration option to the venv to check whether all
dependencies should be treated as locked or not.
"""
parser.add_testenv_attribute(
name="require_locked_deps",
type="bool",
default=False,
help="Require all dependencies in the environment be installed using the Poetry lockfile",
)
@hookimpl
def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction):
"""Install the dependencies for the current environment
Loads the local Poetry environment and the corresponding lockfile then pulls the dependencies
specified by the Tox environment. Finally these dependencies are installed into the Tox
environment using the Poetry ``PipInstaller`` backend.
:param venv: Tox virtual environment object with configuration for the local Tox environment.
:param action: Tox action object
"""
if action.name == venv.envconfig.config.isolated_build_env:
# Skip running the plugin for the packaging environment. PEP-517 front ends can handle
# that better than we can, so let them do their thing. More to the point: if you're having
# problems in the packaging env that this plugin would solve, god help you.
reporter.verbosity1(
f"{_REPORTER_PREFIX} skipping isolated build env '{action.name}'"
)
return
try:
poetry = PoetryFactory().create_poetry(venv.envconfig.config.toxinidir)
except RuntimeError:
# Support running the plugin when the current tox project does not use Poetry for its
# environment/dependency management.
#
# ``RuntimeError`` is dangerous to blindly catch because it can be (and in Poetry's case,
# is) raised in many different places for different purposes.
reporter.verbosity1(
f"{_REPORTER_PREFIX} project does not use Poetry for env management, skipping installation of locked dependencies"
)
return
reporter.verbosity1(
f"{_REPORTER_PREFIX} loaded project pyproject.toml from {poetry.file}"
)
package_map: PackageMap = {
def _find_locked_dependencies(poetry: Poetry, dependency_name: str) -> List[Package]:
packages: Dict[str, Package] = {
package.name: package
for package in poetry.locker.locked_repository(True).packages
}
# Handle the installation of any locked env dependencies from the lockfile
_install_env_dependencies(venv, poetry, package_map)
try:
top_level = packages[dependency_name]
except KeyError:
raise
# Handle the installation of the package dependencies from the lockfile if the package is
# being installed to this venv; otherwise skip installing the package dependencies
if venv.envconfig.skip_install:
reporter.verbosity1(
f"{_REPORTER_PREFIX} env specifies 'skip_install = true', skipping installation of project package"
def find_transients(name: str) -> List[Package]:
transients = [packages[name]]
for dep in packages[name].requires:
transients += find_transients(dep.name)
return transients
return find_transients(top_level.name)
@hookimpl
def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction):
logger = logging.getLogger(__name__)
if action.name == venv.envconfig.config.isolated_build_env:
logger.debug(f"Environment {action.name} is isolated build environment; skipping Poetry-based dependency installation")
return None
poetry = _make_poetry(venv)
logger.debug(f"Loaded project pyproject.toml from {poetry.file}")
dependencies = []
for env_dependency in venv.envconfig.deps:
dependencies += _find_locked_dependencies(poetry, env_dependency.name)
logger.debug(f"Identified {len(dependencies)} dependencies for environment {action.name}")
installer = PipInstaller(
env=VirtualEnv(path=Path(venv.envconfig.envdir)),
io=NullIO(),
pool=poetry.pool
)
return
if venv.envconfig.config.skipsdist:
reporter.verbosity1(
f"{_REPORTER_PREFIX} config specifies 'skipsdist = true', skipping installation of project package"
)
return
for dependency in dependencies:
logger.info(f"Installing environment dependency: {dependency}")
installer.install(dependency)
_install_project_dependencies(venv, poetry, package_map)
return dependencies