marshmallow-sqlalchemy icon indicating copy to clipboard operation
marshmallow-sqlalchemy copied to clipboard

Loading nested object with association_proxy occasionally works

Open philip-goh opened this issue 5 years ago • 1 comments

I have the following code that defines some geographic entities, City, State and Country. City is foreign keyed to a State, and a State is foreign keyed to a Country. In the City object, we have a relationship to a Country that is an association_proxy which traverses the State relationship.

import sqlalchemy as sa

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
from sqlalchemy.ext.associationproxy import association_proxy

engine = sa.create_engine("sqlite:///:memory:")
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()


class Country(Base):
    __tablename__ = "Country"
    __table_args__ = ({"extend_existing": True},)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(128), index=True, unique=True)
    code = sa.Column(sa.String(4), index=True, unique=True)


class State(Base):
    __tablename__ = "State"
    __table_args__ = (sa.UniqueConstraint("name", "country_id", name="__uix_state_country"),)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(128), index=True)
    country_id = sa.Column(sa.Integer, sa.ForeignKey("Country.id"), index=True)

    country = relationship(Country, backref="states")


class City(Base):
    __tablename__ = "City"
    __table_args__ = (sa.UniqueConstraint("name", "state_id", name="__uix_city_state"),)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(128), index=True)
    state_id = sa.Column(sa.Integer, sa.ForeignKey("State.id"), index=True)

    state = relationship(State, backref="cities")
    country = association_proxy("state", "country")


Base.metadata.create_all(engine)

I have the following schemas defined.

from marshmallow import fields
from marshmallow_sqlalchemy import ModelSchema

class CitySchema(ModelSchema):
    class Meta:
        model = City
        sqla_session = session
        exclude = ("city_region",)

    state = fields.Nested("StateSchema", many=False, only=("id", "name"))
    country = fields.Nested("CountrySchema", many=False, only=("id", "name"))


class StateSchema(ModelSchema):
    class Meta:
        model = State
        sqla_session = session
        exclude = ("state_region",)

    cities = fields.Nested(CitySchema, only=("id", "name"), many=True)
    country = fields.Nested("CountrySchema", many=False, only=("id", "name"))


class CountrySchema(ModelSchema):
    class Meta:
        model = Country
        sqla_session = session
        exclude = ("country_region",)

    states = fields.Nested(StateSchema, only=("id", "name"), many=True)

Now when I attempt to load a City object using the following, there's a small chance that it succeeds but most of the time it fails.

data = {
    "name": "Cambridge",  # new city
    "state": {"name": "Massachusetts"},  # A new state!
    "country": {"name": "United States"},  # A new country!
}

city_schema = CitySchema()
city = City()

# Sometimes this line works, but most of the time it fails
city_schema.load(data, instance=city, partial=True)

I'm getting a TypeError exception. TypeError: __init__() takes 1 positional argument but 2 were given. The full stack trace is as follows:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-a0d3fd8aacc8> in <module>
      7 city_schema = CitySchema()
      8 city = City()
----> 9 city_schema.load(data, instance=city, partial=True)

~/anaconda3_501/lib/python3.6/site-packages/marshmallow_sqlalchemy/schema.py in load(self, data, session, instance, transient, *args, **kwargs)
    214         self.instance = instance or self.instance
    215         try:
--> 216             return super(ModelSchema, self).load(data, *args, **kwargs)
    217         finally:
    218             self.instance = None

~/anaconda3_501/lib/python3.6/site-packages/marshmallow/schema.py in load(self, data, many, partial)
    586         .. versionadded:: 1.0.0
    587         """
--> 588         result, errors = self._do_load(data, many, partial=partial, postprocess=True)
    589         return UnmarshalResult(data=result, errors=errors)
    590 

~/anaconda3_501/lib/python3.6/site-packages/marshmallow/schema.py in _do_load(self, data, many, partial, postprocess)
    693                     result,
    694                     many,
--> 695                     original_data=data)
    696             except ValidationError as err:
    697                 errors = err.normalized_messages()

~/anaconda3_501/lib/python3.6/site-packages/marshmallow/schema.py in _invoke_load_processors(self, tag_name, data, many, original_data)
    858             data=data, many=many, original_data=original_data)
    859         data = self._invoke_processors(tag_name, pass_many=False,
--> 860             data=data, many=many, original_data=original_data)
    861         return data
    862 

~/anaconda3_501/lib/python3.6/site-packages/marshmallow/schema.py in _invoke_processors(self, tag_name, pass_many, data, many, original_data)
    961                     data = utils.if_none(processor(data, original_data), data)
    962                 else:
--> 963                     data = utils.if_none(processor(data), data)
    964         return data
    965 

~/anaconda3_501/lib/python3.6/site-packages/marshmallow_sqlalchemy/schema.py in make_instance(self, data, **kwargs)
    193         if instance is not None:
    194             for key, value in iteritems(data):
--> 195                 setattr(instance, key, value)
    196             return instance
    197         kwargs, association_attrs = self._split_model_kwargs_association(data)

~/anaconda3_501/lib/python3.6/site-packages/sqlalchemy/ext/associationproxy.py in __set__(self, obj, values)
    309             target = getattr(obj, self.target_collection)
    310             if target is None:
--> 311                 setattr(obj, self.target_collection, creator(values))
    312             else:
    313                 self._scalar_set(target, values)

TypeError: __init__() takes 1 positional argument but 2 were given

A couple of questions.

  • Is loading an association_proxy supported?
    • Is there a bug in my code?
    • Is there an alternative that I can try?
  • Why does this occasionally work? This bit has me really puzzled.

A link to the Jupyter Notebook with this code can be found at this link.

Thanks!

philip-goh avatar Jul 28 '19 13:07 philip-goh

It looks like this was addressed in a StackOverflow question: https://stackoverflow.com/questions/41222412/sqlalchemy-init-takes-1-positional-argument-but-2-were-given-many-to-man

keithrz avatar Feb 02 '21 17:02 keithrz