Repository: uralbash/sqlalchemy_mptt Branch: master Commit: 6bb11a2292b2 Files: 53 Total size: 236.2 KB Directory structure: gitextract_78avwcyy/ ├── .coveragerc ├── .editorconfig ├── .flake8 ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── codeql.yml │ ├── publish.yml │ └── run-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .rstcheck.cfg ├── CHANGES.rst ├── CHANGES_OLD.rst ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── RELEASING.rst ├── docs/ │ ├── CONTRIBUTING.rst │ ├── Makefile │ ├── conf.py │ ├── crud.rst │ ├── index.rst │ ├── initialize.rst │ ├── make.bat │ ├── sqlalchemy_mptt.rst │ └── tut_flask.rst ├── noxfile.py ├── pyproject.toml ├── requirements-doctest.txt ├── requirements-test.txt ├── requirements.txt ├── setup.py ├── sqlalchemy_mptt/ │ ├── __init__.py │ ├── events.py │ ├── mixins.py │ ├── sqlalchemy_compat.py │ └── tests/ │ ├── __init__.py │ ├── cases/ │ │ ├── __init__.py │ │ ├── edit_node.py │ │ ├── get_node.py │ │ ├── get_tree.py │ │ ├── initialize.py │ │ ├── integrity.py │ │ └── move_node.py │ ├── fixtures/ │ │ ├── tmp_tree.json │ │ ├── tree.json │ │ └── tree_3.json │ ├── test_events.py │ ├── test_inheritance.py │ ├── test_mixins.py │ └── test_stateful.py └── test.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] relative_files = True [report] omit = */tests/* ================================================ FILE: .editorconfig ================================================ root = true [*] insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .flake8 ================================================ [flake8] extend-exclude = .venv statistics = True count = True show-source = True # The GitHub editor is 127 chars wide max-line-length = 127 # Ignore valid SQLAlchemy NULL query syntax extend-ignore = E711 ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL Advanced" on: push: branches: [ "master" ] pull_request: branches: [ "master" ] schedule: - cron: '20 16 * * 3' jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: actions build-mode: none - language: python build-mode: none # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@v4 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` # or others). This is typically only required for manual builds. # - name: Setup runtime (example) # uses: actions/setup-example@v1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above # to set the build mode to "manual" for that language. Then modify this step # to build your code. # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/publish.yml ================================================ # This workflow will upload a Python Package to PyPI when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: release-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - name: Build release distributions run: | # NOTE: put your own distribution build steps here. python -m pip install build python -m build - name: Upload distributions uses: actions/upload-artifact@v4 with: name: release-dists path: dist/ pypi-publish: runs-on: ubuntu-latest needs: - release-build permissions: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write # Dedicated environments with protections for publishing are strongly recommended. # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules environment: name: pypi # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: # url: https://pypi.org/p/YOURPROJECT # # ALTERNATIVE: if your GitHub Release name is the PyPI project version string # ALTERNATIVE: exactly, uncomment the following line instead: url: https://pypi.org/project/sqlalchemy_mptt/${{ github.event.release.name }} steps: - name: Retrieve release distributions uses: actions/download-artifact@v4 with: name: release-dists path: dist/ - name: Publish release distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ ================================================ FILE: .github/workflows/run-tests.yml ================================================ name: Check code and run tests on: push: branches: [ "master" ] pull_request: branches: [ "master" ] permissions: contents: read jobs: generate-jobs: name: Generate build matrix for tests runs-on: ubuntu-latest outputs: session: ${{ steps.set-matrix.outputs.session }} steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 - name: Generate build matrix id: set-matrix shell: bash run: echo session=$(uv run noxfile.py --json -l | jq -c '[.[].session]') | tee --append $GITHUB_OUTPUT checks: name: Run ${{ matrix.session }} needs: [generate-jobs] runs-on: ubuntu-latest strategy: fail-fast: false matrix: session: ${{ fromJson(needs.generate-jobs.outputs.session) }} steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 - name: Run nox run: > uv run noxfile.py -s "${{ matrix.session }}" -- --pyargs sqlalchemy_mptt --cov-report xml - name: Upload coverage data if: ${{ startsWith(matrix.session, 'test(') }} uses: coverallsapp/github-action@v2 with: flag-name: run-${{ join(matrix.*, '-') }} parallel: true debug: true coveralls_finish: name: Finalize coverage needs: checks runs-on: ubuntu-latest steps: - uses: coverallsapp/github-action@v2 with: parallel-finished: true ================================================ FILE: .gitignore ================================================ uv.lock .eggs .env *~ *.swo *.swp .settings .project .pydevproject sqlalchemy_mptt/.coverage sqlalchemy_mptt/TODO *.pyc sqlalchemy_mptt/cover cover .coverage build dist *.egg-info example TODO TODO.txt nohup.out .cache .tox __pycache__ _build .ropeproject _themes .aider.* .vscode/ coverage.xml .hypothesis/ .nox .pytest_cache .venv ================================================ FILE: .pre-commit-config.yaml ================================================ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - 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.3.0' hooks: - id: flake8 - repo: https://github.com/pappasam/toml-sort rev: 'v0.24.2' hooks: - id: toml-sort - repo: https://github.com/pycqa/isort rev: '7.0.0' hooks: - id: isort name: isort (python) ================================================ FILE: .readthedocs.yaml ================================================ # Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" # You can also specify other tool versions: # nodejs: "20" # rust: "1.70" # golang: "1.20" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs # builder: "dirhtml" # Fail on all warnings to avoid broken references # fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub # formats: # - pdf # - epub # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html #python: # install: # - requirements: requirements.txt python: install: - method: pip path: . ================================================ FILE: .rstcheck.cfg ================================================ [rstcheck] ignore_directives=automodule,autofunction,autoclass,code-block ignore_roles=mod,py:mod ================================================ FILE: CHANGES.rst ================================================ Versions releases 0.2.x & above ############################### 0.6.0 (2025-11-29) ================== see issues #109, #111, #112 & #113 - Add support for SQLAlchemy 2.0. - Add official support for Python 3.12, 3.13 and 3.14. - Remove examples of defunct features from the documentation. 0.5.0 (2025-11-18) ================== 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) ================== see issue #97 - Support for Python 3.10 and 3.11 - Dropped support for Python 3.7 0.3.0 (2025-05-10) ================== see issues #63, #87, #89 & #90 - Support for joined-table inheritance - Restrict to Python & PyPy 3.7 - 3.9 - Restrict to SQLA 1.0 - 1.3 - Fixes race condition with garbage collection on PyPy versions 0.2.5 (2019-07-23) ================== see issue #64 - Added similar `django_mptt` methods `get_siblings` and `get_children` 0.2.4 (2018-12-14) ================== see PR #61 - Allow to specify ordering of path_to_root 0.2.3 (2018-06-03) ================== see issue #57 - Fix rebuild tree - Added support node's identifier start from 0 0.2.2 (2017-10-05) ================== see issue #56 - Added custom default root level. Support Django style level=0 0.2.1 (2016-01-23) ================== see PR #51 - fix of index columns names 0.2.0 (2015-11-13) ================== see PR #50 - Changed ``parent_id`` to dynamically match the type of the primary_key - exposed drilldown_tree's logic and path_to_root's logic as both instance and class level method ================================================ FILE: CHANGES_OLD.rst ================================================ Versions releases 0.1.x ####################### 0.1.9 (2015-09-24) ================== - add option ``remove`` to ``sqlalchemy.events.TreesManager.register_mapper`` 0.1.8 (2015-09-14) ================== - add method ``_base_query_obj`` 0.1.7 (2015-08-18) ================== - add method ``path_to_root`` (see #46) - add data integrity tests 0.1.6 (2015-07-03) ================== - fix bug with ``get_tree`` when no rows in database. 0.1.5 (2015-06-25) ================== - Add drilldown_tree method (see #41) - Add custom ``query`` atribute to ``get_tree`` method 0.1.4 (2015-06-19) ================== - delete method ``get_pk_with_class_name`` Bug Fixes --------- - fix ``_get_tree_table`` function for inheritance models 0.1.3 (2015-06-17) ================== - Add test for swap trees - rename ``get_pk`` method to ``get_pk_name`` - rename ``get_db_pk`` method to ``get_pk_column`` - rename ``get_class_pk`` method to ``get_pk_with_class_name`` Bug Fixes --------- - Fix order of elements in tree 0.1.2 (2015-04-22) ================== Bug Fixes --------- - Fix MANIFEST.in file Deprecation ----------- - Delete ``BaseNestedSets.register_tree`` method - Delete ``BaseNestedSets.get_tree_recursively`` method 0.1.1 (2015-04-21) ================== Features -------- - Add test for rst docs and migrate on new itcase_sphinx_theme (#40) Bug Fixes --------- - Remove recursion from BaseNestedSets.get_tree method (#39) 0.1.0 (2014-11-18) ================== Bug Fixes --------- - Fix concurrency issue with multiple session (#36) - Flushing the session now expire the instance and it's children (#33) 0.0.9 (2014-10-09) ================== - Add MANIFEST.in - New docs - fixes in setup.py 0.0.8 (2014-08-15) ================== - Add CONTRIBUTORS.txt Features -------- - Automatically register tree classes enhancement (#28) - Added support polymorphic tree models (#24) Bug Fixes --------- - Fix expire left/right attributes of parent somewhen after the `before_insert` event (#30) - Fix tree_id is incorrectly set to an existing tree if no parent is set (#23) - Fix package is not installable if sqlalchemy is not (yet) installed (#22) 0.0.7 (2014-08-04) ================== - Add LICENSE.txt Bug Fixes --------- - fix get_db_pk function 0.0.6 (2014-07-31) ================== Bug Fixes --------- - Allow the primary key to not be named "id" #20. See https://github.com/uralbash/sqlalchemy_mptt/issues/20 ================================================ FILE: CONTRIBUTORS.txt ================================================ Contributors ------------ - Dmitry Svintsov (uralbash), 2014/04/16 - Jonathan Stoppani, 2014/08/11 - Fayaz Yusuf Khan, 2014/10/10 - Greg Klupar, 2015/11/13 - Jiri Kuncar, 2016/01/20 - Sylvain Thénault (sthenault), 2018/06/03 - Musharraf (mush42), 2019/02/12 - Tim Gates (timgates42), 2020/03/27 ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2013 uralbash Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include requirements.txt include requirements-test.txt include README.rst CHANGES.rst ================================================ FILE: README.rst ================================================ |PyPI Version| |PyPI Downloads| |PyPI Python Versions| |Build Status| |Coverage Status| Library for implementing Modified Preorder Tree Traversal with your SQLAlchemy Models and working with trees of Model instances, like django-mptt. Docs http://sqlalchemy-mptt.readthedocs.io/ .. image:: https://cdn.rawgit.com/uralbash/sqlalchemy_mptt/master/docs/img/2_sqlalchemy_mptt_traversal.svg :alt: Nested sets traversal :width: 800px The nested set model is a particular technique for representing nested sets (also known as trees or hierarchies) in relational databases. Installing ---------- Install from github: .. code-block:: bash pip install git+http://github.com/uralbash/sqlalchemy_mptt.git PyPi: .. code-block:: bash pip install sqlalchemy_mptt Source: .. code-block:: bash pip install -e . Usage ----- Add mixin to model .. code-block:: python from sqlalchemy import Column, Integer, Boolean from sqlalchemy.ext.declarative import declarative_base from sqlalchemy_mptt.mixins import BaseNestedSets Base = declarative_base() class Tree(Base, BaseNestedSets): __tablename__ = "tree" id = Column(Integer, primary_key=True) visible = Column(Boolean) def __repr__(self): return "" % self.id Now you can add, move and delete obj! Insert node ----------- .. code-block:: python node = Tree(parent_id=6) session.add(node) :: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Insert node with parent_id == 6 1 1(1)24 _______________|_________________ | | | 2 2(2)5 6(4)13 14(7)23 | ____|____ ___|____ | | | | | 3 3(3)4 7(5)8 9(6)12 15(8)18 19(10)22 | | | 4 10(23)11 16(9)17 20(11)21 Delete node ----------- .. code:: python node = session.query(Tree).filter(Tree.id == 4).one() session.delete(node) :: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Delete node == 4 1 1(1)16 _______________|_____ | | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13 Update node ----------- .. code:: python node = session.query(Tree).filter(Tree.id == 8).one() node.parent_id = 5 session.add(node) :: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 8 - > 5 1 1(1)22 _______________|__________________ | | | 2 2(2)5 6(4)15 16(7)21 | ^ | 3 3(3)4 7(5)12 13(6)14 17(10)20 | | 4 8(8)11 18(11)19 | 5 9(9)10 Move node (support multitree) ----------------------------- .. figure:: https://cdn.rawgit.com/uralbash/sqlalchemy_mptt/master/docs/img/3_sqlalchemy_mptt_multitree.svg :alt: Nested sets multitree :width: 600px Nested sets multitree Move inside .. code:: python node = session.query(Tree).filter(Tree.id == 4).one() node.move_inside("15") :: 4 -> 15 level Nested sets tree1 1 1(1)16 _______________|_____________________ | | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13 level Nested sets tree2 1 1(12)28 ________________|_______________________ | | | 2 2(13)5 6(15)17 18(18)27 | ^ ^ 3 3(14)4 7(4)12 13(16)14 15(17)16 19(19)22 23(21)26 ^ | | 4 8(5)9 10(6)11 20(20)21 24(22)25 Move after .. code:: python node = session.query(Tree).filter(Tree.id == 8).one() node.move_after("5") :: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 8 after 5 1 1(1)22 _______________|__________________ | | | 2 2(2)5 6(4)15 16(7)21 | ^ | 3 3(3)4 7(5)8 9(8)12 13(6)14 17(10)20 | | 4 10(9)11 18(11)19 Move to top level .. code:: python node = session.query(Tree).filter(Tree.id == 15).one() node.move_after("1") :: level tree_id = 1 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level tree_id = 2 1 1(15)6 ^ 2 2(16)3 4(17)5 level tree_id = 3 1 1(12)16 _______________| | | 2 2(13)5 6(18)15 | ^ 3 3(14)4 7(19)10 11(21)14 | | 4 8(20)9 12(22)13 Support and Development ======================= To report bugs, use the `issue tracker `_. We welcome any contribution: suggestions, ideas, commits with new futures, bug fixes, refactoring, docs, tests, translations, etc... If you have any questions: * Use the `Discussion board `_ * Contact the maintainer via email: fayaz.yusuf.khan@gmail.com * Contact the author via email: sacrud@uralbash.ru or #sacrud IRC channel |IRC Freenode| Refer the detailed contribution guide in the `docs `_ for more information on setting up the development environment, running tests, and contributing to the project. License ======= The project is licensed under the MIT license. .. |PyPI Version| image:: https://img.shields.io/pypi/v/sqlalchemy_mptt :alt: PyPI - Version .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/sqlalchemy_mptt :alt: PyPI - Downloads .. |PyPI Python Versions| image:: https://img.shields.io/pypi/pyversions/sqlalchemy_mptt :alt: PyPI - Python Version .. |Build Status| image:: https://github.com/uralbash/sqlalchemy_mptt/actions/workflows/run-tests.yml/badge.svg?branch=master :target: https://github.com/uralbash/sqlalchemy_mptt/actions/workflows/run-tests.yml .. |Coverage Status| image:: https://coveralls.io/repos/uralbash/sqlalchemy_mptt/badge.png :target: https://coveralls.io/r/uralbash/sqlalchemy_mptt .. |IRC Freenode| image:: https://img.shields.io/badge/irc-freenode-blue.svg :target: https://webchat.freenode.net/?channels=sacrud ================================================ FILE: RELEASING.rst ================================================ Releasing ========= 1. Merge all intended and verified pull requests into the ``master`` branch. 2. Create a local build and test: - Run ``uv run noxfile.py -s build`` to create a source distribution and a wheel. - Install both artifacts in a fresh virtual environment to ensure they work correctly. 3. Bump the version number in ``setup.py``. (May be included in the pull request.) 4. Update the changelog in ``CHANGES.rst``. 5. Add new contributors to the ``CONTRIBUTORS.rst`` file. 6. Update the release date in ``CHANGES.rst``. 7. Ensure the latest build passes on GitHub Actions. 8. Rebuild the documentation and check that it looks correct. 9. Create a new release on GitHub: - Use the version number as the tag. - Include the changelog in the release notes. 10. Ensure the release is published. 11. Test the release by installing it in a fresh virtual environment. ================================================ FILE: docs/CONTRIBUTING.rst ================================================ Contribution Guidelines ======================= All types of contributions are welcome: suggestions, ideas, commits with new features, bug fixes, refactoring, docs, tests, translations, etc... If you have any questions: * Use the `Discussion board `_ * Contact the maintainer via email: fayaz.yusuf.khan@gmail.com * Contact the author via email: sacrud@uralbash.ru or #sacrud IRC channel |IRC Freenode| Development Setup ----------------- To set up the development environment, you can run: .. code-block:: bash # Install uv $ pip install uv # Run the noxfile.py script $ uv run noxfile.py -s dev By default, this will create a virtual environment with Python 3.8 and install all the required dependencies. If you need to setup the development environment with a specific Python version, you can run: .. code-block:: bash $ uv run noxfile.py -s dev -P 3.10 Running Tests ------------- To run the tests and linters, you can use the following command: .. code-block:: bash $ uv run noxfile.py For futher details, refer to the ``noxfile.py`` script. Building Documentation ---------------------- The documentation on `ReadtheDocs `_ 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 ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sqlalchemy_mptt.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sqlalchemy_mptt.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/sqlalchemy_mptt" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sqlalchemy_mptt" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: docs/conf.py ================================================ # -*- coding: utf-8 -*- # # sqlalchemy_mptt documentation build configuration file, created by # sphinx-quickstart on Wed Jun 25 14:00:12 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from datetime import date import os import sys from docutils.parsers.rst import directives from sphinx.directives.code import CodeBlock directives.register_directive('no-code-block', CodeBlock) sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("../")) # -- General configuration ------------------------------------------------ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax', 'sphinx.ext.doctest', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'sqlalchemy_mptt' copyright = '{}, uralbash'.format(date.today().year) # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output ---------------------------------------------- # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Output file base name for HTML help builder. htmlhelp_basename = 'sqlalchemy_mpttdoc' html_theme_options = { 'github_button': True, '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 """ ================================================ FILE: docs/crud.rst ================================================ Usage ===== INSERT ------ Insert node with parent_id==6 .. testsetup:: 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 "" % 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) Tree state before insert .. code:: level Before INSERT 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 After insert .. code:: level After INSERT 1 1(1)24 _______________|_________________ | | | 2 2(2)5 6(4)13 14(7)23 | ____|___ ____|____ | | | | | 3 3(3)4 7(5)8 9(6)12 15(8)18 19(10)22 | | | 4 10(23)11 16(9)17 20(11)21 UPDATE ------ Set parent_id=5 for node with id==8 .. testcode:: node = session.query(Tree).filter(Tree.id == 8).one() node.parent_id = 5 session.add(node) Tree state before update .. code:: level Before UPDATE 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 After update .. code:: level Move 8 - > 5 1 1(1)22 _______________|__________________ | | | 2 2(2)5 6(4)15 16(7)21 | ^ | 3 3(3)4 7(5)12 13(6)14 17(10)20 | | 4 8(8)11 18(11)19 | 5 9(9)10 DELETE ------ Delete node with id==4 .. testcode:: node = session.query(Tree).filter(Tree.id == 4).one() session.delete(node) Tree state before delete .. code:: level Before DELETE 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 After delete .. code:: level Delete node == 4 1 1(1)16 _______________|_____ | | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13 For more example see :mod:`sqlalchemy_mptt.tests.TestTree` ================================================ FILE: docs/index.rst ================================================ .. sqlalchemy_mptt documentation master file, created by sphinx-quickstart on Wed Jun 25 14:00:12 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. sqlalchemy_mptt =============== .. image:: _static/mptt_insert.jpg :alt: MPTT (nested sets) INSERT Library for implementing Modified Preorder Tree Traversal with your `SQLAlchemy` Models and working with trees of Model instances, like `django-mptt`. The nested set model is a particular technique for representing nested sets (also known as trees or hierarchies) in relational databases. Where used ---------- * ps_tree_ * pyramid_pages_ * your project ... Manual ------ .. toctree:: initialize.rst crud.rst API: ---- .. toctree:: :maxdepth: 2 sqlalchemy_mptt Tutorial -------- .. toctree:: tut_flask.rst A lot of examples and logic in :py:mod:`sqlalchemy_mptt.tests.cases` Support and Development ======================= .. toctree:: CONTRIBUTING.rst Changelog ========= .. toctree:: :maxdepth: 1 CHANGES.rst CHANGES_OLD.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _ps_tree: https://github.com/sacrud/ps_tree .. _pyramid_pages: https://github.com/uralbash/pyramid_pages ================================================ FILE: docs/initialize.rst ================================================ Setup ===== Create model with MPTT mixin: .. testcode:: from sqlalchemy import Column, Integer, Boolean from sqlalchemy.ext.declarative import declarative_base from sqlalchemy_mptt.mixins import BaseNestedSets Base = declarative_base() class Tree(Base, BaseNestedSets): __tablename__ = "tree" id = Column(Integer, primary_key=True) visible = Column(Boolean) # you custom field def __repr__(self): return "" % self.id 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` .. testcode:: from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy_mptt import mptt_sessionmaker engine = create_engine('sqlite:///:memory:') Session = mptt_sessionmaker(sessionmaker(bind=engine)) Using session factory wrapper with flask_sqlalchemy --------------------------------------------------- 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: .. testcode:: from sqlalchemy_mptt import mptt_sessionmaker from flask_sqlalchemy import SQLAlchemy # instead of creating db object directly db = SQLAlchemy() # subclass the db manager and insert the wrapper at session creation class CustomSQLAlchemy(SQLAlchemy): """A custom SQLAlchemy manager, to have control on session creation""" def create_session(self, options): """Override the original session factory creation""" Session = super().create_session(options) # Use wrapper from sqlalchemy_mptt that manage tree tables return mptt_sessionmaker(Session) # then db = CustomSQLAlchemy() Events ------ The tree manager automatically registers events. But you can do it manually: .. testcode:: from sqlalchemy_mptt import tree_manager tree_manager.register_events() # register events before_insert, # before_update and before_delete Or disable events if it required: .. testcode:: from sqlalchemy_mptt import tree_manager tree_manager.register_events(remove=True) # remove events before_insert, # before_update and before_delete Data structure -------------- Fill table with records, for example, as shown in the picture .. image:: img/2_sqlalchemy_mptt_traversal.svg :width: 500px :alt: SQLAlchemy MPTT (nested sets) Represented data of tree like dict .. testcode:: tree = ( {'id': '1', 'parent_id': None}, {'id': '2', 'visible': True, 'parent_id': '1'}, {'id': '3', 'visible': True, 'parent_id': '2'}, {'id': '4', 'visible': True, 'parent_id': '1'}, {'id': '5', 'visible': True, 'parent_id': '4'}, {'id': '6', 'visible': True, 'parent_id': '4'}, {'id': '7', 'visible': True, 'parent_id': '1'}, {'id': '8', 'visible': True, 'parent_id': '7'}, {'id': '9', 'parent_id': '8'}, {'id': '10', 'parent_id': '7'}, {'id': '11', 'parent_id': '10'}, ) Initializing a tree with data ----------------------------- When you add nodes to the table, the tree manager subsequently updates the level, left and right attributes in the reset of the table. This is done very quickly if the tree already exists in the database, but for initializing the 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: .. testcode:: :hide: # This is some more setup code. 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 "" % 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 ... tree_manager.register_events(remove=True) # Disable MPTT events # Fill tree for item in items: item.left = 0 item.right = 0 item.tree_id = 1 db.session.add(item) db.session.commit() ... tree_manager.register_events() # enabled MPTT events back MyModelTree.rebuild_tree(db.session, 1) # rebuild lft, rgt value automatically After an initial table with tree you can use mptt features. ================================================ FILE: docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sqlalchemy_mptt.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sqlalchemy_mptt.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ================================================ FILE: docs/sqlalchemy_mptt.rst ================================================ :mod:`sqlalchemy_mptt` package ============================== Events ------ Base events ~~~~~~~~~~~ .. automodule:: sqlalchemy_mptt.events .. autofunction:: mptt_before_insert .. autofunction:: mptt_before_delete .. autofunction:: mptt_before_update .. autoclass:: TreesManager Hidden method ~~~~~~~~~~~~~ .. autofunction:: _insert_subtree Mixins ------ .. automodule:: sqlalchemy_mptt.mixins .. autoclass:: BaseNestedSets :members: .. automethod:: tree_id .. attribute:: parent_id .. attribute:: parent .. automethod:: left .. automethod:: right .. automethod:: level Tests ----- .. automodule:: sqlalchemy_mptt.tests.test_events :members: :undoc-members: :show-inheritance: .. automodule:: sqlalchemy_mptt.tests.test_inheritance :members: :undoc-members: :show-inheritance: .. automodule:: sqlalchemy_mptt.tests.test_mixins :members: :undoc-members: :show-inheritance: Cases tests ~~~~~~~~~~~ .. automodule:: sqlalchemy_mptt.tests.cases.edit_node :members: :undoc-members: :show-inheritance: .. automodule:: sqlalchemy_mptt.tests.cases.get_node :members: :undoc-members: :show-inheritance: .. automodule:: sqlalchemy_mptt.tests.cases.get_tree :members: :undoc-members: :show-inheritance: .. automodule:: sqlalchemy_mptt.tests.cases.initialize :members: :undoc-members: :show-inheritance: .. automodule:: sqlalchemy_mptt.tests.cases.integrity :members: :undoc-members: :show-inheritance: .. automodule:: sqlalchemy_mptt.tests.cases.move_node :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/tut_flask.rst ================================================ Usage with Flask-SQLAlchemy =========================== Initialize Flask app and sqlalchemy .. testsetup:: __name__ = "__main__" .. testcode:: from pprint import pprint from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy_mptt.mixins import BaseNestedSets app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' db = SQLAlchemy(app) Make models. .. testcode:: class Category(db.Model, BaseNestedSets): __tablename__ = 'categories' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(400), index=True, unique=True) items = db.relationship("Product", backref='item', lazy='dynamic') def __repr__(self): return ''.format(self.name) class Product(db.Model): __tablename__ = 'products' id = db.Column(db.Integer, primary_key=True) category_id = db.Column(db.Integer, db.ForeignKey('categories.id')) name = db.Column(db.String(475), index=True) Represent data of tree in table ------------------------------- Add data to table with tree. .. testcode:: :hide: # This is some more setup code. app.app_context().push() .. testcode:: db.session.add(Category(name="root")) # root node db.session.add_all( # first branch of tree [ Category(name="foo", parent_id=1), Category(name="bar", parent_id=2), Category(name="baz", parent_id=3), ] ) db.session.add_all( # second branch of tree [ Category(name="foo1", parent_id=1), Category(name="bar1", parent_id=5), Category(name="baz1", parent_id=5), ] ) db.drop_all() db.create_all() db.session.commit() The database entries are added: .. code-block:: text "id" "name" "lft" "rgt" "level" "parent_id" "tree_id" 1 "root" 1 14 1 1 2 "foo" 2 7 2 1 1 3 "bar" 3 6 3 2 1 4 "baz" 4 5 4 3 1 5 "foo1" 8 13 2 1 1 6 "bar1" 9 10 3 5 1 7 "baz1" 11 12 3 5 1 ``Lft`` of root element every time :math:`1`. :math:`root_{lft} = 1` ``Rgt`` of root element always equal 2 * quantity of tree nodes. :math:`root_{rgt} = 2 * | P |` :math:`root_{rgt} = 2 * 7 = 14` The tree that displays the records in the database is represented schematically below: .. code-block:: text level 1 1(root)14 | --------------------- | | 2 2(foo)7 8(foo1)13 | / \ 3 3(bar)6 9(bar1)10 11(baz1)12 | 4 4(baz)5 Drilldown --------- Drilldown tree for a given node. A drilldown tree consists of a node’s ancestors, itself and its immediate children. For example, a drilldown tree for a ``foo1`` category might look something like: .. code-block:: text Drilldown for foo1 node level 1 1(root)14 | --------------------- | ----------|--------------- 2 2(foo)7 | 8(foo1)13 | | | / \ | 3 3(bar)6 | 9(bar1)10 11(baz1)12 | | -------------------------- 4 4(baz)5 .. testcode:: categories = Category.query.all() for item in categories: print(item) pprint(item.drilldown_tree()) print() .. testoutput:: :options: +NORMALIZE_WHITESPACE [{'children': [{'children': [{'children': [{'node': }], 'node': }], 'node': }, {'children': [{'node': }, {'node': }], 'node': }], 'node': }] [{'children': [{'children': [{'node': }], 'node': }], 'node': }] [{'children': [{'node': }], 'node': }] [{'node': }] [{'children': [{'node': }, {'node': }], 'node': }] [{'node': }] [{'node': }] Represent it to JSON format: .. testcode:: def cat_to_json(item): return { 'id': item.id, 'name': item.name } for item in categories: pprint(item.drilldown_tree(json=True, json_fields=cat_to_json)) print() .. testoutput:: :options: +NORMALIZE_WHITESPACE [{'children': [{'children': [{'children': [{'id': 4, 'label': '', 'name': 'baz'}], 'id': 3, 'label': '', 'name': 'bar'}], 'id': 2, 'label': '', 'name': 'foo'}, {'children': [{'id': 6, 'label': '', 'name': 'bar1'}, {'id': 7, 'label': '', 'name': 'baz1'}], 'id': 5, 'label': '', 'name': 'foo1'}], 'id': 1, 'label': '', 'name': 'root'}] [{'children': [{'children': [{'id': 4, 'label': '', 'name': 'baz'}], 'id': 3, 'label': '', 'name': 'bar'}], 'id': 2, 'label': '', 'name': 'foo'}] [{'children': [{'id': 4, 'label': '', 'name': 'baz'}], 'id': 3, 'label': '', 'name': 'bar'}] [{'id': 4, 'label': '', 'name': 'baz'}] [{'children': [{'id': 6, 'label': '', 'name': 'bar1'}, {'id': 7, 'label': '', 'name': 'baz1'}], 'id': 5, 'label': '', 'name': 'foo1'}] [{'id': 6, 'label': '', 'name': 'bar1'}] [{'id': 7, 'label': '', 'name': 'baz1'}] Path to root ------------ Returns a list containing the ancestors and the node itself in tree order. .. code-block:: text Path to root of bar node level --------------------- 1 | 1(root)14 | | | | | ---------------|----- | | ----------- | 2 | 2(foo)7 | 8(foo1)13 | | | / \ 3 | 3(bar)6 | 9(bar1)10 11(baz1)12 -----|----- 4 4(baz)5 .. testcode:: for item in categories: print(item) pprint(item.path_to_root().all()) print() .. testoutput:: :options: +NORMALIZE_WHITESPACE [] [, ] [, , ] [, , , ] [, ] [, , ] [, , ] Full code --------- .. testcode:: from pprint import pprint from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy_mptt.mixins import BaseNestedSets app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' db = SQLAlchemy(app) class Category(db.Model, BaseNestedSets): __tablename__ = 'categories' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(400), index=True, unique=True) items = db.relationship("Product", backref='item', lazy='dynamic') def __repr__(self): return ''.format(self.name) class Product(db.Model): __tablename__ = 'products' id = db.Column(db.Integer, primary_key=True) category_id = db.Column(db.Integer, db.ForeignKey('categories.id')) name = db.Column(db.String(475), index=True) app.app_context().push() db.session.add(Category(name="root")) # root node db.session.add_all( # first branch of tree [ Category(name="foo", parent_id=1), Category(name="bar", parent_id=2), Category(name="baz", parent_id=3), ] ) db.session.add_all( # second branch of tree [ Category(name="foo1", parent_id=1), Category(name="bar1", parent_id=5), Category(name="baz1", parent_id=5), ] ) ''' "id" "name" "lft" "rgt" "level" "parent_id" "tree_id" 1 "root" 1 14 1 1 2 "foo" 2 7 2 1 1 3 "bar" 3 6 3 2 1 4 "baz" 4 5 4 3 1 5 "foo1" 8 13 2 1 1 6 "bar1" 9 10 3 5 1 7 "baz1" 11 12 3 5 1 root lft everytime = 1 root rgt = qty_nodes * 2 level 1 1(root)14 | --------------------- | | 2 2(foo)7 8(foo1)13 | / \ 3 3(bar)6 9(bar1)10 11(baz1)12 | 4 4(baz)5 ''' db.drop_all() db.create_all() db.session.commit() categories = Category.query.all() for item in categories: print(item) pprint(item.drilldown_tree()) print() ''' [{'children': [{'children': [{'children': [{'node': }], 'node': }], 'node': }, {'children': [{'node': }, {'node': }], 'node': }], 'node': }] [{'children': [{'children': [{'node': }], 'node': }], 'node': }] [{'children': [{'node': }], 'node': }] [{'node': }] [{'children': [{'node': }, {'node': }], 'node': }] [{'node': }] [{'node': }] ''' for item in categories: print(item) pprint(item.path_to_root().all()) print() ''' [] [, ] [, , ] [, , , ] [, ] [, , ] [, , ] ''' .. testoutput:: :options: +NORMALIZE_WHITESPACE :hide: [{'children': [{'children': [{'children': [{'node': }], 'node': }], 'node': }, {'children': [{'node': }, {'node': }], 'node': }], 'node': }] [{'children': [{'children': [{'node': }], 'node': }], 'node': }] [{'children': [{'node': }], 'node': }] [{'node': }] [{'children': [{'node': }, {'node': }], 'node': }] [{'node': }] [{'node': }] [] [, ] [, , ] [, , , ] [, ] [, , ] [, , ] ================================================ FILE: noxfile.py ================================================ # -*- coding: utf-8 -*- # # Copyright (c) 2025 Fayaz Yusuf Khan # # Distributed under terms of the MIT license. # # /// script # requires-python = ">=3.9" # dependencies = [ # "nox", # "nox-uv", # "requests", # ] # /// """ Entry point script for testing, linting, and development of the package. This project uses Nox to create isolated environments. Requirements: - uv Usage: Run all tests and linting: $ uv run noxfile.py 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 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 """ import sys from itertools import groupby import nox import requests from packaging.requirements import Requirement from packaging.version import Version # Python versions supported and tested against: 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 PYTHON_MINOR_VERSION_MIN = 8 PYTHON_MINOR_VERSION_MAX = 14 nox.options.default_venv_backend = "uv" @nox.session() def lint(session): """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") def parametrize_test_versions(): """Parametrize the session with all supported Python & SQLAlchemy versions.""" print("Requesting all SQLAlchemy versions from PyPI...", file=sys.stderr) response = requests.get("https://pypi.org/pypi/SQLAlchemy/json") print("Preparing test version candidates...", file=sys.stderr) response.raise_for_status() data = response.json() all_major_and_minor_sqlalchemy_versions = [ Version(f"{major}.{minor}") for (major, minor), _ in groupby( sorted(Version(version) for version in data["releases"].keys()), key=lambda v: (v.major, v.minor) ) ] with open("requirements.txt", "r") as f: requirement = Requirement(f.read().strip()) filtered_sqlalchemy_versions = [ version for version in all_major_and_minor_sqlalchemy_versions if version in requirement.specifier ] return [ nox.param( f"3.{python_minor}", str(sqlalchemy_version), tags=[f"sqla{sqlalchemy_version.major}{sqlalchemy_version.minor}"] ) 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+ # SQLA 1.2 doesn't seem to support Python 3.14+ if ((sqlalchemy_version >= Version("1.2") or python_minor <= 9) and (sqlalchemy_version >= Version("1.3") or python_minor <= 13))] PARAMETRIZED_TEST_VERSIONS = parametrize_test_versions() def install_dependencies(session, session_name, sqlalchemy_version): """Install dependencies for the given session.""" session.install( "-r", f"requirements-{session_name}.txt", f"sqlalchemy~={sqlalchemy_version}.0", "-e", "." ) @nox.session() @nox.parametrize("python,sqlalchemy", PARAMETRIZED_TEST_VERSIONS) def test(session, sqlalchemy): """Run tests with pytest. 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: $ uv run noxfile.py -s test -t sqla12 For fine-grained control over running the tests, refer the nox documentation: https://nox.thea.codes/en/stable/usage.html """ install_dependencies(session, "test", sqlalchemy) pytest_args = session.posargs or ["--pyargs", "sqlalchemy_mptt"] session.run("pytest", *pytest_args, env={"SQLALCHEMY_WARN_20": "1"}) @nox.session() @nox.parametrize("python,sqlalchemy", PARAMETRIZED_TEST_VERSIONS[-1:]) def doctest(session, sqlalchemy): """Run doctests in the documentation.""" install_dependencies(session, "doctest", sqlalchemy) session.run("sphinx-build", "-b", "doctest", "docs", "docs/_build") @nox.session(default=False) def dev(session): """Set up a development environment. This will create a virtual environment and install the package in editable mode in .venv. To use a specific Python version, use the -P option: $ uv run noxfile.py -s dev -P 3.X """ 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) session.run(".venv/bin/pip", "install", "-e", ".", external=True) @nox.session(default=False) def build(session): """Build the package.""" session.install("build") session.run("python", "-m", "build") if __name__ == "__main__": nox.main() ================================================ FILE: pyproject.toml ================================================ [tool.black] line-length = 79 include = '\.pyi?$' exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ''' [tool.pytest.ini_options] filterwarnings = [ "error:::sqlalchemy_mptt" ] addopts = "--cov sqlalchemy_mptt --cov-report term-missing:skip-covered" ================================================ FILE: requirements-doctest.txt ================================================ flask-sqlalchemy sphinx ================================================ FILE: requirements-test.txt ================================================ hypothesis pytest pytest-cov ================================================ FILE: requirements.txt ================================================ SQLAlchemy>=1.0.0,<3.0 ================================================ FILE: setup.py ================================================ import os from setuptools import setup this = os.path.dirname(os.path.realpath(__file__)) def read(name): with open(os.path.join(this, name)) as f: return f.read() setup( name="sqlalchemy_mptt", version="0.6.0", url="http://github.com/uralbash/sqlalchemy_mptt/", author="Svintsov Dmitry", author_email="sacrud@uralbash.ru", maintainer="Fayaz Khan", maintainer_email="fayaz.yusuf.khan@gmail.com", packages=["sqlalchemy_mptt"], include_package_data=True, zip_safe=False, license="MIT", description=( "SQLAlchemy mixins for implementing tree-like models" " using Modified Pre-order Tree Traversal (MPTT) / Nested Sets"), long_description=read("README.rst") + "\n" + read("CHANGES.rst"), install_requires=read("requirements.txt"), classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: Web Environment", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "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", "Programming Language :: Python :: 3.14", "Framework :: Pyramid", "Framework :: Flask", "Topic :: Internet", "Topic :: Database", "License :: OSI Approved :: MIT License", ], ) ================================================ FILE: sqlalchemy_mptt/__init__.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright (c) 2014 uralbash # # Distributed under terms of the MIT license. from .events import TreesManager from .mixins import BaseNestedSets __mixins__ = [BaseNestedSets] __all__ = ['BaseNestedSets', 'mptt_sessionmaker'] tree_manager = TreesManager(BaseNestedSets) tree_manager.register_events() mptt_sessionmaker = tree_manager.register_factory ================================================ FILE: sqlalchemy_mptt/events.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2014 uralbash # Copyright (c) 2025 Fayaz Yusuf Khan # # Distributed under terms of the MIT license. """ SQLAlchemy events extension """ # standard library import weakref # SQLAlchemy from sqlalchemy import and_, event, inspection from sqlalchemy.orm import object_session from sqlalchemy.sql import func from sqlalchemy.orm.base import NO_VALUE from sqlalchemy_mptt.sqlalchemy_compat import compat_layer def _insert_subtree( table, connection, node_size, node_pos_left, node_pos_right, parent_pos_left, parent_pos_right, subtree, parent_tree_id, parent_level, node_level, left_sibling, table_pk ): # step 1: rebuild inserted subtree delta_lft = left_sibling['lft'] + 1 if not left_sibling['is_parent']: delta_lft = left_sibling['rgt'] + 1 delta_rgt = delta_lft + node_size - 1 connection.execute( table.update() .where(table_pk.in_(subtree)) .values( lft=table.c.lft - node_pos_left + delta_lft, rgt=table.c.rgt - node_pos_right + delta_rgt, level=table.c.level - node_level + parent_level + 1, tree_id=parent_tree_id ) ) # step 2: update key of right side connection.execute( table.update() .where(table.c.rgt > delta_lft - 1) .where(table_pk.notin_(subtree)) .where(table.c.tree_id == parent_tree_id) .values( rgt=table.c.rgt + node_size, lft=compat_layer.case( (table.c.lft > left_sibling['lft'], table.c.lft + node_size), else_=table.c.lft ) ) ) def _get_tree_table(mapper): for table in mapper.tables: if all(key in table.c for key in ['level', 'lft', 'rgt', 'parent_id']): return table def mptt_before_insert(mapper, connection, instance): """ Based on example https://bitbucket.org/zzzeek/sqlalchemy/src/73095b353124/examples/nested_sets/nested_sets.py?at=master """ table = _get_tree_table(mapper) db_pk = instance.get_pk_column() table_pk = getattr(table.c, db_pk.name) if instance.parent_id is None: instance.left = 1 instance.right = 2 instance.level = instance.get_default_level() tree_id = connection.scalar( compat_layer.select( func.max(table.c.tree_id) + 1 ) ) or 1 instance.tree_id = tree_id else: (parent_pos_left, parent_pos_right, parent_tree_id, parent_level) = connection.execute( compat_layer.select( table.c.lft, table.c.rgt, table.c.tree_id, table.c.level ).where( table_pk == instance.parent_id ) ).fetchone() # Update key of right side connection.execute( table.update() .where(table.c.rgt >= parent_pos_right) .where(table.c.tree_id == parent_tree_id) .values( lft=compat_layer.case( (table.c.lft > parent_pos_right, table.c.lft + 2), else_=table.c.lft ), rgt=compat_layer.case( (table.c.rgt >= parent_pos_right, table.c.rgt + 2), else_=table.c.rgt ) ) ) instance.level = parent_level + 1 instance.tree_id = parent_tree_id instance.left = parent_pos_right instance.right = parent_pos_right + 1 def mptt_before_delete(mapper, connection, instance, delete=True): table = _get_tree_table(mapper) tree_id = instance.tree_id pk = getattr(instance, instance.get_pk_name()) db_pk = instance.get_pk_column() table_pk = getattr(table.c, db_pk.name) lft, rgt = connection.execute( compat_layer.select( table.c.lft, table.c.rgt ).where( table_pk == pk ) ).fetchone() delta = rgt - lft + 1 if delete: mapper.base_mapper.confirm_deleted_rows = False connection.execute( table.delete().where( table_pk == pk ) ) if instance.parent_id is not None or not delete: """ Update key of current tree UPDATE tree SET left_id = CASE WHEN left_id > $leftId THEN left_id - $delta ELSE left_id END, right_id = CASE WHEN right_id >= $rightId THEN right_id - $delta ELSE right_id END """ connection.execute( table.update() .where(table.c.rgt > rgt) .where(table.c.tree_id == tree_id) .values( lft=compat_layer.case( (table.c.lft > lft, table.c.lft - delta), else_=table.c.lft ), rgt=compat_layer.case( (table.c.rgt >= rgt, table.c.rgt - delta), else_=table.c.rgt ) ) ) def mptt_before_update(mapper, connection, instance): """ Based on this example: http://stackoverflow.com/questions/889527/move-node-in-nested-set """ node_id = getattr(instance, instance.get_pk_name()) table = _get_tree_table(mapper) db_pk = instance.get_pk_column() default_level = instance.get_default_level() table_pk = getattr(table.c, db_pk.name) mptt_move_inside = None left_sibling = None left_sibling_tree_id = None if hasattr(instance, 'mptt_move_inside'): mptt_move_inside = instance.mptt_move_inside if hasattr(instance, 'mptt_move_before'): ( right_sibling_left, right_sibling_right, right_sibling_parent, right_sibling_level, right_sibling_tree_id ) = connection.execute( compat_layer.select( table.c.lft, table.c.rgt, table.c.parent_id, table.c.level, table.c.tree_id ).where( table_pk == instance.mptt_move_before ) ).fetchone() current_lvl_nodes = connection.execute( compat_layer.select( table.c.lft, table.c.rgt, table.c.parent_id, table.c.tree_id ).where( and_( table.c.level == right_sibling_level, table.c.tree_id == right_sibling_tree_id, table.c.lft < right_sibling_left ) ) ).fetchall() if current_lvl_nodes: ( left_sibling_left, left_sibling_right, left_sibling_parent, left_sibling_tree_id ) = current_lvl_nodes[-1] instance.parent_id = left_sibling_parent left_sibling = { 'lft': left_sibling_left, 'rgt': left_sibling_right, 'is_parent': False } # if move_before to top level elif not right_sibling_parent: left_sibling_tree_id = right_sibling_tree_id - 1 # if placed after a particular node if hasattr(instance, 'mptt_move_after'): ( left_sibling_left, left_sibling_right, left_sibling_parent, left_sibling_tree_id ) = connection.execute( compat_layer.select( table.c.lft, table.c.rgt, table.c.parent_id, table.c.tree_id ).where( table_pk == instance.mptt_move_after ) ).fetchone() instance.parent_id = left_sibling_parent left_sibling = { 'lft': left_sibling_left, 'rgt': left_sibling_right, 'is_parent': False } """ Get subtree from node SELECT id, name, level FROM my_tree WHERE left_key >= $left_key AND right_key <= $right_key ORDER BY left_key """ subtree = connection.execute( compat_layer.select(table_pk) .where( and_( table.c.lft >= instance.left, table.c.rgt <= instance.right, table.c.tree_id == instance.tree_id ) ).order_by( table.c.lft ) ).fetchall() subtree = [x[0] for x in subtree] """ step 0: Initialize parameters. Put there left and right position of moving node """ ( node_pos_left, node_pos_right, node_tree_id, node_parent_id, node_level ) = connection.execute( compat_layer.select( table.c.lft, table.c.rgt, table.c.tree_id, table.c.parent_id, table.c.level ).where( table_pk == node_id ) ).fetchone() # if instance just update w/o move # XXX why this str() around parent_id comparison? if not left_sibling \ and str(node_parent_id) == str(instance.parent_id) \ and not mptt_move_inside: if left_sibling_tree_id is None: return # fix tree shorting if instance.parent_id is not None: ( parent_id, parent_pos_right, parent_pos_left, parent_tree_id, parent_level ) = connection.execute( compat_layer.select( table_pk, table.c.rgt, table.c.lft, table.c.tree_id, table.c.level ).where( table_pk == instance.parent_id ) ).fetchone() if node_parent_id is None and node_tree_id == parent_tree_id: instance.parent_id = None return # delete from old tree mptt_before_delete(mapper, connection, instance, False) if instance.parent_id is not None: """ Put there right position of new parent node (there moving node should be moved) """ ( parent_id, parent_pos_right, parent_pos_left, parent_tree_id, parent_level ) = connection.execute( compat_layer.select( table_pk, table.c.rgt, table.c.lft, table.c.tree_id, table.c.level ).where( table_pk == instance.parent_id ) ).fetchone() # 'size' of moving node (including all it's sub nodes) node_size = node_pos_right - node_pos_left + 1 # left sibling node if not left_sibling: left_sibling = { 'lft': parent_pos_left, 'rgt': parent_pos_right, 'is_parent': True } # insert subtree in exist tree instance.tree_id = parent_tree_id _insert_subtree( table, connection, node_size, node_pos_left, node_pos_right, parent_pos_left, parent_pos_right, subtree, parent_tree_id, parent_level, node_level, left_sibling, table_pk ) else: # if insert after if left_sibling_tree_id or left_sibling_tree_id == 0: tree_id = left_sibling_tree_id + 1 connection.execute( table.update() .where(table.c.tree_id > left_sibling_tree_id) .values( tree_id=table.c.tree_id + 1 ) ) # if just insert else: tree_id = connection.scalar( compat_layer.select( func.max(table.c.tree_id) + 1 ) ) connection.execute( table.update() .where(table_pk.in_(subtree)) .values( lft=table.c.lft - node_pos_left + 1, rgt=table.c.rgt - node_pos_left + 1, level=table.c.level - node_level + default_level, tree_id=tree_id ) ) 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 = weakref.WeakSet() return value class TreesManager(object): """ Manages events dispatching for all subclasses of a given class. """ def __init__(self, base_class): self.base_class = base_class self.classes = set() self.instances = _WeakDefaultDict() def register_events(self, remove=False): for e, h in ( ('before_insert', self.before_insert), ('before_update', self.before_update), ('before_delete', self.before_delete), ): is_event_exist = event.contains(self.base_class, e, h) if remove and is_event_exist: event.remove(self.base_class, e, h) elif not is_event_exist: event.listen(self.base_class, e, h, propagate=True) return self def register_factory(self, sessionmaker): """ Registers this TreesManager instance to respond on `after_flush_postexec` events on the given session or session factory. This method returns the original argument, so that it can be used by wrapping an already existing instance: .. code-block:: python :linenos: from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, mapper from sqlalchemy_mptt.mixins import BaseNestedSets engine = create_engine('...') trees_manager = TreesManager(BaseNestedSets) trees_manager.register_mapper(mapper) Session = tree_manager.register_factory( sessionmaker(bind=engine) ) A reference to this method, bound to a default instance of this class and already registered to a mapper, is importable directly from `sqlalchemy_mptt`: .. code-block:: python :linenos: from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy_mptt import mptt_sessionmaker engine = create_engine('...') Session = mptt_sessionmaker(sessionmaker(bind=engine)) """ event.listen(sessionmaker, 'after_flush_postexec', self.after_flush_postexec) return sessionmaker def before_insert(self, mapper, connection, instance): session = object_session(instance) self.instances[session].add(instance) mptt_before_insert(mapper, connection, instance) def before_update(self, mapper, connection, instance): session = object_session(instance) self.instances[session].add(instance) mptt_before_update(mapper, connection, instance) def before_delete(self, mapper, connection, instance): session = object_session(instance) self.instances[session].discard(instance) mptt_before_delete(mapper, connection, instance) def after_flush_postexec(self, session, context): """ Event listener to recursively expire `left` and `right` attributes the parents of all modified instances part of this flush. """ instances = self.instances[session] while True: try: instance = instances.pop() except KeyError: break if instance not in session: continue parent = self.get_parent_value(instance) while parent != NO_VALUE and parent is not None: instances.discard(parent) session.expire(parent, ['left', 'right', 'tree_id', 'level']) parent = self.get_parent_value(parent) else: session.expire(instance, ['left', 'right', 'tree_id', 'level']) self.expire_session_for_children(session, instance) @staticmethod def get_parent_value(instance): return inspection.inspect(instance).attrs.parent.loaded_value @staticmethod def expire_session_for_children(session, instance): children = instance.children def expire_recursively(node): children = node.children for item in children: session.expire(item, ['left', 'right', 'tree_id', 'level']) expire_recursively(item) if children != NO_VALUE and children is not None: for item in children: session.expire(item, ['left', 'right', 'tree_id', 'level']) expire_recursively(item) ================================================ FILE: sqlalchemy_mptt/mixins.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2014 uralbash # Copyright © 2016 Jiri Kuncar # # Distributed under terms of the MIT license. """ SQLAlchemy nested sets mixin .. testsetup:: engine = create_engine('sqlite:///:memory:') session = Session(bind=engine) """ # SQLAlchemy from sqlalchemy import Column, Integer, ForeignKey, asc, desc from sqlalchemy.orm import backref, relationship, object_session from sqlalchemy.ext.hybrid import hybrid_method from sqlalchemy.orm.session import Session from sqlalchemy.ext.declarative import declared_attr # local from .events import _get_tree_table class BaseNestedSets(object): """ Base mixin for MPTT model. Example: .. testcode:: from sqlalchemy import Boolean, Column, create_engine, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy_mptt.mixins import BaseNestedSets Base = declarative_base() class Tree(Base, BaseNestedSets): __tablename__ = "tree" id = Column(Integer, primary_key=True) visible = Column(Boolean) def __repr__(self): return "" % self.id .. testcode:: :hide: # This is some more setup code. Base.metadata.create_all(engine) node = Tree() session.add(node) session.flush() node7 = Tree(parent=node) session.add(node7) session.flush() node8 = Tree(parent=node7) session.add(node8) session.flush() node10 = Tree(parent=node7) session.add(node10) session.flush() node11 = Tree(parent=node10) session.add(node11) """ @classmethod def __declare_first__(cls): cls.__mapper__.batch = False @classmethod def get_default_level(cls): """ Compatibility with Django MPTT: level value for root node. See https://github.com/uralbash/sqlalchemy_mptt/issues/56 """ return getattr(cls, "sqlalchemy_mptt_default_level", 1) @classmethod def get_pk_name(cls): return getattr(cls, "sqlalchemy_mptt_pk_name", "id") @classmethod def get_pk_column(cls): return getattr(cls, cls.get_pk_name()) def get_pk_value(self): return getattr(self, self.get_pk_name()) @declared_attr def tree_id(cls): return Column("tree_id", Integer) @declared_attr def parent_id(cls): pk = cls.get_pk_column() if not pk.name: pk.name = cls.get_pk_name() return Column( "parent_id", pk.type, ForeignKey( "{}.{}".format(cls.__tablename__, pk.name), ondelete="CASCADE" ), ) @declared_attr def parent(self): return relationship( self, order_by=lambda: self.left, foreign_keys=[self.parent_id], remote_side="{}.{}".format(self.__name__, self.get_pk_name()), backref=backref( "children", cascade="all,delete", order_by=lambda: (self.tree_id, self.left), ), ) @declared_attr def left(cls): return Column("lft", Integer, nullable=False, index=True) @declared_attr def right(cls): return Column("rgt", Integer, nullable=False, index=True) @declared_attr def level(cls): return Column("level", Integer, nullable=False, default=0, index=True) @hybrid_method def is_ancestor_of(self, other, inclusive=False): """ class or instance level method which returns True if self is ancestor (closer to root) of other else False. Optional flag `inclusive` on whether or not to treat self as ancestor of self. For example see: * :mod:`sqlalchemy_mptt.tests.cases.integrity.test_hierarchy_structure` """ if inclusive: return ( (self.tree_id == other.tree_id) & (self.left <= other.left) & (other.right <= self.right) ) return ( (self.tree_id == other.tree_id) & (self.left < other.left) & (other.right < self.right) ) @hybrid_method def is_descendant_of(self, other, inclusive=False): """ class or instance level method which returns True if self is descendant (farther from root) of other else False. Optional flag `inclusive` on whether or not to treat self as descendant of self. For example see: * :mod:`sqlalchemy_mptt.tests.cases.integrity.test_hierarchy_structure` """ return other.is_ancestor_of(self, inclusive) def move_inside(self, parent_id): """ Moving one node of tree inside another For example see: * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_inside_function` * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_inside_to_the_same_parent_function` """ # noqa session = Session.object_session(self) self.parent_id = parent_id self.mptt_move_inside = parent_id session.add(self) def move_after(self, node_id): """ Moving one node of tree after another For example see :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_after_function` """ # noqa session = Session.object_session(self) self.parent_id = self.parent_id self.mptt_move_after = node_id session.add(self) def move_before(self, node_id): """ Moving one node of tree before another For example see: * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_before_function` * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_before_to_other_tree` * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_before_to_top_level` """ # noqa session = Session.object_session(self) table = _get_tree_table(self.__mapper__) pk = getattr(table.c, self.get_pk_column().name) node = session.query(table).filter(pk == node_id).one() self.parent_id = node.parent_id self.mptt_move_before = node_id session.add(self) def leftsibling_in_level(self): """ Node to the left of the current node at the same level For example see :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_leftsibling_in_level` """ # noqa table = _get_tree_table(self.__mapper__) session = Session.object_session(self) current_lvl_nodes = ( session.query(table) .filter_by(level=self.level) .filter_by(tree_id=self.tree_id) .filter(table.c.lft < self.left) .order_by(table.c.lft) .all() ) if current_lvl_nodes: return current_lvl_nodes[-1] return None @classmethod def _node_to_dict(cls, node, json, json_fields): """ Helper method for ``get_tree``. """ if json: pk_name = node.get_pk_name() # jqTree or jsTree format result = {"id": getattr(node, pk_name), "label": node.__repr__()} if json_fields: result.update(json_fields(node)) else: result = {"node": node} return result @classmethod def _base_query(cls, session=None): return session.query(cls) def _base_query_obj(self, session=None): if not session: session = object_session(self) return self._base_query(session) @classmethod def _base_order(cls, query, order=asc): return ( query.order_by(order(cls.tree_id)) .order_by(order(cls.level)) .order_by(order(cls.left)) ) @classmethod def get_tree(cls, session=None, json=False, json_fields=None, query=None): """ This method generate tree of current node table in dict or json format. You can make custom query with attribute ``query``. By default it return all nodes in table. Args: session (:mod:`sqlalchemy.orm.session.Session`): SQLAlchemy session Kwargs: json (bool): if True return JSON jqTree format json_fields (function): append custom fields in JSON query (function): it takes :class:`sqlalchemy.orm.query.Query` object as an argument, and returns in a modified form .. testcode:: def query(nodes): return nodes.filter(node.__class__.tree_id.is_(node.tree_id)) node.get_tree(session=session, json=True, query=query) Example: * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_get_tree` * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_get_json_tree` * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_get_json_tree_with_custom_field` """ # noqa tree = [] nodes_of_level = {} # handle custom query nodes = cls._base_query(session) if query: nodes = query(nodes) nodes = cls._base_order(nodes).all() # search minimal level of nodes. min_level = min([node.level for node in nodes] or [None]) def get_node_id(node): return getattr(node, node.get_pk_name()) for node in nodes: result = cls._node_to_dict(node, json, json_fields) parent_id = node.parent_id if node.level != min_level: # for children # Find parent in the tree if parent_id not in nodes_of_level.keys(): continue if "children" not in nodes_of_level[parent_id]: nodes_of_level[parent_id]["children"] = [] # Append node to parent nl = nodes_of_level[parent_id]["children"] nl.append(result) nodes_of_level[get_node_id(node)] = nl[-1] else: # for top level nodes tree.append(result) nodes_of_level[get_node_id(node)] = tree[-1] return tree def _drilldown_query(self, nodes=None): table = self.__class__ if not nodes: nodes = self._base_query_obj() return nodes.filter(self.is_ancestor_of(table, inclusive=True)) def drilldown_tree(self, session=None, json=False, json_fields=None): """ This method generate a branch from a tree, beginning with current node. For example: .. testcode:: node7.drilldown_tree() .. code:: level Nested sets example 1 1(1)22 --------------------- _______________|_________|_________ | | | | | | 2 2(2)5 6(4)11 | 12(7)21 | | ^ | ^ | 3 3(3)4 7(5)8 9(6)10 | 13(8)16 17(10)20 | | | | | 4 | 14(9)15 18(11)19 | | | --------------------- Example in tests: * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_drilldown_tree` """ if not session: session = object_session(self) return self.get_tree( session, json=json, json_fields=json_fields, query=self._drilldown_query, ) def path_to_root(self, session=None, order=desc): r"""Generate path from a leaf or intermediate node to the root. For example: .. testcode:: node11.path_to_root() .. code:: level Nested sets example ----------------------------------------- 1 | 1(1)22 | ________|______|_____________________ | | | | | | | ------+--------- | | 2 2(2)5 6(4)11 | -- 12(7)21 | | ^ | / \ | 3 3(3)4 7(5)8 9(6)10 ---/---- \ | 13(8)16 | 17(10)20 | | | | | 4 14(9)15 | 18(11)19 | | | ------------- """ table = self.__class__ query = self._base_query_obj(session=session) query = query.filter(table.is_ancestor_of(self, inclusive=True)) return self._base_order(query, order=order) def get_siblings(self, include_self=False, session=None): r""" * https://github.com/uralbash/sqlalchemy_mptt/issues/64 * https://django-mptt.readthedocs.io/en/latest/models.html#get-siblings-include-self-false Creates a query containing siblings of this model instance. Root nodes are considered to be siblings of other root nodes. For example: .. testcode:: node10.get_siblings() #-> [Node(8)] Only one node is sibling of node10 .. code:: level Nested sets example 1 1(1)22 ______________|____________________ | | | | | | 2 2(2)5 6(4)11 12(7)21 | ^ / \ | 3 3(3)4 7(5)8 9(6)10 / \ | 13(8)16 17(10)20 | | | | 4 14(9)15 18(11)19 | """ table = self.__class__ query = self._base_query_obj(session=session) 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 def get_children(self, session=None): r""" * https://github.com/uralbash/sqlalchemy_mptt/issues/64 * https://github.com/django-mptt/django-mptt/blob/fd76a816e05feb5fb0fc23126d33e514460a0ead/mptt/models.py#L563 Returns a query containing the immediate children of this model instance, in tree order. For example: .. testcode:: node7.get_children() #-> [Node(8), Node(10)] .. code:: level Nested sets example 1 1(1)22 ______________|____________________ | | | | | | 2 2(2)5 6(4)11 12(7)21 | ^ / \ | 3 3(3)4 7(5)8 9(6)10 / \ | 13(8)16 17(10)20 | | | | 4 14(9)15 18(11)19 | """ table = self.__class__ query = self._base_query_obj(session=session) query = query.filter(table.parent_id == self.get_pk_value()) return query @classmethod def rebuild_tree(cls, session, tree_id): """ This method rebuild tree. Args: session (:mod:`sqlalchemy.orm.session.Session`): SQLAlchemy session tree_id (int or str): id of tree Example: * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_rebuild` """ session.query(cls).filter_by(tree_id=tree_id).update( {cls.left: 0, cls.right: 0, cls.level: 0} ) top = ( session.query(cls) .filter_by(parent_id=None) .filter_by(tree_id=tree_id) .one() ) top.left = left = 1 top.right = right = 2 top.level = level = cls.get_default_level() def recursive(children, left, right, level): level = level + 1 for i, node in enumerate(children): same_level_right = children[i - 1].right left = left + 1 if i > 0: left = left + 1 if same_level_right: left = same_level_right + 1 right = left + 1 node.left = left node.right = right parent = node.parent j = 0 while parent: parent.right = right + 1 + j parent = parent.parent j += 1 node.level = level recursive(node.children, left, right, level) recursive(top.children, left, right, level) @classmethod def rebuild(cls, session, tree_id=None): """ This function rebuild tree. Args: session (:mod:`sqlalchemy.orm.session.Session`): SQLAlchemy session Kwargs: tree_id (int or str): id of tree, default None Example: * :mod:`sqlalchemy_mptt.tests.TestTree.test_rebuild` """ trees = session.query(cls).filter_by(parent_id=None) if tree_id: trees = trees.filter_by(tree_id=tree_id) for tree in trees: cls.rebuild_tree(session, tree.tree_id) ================================================ FILE: sqlalchemy_mptt/sqlalchemy_compat.py ================================================ # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright (c) 2025 Fayaz Yusuf Khan # Distributed under terms of the MIT license. """Compatibility layer for SQLAlchemy versions.""" import sqlalchemy as sa class LegacySQLAlchemyAPI: """A class to provide compatibility for legacy SQLAlchemy versions (1.0 - 1.3).""" @staticmethod def declarative_base(*args, **kwargs): from sqlalchemy.ext.declarative import declarative_base return declarative_base(*args, **kwargs) @staticmethod def select(*args, **kwargs): return sa.select(args, **kwargs) @staticmethod def case(*args, **kwargs): return sa.case(args, **kwargs) @staticmethod def get(session, model, id): return session.query(model).get(id) class ModernSQLAlchemyAPI: """A class to provide compatibility for modern SQLAlchemy versions (1.4+).""" @staticmethod def declarative_base(*args, **kwargs): from sqlalchemy.orm import declarative_base return declarative_base(*args, **kwargs) @staticmethod def select(*args, **kwargs): return sa.select(*args, **kwargs) @staticmethod def case(*args, **kwargs): return sa.case(*args, **kwargs) @staticmethod def get(session, model, id): return session.get(model, id) if sa.__version__ < '1.4': compat_layer = LegacySQLAlchemyAPI() else: compat_layer = ModernSQLAlchemyAPI() ================================================ FILE: sqlalchemy_mptt/tests/__init__.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2014 uralbash # # Distributed under terms of the MIT license. """ Base mptt tree .. code:: level Nested sets tree1 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Nested sets tree2 1 1(12)22 _______________|___________________ | | | 2 2(13)5 6(15)11 12(18)21 | ^ ^ 3 3(14)4 7(16)8 9(17)10 13(19)16 17(21)20 | | 4 14(20)15 18(22)19 """ # standard library import contextlib import json import os import sys import typing import unittest # SQLAlchemy import sqlalchemy as sa from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from sqlalchemy_mptt import mptt_sessionmaker from sqlalchemy_mptt.sqlalchemy_compat import compat_layer 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 BaseType = unittest.TestCase if typing.TYPE_CHECKING else object 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 DatabaseSetupMixin(BaseType): base: compat_layer.declarative_base() # 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 def add(self, model, fixtures): here = os.path.dirname(os.path.realpath(__file__)) with open(os.path.join(here, fixtures)) as file: fixtures = json.loads(file.read()) for fixture in fixtures: if hasattr(model, "sqlalchemy_mptt_pk_name"): fixture[model.sqlalchemy_mptt_pk_name] = fixture.pop("id") self.session.add(model(**fixture)) self.session.flush() class TreeTestingMixin( Initialize, Changes, MoveAfter, DataIntegrity, MoveBefore, MoveInside, Tree, GetNodes, DatabaseSetupMixin ): base = None model = None def catch_queries(self, conn, cursor, statement, *args): self.stmts.append(statement) def start_query_counter(self): self.stmts = [] event.listen( self.session.bind.engine, "before_cursor_execute", self.catch_queries ) def stop_query_counter(self): event.remove( self.session.bind.engine, "before_cursor_execute", self.catch_queries ) def setUp(self): super().setUp() self.fixture = Fixtures(self.session) self.fixture.add( self.model, os.path.join("fixtures", getattr(self, "fixtures", "tree.json")) ) self.result = self.session.query( self.model.get_pk_column(), self.model.left, self.model.right, self.model.level, self.model.parent_id, self.model.tree_id, ) def test_session_expire_for_move_after_to_new_tree(self): """ https://github.com/uralbash/sqlalchemy_mptt/issues/33 """ node = ( self.session.query(self.model).filter(self.model.get_pk_column() == 4).one() ) children = ( self.session.query(self.model) .filter(self.model.get_pk_column().in_((5, 6))) .all() ) node.move_after("1") self.session.flush() _level = node.get_default_level() self.assertEqual(node.tree_id, 2) self.assertEqual(node.level, _level) self.assertEqual(node.parent_id, None) self.assertEqual(children[0].tree_id, 2) self.assertEqual(children[0].parent_id, 4) self.assertEqual(children[0].level, _level + 1) self.assertEqual(children[1].tree_id, 2) self.assertEqual(children[1].parent_id, 4) self.assertEqual(children[1].level, _level + 1) ================================================ FILE: sqlalchemy_mptt/tests/cases/__init__.py ================================================ ================================================ FILE: sqlalchemy_mptt/tests/cases/edit_node.py ================================================ class Changes(object): def test_update_wo_move(self): """ Update node w/o move initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 4).one() node.visible = True self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all()) # flake8: noqa def test_update_wo_move_like_sacrud_save(self): """ Just change attr from node w/o move initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 4).one() node.parent_id = '1' node.visible = True self.session.add(node) _level = node.get_default_level() # id lft rgt lvl parent tree self.assertEqual([(1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2)], self.result.all()) def test_insert_node(self): """ Insert node with parent==6 initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Insert node with parent_id == 6 1 1(1)24 _______________|_________________ | | | 2 2(2)5 6(4)13 14(7)23 | ____|____ ___|____ | | | | | 3 3(3)4 7(5)8 9(6)12 15(8)18 19(10)22 | | | 4 10(23)11 16(9)17 20(11)21 """ node = self.model(parent_id=6) self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 24, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 13, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 12, _level + 2, 4, 1), (7, 14, 23, _level + 1, 1, 1), (8, 15, 18, _level + 2, 7, 1), (9, 16, 17, _level + 3, 8, 1), (10, 19, 22, _level + 2, 7, 1), (11, 20, 21, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2), (23, 10, 11, _level + 3, 6, 1) ], self.result.all()) def test_insert_node_near_subtree(self): """ Insert node with parent==4 initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Insert node with parent_id == 4 1 1(1)24 _______________|_____________________ | | | 2 2(2)5 6(4)13 14(7)23 | ______|________ __|______ | | | | | | 3 3(3)4 7(5)8 9(6)10 11(23)12 15(8)18 19(10)22 | | 4 16(9)17 20(11)21 """ node = self.model(parent_id=4) self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 24, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 13, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 14, 23, _level + 1, 1, 1), (8, 15, 18, _level + 2, 7, 1), (9, 16, 17, _level + 3, 8, 1), (10, 19, 22, _level + 2, 7, 1), (11, 20, 21, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2), (23, 11, 12, _level + 2, 4, 1) ], self.result.all()) def test_insert_after_node(self): pass def test_delete_node(self): """ Delete node(4) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Test delete node 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Delete node == 4 1 1(1)16 _______________|_____ | | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 4).one() self.session.delete(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 16, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (7, 6, 15, _level + 1, 1, 1), (8, 7, 10, _level + 2, 7, 1), (9, 8, 9, _level + 3, 8, 1), (10, 11, 14, _level + 2, 7, 1), (11, 12, 13, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all()) def test_update_node(self): """ Set parent_id==5 for node(8) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Test update node 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 8 - > 5 1 1(1)22 _______________|__________________ | | | 2 2(2)5 6(4)15 16(7)21 | ^ | 3 3(3)4 7(5)12 13(6)14 17(10)20 | | 4 8(8)11 18(11)19 | 5 9(9)10 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 8).one() node.parent_id = 5 self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 15, _level + 1, 1, 1), (5, 7, 12, _level + 2, 4, 1), (6, 13, 14, _level + 2, 4, 1), (7, 16, 21, _level + 1, 1, 1), (8, 8, 11, _level + 3, 5, 1), (9, 9, 10, _level + 4, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all()) """ level Move 8 - > 5 1 1(1)22 _______________|__________________ | | | 2 2(2)5 6(4)15 16(7)21 | ^ | 3 3(3)4 7(5)12 13(6)14 17(10)20 | | 4 8(8)11 18(11)19 | 5 9(9)10 level Move 4 - > 2 1 1(1)22 ________|_____________ | | 2 2(2)15 16(7)21 ____|_____ | | | | 3 3(4)12 13(3)14 17(10)20 ^ | 4 4(5)9 10(6)11 18(11)19 | 5 5(8)8 | 6 6(9)7 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 4).one() node.parent_id = 2 self.session.add(node) self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 15, _level + 1, 1, 1), (3, 13, 14, _level + 2, 2, 1), (4, 3, 12, _level + 2, 2, 1), (5, 4, 9, _level + 3, 4, 1), (6, 10, 11, _level + 3, 4, 1), (7, 16, 21, _level + 1, 1, 1), (8, 5, 8, _level + 4, 5, 1), (9, 6, 7, _level + 5, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all()) """ level Move 4 - > 2 1 1(1)22 ________|_____________ | | 2 2(2)15 16(7)21 ____|_____ | | | | 3 3(4)12 13(3)14 17(10)20 ^ | 4 4(5)9 10(6)11 18(11)19 | 5 5(8)8 | 6 6(9)7 level Move 8 - > 10 1 1(1)22 ________|_____________ | | 2 2(2)11 12(7)21 ______|_____ | | | | 3 3(4)8 9(3)10 13(10)20 __|____ _|______ | | | | 4 4(5)5 6(6)7 14(8)17 18(11)19 | 5 15(9)16 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 8).one() node.parent_id = 10 self.session.add(node) self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 11, _level + 1, 1, 1), (3, 9, 10, _level + 2, 2, 1), (4, 3, 8, _level + 2, 2, 1), (5, 4, 5, _level + 3, 4, 1), (6, 6, 7, _level + 3, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 14, 17, _level + 3, 10, 1), (9, 15, 16, _level + 4, 8, 1), (10, 13, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all()) def test_rebuild(self): """ Rebuild tree with tree_id==1 .. code:: level Nested sets w/o left & right (or broken left & right) 1 (1) _______________|___________________ | | | 2 (2) (4) (7) | ^ ^ 3 (3) (5) (6) (8) (10) | | 4 (9) (11) level Nested sets after rebuild 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 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.model.rebuild(self.session, 1) _level = self.model.get_default_level() self.assertEqual( self.result.all(), [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 0, 0, 0, None, 2), (13, 0, 0, 0, 12, 2), (14, 0, 0, 0, 13, 2), (15, 0, 0, 0, 12, 2), (16, 0, 0, 0, 15, 2), (17, 0, 0, 0, 15, 2), (18, 0, 0, 0, 12, 2), (19, 0, 0, 0, 18, 2), (20, 0, 0, 0, 19, 2), (21, 0, 0, 0, 18, 2), (22, 0, 0, 0, 21, 2) ] ) self.model.rebuild(self.session) self.assertEqual( self.result.all(), [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ] ) ================================================ FILE: sqlalchemy_mptt/tests/cases/get_node.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2015 uralbash # # Distributed under terms of the MIT license. class GetNodes(object): def test_get_siblings(self): """ Get siblings of node initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 (12) _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 """ node10 = ( self.session.query(self.model) .filter(self.model.get_pk_column() == 10) .one() ) points = ( self.session.query(self.model).filter(self.model.get_pk_column() == 8).all() ) self.assertEqual(points, node10.get_siblings().all()) # flake8: noqa node9 = ( self.session.query(self.model).filter(self.model.get_pk_column() == 9).one() ) 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 initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 """ node7 = ( self.session.query(self.model).filter(self.model.get_pk_column() == 7).one() ) points = self.session.query(self.model).filter(self.model.parent_id == 7).all() self.assertEqual(points, node7.get_children().all()) # flake8: noqa node9 = ( self.session.query(self.model).filter(self.model.get_pk_column() == 9).one() ) self.assertEqual([], node9.get_children().all()) # flake8: noqa ================================================ FILE: sqlalchemy_mptt/tests/cases/get_tree.py ================================================ from sqlalchemy import asc def get_obj(session, model, id): return session.query(model).filter(model.get_pk_column() == id).one() 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_with_custom_query(self): """ No rows with id < 0. """ query = lambda x: x.filter(self.model.get_pk_column() < 0) # noqa tree = self.model.get_tree(self.session, query=query) self.assertEqual(tree, []) def test_get_tree(self): """.. note:: See [source] for full example Return tree as list of dict .. code:: tree = Tree.get_tree(self.session) """ tree = self.model.get_tree(self.session) def go(id): return get_obj(self.session, self.model, id) reference_tree = [ {'node': go(1), 'children': [{'node': go(2), 'children': [{'node': go(3)}]}, {'node': go(4), 'children': [{'node': go(5)}, {'node': go(6)}]}, {'node': go(7), 'children': [{'node': go(8), 'children': [{'node': go(9)}]}, {'node': go(10), 'children': [{'node': go(11)}]}]}]}, {'node': go(12), 'children': [{'node': go(13), 'children': [{'node': go(14)}]}, {'node': go(15), 'children': [{'node': go(16)}, {'node': go(17)}]}, {'node': go(18), 'children': [{'node': go(19), 'children': [{'node': go(20)}]}, {'node': go(21), 'children': [{'node': go(22)}]}]}]}] self.assertEqual(tree, reference_tree) def test_get_tree_count_query(self): """ Count num of queries to the database. See https://github.com/uralbash/sqlalchemy_mptt/issues/39 """ # from datetime import datetime self.session.commit() # Get tree by for cycle self.start_query_counter() self.assertEqual(0, len(self.stmts)) # startTime = datetime.now() self.model.get_tree(self.session) # delta = datetime.now() - startTime # print("Get tree: {!s:>26}".format(delta)) self.assertEqual(1, len(self.stmts)) self.stop_query_counter() def test_get_json_tree(self): """.. note:: See [source] for full example Return tree as JSON of jqTree format .. code:: tree = Tree.get_tree(self.session, json=True) """ reference_tree = [ {'children': [{'children': [{'id': 3, 'label': ''}], 'id': 2, 'label': ''}, {'children': [{'id': 5, 'label': ''}, {'id': 6, 'label': ''}], 'id': 4, 'label': ''}, {'children': [{'children': [{'id': 9, 'label': ''}], 'id': 8, 'label': ''}, {'children': [{'id': 11, 'label': ''}], 'id': 10, 'label': ''}], 'id': 7, 'label': ''}], 'id': 1, 'label': ''}, {'children': [{'children': [{'id': 14, 'label': ''}], 'id': 13, 'label': ''}, {'children': [{'id': 16, 'label': ''}, {'id': 17, 'label': ''}], 'id': 15, 'label': ''}, {'children': [{'children': [{'id': 20, 'label': ''}], 'id': 19, 'label': ''}, {'children': [{'id': 22, 'label': ''}], 'id': 21, 'label': ''}], 'id': 18, 'label': ''}], 'id': 12, 'label': ''}] tree = self.model.get_tree(self.session, json=True) self.assertEqual(tree, reference_tree) def test_get_json_tree_with_custom_field(self): """.. note:: See [source] for full example Return tree as JSON of jqTree format with additional field .. code-block:: python :linenos: def fields(node): return {'visible': node.visible} tree = Tree.get_tree(self.session, json=True, json_fields=fields) """ self.maxDiff = None def fields(node): return {'visible': node.visible} reference_tree = [ {'visible': None, 'children': [{'visible': True, 'children': [{'visible': True, 'id': 3, 'label': ''}], 'id': 2, 'label': ''}, {'visible': True, 'children': [{'visible': True, 'id': 5, 'label': ''}, {'visible': True, 'id': 6, 'label': ''}], 'id': 4, 'label': ''}, {'visible': True, 'children': [{'visible': True, 'children': [{'visible': None, 'id': 9, 'label': ''}], 'id': 8, 'label': ''}, {'visible': None, 'children': [{'visible': None, 'id': 11, 'label': ''}], 'id': 10, 'label': ''}], 'id': 7, 'label': ''}], 'id': 1, 'label': ''}, {'visible': None, 'children': [{'visible': None, 'children': [{'visible': None, 'id': 14, 'label': ''}], 'id': 13, 'label': ''}, {'visible': None, 'children': [{'visible': None, 'id': 16, 'label': ''}, {'visible': None, 'id': 17, 'label': ''}], 'id': 15, 'label': ''}, {'visible': None, 'children': [{'visible': None, 'children': [{'visible': None, 'id': 20, 'label': ''}], 'id': 19, 'label': ''}, {'visible': None, 'children': [{'visible': None, 'id': 22, 'label': ''}], 'id': 21, 'label': ''}], 'id': 18, 'label': ''}], 'id': 12, 'label': ''}] tree = self.model.get_tree(self.session, json=True, json_fields=fields) self.assertEqual(tree, reference_tree) def test_leftsibling_in_level(self): """ Node to the left of the current node at the same level .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level1 = [1] level2 = [2, 4, 7] level3 = [3, 5, 6, 8, 10] level4 = [9, 11] leftsibling_in_level_of_node_3 = None leftsibling_in_level_of_node_5 = 3 leftsibling_in_level_of_node_6 = 5 leftsibling_in_level_of_node_8 = 6 leftsibling_in_level_of_node_11 = 9 """ q = self.session.query(self.model) node3 = q.filter(self.model.get_pk_column() == 3).one() node5 = q.filter(self.model.get_pk_column() == 5).one() node6 = q.filter(self.model.get_pk_column() == 6).one() node8 = q.filter(self.model.get_pk_column() == 8).one() node10 = q.filter(self.model.get_pk_column() == 10).one() pk_name = self.model.get_pk_name() pk_column_name = self.model.get_pk_column().name self.assertEqual( getattr(node10.leftsibling_in_level(), pk_column_name), getattr(node8, pk_name) ) self.assertEqual( getattr(node8.leftsibling_in_level(), pk_column_name), getattr(node6, pk_name) ) self.assertEqual( getattr(node6.leftsibling_in_level(), pk_column_name), getattr(node5, pk_name) ) self.assertEqual(node3.leftsibling_in_level(), None) def test_drilldown_tree(self): """ .. code:: level Nested sets example 1 1(1)22 --------------------- _______________|_________|_________ | | | | | | 2 2(2)5 6(4)11 | 12(7)21 | | ^ | ^ | 3 3(3)4 7(5)8 9(6)10 | 13(8)16 17(10)20 | | | | | 4 | 14(9)15 18(11)19 | | | --------------------- """ def go(id): return get_obj(self.session, self.model, id) node = go(7) tree = node.drilldown_tree(self.session) reference_tree = [ {'node': go(7), 'children': [ {'node': go(8), 'children': [ {'node': go(9)}]}, {'node': go(10), 'children': [ {'node': go(11)}]}] } ] self.assertEqual(tree, reference_tree) def test_drilldown_tree_without_session(self): def go(id): return get_obj(self.session, self.model, id) node = go(7) tree = node.drilldown_tree() reference_tree = [ {'node': go(7), 'children': [ {'node': go(8), 'children': [ {'node': go(9)}]}, {'node': go(10), 'children': [ {'node': go(11)}]}] } ] self.assertEqual(tree, reference_tree) def test_path_to_root(self): r"""Generate path from a leaf or intermediate node to the root. For example: node11.path_to_root() .. code:: level Nested sets example ----------------------------------------- 1 | 1(1)22 | ________|______|_____________________ | | | | | | | -----+--------- | | 2 2(2)5 6(4)11 | -- 12(7)21 | | ^ | / \ | 3 3(3)4 7(5)8 9(6)10 ---/---- \ | 13(8)16 | 17(10)20 | | | | | 4 14(9)15 | 18(11)19 | | | ------------- """ def go(id): return get_obj(self.session, self.model, id) node11 = go(11) node8 = go(8) node6 = go(6) node1 = go(1) path_11_to_root = node11.path_to_root(self.session).all() path_8_to_root = node8.path_to_root(self.session).all() path_6_to_root = node6.path_to_root(self.session).all() path_1_to_root = node1.path_to_root(self.session).all() self.assertEqual(path_11_to_root, [go(11), go(10), go(7), go(1)]) self.assertEqual(path_8_to_root, [go(8), go(7), go(1)]) self.assertEqual(path_6_to_root, [go(6), go(4), go(1)]) self.assertEqual(path_1_to_root, [go(1)]) asc_path_11_to_root = node11.path_to_root(self.session, order=asc).all() self.assertEqual(asc_path_11_to_root, [go(1), go(7), go(10), go(11)]) ================================================ FILE: sqlalchemy_mptt/tests/cases/initialize.py ================================================ from sqlalchemy.exc import IntegrityError class Initialize(object): def test_tree_orm_initialize(self): pk_name = self.model.get_pk_name() t0 = self.model(**{pk_name: 30}) t1 = self.model(**{pk_name: 31, 'parent': t0}) t2 = self.model(**{pk_name: 32, 'parent': t1}) t3 = self.model(**{pk_name: 33, 'parent': t1}) self.session.add(t0) self.session.flush() self.assertEqual(t0.left, 1) self.assertEqual(t0.right, 8) self.assertEqual(t1.left, 2) self.assertEqual(t1.right, 7) self.assertEqual(t2.left, 3) self.assertEqual(t2.right, 4) self.assertEqual(t3.left, 5) self.assertEqual(t3.right, 6) t0 = self.model(**{pk_name: 40}) t1 = self.model(**{pk_name: 41, 'parent': t0}) t2 = self.model(**{pk_name: 42, 'parent': t1}) t3 = self.model(**{pk_name: 43, 'parent': t2}) t4 = self.model(**{pk_name: 44, 'parent': t3}) t5 = self.model(**{pk_name: 45, 'parent': t4}) self.session.add(t3) self.session.flush() self.assertEqual(t0.left, 1) self.assertEqual(t0.right, 12) self.assertEqual(t1.left, 2) self.assertEqual(t1.right, 11) self.assertEqual(t2.left, 3) self.assertEqual(t2.right, 10) self.assertEqual(t3.left, 4) self.assertEqual(t3.right, 9) self.assertEqual(t4.left, 5) self.assertEqual(t4.right, 8) self.assertEqual(t5.left, 6) self.assertEqual(t5.right, 7) def test_flush_with_transient_nodes_present(self): """ https://github.com/uralbash/sqlalchemy_mptt/issues/34 """ pk_name = self.model.get_pk_name() transient_node = self.model(**{pk_name: 1, 'parent': None}) self.session.add(transient_node) try: self.session.flush() except IntegrityError: pass self.session.rollback() self.session.add(self.model(**{pk_name: 46, 'parent': None})) self.session.flush() def test_tree_initialize(self): """ Initial state of the trees .. code:: level Tree 1 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Tree 2 1 1(12)22 _______________|___________________ | | | 2 2(13)5 6(15)11 12(18)21 | ^ ^ 3 3(14)4 7(16)8 9(17)10 13(19)16 17(21)20 | | 4 14(20)15 18(22)19 """ _level = self.model.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all()) # flake8: noqa ================================================ FILE: sqlalchemy_mptt/tests/cases/integrity.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2015 uralbash # # Distributed under terms of the MIT license. from sqlalchemy.sql import func class DataIntegrity(object): def test_left_is_always_less_than_right(self): """ The left key is always less than the right. The following example should return an empty result. .. code-block:: sql SELECT id FROM tree WHERE left >= right """ table = self.model nop = self.session.query(table).filter(table.left >= table.right).all() self.assertEqual(nop, []) def test_lowest_left_is_always_1(self): """ The lowest left key is always 1. The following example should return 1. .. code-block:: sql SELECT MIN(left) FROM tree """ table = self.model one = self.session.query(func.min(table.left)).scalar() self.assertEqual(one, 1) def test_greatest_right_is_always_double_number_of_nodes(self): """ The greatest right key is always double the number of nodes. The following example should match COUNT(id) * 2 equal MAX(right). .. code-block:: sql SELECT COUNT(id), MAX(right) FROM tree """ table = self.model result = self.session.query( func.count(table.get_pk_name()), func.max(table.right)).group_by(table.tree_id).all() for tree in result: self.assertEqual(tree[0] * 2, tree[1]) def test_right_minus_left_always_odd(self): """ Difference between left and right keys are always an odd number. The following example should return an empty result. .. code-block:: sql SELECT MOD((right - left) / 2) AS modulo FROM tree WHERE modulo = 0 """ table = self.model modulo = (table.right - table.left) % 2 nop = self.session.query(table).filter(modulo == 0).all() self.assertEqual(nop, []) def test_level_odd_when_left_odd_and_vice_versa(self): """ If the node number is odd then the left key is always an odd number, and the same goes for the even numbers. The following example should return an empty result. .. code-block:: sql SELECT id, MOD((left - level + 2) / 2) AS modulo FROM tree WHERE modulo = 1 """ table = self.model level_delta = pow(0, table.get_default_level() % 2) modulo = (table.left - table.level + level_delta + 2) % 2 nop = self.session.query(table).filter(modulo == 1).all() self.assertEqual(nop, []) def test_left_and_right_always_unique_number(self): """ left and right always is unique. """ table = self.model left = self.session.query(table.left) right = self.session.query(table.right) keys = [x[0] for x in left.union(right)] self.assertEqual(len(keys), len(set(keys))) def test_hierarchy_structure(self): """ Nodes with left < self and right > self are considered ancestors, while nodes with left > self and right < self are considered descendants """ table = self.model pivot = self.session.query(table).filter( table.right - table.left != 1 ).filter(table.parent_id != None).first() # noqa # Exclusive Tests ancestors = self.session.query(table).filter( table.is_ancestor_of(pivot) ).all() for ancestor in ancestors: self.assertTrue(ancestor.is_ancestor_of(pivot)) self.assertNotIn(pivot, ancestors) descendants = self.session.query(table).filter( table.is_descendant_of(pivot) ).all() for descendant in descendants: self.assertTrue(descendant.is_descendant_of(pivot)) self.assertNotIn(pivot, descendants) self.assertEqual(set(), set(ancestors).intersection(set(descendants))) # Inclusive Tests - because sometimes inclusivity is nice, like with # self joins ancestors = self.session.query(table).filter( table.is_ancestor_of(pivot, inclusive=True) ).all() for ancestor in ancestors: self.assertTrue(ancestor.is_ancestor_of(pivot, inclusive=True)) self.assertIn(pivot, ancestors) descendants = self.session.query(table).filter( table.is_descendant_of(pivot, inclusive=True) ).all() for descendant in descendants: self.assertTrue(descendant.is_descendant_of(pivot, inclusive=True)) self.assertIn(pivot, descendants) self.assertEqual( set([pivot]), set(ancestors).intersection(set(descendants)) ) ================================================ FILE: sqlalchemy_mptt/tests/cases/move_node.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2015 uralbash # # Distributed under terms of the MIT license. import os class MoveBefore(object): def test_move_before_to_top_level(self): """ For example move node(4) before node(1) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level move 4 before 1 1 1(4)6 1(1)16 ^ _______|_______ 2(5)3 4(6)5 | | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 4).one() node.move_before(1) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 16, _level + 0, None, 2), (2, 2, 5, _level + 1, 1, 2), (3, 3, 4, _level + 2, 2, 2), (4, 1, 6, _level + 0, None, 1), (5, 2, 3, _level + 1, 4, 1), (6, 4, 5, _level + 1, 4, 1), (7, 6, 15, _level + 1, 1, 2), (8, 7, 10, _level + 2, 7, 2), (9, 8, 9, _level + 3, 8, 2), (10, 11, 14, _level + 2, 7, 2), (11, 12, 13, _level + 3, 10, 2), (12, 1, 22, _level + 0, None, 3), (13, 2, 5, _level + 1, 12, 3), (14, 3, 4, _level + 2, 13, 3), (15, 6, 11, _level + 1, 12, 3), (16, 7, 8, _level + 2, 15, 3), (17, 9, 10, _level + 2, 15, 3), (18, 12, 21, _level + 1, 12, 3), (19, 13, 16, _level + 2, 18, 3), (20, 14, 15, _level + 3, 19, 3), (21, 17, 20, _level + 2, 18, 3), (22, 18, 19, _level + 3, 21, 3) ], self.result.all()) # flake8: noqa def test_move_one_tree_before_another(self): """ For example move node(12) before node(1) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: <-------------------------------- | level Nested sets tree1 | 1 1(1)22 | _______________|___________________ | | | | | 2 2(2)5 6(4)11 12(7)21 | | ^ ^ | 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | | | 4 14(9)15 18(11)19 | | level Nested sets tree2 | 1 1(12)22 ---------------------------- _______________|___________________ | | | 2 2(13)5 6(15)11 12(18)21 | ^ ^ 3 3(14)4 7(16)8 9(17)10 13(19)16 17(21)20 | | 4 14(20)15 18(22)19 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 12).one() node.move_before("1") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 2), (2, 2, 5, _level + 1, 1, 2), (3, 3, 4, _level + 2, 2, 2), (4, 6, 11, _level + 1, 1, 2), (5, 7, 8, _level + 2, 4, 2), (6, 9, 10, _level + 2, 4, 2), (7, 12, 21, _level + 1, 1, 2), (8, 13, 16, _level + 2, 7, 2), (9, 14, 15, _level + 3, 8, 2), (10, 17, 20, _level + 2, 7, 2), (11, 18, 19, _level + 3, 10, 2), (12, 1, 22, _level + 0, None, 1), (13, 2, 5, _level + 1, 12, 1), (14, 3, 4, _level + 2, 13, 1), (15, 6, 11, _level + 1, 12, 1), (16, 7, 8, _level + 2, 15, 1), (17, 9, 10, _level + 2, 15, 1), (18, 12, 21, _level + 1, 12, 1), (19, 13, 16, _level + 2, 18, 1), (20, 14, 15, _level + 3, 19, 1), (21, 17, 20, _level + 2, 18, 1), (22, 18, 19, _level + 3, 21, 1) ], self.result.all()) def test_move_before_function(self): """ For example move node(8) before node(4) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level move 8 before 4 1 1(1)22 _______________|___________________ | | | | 2 2(2)5 6(8)9 10(4)15 16(7)21 | | ^ | 3 3(3)4 7(9)8 11(5)12 13(6)14 17(10)20 | 4 18(11)19 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 8).one() node.move_before("4") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 10, 15, _level + 1, 1, 1), (5, 11, 12, _level + 2, 4, 1), (6, 13, 14, _level + 2, 4, 1), (7, 16, 21, _level + 1, 1, 1), (8, 6, 9, _level + 1, 1, 1), (9, 7, 8, _level + 2, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all()) def test_move_one_tree_before_other_tree(self): self.fixture.add( self.model, os.path.join('fixtures', 'tree_3.json') ) self.session.commit() self.maxDiff = None node = self.session.query(self.model).\ filter(self.model.get_pk_column() == 12).one() node.move_before("1") _level = node.get_default_level() self.assertEqual( self.result.all(), [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 2), (2, 2, 5, _level + 1, 1, 2), (3, 3, 4, _level + 2, 2, 2), (4, 6, 11, _level + 1, 1, 2), (5, 7, 8, _level + 2, 4, 2), (6, 9, 10, _level + 2, 4, 2), (7, 12, 21, _level + 1, 1, 2), (8, 13, 16, _level + 2, 7, 2), (9, 14, 15, _level + 3, 8, 2), (10, 17, 20, _level + 2, 7, 2), (11, 18, 19, _level + 3, 10, 2), (12, 1, 22, _level + 0, None, 1), (13, 2, 5, _level + 1, 12, 1), (14, 3, 4, _level + 2, 13, 1), (15, 6, 11, _level + 1, 12, 1), (16, 7, 8, _level + 2, 15, 1), (17, 9, 10, _level + 2, 15, 1), (18, 12, 21, _level + 1, 12, 1), (19, 13, 16, _level + 2, 18, 1), (20, 14, 15, _level + 3, 19, 1), (21, 17, 20, _level + 2, 18, 1), (22, 18, 19, _level + 3, 21, 1), (23, 1, 22, _level + 0, None, 4), (24, 2, 5, _level + 1, 23, 4), (25, 3, 4, _level + 2, 24, 4), (26, 6, 11, _level + 1, 23, 4), (27, 7, 8, _level + 2, 26, 4), (28, 9, 10, _level + 2, 26, 4), (29, 12, 21, _level + 1, 23, 4), (30, 13, 16, _level + 2, 29, 4), (31, 14, 15, _level + 3, 30, 4), (32, 17, 20, _level + 2, 29, 4), (33, 18, 19, _level + 3, 32, 4) ] ) node = self.session.query(self.model).\ filter(self.model.get_pk_column() == 23).one() node.move_before("1") _level = node.get_default_level() self.assertEqual( self.result.all(), [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 3), (2, 2, 5, _level + 1, 1, 3), (3, 3, 4, _level + 2, 2, 3), (4, 6, 11, _level + 1, 1, 3), (5, 7, 8, _level + 2, 4, 3), (6, 9, 10, _level + 2, 4, 3), (7, 12, 21, _level + 1, 1, 3), (8, 13, 16, _level + 2, 7, 3), (9, 14, 15, _level + 3, 8, 3), (10, 17, 20, _level + 2, 7, 3), (11, 18, 19, _level + 3, 10, 3), (12, 1, 22, _level + 0, None, 1), (13, 2, 5, _level + 1, 12, 1), (14, 3, 4, _level + 2, 13, 1), (15, 6, 11, _level + 1, 12, 1), (16, 7, 8, _level + 2, 15, 1), (17, 9, 10, _level + 2, 15, 1), (18, 12, 21, _level + 1, 12, 1), (19, 13, 16, _level + 2, 18, 1), (20, 14, 15, _level + 3, 19, 1), (21, 17, 20, _level + 2, 18, 1), (22, 18, 19, _level + 3, 21, 1), (23, 1, 22, _level + 0, None, 2), (24, 2, 5, _level + 1, 23, 2), (25, 3, 4, _level + 2, 24, 2), (26, 6, 11, _level + 1, 23, 2), (27, 7, 8, _level + 2, 26, 2), (28, 9, 10, _level + 2, 26, 2), (29, 12, 21, _level + 1, 23, 2), (30, 13, 16, _level + 2, 29, 2), (31, 14, 15, _level + 3, 30, 2), (32, 17, 20, _level + 2, 29, 2), (33, 18, 19, _level + 3, 32, 2) ] ) node = self.session.query(self.model).\ filter(self.model.get_pk_column() == 1).one() node.move_before("12") _level = node.get_default_level() self.assertEqual( self.result.all(), [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2), (23, 1, 22, _level + 0, None, 3), (24, 2, 5, _level + 1, 23, 3), (25, 3, 4, _level + 2, 24, 3), (26, 6, 11, _level + 1, 23, 3), (27, 7, 8, _level + 2, 26, 3), (28, 9, 10, _level + 2, 26, 3), (29, 12, 21, _level + 1, 23, 3), (30, 13, 16, _level + 2, 29, 3), (31, 14, 15, _level + 3, 30, 3), (32, 17, 20, _level + 2, 29, 3), (33, 18, 19, _level + 3, 32, 3) ] ) def test_move_before_to_other_tree(self): """ For example move node(8) before node(15) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Move 8 before 15 1 1(1)18 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)17 | ^ | 3 3(3)4 7(5)8 9(6)10 13(10)16 | 4 14(11)15 level 1 1(12)26 _______________|______________________________ | | | | 2 2(13)5 6(8)9 10(15)15 16(18)25 | | ^ ^ 3 3(14)4 7(9)8 11(16)12 13(17)14 17(19)20 21(21)24 | | 4 18(20)19 22(22)23 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 8).one() node.move_before("15") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 18, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 17, _level + 1, 1, 1), (8, 6, 9, _level + 1, 12, 2), (9, 7, 8, _level + 2, 8, 2), (10, 13, 16, _level + 2, 7, 1), (11, 14, 15, _level + 3, 10, 1), (12, 1, 26, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 10, 15, _level + 1, 12, 2), (16, 11, 12, _level + 2, 15, 2), (17, 13, 14, _level + 2, 15, 2), (18, 16, 25, _level + 1, 12, 2), (19, 17, 20, _level + 2, 18, 2), (20, 18, 19, _level + 3, 19, 2), (21, 21, 24, _level + 2, 18, 2), (22, 22, 23, _level + 3, 21, 2) ], self.result.all() ) class MoveAfter(object): def test_move_after_function(self): """ For example move node(8) after node(5) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Initial state 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 8 after 5 1 1(1)22 _______________|__________________ | | | 2 2(2)5 6(4)15 16(7)21 | ^ | 3 3(3)4 7(5)8 9(8)12 13(6)14 17(10)20 | | 4 10(9)11 18(11)19 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 8).one() node.move_after("5") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 15, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 13, 14, _level + 2, 4, 1), (7, 16, 21, _level + 1, 1, 1), (8, 9, 12, _level + 2, 4, 1), (9, 10, 11, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all() ) def test_move_to_toplevel_where_much_trees_from_right_side(self): """ Move 20 after 1 initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level tree_id = 1 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level tree_id = 2 1 1(15)6 ^ 2 2(16)3 4(17)5 level tree_id = 3 1 1(12)16 _______________| | | 2 2(13)5 6(18)15 | ^ 3 3(14)4 7(19)10 11(21)14 | | 4 8(20)9 12(22)13 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 15).one() node.move_after("1") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 16, _level + 0, None, 3), (13, 2, 5, _level + 1, 12, 3), (14, 3, 4, _level + 2, 13, 3), (15, 1, 6, _level + 0, None, 2), (16, 2, 3, _level + 1, 15, 2), (17, 4, 5, _level + 1, 15, 2), (18, 6, 15, _level + 1, 12, 3), (19, 7, 10, _level + 2, 18, 3), (20, 8, 9, _level + 3, 19, 3), (21, 11, 14, _level + 2, 18, 3), (22, 12, 13, _level + 3, 21, 3) ], self.result.all() ) node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 20).one() node.move_after("1") """ level tree_id = 1 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level tree_id = 2 1 1(20)2 level tree_id = 3 1 1(15)6 ^ 2 2(16)3 4(17)5 level tree_id = 4 1 1(12)14 _______________| | | 2 2(13)5 6(18)13 | ^ 3 3(14)4 7(19)8 9(21)12 | 4 10(22)11 """ _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 14, _level + 0, None, 4), (13, 2, 5, _level + 1, 12, 4), (14, 3, 4, _level + 2, 13, 4), (15, 1, 6, _level + 0, None, 3), (16, 2, 3, _level + 1, 15, 3), (17, 4, 5, _level + 1, 15, 3), (18, 6, 13, _level + 1, 12, 4), (19, 7, 8, _level + 2, 18, 4), (20, 1, 2, _level + 0, None, 2), (21, 9, 12, _level + 2, 18, 4), (22, 10, 11, _level + 3, 21, 4) ], self.result.all() ) def test_move_to_toplevel(self): """ Move node(8) to top level after node(1) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 8 after 1 1 1(1)18 1(8)4 _______________|______________ | | | | | 2 2(2)5 6(4)11 12(7)17 2(9)3 | ^ | 3 3(3)4 7(5)8 9(6)10 13(10)16 | 4 14(11)15 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 8).one() node.move_after("1") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 18, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 17, _level + 1, 1, 1), (8, 1, 4, _level + 0, None, 2), (9, 2, 3, _level + 1, 8, 2), (10, 13, 16, _level + 2, 7, 1), (11, 14, 15, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 3), (13, 2, 5, _level + 1, 12, 3), (14, 3, 4, _level + 2, 13, 3), (15, 6, 11, _level + 1, 12, 3), (16, 7, 8, _level + 2, 15, 3), (17, 9, 10, _level + 2, 15, 3), (18, 12, 21, _level + 1, 12, 3), (19, 13, 16, _level + 2, 18, 3), (20, 14, 15, _level + 3, 19, 3), (21, 17, 20, _level + 2, 18, 3), (22, 18, 19, _level + 3, 21, 3) ], self.result.all() ) def test_move_to_toplevel2(self): """ Move node(8) to top level after node(1) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 8 after 1 1 1(1)18 1(8)4 _______________|______________ | | | | | 2 2(2)5 6(4)11 12(7)17 2(9)3 | ^ | 3 3(3)4 7(5)8 9(6)10 13(10)16 | 4 14(11)15 id lft rgt lvl parent tree """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 8).one() node.parent_id = None self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 18, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 17, _level + 1, 1, 1), (8, 1, 4, _level + 0, None, 3), (9, 2, 3, _level + 1, 8, 3), (10, 13, 16, _level + 2, 7, 1), (11, 14, 15, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all() ) def test_move_to_toplevel_big_subtree(self): """ Move node(7) (big subtree) to top level after node(1) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 7 to toplevel 1 1(1)12 1(7)10 _______________| ____|____ | | | | 2 2(2)5 6(4)11 2(8)5 6(10)9 | ^ | | 3 3(3)4 7(5)8 9(6)10 3(9)4 7(11)8 id lft rgt lvl parent tree """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 7).one() node.parent_id = None self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 12, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 1, 10, _level + 0, None, 3), (8, 2, 5, _level + 1, 7, 3), (9, 3, 4, _level + 2, 8, 3), (10, 6, 9, _level + 1, 7, 3), (11, 7, 8, _level + 2, 10, 3), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all() ) def test_move_after_between_tree(self): """ Move node(7) (big subtree) to top level after node(1) and before node(12) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Move 7 to toplevel 1 1(1)12 1(7)10 _______________| ____|____ | | | | 2 2(2)5 6(4)11 2(8)5 6(10)9 | ^ | | 3 3(3)4 7(5)8 9(6)10 3(9)4 7(11)8 id lft rgt lvl parent tree """ node = self.session.query(self.model).\ filter(self.model.get_pk_column() == 7).one() node.move_after("1") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 12, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 1, 10, _level + 0, None, 2), (8, 2, 5, _level + 1, 7, 2), (9, 3, 4, _level + 2, 8, 2), (10, 6, 9, _level + 1, 7, 2), (11, 7, 8, _level + 2, 10, 2), (12, 1, 22, _level + 0, None, 3), (13, 2, 5, _level + 1, 12, 3), (14, 3, 4, _level + 2, 13, 3), (15, 6, 11, _level + 1, 12, 3), (16, 7, 8, _level + 2, 15, 3), (17, 9, 10, _level + 2, 15, 3), (18, 12, 21, _level + 1, 12, 3), (19, 13, 16, _level + 2, 18, 3), (20, 14, 15, _level + 3, 19, 3), (21, 17, 20, _level + 2, 18, 3), (22, 18, 19, _level + 3, 21, 3) ], self.result.all() ) class MoveInside(object): def test_move_between_tree(self): """ Move node(4) to other tree inside node(15) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets tree1 1 1(1)16 _______________|_____________________ | | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13 level Nested sets tree2 1 1(12)28 ________________|_______________________ | | | 2 2(13)5 6(15)17 18(18)27 | ^ ^ 3 3(14)4 7(4)12 13(16)14 15(17)16 19(19)22 23(21)26 ^ | | 4 8(5)9 10(6)11 20(20)21 24(22)25 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 4).one() node.parent_id = 15 self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 16, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 7, 12, _level + 2, 15, 2), (5, 8, 9, _level + 3, 4, 2), (6, 10, 11, _level + 3, 4, 2), (7, 6, 15, _level + 1, 1, 1), (8, 7, 10, _level + 2, 7, 1), (9, 8, 9, _level + 3, 8, 1), (10, 11, 14, _level + 2, 7, 1), (11, 12, 13, _level + 3, 10, 1), (12, 1, 28, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 17, _level + 1, 12, 2), (16, 13, 14, _level + 2, 15, 2), (17, 15, 16, _level + 2, 15, 2), (18, 18, 27, _level + 1, 12, 2), (19, 19, 22, _level + 2, 18, 2), (20, 20, 21, _level + 3, 19, 2), (21, 23, 26, _level + 2, 18, 2), (22, 24, 25, _level + 3, 21, 2) ], self.result.all() ) def test_move_tree_to_another_tree(self): """ Move tree(2) inside tree(1) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Move tree2 to tree1 1 1(1)44 _______________|_________________________________ | | | 2 2(2)5 6(4)11 12(7)43 | ___|___ __|_____________________________________ | | | | | | 3 3(3)4 7(5)8 9(6)10 13(12)34 35(8)38 39(10)42 _______________|___________________ | | | | | 36(9)37 40(11)41 4 14(13)17 18(15)23 24(18)33 | ^ ^ 5 15(14)16 19(16)20 21(17)22 25(19)28 29(21)32 | | 6 26(20)27 30(22)31 """ # noqa node = self.session.query(self.model).\ filter(self.model.get_pk_column() == 12).one() node.parent_id = 7 self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 44, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 43, _level + 1, 1, 1), (8, 35, 38, _level + 2, 7, 1), (9, 36, 37, _level + 3, 8, 1), (10, 39, 42, _level + 2, 7, 1), (11, 40, 41, _level + 3, 10, 1), (12, 13, 34, _level + 2, 7, 1), (13, 14, 17, _level + 3, 12, 1), (14, 15, 16, _level + 4, 13, 1), (15, 18, 23, _level + 3, 12, 1), (16, 19, 20, _level + 4, 15, 1), (17, 21, 22, _level + 4, 15, 1), (18, 24, 33, _level + 3, 12, 1), (19, 25, 28, _level + 4, 18, 1), (20, 26, 27, _level + 5, 19, 1), (21, 29, 32, _level + 4, 18, 1), (22, 30, 31, _level + 5, 21, 1) ], self.result.all() ) def test_move_inside_function(self): """ For example move node(4) inside node(15) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Nested sets tree1 1 1(1)16 _______________|_____________________ | | 2 2(2)5 6(7)15 | ^ 3 3(3)4 7(8)10 11(10)14 | | 4 8(9)9 12(11)13 level Nested sets tree2 1 1(12)28 ________________|_______________________ | | | 2 2(13)5 6(15)17 18(18)27 | ^ ^ 3 3(14)4 7(4)12 13(16)14 15(17)16 19(19)22 23(21)26 ^ | | 4 8(5)9 10(6)11 20(20)21 24(22)25 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 4).one() node.move_inside("15") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 16, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 7, 12, _level + 2, 15, 2), (5, 8, 9, _level + 3, 4, 2), (6, 10, 11, _level + 3, 4, 2), (7, 6, 15, _level + 1, 1, 1), (8, 7, 10, _level + 2, 7, 1), (9, 8, 9, _level + 3, 8, 1), (10, 11, 14, _level + 2, 7, 1), (11, 12, 13, _level + 3, 10, 1), (12, 1, 28, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 17, _level + 1, 12, 2), (16, 13, 14, _level + 2, 15, 2), (17, 15, 16, _level + 2, 15, 2), (18, 18, 27, _level + 1, 12, 2), (19, 19, 22, _level + 2, 18, 2), (20, 20, 21, _level + 3, 19, 2), (21, 23, 26, _level + 2, 18, 2), (22, 24, 25, _level + 3, 21, 2) ], self.result.all() ) def test_tree_shorting(self): """ Try to move top level node(1) inside tree .. code:: level Nested sets example 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level Nested sets example __parent_id______________________ | | 1 1(1)22 | _______________|___________________ | | | | | 2 2(2)5 6(4)11 12(7)21 (X) | ^ ^ | 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | | | 4 14(9)15 18(11)19 | ↑ | ↑________| """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 1).one() node.parent_id = 11 self.session.add(node) _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 7, 8, _level + 2, 4, 1), (6, 9, 10, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all() ) def test_move_inside_to_the_same_parent_function(self): """ For example move node(6) inside node(4) initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree` .. code:: level Initial state 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(5)8 9(6)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 level move 6 inside 4 1 1(1)22 _______________|___________________ | | | 2 2(2)5 6(4)11 12(7)21 | ^ ^ 3 3(3)4 7(6)8 9(5)10 13(8)16 17(10)20 | | 4 14(9)15 18(11)19 """ node = self.session.query(self.model)\ .filter(self.model.get_pk_column() == 6).one() node.move_inside("4") _level = node.get_default_level() self.assertEqual( [ # id lft rgt lvl parent tree (1, 1, 22, _level + 0, None, 1), (2, 2, 5, _level + 1, 1, 1), (3, 3, 4, _level + 2, 2, 1), (4, 6, 11, _level + 1, 1, 1), (5, 9, 10, _level + 2, 4, 1), (6, 7, 8, _level + 2, 4, 1), (7, 12, 21, _level + 1, 1, 1), (8, 13, 16, _level + 2, 7, 1), (9, 14, 15, _level + 3, 8, 1), (10, 17, 20, _level + 2, 7, 1), (11, 18, 19, _level + 3, 10, 1), (12, 1, 22, _level + 0, None, 2), (13, 2, 5, _level + 1, 12, 2), (14, 3, 4, _level + 2, 13, 2), (15, 6, 11, _level + 1, 12, 2), (16, 7, 8, _level + 2, 15, 2), (17, 9, 10, _level + 2, 15, 2), (18, 12, 21, _level + 1, 12, 2), (19, 13, 16, _level + 2, 18, 2), (20, 14, 15, _level + 3, 19, 2), (21, 17, 20, _level + 2, 18, 2), (22, 18, 19, _level + 3, 21, 2) ], self.result.all() ) ================================================ FILE: sqlalchemy_mptt/tests/fixtures/tmp_tree.json ================================================ [ { "id": "1", "parent_id": null }, { "id": "2", "parent_id": "1" }, { "id": "3", "parent_id": "1" }, { "id": "4", "parent_id": "1" }, { "id": "5", "parent_id": "2" }, { "id": "6", "parent_id": "2" }, { "id": "7", "parent_id": "4" }, { "id": "8", "parent_id": "4" }, { "id": "9", "parent_id": "4" }, { "id": "10", "parent_id": "5" }, { "id": "11", "parent_id": "6" }, { "id": "12", "parent_id": "6" }, { "id": "13", "parent_id": "10" }, { "id": "14", "parent_id": "10" } ] ================================================ FILE: sqlalchemy_mptt/tests/fixtures/tree.json ================================================ [ { "parent_id": null, "id": "1" }, { "parent_id": "1", "id": "2", "visible": 1 }, { "parent_id": "2", "id": "3", "visible": 1 }, { "parent_id": "1", "id": "4", "visible": 1 }, { "parent_id": "4", "id": "5", "visible": 1 }, { "parent_id": "4", "id": "6", "visible": 1 }, { "parent_id": "1", "id": "7", "visible": 1 }, { "parent_id": "7", "id": "8", "visible": 1 }, { "parent_id": "8", "id": "9" }, { "parent_id": "7", "id": "10" }, { "parent_id": "10", "id": "11" }, { "parent_id": null, "id": "12" }, { "parent_id": "12", "id": "13", "tree_id": "2" }, { "parent_id": "13", "id": "14", "tree_id": "2" }, { "parent_id": "12", "id": "15", "tree_id": "2" }, { "parent_id": "15", "id": "16", "tree_id": "2" }, { "parent_id": "15", "id": "17", "tree_id": "2" }, { "parent_id": "12", "id": "18", "tree_id": "2" }, { "parent_id": "18", "id": "19", "tree_id": "2" }, { "parent_id": "19", "id": "20", "tree_id": "2" }, { "parent_id": "18", "id": "21", "tree_id": "2" }, { "parent_id": "21", "id": "22", "tree_id": "2" } ] ================================================ FILE: sqlalchemy_mptt/tests/fixtures/tree_3.json ================================================ [ { "parent_id": null, "id": 23, "tree_id": "3" }, { "parent_id": "23", "id": "24", "tree_id": "3" }, { "parent_id": "24", "id": "25" }, { "parent_id": "23", "id": "26", "tree_id": "3" }, { "parent_id": "26", "id": "27", "tree_id": "3" }, { "parent_id": "26", "id": 28, "tree_id": "3" }, { "parent_id": "23", "id": "29", "tree_id": "3" }, { "parent_id": "29", "id": "30", "tree_id": "3" }, { "parent_id": "30", "id": "31", "tree_id": "3" }, { "parent_id": "29", "id": "32", "tree_id": "3" }, { "parent_id": "32", "id": "33", "tree_id": "3" } ] ================================================ FILE: sqlalchemy_mptt/tests/test_events.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2014 uralbash # # Distributed under terms of the MIT license. """ test tree """ import unittest from sqlalchemy import Boolean, Column, Integer from sqlalchemy.event import contains from sqlalchemy_mptt.mixins import BaseNestedSets from sqlalchemy_mptt.sqlalchemy_compat import compat_layer from sqlalchemy_mptt.tests import DatabaseSetupMixin, TreeTestingMixin Base = compat_layer.declarative_base() class Tree(Base, BaseNestedSets): __tablename__ = "tree" id = Column(Integer, primary_key=True) visible = Column(Boolean) def __repr__(self): return "" % self.id class TreeWithCustomId(Base, BaseNestedSets): __tablename__ = "tree2" ppk = Column('idd', Integer, primary_key=True) visible = Column(Boolean) sqlalchemy_mptt_pk_name = 'ppk' def __repr__(self): return "" % self.ppk class TreeWithCustomLevel(Base, BaseNestedSets): __tablename__ = "tree_custom_level" id = Column(Integer, primary_key=True) visible = Column(Boolean) sqlalchemy_mptt_default_level = 0 def __repr__(self): return "" % self.id class TestTree(TreeTestingMixin, unittest.TestCase): base = Base model = Tree class TestTreeWithCustomId(TreeTestingMixin, unittest.TestCase): base = Base model = TreeWithCustomId class TestTreeWithCustomLevel(TreeTestingMixin, unittest.TestCase): base = Base model = TreeWithCustomLevel class Events(unittest.TestCase): def test_register(self): from sqlalchemy_mptt import tree_manager tree_manager.register_events() self.assertTrue( contains( BaseNestedSets, 'before_insert', tree_manager.before_insert ) ) self.assertTrue( contains( BaseNestedSets, 'before_update', tree_manager.before_update ) ) self.assertTrue( contains( BaseNestedSets, 'before_delete', tree_manager.before_delete ) ) def test_register_and_remove(self): from sqlalchemy_mptt import tree_manager tree_manager.register_events() tree_manager.register_events(remove=True) self.assertFalse( contains( Tree, 'before_insert', tree_manager.before_insert ) ) self.assertFalse( contains( Tree, 'before_update', tree_manager.before_update ) ) self.assertFalse( contains( Tree, 'before_delete', tree_manager.before_delete ) ) tree_manager.register_events() def test_remove(self): from sqlalchemy_mptt import tree_manager tree_manager.register_events(remove=True) self.assertFalse( contains( Tree, 'before_insert', tree_manager.before_insert ) ) self.assertFalse( contains( Tree, 'before_update', tree_manager.before_update ) ) self.assertFalse( contains( Tree, 'before_delete', tree_manager.before_delete ) ) tree_manager.register_events() 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 """ base = Base def test(self): root = Tree(id=0) child = Tree(id=1, parent_id=0) 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(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 tree_manager.register_events(remove=True) # Disable MPTT events _tree_id = 1 for node_id, parent_id in [(1, None), (2, 1), (3, 1), (4, 2)]: item = Tree( id=node_id, parent_id=parent_id, left=0, right=0, tree_id=_tree_id ) self.session.add(item) self.session.commit() tree_manager.register_events() # enabled MPTT events back Tree.rebuild_tree( self.session, _tree_id ) # rebuild lft, rgt value automatically ================================================ FILE: sqlalchemy_mptt/tests/test_inheritance.py ================================================ import unittest import sqlalchemy as sa from sqlalchemy_mptt.mixins import BaseNestedSets from sqlalchemy_mptt.sqlalchemy_compat import compat_layer from sqlalchemy_mptt.tests import (DatabaseSetupMixin, TreeTestingMixin, failures_expected_on) Base = compat_layer.declarative_base() class GenericTree(Base, BaseNestedSets): __tablename__ = "generic" ppk = sa.Column('idd', sa.Integer, primary_key=True) type = sa.Column(sa.Integer, default=0) visible = sa.Column(sa.Boolean) sqlalchemy_mptt_pk_name = 'ppk' __mapper_args__ = { 'polymorphic_identity': 0, 'polymorphic_on': type, } def __repr__(self): return "" % self.ppk class SpecializedTree(GenericTree): __tablename__ = "specialized" ppk = sa.Column( 'idd', sa.Integer, sa.ForeignKey(GenericTree.ppk), primary_key=True ) __mapper_args__ = { 'polymorphic_identity': 1, } __table_args__ = tuple() class TestTree(DatabaseSetupMixin, unittest.TestCase): base = Base def test_create_generic(self): self.session.add(GenericTree(ppk=1)) self.session.commit() tree = compat_layer.get(self.session, GenericTree, 1) self.assertEqual(tree.ppk, 1) self.assertEqual(tree.tree_id, 1) def test_create_spec(self): self.session.add(SpecializedTree(ppk=1)) self.session.commit() tree = compat_layer.get(self.session, SpecializedTree, 1) self.assertEqual(tree.ppk, 1) self.assertEqual(tree.tree_id, 1) def test_create_delete(self): parent = SpecializedTree(ppk=1) child1 = SpecializedTree(ppk=2, parent=parent) child2 = GenericTree(ppk=3, parent=parent) GenericTree(ppk=4, parent=child2) SpecializedTree(ppk=5, parent=child2) self.session.add(parent) self.session.commit() tree = compat_layer.get(self.session, SpecializedTree, 1) self.assertEqual(tree.ppk, 1) self.assertEqual(tree.tree_id, 1) self.session.delete(child1) self.session.commit() self.assertEqual(None, compat_layer.get(self.session, SpecializedTree, 2)) self.session.delete(child2) self.session.commit() self.assertEqual(None, compat_layer.get(self.session, SpecializedTree, 3)) self.assertEqual(None, compat_layer.get(self.session, SpecializedTree, 4)) self.assertEqual(None, compat_layer.get(self.session, SpecializedTree, 5)) class TestGenericTree(TreeTestingMixin, unittest.TestCase): base = Base model = GenericTree class TestSpecializedTree(TreeTestingMixin, unittest.TestCase): base = Base model = SpecializedTree @unittest.expectedFailure def test_rebuild(self): # This test will always fail on specialized classes. super().test_rebuild() Base2 = compat_layer.declarative_base() class BaseInheritance(Base2): __tablename__ = "base_inheritance" ppk = sa.Column('idd', sa.Integer, primary_key=True) type = sa.Column(sa.Integer, default=0) visible = sa.Column(sa.Boolean) __mapper_args__ = { 'polymorphic_identity': 0, 'polymorphic_on': type, } def __repr__(self): return "" % self.ppk class InheritanceTree(BaseInheritance, BaseNestedSets): __tablename__ = "inheriance_tree" ppk = sa.Column('idd', sa.Integer, sa.ForeignKey(BaseInheritance.ppk), primary_key=True) sqlalchemy_mptt_pk_name = 'ppk' __mapper_args__ = { 'polymorphic_identity': 1, } class TestInheritanceTree(TreeTestingMixin, unittest.TestCase): base = Base2 model = InheritanceTree @failures_expected_on(sqlalchemy_versions=['1.0', '1.1', '1.2', '1.3']) def test_rebuild(self): super().test_rebuild() ================================================ FILE: sqlalchemy_mptt/tests/test_mixins.py ================================================ #! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2014 uralbash # # Distributed under terms of the MIT license. """ test tree """ import unittest from sqlalchemy import Column, Integer from sqlalchemy_mptt.mixins import BaseNestedSets from sqlalchemy_mptt.sqlalchemy_compat import compat_layer Base = compat_layer.declarative_base() class Tree2(Base, BaseNestedSets): __tablename__ = "tree2" id = Column(Integer, primary_key=True) class TestMixin(unittest.TestCase): def test_mixin_parent_id(self): self.assertEqual( Tree2.parent_id.__class__.__name__, 'InstrumentedAttribute' ) ================================================ FILE: sqlalchemy_mptt/tests/test_stateful.py ================================================ # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright (c) 2025 Fayaz Yusuf Khan # # Distributed under terms of the MIT license. """Test cases written using Hypothesis stateful testing framework.""" 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() class Tree(Base, BaseNestedSets): __tablename__ = "tree" id = Column(Integer, primary_key=True) visible = Column(Boolean) def __repr__(self): return "" % self.id 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.setUp() def teardown(self): super().teardown() self.tearDown() node = Bundle('node') @rule(target=node, visible=st.none() | st.booleans()) def add_root_node(self, visible): node = Tree(visible=visible) self.session.add(node) self.session.commit() assert node.left < node.right return node @rule(node=consumes(node)) def delete_node(self, node): # Consume all descendants of the node for name, value in list(self.names_to_values.items()): if value not in self.session or node.is_ancestor_of(value): for var_reference in self.bundles["node"][:]: if var_reference.name == name: self.bundles["node"].remove(var_reference) # Remove the object as well for garbage collection del self.names_to_values[name] self.session.delete(node) self.session.commit() @rule(target=node, node=node, visible=st.none() | st.booleans()) def add_child(self, node, visible): # Avoid cascade_backrefs here since it is deprecated. child = Tree(visible=visible) node.children.append(child) self.session.commit() assert node.left < child.left < child.right < node.right return child @invariant() def check_get_tree_integrity(self): """Check that get_tree response is valid after each operation.""" response = Tree.get_tree( self.session, query=lambda x: x.execution_options(populate_existing=True).options(joinedload(Tree.children))) assert isinstance(response, list) for node in response: validate_get_tree_node(node) @invariant() def check_get_tree_with_custom_query(self): """Check that get_tree response is valid with custom queries.""" for visible in [None, True, False]: response = Tree.get_tree( self.session, query=lambda x: x.filter_by(visible=visible) .execution_options(populate_existing=True).options(joinedload(Tree.children))) assert isinstance(response, list) for node in response: validate_get_tree_node_for_custom_query(node) def validate_get_tree_node(node_response, level=1): """Validate the structure of a node response from get_tree.""" node = node_response['node'] assert node.level == level if len(node.children): assert 'children' in node_response.keys() children_response = node_response['children'] assert len(node.children) == len(children_response) for child, child_response in zip(node.children, children_response): assert child == child_response['node'] validate_get_tree_node(child_response, level=level + 1) def validate_get_tree_node_for_custom_query(node_response): """Validate the structure of a node response from get_tree with custom query.""" node = node_response['node'] if 'children' in node_response.keys(): for child_response in node_response['children']: assert child_response['node'].parent == node validate_get_tree_node_for_custom_query(child_response) # Export the stateful test case TestTreeStates = TreeStateMachine.TestCase TestTreeStates.settings = settings( max_examples=75, stateful_step_count=25, suppress_health_check=[HealthCheck.too_slow] ) ================================================ FILE: test.sh ================================================ #! /bin/bash # # test.sh # Copyright (C) 2015 uralbash # # Distributed under terms of the MIT license. # RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color PROJECT_NAME='sqlalchemy_mptt' RST_FILES=`find . -name "*.rst" -printf "%p "` RST_CHECK=$(rstcheck $RST_FILES \ --ignore-directives code-block \ --report 2 3>&1 1>&2 2>&3 | tee >(cat - >&2)) # fd=STDERR_FILENO FLAKE8=$(flake8 ./$PROJECT_NAME/) echo -e "${RED}" # if [ -n "$RST_CHECK" ] || if [ -n "$FLAKE8" ] then echo -e "RST_CHECK: ${RST_CHECK:-OK}" echo -e "FLAKE8: ${FLAKE8:-OK}" exit 1 else echo -e "${GREEN}OK!" fi echo -e "${NC}"