Compare commits
7 Commits
d8a950948c
...
master
Author | SHA1 | Date | |
---|---|---|---|
c50cfe88d6
|
|||
1a293563d1
|
|||
acf4c06404
|
|||
1c49c4923f
|
|||
9f89f8cc9d
|
|||
21eb0c065d
|
|||
f5b31b7580
|
34
.github/workflows/ci.yaml
vendored
Normal file
34
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: ['v*.*.*']
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
uses: daniil-berg/reusable-workflows/.github/workflows/python-test.yaml@v0.2.2
|
||||
with:
|
||||
versions: '["3.9", "3.10", "3.11", "3.12"]'
|
||||
unittest-command: 'scripts/test.sh'
|
||||
coverage-command: 'scripts/cov.sh'
|
||||
unittest-requirements: "-e '.[dev]'"
|
||||
typecheck-command: 'scripts/typecheck.sh'
|
||||
typecheck-requirements: '-Ur requirements/dev.txt'
|
||||
typecheck-all-versions: true
|
||||
lint-command: 'scripts/lint.sh'
|
||||
lint-requirements: '-Ur requirements/dev.txt'
|
||||
|
||||
release:
|
||||
name: Release
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
needs: test
|
||||
uses: daniil-berg/reusable-workflows/.github/workflows/python-release.yaml@v0.2.2
|
||||
with:
|
||||
git-ref: ${{ github.ref_name }}
|
||||
secrets:
|
||||
release-token: ${{ secrets.TOKEN_GITHUB_CREATE_RELEASE }}
|
||||
publish-token: ${{ secrets.TOKEN_PYPI_PROJECT }}
|
||||
permissions:
|
||||
contents: write
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -18,8 +18,5 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Testing:
|
||||
/.coverage
|
||||
|
||||
# mypy:
|
||||
.mypy_cache/
|
||||
# Miscellaneous cache:
|
||||
.cache/
|
||||
|
@ -0,0 +1,3 @@
|
||||
.md-typeset code {
|
||||
word-break: keep-all; /* Prevent inline-code from being broken up. */
|
||||
}
|
||||
|
@ -1,15 +1,29 @@
|
||||
# ${REPO_NAME}
|
||||
|
||||
${REPO_DESCRIPTION}
|
||||
**${REPO_DESCRIPTION}**
|
||||
|
||||
## Usage
|
||||
---
|
||||
|
||||
...
|
||||
[📑 Documentation][1] | [🧑💻 Source Code][2] | [🐛 Bug Tracker][3]
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
`pip install ${REPO_NAME}`
|
||||
|
||||
## Usage
|
||||
|
||||
...
|
||||
|
||||
## Dependencies
|
||||
|
||||
Python Version ..., OS ...
|
||||
Python `>=3.8`, OS agnostic
|
||||
|
||||
---
|
||||
|
||||
© 2023 ...
|
||||
|
||||
[1]: https://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}
|
||||
[2]: https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}
|
||||
[3]: https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}/issues
|
||||
|
14
mkdocs.yaml
14
mkdocs.yaml
@ -1,4 +1,4 @@
|
||||
copyright: "© 2023 ${REPO_OWNER_TITLE}"
|
||||
... copyright: "© 2023 ..."
|
||||
site_name: "${REPO_NAME}"
|
||||
site_description: "${REPO_DESCRIPTION}"
|
||||
site_url: "http://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}"
|
||||
@ -26,13 +26,23 @@ extra_css:
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- mkdocstrings
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
options:
|
||||
line_length: 80
|
||||
show_source: false
|
||||
show_root_toc_entry: false
|
||||
separate_signature: true
|
||||
show_signature_annotations: true
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- codehilite
|
||||
- extra
|
||||
- pymdownx.superfences
|
||||
- toc:
|
||||
permalink: true
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
|
141
pyproject.toml
141
pyproject.toml
@ -2,21 +2,24 @@
|
||||
# Python packaging: #
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "setuptools-scm"]
|
||||
requires = [
|
||||
"setuptools",
|
||||
"setuptools-scm",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "${REPO_NAME}"
|
||||
description = "${REPO_DESCRIPTION}"
|
||||
authors = [
|
||||
{ name = "${REPO_OWNER_TITLE}", email = "mail@PLACEHOLDER.to" },
|
||||
{ name = ..., email = ... },
|
||||
]
|
||||
maintainers = [
|
||||
{ name = "${REPO_OWNER_TITLE}", email = "mail@PLACEHOLDER.to" },
|
||||
{ name = ..., email = ... },
|
||||
]
|
||||
requires-python = ">=3.9, <4.0"
|
||||
requires-python = ">=3.8, <4.0"
|
||||
keywords = [
|
||||
|
||||
...
|
||||
]
|
||||
license = { text = "Apache Software License Version 2.0" }
|
||||
classifiers = [
|
||||
@ -30,6 +33,7 @@ classifiers = [
|
||||
# "Intended Audience :: Developers",
|
||||
# "Framework :: AsyncIO",
|
||||
# "Topic :: Internet",
|
||||
...
|
||||
]
|
||||
dynamic = [
|
||||
"dependencies",
|
||||
@ -38,33 +42,34 @@ dynamic = [
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
full = [
|
||||
|
||||
]
|
||||
full = []
|
||||
dev = [
|
||||
"black",
|
||||
"build",
|
||||
"coverage[toml]",
|
||||
"flake8",
|
||||
"mkdocs-material",
|
||||
"mkdocstrings[python]",
|
||||
"mypy",
|
||||
"black==23.10.1",
|
||||
"build==1.0.3",
|
||||
"coverage[toml]==7.3.2",
|
||||
"isort==5.12.0",
|
||||
"mkdocs-material==9.4.6",
|
||||
"mkdocstrings[python]==0.23.0",
|
||||
"mypy==1.5.1",
|
||||
"pytest==7.4.3",
|
||||
"ruff==0.1.3",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
repository = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}"
|
||||
bug_tracker = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}/issues"
|
||||
documentation = "http://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}"
|
||||
"Repository" = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}"
|
||||
"Issue Tracker" = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}/issues"
|
||||
"Documentation" = "http://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
dependencies = { file = "requirements/common.txt" }
|
||||
readme = { file = ["README.md"] }
|
||||
version = {attr = "PACKAGE_NAME_PLACEHOLDER.__version__"}
|
||||
dependencies = { file = "requirements/base.txt" }
|
||||
readme = { file = ["README.md"], content-type = "text/markdown" }
|
||||
version = { attr = ..."${REPO_NAME}.__version__" }
|
||||
|
||||
#########
|
||||
# Mypy: #
|
||||
#########################
|
||||
# Static type checking: #
|
||||
|
||||
[tool.mypy]
|
||||
cache_dir = ".cache/mypy"
|
||||
files = [
|
||||
"src/",
|
||||
"tests/",
|
||||
@ -72,19 +77,28 @@ files = [
|
||||
warn_unused_configs = true
|
||||
strict = true
|
||||
show_error_codes = true
|
||||
plugins = [
|
||||
plugins = []
|
||||
|
||||
############
|
||||
# Testing: #
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
cache_dir = ".cache/pytest"
|
||||
addopts = "-ra -v"
|
||||
testpaths = [
|
||||
"tests",
|
||||
]
|
||||
|
||||
#############
|
||||
# Coverage: #
|
||||
#######################
|
||||
# Unit test coverage: #
|
||||
|
||||
[tool.coverage.run]
|
||||
data_file = ".cache/coverage"
|
||||
source = [
|
||||
"src/",
|
||||
]
|
||||
branch = true
|
||||
command_line = "-m tests"
|
||||
command_line = "-m pytest -qq"
|
||||
omit = [
|
||||
".venv*/*",
|
||||
]
|
||||
@ -100,3 +114,76 @@ exclude_lines = [
|
||||
omit = [
|
||||
"tests/*",
|
||||
]
|
||||
|
||||
###############################
|
||||
# Linting and style checking: #
|
||||
|
||||
[tool.ruff]
|
||||
cache-dir = ".cache/ruff"
|
||||
select = [
|
||||
"F", # pyflakes
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"N", # pep8-naming
|
||||
"D", # pydocstyle
|
||||
"ANN", # flake8-annotations
|
||||
"S", # flake8-bandit
|
||||
"FBT", # flake8-boolean-trap
|
||||
"B", # flake8-bugbear
|
||||
"A", # flake8-builtins
|
||||
"C", # flake8-comprehensions
|
||||
"PIE", # flake8-pie
|
||||
"T20", # flake8-print
|
||||
"RET", # flake8-return
|
||||
"SIM", # flake8-simplify
|
||||
"TD", # flake8-todos
|
||||
"TCH", # flake8-type-checking
|
||||
"ARG", # flake8-unused-arguments
|
||||
"PTH", # flake8-use-pathlib
|
||||
"ERA", # eradicate
|
||||
"PL", # pylint
|
||||
"TRY", # tryceratops
|
||||
"RUF", # ruff-specific
|
||||
]
|
||||
ignore = [
|
||||
"E501", # Line too long -> handled by black
|
||||
"D203", # 1 blank line required before class docstring -> D211 is better
|
||||
"D212", # Multi-line docstring summary should start at the first line -> ugly, D213 is better
|
||||
"D401", # First line of docstring should be in imperative mood -> no, it shouldn't
|
||||
"D407", # Missing dashed underline after section -> different docstring style
|
||||
"ANN101", # Missing type annotation for self in method -> unnecessary
|
||||
"ANN102", # Missing type annotation for cls in classmethod -> unnecessary
|
||||
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed -> we'll use it sparingly
|
||||
"N818", # Exception name should be named with an Error suffix -> absolutely not
|
||||
]
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"src/**/__init__.py" = [
|
||||
"A001", # Variable {name} is shadowing a Python builtin
|
||||
"D104", # Missing docstring in public package
|
||||
"F401", # {...} imported but unused
|
||||
]
|
||||
"tests/*.py" = [
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D102", # Missing docstring in public method
|
||||
"D103", # Missing docstring in public function
|
||||
"D104", # Missing docstring in public package
|
||||
"S101", # Use of `assert` detected
|
||||
"FBT", # flake8-boolean-trap
|
||||
"TRY", # tryceratops
|
||||
]
|
||||
|
||||
####################
|
||||
# Code formatting: #
|
||||
|
||||
[tool.black]
|
||||
line_length = 80
|
||||
|
||||
###################
|
||||
# Import sorting: #
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
extra_standard_library = ["typing_extensions"]
|
||||
line_length = 80
|
||||
|
@ -1,8 +1,10 @@
|
||||
-r common.txt
|
||||
black
|
||||
build
|
||||
coverage[toml]
|
||||
flake8
|
||||
mkdocs-material
|
||||
mkdocstrings[python]
|
||||
mypy
|
||||
-r full.txt
|
||||
black==23.10.1
|
||||
build==1.0.3
|
||||
coverage[toml]==7.3.2
|
||||
isort==5.12.0
|
||||
mkdocs-material==9.4.6
|
||||
mkdocstrings[python]==0.23.0
|
||||
mypy==1.5.1
|
||||
pytest==7.4.3
|
||||
ruff==0.1.3
|
||||
|
1
requirements/full.txt
Normal file
1
requirements/full.txt
Normal file
@ -0,0 +1 @@
|
||||
-r base.txt
|
12
scripts/ci.sh
Executable file
12
scripts/ci.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs full CI pipeline (test, typecheck, lint).
|
||||
|
||||
typeset scripts_dir="$(dirname $(realpath $0))"
|
||||
|
||||
source "${scripts_dir}/util.sh"
|
||||
|
||||
"${scripts_dir}/test.sh"
|
||||
"${scripts_dir}/typecheck.sh"
|
||||
"${scripts_dir}/lint.sh"
|
||||
|
||||
echo -e "${background_black}${bold_green}✅ 🎉 All checks passed!${color_reset}"
|
10
scripts/cov.sh
Executable file
10
scripts/cov.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs unit tests.
|
||||
# If successful, prints only the coverage percentage.
|
||||
# If an error occurs, prints the entire unit tests progress output.
|
||||
|
||||
source "$(dirname $(realpath $0))/util.sh"
|
||||
|
||||
coverage erase
|
||||
run_and_capture coverage run
|
||||
coverage report | awk '$1 == "TOTAL" {print $NF; exit}'
|
@ -1,16 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs type checker and linters.
|
||||
# Runs various linters.
|
||||
|
||||
# Ensure that we return to the current working directory
|
||||
# and exit the script immediately in case of an error:
|
||||
trap "cd $(realpath ${PWD}); exit 1" ERR
|
||||
# Change into project root directory:
|
||||
cd "$(dirname $(dirname $(realpath $0)))"
|
||||
|
||||
echo 'Performing type checks...'
|
||||
mypy
|
||||
echo
|
||||
source "$(dirname $(realpath $0))/util.sh"
|
||||
|
||||
echo 'Linting source and test files...'
|
||||
flake8 src/ tests/
|
||||
echo -e 'No issues found.'
|
||||
|
||||
echo ' ruff - Doing extensive linting'
|
||||
ruff src/ tests/
|
||||
|
||||
echo ' black - Ensuring consistent code style'
|
||||
run_and_capture black src/ tests/ --check
|
||||
|
||||
echo ' isort - Ensuring consistent import structure'
|
||||
isort src/ tests/ --check-only
|
||||
|
||||
echo -e "${bold_green}No issues found${color_reset}\n"
|
||||
|
@ -1,17 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs unit tests and prints only coverage percentage, if successful.
|
||||
# If an error occurs, prints the entire unit tests progress output.
|
||||
# Runs unit tests and reports coverage percentage.
|
||||
|
||||
# Ensure that we return to the current working directory in case of an error:
|
||||
trap "cd $(realpath ${PWD})" ERR
|
||||
# Change into project root directory:
|
||||
cd "$(dirname $(dirname $(realpath $0)))"
|
||||
source "$(dirname $(realpath $0))/util.sh"
|
||||
|
||||
coverage erase
|
||||
# Capture the test progression in a variable:
|
||||
typeset progress
|
||||
progress=$(coverage run 2>&1)
|
||||
# If tests failed or produced errors, write progress/messages to stderr and exit:
|
||||
[[ $? -eq 0 ]] || { >&2 echo "${progress}"; exit 1; }
|
||||
# Otherwise extract the total coverage percentage from the produced report and write it to stdout:
|
||||
coverage report | awk '$1 == "TOTAL" {print $NF; exit}'
|
||||
echo 'Running unit tests...'
|
||||
coverage run
|
||||
typeset percentage
|
||||
typeset color
|
||||
percentage="$(coverage report | awk '$1 == "TOTAL" {print $NF; exit}')"
|
||||
[[ $percentage == "100%" ]] && color="${bold_green}" || color="${yellow}"
|
||||
echo -e "${color}${percentage} coverage${color_reset}\n"
|
||||
|
8
scripts/typecheck.sh
Executable file
8
scripts/typecheck.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs type checker.
|
||||
|
||||
source "$(dirname $(realpath $0))/util.sh"
|
||||
|
||||
echo 'Performing type checks...'
|
||||
mypy
|
||||
echo
|
20
scripts/util.sh
Normal file
20
scripts/util.sh
Normal file
@ -0,0 +1,20 @@
|
||||
run_and_capture() {
|
||||
# Captures stderr of any command passed to it
|
||||
# and releases it only if the command exits with a non-zero code.
|
||||
typeset output
|
||||
output=$($@ 2>&1)
|
||||
typeset exit_status=$?
|
||||
[[ $exit_status == 0 ]] || >&2 echo "${output}"
|
||||
return $exit_status
|
||||
}
|
||||
|
||||
# Ensure that we return to the current working directory
|
||||
# and exit the script immediately in case of an error:
|
||||
trap "cd $(realpath ${PWD}); exit 1" ERR
|
||||
# Change into project root directory:
|
||||
cd "$(dirname $(dirname $(realpath $0)))"
|
||||
|
||||
typeset background_black='\033[40m'
|
||||
typeset bold_green='\033[1;92m'
|
||||
typeset yellow='\033[0;33m'
|
||||
typeset color_reset='\033[0m'
|
@ -1,4 +1,4 @@
|
||||
__copyright__ = "© 2023 ${REPO_OWNER_TITLE}"
|
||||
... __copyright__ = "© 2023 ..."
|
||||
__license__ = """Apache-2.0
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -16,5 +16,5 @@ limitations under the License."""
|
||||
__version__ = "0.0.1"
|
||||
|
||||
__doc__ = """
|
||||
PLACEHOLDER
|
||||
${REPO_DESCRIPTION}
|
||||
"""
|
||||
|
@ -1,13 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
pattern = sys.argv[1]
|
||||
except IndexError:
|
||||
pattern = "test*.py"
|
||||
test_suite = unittest.defaultTestLoader.discover(".", pattern=pattern)
|
||||
test_runner = unittest.TextTestRunner(resultclass=unittest.TextTestResult)
|
||||
result = test_runner.run(test_suite)
|
||||
sys.exit(not result.wasSuccessful())
|
Reference in New Issue
Block a user