Skip to content

Commit 52c3df6

Browse files
authored
Add precommit hooks via prek and default ruff/pytest configuration (#84)
Adds pre-commit hooks via https://github.com/j178/prek and configures stricter code quality defaults. Changes: Pre-commit hooks using prek (format, lint on commit; tests on push) New make dev target to set up dev environment and install hooks Stricter ruff rules: cyclomatic complexity ≤8, max 5 positional args, max 12 branches pytest config with --durations=10 Updated deps: ruff ~=0.14.0, pyright ~=1.1, removed unused type stubs
1 parent 9a9a70a commit 52c3df6

4 files changed

Lines changed: 88 additions & 20 deletions

File tree

hooks/pre_gen_project.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
namespace_import = "{{ cookiecutter.project_namespace_import }}"
77
if namespace_import and not re.match(NAMESPACE_REGEX, namespace_import):
88
print(f"ERROR: '{namespace_import}' is not a valid Python namespace import path!")
9-
print(f" It must follow regex '{NAMESPACE_REGEX}', i.e. 'one_two' or 'one_two.three'")
9+
print(
10+
f" It must follow regex '{NAMESPACE_REGEX}', i.e. 'one_two' or 'one_two.three'"
11+
)
1012
sys.exit(1)
1113

1214

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Pre-commit hooks for code quality
2+
# Uses prek (https://github.com/j178/prek) - a faster pre-commit alternative
3+
repos:
4+
- repo: builtin
5+
hooks:
6+
- id: trailing-whitespace
7+
- id: end-of-file-fixer
8+
- id: check-yaml
9+
- id: check-toml
10+
- id: check-merge-conflict
11+
- id: detect-private-key
12+
13+
- repo: local
14+
hooks:
15+
- id: format
16+
name: format code
17+
entry: make format
18+
language: system
19+
types: [python]
20+
pass_filenames: false
21+
22+
- id: lint
23+
name: lint code
24+
entry: make lint
25+
language: system
26+
types: [python]
27+
pass_filenames: false
28+
require_serial: true
29+
30+
- id: test
31+
name: run tests
32+
entry: make test
33+
language: system
34+
types: [python]
35+
pass_filenames: false
36+
require_serial: true
37+
stages: [pre-push]

{{cookiecutter.project_slug}}/Makefile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ endif
2121
all:
2222
@echo "Run my targets individually!"
2323

24+
.PHONY: dev
25+
dev:
26+
uv sync --group dev
27+
uv run prek install
28+
2429
{%- if cookiecutter.entry_point %}
2530
.PHONY: run
2631
run:
@@ -32,11 +37,8 @@ lint:
3237
uv sync --group lint
3338
uv run ruff format --check && \
3439
uv run ruff check && \
35-
uv run pyright
36-
37-
{%- if cookiecutter.docstring_coverage %}
38-
uv run interrogate -c pyproject.toml .
39-
{%- endif %}
40+
uv run pyright{% if cookiecutter.docstring_coverage %} && \
41+
uv run interrogate -c pyproject.toml .{% endif %}
4042

4143
.PHONY: format
4244
format:

{{cookiecutter.project_slug}}/pyproject.toml

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ version = "{{ cookiecutter.version }}"
44
description = "{{ cookiecutter.project_description }}"
55
readme = "README.md"
66
license-files = ["LICENSE"]
7-
87
{%- if cookiecutter.license == "Apache 2.0" %}
98
license = "Apache-2.0"
109
{%- elif cookiecutter.license == "AGPL v3" %}
@@ -36,11 +35,8 @@ test = ["pytest", "pytest-cov", "pytest-timeout", "pretend", "coverage[toml]"]
3635
lint = [
3736
# NOTE: ruff is under active development, so we pin conservatively here
3837
# and let Dependabot periodically perform this update.
39-
"ruff ~= 0.12",
40-
"pyright ~= 1.1.407",
41-
"types-html5lib",
42-
"types-requests",
43-
"types-toml",
38+
"ruff ~= 0.14.0",
39+
"pyright ~= 1.1",
4440
{%- if cookiecutter.docstring_coverage %}
4541
"interrogate",
4642
{%- endif %}
@@ -49,6 +45,7 @@ dev = [
4945
{include-group = "doc"},
5046
{include-group = "test"},
5147
{include-group = "lint"},
48+
"prek",
5249
]
5350

5451
{% if cookiecutter.entry_point -%}
@@ -68,33 +65,58 @@ omit = ["{{ cookiecutter.__project_src_path }}/_cli.py"]
6865

6966
[tool.pyright]
7067
include = ["src", "test"]
71-
exclude = []
68+
pythonVersion = "3.10"
69+
typeCheckingMode = "strict"
7270
reportUnusedImport = "warning"
7371
reportUnusedVariable = "warning"
7472
reportGeneralTypeIssues = "error"
75-
typeCheckingMode = "strict"
73+
reportMissingTypeStubs = true
7674

7775
[tool.ruff]
7876
line-length = 100
79-
include = ["src/**/*.py", "test/**/*.py"]
77+
target-version = "py310"
78+
79+
[tool.ruff.format]
80+
line-ending = "lf"
81+
quote-style = "double"
8082

8183
[tool.ruff.lint]
8284
select = ["ALL"]
83-
# D203 and D213 are incompatible with D211 and D212 respectively.
84-
# COM812 and ISC001 can cause conflicts when using ruff as a formatter.
85-
# See https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules.
86-
ignore = ["D203", "D213", "COM812", "ISC001"]
85+
ignore = [
86+
"D203", # Incompatible with D211
87+
"D213", # Incompatible with D212
88+
"COM812", # Can conflict with formatter
89+
"ISC001", # Can conflict with formatter
90+
]
91+
92+
[tool.ruff.lint.mccabe]
93+
# Maximum cyclomatic complexity
94+
max-complexity = 8
95+
96+
[tool.ruff.lint.pydocstyle]
97+
# Use Google-style docstrings
98+
convention = "google"
99+
100+
[tool.ruff.lint.pylint]
101+
# Maximum number of branches for function or method
102+
max-branches = 12
103+
# Maximum number of return statements in function or method
104+
max-returns = 6
105+
# Maximum number of positional arguments for function or method
106+
max-positional-args = 5
87107

88108
[tool.ruff.lint.per-file-ignores]
89109
{% if cookiecutter.entry_point -%}
90110
"{{ cookiecutter.__project_src_path }}/_cli.py" = [
91-
"T201", # allow `print` in cli module
111+
"T201", # allow print in cli module
92112
]
93113
{%- endif %}
94114
"test/**/*.py" = [
95115
"D", # no docstrings in tests
96116
"S101", # asserts are expected in tests
117+
"PLR2004", # Allow magic values in tests
97118
]
119+
"**/conftest.py" = ["D"] # No docstrings in pytest config
98120

99121
{%- if cookiecutter.docstring_coverage %}
100122
[tool.interrogate]
@@ -105,6 +127,11 @@ ignore-semiprivate = true
105127
fail-under = 100
106128
{%- endif %}
107129

130+
[tool.pytest.ini_options]
131+
testpaths = ["test"]
132+
python_files = ["test_*.py"]
133+
addopts = "--durations=10"
134+
108135
[tool.uv.sources]
109136
{{ cookiecutter.project_slug }} = { workspace = true }
110137

0 commit comments

Comments
 (0)