Skip to content

Commit 5524980

Browse files
ci: optimize quality checks and isolate dependency tests (#8535)
* ci: deduplicate quality checks and reuse caches * test: isolate validation dependencies in Nox
1 parent 073b5ba commit 5524980

12 files changed

Lines changed: 146 additions & 34 deletions

File tree

‎.github/workflows/quality_check.yml‎

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@ name: Quality check - check code
22

33
# PROCESS
44
#
5-
# 1. Install all dependencies and spin off containers for all supported Python versions
6-
# 2. Run code formatters and linters (various checks) for code standard
7-
# 3. Run static typing checker for potential bugs
8-
# 4. Run entire test suite for regressions except end-to-end (unit, functional, performance)
9-
# 5. Run static analysis (in addition to CodeQL) for common insecure code practices
10-
# 6. Run complexity baseline to avoid error-prone bugs and keep maintenance lower
11-
# 7. Collect and report on test coverage
5+
# 1. Run formatting, linting, security and complexity checks once
6+
# 2. Run both type checkers and the test suite on every supported Python version
7+
# 3. Validate optional dependencies with Nox on every supported Python version
8+
# 4. Collect and report on test coverage
129

1310
# USAGE
1411
#
@@ -25,6 +22,8 @@ on:
2522
- "mypy.ini"
2623
- "Makefile"
2724
- "noxfile.py"
25+
- "ruff.toml"
26+
- "bandit.baseline"
2827
- ".pre-commit-config.yaml"
2928
- ".github/workflows/quality_check.yml"
3029
- ".github/workflows/release-v3.yml"
@@ -43,6 +42,8 @@ on:
4342
- "mypy.ini"
4443
- "Makefile"
4544
- "noxfile.py"
45+
- "ruff.toml"
46+
- "bandit.baseline"
4647
- ".pre-commit-config.yaml"
4748
- ".github/workflows/quality_check.yml"
4849
- ".github/workflows/release-v3.yml"
@@ -55,7 +56,39 @@ on:
5556
permissions:
5657
contents: read
5758

59+
concurrency:
60+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
61+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
62+
5863
jobs:
64+
static_checks:
65+
runs-on: ubuntu-latest
66+
steps:
67+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
68+
- name: Set up Python
69+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
70+
with:
71+
python-version: "3.14"
72+
- name: Set up uv
73+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
74+
with:
75+
version: "0.12.19"
76+
enable-cache: true
77+
cache-suffix: static-checks
78+
cache-dependency-glob: |
79+
pyproject.toml
80+
uv.lock
81+
- name: Install dependencies
82+
run: make dev-quality-code
83+
- name: Check formatting
84+
run: make format-check
85+
- name: Lint
86+
run: make lint-check
87+
- name: Security baseline
88+
run: make security-baseline
89+
- name: Complexity baseline
90+
run: make complexity-baseline
91+
5992
quality_check:
6093
runs-on: ubuntu-latest
6194
strategy:
@@ -69,21 +102,30 @@ jobs:
69102
contents: read # checkout code only
70103
steps:
71104
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
105+
- name: Set up Python ${{ matrix.python-version }}
106+
id: python
107+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
108+
with:
109+
python-version: ${{ matrix.python-version }}
72110
- name: Set up uv
73111
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
74112
with:
75113
version: "0.12.19"
76114
enable-cache: true
77-
- name: Set up Python ${{ matrix.python-version }}
78-
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
79-
with:
80-
python-version: ${{ matrix.python-version }}
115+
cache-suffix: quality-${{ matrix.python-version }}
116+
cache-dependency-glob: |
117+
pyproject.toml
118+
uv.lock
119+
noxfile.py
81120
- name: Install dependencies
82121
run: make dev-quality-code
83-
- name: Checking and enforcing format
84-
run: make format-check
85-
- name: Formatting and Linting
86-
run: make lint
122+
- name: Cache mypy analysis
123+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
124+
with:
125+
path: .mypy_cache
126+
key: mypy-${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'mypy.ini') }}-${{ github.sha }}
127+
restore-keys: |
128+
mypy-${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'mypy.ini') }}-
87129
- name: Static type checking (mypy)
88130
run: make mypy
89131
- name: Static type checking (ty)
@@ -92,10 +134,6 @@ jobs:
92134
run: make test
93135
- name: Test dependencies with Nox
94136
run: make test-dependencies
95-
- name: Security baseline
96-
run: make security-baseline
97-
- name: Complexity baseline
98-
run: make complexity-baseline
99137
- name: Upload coverage to Codecov
100138
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # 7.1.1
101139
with:

