Compare commits

..

9 Commits

Author SHA1 Message Date
Brad Warren
b24d932e9b Revert "stop nginx"
This reverts commit 0dc3f78339.
2018-07-05 18:19:29 -07:00
Brad Warren
0dc3f78339 stop nginx 2018-07-05 18:15:36 -07:00
Brad Warren
44785ed954 install apache2 2018-07-05 18:15:03 -07:00
Brad Warren
cc5a1c077a Revert "We don't need to run dpkg -s in before_install."
This reverts commit e5d35099a7.
2018-07-05 18:05:54 -07:00
Brad Warren
ba8d12952e quoet 2018-07-05 17:54:06 -07:00
Brad Warren
29a75eb8a7 Upgrade Python 3.6 tests to 3.7.
Let's continue the approach of testing on the oldest and newest versions of Python 3. We will continue testing on Python 3.6 in the nightly tests.
2018-07-05 17:52:34 -07:00
Brad Warren
309a70c3fe Remove augeas sources.
We only needed it for Ubuntu Precise which is dead and it doesn't work in Ubuntu Xenial.
2018-07-05 17:51:23 -07:00
Brad Warren
e5d35099a7 We don't need to run dpkg -s in before_install. 2018-07-05 17:51:05 -07:00
Brad Warren
9fade9c85c Remove apacheconftest packages.
The apacheconftests handle installing Apache dependencies, so let's remove it from the general case.
2018-07-05 17:48:19 -07:00
32 changed files with 52 additions and 167 deletions

View File

@@ -99,10 +99,3 @@ after_success: '[ "$TOXENV" == "cover" ] && coveralls'
notifications:
email: false
irc:
channels:
- secure: "SGWZl3ownKx9xKVV2VnGt7DqkTmutJ89oJV9tjKhSs84kLijU6EYdPnllqISpfHMTxXflNZuxtGo0wTDYHXBuZL47w1O32W6nzuXdra5zC+i4sYQwYULUsyfOv9gJX8zWAULiK0Z3r0oho45U+FR5ZN6TPCidi8/eGU+EEPwaAw="
on_cancel: never
on_success: never
on_failure: always
use_notice: true

View File

@@ -68,7 +68,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],

View File

@@ -165,7 +165,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
# These will be set in the prepare function
self._prepared = False
self.parser = None
self.version = version
self.vhosts = None
@@ -250,7 +249,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
logger.debug("Encountered error:", exc_info=True)
raise errors.PluginError(
"Unable to lock %s", self.conf("server-root"))
self._prepared = True
def _check_aug_version(self):
""" Checks that we have recent enough version of libaugeas.
@@ -2396,9 +2394,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
continue
nextstep = config["laststep"] + 1
if nextstep < len(constants.AUTOHSTS_STEPS):
# If installer hasn't been prepared yet, do it now
if not self._prepared:
self.prepare()
# Have not reached the max value yet
try:
vhost = self.find_vhost_by_id(id_str)

View File

@@ -55,9 +55,7 @@ class AutoHSTSTest(util.ApacheTest):
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.prepare")
def test_autohsts_increase(self, mock_prepare, _mock_restart):
self.config._prepared = False
def test_autohsts_increase(self, _mock_restart):
maxage = "\"max-age={0}\""
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])
@@ -71,7 +69,6 @@ class AutoHSTSTest(util.ApacheTest):
# Verify increased value
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
inc_val)
self.assertTrue(mock_prepare.called)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator._autohsts_increase")

View File

@@ -43,7 +43,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -46,7 +46,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],

View File

@@ -42,7 +42,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -42,7 +42,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -43,7 +43,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -42,7 +42,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -42,7 +42,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -47,7 +47,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -42,7 +42,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -42,7 +42,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -42,7 +42,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -36,7 +36,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -43,7 +43,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -40,7 +40,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Communications :: Email :: Mail Transport Agents',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -620,9 +620,6 @@ class GenericUpdater(object):
methods, and interfaces.GenericUpdater.register(InstallerClass) should
be called from the installer code.
The plugins implementing this enhancement are responsible of handling
the saving of configuration checkpoints as well as other calls to
interface methods of `interfaces.IInstaller` such as prepare() and restart()
"""
@abc.abstractmethod

