├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ ├── requirements.txt │ └── test.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── README.md ├── derive ├── Cargo.toml └── lib.rs ├── docs ├── .gitignore ├── Makefile ├── _images │ └── logo.png ├── _static │ ├── css │ │ └── main.css │ ├── js │ │ └── custom-icon.js │ └── json │ │ └── switcher.json ├── api │ ├── abc.rst │ ├── doc.rst │ ├── exceptions.rst │ ├── header.rst │ ├── id.rst │ ├── index.rst │ ├── pv.rst │ ├── syn.rst │ ├── term.rst │ ├── typedef.rst │ └── xref.rst ├── conf.py ├── examples │ ├── descriptions.ipynb │ ├── graph.ipynb │ ├── graph.png │ ├── index.rst │ └── obsolete.ipynb ├── guide │ ├── about.rst │ ├── changes.md │ ├── index.rst │ ├── install.rst │ └── publications.rst ├── index.rst ├── make.bat └── requirements.txt ├── pyproject.toml ├── src ├── build.rs ├── built.rs ├── date.rs ├── error.rs ├── iter.rs ├── lib.rs ├── macros.rs ├── py │ ├── abc.rs │ ├── doc.rs │ ├── exceptions.rs │ ├── header │ │ ├── clause.rs │ │ ├── frame.rs │ │ └── mod.rs │ ├── id.rs │ ├── instance │ │ ├── clause.rs │ │ ├── frame.rs │ │ └── mod.rs │ ├── mod.rs │ ├── pv.rs │ ├── syn.rs │ ├── term │ │ ├── clause.rs │ │ ├── frame.rs │ │ └── mod.rs │ ├── typedef │ │ ├── clause.rs │ │ ├── frame.rs │ │ └── mod.rs │ └── xref.rs ├── pyfile.rs └── utils.rs └── tests ├── __init__.py ├── common.py ├── data ├── .gitignore ├── ms.obo ├── pato.json └── plana.obo ├── test_doc.py ├── test_doctests.py ├── test_fastobo.py ├── test_header.py ├── test_id.py ├── test_pv.py ├── test_term.py ├── test_typedef.py ├── test_xref.py └── unittest.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: syn 10 | versions: 11 | - 1.0.60 12 | - 1.0.61 13 | - 1.0.62 14 | - 1.0.63 15 | - 1.0.64 16 | - 1.0.67 17 | - 1.0.68 18 | - 1.0.69 19 | - 1.0.70 20 | - dependency-name: libc 21 | versions: 22 | - 0.2.84 23 | - 0.2.85 24 | - 0.2.86 25 | - 0.2.87 26 | - 0.2.88 27 | - 0.2.89 28 | - 0.2.90 29 | - 0.2.91 30 | - 0.2.93 31 | - dependency-name: quote 32 | versions: 33 | - 1.0.8 34 | - 1.0.9 35 | - dependency-name: url 36 | versions: 37 | - 2.2.0 38 | - dependency-name: proc-macro2 39 | versions: 40 | - 1.0.24 41 | - dependency-name: fastobo-graphs 42 | versions: 43 | - 0.4.2 44 | - dependency-name: fastobo 45 | versions: 46 | - 0.12.0 47 | - dependency-name: built 48 | versions: 49 | - 0.4.4 50 | - dependency-name: pyo3 51 | versions: 52 | - 0.13.1 53 | - 0.13.2 54 | - package-ecosystem: pip 55 | directory: "/" 56 | schedule: 57 | interval: daily 58 | open-pull-requests-limit: 10 59 | ignore: 60 | - dependency-name: fastobo 61 | versions: 62 | - "> 0.9.0, < 0.10" 63 | - dependency-name: ipykernel 64 | versions: 65 | - "> 5.3, < 6" 66 | - dependency-name: ipython 67 | versions: 68 | - "> 7.15, < 8" 69 | - dependency-name: sphinx 70 | versions: 71 | - "> 3.1, < 4" 72 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | 10 | wheel-linux-aarch64: 11 | name: Build Linux wheels (Aarch64) 12 | runs-on: ubuntu-22.04-arm 13 | if: "startsWith(github.ref, 'refs/tags/v')" 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Build manylinux wheels 17 | uses: pypa/cibuildwheel@v2.21.3 18 | env: 19 | CIBW_ARCHS: aarch64 20 | CIBW_BUILD: 'cp*-manylinux_aarch64' 21 | with: 22 | output-dir: dist 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | name: wheels-manylinux_aarch64 26 | path: dist/* 27 | 28 | wheel-linux-x86_64: 29 | name: Build Linux wheels (x86-64) 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Build manylinux wheels 34 | uses: pypa/cibuildwheel@v2.21.3 35 | env: 36 | CIBW_ARCHS: x86_64 37 | CIBW_BUILD: 'cp*-manylinux_x86_64' 38 | with: 39 | output-dir: dist 40 | - uses: actions/upload-artifact@v4 41 | with: 42 | name: wheels-manylinux_x86_64 43 | path: dist/* 44 | 45 | wheel-macos-x86_64: 46 | name: Build MacOS wheels (x86-64) 47 | runs-on: macOS-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: dtolnay/rust-toolchain@stable 51 | with: 52 | targets: x86_64-apple-darwin 53 | - name: Build manylinux wheels 54 | uses: pypa/cibuildwheel@v2.21.3 55 | env: 56 | CIBW_ARCHS: x86_64 57 | CIBW_BUILD: 'cp*-macosx_x86_64' 58 | CIBW_ENVIRONMENT: MACOSX_DEPLOYMENT_TARGET=12.0 59 | CIBW_TEST_SKIP: "*" 60 | with: 61 | output-dir: dist 62 | - uses: actions/upload-artifact@v4 63 | with: 64 | name: wheels-macosx_x86_64 65 | path: dist/* 66 | 67 | wheel-macos-aarch64: 68 | name: Build MacOS wheels (Aarch64) 69 | runs-on: macOS-latest 70 | steps: 71 | - uses: actions/checkout@v4 72 | - uses: dtolnay/rust-toolchain@stable 73 | with: 74 | targets: aarch64-apple-darwin 75 | - name: Build manylinux wheels 76 | uses: pypa/cibuildwheel@v2.21.3 77 | env: 78 | CIBW_ARCHS: arm64 79 | CIBW_BUILD: 'cp*-macosx_arm64' 80 | with: 81 | output-dir: dist 82 | - uses: actions/upload-artifact@v4 83 | with: 84 | name: wheels-macosx_arm64 85 | path: dist/* 86 | 87 | wheel-win32-x86_64: 88 | name: Build Windows wheels (x86-64) 89 | runs-on: windows-latest 90 | steps: 91 | - uses: actions/checkout@v4 92 | - uses: dtolnay/rust-toolchain@stable 93 | - name: Build manylinux wheels 94 | uses: pypa/cibuildwheel@v2.21.3 95 | env: 96 | CIBW_ARCHS: AMD64 97 | CIBW_BUILD: 'cp*-win_amd64' 98 | with: 99 | output-dir: dist 100 | - uses: actions/upload-artifact@v4 101 | with: 102 | name: wheels-win_amd64 103 | path: dist/* 104 | 105 | sdist: 106 | runs-on: ubuntu-latest 107 | name: Build source distribution 108 | steps: 109 | - uses: actions/checkout@v4 110 | with: 111 | submodules: true 112 | - name: Set up Python 3.13 113 | uses: actions/setup-python@v5 114 | with: 115 | python-version: 3.13 116 | - name: Install CI requirements 117 | run: python -m pip install -U build maturin 118 | - name: Build source distribution without vendored sources 119 | run: python -m build -s . -n 120 | - uses: actions/upload-artifact@v4 121 | with: 122 | name: sdist 123 | path: dist/* 124 | 125 | upload: 126 | environment: PyPI 127 | runs-on: ubuntu-latest 128 | name: Upload 129 | if: "startsWith(github.ref, 'refs/tags/v')" 130 | permissions: 131 | # IMPORTANT: this permission is mandatory for trusted publishing 132 | id-token: write 133 | needs: 134 | - sdist 135 | - wheel-linux-aarch64 136 | - wheel-linux-x86_64 137 | - wheel-macos-aarch64 138 | - wheel-macos-x86_64 139 | - wheel-win32-x86_64 140 | steps: 141 | - name: Download source distribution 142 | uses: actions/download-artifact@v4 143 | with: 144 | name: sdist 145 | path: dist/ 146 | merge-multiple: true 147 | - name: Download wheel distributions 148 | uses: actions/download-artifact@v4 149 | with: 150 | pattern: wheels-* 151 | path: dist/ 152 | merge-multiple: true 153 | - name: Publish distributions to PyPI 154 | if: startsWith(github.ref, 'refs/tags/v') 155 | uses: pypa/gh-action-pypi-publish@release/v1 156 | 157 | release: 158 | environment: GitHub Releases 159 | runs-on: ubuntu-latest 160 | if: "startsWith(github.ref, 'refs/tags/v')" 161 | name: Release 162 | needs: upload 163 | permissions: write-all 164 | steps: 165 | - name: Checkout code 166 | uses: actions/checkout@v4 167 | - name: Release a Changelog 168 | uses: rasmus-saks/release-a-changelog-action@v1.2.0 169 | with: 170 | github-token: '${{ secrets.GITHUB_TOKEN }}' 171 | -------------------------------------------------------------------------------- /.github/workflows/requirements.txt: -------------------------------------------------------------------------------- 1 | maturin ~=1.2 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | 9 | test_linux: 10 | name: Test (Linux) 11 | runs-on: ubuntu-latest 12 | env: 13 | OS: Linux 14 | strategy: 15 | matrix: 16 | include: 17 | - python-version: 3.9 18 | python-release: v3.9 19 | python-impl: CPython 20 | - python-version: "3.10" 21 | python-release: v3.10 22 | python-impl: CPython 23 | - python-version: "3.11" 24 | python-release: v3.11 25 | python-impl: CPython 26 | - python-version: "3.12" 27 | python-release: v3.12 28 | python-impl: CPython 29 | - python-version: "3.13" 30 | python-release: v3.13 31 | python-impl: CPython 32 | - python-version: pypy-3.9 33 | python-release: v3.9 34 | python-impl: PyPy 35 | - python-version: pypy-3.10 36 | python-release: v3.10 37 | python-impl: PyPy 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v3 41 | - name: Setup Python ${{ matrix.python-version }} 42 | uses: actions/setup-python@v5 43 | with: 44 | python-version: ${{ matrix.python-version }} 45 | - name: Setup Rust 46 | uses: dtolnay/rust-toolchain@stable 47 | - name: Update CI requirements 48 | run: python -m pip install -U -r .github/workflows/requirements.txt 49 | - name: Build Rust extension 50 | run: python -m pip install --no-build-isolation -e . -vv 51 | - name: Test Rust extension 52 | run: python -m unittest discover -vv 53 | 54 | test_osx: 55 | name: Test (OSX) 56 | runs-on: macos-latest 57 | env: 58 | OS: OSX 59 | strategy: 60 | matrix: 61 | include: 62 | - python-version: 3.9 63 | python-release: v3.9 64 | python-impl: CPython 65 | - python-version: "3.10" 66 | python-release: "v3.10" 67 | python-impl: CPython 68 | - python-version: "3.11" 69 | python-release: "v3.11" 70 | python-impl: CPython 71 | - python-version: "3.12" 72 | python-release: "v3.12" 73 | python-impl: CPython 74 | - python-version: "3.13" 75 | python-release: "v3.13" 76 | python-impl: CPython 77 | - python-version: pypy-3.9 78 | python-release: v3.9 79 | python-impl: PyPy 80 | - python-version: pypy-3.10 81 | python-release: v3.10 82 | python-impl: PyPy 83 | steps: 84 | - name: Checkout code 85 | uses: actions/checkout@v3 86 | - name: Setup Python ${{ matrix.python-version }} 87 | uses: actions/setup-python@v5 88 | with: 89 | python-version: ${{ matrix.python-version }} 90 | - name: Setup Rust 91 | uses: dtolnay/rust-toolchain@stable 92 | - name: Update CI requirements 93 | run: python -m pip install -U -r .github/workflows/requirements.txt 94 | - name: Build Rust extension 95 | run: python -m pip install --no-build-isolation -e . -vv 96 | - name: Test Rust extension 97 | run: python -m unittest discover -vv 98 | 99 | coverage: 100 | name: Coverage 101 | runs-on: ubuntu-latest 102 | strategy: 103 | matrix: 104 | include: 105 | - python-version: "3.13" 106 | python-release: "v3.13" 107 | python-impl: CPython 108 | steps: 109 | - name: Checkout code 110 | uses: actions/checkout@v3 111 | - name: Set up Python ${{ matrix.python-version }} 112 | uses: actions/setup-python@v5 113 | with: 114 | python-version: ${{ matrix.python-version }} 115 | - name: Setup Rust 116 | uses: dtolnay/rust-toolchain@stable 117 | - name: Install tarpaulin 118 | run: cargo install cargo-tarpaulin 119 | - name: Measure code coverage 120 | run: cargo tarpaulin -v --out Xml --ciserver github-actions 121 | - name: Upload coverage statistics 122 | uses: codecov/codecov-action@v2 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/rust,python 3 | # Edit at https://www.gitignore.io/?templates=rust,python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | 67 | # Flask stuff: 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | 129 | ### Rust ### 130 | # Generated by Cargo 131 | # will have compiled files and executables 132 | /target/ 133 | .cargo 134 | 135 | # These are backup files generated by rustfmt 136 | **/*.rs.bk 137 | 138 | # cargo vendor 139 | .cargo/config.toml 140 | crates/ 141 | 142 | # End of https://www.gitignore.io/api/rust,python 143 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | rust: "1.78" 13 | 14 | # VCS submodules configuration. 15 | submodules: 16 | include: all 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # Optional but recommended, declare the Python requirements required 23 | # to build your documentation 24 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 25 | python: 26 | install: 27 | - requirements: docs/requirements.txt 28 | - method: pip 29 | path: . 30 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 Martin Larralde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["derive"] 3 | 4 | [package] 5 | name = "fastobo-py" 6 | version = "0.13.0" 7 | authors = ["Martin Larralde "] 8 | license = "MIT" 9 | publish = false 10 | build = "src/build.rs" 11 | edition = '2021' 12 | 13 | [lib] 14 | crate-type = ["cdylib", "rlib"] 15 | name = "fastobo_py" 16 | doctest = false 17 | 18 | [[test]] 19 | name = "unittest" 20 | path = "tests/unittest.rs" 21 | harness = false 22 | 23 | [dev-dependencies] 24 | lazy_static = "1.4.0" 25 | 26 | [build-dependencies.built] 27 | version = "0.7.6" 28 | features = ["chrono", "cargo-lock"] 29 | 30 | [dependencies] 31 | libc = "0.2.70" 32 | pyo3-built = "0.6.0" 33 | [dependencies.pyo3] 34 | version = "0.23.4" 35 | [dependencies.fastobo] 36 | version = "0.15.4" 37 | features = ["threading", "smartstring"] 38 | [dependencies.fastobo-graphs] 39 | version = "0.4.8" 40 | features = ["obo"] 41 | [dependencies.fastobo-owl] 42 | version = "0.3.2" 43 | [dependencies.horned-owl] 44 | version = "1.0" 45 | [dependencies.fastobo-py-derive-internal] 46 | version = "0.13.0" 47 | path = "./derive" 48 | 49 | [features] 50 | default = [] 51 | extension-module = ["pyo3/extension-module"] 52 | nightly = ["pyo3/nightly"] 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `fastobo-py` [![Star me](https://img.shields.io/github/stars/fastobo/fastobo-py.svg?style=social&label=Star&maxAge=3600)](https://github.com/fastobo/fastobo-py/stargazers) 2 | 3 | *Faultless AST for Open Biomedical Ontologies in Python.* 4 | 5 | [![Actions](https://img.shields.io/github/actions/workflow/status/fastobo/fastobo-py/test.yml?branch=master&style=flat-square&maxAge=600)](https://github.com/fastobo/fastobo-py/actions) 6 | [![Codecov](https://img.shields.io/codecov/c/gh/fastobo/fastobo-py/master.svg?style=flat-square&maxAge=600)](https://codecov.io/gh/fastobo/fastobo-py) 7 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square&maxAge=2678400)](https://choosealicense.com/licenses/mit/) 8 | [![Source](https://img.shields.io/badge/source-GitHub-303030.svg?maxAge=2678400&style=flat-square)](https://github.com/fastobo/fastobo-py/) 9 | [![PyPI](https://img.shields.io/pypi/v/fastobo.svg?style=flat-square&maxAge=600)](https://pypi.org/project/fastobo) 10 | [![Wheel](https://img.shields.io/pypi/wheel/fastobo.svg?style=flat-square&maxAge=2678400)](https://pypi.org/project/fastobo/#files) 11 | [![Conda](https://img.shields.io/conda/vn/conda-forge/fastobo?style=flat-square&maxAge=3600)](https://anaconda.org/conda-forge/fastobo) 12 | [![Python Versions](https://img.shields.io/pypi/pyversions/fastobo.svg?style=flat-square&maxAge=600)](https://pypi.org/project/fastobo/#files) 13 | [![PyPI - Implementation](https://img.shields.io/pypi/implementation/fastobo.svg?style=flat-square&maxAge=600)](https://pypi.org/project/fastobo/#files) 14 | [![Changelog](https://img.shields.io/badge/keep%20a-changelog-8A0707.svg?maxAge=2678400&style=flat-square)](https://github.com/fastobo/fastobo-py/blob/master/CHANGELOG.md) 15 | [![Documentation](https://img.shields.io/readthedocs/fastobo.svg?maxAge=3600&style=flat-square)](https://fastobo.readthedocs.io/) 16 | [![GitHub issues](https://img.shields.io/github/issues/fastobo/fastobo-py.svg?style=flat-square&maxAge=600)](https://github.com/fastobo/fastobo-py/issues) 17 | [![DOI](https://img.shields.io/badge/doi-10.7490%2Ff1000research.1117405.1-brightgreen?style=flat-square&maxAge=31536000)](https://f1000research.com/posters/8-1500) 18 | [![Downloads](https://img.shields.io/pypi/dm/fastobo?style=flat-square&color=303f9f&maxAge=86400&label=downloads)](https://pepy.tech/project/fastobo) 19 | 20 | 21 | ## Overview 22 | 23 | [`fastobo`](https://crates.io/crates/fastobo) is a Rust library implementing a 24 | reliable parser for the OBO file format 1.4. This extension module exports 25 | idiomatic Python bindings that can be used to load, edit and serialize ontologies 26 | in the OBO format. 27 | 28 | 29 | ## Installation 30 | 31 | If your platform has no pre-built binaries available, you will need to have the Rust 32 | compiler installed. See the [documentation on `rust-lang.org`](https://forge.rust-lang.org/other-installation-methods.html) 33 | to learn how to install Rust on your machine. 34 | 35 | Installation is then supported through `pip`: 36 | ```console 37 | $ pip install fastobo --user 38 | ``` 39 | 40 | 41 | ## Usage 42 | 43 | An `OboDoc` instance can be instantiated from a path or from a binary file handle 44 | using the `fastobo.load` function, or from a string using the `fastobo.loads` function. 45 | 46 | ```python 47 | import fastobo 48 | obodoc = fastobo.load("../data/ms.obo") 49 | ``` 50 | 51 | Loading from a `gzip` file is supported: 52 | ```python 53 | import fastobo 54 | import gzip 55 | gzdoc = fastobo.load(gzip.open("../data/cl.obo.gz")) 56 | ``` 57 | 58 | *Comments can be parsed but neither edited nor serialized, because of a limitation 59 | with `pyo3` (the library used to generate the Python bindings). They are supported 60 | in the Rust version of `fastobo`.* 61 | 62 | ## Feedback 63 | 64 | Found a bug ? Have an enhancement request ? Head over to the 65 | [GitHub issue tracker](https://github.com/fastobo/fastobo-py/issues) of the project if 66 | you need to report or ask something. If you are filling in on a bug, please include as much 67 | information as you can about the issue, and try to recreate the same bug in a simple, easily 68 | reproducible situation. 69 | 70 | The following people have contributed to this project: 71 | 72 | - Alex Henrie ([@alexhenrie](https://github.com/alexhenrie)) 73 | - Patrick Kalita ([@pkalita-lbl](https://github.com/pkalita-lbl)) 74 | 75 | 76 | ## About 77 | 78 | This project was developed by [Martin Larralde](https://github.com/althonos) 79 | as part of a Master's Degree internship in the [BBOP team](http://berkeleybop.org/) of the 80 | [Lawrence Berkeley National Laboratory](https://www.lbl.gov/), under the supervision of 81 | [Chris Mungall](http://biosciences.lbl.gov/profiles/chris-mungall/). Cite this project as: 82 | 83 | *Larralde M.* **Developing Python and Rust libraries to improve the ontology ecosystem** 84 | *\[version 1; not peer reviewed\].* F1000Research 2019, 8(ISCB Comm J):1500 (poster) 85 | ([https://doi.org/10.7490/f1000research.1117405.1](https://doi.org/10.7490/f1000research.1117405.1)) 86 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fastobo-py-derive-internal" 3 | version = "0.13.0" 4 | authors = ["Martin Larralde "] 5 | edition = "2018" 6 | publish = false 7 | workspace = ".." 8 | 9 | [lib] 10 | proc-macro = true 11 | path = "lib.rs" 12 | 13 | [dependencies.syn] 14 | version = "2.0" 15 | default-features = false 16 | features = ["derive", "clone-impls", "parsing", "printing", "proc-macro", "full"] 17 | [dependencies.quote] 18 | version = "1.0" 19 | [dependencies.proc-macro2] 20 | version = "1.0" 21 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | changes.md 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastobo/fastobo-py/18da7270faf173c77bb42ec9c63b0bb1331952e7/docs/_images/logo.png -------------------------------------------------------------------------------- /docs/_static/css/main.css: -------------------------------------------------------------------------------- 1 | p { 2 | text-align: justify; 3 | } 4 | 5 | /* a.reference strong { 6 | font-weight: bold; 7 | font-size: 90%; 8 | color: #c7254e; 9 | box-sizing: border-box; 10 | font-family: Menlo,Monaco,Consolas,"Courier New",monospace; 11 | } */ 12 | 13 | .field-list a.reference { 14 | font-weight: bold; 15 | font-size: 90%; 16 | color: #c7254e; 17 | box-sizing: border-box; 18 | font-family: Menlo,Monaco,Consolas,"Courier New",monospace; 19 | } 20 | -------------------------------------------------------------------------------- /docs/_static/js/custom-icon.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Set a custom icon for pypi as it's not available in the fa built-in brands 3 | */ 4 | FontAwesome.library.add( 5 | (faListOldStyle = { 6 | prefix: "fa-custom", 7 | iconName: "pypi", 8 | icon: [ 9 | 17.313, // viewBox width 10 | 19.807, // viewBox height 11 | [], // ligature 12 | "e001", // unicode codepoint - private use area 13 | "m10.383 0.2-3.239 1.1769 3.1883 1.1614 3.239-1.1798zm-3.4152 1.2411-3.2362 1.1769 3.1855 1.1614 3.2369-1.1769zm6.7177 0.00281-3.2947 1.2009v3.8254l3.2947-1.1988zm-3.4145 1.2439-3.2926 1.1981v3.8254l0.17548-0.064132 3.1171-1.1347zm-6.6564 0.018325v3.8247l3.244 1.1805v-3.8254zm10.191 0.20931v2.3137l3.1777-1.1558zm3.2947 1.2425-3.2947 1.1988v3.8254l3.2947-1.1988zm-8.7058 0.45739c0.00929-1.931e-4 0.018327-2.977e-4 0.027485 0 0.25633 0.00851 0.4263 0.20713 0.42638 0.49826 1.953e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36226 0.13215-0.65608-0.073306-0.65613-0.4588-6.28e-5 -0.38556 0.2938-0.80504 0.65613-0.93662 0.068422-0.024919 0.13655-0.038114 0.20156-0.039466zm5.2913 0.78369-3.2947 1.1988v3.8247l3.2947-1.1981zm-10.132 1.239-3.2362 1.1769 3.1883 1.1614 3.2362-1.1769zm6.7177 0.00213-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2439-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.016195v3.8275l3.244 1.1805v-3.8254zm16.9 0.21143-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2432-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.019027v3.8247l3.244 1.1805v-3.8254zm13.485 1.4497-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm2.4018 0.38127c0.0093-1.83e-4 0.01833-3.16e-4 0.02749 0 0.25633 0.0085 0.4263 0.20713 0.42638 0.49826 1.97e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36188 0.1316-0.65525-0.07375-0.65542-0.4588-1.95e-4 -0.38532 0.29328-0.80469 0.65542-0.93662 0.06842-0.02494 0.13655-0.03819 0.20156-0.03947zm-5.8142 0.86403-3.244 1.1805v1.4201l3.244 1.1805z", // svg path (https://simpleicons.org/icons/pypi.svg) 14 | ], 15 | }), 16 | ); 17 | 18 | FontAwesome.library.add( 19 | (faListOldStyle = { 20 | prefix: "fa-custom", 21 | iconName: "sword", 22 | icon: [ 23 | 256, // viewBox width 24 | 256, // viewBox height 25 | [], // ligature 26 | "e002", // unicode codepoint - private use area 27 | "M221.65723,34.34326A8.00246,8.00246,0,0,0,216,32h-.02539l-63.79883.20117A8.00073,8.00073,0,0,0,146.0332,35.106L75.637,120.32275,67.31348,111.999A16.02162,16.02162,0,0,0,44.68555,112L32.001,124.68555A15.99888,15.99888,0,0,0,32,147.31348l20.88672,20.88769L22.94531,198.14258a16.01777,16.01777,0,0,0,.001,22.62695l12.28418,12.28418a16.00007,16.00007,0,0,0,22.62793,0L87.79883,203.1123,108.68652,224.001A16.02251,16.02251,0,0,0,131.31445,224L143.999,211.31445A15.99888,15.99888,0,0,0,144,188.68652l-8.32324-8.32324,85.21679-70.39648a8.00125,8.00125,0,0,0,2.90528-6.14258L224,40.02539A8.001,8.001,0,0,0,221.65723,34.34326Zm-13.84668,65.67822-83.49829,68.97706L111.314,156l54.34327-54.34277a8.00053,8.00053,0,0,0-11.31446-11.31446L100,144.686,87.00195,131.6875,155.97852,48.189l51.99609-.16357Z", // svg path (https://simpleicons.org/icons/pypi.svg) 28 | ], 29 | }), 30 | ); 31 | 32 | FontAwesome.library.add( 33 | (faListOldStyle = { 34 | prefix: "fa-custom", 35 | iconName: "knife", 36 | icon: [ 37 | 256, // viewBox width 38 | 256, // viewBox height 39 | [], // ligature 40 | "e003", // unicode codepoint - private use area 41 | "M231.79883,32.2002a28.05536,28.05536,0,0,0-39.667.06933L18.27441,210.41211a8,8,0,0,0,3.92676,13.38281,155.06019,155.06019,0,0,0,34.957,4.00293c33.4209-.001,66.877-10.86914,98.32813-32.1748,31.74512-21.50391,50.14551-45.79981,50.91406-46.82325a8.00114,8.00114,0,0,0-.74316-10.457L186.919,119.60547l44.97753-47.90332A28.03445,28.03445,0,0,0,231.79883,32.2002ZM189.207,144.52148a225.51045,225.51045,0,0,1-43.10351,38.13184c-34.46973,23.23145-69.999,32.665-105.83887,28.13477l106.29492-108.915,23.30176,23.30175q.208.22852.43847.44434l.082.07617Z", // svg path (https://simpleicons.org/icons/pypi.svg) 42 | ], 43 | }), 44 | ); 45 | -------------------------------------------------------------------------------- /docs/_static/json/switcher.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "v0.13 (latest)", 4 | "version": "0.13.0", 5 | "url": "https://fastobo.readthedocs.io/en/v0.13.0/" 6 | }, 7 | { 8 | "name": "v0.12", 9 | "version": "0.12.3", 10 | "url": "https://fastobo.readthedocs.io/en/v0.12.3/" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /docs/api/abc.rst: -------------------------------------------------------------------------------- 1 | Abstract Base Classes 2 | ===================== 3 | 4 | .. currentmodule:: fastobo.abc 5 | .. automodule:: fastobo.abc 6 | 7 | Frame 8 | ----- 9 | 10 | .. autoclass:: AbstractFrame(collections.abc.MutableSeq) 11 | :members: 12 | :special-members: 13 | 14 | .. autoclass:: AbstractEntityFrame(AbstractFrame) 15 | :members: 16 | :special-members: 17 | 18 | 19 | Clauses 20 | ------- 21 | 22 | .. autoclass:: AbstractClause 23 | :members: 24 | :special-members: 25 | 26 | .. autoclass:: AbstractEntityClause(AbstractClause) 27 | :members: 28 | :special-members: 29 | -------------------------------------------------------------------------------- /docs/api/doc.rst: -------------------------------------------------------------------------------- 1 | Document 2 | ======== 3 | 4 | .. currentmodule:: fastobo.doc 5 | .. automodule:: fastobo.doc 6 | 7 | 8 | ``OboDoc`` 9 | ---------- 10 | 11 | .. autoclass:: OboDoc 12 | :members: 13 | :special-members: 14 | -------------------------------------------------------------------------------- /docs/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | .. currentmodule:: fastobo.exceptions 5 | .. automodule:: fastobo.exceptions 6 | 7 | 8 | Cardinality Errors 9 | ------------------ 10 | 11 | .. autoexception:: MissingClauseError 12 | 13 | 14 | .. autoexception:: DuplicateClausesError 15 | 16 | 17 | .. autoexception:: SingleClauseError 18 | 19 | 20 | Threading Errors 21 | ---------------- 22 | 23 | .. autoexception:: DisconnectedChannelError 24 | -------------------------------------------------------------------------------- /docs/api/header.rst: -------------------------------------------------------------------------------- 1 | Header 2 | ====== 3 | 4 | Frame 5 | ----- 6 | 7 | .. currentmodule:: fastobo.header 8 | .. automodule:: fastobo.header 9 | 10 | 11 | .. autoclass:: HeaderFrame(AbstractFrame) 12 | :members: 13 | 14 | 15 | Clauses 16 | ------- 17 | 18 | .. autoclass:: BaseHeaderClause(AbstractClause) 19 | :members: 20 | 21 | .. autoclass:: FormatVersionClause(BaseHeaderClause) 22 | :members: 23 | 24 | .. autoclass:: DataVersionClause(BaseHeaderClause) 25 | :members: 26 | 27 | .. autoclass:: DateClause(BaseHeaderClause) 28 | :members: 29 | 30 | .. autoclass:: SavedByClause(BaseHeaderClause) 31 | :members: 32 | 33 | .. autoclass:: AutoGeneratedByClause(BaseHeaderClause) 34 | :members: 35 | 36 | .. autoclass:: ImportClause(BaseHeaderClause) 37 | :members: 38 | 39 | .. autoclass:: SubsetdefClause(BaseHeaderClause) 40 | :members: 41 | 42 | .. autoclass:: SynonymTypedefClause(BaseHeaderClause) 43 | :members: 44 | 45 | .. autoclass:: DefaultNamespaceClause(BaseHeaderClause) 46 | :members: 47 | 48 | .. autoclass:: IdspaceClause(BaseHeaderClause) 49 | :members: 50 | 51 | .. autoclass:: TreatXrefsAsEquivalentClause(BaseHeaderClause) 52 | :members: 53 | 54 | .. autoclass:: TreatXrefsAsGenusDifferentiaClause(BaseHeaderClause) 55 | :members: 56 | 57 | .. autoclass:: TreatXrefsAsReverseGenusDifferentiaClause(BaseHeaderClause) 58 | :members: 59 | 60 | .. autoclass:: TreatXrefsAsRelationshipClause(BaseHeaderClause) 61 | :members: 62 | 63 | .. autoclass:: TreatXrefsAsIsAClause(BaseHeaderClause) 64 | :members: 65 | 66 | .. autoclass:: TreatXrefsAsHasSubclassClause(BaseHeaderClause) 67 | :members: 68 | 69 | .. autoclass:: PropertyValueClause(BaseHeaderClause) 70 | :members: 71 | 72 | .. autoclass:: RemarkClause(BaseHeaderClause) 73 | :members: 74 | 75 | .. autoclass:: OntologyClause(BaseHeaderClause) 76 | :members: 77 | 78 | .. autoclass:: OwlAxiomsClause(BaseHeaderClause) 79 | :members: 80 | 81 | .. autoclass:: UnreservedClause(BaseHeaderClause) 82 | :members: 83 | -------------------------------------------------------------------------------- /docs/api/id.rst: -------------------------------------------------------------------------------- 1 | Identifier 2 | ========== 3 | 4 | .. currentmodule:: fastobo.id 5 | .. automodule:: fastobo.id 6 | 7 | .. autoclass:: BaseIdent 8 | :members: 9 | :special-members: 10 | 11 | 12 | .. autoclass:: PrefixedIdent(BaseIdent) 13 | :members: 14 | :special-members: 15 | 16 | 17 | .. autoclass:: UnprefixedIdent(BaseIdent) 18 | :members: 19 | :special-members: 20 | 21 | 22 | .. autoclass:: Url(BaseIdent) 23 | :members: 24 | :special-members: 25 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | Library Reference 2 | ================= 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 2 7 | 8 | doc 9 | abc 10 | header 11 | term 12 | typedef 13 | id 14 | pv 15 | syn 16 | xref 17 | exceptions 18 | 19 | 20 | .. currentmodule:: fastobo 21 | .. automodule:: fastobo 22 | 23 | 24 | Functions 25 | --------- 26 | 27 | .. autofunction:: fastobo.dump_graph 28 | 29 | .. autofunction:: fastobo.dump_owl 30 | 31 | .. autofunction:: fastobo.iter 32 | 33 | .. autofunction:: fastobo.load 34 | 35 | .. autofunction:: fastobo.loads 36 | 37 | .. autofunction:: fastobo.load_graph 38 | 39 | .. autofunction:: fastobo.id.is_valid 40 | 41 | .. autofunction:: fastobo.id.parse 42 | 43 | 44 | Data structures 45 | --------------- 46 | 47 | Document (`fastobo.doc`) 48 | ^^^^^^^^^^^^^^^^^^^^^^^^ 49 | 50 | .. currentmodule:: fastobo.doc 51 | .. autosummary:: 52 | :nosignatures: 53 | 54 | OboDoc 55 | 56 | 57 | Abstract Base Classes (`fastobo.abc`) 58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 59 | 60 | .. currentmodule:: fastobo.abc 61 | .. autosummary:: 62 | :nosignatures: 63 | 64 | AbstractClause 65 | AbstractEntityClause 66 | AbstractFrame 67 | AbstractEntityFrame 68 | 69 | 70 | 71 | Identifier (`fastobo.id`) 72 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 73 | 74 | .. currentmodule:: fastobo.id 75 | .. autosummary:: 76 | :nosignatures: 77 | 78 | BaseIdent 79 | PrefixedIdent 80 | UnprefixedIdent 81 | Url 82 | 83 | 84 | Header (`fastobo.header`) 85 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | .. currentmodule:: fastobo.header 88 | .. autosummary:: 89 | :nosignatures: 90 | 91 | HeaderFrame 92 | BaseHeaderClause 93 | 94 | FormatVersionClause 95 | DataVersionClause 96 | DateClause 97 | SavedByClause 98 | AutoGeneratedByClause 99 | ImportClause 100 | SubsetdefClause 101 | SynonymTypedefClause 102 | DefaultNamespaceClause 103 | IdspaceClause 104 | TreatXrefsAsEquivalentClause 105 | TreatXrefsAsGenusDifferentiaClause 106 | TreatXrefsAsReverseGenusDifferentiaClause 107 | TreatXrefsAsRelationshipClause 108 | TreatXrefsAsIsAClause 109 | TreatXrefsAsHasSubclassClause 110 | PropertyValueClause 111 | RemarkClause 112 | OntologyClause 113 | OwlAxiomsClause 114 | UnreservedClause 115 | 116 | 117 | Term (`fastobo.term`) 118 | ^^^^^^^^^^^^^^^^^^^^^ 119 | 120 | .. currentmodule:: fastobo.term 121 | .. autosummary:: 122 | :nosignatures: 123 | 124 | TermFrame 125 | BaseTermClause 126 | 127 | AltIdClause 128 | BuiltinClause 129 | CommentClause 130 | ConsiderClause 131 | CreatedByClause 132 | CreationDateClause 133 | DefClause 134 | DisjointFromClause 135 | EquivalentToClause 136 | IntersectionOfClause 137 | IsAClause 138 | IsAnonymousClause 139 | IsObsoleteClause 140 | NameClause 141 | NamespaceClause 142 | PropertyValueClause 143 | RelationshipClause 144 | ReplacedByClause 145 | SubsetClause 146 | SynonymClause 147 | UnionOfClause 148 | XrefClause 149 | 150 | 151 | Typedef (`fastobo.typedef`) 152 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 153 | 154 | .. currentmodule:: fastobo.typedef 155 | .. autosummary:: 156 | :nosignatures: 157 | 158 | TypedefFrame 159 | BaseTypedefClause 160 | 161 | AltIdClause 162 | BuiltinClause 163 | CommentClause 164 | ConsiderClause 165 | CreatedByClause 166 | CreationDateClause 167 | DefClause 168 | DisjointFromClause 169 | DisjointOverClause 170 | DomainClause 171 | EquivalentToChainClause 172 | EquivalentToClause 173 | ExpandAssertionToClause 174 | ExpandExpressionToClause 175 | HoldsOverChainClause 176 | IntersectionOfClause 177 | InverseOfClause 178 | IsAClause 179 | IsAnonymousClause 180 | IsAntiSymmetricClause 181 | IsAsymmetricClause 182 | IsClassLevelClause 183 | IsCyclicClause 184 | IsFunctionalClause 185 | IsInverseFunctionalClause 186 | IsMetadataTagClause 187 | IsObsoleteClause 188 | IsReflexiveClause 189 | IsSymmetricClause 190 | IsTransitiveClause 191 | NameClause 192 | NamespaceClause 193 | PropertyValueClause 194 | RangeClause 195 | RelationshipClause 196 | ReplacedByClause 197 | SubsetClause 198 | SynonymClause 199 | TransitiveOverClause 200 | UnionOfClause 201 | XrefClause 202 | 203 | 204 | Property Value (`fastobo.pv`) 205 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 206 | 207 | .. currentmodule:: fastobo.pv 208 | .. autosummary:: 209 | :nosignatures: 210 | 211 | AbstractPropertyValue 212 | LiteralPropertyValue 213 | ResourcePropertyValue 214 | 215 | 216 | Synonym (`fastobo.syn`) 217 | ^^^^^^^^^^^^^^^^^^^^^^^ 218 | 219 | .. currentmodule:: fastobo.syn 220 | .. autosummary:: 221 | :nosignatures: 222 | 223 | Synonym 224 | 225 | 226 | Xref (`fastobo.xref`) 227 | ^^^^^^^^^^^^^^^^^^^^^ 228 | 229 | .. currentmodule:: fastobo.xref 230 | .. autosummary:: 231 | :nosignatures: 232 | 233 | Xref 234 | XrefList 235 | 236 | 237 | 238 | Exceptions 239 | ---------- 240 | 241 | Cardinality Errors 242 | ^^^^^^^^^^^^^^^^^^ 243 | 244 | .. currentmodule:: fastobo.exceptions 245 | .. autosummary:: 246 | :nosignatures: 247 | 248 | MissingClauseError 249 | DuplicateClausesError 250 | SingleClauseError 251 | 252 | 253 | Threading Errors 254 | ^^^^^^^^^^^^^^^^^^ 255 | 256 | .. currentmodule:: fastobo.exceptions 257 | .. autosummary:: 258 | :nosignatures: 259 | 260 | DisconnectedChannelError 261 | -------------------------------------------------------------------------------- /docs/api/pv.rst: -------------------------------------------------------------------------------- 1 | Property-Value 2 | ============== 3 | 4 | .. currentmodule:: fastobo.pv 5 | .. automodule:: fastobo.pv 6 | 7 | 8 | .. autoclass:: AbstractPropertyValue 9 | :members: 10 | :special-members: 11 | 12 | 13 | .. autoclass:: LiteralPropertyValue(AbstractPropertyValue) 14 | :members: 15 | :special-members: 16 | 17 | 18 | .. autoclass:: ResourcePropertyValue(AbstractPropertyValue) 19 | :members: 20 | :special-members: 21 | -------------------------------------------------------------------------------- /docs/api/syn.rst: -------------------------------------------------------------------------------- 1 | Synonym 2 | ======= 3 | 4 | .. currentmodule:: fastobo.syn 5 | .. automodule:: fastobo.syn 6 | 7 | 8 | .. autoclass:: Synonym 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/api/term.rst: -------------------------------------------------------------------------------- 1 | Term 2 | ==== 3 | 4 | Frame 5 | ----- 6 | 7 | .. currentmodule:: fastobo.term 8 | .. automodule:: fastobo.term 9 | 10 | 11 | .. autoclass:: TermFrame(AbstractEntityFrame) 12 | :members: 13 | :special-members: 14 | 15 | 16 | Clauses 17 | ------- 18 | 19 | 20 | .. autoclass:: BaseTermClause(AbstractEntityClause) 21 | :members: 22 | :special-members: 23 | 24 | .. autoclass:: AltIdClause(BaseTermClause) 25 | :members: 26 | :special-members: 27 | 28 | .. autoclass:: BuiltinClause(BaseTermClause) 29 | :members: 30 | :special-members: 31 | 32 | .. autoclass:: CommentClause(BaseTermClause) 33 | :members: 34 | :special-members: 35 | 36 | .. autoclass:: ConsiderClause(BaseTermClause) 37 | :members: 38 | :special-members: 39 | 40 | .. autoclass:: CreatedByClause(BaseTermClause) 41 | :members: 42 | :special-members: 43 | 44 | .. autoclass:: CreationDateClause(BaseTermClause) 45 | :members: 46 | :special-members: 47 | 48 | .. autoclass:: DefClause(BaseTermClause) 49 | :members: 50 | :special-members: 51 | 52 | .. autoclass:: DisjointFromClause(BaseTermClause) 53 | :members: 54 | :special-members: 55 | 56 | .. autoclass:: EquivalentToClause(BaseTermClause) 57 | :members: 58 | :special-members: 59 | 60 | .. autoclass:: IntersectionOfClause(BaseTermClause) 61 | :members: 62 | :special-members: 63 | 64 | .. autoclass:: IsAClause(BaseTermClause) 65 | :members: 66 | :special-members: 67 | 68 | .. autoclass:: IsAnonymousClause(BaseTermClause) 69 | :members: 70 | :special-members: 71 | 72 | .. autoclass:: IsObsoleteClause(BaseTermClause) 73 | :members: 74 | :special-members: 75 | 76 | .. autoclass:: NameClause(BaseTermClause) 77 | :members: 78 | :special-members: 79 | 80 | .. autoclass:: NamespaceClause(BaseTermClause) 81 | :members: 82 | :special-members: 83 | 84 | .. autoclass:: PropertyValueClause(BaseTermClause) 85 | :members: 86 | :special-members: 87 | 88 | .. autoclass:: RelationshipClause(BaseTermClause) 89 | :members: 90 | :special-members: 91 | 92 | .. autoclass:: ReplacedByClause(BaseTermClause) 93 | :members: 94 | :special-members: 95 | 96 | .. autoclass:: SubsetClause(BaseTermClause) 97 | :members: 98 | :special-members: 99 | 100 | .. autoclass:: SynonymClause(BaseTermClause) 101 | :members: 102 | :special-members: 103 | 104 | .. autoclass:: UnionOfClause(BaseTermClause) 105 | :members: 106 | :special-members: 107 | 108 | .. autoclass:: XrefClause(BaseTermClause) 109 | :members: 110 | :special-members: 111 | -------------------------------------------------------------------------------- /docs/api/typedef.rst: -------------------------------------------------------------------------------- 1 | Typedef 2 | ======= 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 2 7 | 8 | Frame 9 | ----- 10 | 11 | .. currentmodule:: fastobo.typedef 12 | .. automodule:: fastobo.typedef 13 | 14 | .. autoclass:: TypedefFrame(AbstractEntityFrame) 15 | :members: 16 | :special-members: 17 | 18 | 19 | Clauses 20 | ------- 21 | 22 | .. autoclass:: BaseTypedefClause(AbstractEntityClause) 23 | :members: 24 | :special-members: 25 | 26 | .. autoclass:: AltIdClause(BaseTypedefClause) 27 | :members: 28 | :special-members: 29 | 30 | .. autoclass:: BuiltinClause(BaseTypedefClause) 31 | :members: 32 | :special-members: 33 | 34 | .. autoclass:: CommentClause(BaseTypedefClause) 35 | :members: 36 | :special-members: 37 | 38 | .. autoclass:: ConsiderClause(BaseTypedefClause) 39 | :members: 40 | :special-members: 41 | 42 | .. autoclass:: CreatedByClause(BaseTypedefClause) 43 | :members: 44 | :special-members: 45 | 46 | .. autoclass:: CreationDateClause(BaseTypedefClause) 47 | :members: 48 | :special-members: 49 | 50 | .. autoclass:: DefClause(BaseTypedefClause) 51 | :members: 52 | :special-members: 53 | 54 | .. autoclass:: DisjointFromClause(BaseTypedefClause) 55 | :members: 56 | :special-members: 57 | 58 | .. autoclass:: DisjointOverClause(BaseTypedefClause) 59 | :members: 60 | :special-members: 61 | 62 | .. autoclass:: DomainClause(BaseTypedefClause) 63 | :members: 64 | :special-members: 65 | 66 | .. autoclass:: EquivalentToChainClause(BaseTypedefClause) 67 | :members: 68 | :special-members: 69 | 70 | .. autoclass:: EquivalentToClause(BaseTypedefClause) 71 | :members: 72 | :special-members: 73 | 74 | .. autoclass:: ExpandAssertionToClause(BaseTypedefClause) 75 | :members: 76 | :special-members: 77 | 78 | .. autoclass:: ExpandExpressionToClause(BaseTypedefClause) 79 | :members: 80 | :special-members: 81 | 82 | .. autoclass:: HoldsOverChainClause(BaseTypedefClause) 83 | :members: 84 | :special-members: 85 | 86 | .. autoclass:: IntersectionOfClause(BaseTypedefClause) 87 | :members: 88 | :special-members: 89 | 90 | .. autoclass:: InverseOfClause(BaseTypedefClause) 91 | :members: 92 | :special-members: 93 | 94 | .. autoclass:: IsAClause(BaseTypedefClause) 95 | :members: 96 | :special-members: 97 | 98 | .. autoclass:: IsAnonymousClause(BaseTypedefClause) 99 | :members: 100 | :special-members: 101 | 102 | .. autoclass:: IsAntiSymmetricClause(BaseTypedefClause) 103 | :members: 104 | :special-members: 105 | 106 | .. autoclass:: IsAsymmetricClause(BaseTypedefClause) 107 | :members: 108 | :special-members: 109 | 110 | .. autoclass:: IsClassLevelClause(BaseTypedefClause) 111 | :members: 112 | :special-members: 113 | 114 | .. autoclass:: IsCyclicClause(BaseTypedefClause) 115 | :members: 116 | :special-members: 117 | 118 | .. autoclass:: IsFunctionalClause(BaseTypedefClause) 119 | :members: 120 | :special-members: 121 | 122 | .. autoclass:: IsInverseFunctionalClause(BaseTypedefClause) 123 | :members: 124 | :special-members: 125 | 126 | .. autoclass:: IsMetadataTagClause(BaseTypedefClause) 127 | :members: 128 | :special-members: 129 | 130 | .. autoclass:: IsObsoleteClause(BaseTypedefClause) 131 | :members: 132 | :special-members: 133 | 134 | .. autoclass:: IsReflexiveClause(BaseTypedefClause) 135 | :members: 136 | :special-members: 137 | 138 | .. autoclass:: IsSymmetricClause(BaseTypedefClause) 139 | :members: 140 | :special-members: 141 | 142 | .. autoclass:: IsTransitiveClause(BaseTypedefClause) 143 | :members: 144 | :special-members: 145 | 146 | .. autoclass:: NameClause(BaseTypedefClause) 147 | :members: 148 | :special-members: 149 | 150 | .. autoclass:: NamespaceClause(BaseTypedefClause) 151 | :members: 152 | :special-members: 153 | 154 | .. autoclass:: PropertyValueClause(BaseTypedefClause) 155 | :members: 156 | :special-members: 157 | 158 | .. autoclass:: RangeClause(BaseTypedefClause) 159 | :members: 160 | :special-members: 161 | 162 | .. autoclass:: RelationshipClause(BaseTypedefClause) 163 | :members: 164 | :special-members: 165 | 166 | .. autoclass:: ReplacedByClause(BaseTypedefClause) 167 | :members: 168 | :special-members: 169 | 170 | .. autoclass:: SubsetClause(BaseTypedefClause) 171 | :members: 172 | :special-members: 173 | 174 | .. autoclass:: SynonymClause(BaseTypedefClause) 175 | :members: 176 | :special-members: 177 | 178 | .. autoclass:: TransitiveOverClause(BaseTypedefClause) 179 | :members: 180 | :special-members: 181 | 182 | .. autoclass:: UnionOfClause(BaseTypedefClause) 183 | :members: 184 | :special-members: 185 | 186 | .. autoclass:: XrefClause(BaseTypedefClause) 187 | :members: 188 | :special-members: 189 | -------------------------------------------------------------------------------- /docs/api/xref.rst: -------------------------------------------------------------------------------- 1 | Xref 2 | ==== 3 | 4 | .. currentmodule:: fastobo.xref 5 | .. automodule:: fastobo.xref 6 | 7 | .. autoclass:: Xref 8 | :members: 9 | :special-members: 10 | 11 | .. autoclass:: XrefList 12 | :members: 13 | :special-members: 14 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import datetime 14 | import os 15 | import re 16 | import semantic_version 17 | import shutil 18 | import sys 19 | import urllib.request 20 | 21 | docssrc_dir = os.path.abspath(os.path.join(__file__, "..")) 22 | project_dir = os.path.dirname(docssrc_dir) 23 | 24 | sys.path.insert(0, os.path.abspath(os.path.join(__file__, project_dir))) 25 | 26 | # -- Imports ----------------------------------------------------------------- 27 | 28 | import fastobo 29 | 30 | # -- Project information ----------------------------------------------------- 31 | 32 | # General information 33 | project = fastobo.__name__ 34 | author = re.match("(.*) <.*>", fastobo.__author__).group(1) 35 | year = datetime.date.today().year 36 | copyright = "{}, {}".format("2019" if year == 2019 else "2019-{}".format(year), author) 37 | 38 | 39 | # extract the semantic version 40 | semver = semantic_version.Version.coerce(fastobo.__version__) 41 | version = str(semver.truncate(level="patch")) 42 | release = str(semver) 43 | 44 | # patch the docstring so that we don't show the link to redirect 45 | # to the docs (we don't want to see it when reading the docs already, duh!) 46 | doc_lines = fastobo.__doc__.splitlines() 47 | if "See Also:" in doc_lines: 48 | see_also = doc_lines.index("See Also:") 49 | fastobo.__doc__ = "\n".join(doc_lines[:see_also]) 50 | 51 | 52 | # -- General configuration --------------------------------------------------- 53 | 54 | # Add any Sphinx extension module names here, as strings. They can be 55 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 56 | # ones. 57 | extensions = [ 58 | "sphinx.ext.autodoc", 59 | "sphinx.ext.autosummary", 60 | "sphinx.ext.intersphinx", 61 | "sphinx.ext.napoleon", 62 | "sphinx.ext.coverage", 63 | "sphinx.ext.mathjax", 64 | "sphinx.ext.todo", 65 | "sphinx.ext.extlinks", 66 | "sphinx_design", 67 | "sphinxcontrib.jquery", 68 | "recommonmark", 69 | "nbsphinx", 70 | "IPython.sphinxext.ipython_console_highlighting", 71 | ] 72 | 73 | # Add any paths that contain templates here, relative to this directory. 74 | templates_path = ['_templates'] 75 | 76 | # The suffix(es) of source filenames. 77 | # You can specify multiple suffix as a list of string: 78 | # 79 | # source_suffix = ['.rst', '.md'] 80 | source_suffix = [".rst", ".md"] 81 | 82 | # The master toctree document. 83 | master_doc = "index" 84 | 85 | # The language for content autogenerated by Sphinx. Refer to documentation 86 | # for a list of supported languages. 87 | # 88 | # This is also used if you do content translation via gettext catalogs. 89 | # Usually you set "language" from the command line for these cases. 90 | language = 'en' 91 | 92 | # List of patterns, relative to source directory, that match files and 93 | # directories to ignore when looking for source files. 94 | # This pattern also affects html_static_path and html_extra_path . 95 | exclude_patterns = ["_build", "**.ipynb_checkpoints", 'Thumbs.db', '.DS_Store'] 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = "monokailight" 99 | 100 | # The name of the default role for inline references 101 | default_role = "py:obj" 102 | 103 | 104 | # -- Options for HTML output ------------------------------------------------- 105 | 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | # 109 | html_theme = 'pydata_sphinx_theme' 110 | 111 | # Add any paths that contain custom static files (such as style sheets) here, 112 | # relative to this directory. They are copied after the builtin static files, 113 | # so a file named "default.css" will overwrite the builtin "default.css". 114 | html_static_path = ['_static/js', '_static/json'] 115 | html_js_files = ["custom-icon.js"] 116 | html_css_files = ["custom.css"] 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | # 122 | html_theme_options = { 123 | "external_links": [], 124 | "show_toc_level": 2, 125 | "use_edit_page_button": True, 126 | "icon_links": [ 127 | { 128 | "name": "GitHub", 129 | "url": "https://github.com/fastobo/fastobo-py", 130 | "icon": "fa-brands fa-github", 131 | }, 132 | { 133 | "name": "PyPI", 134 | "url": "https://pypi.org/project/fastobo", 135 | "icon": "fa-custom fa-pypi", 136 | }, 137 | ], 138 | "logo": { 139 | "text": "FastOBO", 140 | "image_light": "_images/logo.png", 141 | "image_dark": "_images/logo.png", 142 | }, 143 | "navbar_start": ["navbar-logo", "version-switcher"], 144 | "navbar_align": "left", 145 | "footer_start": ["copyright"], 146 | "footer_center": ["sphinx-version"], 147 | "switcher": { 148 | "json_url": "https://fastobo.readthedocs.io/en/latest/_static/switcher.json", 149 | "version_match": version, 150 | } 151 | } 152 | 153 | html_context = { 154 | "github_user": "fastobo", 155 | "github_repo": "fastobo-py", 156 | "github_version": "main", 157 | "doc_path": "docs", 158 | } 159 | 160 | html_favicon = '_images/favicon.ico' 161 | 162 | # -- Extension configuration ------------------------------------------------- 163 | 164 | # -- Options for imgmath extension ------------------------------------------- 165 | 166 | imgmath_image_format = "svg" 167 | 168 | # -- Options for napoleon extension ------------------------------------------ 169 | 170 | napoleon_include_init_with_doc = True 171 | napoleon_include_special_with_doc = True 172 | napoleon_include_private_with_doc = True 173 | napoleon_use_admonition_for_examples = True 174 | napoleon_use_admonition_for_notes = True 175 | napoleon_use_admonition_for_references = True 176 | napoleon_use_rtype = False 177 | 178 | # -- Options for autodoc extension ------------------------------------------- 179 | 180 | autoclass_content = "class" 181 | autodoc_member_order = "bysource" 182 | autosummary_generate = [] 183 | 184 | # -- Options for intersphinx extension --------------------------------------- 185 | 186 | # Example configuration for intersphinx: refer to the Python standard library. 187 | intersphinx_mapping = { 188 | "python": ("https://docs.python.org/3/", None), 189 | "biopython": ("https://biopython.org/docs/latest/", None), 190 | "scoring-matrices": ("https://scoring-matrices.readthedocs.io/en/stable/", None), 191 | } 192 | 193 | # -- Options for recommonmark extension -------------------------------------- 194 | 195 | source_suffix = { 196 | ".rst": "restructuredtext", 197 | ".txt": "markdown", 198 | ".md": "markdown", 199 | } 200 | 201 | # -- Options for nbsphinx extension ------------------------------------------ 202 | 203 | nbsphinx_execute = "auto" 204 | nbsphinx_execute_arguments = [ 205 | "--InlineBackend.figure_formats={'svg', 'pdf'}", 206 | "--InlineBackend.rc={'figure.dpi': 96}", 207 | ] 208 | 209 | # -- Options for extlinks extension ------------------------------------------ 210 | 211 | extlinks = { 212 | "doi": ("https://doi.org/%s", "doi:%s"), 213 | "pmid": ("https://pubmed.ncbi.nlm.nih.gov/%s", "PMID:%s"), 214 | "pmc": ("https://www.ncbi.nlm.nih.gov/pmc/articles/PMC%s", "PMC%s"), 215 | "isbn": ("https://www.worldcat.org/isbn/%s", "ISBN:%s"), 216 | "wiki": ("https://en.wikipedia.org/wiki/%s", "Wikipedia:%s"), 217 | } -------------------------------------------------------------------------------- /docs/examples/descriptions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Checking empty descriptions" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "In this example, we use `fastobo` to create a small validation script which will report empty definitions in an OBO file. We also use `requests` in order to connect to the OBO library." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import fastobo\n", 24 | "import requests" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "`fastobo.load` takes a file-handle, which can be accessed using the `raw` property of the `Response` object returned by `requests.get`:" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 2, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "res = requests.get(\"http://purl.obolibrary.org/obo/ms.obo\", stream=True)\n", 41 | "doc = fastobo.load(res.raw)" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "## Header\n", 49 | "\n", 50 | "Now, we can check the header for empty descriptions in definition clauses: " 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 3, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "for clause in doc.header:\n", 60 | " if isinstance(clause, fastobo.header.SynonymTypedefClause) and not clause.description:\n", 61 | " print(\"Empty description in definition of\", clause.typedef)\n", 62 | " elif isinstance(clause, fastobo.header.SubsetdefClause) and not clause.description:\n", 63 | " print(\"Empty description in definition of\", clause.subset)\n", 64 | " " 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "Note that we are using `isinstance` a lot compared to what you may be used to in other Python library: this is because `fastobo` is based on a Rust library which is strongly-typed, so that is reflected in the Python library that wraps it. We could use the strong typing to write the same snippet using type-specific callback wrapped in a `dict`:" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 4, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "def check_synonym_typedef(clause):\n", 81 | " if not clause.description:\n", 82 | " print(\"Empty description in definition of\", clause.typedef, \"in header\")\n", 83 | "\n", 84 | "def check_subsetdef(clause):\n", 85 | " if not clause.description:\n", 86 | " print(\"Empty description in definition of\", clause.subset, \"in header\")\n", 87 | " \n", 88 | "CALLBACKS = {\n", 89 | " fastobo.header.SynonymTypedefClause: check_synonym_typedef,\n", 90 | " fastobo.header.SynonymTypedefClause: check_subsetdef,\n", 91 | "}\n", 92 | "\n", 93 | "for clause in doc.header:\n", 94 | " callback = CALLBACKS.get(type(clause))\n", 95 | " if callback is not None:\n", 96 | " callback(clause)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "Such a construct can be used to process all possible clauses while reducing the number of `if`/`elif` branches, in particular when many different clauses are processed at the same time." 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "## Entities" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "Checking for definitions in entity frames is straightforward: all definition clauses have a `definition` property that returns the textual definition of the entity. We can use duck-typing here to check for empty definitions:" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 5, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "for frame in doc:\n", 127 | " for clause in frame:\n", 128 | " try:\n", 129 | " if not clause.definition:\n", 130 | " print(\"Empty definition of\", frame.id)\n", 131 | " except AttributeError:\n", 132 | " pass" 133 | ] 134 | } 135 | ], 136 | "metadata": { 137 | "kernelspec": { 138 | "display_name": "Python 3", 139 | "language": "python", 140 | "name": "python3" 141 | }, 142 | "language_info": { 143 | "codemirror_mode": { 144 | "name": "ipython", 145 | "version": 3 146 | }, 147 | "file_extension": ".py", 148 | "mimetype": "text/x-python", 149 | "name": "python", 150 | "nbconvert_exporter": "python", 151 | "pygments_lexer": "ipython3", 152 | "version": "3.9.1" 153 | } 154 | }, 155 | "nbformat": 4, 156 | "nbformat_minor": 4 157 | } 158 | -------------------------------------------------------------------------------- /docs/examples/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastobo/fastobo-py/18da7270faf173c77bb42ec9c63b0bb1331952e7/docs/examples/graph.png -------------------------------------------------------------------------------- /docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | This section contains examples of Python code, generated from Jupyter notebooks 5 | with `nbsphinx `_ against the latest version of 6 | ``fastobo``. 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | descriptions 12 | obsolete 13 | graph 14 | -------------------------------------------------------------------------------- /docs/examples/obsolete.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Looking for obsolete terms without replacements" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "In this example, we use `fastobo` to create a small validation script which will retrieve obsolete terms without replacement." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import fastobo\n", 24 | "import requests" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "`fastobo.load` takes a file-handle, which can be accessed using the `raw` property of the `Response` object returned by `requests.get`:" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 2, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "res = requests.get(\"http://purl.obolibrary.org/obo/go.obo\", stream=True)\n", 41 | "doc = fastobo.load(res.raw)" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "Note that we are using `isinstance` a lot compared to what you may be used to in other Python library: this is because `fastobo` is based on a Rust library which is strongly-typed, so that is reflected in the Python library that wraps it. We could use the strong typing to write the same snippet using type-specific callback wrapped in a `dict`:" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 3, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "for frame in doc:\n", 58 | " \n", 59 | " if isinstance(frame, fastobo.term.TermFrame):\n", 60 | " \n", 61 | " obsolete = False\n", 62 | " replacements = []\n", 63 | "\n", 64 | " for clause in frame:\n", 65 | " if clause.raw_tag == \"is_obsolete\":\n", 66 | " obsolete |= clause.obsolete\n", 67 | " elif clause.raw_tag in (\"consider\", \"replaced_by\"):\n", 68 | " replacements.append(clause.term)\n", 69 | "\n", 70 | " if obsolete and not replacements:\n", 71 | " print(frame.id, \"is obsolete but has no replacement.\")" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "Note that we could use the same kind of logic to retrieve terms with more than one replacement, which can be the case when an obsolete term does not have a strictly equivalent substitute in the newer versions of an ontology" 79 | ] 80 | } 81 | ], 82 | "metadata": { 83 | "kernelspec": { 84 | "display_name": "Python 3", 85 | "language": "python", 86 | "name": "python3" 87 | }, 88 | "language_info": { 89 | "codemirror_mode": { 90 | "name": "ipython", 91 | "version": 3 92 | }, 93 | "file_extension": ".py", 94 | "mimetype": "text/x-python", 95 | "name": "python", 96 | "nbconvert_exporter": "python", 97 | "pygments_lexer": "ipython3", 98 | "version": "3.9.1" 99 | } 100 | }, 101 | "nbformat": 4, 102 | "nbformat_minor": 4 103 | } 104 | -------------------------------------------------------------------------------- /docs/guide/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Authors 5 | ------- 6 | 7 | ``fastobo`` is developped and maintained by: 8 | 9 | +-------------------------------------------------+ 10 | | | **Martin Larralde** | 11 | | | PhD Candidate, Zeller Team | 12 | | | EMBL Heidelberg | 13 | | | martin.larralde@embl.de | 14 | +-------------------------------------------------+ 15 | 16 | This library was developped during a Master's internship at 17 | **Lawrence Berkeley National Laboratory**, under the supervision of: 18 | 19 | +-------------------------------------------------+ 20 | | | **Chris Mungall** | 21 | | | Department Head, Molecular Ecosystems Biology | 22 | | | Lawrence Berkeley National Laboratory | 23 | | | cjmungall@lbl.gov | 24 | +-------------------------------------------------+ 25 | 26 | 27 | Citation 28 | -------- 29 | 30 | This library was created by one of your colleagues. Please acknowledge the 31 | Principal Investigator, and cite the reference document in which it was 32 | described: 33 | 34 | *Larralde M.* **Developing Python and Rust libraries to improve the ontology ecosystem** 35 | *\[version 1; not peer reviewed\].* F1000Research 2019, 8(ISCB Comm J):1500 (poster) 36 | (`https://doi.org/10.7490/f1000research.1117405.1 `_) 37 | 38 | 39 | 40 | License 41 | ------- 42 | 43 | This project is licensed under the `MIT License `_. 44 | -------------------------------------------------------------------------------- /docs/guide/changes.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /docs/guide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | This section contains guides and documents about ``fastobo`` usage. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | :caption: Getting Started 9 | 10 | Installation 11 | Publications 12 | 13 | .. toctree:: 14 | :maxdepth: 1 15 | :caption: Resources 16 | 17 | Changelog 18 | -------------------------------------------------------------------------------- /docs/guide/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | .. highlight:: console 5 | 6 | Precompiled Wheels 7 | ------------------ 8 | 9 | The ``fastobo`` Python module is implemented in Rust, but the Rust compiler 10 | is only required if your platform does not have precompiled wheels available. 11 | Currently, we provide `wheels `_ for the following 12 | platforms: 13 | 14 | * **Linux**: *x86-64* and *Aarch64* 15 | * **MacOS**: *x86-64* and *Aarch64* 16 | * **Windows**: *x86-64* only. 17 | 18 | The supported Python versions are provided with the 19 | `cibuildwheel `_ tool. Downloading and 20 | installing from a wheel is then as simple as:: 21 | 22 | $ pip install fastobo --user 23 | 24 | If your platform and implementation is not listed above, you will need to build 25 | from source (see next section). 26 | 27 | 28 | Conda package 29 | ------------- 30 | 31 | ``fastobo`` is also available for `Conda `_ in the 32 | ``conda-forge`` channel:: 33 | 34 | $ conda install conda-forge::fastobo 35 | 36 | 37 | Piwheels 38 | ^^^^^^^^ 39 | 40 | ``fastobo`` works on Raspberry Pi computers, and pre-built wheels are compiled 41 | for `armv7l` on `piwheels `_. 42 | Run the following command to install these instead of compiling from source: 43 | 44 | .. code:: console 45 | 46 | $ pip3 install fastobo --extra-index-url https://www.piwheels.org/simple 47 | 48 | Check the `piwheels documentation `_ for 49 | more information. 50 | 51 | 52 | Building from source 53 | -------------------- 54 | 55 | In order to build the code from source, you will need to have 56 | the Rust compiler installed and available in your ``$PATH``. See 57 | `documentation on rust-lang.org `_ 58 | to learn how to install Rust on your machine. 59 | 60 | Then installing with ``pip`` will build the pacakge:: 61 | 62 | $ pip install fastobo --user -v --no-binary :all: 63 | 64 | **Be patient, it can take a long time on lower-end machine!** 65 | 66 | Note that this will install a static library that have been built with most 67 | feature flags disabled for compatibility purposes. If you wish to build the 68 | optimized library from source, with all feature flags enabled, make sure to 69 | have ``-C target-cpu=native`` in your ``$RUSTFLAGS`` environment while building:: 70 | 71 | $ RUSTFLAGS="-Ctarget-cpu=native" pip install fastobo --user --no-binary :all: 72 | -------------------------------------------------------------------------------- /docs/guide/publications.rst: -------------------------------------------------------------------------------- 1 | Publications 2 | ============ 3 | 4 | How to cite 5 | ----------- 6 | 7 | This library is scientific software, and can be cited with the following 8 | reference: 9 | 10 | *Larralde M.* **Developing Python and Rust libraries to improve the ontology ecosystem** 11 | *\[version 1; not peer reviewed\].* F1000Research 2019, 8(ISCB Comm J):1500 (poster) 12 | (`https://doi.org/10.7490/f1000research.1117405.1 `_) 13 | 14 | 15 | Citations 16 | --------- 17 | 18 | ``fastobo`` has been used in the following research works: 19 | 20 | - Bittremieux, W., Levitsky, L., Pilz, M., Sachsenberg, T., Huber, F., Wang, M., & Dorrestein, P. C. (2023). Unified and Standardized Mass Spectrometry Data Processing in Python Using spectrum_utils. Journal of Proteome Research, 22(2), 625–631. https://doi.org/10.1021/acs.jproteome.2c00632 21 | - Glauer, M. (2024). Knowledge and learning: Synergies between ontologies and machine learning. https://doi.org/10.25673/116961 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | |Logo| ``fastobo`` |Stars| 2 | ========================== 3 | 4 | .. |Logo| image:: /_images/logo.png 5 | :scale: 10% 6 | :class: dark-light 7 | 8 | .. |Stars| image:: https://img.shields.io/github/stars/fastobo/fastobo-py.svg?style=social&maxAge=3600&label=Star 9 | :target: https://github.com/fastobo/fastobo-py/stargazers 10 | 11 | *Faultless AST for Open Biomedical Ontologies in Python.* 12 | 13 | |Actions| |License| |Source| |PyPI| |Wheel| |Conda| |Versions| |Implementation| |Changelog| |Docs| |Issues| |DOI| |Downloads| 14 | 15 | .. |PyPI| image:: https://img.shields.io/pypi/v/fastobo.svg?style=flat-square&maxAge=300 16 | :target: https://pypi.org/project/fastobo 17 | 18 | .. |Conda| image:: https://img.shields.io/conda/vn/conda-forge/fastobo?style=flat-square&maxAge=3600 19 | :target: https://anaconda.org/conda-forge/fastobo 20 | 21 | .. |Actions| image:: https://img.shields.io/github/actions/workflow/status/fastobo/fastobo-py/test.yml?branch=master&style=flat-square&maxAge=600 22 | :target: https://github.com/fastobo/fastobo-py/actions 23 | 24 | .. |Wheel| image:: https://img.shields.io/pypi/wheel/fastobo.svg?style=flat-square&maxAge=2678400 25 | :target: https://pypi.org/project/fastobo 26 | 27 | .. |Versions| image:: https://img.shields.io/pypi/pyversions/fastobo.svg?style=flat-square&maxAge=300 28 | :target: https://travis-ci.org/fastobo/fastobo-py 29 | 30 | .. |Changelog| image:: https://img.shields.io/badge/keep%20a-changelog-8A0707.svg?maxAge=2678400&style=flat-square 31 | :target: https://github.com/fastobo/fastobo-py/blob/master/CHANGELOG.md 32 | 33 | .. |License| image:: https://img.shields.io/pypi/l/fastobo.svg?style=flat-square&maxAge=300 34 | :target: https://choosealicense.com/licenses/mit/ 35 | 36 | .. |Source| image:: https://img.shields.io/badge/source-GitHub-303030.svg?maxAge=3600&style=flat-square 37 | :target: https://github.com/fastobo/fastobo-py 38 | 39 | .. |Implementation| image:: https://img.shields.io/pypi/implementation/fastobo.svg?style=flat-square&maxAge=600 40 | :target: https://pypi.org/project/fastobo/#files 41 | 42 | .. |Docs| image:: https://img.shields.io/readthedocs/fastobo.svg?maxAge=3600&style=flat-square 43 | :target: https://fastobo.readthedocs.io/ 44 | 45 | .. |Issues| image:: https://img.shields.io/github/issues/fastobo/fastobo-py.svg?style=flat-square&maxAge=600 46 | :target: https://github.com/fastobo/fastobo-py/issues 47 | 48 | .. |DOI| image:: https://img.shields.io/badge/doi-10.7490%2Ff1000research.1117405.1-brightgreen?style=flat-square&maxAge=31536000 49 | :target: https://f1000research.com/posters/8-1500 50 | 51 | .. |Downloads| image:: https://img.shields.io/pypi/dm/fastobo?style=flat-square&color=303f9f&maxAge=86400&label=downloads 52 | :target: https://pepy.tech/project/fastobo 53 | 54 | About 55 | ----- 56 | 57 | ``fastobo`` is a Rust library implementing a reliable parser for the 58 | `OBO file format 1.4 `_. 59 | This extension module exports idiomatic Python bindings that can be used to load, edit and 60 | serialize ontologies in the OBO format. 61 | 62 | Setup 63 | ----- 64 | 65 | Run ``pip install fastobo`` in a shell to download the latest release 66 | from PyPi, or have a look at the :doc:`Installation page ` to find 67 | other ways to install ``diced``. 68 | 69 | 70 | Library 71 | ------- 72 | 73 | .. toctree:: 74 | :maxdepth: 2 75 | 76 | User Guide 77 | Examples 78 | API Reference 79 | 80 | 81 | License 82 | ------- 83 | 84 | This library is provided under the `MIT license `_. 85 | 86 | *This project was was developed by* `Martin Larralde `_ 87 | *during his MSc thesis at the* 88 | `Lawrence Berkeley National Laboratory `_ 89 | *in the* `BBOP team `_. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # sphinx documentation dependencies 2 | semantic_version ~=2.8 3 | sphinx >=5.0 4 | recommonmark ~=0.7 5 | pygments-style-monokailight ~=0.4 6 | ipython ~=7.19 7 | pygments ~=2.4 8 | nbsphinx ~=0.8 9 | sphinxcontrib-jquery ~=4.1 10 | sphinx-design 11 | pydata-sphinx-theme 12 | ipykernel >=5.3 13 | networkx >=2.5 14 | pygraphviz >=1.7 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin ~=1.2"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "fastobo" 7 | dynamic = ["version"] 8 | description = "Faultless AST for Open Biomedical Ontologies in Python." 9 | readme = "README.md" 10 | requires-python = ">=3.7" 11 | license = { file = "COPYING" } 12 | authors = [ 13 | { name = "Martin Larralde", email = "martin.larralde@embl.de" }, 14 | ] 15 | keywords = ["ontologies", "ontology", "obo", "obofoundry", "parser", "syntax", "ast"] 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Intended Audience :: Developers", 19 | "Intended Audience :: Science/Research", 20 | "Intended Audience :: Healthcare Industry", 21 | "License :: OSI Approved :: MIT License", 22 | "Programming Language :: Rust", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "Programming Language :: Python :: Implementation :: CPython", 31 | "Programming Language :: Python :: Implementation :: PyPy", 32 | "Topic :: Scientific/Engineering :: Bio-Informatics", 33 | "Topic :: Scientific/Engineering :: Medical Science Apps.", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | "Typing :: Typed", 36 | ] 37 | 38 | [project.urls] 39 | "Homepage" = "https://github.com/fastobo/fastobo-py/" 40 | "Bug Tracker" = "https://github.com/fastobo/fastobo-py/issues" 41 | "Changelog" = "https://fastobo.readthedocs.io/en/latest/changes.html" 42 | "Documentation" = "https://fastobo.readthedocs.io/" 43 | "Builds" = "https://github.com/fastobo/fastobo-py/actions/" 44 | "PyPI" = "https://pypi.org/project/fastobo" 45 | "Conda" = "https://anaconda.org/conda-forge/fastobo" 46 | "PiWheels" = "https://www.piwheels.org/project/fastobo/" 47 | 48 | [tool.maturin] 49 | manifest-path = "Cargo.toml" 50 | features = ["extension-module"] 51 | # python-source = "src" 52 | module-name = "fastobo" 53 | 54 | [tool.cibuildwheel] 55 | skip = ["*-musllinux_i686"] 56 | before-build = "pip install maturin" 57 | test-command = "python -m unittest discover -s {project} -v" 58 | build-verbosity = 1 59 | free-threaded-support = false 60 | 61 | [tool.cibuildwheel.linux] 62 | environment = { PATH="$HOME/.cargo/bin:$PATH" } 63 | before-all = "curl -sSf https://sh.rustup.rs | sh -s -- -y" 64 | 65 | [tool.cibuildwheel.macos] 66 | before-all = ["curl -sSf https://sh.rustup.rs | sh -s -- -y"] 67 | environment = { MACOSX_DEPLOYMENT_TARGET = "10.12" } 68 | 69 | [[tool.cibuildwheel.overrides]] 70 | select = "*-macosx_x86_64" 71 | inherit.before-all = "append" 72 | before-all = ["rustup target add x86_64-apple-darwin"] 73 | 74 | [[tool.cibuildwheel.overrides]] 75 | select = "*-macosx_arm64" 76 | inherit.before-all = "append" 77 | before-all = ["rustup target add aarch64-apple-darwin"] -------------------------------------------------------------------------------- /src/build.rs: -------------------------------------------------------------------------------- 1 | extern crate built; 2 | 3 | fn main() { 4 | built::write_built_file().expect("Failed to acquire build-time information"); 5 | } 6 | -------------------------------------------------------------------------------- /src/built.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use pyo3::prelude::*; 4 | 5 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 6 | -------------------------------------------------------------------------------- /src/date.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ord; 2 | use std::cmp::Ordering; 3 | 4 | use fastobo::ast::Date; 5 | use fastobo::ast::Time; 6 | 7 | use pyo3::prelude::*; 8 | use pyo3::types::PyDate; 9 | use pyo3::types::PyDateAccess; 10 | use pyo3::types::PyDateTime; 11 | use pyo3::types::PyTimeAccess; 12 | use pyo3::types::PyTzInfo; 13 | 14 | /// Extract the timezone from a Python datetime using the `tzinfo` attribute. 15 | pub fn extract_timezone<'py>( 16 | py: Python<'py>, 17 | datetime: &Bound<'py, PyDateTime>, 18 | ) -> PyResult> { 19 | use fastobo::ast::IsoTimezone::*; 20 | let tzinfo = datetime.getattr("tzinfo")?; 21 | if !tzinfo.is_none() { 22 | let timedelta = tzinfo.call_method1("utcoffset", (datetime,))?; 23 | let total_seconds = timedelta.call_method0("total_seconds")?.extract::()? as i64; 24 | let hh = total_seconds / 3600; 25 | let mm = (total_seconds / 60) % 60; 26 | match total_seconds.cmp(&0) { 27 | Ordering::Equal => Ok(Some(Utc)), 28 | Ordering::Less => Ok(Some(Minus((-hh) as u8, ((mm + 60) % 60) as u8))), 29 | Ordering::Greater => Ok(Some(Plus(hh as u8, mm as u8))), 30 | } 31 | } else { 32 | Ok(None) 33 | } 34 | } 35 | 36 | /// Convert a Python `datetime.datetime` to a `fastobo::ast::IsoDateTime`. 37 | pub fn datetime_to_isodatetime<'py>( 38 | py: Python<'py>, 39 | datetime: &Bound<'py, PyDateTime>, 40 | ) -> PyResult { 41 | let date = fastobo::ast::IsoDate::new( 42 | datetime.get_year() as u16, 43 | datetime.get_month(), 44 | datetime.get_day(), 45 | ); 46 | let mut time = fastobo::ast::IsoTime::new( 47 | datetime.get_hour(), 48 | datetime.get_minute(), 49 | datetime.get_second(), 50 | ); 51 | if let Some(timezone) = extract_timezone(py, datetime)? { 52 | time = time.with_timezone(timezone); 53 | } 54 | Ok(fastobo::ast::IsoDateTime::new(date, time)) 55 | } 56 | 57 | /// Convert a `fastobo::ast::IsoDateTime` to a Python `datetime.datetime`. 58 | pub fn isodatetime_to_datetime<'py>( 59 | py: Python<'py>, 60 | datetime: &fastobo::ast::IsoDateTime, 61 | ) -> PyResult> { 62 | use fastobo::ast::IsoTimezone::*; 63 | 64 | // Extract the timezone if there is any 65 | let tz = if let Some(tz) = datetime.time().timezone() { 66 | let datetime = py.import("datetime")?; 67 | let timezone = datetime.getattr("timezone")?; 68 | let timedelta = datetime.getattr("timedelta")?; 69 | match tz { 70 | Utc => Some(timezone.getattr("utc")?), 71 | Plus(hh, mm) => { 72 | let args = (0u8, 0u8, 0u8, 0u8, *mm, *hh); 73 | Some(timezone.call1((timedelta.call1(args)?,))?) 74 | } 75 | Minus(hh, mm) => { 76 | let args = (0u8, 0u8, 0u8, 0u8, -(*mm as i8), -(*hh as i8)); 77 | Some(timezone.call1((timedelta.call1(args)?,))?) 78 | } 79 | } 80 | } else { 81 | None 82 | }; 83 | 84 | // Create the `datetime.datetime` instance 85 | PyDateTime::new( 86 | py, 87 | datetime.year() as i32, 88 | datetime.month(), 89 | datetime.day(), 90 | datetime.hour(), 91 | datetime.minute(), 92 | datetime.second(), 93 | datetime 94 | .time() 95 | .fraction() 96 | .map(|f| (f * 1000.0) as u32) 97 | .unwrap_or(0), 98 | tz.as_ref() 99 | .map(|obj| obj.downcast::()) 100 | .transpose()?, 101 | ) 102 | } 103 | 104 | /// Convert a Python `datetime.date` to a `fastobo::ast::IsoDate`. 105 | pub fn date_to_isodate<'py>( 106 | py: Python<'py>, 107 | date: &Bound<'py, PyDate>, 108 | ) -> PyResult { 109 | Ok(fastobo::ast::IsoDate::new( 110 | date.get_year() as u16, 111 | date.get_month(), 112 | date.get_day(), 113 | )) 114 | } 115 | 116 | /// Convert a `fastobo::ast::IsoDateTime` to a Python `datetime.datetime`. 117 | pub fn isodate_to_date<'py>( 118 | py: Python<'py>, 119 | date: &fastobo::ast::IsoDate, 120 | ) -> PyResult> { 121 | // Create the `datetime.datetime` instance 122 | PyDate::new(py, date.year() as i32, date.month(), date.day()) 123 | } 124 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error as IOError; 2 | use std::path::Path; 3 | 4 | use pyo3::exceptions::PyFileNotFoundError; 5 | use pyo3::exceptions::PyOSError; 6 | use pyo3::exceptions::PyRuntimeError; 7 | use pyo3::exceptions::PySyntaxError; 8 | use pyo3::exceptions::PyValueError; 9 | use pyo3::PyErr; 10 | 11 | use fastobo::ast as obo; 12 | use fastobo::syntax::pest::error::ErrorVariant; 13 | use fastobo::syntax::pest::error::InputLocation; 14 | use fastobo::syntax::pest::error::LineColLocation; 15 | use fastobo::syntax::Rule; 16 | 17 | use crate::py::exceptions::DisconnectedChannelError; 18 | use crate::py::exceptions::DuplicateClausesError; 19 | use crate::py::exceptions::MissingClauseError; 20 | use crate::py::exceptions::SingleClauseError; 21 | 22 | // --------------------------------------------------------------------------- 23 | 24 | #[macro_export] 25 | macro_rules! raise( 26 | ($py:expr, $error_type:ident ($msg:expr) from $inner:expr ) => ({ 27 | let err = $error_type::new_err($msg).to_object($py); 28 | err.call_method1( 29 | $py, 30 | "__setattr__", 31 | ("__cause__".to_object($py), $inner.to_object($py)), 32 | )?; 33 | return Err(PyErr::from_value(err.bind($py).clone())) 34 | }) 35 | ); 36 | 37 | // --------------------------------------------------------------------------- 38 | 39 | /// A wrapper to convert `fastobo::error::Error` into a `PyErr`. 40 | pub struct Error { 41 | err: fastobo::error::Error, 42 | path: Option, 43 | } 44 | 45 | impl Error { 46 | pub fn with_path>(mut self, path: S) -> Self { 47 | self.path = Some(path.into()); 48 | self 49 | } 50 | } 51 | 52 | impl From for fastobo::error::Error { 53 | fn from(error: Error) -> Self { 54 | error.err 55 | } 56 | } 57 | 58 | impl From for Error { 59 | fn from(err: std::io::Error) -> Self { 60 | fastobo::error::Error::from(err).into() 61 | } 62 | } 63 | 64 | impl From for Error { 65 | fn from(err: fastobo::error::SyntaxError) -> Self { 66 | fastobo::error::Error::from(err).into() 67 | } 68 | } 69 | 70 | impl From for Error { 71 | fn from(err: fastobo::error::Error) -> Self { 72 | Self { err, path: None } 73 | } 74 | } 75 | 76 | impl From for PyErr { 77 | fn from(error: Error) -> Self { 78 | match error.err { 79 | fastobo::error::Error::SyntaxError { error } => match error { 80 | fastobo::error::SyntaxError::ParserError { error } => { 81 | let msg = error.variant.message().into_owned(); 82 | let path = error 83 | .path() 84 | .map(String::from) 85 | .unwrap_or_else(|| String::from("")); 86 | let line = error.line().to_string(); 87 | let (l, c) = match error.line_col { 88 | LineColLocation::Pos((l, c)) => (l, c), 89 | LineColLocation::Span((l, c), _) => (l, c), 90 | }; 91 | PySyntaxError::new_err((msg, (path, l, c, line))) 92 | } 93 | fastobo::error::SyntaxError::UnexpectedRule { expected, actual } => { 94 | PyRuntimeError::new_err("unexpected rule") 95 | } 96 | }, 97 | 98 | fastobo::error::Error::IOError { error: ioerror } => { 99 | let desc = ioerror.to_string(); 100 | match ioerror.raw_os_error() { 101 | Some(2) => PyFileNotFoundError::new_err((2, desc, error.path)), 102 | Some(code) => PyOSError::new_err((code, desc, error.path)), 103 | None => PyOSError::new_err((desc,)), 104 | } 105 | } 106 | 107 | fastobo::error::Error::CardinalityError { id, inner } => { 108 | let idstr = id.map(|ident| ident.to_string()); 109 | match inner { 110 | fastobo::error::CardinalityError::MissingClause { name } => { 111 | MissingClauseError::new_err((name, idstr)) 112 | } 113 | fastobo::error::CardinalityError::DuplicateClauses { name } => { 114 | DuplicateClausesError::new_err((name, idstr)) 115 | } 116 | fastobo::error::CardinalityError::SingleClause { name } => { 117 | SingleClauseError::new_err((name, idstr)) 118 | } 119 | } 120 | } 121 | 122 | fastobo::error::Error::ThreadingError { error } => match error { 123 | fastobo::error::ThreadingError::DisconnectedChannel => { 124 | DisconnectedChannelError::new_err(()) 125 | } 126 | }, // other => PyRuntimeError::new_err(format!("{}", other)), 127 | } 128 | } 129 | } 130 | 131 | impl Into> for Error { 132 | fn into(self) -> pyo3::PyResult { 133 | Err(pyo3::PyErr::from(self)) 134 | } 135 | } 136 | 137 | // --------------------------------------------------------------------------- 138 | 139 | /// A wrapper to convert `fastobo_graphs::error::Error` into a `PyErr`. 140 | pub struct GraphError(fastobo_graphs::error::Error); 141 | 142 | impl From for GraphError { 143 | fn from(e: fastobo_graphs::error::Error) -> Self { 144 | GraphError(e) 145 | } 146 | } 147 | 148 | impl From for PyErr { 149 | fn from(err: GraphError) -> Self { 150 | match err.0 { 151 | fastobo_graphs::error::Error::OboSyntaxError(error) => Error::from(error).into(), 152 | fastobo_graphs::error::Error::IOError(error) => { 153 | let desc = error.to_string(); 154 | match error.raw_os_error() { 155 | Some(code) => PyOSError::new_err((code, desc)), 156 | None => PyOSError::new_err((desc,)), 157 | } 158 | } 159 | other => PyValueError::new_err(other.to_string()), 160 | } 161 | } 162 | } 163 | 164 | // --------------------------------------------------------------------------- 165 | 166 | /// A wrapper to convert `fastobo_graphs::error::Error` into a `PyErr`. 167 | pub struct OwlError(fastobo_owl::Error); 168 | 169 | impl From for OwlError { 170 | fn from(e: fastobo_owl::Error) -> Self { 171 | OwlError(e) 172 | } 173 | } 174 | 175 | impl From for PyErr { 176 | fn from(err: OwlError) -> Self { 177 | match err.0 { 178 | fastobo_owl::Error::Cardinality(error) => { 179 | Error::from(fastobo::error::Error::CardinalityError { 180 | id: Some(obo::Ident::from(obo::UnprefixedIdent::new("header"))), 181 | inner: error, 182 | }) 183 | .into() 184 | } 185 | fastobo_owl::Error::Syntax(error) => Error::from(error).into(), 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/iter.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::convert::TryInto; 3 | use std::fs::File; 4 | use std::io::BufRead; 5 | use std::io::BufReader; 6 | use std::io::Error as IoError; 7 | use std::io::Read; 8 | use std::iter::Iterator; 9 | use std::num::NonZeroUsize; 10 | use std::ops::Deref; 11 | use std::ops::DerefMut; 12 | use std::path::Path; 13 | use std::path::PathBuf; 14 | 15 | use pyo3::exceptions::PyValueError; 16 | use pyo3::prelude::*; 17 | use pyo3::types::PyBytes; 18 | use pyo3::types::PyString; 19 | use pyo3::AsPyPointer; 20 | 21 | use fastobo::parser::Parser; 22 | use fastobo::parser::SequentialParser; 23 | use fastobo::parser::ThreadedParser; 24 | 25 | use crate::error::Error; 26 | use crate::py::doc::EntityFrame; 27 | use crate::py::header::frame::HeaderFrame; 28 | use crate::pyfile::PyFileGILRead; 29 | use crate::transmute_file_error; 30 | use crate::utils::ClonePy; 31 | use crate::utils::IntoPy; 32 | 33 | // --------------------------------------------------------------------------- 34 | 35 | /// An enum providing `Read` for either Python file-handles or filesystem files. 36 | pub enum Handle { 37 | FsFile(File, PathBuf), 38 | PyFile(PyFileGILRead), 39 | } 40 | 41 | impl Handle { 42 | fn handle(&self) -> PyObject { 43 | Python::with_gil(|py| match self { 44 | Handle::FsFile(_, path) => path.display().to_string().to_object(py), 45 | Handle::PyFile(f) => f.file().lock().unwrap().to_object(py), 46 | }) 47 | } 48 | } 49 | 50 | impl TryFrom for Handle { 51 | type Error = std::io::Error; 52 | fn try_from(p: PathBuf) -> Result { 53 | let file = File::open(&p)?; 54 | Ok(Handle::FsFile(file, p)) 55 | } 56 | } 57 | 58 | impl Read for Handle { 59 | fn read(&mut self, buf: &mut [u8]) -> Result { 60 | match self { 61 | Handle::FsFile(f, _) => f.read(buf), 62 | Handle::PyFile(f) => f.read(buf), 63 | } 64 | } 65 | } 66 | 67 | // --------------------------------------------------------------------------- 68 | 69 | /// An enum providing the same API for the sequential and threaded parsers from `fastobo`. 70 | pub enum InternalParser { 71 | Sequential(SequentialParser), 72 | Threaded(ThreadedParser), 73 | } 74 | 75 | impl InternalParser { 76 | pub fn with_thread_count(stream: B, n: i16) -> Result { 77 | match n { 78 | 0 => Ok(InternalParser::Threaded(ThreadedParser::new(stream))), 79 | 1 => Ok(InternalParser::Sequential(SequentialParser::new(stream))), 80 | n if n < 0 => Err(PyValueError::new_err( 81 | "threads count must be positive or null", 82 | )), 83 | n => { 84 | let t = std::num::NonZeroUsize::new(n as usize).unwrap(); 85 | Ok(InternalParser::Threaded(ThreadedParser::with_threads( 86 | stream, t, 87 | ))) 88 | } 89 | } 90 | } 91 | 92 | pub fn try_into_doc(&mut self) -> Result { 93 | match self { 94 | InternalParser::Sequential(parser) => parser.try_into(), 95 | InternalParser::Threaded(parser) => parser.try_into(), 96 | } 97 | } 98 | } 99 | 100 | impl AsMut for InternalParser { 101 | fn as_mut(&mut self) -> &mut B { 102 | match self { 103 | InternalParser::Sequential(parser) => parser.as_mut(), 104 | InternalParser::Threaded(parser) => parser.as_mut(), 105 | } 106 | } 107 | } 108 | 109 | impl AsRef for InternalParser { 110 | fn as_ref(&self) -> &B { 111 | match self { 112 | InternalParser::Sequential(parser) => parser.as_ref(), 113 | InternalParser::Threaded(parser) => parser.as_ref(), 114 | } 115 | } 116 | } 117 | 118 | impl From for InternalParser { 119 | fn from(stream: B) -> Self { 120 | Self::Sequential(SequentialParser::from(stream)) 121 | } 122 | } 123 | 124 | impl Iterator for InternalParser { 125 | type Item = fastobo::error::Result; 126 | fn next(&mut self) -> Option { 127 | match self { 128 | InternalParser::Sequential(parser) => parser.next(), 129 | InternalParser::Threaded(parser) => parser.next(), 130 | } 131 | } 132 | } 133 | 134 | impl Parser for InternalParser { 135 | fn new(stream: B) -> Self { 136 | InternalParser::Sequential(SequentialParser::new(stream)) 137 | } 138 | 139 | fn with_threads(stream: B, threads: NonZeroUsize) -> Self { 140 | if threads.get() == 1 { 141 | Self::new(stream) 142 | } else { 143 | InternalParser::Threaded(ThreadedParser::with_threads(stream, threads)) 144 | } 145 | } 146 | 147 | fn ordered(&mut self, ordered: bool) -> &mut Self { 148 | match self { 149 | InternalParser::Sequential(parser) => { 150 | parser.ordered(ordered); 151 | } 152 | InternalParser::Threaded(parser) => { 153 | parser.ordered(ordered); 154 | } 155 | }; 156 | self 157 | } 158 | 159 | fn into_inner(self) -> B { 160 | match self { 161 | InternalParser::Sequential(parser) => parser.into_inner(), 162 | InternalParser::Threaded(parser) => parser.into_inner(), 163 | } 164 | } 165 | } 166 | 167 | // --------------------------------------------------------------------------- 168 | 169 | // FIXME: May cause memory leaks? 170 | /// An iterator over the frames of an OBO document. 171 | /// 172 | /// See help(fastobo.iter) for more information. 173 | #[pyclass(module = "fastobo")] 174 | pub struct FrameReader { 175 | inner: InternalParser>, 176 | header: Py, 177 | } 178 | 179 | impl FrameReader { 180 | fn new(handle: BufReader, ordered: bool, threads: i16) -> PyResult { 181 | let mut inner = InternalParser::with_thread_count(handle, threads)?; 182 | inner.ordered(ordered); 183 | let frame = inner 184 | .next() 185 | .unwrap() 186 | .map_err(Error::from)? 187 | .into_header() 188 | .unwrap(); 189 | let header = Python::with_gil(|py| Py::new(py, frame.into_py(py)))?; 190 | Ok(Self { inner, header }) 191 | } 192 | 193 | pub fn from_path>(path: P, ordered: bool, threads: i16) -> PyResult { 194 | let p = path.as_ref(); 195 | match Handle::try_from(p.to_owned()) { 196 | Ok(inner) => Self::new(BufReader::new(inner), ordered, threads), 197 | Err(e) => Error::from(e).with_path(p.display().to_string()).into(), 198 | } 199 | } 200 | 201 | pub fn from_handle<'py>( 202 | obj: &Bound<'py, PyAny>, 203 | ordered: bool, 204 | threads: i16, 205 | ) -> PyResult { 206 | let py = obj.py(); 207 | match PyFileGILRead::from_ref(obj).map(Handle::PyFile) { 208 | Ok(inner) => Self::new(BufReader::new(inner), ordered, threads), 209 | Err(e) => Err(e), 210 | } 211 | } 212 | } 213 | 214 | #[pymethods] 215 | impl FrameReader { 216 | fn __repr__(&self) -> PyResult { 217 | Python::with_gil(|py| { 218 | let fmt = PyString::new(py, "fastobo.iter({!r})").to_object(py); 219 | fmt.call_method1(py, "format", (&self.inner.as_ref().get_ref().handle(),)) 220 | }) 221 | } 222 | 223 | fn __iter__(slf: PyRefMut<'_, Self>) -> PyResult> { 224 | Ok(slf) 225 | } 226 | 227 | fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult> { 228 | match slf.deref_mut().inner.next() { 229 | None => Ok(None), 230 | Some(Ok(frame)) => { 231 | let entity = frame.into_entity().unwrap(); 232 | Ok(Some(Python::with_gil(|py| entity.into_py(py)))) 233 | } 234 | Some(Err(e)) => Python::with_gil(|py| { 235 | if PyErr::occurred(py) { 236 | Err(PyErr::fetch(py)) 237 | } else { 238 | Err(Error::from(e).into()) 239 | } 240 | }), 241 | } 242 | } 243 | 244 | fn header<'py>(&self, py: Python<'py>) -> Py { 245 | self.header.clone_py(py) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | // #![allow(unused_imports, unused_variables)] 3 | #![allow(unused, dead_code, deprecated)] 4 | 5 | extern crate fastobo; 6 | extern crate pyo3; 7 | #[macro_use] 8 | extern crate pyo3_built; 9 | extern crate libc; 10 | #[macro_use] 11 | extern crate fastobo_py_derive_internal; 12 | extern crate fastobo_graphs; 13 | extern crate fastobo_owl; 14 | extern crate horned_owl; 15 | 16 | #[macro_use] 17 | pub mod macros; 18 | pub mod built; 19 | pub mod date; 20 | pub mod error; 21 | pub mod iter; 22 | pub mod py; 23 | pub mod pyfile; 24 | pub mod utils; 25 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! impl_hash { 2 | ($($field:expr),*) => ({ 3 | use std::hash::Hasher; 4 | use std::hash::Hash; 5 | let mut hasher = crate::utils::Hasher::default(); 6 | $($field.hash(&mut hasher);)* 7 | hasher.finish() 8 | }); 9 | } 10 | 11 | macro_rules! impl_richcmp { 12 | ($self:ident, $other:ident, $op:ident, $(self . $attr:ident)&&*) => ({ 13 | match $op { 14 | $crate::pyo3::class::basic::CompareOp::Eq => { 15 | let py = $other.py(); 16 | if let Ok(ref clause) = $other.extract::>() { 17 | let clause = clause.bind(py).borrow(); 18 | let res = $($self.$attr == clause.$attr)&&*; 19 | Ok(res.to_object(py)) 20 | } else { 21 | Ok(false.to_object(py)) 22 | } 23 | } 24 | _ => Ok($other.py().NotImplemented()) 25 | } 26 | }); 27 | } 28 | 29 | macro_rules! impl_richcmp_py { 30 | ($self:ident, $other:ident, $op:ident, $(self . $attr:ident)&&*) => ({ 31 | match $op { 32 | $crate::pyo3::class::basic::CompareOp::Eq => { 33 | let py = $other.py(); 34 | if let Ok(ref clause) = $other.extract::>() { 35 | let clause = clause.bind(py).borrow(); 36 | let res = $($self.$attr.eq_py(&clause.$attr, py))&&*; 37 | Ok(res.to_object(py)) 38 | } else { 39 | Ok(false.to_object(py)) 40 | } 41 | } 42 | _ => Ok($other.py().NotImplemented()) 43 | } 44 | }); 45 | } 46 | 47 | macro_rules! impl_repr { 48 | ($self:ident, $cls:ident($($field:expr),*)) => ({ 49 | Python::with_gil(|py| { 50 | let args = &[ 51 | $((&$field).into_pyobject(py)?.as_any().repr()?.to_str()?,)* 52 | ].join(", "); 53 | Ok(PyString::new(py, &format!("{}({})", stringify!($cls), args)).to_object(py)) 54 | }) 55 | }) 56 | } 57 | 58 | macro_rules! register { 59 | ($py:ident, $m:ident, $cls:ident, $module:expr, $metacls:ident) => { 60 | $py.import($module)? 61 | .getattr(stringify!($metacls))? 62 | .to_object($py) 63 | .call_method1($py, "register", ($m.getattr(stringify!($cls))?,))?; 64 | }; 65 | } 66 | 67 | macro_rules! add_submodule { 68 | ($py:ident, $sup:ident, $sub:ident) => {{ 69 | use super::*; 70 | 71 | // create new module object and initialize it 72 | let module = PyModule::new($py, stringify!($ub))?; 73 | self::$sub::init($py, &module)?; 74 | module.add("__package__", $sup.getattr("__package__")?)?; 75 | 76 | // add the submodule to the supermodule 77 | $sup.add(stringify!($sub), &module)?; 78 | 79 | // add the submodule to the `sys.modules` index 80 | $py.import("sys")? 81 | .getattr("modules")? 82 | .downcast::()? 83 | .set_item(concat!("fastobo.", stringify!($sub)), &module)?; 84 | }}; 85 | } 86 | -------------------------------------------------------------------------------- /src/py/abc.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::fmt::Formatter; 3 | use std::fmt::Result as FmtResult; 4 | use std::rc::Rc; 5 | use std::str::FromStr; 6 | use std::string::ToString; 7 | 8 | use pyo3::class::gc::PyVisit; 9 | use pyo3::exceptions::PyNotImplementedError; 10 | use pyo3::gc::PyTraverseError; 11 | use pyo3::prelude::*; 12 | use pyo3::types::PyAny; 13 | use pyo3::types::PyList; 14 | use pyo3::types::PyString; 15 | use pyo3::PyTypeInfo; 16 | 17 | use fastobo::ast as obo; 18 | 19 | use crate::error::Error; 20 | use crate::utils::AbstractClass; 21 | use crate::utils::ClonePy; 22 | 23 | // use super::header::frame::HeaderFrame; 24 | use super::id::BaseIdent; 25 | use super::id::Ident; 26 | // use super::term::frame::TermFrame; 27 | // use super::typedef::frame::TypedefFrame; 28 | 29 | // --- Module export --------------------------------------------------------- 30 | 31 | /// Base Classes defining common interfaces for classes in this library. 32 | /// 33 | /// These base classes are here to define common methods and attributes shared 34 | /// by numerous classes in the ``fastobo`` submodules. Since Rust is a 35 | /// statically-typed language, all "subclasses" are known at compile-time, so 36 | /// creating new subclasses hoping to use them with the current classes (and 37 | /// in particular, collections such as `~fastobo.doc.OboDoc`) will not work, 38 | /// and is likely to cause an undefined behaviour. 39 | /// 40 | #[pymodule] 41 | #[pyo3(name = "abc")] 42 | pub fn init<'py>(_py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 43 | m.add_class::()?; 44 | m.add_class::()?; 45 | m.add_class::()?; 46 | m.add_class::()?; 47 | m.add("__name__", "fastobo.abc")?; 48 | Ok(()) 49 | } 50 | 51 | // --- 52 | 53 | /// An abstract OBO frame, storing a sequence of various clauses. 54 | /// 55 | /// An OBO document contains a header frame (which may be empty, but should 56 | /// at least contain a `~fastobo.header.FormatVersionClause` and a 57 | /// `~fastobo.header.OntologyClause` for compatibility purposes), followed by 58 | /// a various number of entity frames. 59 | #[pyclass(subclass, module = "fastobo.abc")] 60 | #[derive(Default)] 61 | pub struct AbstractFrame {} 62 | 63 | impl AbstractClass for AbstractFrame { 64 | fn initializer() -> PyClassInitializer { 65 | PyClassInitializer::from(Self {}) 66 | } 67 | } 68 | 69 | /// An abstract entity frame, which clauses define an entity. 70 | /// 71 | /// Entity frames define OBO entities, which can be classes (terms), 72 | /// relations (typedefs) and instances. All OBO entities have an identifier, 73 | /// which is supposedly unique, that can be accessed through the ``id`` 74 | /// property in any concrete subclass. 75 | #[pyclass(subclass, extends=AbstractFrame, module="fastobo.abc")] 76 | #[derive(Default, AbstractClass)] 77 | #[base(AbstractFrame)] 78 | pub struct AbstractEntityFrame {} 79 | 80 | #[pymethods] 81 | impl AbstractEntityFrame { 82 | /// `~fastobo.id.Ident`: the identifier of the described entity. 83 | #[getter] 84 | pub fn get_id(&self) -> PyResult { 85 | Err(PyNotImplementedError::new_err( 86 | "AbstractEntityFrame.raw_tag", 87 | )) 88 | } 89 | } 90 | 91 | // --- 92 | 93 | /// An abstract clause. 94 | /// 95 | /// An OBO clause is a tag/value pair, with additional syntax requirements 96 | /// depending on the tag. The raw tag and raw value of an OBO clause can be 97 | /// accessed with the `raw_tag` and `raw_value` methods, for instance to 98 | /// convert a frame into a Python `dict`. 99 | /// 100 | /// Example: 101 | /// >>> d = {} 102 | /// >>> for clause in ms[1]: 103 | /// ... d.setdefault(clause.raw_tag(), []).append(clause.raw_value()) 104 | /// >>> pprint(d) 105 | /// {'def': ['"A reference number relevant to the sample under study."'], 106 | /// 'is_a': ['MS:1000548'], 107 | /// 'name': ['sample number'], 108 | /// 'xref': ['value-type:xsd\\:string "The allowed value-type for this CV term."']} 109 | /// 110 | #[pyclass(subclass, module = "fastobo.abc")] 111 | #[derive(Default)] 112 | pub struct AbstractClause {} 113 | 114 | impl AbstractClass for AbstractClause { 115 | fn initializer() -> PyClassInitializer { 116 | PyClassInitializer::from(Self {}) 117 | } 118 | } 119 | 120 | #[pymethods] 121 | impl AbstractClause { 122 | /// Get the raw tag of the header clause. 123 | /// 124 | /// Returns: 125 | /// `str`: the header clause value as it was extracted from the OBO 126 | /// header, stripped from trailing qualifiers and comment. 127 | /// 128 | /// Example: 129 | /// >>> clause = fastobo.header.OntologyClause("test") 130 | /// >>> clause.raw_tag() 131 | /// 'ontology' 132 | /// >>> str(clause) 133 | /// 'ontology: test' 134 | pub fn raw_tag(&self) -> PyResult { 135 | Err(PyNotImplementedError::new_err("BaseHeaderClause.raw_tag")) 136 | } 137 | 138 | /// Get the raw value of the header clause. 139 | /// 140 | /// Returns: 141 | /// `str`: the header clause value as it was extracted from the OBO 142 | /// header, stripped from trailing qualifiers and comment. 143 | /// 144 | /// Example: 145 | /// >>> dt = datetime.datetime(2019, 4, 29, 21, 52) 146 | /// >>> clause = fastobo.header.DateClause(dt) 147 | /// >>> clause.date 148 | /// datetime.datetime(2019, 4, 29, 21, 52) 149 | /// >>> clause.raw_value() 150 | /// '29:04:2019 21:52' 151 | pub fn raw_value(&self) -> PyResult { 152 | Err(PyNotImplementedError::new_err("BaseHeaderClause.raw_value")) 153 | } 154 | } 155 | 156 | /// An abstract entity clause. 157 | #[pyclass(subclass, extends=AbstractClause, module="fastobo.abc")] 158 | #[derive(Default, AbstractClass)] 159 | #[base(AbstractClause)] 160 | pub struct AbstractEntityClause {} 161 | -------------------------------------------------------------------------------- /src/py/exceptions.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyChildProcessError; 2 | use pyo3::exceptions::PyRuntimeError; 3 | use pyo3::exceptions::PyValueError; 4 | use pyo3::prelude::*; 5 | use pyo3::types::PyString; 6 | use pyo3::types::PyTuple; 7 | 8 | // --- Macros ---------------------------------------------------------------- 9 | 10 | macro_rules! impl_pyerr { 11 | ($name:ident) => { 12 | impl $name { 13 | /// Creates a new [`PyErr`] of this type. 14 | /// 15 | /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" 16 | #[inline] 17 | pub fn new_err(args: A) -> pyo3::PyErr 18 | where 19 | A: pyo3::PyErrArguments + Send + Sync + 'static, 20 | { 21 | pyo3::PyErr::new::<$name, A>(args) 22 | } 23 | } 24 | }; 25 | } 26 | 27 | // --- Module export --------------------------------------------------------- 28 | 29 | #[pymodule] 30 | #[pyo3(name = "exceptions")] 31 | pub fn init<'py>(py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 32 | m.add_class::()?; 33 | m.add_class::()?; 34 | m.add_class::()?; 35 | m.add_class::()?; 36 | m.add("__name__", "fastobo.exceptions")?; 37 | Ok(()) 38 | } 39 | 40 | // --- MissingClauseError ---------------------------------------------------- 41 | 42 | /// An error indicating a required clause is missing. 43 | #[pyclass(module = "fastobo.exceptions", extends = PyValueError)] 44 | pub struct MissingClauseError { 45 | clause: String, 46 | frame: Option, 47 | } 48 | 49 | impl_pyerr!(MissingClauseError); 50 | 51 | #[pymethods] 52 | impl MissingClauseError { 53 | #[new] 54 | #[pyo3(signature = (clause, frame = None))] 55 | fn __init__(clause: String, frame: Option) -> Self { 56 | Self { clause, frame } 57 | } 58 | 59 | fn __repr__(&self) -> String { 60 | match &self.frame { 61 | None => format!("MissingClauseError({})", self.clause.as_str()), 62 | Some(f) => format!("MissingClauseError({}, {})", self.clause.as_str(), f), 63 | } 64 | } 65 | 66 | fn __str__(&self) -> String { 67 | match &self.frame { 68 | None => format!("missing '{}' clause", &self.clause), 69 | Some(f) => format!("missing '{}' clause in '{}' frame", &self.clause, &f), 70 | } 71 | } 72 | } 73 | 74 | // --- DuplicateClausesError ------------------------------------------------- 75 | 76 | /// An error indicating a unique clause appears more than one. 77 | #[pyclass(module = "fastobo.exceptions", extends = PyValueError)] 78 | pub struct DuplicateClausesError { 79 | clause: String, 80 | frame: Option, 81 | } 82 | 83 | impl_pyerr!(DuplicateClausesError); 84 | 85 | #[pymethods] 86 | impl DuplicateClausesError { 87 | #[new] 88 | #[pyo3(signature = (clause, frame = None))] 89 | fn __init__(clause: String, frame: Option) -> Self { 90 | Self { clause, frame } 91 | } 92 | 93 | fn __repr__(&self) -> String { 94 | match &self.frame { 95 | None => format!("DuplicateClausesError({})", self.clause.as_str()), 96 | Some(f) => format!("DuplicateClausesError({}, {})", self.clause.as_str(), f), 97 | } 98 | } 99 | 100 | fn __str__(&self) -> String { 101 | match &self.frame { 102 | None => format!("duplicate '{}' clauses", &self.clause), 103 | Some(f) => format!("duplicate '{}' clauses in '{}' frame", &self.clause, &f), 104 | } 105 | } 106 | } 107 | 108 | // --- SingleClauseError ----------------------------------------------------- 109 | 110 | /// An error indicating a clause appears only once when it shouldn't. 111 | #[pyclass(module = "fastobo.exceptions", extends = PyValueError)] 112 | pub struct SingleClauseError { 113 | clause: String, 114 | frame: Option, 115 | } 116 | 117 | impl_pyerr!(SingleClauseError); 118 | 119 | #[pymethods] 120 | impl SingleClauseError { 121 | #[new] 122 | #[pyo3(signature = (clause, frame = None))] 123 | fn __init__(clause: String, frame: Option) -> Self { 124 | Self { clause, frame } 125 | } 126 | 127 | fn __repr__(&self) -> String { 128 | match &self.frame { 129 | None => format!("SingleClauseError({})", self.clause.as_str()), 130 | Some(f) => format!("SingleClauseError({}, {})", self.clause.as_str(), f), 131 | } 132 | } 133 | 134 | fn __str__(&self) -> String { 135 | match &self.frame { 136 | None => format!("single '{}' clause", &self.clause), 137 | Some(f) => format!("single '{}' clause in '{}' frame", &self.clause, &f), 138 | } 139 | } 140 | } 141 | 142 | // --- DisconnectedChannelError ---------------------------------------------- 143 | 144 | #[pyclass(module = "fastobo.exceptions", extends = PyRuntimeError)] 145 | pub struct DisconnectedChannelError {} 146 | 147 | impl_pyerr!(DisconnectedChannelError); 148 | 149 | #[pymethods] 150 | impl DisconnectedChannelError { 151 | #[new] 152 | fn __init__() -> Self { 153 | Self {} 154 | } 155 | 156 | fn __repr__(&self) -> String { 157 | String::from("DisconnectedChannelError()") 158 | } 159 | 160 | fn __str__(&self) -> String { 161 | String::from("disconnected thread communication channel") 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/py/header/frame.rs: -------------------------------------------------------------------------------- 1 | use std::iter::FromIterator; 2 | use std::iter::IntoIterator; 3 | 4 | use fastobo::ast as obo; 5 | use pyo3::class::gc::PyVisit; 6 | use pyo3::exceptions::PyIndexError; 7 | use pyo3::gc::PyTraverseError; 8 | use pyo3::prelude::*; 9 | use pyo3::types::PyAny; 10 | use pyo3::types::PyIterator; 11 | use pyo3::types::PyList; 12 | use pyo3::types::PyString; 13 | use pyo3::AsPyPointer; 14 | use pyo3::PyTypeInfo; 15 | 16 | use super::super::abc::AbstractFrame; 17 | use super::clause::BaseHeaderClause; 18 | use super::clause::HeaderClause; 19 | use crate::utils::AbstractClass; 20 | use crate::utils::ClonePy; 21 | use crate::utils::EqPy; 22 | use crate::utils::FinalClass; 23 | use crate::utils::IntoPy; 24 | 25 | #[pyclass(extends=AbstractFrame, module="fastobo.header")] 26 | #[derive(Debug, FinalClass, EqPy)] 27 | #[base(AbstractFrame)] 28 | pub struct HeaderFrame { 29 | clauses: Vec, 30 | } 31 | 32 | impl HeaderFrame { 33 | pub fn empty() -> Self { 34 | Self::new(Vec::new()) 35 | } 36 | 37 | pub fn new(clauses: Vec) -> Self { 38 | Self { clauses } 39 | } 40 | } 41 | 42 | impl ClonePy for HeaderFrame { 43 | fn clone_py(&self, py: Python) -> Self { 44 | Self { 45 | clauses: self.clauses.clone_py(py), 46 | } 47 | } 48 | } 49 | 50 | impl FromIterator for HeaderFrame { 51 | fn from_iter(iter: T) -> Self 52 | where 53 | T: IntoIterator, 54 | { 55 | Self::new(iter.into_iter().collect()) 56 | } 57 | } 58 | 59 | impl IntoPy for fastobo::ast::HeaderFrame { 60 | fn into_py(self, py: Python) -> HeaderFrame { 61 | self.into_iter().map(|clause| clause.into_py(py)).collect() 62 | } 63 | } 64 | 65 | impl IntoPy for HeaderFrame { 66 | fn into_py(self, py: Python) -> obo::HeaderFrame { 67 | self.clauses 68 | .into_iter() 69 | .map(|clause| clause.into_py(py)) 70 | .collect() 71 | } 72 | } 73 | 74 | // impl ToPyObject for HeaderFrame { 75 | // fn to_object(&self, py: Python) -> PyObject { 76 | // IntoPy::into_py(PyList::new(py, &self.clauses), py) 77 | // } 78 | // } 79 | 80 | #[listlike(field = "clauses", type = "HeaderClause")] 81 | #[pymethods] 82 | impl HeaderFrame { 83 | #[new] 84 | #[pyo3(signature = (clauses = None))] 85 | pub fn __init__<'py>( 86 | clauses: Option<&Bound<'py, PyAny>>, 87 | ) -> PyResult> { 88 | let mut vec = Vec::new(); 89 | if let Some(c) = clauses { 90 | for item in PyIterator::from_object(c)? { 91 | vec.push(HeaderClause::extract_bound(&item?)?); 92 | } 93 | } 94 | Ok(Self::new(vec).into()) 95 | } 96 | 97 | fn __repr__(&self) -> PyResult { 98 | impl_repr!(self, HeaderFrame(self.clauses)) 99 | } 100 | 101 | fn __str__(&self) -> PyResult { 102 | let frame: obo::HeaderFrame = Python::with_gil(|py| self.clone_py(py).into_py(py)); 103 | Ok(frame.to_string()) 104 | } 105 | 106 | fn __len__(&self) -> PyResult { 107 | Ok(self.clauses.len()) 108 | } 109 | 110 | fn __getitem__(&self, index: isize) -> PyResult> { 111 | if index < self.clauses.len() as isize { 112 | Python::with_gil(|py| { 113 | let item = &self.clauses[index as usize]; 114 | Ok(item.into_pyobject(py)?.unbind()) 115 | }) 116 | } else { 117 | Err(PyIndexError::new_err("list index out of range")) 118 | } 119 | } 120 | 121 | fn __setitem__<'py>(&mut self, index: isize, elem: &Bound<'py, PyAny>) -> PyResult<()> { 122 | if index as usize > self.clauses.len() { 123 | return Err(PyIndexError::new_err("list index out of range")); 124 | } 125 | let clause = HeaderClause::extract_bound(elem)?; 126 | self.clauses[index as usize] = clause; 127 | Ok(()) 128 | } 129 | 130 | fn __delitem__(&mut self, index: isize) -> PyResult<()> { 131 | if index as usize > self.clauses.len() { 132 | return Err(PyIndexError::new_err("list index out of range")); 133 | } 134 | self.clauses.remove(index as usize); 135 | Ok(()) 136 | } 137 | 138 | fn __concat__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { 139 | let py = other.py(); 140 | 141 | let iterator = PyIterator::from_object(other)?; 142 | let mut new_clauses = self.clauses.clone_py(py); 143 | for item in iterator { 144 | new_clauses.push(HeaderClause::extract_bound(&item?)?); 145 | } 146 | 147 | let init = PyClassInitializer::from(AbstractFrame {}).add_subclass(Self::new(new_clauses)); 148 | Bound::new(py, init) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/py/header/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clause; 2 | pub mod frame; 3 | 4 | use pyo3::prelude::*; 5 | 6 | #[pymodule] 7 | #[pyo3(name = "header")] 8 | pub fn init<'py>(py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 9 | m.add_class::()?; 10 | m.add_class::()?; 11 | m.add_class::()?; 12 | m.add_class::()?; 13 | m.add_class::()?; 14 | m.add_class::()?; 15 | m.add_class::()?; 16 | m.add_class::()?; 17 | m.add_class::()?; 18 | m.add_class::()?; 19 | m.add_class::()?; 20 | m.add_class::()?; 21 | m.add_class::()?; 22 | m.add_class::()?; 23 | m.add_class::()?; 24 | m.add_class::()?; 25 | m.add_class::()?; 26 | m.add_class::()?; 27 | m.add_class::()?; 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | m.add_class::()?; 32 | m.add_class::()?; 33 | 34 | register!(py, m, HeaderFrame, "collections.abc", MutableSequence); 35 | 36 | m.add("__name__", "fastobo.header")?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/py/instance/clause.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/py/instance/frame.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::fmt::Formatter; 3 | use std::fmt::Result as FmtResult; 4 | use std::fmt::Write; 5 | use std::str::FromStr; 6 | 7 | use pyo3::prelude::*; 8 | use pyo3::types::PyAny; 9 | use pyo3::types::PyIterator; 10 | use pyo3::types::PyString; 11 | use pyo3::AsPyPointer; 12 | use pyo3::PyTypeInfo; 13 | 14 | use fastobo::ast; 15 | 16 | use super::super::abc::AbstractEntityFrame; 17 | use super::super::id::Ident; 18 | use crate::utils::AbstractClass; 19 | use crate::utils::ClonePy; 20 | use crate::utils::EqPy; 21 | use crate::utils::FinalClass; 22 | use crate::utils::IntoPy; 23 | 24 | #[pyclass(extends=AbstractEntityFrame, module="fastobo.instance")] 25 | #[derive(Debug, FinalClass, EqPy)] 26 | #[base(AbstractEntityFrame)] 27 | pub struct InstanceFrame { 28 | id: Ident, 29 | //clauses: Vec, 30 | } 31 | 32 | impl InstanceFrame { 33 | pub fn new(id: Ident) -> Self { 34 | // Self::with_clauses(id, Vec::new()) 35 | Self { id } 36 | } 37 | 38 | // pub fn with_clauses(id: Ident, clauses: Vec) -> Self { 39 | // Self { id, clauses } 40 | // } 41 | } 42 | 43 | impl ClonePy for InstanceFrame { 44 | fn clone_py(&self, py: Python) -> Self { 45 | Self { 46 | id: self.id.clone_py(py), 47 | // clauses: self.clauses.clone_py(py), 48 | } 49 | } 50 | } 51 | 52 | impl Display for InstanceFrame { 53 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 54 | let frame: fastobo::ast::InstanceFrame = 55 | Python::with_gil(|py| self.clone_py(py).into_py(py)); 56 | frame.fmt(f) 57 | } 58 | } 59 | 60 | impl IntoPy for fastobo::ast::InstanceFrame { 61 | fn into_py(self, py: Python) -> InstanceFrame { 62 | let id: Ident = self.id().as_ref().clone().into_py(py); 63 | InstanceFrame::new(id) 64 | } 65 | } 66 | 67 | impl IntoPy for InstanceFrame { 68 | fn into_py(self, py: Python) -> fastobo::ast::InstanceFrame { 69 | fastobo::ast::InstanceFrame::new(fastobo::ast::InstanceIdent::new(self.id.into_py(py))) 70 | } 71 | } 72 | 73 | impl IntoPy for InstanceFrame { 74 | fn into_py(self, py: Python) -> fastobo::ast::EntityFrame { 75 | let frame: fastobo::ast::InstanceFrame = self.into_py(py); 76 | fastobo::ast::EntityFrame::from(frame) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/py/instance/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clause; 2 | pub mod frame; 3 | 4 | use pyo3::prelude::*; 5 | 6 | #[pymodule] 7 | #[pyo3(name = "instance")] 8 | pub fn init<'py>(py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 9 | m.add_class::()?; 10 | 11 | register!(py, m, InstanceFrame, "collections.abc", MutableSequence); 12 | 13 | m.add("__name__", "fastobo.instance")?; 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /src/py/pv.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::fmt::Formatter; 3 | use std::fmt::Result as FmtResult; 4 | use std::ops::Deref; 5 | use std::str::FromStr; 6 | 7 | use pyo3::exceptions::PyTypeError; 8 | use pyo3::prelude::*; 9 | use pyo3::types::PyAny; 10 | use pyo3::types::PyString; 11 | use pyo3::PyTypeInfo; 12 | 13 | use fastobo::ast; 14 | 15 | use super::id::Ident; 16 | use crate::utils::AbstractClass; 17 | use crate::utils::ClonePy; 18 | use crate::utils::EqPy; 19 | use crate::utils::FinalClass; 20 | use crate::utils::IntoPy; 21 | 22 | // --- Module export --------------------------------------------------------- 23 | 24 | #[pymodule] 25 | #[pyo3(name = "pv")] 26 | pub fn init<'py>(_py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 27 | m.add_class::()?; 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add("__name__", "fastobo.pv")?; 31 | Ok(()) 32 | } 33 | 34 | // --- Conversion Wrapper ---------------------------------------------------- 35 | 36 | #[derive(ClonePy, Debug, PyWrapper, EqPy)] 37 | #[wraps(AbstractPropertyValue)] 38 | pub enum PropertyValue { 39 | Literal(Py), 40 | Resource(Py), 41 | } 42 | 43 | impl Display for PropertyValue { 44 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 45 | Python::with_gil(|py| match self { 46 | PropertyValue::Literal(lpv) => lpv.bind(py).borrow().fmt(f), 47 | PropertyValue::Resource(rpv) => rpv.bind(py).borrow().fmt(f), 48 | }) 49 | } 50 | } 51 | 52 | impl IntoPy for fastobo::ast::PropertyValue { 53 | fn into_py(self, py: Python) -> PropertyValue { 54 | match self { 55 | fastobo::ast::PropertyValue::Literal(lpv) => { 56 | Py::new(py, lpv.into_py(py)).map(PropertyValue::Literal) 57 | } 58 | fastobo::ast::PropertyValue::Resource(rpv) => { 59 | Py::new(py, rpv.into_py(py)).map(PropertyValue::Resource) 60 | } 61 | } 62 | .expect("could not allocate on Python heap") 63 | } 64 | } 65 | 66 | impl IntoPy for PropertyValue { 67 | fn into_py(self, py: Python) -> fastobo::ast::PropertyValue { 68 | match self { 69 | PropertyValue::Literal(t) => t.bind(py).borrow().deref().clone_py(py).into_py(py), 70 | PropertyValue::Resource(r) => r.bind(py).borrow().deref().clone_py(py).into_py(py), 71 | } 72 | } 73 | } 74 | 75 | // --- Base ------------------------------------------------------------------ 76 | 77 | #[pyclass(subclass, module = "fastobo.pv")] 78 | #[derive(Debug, Default)] 79 | pub struct AbstractPropertyValue {} 80 | 81 | impl AbstractClass for AbstractPropertyValue { 82 | fn initializer() -> PyClassInitializer { 83 | PyClassInitializer::from(Self {}) 84 | } 85 | } 86 | 87 | // --- Literal ----------------------------------------------------------------- 88 | 89 | #[pyclass(extends=AbstractPropertyValue, module="fastobo.pv")] 90 | #[derive(Debug, FinalClass, EqPy)] 91 | #[base(AbstractPropertyValue)] 92 | pub struct LiteralPropertyValue { 93 | relation: Ident, 94 | value: ast::QuotedString, 95 | datatype: Ident, 96 | } 97 | 98 | impl LiteralPropertyValue { 99 | pub fn new(relation: Ident, value: fastobo::ast::QuotedString, datatype: Ident) -> Self { 100 | LiteralPropertyValue { 101 | relation, 102 | value, 103 | datatype, 104 | } 105 | } 106 | } 107 | 108 | impl ClonePy for LiteralPropertyValue { 109 | fn clone_py(&self, py: Python) -> Self { 110 | Self { 111 | relation: self.relation.clone_py(py), 112 | value: self.value.clone(), 113 | datatype: self.datatype.clone_py(py), 114 | } 115 | } 116 | } 117 | 118 | impl Display for LiteralPropertyValue { 119 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 120 | let pv: fastobo::ast::PropertyValue = Python::with_gil(|py| self.clone_py(py).into_py(py)); 121 | pv.fmt(f) 122 | } 123 | } 124 | 125 | impl IntoPy for LiteralPropertyValue { 126 | fn into_py(self, py: Python) -> fastobo::ast::LiteralPropertyValue { 127 | fastobo::ast::LiteralPropertyValue::new( 128 | self.relation.into_py(py), 129 | self.value, 130 | self.datatype.into_py(py), 131 | ) 132 | } 133 | } 134 | 135 | impl IntoPy for LiteralPropertyValue { 136 | fn into_py(self, py: Python) -> fastobo::ast::PropertyValue { 137 | fastobo::ast::PropertyValue::Literal(Box::new(self.into_py(py))) 138 | } 139 | } 140 | 141 | impl IntoPy for fastobo::ast::LiteralPropertyValue { 142 | fn into_py(mut self, py: Python) -> LiteralPropertyValue { 143 | let value = std::mem::take(self.literal_mut()); 144 | let datatype = self.datatype().clone().into_py(py); 145 | let relation = self.property().clone().into_py(py); 146 | LiteralPropertyValue::new(relation, value, datatype) 147 | } 148 | } 149 | 150 | #[pymethods] 151 | impl LiteralPropertyValue { 152 | #[new] 153 | fn __init__<'py>( 154 | relation: Bound<'py, PyAny>, 155 | value: Bound<'py, PyAny>, 156 | datatype: Bound<'py, PyAny>, 157 | ) -> PyResult> { 158 | let r = relation.extract::()?; 159 | let v = if let Ok(s) = value.downcast::() { 160 | ast::QuotedString::new(s.to_str()?.to_string()) 161 | } else { 162 | let n = value.get_type().name()?; 163 | let msg = format!("expected str for value, found {}", n); 164 | return Err(PyTypeError::new_err(msg)); 165 | }; 166 | let dt = datatype.extract::()?; 167 | Ok(Self::new(r, v, dt).into()) 168 | } 169 | 170 | fn __repr__<'py>(&self, py: Python<'py>) -> PyResult> { 171 | let fmt = PyString::new(py, "LiteralPropertyValue({!r}, {!r}, {!r})"); 172 | fmt.call_method1( 173 | "format", 174 | (&self.relation, self.value.as_str(), &self.datatype), 175 | ) 176 | } 177 | 178 | fn __str__(&self) -> PyResult { 179 | let pv: fastobo::ast::PropertyValue = Python::with_gil(|py| self.clone_py(py).into_py(py)); 180 | Ok(pv.to_string()) 181 | } 182 | 183 | #[getter] 184 | fn get_relation(&self) -> PyResult<&Ident> { 185 | Ok(&self.relation) 186 | } 187 | 188 | #[setter] 189 | fn set_relation(&mut self, relation: Ident) -> PyResult<()> { 190 | self.relation = relation; 191 | Ok(()) 192 | } 193 | 194 | #[getter] 195 | fn get_value(&self) -> PyResult<&str> { 196 | Ok(self.value.as_str()) 197 | } 198 | 199 | #[setter] 200 | fn set_value(&mut self, value: String) -> PyResult<()> { 201 | self.value = fastobo::ast::QuotedString::new(value); 202 | Ok(()) 203 | } 204 | 205 | #[getter] 206 | fn get_datatype(&self) -> PyResult<&Ident> { 207 | Ok(&self.datatype) 208 | } 209 | 210 | #[setter] 211 | fn set_datatype(&mut self, datatype: Ident) -> PyResult<()> { 212 | self.datatype = datatype; 213 | Ok(()) 214 | } 215 | } 216 | 217 | // --- Resource ------------------------------------------------------------ 218 | 219 | #[pyclass(extends=AbstractPropertyValue, module="fastobo.pv")] 220 | #[derive(Debug, FinalClass, EqPy)] 221 | #[base(AbstractPropertyValue)] 222 | pub struct ResourcePropertyValue { 223 | relation: Ident, 224 | value: Ident, 225 | } 226 | 227 | impl ResourcePropertyValue { 228 | pub fn new(relation: Ident, value: Ident) -> Self { 229 | ResourcePropertyValue { relation, value } 230 | } 231 | } 232 | 233 | impl ClonePy for ResourcePropertyValue { 234 | fn clone_py(&self, py: Python) -> Self { 235 | Self { 236 | relation: self.relation.clone_py(py), 237 | value: self.value.clone_py(py), 238 | } 239 | } 240 | } 241 | 242 | impl Display for ResourcePropertyValue { 243 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 244 | let pv: fastobo::ast::PropertyValue = Python::with_gil(|py| self.clone_py(py).into_py(py)); 245 | pv.fmt(f) 246 | } 247 | } 248 | 249 | impl IntoPy for ResourcePropertyValue { 250 | fn into_py(self, py: Python) -> fastobo::ast::ResourcePropertyValue { 251 | fastobo::ast::ResourcePropertyValue::new(self.relation.into_py(py), self.value.into_py(py)) 252 | } 253 | } 254 | 255 | impl IntoPy for ResourcePropertyValue { 256 | fn into_py(self, py: Python) -> fastobo::ast::PropertyValue { 257 | fastobo::ast::PropertyValue::Resource(Box::new(self.into_py(py))) 258 | } 259 | } 260 | 261 | impl IntoPy for fastobo::ast::ResourcePropertyValue { 262 | fn into_py(self, py: Python) -> ResourcePropertyValue { 263 | let relation = self.property().clone().into_py(py); 264 | let value = self.target().clone().into_py(py); 265 | ResourcePropertyValue::new(relation, value) 266 | } 267 | } 268 | 269 | #[pymethods] 270 | impl ResourcePropertyValue { 271 | #[new] 272 | fn __init__(relation: Ident, value: Ident) -> PyClassInitializer { 273 | Self::new(relation, value).into() 274 | } 275 | 276 | fn __repr__(&self) -> PyResult { 277 | impl_repr!(self, ResourcePropertyValue(self.relation, self.value)) 278 | } 279 | 280 | fn __str__(&self) -> PyResult { 281 | let pv: fastobo::ast::PropertyValue = Python::with_gil(|py| self.clone_py(py).into_py(py)); 282 | Ok(pv.to_string()) 283 | } 284 | 285 | #[getter] 286 | fn get_relation(&self) -> PyResult<&Ident> { 287 | Ok(&self.relation) 288 | } 289 | 290 | #[setter] 291 | fn set_relation(&mut self, relation: Ident) -> PyResult<()> { 292 | self.relation = relation; 293 | Ok(()) 294 | } 295 | 296 | #[getter] 297 | fn get_value(&self) -> PyResult<&Ident> { 298 | Ok(&self.value) 299 | } 300 | 301 | #[setter] 302 | fn set_value(&mut self, value: Ident) -> PyResult<()> { 303 | self.value = value; 304 | Ok(()) 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/py/syn.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::fmt::Display; 3 | use std::fmt::Formatter; 4 | use std::fmt::Result as FmtResult; 5 | use std::rc::Rc; 6 | use std::str::FromStr; 7 | use std::string::ToString; 8 | 9 | use pyo3::class::basic::CompareOp; 10 | use pyo3::class::gc::PyVisit; 11 | use pyo3::exceptions::PyValueError; 12 | use pyo3::gc::PyTraverseError; 13 | use pyo3::prelude::*; 14 | use pyo3::types::PyAny; 15 | use pyo3::types::PyIterator; 16 | use pyo3::types::PyList; 17 | use pyo3::types::PyString; 18 | use pyo3::AsPyPointer; 19 | use pyo3::PyTypeInfo; 20 | 21 | use super::id::Ident; 22 | use super::xref::XrefList; 23 | use crate::utils::ClonePy; 24 | use crate::utils::EqPy; 25 | use crate::utils::IntoPy; 26 | 27 | // --- Module export --------------------------------------------------------- 28 | 29 | #[pymodule] 30 | #[pyo3(name = "syn")] 31 | pub fn init<'py>(_py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 32 | m.add_class::()?; 33 | m.add("__name__", "fastobo.syn")?; 34 | Ok(()) 35 | } 36 | 37 | // --- SynonymScope ---------------------------------------------------------- 38 | 39 | // #[pyclass(module = "fastobo.syn")] // FIXME(@althonos): probably not needed since it is not exposed. 40 | #[derive(Clone, ClonePy, Debug, Eq, PartialEq, EqPy)] 41 | pub struct SynonymScope { 42 | inner: fastobo::ast::SynonymScope, 43 | } 44 | 45 | impl SynonymScope { 46 | pub fn new(scope: fastobo::ast::SynonymScope) -> Self { 47 | Self { inner: scope } 48 | } 49 | } 50 | 51 | impl Display for SynonymScope { 52 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 53 | self.inner.fmt(f) 54 | } 55 | } 56 | 57 | impl From for SynonymScope { 58 | fn from(scope: fastobo::ast::SynonymScope) -> Self { 59 | Self::new(scope) 60 | } 61 | } 62 | 63 | impl From for fastobo::ast::SynonymScope { 64 | fn from(scope: SynonymScope) -> Self { 65 | scope.inner 66 | } 67 | } 68 | 69 | impl FromStr for SynonymScope { 70 | type Err = PyErr; 71 | fn from_str(s: &str) -> PyResult { 72 | match s { 73 | "EXACT" => Ok(Self::new(fastobo::ast::SynonymScope::Exact)), 74 | "BROAD" => Ok(Self::new(fastobo::ast::SynonymScope::Broad)), 75 | "NARROW" => Ok(Self::new(fastobo::ast::SynonymScope::Narrow)), 76 | "RELATED" => Ok(Self::new(fastobo::ast::SynonymScope::Related)), 77 | invalid => Err(PyValueError::new_err(format!( 78 | "expected 'EXACT', 'BROAD', 'NARROW' or 'RELATED', found {:?}", 79 | invalid 80 | ))), 81 | } 82 | } 83 | } 84 | 85 | impl IntoPy for fastobo::ast::SynonymScope { 86 | fn into_py(self, _py: Python) -> SynonymScope { 87 | SynonymScope::from(self) 88 | } 89 | } 90 | 91 | impl IntoPy for SynonymScope { 92 | fn into_py(self, _py: Python) -> fastobo::ast::SynonymScope { 93 | self.inner 94 | } 95 | } 96 | 97 | impl ToPyObject for SynonymScope { 98 | fn to_object(&self, py: Python) -> PyObject { 99 | self.to_string().to_object(py) 100 | } 101 | } 102 | 103 | impl<'py> IntoPyObject<'py> for &SynonymScope { 104 | type Error = Infallible; 105 | type Target = PyString; 106 | type Output = Bound<'py, PyString>; 107 | fn into_pyobject(self, py: Python<'py>) -> Result { 108 | match self.inner { 109 | fastobo::ast::SynonymScope::Exact => Ok(pyo3::intern!(py, "EXACT").clone()), 110 | fastobo::ast::SynonymScope::Broad => Ok(pyo3::intern!(py, "BROAD").clone()), 111 | fastobo::ast::SynonymScope::Narrow => Ok(pyo3::intern!(py, "NARROW").clone()), 112 | fastobo::ast::SynonymScope::Related => Ok(pyo3::intern!(py, "RELATED").clone()), 113 | _ => unimplemented!(), 114 | } 115 | } 116 | } 117 | 118 | impl<'py> IntoPyObject<'py> for SynonymScope { 119 | type Error = Infallible; 120 | type Target = PyString; 121 | type Output = Bound<'py, PyString>; 122 | fn into_pyobject(self, py: Python<'py>) -> Result { 123 | (&self).into_pyobject(py) 124 | } 125 | } 126 | 127 | // --- Synonym --------------------------------------------------------------- 128 | 129 | #[pyclass(module = "fastobo.syn")] 130 | #[derive(Debug, EqPy)] 131 | pub struct Synonym { 132 | desc: fastobo::ast::QuotedString, 133 | scope: SynonymScope, 134 | ty: Option, 135 | #[pyo3(get, set)] 136 | xrefs: Py, 137 | } 138 | 139 | impl ClonePy for Synonym { 140 | fn clone_py(&self, py: Python) -> Self { 141 | Self { 142 | desc: self.desc.clone(), 143 | scope: self.scope.clone_py(py), 144 | ty: self.ty.clone_py(py), 145 | xrefs: self.xrefs.clone_py(py), 146 | } 147 | } 148 | } 149 | 150 | impl Display for Synonym { 151 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 152 | let syn: fastobo::ast::Synonym = Python::with_gil(|py| self.clone_py(py).into_py(py)); 153 | syn.fmt(f) 154 | } 155 | } 156 | 157 | impl IntoPy for fastobo::ast::Synonym { 158 | fn into_py(mut self, py: Python) -> Synonym { 159 | Synonym { 160 | desc: std::mem::take(self.description_mut()), 161 | scope: SynonymScope::new(self.scope().clone()), 162 | ty: self.ty().map(|id| id.clone().into_py(py)), 163 | xrefs: Py::new(py, std::mem::take(self.xrefs_mut()).into_py(py)) 164 | .expect("failed allocating memory on Python heap"), 165 | } 166 | } 167 | } 168 | 169 | impl IntoPy for Synonym { 170 | fn into_py(self, py: Python) -> fastobo::ast::Synonym { 171 | fastobo::ast::Synonym::with_type_and_xrefs( 172 | self.desc, 173 | self.scope.inner, 174 | self.ty.map(|ty| ty.into_py(py)), 175 | (&*self.xrefs.bind(py).borrow()).into_py(py), 176 | ) 177 | } 178 | } 179 | 180 | #[pymethods] 181 | impl Synonym { 182 | #[new] 183 | #[pyo3(signature = (desc, scope, ty = None, xrefs = None))] 184 | pub fn __init__<'py>( 185 | desc: String, 186 | scope: &str, 187 | ty: Option, 188 | xrefs: Option>, 189 | ) -> PyResult { 190 | let xrefs = Python::with_gil(|py| { 191 | let list = xrefs 192 | .map(|x| XrefList::collect(py, &x)) 193 | .transpose()? 194 | .unwrap_or_default(); 195 | Py::new(py, list) 196 | })?; 197 | Ok(Self { 198 | desc: fastobo::ast::QuotedString::new(desc), 199 | scope: SynonymScope::from_str(scope)?, 200 | xrefs, 201 | ty, 202 | }) 203 | } 204 | 205 | fn __repr__(&self) -> PyResult { 206 | impl_repr!(self, Synonym(self.desc, self.scope)) 207 | } 208 | 209 | fn __str__(&self) -> PyResult { 210 | Ok(self.to_string()) 211 | } 212 | 213 | fn __richcmp__<'py>(&self, other: &Bound<'py, PyAny>, op: CompareOp) -> PyResult { 214 | impl_richcmp_py!( 215 | self, 216 | other, 217 | op, 218 | self.desc && self.scope && self.ty && self.xrefs 219 | ) 220 | } 221 | 222 | #[getter] 223 | pub fn get_desc(&self) -> PyResult { 224 | Ok(self.desc.as_str().to_owned()) 225 | } 226 | 227 | #[setter] 228 | pub fn set_desc(&mut self, desc: String) -> PyResult<()> { 229 | self.desc = fastobo::ast::QuotedString::new(desc); 230 | Ok(()) 231 | } 232 | 233 | #[getter] 234 | pub fn get_scope(&self) -> PyResult { 235 | Ok(self.scope.to_string()) 236 | } 237 | 238 | #[setter] 239 | pub fn set_scope(&mut self, scope: &str) -> PyResult<()> { 240 | self.scope = scope.parse()?; 241 | Ok(()) 242 | } 243 | 244 | #[getter] 245 | pub fn get_type(&self) -> PyResult> { 246 | Ok(self.ty.as_ref()) 247 | } 248 | 249 | #[setter] 250 | pub fn set_type(&mut self, ty: Option) -> PyResult<()> { 251 | self.ty = ty; 252 | Ok(()) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/py/term/frame.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::fmt::Formatter; 3 | use std::fmt::Result as FmtResult; 4 | use std::fmt::Write; 5 | use std::str::FromStr; 6 | 7 | use pyo3::exceptions::PyIndexError; 8 | use pyo3::exceptions::PyTypeError; 9 | use pyo3::prelude::*; 10 | use pyo3::types::PyAny; 11 | use pyo3::types::PyIterator; 12 | use pyo3::types::PyString; 13 | use pyo3::AsPyPointer; 14 | use pyo3::PyTypeInfo; 15 | 16 | use fastobo::ast; 17 | 18 | use super::super::abc::AbstractEntityFrame; 19 | use super::super::id::Ident; 20 | use super::clause::BaseTermClause; 21 | use super::clause::TermClause; 22 | use crate::utils::AbstractClass; 23 | use crate::utils::ClonePy; 24 | use crate::utils::EqPy; 25 | use crate::utils::FinalClass; 26 | use crate::utils::IntoPy; 27 | 28 | #[pyclass(extends=AbstractEntityFrame, module="fastobo.term")] 29 | #[derive(Debug, FinalClass, EqPy)] 30 | #[base(AbstractEntityFrame)] 31 | pub struct TermFrame { 32 | #[pyo3(set)] 33 | id: Ident, 34 | clauses: Vec, 35 | } 36 | 37 | impl TermFrame { 38 | pub fn new(id: Ident) -> Self { 39 | Self::with_clauses(id, Vec::new()) 40 | } 41 | 42 | pub fn with_clauses(id: Ident, clauses: Vec) -> Self { 43 | Self { id, clauses } 44 | } 45 | } 46 | 47 | impl ClonePy for TermFrame { 48 | fn clone_py(&self, py: Python) -> Self { 49 | Self { 50 | id: self.id.clone_py(py), 51 | clauses: self.clauses.clone_py(py), 52 | } 53 | } 54 | } 55 | 56 | impl Display for TermFrame { 57 | // FIXME: no clone 58 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 59 | let frame: fastobo::ast::TermFrame = Python::with_gil(|py| self.clone_py(py).into_py(py)); 60 | frame.fmt(f) 61 | } 62 | } 63 | 64 | impl IntoPy for fastobo::ast::TermFrame { 65 | fn into_py(self, py: Python) -> TermFrame { 66 | TermFrame::with_clauses( 67 | self.id().as_ref().clone().into_py(py), 68 | self.into_iter() 69 | .map(|line| line.into_inner().into_py(py)) 70 | .collect(), 71 | ) 72 | } 73 | } 74 | 75 | impl IntoPy for TermFrame { 76 | fn into_py(self, py: Python) -> fastobo::ast::TermFrame { 77 | fastobo::ast::TermFrame::with_clauses( 78 | fastobo::ast::ClassIdent::new(self.id.into_py(py)), 79 | self.clauses 80 | .iter() 81 | .map(|f| f.clone_py(py).into_py(py)) 82 | .map(|c| fastobo::ast::Line::new().and_inner(c)) 83 | .collect(), 84 | ) 85 | } 86 | } 87 | 88 | impl IntoPy for TermFrame { 89 | fn into_py(self, py: Python) -> fastobo::ast::EntityFrame { 90 | let frame: fastobo::ast::TermFrame = self.into_py(py); 91 | frame.into() 92 | } 93 | } 94 | 95 | #[listlike(field = "clauses", type = "TermClause")] 96 | #[pymethods] 97 | impl TermFrame { 98 | // FIXME: should accept any iterable. 99 | #[new] 100 | #[pyo3(signature = (id, clauses = None))] 101 | fn __init__<'py>( 102 | id: Ident, 103 | clauses: Option<&Bound<'py, PyAny>>, 104 | ) -> PyResult> { 105 | if let Some(clauses) = clauses { 106 | match clauses.extract() { 107 | Ok(c) => Ok(Self::with_clauses(id, c).into()), 108 | Err(_) => Err(PyTypeError::new_err("Expected list of `TermClause`")), 109 | } 110 | } else { 111 | Ok(Self::new(id).into()) 112 | } 113 | } 114 | 115 | fn __repr__(&self) -> PyResult { 116 | impl_repr!(self, TermFrame(self.id)) 117 | } 118 | 119 | fn __str__(&self) -> PyResult { 120 | Ok(self.to_string()) 121 | } 122 | 123 | fn __len__(&self) -> PyResult { 124 | Ok(self.clauses.len()) 125 | } 126 | 127 | fn __getitem__<'py>(&self, index: isize) -> PyResult> { 128 | if index < self.clauses.len() as isize { 129 | let item = &self.clauses[index as usize]; 130 | Python::with_gil(|py| Ok(item.into_pyobject(py)?.unbind())) 131 | } else { 132 | Err(PyIndexError::new_err("list index out of range")) 133 | } 134 | } 135 | 136 | fn __setitem__<'py>(&mut self, index: isize, elem: &Bound<'py, PyAny>) -> PyResult<()> { 137 | if index as usize > self.clauses.len() { 138 | return Err(PyIndexError::new_err("list index out of range")); 139 | } 140 | let clause = TermClause::extract_bound(elem)?; 141 | self.clauses[index as usize] = clause; 142 | Ok(()) 143 | } 144 | 145 | fn __delitem__(&mut self, index: isize) -> PyResult<()> { 146 | if index as usize > self.clauses.len() { 147 | return Err(PyIndexError::new_err("list index out of range")); 148 | } 149 | self.clauses.remove(index as usize); 150 | Ok(()) 151 | } 152 | 153 | fn __concat__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { 154 | let py = other.py(); 155 | 156 | let iterator = PyIterator::from_object(other)?; 157 | let mut new_clauses = self.clauses.clone_py(py); 158 | for item in iterator { 159 | new_clauses.push(TermClause::extract_bound(&item?)?); 160 | } 161 | 162 | Py::new(py, Self::with_clauses(self.id.clone_py(py), new_clauses)) 163 | } 164 | 165 | #[getter] 166 | /// `~fastobo.id.Ident`: the identifier of the term frame. 167 | fn get_id(&self) -> PyResult<&Ident> { 168 | Ok(&self.id) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/py/term/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clause; 2 | pub mod frame; 3 | 4 | use pyo3::prelude::*; 5 | 6 | #[pymodule] 7 | #[pyo3(name = "term")] 8 | pub fn init<'py>(py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 9 | m.add_class::()?; 10 | m.add_class::()?; 11 | m.add_class::()?; 12 | m.add_class::()?; 13 | m.add_class::()?; 14 | m.add_class::()?; 15 | m.add_class::()?; 16 | m.add_class::()?; 17 | m.add_class::()?; 18 | m.add_class::()?; 19 | m.add_class::()?; 20 | m.add_class::()?; 21 | m.add_class::()?; 22 | m.add_class::()?; 23 | m.add_class::()?; 24 | m.add_class::()?; 25 | m.add_class::()?; 26 | m.add_class::()?; 27 | m.add_class::()?; 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | m.add_class::()?; 32 | m.add_class::()?; 33 | 34 | register!(py, m, TermFrame, "collections.abc", MutableSequence); 35 | 36 | m.add("__name__", "fastobo.term")?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/py/typedef/frame.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::fmt::Formatter; 3 | use std::fmt::Result as FmtResult; 4 | use std::fmt::Write; 5 | use std::str::FromStr; 6 | 7 | use pyo3::exceptions::PyIndexError; 8 | use pyo3::exceptions::PyTypeError; 9 | use pyo3::exceptions::PyValueError; 10 | use pyo3::prelude::*; 11 | use pyo3::types::PyAny; 12 | use pyo3::types::PyIterator; 13 | use pyo3::types::PyString; 14 | use pyo3::AsPyPointer; 15 | use pyo3::PyTypeInfo; 16 | 17 | use fastobo::ast; 18 | 19 | use super::super::abc::AbstractEntityFrame; 20 | use super::super::id::Ident; 21 | use super::clause::BaseTypedefClause; 22 | use super::clause::TypedefClause; 23 | use crate::utils::AbstractClass; 24 | use crate::utils::ClonePy; 25 | use crate::utils::EqPy; 26 | use crate::utils::FinalClass; 27 | use crate::utils::IntoPy; 28 | 29 | #[pyclass(extends=AbstractEntityFrame, module="fastobo.typedef")] 30 | #[derive(Debug, FinalClass, EqPy)] 31 | #[base(AbstractEntityFrame)] 32 | pub struct TypedefFrame { 33 | #[pyo3(set)] 34 | id: Ident, 35 | clauses: Vec, 36 | } 37 | 38 | impl TypedefFrame { 39 | pub fn new(id: Ident) -> Self { 40 | Self::with_clauses(id, Vec::new()) 41 | } 42 | 43 | pub fn with_clauses(id: Ident, clauses: Vec) -> Self { 44 | Self { id, clauses } 45 | } 46 | } 47 | 48 | impl ClonePy for TypedefFrame { 49 | fn clone_py(&self, py: Python) -> Self { 50 | Self { 51 | id: self.id.clone_py(py), 52 | clauses: self.clauses.clone_py(py), 53 | } 54 | } 55 | } 56 | 57 | impl Display for TypedefFrame { 58 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 59 | let frame: fastobo::ast::TypedefFrame = 60 | Python::with_gil(|py| self.clone_py(py).into_py(py)); 61 | frame.fmt(f) 62 | } 63 | } 64 | 65 | impl IntoPy for fastobo::ast::TypedefFrame { 66 | fn into_py(self, py: Python) -> TypedefFrame { 67 | TypedefFrame::with_clauses( 68 | self.id().as_ref().clone().into_py(py), 69 | self.into_iter() 70 | .map(|line| line.into_inner().into_py(py)) 71 | .collect(), 72 | ) 73 | } 74 | } 75 | 76 | impl IntoPy for TypedefFrame { 77 | fn into_py(self, py: Python) -> fastobo::ast::TypedefFrame { 78 | fastobo::ast::TypedefFrame::with_clauses( 79 | fastobo::ast::RelationIdent::new(self.id.into_py(py)), 80 | self.clauses 81 | .iter() 82 | .map(|f| f.clone_py(py).into_py(py)) 83 | .map(|c| fastobo::ast::Line::new().and_inner(c)) 84 | .collect(), 85 | ) 86 | } 87 | } 88 | 89 | impl IntoPy for TypedefFrame { 90 | fn into_py(self, py: Python) -> fastobo::ast::EntityFrame { 91 | let frame: fastobo::ast::TypedefFrame = self.into_py(py); 92 | fastobo::ast::EntityFrame::from(frame) 93 | } 94 | } 95 | 96 | #[listlike(field = "clauses", type = "TypedefClause")] 97 | #[pymethods] 98 | impl TypedefFrame { 99 | // FIXME: should accept any iterable. 100 | #[new] 101 | fn __init__<'py>( 102 | id: Ident, 103 | clauses: Option<&Bound<'py, PyAny>>, 104 | ) -> PyResult> { 105 | if let Some(clauses) = clauses { 106 | match clauses.extract() { 107 | Ok(c) => Ok(Self::with_clauses(id, c).into()), 108 | Err(_) => Err(PyTypeError::new_err("Expected list of `TypedefClause`")), 109 | } 110 | } else { 111 | Ok(Self::new(id).into()) 112 | } 113 | } 114 | 115 | fn __repr__(&self) -> PyResult { 116 | impl_repr!(self, TypedefFrame(self.id)) 117 | } 118 | 119 | fn __str__(&self) -> PyResult { 120 | Ok(self.to_string()) 121 | } 122 | 123 | fn __len__(&self) -> PyResult { 124 | Ok(self.clauses.len()) 125 | } 126 | 127 | fn __getitem__(&self, index: isize) -> PyResult> { 128 | if index < self.clauses.len() as isize { 129 | let item = &self.clauses[index as usize]; 130 | Python::with_gil(|py| Ok(item.into_pyobject(py)?.unbind())) 131 | } else { 132 | Err(PyIndexError::new_err("list index out of range")) 133 | } 134 | } 135 | 136 | fn __setitem__<'py>(&mut self, index: isize, elem: &Bound<'py, PyAny>) -> PyResult<()> { 137 | if index as usize > self.clauses.len() { 138 | return Err(PyIndexError::new_err("list index out of range")); 139 | } 140 | let clause = TypedefClause::extract_bound(elem)?; 141 | self.clauses[index as usize] = clause; 142 | Ok(()) 143 | } 144 | 145 | fn __delitem__(&mut self, index: isize) -> PyResult<()> { 146 | if index as usize > self.clauses.len() { 147 | return Err(PyIndexError::new_err("list index out of range")); 148 | } 149 | self.clauses.remove(index as usize); 150 | Ok(()) 151 | } 152 | 153 | fn __concat__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { 154 | let py = other.py(); 155 | 156 | let iterator = PyIterator::from_object(other)?; 157 | let mut new_clauses = self.clauses.clone_py(py); 158 | for item in iterator { 159 | new_clauses.push(TypedefClause::extract_bound(&item?)?); 160 | } 161 | 162 | Py::new(py, Self::with_clauses(self.id.clone_py(py), new_clauses)) 163 | } 164 | 165 | #[getter] 166 | fn get_id(&self) -> PyResult<&Ident> { 167 | Ok(&self.id) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/py/typedef/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clause; 2 | pub mod frame; 3 | 4 | use pyo3::prelude::*; 5 | 6 | #[pymodule] 7 | #[pyo3(name = "typedef")] 8 | pub fn init<'py>(py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 9 | m.add_class::()?; 10 | m.add_class::()?; 11 | m.add_class::()?; 12 | m.add_class::()?; 13 | m.add_class::()?; 14 | m.add_class::()?; 15 | m.add_class::()?; 16 | m.add_class::()?; 17 | m.add_class::()?; 18 | m.add_class::()?; 19 | m.add_class::()?; 20 | m.add_class::()?; 21 | m.add_class::()?; 22 | m.add_class::()?; 23 | m.add_class::()?; 24 | m.add_class::()?; 25 | m.add_class::()?; 26 | m.add_class::()?; 27 | m.add_class::()?; 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | m.add_class::()?; 32 | m.add_class::()?; 33 | m.add_class::()?; 34 | m.add_class::()?; 35 | m.add_class::()?; 36 | m.add_class::()?; 37 | m.add_class::()?; 38 | m.add_class::()?; 39 | m.add_class::()?; 40 | m.add_class::()?; 41 | m.add_class::()?; 42 | m.add_class::()?; 43 | m.add_class::()?; 44 | m.add_class::()?; 45 | m.add_class::()?; 46 | m.add_class::()?; 47 | m.add_class::()?; 48 | m.add_class::()?; 49 | m.add_class::()?; 50 | m.add_class::()?; 51 | m.add_class::()?; 52 | 53 | register!(py, m, TypedefFrame, "collections.abc", MutableSequence); 54 | 55 | m.add("__name__", "fastobo.typedef")?; 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/py/xref.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::fmt::Display; 3 | use std::fmt::Formatter; 4 | use std::fmt::Result as FmtResult; 5 | use std::rc::Rc; 6 | use std::str::FromStr; 7 | use std::string::ToString; 8 | 9 | use pyo3::class::basic::CompareOp; 10 | use pyo3::class::gc::PyVisit; 11 | use pyo3::exceptions::PyIndexError; 12 | use pyo3::exceptions::PyTypeError; 13 | use pyo3::gc::PyTraverseError; 14 | use pyo3::prelude::*; 15 | use pyo3::types::PyAny; 16 | use pyo3::types::PyIterator; 17 | use pyo3::types::PyList; 18 | use pyo3::types::PyString; 19 | use pyo3::AsPyPointer; 20 | use pyo3::PyTypeInfo; 21 | 22 | use super::id::Ident; 23 | use crate::utils::ClonePy; 24 | use crate::utils::EqPy; 25 | use crate::utils::IntoPy; 26 | 27 | // --- Module export --------------------------------------------------------- 28 | 29 | #[pymodule] 30 | #[pyo3(name = "xref")] 31 | pub fn init<'py>(_py: Python<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { 32 | m.add_class::()?; 33 | m.add_class::()?; 34 | m.add("__name__", "fastobo.xref")?; 35 | Ok(()) 36 | } 37 | 38 | // --- Xref ------------------------------------------------------------------ 39 | 40 | /// A cross-reference to another entity or an external resource. 41 | /// 42 | /// Xrefs can be used in a `~fastobo.term.DefClause` to indicate the provenance 43 | /// of the definition, or in a `~fastobo.syn.Synonym` to add evidence from 44 | /// literature supporting the origin of the synonym. 45 | /// 46 | /// Example: 47 | /// >>> xref = fastobo.xref.Xref( 48 | /// ... fastobo.id.PrefixedIdent('ISBN', '978-0-321-84268-8'), 49 | /// ... ) 50 | #[pyclass(module = "fastobo.xref")] 51 | #[derive(Debug, EqPy)] 52 | pub struct Xref { 53 | #[pyo3(set)] 54 | id: Ident, 55 | desc: Option, 56 | } 57 | 58 | impl Xref { 59 | pub fn new(id: Ident) -> Self { 60 | Self { id, desc: None } 61 | } 62 | 63 | pub fn with_desc(id: Ident, desc: Option) -> Self { 64 | Self { id, desc } 65 | } 66 | } 67 | 68 | impl ClonePy for Xref { 69 | fn clone_py(&self, py: Python) -> Self { 70 | Xref { 71 | id: self.id.clone_py(py), 72 | desc: self.desc.clone(), 73 | } 74 | } 75 | } 76 | 77 | impl Display for Xref { 78 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 79 | let xref: fastobo::ast::Xref = Python::with_gil(|py| self.clone_py(py).into_py(py)); 80 | xref.fmt(f) 81 | } 82 | } 83 | 84 | impl IntoPy for fastobo::ast::Xref { 85 | fn into_py(mut self, py: Python) -> Xref { 86 | // Take ownership over `xref.description` w/o reallocation or clone. 87 | let desc = self.description_mut().map(|d| std::mem::take(d)); 88 | 89 | // Take ownership over `xref.id` w/o reallocation or clone. 90 | let empty = fastobo::ast::UnprefixedIdent::new(String::new()); 91 | let id = std::mem::replace(self.id_mut(), empty.into()); 92 | 93 | Xref::with_desc(id.into_py(py), desc) 94 | } 95 | } 96 | 97 | impl IntoPy for Xref { 98 | fn into_py(self, py: Python) -> fastobo::ast::Xref { 99 | let id: fastobo::ast::Ident = self.id.into_py(py); 100 | fastobo::ast::Xref::with_desc(id, self.desc) 101 | } 102 | } 103 | 104 | #[pymethods] 105 | impl Xref { 106 | /// Create a new `Xref` instance from an ID and an optional description. 107 | /// 108 | /// Arguments: 109 | /// id (~fastobo.id.Ident): the identifier of the reference. 110 | /// desc (str, optional): an optional description for the reference. 111 | #[new] 112 | fn __init__(id: Ident, desc: Option) -> Self { 113 | if let Some(s) = desc { 114 | Self::with_desc(id, Some(fastobo::ast::QuotedString::new(s))) 115 | } else { 116 | Self::new(id) 117 | } 118 | } 119 | 120 | fn __repr__<'py>(&self) -> PyResult { 121 | Python::with_gil(|py| { 122 | if let Some(ref d) = self.desc { 123 | PyString::new(py, "Xref({!r}, {!r})") 124 | .call_method1("format", (&self.id, d.as_str())) 125 | .map(|x| x.unbind()) 126 | } else { 127 | PyString::new(py, "Xref({!r})") 128 | .call_method1("format", (&self.id,)) 129 | .map(|x| x.unbind()) 130 | } 131 | }) 132 | } 133 | 134 | fn __str__(&self) -> PyResult { 135 | Ok(self.to_string()) 136 | } 137 | 138 | fn __richcmp__<'py>(&self, other: &Bound<'py, PyAny>, op: CompareOp) -> PyResult { 139 | impl_richcmp_py!(self, other, op, self.id && self.desc) 140 | } 141 | 142 | /// `~fastobo.id.Ident`: the identifier of the reference. 143 | #[getter] 144 | fn get_id(&self) -> PyResult<&Ident> { 145 | Ok(&self.id) 146 | } 147 | 148 | /// `str` or `None`: the description of the reference, if any. 149 | #[getter] 150 | fn get_desc(&self) -> PyResult> { 151 | match &self.desc { 152 | Some(d) => Ok(Some(d.as_str())), 153 | None => Ok(None), 154 | } 155 | } 156 | 157 | #[setter] 158 | fn set_desc(&mut self, desc: Option) -> PyResult<()> { 159 | self.desc = desc.map(fastobo::ast::QuotedString::new); 160 | Ok(()) 161 | } 162 | } 163 | 164 | // --- XrefList -------------------------------------------------------------- 165 | 166 | /// A list of cross-references. 167 | /// 168 | /// Example: 169 | /// >>> xrefs = ms[0][1].xrefs 170 | /// >>> print(xrefs) 171 | /// [PSI:MS] 172 | /// >>> xrefs[0] 173 | /// Xref(PrefixedIdent('PSI', 'MS')) 174 | /// 175 | #[pyclass(module = "fastobo.xref")] 176 | #[derive(Debug, Default, EqPy)] 177 | pub struct XrefList { 178 | xrefs: Vec>, 179 | } 180 | 181 | impl XrefList { 182 | /// Create a new `XrefList` from a vector of Xrefs. 183 | pub fn new(xrefs: Vec>) -> Self { 184 | Self { xrefs } 185 | } 186 | 187 | /// Create a new `XrefList` from a `PyIterator`. 188 | pub fn collect<'py>(py: Python<'py>, xrefs: &Bound<'py, PyAny>) -> PyResult { 189 | let mut vec = Vec::new(); 190 | for item in PyIterator::from_object(xrefs)? { 191 | let i = item?; 192 | if let Ok(xref) = i.extract::>() { 193 | vec.push(xref.clone_ref(py)); 194 | } else { 195 | let ty = i.get_type().name()?; 196 | let msg = format!("expected Xref, found {}", ty); 197 | return Err(PyTypeError::new_err(msg)); 198 | } 199 | } 200 | Ok(Self { xrefs: vec }) 201 | } 202 | 203 | /// Check whether the `XrefList` is empty 204 | pub fn is_empty(&self) -> bool { 205 | self.xrefs.is_empty() 206 | } 207 | } 208 | 209 | impl ClonePy for XrefList { 210 | fn clone_py(&self, py: Python) -> Self { 211 | XrefList { 212 | xrefs: self.xrefs.clone_py(py), 213 | } 214 | } 215 | } 216 | 217 | impl IntoPy for fastobo::ast::XrefList { 218 | fn into_py(self, py: Python) -> XrefList { 219 | let mut xrefs = Vec::with_capacity((&self).len()); 220 | for xref in self.into_iter() { 221 | xrefs.push(Py::new(py, xref.into_py(py)).unwrap()) 222 | } 223 | XrefList::new(xrefs) 224 | } 225 | } 226 | 227 | impl IntoPy for XrefList { 228 | fn into_py(self, py: Python) -> fastobo::ast::XrefList { 229 | (&self).into_py(py) 230 | } 231 | } 232 | 233 | impl IntoPy for &XrefList { 234 | fn into_py<'py>(self, py: Python) -> fastobo::ast::XrefList { 235 | self.xrefs 236 | .iter() 237 | .map(|xref| xref.bind(py).borrow().clone_py(py).into_py(py)) 238 | .collect() 239 | } 240 | } 241 | 242 | #[listlike(field = "xrefs", type = "Py")] 243 | #[pymethods] 244 | impl XrefList { 245 | #[new] 246 | #[pyo3(signature = (xrefs = None))] 247 | fn __init__<'py>(xrefs: Option<&Bound<'py, PyAny>>) -> PyResult { 248 | if let Some(x) = xrefs { 249 | Python::with_gil(|py| Self::collect(py, x)) 250 | } else { 251 | Ok(Self::new(Vec::new())) 252 | } 253 | } 254 | 255 | fn __repr__(&self) -> PyResult { 256 | Python::with_gil(|py| { 257 | if self.xrefs.is_empty() { 258 | Ok("XrefList()".to_object(py)) 259 | } else { 260 | let fmt = PyString::new(py, "XrefList({!r})").to_object(py); 261 | fmt.call_method1(py, "format", (&self.xrefs.to_object(py),)) 262 | } 263 | }) 264 | } 265 | 266 | fn __str__(&self) -> PyResult { 267 | let frame: fastobo::ast::XrefList = Python::with_gil(|py| self.clone_py(py).into_py(py)); 268 | Ok(frame.to_string()) 269 | } 270 | 271 | fn __len__(&self) -> PyResult { 272 | Ok(self.xrefs.len()) 273 | } 274 | 275 | fn __getitem__(&self, index: isize) -> PyResult> { 276 | if index < self.xrefs.len() as isize { 277 | Python::with_gil(|py| Ok(self.xrefs[index as usize].clone_ref(py))) 278 | } else { 279 | Err(PyIndexError::new_err("list index out of range")) 280 | } 281 | } 282 | 283 | fn __contains__<'py>(&self, item: &Bound<'py, PyAny>) -> PyResult { 284 | if let Ok(xref) = item.extract::>() { 285 | let py = item.py(); 286 | Ok(self 287 | .xrefs 288 | .iter() 289 | .any(|x| (*x.bind(py).borrow()).eq_py(&xref.bind(py).borrow(), py))) 290 | } else { 291 | let ty = item.get_type().name()?; 292 | let msg = format!("'in ' requires Xref as left operand, not {}", ty); 293 | Err(PyTypeError::new_err(msg)) 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/pyfile.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::io::Error as IoError; 3 | use std::io::Read; 4 | use std::io::Write; 5 | use std::marker::PhantomData; 6 | use std::sync::Arc; 7 | use std::sync::Mutex; 8 | 9 | use pyo3::exceptions::PyOSError; 10 | use pyo3::exceptions::PyTypeError; 11 | use pyo3::gc::PyTraverseError; 12 | use pyo3::gc::PyVisit; 13 | use pyo3::prelude::*; 14 | use pyo3::types::PyBytes; 15 | use pyo3::AsPyPointer; 16 | use pyo3::PyObject; 17 | 18 | // --------------------------------------------------------------------------- 19 | 20 | #[macro_export] 21 | macro_rules! transmute_file_error { 22 | ($self:ident, $e:ident, $msg:expr, $py:expr) => {{ 23 | // Attempt to transmute the Python OSError to an actual 24 | // Rust `std::io::Error` using `from_raw_os_error`. 25 | if $e.is_instance_of::($py) { 26 | if let Ok(code) = &$e.value($py).getattr("errno") { 27 | if let Ok(n) = code.extract::() { 28 | return Err(IoError::from_raw_os_error(n)); 29 | } 30 | } 31 | } 32 | 33 | // if the conversion is not possible for any reason we fail 34 | // silently, wrapping the Python error, and returning a 35 | // generic Rust error instead. 36 | $e.restore($py); 37 | Err(IoError::new(std::io::ErrorKind::Other, $msg)) 38 | }}; 39 | } 40 | 41 | // --------------------------------------------------------------------------- 42 | 43 | /// A wrapper around a readable Python file borrowed within a GIL lifetime. 44 | pub struct PyFileRead<'py> { 45 | file: Bound<'py, PyAny>, 46 | } 47 | 48 | impl<'py> PyFileRead<'py> { 49 | pub fn from_ref(file: &Bound<'py, PyAny>) -> PyResult> { 50 | let res = file.call_method1("read", (0,))?; 51 | if res.downcast::().is_ok() { 52 | Ok(PyFileRead { file: file.clone() }) 53 | } else { 54 | let ty = res.get_type().name()?.to_string(); 55 | Err(PyTypeError::new_err(format!( 56 | "expected bytes, found {}", 57 | ty 58 | ))) 59 | } 60 | } 61 | } 62 | 63 | impl<'p> Read for PyFileRead<'p> { 64 | fn read(&mut self, buf: &mut [u8]) -> Result { 65 | match self.file.call_method1("read", (buf.len(),)) { 66 | Ok(obj) => { 67 | // Check `fh.read` returned bytes, else raise a `TypeError`. 68 | if let Ok(bytes) = obj.downcast::() { 69 | let b = bytes.as_bytes(); 70 | (&mut buf[..b.len()]).copy_from_slice(b); 71 | Ok(b.len()) 72 | } else { 73 | let ty = obj.get_type().name()?.to_string(); 74 | let msg = format!("expected bytes, found {}", ty); 75 | PyTypeError::new_err(msg).restore(self.file.py()); 76 | Err(IoError::new( 77 | std::io::ErrorKind::Other, 78 | "fh.read did not return bytes", 79 | )) 80 | } 81 | } 82 | Err(e) => { 83 | transmute_file_error!(self, e, "read method failed", self.file.py()) 84 | } 85 | } 86 | } 87 | } 88 | 89 | // --------------------------------------------------------------------------- 90 | 91 | /// A wrapper around a writable Python file borrowed within a GIL lifetime. 92 | pub struct PyFileWrite<'py> { 93 | file: Bound<'py, PyAny>, 94 | } 95 | 96 | impl<'py> PyFileWrite<'py> { 97 | pub fn from_ref(file: &Bound<'py, PyAny>) -> PyResult> { 98 | file.call_method1("write", (PyBytes::new(file.py(), b""),)) 99 | .map(|_| PyFileWrite { file: file.clone() }) 100 | } 101 | } 102 | 103 | impl<'py> Write for PyFileWrite<'py> { 104 | fn write(&mut self, buf: &[u8]) -> Result { 105 | let bytes = PyBytes::new(self.file.py(), buf); 106 | match self.file.call_method1("write", (bytes,)) { 107 | Ok(obj) => { 108 | // Check `fh.write` returned int, else raise a `TypeError`. 109 | if let Ok(len) = obj.extract::() { 110 | Ok(len) 111 | } else { 112 | let ty = obj.get_type().name()?.to_string(); 113 | let msg = format!("expected int, found {}", ty); 114 | PyTypeError::new_err(msg).restore(self.file.py()); 115 | Err(IoError::new( 116 | std::io::ErrorKind::Other, 117 | "write method did not return int", 118 | )) 119 | } 120 | } 121 | Err(e) => { 122 | transmute_file_error!(self, e, "write method failed", self.file.py()) 123 | } 124 | } 125 | } 126 | 127 | fn flush(&mut self) -> Result<(), IoError> { 128 | match self.file.call_method0("flush") { 129 | Ok(_) => Ok(()), 130 | Err(e) => { 131 | transmute_file_error!(self, e, "flush method failed", self.file.py()) 132 | } 133 | } 134 | } 135 | } 136 | 137 | // --------------------------------------------------------------------------- 138 | 139 | /// A wrapper for a Python file that can outlive the GIL. 140 | pub struct PyFileGILRead { 141 | file: Mutex, 142 | } 143 | 144 | impl PyFileGILRead { 145 | pub fn from_ref<'py>(file: &Bound<'py, PyAny>) -> PyResult { 146 | let res = file.call_method1("read", (0,))?; 147 | if res.downcast::().is_ok() { 148 | Ok(PyFileGILRead { 149 | file: Mutex::new(file.clone().unbind()), 150 | }) 151 | } else { 152 | let ty = res.get_type().name()?.to_string(); 153 | Err(PyTypeError::new_err(format!( 154 | "expected bytes, found {}", 155 | ty 156 | ))) 157 | } 158 | } 159 | 160 | pub fn file(&self) -> &Mutex { 161 | &self.file 162 | } 163 | } 164 | 165 | impl Read for PyFileGILRead { 166 | fn read(&mut self, buf: &mut [u8]) -> Result { 167 | Python::with_gil(|py| { 168 | let guard = self.file.lock().unwrap(); 169 | let file = guard.bind(py); 170 | match file.call_method1("read", (buf.len(),)) { 171 | Ok(obj) => { 172 | // Check `fh.read` returned bytes, else raise a `TypeError`. 173 | if let Ok(bytes) = obj.downcast::() { 174 | let b = bytes.as_bytes(); 175 | (&mut buf[..b.len()]).copy_from_slice(b); 176 | Ok(b.len()) 177 | } else { 178 | let ty = obj.as_ref().get_type().name()?.to_string(); 179 | let msg = format!("expected bytes, found {}", ty); 180 | PyTypeError::new_err(msg).restore(py); 181 | Err(IoError::new( 182 | std::io::ErrorKind::Other, 183 | "fh.read did not return bytes", 184 | )) 185 | } 186 | } 187 | Err(e) => transmute_file_error!(self, e, "read method failed", py), 188 | } 189 | }) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use pyo3::ffi::PyObject; 4 | use pyo3::AsPyPointer; 5 | use pyo3::Py; 6 | use pyo3::PyClass; 7 | use pyo3::PyClassInitializer; 8 | use pyo3::PyRef; 9 | use pyo3::PyTypeInfo; 10 | use pyo3::Python; 11 | 12 | use fastobo::ast; 13 | 14 | // --- 15 | 16 | pub trait IntoPy { 17 | fn into_py(self, py: Python) -> T; 18 | } 19 | 20 | // --- 21 | 22 | /// A trait for objects that can be cloned while the GIL is held. 23 | pub trait ClonePy { 24 | fn clone_py(&self, py: Python) -> Self; 25 | } 26 | 27 | impl ClonePy for Py { 28 | fn clone_py(&self, py: Python) -> Self { 29 | self.clone_ref(py) 30 | } 31 | } 32 | 33 | impl ClonePy for Vec 34 | where 35 | T: ClonePy, 36 | { 37 | fn clone_py(&self, py: Python) -> Self { 38 | self.iter().map(|x| x.clone_py(py)).collect() 39 | } 40 | } 41 | 42 | impl ClonePy for Option 43 | where 44 | T: ClonePy, 45 | { 46 | fn clone_py(&self, py: Python) -> Self { 47 | self.as_ref().map(|x| x.clone_py(py)) 48 | } 49 | } 50 | 51 | // --- 52 | 53 | macro_rules! derive_eqpy { 54 | ($type:ty) => { 55 | impl EqPy for $type { 56 | fn eq_py(&self, other: &Self, _py: Python) -> bool { 57 | self == other 58 | } 59 | } 60 | }; 61 | } 62 | 63 | /// A trait for objects that can be compared for equality while the GIL is held. 64 | pub trait EqPy { 65 | fn eq_py(&self, other: &Self, py: Python) -> bool; 66 | fn neq_py(&self, other: &Self, py: Python) -> bool { 67 | !self.eq_py(other, py) 68 | } 69 | } 70 | 71 | impl EqPy for Option 72 | where 73 | T: EqPy, 74 | { 75 | fn eq_py(&self, other: &Self, py: Python) -> bool { 76 | match (self, other) { 77 | (Some(l), Some(r)) => l.eq_py(r, py), 78 | (None, None) => true, 79 | _ => false, 80 | } 81 | } 82 | } 83 | 84 | impl EqPy for Vec 85 | where 86 | T: EqPy, 87 | { 88 | fn eq_py(&self, other: &Self, py: Python) -> bool { 89 | if self.len() == other.len() { 90 | for (x, y) in self.iter().zip(other.iter()) { 91 | if !x.eq_py(y, py) { 92 | return false; 93 | } 94 | } 95 | true 96 | } else { 97 | false 98 | } 99 | } 100 | } 101 | 102 | impl EqPy for Py 103 | where 104 | T: EqPy + PyClass, 105 | { 106 | fn eq_py(&self, other: &Self, py: Python) -> bool { 107 | let l = self.borrow(py); 108 | let r = other.borrow(py); 109 | (*l).eq_py(&*r, py) 110 | } 111 | } 112 | 113 | derive_eqpy!(bool); 114 | derive_eqpy!(fastobo::ast::CreationDate); 115 | derive_eqpy!(fastobo::ast::IdentPrefix); 116 | derive_eqpy!(fastobo::ast::Import); 117 | derive_eqpy!(fastobo::ast::NaiveDateTime); 118 | derive_eqpy!(fastobo::ast::PrefixedIdent); 119 | derive_eqpy!(fastobo::ast::QuotedString); 120 | derive_eqpy!(fastobo::ast::SynonymScope); 121 | derive_eqpy!(fastobo::ast::UnprefixedIdent); 122 | derive_eqpy!(fastobo::ast::UnquotedString); 123 | derive_eqpy!(fastobo::ast::Url); 124 | 125 | // --- 126 | 127 | /// A trait for Python classes that are purely abstract. 128 | pub trait AbstractClass: PyClass { 129 | fn initializer() -> PyClassInitializer; 130 | } 131 | 132 | /// A trait for Python classes that are final. 133 | pub trait FinalClass: PyClass {} 134 | 135 | // --- 136 | 137 | pub type Hasher = std::collections::hash_map::DefaultHasher; 138 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | test_doc, 3 | test_doctests, 4 | test_fastobo, 5 | test_header, 6 | test_id, 7 | test_pv, 8 | test_term, 9 | test_typedef, 10 | test_xref 11 | ) 12 | 13 | def load_tests(loader, suite, pattern): 14 | suite.addTests(loader.loadTestsFromModule(test_doc)) 15 | suite.addTests(loader.loadTestsFromModule(test_doctests)) 16 | suite.addTests(loader.loadTestsFromModule(test_fastobo)) 17 | suite.addTests(loader.loadTestsFromModule(test_header)) 18 | suite.addTests(loader.loadTestsFromModule(test_id)) 19 | suite.addTests(loader.loadTestsFromModule(test_pv)) 20 | suite.addTests(loader.loadTestsFromModule(test_term)) 21 | suite.addTests(loader.loadTestsFromModule(test_typedef)) 22 | suite.addTests(loader.loadTestsFromModule(test_xref)) 23 | return suite -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | 6 | import fastobo 7 | 8 | 9 | # --- TermFrame -------------------------------------------------------------- 10 | 11 | class _TestFrame(object): 12 | 13 | Frame = NotImplementedError 14 | NameClause = NotImplementedError 15 | CreatedByClause = NotImplementedError 16 | 17 | def setUp(self): 18 | self.id = fastobo.id.PrefixedIdent("MS", "1000031") 19 | 20 | def test_init(self): 21 | try: 22 | frame = self.Frame(self.id) 23 | except Exception: 24 | self.fail("could not create frame instances") 25 | 26 | def test_init_iterable(self): 27 | try: 28 | frame = self.Frame(self.id, []) 29 | except Exception: 30 | self.fail("could not create frame instances") 31 | try: 32 | frame = self.Frame(self.id, [ 33 | self.NameClause("thing"), 34 | self.CreatedByClause("Martin Larralde") 35 | ]) 36 | except Exception: 37 | self.fail("could not create frame from iterable") 38 | 39 | def test_init_type_error(self): 40 | self.assertRaises(TypeError, self.Frame, 1) 41 | self.assertRaises(TypeError, self.Frame, [1]) 42 | self.assertRaises(TypeError, self.Frame, ["abc"]) 43 | self.assertRaises(TypeError, self.Frame, "abc") 44 | self.assertRaises(TypeError, self.Frame, self.id, 1) 45 | self.assertRaises(TypeError, self.Frame, self.id, [1]) 46 | self.assertRaises(TypeError, self.Frame, self.id, ["abc"]) 47 | self.assertRaises(TypeError, self.Frame, self.id, "abc") 48 | 49 | def test_append(self): 50 | frame = self.Frame(self.id) 51 | self.assertEqual(len(frame), 0) 52 | c1 = self.NameClause("thing") 53 | frame.append(c1) 54 | self.assertEqual(len(frame), 1) 55 | self.assertEqual(frame[0], c1) 56 | c2 = self.CreatedByClause("Martin Larralde") 57 | frame.append(c2) 58 | self.assertEqual(len(frame), 2) 59 | self.assertEqual(frame[0], c1) 60 | self.assertEqual(frame[1], c2) 61 | 62 | def test_reverse(self): 63 | c1 = self.NameClause("thing") 64 | c2 = self.CreatedByClause("Martin Larralde") 65 | frame = self.Frame(self.id, [c1, c2]) 66 | self.assertEqual(list(frame), [c1, c2]) 67 | frame.reverse() 68 | self.assertEqual(list(frame), [c2, c1]) 69 | 70 | def test_clear(self): 71 | c1 = self.NameClause("thing") 72 | c2 = self.CreatedByClause("Martin Larralde") 73 | frame = self.Frame(self.id, [c1, c2]) 74 | self.assertEqual(len(frame), 2) 75 | frame.clear() 76 | self.assertEqual(len(frame), 0) 77 | self.assertEqual(list(frame), []) 78 | 79 | def test_pop(self): 80 | c1 = self.NameClause("thing") 81 | c2 = self.CreatedByClause("Martin Larralde") 82 | frame = self.Frame(self.id, [c1, c2]) 83 | self.assertEqual(len(frame), 2) 84 | x1 = frame.pop() 85 | self.assertEqual(len(frame), 1) 86 | self.assertEqual(x1, c2) 87 | x2 = frame.pop() 88 | self.assertEqual(len(frame), 0) 89 | self.assertEqual(x2, c1) 90 | self.assertRaises(IndexError, frame.pop) 91 | 92 | # --- DefClause -------------------------------------------------------------- 93 | 94 | class _TestDefClause(object): 95 | 96 | type = NotImplementedError 97 | 98 | def test_repr(self): 99 | clause = self.type("definition") 100 | self.assertEqual(repr(clause), "DefClause('definition')") 101 | 102 | id_ = fastobo.id.PrefixedIdent('ISBN', '0321842685') 103 | desc = "Hacker's Delight (2nd Edition)" 104 | x = fastobo.xref.Xref(id_, desc) 105 | 106 | clause = self.type("definition", fastobo.xref.XrefList([x])) 107 | self.assertEqual(repr(clause), "DefClause('definition', XrefList([{!r}]))".format(x)) 108 | 109 | 110 | # --- ConsiderClause --------------------------------------------------------- 111 | 112 | class _TestConsiderClause(object): 113 | 114 | type = NotImplementedError 115 | 116 | def setUp(self): 117 | self.id = fastobo.id.PrefixedIdent("MS", "1000031") 118 | self.id2 = fastobo.id.PrefixedIdent("MS", "1000032") 119 | 120 | def test_init(self): 121 | try: 122 | frame = self.type(self.id) 123 | except Exception: 124 | self.fail("could not create `ConsiderClause` instances") 125 | 126 | def test_init_type_error(self): 127 | self.assertRaises(TypeError, self.type) 128 | self.assertRaises(TypeError, self.type, 1) 129 | 130 | def test_eq(self): 131 | self.assertEqual(self.type(self.id), self.type(self.id)) 132 | self.assertNotEqual(self.type(self.id), self.type(self.id2)) 133 | 134 | 135 | # --- IsObsoleteClause ------------------------------------------------------- 136 | 137 | class _TestIsObsoleteClause(object): 138 | 139 | type = NotImplementedError 140 | 141 | def test_init(self): 142 | try: 143 | frame = self.type(True) 144 | except Exception: 145 | self.fail("could not create `IsObsoleteClause` instances") 146 | 147 | def test_property_obsolete(self): 148 | c = self.type(False) 149 | self.assertEqual(c.obsolete, False) 150 | c.obsolete = True 151 | self.assertEqual(c.obsolete, True) 152 | 153 | def test_repr(self): 154 | self.assertEqual(repr(self.type(False)), "IsObsoleteClause(False)") 155 | self.assertEqual(repr(self.type(True)), "IsObsoleteClause(True)") 156 | 157 | def test_str(self): 158 | self.assertEqual(str(self.type(False)), "is_obsolete: false") 159 | self.assertEqual(str(self.type(True)), "is_obsolete: true") 160 | 161 | def test_eq(self): 162 | self.assertEqual(self.type(True), self.type(True)) 163 | self.assertEqual(self.type(False), self.type(False)) 164 | self.assertNotEqual(self.type(False), self.type(True)) 165 | 166 | 167 | # --- CreationDateClause ----------------------------------------------------- 168 | 169 | class _TestCreationDateClause(object): 170 | 171 | type = NotImplementedError 172 | 173 | def test_date(self): 174 | d1 = datetime.date(2021, 1, 23) 175 | clause = self.type(d1) 176 | self.assertEqual(str(clause), "creation_date: 2021-01-23") 177 | self.assertEqual(repr(clause), "CreationDateClause(datetime.date(2021, 1, 23))") 178 | self.assertEqual(clause.date, d1) 179 | self.assertIsInstance(clause.date, datetime.date) 180 | d2 = datetime.date(2021, 2, 15) 181 | clause.date = d2 182 | self.assertIsInstance(clause.date, datetime.date) 183 | 184 | def test_datetime(self): 185 | d1 = datetime.datetime(2021, 1, 23, 12) 186 | clause = self.type(d1) 187 | self.assertEqual(str(clause), "creation_date: 2021-01-23T12:00:00") 188 | self.assertEqual(repr(clause), "CreationDateClause(datetime.datetime(2021, 1, 23, 12, 0))") 189 | self.assertEqual(clause.date, d1) 190 | self.assertIsInstance(clause.date, datetime.datetime) 191 | d2 = datetime.datetime(2021, 2, 15, 12, 30, 0, tzinfo=datetime.timezone.utc) 192 | clause.date = d2 193 | self.assertEqual(str(clause), "creation_date: 2021-02-15T12:30:00Z") 194 | self.assertIsInstance(clause.date, datetime.datetime) 195 | -------------------------------------------------------------------------------- /tests/data/.gitignore: -------------------------------------------------------------------------------- 1 | plana.json 2 | ms.ofn 3 | -------------------------------------------------------------------------------- /tests/test_doc.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | 6 | import fastobo 7 | 8 | 9 | # -- OboDoc ------------------------------------------------------------------ 10 | 11 | class TestOboDoc(unittest.TestCase): 12 | 13 | type = fastobo.doc.OboDoc 14 | 15 | def setUp(self): 16 | self.header = fastobo.header.HeaderFrame([ 17 | fastobo.header.FormatVersionClause("1.4"), 18 | fastobo.header.SavedByClause("Martin Larralde"), 19 | ]) 20 | self.entities = [ 21 | fastobo.term.TermFrame(fastobo.id.PrefixedIdent("MS", "1000031")), 22 | fastobo.typedef.TypedefFrame(fastobo.id.UnprefixedIdent("part_of")) 23 | ] 24 | 25 | def test_init(self): 26 | try: 27 | doc = self.type() 28 | except Exception: 29 | self.fail("could not create `OboDoc` instances") 30 | self.assertEqual(len(doc.header), 0) 31 | self.assertEqual(len(doc), 0) 32 | 33 | def test_init_header(self): 34 | try: 35 | doc = self.type(self.header) 36 | except Exception: 37 | self.fail("could not create `OboDoc` instances with a header") 38 | self.assertEqual(len(doc.header), 2) 39 | self.assertEqual(doc.header[0], self.header[0]) 40 | self.assertEqual(doc.header[1], self.header[1]) 41 | self.assertEqual(len(doc), 0) 42 | 43 | def test_init_entities(self): 44 | try: 45 | doc = self.type(entities=self.entities) 46 | except Exception: 47 | self.fail("could not create `OboDoc` instances with a header") 48 | self.assertEqual(len(doc.header), 0) 49 | self.assertEqual(len(doc), 2) 50 | self.assertEqual(doc[0], self.entities[0]) 51 | self.assertEqual(doc[1], self.entities[1]) 52 | 53 | def test_init_type_error(self): 54 | self.assertRaises(TypeError, self.type, 1) 55 | self.assertRaises(TypeError, self.type, [1]) 56 | self.assertRaises(TypeError, self.type, ["abc"]) 57 | self.assertRaises(TypeError, self.type, "abc") 58 | self.assertRaises(TypeError, self.type, self.header, 1) 59 | self.assertRaises(TypeError, self.type, self.header, [1]) 60 | self.assertRaises(TypeError, self.type, self.header, ["abc"]) 61 | self.assertRaises(TypeError, self.type, self.header, "abc") 62 | self.assertRaises(TypeError, self.type, 1, self.entities) 63 | self.assertRaises(TypeError, self.type, [1], self.entities) 64 | self.assertRaises(TypeError, self.type, ["abc"], self.entities) 65 | self.assertRaises(TypeError, self.type, "abc", self.entities) 66 | -------------------------------------------------------------------------------- /tests/test_doctests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Test doctest contained tests in every file of the module. 3 | """ 4 | 5 | import os 6 | import sys 7 | import datetime 8 | import doctest 9 | import warnings 10 | import pprint 11 | import textwrap 12 | import types 13 | 14 | import fastobo 15 | 16 | 17 | def _load_tests_from_module(tests, module, globs, setUp=None, tearDown=None): 18 | """Load tests from module, iterating through submodules""" 19 | 20 | module.__test__ = {} 21 | for attr in (getattr(module, x) for x in dir(module) if not x.startswith('_')): 22 | if isinstance(attr, types.ModuleType): 23 | _load_tests_from_module(tests, attr, globs, setUp, tearDown) 24 | else: 25 | module.__test__[attr.__name__] = attr 26 | 27 | tests.addTests(doctest.DocTestSuite( 28 | module, 29 | globs=globs, 30 | setUp=setUp, 31 | tearDown=tearDown, 32 | optionflags=doctest.ELLIPSIS, 33 | )) 34 | 35 | return tests 36 | 37 | 38 | def load_tests(loader, tests, ignore): 39 | """load_test function used by unittest to find the doctests""" 40 | 41 | _current_cwd = os.getcwd() 42 | 43 | def setUp(self): 44 | warnings.simplefilter("ignore") 45 | os.chdir(os.path.realpath(os.path.join(__file__, "..", "data"))) 46 | 47 | def tearDown(self): 48 | os.chdir(_current_cwd) 49 | warnings.simplefilter(warnings.defaultaction) 50 | 51 | globs = { 52 | "fastobo": fastobo, 53 | "datetime": datetime, 54 | "textwrap": textwrap, 55 | "pprint": pprint.pprint, 56 | "ms": fastobo.load(os.path.realpath( 57 | os.path.join(__file__, "..", "data", "ms.obo") 58 | )), 59 | } 60 | 61 | if not sys.argv[0].endswith('green'): 62 | tests = _load_tests_from_module(tests, fastobo, globs, setUp, tearDown) 63 | return tests 64 | 65 | 66 | def setUpModule(): 67 | warnings.simplefilter('ignore') 68 | 69 | 70 | def tearDownModule(): 71 | warnings.simplefilter(warnings.defaultaction) 72 | -------------------------------------------------------------------------------- /tests/test_fastobo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import io 4 | import os 5 | import unittest 6 | import pathlib 7 | 8 | import fastobo 9 | 10 | MS = os.path.realpath(os.path.join(__file__, "..", "data", "ms.obo")) 11 | MS_FRAMES = 2941 12 | 13 | class TestLoad(unittest.TestCase): 14 | 15 | def test_file_not_found(self): 16 | self.assertRaises(FileNotFoundError, fastobo.load, "abcdef") 17 | 18 | def test_type_error(self): 19 | self.assertRaises(TypeError, fastobo.load, 1) 20 | self.assertRaises(TypeError, fastobo.load, []) 21 | 22 | with open(MS) as f: 23 | self.assertRaises(TypeError, fastobo.load, f) 24 | 25 | def test_error_propagation(self): 26 | 27 | def read(x): 28 | if x == 0: 29 | return b'' 30 | raise RuntimeError(x) 31 | 32 | with open(MS, 'rb') as f: 33 | f.read = read 34 | self.assertRaises(RuntimeError, fastobo.load, f) 35 | 36 | def test_syntax_error(self): 37 | self.assertRaises(SyntaxError, fastobo.loads, "hello there") 38 | 39 | def test_threading_single(self): 40 | doc = fastobo.load(MS, threads=1) 41 | self.assertEqual(len(doc), MS_FRAMES) 42 | 43 | with open(MS, 'rb') as f: 44 | doc = fastobo.load(f, threads=1) 45 | self.assertEqual(len(doc), MS_FRAMES) 46 | 47 | def test_threading_explicit(self): 48 | doc = fastobo.load(MS, threads=4) 49 | self.assertEqual(len(doc), MS_FRAMES) 50 | 51 | with open(MS, 'rb') as f: 52 | doc = fastobo.load(f, threads=4) 53 | self.assertEqual(len(doc), MS_FRAMES) 54 | 55 | def test_threading_detect(self): 56 | doc = fastobo.load(MS, threads=0) 57 | self.assertEqual(len(doc), MS_FRAMES) 58 | 59 | with open(MS, 'rb') as f: 60 | doc = fastobo.load(f, threads=0) 61 | self.assertEqual(len(doc), MS_FRAMES) 62 | 63 | def test_threading_invalid(self): 64 | self.assertRaises(ValueError, fastobo.load, MS, threads=-1) 65 | 66 | def test_load_path(self): 67 | doc = fastobo.load(pathlib.Path(MS)) 68 | self.assertEqual(len(doc), MS_FRAMES) 69 | 70 | 71 | class TestIter(unittest.TestCase): 72 | 73 | def test_file_not_found(self): 74 | self.assertRaises(FileNotFoundError, fastobo.iter, "abcdef") 75 | 76 | def test_type_error(self): 77 | self.assertRaises(TypeError, fastobo.iter, 1) 78 | self.assertRaises(TypeError, fastobo.iter, []) 79 | 80 | with open(MS) as f: 81 | self.assertRaises(TypeError, fastobo.iter, f) 82 | 83 | def test_syntax_error(self): 84 | f = io.BytesIO(b"format-version: 1.4\ndate: 05:20:2021 12:00\n") 85 | self.assertRaises(SyntaxError, fastobo.iter, f) 86 | 87 | def test_threading_single(self): 88 | frame_count = sum(1 for _ in fastobo.iter(MS, threads=1)) 89 | self.assertEqual(frame_count, MS_FRAMES) 90 | 91 | with open(MS, 'rb') as f: 92 | frame_count = sum(1 for _ in fastobo.iter(f, threads=1)) 93 | self.assertEqual(frame_count, MS_FRAMES) 94 | 95 | def test_threading_explicit(self): 96 | frame_count = sum(1 for _ in fastobo.iter(MS, threads=4)) 97 | self.assertEqual(frame_count, MS_FRAMES) 98 | 99 | with open(MS, 'rb') as f: 100 | frame_count = sum(1 for _ in fastobo.iter(f, threads=4)) 101 | self.assertEqual(frame_count, MS_FRAMES) 102 | 103 | def test_threading_detect(self): 104 | frame_count = sum(1 for _ in fastobo.iter(MS, threads=0)) 105 | self.assertEqual(frame_count, MS_FRAMES) 106 | 107 | with open(MS, 'rb') as f: 108 | frame_count = sum(1 for _ in fastobo.iter(f, threads=0)) 109 | self.assertEqual(frame_count, MS_FRAMES) 110 | 111 | def test_threading_invalid(self): 112 | self.assertRaises(ValueError, fastobo.iter, MS, threads=-1) 113 | 114 | def test_iter_path(self): 115 | n = sum(1 for _ in fastobo.iter(pathlib.Path(MS))) 116 | self.assertEqual(n, MS_FRAMES) 117 | 118 | 119 | class TestLoads(unittest.TestCase): 120 | 121 | @classmethod 122 | def setUpClass(cls): 123 | with open(MS, 'r') as f: 124 | cls.text = f.read() 125 | 126 | def test_threading_single(self): 127 | doc = fastobo.loads(self.text, threads=1) 128 | self.assertEqual(len(doc), MS_FRAMES) 129 | 130 | def test_threading_explicit(self): 131 | doc = fastobo.loads(self.text, threads=4) 132 | self.assertEqual(len(doc), MS_FRAMES) 133 | 134 | def test_threading_detect(self): 135 | doc = fastobo.loads(self.text, threads=0) 136 | self.assertEqual(len(doc), MS_FRAMES) 137 | 138 | def test_threading_invalid(self): 139 | self.assertRaises(ValueError, fastobo.loads, self.text, threads=-1) 140 | -------------------------------------------------------------------------------- /tests/test_header.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | import sys 6 | 7 | import fastobo 8 | 9 | # --- HeaderFrame ------------------------------------------------------------ 10 | 11 | class TestHeaderFrame(unittest.TestCase): 12 | 13 | type = fastobo.header.HeaderFrame 14 | 15 | def test_init(self): 16 | try: 17 | frame = fastobo.header.HeaderFrame([]) 18 | except Exception: 19 | self.fail("could not create `HeaderFrame` instance") 20 | try: 21 | frame = fastobo.header.HeaderFrame() 22 | except Exception: 23 | self.fail("could not create `HeaderFrame` instance") 24 | try: 25 | frame = fastobo.header.HeaderFrame(( 26 | fastobo.header.FormatVersionClause("1.2"), 27 | fastobo.header.SavedByClause("Martin Larralde"), 28 | )) 29 | except Exception: 30 | self.fail("could not create `HeaderFrame` instance from iterable") 31 | 32 | def test_init_type_error(self): 33 | self.assertRaises(TypeError, self.type, 1) 34 | self.assertRaises(TypeError, self.type, [1]) 35 | self.assertRaises(TypeError, self.type, ["abc"]) 36 | self.assertRaises(TypeError, self.type, "abc") 37 | 38 | # --- HeaderClause ----------------------------------------------------------- 39 | 40 | class _TestUnquotedStringClause(object): 41 | 42 | type = NotImplemented 43 | 44 | def test_init(self): 45 | try: 46 | vc = self.type("1.2") 47 | except Exception: 48 | self.fail("could not create `{}` instance", self.type.__name__) 49 | 50 | def test_init_type_error(self): 51 | self.assertRaises(TypeError, self.type, 1) 52 | self.assertRaises(TypeError, self.type, []) 53 | 54 | def test_repr(self): 55 | x = self.type("abc") 56 | r = self.type.__name__ 57 | if sys.implementation.name == "pypy": 58 | r = r.split(".")[-1] 59 | self.assertEqual(repr(x), "{}('abc')".format(r)) 60 | 61 | def test_eq(self): 62 | x = self.type("1.2") 63 | self.assertEqual(x, x) 64 | y = self.type("1.2") 65 | self.assertEqual(x, y) 66 | z = self.type("1.4") 67 | self.assertNotEqual(x, z) 68 | self.assertNotEqual(y, z) 69 | 70 | # --- FormatVersion ---------------------------------------------------------- 71 | 72 | class TestFormatVersionClause(_TestUnquotedStringClause, unittest.TestCase): 73 | 74 | type = fastobo.header.FormatVersionClause 75 | 76 | def test_str(self): 77 | vc = fastobo.header.FormatVersionClause("1.2") 78 | self.assertEqual(str(vc), "format-version: 1.2") 79 | vc = fastobo.header.FormatVersionClause("x:y") 80 | self.assertEqual(str(vc), "format-version: x:y") 81 | 82 | def test_property_version(self): 83 | vc1 = self.type("1.2") 84 | self.assertEqual(vc1.version, "1.2") 85 | vc1.version = "1.3" 86 | self.assertEqual(vc1.version, "1.3") 87 | self.assertEqual(repr(vc1), "FormatVersionClause('1.3')") 88 | 89 | def test_raw_tag(self): 90 | vc = self.type("1.2") 91 | self.assertEqual(vc.raw_tag(), "format-version") 92 | 93 | # --- DataVersion ------------------------------------------------------------ 94 | 95 | class TestDataVersionClause(_TestUnquotedStringClause, unittest.TestCase): 96 | 97 | type = fastobo.header.DataVersionClause 98 | 99 | def test_str(self): 100 | x = self.type("4.0") 101 | self.assertEqual(str(x), "data-version: 4.0") 102 | 103 | def test_property_version(self): 104 | vc1 = self.type("1.2") 105 | self.assertEqual(vc1.version, "1.2") 106 | vc1.version = "1.3" 107 | self.assertEqual(vc1.version, "1.3") 108 | self.assertEqual(repr(vc1), "DataVersionClause('1.3')") 109 | 110 | def test_raw_tag(self): 111 | vc = self.type("1.2") 112 | self.assertEqual(vc.raw_tag(), "data-version") 113 | 114 | # --- Date ------------------------------------------------------------------- 115 | 116 | class TestDateClause(unittest.TestCase): 117 | 118 | type = fastobo.header.DateClause 119 | 120 | def test_init(self): 121 | try: 122 | vc = self.type(datetime.datetime.now()) 123 | except Exception: 124 | self.fail("could not create `{}` instance", self.type.__name__) 125 | 126 | def test_init_type_error(self): 127 | self.assertRaises(TypeError, self.type, 1) 128 | self.assertRaises(TypeError, self.type, []) 129 | 130 | @unittest.expectedFailure 131 | def test_repr(self): 132 | now = datetime.datetime.now() 133 | x = self.type(now) 134 | self.assertEqual(repr(x), "{}({!r})".format(self.type.__name__, now)) 135 | 136 | @unittest.expectedFailure 137 | def test_eq(self): 138 | now = datetime.datetime.now() 139 | x = self.type(now) 140 | self.assertEqual(x, self.type(now)) 141 | self.assertNotEqual(x, self.type(datetime.datetime.now())) 142 | 143 | def test_str(self): 144 | then = datetime.datetime(2019, 4, 8, 16, 51) 145 | vc = self.type(then) 146 | self.assertEqual(str(vc), "date: 08:04:2019 16:51") 147 | 148 | @unittest.expectedFailure 149 | def test_property_version(self): 150 | now = datetime.datetime.now() 151 | vc1 = self.type(now) 152 | self.assertEqual(vc1.date, now) 153 | 154 | then = datetime.datetime(2019, 4, 8, 16, 51) 155 | vc1.date = then 156 | self.assertEqual(vc1.date, then) 157 | 158 | with self.assertRaises(TypeError): 159 | vc1.date = 1 160 | 161 | # --- SavedBy ---------------------------------------------------------------- 162 | 163 | class TestSavedByClause(_TestUnquotedStringClause, unittest.TestCase): 164 | 165 | type = fastobo.header.SavedByClause 166 | 167 | # --- AutoGeneratedBy -------------------------------------------------------- 168 | 169 | class TestAutoGeneratedByClause(_TestUnquotedStringClause, unittest.TestCase): 170 | 171 | type = fastobo.header.AutoGeneratedByClause 172 | 173 | # --- Import ----------------------------------------------------------------- 174 | # --- Subsetdef -------------------------------------------------------------- 175 | # --- SynonymTypedef --------------------------------------------------------- 176 | # --- DefaultNamespace ------------------------------------------------------- 177 | # --- IdspaceClause ---------------------------------------------------------- 178 | # --- TreatXrefsAsEquivalentClause ------------------------------------------- 179 | # --- TreatXrefsAsGenusDifferentiaClause ------------------------------------- 180 | # --- TreatXrefsAsReverseGenusDifferentiaClause ------------------------------ 181 | # --- TreatXrefsAsRelationshipClause ----------------------------------------- 182 | # --- TreatXrefsAsIsA -------------------------------------------------------- 183 | # --- TreatXrefsAsHasSubclassClause ------------------------------------------ 184 | # --- PropertyValue ---------------------------------------------------------- 185 | # --- Remark ----------------------------------------------------------------- 186 | 187 | class TestRemarkClause(_TestUnquotedStringClause, unittest.TestCase): 188 | 189 | type = fastobo.header.RemarkClause 190 | 191 | # --- Ontology --------------------------------------------------------------- 192 | 193 | class TestOntologyClause(_TestUnquotedStringClause, unittest.TestCase): 194 | 195 | type = fastobo.header.OntologyClause 196 | 197 | # --- OwlAxioms -------------------------------------------------------------- 198 | 199 | class TestOwlAxiomsClause(_TestUnquotedStringClause, unittest.TestCase): 200 | 201 | type = fastobo.header.OwlAxiomsClause 202 | 203 | # --- UnreservedClause ------------------------------------------------------- 204 | -------------------------------------------------------------------------------- /tests/test_id.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | 6 | import fastobo 7 | 8 | 9 | class _TestBaseIdent(object): 10 | 11 | type = NotImplemented 12 | 13 | def test_init_type_error(self): 14 | self.assertRaises(TypeError, self.type, 123) 15 | self.assertRaises(TypeError, self.type, []) 16 | 17 | 18 | class TestUnprefixedIdent(_TestBaseIdent, unittest.TestCase): 19 | 20 | type = fastobo.id.UnprefixedIdent 21 | 22 | def test_init(self): 23 | try: 24 | self.type('created_by') 25 | except Exception: 26 | self.fail("could not instantiate `UnprefixedIdent`") 27 | 28 | def test_eq(self): 29 | ident = self.type('derived_from') 30 | self.assertEqual(ident, self.type('derived_from')) 31 | self.assertNotEqual(ident, self.type('has_elements_from')) 32 | self.assertNotEqual(ident, 123) 33 | 34 | def test_cmp(self): 35 | ident = self.type('derived_from') 36 | self.assertLessEqual(ident, self.type('derived_from')) 37 | self.assertLessEqual(ident, self.type('has_elements_from')) 38 | self.assertRaises(TypeError, ident.__lt__, 123) 39 | self.assertRaises(TypeError, ident.__le__, 123) 40 | self.assertRaises(TypeError, ident.__gt__, 123) 41 | self.assertRaises(TypeError, ident.__ge__, 123) 42 | 43 | def test_hash(self): 44 | ident = self.type('derived_from') 45 | self.assertEqual(hash(ident), hash(self.type('derived_from'))) 46 | self.assertNotEqual(hash(ident), hash(self.type('has_elements_from'))) 47 | 48 | 49 | class TestPrefixedIdent(_TestBaseIdent, unittest.TestCase): 50 | 51 | type = fastobo.id.PrefixedIdent 52 | 53 | def test_init(self): 54 | try: 55 | self.type('GO', '0070412') 56 | except Exception: 57 | self.fail("could not instantiate `PrefixedIdent`") 58 | 59 | def test_init_type_error(self): 60 | self.assertRaises(TypeError, self.type, "GO", 123) 61 | self.assertRaises(TypeError, self.type, "GO", []) 62 | self.assertRaises(TypeError, self.type, 123, "0070412") 63 | self.assertRaises(TypeError, self.type, [], "0070412") 64 | 65 | def test_hash(self): 66 | ident = self.type("GO", "0070412") 67 | self.assertEqual(hash(ident), hash(self.type("GO", "0070412"))) 68 | self.assertNotEqual(hash(ident), hash(self.type("GO", "0070413"))) 69 | 70 | 71 | class TestUrl(_TestBaseIdent, unittest.TestCase): 72 | 73 | type = fastobo.id.Url 74 | 75 | def test_init(self): 76 | try: 77 | self.type('http://purl.obolibrary.org/obo/GO_0070412') 78 | except Exception: 79 | self.fail("could not instantiate `Url`") 80 | 81 | def test_init_type_error(self): 82 | self.assertRaises(TypeError, self.type, 123) 83 | self.assertRaises(TypeError, self.type, []) 84 | 85 | def test_init_value_error(self): 86 | self.assertRaises(ValueError, self.type, "not a URL at all") 87 | 88 | def test_eq(self): 89 | url = self.type('http://purl.obolibrary.org/obo/GO_0070412') 90 | self.assertEqual(url, self.type('http://purl.obolibrary.org/obo/GO_0070412')) 91 | self.assertNotEqual(url, self.type('http://purl.obolibrary.org/obo/GO_0070413')) 92 | self.assertNotEqual(url, 123) 93 | 94 | def test_cmp(self): 95 | url = self.type('http://purl.obolibrary.org/obo/GO_0070412') 96 | self.assertLess(url, self.type('http://purl.obolibrary.org/obo/GO_0070413')) 97 | self.assertRaises(TypeError, url.__lt__, 'http://purl.obolibrary.org/obo/GO_0070413') 98 | 99 | def test_hash(self): 100 | url = self.type('http://purl.obolibrary.org/obo/GO_0070412') 101 | self.assertEqual(hash(url), hash(self.type('http://purl.obolibrary.org/obo/GO_0070412'))) 102 | self.assertNotEqual(hash(url), hash(self.type('http://purl.obolibrary.org/obo/GO_0070413'))) 103 | -------------------------------------------------------------------------------- /tests/test_pv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | 6 | import fastobo 7 | 8 | 9 | 10 | class TestLiteralPropertyValue(unittest.TestCase): 11 | 12 | type = fastobo.pv.LiteralPropertyValue 13 | 14 | def test_init(self): 15 | rel = fastobo.id.UnprefixedIdent("creation_date") 16 | value = "2019-04-08T23:21:05Z" 17 | dt = fastobo.id.PrefixedIdent("xsd", "date") 18 | try: 19 | pv = self.type(rel, value, dt) 20 | except Exception: 21 | self.fail("could not create `LiteralPropertyValue` instance") 22 | 23 | def test_init_type_error(self): 24 | rel = fastobo.id.UnprefixedIdent("creation_date") 25 | value = "2019-04-08T23:21:05Z" 26 | dt = fastobo.id.PrefixedIdent("xsd", "date") 27 | self.assertRaises(TypeError, self.type, 1, value, dt) 28 | self.assertRaises(TypeError, self.type, rel, 1, dt) 29 | self.assertRaises(TypeError, self.type, rel, value, 1) 30 | 31 | def test_property_relation(self): 32 | rel = fastobo.id.UnprefixedIdent("creation_date") 33 | value = "2019-04-08T23:21:05Z" 34 | dt = fastobo.id.PrefixedIdent("xsd", "date") 35 | pv = self.type(rel, value, dt) 36 | self.assertEqual(pv.relation, rel) 37 | 38 | rel2 = fastobo.id.PrefixedIdent("IAO", "0000219") 39 | pv.relation = rel2 40 | self.assertEqual(pv.relation, rel2) 41 | 42 | with self.assertRaises(TypeError): 43 | pv.relation = "IAO:0000219" 44 | 45 | def test_str(self): 46 | rel = fastobo.id.UnprefixedIdent("creation_date") 47 | value = "2019-04-08T23:21:05Z" 48 | dt = fastobo.id.PrefixedIdent("xsd", "date") 49 | pv = self.type(rel, value, dt) 50 | self.assertEqual( 51 | str(pv), 52 | 'creation_date "2019-04-08T23:21:05Z" xsd:date' 53 | ) 54 | 55 | def test_repr(self): 56 | rel = fastobo.id.UnprefixedIdent("creation_date") 57 | value = "2019-04-08T23:21:05Z" 58 | dt = fastobo.id.PrefixedIdent("xsd", "date") 59 | pv = self.type(rel, value, dt) 60 | self.assertEqual( 61 | repr(pv), 62 | "LiteralPropertyValue(" 63 | "UnprefixedIdent('creation_date'), " 64 | "'2019-04-08T23:21:05Z', " 65 | "PrefixedIdent('xsd', 'date'))" 66 | ) 67 | 68 | 69 | 70 | class TestResourcePropertyValue(unittest.TestCase): 71 | 72 | type = fastobo.pv.ResourcePropertyValue 73 | 74 | def test_init(self): 75 | rel = fastobo.id.UnprefixedIdent("derived_from") 76 | value = fastobo.id.PrefixedIdent("MS", "1000031") 77 | try: 78 | pv = self.type(rel, value) 79 | except Exception: 80 | self.fail("could not create `ResourcePropertyValue` instance") 81 | 82 | def test_init_type_error(self): 83 | rel = fastobo.id.UnprefixedIdent("derived_from") 84 | value = fastobo.id.PrefixedIdent("MS", "1000031") 85 | self.assertRaises(TypeError, self.type, 1, value) 86 | self.assertRaises(TypeError, self.type, rel, 1) 87 | 88 | def test_property_relation(self): 89 | rel = fastobo.id.UnprefixedIdent("derived_from") 90 | value = fastobo.id.PrefixedIdent("MS", "1000031") 91 | pv = self.type(rel, value) 92 | self.assertEqual(pv.relation, rel) 93 | 94 | rel2 = fastobo.id.UnprefixedIdent("something") 95 | pv.relation = rel2 96 | self.assertEqual(pv.relation, rel2) 97 | 98 | with self.assertRaises(TypeError): 99 | pv.relation = "IAO:0000219" 100 | 101 | def test_str(self): 102 | rel = fastobo.id.UnprefixedIdent("derived_from") 103 | value = fastobo.id.PrefixedIdent("MS", "1000031") 104 | pv = self.type(rel, value) 105 | self.assertEqual(str(pv), "derived_from MS:1000031") 106 | 107 | def test_repr(self): 108 | rel = fastobo.id.UnprefixedIdent("derived_from") 109 | value = fastobo.id.PrefixedIdent("MS", "1000031") 110 | pv = self.type(rel, value) 111 | self.assertEqual( 112 | repr(pv), 113 | "ResourcePropertyValue(" 114 | "UnprefixedIdent('derived_from'), " 115 | "PrefixedIdent('MS', '1000031'))" 116 | ) 117 | -------------------------------------------------------------------------------- /tests/test_term.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | 6 | import fastobo 7 | 8 | from .common import ( 9 | _TestFrame, 10 | _TestIsObsoleteClause, 11 | _TestDefClause, 12 | _TestConsiderClause, 13 | _TestCreationDateClause, 14 | ) 15 | 16 | # --- TermFrame -------------------------------------------------------------- 17 | 18 | class TestTermFrame(_TestFrame, unittest.TestCase): 19 | Frame = fastobo.term.TermFrame 20 | NameClause = fastobo.term.NameClause 21 | CreatedByClause = fastobo.term.CreatedByClause 22 | 23 | 24 | # --- DefClause -------------------------------------------------------------- 25 | 26 | class TestDefClause(_TestDefClause, unittest.TestCase): 27 | type = fastobo.term.DefClause 28 | 29 | 30 | # --- ConsiderClause --------------------------------------------------------- 31 | 32 | class TestConsiderClause(_TestConsiderClause, unittest.TestCase): 33 | type = fastobo.term.ConsiderClause 34 | 35 | 36 | # --- IsObsoleteClause ------------------------------------------------------- 37 | 38 | class TestIsObsoleteClause(_TestIsObsoleteClause, unittest.TestCase): 39 | type = fastobo.term.IsObsoleteClause 40 | 41 | 42 | # --- CreationDateClause ----------------------------------------------------- 43 | 44 | class TestCreationDateClause(_TestCreationDateClause, unittest.TestCase): 45 | type = fastobo.term.CreationDateClause 46 | -------------------------------------------------------------------------------- /tests/test_typedef.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | 6 | import fastobo 7 | 8 | from .common import ( 9 | _TestFrame, 10 | _TestIsObsoleteClause, 11 | _TestDefClause, 12 | _TestConsiderClause, 13 | _TestCreationDateClause, 14 | ) 15 | 16 | # --- TypedefFrame ----------------------------------------------------------- 17 | 18 | class TestTypedefFrame(_TestFrame, unittest.TestCase): 19 | Frame = fastobo.typedef.TypedefFrame 20 | NameClause = fastobo.typedef.NameClause 21 | CreatedByClause = fastobo.typedef.CreatedByClause 22 | 23 | 24 | # --- DefClause -------------------------------------------------------------- 25 | 26 | class TestDefClause(_TestDefClause, unittest.TestCase): 27 | type = fastobo.typedef.DefClause 28 | 29 | 30 | # --- ConsiderClause --------------------------------------------------------- 31 | 32 | class TestConsiderClause(_TestConsiderClause, unittest.TestCase): 33 | type = fastobo.typedef.ConsiderClause 34 | 35 | 36 | # --- IsObsoleteClause ------------------------------------------------------- 37 | 38 | class TestIsObsoleteClause(_TestIsObsoleteClause, unittest.TestCase): 39 | type = fastobo.typedef.IsObsoleteClause 40 | 41 | 42 | # --- CreationDateClause ----------------------------------------------------- 43 | 44 | class TestCreationDateClause(_TestCreationDateClause, unittest.TestCase): 45 | type = fastobo.typedef.CreationDateClause 46 | -------------------------------------------------------------------------------- /tests/test_xref.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import datetime 4 | import unittest 5 | 6 | import fastobo 7 | 8 | 9 | class TestXref(unittest.TestCase): 10 | 11 | type = fastobo.xref.Xref 12 | 13 | def test_init(self): 14 | id = fastobo.id.PrefixedIdent('ISBN', '0321842685') 15 | try: 16 | xref = self.type(id) 17 | except Exception: 18 | self.fail("could not create `Xref` instance without description") 19 | try: 20 | xref = self.type(id, "Hacker's Delight (2nd Edition)") 21 | except Exception: 22 | self.fail("could not create `Xref` instance with description") 23 | 24 | def test_init_type_error(self): 25 | id = fastobo.id.PrefixedIdent('ISBN', '0321842685') 26 | desc = "Hacker's Delight (2nd Edition)" 27 | self.assertRaises(TypeError, self.type, 1) 28 | self.assertRaises(TypeError, self.type, 1, desc) 29 | self.assertRaises(TypeError, self.type, id, 1) 30 | 31 | def test_str(self): 32 | id = fastobo.id.PrefixedIdent('ISBN', '0321842685') 33 | desc = "Hacker's Delight (2nd Edition)" 34 | self.assertEqual(str(self.type(id)), "ISBN:0321842685") 35 | self.assertEqual( 36 | str(self.type(id, desc)), 37 | 'ISBN:0321842685 "Hacker\'s Delight (2nd Edition)"' 38 | ) 39 | 40 | def test_eq(self): 41 | i1 = fastobo.id.UnprefixedIdent('a') 42 | i2 = fastobo.id.UnprefixedIdent('b') 43 | x1 = self.type(i1) 44 | self.assertEqual(x1, x1) 45 | x2 = self.type(i1) 46 | self.assertIsNot(x1, x2) 47 | self.assertEqual(x1, x2) 48 | x3 = self.type(i2) 49 | self.assertNotEqual(x1, x2) 50 | 51 | 52 | class TestXrefList(unittest.TestCase): 53 | 54 | type = fastobo.xref.XrefList 55 | 56 | def setUp(self): 57 | id = fastobo.id.PrefixedIdent('ISBN', '0321842685') 58 | desc = "Hacker's Delight (2nd Edition)" 59 | self.x1 = fastobo.xref.Xref(id, desc) 60 | self.x2 = fastobo.xref.Xref(fastobo.id.UnprefixedIdent("fastobo")) 61 | 62 | def test_init(self): 63 | try: 64 | xref = self.type() 65 | except Exception: 66 | self.fail("could not create `XrefList` instance without argument") 67 | try: 68 | xref = self.type([self.x1, self.x2]) 69 | except Exception: 70 | self.fail("could not create `XrefList` instance from list") 71 | try: 72 | xref = self.type(iter([self.x1, self.x2])) 73 | except Exception: 74 | self.fail("could not create `XrefList` instance from iterator") 75 | 76 | def test_init_type_error(self): 77 | # Errors on an iterator of type != Xref 78 | self.assertRaises(TypeError, self.type, "abc") 79 | self.assertRaises(TypeError, self.type, ["abc", "def"]) 80 | 81 | def test_str(self): 82 | x1, x2 = self.x1, self.x2 83 | self.assertEqual(str(self.type()), "[]") 84 | self.assertEqual(str(self.type([x1])), '[{}]'.format(x1)) 85 | self.assertEqual(str(self.type([x1, x2])), '[{}, {}]'.format(x1, x2)) 86 | 87 | def test_append(self): 88 | x1, x2 = self.x1, self.x2 89 | l = self.type() 90 | self.assertEqual(len(l), 0) 91 | l.append(x1) 92 | self.assertEqual(len(l), 1) 93 | self.assertEqual(l[0], x1) 94 | l.append(x2) 95 | self.assertEqual(len(l), 2) 96 | self.assertEqual(l[0], x1) 97 | self.assertEqual(l[1], x2) 98 | 99 | def test_contains(self): 100 | x1, x2 = self.x1, self.x2 101 | l1 = self.type() 102 | self.assertNotIn(x1, l1) 103 | self.assertNotIn(x2, l1) 104 | l2 = self.type([x1]) 105 | self.assertIn(x1, l2) 106 | self.assertNotIn(x2, l2) 107 | l3 = self.type([x1, x2]) 108 | self.assertIn(x1, l3) 109 | self.assertIn(x2, l3) 110 | 111 | def test_repr(self): 112 | x1, x2 = self.x1, self.x2 113 | self.assertEqual( repr(self.type()), "XrefList()" ) 114 | self.assertEqual( repr(self.type([x1])), "XrefList([{!r}])".format(x1) ) 115 | self.assertEqual( repr(self.type([x1, x2])), "XrefList([{!r}, {!r}])".format(x1, x2) ) 116 | -------------------------------------------------------------------------------- /tests/unittest.rs: -------------------------------------------------------------------------------- 1 | extern crate fastobo_py; 2 | extern crate pyo3; 3 | 4 | use std::path::Path; 5 | 6 | use pyo3::prelude::*; 7 | use pyo3::types::PyDict; 8 | use pyo3::types::PyList; 9 | use pyo3::types::PyModule; 10 | use pyo3::Python; 11 | 12 | pub fn main() -> PyResult<()> { 13 | // get the relative path to the project folder 14 | let folder = Path::new(file!()).parent().unwrap().parent().unwrap(); 15 | 16 | // spawn a Python interpreter 17 | pyo3::prepare_freethreaded_python(); 18 | Python::with_gil(|py| { 19 | // insert the project folder in `sys.modules` so that 20 | // the main module can be imported by Python 21 | let sys = py.import("sys").unwrap(); 22 | sys.getattr("path") 23 | .unwrap() 24 | .downcast::() 25 | .unwrap() 26 | .insert(0, folder) 27 | .unwrap(); 28 | 29 | // create a Python module from our rust code with debug symbols 30 | let module = PyModule::new(py, "fastobo").unwrap(); 31 | fastobo_py::py::init(py, &module).unwrap(); 32 | sys.getattr("modules") 33 | .unwrap() 34 | .downcast::() 35 | .unwrap() 36 | .set_item("fastobo", module) 37 | .unwrap(); 38 | 39 | // run unittest on the tests 40 | let kwargs = PyDict::new(py); 41 | kwargs.set_item("exit", false).unwrap(); 42 | kwargs.set_item("verbosity", 2u8).unwrap(); 43 | py.import("unittest") 44 | .unwrap() 45 | .call_method("TestProgram", ("tests",), Some(&kwargs)) 46 | .map(|_| ()) 47 | }) 48 | } 49 | --------------------------------------------------------------------------------