Skip to content

Commit ed107a8

Browse files
Hugo Pellissari Pavanfrancoisfreitag
authored andcommitted
Support creating SQLAlchemy sessions from a callable
Allows setting the database session through the sqlalchemy_session_factory attribute, receiving a Callable that returns a sqlalchemy.orm.Session instance. This ensures better control to dynamically set the Session in the factory.
1 parent 31e8a41 commit ed107a8

4 files changed

Lines changed: 61 additions & 0 deletions

File tree

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ ChangeLog
1010

1111
- :issue:`366`: Add :class:`factory.django.Password` to generate Django :class:`~django.contrib.auth.models.User`
1212
passwords.
13+
- :issue:`304`: Add :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session_factory` to dynamically
14+
create sessions for use by the :class:`~factory.alchemy.SQLAlchemyModelFactory`.
1315
- Add support for Django 3.2
1416
- Add support for Django 4.0
1517
- Add support for Python 3.10

docs/orms.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,25 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:
369369
SQLAlchemy session to use to communicate with the database when creating
370370
an object through this :class:`SQLAlchemyModelFactory`.
371371

372+
.. attribute:: sqlalchemy_session_factory
373+
374+
.. versionadded:: 3.3.0
375+
376+
:class:`~collections.abc.Callable` returning a :class:`~sqlalchemy.orm.Session` instance to use to communicate
377+
with the database. You can either provide the session through this attribute, or through
378+
:attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session`, but not both at the same time.
379+
380+
.. code-block:: python
381+
382+
from . import common
383+
384+
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
385+
class Meta:
386+
model = User
387+
sqlalchemy_session_factory = lambda: common.Session()
388+
389+
username = 'john'
390+
372391
.. attribute:: sqlalchemy_session_persistence
373392

374393
Control the action taken by ``sqlalchemy_session`` at the end of a create call.

factory/alchemy.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ def _check_sqlalchemy_session_persistence(self, meta, value):
2222
(meta, VALID_SESSION_PERSISTENCE_TYPES, value)
2323
)
2424

25+
@staticmethod
26+
def _check_has_sqlalchemy_session_set(meta, value):
27+
if value and meta.sqlalchemy_session:
28+
raise RuntimeError("Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both")
29+
2530
def _build_default_options(self):
2631
return super()._build_default_options() + [
2732
base.OptionDefault('sqlalchemy_get_or_create', (), inherit=True),
2833
base.OptionDefault('sqlalchemy_session', None, inherit=True),
34+
base.OptionDefault(
35+
'sqlalchemy_session_factory', None, inherit=True, checker=self._check_has_sqlalchemy_session_set
36+
),
2937
base.OptionDefault(
3038
'sqlalchemy_session_persistence',
3139
None,
@@ -90,6 +98,10 @@ def _get_or_create(cls, model_class, session, args, kwargs):
9098
@classmethod
9199
def _create(cls, model_class, *args, **kwargs):
92100
"""Create an instance of the model, and save it to the database."""
101+
session_factory = cls._meta.sqlalchemy_session_factory
102+
if session_factory:
103+
cls._meta.sqlalchemy_session = session_factory()
104+
93105
session = cls._meta.sqlalchemy_session
94106

95107
if session is None:

tests/test_alchemy.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,34 @@ def test_build_does_not_raises_exception_when_no_session_was_set(self):
264264
self.assertEqual(inst1.id, 1)
265265

266266

267+
class SQLAlchemySessionFactoryTestCase(unittest.TestCase):
268+
269+
def test_create_get_session_from_sqlalchemy_session_factory(self):
270+
class SessionGetterFactory(SQLAlchemyModelFactory):
271+
class Meta:
272+
model = models.StandardModel
273+
sqlalchemy_session = None
274+
sqlalchemy_session_factory = lambda: models.session
275+
276+
id = factory.Sequence(lambda n: n)
277+
278+
SessionGetterFactory.create()
279+
self.assertEqual(SessionGetterFactory._meta.sqlalchemy_session, models.session)
280+
# Reuse the session obtained from sqlalchemy_session_factory.
281+
SessionGetterFactory.create()
282+
283+
def test_create_raise_exception_sqlalchemy_session_factory_not_callable(self):
284+
message = "^Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both$"
285+
with self.assertRaisesRegex(RuntimeError, message):
286+
class SessionAndGetterFactory(SQLAlchemyModelFactory):
287+
class Meta:
288+
model = models.StandardModel
289+
sqlalchemy_session = models.session
290+
sqlalchemy_session_factory = lambda: models.session
291+
292+
id = factory.Sequence(lambda n: n)
293+
294+
267295
class NameConflictTests(unittest.TestCase):
268296
"""Regression test for `TypeError: _save() got multiple values for argument 'session'`
269297

0 commit comments

Comments
 (0)