diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .stestr.conf | 4 | ||||
-rw-r--r-- | .testr.conf | 4 | ||||
-rw-r--r-- | .zuul.yaml | 52 | ||||
-rw-r--r-- | README.rst | 3 | ||||
-rw-r--r-- | lower-constraints.txt | 2 | ||||
-rw-r--r-- | swiftclient/client.py | 53 | ||||
-rw-r--r-- | test-requirements.txt | 2 | ||||
-rw-r--r-- | tests/unit/test_shell.py | 63 | ||||
-rw-r--r-- | tests/unit/test_swiftclient.py | 26 | ||||
-rw-r--r-- | tests/unit/utils.py | 8 | ||||
-rw-r--r-- | tox.ini | 36 |
12 files changed, 160 insertions, 95 deletions
@@ -7,6 +7,7 @@ dist/ .DS_Store *.log .testrepository +.stestr/ subunit.log build swiftclient/versioninfo @@ -16,3 +17,4 @@ cover/ coverage.xml doc/build doc/source/api/ +.idea diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..5228f20 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./tests/unit} +top_dir=./ + diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index f3fca90..0000000 --- a/.testr.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./tests/unit} $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list @@ -1,7 +1,59 @@ +- job: + name: swiftclient-swift-functional + parent: swift-dsvm-functional + description: | + Run swift's functional tests with python-swiftclient + installed from source instead as package from PyPI. + # Ensure that we install python-swiftclient from git and + # do not install from pypi. This is needed since the parent + # job sets zuul_work_dir to the swift directory and uses tox + # for installation. + required-projects: + - git.openstack.org/openstack/python-swiftclient + +- job: + name: swiftclient-functional + parent: swift-dsvm-functional + description: | + Run functional tests of python-swiftclient with + python-swiftclient installed from source instead as package from + PyPI. + required-projects: + - git.openstack.org/openstack/python-swiftclient + vars: + # Override value from parent job to use swiftclient tests + zuul_work_dir: "{{ zuul.projects['git.openstack.org/openstack/python-swiftclient'].src_dir }}" + +- job: + name: swiftclient-functional-py2 + parent: swiftclient-functional + description: | + Run functional tests of python-swiftclient under Python 2 + vars: + tox_envlist: py2func + - project: + templates: + - openstack-python-jobs + - openstack-python35-jobs + - openstack-python36-jobs + - publish-openstack-docs-pti + - check-requirements + - openstack-pypy-jobs-nonvoting + - lib-forward-testing + - release-notes-jobs-python3 check: jobs: + - swiftclient-swift-functional + - swiftclient-functional + - swiftclient-functional-py2 - openstack-tox-lower-constraints gate: jobs: + - swiftclient-swift-functional + - swiftclient-functional + - swiftclient-functional-py2 - openstack-tox-lower-constraints + post: + jobs: + - openstack-tox-cover @@ -37,6 +37,7 @@ __ https://github.com/openstack/swift * `Source`_ * `Specs`_ * `How to Contribute`_ +* `Release Notes`_ .. _PyPI: https://pypi.org/project/python-swiftclient .. _Online Documentation: https://docs.openstack.org/python-swiftclient/latest/ @@ -46,7 +47,7 @@ __ https://github.com/openstack/swift .. _Source: https://git.openstack.org/cgit/openstack/python-swiftclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/swift-specs/ - +.. _Release Notes: https://docs.openstack.org/releasenotes/python-swiftclient .. contents:: Contents: :local: diff --git a/lower-constraints.txt b/lower-constraints.txt index 9aae792..fefb90a 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -39,7 +39,7 @@ six==1.9.0 snowballstemmer==1.2.1 sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 -testrepository==0.0.18 +stestr==2.0.0 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 diff --git a/swiftclient/client.py b/swiftclient/client.py index 71801c6..d843aec 100644 --- a/swiftclient/client.py +++ b/swiftclient/client.py @@ -60,6 +60,19 @@ except ImportError: def createLock(self): self.lock = None +ksexceptions = ksclient_v2 = ksclient_v3 = None +try: + from keystoneclient import exceptions as ksexceptions + # prevent keystoneclient warning us that it has no log handlers + logging.getLogger('keystoneclient').addHandler(NullHandler()) + from keystoneclient.v2_0 import client as ksclient_v2 +except ImportError: + pass +try: + from keystoneclient.v3 import client as ksclient_v3 +except ImportError: + pass + # requests version 1.2.3 try to encode headers in ascii, preventing # utf-8 encoded header to be 'prepared' if StrictVersion(requests.__version__) < StrictVersion('2.0.0'): @@ -540,25 +553,6 @@ def get_keystoneclient_2_0(auth_url, user, key, os_options, **kwargs): return get_auth_keystone(auth_url, user, key, os_options, **kwargs) -def _import_keystone_client(auth_version): - # the attempted imports are encapsulated in this function to allow - # mocking for tests - try: - if auth_version in AUTH_VERSIONS_V3: - from keystoneclient.v3 import client as ksclient - else: - from keystoneclient.v2_0 import client as ksclient - from keystoneclient import exceptions - # prevent keystoneclient warning us that it has no log handlers - logging.getLogger('keystoneclient').addHandler(NullHandler()) - return ksclient, exceptions - except ImportError: - raise ClientException(''' -Auth versions 2.0 and 3 require python-keystoneclient, install it or use Auth -version 1.0 which requires ST_AUTH, ST_USER, and ST_KEY environment -variables to be set or overridden with -A, -U, or -K.''') - - def get_auth_keystone(auth_url, user, key, os_options, **kwargs): """ Authenticate against a keystone server. @@ -587,7 +581,20 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs): # Legacy default if not set if auth_version is None: auth_version = '2' - ksclient, exceptions = _import_keystone_client(auth_version) + + ksclient = None + if auth_version in AUTH_VERSIONS_V3: + if ksclient_v3 is not None: + ksclient = ksclient_v3 + else: + if ksclient_v2 is not None: + ksclient = ksclient_v2 + + if ksclient is None: + raise ClientException(''' +Auth versions 2.0 and 3 require python-keystoneclient, install it or use Auth +version 1.0 which requires ST_AUTH, ST_USER, and ST_KEY environment +variables to be set or overridden with -A, -U, or -K.''') try: _ksclient = ksclient.Client( @@ -608,13 +615,13 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs): cert=kwargs.get('cert'), key=kwargs.get('cert_key'), auth_url=auth_url, insecure=insecure, timeout=timeout) - except exceptions.Unauthorized: + except ksexceptions.Unauthorized: msg = 'Unauthorized. Check username, password and tenant name/id.' if auth_version in AUTH_VERSIONS_V3: msg = ('Unauthorized. Check username/id, password, ' 'tenant name/id and user/tenant domain name/id.') raise ClientException(msg) - except exceptions.AuthorizationFailure as err: + except ksexceptions.AuthorizationFailure as err: raise ClientException('Authorization Failure. %s' % err) service_type = os_options.get('service_type') or 'object-store' endpoint_type = os_options.get('endpoint_type') or 'publicURL' @@ -627,7 +634,7 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs): service_type=service_type, endpoint_type=endpoint_type, **filter_kwargs) - except exceptions.EndpointNotFound: + except ksexceptions.EndpointNotFound: raise ClientException('Endpoint for %s not found - ' 'have you specified a region?' % service_type) return endpoint, _ksclient.auth_token diff --git a/test-requirements.txt b/test-requirements.txt index 634851e..9e6b84a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,6 @@ keystoneauth1>=3.4.0 # Apache-2.0 mock>=1.2.0 # BSD oslosphinx>=4.7.0 # Apache-2.0 sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD -testrepository>=0.0.18 +stestr>=2.0.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 openstackdocstheme>=1.18.1 # Apache-2.0 diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index 91496b8..9ef4685 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -37,7 +37,7 @@ import swiftclient.utils from os.path import basename, dirname from .utils import ( - CaptureOutput, fake_get_auth_keystone, _make_fake_import_keystone_client, + CaptureOutput, fake_get_auth_keystone, FakeKeystone, StubResponse, MockHttpTest) from swiftclient.utils import ( EMPTY_ETAG, EXPIRES_ISO8601_FORMAT, @@ -2534,7 +2534,17 @@ class TestKeystoneOptions(MockHttpTest): cmd_args=cmd_args) ks_endpoint = 'http://example.com:8080/v1/AUTH_acc' ks_token = 'fake_auth_token' + # check correct auth version gets used + key = 'auth-version' fake_ks = FakeKeystone(endpoint=ks_endpoint, token=ks_token) + if no_auth: + fake_ks2 = fake_ks3 = None + elif opts.get(key, self.defaults.get(key)) == '2.0': + fake_ks2 = fake_ks + fake_ks3 = None + else: + fake_ks2 = None + fake_ks3 = fake_ks # fake_conn will check that storage_url and auth_token are as expected endpoint = os_opts.get('storage-url', ks_endpoint) token = os_opts.get('auth-token', ks_token) @@ -2542,8 +2552,8 @@ class TestKeystoneOptions(MockHttpTest): storage_url=endpoint, auth_token=token) - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)), \ + with mock.patch('swiftclient.client.ksclient_v2', fake_ks2), \ + mock.patch('swiftclient.client.ksclient_v3', fake_ks3), \ mock.patch('swiftclient.client.http_connection', fake_conn), \ mock.patch.dict(os.environ, env, clear=True), \ patch_disable_warnings() as mock_disable_warnings: @@ -2562,16 +2572,11 @@ class TestKeystoneOptions(MockHttpTest): self.assertEqual([], mock_disable_warnings.mock_calls) if no_auth: - # check that keystone client was not used and terminate tests - self.assertIsNone(getattr(fake_ks, 'auth_version')) - self.assertEqual(len(fake_ks.calls), 0) + # We patched out both keystoneclient versions to be None; + # they *can't* have been used and if we tried to, we would + # have raised ClientExceptions return - # check correct auth version was passed to _import_keystone_client - key = 'auth-version' - expected = opts.get(key, self.defaults.get(key)) - self.assertEqual(expected, fake_ks.auth_version) - # check args passed to keystone Client __init__ self.assertEqual(len(fake_ks.calls), 1) actual_args = fake_ks.calls[0] @@ -2942,9 +2947,9 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): self.account = 'AUTH_alice' # keystone returns endpoint for another account - fake_ks = FakeKeystone(endpoint='http://example.com:8080/v1/AUTH_bob', - token='bob_token') - self.fake_ks_import = _make_fake_import_keystone_client(fake_ks) + self.fake_ks = FakeKeystone( + endpoint='http://example.com:8080/v1/AUTH_bob', + token='bob_token') self.cont = 'c1' self.cont_path = '/v1/%s/%s' % (self.account, self.cont) @@ -3023,8 +3028,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj, '--leave-segments']) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3046,8 +3050,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): on_request=req_handler) args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj, '--leave-segments']) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3073,8 +3076,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): '--segment-size=10', '--segment-container=%s' % self.cont]) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3112,8 +3114,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): cmd_args=[self.cont, self.obj, '--leave-segments', '--segment-size=10']) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3149,8 +3150,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj, '--leave-segments']) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3207,8 +3207,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): args, env = self._make_cmd('download', cmd_args=[self.cont, self.obj.lstrip('/'), '--no-download']) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3229,8 +3228,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): args, env = self._make_cmd('download', cmd_args=[self.cont, self.obj.lstrip('/'), '--no-download']) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3248,8 +3246,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): args, env = self._make_cmd('download', cmd_args=[self.cont, self.obj.lstrip('/'), '--no-download']) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3273,8 +3270,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): fake_conn = self.fake_http_connection(resp, on_request=req_handler) args, env = self._make_cmd('download', cmd_args=[self.cont]) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: @@ -3291,8 +3287,7 @@ class TestCrossAccountObjectAccess(TestBase, MockHttpTest): fake_conn = self.fake_http_connection(403) args, env = self._make_cmd('download', cmd_args=[self.cont]) - with mock.patch('swiftclient.client._import_keystone_client', - self.fake_ks_import): + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): with mock.patch('swiftclient.client.http_connection', fake_conn): with mock.patch.dict(os.environ, env): with CaptureOutput() as out: diff --git a/tests/unit/test_swiftclient.py b/tests/unit/test_swiftclient.py index 3303372..f114774 100644 --- a/tests/unit/test_swiftclient.py +++ b/tests/unit/test_swiftclient.py @@ -29,7 +29,7 @@ from six.moves.urllib.parse import urlparse from requests.exceptions import RequestException from .utils import (MockHttpTest, fake_get_auth_keystone, StubResponse, - FakeKeystone, _make_fake_import_keystone_client) + FakeKeystone) from swiftclient.utils import EMPTY_ETAG from swiftclient.exceptions import ClientException @@ -322,8 +322,7 @@ class TestGetAuth(MockHttpTest): # TestConnection.test_timeout_passed_down but is required to check that # get_auth does the right thing when it is not passed a timeout arg fake_ks = FakeKeystone(endpoint='http://some_url', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v2', fake_ks): c.get_auth('http://www.test.com', 'asdf', 'asdf', os_options=dict(tenant_name='tenant'), auth_version="2.0", timeout=42.0) @@ -580,8 +579,7 @@ class TestGetAuth(MockHttpTest): def test_get_auth_keystone_versionless(self): fake_ks = FakeKeystone(endpoint='http://some_url', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v3', fake_ks): c.get_auth_keystone('http://authurl', 'user', 'key', {}) self.assertEqual(1, len(fake_ks.calls)) self.assertEqual('http://authurl/v3', fake_ks.calls[0].get('auth_url')) @@ -589,8 +587,7 @@ class TestGetAuth(MockHttpTest): def test_get_auth_keystone_versionless_auth_version_set(self): fake_ks = FakeKeystone(endpoint='http://some_url', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v2', fake_ks): c.get_auth_keystone('http://auth_url', 'user', 'key', {}, auth_version='2.0') self.assertEqual(1, len(fake_ks.calls)) @@ -600,8 +597,7 @@ class TestGetAuth(MockHttpTest): def test_get_auth_keystone_versionful(self): fake_ks = FakeKeystone(endpoint='http://some_url', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v3', fake_ks): c.get_auth_keystone('http://auth_url/v3', 'user', 'key', {}, auth_version='3') self.assertEqual(1, len(fake_ks.calls)) @@ -611,8 +607,7 @@ class TestGetAuth(MockHttpTest): def test_get_auth_keystone_devstack_versionful(self): fake_ks = FakeKeystone( endpoint='http://storage.example.com/v1/AUTH_user', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v3', fake_ks): c.get_auth_keystone('https://192.168.8.8/identity/v3', 'user', 'key', {}, auth_version='3') self.assertEqual(1, len(fake_ks.calls)) @@ -622,8 +617,7 @@ class TestGetAuth(MockHttpTest): def test_get_auth_keystone_devstack_versionless(self): fake_ks = FakeKeystone( endpoint='http://storage.example.com/v1/AUTH_user', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v3', fake_ks): c.get_auth_keystone('https://192.168.8.8/identity', 'user', 'key', {}, auth_version='3') self.assertEqual(1, len(fake_ks.calls)) @@ -634,8 +628,7 @@ class TestGetAuth(MockHttpTest): fake_ks = FakeKeystone( endpoint='http://storage.example.com/v1/AUTH_user', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v3', fake_ks): c.get_auth_keystone('http://blah.example.com/v2moo', 'user', 'key', {}, auth_version='3') self.assertEqual(1, len(fake_ks.calls)) @@ -2456,8 +2449,7 @@ class TestConnection(MockHttpTest): 'http://auth.example.com', 'user', 'password', timeout=33.0, os_options=os_options, auth_version=2.0) fake_ks = FakeKeystone(endpoint='http://some_url', token='secret') - with mock.patch('swiftclient.client._import_keystone_client', - _make_fake_import_keystone_client(fake_ks)): + with mock.patch('swiftclient.client.ksclient_v2', fake_ks): with mock.patch.multiple('swiftclient.client', http_connection=shim_connection, sleep=mock.DEFAULT): diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 2def73f..aab3b59 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -542,14 +542,6 @@ class FakeKeystone(object): pass -def _make_fake_import_keystone_client(fake_import): - def _fake_import_keystone_client(auth_version): - fake_import.auth_version = auth_version - return fake_import, fake_import - - return _fake_import_keystone_client - - class FakeStream(object): def __init__(self, size): self.bytes_read = 0 @@ -16,33 +16,55 @@ deps = -r{toxinidir}/requirements.txt .[keystone] commands = sh -c '(find . -not \( -type d -name .?\* -prune \) \ \( -type d -name "__pycache__" -or -type f -name "*.py[co]" \) \ - -print0; find . -name "*.dbm*" -print0) | xargs -0 rm -rf' - python setup.py testr --testr-args="{posargs}" + -print0) | xargs -0 rm -rf' + stestr run {posargs} whitelist_externals = sh passenv = SWIFT_* *_proxy [testenv:pep8] +basepython = python3 commands = python -m flake8 swiftclient tests [testenv:venv] +basepython = python3 commands = {posargs} [testenv:cover] -commands = python setup.py testr --coverage - coverage report +basepython = python3 +setenv = + PYTHON=coverage run --source swiftclient --parallel-mode +commands = + stestr run + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report [testenv:func] -setenv = OS_TEST_PATH=tests.functional +basepython = python3 +setenv = + OS_TEST_PATH=tests.functional + PYTHON=coverage run --source swiftclient --parallel-mode whitelist_externals = coverage rm commands = - python setup.py testr --coverage --testr-args="--concurrency=1" + stestr run --concurrency=1 + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml coverage report -m rm -f .coverage +[testenv:py2func] +basepython=python2 +setenv = {[testenv:func]setenv} +whitelist_externals = {[testenv:func]whitelist_externals} +commands = {[testenv:func]commands} + [testenv:docs] +basepython = python3 commands= python setup.py build_sphinx @@ -60,6 +82,7 @@ show-source = True exclude = .venv,.tox,dist,doc,*egg [testenv:bindep] +basepython = python3 # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed @@ -69,6 +92,7 @@ deps = bindep commands = bindep test [testenv:releasenotes] +basepython = python3 commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:lower-constraints] |