Skip to content

Commit abad019

Browse files
authored
Enable doctests for documentation snippets (#110)
* Enable doctest * Add doctest * Test all of Usage documentation * Use a separate session for doctest * Test all of Setup documentation * Use the right type * Segregate test and doctest installs * Only match test sessions * Update release notes * Fix typo * Add doctest for README * Add sphinx-build instructions * Revert doctest changes for README as it was not displayed correctly on Github * Update install instructions * Add doctests for flask-sqlalchemy docs * Simplify title * Add doctests * Fix formatting * Hide setup code * Document the :hide: hack * Fix typo * Remove redundant imports * Log request operations * Fix build matrix generation output
1 parent da5b694 commit abad019

File tree

14 files changed

+318
-90
lines changed

14 files changed

+318
-90
lines changed

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
uv run noxfile.py -s "${{ matrix.session }}"
3939
-- --pyargs sqlalchemy_mptt --cov-report xml
4040
- name: Upload coverage data
41-
if: ${{ matrix.session != 'lint' }}
41+
if: ${{ startsWith(matrix.session, 'test(') }}
4242
uses: coverallsapp/github-action@v2
4343
with:
4444
flag-name: run-${{ join(matrix.*, '-') }}

CHANGES.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ Versions releases 0.2.x & above
44
0.5.0 (Unreleased)
55
==================
66

7-
see issue #104
7+
see issues #104 & #110
88

99
- Add support for SQLAlchemy 1.4.
1010
- Drop official support for PyPy.
1111
- Simplify memory management by using ``weakref.WeakSet`` instead of rolling our own
1212
weak reference set.
1313
- Unify ``after_flush_postexec`` execution path for CPython & PyPy.
1414
- Simplify ``get_siblings``.
15+
- Run doctest on all code snippets in the documentation.
16+
- Fix some of the incorrect documentation snippets.
1517

1618
0.4.0 (2025-05-30)
1719
==================

docs/CONTRIBUTING.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,19 @@ To run the tests and linters, you can use the following command:
4040
4141
For futher details, refer to the ``noxfile.py`` script.
4242

43+
Building Documentation
44+
----------------------
45+
46+
The documentation on `ReadtheDocs <https://app.readthedocs.org/projects/sqlalchemy-mptt/>`_ is manually built from the master branch.
47+
To build the documentation locally, you can run:
48+
49+
.. code-block:: bash
50+
51+
$ uv tool install sphinx --with-editable . --with-requirements requirements-doctest.txt
52+
$ cd docs
53+
$ make html
54+
55+
For futher details, refer to the ``docs/Makefile``.
56+
4357
.. |IRC Freenode| image:: https://img.shields.io/badge/irc-freenode-blue.svg
4458
:target: https://webchat.freenode.net/?channels=sacrud

docs/conf.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
'sphinx.ext.autodoc',
3333
'sphinx.ext.viewcode',
3434
'sphinx.ext.mathjax',
35-
# 'sphinx.ext.mathbase',
35+
'sphinx.ext.doctest',
3636
]
3737

3838
# Add any paths that contain templates here, relative to this directory.
@@ -69,3 +69,19 @@
6969
'github_user': 'uralbash',
7070
'github_repo': 'sqlalchemy_mptt',
7171
}
72+
73+
# -- Options for doctest extension ------------------------------------------
74+
doctest_global_setup = """
75+
from sqlalchemy import create_engine, Column, Integer, Boolean
76+
from sqlalchemy.ext.declarative import declarative_base
77+
from sqlalchemy.orm import Session
78+
79+
from sqlalchemy_mptt import tree_manager
80+
from sqlalchemy_mptt.mixins import BaseNestedSets
81+
"""
82+
doctest_global_cleanup = """
83+
try:
84+
session.flush()
85+
except NameError:
86+
pass
87+
"""

docs/crud.rst

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,47 @@ INSERT
66

77
Insert node with parent_id==6
88

