diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 8c7a3ef..80a0ff1 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -6,7 +6,9 @@ namespace_import = "{{ cookiecutter.project_namespace_import }}" if namespace_import and not re.match(NAMESPACE_REGEX, namespace_import): print(f"ERROR: '{namespace_import}' is not a valid Python namespace import path!") - print(f" It must follow regex '{NAMESPACE_REGEX}', i.e. 'one_two' or 'one_two.three'") + print( + f" It must follow regex '{NAMESPACE_REGEX}', i.e. 'one_two' or 'one_two.three'" + ) sys.exit(1) diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml new file mode 100644 index 0000000..67afd28 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +# Pre-commit hooks for code quality +# See https://pre-commit.com for more information +repos: + - repo: local + hooks: + - id: format + name: format code + entry: make format + language: system + types: [python] + pass_filenames: false + + - id: lint + name: lint code + entry: make lint + language: system + types: [python] + pass_filenames: false + require_serial: true + + - id: test + name: run tests + entry: make test + language: system + types: [python] + pass_filenames: false + require_serial: true + stages: [pre-commit] + +# Configuration +ci: + autofix_prs: true diff --git a/{{cookiecutter.project_slug}}/Makefile b/{{cookiecutter.project_slug}}/Makefile index 39305db..6988f95 100644 --- a/{{cookiecutter.project_slug}}/Makefile +++ b/{{cookiecutter.project_slug}}/Makefile @@ -21,6 +21,11 @@ endif all: @echo "Run my targets individually!" +.PHONY: dev +dev: + uv sync --group dev + uv run pre-commit install + {%- if cookiecutter.entry_point %} .PHONY: run run: @@ -32,11 +37,8 @@ lint: uv sync --group lint uv run ruff format --check && \ uv run ruff check && \ - uv run pyright - - {%- if cookiecutter.docstring_coverage %} - uv run interrogate -c pyproject.toml . - {%- endif %} + uv run pyright{% if cookiecutter.docstring_coverage %} && \ + uv run interrogate -c pyproject.toml .{% endif %} .PHONY: format format: diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 0dd3cf0..6bda506 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -4,7 +4,6 @@ version = "{{ cookiecutter.version }}" description = "{{ cookiecutter.project_description }}" readme = "README.md" license-files = ["LICENSE"] - {%- if cookiecutter.license == "Apache 2.0" %} license = "Apache-2.0" {%- elif cookiecutter.license == "AGPL v3" %} @@ -36,11 +35,8 @@ test = ["pytest", "pytest-cov", "pytest-timeout", "pretend", "coverage[toml]"] lint = [ # NOTE: ruff is under active development, so we pin conservatively here # and let Dependabot periodically perform this update. - "ruff ~= 0.12", - "pyright ~= 1.1.407", - "types-html5lib", - "types-requests", - "types-toml", + "ruff ~= 0.14.0", + "pyright", {%- if cookiecutter.docstring_coverage %} "interrogate", {%- endif %} @@ -49,6 +45,7 @@ dev = [ {include-group = "doc"}, {include-group = "test"}, {include-group = "lint"}, + "pre-commit", ] {% if cookiecutter.entry_point -%} @@ -68,33 +65,58 @@ omit = ["{{ cookiecutter.__project_src_path }}/_cli.py"] [tool.pyright] include = ["src", "test"] -exclude = [] +pythonVersion = "3.10" +typeCheckingMode = "strict" reportUnusedImport = "warning" reportUnusedVariable = "warning" reportGeneralTypeIssues = "error" -typeCheckingMode = "strict" +reportMissingTypeStubs = true [tool.ruff] line-length = 100 -include = ["src/**/*.py", "test/**/*.py"] +target-version = "py310" + +[tool.ruff.format] +line-ending = "lf" +quote-style = "double" [tool.ruff.lint] select = ["ALL"] -# D203 and D213 are incompatible with D211 and D212 respectively. -# COM812 and ISC001 can cause conflicts when using ruff as a formatter. -# See https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules. -ignore = ["D203", "D213", "COM812", "ISC001"] +ignore = [ + "D203", # Incompatible with D211 + "D213", # Incompatible with D212 + "COM812", # Can conflict with formatter + "ISC001", # Can conflict with formatter +] + +[tool.ruff.lint.mccabe] +# Maximum cyclomatic complexity +max-complexity = 8 + +[tool.ruff.lint.pydocstyle] +# Use Google-style docstrings +convention = "google" + +[tool.ruff.lint.pylint] +# Maximum number of branches for function or method +max-branches = 12 +# Maximum number of return statements in function or method +max-returns = 6 +# Maximum number of positional arguments for function or method +max-positional-args = 5 [tool.ruff.lint.per-file-ignores] {% if cookiecutter.entry_point -%} "{{ cookiecutter.__project_src_path }}/_cli.py" = [ - "T201", # allow `print` in cli module + "T201", # allow print in cli module ] {%- endif %} "test/**/*.py" = [ "D", # no docstrings in tests "S101", # asserts are expected in tests + "PLR2004", # Allow magic values in tests ] +"**/conftest.py" = ["D"] # No docstrings in pytest config {%- if cookiecutter.docstring_coverage %} [tool.interrogate] @@ -105,6 +127,11 @@ ignore-semiprivate = true fail-under = 100 {%- endif %} +[tool.pytest.ini_options] +testpaths = ["test"] +python_files = ["test_*.py"] +addopts = "--durations=10" + [tool.uv.sources] {{ cookiecutter.project_slug }} = { workspace = true }