Repository: marshmallow-code/marshmallow

Issue:
3.12 no longer supports fields named `parent`
Pretty sure that #1631 broke it. Reproducible example:

```py
from marshmallow import INCLUDE
from marshmallow.fields import Nested
from sqlalchemy import Column, DATE, create_engine, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import declarative_base, relationship
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from testing.postgresql import Postgresql


Base = declarative_base()


class Author(Base):
    __tablename__ = 'author'
    id = Column(UUID(as_uuid=True), primary_key=True)
    docs = relationship('Document', back_populates='parent')


class Document(Base):
    __tablename__ = 'document'
    id = Column(UUID(as_uuid=True), primary_key=True)
    parent_id = Column(UUID(as_uuid=True), ForeignKey('author.id'))
    parent = relationship(Author, back_populates='docs')
    last_updated = Column(DATE)


class AuthorSchema(SQLAlchemyAutoSchema):
    class Meta(SQLAlchemyAutoSchema.Meta):
        model = Author


class DocumentSchema(SQLAlchemyAutoSchema):
    parent = Nested(AuthorSchema)

    class Meta(SQLAlchemyAutoSchema.Meta):
        model = Document


with Postgresql() as postgresql:
    url = postgresql.url(drivername='postgresql+psycopg2')
    engine = create_engine(url, echo=True)
    Base.metadata.create_all(engine)

    DocumentSchema(unknown=INCLUDE)
```

Results in:

```pytb
Traceback (most recent call last):
  File "/home/phil/.config/JetBrains/PyCharm2021.1/scratches/sqlalchemy-marshmallow-reprex.py", line 44, in <module>
    DocumentSchema(unknown=INCLUDE)
  File "/home/phil/Dev/Python/venvs/cellarity/lib/python3.9/site-packages/marshmallow_sqlalchemy/schema/load_instance_mixin.py", line 43, in __init__
    super().__init__(*args, **kwargs)
  File "/home/phil/Dev/Python/venvs/cellarity/lib/python3.9/site-packages/marshmallow/schema.py", line 392, in __init__
    self._init_fields()
  File "/home/phil/Dev/Python/venvs/cellarity/lib/python3.9/site-packages/marshmallow/schema.py", line 971, in _init_fields
    self._bind_field(field_name, field_obj)
  File "/home/phil/Dev/Python/venvs/cellarity/lib/python3.9/site-packages/marshmallow/schema.py", line 1030, in _bind_field
    field_obj._bind_to_schema(field_name, self)
  File "/home/phil/Dev/Python/venvs/cellarity/lib/python3.9/site-packages/marshmallow/fields.py", line 1201, in _bind_to_schema
    or getattr(self.root.opts, self.SCHEMA_OPTS_VAR_NAME)
AttributeError: 'NoneType' object has no attribute 'opts'
```

Here, `self.root` resolves to `None` for the `last_updated` field:

https://github.com/marshmallow-code/marshmallow/blob/69270215ab9275dc566b010ecdb8777c186aa776/src/marshmallow/fields.py#L411-L420

This happens since that field’s `.parent` is the `DocumentSchema` class, which *does* have a `.parent` attribute. However that attribute is a `Nested` instance, not another schema as expected
Return a field’s root schema as soon as it is found
This prevents accessing a schema’s `.parent` attribute if it has one (e.g. a field called `parent`)

Fixes #1808, I think.


Discussion:

Thanks @flying-sheep. This looks correct. Do you have time to write a regression test for this? If not, I can do it myself when I'm off the clock (this weekend, probably). Let me know either way

Patch:
diff --git a/src/marshmallow/base.py b/src/marshmallow/base.py
--- a/src/marshmallow/base.py
+++ b/src/marshmallow/base.py
@@ -15,6 +15,7 @@ class FieldABC:
 
     parent = None
     name = None
+    root = None
 
     def serialize(self, attr, obj, accessor=None):
         raise NotImplementedError
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -352,10 +352,13 @@ def _bind_to_schema(self, field_name, schema):
         :meth:`Schema._bind_field <marshmallow.Schema._bind_field>`.
 
         :param str field_name: Field name set in schema.
-        :param Schema schema: Parent schema.
+        :param Schema|Field schema: Parent object.
         """
         self.parent = self.parent or schema
         self.name = self.name or field_name
+        self.root = self.root or (
+            self.parent.root if isinstance(self.parent, FieldABC) else self.parent
+        )
 
     def _serialize(self, value: typing.Any, attr: str, obj: typing.Any, **kwargs):
         """Serializes ``value`` to a basic Python datatype. Noop by default.
@@ -408,17 +411,6 @@ def context(self):
         """The context dictionary for the parent :class:`Schema`."""
         return self.parent.context
 
-    @property
-    def root(self):
-        """Reference to the `Schema` that this field belongs to even if it is buried in a
-        container field (e.g. `List`).
-        Return `None` for unbound fields.
-        """
-        ret = self
-        while hasattr(ret, "parent"):
-            ret = ret.parent
-        return ret if isinstance(ret, SchemaABC) else None
-
 
 class Raw(Field):
     """Field that applies no formatting."""


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -187,6 +187,14 @@ class Meta:
         for field_name in ("bar", "qux"):
             assert schema.fields[field_name].tuple_fields[0].format == "iso8601"
 
+    # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1808
+    def test_field_named_parent_has_root(self, schema):
+        class MySchema(Schema):
+            parent = fields.Field()
+
+        schema = MySchema()
+        assert schema.fields["parent"].root == schema
+
 
 class TestMetadata:
     @pytest.mark.parametrize("FieldClass", ALL_FIELDS)