Compare commits

...

7 Commits

Author SHA1 Message Date
c50cfe88d6 Add pytest to dev dependencies;
upgrade dev dependencies;
remove unnecessary main test script entrypoint;
remove unnecessary line from `.gitignore`;
put repo description variable into top-level `__init__.py`;
introduce syntax-error as a reminder to fix copyright;
add CSS fix for broken inline-code segments;
update `index.md` layout;
reorder linting script calls and add a bit more output;
support Python 3.8+ by default;
fix wrong dependency file reference in `pyproject.toml`;
add `pytest` config to `pyproject.toml`;
call `pytest` in `coverage` config;
add a few sensible rule-ignores to `ruff` config
2023-10-27 12:20:41 +02:00
1a293563d1 Upgrade dev dependencies;
upgrade to newer reusable Github workflow;
rename requirements files;
add `ruff` linting rules;
add `black` config for line length 80
2023-10-16 17:34:07 +02:00
acf4c06404 Upgrade dev dependencies; leave more forced placeholders 2023-07-14 11:22:37 +02:00
1c49c4923f Add generic .cache to .gitignore 2023-06-24 20:36:43 +02:00
9f89f8cc9d Add relevant links to home/readme page 2023-06-24 20:34:46 +02:00
21eb0c065d Add/update dev scripts and Github workflow 2023-06-24 17:10:24 +02:00
f5b31b7580 Update dev dependencies; add settings; insert syntax-bug-placeholders 2023-06-24 17:09:05 +02:00
17 changed files with 268 additions and 87 deletions

34
.github/workflows/ci.yaml vendored Normal file
View 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
View File

@ -18,8 +18,5 @@
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
# Testing: # Miscellaneous cache:
/.coverage .cache/
# mypy:
.mypy_cache/

View File

@ -0,0 +1,3 @@
.md-typeset code {
word-break: keep-all; /* Prevent inline-code from being broken up. */
}

View File

@ -1,15 +1,29 @@
# ${REPO_NAME} # ${REPO_NAME}
${REPO_DESCRIPTION} **${REPO_DESCRIPTION}**
## Usage ---
... [📑 Documentation][1]   |   [🧑‍💻 Source Code][2]   |   [🐛 Bug Tracker][3]
---
## Installation ## Installation
`pip install ${REPO_NAME}` `pip install ${REPO_NAME}`
## Usage
...
## Dependencies ## 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

View File

@ -1,4 +1,4 @@
copyright: "© 2023 ${REPO_OWNER_TITLE}" ... copyright: "© 2023 ..."
site_name: "${REPO_NAME}" site_name: "${REPO_NAME}"
site_description: "${REPO_DESCRIPTION}" site_description: "${REPO_DESCRIPTION}"
site_url: "http://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}" site_url: "http://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}"
@ -26,13 +26,23 @@ extra_css:
plugins: plugins:
- search - 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: markdown_extensions:
- admonition - admonition
- codehilite - codehilite
- extra - extra
- pymdownx.superfences - pymdownx.superfences
- toc:
permalink: true
nav: nav:
- Home: index.md - Home: index.md

View File

