Skip to content

Commit

Permalink
Implemented Entity __eq__ method comparing all fields (#350) (#353)
Browse files Browse the repository at this point in the history
(cherry picked from commit ecf91be)
  • Loading branch information
renzon authored and rochacbruno committed Jan 16, 2017
1 parent 0c725fe commit 398e7c5
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 6 deletions.
15 changes: 15 additions & 0 deletions nailgun/entities.py
Expand Up @@ -24,8 +24,10 @@
import random
from datetime import datetime
from sys import version_info

from fauxfactory import gen_alphanumeric
from packaging.version import Version

from nailgun import client, entity_fields, signals
from nailgun.entity_mixins import (
Entity,
Expand All @@ -36,6 +38,7 @@
EntityUpdateMixin,
MissingValueError,
_poll_task,
to_json_serializable as to_json
)

if version_info.major == 2: # pragma: no cover
Expand Down Expand Up @@ -193,6 +196,18 @@ def _get_version(server_config):
return getattr(server_config, 'version', Version('1!0'))


def to_json_serializable(obj):
"""Just an alias to entity_mixins.to_json_seriazable so this module can
be used as a facade
:param obj: entity or any json serializable object
:return: serializable object
"""
return to_json(obj)


class ActivationKey(
Entity,
EntityCreateMixin,
Expand Down
103 changes: 98 additions & 5 deletions nailgun/entity_mixins.py
@@ -1,10 +1,14 @@
# -*- encoding: utf-8 -*-
"""Defines a set of mixins that provide tools for interacting with entities."""
import json as std_json
from collections import Iterable
from datetime import date, datetime

from fauxfactory import gen_choice
from inflection import pluralize
from nailgun import client, config, signals
from nailgun.entity_fields import IntegerField, OneToManyField, OneToOneField
from nailgun.entity_fields import (
IntegerField, OneToManyField, OneToOneField, ListField)
import threading
import time

Expand Down Expand Up @@ -79,6 +83,7 @@ def _poll_task(task_id, server_config, poll_rate=None, timeout=None):
def raise_task_timeout(): # pragma: no cover
"""Raise a KeyboardInterrupt exception in the main thread."""
thread.interrupt_main()

timer = threading.Timer(timeout, raise_task_timeout)

# Poll until the task finishes. The timeout prevents an infinite loop.
Expand Down Expand Up @@ -191,6 +196,15 @@ def _payload(fields, values):
values[field_name + '_ids'] = [
entity.id for entity in values.pop(field_name)
]
elif isinstance(field, ListField):
def parse(obj):
"""parse obj payload if it is an Entity"""
if isinstance(obj, Entity):
return _payload(obj.get_fields(), obj.get_values())
return obj

values[field_name] = [
parse(obj) for obj in values[field_name]]
return values


Expand Down Expand Up @@ -485,12 +499,11 @@ def get_fields(self):
def get_values(self):
"""Return a copy of field values on the current object.
This method is almost identical to ``vars(self).copy()``. However, only
instance attributes that correspond to a field are included in the
returned dict.
This method is almost identical to ``vars(self).copy()``. However,
only instance attributes that correspond to a field are included in
the returned dict.
:return: A dict mapping field names to user-provided values.
"""
attrs = vars(self).copy()
attrs.pop('_server_config')
Expand All @@ -510,6 +523,63 @@ def __repr__(self):
)
)

def to_json(self):
r"""Create a JSON encoded string with Entity properties. Ex:
>>> from nailgun import entities, config
>>> kwargs = {
... 'id': 1,
... 'name': 'Nailgun Org',
... }
>>> org = entities.Organization(config.ServerConfig('foo'), \*\*kwargs)
>>> org.to_json()
'{"id": 1, "name": "Nailgun Org"}'
:return: str
"""
return std_json.dumps(self.to_json_dict())

def to_json_dict(self):
"""Create a dct with Entity properties for json encoding.
It can be overridden by subclasses for each standard serialization
doesn't work. By default it call _to_json_dict on OneToOne fields
and build a list calling the same method on each object on OneToMany
fields.
:return: dct
"""
fields, values = self.get_fields(), self.get_values()
json_dct = {}
for field_name, field in fields.items():
if field_name in values:
value = values[field_name]
if value is None:
json_dct[field_name] = None
# This conditions is needed because some times you get
# None on an OneToOneField what lead to an error
# on bellow condition, e.g., calling value.to_json_dict()
# when value is None
elif isinstance(field, OneToOneField):
json_dct[field_name] = value.to_json_dict()
elif isinstance(field, OneToManyField):
json_dct[field_name] = [
entity.to_json_dict() for entity in value
]
else:
json_dct[field_name] = to_json_serializable(value)
return json_dct

