From c6fc8e301fc43036e7bf8c0c53b7a3f256ae27d6 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 26 Nov 2025 03:42:51 -0500 Subject: [PATCH 1/6] Add support for Python 3.12 and 3.13 --- CHANGES.rst | 1 + noxfile.py | 4 ++-- setup.py | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2fd09ba..5e1d934 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ Versions releases 0.2.x & above see issues #109 & #111 - 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) diff --git a/noxfile.py b/noxfile.py index 4c5b2b8..ffdad78 100644 --- a/noxfile.py +++ b/noxfile.py @@ -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" diff --git a/setup.py b/setup.py index e824df2..a530939 100644 --- a/setup.py +++ b/setup.py @@ -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", @@ -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", From 64e7445529916926c1a4375a3198a58a36a9830a Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 26 Nov 2025 06:25:06 -0500 Subject: [PATCH 2/6] Add isort pre-commit --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd1422b..3b62b14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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) From d971fb5e126eccd474f6d3a9751d6273ca346edd Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 29 Nov 2025 12:01:45 -0500 Subject: [PATCH 3/6] Cleanup all test connections --- sqlalchemy_mptt/tests/__init__.py | 48 ++++++++++++++++------- sqlalchemy_mptt/tests/test_events.py | 40 ++++++++----------- sqlalchemy_mptt/tests/test_inheritance.py | 16 ++------ sqlalchemy_mptt/tests/test_stateful.py | 29 ++++++++------ 4 files changed, 71 insertions(+), 62 deletions(-) diff --git a/sqlalchemy_mptt/tests/__init__.py b/sqlalchemy_mptt/tests/__init__.py index 4a64f73..3ef3ce2 100644 --- a/sqlalchemy_mptt/tests/__init__.py +++ b/sqlalchemy_mptt/tests/__init__.py @@ -31,26 +31,33 @@ """ # 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 + +if typing.TYPE_CHECKING: + BaseType = unittest.TestCase +else: + BaseType = object +DeclarativeBase = compat_layer.declarative_base() def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[]): @@ -73,6 +80,24 @@ def decorator(test_method): return decorator +class DatabaseSetupMixin(BaseType): + base: DeclarativeBase # 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 @@ -97,6 +122,7 @@ class TreeTestingMixin( MoveInside, Tree, GetNodes, + DatabaseSetupMixin ): base = None model = None @@ -116,10 +142,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")) @@ -134,9 +157,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 diff --git a/sqlalchemy_mptt/tests/test_events.py b/sqlalchemy_mptt/tests/test_events.py index 39a5bd1..71acaf9 100644 --- a/sqlalchemy_mptt/tests/test_events.py +++ b/sqlalchemy_mptt/tests/test_events.py @@ -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() @@ -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 @@ -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 diff --git a/sqlalchemy_mptt/tests/test_inheritance.py b/sqlalchemy_mptt/tests/test_inheritance.py index 1ac271d..d0ec9ad 100644 --- a/sqlalchemy_mptt/tests/test_inheritance.py +++ b/sqlalchemy_mptt/tests/test_inheritance.py @@ -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() @@ -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)) diff --git a/sqlalchemy_mptt/tests/test_stateful.py b/sqlalchemy_mptt/tests/test_stateful.py index be93705..9f79d5e 100644 --- a/sqlalchemy_mptt/tests/test_stateful.py +++ b/sqlalchemy_mptt/tests/test_stateful.py @@ -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() @@ -27,15 +29,18 @@ def __repr__(self): return "" % 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') From 9ea7329f1d4fb4bd59bd4ec33440bd849daab419 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 29 Nov 2025 12:26:16 -0500 Subject: [PATCH 4/6] Use ternary --- sqlalchemy_mptt/tests/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sqlalchemy_mptt/tests/__init__.py b/sqlalchemy_mptt/tests/__init__.py index 3ef3ce2..16b1092 100644 --- a/sqlalchemy_mptt/tests/__init__.py +++ b/sqlalchemy_mptt/tests/__init__.py @@ -53,10 +53,7 @@ from .cases.integrity import DataIntegrity from .cases.move_node import MoveAfter, MoveBefore, MoveInside -if typing.TYPE_CHECKING: - BaseType = unittest.TestCase -else: - BaseType = object +BaseType = unittest.TestCase if typing.TYPE_CHECKING else object DeclarativeBase = compat_layer.declarative_base() From d62bbc9439ff49d71564c9d9fc0804af0b64fae4 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 29 Nov 2025 13:05:19 -0500 Subject: [PATCH 5/6] Add PR link --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5e1d934..34a67ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,7 @@ 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. From 4e00103d7cd11715d1734deb471d2e804f57a2f3 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 29 Nov 2025 13:14:42 -0500 Subject: [PATCH 6/6] Simplify code --- sqlalchemy_mptt/tests/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sqlalchemy_mptt/tests/__init__.py b/sqlalchemy_mptt/tests/__init__.py index 16b1092..ed454c3 100644 --- a/sqlalchemy_mptt/tests/__init__.py +++ b/sqlalchemy_mptt/tests/__init__.py @@ -54,7 +54,6 @@ from .cases.move_node import MoveAfter, MoveBefore, MoveInside BaseType = unittest.TestCase if typing.TYPE_CHECKING else object -DeclarativeBase = compat_layer.declarative_base() def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[]): @@ -78,7 +77,7 @@ def decorator(test_method): class DatabaseSetupMixin(BaseType): - base: DeclarativeBase # type: ignore + base: compat_layer.declarative_base() # type: ignore def setUp(self): with contextlib.suppress(AttributeError):