Skip to content

Commit

Permalink
Add Nested field that inherits session (#239)
Browse files Browse the repository at this point in the history
* Allow session to be passed to nested fields.

Fixes #67

* Update tests/test_marshmallow_sqlalchemy.py

Respond to feedback.

Co-Authored-By: Steven Loria <sloria1@gmail.com>

* Allow many (and all args) to be passed through

* Fix formatting; update changelog

* Update recipes to use ma-sqla's Nested
  • Loading branch information
samueljsb authored and sloria committed Aug 31, 2019
1 parent be6df3e commit cf996b1
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 4 deletions.
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
- jean-philippe serafin `@jeanphix <https://github.com/jeanphix>`_
- Jack Smith `@jacksmith15 <https://github.com/jacksmith15>`_
- Kazantcev Andrey `@heckad <https://github.com/heckad>`_
- Samuel Searles-Bryant `@samueljsb <https://github.com/samueljsb>`_
46 changes: 46 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,52 @@
Changelog
---------

0.17.1 (unreleased)
+++++++++++++++++++

Bug fixes:

* Add ``marshmallow_sqlalchemy.fields.Nested`` field that inherits its session from its schema. This fixes a bug where an exception was raised when using ``Nested`` within a ``ModelSchema`` (:issue:`67`).
Thanks :user:`nickw444` for reporting and thanks :user:`samueljsb` for the PR.

User code should be updated to use marshmallow-sqlalchemy's ``Nested`` instead of ``marshmallow.fields.Nested``.

.. code-block:: python
# Before
from marshmallow import fields
from marshmallow_sqlalchemy import ModelSchema
class ArtistSchema(ModelSchema):
class Meta:
model = models.Artist
class AlbumSchema(ModelSchema):
class Meta:
model = models.Album
artist = fields.Nested(ArtistSchema)
# After
from marshmallow import fields
from marshmallow_sqlalchemy import ModelSchema
from marshmallow_sqlalchemy.fields import Nested
class ArtistSchema(ModelSchema):
class Meta:
model = models.Artist
class AlbumSchema(ModelSchema):
class Meta:
model = models.Album
artist = Nested(ArtistSchema)
0.17.0 (2019-06-22)
+++++++++++++++++++

Expand Down
10 changes: 7 additions & 3 deletions docs/recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,12 @@ Any field generated by a `ModelSchema <marshmallow_sqlalchemy.ModelSchema>` can
from marshmallow import fields
from marshmallow_sqlalchemy import ModelSchema
from marshmallow_sqlalchemy.fields import Nested
class AuthorSchema(ModelSchema):
# Override books field to use a nested representation rather than pks
books = fields.Nested(BookSchema, many=True, exclude=("author",))
books = Nested(BookSchema, many=True, exclude=("author",))
class Meta:
model = Author
Expand Down Expand Up @@ -162,7 +163,7 @@ You can customize the keyword arguments passed to a column property's correspond
Automatically Generating Schemas For SQLAlchemy Models
======================================================

It can be tedious to implement a large number of schemas if not overriding any of the generated fields as detailed above. SQLAlchemy has a hook that can be used to trigger the creation of the schemas, assigning them to the SQLAlchemy model property `<Model.__marshmallow__>`.
It can be tedious to implement a large number of schemas if not overriding any of the generated fields as detailed above. SQLAlchemy has a hook that can be used to trigger the creation of the schemas, assigning them to the SQLAlchemy model property ``Model.__marshmallow__``.

.. code-block:: python
Expand Down Expand Up @@ -243,7 +244,10 @@ To serialize nested attributes to primary keys unless they are already loaded, y

.. code-block:: python
class SmartNested(fields.Nested):
from marshmallow_sqlalchemy.fields import Nested
class SmartNested(Nested):
def serialize(self, attr, obj, accessor=None):
if attr not in obj.__dict__:
return {"id": int(getattr(obj, attr + "_id"))}
Expand Down
13 changes: 13 additions & 0 deletions src/marshmallow_sqlalchemy/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,16 @@ def _get_existing_instance(self, query, value):
if result is None:
raise NoResultFound
return result


class Nested(fields.Nested):
"""Nested field that inherits the session from its parent."""

def _deserialize(self, *args, **kwargs):
if hasattr(self.schema, "session"):
try:
self.schema.session = self.root.session
except AttributeError:
# Marshmallow 2.0.0 has no root property.
self.schema.session = self.parent.session
return super(Nested, self)._deserialize(*args, **kwargs)
73 changes: 72 additions & 1 deletion tests/test_marshmallow_sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
field_for,
ModelConversionError,
)
from marshmallow_sqlalchemy.fields import Related, RelatedList
from marshmallow_sqlalchemy.fields import Related, RelatedList, Nested

MARSHMALLOW_VERSION_INFO = tuple(
[int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
Expand Down Expand Up @@ -1318,6 +1318,77 @@ class SchoolWrapperSchema(Schema):
assert dump_result == data


class TestNestedFieldSession:
"""Test the session can be passed to nested fields."""

@pytest.fixture
def association_table(self, Base):
return sa.Table(
"association",
Base.metadata,
sa.Column("left_id", sa.Integer, sa.ForeignKey("left.id")),
sa.Column("right_id", sa.Integer, sa.ForeignKey("right.id")),
)

@pytest.fixture
def parent_model(self, Base, association_table):
class Parent(Base):
__tablename__ = "left"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.Text)
children = relationship(
"Child", secondary=association_table, back_populates="parents"
)

return Parent

@pytest.fixture
def child_model(self, Base, parent_model, association_table):
class Child(Base):
__tablename__ = "right"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.Text)
parents = relationship(
"Parent", secondary=association_table, back_populates="children"
)

return Child

@pytest.fixture
def child_schema(self, child_model):
class ChildSchema(ModelSchema):
class Meta:
model = child_model

return ChildSchema

def test_session_is_passed_to_nested_field(
self, child_schema, parent_model, session
):
class ParentSchema(ModelSchema):
children = Nested(child_schema, many=True)

class Meta:
model = parent_model

data = {"name": "Parent1", "children": [{"name": "Child1"}]}
ParentSchema().load(data, session=session)

def test_session_is_passed_to_nested_field_in_list_field(
self, parent_model, child_model, child_schema, session
):
"""The session is passed to a nested field in a List field."""

class ParentSchema(ModelSchema):
children = fields.List(Nested(child_schema))

class Meta:
model = parent_model

data = {"name": "Jorge", "children": [{"name": "Jose"}]}
ParentSchema().load(data, session=session)


def _repr_validator_list(validators):
return sorted([repr(validator) for validator in validators])

Expand Down

0 comments on commit cf996b1

Please sign in to comment.