Add computed attribute type and location to base resource
The shade objects all have a 'location' attribute which includes information about the cloud, domain and project an object is from. Add this to the base Resource object so that Resource objects start to match shade objects. Also, add a new type of attribute, 'computed' to set location to since location is not necessarily information from the remote payload. Also add an attribute parameter "coerce_to_default" which can be set to tell the SDK to force the use of the default value if the attribute would have been None or missing. Change-Id: I315bc863bc87a17fb6f6a3e265a46b9e0acd41ed
This commit is contained in:
parent
2d472aea49
commit
e3e9a9d39c
@ -86,31 +86,41 @@ class _BaseComponent(object):
|
|||||||
_map_cls = dict
|
_map_cls = dict
|
||||||
|
|
||||||
def __init__(self, name, type=None, default=None, alias=None,
|
def __init__(self, name, type=None, default=None, alias=None,
|
||||||
alternate_id=False, list_type=None, **kwargs):
|
alternate_id=False, list_type=None, coerce_to_default=False,
|
||||||
|
**kwargs):
|
||||||
"""A typed descriptor for a component that makes up a Resource
|
"""A typed descriptor for a component that makes up a Resource
|
||||||
|
|
||||||
:param name: The name this component exists as on the server
|
:param name: The name this component exists as on the server
|
||||||
:param type: The type this component is expected to be by the server.
|
:param type:
|
||||||
By default this is None, meaning any value you specify
|
The type this component is expected to be by the server.
|
||||||
will work. If you specify type=dict and then set a
|
By default this is None, meaning any value you specify
|
||||||
component to a string, __set__ will fail, for example.
|
will work. If you specify type=dict and then set a
|
||||||
|
component to a string, __set__ will fail, for example.
|
||||||
:param default: Typically None, but any other default can be set.
|
:param default: Typically None, but any other default can be set.
|
||||||
:param alias: If set, alternative attribute on object to return.
|
:param alias: If set, alternative attribute on object to return.
|
||||||
:param alternate_id: When `True`, this property is known
|
:param alternate_id:
|
||||||
internally as a value that can be sent
|
When `True`, this property is known internally as a value that
|
||||||
with requests that require an ID but
|
can be sent with requests that require an ID but when `id` is
|
||||||
when `id` is not a name the Resource has.
|
not a name the Resource has. This is a relatively uncommon case,
|
||||||
This is a relatively uncommon case, and this
|
and this setting should only be used once per Resource.
|
||||||
setting should only be used once per Resource.
|
:param list_type:
|
||||||
:param list_type: If type is `list`, list_type designates what the
|
If type is `list`, list_type designates what the type of the
|
||||||
type of the elements of the list should be.
|
elements of the list should be.
|
||||||
|
:param coerce_to_default:
|
||||||
|
If the Component is None or not present, force the given default
|
||||||
|
to be used. If a default is not given but a type is given,
|
||||||
|
construct an empty version of the type in question.
|
||||||
"""
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type
|
self.type = type
|
||||||
self.default = default
|
if type is not None and coerce_to_default and not default:
|
||||||
|
self.default = type()
|
||||||
|
else:
|
||||||
|
self.default = default
|
||||||
self.alias = alias
|
self.alias = alias
|
||||||
self.alternate_id = alternate_id
|
self.alternate_id = alternate_id
|
||||||
self.list_type = list_type
|
self.list_type = list_type
|
||||||
|
self.coerce_to_default = coerce_to_default
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
@ -150,6 +160,8 @@ class _BaseComponent(object):
|
|||||||
return _convert_type(value, self.type, self.list_type)
|
return _convert_type(value, self.type, self.list_type)
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
|
if self.coerce_to_default and value is None:
|
||||||
|
value = self.default
|
||||||
if value != self.default:
|
if value != self.default:
|
||||||
value = _convert_type(value, self.type, self.list_type)
|
value = _convert_type(value, self.type, self.list_type)
|
||||||
|
|
||||||
@ -183,6 +195,12 @@ class URI(_BaseComponent):
|
|||||||
key = "_uri"
|
key = "_uri"
|
||||||
|
|
||||||
|
|
||||||
|
class Computed(_BaseComponent):
|
||||||
|
"""Computed attributes"""
|
||||||
|
|
||||||
|
key = "_computed"
|
||||||
|
|
||||||
|
|
||||||
class _ComponentManager(collections.MutableMapping):
|
class _ComponentManager(collections.MutableMapping):
|
||||||
"""Storage of a component type"""
|
"""Storage of a component type"""
|
||||||
|
|
||||||
@ -313,8 +331,8 @@ class Resource(object):
|
|||||||
id = Body("id")
|
id = Body("id")
|
||||||
#: The name of this resource.
|
#: The name of this resource.
|
||||||
name = Body("name")
|
name = Body("name")
|
||||||
#: The location of this resource.
|
#: The OpenStack location of this resource.
|
||||||
location = Header("Location")
|
location = Computed('location')
|
||||||
|
|
||||||
#: Mapping of accepted query parameter names.
|
#: Mapping of accepted query parameter names.
|
||||||
_query_mapping = QueryParameters()
|
_query_mapping = QueryParameters()
|
||||||
@ -359,35 +377,57 @@ class Resource(object):
|
|||||||
#: API microversion (string or None) this Resource was loaded with
|
#: API microversion (string or None) this Resource was loaded with
|
||||||
microversion = None
|
microversion = None
|
||||||
|
|
||||||
def __init__(self, _synchronized=False, **attrs):
|
_connection = None
|
||||||
|
_body = None
|
||||||
|
_header = None
|
||||||
|
_uri = None
|
||||||
|
_computed = None
|
||||||
|
|
||||||
|
def __init__(self, _synchronized=False, connection=None, **attrs):
|
||||||
"""The base resource
|
"""The base resource
|
||||||
|
|
||||||
:param bool _synchronized: This is not intended to be used directly.
|
:param bool _synchronized:
|
||||||
See :meth:`~openstack.resource.Resource.new` and
|
This is not intended to be used directly. See
|
||||||
:meth:`~openstack.resource.Resource.existing`.
|
:meth:`~openstack.resource.Resource.new` and
|
||||||
|
:meth:`~openstack.resource.Resource.existing`.
|
||||||
|
:param openstack.connection.Connection connection:
|
||||||
|
Reference to the Connection being used. Defaults to None to allow
|
||||||
|
Resource objects to be used without an active Connection, such as
|
||||||
|
in unit tests. Use of ``self._connection`` in Resource code should
|
||||||
|
protect itself with a check for None.
|
||||||
"""
|
"""
|
||||||
|
self._connection = connection
|
||||||
self.microversion = attrs.pop('microversion', None)
|
self.microversion = attrs.pop('microversion', None)
|
||||||
# NOTE: _collect_attrs modifies **attrs in place, removing
|
# NOTE: _collect_attrs modifies **attrs in place, removing
|
||||||
# items as they match up with any of the body, header,
|
# items as they match up with any of the body, header,
|
||||||
# or uri mappings.
|
# or uri mappings.
|
||||||
body, header, uri = self._collect_attrs(attrs)
|
body, header, uri, computed = self._collect_attrs(attrs)
|
||||||
# TODO(briancurtin): at this point if attrs has anything left
|
# TODO(briancurtin): at this point if attrs has anything left
|
||||||
# they're not being set anywhere. Log this? Raise exception?
|
# they're not being set anywhere. Log this? Raise exception?
|
||||||
# How strict should we be here? Should strict be an option?
|
# How strict should we be here? Should strict be an option?
|
||||||
|
|
||||||
self._body = _ComponentManager(attributes=body,
|
self._body = _ComponentManager(
|
||||||
synchronized=_synchronized)
|
attributes=body,
|
||||||
self._header = _ComponentManager(attributes=header,
|
synchronized=_synchronized)
|
||||||
synchronized=_synchronized)
|
self._header = _ComponentManager(
|
||||||
self._uri = _ComponentManager(attributes=uri,
|
attributes=header,
|
||||||
synchronized=_synchronized)
|
synchronized=_synchronized)
|
||||||
|
self._uri = _ComponentManager(
|
||||||
|
attributes=uri,
|
||||||
|
synchronized=_synchronized)
|
||||||
|
self._computed = _ComponentManager(
|
||||||
|
attributes=computed,
|
||||||
|
synchronized=_synchronized)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
pairs = ["%s=%s" % (k, v) for k, v in dict(itertools.chain(
|
pairs = [
|
||||||
self._body.attributes.items(),
|
"%s=%s" % (k, v if v is not None else 'None')
|
||||||
self._header.attributes.items(),
|
for k, v in dict(itertools.chain(
|
||||||
self._uri.attributes.items())).items()]
|
self._body.attributes.items(),
|
||||||
|
self._header.attributes.items(),
|
||||||
|
self._uri.attributes.items(),
|
||||||
|
self._computed.attributes.items())).items()
|
||||||
|
]
|
||||||
args = ", ".join(pairs)
|
args = ", ".join(pairs)
|
||||||
|
|
||||||
return "%s.%s(%s)" % (
|
return "%s.%s(%s)" % (
|
||||||
@ -397,9 +437,12 @@ class Resource(object):
|
|||||||
"""Return True if another resource has the same contents"""
|
"""Return True if another resource has the same contents"""
|
||||||
if not isinstance(comparand, Resource):
|
if not isinstance(comparand, Resource):
|
||||||
return False
|
return False
|
||||||
return all([self._body.attributes == comparand._body.attributes,
|
return all([
|
||||||
self._header.attributes == comparand._header.attributes,
|
self._body.attributes == comparand._body.attributes,
|
||||||
self._uri.attributes == comparand._uri.attributes])
|
self._header.attributes == comparand._header.attributes,
|
||||||
|
self._uri.attributes == comparand._uri.attributes,
|
||||||
|
self._computed.attributes == comparand._computed.attributes,
|
||||||
|
])
|
||||||
|
|
||||||
def __getattribute__(self, name):
|
def __getattribute__(self, name):
|
||||||
"""Return an attribute on this instance
|
"""Return an attribute on this instance
|
||||||
@ -427,11 +470,12 @@ class Resource(object):
|
|||||||
been created.
|
been created.
|
||||||
"""
|
"""
|
||||||
self.microversion = attrs.pop('microversion', None)
|
self.microversion = attrs.pop('microversion', None)
|
||||||
body, header, uri = self._collect_attrs(attrs)
|
body, header, uri, computed = self._collect_attrs(attrs)
|
||||||
|
|
||||||
self._body.update(body)
|
self._body.update(body)
|
||||||
self._header.update(header)
|
self._header.update(header)
|
||||||
self._uri.update(uri)
|
self._uri.update(uri)
|
||||||
|
self._computed.update(computed)
|
||||||
|
|
||||||
def _collect_attrs(self, attrs):
|
def _collect_attrs(self, attrs):
|
||||||
"""Given attributes, return a dict per type of attribute
|
"""Given attributes, return a dict per type of attribute
|
||||||
@ -444,7 +488,25 @@ class Resource(object):
|
|||||||
header = self._consume_header_attrs(attrs)
|
header = self._consume_header_attrs(attrs)
|
||||||
uri = self._consume_uri_attrs(attrs)
|
uri = self._consume_uri_attrs(attrs)
|
||||||
|
|
||||||
return body, header, uri
|
if any([body, header, uri]):
|
||||||
|
attrs = self._compute_attributes(body, header, uri)
|
||||||
|
|
||||||
|
body.update(self._consume_attrs(self._body_mapping(), attrs))
|
||||||
|
|
||||||
|
header.update(self._consume_attrs(self._header_mapping(), attrs))
|
||||||
|
uri.update(self._consume_attrs(self._uri_mapping(), attrs))
|
||||||
|
computed = self._consume_attrs(self._computed_mapping(), attrs)
|
||||||
|
# TODO(mordred) We should make a Location Resource and add it here
|
||||||
|
# instead of just the dict.
|
||||||
|
if self._connection:
|
||||||
|
computed['location'] = munch.unmunchify(
|
||||||
|
self._connection._openstackcloud.current_location)
|
||||||
|
|
||||||
|
return body, header, uri, computed
|
||||||
|
|
||||||
|
def _compute_attributes(self, body, header, uri):
|
||||||
|
"""Compute additional attributes from the remote resource."""
|
||||||
|
return {}
|
||||||
|
|
||||||
def _consume_body_attrs(self, attrs):
|
def _consume_body_attrs(self, attrs):
|
||||||
return self._consume_mapped_attrs(Body, attrs)
|
return self._consume_mapped_attrs(Body, attrs)
|
||||||
@ -539,6 +601,11 @@ class Resource(object):
|
|||||||
"""Return all URI members of this class"""
|
"""Return all URI members of this class"""
|
||||||
return cls._get_mapping(URI)
|
return cls._get_mapping(URI)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _computed_mapping(cls):
|
||||||
|
"""Return all URI members of this class"""
|
||||||
|
return cls._get_mapping(Computed)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _alternate_id(cls):
|
def _alternate_id(cls):
|
||||||
"""Return the name of any value known as an alternate_id
|
"""Return the name of any value known as an alternate_id
|
||||||
@ -585,7 +652,7 @@ class Resource(object):
|
|||||||
return cls(_synchronized=False, **kwargs)
|
return cls(_synchronized=False, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def existing(cls, **kwargs):
|
def existing(cls, connection=None, **kwargs):
|
||||||
"""Create an instance of an existing remote resource.
|
"""Create an instance of an existing remote resource.
|
||||||
|
|
||||||
When creating the instance set the ``_synchronized`` parameter
|
When creating the instance set the ``_synchronized`` parameter
|
||||||
@ -598,10 +665,14 @@ class Resource(object):
|
|||||||
:param dict kwargs: Each of the named arguments will be set as
|
:param dict kwargs: Each of the named arguments will be set as
|
||||||
attributes on the resulting Resource object.
|
attributes on the resulting Resource object.
|
||||||
"""
|
"""
|
||||||
return cls(_synchronized=True, **kwargs)
|
res = cls(_synchronized=True, **kwargs)
|
||||||
|
# TODO(shade) Done as a second call rather than a constructor param
|
||||||
|
# because otherwise the mocking in the tests goes nuts.
|
||||||
|
res._connection = connection
|
||||||
|
return res
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_munch(cls, obj, synchronized=True):
|
def _from_munch(cls, obj, synchronized=True, connection=None):
|
||||||
"""Create an instance from a ``munch.Munch`` object.
|
"""Create an instance from a ``munch.Munch`` object.
|
||||||
|
|
||||||
This is intended as a temporary measure to convert between shade-style
|
This is intended as a temporary measure to convert between shade-style
|
||||||
@ -611,16 +682,18 @@ class Resource(object):
|
|||||||
:param bool synchronized: whether this object already exists on server
|
:param bool synchronized: whether this object already exists on server
|
||||||
Must be set to ``False`` for newly created objects.
|
Must be set to ``False`` for newly created objects.
|
||||||
"""
|
"""
|
||||||
return cls(_synchronized=synchronized, **obj)
|
return cls(_synchronized=synchronized, connection=connection, **obj)
|
||||||
|
|
||||||
def to_dict(self, body=True, headers=True, ignore_none=False,
|
def to_dict(self, body=True, headers=True, computed=True,
|
||||||
original_names=False):
|
ignore_none=False, original_names=False):
|
||||||
"""Return a dictionary of this resource's contents
|
"""Return a dictionary of this resource's contents
|
||||||
|
|
||||||
:param bool body: Include the :class:`~openstack.resource.Body`
|
:param bool body: Include the :class:`~openstack.resource.Body`
|
||||||
attributes in the returned dictionary.
|
attributes in the returned dictionary.
|
||||||
:param bool headers: Include the :class:`~openstack.resource.Header`
|
:param bool headers: Include the :class:`~openstack.resource.Header`
|
||||||
attributes in the returned dictionary.
|
attributes in the returned dictionary.
|
||||||
|
:param bool computed: Include the :class:`~openstack.resource.Computed`
|
||||||
|
attributes in the returned dictionary.
|
||||||
:param bool ignore_none: When True, exclude key/value pairs where
|
:param bool ignore_none: When True, exclude key/value pairs where
|
||||||
the value is None. This will exclude
|
the value is None. This will exclude
|
||||||
attributes that the server hasn't returned.
|
attributes that the server hasn't returned.
|
||||||
@ -637,9 +710,11 @@ class Resource(object):
|
|||||||
components.append(Body)
|
components.append(Body)
|
||||||
if headers:
|
if headers:
|
||||||
components.append(Header)
|
components.append(Header)
|
||||||
|
if computed:
|
||||||
|
components.append(Computed)
|
||||||
if not components:
|
if not components:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"At least one of `body` or `headers` must be True")
|
"At least one of `body`, `headers` or `computed` must be True")
|
||||||
|
|
||||||
# isinstance stricly requires this to be a tuple
|
# isinstance stricly requires this to be a tuple
|
||||||
components = tuple(components)
|
components = tuple(components)
|
||||||
@ -1082,6 +1157,10 @@ class Resource(object):
|
|||||||
raw_resource.pop("self", None)
|
raw_resource.pop("self", None)
|
||||||
|
|
||||||
value = cls.existing(microversion=microversion, **raw_resource)
|
value = cls.existing(microversion=microversion, **raw_resource)
|
||||||
|
# TODO(shade) Done as a second call rather than a constructor
|
||||||
|
# param because otherwise the mocking in the tests goes nuts.
|
||||||
|
if hasattr(session, '_sdk_connection'):
|
||||||
|
value._connection = session._sdk_connection
|
||||||
marker = value.id
|
marker = value.id
|
||||||
yield value
|
yield value
|
||||||
total_yielded += 1
|
total_yielded += 1
|
||||||
@ -1189,6 +1268,10 @@ class Resource(object):
|
|||||||
# Try to short-circuit by looking directly for a matching ID.
|
# Try to short-circuit by looking directly for a matching ID.
|
||||||
try:
|
try:
|
||||||
match = cls.existing(id=name_or_id, **params)
|
match = cls.existing(id=name_or_id, **params)
|
||||||
|
# TODO(shade) Done as a second call rather than a constructor
|
||||||
|
# param because otherwise the mocking in the tests goes nuts.
|
||||||
|
if hasattr(session, '_sdk_connection'):
|
||||||
|
match._connection = session._sdk_connection
|
||||||
return match.fetch(session)
|
return match.fetch(session)
|
||||||
except exceptions.NotFoundException:
|
except exceptions.NotFoundException:
|
||||||
pass
|
pass
|
||||||
|
@ -401,17 +401,22 @@ class TestResource(base.TestCase):
|
|||||||
body = {"body": 1}
|
body = {"body": 1}
|
||||||
header = {"header": 2, "Location": "somewhere"}
|
header = {"header": 2, "Location": "somewhere"}
|
||||||
uri = {"uri": 3}
|
uri = {"uri": 3}
|
||||||
everything = dict(itertools.chain(body.items(), header.items(),
|
computed = {"computed": 4}
|
||||||
uri.items()))
|
everything = dict(itertools.chain(
|
||||||
|
body.items(),
|
||||||
|
header.items(),
|
||||||
|
uri.items(),
|
||||||
|
computed.items(),
|
||||||
|
))
|
||||||
|
|
||||||
mock_collect = mock.Mock()
|
mock_collect = mock.Mock()
|
||||||
mock_collect.return_value = body, header, uri
|
mock_collect.return_value = body, header, uri, computed
|
||||||
|
|
||||||
with mock.patch.object(resource.Resource,
|
with mock.patch.object(resource.Resource,
|
||||||
"_collect_attrs", mock_collect):
|
"_collect_attrs", mock_collect):
|
||||||
sot = resource.Resource(_synchronized=False, **everything)
|
sot = resource.Resource(_synchronized=False, **everything)
|
||||||
mock_collect.assert_called_once_with(everything)
|
mock_collect.assert_called_once_with(everything)
|
||||||
self.assertEqual("somewhere", sot.location)
|
self.assertIsNone(sot.location)
|
||||||
|
|
||||||
self.assertIsInstance(sot._body, resource._ComponentManager)
|
self.assertIsInstance(sot._body, resource._ComponentManager)
|
||||||
self.assertEqual(body, sot._body.dirty)
|
self.assertEqual(body, sot._body.dirty)
|
||||||
@ -433,6 +438,7 @@ class TestResource(base.TestCase):
|
|||||||
a = {"a": 1}
|
a = {"a": 1}
|
||||||
b = {"b": 2}
|
b = {"b": 2}
|
||||||
c = {"c": 3}
|
c = {"c": 3}
|
||||||
|
d = {"d": 4}
|
||||||
|
|
||||||
class Test(resource.Resource):
|
class Test(resource.Resource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -448,6 +454,10 @@ class TestResource(base.TestCase):
|
|||||||
self._uri.attributes.items = mock.Mock(
|
self._uri.attributes.items = mock.Mock(
|
||||||
return_value=c.items())
|
return_value=c.items())
|
||||||
|
|
||||||
|
self._computed = mock.Mock()
|
||||||
|
self._computed.attributes.items = mock.Mock(
|
||||||
|
return_value=d.items())
|
||||||
|
|
||||||
the_repr = repr(Test())
|
the_repr = repr(Test())
|
||||||
|
|
||||||
# Don't test the arguments all together since the dictionary order
|
# Don't test the arguments all together since the dictionary order
|
||||||
@ -456,6 +466,7 @@ class TestResource(base.TestCase):
|
|||||||
self.assertIn("a=1", the_repr)
|
self.assertIn("a=1", the_repr)
|
||||||
self.assertIn("b=2", the_repr)
|
self.assertIn("b=2", the_repr)
|
||||||
self.assertIn("c=3", the_repr)
|
self.assertIn("c=3", the_repr)
|
||||||
|
self.assertIn("d=4", the_repr)
|
||||||
|
|
||||||
def test_equality(self):
|
def test_equality(self):
|
||||||
class Example(resource.Resource):
|
class Example(resource.Resource):
|
||||||
@ -477,11 +488,14 @@ class TestResource(base.TestCase):
|
|||||||
body = "body"
|
body = "body"
|
||||||
header = "header"
|
header = "header"
|
||||||
uri = "uri"
|
uri = "uri"
|
||||||
|
computed = "computed"
|
||||||
|
|
||||||
sot._collect_attrs = mock.Mock(return_value=(body, header, uri))
|
sot._collect_attrs = mock.Mock(
|
||||||
|
return_value=(body, header, uri, computed))
|
||||||
sot._body.update = mock.Mock()
|
sot._body.update = mock.Mock()
|
||||||
sot._header.update = mock.Mock()
|
sot._header.update = mock.Mock()
|
||||||
sot._uri.update = mock.Mock()
|
sot._uri.update = mock.Mock()
|
||||||
|
sot._computed.update = mock.Mock()
|
||||||
|
|
||||||
args = {"arg": 1}
|
args = {"arg": 1}
|
||||||
sot._update(**args)
|
sot._update(**args)
|
||||||
@ -490,19 +504,7 @@ class TestResource(base.TestCase):
|
|||||||
sot._body.update.assert_called_once_with(body)
|
sot._body.update.assert_called_once_with(body)
|
||||||
sot._header.update.assert_called_once_with(header)
|
sot._header.update.assert_called_once_with(header)
|
||||||
sot._uri.update.assert_called_once_with(uri)
|
sot._uri.update.assert_called_once_with(uri)
|
||||||
|
sot._computed.update.assert_called_with(computed)
|
||||||
def test__collect_attrs(self):
|
|
||||||
sot = resource.Resource()
|
|
||||||
|
|
||||||
expected_attrs = ["body", "header", "uri"]
|
|
||||||
|
|
||||||
sot._consume_attrs = mock.Mock()
|
|
||||||
sot._consume_attrs.side_effect = expected_attrs
|
|
||||||
|
|
||||||
# It'll get passed an empty dict at the least.
|
|
||||||
actual_attrs = sot._collect_attrs(dict())
|
|
||||||
|
|
||||||
self.assertItemsEqual(expected_attrs, actual_attrs)
|
|
||||||
|
|
||||||
def test__consume_attrs(self):
|
def test__consume_attrs(self):
|
||||||
serverside_key1 = "someKey1"
|
serverside_key1 = "someKey1"
|
||||||
@ -538,7 +540,7 @@ class TestResource(base.TestCase):
|
|||||||
# Check that even on an empty class, we get the expected
|
# Check that even on an empty class, we get the expected
|
||||||
# built-in attributes.
|
# built-in attributes.
|
||||||
|
|
||||||
self.assertIn("location", resource.Resource._header_mapping())
|
self.assertIn("location", resource.Resource._computed_mapping())
|
||||||
self.assertIn("name", resource.Resource._body_mapping())
|
self.assertIn("name", resource.Resource._body_mapping())
|
||||||
self.assertIn("id", resource.Resource._body_mapping())
|
self.assertIn("id", resource.Resource._body_mapping())
|
||||||
|
|
||||||
@ -695,7 +697,8 @@ class TestResource(base.TestCase):
|
|||||||
expected = {
|
expected = {
|
||||||
'id': 'FAKE_ID',
|
'id': 'FAKE_ID',
|
||||||
'name': None,
|
'name': None,
|
||||||
'bar': None
|
'bar': None,
|
||||||
|
'location': None,
|
||||||
}
|
}
|
||||||
self.assertEqual(expected, res.to_dict(headers=False))
|
self.assertEqual(expected, res.to_dict(headers=False))
|
||||||
|
|
||||||
@ -744,10 +747,13 @@ class TestResource(base.TestCase):
|
|||||||
|
|
||||||
res = Test(id='FAKE_ID')
|
res = Test(id='FAKE_ID')
|
||||||
|
|
||||||
err = self.assertRaises(ValueError,
|
err = self.assertRaises(
|
||||||
res.to_dict, body=False, headers=False)
|
ValueError,
|
||||||
self.assertEqual('At least one of `body` or `headers` must be True',
|
res.to_dict,
|
||||||
six.text_type(err))
|
body=False, headers=False, computed=False)
|
||||||
|
self.assertEqual(
|
||||||
|
'At least one of `body`, `headers` or `computed` must be True',
|
||||||
|
six.text_type(err))
|
||||||
|
|
||||||
def test_to_dict_with_mro_no_override(self):
|
def test_to_dict_with_mro_no_override(self):
|
||||||
|
|
||||||
|
8
releasenotes/notes/shade-location-b0d2e5cae743b738.yaml
Normal file
8
releasenotes/notes/shade-location-b0d2e5cae743b738.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
The base ``Resource`` field ``location`` is no longer drawn from the
|
||||||
|
``Location`` HTTP header, but is instead a dict containing information
|
||||||
|
about cloud, domain and project. The location dict is a feature of shade
|
||||||
|
objects and is being added to all objects as part of the alignment of
|
||||||
|
shade and sdk.
|
Loading…
x
Reference in New Issue
Block a user