diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4da3050..8304a8f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,6 +47,9 @@ jobs: - name: Lint with Sqlfluff run: make lint-sql + - name: Quality check with pyscn + run: make lint-quality + type-check: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 3f2cbf1..9437f13 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +.pyscn/ # Translations *.mo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66b218f..924d696 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,6 +41,15 @@ repos: - id: interrogate pass_filenames: false # config (incl. exclude paths) lives in pyproject.toml +# pyscn (code quality gate: complexity, dead code, clones, circular deps) +- repo: local + hooks: + - id: pyscn + name: pyscn + entry: uv run pyscn check + language: system + pass_filenames: false + # sqlfluff - repo: https://github.com/sqlfluff/sqlfluff rev: 3.5.0 diff --git a/Makefile b/Makefile index 9b7c924..f81de20 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ lock: format: uv run ruff format --check . -lint: lint-python lint-sql lint-docstrings +lint: lint-python lint-sql lint-docstrings lint-quality lint-python: uv run ruff check --output-format=github . @@ -50,6 +50,9 @@ lint-sql: lint-docstrings: uv run interrogate +lint-quality: + uv run pyscn check . + type: uv run pyrefly check diff --git a/README.md b/README.md index b19b7ed..6b59d08 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ The checks contained in this repo include (in the order in which they run): * `pyrefly` is used for static type checking * `sqlfluff` checks and fixes sql formatting and linting * `interrogate` checks docstring coverage +* `pyscn` checks code quality (complexity, dead code, clones, circular deps) * `uv` checks on valid and aligned pyproject.toml and uv.lock files * `commitlint` enforces commit message conforms to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format diff --git a/pyproject.toml b/pyproject.toml index bbe47e0..9a7636e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dev = [ "nox >=2024.4.15,<2025", "prek >=0.1.0,<1", "pyrefly >=1.2.0,<2", + "pyscn >=1.31.2,<2", "pytest >=8.3.2,<9", "pytest-cov >=5.0.0,<6", "python-semantic-release >=10.0.0,<11", @@ -52,6 +53,39 @@ verbose = 1 exclude = ["tests", "docs", "noxfile.py"] +####### +# Pyscn +####### +# Code quality gate: complexity, dead code, clones, coupling, architecture. +# This only sets what deviates from pyscn's own defaults (or is left as an +# explicit toggle). Full option reference: https://docs.codescan.dev/configuration/reference/ + +[tool.pyscn.output] +format = "text" # Default output format: text, json, yaml, csv, html +show_details = false # Show detailed breakdown by default +sort_by = "complexity" # Default sort: name, complexity, risk +min_complexity = 1 # Minimum complexity to report +directory = "" # Output directory for reports (empty = default .pyscn/reports/, gitignored) + +[tool.pyscn.complexity] +enabled = true # Enable complexity analysis +low_threshold = 9 # Functions with complexity ≤ 9 are low risk +medium_threshold = 19 # Functions with complexity 10-19 are medium risk + # Functions with complexity ≥ 20 are high risk +max_complexity = 0 # Maximum allowed complexity (0 = no limit). Must be 0 or > medium_threshold; + # the CI gate's own limit (10) is set independently via `pyscn check --max-complexity` +report_unchanged = true # Report functions with complexity = 1 + +[tool.pyscn.dead_code] +enabled = true # Enable dead code detection; see reference link above for tunable options + +[tool.pyscn.cbo] +enabled = true # Enable class coupling (CBO) analysis; see reference link above for tunable options + +[tool.pyscn.architecture] +enabled = false # This project is a flat single-package library, not a layered app + + ########## # Coverage ########## diff --git a/uv.lock b/uv.lock index 718b662..033c6f3 100644 --- a/uv.lock +++ b/uv.lock @@ -1988,6 +1988,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/6f/459f56dc345c32f8597acef13675e7e67679fb56bc3fd8f0a280eaeb8c02/pyrefly-1.3.0-py3-none-win_arm64.whl", hash = "sha256:3a3fb7c07dfb9b43d4205d8e85a83aaefc85377d0437dc8469c132836a8a7e5d", size = 14347403, upload-time = "2026-09-11T00:41:35.01Z" }, ] +[[package]] +name = "pyscn" +version = "1.31.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/8f/da2c6b5e9c4f0b70ccb48fdfd0c0b96534ac0f5d2097c20f48664cf1c574/pyscn-1.31.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:87d0e0d1813c384345c3f7cc726678f095c975404f59039127f50a62e5708afa", size = 10232689, upload-time = "2026-09-10T09:12:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/76/26/8dafae4879b88453c4db396f73b102773453c21d05a99291ad193ac01451/pyscn-1.31.2-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e07658e496fd0cd0ba60640f6e8cf0da316a1258a44a9790fd55535df0131b31", size = 10046867, upload-time = "2026-09-10T09:12:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/d7d125796d0e6db9d8103dc0174255354561f0abf89c004851afaabaddec/pyscn-1.31.2-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:55c6a22d4b1ee24628a62b06c9967d7e9b2619067097793172414e03c1505621", size = 11162333, upload-time = "2026-09-10T09:12:54.705Z" }, + { url = "https://files.pythonhosted.org/packages/65/74/b70422a79032bf0d39fb8b7d5fd4b08fea03aede6424d288f3f55422742b/pyscn-1.31.2-py3-none-win_amd64.whl", hash = "sha256:c8349f184d40e16841466054738f5adacd8df4cba2e6458a80e7a0d8ec6e383f", size = 11630685, upload-time = "2026-09-10T09:12:57.287Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -2613,6 +2624,7 @@ dev = [ { name = "nox" }, { name = "prek" }, { name = "pyrefly" }, + { name = "pyscn" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "python-semantic-release" }, @@ -2633,6 +2645,7 @@ dev = [ { name = "nox", specifier = ">=2024.4.15,<2025" }, { name = "prek", specifier = ">=0.1.0,<1" }, { name = "pyrefly", specifier = ">=1.2.0,<2" }, + { name = "pyscn", specifier = ">=1.31.2,<2" }, { name = "pytest", specifier = ">=8.3.2,<9" }, { name = "pytest-cov", specifier = ">=5.0.0,<6" }, { name = "python-semantic-release", specifier = ">=10.0.0,<11" },