Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ repos:
rev: 'v0.24.2'
hooks:
- id: toml-sort
- repo: https://github.com/pycqa/isort
rev: '7.0.0'
hooks:
- id: isort
name: isort (python)
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ Versions releases 0.2.x & above
0.6.0 (Unreleased)
==================

see issues #109 & #111
see issues #109, #111 & #112

- Add support for SQLAlchemy 2.0.
- Add official support for Python 3.12 and 3.13.
- Remove examples of defunct features from the documentation.

0.5.0 (2025-11-18)
Expand Down
4 changes: 2 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
import requests


# Python versions supported and tested against: 3.8, 3.9, 3.10, 3.11
# Python versions supported and tested against: 3.8, 3.9, 3.10, 3.11, 3.12, 3.13
PYTHON_MINOR_VERSION_MIN = 8
PYTHON_MINOR_VERSION_MAX = 11
PYTHON_MINOR_VERSION_MAX = 13

nox.options.default_venv_backend = "uv"

Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def read(name):

setup(
name="sqlalchemy_mptt",
version="0.5.0",
version="0.6.0",
url="http://github.com/uralbash/sqlalchemy_mptt/",
author="Svintsov Dmitry",
author_email="sacrud@uralbash.ru",
Expand Down Expand Up @@ -39,6 +39,8 @@ def read(name):
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Framework :: Pyramid",
"Framework :: Flask",
"Topic :: Internet",
Expand Down
44 changes: 30 additions & 14 deletions sqlalchemy_mptt/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,29 @@

"""
# standard library
import os
import contextlib
import json
import os
import sys
import typing
import unittest

# SQLAlchemy
import sqlalchemy as sa
from sqlalchemy import event, create_engine
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker

# third-party
from sqlalchemy_mptt import mptt_sessionmaker
from sqlalchemy_mptt.sqlalchemy_compat import compat_layer

# local
from .cases.get_tree import Tree
from .cases.get_node import GetNodes
from .cases.edit_node import Changes
from .cases.get_node import GetNodes
from .cases.get_tree import Tree
from .cases.initialize import Initialize
from .cases.integrity import DataIntegrity
from .cases.move_node import MoveAfter, MoveBefore, MoveInside
from .cases.initialize import Initialize

BaseType = unittest.TestCase if typing.TYPE_CHECKING else object


def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[]):
Expand All @@ -73,6 +76,24 @@ def decorator(test_method):
return decorator


class DatabaseSetupMixin(BaseType):
base: compat_layer.declarative_base() # type: ignore

def setUp(self):
with contextlib.suppress(AttributeError):
super().setUp()
self.engine: sa.engine.Engine = create_engine("sqlite:///:memory:")
Session = mptt_sessionmaker(sessionmaker(bind=self.engine))
self.session = Session()
self.base.metadata.create_all(self.engine)

def tearDown(self):
with contextlib.suppress(AttributeError):
super().tearDown()
self.session.close()
self.engine.dispose()


class Fixtures(object):
def __init__(self, session):
self.session = session
Expand All @@ -97,6 +118,7 @@ class TreeTestingMixin(
MoveInside,
Tree,
GetNodes,
DatabaseSetupMixin
):
base = None
model = None
Expand All @@ -116,10 +138,7 @@ def stop_query_counter(self):
)

def setUp(self):
self.engine = create_engine("sqlite:///:memory:")
Session = mptt_sessionmaker(sessionmaker(bind=self.engine))
self.session = Session()
self.base.metadata.create_all(self.engine)
super().setUp()
self.fixture = Fixtures(self.session)
self.fixture.add(
self.model, os.path.join("fixtures", getattr(self, "fixtures", "tree.json"))
Expand All @@ -134,9 +153,6 @@ def setUp(self):
self.model.tree_id,
)

def tearDown(self):
self.base.metadata.drop_all(self.engine)

def test_session_expire_for_move_after_to_new_tree(self):
"""
https://github.com/uralbash/sqlalchemy_mptt/issues/33
Expand Down
40 changes: 16 additions & 24 deletions sqlalchemy_mptt/tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@

import unittest

from sqlalchemy import Column, Boolean, Integer, create_engine
from sqlalchemy import Boolean, Column, Integer
from sqlalchemy.event import contains
from sqlalchemy.orm import sessionmaker

from sqlalchemy_mptt import mptt_sessionmaker

from sqlalchemy_mptt.mixins import BaseNestedSets
from sqlalchemy_mptt.sqlalchemy_compat import compat_layer
from sqlalchemy_mptt.tests import TreeTestingMixin

from sqlalchemy_mptt.tests import DatabaseSetupMixin, TreeTestingMixin

Base = compat_layer.declarative_base()

Expand Down Expand Up @@ -156,40 +152,36 @@ def test_remove(self):
tree_manager.register_events()


class Tree0Id(unittest.TestCase):
class Tree0Id(DatabaseSetupMixin, unittest.TestCase):
"""Test case where node id is provided and starts with 0

See comments in https://github.com/uralbash/sqlalchemy_mptt/issues/57
"""
def test(self):
engine = create_engine('sqlite:///:memory:')
Session = mptt_sessionmaker(sessionmaker(bind=engine))
session = Session()
Base.metadata.create_all(engine)

base = Base

def test(self):
root = Tree(id=0)
child = Tree(id=1, parent_id=0)

session.add(root)
session.add(child)
session.commit()
self.session.add(root)
self.session.add(child)
self.session.commit()

self.assertEqual(root.tree_id, 1)
self.assertEqual(child.tree_id, 1)


class InitialInsert(unittest.TestCase):
class InitialInsert(DatabaseSetupMixin, unittest.TestCase):
"""Test case for initial insertion of node as specified in
docs/initialize.rst
"""

base = Base

def test_documented_initial_insert(self):
from sqlalchemy_mptt import tree_manager

engine = create_engine('sqlite:///:memory:')
Session = mptt_sessionmaker(sessionmaker(bind=engine))
session = Session()
Base.metadata.create_all(engine)

tree_manager.register_events(remove=True) # Disable MPTT events

_tree_id = 1
Expand All @@ -202,11 +194,11 @@ def test_documented_initial_insert(self):
right=0,
tree_id=_tree_id
)
session.add(item)
session.commit()
self.session.add(item)
self.session.commit()

