From bd865e9611c530b9e4e0b9f943aeda10ced5c4a0 Mon Sep 17 00:00:00 2001 From: Kennedy Kori Date: Mon, 11 Sep 2023 12:43:56 +0300 Subject: [PATCH] chore(build): setup project Set up the following project items: - Linters - Tests - Coverage Reporting - Docs - CI Workflows - Pre-Commit Hooks --- .github/workflows/ci.yml | 35 ++++ .gitignore | 1 + .pre-commit-config.yaml | 30 ++++ README.md | 18 +++ docs/conf.py | 175 ++++++++++++++++++++ docs/images/sghi_globe.png | Bin 0 -> 24057 bytes docs/images/sghi_logo.webp | Bin 0 -> 20264 bytes docs/index.rst | 48 ++++++ docs/static/.gitkeep | 0 docs/templates/class.rst | 45 ++++++ docs/templates/exception.rst | 44 ++++++ docs/templates/module.rst | 67 ++++++++ pyproject.toml | 299 +++++++++++++++++++++++++++++++++++ src/sghi/app.py | 0 src/sghi/exceptions.py | 27 ++++ test/sghi/test_app.py | 0 test/sghi/test_exceptions.py | 25 +++ 17 files changed, 814 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 docs/conf.py create mode 100644 docs/images/sghi_globe.png create mode 100644 docs/images/sghi_logo.webp create mode 100644 docs/index.rst create mode 100644 docs/static/.gitkeep create mode 100644 docs/templates/class.rst create mode 100644 docs/templates/exception.rst create mode 100644 docs/templates/module.rst create mode 100644 pyproject.toml create mode 100644 src/sghi/app.py create mode 100644 src/sghi/exceptions.py create mode 100644 test/sghi/test_app.py create mode 100644 test/sghi/test_exceptions.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f992f3d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: [push] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up project using python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + # Run all pre-commit hooks on all the files. + # Getting only staged files can be tricky in case a new PR is opened + # since the action is run on a branch in detached head state + - name: Install and run pre-commit + uses: pre-commit/action@v3.0.0 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + + - name: Test with tox + run: tox -r diff --git a/.gitignore b/.gitignore index a5458a8..c547bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -303,6 +303,7 @@ tags ### Project template dist/* +docs/api .pytest_cache/ .ipython/ .env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a1f6330 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-toml + - id: check-vcs-permalinks + - id: check-yaml + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.0.287" + hooks: + - id: ruff + args: [ --fix, --exit-non-zero-on-fix ] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.10.1 + hooks: + - id: pyupgrade + args: ["--py310-plus"] + +# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/README.md b/README.md index 733bd70..1166d19 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # SGHI Commons +[![pyversion](https://camo.githubusercontent.com/64bafa7ada773716674e93fd8fbaa3f681e1748865cdcb47cc373579079b767f/68747470733a2f2f696d672e736869656c64732e696f2f707970692f707976657273696f6e732f7365747570746f6f6c732e737667)](https://camo.githubusercontent.com/64bafa7ada773716674e93fd8fbaa3f681e1748865cdcb47cc373579079b767f/68747470733a2f2f696d672e736869656c64732e696f2f707970692f707976657273696f6e732f7365747570746f6f6c732e737667) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) + A collection of utilities and reusable components used throughout our Python projects. They include: @@ -7,6 +12,19 @@ projects. They include: - A registry component for storing key-value pairs. - A signal dispatcher inspired by [PyDispatch](https://grass.osgeo.org/grass83/manuals/libpython/pydispatch.html) and [Django Dispatch](https://docs.djangoproject.com/en/dev/topics/signals/). +## Contribute + +Clone the project and run the following command to install dependencies: + +```bash +pip install -e .[dev,test,docs] +``` + +Set up pre-commit hooks: +```bash +pre-commit install +``` + ## License [MIT License](https://github.com/savannahghi/sghi-commons/blob/main/LICENSE) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..027b27d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,175 @@ +# ruff: noqa + +# 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 sys + +sys.path.insert(0, os.path.abspath("src")) + + +# ----------------------------------------------------------------------------- +# Project information +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +# ----------------------------------------------------------------------------- + +author = "Savannah Global Health Institute" +copyright = "2023, Savannah Global Health Institute" +project = "sghi-commons" + + +# ----------------------------------------------------------------------------- +# General configuration +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration +# ----------------------------------------------------------------------------- + +extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosummary"] + +# Preserve authored syntax for defaults +autodoc_preserve_defaults = True + +autodoc_default_flags = { + "inherited-members": True, + "show-inheritance": True, + "special-members": ( + "__enter__", + "__exit__", + "__call__", + "__getattr__", + "__setattr_", + ), +} + +autodoc_member_order = 'groupwise' + +autoapi_python_use_implicit_namespaces = True + +autosummary_generate = True # Turn on sphinx.ext.autosummary + +exclude_patterns = [] + +# Be strict about any broken references +nitpicky = True + +nitpick_ignore = [ + ("py:class", "concurrent.futures._base.Executor"), # sphinx can't find it + ("py:class", "concurrent.futures._base.Future"), # sphinx can't find it + ("py:class", "sghi.disposable.decorators._D"), # private type annotations + ("py:class", "sghi.disposable.decorators._DE"), # private type annotations + ("py:class", "sghi.disposable.decorators._P"), # private type annotations + ("py:class", "sghi.disposable.decorators._R"), # private type annotations + ("py:class", "sghi.disposable.decorators.not_disposed._D"), # private type annotations + ("py:class", "sghi.disposable.decorators.not_disposed._DE"), # private type annotations + ("py:class", "sghi.disposable.decorators.not_disposed._P"), # private type annotations + ("py:class", "sghi.disposable.decorators.not_disposed._R"), # private type annotations + ("py:class", "sghi.task.task._IT"), # private type annotations + ("py:class", "sghi.task.task._OT"), # private type annotations + ("py:class", "sghi.task.common._IT"), # private type annotations + ("py:class", "sghi.task.common._OT"), # private type annotations + ("py:class", "sghi.task.concurrent._IT"), # private type annotations + ("py:class", "sghi.task.concurrent._OT"), # private type annotations + ("py:class", "sghi.utils.checkers._Comparable"), # private type annotations + ("py:class", "sghi.utils.checkers._S"), # private type annotations + ("py:class", "sghi.utils.checkers._T"), # private type annotations + ("py:obj", "sghi.disposable.decorators.not_disposed._P"), # private type annotations + ("py:obj", "sghi.disposable.decorators.not_disposed._R"), # private type annotations + ("py:obj", "sghi.task.task._IT"), # private type annotations + ("py:obj", "sghi.task.task._OT"), # private type annotations + ("py:obj", "sghi.task.common._IT"), # private type annotations + ("py:obj", "sghi.task.common._OT"), # private type annotations + ("py:obj", "sghi.task.concurrent._IT"), # private type annotations + ("py:obj", "sghi.task.concurrent._OT"), # private type annotations +] + +templates_path = ["templates"] + +root_doc = "index" + + +# ----------------------------------------------------------------------------- +# Options for HTML output +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output +# ----------------------------------------------------------------------------- + +html_logo = "images/sghi_globe.png" + +html_static_path = ["static"] + +html_theme = "furo" + +html_theme_options = { + "sidebar_hide_name": True, + "light_css_variables": { + "color-brand-primary": "#0474AC", # "blue" + "color-brand-content": "#0474AC", + }, + "dark_css_variables": { + "color-brand-primary": "#C1368C", # "purple" + "color-brand-content": "#C1368C", + }, +} + + +# ----------------------------------------------------------------------------- +# Include Python intersphinx mapping to prevent failures +# jaraco/skeleton#51 +# ----------------------------------------------------------------------------- + +extensions += ["sphinx.ext.intersphinx"] +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "pypackage": ("https://packaging.python.org/en/latest/", None), + "importlib-resources": ( + "https://importlib-resources.readthedocs.io/en/latest", + None, + ), + "django": ( + "http://docs.djangoproject.com/en/dev/", + "http://docs.djangoproject.com/en/dev/_objects/" + ), +} + + +# ----------------------------------------------------------------------------- +# Support tooltips on references +# ----------------------------------------------------------------------------- + +extensions += ["hoverxref.extension"] +hoverxref_auto_ref = True +hoverxref_intersphinx = [ + "python", + "pip", + "pypackage", + "importlib-resources", + "django", +] + + +# ----------------------------------------------------------------------------- +# Add support for nice Not Found 404 pages +# ----------------------------------------------------------------------------- + +extensions += ["notfound.extension"] + + +# ----------------------------------------------------------------------------- +# Add icons (aka "favicons") to documentation +# ----------------------------------------------------------------------------- + +extensions += ["sphinx_favicon"] +html_static_path += ["images"] # should contain the folder with icons + +# List of dicts with HTML attributes +# static-file points to files in the html_static_path (href is computed) + +favicons = [ + { + "rel": "icon", + "type": "image/png", + "static-file": "sghi_globe.png", + "sizes": "any", + }, +] diff --git a/docs/images/sghi_globe.png b/docs/images/sghi_globe.png new file mode 100644 index 0000000000000000000000000000000000000000..525a8930e6e4539e98805bc0d3f53ca4d39a8fde GIT binary patch literal 24057 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 IvSwlb2a?v`!~g&Q literal 0 HcmV?d00001 diff --git a/docs/images/sghi_logo.webp b/docs/images/sghi_logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..3b6cedc43a9b947699f147d14e24de81fdff4a41 GIT binary patch literal 20264 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 pf;`pJ|1cGW0YLwrSCE&dLjT8jehvU2^nZ+pl`_. +They include utilities such as: + +- Components for defining and accessing application configurations. +- A registry component for storing key-value pairs. +- A signal dispatcher inspired by `PyDispatch `_ and `Django Dispatch `_. + +Installation +------------ + +We recommend using the latest version of Python. Python 3.10 and newer is +supported. We also recommend using a `virtual environment`_ in order +to isolate your project dependencies from other projects and the system. + +Install the latest sghi-commons version using pip: + +.. code-block:: bash + + pip install sghi-commons + + +API Reference +------------- + +.. autosummary:: + :template: module.rst + :toctree: api + :caption: API + :recursive: + + sghi.app + sghi.exceptions + + +.. _sghi_github_py_projects: https://github.com/savannahghi/?q=&type=all&language=python&sort= +.. _virtual environment: https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments diff --git a/docs/static/.gitkeep b/docs/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/templates/class.rst b/docs/templates/class.rst new file mode 100644 index 0000000..ca179e6 --- /dev/null +++ b/docs/templates/class.rst @@ -0,0 +1,45 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :special-members: __contains__, __delitem__, __enter__, __exit__, __call__, __getattr__, __setattr__, __getitem__, __setitem__ + :show-inheritance: + :inherited-members: + :member-order: groupwise + + {% if methods or attributes %} + ---- + {% endif %} + + {% block methods %} + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% if methods or attributes %} + ---- + {% endif %} + + {% block constructor %} + .. automethod:: __init__ + {% endblock %} diff --git a/docs/templates/exception.rst b/docs/templates/exception.rst new file mode 100644 index 0000000..5bad225 --- /dev/null +++ b/docs/templates/exception.rst @@ -0,0 +1,44 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoexception:: {{ objname }} + :members: + :show-inheritance: + :inherited-members: + :member-order: groupwise + + {% if methods or attributes %} + ---- + {% endif %} + + {% block methods %} + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% if methods or attributes %} + ---- + {% endif %} + + {% block constructor %} + .. automethod:: __init__ + {% endblock %} diff --git a/docs/templates/module.rst b/docs/templates/module.rst new file mode 100644 index 0000000..b5195e9 --- /dev/null +++ b/docs/templates/module.rst @@ -0,0 +1,67 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Module Attributes') }} + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :template: class.rst + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + :template: exception.rst + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :template: module.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..320b3c4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,299 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools~=68.2.0", + "setuptools_scm[toml]~=7.1.0" +] + +[project] +authors = [ + {name = "Savannah Informatics Global Health Institute"} +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: Freeware", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: POSIX", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Utilities", + "Typing :: Typed" +] +dependencies = [] +description = "A collection of utilities used throughout SGHI's Python projects." +dynamic = ["version"] +keywords = ["sghi", "common", "commons", "util", "utils"] +license = {file = "LICENSE"} +maintainers = [ + {name = "Kennedy Kori", email = "kennedykori47@gmail.com"}, + {name = "Salad Guyo", email = "saladguyo60@gmail.com"} +] +name = "sghi-commons" +readme = "README.md" +requires-python = ">=3.10" # Support Python 3.10+. + +[project.optional-dependencies] +dev = [ + "pre-commit~=3.4.0", +] + +docs = [ + "furo==2023.8.19", + "jaraco.packaging~=9.4.0", + "rst.linker~=2.4.0", + "Sphinx~=7.2.5", + "sphinx-favicon~=1.0.1", + "sphinx-hoverxref~=1.3.0", + "sphinx-inline-tabs~=2023.4.21", + "sphinx-lint~=0.6.8", + "sphinx-notfound-page~=1.0.0", +] + +test = [ + "coverage~=6.5.0", + "coveralls~=3.3.1", + "factory-boy~=3.3.0", + "packaging", + "pyright~=1.1.325", + "pytest~=7.4.1", + "pytest-cov~=4.1.0", + "pytest-forked~=1.6.0", + "pytest-sugar~=0.9.7", + "pytest-xdist~=3.3.1", + "ruff~=0.0.287", + "tomli~=2.0.1", + "tox~=4.11.1", + "tox-gh-actions~=3.1.3", +] + +[project.urls] +changelog = "https://github.com/savannahghi/sghi-commons/blob/develop/docs/CHANGELOG.md" +documentation = "https://github.com/savannahghi/sghi-commons/blob/develop/README.md" +homepage = "https://github.com/savannahghi/sghi-commons/" +repository = "https://github.com/savannahghi/sghi-commons.git" + +[tool.black] +line-length = 79 +target-version = ["py310"] + +[tool.coverage.html] +directory = "coverage" + +[tool.coverage.report] +exclude_lines = [ + # Have to re-enable the standard pragma" + "pragma: no branch", + "pragma: nocover", + "pragma: no cover", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + + # Don't complain about abstract methods, they aren't run: + '@(abc\.)?abstractmethod' +] +show_missing = true + +[tool.coverage.run] +branch = true +omit = [".tox/*", "docs/*", "test/*"] +# include = ["src/**/*.py"] + +[tool.isort] +extend_skip = "docs" +extend_skip_glob = ["*/build/*", "*/docs/*"] +known_first_party = ["sghi", "test"] +line_length = 79 +multi_line_output = 3 +profile = "black" + +[tool.pyright] +analyzeUnannotatedFunctions = true +enableTypeIgnoreComments = true +reportConstantRedefinition = "error" +reportDeprecated = "warning" +reportDuplicateImport = "error" +reportGeneralTypeIssues = "error" +reportIncompatibleMethodOverride = "error" +reportIncompatibleVariableOverride = "error" +reportMissingSuperCall = "warning" +reportMissingTypeStubs = "information" +reportOverlappingOverload = "warning" +reportPrivateUsage = "error" +reportPropertyTypeMismatch = "error" +reportUnnecessaryTypeIgnoreComment = "error" +reportUnusedClass = "warning" +reportUnusedFunction = "warning" +reportUnusedImport = "error" +reportUnusedVariable = "warning" +reportWildcardImportFromLibrary = "warning" +strictDictionaryInference = true +strictListInference = true +strictParameterNoneValue = true +strictSetInference = true +typeCheckingMode = "basic" + +[tool.pytest.ini_options] +addopts = "--cov=sghi --cov-fail-under=100 --cov-report=html --cov-report=term-missing -n auto --junitxml='junitxml_report/report.xml' -vv --durations=10 --cache-clear" +console_output_style = "progress" +log_cli = 1 +log_cli_date_format = "%Y-%m-%d %H:%M:%S" +log_cli_format = "[%(levelname)8s]-%(asctime)s (%(filename)s:%(lineno)s) - %(message)s" +log_cli_level = "WARNING" +python_files = ["*test.py", "test_*.py"] +norecursedirs = "node_modules venv build env bin .cache .tox" + +[tool.ruff] +exclude = [ + ".eggs", + ".git", + ".mypy_cache", + ".nox", + ".pytype", + ".ruff_cache", + ".tox", + ".venv", + "__pycache__", + "__pypackages__", + "_build", + "build", + "dist", + "docs", + "node_modules", + "venv", +] +ignore = [ + "ANN002", + "ANN003", + "ANN101", + "ANN102", + "ANN204", + "S101" +] +line-length = 79 +select = [ + "A", # flake8-builtins + "ANN", # flake8-annotations + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + "COM", # flake8-commas + # "D", # pydocstyle + "E", # pycodestyle Error + "EM", # flake8-errmsg + "ERA", # eradicate + "F", # pyflakes + "G", # flake8-logging-format + "I", # isort + "ISC", # flake8-implicit-str-concat + "N", # pep8 Naming + "PD", # pandas-vet + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RET", # flake8-return + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "T10", # flake8-debugger + "T20", # flake8-print + "TCH", # flake8-type-checking + "UP", # pyupgrade + "W", # pycodestyle Warning + "YTT", # flake8-2020 +] +src = ["src", "test"] +target-version = "py310" + +[tool.ruff.flake8-quotes] +inline-quotes = "double" +docstring-quotes = "double" + +[tool.ruff.isort] +known-first-party = ["src", "test"] + +[tool.ruff.mccabe] +max-complexity = 10 + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +namespaces = true +where = ["src"] + +[tool.setuptools_scm] +root = "." + +[tool.tox] +legacy_tox_ini = """ + [tox] + env_list = {py310, py311}, coveralls, docs, package + isolated_build = true + no_package = false + requires = + tox>4 + skip_missing_interpreters = true + + + [gh-actions] + python = + 3.10: py310 + 3.11: py311, coveralls, docs, package + + + [testenv] + commands = + ruff . + pyright . + coverage erase + pytest {posargs:.} + coverage html + deps = + . + description = test and lint the project + download = true + extras = + test + set_env = + PYTHONPATH = {toxinidir}/src + + + ;If running outside Github, ensure that the the `COVERALLS_REPO_TOKEN` + ;environment variable is set. + [testenv:coveralls] + commands = + coveralls --service=github + description = submit coverage results to coverall.io + extras = + test + pass_env = + COVERALLS_REPO_TOKEN + GITHUB_* + + + [testenv:docs] + changedir = docs + commands = + sphinx-build -EW --keep-going -b html . {toxinidir}/docs/build/html + sphinx-lint -i api + description = build sphinx documentation + extras = + docs + + + [testenv:package] + commands = + python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" + python -m build + depends = testenv + deps = + build + description = build the library + skip_install = true +""" diff --git a/src/sghi/app.py b/src/sghi/app.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sghi/exceptions.py b/src/sghi/exceptions.py new file mode 100644 index 0000000..c3a7278 --- /dev/null +++ b/src/sghi/exceptions.py @@ -0,0 +1,27 @@ +"""Base exceptions for SGHI projects.""" + + +class SGHIError(Exception): + """ + Base exception for most non-builtin exceptions raised within SGHI projects. + """ + + def __init__(self, message: str | None = None, *args): + """Initialize an ``SGHIError`` with the given parameters. + + :param message: An optional error message. + :param args: Optional args to pass to forward to the base exception. + """ + self._message: str | None = message + super().__init__(self._message or "", *args) + + @property + def message(self) -> str | None: + """ + Return the error message passed to this exception at initialization + or ``None`` if one was not given. + + :return: The error message passed to this exception at initialization + or ``None`` if one wasn't given. + """ + return self._message diff --git a/test/sghi/test_app.py b/test/sghi/test_app.py new file mode 100644 index 0000000..e69de29 diff --git a/test/sghi/test_exceptions.py b/test/sghi/test_exceptions.py new file mode 100644 index 0000000..0755655 --- /dev/null +++ b/test/sghi/test_exceptions.py @@ -0,0 +1,25 @@ +from unittest import TestCase + +from sghi.exceptions import SGHIError + + +class TestSGHIError(TestCase): + """Tests for the ``SGHIError`` class.""" + + def test_message_prop_return_value(self) -> None: + """Ensure the ``message`` property returns the expected value.""" + + error1 = SGHIError(message="Fatal error.") + error2 = SGHIError() + + assert error1.message == "Fatal error." + assert error2.message is None + + def test_str_representation(self) -> None: + """Ensure the string representation of an ``SGHIError`` is correct.""" + + error1 = SGHIError(message="Fatal error :(") + error2 = SGHIError() + + assert str(error1) == "Fatal error :(" + assert str(error2) == ""