‎CONTRIBUTING.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then run
104104
This creates a `.venv` with the locked development dependencies and installs the Git hooks.
105105
Run tools with `uv run --locked`, or activate `.venv` when working in your editor.
106106

107+
CI runs formatting, linting, security and complexity checks once. Tests, dependency
108+
checks with Nox, and both type checkers run on every supported Python version.
109+
Run `make format-check lint-check` to check formatting and linting without modifying
110+
files. `make lint` still formats files before linting. CI uses `make dev-quality-code`
111+
to install the same dependencies as `make dev`, without installing Git hooks.
112+
113+
`make test-dependencies` recreates the Nox environments so packages left over from
114+
earlier runs cannot hide missing dependencies. Tests marked `requires_validation`
115+
run in dedicated sessions with the Validation extra, keeping the SDK and Parser
116+
sessions independent of it. The full test suite still runs these tests.
117+
107118
To send us a pull request, please follow these steps:
108119

109120
1. Create a new branch to focus on the specific change you are contributing e.g. `improv/logger-debug-sampling`

‎Makefile‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
.PHONY: target dev format lint test coverage-html pr build build-docs build-docs-website
1+
.PHONY: target dev dev-quality-code format format-check lint lint-check test coverage-html pr build build-docs build-docs-website
22
.PHONY: docs-local security-baseline complexity-baseline release-prod release-test release
33

44
target:
55
@$(MAKE) pr
66

7-
dev dev-quality-code:
8-
uv sync --locked --extra all --extra redis --extra datamasking --extra valkey
7+
dev: dev-quality-code
98
uv run --locked pre-commit install
109

10+
dev-quality-code:
11+
uv sync --locked --extra all --extra redis --extra datamasking --extra valkey
12+
1113
format-check:
1214
uv run --locked ruff format aws_lambda_powertools tests examples --check
1315

1416
format:
1517
uv run --locked ruff format aws_lambda_powertools tests examples
1618

1719
lint: format
20+
$(MAKE) lint-check
21+
22+
lint-check:
1823
uv run --locked ruff check aws_lambda_powertools tests examples
1924

2025
lint-docs:
@@ -28,7 +33,7 @@ test:
2833
uv run --locked pytest --cache-clear tests/performance
2934

3035
test-dependencies:
31-
uv run --locked nox --error-on-external-run --reuse-venv=yes --non-interactive
36+
uv run --locked nox --error-on-external-run --reuse-venv=no --non-interactive
3237

3338
test-pydanticv2:
3439
uv run --locked pytest -m "not perf" --ignore tests/e2e

‎noxfile.py‎

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Run nox tests
22
#
33
# usage:
4-
# uv run --locked nox --error-on-external-run --reuse-venv=yes --non-interactive
4+
# uv run --locked nox --error-on-external-run --reuse-venv=no --non-interactive
55
#
66
# If you want to target a specific Python version, add -p parameter
77
from __future__ import annotations
@@ -14,7 +14,12 @@
1414
PREFIX_TESTS_UNIT = "tests/unit"
1515

1616

17-
def build_and_run_test(session: nox.Session, folders: list, extras: str = "") -> None:
17+
def build_and_run_test(
18+
session: nox.Session,
19+
folders: list,
20+
extras: str = "",
21+
pytest_args: tuple[str, ...] = (),
22+
) -> None:
1823
"""
1924
This function is responsible for setting up the testing environment and running the test suite for specific feature.
2025
@@ -32,20 +37,29 @@ def build_and_run_test(session: nox.Session, folders: list, extras: str = "") ->
3237
extras: Optional[str]
3338
A string representing additional dependencies that should be installed for the test environment.
3439
If not provided, the function will install the project with basic dependencies
40+
pytest_args: tuple[str, ...]
41+
Additional pytest arguments for selecting tests with matching dependencies
3542
"""
3643

