litestar-org / polyfactory

Simple and powerful factories for mock data generation
https://polyfactory.litestar.dev/
MIT License
988 stars 78 forks source link

Bug: Try to use SQLAlchemyFactory for model with column_property #508

Closed andy-takker closed 5 months ago

andy-takker commented 6 months ago

Description

Create model with declared column_property and try create model object with it.

Got error AttributeError: Neither 'Label' object nor 'Comparator' object has an attribute 'nullable'

URL to code causing the issue

No response

MCVE

from sqlalchemy import Integer, String
from sqlalchemy.orm import DeclarativeBase, column_property, mapped_column

from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory

class Base(DeclarativeBase):
    ...

class Student(Base):
    __tablename__ = "user"
    id = mapped_column(Integer(), primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))
    fullname = column_property(firstname + " " + lastname)

class StudentFactory(SQLAlchemyFactory[Student]):
    __model__ = Student
    __set_relationships__ = True
    __set_foreign_keys__ = False

student = StudentFactory.build()

Steps to reproduce

1. Define SQLAlchemy model with column_property
2. Define SQLAlchemyFactory with model
3. Try to create object with factory

Screenshots

No response

Logs

tests/test_api/test_student/test_change_teacher.py:100: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.11/site-packages/polyfactory/factories/base.py:1031: in build
    return cast("T", cls.__model__(**cls.process_kwargs(**kwargs)))
.venv/lib/python3.11/site-packages/polyfactory/factories/base.py:931: in process_kwargs
    for field_meta in cls.get_model_fields():
.venv/lib/python3.11/site-packages/polyfactory/factories/sqlalchemy_factory.py:141: in get_model_fields
    fields_meta.extend(
.venv/lib/python3.11/site-packages/polyfactory/factories/sqlalchemy_factory.py:143: in <genexpr>
    annotation=cls.get_type_from_column(column),
.venv/lib/python3.11/site-packages/polyfactory/factories/sqlalchemy_factory.py:131: in get_type_from_column
    if column.nullable:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <sqlalchemy.sql.elements.Label at 0x1079e01d0; %(4422762960 anon)s>, key = 'nullable'

    def __getattr__(self, key: str) -> Any:
        try:
            return getattr(self.comparator, key)
        except AttributeError as err:
>           raise AttributeError(
                "Neither %r object nor %r object has an attribute %r"
                % (
                    type(self).__name__,
                    type(self.comparator).__name__,
                    key,
                )
            ) from err
E           AttributeError: Neither 'Label' object nor 'Comparator' object has an attribute 'nullable'

.venv/lib/python3.11/site-packages/sqlalchemy/sql/elements.py:1604: AttributeError
----------------------------------------------------------------- Captured stderr setup -----------------------------------------------------------------
Context impl PostgresqlImpl.
Will assume transactional DDL.
----------------------------------------------------------------- Captured stdout call ------------------------------------------------------------------
student.first_name || :param_1 || student.last_name  # add my print to find a column with problem

Release Version

2.9.0

Platform


[!NOTE]
While we are open for sponsoring on GitHub Sponsors and OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh dashboard

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
  • This, along with engagement in the community, helps us know which features are a priority to our users.

Fund with Polar

guacs commented 5 months ago

@andy-takker thanks for reporting this! I'm able to reproduce this and the reason for this is that we should be skipping column_properties, but we're not.

Also, I edited your original MCVE because that was not a working program in the sense that it wouldn't even run.

b4sen commented 5 months ago

Hey! I have a similar issue with the hybrid_property decorator. I even tried to Ignore() the field but didn't work.

b4sen commented 5 months ago

Maybe something like this could work:

if getattr(column, "nullable", False):
    annotation = Union[annotation, None]  # type: ignore[assignment]
guacs commented 5 months ago

Maybe something like this could work:

if getattr(column, "nullable", False):
    annotation = Union[annotation, None]  # type: ignore[assignment]

I think the fix in #510 is probably the right approach. While your approach of using getattr works, we actually don't want polyfactory to to set the value for column_properties since they're computed based on other fields.