Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Fix included_data and data["included"] type consistency. #268

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ Contributors (chronological)
- Robert Sawicki `@ww3pl <https://github.com/ww3pl>`_
- `@aberres <https://github.com/aberres>`_
- George Alton `@georgealton <https://github.com/georgealton>`_
- Adrian Vandier Ast `@AdrianVandierAst <https://github.com/AdrianVandierAst>`_
2 changes: 1 addition & 1 deletion marshmallow_jsonapi/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def extract_value(self, data):
# fall back below to old behaviour of only IDs.
if "attributes" in data and self.__schema:
result = self.schema.load(
{"data": data, "included": self.root.included_data}
{"data": data, "included": self.root.included_data.values()}
)
return result.data if _MARSHMALLOW_VERSION_INFO[0] < 3 else result

Expand Down
39 changes: 25 additions & 14 deletions marshmallow_jsonapi/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,19 +166,18 @@ def unwrap_item(self, item):
# Fold included data related to this relationship into the item, so
# that we can deserialize the whole objects instead of just IDs.
if self.included_data:
included_data = []
included_data = None
inner_data = value.get("data", [])

# Data may be ``None`` (for empty relationships), but we only
# need to process it when it's present.
if inner_data:
if not is_collection(inner_data):
included_data = next(
self._extract_from_included(inner_data), None
)
included_data = self._extract_from_included(inner_data)
else:
included_data = []
for data in inner_data:
included_data.extend(self._extract_from_included(data))
included_data.append(self._extract_from_included(data))

if included_data:
value["data"] = included_data
Expand Down Expand Up @@ -235,7 +234,7 @@ def _do_load(self, data, many=None, **kwargs):
# Store this on the instance so we have access to the included data
# when processing relationships (``included`` is outside of the
# ``data``).
self.included_data = data.get("included", {})
self.included_data = self._load_included_data(data.get("included", []))
self.document_meta = data.get("meta", {})

try:
Expand All @@ -257,16 +256,28 @@ def _do_load(self, data, many=None, **kwargs):
return data, formatted_messages
return result

def _extract_from_included(self, data):
"""Extract included data matching the items in ``data``.
def _load_included_data(self, included):
""" Transform a list of resource object into a dict indexed by object type and id.
"""
included_data = {}
for item in included:
if "type" not in item.keys() or "id" not in item.keys():
raise ma.ValidationError(
[
{
"detail": "`included` objects must include `type` and `id` keys.",
"source": {"pointer": "/included"},
}
]
)
included_data[(item["type"], item["id"])] = item
return included_data

For each item in ``data``, extract the full data from the included
data.
def _extract_from_included(self, data):
"""Extract included data matching the item in ``data``.
"""
return (
item
for item in self.included_data
if item["type"] == data["type"] and str(item["id"]) == str(data["id"])
return self.included_data.get(
(data["type"], data["id"]), {"type": data["type"], "id": data["id"]}
)

def inflect(self, text):
Expand Down
1 change: 1 addition & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class CommentSchema(Schema):
related_url_kwargs={"id": "<id>"},
schema=AuthorSchema,
many=False,
type_="people",
)

class Meta:
Expand Down
26 changes: 25 additions & 1 deletion tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
from marshmallow_jsonapi import Schema, fields
from marshmallow_jsonapi.exceptions import IncorrectTypeError
from marshmallow_jsonapi.utils import _MARSHMALLOW_VERSION_INFO
from tests.base import unpack
from tests.base import unpack, fake
from tests.base import (
AuthorSchema,
CommentSchema,
PostSchema,
PolygonSchema,
ArticleSchema,
Comment,
)


Expand Down Expand Up @@ -372,6 +373,29 @@ class Meta(PostSchema.Meta):
assert "from_context" in included["attributes"]
assert included["attributes"]["from_context"] == "Hello World"

def test_load_n_dump_same_schema(self):
json_data = {
"data": {
"type": "comments",
"id": "1",
"attributes": {"body": fake.bs()},
"relationships": {"author": {"data": {"type": "people", "id": "1"}}},
},
"included": [
{
"type": "people",
"id": "1",
"first_name": fake.first_name(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These attributes should be inside the "attributes": {} key. I'm a tad surprised that this works.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected in the last commit

"last_name": fake.last_name(),
}
],
}
schema = CommentSchema()
data = unpack(schema.load(json_data))
comment = Comment(**data)
out_json_data = unpack(schema.dump(comment))
assert json_data["included"] == out_json_data["included"]


def get_error_by_field(errors, field):
for err in errors["errors"]:
Expand Down