[tox] requires = tox>=4.2 tox-uv>=1 env_list = py315 flake8 autopep8 doc8 pyupgrade refurb ruff-check black mypy isort interrogate coverage diff-cover djlint djlint-check codespell pymarkdown mdformat pyroma pyproject-fmt validate-pyproject tox-ini-fmt yamllint check-jsonschema deptry unimport pydocstyle vulture blocklint gitlint absolufy-imports autoflake black add-trailing-comma rstcheck pip-audit bandit safety security licenses doctest docformatter py{314, 313, 312} skip_missing_interpreters = true labels = critical = ruff-check, black, flake8 quality = mypy, isort, interrogate, pydocstyle, autopep8, doc8, codespell, pymarkdown, mdformat, pyroma, pyproject-fmt, validate-pyproject, tox-ini-fmt, yamllint, check-jsonschema, deptry, unimport, vulture, blocklint, gitlint, absolufy-imports, autoflake, black, docformatter, add-trailing-comma, rstcheck, pyupgrade, refurb, djlint-check format = autopep8, black, docformatter, add-trailing-comma, djlint test = py{312, 313, 314}, doctest coverage = coverage, diff-cover security = pip-audit, bandit, safety, security, licenses parallel_show_output = true [testenv] description = Run unit and integration tests with pytest extras = test pass_env = CI commands_pre = pytest --version commands = pytest --junitxml=junit.xml {posargs:tests/} [testenv:py315] description = Test with Python 3.15-dev (experimental, may fail) extras = test commands_pre = pytest --version commands = pytest {posargs:tests/} [testenv:flake8] description = Lint Python code with flake8 - Tier 1: Fast quality check skip_install = true deps = flake8 flake8-pyproject commands_pre = flake8 --version commands = flake8 {posargs:src tests} labels = quality, critical [testenv:autopep8] description = Format Python code with autopep8 skip_install = true deps = autopep8 commands_pre = autopep8 --version commands = autopep8 --diff --recursive src tests {posargs} labels = format, quality [testenv:doc8] description = Lint RST documentation with doc8 skip_install = true deps = doc8 commands_pre = doc8 --version commands = doc8 --ignore-path docs/_build {posargs:docs} labels = quality, docs [testenv:pyupgrade] description = Modernize Python syntax with pyupgrade skip_install = true deps = pyupgrade commands_pre = commands = bash -c 'pyupgrade --py312-plus $(find src tests -name "*.py")' allowlist_externals = bash find labels = format, quality [testenv:refurb] description = Run refurb Python modernization linter (pathlib, comprehensions, modern idioms) - informational only skip_install = true deps = refurb>=2.3.1 commands_pre = refurb --version commands = - refurb src/ --enable-all allowlist_externals = labels = quality, modernization [testenv:ruff-check] description = Run linting (ruff check) - Tier 1: Fast quality check extras = lint commands_pre = ruff --version commands = ruff check src tests {posargs} labels = lint, quality, critical [testenv:black] description = Check formatting (black --check) - Tier 1: Fast quality check (authoritative formatter) skip_install = true deps = black==26.3.1 commands_pre = black --version commands = black --check --diff src tests {posargs} labels = format, quality, critical [testenv:mypy] description = Run type checking (mypy) - Tier 2: Type checking (depends on tier 1) extras = type commands_pre = mypy --version commands = mypy src {posargs} depends = flake8 ruff-check black labels = type, quality [testenv:isort] description = Sort Python imports with isort - Tier 2: Advanced linting (depends on tier 1) skip_install = true deps = isort commands_pre = isort --version commands = isort --check-only --diff src tests {posargs} depends = flake8 ruff-check black labels = quality [testenv:interrogate] description = Check docstring coverage with interrogate - Tier 2: Advanced linting (depends on tier 1) skip_install = true deps = interrogate commands_pre = interrogate --version commands = interrogate {posargs:src} depends = flake8 ruff-check black labels = quality, docs [testenv:coverage] description = Run tests with coverage reporting - Tier 4: Coverage (depends on tier 3) package = editable extras = {[testenv]extras} commands_pre = pytest --version commands = pytest \ --cov=nhl_scrabble \ --cov-report=term-missing:skip-covered \ --cov-report=html \ --cov-report=xml \ --cov-fail-under=49 \ --junitxml=junit.xml \ {posargs:tests/} python -c 'import os; print("\nCoverage report: file://" + os.path.abspath("htmlcov/index.html"))' depends = py314 py313 py312 coverage labels = test [testenv:diff-cover] description = Check test coverage on changed lines only (diff coverage) - Tier 4: Coverage (depends on tier 3) package = editable extras = test commands_pre = diff-cover --version git fetch --depth=100 origin main commands = pytest --cov=nhl_scrabble --cov-report=xml diff-cover coverage.xml --compare-branch=origin/main --fail-under=80 allowlist_externals = git depends = py314 py313 py312 coverage labels = quality [testenv:djlint] description = Lint and format HTML/Jinja2 templates skip_install = true deps = djlint>=1.34 commands_pre = djlint --version commands = djlint src/ --check --profile=jinja --lint djlint src/ --reformat --profile=jinja allowlist_externals = djlint labels = quality, format [testenv:djlint-check] description = Check HTML/Jinja2 template linting (no formatting) skip_install = true deps = djlint>=1.34 commands_pre = djlint --version commands = djlint src/ --check --profile=jinja --lint allowlist_externals = djlint labels = quality [testenv:codespell] description = Check spelling in code and documentation skip_install = true deps = codespell commands_pre = codespell --version commands = codespell {posargs:src tests docs *.md} labels = quality [testenv:pymarkdown] description = Lint markdown files with pymarkdown skip_install = true deps = pymarkdownlnt commands_pre = pymarkdown version commands = pymarkdown scan {posargs:*.md docs} labels = quality [testenv:mdformat] description = Format markdown files with mdformat (excludes docs with pseudo-code) skip_install = true deps = mdformat mdformat-black mdformat-gfm mdformat-ruff mdformat-web commands_pre = mdformat --version commands = bash -c 'mdformat --check $(find . -maxdepth 1 -name "*.md" -not -name "sync-report.md") $(find docs -name "*.md" -not -path "docs/reference/cli-generated.md" -not -path "docs/explanation/*" -not -path "docs/contributing/*" -not -path "docs/how-to/run-tests.md" -not -path "docs/how-to/build-documentation.md" -not -path "docs/tutorials/01-getting-started.md")' allowlist_externals = bash find labels = quality [testenv:pyroma] description = Rate Python package metadata quality skip_install = true deps = pyroma commands_pre = commands = pyroma {posargs:.} labels = quality [testenv:pyproject-fmt] description = Format pyproject.toml configuration file skip_install = true deps = pyproject-fmt commands_pre = pyproject-fmt --version commands = pyproject-fmt {posargs:pyproject.toml} labels = format, quality [testenv:validate-pyproject] description = Validate pyproject.toml against PEP standards skip_install = true deps = validate-pyproject[all] commands_pre = validate-pyproject --version commands = validate-pyproject {posargs:pyproject.toml} labels = quality [testenv:tox-ini-fmt] description = Format tox.ini configuration file skip_install = true deps = tox-ini-fmt commands_pre = commands = tox-ini-fmt {posargs:tox.ini} labels = format, quality [testenv:yamllint] description = Lint YAML files with yamllint skip_install = true deps = yamllint commands_pre = yamllint --version commands = yamllint {posargs:.} labels = quality [testenv:check-jsonschema] description = Validate JSON and YAML files against schemas skip_install = true deps = check-jsonschema>=0.37.1 commands_pre = check-jsonschema --version commands = bash -c 'check-jsonschema --schemafile "https://json.schemastore.org/github-workflow.json" .github/workflows/*.yml' bash -c 'if [ -d .github/actions ]; then check-jsonschema --schemafile "https://json.schemastore.org/github-action.json" .github/actions/*/action.yml || true; fi' bash -c 'if [ -f .github/dependabot.yml ]; then check-jsonschema --schemafile "https://json.schemastore.org/dependabot-2.0.json" .github/dependabot.yml || true; fi' bash -c 'if [ -f .codecov.yml ]; then check-jsonschema --schemafile "https://json.schemastore.org/codecov.json" .codecov.yml; fi' bash -c 'if [ -f .pre-commit-config.yaml ]; then check-jsonschema --schemafile "https://json.schemastore.org/pre-commit-config.json" .pre-commit-config.yaml; fi' allowlist_externals = bash labels = quality, validation [testenv:deptry] description = Check for dependency issues with deptry skip_install = true deps = deptry commands_pre = deptry --version commands = deptry {posargs:src} labels = quality [testenv:unimport] description = Check for unused imports with unimport skip_install = true deps = unimport commands_pre = unimport --version commands = unimport --check --diff {posargs:src} labels = quality [testenv:pydocstyle] description = Check docstring style with pydocstyle skip_install = true deps = pydocstyle tomli commands_pre = pydocstyle --version commands = pydocstyle {posargs:src} labels = quality, docs [testenv:vulture] description = Find dead code with vulture skip_install = true deps = vulture commands_pre = vulture --version commands = vulture {posargs:src .vulture_allowlist} labels = quality [testenv:blocklint] description = Check for non-inclusive language with blocklint skip_install = true deps = blocklint commands_pre = python -c "import blocklint; print(f'blocklint {blocklint.__version__}')" commands = blocklint --skip-files .git,.tox,.venv,venv,build,dist,*.egg-info,__pycache__,coverage.xml,htmlcov,.coverage,.pytest_cache,.mypy_cache,.ruff_cache,*.log,*.tmp {posargs:.} labels = quality [testenv:gitlint] description = Lint commit messages with gitlint skip_install = true deps = gitlint commands_pre = gitlint --version commands = gitlint --commit {posargs:HEAD} labels = quality [testenv:absolufy-imports] description = Convert relative imports to absolute imports skip_install = true deps = absolufy-imports commands_pre = commands = bash -c 'absolufy-imports --application-directories=src src/**/*.py' allowlist_externals = bash labels = quality [testenv:autoflake] description = Remove unused imports and variables with autoflake skip_install = true deps = autoflake commands_pre = autoflake --version commands = autoflake --check --recursive --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys --ignore-init-module-imports src tests {posargs} labels = format, quality [testenv:add-trailing-comma] description = Add trailing commas to Python code for better git diffs skip_install = true deps = add-trailing-comma>=4 commands_pre = add-trailing-comma --version commands = bash -c 'find src tests -name "*.py" -type f | xargs add-trailing-comma' allowlist_externals = bash find labels = format, quality [testenv:rstcheck] description = Check RST syntax with rstcheck skip_install = true deps = rstcheck sphinx commands_pre = rstcheck --version commands = rstcheck --recursive {posargs:docs} labels = quality, docs [testenv:pip-audit] description = Run security audit (pip-audit) extras = security commands_pre = pip-audit --version commands = pip-audit {posargs} labels = security [testenv:bandit] description = Run bandit security scanner extras = security commands_pre = bandit --version commands = bandit -r src/ --configfile pyproject.toml --severity-level medium --confidence-level medium {posargs} labels = security [testenv:safety] description = Run safety dependency vulnerability scanner extras = security commands_pre = safety --version commands = safety check --full-report labels = security [testenv:security] description = Run comprehensive security vulnerability scanning extras = security commands_pre = bandit --version safety --version commands = bandit -r src/ --format json --output {envtmpdir}/bandit-report.json --configfile pyproject.toml bandit -r src/ --format txt --configfile pyproject.toml python -c "import json; data = json.load(open('{envtmpdir}/bandit-report.json')); high_issues = [r for r in data['results'] if r['issue_severity'] == 'HIGH']; exit(1 if high_issues else 0)" bash -c "safety check --output json > {envtmpdir}/safety-report.json || true" safety check allowlist_externals = bash labels = security [testenv:licenses] description = Check dependency license compliance extras = security commands_pre = pip-licenses --version commands = pip-licenses --format=plain --order=license pip-licenses --fail-on=GPL;AGPL;Proprietary --ignore-packages=blocklint;CairoSVG;pyenchant;docutils;LinkChecker;Unidecode;djlint;docformatter;refurb;dicttoxml labels = security, compliance [testenv:doctest] description = Test code examples in docstrings and documentation extras = test commands_pre = pytest --version commands = pytest --doctest-modules src/nhl_scrabble/ {posargs} python scripts/test_markdown_examples.py labels = test, docs [testenv:docformatter] description = Format Python docstrings with docformatter skip_install = true deps = docformatter==1.7.7 commands_pre = docformatter --version commands = docformatter --diff --recursive --wrap-summaries 100 --wrap-descriptions 100 src tests {posargs} labels = format, quality [testenv:validate-task-docs] description = Validate task documentation consistency (README, IMPLEMENTATION_SEQUENCE, filesystem) skip_install = true deps = commands_pre = commands = python scripts/validate_task_docs.py labels = quality [testenv:licenses-check] description = Check if LICENSES.md is up-to-date with current dependencies extras = dev commands_pre = pip-licenses --version commands = python scripts/update_licenses.py --check --verbose labels = security, compliance [testenv:licenses-update] description = Update LICENSES.md with current dependency licenses extras = dev commands_pre = pip-licenses --version commands = python scripts/update_licenses.py --update --verbose labels = security, compliance [testenv:ssort] description = Sort Python class members and statements deps = ssort>=0.16 commands_pre = ssort --version commands = ssort --check --diff src/ tests/ labels = quality [testenv:ssort-apply] description = Apply ssort statement sorting deps = {[testenv:ssort]deps} commands_pre = ssort --version commands = ssort src/ tests/ labels = quality [testenv:ty] description = Run type checking (Astral ty - fast Rust-based checker) extras = type commands_pre = ty --version commands = ty check src {posargs} labels = type, quality [testenv:type-check] description = Run all type checkers (ty + mypy comprehensive validation) extras = type commands_pre = ty --version mypy --version commands = ty check src mypy src labels = type, quality, comprehensive [testenv:licenses-summary] description = Generate license summary report extras = security commands_pre = pip-licenses --version commands = pip-licenses --summary --format=plain labels = security, compliance [testenv:py{312,313,314}] description = Run tests with Python {envname} - Tier 3: Tests (depends on tier 2) extras = {[testenv]extras} commands_pre = pytest --version commands = pytest --junitxml=junit-{envname}.xml -v {posargs:tests/} depends = mypy isort interrogate labels = test [testenv:unit] description = Run unit tests only extras = {[testenv]extras} commands_pre = pytest --version commands = pytest -v tests/unit {posargs} labels = test [testenv:integration] description = Run integration tests only extras = {[testenv]extras} commands_pre = pytest --version commands = pytest -v tests/integration -m integration {posargs} labels = test [testenv:benchmark] description = Run performance benchmark tests package = editable extras = test commands_pre = pytest --version commands = pytest tests/benchmarks/ --benchmark-only -n 0 --benchmark-save=latest {posargs} labels = quality, performance [testenv:benchmark-compare] description = Compare benchmark results against baseline package = editable extras = test commands_pre = pytest --version commands = pytest tests/benchmarks/ --benchmark-only -n 0 --benchmark-compare=0001 {posargs} labels = quality, performance [testenv:ruff-format-fix] description = Auto-format code (ruff format + check --fix) extras = lint commands_pre = ruff --version commands = ruff format src tests {posargs} ruff check --fix src tests {posargs} labels = format [testenv:check] description = Run all checks before commit (quality + tests) extras = lint test type commands_pre = ruff --version mypy --version pytest --version commands = ruff format --check src tests ruff check src tests mypy src pytest -v labels = test [testenv:docs] description = Build Sphinx documentation extras = docs change_dir = docs commands_pre = sphinx-build --version commands = sphinx-build -b html . _build/html {posargs} labels = docs [testenv:clean] description = Remove build, test, and coverage artifacts skip_install = true commands_pre = python --version commands = rm -rf build dist .eggs *.egg-info rm -rf .tox .pytest_cache .mypy_cache .ruff_cache htmlcov rm -f .coverage coverage.xml find . -type d -name __pycache__ -exec rm -rf {{}} + find . -type f -name "*.pyc" -delete find . -type f -name "*.pyo" -delete allowlist_externals = find rm labels = util [testenv:build] description = Build source and wheel distributions skip_install = true extras = build commands_pre = python --version python -m build --version commands = python -m build labels = build [testenv:check-wheel] description = Check wheel package contents skip_install = true deps = build check-wheel-contents>=0.6 commands_pre = python --version check-wheel-contents --version commands = python -m build --wheel bash -c "check-wheel-contents dist/*.whl" allowlist_externals = bash labels = package,build [testenv:package] description = Build and validate package (wheel + twine) skip_install = true deps = build check-wheel-contents>=0.6 twine commands_pre = python --version python -m build --version check-wheel-contents --version twine --version commands = python -m build bash -c "check-wheel-contents dist/*.whl" bash -c "twine check dist/*" allowlist_externals = bash labels = package,build [testenv:run] description = Run the NHL Scrabble analyzer package = editable commands_pre = nhl-scrabble --version commands = nhl-scrabble {posargs:analyze} labels = run [testenv:ci] description = Simulate CI/CD pipeline locally deps = absolufy-imports autopep8 black codespell doc8 docformatter==1.7.7 flake8 flake8-pyproject isort mdformat mdformat-black mdformat-gfm mdformat-ruff mdformat-web pymarkdownlnt rstcheck sphinx validate-pyproject[all] yamllint extras = lint security test type commands_pre = ruff --version mypy --version pytest --version pip-audit --version codespell --version pymarkdown version mdformat --version validate-pyproject --version yamllint --version flake8 --version isort --version black --version docformatter --version autopep8 --version doc8 --version rstcheck --version commands = ruff format --check src tests ruff check src tests mypy src pytest --cov=nhl_scrabble --cov-report=term-missing --cov-fail-under=49 pip-audit codespell src tests docs *.md pymarkdown scan *.md docs bash -c 'mdformat --check *.md docs' validate-pyproject pyproject.toml yamllint . flake8 src tests isort --check-only --diff src tests bash -c 'absolufy-imports --application-directories=src src/**/*.py' black --check --diff src tests docformatter --diff --recursive --wrap-summaries 100 --wrap-descriptions 100 src tests autopep8 --diff --recursive src tests doc8 docs rstcheck --recursive docs allowlist_externals = bash labels = ci [testenv:fast] description = Run tests quickly without coverage extras = {[testenv]extras} commands_pre = pytest --version commands = pytest -v -x {posargs:tests/} labels = test [testenv:watch] description = Run tests in watch mode (requires pytest-watch) extras = test watch commands_pre = ptw --version commands = ptw -- -v {posargs} labels = test [testenv:publish-test] description = Publish package to TestPyPI skip_install = true extras = publish commands_pre = twine --version ls -lh dist/ commands = twine upload --repository testpypi dist/* {posargs} allowlist_externals = ls depends = build labels = publish [testenv:publish] description = Publish package to PyPI (use with caution!) skip_install = true extras = publish commands_pre = twine --version ls -lh dist/ commands = twine upload dist/* {posargs} allowlist_externals = ls depends = build labels = publish [testenv:serve-docs] description = Build and serve documentation locally at http://localhost:8000 extras = docs commands_pre = python --version commands = bash -c "cd docs/_build/html && python -m http.server 8000" allowlist_externals = bash depends = docs labels = docs [testenv:version] description = Display current package version package = editable commands_pre = python --version commands = python -c "from nhl_scrabble import __version__; print(f'nhl-scrabble version: {{__version__}}')" labels = util [testenv:codeql] description = Run CodeQL security analysis locally skip_install = true commands = bash -c 'if ! command -v codeql &> /dev/null; then echo "CodeQL CLI not installed. Run: make install-codeql"; exit 1; fi' bash scripts/codeql_local.sh {posargs} allowlist_externals = bash codeql labels = security [testenv:codeql-check] description = Run CodeQL and fail on findings (CI mode) skip_install = true commands = bash -c 'if ! command -v codeql &> /dev/null; then echo "CodeQL CLI not installed"; exit 1; fi' bash scripts/codeql_local.sh --fail-on-findings {posargs} allowlist_externals = bash codeql labels = security [testenv:beautysh] description = Format Bash scripts with beautysh skip_install = true deps = beautysh>=6.2.1 commands = beautysh --indent-size 2 --force-function-style fnpar scripts/*.sh labels = format, shell [testenv:beautysh-check] description = Check Bash script formatting (no changes) skip_install = true deps = beautysh>=6.2.1 commands = beautysh --check --indent-size 2 scripts/*.sh labels = quality, shell [testenv:bashate] description = Lint Bash scripts with bashate skip_install = true deps = bashate>=2.1.1 commands = bashate --ignore=E003,E006 --max-line-length=100 scripts/*.sh labels = lint, quality, shell [testenv:check-bash-docs] description = Check Bash script documentation completeness skip_install = true commands = python scripts/check_bash_docs.py labels = docs, shell [testenv:bash-deps] description = Check Bash script dependencies skip_install = true commands = python scripts/check_bash_deps.py labels = shell