Skip to content

Commit 60c89be

Browse files
authored
Merge pull request #125 from python-odin/development
Release 1.8
2 parents 5b93380 + a8eaaec commit 60c89be

File tree

7 files changed

+178
-87
lines changed

7 files changed

+178
-87
lines changed

HISTORY

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
1.8.0
2+
=====
3+
4+
- Allow types to be resolved by assuming a types names space (simplifies
5+
specification of types where the full name space can be assumed)
6+
- Inherit a custom type_field (this could cause unexpected side effects,
7+
although unlikely as this is a required change)
8+
19
1.7.3
210
=====
311

poetry.lock

+81-74
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "odin"
3-
version = "1.7.3"
3+
version = "1.8.0"
44
description = "Data-structure definition/validation/traversal, mapping and serialisation toolkit for Python"
55
authors = ["Tim Savage <tim@savage.company>"]
66
license = "BSD-3-Clause"

src/odin/adapters.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
class CurriedAdapter(object):
1010
"""
11-
Curry wrapper for a Adapter to allow for pre-config of include/exclude and
11+
Curry wrapper for an Adapter to allow for pre-config of include/exclude and
1212
any other user defined arguments provided in kwargs.
1313
"""
1414

src/odin/codecs/xml_codec.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
resource will be exported (if multiple are defined they will all be exported
1212
into the one text block).
1313
14-
The TextField is for all intensive purposes just a StringField, other codecs
14+
The TextField is for all intents and purposes just a StringField, other codecs
1515
will export any value as a String.
1616
1717
"""

src/odin/resources.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __init__(self, meta):
5454
self.verbose_name_plural = None
5555
self.abstract = False
5656
self.doc_group = None
57-
self.type_field = DEFAULT_TYPE_FIELD
57+
self.type_field = NotProvided
5858
self.key_field_names = None
5959
self.field_sorting = NotProvided
6060
self.user_data = None
@@ -292,14 +292,18 @@ def __new__(mcs, name, bases, attrs):
292292
new_meta = mcs.meta_options(meta)
293293
new_class.add_to_class("_meta", new_meta)
294294

295-
# Namespace is inherited
295+
# Namespace is inherited and default if not provided
296296
if base_meta and new_meta.name_space is NotProvided:
297297
new_meta.name_space = base_meta.name_space
298-
299-
# Generate a namespace if one is not provided
300298
if new_meta.name_space is NotProvided:
301299
new_meta.name_space = module
302300

301+
# Type field is inherited and default if not provided
302+
if base_meta and new_meta.type_field is NotProvided:
303+
new_meta.type_field = base_meta.type_field
304+
if new_meta.type_field is NotProvided:
305+
new_meta.type_field = DEFAULT_TYPE_FIELD
306+
303307
# Key field is inherited
304308
if base_meta and new_meta.key_field_names is None:
305309
new_meta.key_field_names = base_meta.key_field_names
@@ -570,9 +574,9 @@ class Resource(six.with_metaclass(ResourceType, ResourceBase)):
570574
def resolve_resource_type(resource):
571575
if isinstance(resource, type) and issubclass(resource, ResourceBase):
572576
meta = getmeta(resource)
573-
return meta.resource_name, meta.type_field
577+
return meta.resource_name, meta.type_field, meta.name_space
574578
else:
575-
return resource, DEFAULT_TYPE_FIELD
579+
return resource, DEFAULT_TYPE_FIELD, ""
576580

577581

578582
def create_resource_from_iter(
@@ -647,11 +651,16 @@ def _resolve_type_from_resource(data, resource):
647651
else:
648652
resources = [resolve_resource_type(resource)]
649653

650-
for resource_name, type_field in resources:
654+
for resource_name, type_field, name_space in resources:
651655
# See if the input includes a type field and check it's registered
652656
document_resource_name = data.get(type_field, None)
653657
if document_resource_name:
654658
resource_type = registration.get_resource(document_resource_name)
659+
# Fall back to try applying a prefix
660+
if not resource_type and name_space:
661+
resource_type = registration.get_resource(
662+
"{}.{}".format(name_space, document_resource_name)
663+
)
655664
else:
656665
resource_type = registration.get_resource(resource_name)
657666

@@ -702,9 +711,12 @@ def _resolve_type_from_data(data):
702711

703712

704713
def create_resource_from_dict(
705-
d, resource=None, full_clean=True, copy_dict=True, default_to_not_provided=False
714+
d, # type: Dict[str, Any]
715+
resource=None, # type: Type[R]
716+
full_clean=True, # type: bool
717+
copy_dict=True, # type: bool
718+
default_to_not_provided=False, # type: bool
706719
):
707-
# type: (Dict[str, Any], Type[R], bool, bool, bool) -> R
708720
"""
709721
Create a resource from a dict.
710722
@@ -764,7 +776,11 @@ def create_resource_from_dict(
764776

765777

766778
def build_object_graph(
767-
d, resource=None, full_clean=True, copy_dict=True, default_to_not_supplied=False
779+
d, # type: Dict[str, Any]
780+
resource=None, # type: Type[R]
781+
full_clean=True, # type: bool
782+
copy_dict=True, # type: bool
783+
default_to_not_supplied=False, # type: bool
768784
):
769785
"""
770786
Generate an object graph from a dict

tests/test_polymorphic.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Tests of polymorphic behaviours
3+
"""
4+
import pytest
5+
6+
import odin
7+
from odin.resources import create_resource_from_dict
8+
from odin.utils import getmeta
9+
10+
11+
class AbstractResource(odin.Resource):
12+
class Meta:
13+
abstract = True
14+
namespace = "au.com.example.abstracts"
15+
type_field = "type"
16+
17+
18+
class ResourceA(AbstractResource):
19+
class Meta:
20+
type_field = "type"
21+
22+
23+
class ResourceB(AbstractResource):
24+
pass
25+
26+
27+
def test_resolve_resource_using_full_type_field():
28+
actual = create_resource_from_dict(
29+
{"type": "au.com.example.abstracts.ResourceA"},
30+
AbstractResource,
31+
full_clean=False,
32+
)
33+
34+
assert isinstance(actual, ResourceA)
35+
36+
37+
def test_resolve_resource_using_partial_type_field():
38+
actual = create_resource_from_dict(
39+
{"type": "ResourceB"},
40+
AbstractResource,
41+
full_clean=False,
42+
)
43+
44+
assert isinstance(actual, ResourceB)
45+
46+
47+
@pytest.mark.parametrize("resource", (ResourceA, ResourceB))
48+
def test_type_field_is_inherited(resource):
49+
expected = getmeta(AbstractResource).type_field
50+
actual = getmeta(resource).type_field
51+
52+
assert actual == expected
53+
54+
55+
@pytest.mark.parametrize("resource", (ResourceA, ResourceB))
56+
def test_name_space_is_inherited(resource):
57+
expected = getmeta(AbstractResource).name_space
58+
actual = getmeta(resource).name_space
59+
60+
assert actual == expected

0 commit comments

Comments
 (0)