View File

@@ -1199,11 +1199,11 @@ def renew_cert(config, plugins, lineage):
# In case of a renewal, reload server to pick up new certificate.
# In principle we could have a configuration option to inhibit this
# from happening.
# Run deployer
updater.run_renewal_deployer(config, renewed_lineage, installer)
installer.restart()
notify("new certificate deployed with reload of {0} server; fullchain is {1}".format(
config.installer, lineage.fullchain), pause=False)
# Run deployer
def certonly(config, plugins):
"""Authenticate & obtain cert, but do not install it.

View File

@@ -88,8 +88,7 @@ class AutoHSTSEnhancement(object):
The plugins implementing new style enhancements are responsible of handling
the saving of configuration checkpoints as well as calling possible restarts
of managed software themselves. For update_autohsts method, the installer may
have to call prepare() to finalize the plugin initialization.
of managed software themselves.
Methods:
enable_autohsts is called when the header is initially installed using a
@@ -113,10 +112,6 @@ class AutoHSTSEnhancement(object):
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
.. note:: prepare() method inherited from `interfaces.IPlugin` might need
to be called manually within implementation of this interface method
to finalize the plugin initialization.
"""
@abc.abstractmethod

View File

@@ -39,35 +39,6 @@ def pick_authenticator(
return pick_plugin(
config, default, plugins, question, (interfaces.IAuthenticator,))
def get_unprepared_installer(config, plugins):
"""
Get an unprepared interfaces.IInstaller object.
:param certbot.interfaces.IConfig config: Configuration
:param certbot.plugins.disco.PluginsRegistry plugins:
All plugins registered as entry points.
:returns: Unprepared installer plugin or None
:rtype: IPlugin or None
"""
_, req_inst = cli_plugin_requests(config)
if not req_inst:
return None
installers = plugins.filter(lambda p_ep: p_ep.name == req_inst)
installers.init(config)
installers = installers.verify((interfaces.IInstaller,))
if len(installers) > 1:
raise errors.PluginSelectionError(
"Found multiple installers with the name %s, Certbot is unable to "
"determine which one to use. Skipping." % req_inst)
if installers:
inst = list(installers.values())[0]
logger.debug("Selecting plugin: %s", inst)
return inst.init(config)
else:
raise errors.PluginSelectionError(
"Could not select or initialize the requested installer %s." % req_inst)
def pick_plugin(config, default, plugins, question, ifaces):
"""Pick plugin.
@@ -263,10 +234,6 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches
:rtype: tuple
"""
req_inst = req_auth = config.configurator
if config.installer == 'None':
config.installer = None
if config.authenticator == 'None':
config.authenticator = None
req_inst = set_configurator(req_inst, config.installer)
req_auth = set_configurator(req_auth, config.authenticator)

View File

@@ -6,13 +6,10 @@ import unittest
import mock
import zope.component
from certbot import errors
from certbot import interfaces
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from certbot.display import util as display_util
from certbot.plugins.disco import PluginsRegistry
from certbot.tests import util as test_util
from certbot import interfaces
class ConveniencePickPluginTest(unittest.TestCase):
@@ -173,56 +170,5 @@ class ChoosePluginTest(unittest.TestCase):
self.assertTrue("default" in mock_util().menu.call_args[1])
class GetUnpreparedInstallerTest(test_util.ConfigTestCase):
"""Tests for certbot.plugins.selection.get_unprepared_installer."""
def setUp(self):
super(GetUnpreparedInstallerTest, self).setUp()
self.mock_apache_fail_ep = mock.Mock(
description_with_name="afail")
self.mock_apache_fail_ep.name = "afail"
self.mock_apache_ep = mock.Mock(
description_with_name="apache")
self.mock_apache_ep.name = "apache"
self.mock_apache_plugin = mock.MagicMock()
self.mock_apache_ep.init.return_value = self.mock_apache_plugin
self.plugins = PluginsRegistry({
"afail": self.mock_apache_fail_ep,
"apache": self.mock_apache_ep,
})
def _call(self):
from certbot.plugins.selection import get_unprepared_installer
return get_unprepared_installer(self.config, self.plugins)
def test_no_installer_defined(self):
self.config.configurator = None
self.assertEquals(self._call(), None)
def test_no_available_installers(self):
self.config.configurator = "apache"
self.plugins = PluginsRegistry({})
self.assertRaises(errors.PluginSelectionError, self._call)
def test_get_plugin(self):
self.config.configurator = "apache"
installer = self._call()
self.assertTrue(installer is self.mock_apache_plugin)
def test_multiple_installers_returned(self):
self.config.configurator = "apache"
# Two plugins with the same name
self.mock_apache_fail_ep.name = "apache"
self.assertRaises(errors.PluginSelectionError, self._call)
def test_return_early_if_none(self):
self.config.installer = 'None'
# Make sure that the function returns early. PluginsRegistry.filter is
# called right after we should return.
with mock.patch('certbot.plugins.disco.PluginsRegistry.filter') as mock_f:
self._call()
self.assertFalse(mock_f.called)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -1493,17 +1493,17 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
self.assertTrue(mock_handle.called)
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
@mock.patch('certbot.updater._run_updaters')
def test_plugin_selection_error(self, mock_run, mock_choose):
def test_plugin_selection_error(self, mock_choose):
mock_choose.side_effect = errors.PluginSelectionError
self.assertRaises(errors.PluginSelectionError, main.renew_cert,
None, None, None)
self.config.dry_run = False
updater.run_generic_updaters(self.config, None, None)
# Make sure we're returning None, and hence not trying to run the
# without installer
self.assertFalse(mock_run.called)
with mock.patch('certbot.updater.logger.warning') as mock_log:
self.config.dry_run = False
updater.run_generic_updaters(self.config, None, None)
self.assertTrue(mock_log.called)
self.assertTrue("Could not choose appropriate plugin for updaters"
in mock_log.call_args[0][0])
class UnregisterTest(unittest.TestCase):

View File

@@ -23,15 +23,13 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
@mock.patch('certbot.main._get_and_save_cert')
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
@mock.patch('certbot.plugins.selection.get_unprepared_installer')
@test_util.patch_get_utility()
def test_server_updates(self, _, mock_geti, mock_select, mock_getsave):
def test_server_updates(self, _, mock_select, mock_getsave):
mock_getsave.return_value = mock.MagicMock()
mock_generic_updater = self.generic_updater
# Generic Updater
mock_select.return_value = (mock_generic_updater, None)
mock_geti.return_value = mock_generic_updater
with mock.patch('certbot.main._init_le_client'):
main.renew_cert(self.config, None, mock.MagicMock())
self.assertTrue(mock_generic_updater.restart.called)
@@ -64,9 +62,9 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
self.assertEquals(mock_log.call_args[0][0],
"Skipping renewal deployer in dry-run mode.")
@mock.patch('certbot.plugins.selection.get_unprepared_installer')
def test_enhancement_updates(self, mock_geti):
mock_geti.return_value = self.mockinstaller
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
def test_enhancement_updates(self, mock_select):
mock_select.return_value = (self.mockinstaller, None)
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
self.assertTrue(self.mockinstaller.update_autohsts.called)
self.assertEqual(self.mockinstaller.update_autohsts.call_count, 1)
@@ -76,10 +74,10 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
self.mockinstaller)
self.assertTrue(self.mockinstaller.deploy_autohsts.called)
@mock.patch('certbot.plugins.selection.get_unprepared_installer')
def test_enhancement_updates_not_called(self, mock_geti):
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
def test_enhancement_updates_not_called(self, mock_select):
self.config.disable_renew_updates = True
mock_geti.return_value = self.mockinstaller
mock_select.return_value = (self.mockinstaller, None)
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
self.assertFalse(self.mockinstaller.update_autohsts.called)
@@ -89,8 +87,8 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
self.mockinstaller)
self.assertFalse(self.mockinstaller.deploy_autohsts.called)
@mock.patch('certbot.plugins.selection.get_unprepared_installer')
def test_enhancement_no_updater(self, mock_geti):
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
def test_enhancement_no_updater(self, mock_select):
FAKEINDEX = [
{
"name": "Test",
@@ -100,7 +98,7 @@ class RenewUpdaterTest(test_util.ConfigTestCase):
"enable_function": "enable_autohsts"
}
]
mock_geti.return_value = self.mockinstaller
mock_select.return_value = (self.mockinstaller, None)
with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX):
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
self.assertFalse(self.mockinstaller.update_autohsts.called)

