Add base data model in order to improve data representation
The proposed data model improves the way how the information is structured and allows related entities to to inherit data.
This commit is contained in:
parent
ca738d46ee
commit
6251ca5344
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.eggs
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
.testrepository
|
||||
.venv
|
||||
cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Complexity
|
||||
output/*.html
|
||||
output/*/index.html
|
||||
|
||||
# Sphinx
|
||||
doc/build
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
|
||||
# Editors
|
||||
*~
|
||||
.*.swp
|
||||
.*sw?
|
10
.pylintrc
10
.pylintrc
@ -13,7 +13,7 @@ ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
ignore-patterns=test_*
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
@ -166,12 +166,12 @@ ignore-mixin-members=yes
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=hnv_client.tests
|
||||
ignored-modules=
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local, hnv_client.common.model.Model
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
@ -366,7 +366,7 @@ analyse-fallback-blocks=no
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
max-args=15
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
@ -391,7 +391,7 @@ max-parents=7
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
min-public-methods=1
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
@ -0,0 +1,15 @@
|
||||
# Copyright 2017 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Shared constants across the bcbio-nextgen-vm project."""
|
@ -51,6 +51,13 @@ class HNVException(Exception):
|
||||
super(HNVException, self).__init__(message)
|
||||
|
||||
|
||||
class DataProcessingError(HNVException):
|
||||
|
||||
"""Base exception class for data processing related errors."""
|
||||
|
||||
template = "The provided information is incomplete or invalid."
|
||||
|
||||
|
||||
class NotFound(HNVException):
|
||||
|
||||
"""The required object is not available in container."""
|
||||
|
308
hnv_client/common/model.py
Normal file
308
hnv_client/common/model.py
Normal file
@ -0,0 +1,308 @@
|
||||
# Copyright 2017 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""This module contains data models used across the project."""
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
import copy
|
||||
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
|
||||
from hnv_client.common import exception
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _FieldDescriptor(object):
|
||||
|
||||
"""Descriptor for all the available fields for a model.
|
||||
|
||||
Fields are exposed as descriptors in order to control access to the
|
||||
underlying raw data.
|
||||
"""
|
||||
|
||||
def __init__(self, field):
|
||||
self._field = field
|
||||
self._attribute = field.key
|
||||
|
||||
@property
|
||||
def field(self):
|
||||
"""Expose the received field object."""
|
||||
return self._field
|
||||
|
||||
def __get__(self, instance, instance_type=None):
|
||||
if instance is not None:
|
||||
return instance._data.get(self._attribute)
|
||||
return self._field
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if instance.provision_done:
|
||||
if self._field.is_read_only:
|
||||
raise TypeError("%r does not support item assignment" %
|
||||
self._field.name)
|
||||
|
||||
# Update the value of the field
|
||||
instance._data[self._attribute] = value
|
||||
# Keep track of the changes
|
||||
instance._changes[self._attribute] = value
|
||||
|
||||
|
||||
class Field(object):
|
||||
|
||||
"""Meta information regarding the data components.
|
||||
|
||||
:param name: The name of the current piece of information.
|
||||
:param key: The internal name for the current piece of
|
||||
information.
|
||||
:param default: Default value for the current field.
|
||||
(default: `None`)
|
||||
:param is_required: Whether the current piece of information is required
|
||||
for the container object or can be missing.
|
||||
(default: `False`)
|
||||
:param is_property: Whether the current piece of information is a
|
||||
propriety of the model. (default: `True`)
|
||||
:param is_read_only: Whether the current piece of information can
|
||||
be updated. (Default: `False`)
|
||||
"""
|
||||
|
||||
def __init__(self, name, key, default=None, is_required=False,
|
||||
is_property=True, is_read_only=False):
|
||||
self._name = name
|
||||
self._key = key
|
||||
self._default = default
|
||||
self._is_required = is_required
|
||||
self._is_property = is_property
|
||||
self._is_read_only = is_read_only
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the current field."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
"""The internal name of the current field."""
|
||||
return self._key
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
"""Default value for the current field."""
|
||||
return self._default
|
||||
|
||||
@property
|
||||
def is_required(self):
|
||||
"""Whether the current field is required or can be missing."""
|
||||
return self._is_required
|
||||
|
||||
@property
|
||||
def is_property(self):
|
||||
"""Whether the current field is a model property."""
|
||||
return self._is_property
|
||||
|
||||
@property
|
||||
def is_read_only(self):
|
||||
"""Whether the current field can be updated."""
|
||||
return self._is_read_only
|
||||
|
||||
def add_to_class(self, model_class):
|
||||
"""Replace the `Field` attribute with a named `_FieldDescriptor`.
|
||||
|
||||
.. note::
|
||||
This method is called during construction of the `Model`.
|
||||
"""
|
||||
model_class._meta.add_field(self)
|
||||
setattr(model_class, self.name, _FieldDescriptor(self))
|
||||
|
||||
|
||||
class _ModelOptions(object):
|
||||
|
||||
"""Container for all the model options.
|
||||
|
||||
.. note::
|
||||
The current object will be created by the model metaclass.
|
||||
"""
|
||||
|
||||
def __init__(self, cls):
|
||||
self._model_class = cls
|
||||
self._name = cls.__name__
|
||||
|
||||
self._fields = {}
|
||||
self._defaults = {}
|
||||
self._default_callables = {}
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
"""All the available fields for the current model."""
|
||||
return self._fields
|
||||
|
||||
def add_field(self, field):
|
||||
"""Add the received field to the model."""
|
||||
self.remove_field(field.name)
|
||||
self._fields[field.name] = field
|
||||
|
||||
if field.default is not None:
|
||||
if six.callable(field.default):
|
||||
self._default_callables[field.key] = field.default
|
||||
else:
|
||||
self._defaults[field.key] = field.default
|
||||
|
||||
def remove_field(self, field_name):
|
||||
"""Remove the field with the received field name from model."""
|
||||
field = self._fields.pop(field_name, None)
|
||||
if field is not None and field.default is not None:
|
||||
if six.callable(field.default):
|
||||
self._default_callables.pop(field.name, None)
|
||||
else:
|
||||
self._defaults.pop(field.name, None)
|
||||
|
||||
def get_defaults(self):
|
||||
"""Get a dictionary that contains all the available defaults."""
|
||||
defaults = self._defaults.copy()
|
||||
for field_name, default in self._default_callables.items():
|
||||
defaults[field_name] = default()
|
||||
return defaults
|
||||
|
||||
|
||||
class _BaseModel(type):
|
||||
|
||||
"""Metaclass used for properly setting up a new model."""
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
# The inherit is made by deep copying the underlying field into
|
||||
# the attributes of the new model.
|
||||
for base in bases:
|
||||
for key, attribute in base.__dict__.items():
|
||||
if key not in attrs and isinstance(attribute,
|
||||
_FieldDescriptor):
|
||||
attrs[key] = copy.deepcopy(attribute.field)
|
||||
|
||||
# Initialize the new class and set the magic attributes
|
||||
cls = super(_BaseModel, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
# Create the _ModelOptions object and inject it in the new class
|
||||
setattr(cls, "_meta", _ModelOptions(cls))
|
||||
|
||||
# Get all the available fields for the current model.
|
||||
for name, field in list(cls.__dict__.items()):
|
||||
if isinstance(field, Field) and not name.startswith("_"):
|
||||
field.add_to_class(cls)
|
||||
|
||||
# Create string representation for the current model before finalizing
|
||||
setattr(cls, '__str__', lambda self: '%s' % cls.__name__)
|
||||
return cls
|
||||
|
||||
|
||||
@six.add_metaclass(_BaseModel)
|
||||
class Model(object):
|
||||
|
||||
"""Container for meta information regarding the data structure."""
|
||||
|
||||
def __init__(self, **fields):
|
||||
self._data = self._meta.get_defaults()
|
||||
self._changes = {}
|
||||
self._provision_done = False
|
||||
|
||||
for field in self._meta.fields.values():
|
||||
value = fields.pop(field.name, None)
|
||||
if field.key not in self._data or value:
|
||||
setattr(self, field.name, value)
|
||||
|
||||
if fields:
|
||||
LOG.debug("Unrecognized fields: %r", fields)
|
||||
|
||||
self._provision_done = True
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
|
||||
my_properties = self.dump().get("properties", {})
|
||||
other_properties = other.dump().get("properties", {})
|
||||
return my_properties == other_properties
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@classmethod
|
||||
def from_raw_data(cls, raw_data):
|
||||
"""Create a new model using raw API response."""
|
||||
content = {}
|
||||
properties = raw_data.pop("properties", {})
|
||||
for field_name, field in cls._meta.fields.items():
|
||||
if field.is_property:
|
||||
value = properties.pop(field.key, None)
|
||||
else:
|
||||
value = raw_data.pop(field.key, None)
|
||||
content[field_name] = value
|
||||
|
||||
if raw_data:
|
||||
LOG.debug("Unrecognized fields: %r", raw_data)
|
||||
if properties:
|
||||
LOG.debug("Unrecognized properties: %r", properties)
|
||||
|
||||
return cls(**content)
|
||||
|
||||
@property
|
||||
def provision_done(self):
|
||||
"""Whether the creation of the model is complete."""
|
||||
return self._provision_done
|
||||
|
||||
def validate(self):
|
||||
"""Check if the current model was properly created."""
|
||||
for field_name, field in self._meta.fields.items():
|
||||
if field.is_required and self._data.get(field.name) is None:
|
||||
raise exception.DataProcessingError(
|
||||
"The required field %(field)r is missing.",
|
||||
field=field_name)
|
||||
|
||||
def update(self, fields=None):
|
||||
"""Update the value of one or more fields."""
|
||||
for field_name, field in self._meta.fields.items():
|
||||
if field_name in fields:
|
||||
self._changes[field.key] = fields[field_name]
|
||||
self._data.update(self._changes)
|
||||
|
||||
def commit(self, wait=False, timeout=None):
|
||||
"""Apply all the changes on the current model."""
|
||||
# pylint: disable=unused-argument
|
||||
self._data.update(self._changes)
|
||||
self._changes.clear()
|
||||
|
||||
def dump(self, include_read_only=True):
|
||||
"""Create a dictionary with the content of the current model."""
|
||||
content = {}
|
||||
for field in self._meta.fields.values():
|
||||
if field.is_read_only and not include_read_only:
|
||||
continue
|
||||
|
||||
value = self._data.get(field.key)
|
||||
if isinstance(value, Model):
|
||||
# The raw content of the model is required
|
||||
value = value.dump()
|
||||
|
||||
if not field.is_required and value is None:
|
||||
# The value of this field is not relevant
|
||||
continue
|
||||
|
||||
if field.is_property:
|
||||
# The current field is a property and its value should
|
||||
# be stored into the `properties` key.
|
||||
properties = content.setdefault("properties", {})
|
||||
properties[field.key] = value
|
||||
else:
|
||||
content[field.key] = value
|
||||
|
||||
return content
|
0
hnv_client/tests/common/__init__.py
Normal file
0
hnv_client/tests/common/__init__.py
Normal file
238
hnv_client/tests/common/test_model.py
Normal file
238
hnv_client/tests/common/test_model.py
Normal file
@ -0,0 +1,238 @@
|
||||
# Copyright 2017 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# pylint: disable=protected-access, missing-docstring
|
||||
|
||||
import unittest
|
||||
|
||||
try:
|
||||
import unittest.mock as mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from hnv_client.common import exception
|
||||
from hnv_client.common import model
|
||||
|
||||
|
||||
class TestFieldDescriptor(unittest.TestCase):
|
||||
|
||||
def test_field_access(self):
|
||||
instance = mock.Mock()
|
||||
field = mock.Mock()
|
||||
field_descriptor = model._FieldDescriptor(field)
|
||||
|
||||
self.assertIs(field, field_descriptor.__get__(None))
|
||||
|
||||
field_descriptor.__get__(instance)
|
||||
instance._data.get.assert_called_once_with(field.key)
|
||||
|
||||
def test_set_field(self):
|
||||
instance = mock.MagicMock()
|
||||
instance._changes = {}
|
||||
|
||||
field = mock.Mock()
|
||||
field.is_read_only = False
|
||||
field_descriptor = model._FieldDescriptor(field)
|
||||
field_descriptor.__set__(instance, mock.sentinel.value)
|
||||
|
||||
self.assertIs(instance._changes[field.key], mock.sentinel.value)
|
||||
|
||||
def test_set_read_only_field(self):
|
||||
instance = mock.MagicMock()
|
||||
|
||||
field = mock.Mock()
|
||||
field.is_read_only = True
|
||||
field_descriptor = model._FieldDescriptor(field)
|
||||
with self.assertRaises(TypeError):
|
||||
field_descriptor.__set__(instance, mock.sentinel.value)
|
||||
|
||||
def test_field_property(self):
|
||||
field = mock.Mock()
|
||||
field_descriptor = model._FieldDescriptor(field)
|
||||
|
||||
self.assertIs(field_descriptor.field, field)
|
||||
|
||||
|
||||
class TestField(unittest.TestCase):
|
||||
|
||||
def test_properties(self):
|
||||
field = model.Field(name=mock.sentinel.name,
|
||||
key=mock.sentinel.name,
|
||||
default=mock.sentinel.default,
|
||||
is_required=mock.sentinel.is_required,
|
||||
is_property=mock.sentinel.is_property,
|
||||
is_read_only=mock.sentinel.is_read_only)
|
||||
|
||||
self.assertIs(field.name, mock.sentinel.name)
|
||||
self.assertIs(field.default, mock.sentinel.default)
|
||||
self.assertIs(field.is_required, mock.sentinel.is_required)
|
||||
self.assertIs(field.is_property, mock.sentinel.is_property)
|
||||
self.assertIs(field.is_read_only, mock.sentinel.is_read_only)
|
||||
|
||||
@mock.patch("hnv_client.common.model._FieldDescriptor")
|
||||
def test_add_to_class(self, mock_field_descriptor):
|
||||
field = model.Field(name="test_add_to_class", key="test")
|
||||
model_class = mock.Mock()
|
||||
|
||||
field.add_to_class(model_class)
|
||||
|
||||
mock_field_descriptor.assert_called_once_with(field)
|
||||
self.assertIsNotNone(getattr(model_class, "test_add_to_class"))
|
||||
|
||||
|
||||
class TestModelOptions(unittest.TestCase):
|
||||
|
||||
def test_initialization(self):
|
||||
mock.sentinel.cls.__name__ = mock.sentinel.cls.name
|
||||
model_options = model._ModelOptions(cls=mock.sentinel.cls)
|
||||
|
||||
self.assertIs(model_options._model_class, mock.sentinel.cls)
|
||||
self.assertEqual(model_options._name, mock.sentinel.cls.name)
|
||||
|
||||
@mock.patch("six.callable")
|
||||
@mock.patch("hnv_client.common.model._ModelOptions.remove_field")
|
||||
def _test_add_field(self, mock_remove_field, mock_callable,
|
||||
callable_default):
|
||||
model_options = model._ModelOptions(self.__class__)
|
||||
test_field = model.Field(name=mock.sentinel.name,
|
||||
key=mock.sentinel.key,
|
||||
is_required=False,
|
||||
default=mock.sentinel.default)
|
||||
mock_callable.return_value = callable_default
|
||||
|
||||
model_options.add_field(test_field)
|
||||
|
||||
mock_remove_field.assert_called_once_with(mock.sentinel.name)
|
||||
self.assertIs(model_options._fields[test_field.name], test_field)
|
||||
if callable_default:
|
||||
self.assertIs(model_options._default_callables[test_field.key],
|
||||
mock.sentinel.default)
|
||||
else:
|
||||
self.assertIs(model_options._defaults[test_field.key],
|
||||
mock.sentinel.default)
|
||||
|
||||
def test_add_field(self):
|
||||
self._test_add_field(callable_default=False)
|
||||
self._test_add_field(callable_default=True)
|
||||
|
||||
@mock.patch("six.callable")
|
||||
def _test_remove_field(self, mock_callable, callable_default):
|
||||
mock_callable.return_value = callable_default
|
||||
model_options = model._ModelOptions(self.__class__)
|
||||
test_field = model.Field(name=mock.sentinel.name,
|
||||
key="test_field",
|
||||
is_required=False,
|
||||
default=mock.sentinel.default)
|
||||
model_options.add_field(test_field)
|
||||
|
||||
model_options.remove_field(test_field.name)
|
||||
|
||||
self.assertNotIn(test_field.name, model_options._fields)
|
||||
if callable_default:
|
||||
self.assertNotIn(test_field.name,
|
||||
model_options._default_callables)
|
||||
else:
|
||||
self.assertNotIn(test_field.name, model_options._defaults)
|
||||
|
||||
def test_remove_field(self):
|
||||
self._test_remove_field(callable_default=False)
|
||||
self._test_remove_field(callable_default=True)
|
||||
|
||||
def test_get_defaults(self):
|
||||
test_field = model.Field(
|
||||
name=mock.sentinel.name, key=mock.sentinel.key,
|
||||
is_required=False, default=lambda: mock.sentinel.default)
|
||||
model_options = model._ModelOptions(self.__class__)
|
||||
model_options.add_field(test_field)
|
||||
|
||||
defaults = model_options.get_defaults()
|
||||
|
||||
self.assertEqual(defaults, {mock.sentinel.key: mock.sentinel.default})
|
||||
|
||||
|
||||
class TestBaseModel(unittest.TestCase):
|
||||
|
||||
def test_create_model(self):
|
||||
|
||||
class _Test(model.Model):
|
||||
field1 = model.Field(name="field1", key="field1", default=1)
|
||||
|
||||
self.assertTrue(hasattr(_Test, "_meta"))
|
||||
self.assertEqual(_Test().field1, 1)
|
||||
|
||||
def test_inherit_fields(self):
|
||||
|
||||
class _TestBase(model.Model):
|
||||
field1 = model.Field(name="field1", key="key1",
|
||||
default=1)
|
||||
field2 = model.Field(name="field2", key="key2")
|
||||
|
||||
class _Test(_TestBase):
|
||||
field2 = model.Field(name="field2", key="key2",
|
||||
default=2)
|
||||
|
||||
class _FinalTest(_Test):
|
||||
field3 = model.Field(name="field3", key="key3",
|
||||
is_required=True)
|
||||
|
||||
final_test = _FinalTest(field3=3)
|
||||
self.assertEqual(final_test.field1, 1)
|
||||
self.assertEqual(final_test.field2, 2)
|
||||
self.assertEqual(final_test.field3, 3)
|
||||
|
||||
|
||||
class TestModel(unittest.TestCase):
|
||||
|
||||
class _Test(model.Model):
|
||||
field1 = model.Field(name="field1", key="key1",
|
||||
is_required=True,
|
||||
is_property=False)
|
||||
field2 = model.Field(name="field2", key="key2",
|
||||
is_required=False,
|
||||
is_property=False)
|
||||
field3 = model.Field(name="field3", key="key3",
|
||||
is_required=True,
|
||||
is_property=True)
|
||||
|
||||
def setUp(self):
|
||||
self._raw_data = {"key1": 1, "key2": 2, "properties": {"key3": 3}}
|
||||
|
||||
def test_model_eq(self):
|
||||
test = self._Test(field1=1, field3=2)
|
||||
test2 = self._Test(field1=1, field3=3)
|
||||
self.assertFalse(test == self)
|
||||
self.assertTrue(test == test)
|
||||
self.assertFalse(test == test2)
|
||||
|
||||
def test_validate(self):
|
||||
test = self._Test(field1=1, key="test_validate")
|
||||
self.assertRaises(exception.DataProcessingError, test.validate)
|
||||
|
||||
def test_update(self):
|
||||
test = self._Test(field1="change_me", field2="change_me")
|
||||
test.update({"field1": mock.sentinel.field1,
|
||||
"field2": mock.sentinel.field2})
|
||||
|
||||
self.assertIs(test.field1, mock.sentinel.field1)
|
||||
self.assertIs(test.field2, mock.sentinel.field2)
|
||||
|
||||
def test_dump(self):
|
||||
test = self._Test(field1=1, field2=2, field3=3)
|
||||
self.assertEqual(test.dump(), self._raw_data)
|
||||
|
||||
def test_from_raw_data(self):
|
||||
test = self._Test.from_raw_data(self._raw_data)
|
||||
self.assertEqual(test.field1, 1)
|
||||
self.assertEqual(test.field2, 2)
|
||||
self.assertEqual(test.field3, 3)
|
@ -1,4 +1,7 @@
|
||||
pbr>=1.8
|
||||
six>=1.7.0
|
||||
oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.log>=3.11.0 # Apache-2.0
|
||||
requests
|
||||
requests_ntlm
|
||||
|
Loading…
x
Reference in New Issue
Block a user