@ -2,21 +2,24 @@
# Python packaging: # # Python packaging: #
[build-system] [build-system]
requires = ["setuptools", "setuptools-scm"] requires = [
"setuptools",
"setuptools-scm",
]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "${REPO_NAME}" name = "${REPO_NAME}"
description = "${REPO_DESCRIPTION}" description = "${REPO_DESCRIPTION}"
authors = [ authors = [
{ name = "${REPO_OWNER_TITLE}", email = "mail@PLACEHOLDER.to" }, { name = ..., email = ... },
] ]
maintainers = [ maintainers = [
{ name = "${REPO_OWNER_TITLE}", email = "mail@PLACEHOLDER.to" }, { name = ..., email = ... },
] ]
requires-python = ">=3.9, <4.0" requires-python = ">=3.8, <4.0"
keywords = [ keywords = [
...
] ]
license = { text = "Apache Software License Version 2.0" } license = { text = "Apache Software License Version 2.0" }
classifiers = [ classifiers = [
@ -30,6 +33,7 @@ classifiers = [
# "Intended Audience :: Developers", # "Intended Audience :: Developers",
# "Framework :: AsyncIO", # "Framework :: AsyncIO",
# "Topic :: Internet", # "Topic :: Internet",
...
] ]
dynamic = [ dynamic = [
"dependencies", "dependencies",
@ -38,33 +42,34 @@ dynamic = [
] ]
[project.optional-dependencies] [project.optional-dependencies]
full = [ full = []
]
dev = [ dev = [
"black", "black==23.10.1",
"build", "build==1.0.3",
"coverage[toml]", "coverage[toml]==7.3.2",
"flake8", "isort==5.12.0",
"mkdocs-material", "mkdocs-material==9.4.6",
"mkdocstrings[python]", "mkdocstrings[python]==0.23.0",
"mypy", "mypy==1.5.1",
"pytest==7.4.3",
"ruff==0.1.3",
] ]
[project.urls] [project.urls]
repository = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}" "Repository" = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}"
bug_tracker = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}/issues" "Issue Tracker" = "https://github.com/${REPO_OWNER_LOWER}/${REPO_NAME_LOWER}/issues"
documentation = "http://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}" "Documentation" = "http://${REPO_OWNER_LOWER}.github.io/${REPO_NAME_LOWER}"
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
dependencies = { file = "requirements/common.txt" } dependencies = { file = "requirements/base.txt" }
readme = { file = ["README.md"] } readme = { file = ["README.md"], content-type = "text/markdown" }
version = {attr = "PACKAGE_NAME_PLACEHOLDER.__version__"} version = { attr = ..."${REPO_NAME}.__version__" }
######### #########################
# Mypy: # # Static type checking: #
[tool.mypy] [tool.mypy]
cache_dir = ".cache/mypy"
files = [ files = [
"src/", "src/",
"tests/", "tests/",
@ -72,19 +77,28 @@ files = [
warn_unused_configs = true warn_unused_configs = true
strict = true strict = true
show_error_codes = 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] [tool.coverage.run]
data_file = ".cache/coverage"
source = [ source = [
"src/", "src/",
] ]
branch = true branch = true
command_line = "-m tests" command_line = "-m pytest -qq"
omit = [ omit = [
".venv*/*", ".venv*/*",
] ]
@ -100,3 +114,76 @@ exclude_lines = [
omit = [ omit = [
"tests/*", "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

View File

@ -1,8 +1,10 @@
-r common.txt -r full.txt
black black==23.10.1
build build==1.0.3
coverage[toml] coverage[toml]==7.3.2
flake8 isort==5.12.0
mkdocs-material mkdocs-material==9.4.6
mkdocstrings[python] mkdocstrings[python]==0.23.0
mypy mypy==1.5.1
pytest==7.4.3
ruff==0.1.3

1
requirements/full.txt Normal file
View File

@ -0,0 +1 @@
-r base.txt

12
scripts/ci.sh Executable file
View 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
View 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}'

View File

@ -1,16 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runs type checker and linters. # Runs various linters.
# Ensure that we return to the current working directory source "$(dirname $(realpath $0))/util.sh"
# 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
echo 'Linting source and test files...' 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"

View File

@ -1,17 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runs unit tests and prints only coverage percentage, if successful. # Runs unit tests and reports coverage percentage.
# If an error occurs, prints the entire unit tests progress output.
# Ensure that we return to the current working directory in case of an error: source "$(dirname $(realpath $0))/util.sh"
trap "cd $(realpath ${PWD})" ERR
# Change into project root directory:
cd "$(dirname $(dirname $(realpath $0)))"
coverage erase echo 'Running unit tests...'
# Capture the test progression in a variable: coverage run
typeset progress typeset percentage
progress=$(coverage run 2>&1) typeset color
# If tests failed or produced errors, write progress/messages to stderr and exit: percentage="$(coverage report | awk '$1 == "TOTAL" {print $NF; exit}')"
[[ $? -eq 0 ]] || { >&2 echo "${progress}"; exit 1; } [[ $percentage == "100%" ]] && color="${bold_green}" || color="${yellow}"
# Otherwise extract the total coverage percentage from the produced report and write it to stdout: echo -e "${color}${percentage} coverage${color_reset}\n"
coverage report | awk '$1 == "TOTAL" {print $NF; exit}'

8
scripts/typecheck.sh Executable file
View 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
View 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'

View File

@ -1,4 +1,4 @@
__copyright__ = "© 2023 ${REPO_OWNER_TITLE}" ... __copyright__ = "© 2023 ..."
__license__ = """Apache-2.0 __license__ = """Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -16,5 +16,5 @@ limitations under the License."""
__version__ = "0.0.1" __version__ = "0.0.1"
__doc__ = """ __doc__ = """
PLACEHOLDER ${REPO_DESCRIPTION}
""" """

View File

@ -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())