Skip to content

Commit db2972d

Browse files
committed
feat: add consumer_version method
The new `consumer_version` replaces the (now deprecated) `consumer_versions` method within the broker source selector class. This replaces the explicit JSON-serialisation of an untyped dictionary into a much more structured call with documented keyword argument. Signed-off-by: JP-Ellis <[email protected]>
1 parent da29864 commit db2972d

File tree

3 files changed

+297
-4
lines changed

3 files changed

+297
-4
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies = [
4444
"pact-python-ffi~=0.4.0",
4545
# External dependencies
4646
"yarl~=1.0",
47+
"typing-extensions~=4.0 ; python_version < '3.13'",
4748
]
4849

4950
[project.urls]

src/pact/verifier.py

Lines changed: 179 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import json
7777
import logging
7878
import os
79+
import sys
7980
from collections.abc import Callable, Mapping
8081
from contextlib import nullcontext
8182
from datetime import date
@@ -96,6 +97,12 @@
9697
Unset,
9798
)
9899

100+
if sys.version_info < (3, 13):
101+
from typing_extensions import deprecated
102+
else:
103+
from warnings import deprecated
104+
105+
99106
if TYPE_CHECKING:
100107
from collections.abc import Iterable
101108

@@ -1386,8 +1393,8 @@ def __init__(
13861393
self._provider_branch: str | None = None
13871394
"The provider branch."
13881395

1389-
self._consumer_versions: list[str] | None = None
1390-
"List of consumer version regex patterns."
1396+
self._consumer_versions: list[str | dict[str, Any]] | None = None
1397+
"List of consumer version selectors."
13911398

13921399
self._consumer_tags: list[str] | None = None
13931400
"List of consumer tags to match."
@@ -1442,11 +1449,174 @@ def provider_branch(self, branch: str) -> Self:
14421449
self._verifier._branch = branch # type: ignore # noqa: PGH003, SLF001
14431450
return self
14441451

1452+
def consumer_version( # noqa: PLR0913
1453+
self,
1454+
*,
1455+
consumer: str | None = None,
1456+
tag: str | None = None,
1457+
fallback_tag: str | None = None,
1458+
latest: bool | None = None,
1459+
deployed_or_released: Literal[True] | None = None,
1460+
deployed: Literal[True] | None = None,
1461+
released: Literal[True] | None = None,
1462+
environment: str | None = None,
1463+
main_branch: Literal[True] | None = None,
1464+
branch: str | None = None,
1465+
matching_branch: Literal[True] | None = None,
1466+
fallback_branch: str | None = None,
1467+
) -> Self:
1468+
"""
1469+
Add a consumer version selector.
1470+
1471+
This method allows specifying consumer version selection criteria to
1472+
filter which consumer pacts are verified from the broker.
1473+
1474+
This function can be called multiple times to add multiple selectors.
1475+
The resulting selectors are combined with a logical OR, meaning that
1476+
pacts matching any of the selectors will be included in the
1477+
verification.
1478+
1479+
Args:
1480+
consumer:
1481+
Application name to filter the results on.
1482+
1483+
Allows a selector to only be applied to a certain consumer.
1484+
1485+
tag:
1486+
The tag name(s) of the consumer versions to get the pacts for.
1487+
1488+
This field is still supported but it is recommended to use the
1489+
`branch` in preference now.
1490+
1491+
fallback_tag:
1492+
The name of the tag to fallback to if the specified `tag` does
1493+
not exist.
1494+
1495+
This is useful when the consumer and provider use matching
1496+
branch names to coordinate the development of new features. This
1497+
field is still supported but it is recommended to use two
1498+
separate selectors - one with the main branch name and one with
1499+
the feature branch name.
1500+
1501+
latest:
1502+
Only select the latest (if false, this selects all pacts for a
1503+
tag).
1504+
1505+
Used in conjunction with the tag property. If a tag is
1506+
specified, and latest is true, then the latest pact for each of
1507+
the consumers with that tag will be returned. If a tag is
1508+
specified and the latest flag is not set to true, all the pacts
1509+
with the specified tag will be returned.
1510+
1511+
deployed_or_released:
1512+
Applications that have been deployed or released.
1513+
1514+
If the key is specified, can only be set to `True`. Returns the
1515+
pacts for all versions of the consumer that are currently
1516+
deployed or released and currently supported in any environment.
1517+
Use of this selector requires that the deployment of the
1518+
consumer application is recorded in the Pact Broker using the
1519+
`pact-broker record-deployment` or `pact-broker record-release`
1520+
CLI.
1521+
1522+
deployed:
1523+
Applications that have been deployed.
1524+
1525+
If the key is specified, can only be set to `True`. Returns the
1526+
pacts for all versions of the consumer that are currently
1527+
deployed to any environment. Use of this selector requires that
1528+
the deployment of the consumer application is recorded in the
1529+
Pact Broker using the `pact-broker record-deployment` CLI.
1530+
1531+
released:
1532+
Applications that have been released.
1533+
1534+
If the key is specified, can only be set to `True`. Returns the
1535+
pacts for all versions of the consumer that are released and
1536+
currently supported in any environment. Use of this selector
1537+
requires that the deployment of the consumer application is
1538+
recorded in the Pact Broker using the `pact-broker
1539+
record-release` CLI.
1540+
1541+
environment:
1542+
Applications in a given environment.
1543+
1544+
The name of the environment containing the consumer versions for
1545+
which to return the pacts. Used to further qualify `{
1546+
"deployed": true }` or `{ "released": true }`. Normally, this
1547+
would not be needed, as it is recommended to verify the pacts
1548+
for all currently deployed/currently supported released
1549+
versions.
1550+
1551+
main_branch:
1552+
Applications with the default branch set in the broker.
1553+
1554+
If the key is specified, can only be set to `True`. Return the
1555+
pacts for the configured `mainBranch` of each consumer. Use of
1556+
this selector requires that the consumer has configured the
1557+
`mainBranch` property, and has set a branch name when publishing
1558+
the pacts.
1559+
1560+
branch:
1561+
Applications with the given branch.
1562+
1563+
The branch name of the consumer versions to get the pacts for.
1564+
Use of this selector requires that the consumer has configured a
1565+
branch name when publishing the pacts.
1566+
1567+
matching_branch:
1568+
Applications that match the provider version branch sent during
1569+
verification.
1570+
1571+
If the key is specified, can only be set to `True`. When true,
1572+
returns the latest pact for any branch with the same name as the
1573+
specified `provider_version_branch`.
1574+
1575+
fallback_branch:
1576+
Fallback branch if branch doesn't exist.
1577+
1578+
The name of the branch to fallback to if the specified branch
1579+
does not exist. Use of this property is discouraged as it may
1580+
allow a pact to pass on a feature branch while breaking
1581+
backwards compatibility with the main branch, which is generally
1582+
not desired. It is better to use two separate consumer version
1583+
selectors, one with the main branch name, and one with the
1584+
feature branch name, rather than use this property.
1585+
1586+
Returns:
1587+
The builder instance for method chaining.
1588+
"""
1589+
if self._consumer_versions is None:
1590+
self._consumer_versions = []
1591+
1592+
param_mapping = [
1593+
("consumer", consumer),
1594+
("tag", tag),
1595+
("fallbackTag", fallback_tag),
1596+
("latest", latest),
1597+
("deployedOrReleased", deployed_or_released),
1598+
("deployed", deployed),
1599+
("released", released),
1600+
("environment", environment),
1601+
("mainBranch", main_branch),
1602+
("branch", branch),
1603+
("matchingBranch", matching_branch),
1604+
("fallbackBranch", fallback_branch),
1605+
]
1606+
1607+
self._consumer_versions.append({
1608+
key: value for key, value in param_mapping if value is not None
1609+
})
1610+
return self
1611+
1612+
@deprecated("Use `consumer_version` method with keyword arguments instead.")
14451613
def consumer_versions(self, *versions: str) -> Self:
14461614
"""
14471615
Set the consumer versions.
14481616
"""
1449-
self._consumer_versions = list(versions)
1617+
if self._consumer_versions is None:
1618+
self._consumer_versions = []
1619+
self._consumer_versions.extend(versions)
14501620
return self
14511621

14521622
def consumer_tags(self, *tags: str) -> Self:
@@ -1463,6 +1633,11 @@ def build(self) -> Verifier:
14631633
Returns:
14641634
The Verifier instance with the broker source added.
14651635
"""
1636+
consumer_versions = [
1637+
json.dumps(cv) if not isinstance(cv, str) else cv
1638+
for cv in (self._consumer_versions or [])
1639+
]
1640+
14661641
self._verifier._broker_source_hook = ( # noqa: SLF001
14671642
lambda: pact_ffi.verifier_broker_source_with_selectors(
14681643
self._verifier._handle, # noqa: SLF001
@@ -1474,7 +1649,7 @@ def build(self) -> Verifier:
14741649
self._include_wip_since,
14751650
self._provider_tags or [],
14761651
self._provider_branch or self._verifier._branch, # noqa: SLF001
1477-
self._consumer_versions or [],
1652+
consumer_versions,
14781653
self._consumer_tags or [],
14791654
)
14801655
)

tests/test_verifier.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88

99
from __future__ import annotations
1010

11+
import json
1112
import re
1213
from pathlib import Path
14+
from typing import Any
15+
from unittest.mock import patch
1316

1417
import pytest
1518

@@ -166,3 +169,117 @@ def test_logs(verifier: Verifier) -> None:
166169
def test_output(verifier: Verifier) -> None:
167170
output = verifier.output()
168171
assert output == ""
172+
173+
174+
@pytest.mark.parametrize(
175+
("selector_calls", "expected_selectors"),
176+
[
177+
pytest.param(
178+
[{"consumer": "test-consumer"}],
179+
[{"consumer": "test-consumer"}],
180+
id="single_parameter",
181+
),
182+
pytest.param(
183+
[{"consumer": "test-consumer", "branch": "main", "latest": True}],
184+
[{"consumer": "test-consumer", "branch": "main", "latest": True}],
185+
id="multiple_parameters",
186+
),
187+
pytest.param(
188+
[{"deployed_or_released": True, "fallback_tag": "latest"}],
189+
[{"deployedOrReleased": True, "fallbackTag": "latest"}],
190+
id="camelcase_conversion",
191+
),
192+
pytest.param(
193+
[
194+
{"branch": "main", "latest": True},
195+
{"branch": "feature-branch", "latest": True},
196+
{"deployed": True},
197+
],
198+
[
199+
{"branch": "main", "latest": True},
200+
{"branch": "feature-branch", "latest": True},
201+
{"deployed": True},
202+
],
203+
id="multiple_selectors",
204+
),
205+
pytest.param(
206+
[
207+
{
208+
"consumer": "test-consumer",
209+
"tag": "v1.0",
210+
"fallback_tag": "latest",
211+
"latest": True,
212+
"deployed_or_released": True,
213+
"deployed": True,
214+
"released": True,
215+
"environment": "staging",
216+
"main_branch": True,
217+
"branch": "feature-123",
218+
"matching_branch": True,
219+
"fallback_branch": "develop",
220+
}
221+
],
222+
[
223+
{
224+
"consumer": "test-consumer",
225+
"tag": "v1.0",
226+
"fallbackTag": "latest",
227+
"latest": True,
228+
"deployedOrReleased": True,
229+
"deployed": True,
230+
"released": True,
231+
"environment": "staging",
232+
"mainBranch": True,
233+
"branch": "feature-123",
234+
"matchingBranch": True,
235+
"fallbackBranch": "develop",
236+
}
237+
],
238+
id="all_parameters",
239+
),
240+
pytest.param(
241+
[
242+
{
243+
"consumer": "test-consumer",
244+
"branch": "main",
245+
"tag": None,
246+
"latest": None,
247+
}
248+
],
249+
[{"consumer": "test-consumer", "branch": "main"}],
250+
id="none_values_excluded",
251+
),
252+
],
253+
)
254+
def test_consumer_version(
255+
verifier: Verifier,
256+
selector_calls: list[dict[str, Any]],
257+
expected_selectors: list[dict[str, Any]],
258+
) -> None:
259+
"""Test consumer_version with various parameter combinations and selector counts."""
260+
with patch("pact_ffi.verifier_broker_source_with_selectors") as mock_ffi:
261+
selector_builder = verifier.broker_source(
262+
"http://localhost:8080",
263+
selector=True,
264+
)
265+
266+
# Call consumer_version for each set of parameters
267+
for params in selector_calls:
268+
selector_builder.consumer_version(**params)
269+
270+
selector_builder.build()
271+
# We call the hook explicitly to trigger the FFI call
272+
assert verifier._broker_source_hook is not None # noqa: SLF001
273+
verifier._broker_source_hook() # noqa: SLF001
274+
275+
# Verify FFI was called with correct selectors
276+
mock_ffi.assert_called_once()
277+
selectors = [json.loads(s) for s in mock_ffi.call_args[0][9]]
278+
279+
assert len(selectors) == len(expected_selectors)
280+
for actual, expected in zip(selectors, expected_selectors, strict=True):
281+
assert actual == expected
282+
# For None value test case, verify excluded keys
283+
if "tag" not in expected and "latest" not in expected:
284+
assert "tag" not in actual
285+
assert "latest" not in actual

0 commit comments

Comments
 (0)