9-
.. code-block:: python
9+
.. testsetup::
10+
11+
Base = declarative_base()
12+
engine = create_engine("sqlite:///:memory:")
13+
session = Session(engine)
14+
15+
class Tree(Base, BaseNestedSets):
16+
__tablename__ = "tree"
17+
18+
id = Column(Integer, primary_key=True)
19+
visible = Column(Boolean)
20+
21+
def __repr__(self):
22+
return "<Node (%s)>" % self.id
23+
24+
Base.metadata.create_all(engine)
25+
tree_manager.register_events(remove=True)
26+
instances = [
27+
Tree(id=1, parent_id=None),
28+
Tree(id=2, parent_id=1),
29+
Tree(id=3, parent_id=2),
30+
Tree(id=4, parent_id=1),
31+
Tree(id=5, parent_id=4),
32+
Tree(id=6, parent_id=4),
33+
Tree(id=7, parent_id=1),
34+
Tree(id=8, parent_id=7),
35+
Tree(id=9, parent_id=8),
36+
Tree(id=10, parent_id=7),
37+
Tree(id=11, parent_id=10)
38+
]
39+
for instance in instances:
40+
instance.left = 0
41+
instance.right = 0
42+
instance.visible = True
43+
session.add_all(instances)
44+
session.flush()
45+
tree_manager.register_events()
46+
Tree.rebuild_tree(session, tree_id=None)
47+
48+
49+
.. testcode::
1050

1151
node = Tree(parent_id=6)
1252
session.add(node)
@@ -45,7 +85,7 @@ UPDATE
4585

4686
Set parent_id=5 for node with id==8
4787

48-
.. code-block:: python
88+
.. testcode::
4989

5090
node = session.query(Tree).filter(Tree.id == 8).one()
5191
node.parent_id = 5
@@ -86,7 +126,7 @@ DELETE
86126

87127
Delete node with id==4
88128

89-
.. code-block:: python
129+
.. testcode::
90130

91131
node = session.query(Tree).filter(Tree.id == 4).one()
92132
session.delete(node)

docs/initialize.rst

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ Setup
33

44
Create model with MPTT mixin:
55

6-
.. code-block:: python
7-
:linenos:
6+
.. testcode::
87

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

34-
.. code-block:: python
35-
:linenos:
33+
.. testcode::
3634

3735
from sqlalchemy import create_engine
3836
from sqlalchemy.orm import sessionmaker
3937
from sqlalchemy_mptt import mptt_sessionmaker
4038

41-
engine = create_engine('...')
39+
engine = create_engine('sqlite:///:memory:')
4240
Session = mptt_sessionmaker(sessionmaker(bind=engine))
4341

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

51-
.. code-block:: python
52-
:linenos:
49+
.. testcode::
5350

5451
from sqlalchemy_mptt import mptt_sessionmaker
5552
from flask_sqlalchemy import SQLAlchemy
@@ -76,7 +73,7 @@ Events
7673

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

79-
.. code-block:: python
76+
.. testcode::
8077

8178
from sqlalchemy_mptt import tree_manager
8279

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

8683
Or disable events if it required:
8784

88-
.. code-block:: python
85+
.. testcode::
8986

9087
from sqlalchemy_mptt import tree_manager
9188

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

104101
Represented data of tree like dict
105102

106-
.. code-block:: python
103+
.. testcode::
107104

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

135-
.. no-code-block:: python
132+
.. testcode::
133+
:hide:
134+
135+
# This is some more setup code.
136+
from flask import Flask
137+
138+
class MyModelTree(db.Model, BaseNestedSets):
139+
__tablename__ = "my_model_tree"
140+
141+
id = db.Column(db.Integer, primary_key=True)
142+
visible = db.Column(db.Boolean) # you custom field
143+
144+
def __repr__(self):
145+
return "<Node (%s)>" % self.id
146+
147+
app = Flask('test')
148+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
149+
db.init_app(app)
150+
app.app_context().push()
151+
db.create_all()
152+
items = [MyModelTree(**data) for data in tree]
153+
154+
.. testcode::
136155

137156
from sqlalchemy_mptt import tree_manager
138157

@@ -144,13 +163,13 @@ tree management and finally call manually a rebuild of the tree once at the end:
144163
for item in items:
145164
item.left = 0
146165
item.right = 0
147-
item.tree_id = 'my_tree_1'
166+
item.tree_id = 1
148167
db.session.add(item)
149168
db.session.commit()
150169

151170
...
152171

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

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

0 commit comments

Comments
 (0)