3744
# Required install to execute any test
38-
# Shared fixtures use JSON Schema validation; previously Poetry installed it transitively.
39-
session.install("pytest", "pytest-mock", "pytest_socket", "pytest-asyncio", "fastjsonschema")
45+
session.install("pytest", "pytest-mock", "pytest_socket", "pytest-asyncio")
4046

4147
# Powertools project folder is in the root
4248
if extras:
4349
session.install(f"./[{extras}]")
4450
else:
4551
session.install("./")
52+
session.run(
53+
"python",
54+
"-I",
55+
"-c",
56+
"from importlib.util import find_spec; "
57+
"assert find_spec('fastjsonschema') is None, "
58+
"'fastjsonschema leaked into the required-dependencies environment'",
59+
)
4660

4761
# Execute test in specific folders
48-
session.run("pytest", *folders)
62+
session.run("pytest", *folders, *pytest_args)
4963

5064

5165
@nox.session()
@@ -72,6 +86,7 @@ def test_with_only_required_packages(session: nox.Session):
7286
f"{PREFIX_TESTS_FUNCTIONAL}/batch/required_dependencies/",
7387
f"{PREFIX_TESTS_FUNCTIONAL}/kafka_consumer/required_dependencies/",
7488
],
89+
pytest_args=("-m", "not requires_validation"),
7590
)
7691

7792

@@ -121,6 +136,7 @@ def test_with_boto3_sdk_as_required_package(session: nox.Session):
121136
f"{PREFIX_TESTS_FUNCTIONAL}/idempotency/_boto3/",
122137
],
123138
extras="aws-sdk",
139+
pytest_args=("-m", "not requires_validation"),
124140
)
125141

126142

@@ -132,8 +148,10 @@ def test_with_fastjsonschema_as_required_package(session: nox.Session):
132148
session,
133149
folders=[
134150
f"{PREFIX_TESTS_FUNCTIONAL}/validator/_fastjsonschema/",
151+
f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/required_dependencies/test_api_middlewares.py",
135152
],
136153
extras="validation",
154+
pytest_args=("-m", "requires_validation"),
137155
)
138156

139157

