Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
uv run noxfile.py -s "${{ matrix.session }}"
-- --pyargs sqlalchemy_mptt --cov-report xml
- name: Upload coverage data
if: ${{ matrix.session != 'lint' }}
if: ${{ startsWith(matrix.session, 'test(') }}
uses: coverallsapp/github-action@v2
with:
flag-name: run-${{ join(matrix.*, '-') }}
Expand Down
4 changes: 3 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ Versions releases 0.2.x & above
0.5.0 (Unreleased)
==================

see issue #104
see issues #104 & #110

- 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``.
- Run doctest on all code snippets in the documentation.
- Fix some of the incorrect documentation snippets.

0.4.0 (2025-05-30)
==================
Expand Down
14 changes: 14 additions & 0 deletions docs/CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,19 @@ To run the tests and linters, you can use the following command:

For futher details, refer to the ``noxfile.py`` script.

Building Documentation
----------------------

The documentation on `ReadtheDocs <https://app.readthedocs.org/projects/sqlalchemy-mptt/>`_ is manually built from the master branch.
To build the documentation locally, you can run:

.. code-block:: bash

$ uv tool install sphinx --with-editable . --with-requirements requirements-doctest.txt
$ cd docs
$ make html

For futher details, refer to the ``docs/Makefile``.

.. |IRC Freenode| image:: https://img.shields.io/badge/irc-freenode-blue.svg
:target: https://webchat.freenode.net/?channels=sacrud
18 changes: 17 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.mathjax',
# 'sphinx.ext.mathbase',
'sphinx.ext.doctest',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down Expand Up @@ -69,3 +69,19 @@
'github_user': 'uralbash',
'github_repo': 'sqlalchemy_mptt',
}

# -- Options for doctest extension ------------------------------------------
doctest_global_setup = """
from sqlalchemy import create_engine, Column, Integer, Boolean
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session

from sqlalchemy_mptt import tree_manager
from sqlalchemy_mptt.mixins import BaseNestedSets
"""
doctest_global_cleanup = """
try:
session.flush()
except NameError:
pass
"""
52 changes: 49 additions & 3 deletions docs/crud.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,53 @@ INSERT

Insert node with parent_id==6

.. code-block:: python
.. testsetup::

from sqlalchemy import create_engine, Column, Integer, Boolean
from sqlalchemy.orm import Session
from sqlalchemy_mptt import tree_manager
from sqlalchemy_mptt.mixins import BaseNestedSets
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
engine = create_engine("sqlite:///:memory:")
session = Session(engine)

class Tree(Base, BaseNestedSets):
__tablename__ = "tree"

id = Column(Integer, primary_key=True)
visible = Column(Boolean)

def __repr__(self):
return "<Node (%s)>" % self.id

Base.metadata.create_all(engine)
tree_manager.register_events(remove=True)
instances = [
Tree(id=1, parent_id=None),
Tree(id=2, parent_id=1),
Tree(id=3, parent_id=2),
Tree(id=4, parent_id=1),
Tree(id=5, parent_id=4),
Tree(id=6, parent_id=4),
Tree(id=7, parent_id=1),
Tree(id=8, parent_id=7),
Tree(id=9, parent_id=8),
Tree(id=10, parent_id=7),
Tree(id=11, parent_id=10)
]
for instance in instances:
instance.left = 0
instance.right = 0
instance.visible = True
session.add_all(instances)
session.flush()
tree_manager.register_events()
Tree.rebuild_tree(session, tree_id=None)


.. testcode::

node = Tree(parent_id=6)
session.add(node)
Expand Down Expand Up @@ -45,7 +91,7 @@ UPDATE

Set parent_id=5 for node with id==8

.. code-block:: python
.. testcode::

node = session.query(Tree).filter(Tree.id == 8).one()
node.parent_id = 5
Expand Down Expand Up @@ -86,7 +132,7 @@ DELETE

Delete node with id==4

.. code-block:: python
.. testcode::

node = session.query(Tree).filter(Tree.id == 4).one()
session.delete(node)
Expand Down
44 changes: 31 additions & 13 deletions docs/initialize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ Setup

Create model with MPTT mixin:

.. code-block:: python
:linenos:
.. testcode::

from sqlalchemy import Column, Integer, Boolean
from sqlalchemy.ext.declarative import declarative_base
Expand All @@ -31,14 +30,13 @@ Session factory wrapper
For the automatic tree maintainance triggered after session flush to work
correctly, wrap the Session factory with :mod:`sqlalchemy_mptt.mptt_sessionmaker`

.. code-block:: python
:linenos:
.. testcode::

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy_mptt import mptt_sessionmaker

engine = create_engine('...')
engine = create_engine('sqlite:///:memory:')
Session = mptt_sessionmaker(sessionmaker(bind=engine))

Using session factory wrapper with flask_sqlalchemy
Expand All @@ -48,8 +46,7 @@ If you use Flask and SQLAlchemy, you probably use also flask_sqlalchemy
extension for integration. In that case the Session creation is not directly
accessible. The following allows you to use the wrapper:

.. code-block:: python
:linenos:
.. testcode::

from sqlalchemy_mptt import mptt_sessionmaker
from flask_sqlalchemy import SQLAlchemy
Expand All @@ -76,7 +73,7 @@ Events

The tree manager automatically registers events. But you can do it manually:

.. code-block:: python
.. testcode::

from sqlalchemy_mptt import tree_manager

Expand All @@ -85,7 +82,7 @@ The tree manager automatically registers events. But you can do it manually:

Or disable events if it required:

.. code-block:: python
.. testcode::

from sqlalchemy_mptt import tree_manager

Expand All @@ -103,7 +100,7 @@ Fill table with records, for example, as shown in the picture

Represented data of tree like dict

.. code-block:: python
.. testcode::

tree = (
{'id': '1', 'parent_id': None},
Expand Down Expand Up @@ -132,7 +129,28 @@ tree, it might become a big overhead. In this case, it is recommended to
deactivate automatic tree management, fill in the data, reactivate automatic
tree management and finally call manually a rebuild of the tree once at the end:

.. no-code-block:: python
.. testcode::
:hide:

from flask import Flask

class MyModelTree(db.Model, BaseNestedSets):
__tablename__ = "my_model_tree"

id = db.Column(db.Integer, primary_key=True)
visible = db.Column(db.Boolean) # you custom field

def __repr__(self):
return "<Node (%s)>" % self.id

app = Flask('test')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db.init_app(app)
app.app_context().push()
db.create_all()
items = [MyModelTree(**data) for data in tree]

.. testcode::

from sqlalchemy_mptt import tree_manager

Expand All @@ -144,13 +162,13 @@ tree management and finally call manually a rebuild of the tree once at the end:
for item in items:
item.left = 0
item.right = 0
item.tree_id = 'my_tree_1'
item.tree_id = 1
db.session.add(item)
db.session.commit()

...

tree_manager.register_events() # enabled MPTT events back
models.MyModelTree.rebuild_tree(db.session, 'my_tree_1') # rebuild lft, rgt value automatically
MyModelTree.rebuild_tree(db.session, 1) # rebuild lft, rgt value automatically

After an initial table with tree you can use mptt features.
Loading