View File

@@ -28,13 +28,13 @@ def run_generic_updaters(config, lineage, plugins):
logger.debug("Skipping updaters in dry-run mode.")
return
try:
installer = plug_sel.get_unprepared_installer(config, plugins)
except errors.Error as e:
# installers are used in auth mode to determine domain names
installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
except errors.PluginSelectionError as e:
logger.warning("Could not choose appropriate plugin for updaters: %s", e)
return
if installer:
_run_updaters(lineage, installer, config)
_run_enhancement_updaters(lineage, installer, config)
_run_updaters(lineage, installer, config)
_run_enhancement_updaters(lineage, installer, config)
def run_renewal_deployer(config, lineage, installer):
"""Helper function to run deployer interface method if supported by the used

View File

@@ -1092,9 +1092,9 @@ idna==2.5 \
ipaddress==1.0.16 \
--hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \
--hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0
josepy==1.1.0 \
--hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \
--hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086
josepy==1.0.1 \
--hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \
--hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c

View File

@@ -96,9 +96,9 @@ idna==2.5 \
ipaddress==1.0.16 \
--hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \
--hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0
josepy==1.1.0 \
--hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \
--hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086
josepy==1.0.1 \
--hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \
--hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c

View File

@@ -35,7 +35,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -99,7 +99,6 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',

View File

@@ -26,7 +26,7 @@ ipython==5.5.0
ipython-genutils==0.2.0
Jinja2==2.9.6
jmespath==0.9.3
josepy==1.1.0
josepy==1.0.1
logger==1.4
logilab-common==1.4.1
MarkupSafe==1.0
@@ -51,7 +51,7 @@ pytest-forked==0.2
pytest-xdist==1.20.1
python-dateutil==2.6.1
python-digitalocean==1.11
PyYAML==3.13
PyYAML==3.12
repoze.sphinx.autointerface==0.8
requests-file==1.4.2
requests-toolbelt==0.8.0

22
tox.ini
View File

@@ -14,8 +14,7 @@ pip_install = {toxinidir}/tools/pip_install_editable.sh
# before the script moves on to the next package. All dependencies are pinned
# to a specific version for increased stability for developers.
install_and_test = {toxinidir}/tools/install_and_test.sh
dns_packages =
certbot-dns-cloudflare \
python37_compatible_dns_packages =
certbot-dns-cloudxns \
certbot-dns-digitalocean \
certbot-dns-dnsimple \
@@ -25,14 +24,22 @@ dns_packages =
certbot-dns-nsone \
certbot-dns-rfc2136 \
certbot-dns-route53
all_packages =
dns_packages =
certbot-dns-cloudflare \
{[base]python37_compatible_dns_packages}
nondns_packages =
acme[dev] \
.[dev] \
certbot-apache \
{[base]dns_packages} \
certbot-nginx \
certbot-postfix \
letshelp-certbot
python37_compatible_packages =
{[base]nondns_packages} \
{[base]python37_compatible_dns_packages}
all_packages =
{[base]nondns_packages} \
{[base]dns_packages}
install_packages =
{toxinidir}/tools/pip_install_editable.sh {[base]all_packages}
source_paths =
@@ -62,6 +69,13 @@ commands =
setenv =
PYTHONHASHSEED = 0
[testenv:py37]
commands =
{[base]install_and_test} {[base]python37_compatible_packages}
python tests/lock_test.py
setenv =
{[testenv]setenv}
[testenv:py27-oldest]
commands =
{[testenv]commands}