@@ -170,6 +188,29 @@ def test_with_pydantic_required_package(session: nox.Session):
170188
f"{PREFIX_TESTS_UNIT}/event_handler/_pydantic/",
171189
],
172190
extras="parser",
191+
pytest_args=("-m", "not requires_validation"),
192+
)
193+
194+
195+
@nox.session()
196+
def test_with_parser_and_validation_required_packages(session: nox.Session):
197+
"""Validate generated OpenAPI schemas with Parser and Validation dependencies."""
198+
build_and_run_test(
199+
session,
200+
folders=[f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/_pydantic/"],
201+
extras="parser,validation",
202+
pytest_args=("-m", "requires_validation"),
203+
)
204+
205+
206+
@nox.session()
207+
def test_with_boto3_and_validation_required_packages(session: nox.Session):
208+
"""Test Idempotency combined with the Validation decorator."""
209+
build_and_run_test(
210+
session,
211+
folders=[f"{PREFIX_TESTS_FUNCTIONAL}/idempotency/_boto3/"],
212+
extras="aws-sdk,validation",
213+
pytest_args=("-m", "requires_validation"),
173214
)
174215

175216

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ addopts = "-ra -vv"
225225
testpaths = "./tests"
226226
markers = [
227227
"perf: marks perf tests to be deselected (deselect with '-m \"not perf\"')",
228+
"requires_validation: tests that also require the validation extra",
228229
]
229230

230231
[tool.ty]

‎tests/functional/event_handler/_pydantic/conftest.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import json
44

5-
import fastjsonschema
65
import pytest
76

87
from aws_lambda_powertools.event_handler.openapi.models import APIKey, APIKeyIn
@@ -99,6 +98,8 @@ def pydanticv2_only():
9998
def openapi30_schema():
10099
from urllib.request import urlopen
101100

101+
import fastjsonschema
102+
102103
f = urlopen("https://spec.openapis.org/oas/3.0/schema/2021-09-28")
103104
data = json.loads(f.read().decode("utf-8"))
104105
return fastjsonschema.compile(
@@ -111,6 +112,8 @@ def openapi30_schema():
111112
def openapi31_schema():
112113
from urllib.request import urlopen
113114

115+
import fastjsonschema
116+
114117
f = urlopen("https://spec.openapis.org/oas/3.1/schema/2022-10-07")
115118
data = json.loads(f.read().decode("utf-8"))
116119
return fastjsonschema.compile(

‎tests/functional/event_handler/_pydantic/test_bedrock_agent.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def send_reminders(
186186

187187

188188
@pytest.mark.usefixtures("pydanticv2_only")
189+
@pytest.mark.requires_validation
189190
def test_openapi_schema_for_pydanticv2(openapi30_schema):
190191
# GIVEN BedrockAgentResolver is initialized with enable_validation=True
191192
app = BedrockAgentResolver(enable_validation=True)

‎tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414

1515
@pytest.mark.usefixtures("pydanticv2_only")
16+
@pytest.mark.requires_validation
1617
def test_openapi_3_1_simple_handler(openapi31_schema):
1718
# GIVEN APIGatewayRestResolver is initialized with enable_validation=True
1819
app = APIGatewayRestResolver(enable_validation=True)
@@ -45,6 +46,7 @@ def test_openapi_3_0_with_pydantic_v2():
4546

4647

4748
@pytest.mark.usefixtures("pydanticv2_only")
49+
@pytest.mark.requires_validation
4850
def test_openapi_3_1_complex_handler(openapi31_schema):
4951
# GIVEN APIGatewayRestResolver is initialized with enable_validation=True
5052
app = APIGatewayRestResolver(enable_validation=True)

‎tests/functional/event_handler/required_dependencies/test_api_middlewares.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919
BaseMiddlewareHandler,
2020
NextMiddleware,
2121
)
22-
from aws_lambda_powertools.event_handler.middlewares.schema_validation import (
23-
SchemaValidationMiddleware,
24-
)
2522
from tests.functional.utils import load_event
2623

2724
if TYPE_CHECKING:
@@ -236,7 +233,10 @@ def get_lambda(my_id: str, name: str) -> Response:
236233
),
237234
],
238235
)
236+
@pytest.mark.requires_validation
239237
def test_pass_schema_validation(app: ApiGatewayResolver, event, validation_schema):
238+
from aws_lambda_powertools.event_handler.middlewares.schema_validation import SchemaValidationMiddleware
239+
240240
@app.post("/my/path", middlewares=[SchemaValidationMiddleware(validation_schema)])
241241
def post_lambda() -> Response:
242242
return Response(200, content_types.TEXT_HTML, "path")
@@ -266,7 +266,10 @@ def post_lambda() -> Response:
266266
),
267267
],
268268
)
269+
@pytest.mark.requires_validation
269270
def test_fail_schema_validation(app: ApiGatewayResolver, event, validation_schema):
271+
from aws_lambda_powertools.event_handler.middlewares.schema_validation import SchemaValidationMiddleware
272+
270273
@app.post("/my/path", middlewares=[SchemaValidationMiddleware(validation_schema)])
271274
def post_lambda() -> Response:
272275
return Response(200, content_types.TEXT_HTML, "Should not be returned")
@@ -300,7 +303,10 @@ def post_lambda() -> Response:
300303
),
301304
],
302305
)
306+
@pytest.mark.requires_validation
303307
def test_invalid_schema_validation(app: ApiGatewayResolver, event):
308+
from aws_lambda_powertools.event_handler.middlewares.schema_validation import SchemaValidationMiddleware
309+
304310
@app.post("/my/path", middlewares=[SchemaValidationMiddleware(inbound_schema="schema.json")])
305311
def post_lambda() -> Response:
306312
return Response(200, content_types.TEXT_HTML, "Should not be returned")

0 commit comments

Comments
 (0)