Skip to content

Commit 2e16752

Browse files
committed
Skip SQLAlchemy model fields with default values
1 parent 0fd06a3 commit 2e16752

2 files changed

Lines changed: 66 additions & 4 deletions

File tree

polyfactory/factories/sqlalchemy_factory.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import enum
4+
import inspect
45
from collections.abc import Collection, Mapping
56
from dataclasses import is_dataclass
67
from datetime import date, datetime
@@ -22,16 +23,18 @@
2223
from polyfactory.exceptions import ConfigurationException, MissingDependencyException, ParameterException
2324
from polyfactory.factories.base import BaseFactory
2425
from polyfactory.factories.base import BuildContext as BaseBuildContext
25-
from polyfactory.field_meta import Constraints, FieldMeta
26+
from polyfactory.field_meta import Constraints, FieldMeta, Null
2627
from polyfactory.persistence import AsyncPersistenceProtocol, SyncPersistenceProtocol
2728
from polyfactory.utils.types import Frozendict
2829

2930
try:
30-
from sqlalchemy import ARRAY, Column, Numeric, String, inspect, types
31+
from sqlalchemy import ARRAY, Column, Numeric, String, types
32+
from sqlalchemy import inspect as sqla_inspect
3133
from sqlalchemy.dialects import mssql, mysql, postgresql, sqlite
3234
from sqlalchemy.exc import NoInspectionAvailable
3335
from sqlalchemy.ext.associationproxy import AssociationProxy
3436
from sqlalchemy.orm import InstanceState, Mapper, RelationshipProperty
37+
from sqlalchemy.sql.elements import TextClause
3538
except ImportError as e:
3639
msg = "sqlalchemy is not installed"
3740
raise MissingDependencyException(msg) from e
@@ -178,6 +181,21 @@ async def create_async(cls, **kwargs: Any) -> T:
178181
kwargs["_build_context"] = build_context
179182
return await super().create_async(**kwargs)
180183

184+
@classmethod
185+
def build(cls, *_: Any, **kwargs: Any) -> T:
186+
"""Build an instance of the factory's __model__ and apply field defaults."""
187+
data = super().process_kwargs(**kwargs)
188+
189+
if cls.__use_defaults__:
190+
for field_meta in cls.get_model_fields():
191+
if field_meta.name in data:
192+
continue
193+
194+
if cls.should_use_default_value(field_meta):
195+
data[field_meta.name] = field_meta.default
196+
197+
return cls.__model__(**data)
198+
181199
@classmethod
182200
def get_sqlalchemy_types(cls) -> dict[Any, Callable[[], Any]]:
183201
"""Get mapping of types where column type should be used directly.
@@ -227,7 +245,7 @@ def get_provider_map(cls) -> dict[Any, Callable[[], Any]]:
227245
@classmethod
228246
def is_supported_type(cls, value: Any) -> TypeGuard[type[T]]:
229247
try:
230-
inspected = inspect(value)
248+
inspected = sqla_inspect(value)
231249
except NoInspectionAvailable:
232250
return False
233251
return isinstance(inspected, (Mapper, InstanceState))
@@ -365,15 +383,39 @@ def _get_association_proxy_type(cls, table: Mapper, proxy: AssociationProxy) ->
365383
class_ = target_attr.entity.class_
366384
return class_ if not target_collection.uselist else list[class_] # type: ignore[valid-type]
367385

386+
@classmethod
387+
def get_column_default(cls, col: Column) -> Any:
388+
default = col.default
389+
if default is None or not default.has_arg:
390+
return Null
391+
392+
arg: Any = default.arg # type: ignore[attr-defined]
393+
394+
if default.is_scalar:
395+
return arg
396+
397+
if default.is_clause_element and isinstance(arg, TextClause):
398+
return arg.text
399+
400+
if default.is_callable:
401+
sig = inspect.signature(arg)
402+
# Skip context-sensitive default functions as there is no
403+
# obvious and correct way to construct the SQLAlchemy context manually.
404+
if not sig.parameters:
405+
return arg(None)
406+
407+
return Null
408+
368409
@classmethod
369410
def get_model_fields(cls) -> list[FieldMeta]:
370411
fields_meta: list[FieldMeta] = []
371412

372-
table: Mapper = inspect(cls.__model__) # type: ignore[assignment]
413+
table: Mapper = sqla_inspect(cls.__model__) # type: ignore[assignment]
373414
fields_meta.extend(
374415
FieldMeta.from_type(
375416
annotation=cls.get_type_from_column(column),
376417
name=name,
418+
default=cls.get_column_default(column),
377419
)
378420
for name, column in table.columns.items()
379421
if cls.should_column_be_set(column)

tests/sqlalchemy_factory/test_sqlalchemy_factory_common.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
2323
from sqlalchemy.ext.hybrid import hybrid_property
2424
from sqlalchemy.orm import Session
25+
from sqlalchemy.sql.expression import text
2526

2627
try:
2728
from sqlalchemy.orm.collections import (
@@ -202,6 +203,25 @@ class ModelFactory(SQLAlchemyFactory[Model]):
202203
assert result.optional_field is None
203204

204205

206+
def test_default_fields() -> None:
207+
class Model(Base):
208+
__tablename__ = "model_with_default_fields"
209+
210+
id = Column(Integer(), primary_key=True)
211+
default_scalar = Column(Integer(), default=10)
212+
default_callable = Column(String(), default=lambda: "callable")
213+
default_sql = Column(String(), default=text("0"))
214+
215+
class ModelFactory(SQLAlchemyFactory[Model]):
216+
__model__ = Model
217+
__use_defaults__ = True
218+
219+
result = ModelFactory.build()
220+
assert result.default_scalar == 10
221+
assert result.default_callable == "callable"
222+
assert result.default_sql == "0"
223+
224+
205225
@pytest.mark.parametrize("set_primary_key", (True, False))
206226
def test_set_primary_key(set_primary_key: bool) -> None:
207227
class AuthorFactory(SQLAlchemyFactory[Author]):

0 commit comments

Comments
 (0)