fix: require zope.sqlalchemy >= 1.5

so we can do away with some old cruft, since latest zope.sqlalchemy is
3.1 from 2023-09-12
This commit is contained in:
Lance Edgar 2024-07-02 11:14:03 -05:00
parent aab4dec27e
commit d72d6f8c7c
5 changed files with 129 additions and 118 deletions

6
docs/api/db.rst Normal file
View file

@ -0,0 +1,6 @@
``tailbone.db``
===============
.. automodule:: tailbone.db
:members:

View file

@ -44,6 +44,7 @@ Package API:
api/api/batch/core api/api/batch/core
api/api/batch/ordering api/api/batch/ordering
api/db
api/diffs api/diffs
api/forms api/forms
api/forms.widgets api/forms.widgets

View file

@ -61,7 +61,7 @@ install_requires =
transaction transaction
waitress waitress
WebHelpers2 WebHelpers2
zope.sqlalchemy zope.sqlalchemy>=1.5
[options.packages.find] [options.packages.find]

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar # Copyright © 2010-2024 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -21,14 +21,13 @@
# #
################################################################################ ################################################################################
""" """
Database Stuff Database sessions etc.
""" """
import sqlalchemy as sa import sqlalchemy as sa
from zope.sqlalchemy import datamanager from zope.sqlalchemy import datamanager
import sqlalchemy_continuum as continuum import sqlalchemy_continuum as continuum
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session
from pkg_resources import get_distribution, parse_version
from rattail.db import SessionBase from rattail.db import SessionBase
from rattail.db.continuum import versioning_manager from rattail.db.continuum import versioning_manager
@ -43,23 +42,28 @@ TrainwreckSession = scoped_session(sessionmaker())
# empty dict for now, this must populated on app startup (if needed) # empty dict for now, this must populated on app startup (if needed)
ExtraTrainwreckSessions = {} ExtraTrainwreckSessions = {}
# some of the logic below may need to vary somewhat, based on which version of
# zope.sqlalchemy we have installed
zope_sqlalchemy_version = get_distribution('zope.sqlalchemy').version
zope_sqlalchemy_version_parsed = parse_version(zope_sqlalchemy_version)
class TailboneSessionDataManager(datamanager.SessionDataManager): class TailboneSessionDataManager(datamanager.SessionDataManager):
"""Integrate a top level sqlalchemy session transaction into a zope transaction """
Integrate a top level sqlalchemy session transaction into a zope
transaction
One phase variant. One phase variant.
.. note:: .. note::
This class appears to be necessary in order for the Continuum
integration to work alongside the Zope transaction integration. This class appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It subclasses
``zope.sqlalchemy.datamanager.SessionDataManager`` but injects
some SQLAlchemy-Continuum logic within :meth:`tpc_vote()`, and
is sort of monkey-patched into the mix.
""" """
def tpc_vote(self, trans): def tpc_vote(self, trans):
""" """
# for a one phase data manager commit last in tpc_vote # for a one phase data manager commit last in tpc_vote
if self.tx is not None: # there may have been no work to do if self.tx is not None: # there may have been no work to do
@ -71,28 +75,41 @@ class TailboneSessionDataManager(datamanager.SessionDataManager):
self._finish('committed') self._finish('committed')
def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transaction_manager=datamanager.zope_transaction.manager, keep_session=False): def join_transaction(
"""Join a session to a transaction using the appropriate datamanager. session,
initial_state=datamanager.STATUS_ACTIVE,
transaction_manager=datamanager.zope_transaction.manager,
keep_session=False,
):
"""
Join a session to a transaction using the appropriate datamanager.
It is safe to call this multiple times, if the session is already joined It is safe to call this multiple times, if the session is already
then it just returns. joined then it just returns.
`initial_state` is either STATUS_ACTIVE, STATUS_INVALIDATED or STATUS_READONLY `initial_state` is either STATUS_ACTIVE, STATUS_INVALIDATED or
STATUS_READONLY
If using the default initial status of STATUS_ACTIVE, you must ensure that If using the default initial status of STATUS_ACTIVE, you must
mark_changed(session) is called when data is written to the database. ensure that mark_changed(session) is called when data is written
to the database.
The ZopeTransactionExtesion SessionExtension can be used to ensure that this is The ZopeTransactionExtesion SessionExtension can be used to ensure
called automatically after session write operations. that this is called automatically after session write operations.
.. note:: .. note::
This function is copied from upstream, and tweaked so that our custom
:class:`TailboneSessionDataManager` will be used. This function appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It overrides ``zope.sqlalchemy.datamanager.join_transaction()``
to ensure the custom :class:`TailboneSessionDataManager` is
used, and is sort of monkey-patched into the mix.
""" """
# the upstream internals of this function has changed a little over time. # the upstream internals of this function has changed a little over time.
# unfortunately for us, that means we must include each variant here. # unfortunately for us, that means we must include each variant here.
if zope_sqlalchemy_version_parsed >= parse_version('1.1'): # 1.1+
if datamanager._SESSION_STATE.get(session, None) is None: if datamanager._SESSION_STATE.get(session, None) is None:
if session.twophase: if session.twophase:
DataManager = datamanager.TwoPhaseSessionDataManager DataManager = datamanager.TwoPhaseSessionDataManager
@ -100,98 +117,79 @@ def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transacti
DataManager = TailboneSessionDataManager DataManager = TailboneSessionDataManager
DataManager(session, initial_state, transaction_manager, keep_session=keep_session) DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
else: # pre-1.1
if datamanager._SESSION_STATE.get(id(session), None) is None:
if session.twophase:
DataManager = datamanager.TwoPhaseSessionDataManager
else:
DataManager = TailboneSessionDataManager
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
class ZopeTransactionEvents(datamanager.ZopeTransactionEvents):
if zope_sqlalchemy_version_parsed >= parse_version('1.2'): # 1.2+
class ZopeTransactionEvents(datamanager.ZopeTransactionEvents):
""" """
Record that a flush has occurred on a session's Record that a flush has occurred on a session's connection. This
connection. This allows the DataManager to rollback rather allows the DataManager to rollback rather than commit on read only
than commit on read only transactions. transactions.
.. note:: .. note::
This class is copied from upstream, and tweaked so that our
custom :func:`join_transaction()` will be used. This class appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It subclasses
``zope.sqlalchemy.datamanager.ZopeTransactionEvents`` but
overrides various methods to ensure the custom
:func:`join_transaction()` is called, and is sort of
monkey-patched into the mix.
""" """
def after_begin(self, session, transaction, connection): def after_begin(self, session, transaction, connection):
""" """
join_transaction(session, self.initial_state, join_transaction(session, self.initial_state,
self.transaction_manager, self.keep_session) self.transaction_manager, self.keep_session)
def after_attach(self, session, instance): def after_attach(self, session, instance):
""" """
join_transaction(session, self.initial_state, join_transaction(session, self.initial_state,
self.transaction_manager, self.keep_session) self.transaction_manager, self.keep_session)
def join_transaction(self, session): def join_transaction(self, session):
""" """
join_transaction(session, self.initial_state, join_transaction(session, self.initial_state,
self.transaction_manager, self.keep_session) self.transaction_manager, self.keep_session)
else: # pre-1.2
class ZopeTransactionExtension(datamanager.ZopeTransactionExtension): def register(
session,
initial_state=datamanager.STATUS_ACTIVE,
transaction_manager=datamanager.zope_transaction.manager,
keep_session=False,
):
""" """
Record that a flush has occurred on a session's Register ZopeTransaction listener events on the given Session or
connection. This allows the DataManager to rollback rather Session factory/class.
than commit on read only transactions.
.. note:: This function requires at least SQLAlchemy 0.7 and makes use of
This class is copied from upstream, and tweaked so that our the newer sqlalchemy.event package in order to register event
custom :func:`join_transaction()` will be used. listeners on the given Session.
"""
def after_begin(self, session, transaction, connection):
join_transaction(session, self.initial_state,
self.transaction_manager, self.keep_session)
def after_attach(self, session, instance):
join_transaction(session, self.initial_state,
self.transaction_manager, self.keep_session)
def register(session, initial_state=datamanager.STATUS_ACTIVE,
transaction_manager=datamanager.zope_transaction.manager, keep_session=False):
"""Register ZopeTransaction listener events on the
given Session or Session factory/class.
This function requires at least SQLAlchemy 0.7 and makes use
of the newer sqlalchemy.event package in order to register event listeners
on the given Session.
The session argument here may be a Session class or subclass, a The session argument here may be a Session class or subclass, a
sessionmaker or scoped_session instance, or a specific Session instance. sessionmaker or scoped_session instance, or a specific Session
Event listening will be specific to the scope of the type of argument instance. Event listening will be specific to the scope of the
passed, including specificity to its subclass as well as its identity. type of argument passed, including specificity to its subclass as
well as its identity.
.. note:: .. note::
This function is copied from upstream, and tweaked so that our custom
:class:`ZopeTransactionExtension` will be used. This function appears to be necessary in order for the
SQLAlchemy-Continuum integration to work alongside the Zope
transaction integration.
It overrides ``zope.sqlalchemy.datamanager.regsiter()`` to
ensure the custom :class:`ZopeTransactionEvents` is used.
""" """
from sqlalchemy import event from sqlalchemy import event
if zope_sqlalchemy_version_parsed >= parse_version('1.2'): # 1.2+
ext = ZopeTransactionEvents( ext = ZopeTransactionEvents(
initial_state=initial_state, initial_state=initial_state,
transaction_manager=transaction_manager, transaction_manager=transaction_manager,
keep_session=keep_session, keep_session=keep_session,
) )
else: # pre-1.2
ext = ZopeTransactionExtension(
initial_state=initial_state,
transaction_manager=transaction_manager,
keep_session=keep_session,
)
event.listen(session, "after_begin", ext.after_begin) event.listen(session, "after_begin", ext.after_begin)
event.listen(session, "after_attach", ext.after_attach) event.listen(session, "after_attach", ext.after_attach)
event.listen(session, "after_flush", ext.after_flush) event.listen(session, "after_flush", ext.after_flush)
@ -199,7 +197,6 @@ def register(session, initial_state=datamanager.STATUS_ACTIVE,
event.listen(session, "after_bulk_delete", ext.after_bulk_delete) event.listen(session, "after_bulk_delete", ext.after_bulk_delete)
event.listen(session, "before_commit", ext.before_commit) event.listen(session, "before_commit", ext.before_commit)
if zope_sqlalchemy_version_parsed >= parse_version('1.5'): # 1.5+
if datamanager.SA_GE_14: if datamanager.SA_GE_14:
event.listen(session, "do_orm_execute", ext.do_orm_execute) event.listen(session, "do_orm_execute", ext.do_orm_execute)

7
tests/test_db.py Normal file
View file

@ -0,0 +1,7 @@
# -*- coding: utf-8; -*-
# TODO: add real tests at some point but this at least gives us basic
# coverage when running this "test" module alone
from tailbone import db