Browse Source

Merge pull request #2783 from aanand/fix-validation-v2

Fix validation and version checking
Aanand Prasad 10 years ago
parent
commit
60a5b39f6f

+ 39 - 19
compose/config/config.py

@@ -14,10 +14,12 @@ import six
 import yaml
 import yaml
 from cached_property import cached_property
 from cached_property import cached_property
 
 
-from ..const import COMPOSEFILE_VERSIONS
+from ..const import COMPOSEFILE_V1 as V1
+from ..const import COMPOSEFILE_V2_0 as V2_0
 from .errors import CircularReference
 from .errors import CircularReference
 from .errors import ComposeFileNotFound
 from .errors import ComposeFileNotFound
 from .errors import ConfigurationError
 from .errors import ConfigurationError
+from .errors import VERSION_EXPLANATION
 from .interpolation import interpolate_environment_variables
 from .interpolation import interpolate_environment_variables
 from .sort_services import get_container_name_from_network_mode
 from .sort_services import get_container_name_from_network_mode
 from .sort_services import get_service_name_from_network_mode
 from .sort_services import get_service_name_from_network_mode
@@ -103,6 +105,7 @@ SUPPORTED_FILENAMES = [
 
 
 DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml'
 DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml'
 
 
+
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 
 
 
 
@@ -129,27 +132,48 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
 
 
     @cached_property
     @cached_property
     def version(self):
     def version(self):
-        if self.config is None:
-            return 1
-        version = self.config.get('version', 1)
+        if 'version' not in self.config:
+            return V1
+
+        version = self.config['version']
+
         if isinstance(version, dict):
         if isinstance(version, dict):
-            log.warn("Unexpected type for field 'version', in file {} assuming "
-                     "version is the name of a service, and defaulting to "
-                     "Compose file version 1".format(self.filename))
-            return 1
+            log.warn('Unexpected type for "version" key in "{}". Assuming '
+                     '"version" is the name of a service, and defaulting to '
+                     'Compose file version 1.'.format(self.filename))
+            return V1
+
+        if not isinstance(version, six.string_types):
+            raise ConfigurationError(
+                'Version in "{}" is invalid - it should be a string.'
+                .format(self.filename))
+
+        if version == '1':
+            raise ConfigurationError(
+                'Version in "{}" is invalid. {}'
+                .format(self.filename, VERSION_EXPLANATION))
+
+        if version == '2':
+            version = V2_0
+
+        if version != V2_0:
+            raise ConfigurationError(
+                'Version in "{}" is unsupported. {}'
+                .format(self.filename, VERSION_EXPLANATION))
+
         return version
         return version
 
 
     def get_service(self, name):
     def get_service(self, name):
         return self.get_service_dicts()[name]
         return self.get_service_dicts()[name]
 
 
     def get_service_dicts(self):
     def get_service_dicts(self):
-        return self.config if self.version == 1 else self.config.get('services', {})
+        return self.config if self.version == V1 else self.config.get('services', {})
 
 
     def get_volumes(self):
     def get_volumes(self):
-        return {} if self.version == 1 else self.config.get('volumes', {})
+        return {} if self.version == V1 else self.config.get('volumes', {})
 
 
     def get_networks(self):
     def get_networks(self):
-        return {} if self.version == 1 else self.config.get('networks', {})
+        return {} if self.version == V1 else self.config.get('networks', {})
 
 
 
 
 class Config(namedtuple('_Config', 'version services volumes networks')):
 class Config(namedtuple('_Config', 'version services volumes networks')):
@@ -211,10 +235,6 @@ def validate_config_version(config_files):
                     next_file.filename,
                     next_file.filename,
                     next_file.version))
                     next_file.version))
 
 
-    if main_file.version not in COMPOSEFILE_VERSIONS:
-        raise ConfigurationError(
-            'Invalid Compose file version: {0}'.format(main_file.version))
-
 
 
 def get_default_config_files(base_dir):
 def get_default_config_files(base_dir):
     (candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir)
     (candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir)
@@ -278,7 +298,7 @@ def load(config_details):
         main_file,
         main_file,
         [file.get_service_dicts() for file in config_details.config_files])
         [file.get_service_dicts() for file in config_details.config_files])
 
 
-    if main_file.version >= 2:
+    if main_file.version != V1:
         for service_dict in service_dicts:
         for service_dict in service_dicts:
             match_named_volumes(service_dict, volumes)
             match_named_volumes(service_dict, volumes)
 
 
@@ -363,7 +383,7 @@ def process_config_file(config_file, service_name=None):
 
 
     interpolated_config = interpolate_environment_variables(service_dicts, 'service')
     interpolated_config = interpolate_environment_variables(service_dicts, 'service')
 
 
