Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/allow more flexible arguments #1035

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions docs/blog/posts/2024/12-30 functional arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The new `state_handler` method replaces the `set_state` method and simplifies th
def provider_state_callback(
name: str, # (1)
action: Literal["setup", "teardown"], # (2)
params: dict[str, Any] | None, # (3)
parameters: dict[str, Any] | None, # (3)
) -> None:
"""
Callback to set up and tear down the provider state.
Expand All @@ -105,7 +105,7 @@ The new `state_handler` method replaces the `set_state` method and simplifies th
action should create the provider state, and the teardown action
should remove it.

params:
parameters:
If the provider state has additional parameters, they will be
passed here. For example, instead of `"a user with ID 123 exists"`,
the provider state might be `"a user with the given ID exists"` and
Expand All @@ -120,12 +120,14 @@ The new `state_handler` method replaces the `set_state` method and simplifies th

1. The `name` parameter is the name of the provider state. For example, `"a user with ID 123 exists"` or `"no users exist"`. If you instead use a mapping of provider state names to functions, this parameter is not passed to the function.
2. The `action` parameter is either `"setup"` or `"teardown"`. The setup action should create the provider state, and the teardown action should remove it. If you specify `teardown=False`, then the `action` parameter is _not_ passed to the callback function.
3. The `params` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `params` dictionary. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer.
3. The `parameters` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `parameters` dictionary. Note that `parameters` is always present, but may be `None` if no parameters are specified by the consumer.
<!-- markdownlint-enable code-block-style -->

The function arguments must include the relevant keys from the [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments.

This snippet showcases a way to set up the provider state with a function that is fully parameterized. The `state_handler` method also handles the following scenarios:

- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter is _not_ passed to the callback function.
- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter can be omitted from the signature of the callback function. This is useful when the provider state does not require any cleanup after the test has run.

<!-- markdownlint-disable code-block-style -->
??? example
Expand All @@ -135,7 +137,7 @@ This snippet showcases a way to set up the provider state with a function that i

def provider_state_callback(
name: str,
params: dict[str, Any] | None,
parameters: dict[str, Any] | None,
) -> None:
...

Expand All @@ -155,13 +157,13 @@ This snippet showcases a way to set up the provider state with a function that i

def user_state_callback(
action: Literal["setup", "teardown"],
params: dict[str, Any] | None,
parameters: dict[str, Any] | None,
) -> None:
...

def no_users_state_callback(
action: Literal["setup", "teardown"],
params: dict[str, Any] | None,
parameters: dict[str, Any] | None,
) -> None:
...

Expand All @@ -176,7 +178,7 @@ This snippet showcases a way to set up the provider state with a function that i
```
<!-- markdownlint-enable code-block-style -->

- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `params` dictionary (which itself may be `None`).
- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `parameters` dictionary (which itself may be `None`).

<!-- markdownlint-disable code-block-style -->
??? example
Expand All @@ -185,12 +187,12 @@ This snippet showcases a way to set up the provider state with a function that i
from pact.v3 import Verifier

def user_state_callback(
params: dict[str, Any] | None,
parameters: dict[str, Any] | None,
) -> None:
...

def no_users_state_callback(
params: dict[str, Any] | None,
parameters: dict[str, Any] | None,
) -> None:
...

Expand Down Expand Up @@ -220,7 +222,7 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho

def message_producer_callback(
name: str, # (1)
params: dict[str, Any] | None, # (2)
metadata: dict[str, Any] | None, # (2)
) -> Message:
"""
Callback to produce the message that the consumer expects.
Expand All @@ -229,10 +231,8 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho
name:
The name of the message. For example `"request to delete a user"`.

params:
If the message has additional parameters, they will be passed here.
For example, one could specify the user ID to delete in the
parameters instead of the message.
metadata:
Metadata that is passed along with the message. This could include information about the queue name, message type, creation timestamp, etc.

Returns:
The message that the consumer expects.
Expand All @@ -247,6 +247,7 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho
1. The `name` parameter is the name of the message. For example, `"request to delete a user"`. If you instead use a mapping of message names to functions, this parameter is not passed to the function.
2. The `params` parameter is a dictionary of additional parameters that the message requires. For example, one could specify the user ID to delete in the parameters instead of the message. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer.
<!-- markdownlint-enable code-block-style -->
The function arguments must include the relevant keys from the [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments.

The output of the callback function should be an instance of the `Message` type. This is a simple [TypedDict][typing.TypedDict] that represents the message that the consumer expects and can be specified as a simple dictionary, or with typing hints through the `Message` constructor:

Expand Down Expand Up @@ -289,7 +290,7 @@ The output of the callback function should be an instance of the `Message` type.
}
```

In much the same way as the `state_handler` method, the `message_handler` method can also accept a mapping of message names to functions or raw messages. The function should expect only one argument: the `params` dictionary (which itself may be `None`); or if the message is static, the message can be provided directly:
In much the same way as the `state_handler` method, the `message_handler` method can also accept a mapping of message names to functions or raw messages. The function should expect only one argument: the `metadata` dictionary (which itself may be `None`); or if the message is static, the message can be provided directly:

<!-- markdownlint-disable code-block-style -->
???+ example
Expand All @@ -298,7 +299,7 @@ In much the same way as the `state_handler` method, the `message_handler` method
from pact.v3 import Verifier
from pact.v3.types import Message

def delete_user_message(params: dict[str, Any] | None) -> Message:
def delete_user_message(metadata: dict[str, Any] | None) -> Message:
...

def test_provider():
Expand All @@ -315,3 +316,12 @@ In much the same way as the `state_handler` method, the `message_handler` method
)
```
<!-- markdownlint-enable code-block-style -->

