Skip to content

Commit

Permalink
feat!: add Python 3.13, drop 3.8
Browse files Browse the repository at this point in the history
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 <josh@jpellis.me>
  • Loading branch information
JP-Ellis committed Oct 9, 2024
1 parent d582e4b commit 393190b
Show file tree
Hide file tree
Showing 36 changed files with 160 additions and 123 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- master

env:
STABLE_PYTHON_VERSION: "3.12"
STABLE_PYTHON_VERSION: "3.13"
FORCE_COLOR: "1"
HATCH_VERBOSE: "1"

Expand Down
14 changes: 5 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
24 changes: 15 additions & 9 deletions docs/scripts/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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())
7 changes: 5 additions & 2 deletions examples/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions examples/src/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}")
Expand Down
10 changes: 5 additions & 5 deletions examples/src/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/<int:uid>")
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.
Expand All @@ -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,
Expand All @@ -129,7 +129,7 @@ def create_user() -> Response:


@app.route("/users/<int:uid>", 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]
Expand Down
6 changes: 3 additions & 3 deletions examples/src/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from __future__ import annotations

from pathlib import Path
from typing import Any, Dict, Union
from typing import Any


class Filesystem:
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions examples/tests/test_00_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
9 changes: 6 additions & 3 deletions examples/tests/test_01_provider_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")


Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions examples/tests/test_01_provider_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion examples/tests/test_02_message_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions examples/tests/test_03_message_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,15 +38,15 @@
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",
"contents": "Hello world!",
}


def generate_read_message() -> Dict[str, str]:
def generate_read_message() -> dict[str, str]:
return {
"action": "READ",
"path": "test.txt",
Expand Down
3 changes: 2 additions & 1 deletion examples/tests/v3/basic_flask_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 393190b

Please sign in to comment.