tree_manager.register_events() # enabled MPTT events back
Tree.rebuild_tree(
session,
self.session,
_tree_id
) # rebuild lft, rgt value automatically
16 changes: 4 additions & 12 deletions sqlalchemy_mptt/tests/test_inheritance.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import unittest

import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker

from sqlalchemy_mptt.mixins import BaseNestedSets
from sqlalchemy_mptt.sqlalchemy_compat import compat_layer
from sqlalchemy_mptt.tests import TreeTestingMixin, failures_expected_on

from sqlalchemy_mptt.tests import (DatabaseSetupMixin, TreeTestingMixin,
failures_expected_on)

Base = compat_layer.declarative_base()

Expand Down Expand Up @@ -45,16 +44,9 @@ class SpecializedTree(GenericTree):
__table_args__ = tuple()


class TestTree(unittest.TestCase):

def setUp(self):
self.engine = sa.create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=self.engine)
self.session = Session()
Base.metadata.create_all(self.engine)
class TestTree(DatabaseSetupMixin, unittest.TestCase):

def tearDown(self):
Base.metadata.drop_all(self.engine)
base = Base

def test_create_generic(self):
self.session.add(GenericTree(ppk=1))
Expand Down
29 changes: 17 additions & 12 deletions sqlalchemy_mptt/tests/test_stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
#
# Distributed under terms of the MIT license.
"""Test cases written using Hypothesis stateful testing framework."""
from hypothesis import HealthCheck, settings, strategies as st
from hypothesis.stateful import Bundle, RuleBasedStateMachine, consumes, invariant, rule
from sqlalchemy import Column, Integer, Boolean, create_engine
from sqlalchemy.orm import joinedload, sessionmaker

from sqlalchemy_mptt import BaseNestedSets, mptt_sessionmaker
from hypothesis import HealthCheck, settings
from hypothesis import strategies as st
from hypothesis.stateful import (Bundle, RuleBasedStateMachine, consumes,
invariant, rule)
from sqlalchemy import Boolean, Column, Integer
from sqlalchemy.orm import joinedload

from sqlalchemy_mptt import BaseNestedSets
from sqlalchemy_mptt.sqlalchemy_compat import compat_layer

from sqlalchemy_mptt.tests import DatabaseSetupMixin

Base = compat_layer.declarative_base()

Expand All @@ -27,15 +29,18 @@ def __repr__(self):
return "<Node (%s)>" % self.id


class TreeStateMachine(RuleBasedStateMachine):
class TreeStateMachine(DatabaseSetupMixin, RuleBasedStateMachine):
"""A state machine with various possible actions and transitions for the Tree model."""

base = Base

def __init__(self):
super().__init__()
self.engine = create_engine("sqlite:///:memory:")
Session = mptt_sessionmaker(sessionmaker(bind=self.engine))
self.session = Session()
Base.metadata.create_all(self.engine)
self.setUp()

def teardown(self):
super().teardown()
self.tearDown()

node = Bundle('node')

Expand Down