I am using the clean architecture & TDD development method in my Python project. Updating from SQLAlchemy 1.3 to SQLAlchemy 1.4 broke the ability to test against an in-memory Postgres DB, and I can’t find how to fix the problem.
Following DDD principles, the project uses the new imperative mapping syntax which replace classical mapping declarations.
Here is a minimal (non)-working example, adapted from SQLAlchemy documentation: https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html#orm-imperative-mapping
It requires installing and run PostgreSQL locally.
myapp/orm.py
from sqlalchemy import MetaData, Table, Column, Integer, String
from sqlalchemy.orm import registry
from myapp import model
mapper_registry = registry()
metadata = MetaData()
user_table = Table(
'tb_user',
mapper_registry.metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('fullname', String(50)),
Column('nickname', String(12))
)
mapper_registry.map_imperatively(model.User, user_table)
myapp/model.py
from dataclasses import dataclass
from dataclasses import field
@dataclass
class User:
id: int = field(init=False)
name: str = ""
fullname: str = ""
nickname: str = ""
tests/test_postgresql_inmemory.py
import tempfile
import pytest
from pytest_postgresql import factories
from sqlalchemy import create_engine
from sqlalchemy import text
from sqlalchemy.orm import clear_mappers
from sqlalchemy.orm import configure_mappers
from myapp import model
from myapp.orm import mapper_registry
from sqlalchemy.orm import sessionmaker
# here, we set up postgresql in-memory:
socket_dir = tempfile.TemporaryDirectory()
postgresql_my_proc = factories.postgresql_proc(
port=None,
unixsocketdir=socket_dir.name,
)
postgresql_my = factories.postgresql("postgresql_my_proc")
@pytest.fixture
def in_memory_db(postgresql_my):
def db_creator():
return postgresql_my.cursor().connection
engine = create_engine("postgresql+psycopg2://", creator=db_creator)
mapper_registry.metadata.create_all(bind=engine)
return engine
@pytest.fixture
def session(in_memory_db):
clear_mappers()
configure_mappers()
Session = sessionmaker(bind=in_memory_db)
session = Session()
yield session
clear_mappers()
def test_User_mapper_can_add(session):
user = model.User(fullname="John Smith")
session.add(user)
session.commit()
rows = list(session.execute("SELECT fullname FROM tb_user"))
assert rows == [("John Smith",)]
Result
===== test session starts =====
platform linux -- Python 3.9.4, pytest-5.4.3, py-1.10.0, pluggy-0.13.1
rootdir: /home/lionel/code/sqla14_test
plugins: postgresql-3.1.1
collected 1 item
tests/test_postgresql_inmemory.py F [100%]
==== FAILURES =====
___ test_User_mapper_can_add ___
self = <sqlalchemy.orm.session.Session object at 0x7fe876060c70>
instance = <[AttributeError("'User' object has no attribute 'id'") raised in repr()] User object at 0x7fe875f487c0>
_warn = True
def add(self, instance, _warn=True):
"""Place an object in the ``Session``.
Its state will be persisted to the database on the next flush
operation.
Repeated calls to ``add()`` will be ignored. The opposite of ``add()``
is ``expunge()``.
"""
if _warn and self._warn_on_events:
self._flush_warning("Session.add()")
try:
> state = attributes.instance_state(instance)
E AttributeError: 'User' object has no attribute '_sa_instance_state'
../../myvenv/lib/python3.9/site-packages/sqlalchemy/orm/session.py:2554: AttributeError
The above exception was the direct cause of the following exception:
session = <sqlalchemy.orm.session.Session object at 0x7fe876060c70>
def test_User_mapper_can_add(session):
user = model.User(fullname="John Smith")
> session.add(user)
tests/test_postgresql_inmemory.py:53:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../.cache/pypoetry/virtualenvs/sqla14-5BJjO56U-py3.9/lib/python3.9/site-packages/sqlalchemy/orm/session.py:2556: in add
util.raise_(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def raise_(
exception, with_traceback=None, replace_context=None, from_=False
):
r"""implement "raise" with cause support.
:param exception: exception to raise
:param with_traceback: will call exception.with_traceback()
:param replace_context: an as-yet-unsupported feature. This is
an exception object which we are "replacing", e.g., it's our
"cause" but we don't want it printed. Basically just what
``__suppress_context__`` does but we don't want to suppress
the enclosing context, if any. So for now we make it the
cause.
:param from_: the cause. this actually sets the cause and doesn't
hope to hide it someday.
"""
if with_traceback is not None:
exception = exception.with_traceback(with_traceback)
if from_ is not False:
exception.__cause__ = from_
elif replace_context is not None:
# no good solution here, we would like to have the exception
# have only the context of replace_context.__context__ so that the
# intermediary exception does not change, but we can't figure
# that out.
exception.__cause__ = replace_context
try:
> raise exception
E sqlalchemy.orm.exc.UnmappedInstanceError: Class 'myapp.model.User' is not mapped
../../myvenv/lib/python3.9/site-packages/sqlalchemy/util/compat.py:207: UnmappedInstanceError
-------- Captured stderr setup ----
sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8)
/bin/sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8)
========short test summary info =====
FAILED tests/test_postgresql_inmemory.py::test_User_mapper_can_add - sqlalchemy.orm.exc.UnmappedInstanceError: Clas
=========1 failed in 1.18s ==========
Do you see what needs to be changed to make the test pass?
Advertisement
Answer
I could make the test pass by wrapping the mapper_registry.map_imperatively(…)
in a start_mappers
function, like I was doing before.
I initially thought that I had to replace this by configure_mappers
– SQLAlchemy documentation.
myapp/orm.py
from sqlalchemy import MetaData, Table, Column, Integer, String
from sqlalchemy.orm import registry
from myapp import model
metadata = MetaData()
mapper_registry = registry(metadata=metadata)
user_table = Table(
'tb_user',
mapper_registry.metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('fullname', String(50)),
Column('nickname', String(12))
)
def start_mappers():
mapper_registry.map_imperatively(model.User, user_table)
tests/test_postgresql_inmemory.py
extract:
from myapp.orm import start_mappers
# …
@pytest.fixture
def session(in_memory_db):
clear_mappers()
start_mappers()
Session = sessionmaker(bind=in_memory_db)
session = Session()
yield session
clear_mappers()
def test_User_mapper_can_add(session):
user = model.User(fullname="John Smith")
session.add(user)
session.commit()
rows = list(session.execute("SELECT fullname FROM tb_user"))
assert rows == [("John Smith", )]
result
[Sat Jun 5 19:42:46 2021] Running: py.test tests/test_postgresql_inmemory.py
===== test session starts ====
platform linux -- Python 3.9.4, pytest-5.4.3, py-1.10.0, pluggy-0.13.1
rootdir: /home/lionel/code/myapp
plugins: postgresql-3.1.1
collected 1 item
tests/test_postgresql_inmemory.py . [100%]
===== 1 passed in 1.12s ====