From 6feda7d0761c65c9ec09cfceca0394c59c829ba5 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Mon, 9 Jun 2025 08:06:37 -0400 Subject: [PATCH 01/22] Fix tests for SQLAlchemy 1.4 --- requirements.txt | 2 +- setup.py | 2 +- sqlalchemy_mptt/tests/cases/edit_node.py | 13 ++++++++----- sqlalchemy_mptt/tests/cases/get_tree.py | 4 +++- sqlalchemy_mptt/tests/test_inheritance.py | 10 ++++------ 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index b794599..7149786 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -SQLAlchemy>=1.0.0,<1.4 +SQLAlchemy>=1.0.0,<2.0 diff --git a/setup.py b/setup.py index af9250b..ddf84c3 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def read(name): setup( name="sqlalchemy_mptt", - version="0.4.0", + version="0.5.0", url="http://github.com/uralbash/sqlalchemy_mptt/", author="Svintsov Dmitry", author_email="sacrud@uralbash.ru", diff --git a/sqlalchemy_mptt/tests/cases/edit_node.py b/sqlalchemy_mptt/tests/cases/edit_node.py index de109d6..76431b0 100644 --- a/sqlalchemy_mptt/tests/cases/edit_node.py +++ b/sqlalchemy_mptt/tests/cases/edit_node.py @@ -478,11 +478,14 @@ def test_rebuild(self): 4 14(9)15 18(11)19 """ - self.session.query(self.model).update({ - self.model.left: 0, - self.model.right: 0, - self.model.level: 0 - }) + self.session.query(self.model).update( + { + self.model.left: 0, + self.model.right: 0, + self.model.level: 0 + }, + synchronize_session=False # Fails with the default 'evaluate' option for SQLAlchemy 1.4 on PyPy + ) self.model.rebuild(self.session, 1) _level = self.model.get_default_level() self.assertEqual( diff --git a/sqlalchemy_mptt/tests/cases/get_tree.py b/sqlalchemy_mptt/tests/cases/get_tree.py index 7f00184..4a24e5f 100644 --- a/sqlalchemy_mptt/tests/cases/get_tree.py +++ b/sqlalchemy_mptt/tests/cases/get_tree.py @@ -11,7 +11,9 @@ def test_get_empty_tree(self): """ No rows in database. """ - self.session.query(self.model).delete() + self.session.query(self.model).delete( + synchronize_session=False # Fails with the default'evaluate' option for SQLAlchemy 1.4 on PyPy + ) self.session.flush() tree = self.model.get_tree(self.session) self.assertEqual(tree, []) diff --git a/sqlalchemy_mptt/tests/test_inheritance.py b/sqlalchemy_mptt/tests/test_inheritance.py index 8f6f83c..57c81a2 100644 --- a/sqlalchemy_mptt/tests/test_inheritance.py +++ b/sqlalchemy_mptt/tests/test_inheritance.py @@ -150,11 +150,9 @@ class TestInheritanceTree(TreeTestingMixin, unittest.TestCase): base = Base2 model = InheritanceTree - # For SQLAlchemy 1.4 support - # @unittest.skipIf( - # sa.__version__ < "1.4", - # "Trees involving inheritance are only supported on " - # "SQLAlchemy version 1.4 and above") - @unittest.expectedFailure + @unittest.skipIf( + sa.__version__ < "1.4", + "Trees involving inheritance are only supported on " + "SQLAlchemy version 1.4 and above") def test_rebuild(self): super(TestInheritanceTree, self).test_rebuild() From 4e62ba9bc8b143c8d8bed41b27a5e62448fccf67 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Mon, 9 Jun 2025 09:36:38 -0400 Subject: [PATCH 02/22] Disable coverage by default for PyPy --- noxfile.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/noxfile.py b/noxfile.py index 8fe9295..860a7dd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -116,13 +116,24 @@ def test(session, sqlalchemy): with_coverage = False else: with_coverage = True - pytest_cmd = ["pytest"] + ( - session.posargs or [ - "--pyargs", "sqlalchemy_mptt", - "--cov", "sqlalchemy_mptt", "--cov-report", "term-missing:skip-covered", - "-W", "error:::sqlalchemy_mptt" + if with_coverage: + coverage_options = [ + "--cov", "sqlalchemy_mptt", + "--cov-report", "term-missing:skip-covered", + "--cov-report", "xml" ] - ) + (["--cov-report", "xml"] if with_coverage else []) + elif session.python.startswith("pypy"): + # Disable coverage for PyPy as it slows down the tests significantly + # See: https://github.com/sqlalchemy/sqlalchemy/issues/9154#issuecomment-1687420057 + coverage_options = [] + else: + coverage_options = [ + "--cov", "sqlalchemy_mptt", + "--cov-report", "term-missing:skip-covered" + ] + pytest_cmd = ["pytest"] + coverage_options + ( + session.posargs or ["--pyargs", "sqlalchemy_mptt", "-W", "error:::sqlalchemy_mptt"] + ) session.run(*pytest_cmd) From a97cac95919bf4b6376dd2417b7c373cc19ac70d Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Mon, 9 Jun 2025 13:46:37 -0400 Subject: [PATCH 03/22] Suppress coverage reporting for tests that don't run for all versions --- sqlalchemy_mptt/tests/test_inheritance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_mptt/tests/test_inheritance.py b/sqlalchemy_mptt/tests/test_inheritance.py index 57c81a2..5aba3eb 100644 --- a/sqlalchemy_mptt/tests/test_inheritance.py +++ b/sqlalchemy_mptt/tests/test_inheritance.py @@ -154,5 +154,5 @@ class TestInheritanceTree(TreeTestingMixin, unittest.TestCase): sa.__version__ < "1.4", "Trees involving inheritance are only supported on " "SQLAlchemy version 1.4 and above") - def test_rebuild(self): + def test_rebuild(self): # pragma: no cover super(TestInheritanceTree, self).test_rebuild() From 9c1fadcabc45d22efff9e53dd192bec147bc40ca Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 18 Jun 2025 00:53:24 -0400 Subject: [PATCH 04/22] Remove bespoke WeakSet implementation --- sqlalchemy_mptt/events.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/sqlalchemy_mptt/events.py b/sqlalchemy_mptt/events.py index 268837f..b92e343 100644 --- a/sqlalchemy_mptt/events.py +++ b/sqlalchemy_mptt/events.py @@ -479,29 +479,13 @@ def mptt_before_update(mapper, connection, instance): ) -class _WeakDictBasedSet(weakref.WeakKeyDictionary, object): - """ - In absence of a default weakset implementation, provide our own dict - based solution. - """ - - def add(self, obj): - self[obj] = None - - def discard(self, obj): - super(_WeakDictBasedSet, self).pop(obj, None) - - def pop(self): - return self.popitem()[0] - - -class _WeakDefaultDict(weakref.WeakKeyDictionary, object): +class _WeakDefaultDict(weakref.WeakKeyDictionary): def __getitem__(self, key): try: return super(_WeakDefaultDict, self).__getitem__(key) except KeyError: - self[key] = value = _WeakDictBasedSet() + self[key] = value = weakref.WeakSet() return value From 8e64414782ffdecf2bef0a6f10a8d732f0022c07 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 18 Jun 2025 14:17:44 -0400 Subject: [PATCH 05/22] Ignore coverage for tests --- .coveragerc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index a92d227..bcc7f01 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,5 +2,4 @@ relative_files = True [report] omit = - */python?.?/* - */site-packages/nose/* + */tests/* From 915b122284603e146f4c51ff2adf2c3c33aef74a Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 18 Jun 2025 20:40:33 -0400 Subject: [PATCH 06/22] Unify after_flush_postexec execution path for CPython & PyPy --- sqlalchemy_mptt/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_mptt/events.py b/sqlalchemy_mptt/events.py index b92e343..70bdad2 100644 --- a/sqlalchemy_mptt/events.py +++ b/sqlalchemy_mptt/events.py @@ -573,7 +573,7 @@ def after_flush_postexec(self, session, context): parents of all modified instances part of this flush. """ instances = self.instances[session] - while instances: + while True: try: instance = instances.pop() except KeyError: From c84f8d0c753685a43fc00a588498b86c9f5bf85c Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 18 Jun 2025 21:12:54 -0400 Subject: [PATCH 07/22] Remove redundant execution paths --- sqlalchemy_mptt/mixins.py | 5 +---- sqlalchemy_mptt/tests/cases/get_node.py | 10 +++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/sqlalchemy_mptt/mixins.py b/sqlalchemy_mptt/mixins.py index 3dd63fd..4f7533b 100644 --- a/sqlalchemy_mptt/mixins.py +++ b/sqlalchemy_mptt/mixins.py @@ -405,10 +405,7 @@ def get_siblings(self, include_self=False, session=None): """ table = self.__class__ query = self._base_query_obj(session=session) - if self.parent_id: - query = query.filter(table.parent_id == self.parent_id) - else: - query = query.filter(table.parent_id == None) + query = query.filter(table.parent_id == self.parent_id) if not include_self: query = query.filter(self.get_pk_column() != self.get_pk_value()) return query diff --git a/sqlalchemy_mptt/tests/cases/get_node.py b/sqlalchemy_mptt/tests/cases/get_node.py index e592c6c..3431d86 100644 --- a/sqlalchemy_mptt/tests/cases/get_node.py +++ b/sqlalchemy_mptt/tests/cases/get_node.py @@ -17,7 +17,7 @@ def test_get_siblings(self): .. code:: level Nested sets example - 1 1(1)22 + 1 1(1)22 (12) _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 @@ -42,6 +42,14 @@ def test_get_siblings(self): ) self.assertEqual([], node9.get_siblings().all()) # flake8: noqa + node1 = ( + self.session.query(self.model).filter(self.model.get_pk_column() == 1).one() + ) + points = ( + self.session.query(self.model).filter(self.model.get_pk_column() == 12).all() + ) + self.assertEqual(points, node1.get_siblings().all()) + def test_get_children(self): """ Get children of node From c39eea84f8cda19e76b1e990fb998b20b2d08213 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 18 Jun 2025 21:22:50 -0400 Subject: [PATCH 08/22] Remove individual coverage skip for test --- sqlalchemy_mptt/tests/test_inheritance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_mptt/tests/test_inheritance.py b/sqlalchemy_mptt/tests/test_inheritance.py index 5aba3eb..57c81a2 100644 --- a/sqlalchemy_mptt/tests/test_inheritance.py +++ b/sqlalchemy_mptt/tests/test_inheritance.py @@ -154,5 +154,5 @@ class TestInheritanceTree(TreeTestingMixin, unittest.TestCase): sa.__version__ < "1.4", "Trees involving inheritance are only supported on " "SQLAlchemy version 1.4 and above") - def test_rebuild(self): # pragma: no cover + def test_rebuild(self): super(TestInheritanceTree, self).test_rebuild() From a2b70a31d97c53aad9363c42add920992165b396 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Wed, 18 Jun 2025 23:22:18 -0400 Subject: [PATCH 09/22] Add docstring --- sqlalchemy_mptt/events.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlalchemy_mptt/events.py b/sqlalchemy_mptt/events.py index 70bdad2..cdd1d86 100644 --- a/sqlalchemy_mptt/events.py +++ b/sqlalchemy_mptt/events.py @@ -480,6 +480,8 @@ def mptt_before_update(mapper, connection, instance): class _WeakDefaultDict(weakref.WeakKeyDictionary): + """A weak reference dictionary that returns a new `WeakSet` as a default + value for missing keys.""" def __getitem__(self, key): try: From 80a01c99a237b23b380932364f88070f4307cfc7 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Fri, 20 Jun 2025 13:42:05 -0400 Subject: [PATCH 10/22] Keep existing tests while also run the failing tests to observe future behavior --- sqlalchemy_mptt/tests/__init__.py | 26 +++++++++++++++++++++++ sqlalchemy_mptt/tests/cases/get_tree.py | 9 ++++++++ sqlalchemy_mptt/tests/test_events.py | 14 +++++++++++- sqlalchemy_mptt/tests/test_inheritance.py | 21 ++++++++++++------ 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/sqlalchemy_mptt/tests/__init__.py b/sqlalchemy_mptt/tests/__init__.py index a6ccc9a..daf3f5d 100644 --- a/sqlalchemy_mptt/tests/__init__.py +++ b/sqlalchemy_mptt/tests/__init__.py @@ -33,8 +33,11 @@ # standard library import os import json +import sys +import unittest # SQLAlchemy +import sqlalchemy as sa from sqlalchemy import event, create_engine from sqlalchemy.orm import sessionmaker @@ -50,6 +53,29 @@ from .cases.initialize import Initialize +def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[], interpreters=[]): + """ + Decorator to mark tests that are expected to fail on specific versions of + SQLAlchemy, Python, or specific interpreters. + + If a parameter is not provided, it is assumed that the failure is expected on all versions or interpreters. + If more than one parameter is provided, it is assumed that the failure is expected on all combinations of those parameters. + """ + def decorator(test_method): + if sqlalchemy_versions: + if not any(sa.__version__.startswith(v) for v in sqlalchemy_versions): + return test_method + if python_versions: + if not any(sys.version.startswith(v) for v in python_versions): + return test_method + if interpreters: + if not any(sys.implementation.name == v for v in interpreters): + return test_method + # If we reach here, it means the test is expected to fail + return unittest.expectedFailure(test_method) + return decorator + + class Fixtures(object): def __init__(self, session): self.session = session diff --git a/sqlalchemy_mptt/tests/cases/get_tree.py b/sqlalchemy_mptt/tests/cases/get_tree.py index 4a24e5f..2f7d797 100644 --- a/sqlalchemy_mptt/tests/cases/get_tree.py +++ b/sqlalchemy_mptt/tests/cases/get_tree.py @@ -8,6 +8,15 @@ def get_obj(session, model, id): class Tree(object): def test_get_empty_tree(self): + """ + No rows in database. + """ + self.session.query(self.model).delete() + self.session.flush() + tree = self.model.get_tree(self.session) + self.assertEqual(tree, []) + + def test_get_empty_tree_without_synchronize(self): """ No rows in database. """ diff --git a/sqlalchemy_mptt/tests/test_events.py b/sqlalchemy_mptt/tests/test_events.py index e4bb734..ef3ba07 100644 --- a/sqlalchemy_mptt/tests/test_events.py +++ b/sqlalchemy_mptt/tests/test_events.py @@ -19,7 +19,7 @@ from sqlalchemy_mptt import mptt_sessionmaker -from . import TreeTestingMixin +from . import TreeTestingMixin, failures_expected_on from ..mixins import BaseNestedSets Base = declarative_base() @@ -63,16 +63,28 @@ class TestTree(TreeTestingMixin, unittest.TestCase): base = Base model = Tree + @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) + def test_get_empty_tree(self): + super().test_get_empty_tree() + class TestTreeWithCustomId(TreeTestingMixin, unittest.TestCase): base = Base model = TreeWithCustomId + @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) + def test_get_empty_tree(self): + super().test_get_empty_tree() + class TestTreeWithCustomLevel(TreeTestingMixin, unittest.TestCase): base = Base model = TreeWithCustomLevel + @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) + def test_get_empty_tree(self): + super().test_get_empty_tree() + class Events(unittest.TestCase): diff --git a/sqlalchemy_mptt/tests/test_inheritance.py b/sqlalchemy_mptt/tests/test_inheritance.py index 57c81a2..43199e8 100644 --- a/sqlalchemy_mptt/tests/test_inheritance.py +++ b/sqlalchemy_mptt/tests/test_inheritance.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from . import TreeTestingMixin +from . import TreeTestingMixin, failures_expected_on from ..mixins import BaseNestedSets Base = declarative_base() @@ -104,6 +104,10 @@ class TestGenericTree(TreeTestingMixin, unittest.TestCase): base = Base model = GenericTree + @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) + def test_get_empty_tree(self): + super().test_get_empty_tree() + class TestSpecializedTree(TreeTestingMixin, unittest.TestCase): base = Base @@ -114,6 +118,10 @@ def test_rebuild(self): # This test will always fail on specialized classes. super().test_rebuild() + @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) + def test_get_empty_tree(self): + super().test_get_empty_tree() + Base2 = declarative_base() @@ -150,9 +158,10 @@ class TestInheritanceTree(TreeTestingMixin, unittest.TestCase): base = Base2 model = InheritanceTree - @unittest.skipIf( - sa.__version__ < "1.4", - "Trees involving inheritance are only supported on " - "SQLAlchemy version 1.4 and above") + @failures_expected_on(sqlalchemy_versions=['1.0', '1.1', '1.2', '1.3']) def test_rebuild(self): - super(TestInheritanceTree, self).test_rebuild() + super().test_rebuild() + + @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) + def test_get_empty_tree(self): + super().test_get_empty_tree() From 86b6cc9b40755d425e769c5a7f40c180d1c68838 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Fri, 20 Jun 2025 17:43:02 -0400 Subject: [PATCH 11/22] Add release notes --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4ec570d..0fe026f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Versions releases 0.2.x & above ############################### +0.5.0 (Unreleased) +================== + +- Support for SQLAlchemy 1.4 +- Drop official support for PyPy. + 0.4.0 (2025-05-30) ================== From f72f98d57a8ade284954c19b9cf9e3881c19389b Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Fri, 20 Jun 2025 17:45:10 -0400 Subject: [PATCH 12/22] Drop tests for PyPy --- noxfile.py | 10 +--------- setup.py | 2 -- sqlalchemy_mptt/tests/cases/edit_node.py | 3 +-- sqlalchemy_mptt/tests/cases/get_tree.py | 11 ----------- sqlalchemy_mptt/tests/test_events.py | 14 +------------- sqlalchemy_mptt/tests/test_inheritance.py | 12 ------------ 6 files changed, 3 insertions(+), 49 deletions(-) diff --git a/noxfile.py b/noxfile.py index 860a7dd..144a372 100644 --- a/noxfile.py +++ b/noxfile.py @@ -29,13 +29,11 @@ $ uv run noxfile.py -t sqla12 Run tests for a specific Python version: $ uv run noxfile.py -s test -p 3.X - $ uv run noxfile.py -s test -p pypy-3.X # For PyPy Set up a development environment with the default Python version (3.8): $ uv run noxfile.py -s dev Set up a development environment with a specific Python version: $ uv run noxfile.py -s dev -P 3.X - $ uv run noxfile.py -s dev -P pypy-3.X # For PyPy """ from itertools import groupby @@ -84,10 +82,9 @@ def parametrize_test_versions(): return [ nox.param( - f"{interpreter}3.{python_minor}", str(sqlalchemy_version), + f"3.{python_minor}", str(sqlalchemy_version), tags=[f"sqla{sqlalchemy_version.major}{sqlalchemy_version.minor}"] ) - for interpreter in ("", "pypy-") for python_minor in range(PYTHON_MINOR_VERSION_MIN, PYTHON_MINOR_VERSION_MAX + 1) for sqlalchemy_version in filtered_sqlalchemy_versions # SQLA 1.1 or below doesn't seem to support Python 3.10+ @@ -122,10 +119,6 @@ def test(session, sqlalchemy): "--cov-report", "term-missing:skip-covered", "--cov-report", "xml" ] - elif session.python.startswith("pypy"): - # Disable coverage for PyPy as it slows down the tests significantly - # See: https://github.com/sqlalchemy/sqlalchemy/issues/9154#issuecomment-1687420057 - coverage_options = [] else: coverage_options = [ "--cov", "sqlalchemy_mptt", @@ -144,7 +137,6 @@ def dev(session): To use a specific Python version, use the -P option: $ uv run noxfile.py -s dev -P 3.X - $ uv run noxfile.py -s dev -P pypy-3.X # For PyPy """ session.run("uv", "venv", "--python", session.python or f"3.{PYTHON_MINOR_VERSION_MIN}", "--seed") session.run(".venv/bin/pip", "install", "-r", "requirements-test.txt", external=True) diff --git a/setup.py b/setup.py index ddf84c3..e824df2 100644 --- a/setup.py +++ b/setup.py @@ -39,8 +39,6 @@ def read(name): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Pyramid", "Framework :: Flask", "Topic :: Internet", diff --git a/sqlalchemy_mptt/tests/cases/edit_node.py b/sqlalchemy_mptt/tests/cases/edit_node.py index 76431b0..ffb41ec 100644 --- a/sqlalchemy_mptt/tests/cases/edit_node.py +++ b/sqlalchemy_mptt/tests/cases/edit_node.py @@ -483,8 +483,7 @@ def test_rebuild(self): self.model.left: 0, self.model.right: 0, self.model.level: 0 - }, - synchronize_session=False # Fails with the default 'evaluate' option for SQLAlchemy 1.4 on PyPy + } ) self.model.rebuild(self.session, 1) _level = self.model.get_default_level() diff --git a/sqlalchemy_mptt/tests/cases/get_tree.py b/sqlalchemy_mptt/tests/cases/get_tree.py index 2f7d797..7f00184 100644 --- a/sqlalchemy_mptt/tests/cases/get_tree.py +++ b/sqlalchemy_mptt/tests/cases/get_tree.py @@ -16,17 +16,6 @@ def test_get_empty_tree(self): tree = self.model.get_tree(self.session) self.assertEqual(tree, []) - def test_get_empty_tree_without_synchronize(self): - """ - No rows in database. - """ - self.session.query(self.model).delete( - synchronize_session=False # Fails with the default'evaluate' option for SQLAlchemy 1.4 on PyPy - ) - self.session.flush() - tree = self.model.get_tree(self.session) - self.assertEqual(tree, []) - def test_get_empty_tree_with_custom_query(self): """ No rows with id < 0. diff --git a/sqlalchemy_mptt/tests/test_events.py b/sqlalchemy_mptt/tests/test_events.py index ef3ba07..e4bb734 100644 --- a/sqlalchemy_mptt/tests/test_events.py +++ b/sqlalchemy_mptt/tests/test_events.py @@ -19,7 +19,7 @@ from sqlalchemy_mptt import mptt_sessionmaker -from . import TreeTestingMixin, failures_expected_on +from . import TreeTestingMixin from ..mixins import BaseNestedSets Base = declarative_base() @@ -63,28 +63,16 @@ class TestTree(TreeTestingMixin, unittest.TestCase): base = Base model = Tree - @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) - def test_get_empty_tree(self): - super().test_get_empty_tree() - class TestTreeWithCustomId(TreeTestingMixin, unittest.TestCase): base = Base model = TreeWithCustomId - @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) - def test_get_empty_tree(self): - super().test_get_empty_tree() - class TestTreeWithCustomLevel(TreeTestingMixin, unittest.TestCase): base = Base model = TreeWithCustomLevel - @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) - def test_get_empty_tree(self): - super().test_get_empty_tree() - class Events(unittest.TestCase): diff --git a/sqlalchemy_mptt/tests/test_inheritance.py b/sqlalchemy_mptt/tests/test_inheritance.py index 43199e8..3434e93 100644 --- a/sqlalchemy_mptt/tests/test_inheritance.py +++ b/sqlalchemy_mptt/tests/test_inheritance.py @@ -104,10 +104,6 @@ class TestGenericTree(TreeTestingMixin, unittest.TestCase): base = Base model = GenericTree - @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) - def test_get_empty_tree(self): - super().test_get_empty_tree() - class TestSpecializedTree(TreeTestingMixin, unittest.TestCase): base = Base @@ -118,10 +114,6 @@ def test_rebuild(self): # This test will always fail on specialized classes. super().test_rebuild() - @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) - def test_get_empty_tree(self): - super().test_get_empty_tree() - Base2 = declarative_base() @@ -161,7 +153,3 @@ class TestInheritanceTree(TreeTestingMixin, unittest.TestCase): @failures_expected_on(sqlalchemy_versions=['1.0', '1.1', '1.2', '1.3']) def test_rebuild(self): super().test_rebuild() - - @failures_expected_on(sqlalchemy_versions=['1.4'], interpreters=['pypy']) - def test_get_empty_tree(self): - super().test_get_empty_tree() From 928b24ce4e06ea5e69056e81233aad942d752a74 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 21 Jun 2025 16:29:25 -0400 Subject: [PATCH 13/22] Simplify test invocation logic --- .github/workflows/run-tests.yml | 5 +++-- noxfile.py | 25 ++----------------------- pyproject.toml | 4 ++++ 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index dd06282..df81416 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -33,9 +33,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 - - run: uv tool install nox --with nox-uv - name: Run nox - run: uvx nox -s "${{ matrix.session }}" -- --coverage + run: > + uv run noxfile.py -s "${{ matrix.session }}" + -- --pyargs sqlalchemy_mptt --cov-report xml - name: Upload coverage data if: ${{ matrix.session != 'lint' }} uses: coverallsapp/github-action@v2 diff --git a/noxfile.py b/noxfile.py index 144a372..940ccf9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -23,8 +23,6 @@ Run all tests and linting: $ uv run noxfile.py - Run all tests with coverage and linting: - $ uv run noxfile.py -- --coverage Run tests for a specific SQLAlchemy version: $ uv run noxfile.py -t sqla12 Run tests for a specific Python version: @@ -107,27 +105,8 @@ def test(session, sqlalchemy): session.install("-r", "requirements-test.txt") session.install(f"sqlalchemy~={sqlalchemy}.0") session.install("-e", ".") - try: - session.posargs.remove("--coverage") - except ValueError: - with_coverage = False - else: - with_coverage = True - if with_coverage: - coverage_options = [ - "--cov", "sqlalchemy_mptt", - "--cov-report", "term-missing:skip-covered", - "--cov-report", "xml" - ] - else: - coverage_options = [ - "--cov", "sqlalchemy_mptt", - "--cov-report", "term-missing:skip-covered" - ] - pytest_cmd = ["pytest"] + coverage_options + ( - session.posargs or ["--pyargs", "sqlalchemy_mptt", "-W", "error:::sqlalchemy_mptt"] - ) - session.run(*pytest_cmd) + pytest_args = session.posargs or ["--pyargs", "sqlalchemy_mptt"] + session.run("pytest", *pytest_args) @nox.session(default=False) diff --git a/pyproject.toml b/pyproject.toml index dfab025..3b6eda6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,3 +14,7 @@ exclude = ''' | dist )/ ''' + +[tool.pytest.ini_options] +filterwarnings = "error:::sqlalchemy_mptt" +addopts = "--cov sqlalchemy_mptt --cov-report term-missing:skip-covered" From 5469adf77606fdf523cd8f54e8b68363fee020e9 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 21 Jun 2025 16:34:42 -0400 Subject: [PATCH 14/22] Update docstring --- noxfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 940ccf9..3a5d837 100644 --- a/noxfile.py +++ b/noxfile.py @@ -94,7 +94,8 @@ def parametrize_test_versions(): def test(session, sqlalchemy): """Run tests with pytest. - Use the --coverage option to run tests with coverage. + To pass additional arguments to pytest, use the posargs option: + $ uv run noxfile.py -s test -- -v For running tests for a specific SQLAlchemy version, use the tags option: From e43ad576d348b27344aab1ca73714f6ab369c1bc Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 21 Jun 2025 16:59:59 -0400 Subject: [PATCH 15/22] Validate toml files --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9d22c7..6a33de1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,7 @@ repos: - id: end-of-file-fixer exclude: '^.*\.svg$' - id: check-yaml + - id: check-toml - id: check-added-large-files - repo: https://github.com/pycqa/flake8 rev: '7.2.0' From 0f5be78eb6be3f0d02bdd5ecc042d93b9c8a152a Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 21 Jun 2025 17:08:52 -0400 Subject: [PATCH 16/22] Disable sqla 1.4 warnings for now --- noxfile.py | 2 +- pyproject.toml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 3a5d837..8b51a4d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -107,7 +107,7 @@ def test(session, sqlalchemy): session.install(f"sqlalchemy~={sqlalchemy}.0") session.install("-e", ".") pytest_args = session.posargs or ["--pyargs", "sqlalchemy_mptt"] - session.run("pytest", *pytest_args) + session.run("pytest", *pytest_args, env={"SQLALCHEMY_SILENCE_UBER_WARNING": "1"}) @nox.session(default=False) diff --git a/pyproject.toml b/pyproject.toml index 3b6eda6..2caf7b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,5 +16,7 @@ exclude = ''' ''' [tool.pytest.ini_options] -filterwarnings = "error:::sqlalchemy_mptt" +filterwarnings = [ + "error:::sqlalchemy_mptt" +] addopts = "--cov sqlalchemy_mptt --cov-report term-missing:skip-covered" From 4eef96faea96d26790032b486e1fb6c1c02cc5ce Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sat, 21 Jun 2025 23:05:52 -0400 Subject: [PATCH 17/22] Refine docstring --- noxfile.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 8b51a4d..c9865be 100644 --- a/noxfile.py +++ b/noxfile.py @@ -94,8 +94,11 @@ def parametrize_test_versions(): def test(session, sqlalchemy): """Run tests with pytest. - To pass additional arguments to pytest, use the posargs option: - $ uv run noxfile.py -s test -- -v + You can pass arguments to pytest using the `--` option. + + $ uv run noxfile.py -s test -- sqlalchemy_mptt/tests/test_events.py + + If no arguments are provided, it defaults to running all tests in the package. For running tests for a specific SQLAlchemy version, use the tags option: From 595b24abf2f7e0a453a53d6399fa0042f9831d7f Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sun, 22 Jun 2025 00:01:50 -0400 Subject: [PATCH 18/22] Validate toml files --- .pre-commit-config.yaml | 6 +++++- noxfile.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a33de1..cd1422b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,10 @@ repos: - id: check-toml - id: check-added-large-files - repo: https://github.com/pycqa/flake8 - rev: '7.2.0' + rev: '7.3.0' hooks: - id: flake8 +- repo: https://github.com/pappasam/toml-sort + rev: 'v0.24.2' + hooks: + - id: toml-sort diff --git a/noxfile.py b/noxfile.py index c9865be..2f4ec04 100644 --- a/noxfile.py +++ b/noxfile.py @@ -50,12 +50,14 @@ @nox.session() def lint(session): - """Run flake8.""" - session.install("flake8") + """Run linters.""" + session.install("flake8", "toml-sort") # stop the linter if there are Python syntax errors or undefined names session.run("flake8", "--select=E9,F63,F7,F82", "--show-source") # exit-zero treats all errors as warnings session.run("flake8", "--exit-zero", "--max-complexity=10") + # check the pyproject.toml file for correctness + session.run("toml-sort", "--check", "pyproject.toml") def parametrize_test_versions(): From d48f7218107cf42f761401ecb5a859acfc1d5718 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sun, 22 Jun 2025 00:17:27 -0400 Subject: [PATCH 19/22] Revert toml-sort checks --- noxfile.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 2f4ec04..c9865be 100644 --- a/noxfile.py +++ b/noxfile.py @@ -50,14 +50,12 @@ @nox.session() def lint(session): - """Run linters.""" - session.install("flake8", "toml-sort") + """Run flake8.""" + session.install("flake8") # stop the linter if there are Python syntax errors or undefined names session.run("flake8", "--select=E9,F63,F7,F82", "--show-source") # exit-zero treats all errors as warnings session.run("flake8", "--exit-zero", "--max-complexity=10") - # check the pyproject.toml file for correctness - session.run("toml-sort", "--check", "pyproject.toml") def parametrize_test_versions(): From ca140f2e6d0a36b781580fe91ac6c036e24e7500 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sun, 22 Jun 2025 00:30:08 -0400 Subject: [PATCH 20/22] Remove PyPy helper logic --- sqlalchemy_mptt/tests/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sqlalchemy_mptt/tests/__init__.py b/sqlalchemy_mptt/tests/__init__.py index daf3f5d..4a64f73 100644 --- a/sqlalchemy_mptt/tests/__init__.py +++ b/sqlalchemy_mptt/tests/__init__.py @@ -53,12 +53,12 @@ from .cases.initialize import Initialize -def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[], interpreters=[]): +def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[]): """ Decorator to mark tests that are expected to fail on specific versions of - SQLAlchemy, Python, or specific interpreters. + SQLAlchemy and/or Python. - If a parameter is not provided, it is assumed that the failure is expected on all versions or interpreters. + If a parameter is not provided, it is assumed that the failure is expected on all versions. If more than one parameter is provided, it is assumed that the failure is expected on all combinations of those parameters. """ def decorator(test_method): @@ -68,9 +68,6 @@ def decorator(test_method): if python_versions: if not any(sys.version.startswith(v) for v in python_versions): return test_method - if interpreters: - if not any(sys.implementation.name == v for v in interpreters): - return test_method # If we reach here, it means the test is expected to fail return unittest.expectedFailure(test_method) return decorator From 1f70c36ca1b755f33edb278b1ac0f92f3b0f0d0c Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sun, 22 Jun 2025 00:32:51 -0400 Subject: [PATCH 21/22] Revert formatting change --- sqlalchemy_mptt/tests/cases/edit_node.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sqlalchemy_mptt/tests/cases/edit_node.py b/sqlalchemy_mptt/tests/cases/edit_node.py index ffb41ec..de109d6 100644 --- a/sqlalchemy_mptt/tests/cases/edit_node.py +++ b/sqlalchemy_mptt/tests/cases/edit_node.py @@ -478,13 +478,11 @@ def test_rebuild(self): 4 14(9)15 18(11)19 """ - self.session.query(self.model).update( - { - self.model.left: 0, - self.model.right: 0, - self.model.level: 0 - } - ) + self.session.query(self.model).update({ + self.model.left: 0, + self.model.right: 0, + self.model.level: 0 + }) self.model.rebuild(self.session, 1) _level = self.model.get_default_level() self.assertEqual( From c59a6792e95046ae2cec8b1eaeddf79f5e0152a4 Mon Sep 17 00:00:00 2001 From: Fayaz Yusuf Khan Date: Sun, 22 Jun 2025 00:37:09 -0400 Subject: [PATCH 22/22] Update changelog --- CHANGES.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0fe026f..cc189d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,8 +4,12 @@ Versions releases 0.2.x & above 0.5.0 (Unreleased) ================== -- Support for SQLAlchemy 1.4 +- Add support for SQLAlchemy 1.4. - Drop official support for PyPy. +- Simplify memory management by using ``weakref.WeakSet`` instead of rolling our own + weak reference set. +- Unify ``after_flush_postexec`` execution path for CPython & PyPy. +- Simplify ``get_siblings``. 0.4.0 (2025-05-30) ==================