def __eq__(self, other):
"""Compare two entities based on their properties. Even nested
objects are considered for equality
:param other: entity to compare self to
:return: boolean indicating if entities are equal or not
"""
if other is None:
return False
return self.to_json_dict() == other.to_json_dict()


class EntityDeleteMixin(object):
"""This mixin provides the ability to delete an entity.
Expand Down Expand Up @@ -1304,3 +1374,26 @@ def search_filter(entities, filters):
if getattr(entity, field_name) == field_value
]
return filtered


def to_json_serializable(obj):
""" Transforms obj into a json serializable object.
:param obj: entity or any json serializable object
:return: serializable object
"""
if isinstance(obj, Entity):
return obj.to_json_dict()

if isinstance(obj, dict):
return {k: to_json_serializable(v) for k, v in obj.items()}
elif isinstance(obj, (list, tuple)):
return [to_json_serializable(v) for v in obj]
elif isinstance(obj, datetime):
return obj.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(obj, date):
return obj.strftime('%Y-%m-%d')

return obj
90 changes: 89 additions & 1 deletion tests/test_entities.py
@@ -1,6 +1,7 @@
# -*- encoding: utf-8 -*-
"""Tests for :mod:`nailgun.entities`."""
from datetime import datetime
import json
from datetime import datetime, date
from fauxfactory import gen_integer, gen_string
from nailgun import client, config, entities
from nailgun.entity_mixins import (
Expand Down Expand Up @@ -29,6 +30,14 @@
# `nailgun.entities` and the Satellite API.


def make_entity(cls, **kwargs):
"""Helper function to create entity with dummy ServerConfig"""
cfg = config.ServerConfig(
url='https://foo.bar', verify=False,
auth=('foo', 'bar'))
return cls(cfg, **kwargs)


def _get_required_field_names(entity):
"""Get the names of all required fields from an entity.
Expand Down Expand Up @@ -1875,6 +1884,18 @@ def test_api_response_error(self):
entities._get_org(*self.args)
self.assertEqual(search.call_count, 1)

def test_to_json(self):
"""json serialization"""
kwargs = {
'id': 1,
'description': 'some description',
'label': 'some label',
'name': 'Nailgun Org',
'title': 'some title',
}
org = entities.Organization(config.ServerConfig('foo'), **kwargs)
self.assertEqual(kwargs, json.loads(org.to_json()))


class HandleResponseTestCase(TestCase):
"""Test ``nailgun.entities._handle_response``."""
Expand Down Expand Up @@ -2139,3 +2160,70 @@ def test_systempackage(self):
"""
with self.assertRaises(DeprecationWarning):
entities.SystemPackage(self.cfg_620)


class JsonSerializableTestCase(TestCase):
"""Test regarding Json serializable on different object"""

def test_regular_objects(self):
"""Checking regular objects transformation"""
lst = [[1, 0.3], {'name': 'foo'}]
self.assertEqual(lst, entities.to_json_serializable(lst))

def test_nested_entities(self):
"""Check nested entities serialization"""
env_kwargs = {'id': 1, 'name': 'env'}
env = make_entity(entities.Environment, **env_kwargs)

location_kwargs = {'name': 'loc'}
locations = [make_entity(entities.Location, **location_kwargs)]

hostgroup_kwargs = {'id': 2, 'name': 'hgroup'}
hostgroup = make_entity(
entities.HostGroup,
location=locations,
**hostgroup_kwargs)

hostgroup_kwargs['location'] = [location_kwargs]

combinations = [
{'environment_id': 3, 'hostgroup_id': 4},
make_entity(entities.TemplateCombination,
hostgroup=hostgroup,
environment=env)
]

expected_combinations = [
{'environment_id': 3, 'hostgroup_id': 4},
{'environment': env_kwargs, 'hostgroup': hostgroup_kwargs}
]

cfg_kwargs = {'id': 5, 'snippet': False, 'template': 'cat'}
cfg_template = make_entity(
entities.ConfigTemplate,
template_combinations=combinations,
**cfg_kwargs)

cfg_kwargs['template_combinations'] = expected_combinations
self.assertDictEqual(cfg_kwargs,
entities.to_json_serializable(cfg_template))

def test_date_field(self):
"""Check date field serialization"""

self.assertEqual(
'2016-09-20',
entities.to_json_serializable(date(2016, 9, 20))
)

def test_boolean_datetime_float(self):
"""Check serialization for boolean, datetime and float fields"""
kwargs = {
'pending': True,
'progress': 0.25,
'started_at': datetime(2016, 11, 20, 1, 2, 3)
}
task = make_entity(
entities.ForemanTask, **kwargs)
kwargs['started_at'] = '2016-11-20 01:02:03'
self.assertDictEqual(kwargs, entities.to_json_serializable(task))

0 comments on commit 398e7c5

Please sign in to comment.