diff --git a/colanderalchemy/schema.py b/colanderalchemy/schema.py index 4925e76..c806f4e 100644 --- a/colanderalchemy/schema.py +++ b/colanderalchemy/schema.py @@ -367,12 +367,12 @@ def get_schema_from_column(self, prop, overrides): 1 arg version takes ExecutionContext - set missing to 'drop' to allow SQLA to fill this in and make it an unrequired field - + if nullable, then missing = colander.null (this has to be the case since some colander types won't accept `None` as a value, but all accept `colander.null`) - - all values for server_default should result in 'drop' + + all values for server_default should result in 'drop' for Colander missing autoincrement results in drop @@ -459,11 +459,17 @@ def get_schema_from_relationship(self, prop, overrides): class_ = prop.mapper.class_ - if declarative_overrides.pop('exclude', False): + key = 'exclude' + if declarative_overrides.pop(key, False): log.debug('Relationship %s skipped due to declarative overrides', name) return None + if overrides.pop(key, False): + log.debug('Relationship %s skipped due to imperative overrides', + name) + return None + for key in ['name', 'typ']: self.check_overrides(name, key, {}, declarative_overrides, overrides) @@ -554,16 +560,22 @@ def get_schema_from_relationship(self, prop, overrides): # xToOne relationships. return SchemaNode(Mapping(), *children, **kwargs) - node = SQLAlchemySchemaNode(class_, - name=name, - includes=includes, - excludes=excludes, - overrides=rel_overrides, - missing=missing, - parents_=self.parents_ + [self.class_]) - if prop.uselist: + node = SQLAlchemySchemaNode(class_, + name=name, + includes=includes, + excludes=excludes, + overrides=rel_overrides, + missing=missing, + parents_=self.parents_ + [self.class_]) node = SchemaNode(Sequence(), node, **kwargs) + else: + kwargs['name'] = name + kwargs['includes'] = includes + kwargs['excludes'] = excludes + kwargs['overrides'] = rel_overrides + kwargs['parents_'] = self.parents_ + [self.class_] + node = SQLAlchemySchemaNode(class_, **kwargs) node.name = name @@ -695,7 +707,11 @@ def objectify(self, dict_, context=None): for obj in dict_[attr]] else: # Single object - value = self[attr].objectify(dict_[attr]) + sub_dict = dict_[attr] + if sub_dict: + value = self[attr].objectify(sub_dict) + else: + value = None else: value = dict_[attr] if value is colander.null: diff --git a/tests/test_schema.py b/tests/test_schema.py index ba40cbf..479cbed 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1368,3 +1368,88 @@ def test_relationship_infinite_recursion(self): # Unpatched, creating a bar or baz schema node causes infinite recursion schema = SQLAlchemySchemaNode(Bar) schema = SQLAlchemySchemaNode(Baz) + + def test_exclude_relationship(self): + overrides = { + 'person': { + 'includes': ['name', 'surname', 'gender', 'addresses'], + 'overrides': { + 'addresses': { + 'exclude': True, + } + } + } + } + includes = ['email', 'enabled', 'created', 'timeout', 'person'] + account_schema = SQLAlchemySchemaNode( + Account, overrides=overrides, includes=includes) + address_args = dict(street='My Street', city='My City') + address = Address(**address_args) + + person_args = dict(name='My Name', surname='My Surname', + gender='M', addresses=[address]) + person = Person(**person_args) + + account_args = dict(email='mailbox@domain.tld', + enabled=True, + created=datetime.datetime.now(), + timeout=datetime.time(hour=1, minute=0), + person=person) + account = Account(**account_args) + + appstruct = account_schema.dictify(account) + self.maxDiff = None + + person_args.pop('addresses') + account_args['person'] = person_args + self.assertEqual(appstruct, account_args) + for account_key in account_args: + self.assertIn(account_key, appstruct) + if account_key == 'person': + for person_key in person_args: + self.assertIn(person_key, appstruct[account_key]) + cstruct = account_schema.serialize(appstruct=appstruct) + newappstruct = account_schema.deserialize(cstruct) + self.assertEqual(appstruct, newappstruct) + + cstruct = account_schema.serialize(appstruct=appstruct) + newappstruct = account_schema.deserialize(cstruct) + self.assertEqual(appstruct, newappstruct) + + def test_empty_dict_relationship(self): + dict_ = { + 'person': {}, + 'enabled': True, + 'email': 'mailbox@domain.tld', + 'timeout': datetime.time(hour=0, minute=0), + 'created': datetime.datetime.now(), + 'foobar': 'a fake value', # Not present in schema + 'buzbaz': {} # Not present in schema + } + schema = self._prep_schema() + + objectified = schema.objectify(dict_) + self.assertIsInstance(objectified, Account) + self.assertEqual(objectified.email, 'mailbox@domain.tld') + self.assertIsNone(objectified.person) + self.assertFalse(hasattr(objectified, 'foobar')) + self.assertFalse(hasattr(objectified, 'buzbaz')) + + def test_none_relationship(self): + dict_ = { + 'person': None, + 'enabled': True, + 'email': 'mailbox@domain.tld', + 'timeout': datetime.time(hour=0, minute=0), + 'created': datetime.datetime.now(), + 'foobar': 'a fake value', # Not present in schema + 'buzbaz': None # Not present in schema + } + schema = self._prep_schema() + + objectified = schema.objectify(dict_) + self.assertIsInstance(objectified, Account) + self.assertEqual(objectified.email, 'mailbox@domain.tld') + self.assertIsNone(objectified.person) + self.assertFalse(hasattr(objectified, 'foobar')) + self.assertFalse(hasattr(objectified, 'buzbaz'))