diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..ebe7c8c --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,41 @@ +version = 1 +exclude_patterns = [ + "typing-stubs-for-package-name-to-install-with/**", +] +test_patterns = [ + "**/tests/*.py", +] + +[[analyzers]] +name = "python" +enabled = true +dependency_file_paths = [ + "pyproject.toml", + "requirements/requirements.dev.txt", + "requirements/requirements.doc.txt", + "requirements/requirements.format.txt", + "requirements/requirements.lint.txt", + "requirements/requirements.release.txt", + "requirements/requirements.test.txt", + "requirements/requirements.txt", +] + + [analyzers.meta] + runtime_version = "3.x.x" + max_line_length = 99 + skip_doc_coverage = [ + "init", + ] + type_checker = "mypy" + +[[analyzers]] +name = "secrets" +enabled = true + +[[transformers]] +name = "black" +enabled = true + +[[transformers]] +name = "isort" +enabled = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3ccba44 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# References + +## https://github.com/alexkaratarakis/gitattributes/blob/c9e0391fd0f045478a3d122979eeb23d2311e21a/.gitattributes +## https://github.com/alexkaratarakis/gitattributes/blob/bf082e21993dc589d27ac403b2c189f24c6e57a1/Common.gitattributes +## https://github.com/alexkaratarakis/gitattributes/blob/eeb2ca9a67e5985e1c9e00b3d20ae2842eaa8852/Markdown.gitattributes +## https://github.com/alexkaratarakis/gitattributes/blob/492d0bc3bb09d37cc36e6f1e346b4b61bd620197/Python.gitattributes + +* text=auto + +*.gitattributes text export-ignore linguist-detectable linguist-language=gitattributes +*.gitignore text export-ignore linguist-detectable linguist-language=gitignore +*.gitkeep export-ignore + +*.md text diff=markdown linguist-detectable +*.py text diff=python linguist-detectable +*.toml text diff=toml linguist-detectable +*.yaml text diff=yaml linguist-detectable diff --git a/.github/actions/nox-sessions-action-identifier/action.yml b/.github/actions/nox-sessions-action-identifier/action.yml new file mode 100644 index 0000000..0c6bd72 --- /dev/null +++ b/.github/actions/nox-sessions-action-identifier/action.yml @@ -0,0 +1,33 @@ +name: nox-session-action-name +description: run nox session +inputs: + session-name-input-identifier: + description: name of nox session + required: true + python-version-input-identifier: + description: python version to run session + required: false + default: none +runs: + using: composite + steps: + - id: nox-step-identifier + name: install nox + run: python3 -m pip install nox + shell: bash + - id: session-all-versions-step-identifier + name: run provided nox session for all configured python versions + if: env.PYTHON_VERSION == 'none' + run: nox --sessions ${{env.SESSION_NAME}} --force-venv-backend venv + shell: bash + env: + SESSION_NAME: ${{ inputs.session-name-input-identifier }} + PYTHON_VERSION: ${{ inputs.python-version-input-identifier }} + - id: session-single-version-step-identifier + name: run provided nox session for provided python version + if: env.PYTHON_VERSION != 'none' + run: nox --sessions ${{env.SESSION_NAME}} --pythons ${{env.PYTHON_VERSION}} --force-venv-backend venv + shell: bash + env: + SESSION_NAME: ${{ inputs.session-name-input-identifier }} + PYTHON_VERSION: ${{ inputs.python-version-input-identifier }} diff --git a/.github/actions/nox-tags-action-identifier/action.yml b/.github/actions/nox-tags-action-identifier/action.yml new file mode 100644 index 0000000..5c3b127 --- /dev/null +++ b/.github/actions/nox-tags-action-identifier/action.yml @@ -0,0 +1,33 @@ +name: nox-tag-action-name +description: run nox tag +inputs: + tag-name-input-identifier: + description: name of nox tag + required: true + python-version-input-identifier: + description: python version to run tag + required: false + default: none +runs: + using: composite + steps: + - id: nox-step-identifier + name: install nox + run: python3 -m pip install nox + shell: bash + - id: tag-all-versions-step-identifier + name: run provided nox tag for all configured python versions + if: env.PYTHON_VERSION == 'none' + run: nox --tags ${{env.TAG_NAME}} --force-venv-backend venv + shell: bash + env: + TAG_NAME: ${{ inputs.tag-name-input-identifier }} + PYTHON_VERSION: ${{ inputs.python-version-input-identifier }} + - id: tag-single-version-step-identifier + name: run provided nox tag for provided python version + if: env.PYTHON_VERSION != 'none' + run: nox --pythons ${PYTHON_VERSION} --tags ${{env.TAG_NAME}} --force-venv-backend venv + shell: bash + env: + TAG_NAME: ${{ inputs.tag-name-input-identifier }} + PYTHON_VERSION: ${{ inputs.python-version-input-identifier }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..353022c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,29 @@ +name: docs-workflow-name +run-name: docs workflow run name +on: workflow_dispatch +defaults: + run: + shell: bash +jobs: + docs-job-identifier: + name: docs job name + runs-on: ubuntu-latest + steps: + - name: checkout commit + uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + check-latest: true + architecture: x64 + cache: pip + - name: build documentation + uses: ./.github/actions/nox-sessions-action-identifier + with: + session-name-input-identifier: sphinx + - name: upload documentation + uses: actions/upload-artifact@v3 + with: + name: docs + path: ./docs/build diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..3623929 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,37 @@ +name: format-workflow-name +run-name: format workflow run name +on: + pull_request: + branches: + - main + types: + - opened + - synchronize +defaults: + run: + shell: bash +jobs: + format-job-identifier: + name: format job name + strategy: + fail-fast: false + matrix: + runner-platform: + - ubuntu-latest + python-version: + - "3.10" + runs-on: ${{ matrix.runner-platform }} + steps: + - name: checkout commit + uses: actions/checkout@v3 + - name: set up python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + check-latest: true + architecture: x64 + cache: pip + - name: run formatters on python ${{ matrix.python-version }} + uses: ./.github/actions/nox-tags-action-identifier + with: + tag-name-input-identifier: format diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6fcfda6 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +name: lint-workflow-name +run-name: lint workflow run name +on: + pull_request: + branches: + - main + types: + - opened + - synchronize +defaults: + run: + shell: bash +jobs: + lint-job-identifier: + name: lint job name + strategy: + fail-fast: false + matrix: + runner-platform: + - ubuntu-latest + python-version: + - "3.10" + - "3.11" + runs-on: ${{ matrix.runner-platform }} + steps: + - name: checkout commit + uses: actions/checkout@v3 + - name: set up python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + check-latest: true + architecture: x64 + cache: pip + - name: run linters on python ${{ matrix.python-version }} + uses: ./.github/actions/nox-tags-action-identifier + with: + tag-name-input-identifier: lint + python-version-input-identifier: ${{ matrix.python-version }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..708077a --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,26 @@ +name: pre-commit-workflow-name +run-name: pre-commit workflow run name +on: push +defaults: + run: + shell: bash +jobs: + pre-commit-job-identifier: + name: pre-commit job name + runs-on: ubuntu-latest + steps: + - name: checkout commit + uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + check-latest: true + architecture: x64 + cache: pip + - name: run all pre-commit hooks + env: + SKIP: no-commit-to-branch + uses: ./.github/actions/nox-sessions-action-identifier + with: + session-name-input-identifier: pre_commit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..287d5ec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: release-workflow-name +run-name: release workflow run name +on: workflow_dispatch +defaults: + run: + shell: bash +jobs: + release-job-identifier: + name: release job name + runs-on: ubuntu-latest + steps: + - name: checkout commit + uses: actions/checkout@v3 + - name: set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + check-latest: true + architecture: x64 + cache: pip + - name: build and release wheel + env: + TWINE_REPOSITORY: ${{ secrets.PYPI_REPOSITORY }} + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + uses: ./.github/actions/nox-sessions-action-identifier + with: + session-name-input-identifier: build + - name: upload wheel + uses: actions/upload-artifact@v3 + with: + name: release + path: ./dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..31bef3c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: test-workflow-name +run-name: test workflow run name +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + push: + branches: + - main +defaults: + run: + shell: bash +jobs: + test-job-identifier: + name: test job name + strategy: + fail-fast: false + matrix: + runner-platform: + - macos-latest + - ubuntu-latest + - windows-latest + python-version: + - "3.10" + - "3.11" + max-parallel: 2 + runs-on: ${{ matrix.runner-platform }} + steps: + - name: checkout commit + uses: actions/checkout@v3 + - name: set up python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + check-latest: true + architecture: x64 + cache: pip + - name: run unit tests on python ${{ matrix.python-version }} in runner ${{ matrix.runner-platform }} + uses: ./.github/actions/nox-sessions-action-identifier + with: + session-name-input-identifier: pytest + python-version-input-identifier: ${{ matrix.python-version }} + - name: upload coverage reports + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage_xml_report.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c793fcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,272 @@ +# Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,visualstudiocode,python +# Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,visualstudiocode,python + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Python ### +# 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/ +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/ +cover/ + +# 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 +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .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 + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,visualstudiocode,python + +# Custom +coverage_data +coverage_html_report/ +coverage_xml_report.xml +out/ +pytest_junit_report.xml +typings/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..52fab77 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,152 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-ast + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: mixed-line-ending + - id: name-tests-test + args: + - --pytest-test-first + - id: no-commit-to-branch + - id: pretty-format-json + args: + - --autofix + - --indent + - "4" + - id: requirements-txt-fixer + - id: trailing-whitespace + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: + - --py310-plus + - repo: https://github.com/pycqa/autoflake + rev: v2.0.2 + hooks: + - id: autoflake + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: + - src + pass_filenames: false + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + args: + - src + pass_filenames: false + - repo: https://github.com/pycqa/bandit + rev: 1.7.5 + hooks: + - id: bandit + args: + - --recursive + - --severity-level + - high + - --confidence-level + - high + - src + pass_filenames: false + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + args: + - --extend-ignore + - E203 + - --per-file-ignores + - __init__.py:F401 + - --max-line-length + - "99" + - src + pass_filenames: false + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.2.0 + hooks: + - id: mypy + pass_filenames: false + stages: + - manual + - repo: https://github.com/PyCQA/pylint + rev: v3.0.0a6 + hooks: + - id: pylint + args: + - src + pass_filenames: false + stages: + - manual + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.302 + hooks: + - id: pyright + pass_filenames: false + stages: + - manual + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.261 + hooks: + - id: ruff + - repo: https://github.com/jendrikseipp/vulture + rev: v2.7 + hooks: + - id: vulture + pass_filenames: false + - repo: https://github.com/PyCQA/docformatter + rev: v1.6.0 + hooks: + - id: docformatter + additional_dependencies: + - tomli + args: + - --in-place + - src + pass_filenames: false + - repo: https://github.com/adamchainz/blacken-docs + rev: "1.13.0" + hooks: + - id: blacken-docs + args: + - --line-length + - "87" + - --target-version + - py310 + - repo: https://github.com/econchick/interrogate + rev: 1.5.0 + hooks: + - id: interrogate + args: + - src + pass_filenames: false + - repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + additional_dependencies: + - tomli + args: + - src + pass_filenames: false + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 0.9.2 + hooks: + - id: pyproject-fmt + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.12.2 + hooks: + - id: validate-pyproject + - repo: https://github.com/codespell-project/codespell + rev: v2.2.4 + hooks: + - id: codespell +fail_fast: false diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..f2c365d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 +python: + install: + - path: . + method: pip + extra_requirements: + - doc +build: + os: ubuntu-22.04 + tools: + python: "3.10" +sphinx: + builder: html + configuration: docs/source/conf.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7c12348 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2023 Anirban Ray, First Maintainer, Second Maintainer + +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/Makefile b/Makefile new file mode 100644 index 0000000..9de22b7 --- /dev/null +++ b/Makefile @@ -0,0 +1,418 @@ +MAKEFLAGS += --silent + +SHELL := /usr/bin/env +.SHELLFLAGS := bash -c -e +x + +.DEFAULT_GOAL := help + +PYTHON_DEPENDENCIES_DIRECTORY := ./requirements + +PYTHON_SOURCE_DIRECTORY := ./src +PYTHON_DOCS_DIRECTORY := ./docs +PYTHON_DIST_DIRECTORY := ./dist + +PYTHON_SOURCE_SCRIPTS_PATTERN := "${PYTHON_SOURCE_DIRECTORY}/**/*.py" +PYTHON_TEST_SCRIPTS_PATTERN := "**/tests/*.py" +PYTHON_SKIP_TEST_SCRIPTS_PATTERN := "./src/*tests*" + +PYTHON_SOURCE_SCRIPTS := $(shell find ${PYTHON_SOURCE_DIRECTORY} -type f -name "*.py") + +define check_install_status = + $(shell python3 -m pip show ${1} > /dev/null 2>&1) + if [ ${.SHELLSTATUS} -eq 1 ]; then + python3 -m pip install --upgrade ${1} + fi +endef + +## help +## list all wrapper targets +## show documentaions of wrapper targets +.PHONY: help +help: Makefile + @sed -n 's/^## //p' $< + +.ONESHELL: +venv: + python3 -m venv venv + echo "*" > venv/.gitignore + source venv/bin/activate + python3 -m pip install --upgrade pip setuptools wheel + python3 -m pip install --editable ".[all]" + +.ONESHELL: +.PHONY: pre-commit-install +pre-commit-install: venv + source venv/bin/activate + $(call check_install_status,pre-commit) + pre-commit install + +## setup +## create isolated environment with editable package and dependencies (venv) +## add pre-commit to git hooks (pre-commit-install) +.PHONY: setup +setup: venv pre-commit-install + +.ONESHELL: +venv-upgrade: venv + source venv/bin/activate + python3 -m pip install --upgrade pip setuptools wheel + python3 -m pip install --upgrade \ + --requirement ${PYTHON_DEPENDENCIES_DIRECTORY}/requirements.txt \ + --requirement ${PYTHON_DEPENDENCIES_DIRECTORY}/requirements.dev.txt \ + --requirement ${PYTHON_DEPENDENCIES_DIRECTORY}/requirements.doc.txt \ + --requirement ${PYTHON_DEPENDENCIES_DIRECTORY}/requirements.format.txt \ + --requirement ${PYTHON_DEPENDENCIES_DIRECTORY}/requirements.lint.txt \ + --requirement ${PYTHON_DEPENDENCIES_DIRECTORY}/requirements.release.txt \ + --requirement ${PYTHON_DEPENDENCIES_DIRECTORY}/requirements.test.txt + +.ONESHELL: +.PHONY: pre-commit-autoupdate +pre-commit-autoupdate: venv + source venv/bin/activate + pre-commit autoupdate + +## update +## upgrade isolated environment to latest package and dependencies (venv-upgrade) +## update versions of pre-commit hooks (pre-commit-autoupdate) +.PHONY: update +update: venv-upgrade pre-commit-autoupdate + +.ONESHELL: +.PHONY: autoflake +autoflake: venv + source venv/bin/activate + $(call check_install_status,autoflake) + autoflake $(PYTHON_SOURCE_SCRIPTS) + +.ONESHELL: +.PHONY: black +black: venv + source venv/bin/activate + $(call check_install_status,black) + black ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: blacken-docs +blacken-docs: venv + source venv/bin/activate + $(call check_install_status,blacken-docs) + blacken-docs \ + --line-length 87 \ + --target-version py310 \ + $(PYTHON_SOURCE_SCRIPTS) + +.ONESHELL: +.PHONY: docformatter +docformatter: venv + source venv/bin/activate + $(call check_install_status,docformatter) + $(call check_install_status,tomli) + docformatter ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: isort +isort: venv + source venv/bin/activate + $(call check_install_status,isort) + isort ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: pyupgrade +pyupgrade: venv + source venv/bin/activate + $(call check_install_status,pyupgrade) + pyupgrade --py310-plus $(PYTHON_SOURCE_SCRIPTS) + +## format +## change codes for older versions (pyupgrade) +## remove pyflake detected issues (autoflake) +## sort imports (isort) +## format docstrings (docformatter) +## format code style (black) +.PHONY: format +format: pyupgrade autoflake isort docformatter blacken-docs black + +.ONESHELL: +.PHONY: bandit +bandit: venv + source venv/bin/activate + $(call check_install_status,bandit) + bandit \ + --recursive \ + --severity-level high \ + --confidence-level high \ + ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: flake8 +flake8: venv + source venv/bin/activate + $(call check_install_status,flake8) + flake8 \ + --extend-ignore E203 \ + --per-file-ignores __init__.py:F401 \ + --max-line-length 99 \ + ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: interrogate +interrogate: venv + source venv/bin/activate + $(call check_install_status,interrogate) + interrogate ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: pydocstyle +pydocstyle: venv + source venv/bin/activate + $(call check_install_status,pydocstyle) + $(call check_install_status,tomli) + pydocstyle ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: pylint +pylint: venv + source venv/bin/activate + $(call check_install_status,pylint) + pylint ${PYTHON_SOURCE_DIRECTORY} + +.ONESHELL: +.PHONY: vulture +vulture: venv + source venv/bin/activate + $(call check_install_status,vulture) + vulture + +## lint +## find security issues (bandit) +## lint all python scripts (flake8) +## check docstring coverage (interrogate) +## check docstring presence and formats (pydocstyle) +## lint all python scripts (pylint) +## find dead code (vulture) +.PHONY: lint +lint: bandit flake8 interrogate pydocstyle pylint vulture + +.ONESHELL: +.PHONY: pytest-doctest +pytest-doctest: venv + source venv/bin/activate + $(call check_install_status,pytest) + pytest -k "not test_" + +.ONESHELL: +.PHONY: pytest-failure +pytest-failure: venv + source venv/bin/activate + $(call check_install_status,pytest) + pytest -k "failure" + +.ONESHELL: +.PHONY: pytest-hypotheses +pytest-hypotheses: venv + source venv/bin/activate + $(call check_install_status,hypothesis) + $(call check_install_status,pytest) + pytest -k "hypothesis" + +.ONESHELL: +.PHONY: pytest-others +pytest-others: venv + source venv/bin/activate + $(call check_install_status,pytest) + pytest -k "test and not failure and not hypothesis and not successful" + +.ONESHELL: +.PHONY: pytest-successful +pytest-successful: venv + source venv/bin/activate + $(call check_install_status,pytest) + pytest -k "successful" + +## test +## test doctests (pytest-doctest) +## test successful operations (pytest-successful) +## test failed operations (pytest-failure) +## test hypotheses (pytest-others) +## test other unit tests (pytest-others) +.PHONY: test +test: pytest-doctest pytest-successful pytest-failure pytest-hypotheses pytest-others + +.ONESHELL: +.PHONY: coverage-erase +coverage-erase: venv + source venv/bin/activate + $(call check_install_status,coverage) + $(call check_install_status,tomli) + coverage erase + +.ONESHELL: +.PHONY: coverage-html +coverage-html: venv + source venv/bin/activate + $(call check_install_status,coverage) + $(call check_install_status,tomli) + coverage html + +.ONESHELL: +.PHONY: coverage-report +coverage-report: venv + source venv/bin/activate + $(call check_install_status,coverage) + $(call check_install_status,tomli) + coverage report + +.ONESHELL: +.PHONY: coverage-run +coverage-run: venv + source venv/bin/activate + $(call check_install_status,coverage) + $(call check_install_status,hypothesis) + $(call check_install_status,pytest) + $(call check_install_status,tomli) + coverage run + +.ONESHELL: +.PHONY: coverage-xml +coverage-xml: venv + source venv/bin/activate + $(call check_install_status,coverage) + $(call check_install_status,tomli) + coverage xml + +## coverage +## test all doctests and unit tests (coverage-run) +## create test coverage report (coverage-html) +## delete collected coverage data (coverage-erase) +.PHONY: coverage +coverage: coverage-run coverage-report coverage-html coverage-xml coverage-erase + +.ONESHELL: +.PHONY: sphinx-source +sphinx-source: venv + source venv/bin/activate + $(call check_install_status,Sphinx) + sphinx-apidoc \ + --output-dir ${PYTHON_DOCS_DIRECTORY}/source \ + --maxdepth 3 \ + --force \ + --follow-links \ + --separate \ + ${PYTHON_SOURCE_DIRECTORY} \ + ${PYTHON_SKIP_TEST_SCRIPTS_PATTERN} + +.ONESHELL: +.PHONY: sphinx-build +sphinx-build: venv + source venv/bin/activate + $(call check_install_status,furo) + $(call check_install_status,Sphinx) + $(call check_install_status,sphinx-copybutton) + sphinx-build \ + -b html \ + ${PYTHON_DOCS_DIRECTORY}/source \ + ${PYTHON_DOCS_DIRECTORY}/build + +## docs +## prepare documentation sources with directives (sphinx-source) +## create HTML documentation from source files (sphinx-build) +.PHONY: docs +docs: sphinx-source sphinx-build + +.ONESHELL: +.PHONY: build +build: venv + source venv/bin/activate + $(call check_install_status,build) + python3 -m build --outdir ${PYTHON_DIST_DIRECTORY} + +.ONESHELL: +.PHONY: twine-check +twine-check: venv + source venv/bin/activate + $(call check_install_status,twine) + twine check --strict ${PYTHON_DIST_DIRECTORY}/* + +.ONESHELL: +.PHONY: twine-upload +twine-upload: venv + source venv/bin/activate + $(call check_install_status,twine) + twine upload ${PYTHON_DIST_DIRECTORY}/* + +## release +## create distribution files (build) +## check package description (twine-check) +## upload distribution files (twine-upload) +.PHONY: release +release: build twine-check twine-upload + +.PHONY: clean-coverage +clean-coverage: + find . \ + -type f -name .coverage -delete \ + -o \ + -type d -name htmlcov \ + -exec rm -r "{}" + + +.PHONY: clean-mypy_cache +clean-mypy_cache: + find . \ + -type d -name .mypy_cache \ + -exec rm -r "{}" + + +.PHONY: clean-pycache +clean-pycache: + find . \ + -type f -name '*.py[co]' -delete \ + -o \ + -type d -name __pycache__ -delete + +.PHONY: clean-pytest_cache +clean-pytest_cache: + find . \ + -type d -name .pytest_cache \ + -exec rm -r "{}" + + +## cleanup +## delete all pycache directories and other cache files (clean-pycache) +## delete all mypy cache (clean-mypy_cache) +## delete all unit test cache (clean-pytest_cache) +## delete all coverage results (clean-coverage) +.PHONY: cleanup +cleanup: clean-pycache clean-mypy_cache clean-pytest_cache clean-coverage + +.ONESHELL: +.PHONY: mypy +mypy: venv + source venv/bin/activate + $(call check_install_status,mypy) + mypy + +.ONESHELL: +.PHONY: mypy-stubgen +mypy-stubgen: venv + source venv/bin/activate + $(call check_install_status,mypy) + stubgen \ + --package package_name_to_import_with \ + --module module_that_can_be_imported_directly \ + --module module_that_can_be_invoked_from_cli \ + --module module_that_can_invoke_gui_from_cli + +.ONESHELL: +.PHONY: pyright +pyright: venv + source venv/bin/activate + $(call check_install_status,pyright) + pyright + +.ONESHELL: +.PHONY: pyright-stubs +pyright-stubs: venv + source venv/bin/activate + $(call check_install_status,pyright) + pyright --createstub package_name_to_import_with + pyright --createstub module_that_can_be_imported_directly + pyright --createstub module_that_can_be_invoked_from_cli + pyright --createstub module_that_can_invoke_gui_from_cli diff --git a/README.md b/README.md new file mode 100644 index 0000000..d761bb0 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Learn Python Packaging + +[![codecov][codecov-badge-image]][codecov-badge-url] +[![DeepSource][deepsource-badge-image]][deepsource-badge-url] +[![Documentation Status][read-the-docs-badge-image]][read-the-docs-badge-url] + +[![docs workflow][docs-workflow-badge-image]][docs-workflow-badge-url] +[![format workflow][format-workflow-badge-image]][format-workflow-badge-url] +[![lint workflow][lint-workflow-badge-image]][lint-workflow-badge-url] +[![pre-commit workflow][pre-commit-workflow-badge-image]][pre-commit-workflow-badge-url] +[![release workflow][release-workflow-badge-image]][release-workflow-badge-url] +[![test workflow][test-workflow-badge-image]][test-workflow-badge-url] + +[![pre-commit][pre-commit-badge-image]][pre-commit-badge-url] +[![Ruff][ruff-badge-image]][ruff-badge-url] + +[![Code style: black][black-badge-image]][black-badge-url] +[![Docstring formatter: docformatter][docformatter-badge-image]][docformatter-badge-url] +[![Imports: isort][isort-badge-image]][isort-badge-url] + +[![security: bandit][bandit-badge-image]][bandit-badge-url] +[![linting: pylint][pylint-badge-image]][pylint-badge-url] + +[![Docstring style: numpy][numpydoc-badge-image]][numpydoc-badge-url] + +- [x] ~~Create python package~~ +- [x] ~~Build wheel~~ +- [x] ~~Release wheel~~ +- [x] ~~Setup Github Actions~~ +- [x] ~~Host documentation~~ +- [x] ~~Hypotheses testing~~ + +## Tools Used + +- Development + - [codespell](https://github.com/codespell-project/codespell) + - [Nox](https://github.com/wntrblm/nox) + - [pre-commit](https://github.com/pre-commit/pre-commit) + - [Ruff](https://github.com/charliermarsh/ruff) +- Formatting + - [autoflake](https://www.github.com/PyCQA/autoflake) + - [Black](https://github.com/psf/black) + - [blacken-docs](https://github.com/adamchainz/blacken-docs) + - [docformatter](https://github.com/PyCQA/docformatter) + - [isort](https://pycqa.github.io/isort/) + - [pyproject-fmt](https://github.com/tox-dev/pyproject-fmt) + - [pyupgrade](https://github.com/asottile/pyupgrade) +- Linting + - [Bandit](https://bandit.readthedocs.io/) + - [Flake8](https://github.com/pycqa/flake8) + - [interrogate](https://interrogate.readthedocs.io/) + - [Mypy](http://www.mypy-lang.org/) + - [pydocstyle](https://www.pydocstyle.org/en/stable/) + - [Pylint](https://github.com/PyCQA/pylint) + - [Pyright for Python](https://github.com/RobertCraigie/pyright-python) + - [validate-pyproject](https://github.com/abravalheri/validate-pyproject/) + - [Vulture](https://github.com/jendrikseipp/vulture) +- Testing + - [Coverage.py](https://github.com/nedbat/coveragepy) + - [Hypothesis](https://hypothesis.works/) + - [pytest](https://docs.pytest.org/en/latest/) +- Documentation + - [Furo](https://github.com/pradyunsg/furo) + - [Read the Docs](https://readthedocs.org/) + - [Sphinx](https://www.sphinx-doc.org/) + - [sphinx-copybutton](https://github.com/executablebooks/sphinx-copybutton) + +[bandit-badge-image]: https://img.shields.io/badge/security-bandit-yellow.svg +[bandit-badge-url]: https://github.com/PyCQA/bandit + +[black-badge-image]: https://img.shields.io/badge/code%20style-black-000000.svg +[black-badge-url]: https://github.com/psf/black + +[docformatter-badge-image]: https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg +[docformatter-badge-url]: https://github.com/PyCQA/docformatter + +[codecov-badge-image]: https://codecov.io/gh/yarnabrina/learn-python-packaging/branch/main/graph/badge.svg?token=BG1ECA7E14 +[codecov-badge-url]: https://codecov.io/gh/yarnabrina/learn-python-packaging + +[deepsource-badge-image]: https://deepsource.io/gh/yarnabrina/learn-python-packaging.svg/?label=active+issues&token=tfsfTm2RCqlPTgF3dN31q-0e +[deepsource-badge-url]: https://deepsource.io/gh/yarnabrina/learn-python-packaging/?ref=repository-badge + +[docs-workflow-badge-image]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/docs.yml/badge.svg +[docs-workflow-badge-url]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/docs.yml/ + +[format-workflow-badge-image]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/format.yml/badge.svg +[format-workflow-badge-url]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/format.yml/ + +[isort-badge-image]: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 +[isort-badge-url]: https://pycqa.github.io/isort/ + +[lint-workflow-badge-image]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/lint.yml/badge.svg +[lint-workflow-badge-url]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/lint.yml/ + +[numpydoc-badge-image]: https://img.shields.io/badge/%20style-numpy-459db9.svg +[numpydoc-badge-url]: https://numpydoc.readthedocs.io/en/latest/format.html + +[pre-commit-badge-image]: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit +[pre-commit-badge-url]: https://github.com/pre-commit/pre-commit + +[pre-commit-workflow-badge-image]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/pre-commit.yml/badge.svg +[pre-commit-workflow-badge-url]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/pre-commit.yml/ + +[pylint-badge-image]: https://img.shields.io/badge/linting-pylint-yellowgreen +[pylint-badge-url]: https://github.com/PyCQA/pylint + +[read-the-docs-badge-image]: https://readthedocs.org/projects/learn-python-packaging/badge/?version=latest +[read-the-docs-badge-url]: https://learn-python-packaging.readthedocs.io/en/latest/?badge=latest + +[release-workflow-badge-image]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/release.yml/badge.svg +[release-workflow-badge-url]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/release.yml/ + +[ruff-badge-image]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v0.json +[ruff-badge-url]: https://github.com/charliermarsh/ruff + +[test-workflow-badge-image]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/test.yml/badge.svg +[test-workflow-badge-url]: https://github.com/yarnabrina/learn-python-packaging/actions/workflows/test.yml/ 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/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /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/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..e8c4bc3 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,69 @@ +"""Configure Sphinx documentation.""" +# pylint: disable=invalid-name +import sys + +import package_name_to_import_with + +sys.path.insert(0, "../src") + +project = "package-name-to-install-with" +version = str(package_name_to_import_with.__version__) +project_copyright = "2022-2023, Anirban Ray, First Maintainer, Second Maintainer" +author = "Anirban Ray, First Author, Second Author" +release = f"v{version}" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx_copybutton", +] + +smartquotes = False +today_fmt = "%Y-%m-%d" +highlight_language = "python3" +pygments_style = "friendly" +add_function_parentheses = False +add_module_names = False +trim_doctest_flags = True + +html_theme = "furo" +html_theme_options = { + "top_of_page_button": None, + "announcement": "all page banner", +} +html_title = f"Title of {project}'s Documentation at Release {release}" + +html_last_updated_fmt = "%B %d, %Y" +html_use_index = True +html_split_index = False +html_copy_source = False +html_show_sourcelink = False +html_show_sphinx = False +html_output_encoding = "utf-8" + +autoclass_content = "class" +autodoc_inherit_docstrings = True +autodoc_member_order = "bysource" +autodoc_typehints = "description" +autodoc_typehints_description_target = "documented" +autodoc_typehints_format = "fully-qualified" + +napoleon_google_docstring = False +napoleon_numpy_docstring = True +napoleon_include_init_with_doc = False +napoleon_use_param = True +napoleon_use_keyword = True +napoleon_use_rtype = True +napoleon_preprocess_types = True + +todo_include_todos = True + +viewcode_follow_imported_members = True + +copybutton_prompt_text = r">>> |\.\.\. |\$ " +copybutton_prompt_is_regexp = True +copybutton_line_continuation_character = "\\" diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..ab3d669 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,19 @@ +Welcome to the documentation of package-name-to-install-with! +============================================================= + +**Date**: |today| + +**Version**: |version| + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/module_that_can_be_imported_directly.rst b/docs/source/module_that_can_be_imported_directly.rst new file mode 100644 index 0000000..646b584 --- /dev/null +++ b/docs/source/module_that_can_be_imported_directly.rst @@ -0,0 +1,7 @@ +module\_that\_can\_be\_imported\_directly module +================================================ + +.. automodule:: module_that_can_be_imported_directly + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/module_that_can_be_invoked_from_cli.rst b/docs/source/module_that_can_be_invoked_from_cli.rst new file mode 100644 index 0000000..490f2ab --- /dev/null +++ b/docs/source/module_that_can_be_invoked_from_cli.rst @@ -0,0 +1,7 @@ +module\_that\_can\_be\_invoked\_from\_cli module +================================================ + +.. automodule:: module_that_can_be_invoked_from_cli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/module_that_can_invoke_gui_from_cli.rst b/docs/source/module_that_can_invoke_gui_from_cli.rst new file mode 100644 index 0000000..e7a8206 --- /dev/null +++ b/docs/source/module_that_can_invoke_gui_from_cli.rst @@ -0,0 +1,7 @@ +module\_that\_can\_invoke\_gui\_from\_cli module +================================================ + +.. automodule:: module_that_can_invoke_gui_from_cli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 0000000..a27fc6f --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,10 @@ +src +=== + +.. toctree:: + :maxdepth: 3 + + module_that_can_be_imported_directly + module_that_can_be_invoked_from_cli + module_that_can_invoke_gui_from_cli + package_name_to_import_with diff --git a/docs/source/package_name_to_import_with.calculator_sub_package.inverses_module.rst b/docs/source/package_name_to_import_with.calculator_sub_package.inverses_module.rst new file mode 100644 index 0000000..d2551de --- /dev/null +++ b/docs/source/package_name_to_import_with.calculator_sub_package.inverses_module.rst @@ -0,0 +1,7 @@ +package\_name\_to\_import\_with.calculator\_sub\_package.inverses\_module module +================================================================================ + +.. automodule:: package_name_to_import_with.calculator_sub_package.inverses_module + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/package_name_to_import_with.calculator_sub_package.operations_module.rst b/docs/source/package_name_to_import_with.calculator_sub_package.operations_module.rst new file mode 100644 index 0000000..7542867 --- /dev/null +++ b/docs/source/package_name_to_import_with.calculator_sub_package.operations_module.rst @@ -0,0 +1,7 @@ +package\_name\_to\_import\_with.calculator\_sub\_package.operations\_module module +================================================================================== + +.. automodule:: package_name_to_import_with.calculator_sub_package.operations_module + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/package_name_to_import_with.calculator_sub_package.rst b/docs/source/package_name_to_import_with.calculator_sub_package.rst new file mode 100644 index 0000000..4cdad56 --- /dev/null +++ b/docs/source/package_name_to_import_with.calculator_sub_package.rst @@ -0,0 +1,21 @@ +package\_name\_to\_import\_with.calculator\_sub\_package package +================================================================ + +Submodules +---------- + +.. toctree:: + :maxdepth: 3 + + package_name_to_import_with.calculator_sub_package.inverses_module + package_name_to_import_with.calculator_sub_package.operations_module + package_name_to_import_with.calculator_sub_package.utility_module + package_name_to_import_with.calculator_sub_package.wrapper_module + +Module contents +--------------- + +.. automodule:: package_name_to_import_with.calculator_sub_package + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/package_name_to_import_with.calculator_sub_package.utility_module.rst b/docs/source/package_name_to_import_with.calculator_sub_package.utility_module.rst new file mode 100644 index 0000000..25aaa9c --- /dev/null +++ b/docs/source/package_name_to_import_with.calculator_sub_package.utility_module.rst @@ -0,0 +1,7 @@ +package\_name\_to\_import\_with.calculator\_sub\_package.utility\_module module +=============================================================================== + +.. automodule:: package_name_to_import_with.calculator_sub_package.utility_module + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/package_name_to_import_with.calculator_sub_package.wrapper_module.rst b/docs/source/package_name_to_import_with.calculator_sub_package.wrapper_module.rst new file mode 100644 index 0000000..0cc2b6d --- /dev/null +++ b/docs/source/package_name_to_import_with.calculator_sub_package.wrapper_module.rst @@ -0,0 +1,7 @@ +package\_name\_to\_import\_with.calculator\_sub\_package.wrapper\_module module +=============================================================================== + +.. automodule:: package_name_to_import_with.calculator_sub_package.wrapper_module + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/package_name_to_import_with.data_using_module.rst b/docs/source/package_name_to_import_with.data_using_module.rst new file mode 100644 index 0000000..145d9cc --- /dev/null +++ b/docs/source/package_name_to_import_with.data_using_module.rst @@ -0,0 +1,7 @@ +package\_name\_to\_import\_with.data\_using\_module module +========================================================== + +.. automodule:: package_name_to_import_with.data_using_module + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/package_name_to_import_with.garbage_collection_module.rst b/docs/source/package_name_to_import_with.garbage_collection_module.rst new file mode 100644 index 0000000..df38b23 --- /dev/null +++ b/docs/source/package_name_to_import_with.garbage_collection_module.rst @@ -0,0 +1,7 @@ +package\_name\_to\_import\_with.garbage\_collection\_module module +================================================================== + +.. automodule:: package_name_to_import_with.garbage_collection_module + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/package_name_to_import_with.rst b/docs/source/package_name_to_import_with.rst new file mode 100644 index 0000000..63e2d2d --- /dev/null +++ b/docs/source/package_name_to_import_with.rst @@ -0,0 +1,27 @@ +package\_name\_to\_import\_with package +======================================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 3 + + package_name_to_import_with.calculator_sub_package + +Submodules +---------- + +.. toctree:: + :maxdepth: 3 + + package_name_to_import_with.data_using_module + package_name_to_import_with.garbage_collection_module + +Module contents +--------------- + +.. automodule:: package_name_to_import_with + :members: + :undoc-members: + :show-inheritance: diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..b6c2c56 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,332 @@ +"""Configure nox.""" +import functools +import pathlib + +import nox + +PYTHON_DEFAULT_VERSION = "3.10" +PYTHON_VERSIONS = ["3.10", "3.11"] + +SOURCE_DIRECTORY = pathlib.Path("src") +DIST_DIRECTORY = pathlib.Path("dist") +DOCS_DIRECTORY = pathlib.Path("docs") + +PYTHON_SCRIPT_PATHS = SOURCE_DIRECTORY.glob("**/*.py") +PYTHON_SCRIPTS = [str(SCRIPT_PATH) for SCRIPT_PATH in PYTHON_SCRIPT_PATHS] + +GENERAL_SESSION_DECORATOR = functools.partial(nox.session, venv_backend="conda", reuse_venv=True) + +FORMAT_SESSION_DECORATOR = functools.partial( + GENERAL_SESSION_DECORATOR, python=PYTHON_DEFAULT_VERSION, tags=["format"] +) +LINT_SESSION_DECORATOR = functools.partial( + GENERAL_SESSION_DECORATOR, python=PYTHON_VERSIONS, tags=["lint"] +) +RELEASE_SESSION_DECORATOR = functools.partial( + GENERAL_SESSION_DECORATOR, python=PYTHON_DEFAULT_VERSION, tags=["release"] +) +TEST_SESSION_DECORATOR = functools.partial( + GENERAL_SESSION_DECORATOR, python=PYTHON_VERSIONS, tags=["test"] +) + + +@FORMAT_SESSION_DECORATOR +def autoflake(session: nox.Session) -> None: + """Run autoflake. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("autoflake") + + session.run("autoflake", *PYTHON_SCRIPTS) + + +@LINT_SESSION_DECORATOR +def bandit(session: nox.Session) -> None: + """Run bandit. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("bandit") + + session.run( + "bandit", + "--recursive", + "--severity-level", + "high", + "--confidence-level", + "high", + str(SOURCE_DIRECTORY), + ) + + +@FORMAT_SESSION_DECORATOR +def black(session: nox.Session) -> None: + """Run black. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("black") + + session.run("black", *PYTHON_SCRIPTS) + + +@FORMAT_SESSION_DECORATOR +def blacken_docs(session: nox.Session) -> None: + """Run blacken-docs. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("blacken-docs") + + session.run("blacken-docs", *PYTHON_SCRIPTS) + + +@RELEASE_SESSION_DECORATOR +def build(session: nox.Session) -> None: + """Run build.""" + session.install("build", "wheel") + + session.run("python3", "-m", "build", "--outdir", f"{DIST_DIRECTORY.name}", "--no-isolation") + + session.notify("twine") + + +@TEST_SESSION_DECORATOR +def coverage(session: nox.Session) -> None: + """Run coverage. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("coverage[toml]") + + session.run("coverage", "report") + session.run("coverage", "html") + session.run("coverage", "xml") + + +@FORMAT_SESSION_DECORATOR +def docformatter(session: nox.Session) -> None: + """Run docformatter. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("docformatter[tomli]") + + session.run("docformatter", *PYTHON_SCRIPTS) + + +@LINT_SESSION_DECORATOR +def flake8(session: nox.Session) -> None: + """Run flake8. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("flake8") + + session.run( + "flake8", + "--extend-ignore", + "E203", + "--per-file-ignores", + "__init__.py:F401", + "--max-line-length", + "99", + *PYTHON_SCRIPTS, + ) + + +@LINT_SESSION_DECORATOR +def interrogate(session: nox.Session) -> None: + """Run interrogate. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("interrogate") + + session.run("interrogate", str(SOURCE_DIRECTORY)) + + +@FORMAT_SESSION_DECORATOR +def isort(session: nox.Session) -> None: + """Run isort. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("isort") + + session.run("isort", *PYTHON_SCRIPTS) + + +@LINT_SESSION_DECORATOR +def mypy(session: nox.Session) -> None: + """Run mypy. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("mypy") + + session.run("mypy") + + +@GENERAL_SESSION_DECORATOR(python=PYTHON_DEFAULT_VERSION) +def pre_commit(session: nox.Session) -> None: + """Run pre-commit. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("pre-commit") + + session.run( + "pre-commit", + "run", + "--color", + "always", + "--verbose", + "--all-files", + "--hook-stage", + "manual", + ) + + +@LINT_SESSION_DECORATOR +def pydocstyle(session: nox.Session) -> None: + """Run pydocstyle. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("pydocstyle") + + session.run("pydocstyle", str(SOURCE_DIRECTORY)) + + +@LINT_SESSION_DECORATOR +def pylint(session: nox.Session) -> None: + """Run pylint. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("pylint") + + session.run("pylint", "--disable", "import-error", str(SOURCE_DIRECTORY)) + + +@LINT_SESSION_DECORATOR +def pyright(session: nox.Session) -> None: + """Run pyright. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("pyright") + + session.run("pyright") + + +@TEST_SESSION_DECORATOR +def pytest(session: nox.Session) -> None: + """Run pytest. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("-e", ".[test]") + + session.run("coverage", "run") + + session.notify("coverage") + + +@FORMAT_SESSION_DECORATOR +def pyupgrade(session: nox.Session) -> None: + """Run pyupgrade. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("pyupgrade") + + session.run("pyupgrade", "--py310-plus", *PYTHON_SCRIPTS) + + +@GENERAL_SESSION_DECORATOR(python=PYTHON_DEFAULT_VERSION) +def sphinx(session: nox.Session) -> None: + """Run sphinx. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("-e", ".[doc]") + + with session.chdir("docs"): + session.run("sphinx-build", "-b", "html", "source", "build") + + +@RELEASE_SESSION_DECORATOR +def twine(session: nox.Session) -> None: + """Run twine.""" + session.install("twine") + + session.run("twine", "check", f"{DIST_DIRECTORY.name}/*") + session.run("twine", "upload", f"{DIST_DIRECTORY.name}/*") + + +@LINT_SESSION_DECORATOR +def vulture(session: nox.Session) -> None: + """Run vulture. + + Parameters + ---------- + session : nox.Session + nox Session object + """ + session.install("vulture") + + session.run("vulture") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0539f5a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,345 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61", +] + +[project] +name = "package-name-to-install-with" +description = "A small example package" +keywords = [ + "development", + "packaging", + "sample", + "setuptools", +] +license = { text = "mit" } # license = { file = "LICENSE.txt" } +maintainers = [ + { name = "Anirban Ray" }, + { email = "39331844+yarnabrina@users.noreply.github.com" }, + { name = "First Maintainer", email = "first.maintainer@example.com" }, + { name = "Second Maintainer", email = "second.maintainer@example.com" }, +] +authors = [ + { name = "Anirban Ray" }, + { email = "39331844+yarnabrina@users.noreply.github.com" }, + { name = "First Author", email = "first.author@example.com" }, + { name = "Second Author", email = "second.author@example.com" }, +] +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Flake8", + "Framework :: Pytest", + "Framework :: Sphinx", + "Intended Audience :: Developers", + "License :: OSI Approved", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + "Typing :: Typed", +] +dynamic=[ + "readme", + "version", +] +dependencies = [ + "PySimpleGUI", +] +[project.optional-dependencies] +all = [ + "autoflake", + "bandit", + "black", + "blacken-docs", + "build", + "codespell", + "coverage[toml]", + "docformatter[tomli]", + "flake8", + "furo", + "hypothesis[pytest]", + "interrogate", + "isort", + "mypy", + "nox", + "pre-commit", + "pydocstyle[toml]", + "pylint", + "pyproject-fmt", + "pyright", + "pytest", + "pyupgrade", + "Sphinx", + "sphinx-copybutton", + "twine", + "validate-pyproject", + "vulture", +] +dev = [ + "codespell", + "nox", + "pre-commit", +] +doc = [ + "furo", + "Sphinx", + "sphinx-copybutton", +] +format = [ + "autoflake", + "black", + "blacken-docs", + "docformatter[tomli]", + "isort", + "pyproject-fmt", + "pyupgrade", +] +lint = [ + "bandit", + "flake8", + "interrogate", + "mypy", + "pydocstyle[toml]", + "pylint", + "pyright", + "validate-pyproject", + "vulture", +] +release = [ + "build", + "twine", +] +test = [ + "coverage[toml]", + "hypothesis[pytest]", + "pytest", +] +[project.urls] +"Bug Tracker" = "https://github.com/yarnabrina/learn-python-packaging/issues" +"Documentation" = "https://learn-python-packaging.readthedocs.io" +"Source Code" = "https://github.com/yarnabrina/learn-python-packaging" +[project.scripts] +console-calculator = "module_that_can_be_invoked_from_cli:console_calculator" +[project.gui-scripts] +gui-calculator = "module_that_can_invoke_gui_from_cli:gui_calculator" + +[tool.setuptools] +py-modules = [ + "module_that_can_be_imported_directly", + "module_that_can_be_invoked_from_cli", + "module_that_can_invoke_gui_from_cli", +] + +[tool.setuptools.dynamic] +version = { attr = "package_name_to_import_with.__version__" } +readme = { file = "README.md", content-type = "text/markdown" } + +[tool.setuptools.packages.find] +where = [ + "src", +] +include = [ + "package_name_to_import_with*", +] +exclude = [ + "*tests*", +] +namespaces = false + +[tool.setuptools.package-data] +"package_name_to_import_with" = [ + "metadata.json", + "py.typed", +] + +[tool.setuptools.exclude-package-data] +"*" = [ + ".gitattributes", + ".gitignore", +] + +[tool.black] +line-length = 99 +target-version = [ + "py310", +] +safe = true + +[tool.isort] +overwrite_in_place = true +profile = "black" +atomic = true +float_to_top = true +line_length = 99 +remove_redundant_aliases = true +src_paths = "src" +py_version = 310 + +[tool.pytest.ini_options] +addopts = "--junit-xml=pytest_junit_report.xml --doctest-modules --doctest-ignore-import-errors --doctest-continue-on-failure" +console_output_style = "count" + +[tool.coverage.run] +branch = true +command_line = "--module pytest" +data_file = "coverage_data" +include = [ + "src/**/*.py", +] +omit = [ + "**/tests/*.py", +] + +[tool.coverage.report] +fail_under = 80 +include = [ + "src/**/*.py", +] +omit = [ + "src/module_that_can_invoke_gui_from_cli.py", + "**/tests/*.py", +] +precision = 2 +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:", + "if typing.TYPE_CHECKING:", +] + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.coverage.xml] +output = "coverage_xml_report.xml" + +[tool.mypy] +files = [ + "src", +] +exclude = [ + "conftest", + "test_", +] +ignore_missing_imports = true +strict = true + +[tool.autoflake] +in-place = true +remove-all-unused-imports = true +recursive = true +expand-star-imports = true +ignore-init-module-imports = true +remove-duplicate-keys = true +remove-unused-variables = true + +[tool.docformatter] +in-place = true +recursive = true +wrap-summaries = 99 +wrap-descriptions = 99 + +[tool.interrogate] +fail-under = 80 +ignore-init-method = true + +[tool.pydocstyle] +convention = "numpy" + +[tool.pylint.main] +fail-under = 8.0 +jobs = 0 +recursive = true + +[tool.pylint.basic] +include-naming-hint = true + +[tool.pylint.format] +max-line-length = 99 + +[tool.pylint.logging] +logging-format-style = "new" + +[tool.pylint."messages control"] +enable = [ + "all", +] + +[tool.pylint.reports] +output-format = "colorized" + +[tool.pyright] +include = [ + "src", +] +exclude = [ + "**/tests/*.py", +] +reportMissingImports = false + +[tool.ruff] +extend-exclude = [ + "*.pyi", +] +fix = true +format = "grouped" +ignore = [ + "ANN401", + "D203", + "D213", + "TRY003", +] +ignore-init-module-imports = true +line-length = 99 +select = [ + "F", + "E", + "W", + "C90", + "I", + "N", + "D", + "UP", + "ANN", + "S", + "B", + "A", + "C4", + "T10", + "ISC", + "G", + "T20", + "PT", + "Q", + "SIM", + "TID", + "ERA", + "PD", + "PGH", + "PL", + "TRY", + "RUF", +] +src = [ + "src", +] +target-version = "py310" + +[tool.ruff.per-file-ignores] +"**/test_*.py" = [ + "S101", +] + +[tool.vulture] +min_confidence = 100 +paths = [ + "src", +] diff --git a/requirements/requirements.dev.txt b/requirements/requirements.dev.txt new file mode 100644 index 0000000..73ccce6 --- /dev/null +++ b/requirements/requirements.dev.txt @@ -0,0 +1,3 @@ +codespell +nox +pre-commit diff --git a/requirements/requirements.doc.txt b/requirements/requirements.doc.txt new file mode 100644 index 0000000..bfb8339 --- /dev/null +++ b/requirements/requirements.doc.txt @@ -0,0 +1,3 @@ +furo +Sphinx +sphinx-copybutton diff --git a/requirements/requirements.format.txt b/requirements/requirements.format.txt new file mode 100644 index 0000000..b4a5500 --- /dev/null +++ b/requirements/requirements.format.txt @@ -0,0 +1,7 @@ +autoflake +black +blacken-docs +docformatter[tomli] +isort +pyproject-fmt +pyupgrade diff --git a/requirements/requirements.lint.txt b/requirements/requirements.lint.txt new file mode 100644 index 0000000..9936dd0 --- /dev/null +++ b/requirements/requirements.lint.txt @@ -0,0 +1,9 @@ +bandit +flake8 +interrogate +mypy +pydocstyle +pylint +pyright +validate-pyproject +vulture diff --git a/requirements/requirements.release.txt b/requirements/requirements.release.txt new file mode 100644 index 0000000..e47b6e9 --- /dev/null +++ b/requirements/requirements.release.txt @@ -0,0 +1,2 @@ +build +twine diff --git a/requirements/requirements.test.txt b/requirements/requirements.test.txt new file mode 100644 index 0000000..8b85357 --- /dev/null +++ b/requirements/requirements.test.txt @@ -0,0 +1,3 @@ +coverage[toml] +hypothesis[pytest] +pytest diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 0000000..4671555 --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1 @@ +PySimpleGUI diff --git a/src/module_that_can_be_imported_directly.py b/src/module_that_can_be_imported_directly.py new file mode 100644 index 0000000..b3be013 --- /dev/null +++ b/src/module_that_can_be_imported_directly.py @@ -0,0 +1,4 @@ +"""Store package version.""" +from package_name_to_import_with import __version__ + +VERSION: str = __version__ diff --git a/src/module_that_can_be_invoked_from_cli.py b/src/module_that_can_be_invoked_from_cli.py new file mode 100644 index 0000000..1e9b825 --- /dev/null +++ b/src/module_that_can_be_invoked_from_cli.py @@ -0,0 +1,43 @@ +"""Calculate arithmetic expressions from command line.""" +import argparse +import sys +import typing + +import package_name_to_import_with + + +def capture_user_inputs() -> dict[str, typing.Any]: + """Capture user inputs for arithmetic expression. + + Returns + ------- + dict[str, typing.Any] + captured user inputs + """ + parser = argparse.ArgumentParser(description="calculator for console", add_help=True) + + parser.add_argument("first_number", help="first number") + parser.add_argument("operator", help="arithmetic operator") + parser.add_argument("second_number", help="second number") + + parsed_arguments, _ = parser.parse_known_args() + + return vars(parsed_arguments) + + +def console_calculator() -> None: + """Calculate arithmetic expressions.""" + user_inputs = capture_user_inputs() + + try: + operation_result = package_name_to_import_with.calculate_results( + user_inputs["first_number"], user_inputs["operator"], user_inputs["second_number"] + ) + except Exception as error: # pylint: disable=broad-except + sys.stderr.write(f"Error: {error}") + else: + sys.stdout.write(f"Result = {operation_result}") + + +if __name__ == "__main__": + console_calculator() diff --git a/src/module_that_can_invoke_gui_from_cli.py b/src/module_that_can_invoke_gui_from_cli.py new file mode 100644 index 0000000..00e0594 --- /dev/null +++ b/src/module_that_can_invoke_gui_from_cli.py @@ -0,0 +1,92 @@ +"""Calculate arithmetic expressions from GUI.""" +import PySimpleGUI + +import package_name_to_import_with + +FIRST_NUMBER_INPUT = "first_number" +SECOND_NUMBER_INPUT = "second_number" +OPERATOR_INPUT = "operator" +OPERATION_RESULT = "result" +CLOSE_BUTTON = "Close" + + +def define_gui_layout() -> list[list[PySimpleGUI.Element]]: + """Prepare design of the GUI. + + Returns + ------- + list[list[PySimpleGUI.Element]] + elements of the GUI + """ + layout = [ + [PySimpleGUI.Text("Enter first number"), PySimpleGUI.Input(key=FIRST_NUMBER_INPUT)], + [ + PySimpleGUI.Text("Enter operator"), + PySimpleGUI.OptionMenu(["+", "-", "*", "/"], key=OPERATOR_INPUT), + ], + [PySimpleGUI.Text("Enter second number"), PySimpleGUI.Input(key=SECOND_NUMBER_INPUT)], + [PySimpleGUI.Button(button_text="Submit")], + [PySimpleGUI.Text("Operation Result", key=OPERATION_RESULT)], + [PySimpleGUI.Button(button_text=CLOSE_BUTTON)], + ] + + return layout + + +def define_gui_window(gui_layout: list[list[PySimpleGUI.Element]]) -> PySimpleGUI.Window: + """Create GUI with provided design. + + Parameters + ---------- + gui_layout : list[list[PySimpleGUI.Element]] + design of the GUI + + Returns + ------- + PySimpleGUI.Window + designed GUI + """ + window = PySimpleGUI.Window("GUI Calculator", layout=gui_layout) + + return window + + +def orchestrate_interaction(gui_window: PySimpleGUI.Window) -> None: + """Control flow of the GUI. + + Parameters + ---------- + gui_window : PySimpleGUI.Window + designed GUI + """ + while True: + gui_event, gui_elements = gui_window.read() + + if PySimpleGUI.WINDOW_CLOSED or gui_event == CLOSE_BUTTON: + break + + try: + operation_result = package_name_to_import_with.calculate_results( + gui_elements[FIRST_NUMBER_INPUT], + gui_elements[OPERATOR_INPUT], + gui_elements[SECOND_NUMBER_INPUT], + ) + except Exception as error: # pylint: disable=broad-except + gui_window[OPERATION_RESULT].update(value=str(error)) + else: + gui_window[OPERATION_RESULT].update(value=operation_result) + + +def gui_calculator() -> None: + """Calculate arithmetic expressions.""" + gui_layout = define_gui_layout() + gui_window = define_gui_window(gui_layout) + + try: + orchestrate_interaction(gui_window) + finally: + gui_window.close() + + +if __name__ == "__main__": + gui_calculator() diff --git a/src/package_name_to_import_with/__init__.py b/src/package_name_to_import_with/__init__.py new file mode 100644 index 0000000..e4f2e73 --- /dev/null +++ b/src/package_name_to_import_with/__init__.py @@ -0,0 +1,7 @@ +"""Expose selected package contents.""" +from .calculator_sub_package import calculate_results +from .data_using_module import METADATA +from .garbage_collection_module import define_garbage_collection_decorator + +__all__ = ["calculate_results", "define_garbage_collection_decorator"] +__version__: str = METADATA["Version"] diff --git a/src/package_name_to_import_with/calculator_sub_package/__init__.py b/src/package_name_to_import_with/calculator_sub_package/__init__.py new file mode 100644 index 0000000..8f662ee --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/__init__.py @@ -0,0 +1,15 @@ +"""Expose selected sub-package contents.""" +from .inverses_module import get_negative, get_reciprocal +from .operations_module import add_numbers, multiply_numbers +from .utility_module import divide_numbers, subtract_numbers +from .wrapper_module import calculate_results + +__all__ = [ + "get_negative", + "get_reciprocal", + "add_numbers", + "multiply_numbers", + "divide_numbers", + "subtract_numbers", + "calculate_results", +] diff --git a/src/package_name_to_import_with/calculator_sub_package/inverses_module.py b/src/package_name_to_import_with/calculator_sub_package/inverses_module.py new file mode 100644 index 0000000..f871497 --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/inverses_module.py @@ -0,0 +1,65 @@ +"""Define inverses.""" + + +def get_negative(input_number: float) -> float: + """Get additive inverse of a real number. + + Parameters + ---------- + input_number : float + value of number + + Returns + ------- + float + negative of ``input_number`` + + Examples + -------- + .. code-block:: pycon + + >>> from package_name_to_import_with.calculator_sub_package import get_negative + >>> get_negative(1) + -1 + >>> get_negative(-1) + 1 + """ + additive_inverse = (-1) * input_number + + return additive_inverse + + +def get_reciprocal(input_number: float) -> float: + """Get multiplicative inverse of a real number. + + Parameters + ---------- + input_number : float + value of number + + Returns + ------- + float + reciprocal of ``input_number`` + + Raises + ------ + ValueError + if ``input_number`` is zero + + Examples + -------- + .. code-block:: pycon + + >>> from package_name_to_import_with.calculator_sub_package import get_reciprocal + >>> get_reciprocal(2) + 0.5 + >>> get_reciprocal(0.5) + 2.0 + """ + try: + multiplicative_inverse = 1 / input_number + except ZeroDivisionError as error: + raise ValueError("Multiplicative inverse is not defined for zero") from error + + return multiplicative_inverse diff --git a/src/package_name_to_import_with/calculator_sub_package/operations_module.py b/src/package_name_to_import_with/calculator_sub_package/operations_module.py new file mode 100644 index 0000000..571c91d --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/operations_module.py @@ -0,0 +1,69 @@ +"""Define functions to add and multiply.""" + + +def add_numbers(first_number: float, second_number: float) -> float: + """Perform addition of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + sum of ``first_number`` and ``second_number`` + + Examples + -------- + .. code-block:: pycon + + >>> from package_name_to_import_with.calculator_sub_package import add_numbers + >>> add_numbers(1, 2) + 3 + >>> add_numbers(1, -2) + -1 + >>> add_numbers(-1, 2) + 1 + >>> add_numbers(-1, -2) + -3 + """ + sum_of_two_numbers = first_number + second_number + + return sum_of_two_numbers + + +def multiply_numbers(first_number: float, second_number: float) -> float: + """Perform multiplication of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + product of two ``first_number`` and ``second_number`` + + Examples + -------- + .. code-block:: pycon + + >>> from package_name_to_import_with.calculator_sub_package import multiply_numbers + >>> multiply_numbers(1, 2) + 2 + >>> multiply_numbers(1, -2) + -2 + >>> multiply_numbers(-1, 2) + -2 + >>> multiply_numbers(-1, -2) + 2 + """ + product_of_two_numbers = first_number * second_number + + return product_of_two_numbers diff --git a/src/package_name_to_import_with/calculator_sub_package/tests/__init__.py b/src/package_name_to_import_with/calculator_sub_package/tests/__init__.py new file mode 100644 index 0000000..fa335fd --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/tests/__init__.py @@ -0,0 +1 @@ +"""Define unit tests for package contents.""" diff --git a/src/package_name_to_import_with/calculator_sub_package/tests/conftest.py b/src/package_name_to_import_with/calculator_sub_package/tests/conftest.py new file mode 100644 index 0000000..50952e5 --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/tests/conftest.py @@ -0,0 +1,209 @@ +"""Define common inputs for unit tests.""" +import typing + +import pytest + +from package_name_to_import_with.calculator_sub_package.wrapper_module import ArithmeticOperator + + +@pytest.fixture(params=[4, -9, 1.02, -3.4], name="first_number") +def fixture_first_number(request: pytest.FixtureRequest) -> float: + """Define first input for unit tests. + + Parameters + ---------- + request : pytest.FixtureRequest + request for fixture from test function + + Returns + ------- + float + value of first input + """ + return request.param + + +@pytest.fixture(params=[5, 10, -5.6, 7.89], name="second_number") +def fixture_second_number(request: pytest.FixtureRequest) -> float: + """Define second input for unit tests. + + Parameters + ---------- + request : pytest.FixtureRequest + request for fixture from test function + + Returns + ------- + float + value of second input + """ + return request.param + + +@pytest.fixture(params=["+", "-", "*", "/"], name="operator") +def fixture_operator(request: pytest.FixtureRequest) -> typing.Literal["+", "-", "*", "/"]: + """Define operator for unit tests. + + Parameters + ---------- + request : pytest.FixtureRequest + request for fixture from test function + + Returns + ------- + typing.Literal[ "+", "-", "*", "/" ] + value of operator + """ + return request.param + + +@pytest.fixture() +def expected_sum(first_number: float, second_number: float) -> float: + """Define expected sum for unit tests. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + value of expected sum + """ + return first_number + second_number + + +@pytest.fixture() +def expected_difference(first_number: float, second_number: float) -> float: + """Define expected difference for unit tests. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + value of expected difference + """ + return first_number - second_number + + +@pytest.fixture() +def expected_product(first_number: float, second_number: float) -> float: + """Define expected product for unit tests. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + value of expected product + """ + return first_number * second_number + + +@pytest.fixture() +def expected_quotient(first_number: float, second_number: float) -> float: + """Define expected quotient for unit tests. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + value of expected quotient + """ + return first_number / second_number + + +@pytest.fixture() +def expected_negative(first_number: float) -> float: + """Define expected negative for unit tests. + + Parameters + ---------- + first_number : float + value of input number + + Returns + ------- + float + value of expected negative + """ + return (-1) * first_number + + +@pytest.fixture() +def expected_reciprocal(second_number: float) -> float: + """Define expected reciprocal for unit tests. + + Parameters + ---------- + second_number : float + value of input number + + Returns + ------- + float + value of expected reciprocal + """ + return 1 / second_number + + +@pytest.fixture() +def expected_result( + first_number: float, + second_number: float, + operator: typing.Literal["+", "-", "*", "/"], +) -> float: + """Define expected result for unit tests. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + operator : typing.Literal[ "+", "-", "*", "/" ] + type of arithmetic operation + + Returns + ------- + float + value of expected result + + Raises + ------ + ValueError + if ``operator`` not one of ``+``, ``-``, ``*``, ``/`` + """ + if ArithmeticOperator(operator) == ArithmeticOperator.ADDITION: + return first_number + second_number + + if ArithmeticOperator(operator) == ArithmeticOperator.SUBTRACTION: + return first_number - second_number + + if ArithmeticOperator(operator) == ArithmeticOperator.MULTIPLICATION: + return first_number * second_number + + if ArithmeticOperator(operator) == ArithmeticOperator.DIVISION: + return first_number / second_number + + raise ValueError("Unexpected value of operation") diff --git a/src/package_name_to_import_with/calculator_sub_package/tests/test_hypotheses.py b/src/package_name_to_import_with/calculator_sub_package/tests/test_hypotheses.py new file mode 100644 index 0000000..1e63aba --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/tests/test_hypotheses.py @@ -0,0 +1,193 @@ +"""Define property based unit tests based on random data.""" +import enum +import math +import sys +import typing + +import hypothesis +import hypothesis.strategies + +from package_name_to_import_with.calculator_sub_package import ( + add_numbers, + calculate_results, + divide_numbers, + get_negative, + get_reciprocal, + multiply_numbers, + subtract_numbers, +) +from package_name_to_import_with.calculator_sub_package.wrapper_module import ArithmeticOperator + + +@enum.unique +class InverseElements(enum.Enum): + """Define supported inverse elements.""" + + ADDITIVE_INVERSE = 0 + MULTIPLICATIVE_INVERSE = 1 + + +def generate_finite_numbers() -> hypothesis.strategies.SearchStrategy: + """Generate real numbers which are neither infinity nor NaN. + + Returns + ------- + hypothesis.strategies.SearchStrategy + updated strategy + """ + generate_numbers_strategy = hypothesis.strategies.one_of( + hypothesis.strategies.integers(), + hypothesis.strategies.floats(allow_nan=False, allow_infinity=False, allow_subnormal=False), + hypothesis.strategies.fractions(), + ) + + return generate_numbers_strategy + + +@hypothesis.given(first_number=generate_finite_numbers(), second_number=generate_finite_numbers()) +def test_addition_hypothesis(first_number: float, second_number: float) -> None: + """Check addition of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + """ + calculated_sum = add_numbers(first_number, second_number) + expected_sum = first_number + second_number + + assert math.isclose(calculated_sum, expected_sum) + + +@hypothesis.given(first_number=generate_finite_numbers(), second_number=generate_finite_numbers()) +def test_multiplication_hypothesis(first_number: float, second_number: float) -> None: + """Check multiplication of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + """ + calculated_product = add_numbers(first_number, second_number) + expected_product = first_number + second_number + + assert math.isclose(calculated_product, expected_product) + + +@hypothesis.given(first_number=generate_finite_numbers()) +def test_additive_inverse_hypothesis(first_number: float) -> None: + """Check additive inverse of a real number. + + Parameters + ---------- + first_number : float + value of first number + """ + calculated_negative = get_negative(first_number) + negative_definition = add_numbers(first_number, calculated_negative) + + assert math.isclose(negative_definition, InverseElements.ADDITIVE_INVERSE.value) + + +@hypothesis.given(second_number=generate_finite_numbers()) +def test_multiplicative_inverse_hypothesis(second_number: float) -> None: + """Check multiplicative inverse of a real number. + + Parameters + ---------- + second_number : float + value of first number + """ + hypothesis.assume(abs(second_number) > sys.float_info.epsilon) + + calculated_reciprocal = get_reciprocal(second_number) + reciprocal_definition = multiply_numbers(second_number, calculated_reciprocal) + + assert math.isclose(reciprocal_definition, InverseElements.MULTIPLICATIVE_INVERSE.value) + + +@hypothesis.given(first_number=generate_finite_numbers(), second_number=generate_finite_numbers()) +def test_subtraction_hypothesis(first_number: float, second_number: float) -> None: + """Check subtraction of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + """ + calculated_difference = subtract_numbers(first_number, second_number) + expected_difference = first_number - second_number + + assert math.isclose(calculated_difference, expected_difference) + + +@hypothesis.given(first_number=generate_finite_numbers(), second_number=generate_finite_numbers()) +def test_division_hypothesis(first_number: float, second_number: float) -> None: + """Check division of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + """ + hypothesis.assume(abs(second_number) > sys.float_info.epsilon) + + calculated_quotient = divide_numbers(first_number, second_number) + expected_quotient = first_number / second_number + + assert math.isclose(calculated_quotient, expected_quotient) + + +@hypothesis.given( + first_number=generate_finite_numbers(), + operator=hypothesis.strategies.sampled_from(ArithmeticOperator), + second_number=generate_finite_numbers(), +) +def test_operation_hypothesis( + first_number: float, operator: typing.Literal["+", "-", "*", "/"], second_number: float +) -> None: + """Check operation of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + operator : typing.Literal[ "+", "-", "*", "/" ] + type of arithmetic operation + second_number : float + value of second number + + Raises + ------ + ValueError + if ``operator`` not one of ``+``, ``-``, ``*``, ``/`` + """ + hypothesis.assume( + not ( + operator == ArithmeticOperator.DIVISION + and abs(second_number) <= sys.float_info.epsilon + ) + ) + + calculated_result = calculate_results(first_number, operator, second_number) + + if ArithmeticOperator(operator) == ArithmeticOperator.ADDITION: + expected_result = first_number + second_number + elif ArithmeticOperator(operator) == ArithmeticOperator.SUBTRACTION: + expected_result = first_number - second_number + elif ArithmeticOperator(operator) == ArithmeticOperator.MULTIPLICATION: + expected_result = first_number * second_number + elif ArithmeticOperator(operator) == ArithmeticOperator.DIVISION: + expected_result = first_number / second_number + else: + raise ValueError("Unexpected value of operation") + + assert math.isclose(calculated_result, expected_result) diff --git a/src/package_name_to_import_with/calculator_sub_package/tests/test_inverses.py b/src/package_name_to_import_with/calculator_sub_package/tests/test_inverses.py new file mode 100644 index 0000000..5d1b443 --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/tests/test_inverses.py @@ -0,0 +1,42 @@ +"""Define unit tests for arithmetic operations.""" +import math + +import pytest + +from package_name_to_import_with import calculator_sub_package + + +def test_successful_additive_inverse(first_number: float, expected_negative: float) -> None: + """Check additive inverse of a real number. + + Parameters + ---------- + first_number : float + value of input number + expected_negative : float + value of expected negative + """ + result = calculator_sub_package.get_negative(first_number) + assert math.isclose(result, expected_negative) # nosec B101 + + +def test_successful_multiplicative_inverse( + second_number: float, expected_reciprocal: float +) -> None: + """Check multiplicative inverse of a real number. + + Parameters + ---------- + second_number : float + value of input number + expected_reciprocal : float + value of expected reciprocal + """ + result = calculator_sub_package.get_reciprocal(second_number) + assert math.isclose(result, expected_reciprocal) # nosec B101 + + +def test_multiplicative_inverse_failure() -> None: + """Check failure because of zero-division.""" + with pytest.raises(ValueError, match="Multiplicative inverse is not defined for zero"): + calculator_sub_package.get_reciprocal(0) diff --git a/src/package_name_to_import_with/calculator_sub_package/tests/test_operations.py b/src/package_name_to_import_with/calculator_sub_package/tests/test_operations.py new file mode 100644 index 0000000..1c3f421 --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/tests/test_operations.py @@ -0,0 +1,76 @@ +"""Define unit tests for arithmetic operations.""" +import math + +from package_name_to_import_with import calculator_sub_package + + +def test_successful_addition( + first_number: float, second_number: float, expected_sum: float +) -> None: + """Check addition of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + expected_sum : float + value of expected sum + """ + result = calculator_sub_package.add_numbers(first_number, second_number) + assert math.isclose(result, expected_sum) # nosec B101 + + +def test_successful_subtraction( + first_number: float, second_number: float, expected_difference: float +) -> None: + """Check subtraction of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + expected_difference : float + value of expected sum + """ + result = calculator_sub_package.subtract_numbers(first_number, second_number) + assert math.isclose(result, expected_difference) # nosec B101 + + +def test_successful_multiplication( + first_number: float, second_number: float, expected_product: float +) -> None: + """Check multiplication of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + expected_product : float + value of expected sum + """ + result = calculator_sub_package.multiply_numbers(first_number, second_number) + assert math.isclose(result, expected_product) # nosec B101 + + +def test_successful_division( + first_number: float, second_number: float, expected_quotient: float +) -> None: + """Check division of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + expected_quotient : float + value of expected sum + """ + result = calculator_sub_package.divide_numbers(first_number, second_number) + assert math.isclose(result, expected_quotient) # nosec B101 diff --git a/src/package_name_to_import_with/calculator_sub_package/tests/test_wrappers.py b/src/package_name_to_import_with/calculator_sub_package/tests/test_wrappers.py new file mode 100644 index 0000000..09663d4 --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/tests/test_wrappers.py @@ -0,0 +1,64 @@ +"""Define unit tests for calculators.""" +import math +import typing + +import pytest + +import package_name_to_import_with + + +def test_successful_operation( + first_number: float, + operator: typing.Literal["+", "-", "*", "/"], + second_number: float, + expected_result: float, +) -> None: + """Check operation of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + operator : typing.Literal[ "+", "-", "*", "/" ] + type of arithmetic operation + second_number : float + value of second number + expected_result : float + value of expected sum + """ + result = package_name_to_import_with.calculate_results(first_number, operator, second_number) + assert math.isclose(result, expected_result) # nosec B101 + + +@pytest.mark.parametrize( + ("first_input", "operator", "second_input", "error"), + [ + ("not_number", "+", 3, "Supports only real numbers"), + ("not_number", "-", 6, "Supports only real numbers"), + ("not_number", "*", 7, "Supports only real numbers"), + ("not_number", "/", 8, "Supports only real numbers"), + (3, "+", "not_number", "Supports only real numbers"), + (6, "-", "not_number", "Supports only real numbers"), + (7, "*", "not_number", "Supports only real numbers"), + (8, "/", "not_number", "Supports only real numbers"), + (0, "^", 0, "Supports only basic arithmetic"), + ], +) +def test_operation_failure( + first_input: typing.Any, operator: typing.Any, second_input: typing.Any, error: str +) -> None: + """Check failure during calculations. + + Parameters + ---------- + first_input : typing.Any + input for first number + operator : typing.Any + input for operator + second_input : typing.Any + input for second number + error : str + expected error message + """ + with pytest.raises(ValueError, match=error): + package_name_to_import_with.calculate_results(first_input, operator, second_input) diff --git a/src/package_name_to_import_with/calculator_sub_package/utility_module.py b/src/package_name_to_import_with/calculator_sub_package/utility_module.py new file mode 100644 index 0000000..f28a7bf --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/utility_module.py @@ -0,0 +1,71 @@ +"""Define functions to subtract and divide.""" +from .inverses_module import get_negative, get_reciprocal +from .operations_module import add_numbers, multiply_numbers + + +def subtract_numbers(first_number: float, second_number: float) -> float: + """Perform subtraction of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + difference of ``first_number`` from ``second_number`` + + Examples + -------- + .. code-block:: pycon + + >>> from package_name_to_import_with.calculator_sub_package import subtract_numbers + >>> subtract_numbers(1, 2) + -1 + >>> subtract_numbers(1, -2) + 3 + >>> subtract_numbers(-1, 2) + -3 + >>> subtract_numbers(-1, -2) + 1 + """ + difference_of_two_numbers = add_numbers(first_number, get_negative(second_number)) + + return difference_of_two_numbers + + +def divide_numbers(first_number: float, second_number: float) -> float: + """Perform division of two real numbers. + + Parameters + ---------- + first_number : float + value of first number + second_number : float + value of second number + + Returns + ------- + float + quotient of ``first_number`` by ``second_number`` + + Examples + -------- + .. code-block:: pycon + + >>> from package_name_to_import_with.calculator_sub_package import divide_numbers + >>> divide_numbers(1, 2) + 0.5 + >>> divide_numbers(1, -2) + -0.5 + >>> divide_numbers(-1, 2) + -0.5 + >>> divide_numbers(-1, -2) + 0.5 + """ + quotient_of_two_numbers = multiply_numbers(first_number, get_reciprocal(second_number)) + + return quotient_of_two_numbers diff --git a/src/package_name_to_import_with/calculator_sub_package/wrapper_module.py b/src/package_name_to_import_with/calculator_sub_package/wrapper_module.py new file mode 100644 index 0000000..7d44f0b --- /dev/null +++ b/src/package_name_to_import_with/calculator_sub_package/wrapper_module.py @@ -0,0 +1,183 @@ +"""Define user level functions.""" +import collections.abc +import dataclasses +import enum +import typing + +from .operations_module import add_numbers, multiply_numbers +from .utility_module import divide_numbers, subtract_numbers + + +@enum.unique +class ArithmeticOperator(enum.Enum): + """Define supported arithmetic operators.""" + + ADDITION = "+" + SUBTRACTION = "-" + MULTIPLICATION = "*" + DIVISION = "/" + + +@dataclasses.dataclass +class ArithmeticOperation: + """Define arithmetic operation. + + Parameters + ---------- + first_number : float + value of first number + operation : collections.abc.Callable[[float, float], float] + type of arithmetic operation + second_number : float + value of second number + """ + + first_number: float + operation: collections.abc.Callable[[float, float], float] + second_number: float + + @property + def result(self: "ArithmeticOperation") -> float: + """Store result of arithmetic operation. + + Returns + ------- + float + result of arithmetic operation + """ + return self.operation(self.first_number, self.second_number) + + +def validate_number_input(user_input: typing.Any) -> float: + """Validate input is a number or attempt to convert. + + Parameters + ---------- + user_input : typing.Any + value of input + + Returns + ------- + float + number input + + Raises + ------ + ValueError + if input is not of number type + """ + if isinstance(user_input, float): + return user_input + + try: + converted_user_input = float(user_input) + except ValueError as error: + raise ValueError("Supports only real numbers") from error + + return converted_user_input + + +def validate_operator_input( + user_input: typing.Any, +) -> collections.abc.Callable[[float, float], float]: + """Validate input is in {``+``, ``-``, ``*``, ``/``} and return corresponding operation. + + Parameters + ---------- + user_input : typing.Any + value of input + + Returns + ------- + collections.abc.Callable[[float, float], float] + operation function + + Raises + ------ + ValueError + if input is not a basic arithmetic operator + """ + try: + processed_user_input = ArithmeticOperator(user_input) + except ValueError as error: + raise ValueError("Supports only basic arithmetic") from error + + if processed_user_input == ArithmeticOperator.ADDITION: + return add_numbers + + if processed_user_input == ArithmeticOperator.SUBTRACTION: + return subtract_numbers + + if processed_user_input == ArithmeticOperator.MULTIPLICATION: + return multiply_numbers + + if processed_user_input == ArithmeticOperator.DIVISION: + return divide_numbers + + raise ValueError("Unexpected value of operation") # pragma: no cover + + +def process_inputs( + first_input: typing.Any, operator: typing.Any, second_input: typing.Any +) -> ArithmeticOperation: + """Validate and convert user inputs. + + Parameters + ---------- + first_input : typing.Any + input for first number + operator : typing.Any + input for arithmetic operator + second_input : typing.Any + input for second number + + Returns + ------- + ArithmeticOperation + validated expression + """ + first_number = validate_number_input(first_input) + operator = validate_operator_input(operator) + second_number = validate_number_input(second_input) + + return ArithmeticOperation(first_number, operator, second_number) + + +def calculate_results( + first_input: typing.Any, + operator: typing.Literal["+", "-", "*", "/"], + second_input: typing.Any, +) -> float: + """Perform basic arithmetic operations. + + Parameters + ---------- + first_input : typing.Any + value of first number + operator : typing.Literal[ "+", "-", "*", "/" ] + type of arithmetic operation + second_input : typing.Any + value of second number + + Returns + ------- + float + result of arithmetic operation + + Examples + -------- + .. code-block:: pycon + + >>> from package_name_to_import_with import calculate_results + >>> calculate_results(1, "+", 2) + 3.0 + >>> calculate_results(1, "-", 2) + -1.0 + >>> calculate_results(1, "*", 2) + 2.0 + >>> calculate_results(1, "/", 2) + 0.5 + """ + arithmetic_expression = process_inputs(first_input, operator, second_input) + + return arithmetic_expression.result diff --git a/src/package_name_to_import_with/data_using_module.py b/src/package_name_to_import_with/data_using_module.py new file mode 100644 index 0000000..14a83ed --- /dev/null +++ b/src/package_name_to_import_with/data_using_module.py @@ -0,0 +1,17 @@ +"""Define package contents.""" +import importlib.resources +import json +import typing + + +class PackageMetadata(typing.TypedDict): + """Define keys and types of corresponding values for package metadata.""" + + Name: str + Version: str + + +METADATA_CONTENTS: str = ( + importlib.resources.files("package_name_to_import_with").joinpath("metadata.json").read_text() +) +METADATA: PackageMetadata = json.loads(METADATA_CONTENTS) diff --git a/src/package_name_to_import_with/garbage_collection_module.py b/src/package_name_to_import_with/garbage_collection_module.py new file mode 100644 index 0000000..a5f966a --- /dev/null +++ b/src/package_name_to_import_with/garbage_collection_module.py @@ -0,0 +1,45 @@ +"""Define package contents.""" +import collections.abc +import functools +import gc +import typing + + +def define_garbage_collection_decorator( + function_to_be_decorated: collections.abc.Callable[..., typing.Any] +) -> "collections.abc.Callable[..., typing.Any]": # pragma: no cover + """Perform forcefully garbage collection after execution of provided function. + + Parameters + ---------- + function_to_be_decorated : collections.abc.Callable[..., typing.Any] + function whose execution may require forceful garbage collection + + Returns + ------- + collections.abc.Callable[..., typing.Any] + decorated function + """ + + @functools.wraps(function_to_be_decorated) + def wrapper_function(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: + """Execute provided function with forceful garbage collection afterwards. + + Parameters + ---------- + args : tuple + positional arguments for ``function_to_be_decorated`` + kwargs : dict + keyword arguments for ``function_to_be_decorated`` + + Returns + ------- + typing.Any + output of the provided function with provided arguments + """ + result = function_to_be_decorated(*args, **kwargs) + _ = gc.collect() + + return result + + return wrapper_function diff --git a/src/package_name_to_import_with/metadata.json b/src/package_name_to_import_with/metadata.json new file mode 100644 index 0000000..aff3ce5 --- /dev/null +++ b/src/package_name_to_import_with/metadata.json @@ -0,0 +1,4 @@ +{ + "Name": "package-name-to-install-with", + "Version": "0.0.2" +} diff --git a/src/package_name_to_import_with/py.typed b/src/package_name_to_import_with/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..fa335fd --- /dev/null +++ b/src/tests/__init__.py @@ -0,0 +1 @@ +"""Define unit tests for package contents.""" diff --git a/src/tests/test_cli.py b/src/tests/test_cli.py new file mode 100644 index 0000000..9f6b11b --- /dev/null +++ b/src/tests/test_cli.py @@ -0,0 +1,112 @@ +"""Define unit tests for console calculator.""" +import re +import unittest.mock + +import pytest + +import module_that_can_be_invoked_from_cli + + +def test_sum(capsys: pytest.CaptureFixture) -> None: + """Check addition of two numbers. + + Parameters + ---------- + capsys : pytest.CaptureFixture + fixture capturing ``sys.stdout`` and ``sys.stderr`` + """ + with unittest.mock.patch("sys.argv", ["prog", "4", "+", "5"]): + module_that_can_be_invoked_from_cli.console_calculator() + sum_result, _ = capsys.readouterr() + + assert re.match("Result = 9.0", sum_result) # nosec B101 + + +def test_difference(capsys: pytest.CaptureFixture) -> None: + """Check subtraction of two numbers. + + Parameters + ---------- + capsys : pytest.CaptureFixture + fixture capturing ``sys.stdout`` and ``sys.stderr`` + """ + with unittest.mock.patch("sys.argv", ["prog", "8", "-", "7"]): + module_that_can_be_invoked_from_cli.console_calculator() + difference_result, _ = capsys.readouterr() + + assert re.match("Result = 1.0", difference_result) # nosec B101 + + +def test_product(capsys: pytest.CaptureFixture) -> None: + """Check multiplication of two numbers. + + Parameters + ---------- + capsys : pytest.CaptureFixture + fixture capturing ``sys.stdout`` and ``sys.stderr`` + """ + with unittest.mock.patch("sys.argv", ["prog", "2", "*", "3"]): + module_that_can_be_invoked_from_cli.console_calculator() + product_result, _ = capsys.readouterr() + + assert re.match("Result = 6.0", product_result) # nosec B101 + + +def test_quotient(capsys: pytest.CaptureFixture) -> None: + """Check division of two numbers. + + Parameters + ---------- + capsys : pytest.CaptureFixture + fixture capturing ``sys.stdout`` and ``sys.stderr`` + """ + with unittest.mock.patch("sys.argv", ["prog", "0", "/", "10"]): + module_that_can_be_invoked_from_cli.console_calculator() + quotient_result, _ = capsys.readouterr() + + assert re.match("Result = 0.0", quotient_result) # nosec B101 + + +def test_first_input_failure(capsys: pytest.CaptureFixture) -> None: + """Check failure in first user input. + + Parameters + ---------- + capsys : pytest.CaptureFixture + fixture capturing ``sys.stdout`` and ``sys.stderr`` + """ + with unittest.mock.patch("sys.argv", ["prog", "one", "+", "1"]): + module_that_can_be_invoked_from_cli.console_calculator() + _, result_error = capsys.readouterr() + + assert re.match("Error: Supports only real numbers", result_error) # nosec B101 + + +def test_second_input_failure(capsys: pytest.CaptureFixture) -> None: + """Check failure in second user input. + + Parameters + ---------- + capsys : pytest.CaptureFixture + fixture capturing ``sys.stdout`` and ``sys.stderr`` + """ + with unittest.mock.patch("sys.argv", ["prog", "2", "*", "two"]): + module_that_can_be_invoked_from_cli.console_calculator() + _, result_error = capsys.readouterr() + + assert re.match("Error: Supports only real numbers", result_error) # nosec B101 + + +def test_operator_input_failure(capsys: pytest.CaptureFixture) -> None: + """Check failure in operator input. + + Parameters + ---------- + capsys : pytest.CaptureFixture + fixture capturing ``sys.stdout`` and ``sys.stderr`` + """ + with unittest.mock.patch("sys.argv", ["prog", "0", "x", "0"]): + module_that_can_be_invoked_from_cli.console_calculator() + _, result_error = capsys.readouterr() + + assert re.match("Error: Supports only basic arithmetic", result_error) # nosec B101 diff --git a/src/tests/test_version.py b/src/tests/test_version.py new file mode 100644 index 0000000..3f8088e --- /dev/null +++ b/src/tests/test_version.py @@ -0,0 +1,11 @@ +"""Define unit tests for version.""" +import module_that_can_be_imported_directly +import package_name_to_import_with + + +def test_package_version() -> None: + """Check version of package and exposed module.""" + package_version = package_name_to_import_with.__version__ + module_version = module_that_can_be_imported_directly.VERSION + + assert package_version == module_version # nosec B101 diff --git a/typing-stubs-for-package-name-to-install-with/module_that_can_be_imported_directly.pyi b/typing-stubs-for-package-name-to-install-with/module_that_can_be_imported_directly.pyi new file mode 100644 index 0000000..3acee93 --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/module_that_can_be_imported_directly.pyi @@ -0,0 +1 @@ +VERSION: str diff --git a/typing-stubs-for-package-name-to-install-with/module_that_can_be_invoked_from_cli.pyi b/typing-stubs-for-package-name-to-install-with/module_that_can_be_invoked_from_cli.pyi new file mode 100644 index 0000000..0da9dcc --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/module_that_can_be_invoked_from_cli.pyi @@ -0,0 +1,4 @@ +import typing + +def capture_user_inputs() -> dict[str, typing.Any]: ... +def console_calculator() -> None: ... diff --git a/typing-stubs-for-package-name-to-install-with/module_that_can_invoke_gui_from_cli.pyi b/typing-stubs-for-package-name-to-install-with/module_that_can_invoke_gui_from_cli.pyi new file mode 100644 index 0000000..cf09f1c --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/module_that_can_invoke_gui_from_cli.pyi @@ -0,0 +1,12 @@ +import PySimpleGUI + +FIRST_NUMBER_INPUT: str +SECOND_NUMBER_INPUT: str +OPERATOR_INPUT: str +OPERATION_RESULT: str +CLOSE_BUTTON: str + +def define_gui_layout() -> list[list[PySimpleGUI.Element]]: ... +def define_gui_window(gui_layout: list[list[PySimpleGUI.Element]]) -> PySimpleGUI.Window: ... +def orchestrate_interaction(gui_window: PySimpleGUI.Window) -> None: ... +def gui_calculator() -> None: ... diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/__init__.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/__init__.pyi new file mode 100644 index 0000000..2ef15c1 --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/__init__.pyi @@ -0,0 +1,2 @@ +from .calculator_sub_package import calculate_results +from .garbage_collection_module import define_garbage_collection_decorator diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/__init__.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/__init__.pyi new file mode 100644 index 0000000..4ab95a8 --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/__init__.pyi @@ -0,0 +1,4 @@ +from .inverses_module import get_negative, get_reciprocal +from .operations_module import add_numbers, multiply_numbers +from .utility_module import divide_numbers, subtract_numbers +from .wrapper_module import calculate_results diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/inverses_module.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/inverses_module.pyi new file mode 100644 index 0000000..22f1548 --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/inverses_module.pyi @@ -0,0 +1,2 @@ +def get_negative(input_number: float) -> float: ... +def get_reciprocal(input_number: float) -> float: ... diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/operations_module.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/operations_module.pyi new file mode 100644 index 0000000..701a4d0 --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/operations_module.pyi @@ -0,0 +1,2 @@ +def add_numbers(first_number: float, second_number: float) -> float: ... +def multiply_numbers(first_number: float, second_number: float) -> float: ... diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/utility_module.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/utility_module.pyi new file mode 100644 index 0000000..c057e29 --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/utility_module.pyi @@ -0,0 +1,5 @@ +from .inverses_module import get_negative, get_reciprocal +from .operations_module import add_numbers, multiply_numbers + +def subtract_numbers(first_number: float, second_number: float) -> float: ... +def divide_numbers(first_number: float, second_number: float) -> float: ... diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/wrapper_module.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/wrapper_module.pyi new file mode 100644 index 0000000..0c641cd --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/calculator_sub_package/wrapper_module.pyi @@ -0,0 +1,31 @@ +import collections.abc +import enum +import typing + +from .operations_module import add_numbers, multiply_numbers +from .utility_module import divide_numbers, subtract_numbers + +class ArithmeticOperator(enum.Enum): + ADDITION: str + SUBTRACTION: str + MULTIPLICATION: str + DIVISION: str + +class ArithmeticOperation: + first_number: float + operation: collections.abc.Callable[[float, float], float] + second_number: float + @property + def result(self) -> float: ... + def __init__(self, first_number, operation, second_number) -> None: ... + +def validate_number_input(user_input: typing.Any) -> float: ... +def validate_operator_input( + user_input: typing.Any, +) -> collections.abc.Callable[[float, float], float]: ... +def process_inputs( + first_input: typing.Any, operator: typing.Any, second_input: typing.Any +) -> ArithmeticOperation: ... +def calculate_results( + first_input: typing.Any, operator: typing.Literal["+", "-", "*", "/"], second_input: typing.Any +) -> float: ... diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/data_using_module.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/data_using_module.pyi new file mode 100644 index 0000000..e8ebb8e --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/data_using_module.pyi @@ -0,0 +1,8 @@ +import typing + +class PackageMetadata(typing.TypedDict): + Name: str + Version: str + +METADATA_CONTENTS: str +METADATA: PackageMetadata diff --git a/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/garbage_collection_module.pyi b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/garbage_collection_module.pyi new file mode 100644 index 0000000..9b3ff06 --- /dev/null +++ b/typing-stubs-for-package-name-to-install-with/package_name_to_import_with/garbage_collection_module.pyi @@ -0,0 +1,6 @@ +import collections.abc +import typing + +def define_garbage_collection_decorator( + function_to_be_decorated: collections.abc.Callable[..., typing.Any] +) -> collections.abc.Callable[..., typing.Any]: ...