From acd5e634ffa62428562c945878dcb67edd6ecf61 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Sep 2015 00:00:06 +0200 Subject: [PATCH] Generic skip decorator for low API version accessible to all tests Add simpler version comparison functions Add decorator to enforce minimum version in API methods Fix utils imports Add minimum_version decorators on API methods that needed it GroupAdd test requires API version >= 1.20 Signed-off-by: Joffrey F --- docker/api/__init__.py | 2 +- docker/api/build.py | 4 +-- docker/api/container.py | 54 +++++++++++++++++--------------------- docker/api/exec_api.py | 18 +++++-------- docker/api/image.py | 14 +++++----- docker/api/volume.py | 24 ++++------------- docker/utils/__init__.py | 5 ++-- docker/utils/decorators.py | 16 +++++++++++ docker/utils/utils.py | 8 ++++++ tests/base.py | 12 +++++++++ tests/integration_test.py | 16 +++++------ tests/test.py | 24 +++++------------ 12 files changed, 98 insertions(+), 99 deletions(-) diff --git a/docker/api/__init__.py b/docker/api/__init__.py index 3fb81203..79796349 100644 --- a/docker/api/__init__.py +++ b/docker/api/__init__.py @@ -4,4 +4,4 @@ from .container import ContainerApiMixin from .daemon import DaemonApiMixin from .exec_api import ExecApiMixin from .image import ImageApiMixin -from .volume import VolumeApiMixin \ No newline at end of file +from .volume import VolumeApiMixin diff --git a/docker/api/build.py b/docker/api/build.py index ce6fd465..b303ba64 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -4,8 +4,8 @@ import re from .. import constants from .. import errors -from ..auth import auth -from ..utils import utils +from .. import auth +from .. import utils log = logging.getLogger(__name__) diff --git a/docker/api/container.py b/docker/api/container.py index f90e8a18..e7ddd733 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -2,11 +2,11 @@ import six import warnings from .. import errors -from ..utils import utils, check_resource +from .. import utils class ContainerApiMixin(object): - @check_resource + @utils.check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): params = { @@ -20,7 +20,7 @@ class ContainerApiMixin(object): return self._get_result(container, stream, response) - @check_resource + @utils.check_resource def attach_socket(self, container, params=None, ws=False): if params is None: params = { @@ -36,7 +36,7 @@ class ContainerApiMixin(object): return self._get_raw_response_socket(self.post( u, None, params=self._attach_params(params), stream=True)) - @check_resource + @utils.check_resource def commit(self, container, repository=None, tag=None, message=None, author=None, conf=None): params = { @@ -73,7 +73,7 @@ class ContainerApiMixin(object): x['Id'] = x['Id'][:12] return res - @check_resource + @utils.check_resource def copy(self, container, resource): res = self._post_json( self._url("/containers/{0}/copy".format(container)), @@ -131,13 +131,13 @@ class ContainerApiMixin(object): kwargs['version'] = self._version return utils.create_host_config(*args, **kwargs) - @check_resource + @utils.check_resource def diff(self, container): return self._result( self._get(self._url("/containers/{0}/changes", container)), True ) - @check_resource + @utils.check_resource def export(self, container): res = self._get( self._url("/containers/{0}/export", container), stream=True @@ -145,13 +145,13 @@ class ContainerApiMixin(object): self._raise_for_status(res) return res.raw - @check_resource + @utils.check_resource def inspect_container(self, container): return self._result( self._get(self._url("/containers/{0}/json", container)), True ) - @check_resource + @utils.check_resource def kill(self, container, signal=None): url = self._url("/containers/{0}/kill", container) params = {} @@ -161,7 +161,7 @@ class ContainerApiMixin(object): self._raise_for_status(res) - @check_resource + @utils.check_resource def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all'): if utils.compare_version('1.11', self._version) >= 0: @@ -185,13 +185,13 @@ class ContainerApiMixin(object): logs=True ) - @check_resource + @utils.check_resource def pause(self, container): url = self._url('/containers/{0}/pause', container) res = self._post(url) self._raise_for_status(res) - @check_resource + @utils.check_resource def port(self, container, private_port): res = self._get(self._url("/containers/{0}/json", container)) self._raise_for_status(res) @@ -211,7 +211,7 @@ class ContainerApiMixin(object): return h_ports - @check_resource + @utils.check_resource def remove_container(self, container, v=False, link=False, force=False): params = {'v': v, 'link': link, 'force': force} res = self._delete( @@ -219,32 +219,29 @@ class ContainerApiMixin(object): ) self._raise_for_status(res) - @check_resource + @utils.minimum_version('1.17') + @utils.check_resource def rename(self, container, name): - if utils.compare_version('1.17', self._version) < 0: - raise errors.InvalidVersion( - 'rename was only introduced in API version 1.17' - ) url = self._url("/containers/{0}/rename", container) params = {'name': name} res = self._post(url, params=params) self._raise_for_status(res) - @check_resource + @utils.check_resource def resize(self, container, height, width): params = {'h': height, 'w': width} url = self._url("/containers/{0}/resize", container) res = self._post(url, params=params) self._raise_for_status(res) - @check_resource + @utils.check_resource def restart(self, container, timeout=10): params = {'t': timeout} url = self._url("/containers/{0}/restart", container) res = self._post(url, params=params) self._raise_for_status(res) - @check_resource + @utils.check_resource def start(self, container, binds=None, port_bindings=None, lxc_conf=None, publish_all_ports=None, links=None, privileged=None, dns=None, dns_search=None, volumes_from=None, network_mode=None, @@ -312,16 +309,13 @@ class ContainerApiMixin(object): res = self._post_json(url, data=start_config) self._raise_for_status(res) - @check_resource + @utils.minimum_version('1.17') + @utils.check_resource def stats(self, container, decode=None): - if utils.compare_version('1.17', self._version) < 0: - raise errors.InvalidVersion( - 'Stats retrieval is not supported in API < 1.17!') - url = self._url("/containers/{0}/stats", container) return self._stream_helper(self._get(url, stream=True), decode=decode) - @check_resource + @utils.check_resource def stop(self, container, timeout=10): params = {'t': timeout} url = self._url("/containers/{0}/stop", container) @@ -330,18 +324,18 @@ class ContainerApiMixin(object): timeout=(timeout + (self.timeout or 0))) self._raise_for_status(res) - @check_resource + @utils.check_resource def top(self, container): u = self._url("/containers/{0}/top", container) return self._result(self._get(u), True) - @check_resource + @utils.check_resource def unpause(self, container): url = self._url('/containers/{0}/unpause', container) res = self._post(url) self._raise_for_status(res) - @check_resource + @utils.check_resource def wait(self, container, timeout=None): url = self._url("/containers/{0}/wait", container) res = self._post(url, timeout=timeout) diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py index 87eb143d..c66b9dd0 100644 --- a/docker/api/exec_api.py +++ b/docker/api/exec_api.py @@ -3,15 +3,14 @@ import shlex import six from .. import errors -from ..utils import utils, check_resource +from .. import utils class ExecApiMixin(object): - @check_resource + @utils.minimum_version('1.15') + @utils.check_resource def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False, privileged=False, user=''): - if utils.compare_version('1.15', self._version) < 0: - raise errors.InvalidVersion('Exec is not supported in API < 1.15') if privileged and utils.compare_version('1.19', self._version) < 0: raise errors.InvalidVersion( 'Privileged exec is not supported in API < 1.19' @@ -38,19 +37,15 @@ class ExecApiMixin(object): res = self._post_json(url, data=data) return self._result(res, True) + @utils.minimum_version('1.16') def exec_inspect(self, exec_id): - if utils.compare_version('1.16', self._version) < 0: - raise errors.InvalidVersion( - 'exec_inspect is not supported in API < 1.16' - ) if isinstance(exec_id, dict): exec_id = exec_id.get('Id') res = self._get(self._url("/exec/{0}/json", exec_id)) return self._result(res, True) + @utils.minimum_version('1.15') def exec_resize(self, exec_id, height=None, width=None): - if utils.compare_version('1.15', self._version) < 0: - raise errors.InvalidVersion('Exec is not supported in API < 1.15') if isinstance(exec_id, dict): exec_id = exec_id.get('Id') @@ -59,9 +54,8 @@ class ExecApiMixin(object): res = self._post(url, params=params) self._raise_for_status(res) + @utils.minimum_version('1.15') def exec_start(self, exec_id, detach=False, tty=False, stream=False): - if utils.compare_version('1.15', self._version) < 0: - raise errors.InvalidVersion('Exec is not supported in API < 1.15') if isinstance(exec_id, dict): exec_id = exec_id.get('Id') diff --git a/docker/api/image.py b/docker/api/image.py index c6939ef4..f891e210 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -4,7 +4,7 @@ import warnings from ..auth import auth from ..constants import INSECURE_REGISTRY_DEPRECATION_WARNING -from ..utils import utils, check_resource +from .. import utils from .. import errors log = logging.getLogger(__name__) @@ -12,13 +12,13 @@ log = logging.getLogger(__name__) class ImageApiMixin(object): - @check_resource + @utils.check_resource def get_image(self, image): res = self._get(self._url("/images/{0}/get", image), stream=True) self._raise_for_status(res) return res.raw - @check_resource + @utils.check_resource def history(self, image): res = self._get(self._url("/images/{0}/history", image)) return self._result(res, True) @@ -124,7 +124,7 @@ class ImageApiMixin(object): return self._result( self._post(u, data=None, params=params)) - @check_resource + @utils.check_resource def insert(self, image, url, path): if utils.compare_version('1.12', self._version) >= 0: raise errors.DeprecatedMethod( @@ -137,7 +137,7 @@ class ImageApiMixin(object): } return self._result(self._post(api_url, params=params)) - @check_resource + @utils.check_resource def inspect_image(self, image): return self._result( self._get(self._url("/images/{0}/json", image)), True @@ -246,7 +246,7 @@ class ImageApiMixin(object): return self._result(response) - @check_resource + @utils.check_resource def remove_image(self, image, force=False, noprune=False): params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/{0}", image), params=params) @@ -258,7 +258,7 @@ class ImageApiMixin(object): True ) - @check_resource + @utils.check_resource def tag(self, image, repository, tag=None, force=False): params = { 'tag': tag, diff --git a/docker/api/volume.py b/docker/api/volume.py index 708cbe52..e9e71273 100644 --- a/docker/api/volume.py +++ b/docker/api/volume.py @@ -1,22 +1,8 @@ -import functools - -from .. import errors -from ..utils import utils - - -def check_api_version(f): - @functools.wraps(f) - def wrapped(self, *args, **kwargs): - if utils.compare_version('1.21', self._version) < 0: - raise errors.InvalidVersion( - 'The volume API is not available for API version < 1.21' - ) - return f(self, *args, **kwargs) - return wrapped +from .. import utils class VolumeApiMixin(object): - @check_api_version + @utils.minimum_version('1.21') def volumes(self, filters=None): params = { 'filter': utils.convert_filters(filters) if filters else None @@ -24,7 +10,7 @@ class VolumeApiMixin(object): url = self._url('/volumes') return self._result(self._get(url, params=params), True) - @check_api_version + @utils.minimum_version('1.21') def create_volume(self, name, driver=None, driver_opts=None): url = self._url('/volumes') if driver_opts is not None and not isinstance(driver_opts, dict): @@ -37,12 +23,12 @@ class VolumeApiMixin(object): } return self._result(self._post_json(url, data=data), True) - @check_api_version + @utils.minimum_version('1.21') def inspect_volume(self, name): url = self._url('/volumes/{0}', name) return self._result(self._get(url), True) - @check_api_version + @utils.minimum_version('1.21') def remove_volume(self, name): url = self._url('/volumes/{0}', name) resp = self._delete(url) diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index deab01df..fd0ef5c0 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -2,8 +2,9 @@ from .utils import ( compare_version, convert_port_bindings, convert_volume_binds, mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host, kwargs_from_env, convert_filters, create_host_config, - create_container_config, parse_bytes, ping_registry, parse_env_file + create_container_config, parse_bytes, ping_registry, parse_env_file, + version_lt, version_gte ) # flake8: noqa from .types import Ulimit, LogConfig # flake8: noqa -from .decorators import check_resource #flake8: noqa +from .decorators import check_resource, minimum_version #flake8: noqa diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index 3c42fe4b..7d3b01a7 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -1,6 +1,7 @@ import functools from .. import errors +from . import utils def check_resource(f): @@ -19,3 +20,18 @@ def check_resource(f): ) return f(self, resource_id, *args, **kwargs) return wrapped + + +def minimum_version(version): + def decorator(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if utils.version_lt(self._version, version): + raise errors.InvalidVersion( + '{0} is not available for version < {1}'.format( + f.__name__, version + ) + ) + return f(self, *args, **kwargs) + return wrapper + return decorator diff --git a/docker/utils/utils.py b/docker/utils/utils.py index a2513419..46b35160 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -164,6 +164,14 @@ def compare_version(v1, v2): return 1 +def version_lt(v1, v2): + return compare_version(v1, v2) > 0 + + +def version_gte(v1, v2): + return not version_lt(v1, v2) + + def ping_registry(url): warnings.warn( 'The `ping_registry` method is deprecated and will be removed.', diff --git a/tests/base.py b/tests/base.py index 1d5a370d..51b23003 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,11 +1,23 @@ import sys import unittest +import pytest import six +import docker + class BaseTestCase(unittest.TestCase): def assertIn(self, object, collection): if six.PY2 and sys.version_info[1] <= 6: return self.assertTrue(object in collection) return super(BaseTestCase, self).assertIn(object, collection) + + +def requires_api_version(version): + return pytest.mark.skipif( + docker.utils.version_lt( + docker.constants.DEFAULT_DOCKER_API_VERSION, version + ), + reason="API version is too low (< {0})".format(version) + ) diff --git a/tests/integration_test.py b/tests/integration_test.py index 8b15b141..fc497975 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -27,17 +27,18 @@ import time import unittest import warnings -import docker -from docker.utils import kwargs_from_env +import pytest import six - from six.moves import BaseHTTPServer from six.moves import socketserver -from .test import Cleanup +import docker from docker.errors import APIError +from docker.utils import kwargs_from_env + +from .base import requires_api_version +from .test import Cleanup -import pytest # FIXME: missing tests for # export; history; insert; port; push; tag; get; load; stats @@ -285,6 +286,7 @@ class TestCreateContainerWithRoBinds(BaseTestCase): self.assertFalse(inspect_data['VolumesRW'][mount_dest]) +@requires_api_version('1.20') class CreateContainerWithGroupAddTest(BaseTestCase): def test_group_id_ints(self): container = self.client.create_container( @@ -1385,9 +1387,7 @@ class TestImportFromURL(ImportTestCase): # VOLUMES TESTS # ################# -@pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION -) < 0, reason="Volume API available for version >=1.21") +@requires_api_version('1.21') class TestVolumes(BaseTestCase): def test_create_volume(self): name = 'perfectcherryblossom' diff --git a/tests/test.py b/tests/test.py index 16ade241..8ed5c14d 100644 --- a/tests/test.py +++ b/tests/test.py @@ -2107,9 +2107,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): # VOLUMES TESTS # ################### - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_list_volumes(self): volumes = self.client.volumes() self.assertIn('Volumes', volumes) @@ -2119,9 +2117,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.assertEqual(args[0][0], 'GET') self.assertEqual(args[0][1], url_prefix + 'volumes') - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_create_volume(self): name = 'perfectcherryblossom' result = self.client.create_volume(name) @@ -2137,9 +2133,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): 'Name': name, 'Driver': None, 'DriverOpts': None }) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_create_volume_with_driver(self): name = 'perfectcherryblossom' driver_name = 'sshfs' @@ -2151,9 +2145,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.assertIn('Driver', args[1]['data']) self.assertEqual(args[1]['data']['Driver'], driver_name) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_create_volume_invalid_opts_type(self): with pytest.raises(TypeError): self.client.create_volume( @@ -2170,9 +2162,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): 'perfectcherryblossom', driver_opts='' ) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_inspect_volume(self): name = 'perfectcherryblossom' result = self.client.inspect_volume(name) @@ -2185,9 +2175,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.assertEqual(args[0][0], 'GET') self.assertEqual(args[0][1], '{0}volumes/{1}'.format(url_prefix, name)) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_remove_volume(self): name = 'perfectcherryblossom' result = self.client.remove_volume(name)