From dac388f426e59c1cadcbe7f632bef7124800433e Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 6 Jul 2016 09:46:40 -0400 Subject: [PATCH 01/10] Restructured tests --- tests/test_fields.py | 171 ++++++++++++++++++++----------------------- tests/test_models.py | 25 ++++--- 2 files changed, 93 insertions(+), 103 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 518af4a..5142e57 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,119 +1,95 @@ # -*- coding: utf-8 -*- -from unittest import TestCase +from decimal import Decimal, InvalidOperation +from unittest import TestCase from rest_orm import errors, fields, models -class FieldsTestCase(TestCase): +class FieldTestCase(TestCase): - def test_deserialize_deeply_nested_field(self): - """Test deserializing a heavily nested field.""" - field = fields.AdaptedField('[x][y][z]') - value = field.deserialize({'x': {'y': {'z': 'value'}}}) + def test_deserialize_missing_value(self): + """Test replacing a missing value with the specified default.""" + field = fields.Field('[key]', missing='value') + value = field.deserialize({}) self.assertTrue(value == 'value') - def test_deserialize_list_item(self): - """Test deserializing a list item.""" - field = fields.AdaptedField('[1]') - value = field.deserialize([1, 2, 3]) - self.assertTrue(value == 2) - def test_validate_required(self): - """Test validating a missing value when required is `True`.""" - field = fields.AdaptedField('[x]', required=True) - try: - field.deserialize({'z': 1}) - self.assertTrue(False) - except KeyError: - self.assertTrue(True) +class BooleanTestCase(TestCase): - def test_replace_missing_value(self): - """Test replacing a missing value with the specified default.""" - field = fields.AdaptedField('[key]', missing='value') - value = field.deserialize({'x': 1}) - self.assertTrue(value == 'value') + def test_deserialize_value(self): + field = fields.Boolean('[key]') + value = field.deserialize({'key': 1}) + self.assertTrue(value is True) + + +class DateTestCase(TestCase): - def test_nullable_field(self): - """Test skipping validation when field is `None`.""" - field = fields.AdaptedDate('[key]', nullable=True) - value = field.deserialize({'key': None}) - self.assertTrue(value is None) + def test_deserialize_value(self): + field = fields.Date('[key]') + value = field.deserialize({'key': '2015-01-31'}) + self.assertTrue(value.year == 2015) + self.assertTrue(value.month == 1) + self.assertTrue(value.day == 31) - def test_validate_deserialized_value(self): - """Test validating a deserialized value.""" - def validate_input(value): - if value > 10: - return - raise errors.AdapterError('Invalid value.') + def test_deserialize_formatted_value(self): + field = fields.Date('[key]', date_format='%m/%d/%Y') + value = field.deserialize({'key': '01/31/2015'}) + self.assertTrue(value.year == 2015) + self.assertTrue(value.month == 1) + self.assertTrue(value.day == 31) - field = fields.AdaptedField('[x]', validate=validate_input) + def test_deserialize_invalid_value(self): try: - field.deserialize({'x': 5}) + fields.Date('[key]').deserialize({'key': 'AB'}) self.assertTrue(False) - except errors.AdapterError: + except ValueError: self.assertTrue(True) - value = field.deserialize({'x': 11}) - self.assertTrue(value == 11) - - def test_adapted_boolean_deserialization(self): - """Test deserializing boolean field.""" - field = fields.AdaptedBoolean('[x]') - value = field.deserialize({'x': 1}) - self.assertTrue(value is True) +class DecimalTestCase(TestCase): - value = field.deserialize({'x': 'hello'}) - self.assertTrue(value is True) + def test_deserialize_value(self): + field = fields.Decimal('[key]') + value = field.deserialize({'key': '10.50'}) + self.assertTrue(value == Decimal('10.50')) - value = field.deserialize({'x': True}) - self.assertTrue(value is True) - - value = field.deserialize({'x': 0}) - self.assertTrue(value is False) - - value = field.deserialize({'x': ''}) - self.assertTrue(value is False) + def test_deserialize_invalid_value(self): + try: + fields.Decimal('[key]').deserialize({'key': 'AB'}) + self.assertTrue(False) + except InvalidOperation: + self.assertTrue(True) - def test_adapted_date_deserialization(self): - """Test deserializing date field.""" - field = fields.AdaptedDate('[x]') - value = field.deserialize({'x': '2015-01-01'}) - self.assertTrue(value.year == 2015) - self.assertTrue(value.month == 1) - self.assertTrue(value.day == 1) - field = fields.AdaptedDate('[x]', date_format='%m/%d/%Y') - value = field.deserialize({'x': '01/01/2015'}) - self.assertTrue(value.year == 2015) - self.assertTrue(value.month == 1) - self.assertTrue(value.day == 1) +class IntegerTestCase(TestCase): - def test_adapted_decimal_deserialization(self): - """Test deserializing decimal field.""" - from decimal import Decimal + def test_deserialize_value(self): + field = fields.Integer('[key]') + value = field.deserialize({'key': '10'}) + self.assertTrue(value == 10) - field = fields.AdaptedDecimal('[x]') - value = field.deserialize({'x': '12.50'}) + def test_deserialize_invalid_value(self): + try: + fields.Integer('[key]').deserialize({'key': 'AB'}) + self.assertTrue(False) + except ValueError: + self.assertTrue(True) - self.assertTrue(value == 12.50) - self.assertTrue(isinstance(value, Decimal)) - def test_adapted_integer_deserialization(self): - """Test deserializing integer field.""" - field = fields.AdaptedInteger('[x]') - value = field.deserialize({'x': '1'}) - self.assertTrue(value == 1) +class FunctionTestCase(TestCase): def test_adapted_function_deserialization(self): """Test deserializing function field.""" - field = fields.AdaptedFunction(lambda x: 'value', path='[x]') + field = fields.Function(lambda x: 'value', path='[x]') value = field.deserialize({'x': 'anything'}) self.assertTrue(value == 'value') - def test_adapted_list_deserialization(self): + +class ListTestCase(TestCase): + + def test_deserialize_value(self): """Test deserializing list field.""" - field = fields.AdaptedList('[x]') + field = fields.List('[x]') value = field.deserialize({'x': 1}) self.assertTrue(value == [1]) @@ -121,19 +97,32 @@ def test_adapted_list_deserialization(self): value = field.deserialize({'x': ['hi']}) self.assertTrue(value == ['hi']) - def test_adapted_nested_deserialization(self): + def test_deserialize_value_from_position(self): + """Test deserializing a list item.""" + field = fields.Field('[1]') + value = field.deserialize([1, 2, 3]) + self.assertTrue(value == 2) + + +class NestedTestCase(TestCase): + + def test_deserialize_value(self): """Test deserializing nested field.""" - class Test(models.AdaptedModel): - number = fields.AdaptedInteger('[y]') + class Test(models.Model): + number = fields.Integer('[y]') - field = fields.AdaptedNested(Test, path='[x]') + field = fields.Nested(Test, path='[x]') value = field.deserialize({'x': {'y': 1}}) - self.assertTrue(isinstance(value, Test)) - self.assertTrue(value.number == 1) + self.assertTrue(isinstance(value, dict)) + self.assertIn('number', value) + self.assertTrue(value['number'] == 1) + + +class StringTestCase(TestCase): - def test_adapted_string_deserialization(self): + def test_deserialize_value(self): """Test deserializing string field.""" - field = fields.AdaptedString('[x]') + field = fields.String('[x]') value = field.deserialize({'x': 1}) self.assertTrue(value == '1') diff --git a/tests/test_models.py b/tests/test_models.py index d5e2c81..836d08c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -4,34 +4,35 @@ from rest_orm import fields, models -class TestModel(models.AdaptedModel): - first = fields.AdaptedString('[first]') +class TestModel(models.Model): + first = fields.String('[first]') def make_request(self): return '{"first": "First Name"}' - def post_load(self): - self.full_name = '{} Last Name'.format(self.first) + def post_load(self, data): + data['full_name'] = '{} Last Name'.format(data['first']) + return data class ModelTestCase(TestCase): def test_model_connect(self): """Test loading the response from the connect method.""" - model = TestModel().connect() - self.assertTrue(model.first == 'First Name') + result = TestModel().connect() + self.assertTrue(result['first'] == 'First Name') def test_model_loads(self): """Test loading the json from the loads method.""" - model = TestModel().loads('{"first": "First Name"}') - self.assertTrue(model.first == 'First Name') + result = TestModel().loads('{"first": "First Name"}') + self.assertTrue(result['first'] == 'First Name') def test_model_load(self): """Test loading the python object from the load method.""" - model = TestModel().load({"first": "First Name"}) - self.assertTrue(model.first == 'First Name') + result = TestModel().load({"first": "First Name"}) + self.assertTrue(result['first'] == 'First Name') def test_model_post_load(self): """Test post-load actions created from the post_load method.""" - model = TestModel().load({"first": "First Name"}) - self.assertTrue(model.full_name == 'First Name Last Name') + result = TestModel().load({"first": "First Name"}) + self.assertTrue(result['full_name'] == 'First Name Last Name') From e9334815e25647a05fcd83b775fb5ebc3a5bab41 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 6 Jul 2016 09:47:19 -0400 Subject: [PATCH 02/10] Restructured model to return a seperate datatype --- rest_orm/models.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/rest_orm/models.py b/rest_orm/models.py index 14575b3..5491a83 100644 --- a/rest_orm/models.py +++ b/rest_orm/models.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from rest_orm.fields import AdaptedField +from rest_orm.fields import Field from rest_orm.utils import ModelRegistry import json @@ -11,34 +11,33 @@ class BaseModel(object): __metaclass__ = ModelRegistry -class AdaptedModel(BaseModel): +class Model(BaseModel): """A flat representation of a single remote endpoint.""" + def loads(self, data): + """Load a JSON string to a flattened dictionary.""" + return self.load(json.loads(data)) + + def load(self, data): + """Flatten a nested dictionary.""" + response = {} + for field_name in dir(self): + field = getattr(self, field_name) + if not isinstance(field, Field): + continue + response[field_name] = field.deserialize(data) + + response = self.post_load(response) + return response + def connect(self, *args, **kwargs): """Make a request to a remote endpoint and load its JSON response.""" response = self.make_request(*args, **kwargs) return self.loads(response) - def loads(self, response): - """Marshal a JSON response object into the model.""" - return self.load(json.loads(response)) - - def load(self, response): - """Marshal a python dictionary object into the model.""" - self._do_load(response) - self.post_load() - return self - - def post_load(self): + def post_load(self, data): """Perform any model level actions after load.""" - pass - - def _do_load(self, data): - for field_name in dir(self): - field = getattr(self, field_name) - if not isinstance(field, AdaptedField): - continue - setattr(self, field_name, field.deserialize(data)) + return data def make_request(self): """Return the response data of a remote endpoint.""" From aaca762942c25dcac0ec9a4083a93c2a03df6fa3 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 6 Jul 2016 09:48:51 -0400 Subject: [PATCH 03/10] Simplified field interactions and removed Adapted prefix --- rest_orm/fields.py | 65 ++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/rest_orm/fields.py b/rest_orm/fields.py index 335a250..6130904 100644 --- a/rest_orm/fields.py +++ b/rest_orm/fields.py @@ -1,33 +1,26 @@ # -*- coding: utf-8 -*- from datetime import datetime -from decimal import Decimal +from decimal import Decimal as PyDecimal from rest_orm.utils import get_class -class AdaptedField(object): +class Field(object): """Flat representaion of remote endpoint's field. - `AdaptedField` and its child classes are self-destructive. Once + `Field` and its child classes are self-destructive. Once deserialization is complete, the instance is replaced by the typed value retrieved. """ - def __init__(self, path, missing=None, nullable=True, required=False, - validate=None): + def __init__(self, path, missing=None): """Key extraction strategy and settings. :param path: A formattable string path. :param missing: The default deserialization value. - :param nullable: If `False`, disallow `None` type values. - :param required: If `True`, raise an error if the key is missing. - :param validate: A callable object. """ self.path = path self.missing = missing - self.nullable = nullable - self.required = required - self.validate = validate def deserialize(self, data): """Extract a value from the provided data object. @@ -38,28 +31,14 @@ def deserialize(self, data): return self._deserialize(data) try: - raw_value = self.map_from_string(self.path, data) + value = self.map_from_string(self.path, data) except (KeyError, IndexError): - if self.required: - raise KeyError('{} not found.'.format(self.path)) value = self.missing - else: - if raw_value is None and self.nullable: - value = None - else: - value = self._deserialize(raw_value) - - self._validate(value) - return value + return self._deserialize(value) def _deserialize(self, value): return value - def _validate(self, value): - if self.validate is not None: - self.validate(value) - return None - def map_from_string(self, path, data): """Return nested value from the string path taken. @@ -77,50 +56,50 @@ def extract_by_type(path): return data -class AdaptedBoolean(AdaptedField): +class Boolean(Field): """Parse an adapted field into the boolean type.""" def _deserialize(self, value): return bool(value) -class AdaptedDate(AdaptedField): +class Date(Field): """Parse an adapted field into the datetime type.""" def __init__(self, *args, **kwargs): self.date_format = kwargs.pop('date_format', '%Y-%m-%d') - super(AdaptedDate, self).__init__(*args, **kwargs) + super(Date, self).__init__(*args, **kwargs) def _deserialize(self, value): return datetime.strptime(value, self.date_format) -class AdaptedDecimal(AdaptedField): +class Decimal(Field): """Parse an adapted field into the decimal type.""" def _deserialize(self, value): - return Decimal(value) + return PyDecimal(value) -class AdaptedInteger(AdaptedField): +class Integer(Field): """Parse an adapted field into the integer type.""" def _deserialize(self, value): return int(value) -class AdaptedFunction(AdaptedField): +class Function(Field): """Parse an adapted field into a specified function's output.""" def __init__(self, f, *args, **kwargs): self.f = f - super(AdaptedFunction, self).__init__(*args, **kwargs) + super(Function, self).__init__(*args, **kwargs) def _deserialize(self, value): return self.f(value) -class AdaptedList(AdaptedField): +class List(Field): """Parse an adapted field into the list type.""" def _deserialize(self, value): @@ -129,20 +108,20 @@ def _deserialize(self, value): return value -class AdaptedNested(AdaptedField): - """Parse an adatped field into the AdaptedModel type.""" +class Nested(Field): + """Parse an adatped field into the Model type.""" def __init__(self, model, *args, **kwargs): - """Parse a list of nested objects into an AdaptedModel. + """Parse a list of nested objects into an Model. - :param model: AdaptedModel name or reference. + :param model: Model name or reference. """ self.nested_model = model - super(AdaptedNested, self).__init__(*args, **kwargs) + super(Nested, self).__init__(*args, **kwargs) @property def model(self): - """Return an AdaptedModel reference.""" + """Return an Model reference.""" if isinstance(self.nested_model, str): return get_class(self.nested_model) return self.nested_model @@ -153,7 +132,7 @@ def _deserialize(self, value): return self.model().load(value) -class AdaptedString(AdaptedField): +class String(Field): """Parse an adapted field into the string type.""" def _deserialize(self, value): From 7c565f8b4a1f1b203603579a03bb48eb22c08e5d Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 6 Jul 2016 11:01:11 -0400 Subject: [PATCH 04/10] Not deserializing null values --- rest_orm/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_orm/fields.py b/rest_orm/fields.py index 6130904..7eaa255 100644 --- a/rest_orm/fields.py +++ b/rest_orm/fields.py @@ -34,6 +34,8 @@ def deserialize(self, data): value = self.map_from_string(self.path, data) except (KeyError, IndexError): value = self.missing + if value is None: + return value return self._deserialize(value) def _deserialize(self, value): From ce98ea236eed5c74571a675901ec6d81eff0cb31 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 6 Jul 2016 12:46:35 -0400 Subject: [PATCH 05/10] Added dump field --- rest_orm/fields.py | 10 ++++++++++ tests/test_fields.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/rest_orm/fields.py b/rest_orm/fields.py index 7eaa255..cb6ac32 100644 --- a/rest_orm/fields.py +++ b/rest_orm/fields.py @@ -76,6 +76,16 @@ def _deserialize(self, value): return datetime.strptime(value, self.date_format) +class Dump(Field): + """Return a pre-determined value.""" + + def __init__(self, value): + self.value = value + + def deserialize(self, data): + return self.value + + class Decimal(Field): """Parse an adapted field into the decimal type.""" diff --git a/tests/test_fields.py b/tests/test_fields.py index 5142e57..86240a6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -46,6 +46,14 @@ def test_deserialize_invalid_value(self): self.assertTrue(True) +class DumpTestCase(TestCase): + + def test_deserialize_value(self): + field = fields.Dump('value') + value = field.deserialize({}) + self.assertTrue(value == 'value') + + class DecimalTestCase(TestCase): def test_deserialize_value(self): From 53c059a032e1c348f15c0676c7ce8991d548c184 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 12 Jul 2016 09:43:14 -0400 Subject: [PATCH 06/10] Copying deserialized output --- rest_orm/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_orm/models.py b/rest_orm/models.py index 5491a83..1f4e1f7 100644 --- a/rest_orm/models.py +++ b/rest_orm/models.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from copy import copy + from rest_orm.fields import Field from rest_orm.utils import ModelRegistry @@ -25,7 +27,7 @@ def load(self, data): field = getattr(self, field_name) if not isinstance(field, Field): continue - response[field_name] = field.deserialize(data) + response[field_name] = copy(field.deserialize(data)) response = self.post_load(response) return response From 3e41c6049ab453a08cbab5e5bffd401070a6c737 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 12 Jul 2016 09:45:10 -0400 Subject: [PATCH 07/10] Added coverage for pass by value --- tests/test_models.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index 836d08c..c6991be 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -6,12 +6,14 @@ class TestModel(models.Model): first = fields.String('[first]') + x = fields.List('[x]', missing=[]) def make_request(self): return '{"first": "First Name"}' def post_load(self, data): data['full_name'] = '{} Last Name'.format(data['first']) + data['x'].append(1) return data @@ -36,3 +38,11 @@ def test_model_post_load(self): """Test post-load actions created from the post_load method.""" result = TestModel().load({"first": "First Name"}) self.assertTrue(result['full_name'] == 'First Name Last Name') + + def test_model_missing_by_value(self): + """Ensure that the field instance is not mutated by post_load.""" + result = TestModel().connect() + self.assertTrue(result['x'] == [1]) + + result = TestModel().connect() + self.assertTrue(result['x'] == [1]) From 1399e7ad4977073b7b1156fe2c3584b7675f1fdc Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 5 Aug 2016 14:18:39 -0400 Subject: [PATCH 08/10] Updated import paths --- requirements.txt | 4 +--- rest_orm/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index baf4151..83762c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ nose==1.3.7 -wsgiref==0.1.2 sphinx sphinx-autobuild -sphinx-rtd-theme --e git+https://github.com/caxiam/model-api.git#egg=Package +sphinx-rtd-theme \ No newline at end of file diff --git a/rest_orm/__init__.py b/rest_orm/__init__.py index 8ea4f1a..ddbdf0e 100644 --- a/rest_orm/__init__.py +++ b/rest_orm/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -import errors -import fields -import models +from rest_orm import errors +from rest_orm import fields +from rest_orm import models From 9731ba5ec2faa6de60973a95511fb9d96e2a9d06 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 5 Aug 2016 15:09:46 -0400 Subject: [PATCH 09/10] Upgraded to python 3.5 --- .gitignore | 1 + rest_orm/models.py | 8 +------- tests/test_fields.py | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 39142d1..9ef33a9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ env/ venv/ docs/_build rest_orm.egg-info +.python-version diff --git a/rest_orm/models.py b/rest_orm/models.py index 1f4e1f7..81bc528 100644 --- a/rest_orm/models.py +++ b/rest_orm/models.py @@ -7,13 +7,7 @@ import json -class BaseModel(object): - """Base model class responsible for registering child classes.""" - - __metaclass__ = ModelRegistry - - -class Model(BaseModel): +class Model(metaclass=ModelRegistry): """A flat representation of a single remote endpoint.""" def loads(self, data): diff --git a/tests/test_fields.py b/tests/test_fields.py index 86240a6..465c3c4 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -119,7 +119,7 @@ def test_deserialize_value(self): class Test(models.Model): number = fields.Integer('[y]') - field = fields.Nested(Test, path='[x]') + field = fields.Nested('Test', path='[x]') value = field.deserialize({'x': {'y': 1}}) self.assertTrue(isinstance(value, dict)) self.assertIn('number', value) From 88e042e317adecb3aa46db87c833fd7fe3ca5625 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 5 Aug 2016 15:33:28 -0400 Subject: [PATCH 10/10] Revert "Upgraded to python 3.5" This reverts commit 9731ba5ec2faa6de60973a95511fb9d96e2a9d06. --- .gitignore | 1 - rest_orm/models.py | 8 +++++++- tests/test_fields.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9ef33a9..39142d1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ env/ venv/ docs/_build rest_orm.egg-info -.python-version diff --git a/rest_orm/models.py b/rest_orm/models.py index 81bc528..1f4e1f7 100644 --- a/rest_orm/models.py +++ b/rest_orm/models.py @@ -7,7 +7,13 @@ import json -class Model(metaclass=ModelRegistry): +class BaseModel(object): + """Base model class responsible for registering child classes.""" + + __metaclass__ = ModelRegistry + + +class Model(BaseModel): """A flat representation of a single remote endpoint.""" def loads(self, data): diff --git a/tests/test_fields.py b/tests/test_fields.py index 465c3c4..86240a6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -119,7 +119,7 @@ def test_deserialize_value(self): class Test(models.Model): number = fields.Integer('[y]') - field = fields.Nested('Test', path='[x]') + field = fields.Nested(Test, path='[x]') value = field.deserialize({'x': {'y': 1}}) self.assertTrue(isinstance(value, dict)) self.assertIn('number', value)