----

<!-- markdownlint-disable code-block-style -->
28 March 2025
: This blog post was updated on 28 March 2025 to reflect changes to the way functional arguments are handled. Instead of requiring positional arguments, Pact Python now inspects the function signature in order to determine whether to pass the arguments as positional or keyword arguments. It will fallback to passing the arguments through variadic arguments (`*args` and `**kwargs`) if present. This was done specific to allow for functions with optional arguments.

For this added flexibility, the function signatures must have parameters that align with the [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs] and [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] typed dictionaries. This allows Pact Python to match a `parameters=...` argument with the `parameters` key in the dictionary. Using an alternative name (e.g., `params`) will not work.
<!-- markdownlint-enable code-block-style -->
11 changes: 10 additions & 1 deletion examples/tests/v3/test_01_fastapi_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def test_provider(server: str) -> None:
def provider_state_handler(
state: str,
action: str,
_parameters: dict[str, Any] | None,
parameters: dict[str, Any] | None = None, # noqa: ARG001
) -> None:
"""
Handler for the provider state callback.
Expand All @@ -170,6 +170,11 @@ def provider_state_handler(
also be used to reset the mock, or in the case were a real database is used,
to clean up any side effects.

This example showcases how a _full_ provider state handler can be
implemented. The handler can also be specified through a mapping of provider
states to functions. See the documentation of the
[`state_handler`][pact.v3.Verifier.state_handler] method for more details.

Args:
action:
One of `setup` or `teardown`. Determines whether the provider state
Expand All @@ -178,6 +183,10 @@ def provider_state_handler(
state:
The name of the state to set up or tear down.

parameters:
A dictionary of parameters to pass to the state handler. This is
not used in this example, but is included for completeness.

Returns:
A dictionary containing the result of the action.
"""
Expand Down
24 changes: 13 additions & 11 deletions src/pact/v3/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@

from pact import __version__
from pact.v3._util import find_free_port
from pact.v3.types import Message

if TYPE_CHECKING:
from types import TracebackType

from pact.v3.types import MessageProducerFull, StateHandlerFull

logger = logging.getLogger(__name__)


_C = TypeVar("_C", bound=Callable[..., Any])
_CM = TypeVar("_CM", bound=Callable[..., Message])
_CN = TypeVar("_CN", bound=Callable[..., None])


class HandlerHttpServer(ThreadingHTTPServer, Generic[_C]):
Expand Down Expand Up @@ -88,7 +90,7 @@ def __init__(
################################################################################


class MessageProducer:
class MessageProducer(Generic[_CM]):
"""
Internal message producer server.

Expand All @@ -102,7 +104,7 @@ class MessageProducer:

def __init__(
self,
handler: MessageProducerFull,
handler: _CM,
host: str = "localhost",
port: int | None = None,
) -> None:
Expand Down Expand Up @@ -134,7 +136,7 @@ def __init__(

self._handler = handler

self._server: HandlerHttpServer[MessageProducerFull] | None = None
self._server: HandlerHttpServer[_CM] | None = None
self._thread: Thread | None = None

@property
Expand Down Expand Up @@ -204,7 +206,7 @@ def __exit__(
self._thread.join()


class MessageProducerHandler(SimpleHTTPRequestHandler):
class MessageProducerHandler(SimpleHTTPRequestHandler, Generic[_CM]):
"""
Request handler for the message relay server.

Expand All @@ -227,7 +229,7 @@ class MessageProducerHandler(SimpleHTTPRequestHandler):
"""

if TYPE_CHECKING:
server: HandlerHttpServer[MessageProducerFull]
server: HandlerHttpServer[_CM]

MESSAGE_PATH = "/_pact/message"

Expand Down Expand Up @@ -306,7 +308,7 @@ def do_GET(self) -> None: # noqa: N802
################################################################################


class StateCallback:
class StateCallback(Generic[_CN]):
"""
Internal server for handling state callbacks.

Expand All @@ -317,7 +319,7 @@ class StateCallback:

def __init__(
self,
handler: StateHandlerFull,
handler: _CN,
host: str = "localhost",
port: int | None = None,
) -> None:
Expand All @@ -343,7 +345,7 @@ def __init__(

self._handler = handler

self._server: HandlerHttpServer[StateHandlerFull] | None = None
self._server: HandlerHttpServer[_CN] | None = None
self._thread: Thread | None = None

@property
Expand Down Expand Up @@ -406,7 +408,7 @@ def __exit__(
self._thread.join()


class StateCallbackHandler(SimpleHTTPRequestHandler):
class StateCallbackHandler(SimpleHTTPRequestHandler, Generic[_CN]):
"""
Request handler for the state callback server.

Expand All @@ -415,7 +417,7 @@ class StateCallbackHandler(SimpleHTTPRequestHandler):
"""

if TYPE_CHECKING:
server: HandlerHttpServer[StateHandlerFull]
server: HandlerHttpServer[_CN]

CALLBACK_PATH = "/_pact/state"

Expand Down
Loading
Loading