diff --git a/.circleci/config.yml b/.circleci/config.yml index 44bffeafb2..15d3624075 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ jobs: auth: username: $DOCKERHUB_USERNAME password: $DOCKERHUB_PASSWORD - parallelism: 5 + parallelism: 6 steps: - checkout - run: | @@ -19,7 +19,8 @@ jobs: 1) component=components/notifier;; 2) component=components/api_server;; 3) component=components/shared_code;; - 4) component=tests/feature_tests;; + 4) component=tests/application_tests;; + 5) component=tests/feature_tests;; esac cd $component mkdir -p build @@ -36,6 +37,10 @@ jobs: path: components/api_server/build - store_artifacts: path: components/shared_code/build + - store_artifacts: + path: components/application_tests/build + - store_artifacts: + path: components/feature_tests/build unittest_frontend: docker: @@ -65,6 +70,18 @@ jobs: ci/unittest.sh ci/quality.sh + unittest_release: + machine: + image: default + steps: + - checkout + - run: | + cd release + python3 -m venv venv + . venv/bin/activate + ci/pip-install.sh + ci/quality.sh + application_tests: machine: image: default @@ -119,6 +136,8 @@ workflows: context: QualityTime - unittest_docs: context: QualityTime + - unittest_release: + context: QualityTime - docker/hadolint: context: QualityTime dockerfiles: "components/collector/Dockerfile:components/database/Dockerfile:\ diff --git a/.github/workflows/application-tests-quality.yml b/.github/workflows/application-tests-quality.yml new file mode 100644 index 0000000000..0e430c4dfa --- /dev/null +++ b/.github/workflows/application-tests-quality.yml @@ -0,0 +1,27 @@ +name: Application tests quality + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4.1.7 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + cd tests/application_tests + ci/pip-install.sh + - name: Test + run: | + cd tests/application_tests + ci/unittest.sh + - name: Quality + run: | + cd tests/application_tests + ci/quality.sh diff --git a/.github/workflows/release-quality.yml b/.github/workflows/release-quality.yml new file mode 100644 index 0000000000..8b37655c49 --- /dev/null +++ b/.github/workflows/release-quality.yml @@ -0,0 +1,22 @@ +name: Release script quality + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4.1.7 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies and run quality checks + run: | + cd release + python -m venv venv + . venv/bin/activate + ci/pip-install.sh + ci/quality.sh diff --git a/ci/base.sh b/ci/base.sh index 993528b88c..98507213b7 100644 --- a/ci/base.sh +++ b/ci/base.sh @@ -4,7 +4,7 @@ set -e -run () { +run() { # Show the invoked command using a subdued text color so it's clear which tool is running. header='\033[95m' endstyle='\033[0m' @@ -12,7 +12,7 @@ run () { eval "$*" } -spec () { +spec() { # The versions of tools are specified in pyproject.toml. This function calls the spec.py script which in turn # reads the version numbers from the pyproject.toml file. @@ -21,6 +21,13 @@ spec () { python $SCRIPT_DIR/spec.py $* } +run_pipx() { + # Look up the version of the command using the spec function and run the command using pipx. + command=$1 + shift 1 + run pipx run `spec $command` $@ +} + # Don't install tools in the global pipx home folder, but locally for each component: export PIPX_HOME=.pipx export PIPX_BIN_DIR=$PIPX_HOME/bin diff --git a/ci/quality-base.sh b/ci/quality-base.sh new file mode 100644 index 0000000000..07a4deba30 --- /dev/null +++ b/ci/quality-base.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Get the dir of this script so the base.sh script that is in the same dir as this script can be sourced: +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source $SCRIPT_DIR/base.sh + +run_ruff() { + run_pipx ruff check . + run_pipx ruff format --check . +} + +run_fixit() { + run_pipx fixit lint ${@:-src tests} +} + +run_mypy() { + run_pipx mypy --python-executable=$(which python) ${@:-src tests} +} + +run_mypy_pydantic() { + # To use the pydantic plugin, we need to first install mypy and then inject the plugin + run pipx install --force `spec mypy` # --force works around this bug: https://github.com/pypa/pipx/issues/795 + run pipx inject mypy `spec pydantic` + run $PIPX_BIN_DIR/mypy src --python-executable=$(which python) +} + +run_pyproject_fmt() { + run_pipx pyproject-fmt --check pyproject.toml +} + +run_bandit() { + run_pipx bandit --configfile pyproject.toml --quiet --recursive src +} + +run_pip_audit() { + run_pipx pip-audit --strict --progress-spinner=off ${@:--r requirements/requirements.txt -r requirements/requirements-dev.txt} +} + +run_vulture() { + run_pipx vulture --min-confidence 0 ${@:-src tests} .vulture_ignore_list.py +} + +run_vale() { + run_pipx vale sync + run_pipx vale --no-wrap src/*.md +} + +run_markdownlint() { + run ./node_modules/markdownlint-cli/markdownlint.js src/*.md +} diff --git a/ci/unittest-base.sh b/ci/unittest-base.sh index e8bd57ca43..4d1917d425 100644 --- a/ci/unittest-base.sh +++ b/ci/unittest-base.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Get the dir of this script so the vbase.sh script that is in the same dir as this script can be sourced: +# Get the dir of this script so the base.sh script that is in the same dir as this script can be sourced: SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) source $SCRIPT_DIR/base.sh diff --git a/components/api_server/ci/quality.sh b/components/api_server/ci/quality.sh index 12a07f1a48..3623d1a66c 100755 --- a/components/api_server/ci/quality.sh +++ b/components/api_server/ci/quality.sh @@ -1,31 +1,11 @@ #!/bin/bash -source ../../ci/base.sh - -# Ruff -run pipx run `spec ruff` check . -run pipx run `spec ruff` format --check . - -# Fixit -run pipx run `spec fixit` lint src tests - -# Mypy -run pipx run `spec mypy` --python-executable=$(which python) src - -# pip-audit -run pipx run `spec pip-audit` --strict --progress-spinner=off -r requirements/requirements.txt -r requirements/requirements-dev.txt - -# Safety -# Vulnerability ID: 67599 -# ADVISORY: ** DISPUTED ** An issue was discovered in pip (all versions) because it installs the version with the -# highest version number, even if the user had intended to obtain a private package from a private index. This only -# affects use of the --extra-index-url option, and exploitation requires that the... -# CVE-2018-20225 -# For more information about this vulnerability, visit https://data.safetycli.com/v/67599/97c -run pipx run `spec safety` check --bare --ignore 67599 -r requirements/requirements.txt -r requirements/requirements-dev.txt - -# Bandit -run pipx run `spec bandit` --quiet --recursive src/ - -# Vulture -run pipx run `spec vulture` --min-confidence 0 src/ tests/ .vulture_ignore_list.py +source ../../ci/quality-base.sh + +run_ruff +run_fixit +run_mypy +run_pyproject_fmt +run_pip_audit +run_bandit +run_vulture diff --git a/components/api_server/pyproject.toml b/components/api_server/pyproject.toml index cdb611f7dd..c30d654d97 100644 --- a/components/api_server/pyproject.toml +++ b/components/api_server/pyproject.toml @@ -1,38 +1,107 @@ [project] -name = "api_server" +name = "api-server" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "bottle==0.12.25", "cryptography==42.0.8", "gevent==24.2.1", "ldap3==2.9.1", - "lxml[html_clean]==5.2.2", + "lxml[html-clean]==5.2.2", "pymongo==4.7.3", - "requests==2.32.3" + "requests==2.32.3", ] - -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "coverage==7.5.3", "pip==24.0", + "pip-tools==7.4.1", # To add hashes to requirements "pipx==1.6.0", - "pip-tools==7.4.1", # To add hashes to requirements - "pydantic==2.7.4", # Needed for importing the data model in the tests from the shared code component + "pydantic==2.7.4", # Needed for importing the data model in the tests from the shared code component "types-cryptography==3.3.23.2", "types-ldap3==2.9.13.20240205", "types-requests==2.32.0.20240602", - "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io + "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io ] -tools = [ +optional-dependencies.tools = [ "bandit==1.7.9", "fixit==2.1.0", "mypy==1.10.0", "pip-audit==2.7.3", + "pyproject-fmt==2.1.3", "ruff==0.4.8", - "safety==3.2.3", - "vulture==2.11" + "vulture==2.11", ] +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", +] +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - too many untyped arguments atm to turn this rule on + "ANN002", # https://docs.astral.sh/ruff/rules/missing-type-args/ - leads to false positives for super().__init__(*args, **kwargs) + "ANN003", # https://docs.astral.sh/ruff/rules/missing-type-kwargs/ - leads to false positives for super().__init__(*args, **kwargs) + "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous + "ANN102", # https://docs.astral.sh/ruff/rules/missing-type-cls/ - type checkers can infer the type of `cls`, so annotating it is superfluous + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - too many untyped return values atm to turn this rule on + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ - requiring __init__() methods to have docstrings seems a bit much + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" + "I001", # https://docs.astral.sh/ruff/rules/unsorted-imports/ - (probably) because ruff is run with pipx it can't differentiate between dependencies and modules + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter + "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", +] +lint.per-file-ignores."__init__.py" = [ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files + "F401", # https://docs.astral.sh/ruff/rules/unused-import/ - routes are imported in __init__.py files to flatten the module hierarchy +] +lint.per-file-ignores."src/model/issue_tracker.py" = [ + "BLE001", # https://docs.astral.sh/ruff/rules/blind-except/ - allow for catching blind exception `Exception` +] +lint.per-file-ignores."src/quality_time_server.py" = [ + "E402", + "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is the main script +] +lint.per-file-ignores."tests/**/*.py" = [ + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types + "S105", # https://docs.astral.sh/ruff/rules/hardcoded-password-string/ - hardcoded passwords in test code are test data + "S106", # https://docs.astral.sh/ruff/rules/hardcoded-password-func-arg/ - hardcoded passwords in test code are test data +] +lint.isort.section-order = [ + "future", + "standard-library", + "third-party", + "second-party", + "first-party", + "tests", + "local-folder", +] +lint.isort.sections."second-party" = [ + "shared", + "shared_data_model", +] +lint.isort.sections.tests = [ + "tests", +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? + [tool.mypy] ignore_missing_imports = false incremental = false @@ -40,14 +109,14 @@ warn_redundant_casts = true warn_return_any = true warn_unreachable = true warn_unused_ignores = true -disable_error_code = "valid-type" # mypy does not yet support PEP 695, Type Parameter Syntax. See https://github.com/python/mypy/issues/15238 +disable_error_code = "valid-type" # mypy does not yet support PEP 695, Type Parameter Syntax. See https://github.com/python/mypy/issues/15238 [[tool.mypy.overrides]] module = [ "bottle", "gevent", "lxml.html", - "lxml.html.clean" + "lxml.html.clean", ] ignore_missing_imports = true @@ -55,56 +124,5 @@ ignore_missing_imports = true allow_unsafe = true generate_hashes = true quiet = true -strip_extras = false # Needed for lxml[html-clean] +strip_extras = false # Needed for lxml[html-clean] upgrade = true - -[tool.ruff] -target-version = "py312" -line-length = 120 -src = ["src"] - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - too many untyped arguments atm to turn this rule on - "ANN002", # https://docs.astral.sh/ruff/rules/missing-type-args/ - leads to false positives for super().__init__(*args, **kwargs) - "ANN003", # https://docs.astral.sh/ruff/rules/missing-type-kwargs/ - leads to false positives for super().__init__(*args, **kwargs) - "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous - "ANN102", # https://docs.astral.sh/ruff/rules/missing-type-cls/ - type checkers can infer the type of `cls`, so annotating it is superfluous - "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - too many untyped return values atm to turn this rule on - "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter - "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ - requiring __init__() methods to have docstrings seems a bit much - "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` - "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` - "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" - "I001", # https://docs.astral.sh/ruff/rules/unsorted-imports/ - (probably) because ruff is run with pipx it can't differentiate between dependencies and modules - "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter - "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used -] - -[tool.ruff.lint.isort] -section-order = ["future", "standard-library", "third-party", "second-party", "first-party", "tests", "local-folder"] - -[tool.ruff.lint.isort.sections] -"second-party" = ["shared", "shared_data_model"] -"tests" = ["tests"] - -[tool.ruff.lint.per-file-ignores] -".vulture_ignore_list.py" = ["ALL"] -"__init__.py" = [ - "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files - "F401", # https://docs.astral.sh/ruff/rules/unused-import/ - routes are imported in __init__.py files to flatten the module hierarchy -] -"src/quality_time_server.py" = [ - "E402", - "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is the main script -] -"src/model/issue_tracker.py" = [ - "BLE001" # https://docs.astral.sh/ruff/rules/blind-except/ - allow for catching blind exception `Exception` -] -"tests/**/*.py" = [ - "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types - "S105", # https://docs.astral.sh/ruff/rules/hardcoded-password-string/ - hardcoded passwords in test code are test data - "S106", # https://docs.astral.sh/ruff/rules/hardcoded-password-func-arg/ - hardcoded passwords in test code are test data -] diff --git a/components/api_server/tests/base.py b/components/api_server/tests/base.py index 2f778b1125..d35acb217c 100644 --- a/components/api_server/tests/base.py +++ b/components/api_server/tests/base.py @@ -6,6 +6,7 @@ import unittest from collections.abc import Callable from unittest.mock import Mock +from typing import ClassVar, cast from shared_data_model import DATA_MODEL_JSON @@ -21,6 +22,8 @@ def setUp(self): class DataModelTestCase(DatabaseTestCase): """Base class for unit tests that use the data model.""" + DATA_MODEL: ClassVar[dict] = {} + @classmethod def setUpClass(cls) -> None: """Override to set up the data model.""" @@ -34,7 +37,7 @@ def setUp(self): @staticmethod def load_data_model() -> dict: """Load the data model from the JSON dump.""" - data_model = json.loads(DATA_MODEL_JSON) + data_model = cast(dict, json.loads(DATA_MODEL_JSON)) data_model["_id"] = "id" data_model["timestamp"] = "now" return data_model diff --git a/components/api_server/tests/initialization/test_migrations.py b/components/api_server/tests/initialization/test_migrations.py index ff032882a2..d7657848bc 100644 --- a/components/api_server/tests/initialization/test_migrations.py +++ b/components/api_server/tests/initialization/test_migrations.py @@ -9,13 +9,27 @@ class MigrationTestCase(DataModelTestCase): """Base class for migration unit tests.""" - def existing_report(self, metric_type: str): + def existing_report( + self, + *, + metric_type: str, + metric_name: str = "", + metric_unit: str = "", + sources: dict[SourceId, dict[str, str | dict[str, str]]] | None = None, + ): """Return a report fixture. To be extended in subclasses.""" - return { + report: dict = { "_id": "id", "report_uuid": REPORT_ID, "subjects": {SUBJECT_ID: {"type": "software", "metrics": {METRIC_ID: {"type": metric_type}}}}, } + if metric_name: + report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["name"] = metric_name + if metric_unit: + report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["unit"] = metric_unit + if sources: + report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["sources"] = sources + return report def inserted_report(self, **kwargs): """Return a report as it is expected to have been inserted into the reports collection. @@ -38,7 +52,7 @@ def test_no_reports(self): def test_empty_reports(self): """Test that the migration succeeds when the report does not have anything to migrate.""" - self.database.reports.find.return_value = [self.existing_report("issues")] + self.database.reports.find.return_value = [self.existing_report(metric_type="issues")] perform_migrations(self.database) self.database.reports.replace_one.assert_not_called() @@ -46,23 +60,6 @@ def test_empty_reports(self): class ChangeAccessibilityViolationsTest(MigrationTestCase): """Unit tests for the accessibility violations database migration.""" - def existing_report( - self, - *, - metric_type: str = "accessibility", - metric_name: str = "", - metric_unit: str = "", - extra_metrics: bool = False, - ): - """Extend to add name and unit to the metric and optional extra metrics.""" - report = super().existing_report(metric_type=metric_type) - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["name"] = metric_name - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["unit"] = metric_unit - if extra_metrics: - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID2] = {"type": "violations"} - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID3] = {"type": "security_warnings"} - return report - def inserted_report( self, metric_name: str = "Accessibility violations", metric_unit: str = "accessibility violations", **kwargs ): @@ -79,13 +76,15 @@ def test_report_without_accessibility_metrics(self): def test_report_with_accessibility_metric(self): """Test that the migration succeeds with an accessibility metric.""" - self.database.reports.find.return_value = [self.existing_report()] + self.database.reports.find.return_value = [self.existing_report(metric_type="accessibility")] perform_migrations(self.database) self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, self.inserted_report()) def test_accessibility_metric_with_name_and_unit(self): """Test that the migration succeeds with an accessibility metric, and existing name and unit are kept.""" - self.database.reports.find.return_value = [self.existing_report(metric_name="name", metric_unit="unit")] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="accessibility", metric_name="name", metric_unit="unit"), + ] perform_migrations(self.database) self.database.reports.replace_one.assert_called_once_with( {"_id": "id"}, @@ -94,36 +93,33 @@ def test_accessibility_metric_with_name_and_unit(self): def test_report_with_accessibility_metric_and_other_types(self): """Test that the migration succeeds with an accessibility metric and other metric types.""" - self.database.reports.find.return_value = [self.existing_report(extra_metrics=True)] + report = self.existing_report(metric_type="accessibility") + report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID2] = {"type": "violations"} + report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID3] = {"type": "security_warnings"} + self.database.reports.find.return_value = [report] perform_migrations(self.database) - self.database.reports.replace_one.assert_called_once_with( - {"_id": "id"}, - self.inserted_report(extra_metrics=True), - ) + inserted_report = self.inserted_report() + inserted_report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID2] = {"type": "violations"} + inserted_report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID3] = {"type": "security_warnings"} + self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) class BranchParameterTest(MigrationTestCase): """Unit tests for the branch parameter database migration.""" - def existing_report( - self, metric_type: str = "loc", sources: dict[SourceId, dict[str, str | dict[str, str]]] | None = None - ): - """Extend to add sources and an extra metric without sources.""" - report = super().existing_report(metric_type=metric_type) - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID2] = {"type": "issues"} - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["sources"] = sources or {} - return report - def test_report_without_branch_parameter(self): """Test that the migration succeeds with reports, but without metrics with a branch parameter.""" - self.database.reports.find.return_value = [self.existing_report()] + self.database.reports.find.return_value = [self.existing_report(metric_type="loc")] perform_migrations(self.database) self.database.reports.replace_one.assert_not_called() def test_report_with_non_empty_branch_parameter(self): """Test that the migration succeeds when the branch parameter is not empty.""" self.database.reports.find.return_value = [ - self.existing_report(sources={SOURCE_ID: {"type": "sonarqube", "parameters": {"branch": "main"}}}) + self.existing_report( + metric_type="loc", + sources={SOURCE_ID: {"type": "sonarqube", "parameters": {"branch": "main"}}}, + ) ] perform_migrations(self.database) self.database.reports.replace_one.assert_not_called() @@ -148,32 +144,28 @@ def test_report_with_branch_parameter_without_value(self): class SourceParameterHashMigrationTest(MigrationTestCase): """Unit tests for the source parameter hash database migration.""" - def existing_report(self, sources: dict[SourceId, dict[str, str | dict[str, str]]] | None = None): - """Extend to add sources and an extra metric without sources.""" - report = super().existing_report(metric_type="loc") - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID2] = {"type": "issues"} - if sources: - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["sources"] = sources - return report - def test_report_with_sources_without_source_parameter_hash(self): """Test a report with sources and measurements.""" self.database.measurements.find_one.return_value = {"_id": "id", "metric_uuid": METRIC_ID} - self.database.reports.find.return_value = [self.existing_report(sources={SOURCE_ID: {"type": "cloc"}})] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="loc", sources={SOURCE_ID: {"type": "cloc"}}) + ] perform_migrations(self.database) inserted_measurement = {"metric_uuid": METRIC_ID, "source_parameter_hash": "8c3b464958e9ad0f20fb2e3b74c80519"} self.database.measurements.replace_one.assert_called_once_with({"_id": "id"}, inserted_measurement) def test_report_without_sources(self): """Test a report without sources.""" - self.database.reports.find.return_value = [self.existing_report()] + self.database.reports.find.return_value = [self.existing_report(metric_type="loc")] perform_migrations(self.database) self.database.measurements.replace_one.assert_not_called() def test_metric_without_measurement(self): """Test a metric without measurements.""" self.database.measurements.find_one.return_value = None - self.database.reports.find.return_value = [self.existing_report(sources={SOURCE_ID: {"type": "cloc"}})] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="loc", sources={SOURCE_ID: {"type": "cloc"}}) + ] perform_migrations(self.database) self.database.measurements.replace_one.assert_not_called() @@ -181,17 +173,6 @@ def test_metric_without_measurement(self): class CIEnvironmentTest(MigrationTestCase): """Unit tests for the CI-environment subject type database migration.""" - def existing_report(self, subject_type: str = "", subject_name: str = "", subject_description: str = ""): - """Extend to set the subject type to CI-environment.""" - report = super().existing_report(metric_type="issues") - if subject_type: - report["subjects"][SUBJECT_ID]["type"] = subject_type - if subject_name: - report["subjects"][SUBJECT_ID]["name"] = subject_name - if subject_description: - report["subjects"][SUBJECT_ID]["description"] = subject_description - return report - def inserted_report(self, **kwargs): """Extend to set the subject type to development environment.""" report = super().inserted_report(**kwargs) @@ -200,72 +181,79 @@ def inserted_report(self, **kwargs): def test_report_without_ci_environment(self): """Test that the migration succeeds without CI-environment subject.""" - self.database.reports.find.return_value = [self.existing_report()] + self.database.reports.find.return_value = [self.existing_report(metric_type="failed_jobs")] perform_migrations(self.database) self.database.reports.replace_one.assert_not_called() def test_report_with_ci_environment(self): """Test that the migration succeeds with CI-environment subject.""" - self.database.reports.find.return_value = [self.existing_report(subject_type="ci")] + report = self.existing_report(metric_type="failed_jobs") + report["subjects"][SUBJECT_ID]["type"] = "ci" + self.database.reports.find.return_value = [report] perform_migrations(self.database) - inserted_report = self.inserted_report( - subject_name="CI-environment", - subject_description="A continuous integration environment.", - ) + inserted_report = self.inserted_report(metric_type="failed_jobs") + inserted_report["subjects"][SUBJECT_ID]["name"] = "CI-environment" + inserted_report["subjects"][SUBJECT_ID]["description"] = "A continuous integration environment." self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) def test_ci_environment_with_title_and_subtitle(self): """Test that the migration succeeds with an CI-environment subject, and existing title and subtitle are kept.""" - self.database.reports.find.return_value = [ - self.existing_report(subject_type="ci", subject_name="CI", subject_description="My CI") - ] + report = self.existing_report(metric_type="failed_jobs") + report["subjects"][SUBJECT_ID]["type"] = "ci" + report["subjects"][SUBJECT_ID]["name"] = "CI" + report["subjects"][SUBJECT_ID]["description"] = "My CI" + self.database.reports.find.return_value = [report] perform_migrations(self.database) - inserted_report = self.inserted_report(subject_name="CI", subject_description="My CI") + inserted_report = self.inserted_report(metric_type="failed_jobs") + inserted_report["subjects"][SUBJECT_ID]["name"] = "CI" + inserted_report["subjects"][SUBJECT_ID]["description"] = "My CI" self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) class SonarQubeParameterTest(MigrationTestCase): """Unit tests for the SonarQube parameter database migration.""" - def existing_report( - self, - metric_type: str = "violations", - sources: dict[SourceId, dict[str, str | dict[str, str | list[str]]]] | None = None, - ): - """Extend to add sources.""" - report = super().existing_report(metric_type=metric_type) - report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["sources"] = sources - return report - def sources(self, source_type: str = "sonarqube", **parameters): """Create the sources fixture.""" return {SOURCE_ID: {"type": source_type, "parameters": {"branch": "main", **parameters}}} def test_report_without_severity_or_types_parameter(self): """Test that the migration succeeds when the SonarQube source has no severity or types parameter.""" - self.database.reports.find.return_value = [self.existing_report(sources=self.sources())] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="violations", sources=self.sources()), + ] perform_migrations(self.database) self.database.reports.replace_one.assert_not_called() def test_report_with_violation_metric_but_no_sonarqube(self): """Test that the migration succeeds when a violations metric has no SonarQube sources.""" - self.database.reports.find.return_value = [self.existing_report(sources=self.sources("sarif"))] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="violations", sources=self.sources("sarif")), + ] perform_migrations(self.database) self.database.reports.replace_one.assert_not_called() def test_report_with_severity_parameter(self): """Test that the migration succeeds when the SonarQube source has a severity parameter.""" - self.database.reports.find.return_value = [self.existing_report(sources=self.sources(severities=["info"]))] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="violations", sources=self.sources(severities=["info"])), + ] perform_migrations(self.database) - inserted_report = self.inserted_report(sources=self.sources(impact_severities=["low"])) + inserted_report = self.inserted_report( + metric_type="violations", + sources=self.sources(impact_severities=["low"]), + ) self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) def test_report_with_multiple_old_severity_values_that_map_to_the_same_new_value(self): """Test a severity parameter with multiple old values that map to the same new value.""" - reports = [self.existing_report(sources=self.sources(severities=["info", "minor"]))] + reports = [self.existing_report(metric_type="violations", sources=self.sources(severities=["info", "minor"]))] self.database.reports.find.return_value = reports perform_migrations(self.database) - inserted_report = self.inserted_report(sources=self.sources(impact_severities=["low"])) + inserted_report = self.inserted_report( + metric_type="violations", + sources=self.sources(impact_severities=["low"]), + ) self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) @disable_logging @@ -273,25 +261,32 @@ def test_report_with_unknown_old_severity_values(self): """Test that unknown severity parameter values are ignored.""" sources = self.sources(severities=["info", ""]) sources[SOURCE_ID2] = {"type": "sonarqube", "parameters": {"branch": "main", "severities": ["foo"]}} - self.database.reports.find.return_value = [self.existing_report(sources=sources)] + self.database.reports.find.return_value = [self.existing_report(metric_type="violations", sources=sources)] perform_migrations(self.database) inserted_sources = self.sources(impact_severities=["low"]) inserted_sources[SOURCE_ID2] = {"type": "sonarqube", "parameters": {"branch": "main"}} - inserted_report = self.inserted_report(sources=inserted_sources) + inserted_report = self.inserted_report(metric_type="violations", sources=inserted_sources) self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) def test_report_with_types_parameter(self): """Test that the migration succeeds when the SonarQube source has a types parameter.""" - self.database.reports.find.return_value = [self.existing_report(sources=self.sources(types=["bug"]))] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="violations", sources=self.sources(types=["bug"])), + ] perform_migrations(self.database) - inserted_report = self.inserted_report(sources=self.sources(impacted_software_qualities=["reliability"])) + inserted_report = self.inserted_report( + metric_type="violations", + sources=self.sources(impacted_software_qualities=["reliability"]), + ) self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) def test_report_with_types_parameter_without_values(self): """Test that the migration succeeds when the SonarQube source has a types parameter without values.""" - self.database.reports.find.return_value = [self.existing_report(sources=self.sources(types=[]))] + self.database.reports.find.return_value = [ + self.existing_report(metric_type="violations", sources=self.sources(types=[])), + ] perform_migrations(self.database) - inserted_report = self.inserted_report(sources=self.sources()) + inserted_report = self.inserted_report(metric_type="violations", sources=self.sources()) self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report) def test_report_with_security_types_parameter(self): diff --git a/components/api_server/tests/routes/plugins/test_route_auth_plugin.py b/components/api_server/tests/routes/plugins/test_route_auth_plugin.py index 2f140cf3a8..39e5d3574a 100644 --- a/components/api_server/tests/routes/plugins/test_route_auth_plugin.py +++ b/components/api_server/tests/routes/plugins/test_route_auth_plugin.py @@ -37,7 +37,7 @@ def tearDown(self): logging.disable(logging.NOTSET) @staticmethod - def route(database: Mock, user: User | None = None) -> tuple[Mock, User]: + def route(database: Mock, user: User | None = None) -> tuple[Mock, User | None]: """Route handler with injected parameters. Returns the parameters for test purposes.""" return database, user diff --git a/components/api_server/tests/routes/test_measurement.py b/components/api_server/tests/routes/test_measurement.py index 1e7a6d3cf5..275582a9d5 100644 --- a/components/api_server/tests/routes/test_measurement.py +++ b/components/api_server/tests/routes/test_measurement.py @@ -1,6 +1,7 @@ """Unit tests for the measurement routes.""" from datetime import timedelta +from typing import cast from unittest.mock import Mock, patch from shared.model.measurement import Measurement @@ -117,7 +118,7 @@ def insert_one(new_measurement) -> None: def set_entity_attribute(self, attribute: str = "attribute", value: str = "value") -> Measurement: """Set the entity attribute and return the new measurement.""" with patch("bottle.request", Mock(json={attribute: value})): - return set_entity_attribute(METRIC_ID, SOURCE_ID, "entity_key", attribute, self.database) + return cast(Measurement, set_entity_attribute(METRIC_ID, SOURCE_ID, "entity_key", attribute, self.database)) def test_set_attribute(self): """Test that setting an attribute inserts a new measurement.""" diff --git a/components/collector/ci/quality.sh b/components/collector/ci/quality.sh index 8933d67612..ec76d4c56f 100755 --- a/components/collector/ci/quality.sh +++ b/components/collector/ci/quality.sh @@ -1,33 +1,12 @@ #!/bin/bash -source ../../ci/base.sh - -# Ruff -run pipx run `spec ruff` check . -run pipx run `spec ruff` format --check . - -# Fixit -run pipx run `spec fixit` lint src tests - -# Mypy -run pipx run `spec mypy` --python-executable=$(which python) src tests - -# pip-audit -# See https://github.com/aio-libs/aiohttp/issues/6772 for why we ignore the CVE -run pipx run `spec pip-audit` --strict --progress-spinner=off -r requirements/requirements.txt -r requirements/requirements-dev.txt - -# Safety -# Vulnerability ID: 67599 -# ADVISORY: ** DISPUTED ** An issue was discovered in pip (all versions) because it installs the version with the -# highest version number, even if the user had intended to obtain a private package from a private index. This only -# affects use of the --extra-index-url option, and exploitation requires that the... -# CVE-2018-20225 -# For more information about this vulnerability, visit https://data.safetycli.com/v/67599/97c -run pipx run `spec safety` check --bare --ignore 67599 -r requirements/requirements.txt -r requirements/requirements-dev.txt - -# Bandit -run pipx run `spec bandit` --quiet --recursive src/ - -# Vulture +source ../../ci/quality-base.sh + +run_ruff +run_fixit +run_mypy +run_pyproject_fmt +run_pip_audit +run_bandit NAMES_TO_IGNORE='Anchore*,Axe*,AzureDevops*,Bandit*,Calendar*,CargoAudit*,Cloc*,Cobertura*,Composer*,CxSAST*,DependencyTrack*,Gatling*,Generic*,GitLab*,Harbor*,Jacoco*,Jenkins*,Jira*,JMeter*,JUnit*,ManualNumber*,NCover*,Npm*,OJAudit*,OpenVAS*,OWASPDependencyCheck*,OWASPZAP*,PerformanceTestRunner*,Pip*,PyupioSafety*,QualityTime*,RobotFramework*,SARIF*,Snyk*,SonarQube*,Trello*,TrivyJSON*' -run pipx run `spec vulture` --min-confidence 0 --ignore-names $NAMES_TO_IGNORE src/ tests/ .vulture_ignore_list.py +run_vulture --ignore-names $NAMES_TO_IGNORE src tests diff --git a/components/collector/pyproject.toml b/components/collector/pyproject.toml index b15d759577..13959a0f67 100644 --- a/components/collector/pyproject.toml +++ b/components/collector/pyproject.toml @@ -1,6 +1,11 @@ [project] name = "collector" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "aiogqlc==5.1.0", "aiohttp==3.9.5", @@ -11,28 +16,91 @@ dependencies = [ "python-dateutil==2.9.0.post0", ] -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "coverage==7.5.3", "mongomock==4.1.2", "pip==24.0", + "pip-tools==7.4.1", # To add hashes to requirements "pipx==1.6.0", - "pip-tools==7.4.1", # To add hashes to requirements - "pydantic==2.7.4", # Needed for importing the data model in the tests from the shared code component + "pydantic==2.7.4", # Needed for importing the data model in the tests from the shared code component "types-beautifulsoup4==4.12.0.20240511", "types-python-dateutil==2.9.0.20240316", - "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io + "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io ] -tools = [ +optional-dependencies.tools = [ "bandit==1.7.9", "fixit==2.1.0", "mypy==1.10.0", "pip-audit==2.7.3", + "pyproject-fmt==2.1.3", "ruff==0.4.8", - "safety==3.2.3", - "vulture==2.11" + "vulture==2.11", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", ] +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - too many untyped arguments atm to turn this rule on + "ANN002", # https://docs.astral.sh/ruff/rules/missing-type-args/ - leads to false positives for super().__init__(*args, **kwargs) + "ANN003", # https://docs.astral.sh/ruff/rules/missing-type-kwargs/ - leads to false positives for super().__init__(*args, **kwargs) + "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous + "ANN102", # https://docs.astral.sh/ruff/rules/missing-type-cls/ - type checkers can infer the type of `cls`, so annotating it is superfluous + "ANN204", # https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ - typing classes that inherit from set and list correctly is surprisingly hard + "ARG002", # https://docs.astral.sh/ruff/rules/unused-method-argument/ - this rule doesn't take inheritance into account + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ - requiring __init__() methods to have docstrings seems a bit much + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter + "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", +] +lint.per-file-ignores."__init__.py" = [ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files + "F401", # https://docs.astral.sh/ruff/rules/unused-import/ - collectors are imported in __init__.py files to flatten the module hierarchy +] +lint.per-file-ignores."src/base_collectors/source_collector.py" = [ + "BLE001", # https://docs.astral.sh/ruff/rules/blind-except/ - allow for catching blind exception `Exception` +] +lint.per-file-ignores."src/quality_time_collector.py" = [ + "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is the main script +] +lint.per-file-ignores."tests/**/*.py" = [ + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types +] +lint.isort.section-order = [ + "future", + "standard-library", + "third-party", + "second-party", + "first-party", + "tests", + "local-folder", +] +lint.isort.sections."second-party" = [ + "shared", + "shared_data_model", +] +lint.isort.sections."tests" = [ + "tests", +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? + [tool.mypy] ignore_missing_imports = false incremental = false @@ -43,7 +111,7 @@ warn_unused_ignores = true [[tool.mypy.overrides]] module = [ - "defusedxml" + "defusedxml", ] ignore_missing_imports = true @@ -53,51 +121,3 @@ generate_hashes = true quiet = true strip_extras = true upgrade = true - -[tool.ruff] -target-version = "py312" -line-length = 120 -src = ["src"] - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - too many untyped arguments atm to turn this rule on - "ANN002", # https://docs.astral.sh/ruff/rules/missing-type-args/ - leads to false positives for super().__init__(*args, **kwargs) - "ANN003", # https://docs.astral.sh/ruff/rules/missing-type-kwargs/ - leads to false positives for super().__init__(*args, **kwargs) - "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous - "ANN102", # https://docs.astral.sh/ruff/rules/missing-type-cls/ - type checkers can infer the type of `cls`, so annotating it is superfluous - "ANN204", # https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ - typing classes that inherit from set and list correctly is surprisingly hard - "ARG002", # https://docs.astral.sh/ruff/rules/unused-method-argument/ - this rule doesn't take inheritance into account - "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter - "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ - requiring __init__() methods to have docstrings seems a bit much - "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` - "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` - "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" - "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter - "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used -] - -[tool.ruff.lint.isort] -section-order = ["future", "standard-library", "third-party", "second-party", "first-party", "tests", "local-folder"] - -[tool.ruff.lint.isort.sections] -"second-party" = ["shared", "shared_data_model"] -"tests" = ["tests"] - -[tool.ruff.lint.per-file-ignores] -".vulture_ignore_list.py" = ["ALL"] -"__init__.py" = [ - "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files - "F401", # https://docs.astral.sh/ruff/rules/unused-import/ - collectors are imported in __init__.py files to flatten the module hierarchy -] -"src/base_collectors/source_collector.py" = [ - "BLE001" # https://docs.astral.sh/ruff/rules/blind-except/ - allow for catching blind exception `Exception` -] -"src/quality_time_collector.py" = [ - "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is the main script -] -"tests/**/*.py" = [ - "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types -] diff --git a/components/frontend/ci/quality.sh b/components/frontend/ci/quality.sh index db07ac2c9e..ea419966c0 100755 --- a/components/frontend/ci/quality.sh +++ b/components/frontend/ci/quality.sh @@ -1,4 +1,6 @@ #!/bin/bash +source ../../ci/quality-base.sh + # Eslint -npx eslint *.js *.mjs src +run npx eslint *.js *.mjs src diff --git a/components/notifier/ci/quality.sh b/components/notifier/ci/quality.sh index e252158282..3623d1a66c 100755 --- a/components/notifier/ci/quality.sh +++ b/components/notifier/ci/quality.sh @@ -1,32 +1,11 @@ #!/bin/bash -source ../../ci/base.sh - -# Ruff -run pipx run `spec ruff` check . -run pipx run `spec ruff` format --check . - -# Fixit -run pipx run `spec fixit` lint src tests - -# Mypy -run pipx run `spec mypy` --python-executable=$(which python) src - -# pip-audit -# See https://github.com/aio-libs/aiohttp/issues/6772 for why we ignore the CVE -run pipx run `spec pip-audit` --strict --progress-spinner=off -r requirements/requirements.txt -r requirements/requirements-dev.txt - -# Safety -# Vulnerability ID: 67599 -# ADVISORY: ** DISPUTED ** An issue was discovered in pip (all versions) because it installs the version with the -# highest version number, even if the user had intended to obtain a private package from a private index. This only -# affects use of the --extra-index-url option, and exploitation requires that the... -# CVE-2018-20225 -# For more information about this vulnerability, visit https://data.safetycli.com/v/67599/97c -run pipx run `spec safety` check --bare --ignore 67599 -r requirements/requirements.txt -r requirements/requirements-dev.txt - -# Bandit -run pipx run `spec bandit` --quiet --recursive src/ - -# Vulture -run pipx run `spec vulture` --min-confidence 0 src/ tests/ .vulture_ignore_list.py +source ../../ci/quality-base.sh + +run_ruff +run_fixit +run_mypy +run_pyproject_fmt +run_pip_audit +run_bandit +run_vulture diff --git a/components/notifier/pyproject.toml b/components/notifier/pyproject.toml index ffee5867fc..a1da6dabf9 100644 --- a/components/notifier/pyproject.toml +++ b/components/notifier/pyproject.toml @@ -1,30 +1,85 @@ [project] name = "notifier" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "aiohttp==3.9.5", - "pymsteams==0.2.2" + "pymsteams==0.2.2", ] -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "coverage==7.5.3", "mongomock==4.1.2", "pip==24.0", + "pip-tools==7.4.1", # To add hashes to requirements "pipx==1.6.0", - "pip-tools==7.4.1", # To add hashes to requirements - "pydantic==2.7.4", # Needed for importing the data model in the tests from the shared code component - "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io + "pydantic==2.7.4", # Needed for importing the data model in the tests from the shared code component + "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io ] -tools = [ +optional-dependencies.tools = [ "bandit==1.7.9", "fixit==2.1.0", "mypy==1.10.0", "pip-audit==2.7.3", + "pyproject-fmt==2.1.3", "ruff==0.4.8", - "safety==3.2.3", - "vulture==2.11" + "vulture==2.11", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", +] +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", +] +lint.per-file-ignores."__init__.py" = [ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files +] +lint.per-file-ignores."src/quality_time_notifier.py" = [ + "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is the main script +] +lint.per-file-ignores."tests/**/*.py" = [ + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types +] +lint.isort.section-order = [ + "future", + "standard-library", + "third-party", + "second-party", + "first-party", + "tests", + "local-folder", +] +lint.isort.sections."second-party" = [ + "shared", + "shared_data_model", ] +lint.isort.sections."tests" = [ + "tests", +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? [tool.mypy] ignore_missing_imports = false @@ -36,7 +91,7 @@ warn_unused_ignores = true [[tool.mypy.overrides]] module = [ - "pymsteams" + "pymsteams", ] ignore_missing_imports = true @@ -46,39 +101,3 @@ generate_hashes = true quiet = true strip_extras = true upgrade = true - -[tool.ruff] -target-version = "py312" -line-length = 120 -src = ["src"] - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous - "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter - "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` - "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` - "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" - "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used -] - -[tool.ruff.lint.isort] -section-order = ["future", "standard-library", "third-party", "second-party", "first-party", "tests", "local-folder"] - -[tool.ruff.lint.isort.sections] -"second-party" = ["shared", "shared_data_model"] -"tests" = ["tests"] - -[tool.ruff.lint.per-file-ignores] -".vulture_ignore_list.py" = ["ALL"] -"__init__.py" = [ - "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files -] -"src/quality_time_notifier.py" = [ - "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is the main script -] -"tests/**/*.py" = [ - "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types -] diff --git a/components/notifier/tests/database/test_measurements.py b/components/notifier/tests/database/test_measurements.py index 5f27efb86e..1c3c4a0ed1 100644 --- a/components/notifier/tests/database/test_measurements.py +++ b/components/notifier/tests/database/test_measurements.py @@ -2,6 +2,7 @@ import unittest from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING import mongomock @@ -11,6 +12,9 @@ from tests.fixtures import METRIC_ID, create_report +if TYPE_CHECKING: + from pymongo.database import Database + class MeasurementsTest(unittest.TestCase): """Unit tests for getting measurements.""" @@ -22,7 +26,7 @@ def setUp(self) -> None: {"_id": 2, "start": "3", "end": "4", "sources": [], "metric_uuid": METRIC_ID}, {"_id": 3, "start": "6", "end": "7", "sources": [], "metric_uuid": METRIC_ID}, ] - self.database = mongomock.MongoClient()["quality_time_db"] + self.database: Database = mongomock.MongoClient()["quality_time_db"] def test_get_recent_measurements(self): """Test that the recent measurements are returned.""" diff --git a/components/notifier/tests/database/test_reports.py b/components/notifier/tests/database/test_reports.py index 3b237b2526..b9018caf7a 100644 --- a/components/notifier/tests/database/test_reports.py +++ b/components/notifier/tests/database/test_reports.py @@ -1,6 +1,7 @@ """Unit tests for getting reports from the database.""" import unittest +from typing import TYPE_CHECKING import mongomock @@ -8,6 +9,9 @@ from tests.fixtures import METRIC_ID, create_report +if TYPE_CHECKING: + from pymongo.database import Database + class ReportsTest(unittest.TestCase): """Unit tests for getting information from the database.""" @@ -19,7 +23,7 @@ def setUp(self) -> None: {"_id": 2, "start": "3", "end": "4", "sources": [], "metric_uuid": METRIC_ID}, {"_id": 3, "start": "6", "end": "7", "sources": [], "metric_uuid": METRIC_ID}, ] - self.database = mongomock.MongoClient()["quality_time_db"] + self.database: Database = mongomock.MongoClient()["quality_time_db"] def test_get_reports_and_measurements(self): """Test that the reports and latest two measurements are returned.""" diff --git a/components/notifier/tests/notifier/test_notifier.py b/components/notifier/tests/notifier/test_notifier.py index 7d1a159374..7d62f8ea2b 100644 --- a/components/notifier/tests/notifier/test_notifier.py +++ b/components/notifier/tests/notifier/test_notifier.py @@ -217,7 +217,7 @@ async def test_no_webhook_in_notification_destination( """Test that the notifier continues if a destination does not have a webhook configured.""" report1 = create_report() report2 = deepcopy(report1) - measurements = [] + measurements: list[dict] = [] mocked_get.side_effect = [([report1], measurements), ([report2], measurements)] mocked_sleep.side_effect = [None, RuntimeError] diff --git a/components/shared_code/ci/quality.sh b/components/shared_code/ci/quality.sh index 599ef9a839..98c4a32a92 100755 --- a/components/shared_code/ci/quality.sh +++ b/components/shared_code/ci/quality.sh @@ -1,34 +1,11 @@ #!/bin/bash -source ../../ci/base.sh - -# Ruff -run pipx run `spec ruff` check . -run pipx run `spec ruff` format --check . - -# Fixit -run pipx run `spec fixit` lint src tests - -# Mypy -# pipx run can't be used because mypy needs the pydantic plugin to be installed in the same venv (using pipx inject) -run pipx install --force `spec mypy` # --force works around this bug: https://github.com/pypa/pipx/issues/795 -run pipx inject mypy `spec pydantic` -run $PIPX_BIN_DIR/mypy src --python-executable=$(which python) - -# pip-audit -run pipx run `spec pip-audit` --strict --progress-spinner=off -r requirements/requirements-dev.txt - -# Safety -# Vulnerability ID: 67599 -# ADVISORY: ** DISPUTED ** An issue was discovered in pip (all versions) because it installs the version with the -# highest version number, even if the user had intended to obtain a private package from a private index. This only -# affects use of the --extra-index-url option, and exploitation requires that the... -# CVE-2018-20225 -# For more information about this vulnerability, visit https://data.safetycli.com/v/67599/97c -run pipx run `spec safety` check --bare --ignore 67599 -r requirements/requirements-dev.txt - -# Bandit -run pipx run `spec bandit` --quiet --recursive src/ - -# Vulture -run pipx run `spec vulture` --min-confidence 0 src/ tests/ .vulture_ignore_list.py +source ../../ci/quality-base.sh + +run_ruff +run_fixit +run_pyproject_fmt +run_mypy_pydantic +run_pip_audit -r requirements/requirements-dev.txt +run_bandit +run_vulture diff --git a/components/shared_code/pyproject.toml b/components/shared_code/pyproject.toml index 756ee5d689..c23c43d96f 100644 --- a/components/shared_code/pyproject.toml +++ b/components/shared_code/pyproject.toml @@ -1,6 +1,11 @@ [project] -name = "shared_code" +name = "shared-code" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "bottle==0.12.25", "packaging==24.1", @@ -9,27 +14,85 @@ dependencies = [ "python-dateutil==2.9.0.post0", ] -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "coverage==7.5.3", "mongomock==4.1.2", "pip==24.0", + "pip-tools==7.4.1", # To add hashes to requirements "pipx==1.6.0", - "pip-tools==7.4.1", # To add hashes to requirements "types-python-dateutil==2.9.0.20240316", - "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io + "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io ] -tools = [ +optional-dependencies.tools = [ "bandit==1.7.9", "fixit==2.1.0", "mypy==1.10.0", "pip-audit==2.7.3", - "pydantic==2.7.4", # Needed because pipx needs to inject Pydantic into the mpyp venv, see ci/quality.sh + "pydantic==2.7.4", # Needed because pipx needs to inject Pydantic into the mpyp venv, see ci/quality.sh + "pyproject-fmt==2.1.3", "ruff==0.4.8", - "safety==3.2.3", - "vulture==2.11" + "vulture==2.11", +] + +[tool.setuptools.packages.find] +where = [ + "src", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", ] +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - too many untyped arguments atm to turn this rule on + "ANN002", # https://docs.astral.sh/ruff/rules/missing-type-args/ - leads to false positives for super().__init__(*args, **kwargs) + "ANN003", # https://docs.astral.sh/ruff/rules/missing-type-kwargs/ - leads to false positives for super().__init__(*args, **kwargs) + "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous + "ANN102", # https://docs.astral.sh/ruff/rules/missing-type-cls/ - type checkers can infer the type of `cls`, so annotating it is superfluous + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ - requiring __init__() methods to have docstrings seems a bit much + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter + "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", +] +lint.per-file-ignores."__init__.py" = [ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files + "F401", # https://docs.astral.sh/ruff/rules/unused-import/ - imports in __init__.py files are used to flatten the module hierarchy +] +lint.per-file-ignores."src/shared_data_model/**/*.py" = [ + "RUF012", # https://docs.astral.sh/ruff/rules/mutable-class-default/ - Pydantic models' class attributes are used to specify instance attributes +] +lint.per-file-ignores."tests/**/*.py" = [ + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types +] +lint.isort.section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "tests", + "local-folder", +] +lint.isort.sections."tests" = [ + "tests", +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? + [tool.mypy] plugins = "pydantic.mypy" ignore_missing_imports = false @@ -38,12 +101,12 @@ warn_redundant_casts = true warn_return_any = true warn_unreachable = true warn_unused_ignores = true -disable_error_code = "valid-type" # mypy does not yet support PEP 695, Type Parameter Syntax. See https://github.com/python/mypy/issues/15238 +disable_error_code = "valid-type" # mypy does not yet support PEP 695, Type Parameter Syntax. See https://github.com/python/mypy/issues/15238 [[tool.mypy.overrides]] module = [ "pymongo", - "bottle" + "bottle", ] ignore_missing_imports = true @@ -53,48 +116,3 @@ generate_hashes = true quiet = true strip_extras = true upgrade = true - -[tool.ruff] -target-version = "py312" -line-length = 120 -src = ["src"] - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - too many untyped arguments atm to turn this rule on - "ANN002", # https://docs.astral.sh/ruff/rules/missing-type-args/ - leads to false positives for super().__init__(*args, **kwargs) - "ANN003", # https://docs.astral.sh/ruff/rules/missing-type-kwargs/ - leads to false positives for super().__init__(*args, **kwargs) - "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ - type checkers can infer the type of `self`, so annotating it is superfluous - "ANN102", # https://docs.astral.sh/ruff/rules/missing-type-cls/ - type checkers can infer the type of `cls`, so annotating it is superfluous - "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter - "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ - requiring __init__() methods to have docstrings seems a bit much - "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` - "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` - "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" - "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter - "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used -] - -[tool.ruff.lint.isort] -section-order = ["future", "standard-library", "third-party", "first-party", "tests", "local-folder"] - -[tool.ruff.lint.isort.sections] -"tests" = ["tests"] - -[tool.ruff.lint.per-file-ignores] -".vulture_ignore_list.py" = ["ALL"] -"__init__.py" = [ - "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files - "F401", # https://docs.astral.sh/ruff/rules/unused-import/ - imports in __init__.py files are used to flatten the module hierarchy -] -"src/shared_data_model/**/*.py" = [ - "RUF012", # https://docs.astral.sh/ruff/rules/mutable-class-default/ - Pydantic models' class attributes are used to specify instance attributes -] -"tests/**/*.py" = [ - "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types -] - -[tool.setuptools.packages.find] -where = ["src"] diff --git a/docs/ci/quality.sh b/docs/ci/quality.sh index 1745cbb158..ecb00932f1 100755 --- a/docs/ci/quality.sh +++ b/docs/ci/quality.sh @@ -1,32 +1,13 @@ #!/bin/bash -source ../ci/base.sh - -# Markdownlint -run ./node_modules/markdownlint-cli/markdownlint.js src/*.md - -# Ruff -run pipx run `spec ruff` check . -run pipx run `spec ruff` format --check . - -# Fixit -run pipx run `spec fixit` lint src tests - -# Mypy -# pipx run can't be used because mypy needs the pydantic plugin to be installed in the same venv (using pipx inject) -run pipx install --force `spec mypy` # --force works around this bug: https://github.com/pypa/pipx/issues/795 -run pipx inject mypy `spec pydantic` -run $PIPX_BIN_DIR/mypy src --python-executable=$(which python) - -# Vale -run pipx run `spec vale` sync -run pipx run `spec vale` --no-wrap src/*.md - -# pip-audit -run pipx run `spec pip-audit` --strict --progress-spinner=off -r requirements/requirements.txt -r requirements/requirements-dev.txt - -# Safety -run pipx run `spec bandit` --quiet --recursive src/ - -# Vulture -run pipx run `spec vulture` --min-confidence 0 src/ tests/ .vulture_ignore_list.py +source ../ci/quality-base.sh + +run_markdownlint +run_ruff +run_fixit +run_mypy_pydantic +run_pyproject_fmt +run_vale +run_pip_audit +run_bandit +run_vulture diff --git a/docs/pyproject.toml b/docs/pyproject.toml index c6fa866de5..0dd5e5fe26 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -1,36 +1,94 @@ [project] name = "docs" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "furo==2023.9.10", "gitpython==3.1.43", "myst-parser==2.0.0", - "pydantic==2.7.4", # Needed for generating the reference docs from the data model - "Sphinx==7.2.6", + "pydantic==2.7.4", # Needed for generating the reference docs from the data model + "sphinx==7.2.6", "sphinx-copybutton==0.5.2", - "sphinx_design==0.5.0" + "sphinx-design==0.5.0", ] - -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "coverage==7.3.4", "pip==24.0", + "pip-tools==7.4.1", # To add hashes to requirements "pipx==1.6.0", - "pip-tools==7.4.1", # To add hashes to requirements - "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io + "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io ] -tools = [ +optional-dependencies.tools = [ "bandit==1.7.9", "fixit==2.1.0", "mypy==1.10.0", "pip-audit==2.7.3", - "pydantic==2.7.4", # Needed because pipx needs to inject Pydantic into the mpyp venv, see ci/quality.sh + "pydantic==2.7.4", # Needed because pipx needs to inject Pydantic into the mpyp venv, see ci/quality.sh + "pyproject-fmt==2.1.3", "ruff==0.4.8", - "safety==3.2.3", - "vale==3.0.3.0", # Documentation grammar and style checker - "vulture==2.11" + "vale==3.0.3.0", # Documentation grammar and style checker + "vulture==2.11", ] +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", +] +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - type checkers can infer the type of `self`, so annotating it is superfluous + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter + "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", +] +lint.per-file-ignores."__init__.py" = [ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files +] +lint.per-file-ignores."src/conf.py" = [ + "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is a configuration file +] +lint.per-file-ignores."src/create_reference_md.py" = [ + "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is a script +] +lint.per-file-ignores."tests/**/*.py" = [ + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types +] +lint.isort.section-order = [ + "future", + "standard-library", + "third-party", + "second-party", + "first-party", + "tests", + "local-folder", +] +lint.isort.sections.second-party = [ + "shared", + "shared_data_model", +] +lint.isort.sections.tests = [ + "tests", +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? + [tool.mypy] plugins = "pydantic.mypy" ignore_missing_imports = false @@ -46,43 +104,3 @@ generate_hashes = true quiet = true strip_extras = true upgrade = true - -[tool.ruff] -target-version = "py312" -line-length = 120 -src = ["src"] - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - type checkers can infer the type of `self`, so annotating it is superfluous - "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter - "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` - "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` - "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" - "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter - "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used -] - -[tool.ruff.lint.isort] -section-order = ["future", "standard-library", "third-party", "second-party", "first-party", "tests", "local-folder"] - -[tool.ruff.lint.isort.sections] -"second-party" = ["shared", "shared_data_model"] -"tests" = ["tests"] - -[tool.ruff.lint.per-file-ignores] -".vulture_ignore_list.py" = ["ALL"] -"__init__.py" = [ - "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files -] -"src/conf.py" = [ - "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is a configuration file -] -"src/create_reference_md.py" = [ - "INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - false positive because this is a script -] -"tests/**/*.py" = [ - "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - don't require test functions to have return types -] diff --git a/release/.vulture_ignore_list.py b/release/.vulture_ignore_list.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/release/ci/quality.sh b/release/ci/quality.sh new file mode 100755 index 0000000000..a3dd8c8bf7 --- /dev/null +++ b/release/ci/quality.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +source ../ci/quality-base.sh + +run_ruff +run_fixit *.py +run_mypy *.py +run_pyproject_fmt +run_pip_audit +run_bandit +run_vulture *.py diff --git a/release/pyproject.toml b/release/pyproject.toml index 4300cb70b2..2823c23133 100644 --- a/release/pyproject.toml +++ b/release/pyproject.toml @@ -1,16 +1,53 @@ [project] name = "release" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "bump-my-version==0.22.0.post197.dev9", "gitpython==3.1.43", ] - -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "pip==24.0", - "pip-tools==7.4.1" # To add hashes to requirements + "pip-tools==7.4.1", # To add hashes to requirements + "pipx==1.6.0", +] +optional-dependencies.tools = [ + "bandit==1.7.9", + "fixit==2.1.0", + "mypy==1.10.0", + "pip-audit==2.7.3", + "pydantic==2.7.4", # Needed because pipx needs to inject Pydantic into the mpyp venv, see ci/quality.sh + "pyproject-fmt==2.1.3", + "ruff==0.4.8", + "vulture==2.11", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", +] +lint.select = [ + "ALL", ] +lint.ignore = [ + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? [tool.bumpversion] current_version = "5.13.0" @@ -33,7 +70,10 @@ commit = true tag = true [tool.bumpversion.parts.pre_release_label] -values = ["rc", "final"] +values = [ + "rc", + "final", +] optional_value = "final" [[tool.bumpversion.files]] @@ -76,6 +116,12 @@ glob = "../**/pyproject.toml" search = 'version = "{current_version}"' replace = 'version = "{new_version}"' +[tool.bandit] +skips = [ + "B404", # Consider possible security implications associated with the subprocess module. + "B603", # subprocess call - check for execution of untrusted input. +] + [tool.pip-tools] allow_unsafe = true generate_hashes = true diff --git a/release/release.py b/release/release.py index 2faddc037c..423efaa5b2 100755 --- a/release/release.py +++ b/release/release.py @@ -26,9 +26,9 @@ def get_version() -> str: with pathlib.Path(release_folder / "pyproject.toml").open(mode="rb") as py_project_toml_fp: py_project_toml = tomllib.load(py_project_toml_fp) version_re = py_project_toml["tool"]["bumpversion"]["parse"] - version_tags = [tag for tag in repo.tags if re.match(version_re, tag.tag.tag.strip("v"), re.MULTILINE)] + version_tags = [tag for tag in repo.tags if tag.tag and re.match(version_re, tag.tag.tag.strip("v"), re.MULTILINE)] latest_tag = sorted(version_tags, key=lambda tag: tag.commit.committed_datetime)[-1] - return latest_tag.tag.tag.strip("v") + return latest_tag.tag.tag.strip("v") if latest_tag.tag else "?" def parse_arguments() -> tuple[str, str, bool]: @@ -44,13 +44,26 @@ def parse_arguments() -> tuple[str, str, bool]: - the changelog has an '[Unreleased]' header - the changelog contains no release candidates - the new release has been added to the version overview""" - parser = ArgumentParser(description=description, epilog=epilog, formatter_class=RawDescriptionHelpFormatter) - allowed_bumps_in_rc_mode = ["rc", "rc-major", "rc-minor", "rc-patch", "drop-rc"] # rc = release candidate + parser = ArgumentParser( + description=description, + epilog=epilog, + formatter_class=RawDescriptionHelpFormatter, + ) + allowed_bumps_in_rc_mode = [ + "rc", + "rc-major", + "rc-minor", + "rc-patch", + "drop-rc", + ] # rc = release candidate allowed_bumps = ["rc-patch", "rc-minor", "rc-major", "patch", "minor", "major"] bumps = allowed_bumps_in_rc_mode if "rc" in current_version else allowed_bumps parser.add_argument("bump", choices=bumps) parser.add_argument( - "-c", "--check-preconditions-only", action="store_true", help="only check the preconditions and then exit" + "-c", + "--check-preconditions-only", + action="store_true", + help="only check the preconditions and then exit", ) arguments = parser.parse_args() return arguments.bump, current_version, arguments.check_preconditions_only @@ -117,7 +130,7 @@ def failed_preconditions_version_overview(current_version: str, root: pathlib.Pa for line in version_overview_lines: if line.startswith(f"| v{current_version} "): if previous_line.startswith("| v"): - today = datetime.date.today().isoformat() + today = utc_today().isoformat() release_date = previous_line.split(" | ")[1].strip() if release_date != today: # Second column is the release date column return [f"{missing} the release date. Expected today: '{today}', found: '{release_date}'."] @@ -127,13 +140,18 @@ def failed_preconditions_version_overview(current_version: str, root: pathlib.Pa return [f"{missing} the current version ({current_version})."] +def utc_today() -> datetime.date: + """Return today in UTC.""" + return datetime.datetime.now(tz=datetime.UTC).date() + + def main() -> None: """Create the release.""" - os.environ["RELEASE_DATE"] = datetime.date.today().isoformat() # Used by bump-my-version to update CHANGELOG.md + os.environ["RELEASE_DATE"] = utc_today().isoformat() # Used by bump-my-version to update CHANGELOG.md bump, current_version, check_preconditions_only = parse_arguments() check_preconditions(bump, current_version) if check_preconditions_only: - return + return # See https://github.com/callowayproject/bump-my-version?tab=readme-ov-file#add-support-for-pre-release-versions # for how bump-my-version deals with pre-release versions if bump.startswith("rc-"): @@ -142,8 +160,8 @@ def main() -> None: bump = "pre_release_label" # Bump the pre-release label from "rc" to "final" (which is optional and omitted) elif bump == "rc": bump = "pre_release_number" # Bump the release candidate number - subprocess.run(("bump-my-version", "bump", bump), check=True) - subprocess.run(("git", "push", "--follow-tags"), check=True) + subprocess.run(("bump-my-version", "bump", bump), check=True) # noqa: S603 + subprocess.run(("git", "push", "--follow-tags"), check=True) # noqa: S603 if __name__ == "__main__": diff --git a/release/requirements/requirements-dev.txt b/release/requirements/requirements-dev.txt index 6e1acc1a49..3734e17d73 100644 --- a/release/requirements/requirements-dev.txt +++ b/release/requirements/requirements-dev.txt @@ -8,6 +8,10 @@ annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 # via pydantic +argcomplete==3.3.0 \ + --hash=sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54 \ + --hash=sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62 + # via pipx bracex==2.4 \ --hash=sha256:a27eaf1df42cf561fed58b7a8f3fdf129d1ea16a81e1fadd1d17989bc6384beb \ --hash=sha256:efdc71eff95eaff5e0f8cfebe7d01adf2c8637c8c92edaf63ef348c241a82418 @@ -27,6 +31,7 @@ click==8.1.7 \ # bump-my-version # pip-tools # rich-click + # userpath gitdb==4.0.11 \ --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b @@ -46,11 +51,21 @@ mdurl==0.1.2 \ packaging==24.1 \ --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 - # via build + # via + # build + # pipx pip-tools==7.4.1 \ --hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \ --hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9 # via release (pyproject.toml) +pipx==1.6.0 \ + --hash=sha256:760889dc3aeed7bf4024973bf22ca0c2a891003f52389159ab5cb0c57d9ebff4 \ + --hash=sha256:840610e00103e3d49ae24b6b51804b60988851a5dd65468adb71e5a97e2699b2 + # via release (pyproject.toml) +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 + # via pipx prompt-toolkit==3.0.36 \ --hash=sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63 \ --hash=sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305 @@ -189,6 +204,10 @@ typing-extensions==4.12.2 \ # pydantic # pydantic-core # rich-click +userpath==1.9.2 \ + --hash=sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d \ + --hash=sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815 + # via pipx wcmatch==8.5.2 \ --hash=sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478 \ --hash=sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2 diff --git a/tests/application_tests/.vulture_ignore_list.py b/tests/application_tests/.vulture_ignore_list.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/application_tests/ci/quality.sh b/tests/application_tests/ci/quality.sh new file mode 100755 index 0000000000..bac4e814a6 --- /dev/null +++ b/tests/application_tests/ci/quality.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +source ../../ci/quality-base.sh + +run_ruff +run_fixit src +run_mypy src +run_pyproject_fmt +run_pip_audit +run_bandit +run_vulture src diff --git a/tests/application_tests/ci/unittest.sh b/tests/application_tests/ci/unittest.sh new file mode 100755 index 0000000000..9a4802fb92 --- /dev/null +++ b/tests/application_tests/ci/unittest.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +# Dummy unittest.sh so we can run the same steps for different components. diff --git a/tests/application_tests/pyproject.toml b/tests/application_tests/pyproject.toml index d4f018d2ee..0b8d8060ee 100644 --- a/tests/application_tests/pyproject.toml +++ b/tests/application_tests/pyproject.toml @@ -1,17 +1,79 @@ [project] -name = "application_tests" +name = "application-tests" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "axe-selenium-python==2.1.6", "requests==2.32.3", - "selenium==4.21.0" + "selenium==4.21.0", ] - -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "pip==24.0", - "pip-tools==7.4.1" # To add hashes to requirements + "pip-tools==7.4.1", # To add hashes to requirements + "pipx==1.6.0", + "types-requests==2.32.0.20240602", +] +optional-dependencies.tools = [ + "bandit==1.7.9", + "fixit==2.1.0", + "mypy==1.10.0", + "pip-audit==2.7.3", + "pyproject-fmt==2.1.3", + "ruff==0.4.8", + "vulture==2.11", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", +] +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - too many untyped arguments atm to turn this rule on + "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ - type checkers can infer the type of `self`, so annotating it is superfluous + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ - too many untyped return values atm to turn this rule on + "ANN204", # https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ - typing classes that inherit from set and list correctly is surprisingly hard + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ - requiring __init__() methods to have docstrings seems a bit much + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter + "PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd - pandas isn't used + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - pytest isn't used +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", +] +lint.per-file-ignores."__init__.py" = [ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? + +[tool.mypy] +ignore_missing_imports = false +incremental = false +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[[tool.mypy.overrides]] +module = [ + "axe_selenium_python", ] +ignore_missing_imports = true [tool.pip-tools] allow_unsafe = true diff --git a/tests/application_tests/requirements/requirements-dev.txt b/tests/application_tests/requirements/requirements-dev.txt index 6ee13d3e1d..73860e1592 100644 --- a/tests/application_tests/requirements/requirements-dev.txt +++ b/tests/application_tests/requirements/requirements-dev.txt @@ -4,6 +4,10 @@ # # ci/pip-compile.sh # +argcomplete==3.3.0 \ + --hash=sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54 \ + --hash=sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62 + # via pipx attrs==23.2.0 \ --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 @@ -13,7 +17,7 @@ attrs==23.2.0 \ axe-selenium-python==2.1.6 \ --hash=sha256:21d10014c0bc3c999c1bfab54b9ef6a5d67f200ffca3db3629e0b315b3f1f136 \ --hash=sha256:9203ff59c79edcfbcbd676ae55ca35f257e9afb663adbcbc210e3b0d802255ae - # via application_tests (pyproject.toml) + # via application-tests (pyproject.toml) build==1.2.1 \ --hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \ --hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4 @@ -119,7 +123,9 @@ charset-normalizer==3.3.2 \ click==8.1.7 \ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via pip-tools + # via + # pip-tools + # userpath h11==0.14.0 \ --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 @@ -143,11 +149,20 @@ packaging==24.1 \ --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 # via # build + # pipx # pytest pip-tools==7.4.1 \ --hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \ --hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9 - # via application_tests (pyproject.toml) + # via application-tests (pyproject.toml) +pipx==1.6.0 \ + --hash=sha256:760889dc3aeed7bf4024973bf22ca0c2a891003f52389159ab5cb0c57d9ebff4 \ + --hash=sha256:840610e00103e3d49ae24b6b51804b60988851a5dd65468adb71e5a97e2699b2 + # via application-tests (pyproject.toml) +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 + # via pipx pluggy==1.5.0 \ --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 @@ -170,12 +185,12 @@ pytest==8.2.2 \ requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via application_tests (pyproject.toml) + # via application-tests (pyproject.toml) selenium==4.21.0 \ --hash=sha256:4770ffe5a5264e609de7dc914be6b89987512040d5a8efb2abb181330d097993 \ --hash=sha256:650dbfa5159895ff00ad16e5ddb6ceecb86b90c7ed2012b3f041f64e6e4904fe # via - # application_tests (pyproject.toml) + # application-tests (pyproject.toml) # axe-selenium-python sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ @@ -195,6 +210,10 @@ trio-websocket==0.11.1 \ --hash=sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f \ --hash=sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638 # via selenium +types-requests==2.32.0.20240602 \ + --hash=sha256:3f98d7bbd0dd94ebd10ff43a7fbe20c3b8528acace6d8efafef0b6a184793f06 \ + --hash=sha256:ed3946063ea9fbc6b5fc0c44fa279188bae42d582cb63760be6cb4b9d06c3de8 + # via application-tests (pyproject.toml) typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 @@ -205,6 +224,11 @@ urllib3==2.2.1 \ # via # requests # selenium + # types-requests +userpath==1.9.2 \ + --hash=sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d \ + --hash=sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815 + # via pipx wheel==0.43.0 \ --hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \ --hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81 @@ -219,7 +243,7 @@ pip==24.0 \ --hash=sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc \ --hash=sha256:ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2 # via - # application_tests (pyproject.toml) + # application-tests (pyproject.toml) # pip-tools setuptools==70.0.0 \ --hash=sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4 \ diff --git a/tests/application_tests/requirements/requirements.txt b/tests/application_tests/requirements/requirements.txt index 12264ce05f..45d8467abb 100644 --- a/tests/application_tests/requirements/requirements.txt +++ b/tests/application_tests/requirements/requirements.txt @@ -13,7 +13,7 @@ attrs==23.2.0 \ axe-selenium-python==2.1.6 \ --hash=sha256:21d10014c0bc3c999c1bfab54b9ef6a5d67f200ffca3db3629e0b315b3f1f136 \ --hash=sha256:9203ff59c79edcfbcbd676ae55ca35f257e9afb663adbcbc210e3b0d802255ae - # via application_tests (pyproject.toml) + # via application-tests (pyproject.toml) certifi==2024.6.2 \ --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 @@ -150,12 +150,12 @@ pytest==8.2.2 \ requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via application_tests (pyproject.toml) + # via application-tests (pyproject.toml) selenium==4.21.0 \ --hash=sha256:4770ffe5a5264e609de7dc914be6b89987512040d5a8efb2abb181330d097993 \ --hash=sha256:650dbfa5159895ff00ad16e5ddb6ceecb86b90c7ed2012b3f041f64e6e4904fe # via - # application_tests (pyproject.toml) + # application-tests (pyproject.toml) # axe-selenium-python sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ diff --git a/tests/application_tests/src/test_api.py b/tests/application_tests/src/test_api.py index 9ac89832f1..0897e53d95 100644 --- a/tests/application_tests/src/test_api.py +++ b/tests/application_tests/src/test_api.py @@ -11,5 +11,5 @@ class ApiTest(unittest.TestCase): def test_documentation(self): """Test that the documentation API is available.""" apis = requests.get("http://www:8080/api", timeout=10).json().keys() - self.assertTrue("/api/internal/login" in apis) - self.assertTrue("/api/v3/login" in apis) + self.assertIn("/api/internal/login", apis) + self.assertIn("/api/v3/login", apis) diff --git a/tests/application_tests/src/test_report.py b/tests/application_tests/src/test_report.py index 1d318fc9fd..8ed3e00a6a 100644 --- a/tests/application_tests/src/test_report.py +++ b/tests/application_tests/src/test_report.py @@ -10,29 +10,31 @@ from selenium.webdriver.support.ui import WebDriverWait -class element_has_no_css_class: - """An expectation for checking that an element has no css class. +class ElementHasNoCCSClass: + """An expectation for checking that an element has no CSS class. locator - used to find the element - returns the WebElement once it has the particular css class + returns the WebElement once it has no particular CSS class """ - def __init__(self, locator): + def __init__(self, locator) -> None: self.locator = locator def __call__(self, driver): + """Return the element if it no longer has a CSS class, otherwise False.""" element = driver.find_element(*self.locator) return element if len(element.get_attribute("class")) == 0 else False -class nr_elements: +class NrElements: """An expectation for the number of matching elements.""" - def __init__(self, locator, expected_nr: int): + def __init__(self, locator, expected_nr: int) -> None: self.locator = locator self.expected_nr = expected_nr def __call__(self, driver): + """Return the element if it has the expected number of elements, otherwise False.""" elements = driver.find_elements(*self.locator) return elements if len(elements) == self.expected_nr else False @@ -60,7 +62,7 @@ def login(self): login_form.find_element(By.NAME, "username").send_keys("jadoe") login_form.find_element(By.NAME, "password").send_keys("secret") login_form.find_element(By.CLASS_NAME, "button").click() - self.wait.until(element_has_no_css_class((By.TAG_NAME, "body"))) # Wait for body dimmer to disappear + self.wait.until(ElementHasNoCCSClass((By.TAG_NAME, "body"))) # Wait for body dimmer to disappear def test_title(self): """Test the title.""" @@ -90,7 +92,7 @@ def test_add_report(self): self.login() nr_reports = len(self.driver.find_elements(By.CLASS_NAME, "card")) self.driver.find_element(By.CLASS_NAME, "button.primary").click() - self.wait.until(nr_elements((By.CLASS_NAME, "card"), nr_reports + 1)) + self.wait.until(NrElements((By.CLASS_NAME, "card"), nr_reports + 1)) def test_report_axe_accessibility(self): """Run axe accessibility check on a report.""" @@ -104,15 +106,16 @@ def test_report_axe_accessibility(self): # Process axe results violation_results = results1["violations"] - axe.write_results(results1, '../../build/a11y.json') + axe.write_results(results1, "../../build/a11y.json") readable_report = axe.report(violation_results) - filename = pathlib.Path('../../build/a11y_violations.txt') + filename = pathlib.Path("../../build/a11y_violations.txt") try: - with open(filename, "w", encoding="utf8") as report_file: + with filename.open("w", encoding="utf8") as report_file: report_file.write(readable_report) except OSError: - print("Could not write axe violations report") + self.fail("Could not write axe violations report") - # If there are violations, output the readable report data - # TODO - assertEqual 0 in https://github.com/ICTU/quality-time/issues/6354 - self.assertTrue(6 >= len(violation_results), readable_report) + # If there are moe violations than expected, output the readable report data + # Fixing the axe violations is on the backlog: https://github.com/ICTU/quality-time/issues/6354 + current_number_of_axe_violations = 6 + self.assertLessEqual(len(violation_results), current_number_of_axe_violations, readable_report) diff --git a/tests/feature_tests/ci/quality.sh b/tests/feature_tests/ci/quality.sh index 686861329f..bac4e814a6 100755 --- a/tests/feature_tests/ci/quality.sh +++ b/tests/feature_tests/ci/quality.sh @@ -1,31 +1,11 @@ #!/bin/bash -source ../../ci/base.sh - -# Ruff -run pipx run `spec ruff` check . -run pipx run `spec ruff` format --check . - -# Fixit -run pipx run `spec fixit` lint src - -# Mypy -run pipx run `spec mypy` --python-executable=$(which python) src - -# pip-audit -run pipx run `spec pip-audit` --strict --progress-spinner=off -r requirements/requirements-dev.txt - -# Safety -# Vulnerability ID: 67599 -# ADVISORY: ** DISPUTED ** An issue was discovered in pip (all versions) because it installs the version with the -# highest version number, even if the user had intended to obtain a private package from a private index. This only -# affects use of the --extra-index-url option, and exploitation requires that the... -# CVE-2018-20225 -# For more information about this vulnerability, visit https://data.safetycli.com/v/67599/97c -run pipx run `spec safety` check --bare --ignore 67599 -r requirements/requirements-dev.txt - -# Bandit -run pipx run `spec bandit` --quiet --recursive src/ - -# Vulture -run pipx run `spec vulture` --min-confidence 0 src/ .vulture_ignore_list.py +source ../../ci/quality-base.sh + +run_ruff +run_fixit src +run_mypy src +run_pyproject_fmt +run_pip_audit +run_bandit +run_vulture src diff --git a/tests/feature_tests/pyproject.toml b/tests/feature_tests/pyproject.toml index 97494d7e75..5225bc0f99 100644 --- a/tests/feature_tests/pyproject.toml +++ b/tests/feature_tests/pyproject.toml @@ -1,33 +1,64 @@ [project] -name = "feature_tests" +name = "feature-tests" version = "5.13.0" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", +] dependencies = [ "asserts==0.13.1", "behave==1.2.6", "gevent==24.2.1", "pymongo==4.7.3", "requests==2.32.3", - "sseclient==0.0.27" + "sseclient==0.0.27", ] - -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "coverage==7.5.3", "pip==24.0", + "pip-tools==7.4.1", # To add hashes to requirements "pipx==1.6.0", - "pip-tools==7.4.1", # To add hashes to requirements "types-requests==2.32.0.20240602", - "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io + "unittest-xml-reporting==3.2.0", # Needed to generate JUnit XML output for Sonarcloud.io ] -tools = [ +optional-dependencies.tools = [ "bandit==1.7.9", "fixit==2.1.0", "mypy==1.10.0", "pip-audit==2.7.3", + "pyproject-fmt==2.1.3", "ruff==0.4.8", - "safety==3.2.3", - "vulture==2.11" + "vulture==2.11", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", + "src/steps", +] +lint.select = [ + "ALL", +] +lint.ignore = [ + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter + "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" + "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter +] +lint.per-file-ignores.".vulture_ignore_list.py" = [ + "ALL", ] +lint.per-file-ignores."__init__.py" = [ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files +] + +[tool.pyproject-fmt] +indent = 4 +keep_full_version = true # Remove trailing zero's from version specifiers? [tool.mypy] ignore_missing_imports = false @@ -43,7 +74,7 @@ module = [ "behave", "behave.model", "behave.runner", - "sseclient" + "sseclient", ] ignore_missing_imports = true @@ -53,24 +84,3 @@ generate_hashes = true quiet = true strip_extras = true upgrade = true - -[tool.ruff] -target-version = "py312" -line-length = 120 -src = ["src", "src/steps"] - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - this rule may cause conflicts when used with the ruff formatter - "D203", # https://docs.astral.sh/ruff/rules/one-blank-line-before-class/ - prevent warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` - "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ - prevent warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line` - "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt - not sure of the value of preventing "boolean traps" - "ISC001", # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ - this rule may cause conflicts when used with the ruff formatter -] - -[tool.ruff.lint.per-file-ignores] -".vulture_ignore_list.py" = ["ALL"] -"__init__.py" = [ - "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ - don't require doc strings in __init__.py files -]