diff --git a/.github/instructions/python-tests.instructions.md b/.github/instructions/python-tests.instructions.md index e5461847e..27ca8c362 100644 --- a/.github/instructions/python-tests.instructions.md +++ b/.github/instructions/python-tests.instructions.md @@ -15,11 +15,10 @@ applyTo: "**/tests/**/*.py" [ pytest.param(v1, x1, r1, id="description1"), pytest.param(v2, x2, r2, id="description2"), - ... - ] + ..., + ], ) - def test_function(param1: Type1, param2: Type2, expected: ReturnType) -> None: - ... + def test_function(param1: Type1, param2: Type2, expected: ReturnType) -> None: ... ``` - Ensure test coverage for: diff --git a/MIGRATION.md b/MIGRATION.md index f28020154..2975ef23d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -55,16 +55,16 @@ The `Consumer` and `Provider` classes have been removed. Instead, a single `Pact ```python title="v2" from pact.v2 import Consumer, Provider -consumer = Consumer('my-web-front-end') -provider = Provider('my-backend-service') +consumer = Consumer("my-web-front-end") +provider = Provider("my-backend-service") -pact = consumer.has_pact_with(provider, pact_dir='/path/to/pacts') +pact = consumer.has_pact_with(provider, pact_dir="/path/to/pacts") ``` ```python title="v3" from pact import Pact -pact = Pact('my-web-front-end', 'my-backend-service') +pact = Pact("my-web-front-end", "my-backend-service") ``` #### Defining Interactions @@ -74,18 +74,18 @@ The v3 interface favours method chaining and provides more granular control over ```python title="v2" ( pact - .given('user exists') - .upon_receiving('a request for user data') + .given("user exists") + .upon_receiving("a request for user data") .with_request( - 'GET', - '/users/123', - headers={'Accept': 'application/json'}, - query={'include': 'profile'} + "GET", + "/users/123", + headers={"Accept": "application/json"}, + query={"include": "profile"}, ) .will_respond_with( 200, - headers={'Content-Type': 'application/json'}, - body={'id': 123, 'name': 'Alice'} + headers={"Content-Type": "application/json"}, + body={"id": 123, "name": "Alice"}, ) ) ``` @@ -93,14 +93,15 @@ The v3 interface favours method chaining and provides more granular control over ```python title="v3" ( pact - .upon_receiving('a request for user data') - .given('user exists', id=123, name='Alice') # (1) - .with_request('GET', '/users/123') - .with_header('Accept', 'application/json') - .with_query_parameter('include', 'profile') + .upon_receiving("a request for user data") + .given("user exists", id=123, name="Alice") # (1) + .with_request("GET", "/users/123") + .with_header("Accept", "application/json") + .with_query_parameter("include", "profile") .will_respond_with(200) - .with_header('Content-Type', 'application/json') - .with_body({'id': 123, 'name': 'Alice'}, content_type='application/json')) + .with_header("Content-Type", "application/json") + .with_body({"id": 123, "name": "Alice"}, content_type="application/json") +) ``` 1. In v2, there was limited support for parameterizing provider states, and each state variation often required a separate definition. For example, `given("user Alice exists with id 123")` and `given("user Bob exists with id 456")` would be two distinct states, which would then need to be handled separately in the provider state setup. @@ -122,7 +123,7 @@ pact = Consumer("my-consumer").has_pact_with( # Context manager automatically calls setup() and verify() with pact: - response = requests.get(pact.uri + '/users/123') + response = requests.get(pact.uri + "/users/123") # Pact file written automatically on exit ``` @@ -138,7 +139,7 @@ pact.start_service() pact.setup() # Configure interactions # Make requests -response = requests.get(pact.uri + '/users/123') +response = requests.get(pact.uri + "/users/123") # Assertions... # Verify and stop @@ -168,9 +169,9 @@ with pact: ``` ```python title="v3" -pact = Pact('consumer', 'provider') +pact = Pact("consumer", "provider") # Define interactions and run tests... -pact.write_file('/path/to/pacts') +pact.write_file("/path/to/pacts") ``` #### Matchers @@ -181,18 +182,18 @@ Support for matchers has been greatly expanded and improved in the v3 API. The o from pact.v2.matchers import Like, EachLike, Regex, Term # Usage: -Like({'id': 123}) -EachLike({'item': 'value'}) -Regex('hello world', r'^hello') +Like({"id": 123}) +EachLike({"item": "value"}) +Regex("hello world", r"^hello") ``` ```python title="v3" from pact import match # Usage: -match.like({'id': 123}) -match.each_like({'item': 'value'}) -match.regex('hello world', r'^hello') +match.like({"id": 123}) +match.each_like({"item": "value"}) +match.regex("hello world", r"^hello") ``` For a full list of available matchers and their usage, refer to the [API documentation][pact.match]. @@ -207,34 +208,29 @@ The provider verification API has been completely redesigned in v3 to provide a from pact.v2 import Provider, Verifier # Create separate Provider and Verifier instances -provider = Provider('my-provider') -verifier = Verifier(provider, 'http://localhost:8080') +provider = Provider("my-provider") +verifier = Verifier(provider, "http://localhost:8080") ``` ```python title="v3" from pact import Verifier # Single Verifier instance with provider name -verifier = Verifier('my-provider') +verifier = Verifier("my-provider") ``` The protocol specification is now done through the `add_transport` method, which allows for more flexible configuration and supports multiple transports if needed. ```python title="v2" -verifier = Verifier(provider, 'http://localhost:8080') +verifier = Verifier(provider, "http://localhost:8080") ``` ```python title="v3" verifier = ( - Verifier('my-provider') - .add_transport(url='http://localhost:8080') + Verifier("my-provider") + .add_transport(url="http://localhost:8080") # Or more granular control: - .add_transport( - protocol='http', - port=8080, - path='/api/v1', - scheme='https' - ) + .add_transport(protocol="http", port=8080, path="/api/v1", scheme="https") ) ``` @@ -246,18 +242,17 @@ Support for both local files and Pact Brokers is retained in v3, with the `verif ```python title="v2" success, logs = verifier.verify_pacts( - './pacts/consumer1-provider.json', - './pacts/consumer2-provider.json' + "./pacts/consumer1-provider.json", "./pacts/consumer2-provider.json" ) ``` ```python title="v3" verifier = ( - Verifier('my-provider') + Verifier("my-provider") # It can discover all Pact files in a directory - .add_source('./pacts/') + .add_source("./pacts/") # Or read individual files - .add_source('./pacts/specific-consumer.json') + .add_source("./pacts/specific-consumer.json") ) ``` @@ -267,33 +262,25 @@ verifier = ( ```python title="v2" success, logs = verifier.verify_with_broker( - broker_url='https://pact-broker.example.com', - broker_username='username', - broker_password='password' + broker_url="https://pact-broker.example.com", + broker_username="username", + broker_password="password", ) ``` ```python title="v3" -verifier = ( - Verifier('my-provider') - .broker_source( - 'https://pact-broker.example.com', - username='username', - password='password' - ) +verifier = Verifier("my-provider").broker_source( + "https://pact-broker.example.com", username="username", password="password" ) # Or with selectors for more control broker_builder = ( verifier - .broker_source( - 'https://pact-broker.example.com', - selector=True - ) + .broker_source("https://pact-broker.example.com", selector=True) .include_pending() - .provider_branch('main') - .consumer_version(branch='main') - .consumer_version(branch='develop') + .provider_branch("main") + .consumer_version(branch="main") + .consumer_version(branch="develop") .build() ) ``` @@ -312,21 +299,21 @@ The old v2 API required the provider to expose an HTTP endpoint dedicated to han ```python title="v2" success, logs = verifier.verify_pacts( - './pacts/consumer-provider.json', - provider_states_setup_url='http://localhost:8080/_pact/provider_states' + "./pacts/consumer-provider.json", + provider_states_setup_url="http://localhost:8080/_pact/provider_states", ) ``` ```python title="v3" # Option 1: URL-based (similar to v2) verifier = ( - Verifier('my-provider') - .add_transport(url='http://localhost:8080') + Verifier("my-provider") + .add_transport(url="http://localhost:8080") .state_handler( - 'http://localhost:8080/_pact/provider_states', - body=True # (1) + "http://localhost:8080/_pact/provider_states", + body=True, # (1) ) - .add_source('./pacts/') + .add_source("./pacts/") ) ``` @@ -342,32 +329,32 @@ verifier = ( ```python title="v3 - Function" def handler(name, params=None): - if name == 'user exists': + if name == "user exists": # Set up user in database/mock - create_user(params.get('id', 123)) - elif name == 'no users exist': + create_user(params.get("id", 123)) + elif name == "no users exist": # Clear users clear_users() verifier = ( - Verifier('my-provider') - .add_transport(url='http://localhost:8080') + Verifier("my-provider") + .add_transport(url="http://localhost:8080") .state_handler(handler) - .add_source('./pacts/') + .add_source("./pacts/") ) ``` ```python title="v3 - Mapping" state_handlers = { - 'user exists': lambda name, params: create_user(params.get('id', 123)), - 'no users exist': lambda name, params: clear_users(), + "user exists": lambda name, params: create_user(params.get("id", 123)), + "no users exist": lambda name, params: clear_users(), } verifier = ( - Verifier('my-provider') - .add_transport(url='http://localhost:8080') + Verifier("my-provider") + .add_transport(url="http://localhost:8080") .state_handler(state_handlers) - .add_source('./pacts/') + .add_source("./pacts/") ) ``` @@ -385,33 +372,24 @@ Message verification is now much more straightforward in v3, with a a similar in ```python title="v3 - Functional Handler" def message_handler(description, metadata): - if description == 'user created event': - return { - 'id': 123, - 'name': 'Alice', - 'event': 'created' - } - elif description == 'user deleted event': - return {'id': 123, 'event': 'deleted'} + if description == "user created event": + return {"id": 123, "name": "Alice", "event": "created"} + elif description == "user deleted event": + return {"id": 123, "event": "deleted"} + verifier = ( - Verifier('my-provider') - .message_handler(message_handler) - .add_source('./pacts/') + Verifier("my-provider").message_handler(message_handler).add_source("./pacts/") ) ``` ```python title="v3 - Dictionary Mapping" messages = { - 'user created event': {'id': 123, 'name': 'Alice', 'event': 'created'}, - 'user deleted event': lambda desc, meta: {'id': 123, 'event': 'deleted'} + "user created event": {"id": 123, "name": "Alice", "event": "created"}, + "user deleted event": lambda desc, meta: {"id": 123, "event": "deleted"}, } -verifier = ( - Verifier('my-provider') - .message_handler(messages) - .add_source('./pacts/') -) +verifier = Verifier("my-provider").message_handler(messages).add_source("./pacts/") ``` #### Running Verification @@ -419,7 +397,7 @@ verifier = ( Verification has been simplified and no longer requires checking return codes. Instead, the `verify()` method raises an exception on failure, or returns normally on success. ```python title="v2" -success, logs = verifier.verify_pacts('./pacts/consumer-provider.json') +success, logs = verifier.verify_pacts("./pacts/consumer-provider.json") if not success: print(logs) raise AssertionError("Verification failed!") diff --git a/docs/blog/posts/2024/07-26 asynchronous message support.md b/docs/blog/posts/2024/07-26 asynchronous message support.md index 2214841c6..4ebf72fc6 100644 --- a/docs/blog/posts/2024/07-26 asynchronous message support.md +++ b/docs/blog/posts/2024/07-26 asynchronous message support.md @@ -75,19 +75,19 @@ from pact import Pact from my_consumer import process_message + def handler(body: str | bytes | None, metadata: dict[str, Any]) -> None: message = json.loads(body) process_message(message) + pact = Pact( consumer="deleteUserService", provider="someProvider", ).with_specification("V3") # (1) ( - pact - .upon_receiving("a request to delete a user", "Async") - .with_body( + pact.upon_receiving("a request to delete a user", "Async").with_body( json.dumps({ "action": "delete_user", "user_id": "123", @@ -114,6 +114,7 @@ As the underlying protocol is abstracted away, Pact uses a local HTTP server to ```python from pact import Verifier + class Provider: """ A simple HTTP provider that sends messages to the consumer. @@ -121,6 +122,7 @@ class Provider: This would typically use the same underlying functions that would generate messages, except that instead of being sent into the message queue, they are sent to the consumer's HTTP server. """ + provider = Provider() ( @@ -132,7 +134,7 @@ provider = Provider() protocol="message", path="/_pact/message", ) - ) +) ``` 1. The provider URL is required, but is only used if the Pact being verified contains both HTTP and message interactions. It is not used for message interactions, and should the Pact not contain any HTTP interactions, the endpoint need not be active. diff --git a/docs/blog/posts/2024/12-30 functional arguments.md b/docs/blog/posts/2024/12-30 functional arguments.md index 8e8cc439e..e7d4fd476 100644 --- a/docs/blog/posts/2024/12-30 functional arguments.md +++ b/docs/blog/posts/2024/12-30 functional arguments.md @@ -92,6 +92,7 @@ The new `state_handler` method replaces the `set_state` method and simplifies th ```python from pact import Verifier + def provider_state_callback( name: str, # (1) action: Literal["setup", "teardown"], # (2) @@ -118,6 +119,7 @@ def provider_state_callback( """ ... + def test_provider(): verifier = Verifier("provider_name") verifier.state_handler(provider_state_callback, teardown=True) @@ -140,11 +142,12 @@ This snippet showcases a way to set up the provider state with a function that i ```python from pact import Verifier + def provider_state_callback( name: str, parameters: dict[str, Any] | None, - ) -> None: - ... + ) -> None: ... + def test_provider(): verifier = Verifier("provider_name") @@ -160,17 +163,18 @@ This snippet showcases a way to set up the provider state with a function that i ```python from pact import Verifier + def user_state_callback( action: Literal["setup", "teardown"], parameters: dict[str, Any] | None, - ) -> None: - ... + ) -> None: ... + def no_users_state_callback( action: Literal["setup", "teardown"], parameters: dict[str, Any] | None, - ) -> None: - ... + ) -> None: ... + def test_provider(): verifier = Verifier("provider_name") @@ -191,15 +195,16 @@ This snippet showcases a way to set up the provider state with a function that i ```python from pact import Verifier + def user_state_callback( parameters: dict[str, Any] | None, - ) -> None: - ... + ) -> None: ... + def no_users_state_callback( parameters: dict[str, Any] | None, - ) -> None: - ... + ) -> None: ... + def test_provider(): verifier = Verifier("provider_name") @@ -226,6 +231,7 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho from pact import Verifier from pact.types import Message + def message_producer_callback( name: str, # (1) metadata: dict[str, Any] | None, # (2) @@ -245,6 +251,7 @@ def message_producer_callback( """ ... + def test_provider(): verifier = Verifier("provider_name") verifier.message_handler(message_producer_callback) @@ -264,6 +271,7 @@ The output of the callback function should be an instance of the `Message` type. ```python from pact.types import Message + def message_producer_callback( name: str, params: dict[str, Any] | None, @@ -306,8 +314,9 @@ In much the same way as the `state_handler` method, the `message_handler` method from pact import Verifier from pact.types import Message -def delete_user_message(metadata: dict[str, Any] | None) -> Message: - ... + +def delete_user_message(metadata: dict[str, Any] | None) -> Message: ... + def test_provider(): verifier = Verifier("provider_name") diff --git a/docs/blog/posts/2025/12-04 pact-python-v3-release.md b/docs/blog/posts/2025/12-04 pact-python-v3-release.md index cd6d18e45..979025728 100644 --- a/docs/blog/posts/2025/12-04 pact-python-v3-release.md +++ b/docs/blog/posts/2025/12-04 pact-python-v3-release.md @@ -59,33 +59,33 @@ What does this look like in practice? Here's a side-by-side comparison of a simp from pact.v2 import Consumer, Provider import requests -consumer = Consumer('my-web-front-end') -provider = Provider('my-backend-service') +consumer = Consumer("my-web-front-end") +provider = Provider("my-backend-service") -pact = consumer.has_pact_with(provider, pact_dir='/path/to/pacts') +pact = consumer.has_pact_with(provider, pact_dir="/path/to/pacts") ( pact - .given('user exists') # (1) - .upon_receiving('a request for user data') + .given("user exists") # (1) + .upon_receiving("a request for user data") .with_request( - 'GET', - '/users/123', - headers={'Accept': 'application/json'}, - query={'include': 'profile'} + "GET", + "/users/123", + headers={"Accept": "application/json"}, + query={"include": "profile"}, ) .will_respond_with( 200, - headers={'Content-Type': 'application/json'}, - body={'id': 123, 'name': 'Alice'} + headers={"Content-Type": "application/json"}, + body={"id": 123, "name": "Alice"}, ) ) pact.start_service() # (2) pact.setup() -response = requests.get(pact.uri + '/users/123') -assert response.json() == {'id': 123, 'name': 'Alice'} -pact.verify() # (3) -pact.stop_service() # (4) +response = requests.get(pact.uri + "/users/123") +assert response.json() == {"id": 123, "name": "Alice"} +pact.verify() # (3) +pact.stop_service() # (4) # Pact file is written as part of verify() or when the service stops ``` @@ -98,22 +98,22 @@ pact.stop_service() # (4) from pact import Pact import requests -pact = Pact('my-web-front-end', 'my-backend-service') +pact = Pact("my-web-front-end", "my-backend-service") ( pact - .upon_receiving('a request for user data') - .given('user exists', id=123, name='Alice') # (1) - .with_request('GET', '/users/123') - .with_header('Accept', 'application/json') - .with_query_parameter('include', 'profile') + .upon_receiving("a request for user data") + .given("user exists", id=123, name="Alice") # (1) + .with_request("GET", "/users/123") + .with_header("Accept", "application/json") + .with_query_parameter("include", "profile") .will_respond_with(200) - .with_body({'id': 123, 'name': 'Alice'}, content_type='application/json') + .with_body({"id": 123, "name": "Alice"}, content_type="application/json") ) with pact.serve() as srv: # (2) response = requests.get(f"{srv.url}/users/123") - assert response.json() == {'id': 123, 'name': 'Alice'} -pact.write_file('/path/to/pacts') # (3) + assert response.json() == {"id": 123, "name": "Alice"} +pact.write_file("/path/to/pacts") # (3) ``` 1. In v3, provider states can be parameterized, making it easier to reuse and manage test data across different scenarios. diff --git a/docs/consumer.md b/docs/consumer.md index 934b1e217..59b13f7b5 100644 --- a/docs/consumer.md +++ b/docs/consumer.md @@ -53,12 +53,14 @@ from datetime import datetime from typing import Any import requests + @dataclass() class User: # (1) id: int name: str created_on: datetime + class UserClient: """Simple HTTP client for interacting with a user provider service.""" @@ -93,6 +95,7 @@ from pathlib import Path import pytest from pact import Pact, match + @pytest.fixture def pact() -> Generator[Pact, None, None]: # (1) """Set up a Pact mock provider for consumer tests.""" @@ -100,6 +103,7 @@ def pact() -> Generator[Pact, None, None]: # (1) yield pact pact.write_file(Path(__file__).parent / "pacts") + def test_get_user(pact: Pact) -> None: """Test the GET request for a user.""" response: dict[str, object] = { # (3) @@ -108,7 +112,8 @@ def test_get_user(pact: Pact) -> None: "created_on": match.datetime(), } ( - pact.upon_receiving("A user request") # (4) + pact + .upon_receiving("A user request") # (4) .given("the user exists", id=123, name="Alice") # (5) .with_request("GET", "/users/123") # (6) .will_respond_with(200) # (7) @@ -157,14 +162,16 @@ The mock service can handle multiple interactions within a single test. This is ```python ( - pact.upon_receiving("A request to create a task") + pact + .upon_receiving("A request to create a task") .with_request("POST", "/tasks", body={"type": "long_running"}) .will_respond_with(202) .with_header("Location", "/tasks/1/status") ) ( - pact.upon_receiving("A request to check task status") + pact + .upon_receiving("A request to check task status") .with_request("GET", "/tasks/1/status") .will_respond_with(200) .with_body({"status": "completed"}) @@ -175,7 +182,8 @@ The mock service can handle multiple interactions within a single test. This is ) ( - pact.upon_receiving("A request to get task result") + pact + .upon_receiving("A request to get task result") .with_request("GET", "/tasks/1/result") .will_respond_with(200) .with_body({"result": "Task completed successfully"}) @@ -194,6 +202,7 @@ The recommended approach is to set up logging in a pytest fixture within your `c import pytest import pact_ffi + @pytest.fixture(autouse=True, scope="session") def pact_logging(): """Configure Pact FFI logging for the test session.""" @@ -263,16 +272,16 @@ from pact import match # Instead of exact matches that break easily: response = { - "id": 12345, # Brittle - specific value - "email": "user@example.com", # Fails if email changes - "created_at": "2024-01-15T10:30:00Z" # Breaks on different timestamps + "id": 12345, # Brittle - specific value + "email": "user@example.com", # Fails if email changes + "created_at": "2024-01-15T10:30:00Z", # Breaks on different timestamps } # Use flexible matchers: response = { - "id": match.int(12345), # Any integer + "id": match.int(12345), # Any integer "email": match.regex("user@example.com", regex=r".+@.+\..+"), - "created_at": match.datetime("2024-01-15T10:30:00Z") + "created_at": match.datetime("2024-01-15T10:30:00Z"), } ``` @@ -296,16 +305,16 @@ from pact import generate # Instead of static values in your mock responses response = { - "user_id": 123, # Always the same - "session_token": "abc-def-123", # Predictable - "created_at": "2024-07-20T14:30:00+00:00" # Never changes + "user_id": 123, # Always the same + "session_token": "abc-def-123", # Predictable + "created_at": "2024-07-20T14:30:00+00:00", # Never changes } # Use generators for dynamic, realistic data response = { "user_id": generate.int(min=1, max=999999), "session_token": generate.uuid(), - "created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z") + "created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z"), } ``` @@ -325,27 +334,22 @@ response = { # Numeric values with constraints "user_id": generate.int(min=1, max=999999), "price": generate.float(precision=2), # 2 total digits - "hex_color": generate.hex(digits=6), # 6-digit hex code - + "hex_color": generate.hex(digits=6), # 6-digit hex code # String and text data - "username": generate.str(size=8), # 8-character string + "username": generate.str(size=8), # 8-character string "confirmation": generate.regex(r"[A-Z]{3}-\d{4}"), # Pattern-based - # Identifiers - "session_id": generate.uuid(), # Standard UUID format + "session_id": generate.uuid(), # Standard UUID format "simple_id": generate.uuid(format="simple"), # No hyphens - # Dates and times "created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z"), "birth_date": generate.date("%Y-%m-%d"), "start_time": generate.time("%H:%M:%S"), - # Boolean values "is_active": generate.bool(), - # Provider-specific values "server_url": generate.mock_server_url(), - "dynamic_value": generate.provider_state("${expression}") + "dynamic_value": generate.provider_state("${expression}"), } ``` @@ -358,19 +362,18 @@ Matchers and generators work together to create flexible, realistic contracts. U request_body = { "email": match.regex("user@example.com", regex=r".+@.+\..+"), "age": match.int(25, min=18, max=100), - "preferences": match.array_containing([match.str("notifications")]) + "preferences": match.array_containing([match.str("notifications")]), } # Response generation with dynamic data response_body = { "id": generate.int(min=100000, max=999999), "email": match.str("user@example.com"), # Echo back the input - "verification_token": generate.uuid(), # Fresh token each time + "verification_token": generate.uuid(), # Fresh token each time "created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z"), "profile_url": generate.mock_server_url( - example="/profiles/12345", - regex=r"/profiles/\d+" - ) + example="/profiles/12345", regex=r"/profiles/\d+" + ), } ``` diff --git a/docs/logging.md b/docs/logging.md index c3285555d..6fb46f28b 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -35,6 +35,7 @@ The recommended way to configure FFI logging in your test suite is to use a pyte import pytest import pact_ffi + @pytest.fixture(autouse=True, scope="session") def pact_logging(): """Configure Pact FFI logging for the test session.""" diff --git a/docs/provider.md b/docs/provider.md index 576f201df..5b01087d1 100644 --- a/docs/provider.md +++ b/docs/provider.md @@ -44,6 +44,7 @@ You can verify Pacts from a local directory as follows: ```python from pact import Verifier + def test_provider(): """Test the provider against the consumer contract.""" verifier = ( @@ -64,6 +65,7 @@ Although local Pact files are useful for quick tests, in most cases you will wan ```python from pact import Verifier + def test_provider_from_broker(): """Test the provider against contracts from a Pact Broker.""" verifier = ( @@ -84,6 +86,7 @@ For advanced broker configurations, use the selector builder pattern to filter w ```python from pact import Verifier + def test_provider_with_selectors(): """Test with advanced broker selectors.""" verifier = ( @@ -140,6 +143,7 @@ The recommended approach is to set up logging in a pytest fixture within your `c import pytest import pact_ffi + @pytest.fixture(autouse=True, scope="session") def pact_logging(): """Configure Pact FFI logging for the test session.""" @@ -227,6 +231,7 @@ A single function can handle all provider states: from pact import Verifier from typing import Literal, Any + def handle_provider_state( state: str, action: Literal["setup", "teardown"], @@ -250,6 +255,7 @@ def handle_provider_state( msg = f"Unknown state/action: {state}/{action}" raise ValueError(msg) + verifier = ( Verifier("my-provider") .add_transport(url="http://localhost:8080") @@ -268,6 +274,7 @@ Map specific state names to dedicated handler functions: from pact import Verifier from typing import Literal, Any + def mock_user_exists( action: Literal["setup", "teardown"], parameters: dict[str, Any] | None, @@ -278,15 +285,18 @@ def mock_user_exists( if action == "setup": # Set up the user in your test database/mock - return UserDb.create(User( - id=user_id, - name=parameters.get("name", "Test User"), - email=parameters.get("email", "test@example.com"), - )) + return UserDb.create( + User( + id=user_id, + name=parameters.get("name", "Test User"), + email=parameters.get("email", "test@example.com"), + ) + ) if action == "teardown": # Clean up after the test return UserDb.delete(user_id) + def mock_user_does_not_exist( action: Literal["setup", "teardown"], parameters: dict[str, Any] | None, @@ -300,6 +310,7 @@ def mock_user_does_not_exist( if UserDb.get(user_id): UserDb.delete(user_id) + # Map state names to handler functions state_handlers = { "user exists": mock_user_exists, diff --git a/examples/http/xml_example/README.md b/examples/http/xml_example/README.md index 894d2c329..c117ed1c1 100644 --- a/examples/http/xml_example/README.md +++ b/examples/http/xml_example/README.md @@ -57,7 +57,8 @@ constructs the body description from nested from pact import match, xml response = xml.body( - xml.element("user", + xml.element( + "user", xml.element("id", match.int(123)), xml.element("name", match.str("Alice")), ) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 1d157db44..06a1266d5 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.22", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.16.1", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==2.3.0"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index f20d34461..5c8e39aac 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.22", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.16.1", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==2.3.0", "typing-extensions~=4.0"] diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 228d6e4a7..0516442dd 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -6532,7 +6532,7 @@ def with_generators( raise RuntimeError(msg) -def with_multipart_file_v2( # noqa: PLR0913 +def with_multipart_file_v2( # noqa: PLR0913, PLR0917 interaction: InteractionHandle, part: InteractionPart, content_type: str | None, @@ -7010,7 +7010,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: lib.pactffi_verifier_shutdown(handle._ref) -def verifier_set_provider_info( # noqa: PLR0913 +def verifier_set_provider_info( # noqa: PLR0913, PLR0917 handle: VerifierHandle, name: str | None, scheme: str | None, @@ -7482,7 +7482,7 @@ def verifier_broker_source( ) -def verifier_broker_source_with_selectors( # noqa: PLR0913 +def verifier_broker_source_with_selectors( # noqa: PLR0913, PLR0917 handle: VerifierHandle, url: str, username: str | None, @@ -8008,7 +8008,7 @@ def matches_bool_value( raise NotImplementedError -def matches_binary_value( # noqa: PLR0913 +def matches_binary_value( # noqa: PLR0913, PLR0917 matching_rule: MatchingRule, expected_value: str, expected_value_len: int, diff --git a/prek.toml b/prek.toml index 9f93ffdf4..0e2a2dc86 100644 --- a/prek.toml +++ b/prek.toml @@ -37,12 +37,12 @@ hooks = [ [[repos]] repo = "https://github.com/biomejs/pre-commit" -rev = "v2.4.12" +rev = "v2.5.6" hooks = [{ id = "biome-check" }] [[repos]] repo = "https://github.com/astral-sh/ruff-pre-commit" -rev = "v0.15.11" +rev = "v0.16.1" [[repos.hooks]] id = "ruff-check" @@ -60,12 +60,12 @@ hooks = [{ id = "committed" }] [[repos]] repo = "https://github.com/DavidAnson/markdownlint-cli2" -rev = "v0.22.0" +rev = "v0.23.2" hooks = [{ id = "markdownlint-cli2" }] [[repos]] repo = "https://github.com/crate-ci/typos" -rev = "v1.45.1" +rev = "v1.48.0" [[repos.hooks]] id = "typos" @@ -73,7 +73,7 @@ rev = "v1.45.1" [[repos]] repo = "https://github.com/tombi-toml/tombi-pre-commit" -rev = "v0.9.20" +rev = "v1.2.6" hooks = [{ id = "tombi-format" }, { id = "tombi-lint" }] [[repos]] @@ -82,10 +82,19 @@ repo = "local" [[repos.hooks]] # Mypy is difficult to run pre-commit's isolated environment as it needs # to be able to find dependencies. + # + # This runs in the root environment, so the sub-projects are excluded as + # their dependencies are only present in their own environments. Generated + # protobuf modules are excluded as passing them explicitly makes mypy read + # the module instead of its accompanying stub. id = "mypy" name = "mypy" entry = "hatch run mypy" language = "system" types = ["python"] - exclude = '(src/pact|tests|examples)/v2/.*\.pyi?' + exclude = '''(?x)( + ^(src/pact|tests|examples)/v2/.*\.pyi? + | ^pact-python-(cli|ffi)/ + | ^examples/plugins/proto/.*_pb2(_grpc)?\.py + )''' stages = ["pre-push"] diff --git a/pyproject.toml b/pyproject.toml index de4a700d4..0b6094d50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.22", + "ruff==0.16.1", { include-group = "docs" }, { include-group = "example" }, { include-group = "example-v2" }, @@ -280,6 +280,7 @@ build-backend = "hatchling.build" select = ["ALL"] ignore = [ + "CPY001", # Require copyright notice at the top of files "D200", # Require single line docstrings to be on one line. "D203", # Require blank line before class docstring "D212", # Multi-line docstring summary must start at the first line diff --git a/src/pact/error.py b/src/pact/error.py index d247d6987..eee6d2925 100644 --- a/src/pact/error.py +++ b/src/pact/error.py @@ -809,6 +809,7 @@ def __init__( # noqa: PLR0913 expected: str, actual: str, mismatch: str, + *, expected_body: bytes | None = None, expectedBody: bytes | None = None, # noqa: N803 actual_body: bytes | None = None, diff --git a/src/pact/pact.py b/src/pact/pact.py index 18c975ec2..80b2da01e 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -670,7 +670,7 @@ def __init__( # noqa: PLR0913 self._transport = transport self._transport_config = transport_config self._pact_handle = pact_handle - self._handle: None | pact_ffi.PactServerHandle = None + self._handle: pact_ffi.PactServerHandle | None = None self._raises = raises self._verbose = verbose self._mismatches: list[Mismatch] | None = None diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 7c3bc7a9a..c706c71c3 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -1151,9 +1151,9 @@ def broker_source( @overload def broker_source( self, - url: str | URL | None | Unset = UNSET, + url: str | URL | Unset | None = UNSET, *, - token: str | None | Unset = UNSET, + token: str | Unset | None = UNSET, selector: Literal[False] = False, use_env: bool = True, ) -> Self: ... @@ -1161,10 +1161,10 @@ def broker_source( @overload def broker_source( self, - url: str | URL | None | Unset = UNSET, + url: str | URL | Unset | None = UNSET, *, - username: str | None | Unset = UNSET, - password: str | None | Unset = UNSET, + username: str | Unset | None = UNSET, + password: str | Unset | None = UNSET, selector: Literal[True], use_env: bool = True, ) -> BrokerSelectorBuilder: ... @@ -1172,20 +1172,20 @@ def broker_source( @overload def broker_source( self, - url: str | URL | None | Unset = UNSET, + url: str | URL | Unset | None = UNSET, *, - token: str | None | Unset = UNSET, + token: str | Unset | None = UNSET, selector: Literal[True], use_env: bool = True, ) -> BrokerSelectorBuilder: ... def broker_source( # noqa: PLR0913 self, - url: str | URL | None | Unset = UNSET, + url: str | URL | Unset | None = UNSET, *, - username: str | None | Unset = UNSET, - password: str | None | Unset = UNSET, - token: str | None | Unset = UNSET, + username: str | Unset | None = UNSET, + password: str | Unset | None = UNSET, + token: str | Unset | None = UNSET, selector: bool = False, use_env: bool = True, ) -> BrokerSelectorBuilder | Self: diff --git a/tests/compatibility_suite/test_v4_matching_rules.py b/tests/compatibility_suite/test_v4_matching_rules.py index f7f4ea741..e679f0d62 100644 --- a/tests/compatibility_suite/test_v4_matching_rules.py +++ b/tests/compatibility_suite/test_v4_matching_rules.py @@ -416,23 +416,31 @@ def the_mismatches_will_contain_a_mismatch_with_error( ): ("/", "Expected body Present(28058 bytes, image/jpeg) but was empty"), ( "$.actions", - 'Variant at index 1 ({\\"href\\":\\"http://api.x.io/orders/42/items\\",' - '\\"method\\":\\"DELETE\\",\\"name\\":\\"delete-item\\",' - '\\"title\\":\\"Delete Item\\"}) was not found in the actual list', + ( + 'Variant at index 1 ({\\"href\\":\\"http://api.x.io/orders/42/items\\",' + '\\"method\\":\\"DELETE\\",\\"name\\":\\"delete-item\\",' + '\\"title\\":\\"Delete Item\\"}) was not found in the actual list' + ), ): ( "$.actions", - 'Variant at index 1 ({"href":"http://api.x.io/orders/42/items",' - '"method":"DELETE","name":"delete-item","title":"Delete Item"}) was ' - "not found in the actual list", + ( + 'Variant at index 1 ({"href":"http://api.x.io/orders/42/items",' + '"method":"DELETE","name":"delete-item","title":"Delete Item"}) was ' + "not found in the actual list" + ), ), ( "$.two", - "Type mismatch: Expected 'b' (String) " - 'to be the same type as [\\"b\\"] (Array)', + ( + "Type mismatch: Expected 'b' (String) " + 'to be the same type as [\\"b\\"] (Array)' + ), ): ( "$.two", - "Type mismatch: Expected 'b' (String) " - 'to be the same type as ["b"] (Array)', + ( + "Type mismatch: Expected 'b' (String) " + 'to be the same type as ["b"] (Array)' + ), ), }.get((path, message), (path, message)) logger.info("Searching for mismatch with path=%r, error=%r", path, message)