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
3 changes: 1 addition & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
relative_files = True
[report]
omit =
*/python?.?/*
*/site-packages/nose/*
*/tests/*
5 changes: 3 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ 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'
rev: '7.3.0'
hooks:
- id: flake8
- repo: https://github.com/pappasam/toml-sort
rev: 'v0.24.2'
hooks:
- id: toml-sort
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Versions releases 0.2.x & above
###############################

0.5.0 (Unreleased)
==================

- 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)
==================

Expand Down
30 changes: 8 additions & 22 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,15 @@

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:
$ 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

Expand Down Expand Up @@ -84,10 +80,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+
Expand All @@ -99,7 +94,11 @@ def parametrize_test_versions():
def test(session, sqlalchemy):
"""Run tests with pytest.

Use the --coverage option to run tests with coverage.
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:

Expand All @@ -110,20 +109,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
pytest_cmd = ["pytest"] + (
session.posargs or [
"--pyargs", "sqlalchemy_mptt",
"--cov", "sqlalchemy_mptt", "--cov-report", "term-missing:skip-covered",
"-W", "error:::sqlalchemy_mptt"
]
) + (["--cov-report", "xml"] if with_coverage else [])
session.run(*pytest_cmd)
pytest_args = session.posargs or ["--pyargs", "sqlalchemy_mptt"]
session.run("pytest", *pytest_args, env={"SQLALCHEMY_SILENCE_UBER_WARNING": "1"})


@nox.session(default=False)
Expand All @@ -133,7 +120,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)
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ exclude = '''
| dist
)/
'''

[tool.pytest.ini_options]
filterwarnings = [
"error:::sqlalchemy_mptt"
]
addopts = "--cov sqlalchemy_mptt --cov-report term-missing:skip-covered"
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
SQLAlchemy>=1.0.0,<1.4
SQLAlchemy>=1.0.0,<2.0
4 changes: 1 addition & 3 deletions 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.4.0",
version="0.5.0",
url="http://github.com/uralbash/sqlalchemy_mptt/",
author="Svintsov Dmitry",
author_email="sacrud@uralbash.ru",
Expand Down Expand Up @@ -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",
Expand Down
24 changes: 5 additions & 19 deletions sqlalchemy_mptt/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,29 +479,15 @@ 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):
"""A weak reference dictionary that returns a new `WeakSet` as a default
value for missing keys."""

def __getitem__(self, key):
try:
return super(_WeakDefaultDict, self).__getitem__(key)
except KeyError:
self[key] = value = _WeakDictBasedSet()
self[key] = value = weakref.WeakSet()
return value


Expand Down Expand Up @@ -589,7 +575,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:
Expand Down
5 changes: 1 addition & 4 deletions sqlalchemy_mptt/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions sqlalchemy_mptt/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -50,6 +53,26 @@
from .cases.initialize import Initialize


def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[]):
"""
Decorator to mark tests that are expected to fail on specific versions of
SQLAlchemy and/or Python.

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):
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 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
Expand Down
10 changes: 9 additions & 1 deletion sqlalchemy_mptt/tests/cases/get_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 3 additions & 8 deletions sqlalchemy_mptt/tests/test_inheritance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -150,11 +150,6 @@ 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
@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()