From 7e39fcc171ae80b5bc5deaf793b4052495e42c76 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 20 Sep 2024 10:29:45 +1000 Subject: [PATCH] feat!: add Python 3.13, drop 3.8 With the release of Python 3.13 and Python 3.8 no longer being maintained, we update the supported Python versions of Pact Python to match what is currently maintained. BREAKING CHANGE: Python 3.8 support dropped Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++------ .pre-commit-config.yaml | 2 +- docs/scripts/other.py | 24 ++++++++------ examples/conftest.py | 7 +++-- examples/src/fastapi.py | 4 +-- examples/src/flask.py | 10 +++--- examples/src/message.py | 6 ++-- examples/tests/test_00_consumer.py | 7 +++-- examples/tests/test_01_provider_fastapi.py | 9 ++++-- examples/tests/test_01_provider_flask.py | 9 ++++-- examples/tests/test_02_message_consumer.py | 3 +- examples/tests/test_03_message_provider.py | 6 ++-- examples/tests/v3/basic_flask_server.py | 3 +- examples/tests/v3/provider_server.py | 9 ++++-- examples/tests/v3/test_00_consumer.py | 7 +++-- examples/tests/v3/test_01_fastapi_provider.py | 6 ++-- examples/tests/v3/test_02_message_consumer.py | 12 +++---- examples/tests/v3/test_03_message_provider.py | 3 +- hatch_build.py | 20 +++++++----- pyproject.toml | 9 +++--- src/pact/v3/ffi.py | 14 ++++----- src/pact/v3/generate/__init__.py | 3 +- src/pact/v3/generate/generator.py | 4 ++- src/pact/v3/interaction/_http_interaction.py | 4 ++- src/pact/v3/match/__init__.py | 3 +- src/pact/v3/match/matcher.py | 4 +-- src/pact/v3/pact.py | 19 +++++------- tests/v3/compatibility_suite/conftest.py | 5 +-- .../compatibility_suite/test_v3_consumer.py | 5 ++- .../test_v3_http_matching.py | 2 +- .../test_v3_message_consumer.py | 6 ++-- .../test_v3_message_producer.py | 4 ++- .../compatibility_suite/test_v4_consumer.py | 5 ++- tests/v3/test_http_interaction.py | 31 ++++++++++++------- 36 files changed, 160 insertions(+), 123 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e93ce195a8..e6e909fca7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - STABLE_PYTHON_VERSION: "3.12" + STABLE_PYTHON_VERSION: "3.13" HATCH_VERBOSE: "1" FORCE_COLOR: "1" CIBW_BUILD_FRONTEND: build diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c1ca1c5f71..7a09bc6e31 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,7 +11,7 @@ on: - master env: - STABLE_PYTHON_VERSION: "3.12" + STABLE_PYTHON_VERSION: "3.13" FORCE_COLOR: "1" HATCH_VERBOSE: "1" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5859ce1c4e..18d09e6eee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ concurrency: cancel-in-progress: true env: - STABLE_PYTHON_VERSION: "3.12" + STABLE_PYTHON_VERSION: "3.13" PYTEST_ADDOPTS: --color=yes HATCH_VERBOSE: "1" FORCE_COLOR: "1" @@ -67,12 +67,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] experimental: [false] include: - # Run tests against the next Python version, but no need for the full list of OSes. os: ubuntu-latest - python-version: "3.13.0-alpha.0 - 3.13" + python-version: "3.14" experimental: true steps: @@ -133,16 +133,12 @@ jobs: fail-fast: false matrix: os: [windows-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - # Python 3.8 and 3.9 aren't supported on macos-latest (ARM) + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # Python 3.9 aren't supported on macos-latest (ARM) exclude: - - os: macos-latest - python-version: "3.8" - os: macos-latest python-version: "3.9" include: - - os: macos-13 - python-version: "3.8" - os: macos-13 python-version: "3.9" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 88ae376e16..a30f8afc98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -81,5 +81,5 @@ repos: entry: hatch run mypy language: system types: [python] - exclude: ^(src/pact|tests|examples/tests)/(?!v3/).*\.py$ + exclude: ^(src/pact|tests|examples|examples/tests)/(?!v3/).*\.py$ stages: [pre-push] diff --git a/docs/scripts/other.py b/docs/scripts/other.py index e6f16148ef..7a309b2316 100644 --- a/docs/scripts/other.py +++ b/docs/scripts/other.py @@ -84,10 +84,13 @@ def is_binary(buffer: bytes) -> bool: if is_binary(buf): if source_path.stat().st_size < 16 * 2**20: # Copy the file only if it's less than 16MB. - with Path(source_path).open("rb") as fi, mkdocs_gen_files.open( - dest_path, - "wb", - ) as fd: + with ( + Path(source_path).open("rb") as fi, + mkdocs_gen_files.open( + dest_path, + "wb", + ) as fd, + ): fd.write(fi.read()) else: # File is too big, create a redirect. @@ -109,9 +112,12 @@ def is_binary(buffer: bytes) -> bool: ) else: - with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open( - dest_path, - "w", - encoding="utf-8", - ) as fd: + with ( + Path(source_path).open("r", encoding="utf-8") as fi, + mkdocs_gen_files.open( + dest_path, + "w", + encoding="utf-8", + ) as fd, + ): fd.write(fi.read()) diff --git a/examples/conftest.py b/examples/conftest.py index b8b06bddc9..a9c7784f13 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -13,12 +13,15 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Generator, Union +from typing import TYPE_CHECKING, Any import pytest from testcontainers.compose import DockerCompose # type: ignore[import-untyped] from yarl import URL +if TYPE_CHECKING: + from collections.abc import Generator + EXAMPLE_DIR = Path(__file__).parent.resolve() @@ -34,7 +37,7 @@ def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: Otherwise, the Pact broker is started in a container. The URL of the containerised broker is then returned. """ - broker_url: Union[str, None] = request.config.getoption("--broker-url") + broker_url: str | None = request.config.getoption("--broker-url") # If we have been given a broker URL, there's nothing more to do here and we # can return early. diff --git a/examples/src/fastapi.py b/examples/src/fastapi.py index 91558a8df1..c9e919c084 100644 --- a/examples/src/fastapi.py +++ b/examples/src/fastapi.py @@ -29,7 +29,7 @@ import logging from datetime import datetime, timezone -from typing import Annotated, Any, Dict, Optional +from typing import Annotated, Any, Optional from pydantic import BaseModel, PlainSerializer @@ -90,7 +90,7 @@ def __repr__(self) -> str: be mocked out to avoid the need for a real database. An example of this can be found in the [test suite][examples.tests.test_01_provider_fastapi]. """ -FAKE_DB: Dict[int, User] = {} +FAKE_DB: dict[int, User] = {} @app.get("/users/{uid}") diff --git a/examples/src/flask.py b/examples/src/flask.py index 256616434d..4d3b09a4cb 100644 --- a/examples/src/flask.py +++ b/examples/src/flask.py @@ -22,7 +22,7 @@ import logging from dataclasses import dataclass from datetime import datetime, timezone -from typing import Any, Dict, Tuple +from typing import Any from flask import Flask, Response, abort, jsonify, request @@ -89,11 +89,11 @@ def dict(self) -> dict[str, Any]: be mocked out to avoid the need for a real database. An example of this can be found in the [test suite][examples.tests.test_01_provider_flask]. """ -FAKE_DB: Dict[int, User] = {} +FAKE_DB: dict[int, User] = {} @app.route("/users/") -def get_user_by_id(uid: int) -> Response | Tuple[Response, int]: +def get_user_by_id(uid: int) -> Response | tuple[Response, int]: """ Fetch a user by their ID. @@ -114,7 +114,7 @@ def create_user() -> Response: if request.json is None: abort(400, description="Invalid JSON data") - user: Dict[str, Any] = request.json + user: dict[str, Any] = request.json uid = len(FAKE_DB) FAKE_DB[uid] = User( id=uid, @@ -129,7 +129,7 @@ def create_user() -> Response: @app.route("/users/", methods=["DELETE"]) -def delete_user(uid: int) -> Tuple[str | Response, int]: +def delete_user(uid: int) -> tuple[str | Response, int]: if uid not in FAKE_DB: return jsonify({"detail": "User not found"}), 404 del FAKE_DB[uid] diff --git a/examples/src/message.py b/examples/src/message.py index 8157199034..13f14c49f3 100644 --- a/examples/src/message.py +++ b/examples/src/message.py @@ -10,7 +10,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Dict, Union +from typing import Any class Filesystem: @@ -58,7 +58,7 @@ def __init__(self) -> None: """ self.fs = Filesystem() - def process(self, event: Dict[str, Any]) -> Union[str, None]: + def process(self, event: dict[str, Any]) -> str | None: """ Process an event from the queue. @@ -84,7 +84,7 @@ def process(self, event: Dict[str, Any]) -> Union[str, None]: raise ValueError(msg) @staticmethod - def validate_event(event: Union[Dict[str, Any], Any]) -> None: # noqa: ANN401 + def validate_event(event: dict[str, Any] | Any) -> None: # noqa: ANN401 """ Validates the event received from the queue. diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py index 273b4405de..5fed1e4498 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/tests/test_00_consumer.py @@ -17,7 +17,7 @@ import logging from http import HTTPStatus -from typing import TYPE_CHECKING, Any, Dict, Generator +from typing import TYPE_CHECKING, Any import pytest import requests @@ -27,6 +27,7 @@ from pact import Consumer, Format, Like, Provider if TYPE_CHECKING: + from collections.abc import Generator from pathlib import Path from pact.pact import Pact @@ -104,7 +105,7 @@ def test_get_existing_user(pact: Pact, user_consumer: UserConsumer) -> None: # what it needs from the provider (as opposed to the full schema). Should # the provider later decide to add or remove fields, Pact's consumer-driven # approach will ensure that interaction is still valid. - expected: Dict[str, Any] = { + expected: dict[str, Any] = { "id": Format().integer, "name": "Verna Hampton", "created_on": Format().iso_8601_datetime(), @@ -154,7 +155,7 @@ def test_create_user(pact: Pact, user_consumer: UserConsumer) -> None: status code is 200 and the response body matches the expected user data. """ body = {"name": "Verna Hampton"} - expected_response: Dict[str, Any] = { + expected_response: dict[str, Any] = { "id": 124, "name": "Verna Hampton", "created_on": Format().iso_8601_datetime(), diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index 1d77f5446e..f0328b4f20 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -27,7 +27,7 @@ import time from datetime import datetime, timezone from multiprocessing import Process -from typing import Any, Dict, Generator, Union +from typing import TYPE_CHECKING, Any, Optional from unittest.mock import MagicMock import pytest @@ -38,6 +38,9 @@ from examples.src.fastapi import User, app from pact import Verifier # type: ignore[import-untyped] +if TYPE_CHECKING: + from collections.abc import Generator + PROVIDER_URL = URL("http://localhost:8080") @@ -51,7 +54,7 @@ class ProviderState(BaseModel): @app.post("/_pact/provider_states") async def mock_pact_provider_states( state: ProviderState, -) -> Dict[str, Union[str, None]]: +) -> dict[str, Optional[str]]: """ Define the provider state. @@ -146,7 +149,7 @@ def mock_post_request_to_create_user() -> None: """ import examples.src.fastapi - local_db: Dict[int, User] = {} + local_db: dict[int, User] = {} def local_setitem(key: int, value: User) -> None: local_db[key] = value diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 797e25da80..bc6d2e037d 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -27,7 +27,7 @@ import time from datetime import datetime, timezone from multiprocessing import Process -from typing import Any, Dict, Generator, Union +from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock import pytest @@ -37,11 +37,14 @@ from flask import request from pact import Verifier # type: ignore[import-untyped] +if TYPE_CHECKING: + from collections.abc import Generator + PROVIDER_URL = URL("http://localhost:8080") @app.route("/_pact/provider_states", methods=["POST"]) -async def mock_pact_provider_states() -> Dict[str, Union[str, None]]: +async def mock_pact_provider_states() -> dict[str, str | None]: """ Define the provider state. @@ -139,7 +142,7 @@ def mock_post_request_to_create_user() -> None: """ import examples.src.flask - local_db: Dict[int, User] = {} + local_db: dict[int, User] = {} def local_setitem(key: int, value: User) -> None: local_db[key] = value diff --git a/examples/tests/test_02_message_consumer.py b/examples/tests/test_02_message_consumer.py index f5689b8397..1498196c7e 100644 --- a/examples/tests/test_02_message_consumer.py +++ b/examples/tests/test_02_message_consumer.py @@ -31,7 +31,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Generator +from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock import pytest @@ -40,6 +40,7 @@ from pact import MessageConsumer, MessagePact, Provider if TYPE_CHECKING: + from collections.abc import Generator from pathlib import Path from yarl import URL diff --git a/examples/tests/test_03_message_provider.py b/examples/tests/test_03_message_provider.py index 9a9ff3f7d4..c657d5d2c3 100644 --- a/examples/tests/test_03_message_provider.py +++ b/examples/tests/test_03_message_provider.py @@ -26,7 +26,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING from flask import Flask from pact import MessageProvider @@ -38,7 +38,7 @@ PACT_DIR = (Path(__file__).parent / "pacts").resolve() -def generate_write_message() -> Dict[str, str]: +def generate_write_message() -> dict[str, str]: return { "action": "WRITE", "path": "test.txt", @@ -46,7 +46,7 @@ def generate_write_message() -> Dict[str, str]: } -def generate_read_message() -> Dict[str, str]: +def generate_read_message() -> dict[str, str]: return { "action": "READ", "path": "test.txt", diff --git a/examples/tests/v3/basic_flask_server.py b/examples/tests/v3/basic_flask_server.py index 18b3c0c954..1dfbd406f6 100644 --- a/examples/tests/v3/basic_flask_server.py +++ b/examples/tests/v3/basic_flask_server.py @@ -8,12 +8,13 @@ import subprocess import sys import time +from collections.abc import Generator from contextlib import contextmanager from datetime import datetime from pathlib import Path from random import randint, uniform from threading import Thread -from typing import Generator, NoReturn +from typing import NoReturn import requests from yarl import URL diff --git a/examples/tests/v3/provider_server.py b/examples/tests/v3/provider_server.py index 23ca0e2ad6..9b42ec4e74 100644 --- a/examples/tests/v3/provider_server.py +++ b/examples/tests/v3/provider_server.py @@ -15,7 +15,7 @@ from importlib import import_module from pathlib import Path from threading import Thread -from typing import Generator, NoReturn, Tuple +from typing import TYPE_CHECKING, NoReturn import requests @@ -25,6 +25,9 @@ import flask +if TYPE_CHECKING: + from collections.abc import Generator + logger = logging.getLogger(__name__) @@ -82,7 +85,7 @@ def ping() -> str: return "pong" @self.app.route(self.produce_message_url, methods=["POST"]) - def produce_message() -> flask.Response | Tuple[str, int]: + def produce_message() -> flask.Response | tuple[str, int]: """ Route a message request to the handler function. @@ -101,7 +104,7 @@ def produce_message() -> flask.Response | Tuple[str, int]: return str(e), 500 @self.app.route(self.set_provider_state_url, methods=["POST"]) - def set_provider_state() -> Tuple[str, int]: + def set_provider_state() -> tuple[str, int]: """ Calls the state provider function with the state provided in the request. diff --git a/examples/tests/v3/test_00_consumer.py b/examples/tests/v3/test_00_consumer.py index 523d55d252..7eeda0acdd 100644 --- a/examples/tests/v3/test_00_consumer.py +++ b/examples/tests/v3/test_00_consumer.py @@ -18,9 +18,10 @@ """ import json +from collections.abc import Generator from datetime import datetime, timezone from pathlib import Path -from typing import Any, Dict, Generator +from typing import Any import pytest import requests @@ -72,7 +73,7 @@ def test_get_existing_user(pact: Pact) -> None: code as shown in [`test_get_non_existent_user`](#test_get_non_existent_user). """ - expected: Dict[str, Any] = { + expected: dict[str, Any] = { "id": 123, "name": "Verna Hampton", "created_on": match.datetime( @@ -136,7 +137,7 @@ def test_create_user(pact: Pact) -> None: status code is 200 and the response body matches the expected user data. """ body = {"name": "Verna Hampton"} - expected_response: Dict[str, Any] = { + expected_response: dict[str, Any] = { "id": 124, "name": "Verna Hampton", "created_on": match.datetime( diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/tests/v3/test_01_fastapi_provider.py index 25174173e2..1e181a4bb1 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/tests/v3/test_01_fastapi_provider.py @@ -29,7 +29,7 @@ import time from datetime import datetime, timezone from multiprocessing import Process -from typing import TYPE_CHECKING, Callable, Dict, Literal +from typing import TYPE_CHECKING, Callable, Literal from unittest.mock import MagicMock import uvicorn @@ -45,7 +45,7 @@ async def mock_pact_provider_states( action: Literal["setup", "teardown"], state: str, -) -> Dict[Literal["result"], str]: +) -> dict[Literal["result"], str]: """ Handler for the provider state callback. @@ -226,7 +226,7 @@ def mock_post_request_to_create_user() -> None: """ import examples.src.fastapi - local_db: Dict[int, User] = {} + local_db: dict[int, User] = {} def local_setitem(key: int, value: User) -> None: local_db[key] = value diff --git a/examples/tests/v3/test_02_message_consumer.py b/examples/tests/v3/test_02_message_consumer.py index 58dc405c16..a3d8b96281 100644 --- a/examples/tests/v3/test_02_message_consumer.py +++ b/examples/tests/v3/test_02_message_consumer.py @@ -13,8 +13,6 @@ from typing import ( TYPE_CHECKING, Any, - Dict, - Generator, ) from unittest.mock import MagicMock @@ -24,7 +22,7 @@ from pact.v3.pact import Pact if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Generator log = logging.getLogger(__name__) @@ -90,7 +88,7 @@ def handler() -> Handler: @pytest.fixture def verifier( handler: Handler, -) -> Generator[Callable[[str | bytes | None, Dict[str, Any]], None], Any, None]: +) -> Generator[Callable[[str | bytes | None, dict[str, Any]], None], Any, None]: """ Verifier function for the Pact. @@ -104,7 +102,7 @@ def verifier( """ assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" - def _verifier(msg: str | bytes | None, context: Dict[str, Any]) -> None: + def _verifier(msg: str | bytes | None, context: dict[str, Any]) -> None: assert msg is not None, "Message is None" data = json.loads(msg) log.info( @@ -121,7 +119,7 @@ def _verifier(msg: str | bytes | None, context: Dict[str, Any]) -> None: def test_async_message_handler_write( pact: Pact, handler: Handler, - verifier: Callable[[str | bytes | None, Dict[str, Any]], None], + verifier: Callable[[str | bytes | None, dict[str, Any]], None], ) -> None: """ Create a pact between the message handler and the message provider. @@ -161,7 +159,7 @@ def test_async_message_handler_write( def test_async_message_handler_read( pact: Pact, handler: Handler, - verifier: Callable[[str | bytes | None, Dict[str, Any]], None], + verifier: Callable[[str | bytes | None, dict[str, Any]], None], ) -> None: """ Create a pact between the message handler and the message provider. diff --git a/examples/tests/v3/test_03_message_provider.py b/examples/tests/v3/test_03_message_provider.py index 472829c1e7..e3b5d61260 100644 --- a/examples/tests/v3/test_03_message_provider.py +++ b/examples/tests/v3/test_03_message_provider.py @@ -8,7 +8,6 @@ from __future__ import annotations from pathlib import Path -from typing import Tuple from unittest.mock import MagicMock from examples.src.message_producer import FileSystemMessageProducer @@ -29,7 +28,7 @@ CURRENT_STATE: str | None = None -def message_producer_function() -> Tuple[str, str]: +def message_producer_function() -> tuple[str, str]: producer = FileSystemMessageProducer() producer.queue = MagicMock() diff --git a/hatch_build.py b/hatch_build.py index dcb65ec1c2..7960f5ffab 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -20,7 +20,7 @@ import warnings import zipfile from pathlib import Path -from typing import Any, Dict +from typing import Any import cffi import requests @@ -83,7 +83,7 @@ def clean(self, versions: list[str]) -> None: # noqa: ARG002 def initialize( self, version: str, # noqa: ARG002 - build_data: Dict[str, Any], + build_data: dict[str, Any], ) -> None: """Hook into Hatchling's build process.""" build_data["infer_tag"] = True @@ -338,9 +338,12 @@ def _pact_lib_extract(self, artifact: Path) -> None: msg = f"Unknown artifact type {artifact}" raise ValueError(msg) - with gzip.open(artifact, "rb") as f_in, ( - self.tmpdir / (artifact.name.split("-")[0] + artifact.suffixes[0]) - ).open("wb") as f_out: + with ( + gzip.open(artifact, "rb") as f_in, + (self.tmpdir / (artifact.name.split("-")[0] + artifact.suffixes[0])).open( + "wb" + ) as f_out, + ): shutil.copyfileobj(f_in, f_out) def _pact_lib_header(self, url: str) -> list[str]: @@ -364,9 +367,10 @@ def _pact_lib_header(self, url: str) -> list[str]: url = url.rsplit("/", 1)[0] + "/pact.h" artifact = self._download(url) includes: list[str] = [] - with artifact.open("r", encoding="utf-8") as f_in, ( - self.tmpdir / "pact.h" - ).open("w", encoding="utf-8") as f_out: + with ( + artifact.open("r", encoding="utf-8") as f_in, + (self.tmpdir / "pact.h").open("w", encoding="utf-8") as f_out, + ): for line in f_in: sline = line.strip() if sline.startswith("#include"): diff --git a/pyproject.toml b/pyproject.toml index f64ee97804..4a699bb96a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,16 +21,16 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Testing", ] -requires-python = ">=3.8" +requires-python = ">=3.9" # Dependencies of Pact Python should be specified using the broadest range # compatible version unless: @@ -172,7 +172,7 @@ installer = "uv" features = ["devel-test"] [[tool.hatch.envs.test.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] +python = ["3.9", "3.10", "3.11", "3.12", "3.13"] ################################################################################ ## PyTest Configuration @@ -230,7 +230,6 @@ exclude_lines = [ ################################################################################ [tool.ruff] -target-version = "py38" # TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. # https://github.com/pact-foundation/pact-python/issues/458 @@ -308,7 +307,7 @@ docstring-code-format = true ################################################################################ [tool.mypy] -exclude = '^(src/pact|tests)/(?!v3).+\.py$' +exclude = '^(src/pact|tests|examples|examples/tests)/(?!v3).+\.py$' ################################################################################ ## CI Build Wheel diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index e874aa51f2..12b80a3765 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -92,14 +92,14 @@ import typing import warnings from enum import Enum -from typing import TYPE_CHECKING, Any, List, Literal, Tuple -from typing import Generator as GeneratorType +from typing import TYPE_CHECKING, Any, Literal from pact.v3._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: import datetime from collections.abc import Collection + from collections.abc import Generator as GeneratorType from pathlib import Path import cffi @@ -1169,7 +1169,7 @@ def name(self) -> str: """ return provider_state_get_name(self) or "" - def parameters(self) -> GeneratorType[Tuple[str, str], None, None]: + def parameters(self) -> GeneratorType[tuple[str, str], None, None]: """ Provider State parameters. @@ -7035,7 +7035,7 @@ def verifier_set_publish_options( handle: VerifierHandle, provider_version: str, build_url: str | None, - provider_tags: List[str] | None, + provider_tags: list[str] | None, provider_branch: str | None, ) -> None: """ @@ -7230,10 +7230,10 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 token: str | None, enable_pending: int, include_wip_pacts_since: datetime.date | None, - provider_tags: List[str], + provider_tags: list[str], provider_branch: str | None, - consumer_version_selectors: List[str], - consumer_version_tags: List[str], + consumer_version_selectors: list[str], + consumer_version_tags: list[str], ) -> None: """ Adds a Pact broker as a source to verify. diff --git a/src/pact/v3/generate/__init__.py b/src/pact/v3/generate/__init__.py index fb1ece3d6a..ba94ca5e55 100644 --- a/src/pact/v3/generate/__init__.py +++ b/src/pact/v3/generate/__init__.py @@ -6,7 +6,7 @@ import builtins import warnings -from typing import TYPE_CHECKING, Literal, Mapping, Sequence +from typing import TYPE_CHECKING, Literal from pact.v3.generate.generator import ( Generator, @@ -15,6 +15,7 @@ from pact.v3.util import strftime_to_simple_date_format if TYPE_CHECKING: + from collections.abc import Mapping, Sequence from types import ModuleType # ruff: noqa: A001 diff --git a/src/pact/v3/generate/generator.py b/src/pact/v3/generate/generator.py index 633740be6b..c686ac088f 100644 --- a/src/pact/v3/generate/generator.py +++ b/src/pact/v3/generate/generator.py @@ -6,9 +6,11 @@ from abc import ABC, abstractmethod from itertools import chain -from typing import TYPE_CHECKING, Any, Mapping +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: + from collections.abc import Mapping + from pact.v3.types import GeneratorType diff --git a/src/pact/v3/interaction/_http_interaction.py b/src/pact/v3/interaction/_http_interaction.py index 08a40a9dfc..38e4aba52c 100644 --- a/src/pact/v3/interaction/_http_interaction.py +++ b/src/pact/v3/interaction/_http_interaction.py @@ -6,7 +6,7 @@ import json from collections import defaultdict -from typing import TYPE_CHECKING, Any, Iterable, Literal +from typing import TYPE_CHECKING, Any, Literal import pact.v3.ffi from pact.v3.interaction._base import Interaction @@ -14,6 +14,8 @@ from pact.v3.match.matcher import IntegrationJSONEncoder if TYPE_CHECKING: + from collections.abc import Iterable + try: from typing import Self except ImportError: diff --git a/src/pact/v3/match/__init__.py b/src/pact/v3/match/__init__.py index 43c9446fa3..247dbff629 100644 --- a/src/pact/v3/match/__init__.py +++ b/src/pact/v3/match/__init__.py @@ -48,7 +48,7 @@ import datetime as dt import warnings from decimal import Decimal -from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence, TypeVar, overload +from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload from pact.v3 import generate from pact.v3.match.matcher import ( @@ -62,6 +62,7 @@ from pact.v3.util import strftime_to_simple_date_format if TYPE_CHECKING: + from collections.abc import Mapping, Sequence from types import ModuleType from pact.v3.generate import Generator diff --git a/src/pact/v3/match/matcher.py b/src/pact/v3/match/matcher.py index 1a24991919..1716a9b5d5 100644 --- a/src/pact/v3/match/matcher.py +++ b/src/pact/v3/match/matcher.py @@ -10,10 +10,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from itertools import chain from json import JSONEncoder -from typing import Any, Generic, Sequence, TypeVar +from typing import Any, Generic, TypeVar from pact.v3.generate.generator import Generator from pact.v3.types import UNSET, Matchable, MatcherType, Unset diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index a6e298539f..9e69c82987 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -68,11 +68,7 @@ from typing import ( TYPE_CHECKING, Callable, - Dict, - Generator, - List, Literal, - Set, overload, ) @@ -90,6 +86,7 @@ from pact.v3.interaction._sync_message_interaction import SyncMessageInteraction if TYPE_CHECKING: + from collections.abc import Generator from types import TracebackType from pact.v3.interaction import Interaction @@ -145,7 +142,7 @@ def __init__( self._consumer = consumer self._provider = provider - self._interactions: Set[Interaction] = set() + self._interactions: set[Interaction] = set() self._handle: pact.v3.ffi.PactHandle = pact.v3.ffi.new_pact( consumer, provider, @@ -409,7 +406,7 @@ def interactions( @overload def verify( self, - handler: Callable[[str | bytes | None, Dict[str, str]], None], + handler: Callable[[str | bytes | None, dict[str, str]], None], kind: Literal["Async", "Sync"], *, raises: Literal[True] = True, @@ -417,19 +414,19 @@ def verify( @overload def verify( self, - handler: Callable[[str | bytes | None, Dict[str, str]], None], + handler: Callable[[str | bytes | None, dict[str, str]], None], kind: Literal["Async", "Sync"], *, raises: Literal[False], - ) -> List[InteractionVerificationError]: ... + ) -> list[InteractionVerificationError]: ... def verify( self, - handler: Callable[[str | bytes | None, Dict[str, str]], None], + handler: Callable[[str | bytes | None, dict[str, str]], None], kind: Literal["Async", "Sync"], *, raises: bool = True, - ) -> List[InteractionVerificationError] | None: + ) -> list[InteractionVerificationError] | None: """ Verify message interactions. @@ -463,7 +460,7 @@ def verify( process a message. If set to `False`, then the function will return a list of errors. """ - errors: List[InteractionVerificationError] = [] + errors: list[InteractionVerificationError] = [] for message in self.interactions(kind): request: pact.v3.ffi.MessageContents | None = None if isinstance(message, pact.v3.ffi.SynchronousMessage): diff --git a/tests/v3/compatibility_suite/conftest.py b/tests/v3/compatibility_suite/conftest.py index be72eebb64..bb5fdbab2b 100644 --- a/tests/v3/compatibility_suite/conftest.py +++ b/tests/v3/compatibility_suite/conftest.py @@ -7,8 +7,9 @@ import shutil import subprocess +from collections.abc import Generator from pathlib import Path -from typing import Any, Generator, Union +from typing import Any import pytest from testcontainers.compose import DockerCompose # type: ignore[import-untyped] @@ -53,7 +54,7 @@ def broker_url(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: Otherwise, the Pact broker is started in a container. The URL of the containerised broker is then returned. """ - broker_url: Union[str, None] = request.config.getoption("--broker-url") + broker_url: str | None = request.config.getoption("--broker-url") # If we have been given a broker URL, there's nothing more to do here and we # can return early. diff --git a/tests/v3/compatibility_suite/test_v3_consumer.py b/tests/v3/compatibility_suite/test_v3_consumer.py index bfc7de169b..0b1e061ff3 100644 --- a/tests/v3/compatibility_suite/test_v3_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_consumer.py @@ -5,7 +5,7 @@ import json import logging import re -from typing import Any, Generator +from typing import TYPE_CHECKING, Any from pytest_bdd import given, parsers, scenario, then @@ -15,6 +15,9 @@ the_pact_file_for_the_test_is_generated, ) +if TYPE_CHECKING: + from collections.abc import Generator + logger = logging.getLogger(__name__) ################################################################################ diff --git a/tests/v3/compatibility_suite/test_v3_http_matching.py b/tests/v3/compatibility_suite/test_v3_http_matching.py index c7116bd8a2..92b0118be6 100644 --- a/tests/v3/compatibility_suite/test_v3_http_matching.py +++ b/tests/v3/compatibility_suite/test_v3_http_matching.py @@ -3,8 +3,8 @@ import pickle import re import sys +from collections.abc import Generator from pathlib import Path -from typing import Generator import pytest from pytest_bdd import ( diff --git a/tests/v3/compatibility_suite/test_v3_message_consumer.py b/tests/v3/compatibility_suite/test_v3_message_consumer.py index aeab8958ec..fb39a3c9e1 100644 --- a/tests/v3/compatibility_suite/test_v3_message_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_message_consumer.py @@ -6,7 +6,7 @@ import json import logging import re -from typing import TYPE_CHECKING, Any, List, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from pytest_bdd import ( given, @@ -47,9 +47,9 @@ class ReceivedMessage(NamedTuple): class PactResult(NamedTuple): """Holder class for Pact Result objects.""" - messages: List[ReceivedMessage] + messages: list[ReceivedMessage] pact_data: dict[str, Any] | None - errors: List[InteractionVerificationError] + errors: list[InteractionVerificationError] def assert_type(expected_type: str, value: Any) -> None: # noqa: ANN401 diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py index b4eab3ee5d..e8087a4dc1 100644 --- a/tests/v3/compatibility_suite/test_v3_message_producer.py +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -8,7 +8,7 @@ import re import sys from pathlib import Path -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING import pytest from pytest_bdd import ( @@ -40,6 +40,8 @@ ) if TYPE_CHECKING: + from collections.abc import Generator + from yarl import URL from pact.v3.verifier import Verifier diff --git a/tests/v3/compatibility_suite/test_v4_consumer.py b/tests/v3/compatibility_suite/test_v4_consumer.py index c6e00aef05..9af176a563 100644 --- a/tests/v3/compatibility_suite/test_v4_consumer.py +++ b/tests/v3/compatibility_suite/test_v4_consumer.py @@ -4,7 +4,7 @@ import json import logging -from typing import Any, Generator +from typing import TYPE_CHECKING, Any from pytest_bdd import given, parsers, scenario, then @@ -14,6 +14,9 @@ the_pact_file_for_the_test_is_generated, ) +if TYPE_CHECKING: + from collections.abc import Generator + logger = logging.getLogger(__name__) ################################################################################ diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index a6f170c38a..77d1819356 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -201,11 +201,14 @@ async def test_set_header_request_repeat( .will_respond_with(200) ) with pact.serve(raises=False) as srv: - async with aiohttp.ClientSession(srv.url) as session, session.request( - "GET", - "/", - headers=headers, - ) as resp: + async with ( + aiohttp.ClientSession(srv.url) as session, + session.request( + "GET", + "/", + headers=headers, + ) as resp, + ): assert resp.status == 500 assert len(srv.mismatches) == 1 @@ -537,10 +540,13 @@ async def test_multipart_file_request(pact: Pact, temp_dir: Path) -> None: {"Content-Type": "image/png"}, # type: ignore[arg-type] ) - async with aiohttp.ClientSession(srv.url) as session, session.post( - "/", - data=mpwriter, - ) as resp: + async with ( + aiohttp.ClientSession(srv.url) as session, + session.post( + "/", + data=mpwriter, + ) as resp, + ): assert resp.status == 200 assert await resp.read() == b"" @@ -583,9 +589,10 @@ async def test_pact_server_verbose( .with_request("GET", "/foo") .will_respond_with(200) ) - with caplog.at_level(logging.WARNING, logger="pact.v3.pact"), pact.serve( - raises=False, verbose=True - ) as srv: + with ( + caplog.at_level(logging.WARNING, logger="pact.v3.pact"), + pact.serve(raises=False, verbose=True) as srv, + ): async with aiohttp.ClientSession(srv.url) as session: async with session.get("/bar") as resp: assert resp.status == 500