diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 0000000..077e3ac --- /dev/null +++ b/.cruft.json @@ -0,0 +1,24 @@ +{ + "template": "https://github.com/monarch-initiative/monarch-project-template", + "commit": "e386290d6a462687e324034af29e879f18ee7364", + "checkout": null, + "context": { + "cookiecutter": { + "project_name": "mondolib", + "github_org_name": "monarch-initiative", + "__project_slug": "mondolib", + "project_description": "Python library for mondo.", + "min_python_version": "3.9", + "file_name": "main", + "greeting_recipient": "World", + "full_name": "Harshad Hegde", + "email": "hhegde@lbl.gov", + "__author": "Harshad Hegde ", + "license": "MIT", + "github_token_for_doc_deployment": "GH_TOKEN", + "github_token_for_pypi_deployment": "PYPI_TOKEN", + "_template": "https://github.com/monarch-initiative/monarch-project-template" + } + }, + "directory": null +} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..aa3b874 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,43 @@ +name: Auto-deployment of Documentation +on: + push: + branches: [ main ] +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3.0.2 + with: + fetch-depth: 0 # otherwise, you will failed to push refs to dest repo + + - name: Set up Python 3. + uses: actions/setup-python@v3 + with: + python-version: 3.9 + + - name: Install Poetry. + uses: snok/install-poetry@v1.3.1 + + - name: Install dependencies. + run: | + poetry install --with docs + + - name: Build documentation. + run: | + echo ${{ secrets.GH_TOKEN }} >> src/mondolib/token.txt + mkdir gh-pages + touch gh-pages/.nojekyll + cd docs/ + poetry run sphinx-apidoc -o . ../src/mondolib/ --ext-autodoc -f + poetry run sphinx-build -b html . _build + cp -r _build/* ../gh-pages/ + + - name: Deploy documentation. + if: ${{ github.event_name == 'push' }} + uses: JamesIves/github-pages-deploy-action@v4.4.1 + with: + branch: gh-pages + force: true + folder: gh-pages + token: ${{ secrets.GH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 0000000..e100f1f --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,36 @@ +name: Publish Python Package + +on: + workflow_dispatch: + release: + types: [created] + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to PyPI + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3.0.2 + + - name: Set up Python + uses: actions/setup-python@v3.1.2 + with: + python-version: 3.9 + + - name: Install Poetry + run: pip install poetry poetry-dynamic-versioning + + - name: Install dependencies + run: poetry install --no-interaction + + - name: Build source and wheel archives + run: poetry build + + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@v1.5.0 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + + \ No newline at end of file diff --git a/.github/workflows/qc.yml b/.github/workflows/qc.yml new file mode 100644 index 0000000..cae1397 --- /dev/null +++ b/.github/workflows/qc.yml @@ -0,0 +1,36 @@ +name: mondolib QC + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ "3.8", "3.9", "3.10" ] + + steps: + - uses: actions/checkout@v3.0.2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1.3.1 + - name: Install dependencies + run: poetry install --no-interaction + + - name: Check common spelling errors + run: poetry run tox -e codespell + + - name: Check code quality with flake8 + run: poetry run tox -e lint + + - name: Test with pytest and generate coverage file + run: poetry run tox -e py diff --git a/.gitignore b/.gitignore index 63f12dd..aaf41ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,134 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# User-defined .idea/ .template.db tests/input/example-relation-graph.tsv.gz diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..54fa3cb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +default_language_version: + python: python3 +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: trailing-whitespace +- repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.278 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] +- repo: https://github.com/codespell-project/codespell + rev: v2.2.4 + hooks: + - id: codespell + additional_dependencies: + - tomli \ No newline at end of file diff --git a/.template.db b/.template.db new file mode 100644 index 0000000..f619abb Binary files /dev/null and b/.template.db differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9a2d8bf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contribution Guidelines + +When contributing to this repository, please first discuss the changes you wish to make via an issue, email, or any other method, with the owners of this repository before issuing a pull request. + +## How to contribute + +### Reporting bugs or making feature requests + +To report a bug or suggest a new feature, please go to the [monarch-initiative/mondolib issue tracker](https://github.com/monarch-initiative/mondolib/issues), as we are +consolidating issues there. + +Please supply enough details to the developers to enable them to verify and troubleshoot your issue: + +* Provide a clear and descriptive title as well as a concise summary of the issue to identify the problem. +* Describe the exact steps which reproduce the problem in as many details as possible. +* Describe the behavior you observed after following the steps and point out what exactly is the problem with that behavior. +* Explain which behavior you expected to see instead and why. +* Provide screenshots of the expected or actual behaviour where applicable. + + +### The development lifecycle + +1. Create a bug fix or feature development branch, based off the `main` branch of the upstream repo, and not your fork. Name the branch appropriately, briefly summarizing the bug fix or feature request. If none come to mind, you can include the issue number in the branch name. Some examples of branch names are, `bugfix/breaking-pipfile-error` or `feature/add-click-cli-layer`, or `bugfix/issue-414` +2. Make sure your development branch has all the latest commits from the `main` branch. +3. After completing work and testing locally, push the code to the appropriate branch on your fork. +4. Create a pull request from the bug/feature branch of your fork to the `main` branch of the upstream repository. + +Note: All the development must be done on a branch on your fork. + +ALSO NOTE: github.com lets you create a pull request from the main branch, automating the steps above. + +> A code review (which happens with both the contributor and the reviewer present) is required for contributing. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf2a691 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2023 Chris Mungall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index d58baa3..6edf489 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,32 @@ # mondolib -Python library for mondo QC - -This has intentionally bespoke and mondo-unique checks +Python library for Mondo. +## Prerequisites +**1. OAK** Currently in order to understand how to use or code with this library you should understand the basics of [OAK](https://incatools.github.io/ontology-access-kit/) -## Install - -No PyPI yet +**2. Inputs** +The intended input of this lib is generally the editors file. -from this repo, do a +We recommend that this is converted into sqlite first for speed ``` -poetry install +robot convert -i mondo-edit.obo -o mondo-edit.owl +poetry run semsql make --docker mondo-edit.db ``` -## Inputs +See the Makefile and tests/input for an example file -The intended input of this lib is generally the editors file. +## Installation -We recommend that this is converted into sqlite first for speed +No PyPI yet + +from this repo, do a ``` -robot convert -i mondo-edit.obo -o mondo-edit.owl -poetry run semsql make --docker mondo-edit.db +poetry install ``` -See the Makefile and tests/input for an example file +## Modules +### QC +This has intentionally bespoke and mondo-unique checks diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..30ac574 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,60 @@ +"""Configuration file for the Sphinx documentation builder.""" +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os +import re +import sys +from datetime import date +from mondolib import __version__ +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'mondolib' +copyright = f"{date.today().year}, Chris Mungall " +author = 'Chris Mungall ' +release = __version__ + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.githubpages", + "sphinx_rtd_theme", + "sphinx_click", + "sphinx_autodoc_typehints", + "myst_parser" +] + +# generate autosummary pages +autosummary_generate = True + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +templates_path = ['_templates'] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +if os.path.exists("logo.png"): + html_logo = "logo.png" diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..bfe0385 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +.. mondolib documentation master file, created by + sphinx-quickstart on Fri Aug 12 08:35:01 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to mondolib's documentation! +========================================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/src/mondolib/__init__.py b/src/mondolib/__init__.py index b794fd4..b6ae6ce 100644 --- a/src/mondolib/__init__.py +++ b/src/mondolib/__init__.py @@ -1 +1,8 @@ -__version__ = '0.1.0' +"""mondolib package.""" +import importlib_metadata + +try: + __version__ = importlib_metadata.version(__name__) +except importlib_metadata.PackageNotFoundError: + # package is not installed + __version__ = "0.0.0" # pragma: no cover diff --git a/src/mondolib/cli.py b/src/mondolib/cli.py new file mode 100644 index 0000000..eb1fa73 --- /dev/null +++ b/src/mondolib/cli.py @@ -0,0 +1,44 @@ +"""Command line interface for mondolib.""" +import logging + +import click + +from mondolib import __version__ +from mondolib.main import demo + +__all__ = [ + "main", +] + +logger = logging.getLogger(__name__) + + +@click.group() +@click.option("-v", "--verbose", count=True) +@click.option("-q", "--quiet") +@click.version_option(__version__) +def main(verbose: int, quiet: bool): + """ + CLI for mondolib. + + :param verbose: Verbosity while running. + :param quiet: Boolean to be quiet or verbose. + """ + if verbose >= 2: + logger.setLevel(level=logging.DEBUG) + elif verbose == 1: + logger.setLevel(level=logging.INFO) + else: + logger.setLevel(level=logging.WARNING) + if quiet: + logger.setLevel(level=logging.ERROR) + + +@main.command() +def run(): + """Run the mondolib's demo command.""" + demo() + + +if __name__ == "__main__": + main() diff --git a/src/mondolib/config/__init__.py b/src/mondolib/config/__init__.py index e69de29..d52ccbe 100644 --- a/src/mondolib/config/__init__.py +++ b/src/mondolib/config/__init__.py @@ -0,0 +1 @@ +"""mondolib config.""" diff --git a/src/mondolib/datamodels/__init__.py b/src/mondolib/datamodels/__init__.py index e69de29..f388e23 100644 --- a/src/mondolib/datamodels/__init__.py +++ b/src/mondolib/datamodels/__init__.py @@ -0,0 +1 @@ +"""mondolib datamodels.""" diff --git a/src/mondolib/datamodels/mondolib_schema.py b/src/mondolib/datamodels/mondolib_schema.py index 4e30035..40ffb8b 100644 --- a/src/mondolib/datamodels/mondolib_schema.py +++ b/src/mondolib/datamodels/mondolib_schema.py @@ -1,3 +1,4 @@ +"""Autogenerated Schema file.""" # Auto generated from mondolib_schema.yaml by pythongen.py version: 0.9.0 # Generation date: 2022-06-10T11:39:26 # Schema: mondolib_schema @@ -7,23 +8,18 @@ # license: https://creativecommons.org/publicdomain/zero/1.0/ import dataclasses -import sys -import re -from jsonasobj2 import JsonObj, as_dict -from typing import Optional, List, Union, Dict, ClassVar, Any from dataclasses import dataclass -from linkml_runtime.linkml_model.meta import EnumDefinition, PermissibleValue, PvFormulaOptions +from typing import Any, ClassVar, Dict, List, Optional, Union -from linkml_runtime.utils.slot import Slot -from linkml_runtime.utils.metamodelcore import empty_list, empty_dict, bnode -from linkml_runtime.utils.yamlutils import YAMLRoot, extended_str, extended_float, extended_int +from jsonasobj2 import as_dict +from linkml_runtime.linkml_model.meta import EnumDefinition, PermissibleValue +from linkml_runtime.utils.curienamespace import CurieNamespace from linkml_runtime.utils.dataclass_extensions_376 import dataclasses_init_fn_with_kwargs -from linkml_runtime.utils.formatutils import camelcase, underscore, sfx from linkml_runtime.utils.enumerations import EnumDefinitionImpl -from rdflib import Namespace, URIRef -from linkml_runtime.utils.curienamespace import CurieNamespace -from linkml_runtime.linkml_model.types import Boolean, Float, String -from linkml_runtime.utils.metamodelcore import Bool +from linkml_runtime.utils.metamodelcore import Bool, empty_dict, empty_list +from linkml_runtime.utils.slot import Slot +from linkml_runtime.utils.yamlutils import YAMLRoot, extended_str +from rdflib import URIRef metamodel_version = "1.7.0" @@ -31,23 +27,26 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs # Namespaces -LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/') -MONDOLIB = CurieNamespace('mondolib', 'https://purl.obolibrary.org/obo/mondo/schema/') +LINKML = CurieNamespace("linkml", "https://w3id.org/linkml/") +MONDOLIB = CurieNamespace("mondolib", "https://purl.obolibrary.org/obo/mondo/schema/") DEFAULT_ = MONDOLIB # Types + # Class references class CandidateObsoletionTerm(extended_str): + """CandidateObsoletionTerm.""" + pass @dataclass class LexicalPattern(YAMLRoot): - """ - A lexical pattern that is matched against labels - """ + + """A lexical pattern that is matched against labels.""" + _inherited_slots: ClassVar[List[str]] = [] class_class_uri: ClassVar[URIRef] = MONDOLIB.LexicalPattern @@ -78,6 +77,8 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]): @dataclass class Configuration(YAMLRoot): + """Configuration.""" + _inherited_slots: ClassVar[List[str]] = [] class_class_uri: ClassVar[URIRef] = MONDOLIB.Configuration @@ -91,9 +92,13 @@ class Configuration(YAMLRoot): def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]): if not isinstance(self.lexical_patterns, list): self.lexical_patterns = [self.lexical_patterns] if self.lexical_patterns is not None else [] - self.lexical_patterns = [v if isinstance(v, LexicalPattern) else LexicalPattern(**as_dict(v)) for v in self.lexical_patterns] + self.lexical_patterns = [ + v if isinstance(v, LexicalPattern) else LexicalPattern(**as_dict(v)) for v in self.lexical_patterns + ] - if self.exclude_terms_with_definitions is not None and not isinstance(self.exclude_terms_with_definitions, Bool): + if self.exclude_terms_with_definitions is not None and not isinstance( + self.exclude_terms_with_definitions, Bool + ): self.exclude_terms_with_definitions = Bool(self.exclude_terms_with_definitions) super().__post_init__(**kwargs) @@ -101,6 +106,8 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]): @dataclass class CandidateObsoletion(YAMLRoot): + """CandidateObsoletion.""" + _inherited_slots: ClassVar[List[str]] = [] class_class_uri: ClassVar[URIRef] = MONDOLIB.CandidateObsoletion @@ -112,7 +119,9 @@ class CandidateObsoletion(YAMLRoot): label: Optional[str] = None confidence: Optional[float] = None is_ordo_only: Optional[Union[bool, Bool]] = None - lexical_pattern_matches: Optional[Union[Union[dict, LexicalPattern], List[Union[dict, LexicalPattern]]]] = empty_list() + lexical_pattern_matches: Optional[ + Union[Union[dict, LexicalPattern], List[Union[dict, LexicalPattern]]] + ] = empty_list() direct_child_terms: Optional[Union[str, List[str]]] = empty_list() is_likely_grouping: Optional[Union[bool, Bool]] = None has_definition: Optional[Union[bool, Bool]] = None @@ -133,8 +142,12 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]): self.is_ordo_only = Bool(self.is_ordo_only) if not isinstance(self.lexical_pattern_matches, list): - self.lexical_pattern_matches = [self.lexical_pattern_matches] if self.lexical_pattern_matches is not None else [] - self.lexical_pattern_matches = [v if isinstance(v, LexicalPattern) else LexicalPattern(**as_dict(v)) for v in self.lexical_pattern_matches] + self.lexical_pattern_matches = ( + [self.lexical_pattern_matches] if self.lexical_pattern_matches is not None else [] + ) + self.lexical_pattern_matches = [ + v if isinstance(v, LexicalPattern) else LexicalPattern(**as_dict(v)) for v in self.lexical_pattern_matches + ] if not isinstance(self.direct_child_terms, list): self.direct_child_terms = [self.direct_child_terms] if self.direct_child_terms is not None else [] @@ -151,9 +164,9 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]): @dataclass class Report(YAMLRoot): - """ - A pan-ontology report. This focuses on bespoke Mondo checks rather than generic OBO checks - """ + + """A pan-ontology report. This focuses on bespoke Mondo checks rather than generic OBO checks.""" + _inherited_slots: ClassVar[List[str]] = [] class_class_uri: ClassVar[URIRef] = MONDOLIB.Report @@ -161,72 +174,180 @@ class Report(YAMLRoot): class_name: ClassVar[str] = "Report" class_model_uri: ClassVar[URIRef] = MONDOLIB.Report - candidate_obsoletions: Optional[Union[Dict[Union[str, CandidateObsoletionTerm], Union[dict, CandidateObsoletion]], List[Union[dict, CandidateObsoletion]]]] = empty_dict() + candidate_obsoletions: Optional[ + Union[ + Dict[Union[str, CandidateObsoletionTerm], Union[dict, CandidateObsoletion]], + List[Union[dict, CandidateObsoletion]], + ] + ] = empty_dict() def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]): - self._normalize_inlined_as_dict(slot_name="candidate_obsoletions", slot_type=CandidateObsoletion, key_name="term", keyed=True) + self._normalize_inlined_as_dict( + slot_name="candidate_obsoletions", slot_type=CandidateObsoletion, key_name="term", keyed=True + ) super().__post_init__(**kwargs) # Enumerations class ValidationCheckScope(EnumDefinitionImpl): - """ - Some validations are only performed in particular contexts or scopes - """ - ORDO_ONLY = PermissibleValue(text="ORDO_ONLY", - description="A scope in which the term is mapped solely to Ordo/Orphanet, and there are no mappings to other ontologies, and there have been no non-ORDO axioms added") + + """Some validations are only performed in particular contexts or scopes.""" + + ORDO_ONLY = PermissibleValue( + text="ORDO_ONLY", + description="A scope in which the term is mapped solely to Ordo/Orphanet, and there are no mappings to other ontologies, and there have been no non-ORDO axioms added", + ) _defn = EnumDefinition( name="ValidationCheckScope", description="Some validations are only performed in particular contexts or scopes", ) + # Slots class slots: - pass - -slots.lexicalPattern__pattern = Slot(uri=MONDOLIB.pattern, name="lexicalPattern__pattern", curie=MONDOLIB.curie('pattern'), - model_uri=MONDOLIB.lexicalPattern__pattern, domain=None, range=Optional[str]) - -slots.lexicalPattern__description = Slot(uri=MONDOLIB.description, name="lexicalPattern__description", curie=MONDOLIB.curie('description'), - model_uri=MONDOLIB.lexicalPattern__description, domain=None, range=Optional[str]) - -slots.lexicalPattern__obsoletion_reason = Slot(uri=MONDOLIB.obsoletion_reason, name="lexicalPattern__obsoletion_reason", curie=MONDOLIB.curie('obsoletion_reason'), - model_uri=MONDOLIB.lexicalPattern__obsoletion_reason, domain=None, range=Optional[str]) - -slots.lexicalPattern__scope = Slot(uri=MONDOLIB.scope, name="lexicalPattern__scope", curie=MONDOLIB.curie('scope'), - model_uri=MONDOLIB.lexicalPattern__scope, domain=None, range=Optional[Union[str, "ValidationCheckScope"]]) - -slots.configuration__lexical_patterns = Slot(uri=MONDOLIB.lexical_patterns, name="configuration__lexical_patterns", curie=MONDOLIB.curie('lexical_patterns'), - model_uri=MONDOLIB.configuration__lexical_patterns, domain=None, range=Optional[Union[Union[dict, LexicalPattern], List[Union[dict, LexicalPattern]]]]) - -slots.configuration__exclude_terms_with_definitions = Slot(uri=MONDOLIB.exclude_terms_with_definitions, name="configuration__exclude_terms_with_definitions", curie=MONDOLIB.curie('exclude_terms_with_definitions'), - model_uri=MONDOLIB.configuration__exclude_terms_with_definitions, domain=None, range=Optional[Union[bool, Bool]]) + """Slots.""" -slots.candidateObsoletion__term = Slot(uri=MONDOLIB.term, name="candidateObsoletion__term", curie=MONDOLIB.curie('term'), - model_uri=MONDOLIB.candidateObsoletion__term, domain=None, range=URIRef) - -slots.candidateObsoletion__label = Slot(uri=MONDOLIB.label, name="candidateObsoletion__label", curie=MONDOLIB.curie('label'), - model_uri=MONDOLIB.candidateObsoletion__label, domain=None, range=Optional[str]) - -slots.candidateObsoletion__confidence = Slot(uri=MONDOLIB.confidence, name="candidateObsoletion__confidence", curie=MONDOLIB.curie('confidence'), - model_uri=MONDOLIB.candidateObsoletion__confidence, domain=None, range=Optional[float]) - -slots.candidateObsoletion__is_ordo_only = Slot(uri=MONDOLIB.is_ordo_only, name="candidateObsoletion__is_ordo_only", curie=MONDOLIB.curie('is_ordo_only'), - model_uri=MONDOLIB.candidateObsoletion__is_ordo_only, domain=None, range=Optional[Union[bool, Bool]]) - -slots.candidateObsoletion__lexical_pattern_matches = Slot(uri=MONDOLIB.lexical_pattern_matches, name="candidateObsoletion__lexical_pattern_matches", curie=MONDOLIB.curie('lexical_pattern_matches'), - model_uri=MONDOLIB.candidateObsoletion__lexical_pattern_matches, domain=None, range=Optional[Union[Union[dict, LexicalPattern], List[Union[dict, LexicalPattern]]]]) - -slots.candidateObsoletion__direct_child_terms = Slot(uri=MONDOLIB.direct_child_terms, name="candidateObsoletion__direct_child_terms", curie=MONDOLIB.curie('direct_child_terms'), - model_uri=MONDOLIB.candidateObsoletion__direct_child_terms, domain=None, range=Optional[Union[str, List[str]]]) - -slots.candidateObsoletion__is_likely_grouping = Slot(uri=MONDOLIB.is_likely_grouping, name="candidateObsoletion__is_likely_grouping", curie=MONDOLIB.curie('is_likely_grouping'), - model_uri=MONDOLIB.candidateObsoletion__is_likely_grouping, domain=None, range=Optional[Union[bool, Bool]]) + pass -slots.candidateObsoletion__has_definition = Slot(uri=MONDOLIB.has_definition, name="candidateObsoletion__has_definition", curie=MONDOLIB.curie('has_definition'), - model_uri=MONDOLIB.candidateObsoletion__has_definition, domain=None, range=Optional[Union[bool, Bool]]) -slots.report__candidate_obsoletions = Slot(uri=MONDOLIB.candidate_obsoletions, name="report__candidate_obsoletions", curie=MONDOLIB.curie('candidate_obsoletions'), - model_uri=MONDOLIB.report__candidate_obsoletions, domain=None, range=Optional[Union[Dict[Union[str, CandidateObsoletionTerm], Union[dict, CandidateObsoletion]], List[Union[dict, CandidateObsoletion]]]]) +slots.lexicalPattern__pattern = Slot( + uri=MONDOLIB.pattern, + name="lexicalPattern__pattern", + curie=MONDOLIB.curie("pattern"), + model_uri=MONDOLIB.lexicalPattern__pattern, + domain=None, + range=Optional[str], +) + +slots.lexicalPattern__description = Slot( + uri=MONDOLIB.description, + name="lexicalPattern__description", + curie=MONDOLIB.curie("description"), + model_uri=MONDOLIB.lexicalPattern__description, + domain=None, + range=Optional[str], +) + +slots.lexicalPattern__obsoletion_reason = Slot( + uri=MONDOLIB.obsoletion_reason, + name="lexicalPattern__obsoletion_reason", + curie=MONDOLIB.curie("obsoletion_reason"), + model_uri=MONDOLIB.lexicalPattern__obsoletion_reason, + domain=None, + range=Optional[str], +) + +slots.lexicalPattern__scope = Slot( + uri=MONDOLIB.scope, + name="lexicalPattern__scope", + curie=MONDOLIB.curie("scope"), + model_uri=MONDOLIB.lexicalPattern__scope, + domain=None, + range=Optional[Union[str, "ValidationCheckScope"]], +) + +slots.configuration__lexical_patterns = Slot( + uri=MONDOLIB.lexical_patterns, + name="configuration__lexical_patterns", + curie=MONDOLIB.curie("lexical_patterns"), + model_uri=MONDOLIB.configuration__lexical_patterns, + domain=None, + range=Optional[Union[Union[dict, LexicalPattern], List[Union[dict, LexicalPattern]]]], +) + +slots.configuration__exclude_terms_with_definitions = Slot( + uri=MONDOLIB.exclude_terms_with_definitions, + name="configuration__exclude_terms_with_definitions", + curie=MONDOLIB.curie("exclude_terms_with_definitions"), + model_uri=MONDOLIB.configuration__exclude_terms_with_definitions, + domain=None, + range=Optional[Union[bool, Bool]], +) + +slots.candidateObsoletion__term = Slot( + uri=MONDOLIB.term, + name="candidateObsoletion__term", + curie=MONDOLIB.curie("term"), + model_uri=MONDOLIB.candidateObsoletion__term, + domain=None, + range=URIRef, +) + +slots.candidateObsoletion__label = Slot( + uri=MONDOLIB.label, + name="candidateObsoletion__label", + curie=MONDOLIB.curie("label"), + model_uri=MONDOLIB.candidateObsoletion__label, + domain=None, + range=Optional[str], +) + +slots.candidateObsoletion__confidence = Slot( + uri=MONDOLIB.confidence, + name="candidateObsoletion__confidence", + curie=MONDOLIB.curie("confidence"), + model_uri=MONDOLIB.candidateObsoletion__confidence, + domain=None, + range=Optional[float], +) + +slots.candidateObsoletion__is_ordo_only = Slot( + uri=MONDOLIB.is_ordo_only, + name="candidateObsoletion__is_ordo_only", + curie=MONDOLIB.curie("is_ordo_only"), + model_uri=MONDOLIB.candidateObsoletion__is_ordo_only, + domain=None, + range=Optional[Union[bool, Bool]], +) + +slots.candidateObsoletion__lexical_pattern_matches = Slot( + uri=MONDOLIB.lexical_pattern_matches, + name="candidateObsoletion__lexical_pattern_matches", + curie=MONDOLIB.curie("lexical_pattern_matches"), + model_uri=MONDOLIB.candidateObsoletion__lexical_pattern_matches, + domain=None, + range=Optional[Union[Union[dict, LexicalPattern], List[Union[dict, LexicalPattern]]]], +) + +slots.candidateObsoletion__direct_child_terms = Slot( + uri=MONDOLIB.direct_child_terms, + name="candidateObsoletion__direct_child_terms", + curie=MONDOLIB.curie("direct_child_terms"), + model_uri=MONDOLIB.candidateObsoletion__direct_child_terms, + domain=None, + range=Optional[Union[str, List[str]]], +) + +slots.candidateObsoletion__is_likely_grouping = Slot( + uri=MONDOLIB.is_likely_grouping, + name="candidateObsoletion__is_likely_grouping", + curie=MONDOLIB.curie("is_likely_grouping"), + model_uri=MONDOLIB.candidateObsoletion__is_likely_grouping, + domain=None, + range=Optional[Union[bool, Bool]], +) + +slots.candidateObsoletion__has_definition = Slot( + uri=MONDOLIB.has_definition, + name="candidateObsoletion__has_definition", + curie=MONDOLIB.curie("has_definition"), + model_uri=MONDOLIB.candidateObsoletion__has_definition, + domain=None, + range=Optional[Union[bool, Bool]], +) + +slots.report__candidate_obsoletions = Slot( + uri=MONDOLIB.candidate_obsoletions, + name="report__candidate_obsoletions", + curie=MONDOLIB.curie("candidate_obsoletions"), + model_uri=MONDOLIB.report__candidate_obsoletions, + domain=None, + range=Optional[ + Union[ + Dict[Union[str, CandidateObsoletionTerm], Union[dict, CandidateObsoletion]], + List[Union[dict, CandidateObsoletion]], + ] + ], +) diff --git a/src/mondolib/datamodels/vocabulary.py b/src/mondolib/datamodels/vocabulary.py index 4e24fc3..7abaed6 100644 --- a/src/mondolib/datamodels/vocabulary.py +++ b/src/mondolib/datamodels/vocabulary.py @@ -1 +1,2 @@ -ORPHANET_PREFIX = 'Orphanet' \ No newline at end of file +"""mondolib datamodel vocabularies.""" +ORPHANET_PREFIX = "Orphanet" diff --git a/src/mondolib/qc/__init__.py b/src/mondolib/qc/__init__.py index e69de29..cba5d40 100644 --- a/src/mondolib/qc/__init__.py +++ b/src/mondolib/qc/__init__.py @@ -0,0 +1 @@ +"""mondolib quality check.""" diff --git a/src/mondolib/qc/term_validator.py b/src/mondolib/qc/term_validator.py index 07be21d..eb7a6ed 100644 --- a/src/mondolib/qc/term_validator.py +++ b/src/mondolib/qc/term_validator.py @@ -1,36 +1,35 @@ +"""mondolib term-validator.""" import logging import os +import re +import sys from collections import defaultdict from dataclasses import dataclass from pathlib import Path from typing import Dict, Optional -import re -import sys from linkml_runtime.loaders import yaml_loader -from mondolib.datamodels.vocabulary import ORPHANET_PREFIX -from mondolib.utilities.curie_utilities import get_curie_prefix -from mondolib.datamodels.mondolib_schema import Configuration, ValidationCheckScope, Report, CandidateObsoletion from oaklib import BasicOntologyInterface -from oaklib.datamodels.oxo import ScopeEnum -from oaklib.interfaces.obograph_interface import OboGraphInterface from oaklib.types import CURIE +from mondolib.datamodels.mondolib_schema import CandidateObsoletion, Configuration, Report, ValidationCheckScope +from mondolib.datamodels.vocabulary import ORPHANET_PREFIX +from mondolib.utilities.curie_utilities import get_curie_prefix MAPPING_DICT_BY_SOURCE = Dict[str, str] @dataclass class TermValidator: - """ - A bespoke Mondo validator - """ + + """A bespoke Mondo validator.""" + ontology: BasicOntologyInterface = None configuration: Configuration = None def get_ontology_report(self, threshold: float = 0.0) -> Report: """ - Gets a report from the entire ontology + Get report from the entire ontology. :return: """ @@ -41,7 +40,6 @@ def get_ontology_report(self, threshold: float = 0.0) -> Report: report.candidate_obsoletions[obs.term] = obs return report - def get_candidate_obsoletion(self, term: CURIE, threshold: float = 0.0) -> Optional[CandidateObsoletion]: """ Given a term, yield the reasons why this should be obsoleted. @@ -51,16 +49,16 @@ def get_candidate_obsoletion(self, term: CURIE, threshold: float = 0.0) -> Optio :param term: :return: """ - logging.info(f'Checking {term}') + logging.info(f"Checking {term}") oi = self.ontology conf = self.configuration confidence = 0.0 label = oi.get_label_by_curie(term) if label is None: - logging.info(f'No name - possible dangling? {term}') + logging.info(f"No name - possible dangling? {term}") return None obs = CandidateObsoletion(term=term, label=label) - incoming = oi.get_incoming_relationships_by_curie(term) + incoming = oi.get_incoming_relationship_map_by_curie(term) mdict = self.get_mappings_by_source(term) prefixes = list(mdict.keys()) @@ -89,23 +87,23 @@ def get_candidate_obsoletion(self, term: CURIE, threshold: float = 0.0) -> Optio def load_configuration(self, path: str = None) -> None: """ - Loads the configuration + Load configuration. :param path: uses default if not specified :return: """ if path is None: d = Path(os.path.dirname(sys.modules["mondolib"].__file__)) - path = d / 'config' / 'qc_config.yaml' - logging.info(f'Loading from {path}') + path = d / "config" / "qc_config.yaml" + logging.info(f"Loading from {path}") self.configuration = yaml_loader.loads(str(path), Configuration) def get_mappings_by_source(self, term: CURIE) -> MAPPING_DICT_BY_SOURCE: + """Get CURIE mappings.""" oi = self.ontology mapping_dict = defaultdict(list) - for m in oi.get_simple_mappings_by_curie(term): + for m in oi.simple_mappings_by_curie(term): # ignore mapping predicate for now x = m[1] mapping_dict[get_curie_prefix(x)].append(x) return mapping_dict - diff --git a/src/mondolib/utilities/__init__.py b/src/mondolib/utilities/__init__.py index e69de29..32bd0fc 100644 --- a/src/mondolib/utilities/__init__.py +++ b/src/mondolib/utilities/__init__.py @@ -0,0 +1 @@ +"""mondolib utilities.""" diff --git a/src/mondolib/utilities/curie_utilities.py b/src/mondolib/utilities/curie_utilities.py index cc54539..e095500 100644 --- a/src/mondolib/utilities/curie_utilities.py +++ b/src/mondolib/utilities/curie_utilities.py @@ -1,12 +1,13 @@ +"""mondolib CURIE utilities.""" from oaklib.types import CURIE def get_curie_prefix(curie: CURIE) -> str: """ - Gets the prefix part of a CURIE, e.g. OMIM for OMIM:100000 + Get the prefix part of a CURIE, e.g. OMIM for OMIM:100000. :param curie: :return: """ - prefix, _ = curie.split(':') - return prefix \ No newline at end of file + prefix, _ = curie.split(":") + return prefix diff --git a/tests/__init__.py b/tests/__init__.py index acdb2f8..dad767d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,7 @@ +"""mondolib tests.""" import os from pathlib import Path ROOT = Path(os.path.abspath(os.path.dirname(__file__))) -INPUT_DIR = ROOT / 'input' -OUTPUT_DIR = ROOT / 'output' - - +INPUT_DIR = ROOT / "input" +OUTPUT_DIR = ROOT / "output" diff --git a/tests/input/example-relation-graph.tsv.gz b/tests/input/example-relation-graph.tsv.gz new file mode 100644 index 0000000..b708803 Binary files /dev/null and b/tests/input/example-relation-graph.tsv.gz differ diff --git a/tests/input/example.obo b/tests/input/example.obo index f1a803d..1a10423 100644 --- a/tests/input/example.obo +++ b/tests/input/example.obo @@ -15,12 +15,12 @@ subset: ordo_malformation_syndrome {source="Orphanet:1552"} synonym: "CURRARINO syndrome" RELATED [OMIM:176450] synonym: "Currarino syndrome" EXACT [Orphanet:1552] synonym: "Currarino triad" EXACT CLINGEN_PREFERRED [OMIM:176450] -synonym: "partial sacral agenesis with intact first sacral vertebra, presacral mass and anorectal malformation" RELATED [GARD:0001626] +synonym: "partial sacral agenesis with intact first sacral vertebra, presacral mass and anorectal malformation" RELATED [GUARD:0001626] synonym: "sacral agenesis syndrome" RELATED [OMIM:176450] synonym: "sacral agenesis, hereditary, with presacral Mass, anterior meningocele, and/or teratoma, and anorectal malformation" RELATED [OMIM:176450] synonym: "Scra1" RELATED [OMIM:176450] xref: DOID:0111546 {source="MONDO:equivalentTo"} -xref: GARD:0001626 {source="MONDO:Orphanet-shared", source="MONDO:equivalentTo", source="MONDO:OMIM-shared"} +xref: GUARD:0001626 {source="MONDO:Orphanet-shared", source="MONDO:equivalentTo", source="MONDO:OMIM-shared"} xref: ICD10CM:Q87.8 {source="Orphanet:1552", source="Orphanet:1552/attributed", source="Orphanet:1552/ntbt"} xref: ICD9:759.89 {source="MONDO:relatedTo", source="MONDO:i2s"} xref: MESH:C536221 {source="Orphanet:1552", source="MONDO:equivalentTo", source="Orphanet:1552/e"} diff --git a/tests/input/example.owl b/tests/input/example.owl index 21018de..2e882ac 100644 --- a/tests/input/example.owl +++ b/tests/input/example.owl @@ -158,7 +158,7 @@ Currarino syndrome (CS) is a rare congenital disease characterized by the triad of anorectal malformations (ARMs) (usually anal stenosis), presacral mass (commonly anterior sacral meningocele (ASM) or teratoma) and sacral anomalies (i.e. total or partial agenesis of the sacrum and coccyx or deformity of the sacral vertebrae). 0.12499999999999978 DOID:0111546 - GARD:0001626 + GUARD:0001626 ICD10CM:Q87.8 ICD9:759.89 MESH:C536221 @@ -189,7 +189,7 @@ - GARD:0001626 + GUARD:0001626 MONDO:OMIM-shared MONDO:Orphanet-shared MONDO:equivalentTo @@ -303,7 +303,7 @@ partial sacral agenesis with intact first sacral vertebra, presacral mass and anorectal malformation - GARD:0001626 + GUARD:0001626 diff --git a/tests/test_qc/__init__.py b/tests/test_qc/__init__.py index e69de29..629443e 100644 --- a/tests/test_qc/__init__.py +++ b/tests/test_qc/__init__.py @@ -0,0 +1 @@ +"""mondolib test QC.""" diff --git a/tests/test_qc/test_term_validator.py b/tests/test_qc/test_term_validator.py index 9bfda12..3f30194 100644 --- a/tests/test_qc/test_term_validator.py +++ b/tests/test_qc/test_term_validator.py @@ -1,5 +1,4 @@ -import logging -import os +"""mondolib QC test: test validator.""" import unittest from linkml_runtime.dumpers import yaml_dumper @@ -11,23 +10,27 @@ class TermValidatorTestCase(unittest.TestCase): + """Tests for term validator.""" + def setUp(self) -> None: + """Set up.""" tv = TermValidator() tv.load_configuration() self.term_validator = tv - tv.ontology = get_implementation_from_shorthand(str(INPUT_DIR / 'example.db')) + tv.ontology = get_implementation_from_shorthand(str(INPUT_DIR / "example.db")) def test_qc(self): + """QC test.""" tv = self.term_validator report = tv.get_ontology_report(threshold=-10) print(yaml_dumper.dumps(report)) print(type(report.candidate_obsoletions)) print(list(report.candidate_obsoletions.keys())) - dysostosis = report.candidate_obsoletions['MONDO:0800075'] + dysostosis = report.candidate_obsoletions["MONDO:0800075"] self.assertIsNotNone(dysostosis) self.assertGreater(dysostosis.confidence, 1.0) self.assertGreater(len(dysostosis.lexical_pattern_matches), 0) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c901c90 --- /dev/null +++ b/tox.ini @@ -0,0 +1,86 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +# To use a PEP 517 build-backend you are required to configure tox to use an isolated_build: +# https://tox.readthedocs.io/en/latest/example/package.html +isolated_build = True +skipsdist = True + +envlist = + # always keep coverage-clean first + coverage-clean + lint-fix + lint + codespell-write + docstr-coverage + py + +[testenv] +allowlist_externals = + poetry +commands = + poetry run pytest {posargs} +description = Run unit tests with pytest. This is a special environment that does not get a name, and + can be referenced with "py". + +[testenv:coverage-clean] +deps = coverage +skip_install = true +commands = coverage erase + +# This is used during development +[testenv:lint-fix] +deps = + black + ruff +skip_install = true +commands = + black src/ tests/ + ruff --fix src/ tests/ +description = Run linters. + +# This is used for QC checks. +[testenv:lint] +deps = + black + ruff +skip_install = true +commands = + black --check --diff src/ tests/ + ruff check src/ tests/ +description = Run linters. + +[testenv:doclint] +deps = + rstfmt +skip_install = true +commands = + rstfmt docs/source/ +description = Run documentation linters. + +[testenv:codespell] +description = Run spell checker. +skip_install = true +deps = + codespell + tomli # required for getting config from pyproject.toml +commands = codespell src/ tests/ + +[testenv:codespell-write] +description = Run spell checker and write corrections. +skip_install = true +deps = + codespell + tomli +commands = codespell src/ tests/ --write-changes + +[testenv:docstr-coverage] +skip_install = true +deps = + docstr-coverage +commands = + docstr-coverage src/ tests/ --skip-private --skip-magic +description = Run the docstr-coverage tool to check documentation coverage \ No newline at end of file