-    if config_file.version == 2:
+    if config_file.version == V2_0:
         processed_config = dict(config_file.config)
         processed_config = dict(config_file.config)
         processed_config['services'] = services = interpolated_config
         processed_config['services'] = services = interpolated_config
         processed_config['volumes'] = interpolate_environment_variables(
         processed_config['volumes'] = interpolate_environment_variables(
@@ -371,7 +391,7 @@ def process_config_file(config_file, service_name=None):
         processed_config['networks'] = interpolate_environment_variables(
         processed_config['networks'] = interpolate_environment_variables(
             config_file.get_networks(), 'network')
             config_file.get_networks(), 'network')
 
 
-    if config_file.version == 1:
+    if config_file.version == V1:
         processed_config = services = interpolated_config
         processed_config = services = interpolated_config
 
 
     config_file = config_file._replace(config=processed_config)
     config_file = config_file._replace(config=processed_config)
@@ -655,7 +675,7 @@ def merge_service_dicts(base, override, version):
         if field in base or field in override:
         if field in base or field in override:
             d[field] = override.get(field, base.get(field))
             d[field] = override.get(field, base.get(field))
 
 
-    if version == 1:
+    if version == V1:
         legacy_v1_merge_image_or_build(d, base, override)
         legacy_v1_merge_image_or_build(d, base, override)
     else:
     else:
         merge_build(d, base, override)
         merge_build(d, base, override)

+ 8 - 0
compose/config/errors.py

@@ -2,6 +2,14 @@ from __future__ import absolute_import
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 
 
+VERSION_EXPLANATION = (
+    'Either specify a version of "2" (or "2.0") and place your service '
+    'definitions under the `services` key, or omit the `version` key and place '
+    'your service definitions at the root of the file to use version 1.\n'
+    'For more on the Compose file format versions, see '
+    'https://docs.docker.com/compose/compose-file/')
+
+
 class ConfigurationError(Exception):
 class ConfigurationError(Exception):
     def __init__(self, msg):
     def __init__(self, msg):
         self.msg = msg
         self.msg = msg

+ 3 - 3
compose/config/fields_schema_v2.json → compose/config/fields_schema_v2.0.json

@@ -1,18 +1,18 @@
 {
 {
   "$schema": "http://json-schema.org/draft-04/schema#",
   "$schema": "http://json-schema.org/draft-04/schema#",
   "type": "object",
   "type": "object",
-  "id": "fields_schema_v2.json",
+  "id": "fields_schema_v2.0.json",
 
 
   "properties": {
   "properties": {
     "version": {
     "version": {
-      "enum": [2]
+      "type": "string"
     },
     },
     "services": {
     "services": {
       "id": "#/properties/services",
       "id": "#/properties/services",
       "type": "object",
       "type": "object",
       "patternProperties": {
       "patternProperties": {
         "^[a-zA-Z0-9._-]+$": {
         "^[a-zA-Z0-9._-]+$": {
-          "$ref": "service_schema_v2.json#/definitions/service"
+          "$ref": "service_schema_v2.0.json#/definitions/service"
         }
         }
       },
       },
       "additionalProperties": false
       "additionalProperties": false

+ 1 - 1
compose/config/service_schema_v2.json → compose/config/service_schema_v2.0.json

@@ -1,6 +1,6 @@
 {
 {
   "$schema": "http://json-schema.org/draft-04/schema#",
   "$schema": "http://json-schema.org/draft-04/schema#",
-  "id": "service_schema_v2.json",
+  "id": "service_schema_v2.0.json",
 
 
   "type": "object",
   "type": "object",
 
 

+ 2 - 1
compose/config/types.py

@@ -7,6 +7,7 @@ from __future__ import unicode_literals
 import os
 import os
 from collections import namedtuple
 from collections import namedtuple
 
 
+from compose.config.config import V1
 from compose.config.errors import ConfigurationError
 from compose.config.errors import ConfigurationError
 from compose.const import IS_WINDOWS_PLATFORM
 from compose.const import IS_WINDOWS_PLATFORM
 
 
@@ -16,7 +17,7 @@ class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
     # TODO: drop service_names arg when v1 is removed
     # TODO: drop service_names arg when v1 is removed
     @classmethod
     @classmethod
     def parse(cls, volume_from_config, service_names, version):
     def parse(cls, volume_from_config, service_names, version):
-        func = cls.parse_v1 if version == 1 else cls.parse_v2
+        func = cls.parse_v1 if version == V1 else cls.parse_v2
         return func(service_names, volume_from_config)
         return func(service_names, volume_from_config)
 
 
     @classmethod
     @classmethod

+ 57 - 47
compose/config/validation.py

@@ -15,6 +15,7 @@ from jsonschema import RefResolver
 from jsonschema import ValidationError
 from jsonschema import ValidationError
 
 
 from .errors import ConfigurationError
 from .errors import ConfigurationError
+from .errors import VERSION_EXPLANATION
 from .sort_services import get_service_name_from_network_mode
 from .sort_services import get_service_name_from_network_mode
 
 
 
 
@@ -174,8 +175,8 @@ def validate_depends_on(service_config, service_names):
                 "undefined.".format(s=service_config, dep=dependency))
                 "undefined.".format(s=service_config, dep=dependency))
 
 
 
 
-def get_unsupported_config_msg(service_name, error_key):
-    msg = "Unsupported config option for '{}' service: '{}'".format(service_name, error_key)
+def get_unsupported_config_msg(path, error_key):
+    msg = "Unsupported config option for {}: '{}'".format(path_string(path), error_key)
     if error_key in DOCKER_CONFIG_HINTS:
     if error_key in DOCKER_CONFIG_HINTS:
         msg += " (did you mean '{}'?)".format(DOCKER_CONFIG_HINTS[error_key])
         msg += " (did you mean '{}'?)".format(DOCKER_CONFIG_HINTS[error_key])
     return msg
     return msg
@@ -191,7 +192,7 @@ def is_service_dict_schema(schema_id):
     return schema_id == 'fields_schema_v1.json' or schema_id == '#/properties/services'
     return schema_id == 'fields_schema_v1.json' or schema_id == '#/properties/services'
 
 
 
 
-def handle_error_for_schema_with_id(error, service_name):
+def handle_error_for_schema_with_id(error, path):
     schema_id = error.schema['id']
     schema_id = error.schema['id']
 
 
     if is_service_dict_schema(schema_id) and error.validator == 'additionalProperties':
     if is_service_dict_schema(schema_id) and error.validator == 'additionalProperties':
@@ -215,62 +216,67 @@ def handle_error_for_schema_with_id(error, service_name):
         # TODO: only applies to v1
         # TODO: only applies to v1
         if 'image' in error.instance and context:
         if 'image' in error.instance and context:
             return (
             return (
-                "Service '{}' has both an image and build path specified. "
+                "{} has both an image and build path specified. "
                 "A service can either be built to image or use an existing "
                 "A service can either be built to image or use an existing "
-                "image, not both.".format(service_name))
+                "image, not both.".format(path_string(path)))
         if 'image' not in error.instance and not context:
         if 'image' not in error.instance and not context:
             return (
             return (
-                "Service '{}' has neither an image nor a build path "
-                "specified. At least one must be provided.".format(service_name))
+                "{} has neither an image nor a build path specified. "
+                "At least one must be provided.".format(path_string(path)))
         # TODO: only applies to v1
         # TODO: only applies to v1
         if 'image' in error.instance and dockerfile:
         if 'image' in error.instance and dockerfile:
             return (
             return (
-                "Service '{}' has both an image and alternate Dockerfile. "
+                "{} has both an image and alternate Dockerfile. "
                 "A service can either be built to image or use an existing "
                 "A service can either be built to image or use an existing "
-                "image, not both.".format(service_name))
+                "image, not both.".format(path_string(path)))
 
 
-    if schema_id == '#/definitions/service':
-        if error.validator == 'additionalProperties':
+    if error.validator == 'additionalProperties':
+        if schema_id == '#/definitions/service':
             invalid_config_key = parse_key_from_error_msg(error)
             invalid_config_key = parse_key_from_error_msg(error)
-            return get_unsupported_config_msg(service_name, invalid_config_key)
+            return get_unsupported_config_msg(path, invalid_config_key)
 
 
+        if not error.path:
+            return '{}\n{}'.format(error.message, VERSION_EXPLANATION)
 
 
-def handle_generic_service_error(error, service_name):
-    config_key = " ".join("'%s'" % k for k in error.path)
+
+def handle_generic_service_error(error, path):
     msg_format = None
     msg_format = None
     error_msg = error.message
     error_msg = error.message
 
 
     if error.validator == 'oneOf':
     if error.validator == 'oneOf':
-        msg_format = "Service '{}' configuration key {} {}"
-        error_msg = _parse_oneof_validator(error)
+        msg_format = "{path} {msg}"
+        config_key, error_msg = _parse_oneof_validator(error)
+        if config_key:
+            path.append(config_key)
 
 
     elif error.validator == 'type':
     elif error.validator == 'type':
-        msg_format = ("Service '{}' configuration key {} contains an invalid "
-                      "type, it should be {}")
+        msg_format = "{path} contains an invalid type, it should be {msg}"
         error_msg = _parse_valid_types_from_validator(error.validator_value)
         error_msg = _parse_valid_types_from_validator(error.validator_value)
 
 
     # TODO: no test case for this branch, there are no config options
     # TODO: no test case for this branch, there are no config options
     # which exercise this branch
     # which exercise this branch
     elif error.validator == 'required':
     elif error.validator == 'required':
-        msg_format = "Service '{}' configuration key '{}' is invalid, {}"
+        msg_format = "{path} is invalid, {msg}"
 
 
     elif error.validator == 'dependencies':
     elif error.validator == 'dependencies':
-        msg_format = "Service '{}' configuration key '{}' is invalid: {}"
         config_key = list(error.validator_value.keys())[0]
         config_key = list(error.validator_value.keys())[0]
         required_keys = ",".join(error.validator_value[config_key])
         required_keys = ",".join(error.validator_value[config_key])
+
+        msg_format = "{path} is invalid: {msg}"
+        path.append(config_key)
         error_msg = "when defining '{}' you must set '{}' as well".format(
         error_msg = "when defining '{}' you must set '{}' as well".format(
             config_key,
             config_key,
             required_keys)
             required_keys)
 
 
     elif error.cause:
     elif error.cause:
         error_msg = six.text_type(error.cause)
         error_msg = six.text_type(error.cause)
-        msg_format = "Service '{}' configuration key {} is invalid: {}"
+        msg_format = "{path} is invalid: {msg}"
 
 
     elif error.path:
     elif error.path:
-        msg_format = "Service '{}' configuration key {} value {}"
+        msg_format = "{path} value {msg}"
 
 
     if msg_format:
     if msg_format:
-        return msg_format.format(service_name, config_key, error_msg)
+        return msg_format.format(path=path_string(path), msg=error_msg)
 
 
     return error.message
     return error.message
 
 
@@ -279,6 +285,10 @@ def parse_key_from_error_msg(error):
     return error.message.split("'")[1]
     return error.message.split("'")[1]
 
 
 
 
+def path_string(path):
+    return ".".join(c for c in path if isinstance(c, six.string_types))
+
+
 def _parse_valid_types_from_validator(validator):
 def _parse_valid_types_from_validator(validator):
     """A validator value can be either an array of valid types or a string of
     """A validator value can be either an array of valid types or a string of
     a valid type. Parse the valid types and prefix with the correct article.
     a valid type. Parse the valid types and prefix with the correct article.
@@ -304,52 +314,52 @@ def _parse_oneof_validator(error):
     for context in error.context:
     for context in error.context:
 
 
         if context.validator == 'required':
         if context.validator == 'required':
-            return context.message
+            return (None, context.message)
 
 
         if context.validator == 'additionalProperties':
         if context.validator == 'additionalProperties':
             invalid_config_key = parse_key_from_error_msg(context)
             invalid_config_key = parse_key_from_error_msg(context)
-            return "contains unsupported option: '{}'".format(invalid_config_key)
+            return (None, "contains unsupported option: '{}'".format(invalid_config_key))
 
 
         if context.path:
         if context.path:
-            invalid_config_key = " ".join(
-                "'{}' ".format(fragment) for fragment in context.path
-                if isinstance(fragment, six.string_types)
+            return (
+                path_string(context.path),
+                "contains {}, which is an invalid type, it should be {}".format(
+                    json.dumps(context.instance),
+                    _parse_valid_types_from_validator(context.validator_value)),
             )
             )
-            return "{}contains {}, which is an invalid type, it should be {}".format(
-                invalid_config_key,
-                # Always print the json repr of the invalid value
-                json.dumps(context.instance),
-                _parse_valid_types_from_validator(context.validator_value))
 
 
         if context.validator == 'uniqueItems':
         if context.validator == 'uniqueItems':
-            return "contains non unique items, please remove duplicates from {}".format(
-                context.instance)
+            return (
+                None,
+                "contains non unique items, please remove duplicates from {}".format(
+                    context.instance),
+            )
 
 
         if context.validator == 'type':
         if context.validator == 'type':
             types.append(context.validator_value)
             types.append(context.validator_value)
 
 
     valid_types = _parse_valid_types_from_validator(types)
     valid_types = _parse_valid_types_from_validator(types)
-    return "contains an invalid type, it should be {}".format(valid_types)
+    return (None, "contains an invalid type, it should be {}".format(valid_types))
 
 
 
 
-def process_errors(errors, service_name=None):
+def process_errors(errors, path_prefix=None):
     """jsonschema gives us an error tree full of information to explain what has
     """jsonschema gives us an error tree full of information to explain what has
     gone wrong. Process each error and pull out relevant information and re-write
     gone wrong. Process each error and pull out relevant information and re-write
     helpful error messages that are relevant.
     helpful error messages that are relevant.
     """
     """
-    def format_error_message(error, service_name):
-        if not service_name and error.path:
-            # field_schema errors will have service name on the path
-            service_name = error.path.popleft()
+    path_prefix = path_prefix or []
+
+    def format_error_message(error):
+        path = path_prefix + list(error.path)
 
 
         if 'id' in error.schema:
         if 'id' in error.schema:
-            error_msg = handle_error_for_schema_with_id(error, service_name)
+            error_msg = handle_error_for_schema_with_id(error, path)
             if error_msg:
             if error_msg:
                 return error_msg
                 return error_msg
 
 
-        return handle_generic_service_error(error, service_name)
+        return handle_generic_service_error(error, path)
 
 
-    return '\n'.join(format_error_message(error, service_name) for error in errors)
+    return '\n'.join(format_error_message(error) for error in errors)
 
 
 
 
 def validate_against_fields_schema(config_file):
 def validate_against_fields_schema(config_file):
@@ -366,14 +376,14 @@ def validate_against_service_schema(config, service_name, version):
         config,
         config,
         "service_schema_v{0}.json".format(version),
         "service_schema_v{0}.json".format(version),
         format_checker=["ports"],
         format_checker=["ports"],
-        service_name=service_name)
+        path_prefix=[service_name])
 
 
 
 
 def _validate_against_schema(
 def _validate_against_schema(
         config,
         config,
         schema_filename,
         schema_filename,
         format_checker=(),
         format_checker=(),
-        service_name=None,
+        path_prefix=None,
         filename=None):
         filename=None):
     config_source_dir = os.path.dirname(os.path.abspath(__file__))
     config_source_dir = os.path.dirname(os.path.abspath(__file__))
 
 
@@ -399,7 +409,7 @@ def _validate_against_schema(
     if not errors:
     if not errors:
         return
         return
 
 
-    error_msg = process_errors(errors, service_name)
+    error_msg = process_errors(errors, path_prefix=path_prefix)
     file_msg = " in file '{}'".format(filename) if filename else ''
     file_msg = " in file '{}'".format(filename) if filename else ''
     raise ConfigurationError("Validation failed{}, reason(s):\n{}".format(
     raise ConfigurationError("Validation failed{}, reason(s):\n{}".format(
         file_msg,
         file_msg,

+ 5 - 3
compose/const.py

@@ -14,9 +14,11 @@ LABEL_PROJECT = 'com.docker.compose.project'
 LABEL_SERVICE = 'com.docker.compose.service'
 LABEL_SERVICE = 'com.docker.compose.service'
 LABEL_VERSION = 'com.docker.compose.version'
 LABEL_VERSION = 'com.docker.compose.version'
 LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
 LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
-COMPOSEFILE_VERSIONS = (1, 2)
+
+COMPOSEFILE_V1 = '1'
+COMPOSEFILE_V2_0 = '2.0'
 
 
 API_VERSIONS = {
 API_VERSIONS = {
-    1: '1.21',
-    2: '1.22',
+    COMPOSEFILE_V1: '1.21',
+    COMPOSEFILE_V2_0: '1.22',
 }
 }

+ 3 - 2
compose/project.py

@@ -10,6 +10,7 @@ from docker.errors import NotFound
 
 
 from . import parallel
 from . import parallel
 from .config import ConfigurationError
 from .config import ConfigurationError
+from .config.config import V1
 from .config.sort_services import get_container_name_from_network_mode
 from .config.sort_services import get_container_name_from_network_mode
 from .config.sort_services import get_service_name_from_network_mode
 from .config.sort_services import get_service_name_from_network_mode
 from .const import DEFAULT_TIMEOUT
 from .const import DEFAULT_TIMEOUT
@@ -56,7 +57,7 @@ class Project(object):
         """
         """
         Construct a Project from a config.Config object.
         Construct a Project from a config.Config object.
         """
         """
-        use_networking = (config_data.version and config_data.version >= 2)
+        use_networking = (config_data.version and config_data.version != V1)
         project = cls(name, [], client, use_networking=use_networking)
         project = cls(name, [], client, use_networking=use_networking)
 
 
         network_config = config_data.networks or {}
         network_config = config_data.networks or {}
@@ -94,7 +95,7 @@ class Project(object):
             network_mode = project.get_network_mode(service_dict, networks)
             network_mode = project.get_network_mode(service_dict, networks)
             volumes_from = get_volumes_from(project, service_dict)
             volumes_from = get_volumes_from(project, service_dict)
 
 
-            if config_data.version == 2:
+            if config_data.version != V1:
                 service_volumes = service_dict.get('volumes', [])
                 service_volumes = service_dict.get('volumes', [])
                 for volume_spec in service_volumes:
                 for volume_spec in service_volumes:
                     if volume_spec.is_named_volume:
                     if volume_spec.is_named_volume:

+ 4 - 4
docker-compose.spec

@@ -23,8 +23,8 @@ exe = EXE(pyz,
                 'DATA'
                 'DATA'
             ),
             ),
             (
             (
-                'compose/config/fields_schema_v2.json',
-                'compose/config/fields_schema_v2.json',
+                'compose/config/fields_schema_v2.0.json',
+                'compose/config/fields_schema_v2.0.json',
                 'DATA'
                 'DATA'
             ),
             ),
             (
             (
@@ -33,8 +33,8 @@ exe = EXE(pyz,
                 'DATA'
                 'DATA'
             ),
             ),
             (
             (
-                'compose/config/service_schema_v2.json',
-                'compose/config/service_schema_v2.json',
+                'compose/config/service_schema_v2.0.json',
+                'compose/config/service_schema_v2.0.json',
                 'DATA'
                 'DATA'
             ),
             ),
             (
             (

+ 1 - 1
tests/acceptance/cli_test.py

@@ -177,7 +177,7 @@ class CLITestCase(DockerClientTestCase):
 
 
         output = yaml.load(result.stdout)
         output = yaml.load(result.stdout)
         expected = {
         expected = {
-            'version': 2,
+            'version': '2.0',
             'volumes': {'data': {'driver': 'local'}},
             'volumes': {'data': {'driver': 'local'}},
             'networks': {'front': {}},
             'networks': {'front': {}},
             'services': {
             'services': {

+ 1 - 1
tests/fixtures/extends/invalid-net-v2.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 services:
 services:
   myweb:
   myweb:
     build: '.'
     build: '.'

+ 1 - 1
tests/fixtures/logging-composefile/docker-compose.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 services:
 services:
   simple:
   simple:
     image: busybox:latest
     image: busybox:latest

+ 1 - 1
tests/fixtures/net-container/v2-invalid.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 
 
 services:
 services:
   foo:
   foo:

+ 1 - 1
tests/fixtures/networks/bridge.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 
 
 services:
 services:
   web:
   web:

+ 1 - 1
tests/fixtures/networks/default-network-config.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 services:
 services:
   simple:
   simple:
     image: busybox:latest
     image: busybox:latest

+ 1 - 1
tests/fixtures/networks/docker-compose.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 
 
 services:
 services:
   web:
   web:

+ 1 - 1
tests/fixtures/networks/external-default.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 services:
 services:
   simple:
   simple:
     image: busybox:latest
     image: busybox:latest

+ 1 - 1
tests/fixtures/networks/external-networks.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 
 
 services:
 services:
   web:
   web:

+ 1 - 1
tests/fixtures/networks/missing-network.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 
 
 services:
 services:
   web:
   web:

+ 1 - 1
tests/fixtures/networks/network-mode.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 
 
 services:
 services:
   bridge:
   bridge:

+ 1 - 1
tests/fixtures/no-services/docker-compose.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 
 
 networks:
 networks:
   foo: {}
   foo: {}

+ 1 - 1
tests/fixtures/sleeps-composefile/docker-compose.yml

@@ -1,5 +1,5 @@
 
 
-version: 2
+version: "2"
 
 
 services:
 services:
   simple:
   simple:

+ 1 - 1
tests/fixtures/v2-full/docker-compose.yml

@@ -1,5 +1,5 @@
 
 
-version: 2
+version: "2"
 
 
 volumes:
 volumes:
   data:
   data:

+ 1 - 1
tests/fixtures/v2-simple/docker-compose.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 services:
 services:
   simple:
   simple:
     image: busybox:latest
     image: busybox:latest

+ 1 - 1
tests/fixtures/v2-simple/links-invalid.yml

@@ -1,4 +1,4 @@
-version: 2
+version: "2"
 services:
 services:
   simple:
   simple:
     image: busybox:latest
     image: busybox:latest

+ 15 - 14
tests/integration/project_test.py

@@ -10,6 +10,7 @@ from docker.errors import NotFound
 from .testcases import DockerClientTestCase
 from .testcases import DockerClientTestCase
 from compose.config import config
 from compose.config import config
 from compose.config import ConfigurationError
 from compose.config import ConfigurationError
+from compose.config.config import V2_0
 from compose.config.types import VolumeFromSpec
 from compose.config.types import VolumeFromSpec
 from compose.config.types import VolumeSpec
 from compose.config.types import VolumeSpec
 from compose.const import LABEL_PROJECT
 from compose.const import LABEL_PROJECT
@@ -112,7 +113,7 @@ class ProjectTest(DockerClientTestCase):
             name='composetest',
             name='composetest',
             client=self.client,
             client=self.client,
             config_data=build_service_dicts({
             config_data=build_service_dicts({
-                'version': 2,
+                'version': V2_0,
                 'services': {
                 'services': {
                     'net': {
                     'net': {
                         'image': 'busybox:latest',
                         'image': 'busybox:latest',
@@ -139,7 +140,7 @@ class ProjectTest(DockerClientTestCase):
             return Project.from_config(
             return Project.from_config(
                 name='composetest',
                 name='composetest',
                 config_data=build_service_dicts({
                 config_data=build_service_dicts({
-                    'version': 2,
+                    'version': V2_0,
                     'services': {
                     'services': {
                         'web': {
                         'web': {
                             'image': 'busybox:latest',
                             'image': 'busybox:latest',
@@ -559,7 +560,7 @@ class ProjectTest(DockerClientTestCase):
     @v2_only()
     @v2_only()
     def test_project_up_networks(self):
     def test_project_up_networks(self):
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -592,7 +593,7 @@ class ProjectTest(DockerClientTestCase):
     @v2_only()
     @v2_only()
     def test_up_with_ipam_config(self):
     def test_up_with_ipam_config(self):
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[],
             services=[],
             volumes={},
             volumes={},
             networks={
             networks={
@@ -651,7 +652,7 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         vol_name = '{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
         full_vol_name = 'composetest_{0}'.format(vol_name)
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -677,7 +678,7 @@ class ProjectTest(DockerClientTestCase):
         base_file = config.ConfigFile(
         base_file = config.ConfigFile(
             'base.yml',
             'base.yml',
             {
             {
-                'version': 2,
+                'version': V2_0,
                 'services': {
                 'services': {
                     'simple': {'image': 'busybox:latest', 'command': 'top'},
                     'simple': {'image': 'busybox:latest', 'command': 'top'},
                     'another': {
                     'another': {
@@ -696,7 +697,7 @@ class ProjectTest(DockerClientTestCase):
         override_file = config.ConfigFile(
         override_file = config.ConfigFile(
             'override.yml',
             'override.yml',
             {
             {
-                'version': 2,
+                'version': V2_0,
                 'services': {
                 'services': {
                     'another': {
                     'another': {
                         'logging': {
                         'logging': {
@@ -729,7 +730,7 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         vol_name = '{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
         full_vol_name = 'composetest_{0}'.format(vol_name)
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -754,7 +755,7 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         vol_name = '{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
         full_vol_name = 'composetest_{0}'.format(vol_name)
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -779,7 +780,7 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         vol_name = '{0:x}'.format(random.getrandbits(32))
 
 
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -802,7 +803,7 @@ class ProjectTest(DockerClientTestCase):
         full_vol_name = 'composetest_{0}'.format(vol_name)
         full_vol_name = 'composetest_{0}'.format(vol_name)
 
 
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -841,7 +842,7 @@ class ProjectTest(DockerClientTestCase):
         full_vol_name = 'composetest_{0}'.format(vol_name)
         full_vol_name = 'composetest_{0}'.format(vol_name)
         self.client.create_volume(vol_name)
         self.client.create_volume(vol_name)
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -866,7 +867,7 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         vol_name = '{0:x}'.format(random.getrandbits(32))
 
 
         config_data = config.Config(
         config_data = config.Config(
-            version=2,
+            version=V2_0,
             services=[{
             services=[{
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
@@ -895,7 +896,7 @@ class ProjectTest(DockerClientTestCase):
         base_file = config.ConfigFile(
         base_file = config.ConfigFile(
             'base.yml',
             'base.yml',
             {
             {
-                'version': 2,
+                'version': V2_0,
                 'services': {
                 'services': {
                     'simple': {
                     'simple': {
                         'image': 'busybox:latest',
                         'image': 'busybox:latest',

+ 4 - 2
tests/integration/testcases.py

@@ -10,6 +10,8 @@ from pytest import skip
 from .. import unittest
 from .. import unittest
 from compose.cli.docker_client import docker_client
 from compose.cli.docker_client import docker_client
 from compose.config.config import resolve_environment
 from compose.config.config import resolve_environment
+from compose.config.config import V1
+from compose.config.config import V2_0
 from compose.const import API_VERSIONS
 from compose.const import API_VERSIONS
 from compose.const import LABEL_PROJECT
 from compose.const import LABEL_PROJECT
 from compose.progress_stream import stream_output
 from compose.progress_stream import stream_output
@@ -54,9 +56,9 @@ class DockerClientTestCase(unittest.TestCase):
     @classmethod
     @classmethod
     def setUpClass(cls):
     def setUpClass(cls):
         if engine_version_too_low_for_v2():
         if engine_version_too_low_for_v2():
-            version = API_VERSIONS[1]
+            version = API_VERSIONS[V1]
         else:
         else:
-            version = API_VERSIONS[2]
+            version = API_VERSIONS[V2_0]
 
 
         cls.client = docker_client(version)
         cls.client = docker_client(version)
 
 

+ 249 - 132
tests/unit/config/config_test.py

@@ -14,14 +14,16 @@ import pytest
 from compose.config import config
 from compose.config import config
 from compose.config.config import resolve_build_args
 from compose.config.config import resolve_build_args
 from compose.config.config import resolve_environment
 from compose.config.config import resolve_environment
+from compose.config.config import V1
+from compose.config.config import V2_0
 from compose.config.errors import ConfigurationError
 from compose.config.errors import ConfigurationError
+from compose.config.errors import VERSION_EXPLANATION
 from compose.config.types import VolumeSpec
 from compose.config.types import VolumeSpec
 from compose.const import IS_WINDOWS_PLATFORM
 from compose.const import IS_WINDOWS_PLATFORM
 from tests import mock
 from tests import mock
 from tests import unittest
 from tests import unittest
 
 
-DEFAULT_VERSION = V2 = 2
-V1 = 1
+DEFAULT_VERSION = V2_0
 
 
 
 
 def make_service_dict(name, service_dict, working_dir, filename=None):
 def make_service_dict(name, service_dict, working_dir, filename=None):
@@ -78,7 +80,7 @@ class ConfigTest(unittest.TestCase):
     def test_load_v2(self):
     def test_load_v2(self):
         config_data = config.load(
         config_data = config.load(
             build_config_details({
             build_config_details({
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'foo': {'image': 'busybox'},
                     'foo': {'image': 'busybox'},
                     'bar': {'image': 'busybox', 'environment': ['FOO=1']},
                     'bar': {'image': 'busybox', 'environment': ['FOO=1']},
@@ -143,9 +145,78 @@ class ConfigTest(unittest.TestCase):
             }
             }
         })
         })
 
 
+    def test_valid_versions(self):
+        for version in ['2', '2.0']:
+            cfg = config.load(build_config_details({'version': version}))
+            assert cfg.version == V2_0
+
+    def test_v1_file_version(self):
+        cfg = config.load(build_config_details({'web': {'image': 'busybox'}}))
+        assert cfg.version == V1
+        assert list(s['name'] for s in cfg.services) == ['web']
+
+        cfg = config.load(build_config_details({'version': {'image': 'busybox'}}))
+        assert cfg.version == V1
+        assert list(s['name'] for s in cfg.services) == ['version']
+
+    def test_wrong_version_type(self):
+        for version in [None, 1, 2, 2.0]:
+            with pytest.raises(ConfigurationError) as excinfo:
+                config.load(
+                    build_config_details(
+                        {'version': version},
+                        filename='filename.yml',
+                    )
+                )
+
+            assert 'Version in "filename.yml" is invalid - it should be a string.' \
+                in excinfo.exconly()
+
+    def test_unsupported_version(self):
+        with pytest.raises(ConfigurationError) as excinfo:
+            config.load(
+                build_config_details(
+                    {'version': '2.1'},
+                    filename='filename.yml',
+                )
+            )
+
+        assert 'Version in "filename.yml" is unsupported' in excinfo.exconly()
+        assert VERSION_EXPLANATION in excinfo.exconly()
+
+    def test_version_1_is_invalid(self):
+        with pytest.raises(ConfigurationError) as excinfo:
+            config.load(
+                build_config_details(
+                    {
+                        'version': '1',
+                        'web': {'image': 'busybox'},
+                    },
+                    filename='filename.yml',
+                )
+            )
+
+        assert 'Version in "filename.yml" is invalid' in excinfo.exconly()
+        assert VERSION_EXPLANATION in excinfo.exconly()
+
+    def test_v1_file_with_version_is_invalid(self):
+        with pytest.raises(ConfigurationError) as excinfo:
+            config.load(
+                build_config_details(
+                    {
+                        'version': '2',
+                        'web': {'image': 'busybox'},
+                    },
+                    filename='filename.yml',
+                )
+            )
+
+        assert 'Additional properties are not allowed' in excinfo.exconly()
+        assert VERSION_EXPLANATION in excinfo.exconly()
+
     def test_named_volume_config_empty(self):
     def test_named_volume_config_empty(self):
         config_details = build_config_details({
         config_details = build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'simple': {'image': 'busybox'}
                 'simple': {'image': 'busybox'}
             },
             },
@@ -161,13 +232,18 @@ class ConfigTest(unittest.TestCase):
         assert volumes['other'] == {}
         assert volumes['other'] == {}
 
 
     def test_load_service_with_name_version(self):
     def test_load_service_with_name_version(self):
-        config_data = config.load(
-            build_config_details({
-                'version': {
-                    'image': 'busybox'
-                }
-            }, 'working_dir', 'filename.yml')
-        )
+        with mock.patch('compose.config.config.log') as mock_logging:
+            config_data = config.load(
+                build_config_details({
+                    'version': {
+                        'image': 'busybox'
+                    }
+                }, 'working_dir', 'filename.yml')
+            )
+
+        assert 'Unexpected type for "version" key in "filename.yml"' \
+            in mock_logging.warn.call_args[0][0]
+
         service_dicts = config_data.services
         service_dicts = config_data.services
         self.assertEqual(
         self.assertEqual(
             service_sort(service_dicts),
             service_sort(service_dicts),
@@ -179,27 +255,6 @@ class ConfigTest(unittest.TestCase):
             ])
             ])
         )
         )
 
 
-    def test_load_invalid_version(self):
-        with self.assertRaises(ConfigurationError):
-            config.load(
-                build_config_details({
-                    'version': 18,
-                    'services': {
-                        'foo': {'image': 'busybox'}
-                    }
-                }, 'working_dir', 'filename.yml')
-            )
-
-        with self.assertRaises(ConfigurationError):
-            config.load(
-                build_config_details({
-                    'version': 'two point oh',
-                    'services': {
-                        'foo': {'image': 'busybox'}
-                    }
-                }, 'working_dir', 'filename.yml')
-            )
-
     def test_load_throws_error_when_not_dict(self):
     def test_load_throws_error_when_not_dict(self):
         with self.assertRaises(ConfigurationError):
         with self.assertRaises(ConfigurationError):
             config.load(
             config.load(
@@ -214,7 +269,7 @@ class ConfigTest(unittest.TestCase):
         with self.assertRaises(ConfigurationError):
         with self.assertRaises(ConfigurationError):
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
-                    {'version': 2, 'services': {'web': 'busybox:latest'}},
+                    {'version': '2', 'services': {'web': 'busybox:latest'}},
                     'working_dir',
                     'working_dir',
                     'filename.yml'
                     'filename.yml'
                 )
                 )
@@ -224,7 +279,7 @@ class ConfigTest(unittest.TestCase):
         with self.assertRaises(ConfigurationError):
         with self.assertRaises(ConfigurationError):
             config.load(
             config.load(
                 build_config_details({
                 build_config_details({
-                    'version': 2,
+                    'version': '2',
                     'services': {'web': 'busybox:latest'},
                     'services': {'web': 'busybox:latest'},
                     'networks': {
                     'networks': {
                         'invalid': {'foo', 'bar'}
                         'invalid': {'foo', 'bar'}
@@ -246,22 +301,38 @@ class ConfigTest(unittest.TestCase):
             with pytest.raises(ConfigurationError) as exc:
             with pytest.raises(ConfigurationError) as exc:
                 config.load(
                 config.load(
                     build_config_details({
                     build_config_details({
-                        'version': 2,
+                        'version': '2',
                         'services': {invalid_name: {'image': 'busybox'}}
                         'services': {invalid_name: {'image': 'busybox'}}
                     }, 'working_dir', 'filename.yml')
                     }, 'working_dir', 'filename.yml')
                 )
                 )
             assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly()
             assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly()
 
 
     def test_load_with_invalid_field_name(self):
     def test_load_with_invalid_field_name(self):
-        config_details = build_config_details(
-            {'web': {'image': 'busybox', 'name': 'bogus'}},
-            'working_dir',
-            'filename.yml')
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
-            config.load(config_details)
-        error_msg = "Unsupported config option for 'web' service: 'name'"
-        assert error_msg in exc.exconly()
-        assert "Validation failed in file 'filename.yml'" in exc.exconly()
+            config.load(build_config_details(
+                {
+                    'version': '2',
+                    'services': {
+                        'web': {'image': 'busybox', 'name': 'bogus'},
+                    }
+                },
+                'working_dir',
+                'filename.yml',
+            ))
+
+        assert "Unsupported config option for services.web: 'name'" in exc.exconly()
+
+    def test_load_with_invalid_field_name_v1(self):
+        with pytest.raises(ConfigurationError) as exc:
+            config.load(build_config_details(
+                {
+                    'web': {'image': 'busybox', 'name': 'bogus'},
+                },
+                'working_dir',
+                'filename.yml',
+            ))
+
+        assert "Unsupported config option for web: 'name'" in exc.exconly()
 
 
     def test_load_invalid_service_definition(self):
     def test_load_invalid_service_definition(self):
         config_details = build_config_details(
         config_details = build_config_details(
@@ -274,9 +345,7 @@ class ConfigTest(unittest.TestCase):
         assert error_msg in exc.exconly()
         assert error_msg in exc.exconly()
 
 
     def test_config_integer_service_name_raise_validation_error(self):
     def test_config_integer_service_name_raise_validation_error(self):
-        expected_error_msg = ("In file 'filename.yml' service name: 1 needs to "
-                              "be a string, eg '1'")
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {1: {'image': 'busybox'}},
                     {1: {'image': 'busybox'}},
@@ -285,15 +354,15 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
-    def test_config_integer_service_name_raise_validation_error_v2(self):
-        expected_error_msg = ("In file 'filename.yml' service name: 1 needs to "
-                              "be a string, eg '1'")
+        assert "In file 'filename.yml' service name: 1 needs to be a string, eg '1'" \
+            in excinfo.exconly()
 
 
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+    def test_config_integer_service_name_raise_validation_error_v2(self):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
-                        'version': 2,
+                        'version': '2',
                         'services': {1: {'image': 'busybox'}}
                         'services': {1: {'image': 'busybox'}}
                     },
                     },
                     'working_dir',
                     'working_dir',
@@ -301,6 +370,9 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "In file 'filename.yml' service name: 1 needs to be a string, eg '1'" \
+            in excinfo.exconly()
+
     def test_load_with_multiple_files_v1(self):
     def test_load_with_multiple_files_v1(self):
         base_file = config.ConfigFile(
         base_file = config.ConfigFile(
             'base.yaml',
             'base.yaml',
@@ -353,7 +425,7 @@ class ConfigTest(unittest.TestCase):
     def test_load_with_multiple_files_and_empty_override_v2(self):
     def test_load_with_multiple_files_and_empty_override_v2(self):
         base_file = config.ConfigFile(
         base_file = config.ConfigFile(
             'base.yml',
             'base.yml',
-            {'version': 2, 'services': {'web': {'image': 'example/web'}}})
+            {'version': '2', 'services': {'web': {'image': 'example/web'}}})
         override_file = config.ConfigFile('override.yml', None)
         override_file = config.ConfigFile('override.yml', None)
         details = config.ConfigDetails('.', [base_file, override_file])
         details = config.ConfigDetails('.', [base_file, override_file])
 
 
@@ -377,7 +449,7 @@ class ConfigTest(unittest.TestCase):
         base_file = config.ConfigFile('base.yml', None)
         base_file = config.ConfigFile('base.yml', None)
         override_file = config.ConfigFile(
         override_file = config.ConfigFile(
             'override.tml',
             'override.tml',
-            {'version': 2, 'services': {'web': {'image': 'example/web'}}}
+            {'version': '2', 'services': {'web': {'image': 'example/web'}}}
         )
         )
         details = config.ConfigDetails('.', [base_file, override_file])
         details = config.ConfigDetails('.', [base_file, override_file])
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
@@ -477,7 +549,7 @@ class ConfigTest(unittest.TestCase):
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
-                        'version': 2,
+                        'version': '2',
                         'services': {
                         'services': {
                             'web': {
                             'web': {
                                 'build': '.',
                                 'build': '.',
@@ -492,7 +564,7 @@ class ConfigTest(unittest.TestCase):
 
 
         service = config.load(
         service = config.load(
             build_config_details({
             build_config_details({
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'build': '.'
                         'build': '.'
@@ -505,7 +577,7 @@ class ConfigTest(unittest.TestCase):
         service = config.load(
         service = config.load(
             build_config_details(
             build_config_details(
                 {
                 {
-                    'version': 2,
+                    'version': '2',
                     'services': {
                     'services': {
                         'web': {
                         'web': {
                             'build': {
                             'build': {
@@ -526,7 +598,7 @@ class ConfigTest(unittest.TestCase):
         base_file = config.ConfigFile(
         base_file = config.ConfigFile(
             'base.yaml',
             'base.yaml',
             {
             {
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'image': 'example/web',
                         'image': 'example/web',
@@ -539,7 +611,7 @@ class ConfigTest(unittest.TestCase):
         override_file = config.ConfigFile(
         override_file = config.ConfigFile(
             'override.yaml',
             'override.yaml',
             {
             {
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'build': '/',
                         'build': '/',
@@ -568,7 +640,7 @@ class ConfigTest(unittest.TestCase):
         base_file = config.ConfigFile(
         base_file = config.ConfigFile(
             'base.yaml',
             'base.yaml',
             {
             {
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'image': 'busybox:latest',
                         'image': 'busybox:latest',
@@ -584,7 +656,7 @@ class ConfigTest(unittest.TestCase):
         base_file = config.ConfigFile(
         base_file = config.ConfigFile(
             'base.yaml',
             'base.yaml',
             {
             {
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'image': 'busybox:latest',
                         'image': 'busybox:latest',
@@ -624,8 +696,7 @@ class ConfigTest(unittest.TestCase):
             assert services[0]['name'] == valid_name
             assert services[0]['name'] == valid_name
 
 
     def test_config_hint(self):
     def test_config_hint(self):
-        expected_error_msg = "(did you mean 'privileged'?)"
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -636,6 +707,8 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "(did you mean 'privileged'?)" in excinfo.exconly()
+
     def test_load_errors_on_uppercase_with_no_image(self):
     def test_load_errors_on_uppercase_with_no_image(self):
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
             config.load(build_config_details({
             config.load(build_config_details({
@@ -643,9 +716,41 @@ class ConfigTest(unittest.TestCase):
             }, 'tests/fixtures/build-ctx'))
             }, 'tests/fixtures/build-ctx'))
             assert "Service 'Foo' contains uppercase characters" in exc.exconly()
             assert "Service 'Foo' contains uppercase characters" in exc.exconly()
 
 
-    def test_invalid_config_build_and_image_specified(self):
-        expected_error_msg = "Service 'foo' has both an image and build path specified."
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+    def test_invalid_config_v1(self):
+        with pytest.raises(ConfigurationError) as excinfo:
+            config.load(
+                build_config_details(
+                    {
+                        'foo': {'image': 1},
+                    },
+                    'tests/fixtures/extends',
+                    'filename.yml'
+                )
+            )
+
+        assert "foo.image contains an invalid type, it should be a string" \
+            in excinfo.exconly()
+
+    def test_invalid_config_v2(self):
+        with pytest.raises(ConfigurationError) as excinfo:
+            config.load(
+                build_config_details(
+                    {
+                        'version': '2',
+                        'services': {
+                            'foo': {'image': 1},
+                        },
+                    },
+                    'tests/fixtures/extends',
+                    'filename.yml'
+                )
+            )
+
+        assert "services.foo.image contains an invalid type, it should be a string" \
+            in excinfo.exconly()
+
+    def test_invalid_config_build_and_image_specified_v1(self):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -656,9 +761,10 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "foo has both an image and build path specified." in excinfo.exconly()
+
     def test_invalid_config_type_should_be_an_array(self):
     def test_invalid_config_type_should_be_an_array(self):
-        expected_error_msg = "Service 'foo' configuration key 'links' contains an invalid type, it should be an array"
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -669,10 +775,11 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "foo.links contains an invalid type, it should be an array" \
+            in excinfo.exconly()
+
     def test_invalid_config_not_a_dictionary(self):
     def test_invalid_config_not_a_dictionary(self):
-        expected_error_msg = ("Top level object in 'filename.yml' needs to be "
-                              "an object.")
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     ['foo', 'lol'],
                     ['foo', 'lol'],
@@ -681,9 +788,11 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "Top level object in 'filename.yml' needs to be an object" \
+            in excinfo.exconly()
+
     def test_invalid_config_not_unique_items(self):
     def test_invalid_config_not_unique_items(self):
-        expected_error_msg = "has non-unique elements"
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -694,10 +803,10 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "has non-unique elements" in excinfo.exconly()
+
     def test_invalid_list_of_strings_format(self):
     def test_invalid_list_of_strings_format(self):
-        expected_error_msg = "Service 'web' configuration key 'command' contains 1"
-        expected_error_msg += ", which is an invalid type, it should be a string"
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -708,7 +817,10 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
-    def test_load_config_dockerfile_without_build_raises_error(self):
+        assert "web.command contains 1, which is an invalid type, it should be a string" \
+            in excinfo.exconly()
+
+    def test_load_config_dockerfile_without_build_raises_error_v1(self):
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
             config.load(build_config_details({
             config.load(build_config_details({
                 'web': {
                 'web': {
@@ -716,12 +828,11 @@ class ConfigTest(unittest.TestCase):
                     'dockerfile': 'Dockerfile.alt'
                     'dockerfile': 'Dockerfile.alt'
                 }
                 }
             }))
             }))
-        assert "Service 'web' has both an image and alternate Dockerfile." in exc.exconly()
 
 
-    def test_config_extra_hosts_string_raises_validation_error(self):
-        expected_error_msg = "Service 'web' configuration key 'extra_hosts' contains an invalid type"
+        assert "web has both an image and alternate Dockerfile." in exc.exconly()
 
 
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+    def test_config_extra_hosts_string_raises_validation_error(self):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {'web': {
                     {'web': {
@@ -733,12 +844,11 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
-    def test_config_extra_hosts_list_of_dicts_validation_error(self):
-        expected_error_msg = (
-            "key 'extra_hosts' contains {\"somehost\": \"162.242.195.82\"}, "
-            "which is an invalid type, it should be a string")
+        assert "web.extra_hosts contains an invalid type" \
+            in excinfo.exconly()
 
 
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+    def test_config_extra_hosts_list_of_dicts_validation_error(self):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {'web': {
                     {'web': {
@@ -753,10 +863,11 @@ class ConfigTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
-    def test_config_ulimits_invalid_keys_validation_error(self):
-        expected = ("Service 'web' configuration key 'ulimits' 'nofile' contains "
-                    "unsupported option: 'not_soft_or_hard'")
+        assert "web.extra_hosts contains {\"somehost\": \"162.242.195.82\"}, " \
+               "which is an invalid type, it should be a string" \
+            in excinfo.exconly()
 
 
+    def test_config_ulimits_invalid_keys_validation_error(self):
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
             config.load(build_config_details(
             config.load(build_config_details(
                 {
                 {
@@ -773,10 +884,11 @@ class ConfigTest(unittest.TestCase):
                 },
                 },
                 'working_dir',
                 'working_dir',
                 'filename.yml'))
                 'filename.yml'))
-        assert expected in exc.exconly()
 
 
-    def test_config_ulimits_required_keys_validation_error(self):
+        assert "web.ulimits.nofile contains unsupported option: 'not_soft_or_hard'" \
+            in exc.exconly()
 
 
+    def test_config_ulimits_required_keys_validation_error(self):
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
             config.load(build_config_details(
             config.load(build_config_details(
                 {
                 {
@@ -787,7 +899,7 @@ class ConfigTest(unittest.TestCase):
                 },
                 },
                 'working_dir',
                 'working_dir',
                 'filename.yml'))
                 'filename.yml'))
-        assert "Service 'web' configuration key 'ulimits' 'nofile'" in exc.exconly()
+        assert "web.ulimits.nofile" in exc.exconly()
         assert "'hard' is a required property" in exc.exconly()
         assert "'hard' is a required property" in exc.exconly()
 
 
     def test_config_ulimits_soft_greater_than_hard_error(self):
     def test_config_ulimits_soft_greater_than_hard_error(self):
@@ -888,7 +1000,7 @@ class ConfigTest(unittest.TestCase):
                     'extra_hosts': "www.example.com: 192.168.0.17",
                     'extra_hosts': "www.example.com: 192.168.0.17",
                 }
                 }
             }))
             }))
-        assert "'extra_hosts' contains an invalid type" in exc.exconly()
+        assert "web.extra_hosts contains an invalid type" in exc.exconly()
 
 
     def test_validate_extra_hosts_invalid_list(self):
     def test_validate_extra_hosts_invalid_list(self):
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
@@ -959,7 +1071,7 @@ class ConfigTest(unittest.TestCase):
 
 
     def test_external_volume_config(self):
     def test_external_volume_config(self):
         config_details = build_config_details({
         config_details = build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'bogus': {'image': 'busybox'}
                 'bogus': {'image': 'busybox'}
             },
             },
@@ -977,7 +1089,7 @@ class ConfigTest(unittest.TestCase):
 
 
     def test_external_volume_invalid_config(self):
     def test_external_volume_invalid_config(self):
         config_details = build_config_details({
         config_details = build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'bogus': {'image': 'busybox'}
                 'bogus': {'image': 'busybox'}
             },
             },
@@ -990,7 +1102,7 @@ class ConfigTest(unittest.TestCase):
 
 
     def test_depends_on_orders_services(self):
     def test_depends_on_orders_services(self):
         config_details = build_config_details({
         config_details = build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'one': {'image': 'busybox', 'depends_on': ['three', 'two']},
                 'one': {'image': 'busybox', 'depends_on': ['three', 'two']},
                 'two': {'image': 'busybox', 'depends_on': ['three']},
                 'two': {'image': 'busybox', 'depends_on': ['three']},
@@ -1005,7 +1117,7 @@ class ConfigTest(unittest.TestCase):
 
 
     def test_depends_on_unknown_service_errors(self):
     def test_depends_on_unknown_service_errors(self):
         config_details = build_config_details({
         config_details = build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'one': {'image': 'busybox', 'depends_on': ['three']},
                 'one': {'image': 'busybox', 'depends_on': ['three']},
             },
             },
@@ -1018,7 +1130,7 @@ class ConfigTest(unittest.TestCase):
 class NetworkModeTest(unittest.TestCase):
 class NetworkModeTest(unittest.TestCase):
     def test_network_mode_standard(self):
     def test_network_mode_standard(self):
         config_data = config.load(build_config_details({
         config_data = config.load(build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'web': {
                 'web': {
                     'image': 'busybox',
                     'image': 'busybox',
@@ -1044,7 +1156,7 @@ class NetworkModeTest(unittest.TestCase):
 
 
     def test_network_mode_container(self):
     def test_network_mode_container(self):
         config_data = config.load(build_config_details({
         config_data = config.load(build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'web': {
                 'web': {
                     'image': 'busybox',
                     'image': 'busybox',
@@ -1069,7 +1181,7 @@ class NetworkModeTest(unittest.TestCase):
 
 
     def test_network_mode_service(self):
     def test_network_mode_service(self):
         config_data = config.load(build_config_details({
         config_data = config.load(build_config_details({
-            'version': 2,
+            'version': '2',
             'services': {
             'services': {
                 'web': {
                 'web': {
                     'image': 'busybox',
                     'image': 'busybox',
@@ -1103,7 +1215,7 @@ class NetworkModeTest(unittest.TestCase):
     def test_network_mode_service_nonexistent(self):
     def test_network_mode_service_nonexistent(self):
         with pytest.raises(ConfigurationError) as excinfo:
         with pytest.raises(ConfigurationError) as excinfo:
             config.load(build_config_details({
             config.load(build_config_details({
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'image': 'busybox',
                         'image': 'busybox',
@@ -1118,7 +1230,7 @@ class NetworkModeTest(unittest.TestCase):
     def test_network_mode_plus_networks_is_invalid(self):
     def test_network_mode_plus_networks_is_invalid(self):
         with pytest.raises(ConfigurationError) as excinfo:
         with pytest.raises(ConfigurationError) as excinfo:
             config.load(build_config_details({
             config.load(build_config_details({
-                'version': 2,
+                'version': '2',
                 'services': {
                 'services': {
                     'web': {
                     'web': {
                         'image': 'busybox',
                         'image': 'busybox',
@@ -1574,11 +1686,7 @@ class MemoryOptionsTest(unittest.TestCase):
         When you set a 'memswap_limit' it is invalid config unless you also set
         When you set a 'memswap_limit' it is invalid config unless you also set
         a mem_limit
         a mem_limit
         """
         """
-        expected_error_msg = (
-            "Service 'foo' configuration key 'memswap_limit' is invalid: when "
-            "defining 'memswap_limit' you must set 'mem_limit' as well"
-        )
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -1589,6 +1697,10 @@ class MemoryOptionsTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "foo.memswap_limit is invalid: when defining " \
+               "'memswap_limit' you must set 'mem_limit' as well" \
+            in excinfo.exconly()
+
     def test_validation_with_correct_memswap_values(self):
     def test_validation_with_correct_memswap_values(self):
         service_dict = config.load(
         service_dict = config.load(
             build_config_details(
             build_config_details(
@@ -1851,7 +1963,7 @@ class ExtendsTest(unittest.TestCase):
         self.assertEqual(path, expected)
         self.assertEqual(path, expected)
 
 
     def test_extends_validation_empty_dictionary(self):
     def test_extends_validation_empty_dictionary(self):
-        with self.assertRaisesRegexp(ConfigurationError, 'service'):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -1862,8 +1974,10 @@ class ExtendsTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert 'service' in excinfo.exconly()
+
     def test_extends_validation_missing_service_key(self):
     def test_extends_validation_missing_service_key(self):
-        with self.assertRaisesRegexp(ConfigurationError, "'service' is a required property"):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -1874,12 +1988,10 @@ class ExtendsTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "'service' is a required property" in excinfo.exconly()
+
     def test_extends_validation_invalid_key(self):
     def test_extends_validation_invalid_key(self):
-        expected_error_msg = (
-            "Service 'web' configuration key 'extends' "
-            "contains unsupported option: 'rogue_key'"
-        )
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -1897,12 +2009,11 @@ class ExtendsTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "web.extends contains unsupported option: 'rogue_key'" \
+            in excinfo.exconly()
+
     def test_extends_validation_sub_property_key(self):
     def test_extends_validation_sub_property_key(self):
-        expected_error_msg = (
-            "Service 'web' configuration key 'extends' 'file' contains 1, "
-            "which is an invalid type, it should be a string"
-        )
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             config.load(
             config.load(
                 build_config_details(
                 build_config_details(
                     {
                     {
@@ -1919,13 +2030,16 @@ class ExtendsTest(unittest.TestCase):
                 )
                 )
             )
             )
 
 
+        assert "web.extends.file contains 1, which is an invalid type, it should be a string" \
+            in excinfo.exconly()
+
     def test_extends_validation_no_file_key_no_filename_set(self):
     def test_extends_validation_no_file_key_no_filename_set(self):
         dictionary = {'extends': {'service': 'web'}}
         dictionary = {'extends': {'service': 'web'}}
 
 
-        def load_config():
-            return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
+        with pytest.raises(ConfigurationError) as excinfo:
+            make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
 
 
-        self.assertRaisesRegexp(ConfigurationError, 'file', load_config)
+        assert 'file' in excinfo.exconly()
 
 
     def test_extends_validation_valid_config(self):
     def test_extends_validation_valid_config(self):
         service = config.load(
         service = config.load(
@@ -1946,7 +2060,7 @@ class ExtendsTest(unittest.TestCase):
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
             load_from_filename('tests/fixtures/extends/service-with-invalid-schema.yml')
             load_from_filename('tests/fixtures/extends/service-with-invalid-schema.yml')
         assert (
         assert (
-            "Service 'myweb' has neither an image nor a build path specified" in
+            "myweb has neither an image nor a build path specified" in
             exc.exconly()
             exc.exconly()
         )
         )
 
 
@@ -1979,16 +2093,17 @@ class ExtendsTest(unittest.TestCase):
         ]))
         ]))
 
 
     def test_invalid_links_in_extended_service(self):
     def test_invalid_links_in_extended_service(self):
-        expected_error_msg = "services with 'links' cannot be extended"
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             load_from_filename('tests/fixtures/extends/invalid-links.yml')
             load_from_filename('tests/fixtures/extends/invalid-links.yml')
 
 
-    def test_invalid_volumes_from_in_extended_service(self):
-        expected_error_msg = "services with 'volumes_from' cannot be extended"
+        assert "services with 'links' cannot be extended" in excinfo.exconly()
 
 
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+    def test_invalid_volumes_from_in_extended_service(self):
+        with pytest.raises(ConfigurationError) as excinfo:
             load_from_filename('tests/fixtures/extends/invalid-volumes.yml')
             load_from_filename('tests/fixtures/extends/invalid-volumes.yml')
 
 
+        assert "services with 'volumes_from' cannot be extended" in excinfo.exconly()
+
     def test_invalid_net_in_extended_service(self):
     def test_invalid_net_in_extended_service(self):
         with pytest.raises(ConfigurationError) as excinfo:
         with pytest.raises(ConfigurationError) as excinfo:
             load_from_filename('tests/fixtures/extends/invalid-net-v2.yml')
             load_from_filename('tests/fixtures/extends/invalid-net-v2.yml')
@@ -2044,10 +2159,12 @@ class ExtendsTest(unittest.TestCase):
         ])
         ])
 
 
     def test_load_throws_error_when_base_service_does_not_exist(self):
     def test_load_throws_error_when_base_service_does_not_exist(self):
-        err_msg = r'''Cannot extend service 'foo' in .*: Service not found'''
-        with self.assertRaisesRegexp(ConfigurationError, err_msg):
+        with pytest.raises(ConfigurationError) as excinfo:
             load_from_filename('tests/fixtures/extends/nonexistent-service.yml')
             load_from_filename('tests/fixtures/extends/nonexistent-service.yml')
 
 
+        assert "Cannot extend service 'foo'" in excinfo.exconly()
+        assert "Service not found" in excinfo.exconly()
+
     def test_partial_service_config_in_extends_is_still_valid(self):
     def test_partial_service_config_in_extends_is_still_valid(self):
         dicts = load_from_filename('tests/fixtures/extends/valid-common-config.yml')
         dicts = load_from_filename('tests/fixtures/extends/valid-common-config.yml')
         self.assertEqual(dicts[0]['environment'], {'FOO': '1'})
         self.assertEqual(dicts[0]['environment'], {'FOO': '1'})
@@ -2140,7 +2257,7 @@ class ExtendsTest(unittest.TestCase):
         tmpdir = py.test.ensuretemp('test_extends_with_mixed_version')
         tmpdir = py.test.ensuretemp('test_extends_with_mixed_version')
         self.addCleanup(tmpdir.remove)
         self.addCleanup(tmpdir.remove)
         tmpdir.join('docker-compose.yml').write("""
         tmpdir.join('docker-compose.yml').write("""
-            version: 2
+            version: "2"
             services:
             services:
               web:
               web:
                 extends:
                 extends:
@@ -2162,7 +2279,7 @@ class ExtendsTest(unittest.TestCase):
         tmpdir = py.test.ensuretemp('test_extends_with_defined_version')
         tmpdir = py.test.ensuretemp('test_extends_with_defined_version')
         self.addCleanup(tmpdir.remove)
         self.addCleanup(tmpdir.remove)
         tmpdir.join('docker-compose.yml').write("""
         tmpdir.join('docker-compose.yml').write("""
-            version: 2
+            version: "2"
             services:
             services:
               web:
               web:
                 extends:
                 extends:
@@ -2171,7 +2288,7 @@ class ExtendsTest(unittest.TestCase):
                 image: busybox
                 image: busybox
         """)
         """)
         tmpdir.join('base.yml').write("""
         tmpdir.join('base.yml').write("""
-            version: 2
+            version: "2"
             services:
             services:
                 base:
                 base:
                   volumes: ['/foo']
                   volumes: ['/foo']

+ 8 - 8
tests/unit/config/types_test.py

@@ -3,13 +3,13 @@ from __future__ import unicode_literals
 
 
 import pytest
 import pytest
 
 
+from compose.config.config import V1
+from compose.config.config import V2_0
 from compose.config.errors import ConfigurationError
 from compose.config.errors import ConfigurationError
 from compose.config.types import parse_extra_hosts
 from compose.config.types import parse_extra_hosts
 from compose.config.types import VolumeFromSpec
 from compose.config.types import VolumeFromSpec
 from compose.config.types import VolumeSpec
 from compose.config.types import VolumeSpec
 from compose.const import IS_WINDOWS_PLATFORM
 from compose.const import IS_WINDOWS_PLATFORM
-from tests.unit.config.config_test import V1
-from tests.unit.config.config_test import V2
 
 
 
 
 def test_parse_extra_hosts_list():
 def test_parse_extra_hosts_list():
@@ -91,26 +91,26 @@ class TestVolumesFromSpec(object):
             VolumeFromSpec.parse('unknown:format:ro', self.services, V1)
             VolumeFromSpec.parse('unknown:format:ro', self.services, V1)
 
 
     def test_parse_v2_from_service(self):
     def test_parse_v2_from_service(self):
-        volume_from = VolumeFromSpec.parse('servicea', self.services, V2)
+        volume_from = VolumeFromSpec.parse('servicea', self.services, V2_0)
         assert volume_from == VolumeFromSpec('servicea', 'rw', 'service')
         assert volume_from == VolumeFromSpec('servicea', 'rw', 'service')
 
 
     def test_parse_v2_from_service_with_mode(self):
     def test_parse_v2_from_service_with_mode(self):
-        volume_from = VolumeFromSpec.parse('servicea:ro', self.services, V2)
+        volume_from = VolumeFromSpec.parse('servicea:ro', self.services, V2_0)
         assert volume_from == VolumeFromSpec('servicea', 'ro', 'service')
         assert volume_from == VolumeFromSpec('servicea', 'ro', 'service')
 
 
     def test_parse_v2_from_container(self):
     def test_parse_v2_from_container(self):
-        volume_from = VolumeFromSpec.parse('container:foo', self.services, V2)
+        volume_from = VolumeFromSpec.parse('container:foo', self.services, V2_0)
         assert volume_from == VolumeFromSpec('foo', 'rw', 'container')
         assert volume_from == VolumeFromSpec('foo', 'rw', 'container')
 
 
     def test_parse_v2_from_container_with_mode(self):
     def test_parse_v2_from_container_with_mode(self):
-        volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, V2)
+        volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, V2_0)
         assert volume_from == VolumeFromSpec('foo', 'ro', 'container')
         assert volume_from == VolumeFromSpec('foo', 'ro', 'container')
 
 
     def test_parse_v2_invalid_type(self):
     def test_parse_v2_invalid_type(self):
         with pytest.raises(ConfigurationError) as exc:
         with pytest.raises(ConfigurationError) as exc:
-            VolumeFromSpec.parse('bogus:foo:ro', self.services, V2)
+            VolumeFromSpec.parse('bogus:foo:ro', self.services, V2_0)
         assert "Unknown volumes_from type 'bogus'" in exc.exconly()
         assert "Unknown volumes_from type 'bogus'" in exc.exconly()
 
 
     def test_parse_v2_invalid(self):
     def test_parse_v2_invalid(self):
         with pytest.raises(ConfigurationError):
         with pytest.raises(ConfigurationError):
-            VolumeFromSpec.parse('unknown:format:ro', self.services, V2)
+            VolumeFromSpec.parse('unknown:format:ro', self.services, V2_0)