Skip to content

Commit 02562fe

Browse files
feat: allow iteration over all interactions
Add support to iterating over all interactions within a Pact. Co-authored-by: JP-Ellis <josh@jpellis.me>
1 parent c5f9e57 commit 02562fe

File tree

6 files changed

+455
-35
lines changed

6 files changed

+455
-35
lines changed

pact-python-ffi/src/pact_ffi/__init__.py

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,48 @@ def port(self) -> int:
960960
return self._ref
961961

962962

963-
class PactInteraction: ...
963+
class PactInteraction:
964+
"""
965+
A Pact Interaction.
966+
967+
This is a minimal implementation to support iteration over all interactions.
968+
Full support requires additional upstream library development.
969+
"""
970+
971+
def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None:
972+
"""
973+
Initialise a new Pact Interaction.
974+
975+
Args:
976+
ptr:
977+
CFFI data structure.
978+
979+
owned:
980+
Whether the interaction is owned by something else or not. This
981+
determines whether the interaction should be freed when the
982+
Python object is destroyed.
983+
"""
984+
self._ptr = ptr
985+
self._owned = owned
986+
987+
def __str__(self) -> str:
988+
"""
989+
Nice string representation.
990+
"""
991+
return "PactInteraction"
992+
993+
def __repr__(self) -> str:
994+
"""
995+
Debugging representation.
996+
"""
997+
return f"PactInteraction({self._ptr!r})"
998+
999+
def __del__(self) -> None:
1000+
"""
1001+
Destructor for the Pact Interaction.
1002+
"""
1003+
if self._owned:
1004+
pass # pact_interaction_delete not implemented yet
9641005

9651006

9661007
class PactInteractionIterator:
@@ -1009,13 +1050,77 @@ def __del__(self) -> None:
10091050
"""
10101051
pact_interaction_iter_delete(self)
10111052

1053+
def __iter__(self) -> Self:
1054+
"""
1055+
Return the iterator itself.
1056+
"""
1057+
return self
1058+
10121059
def __next__(self) -> PactInteraction:
10131060
"""
10141061
Get the next interaction from the iterator.
10151062
"""
10161063
return pact_interaction_iter_next(self)
10171064

10181065

1066+
class PactMessageIterator:
1067+
"""
1068+
Iterator over a Pact's interactions.
1069+
1070+
Interactions encompasses all types of interactions, including HTTP
1071+
interactions and messages.
1072+
"""
1073+
1074+
def __init__(self, ptr: cffi.FFI.CData) -> None:
1075+
"""
1076+
Initialise a new Pact Message Iterator.
1077+
1078+
Args:
1079+
ptr:
1080+
CFFI data structure.
1081+
1082+
Raises:
1083+
TypeError:
1084+
If the `ptr` is not a `struct PactMessageIterator`.
1085+
"""
1086+
if ffi.typeof(ptr).cname != "struct PactMessageIterator *":
1087+
msg = (
1088+
f"ptr must be a struct PactMessageIterator, got {ffi.typeof(ptr).cname}"
1089+
)
1090+
raise TypeError(msg)
1091+
self._ptr = ptr
1092+
1093+
def __str__(self) -> str:
1094+
"""
1095+
Nice string representation.
1096+
"""
1097+
return "PactMessageIterator"
1098+
1099+
def __repr__(self) -> str:
1100+
"""
1101+
Debugging representation.
1102+
"""
1103+
return f"PactMessageIterator({self._ptr!r})"
1104+
1105+
def __del__(self) -> None:
1106+
"""
1107+
Destructor for the Pact Message Iterator.
1108+
"""
1109+
pact_message_iter_delete(self)
1110+
1111+
def __iter__(self) -> Self:
1112+
"""
1113+
Return the iterator itself.
1114+
"""
1115+
return self
1116+
1117+
def __next__(self) -> PactInteraction:
1118+
"""
1119+
Get the next interaction from the iterator.
1120+
"""
1121+
return pact_message_iter_next(self)
1122+
1123+
10191124
class PactSyncHttpIterator:
10201125
"""
10211126
Iterator over a Pact's synchronous HTTP interactions.
@@ -4036,8 +4141,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction
40364141
ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr)
40374142
if ptr == ffi.NULL:
40384143
raise StopIteration
4039-
raise NotImplementedError
4040-
return PactInteraction(ptr)
4144+
return PactInteraction(ptr, owned=True)
40414145

40424146

40434147
def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None:
@@ -4050,6 +4154,33 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None:
40504154
lib.pactffi_pact_interaction_iter_delete(iter._ptr)
40514155

40524156

4157+
def pact_message_iter_next(iter: PactMessageIterator) -> PactInteraction:
4158+
"""
4159+
Get the next interaction from the pact.
4160+
4161+
[Rust
4162+
`pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_message_iter_next)
4163+
4164+
Raises:
4165+
StopIteration:
4166+
If the iterator has reached the end.
4167+
"""
4168+
ptr = lib.pactffi_pact_message_iter_next(iter._ptr)
4169+
if ptr == ffi.NULL:
4170+
raise StopIteration
4171+
return PactInteraction(ptr, owned=True)
4172+
4173+
4174+
def pact_message_iter_delete(iter: PactMessageIterator) -> None:
4175+
"""
4176+
Free the iterator when you're done using it.
4177+
4178+
[Rust
4179+
`pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_message_iter_delete)
4180+
"""
4181+
lib.pactffi_pact_message_iter_delete(iter._ptr)
4182+
4183+
40534184
def matching_rule_to_json(rule: MatchingRule) -> str:
40544185
"""
40554186
Get the JSON form of the matching rule.
@@ -6644,6 +6775,32 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator:
66446775
return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref))
66456776

66466777

6778+
def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator:
6779+
r"""
6780+
Get an iterator over all the interactions of the Pact.
6781+
6782+
The returned iterator needs to be freed with
6783+
`pactffi_pact_message_iter_delete`.
6784+
6785+
[Rust
6786+
`pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_message_iter)
6787+
6788+
# Safety
6789+
6790+
The iterator contains a copy of the Pact, so it is always safe to use.
6791+
6792+
# Error Handling
6793+
6794+
On failure, this function will return a NULL pointer.
6795+
6796+
This function may fail if any of the Rust strings contain embedded null
6797+
('\0') bytes.
6798+
"""
6799+
return PactMessageIterator(
6800+
lib.pactffi_pact_handle_get_message_iter(pact._ref),
6801+
)
6802+
6803+
66476804
def pact_handle_write_file(
66486805
pact: PactHandle,
66496806
directory: Path | str | None,

pact-python-ffi/tests/test_init.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,143 @@ def test_owned_string() -> None:
7777
"-----END CERTIFICATE-----\r\n",
7878
),
7979
)
80+
81+
82+
class TestInteractionIteration:
83+
"""
84+
Test interaction iteration functionality.
85+
"""
86+
87+
def test_pact_interaction(self) -> None:
88+
"""Test PactInteraction class."""
89+
pact = pact_ffi.new_pact("consumer", "provider")
90+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
91+
92+
# Create HTTP interaction
93+
pact_ffi.new_sync_message_interaction(pact, "test")
94+
95+
# Get interactions via iterator
96+
sync_http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact)
97+
list(sync_http_iter)
98+
# Test string representation works on iterator
99+
assert "PactSyncHttpIterator" in str(sync_http_iter) or str(sync_http_iter)
100+
101+
def test_pact_message_iterator(self) -> None:
102+
"""Test PactMessageIterator class."""
103+
pact = pact_ffi.new_pact("consumer", "provider")
104+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
105+
106+
# Create message interaction
107+
pact_ffi.new_message_interaction(pact, "test message")
108+
109+
# Get message iterator
110+
iterator = pact_ffi.pact_handle_get_message_iter(pact)
111+
112+
# Test string representation
113+
assert "PactMessageIterator" in str(iterator)
114+
assert "PactMessageIterator" in repr(iterator)
115+
116+
# Iterate and count messages
117+
message_count = sum(1 for _ in iterator)
118+
119+
# Should have the message
120+
assert message_count >= 1
121+
122+
def test_pact_interaction_owned(self) -> None:
123+
"""Test PactInteraction with owned parameter."""
124+
pact = pact_ffi.new_pact("consumer", "provider")
125+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
126+
pact_ffi.new_sync_message_interaction(pact, "test")
127+
128+
# Get an interaction through the iterator
129+
sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact)
130+
for interaction in sync_iter:
131+
# Interaction should be owned by the iterator
132+
# Test destructor doesn't crash
133+
del interaction
134+
break
135+
136+
def test_pact_message_iterator_empty(self) -> None:
137+
"""Test PactMessageIterator with no messages."""
138+
pact = pact_ffi.new_pact("consumer", "provider")
139+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
140+
141+
iterator = pact_ffi.pact_handle_get_message_iter(pact)
142+
143+
# Should iterate zero times
144+
message_count = sum(1 for _ in iterator)
145+
assert message_count == 0
146+
147+
def test_pact_interaction_iterator_next(self) -> None:
148+
"""Test iterator next functions."""
149+
pact = pact_ffi.new_pact("consumer", "provider")
150+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
151+
152+
# Create multiple interactions
153+
pact_ffi.new_interaction(pact, "http")
154+
pact_ffi.new_message_interaction(pact, "async")
155+
pact_ffi.new_sync_message_interaction(pact, "sync")
156+
157+
# Test each iterator type
158+
http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact)
159+
http_count = sum(1 for _ in http_iter)
160+
assert http_count == 1
161+
162+
async_iter = pact_ffi.pact_handle_get_async_message_iter(pact)
163+
async_count = sum(1 for _ in async_iter)
164+
assert async_count == 1
165+
166+
sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact)
167+
sync_count = sum(1 for _ in sync_iter)
168+
assert sync_count == 1
169+
170+
def test_pact_message_iterator_repr(self) -> None:
171+
"""Test PactMessageIterator __repr__ method."""
172+
pact = pact_ffi.new_pact("consumer", "provider")
173+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
174+
175+
iterator = pact_ffi.pact_handle_get_message_iter(pact)
176+
repr_str = repr(iterator)
177+
178+
assert "PactMessageIterator" in repr_str
179+
assert "0x" in repr_str or ">" in repr_str
180+
181+
def test_pact_interaction_str_repr(self) -> None:
182+
"""Test PactInteraction __str__ and __repr__ methods."""
183+
pact = pact_ffi.new_pact("consumer", "provider")
184+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
185+
pact_ffi.new_sync_message_interaction(pact, "test")
186+
187+
# Get an interaction from iterator
188+
sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact)
189+
for interaction in sync_iter:
190+
str_result = str(interaction)
191+
repr_result = repr(interaction)
192+
193+
assert "SynchronousMessage" in str_result
194+
assert "SynchronousMessage" in repr_result
195+
break
196+
197+
def test_multiple_iterator_types_simultaneously(self) -> None:
198+
"""Test using multiple iterator types at the same time."""
199+
pact = pact_ffi.new_pact("consumer", "provider")
200+
pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4)
201+
202+
# Create one of each type
203+
pact_ffi.new_interaction(pact, "http")
204+
pact_ffi.new_message_interaction(pact, "async")
205+
pact_ffi.new_sync_message_interaction(pact, "sync")
206+
207+
# Create all three iterators
208+
http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact)
209+
async_iter = pact_ffi.pact_handle_get_async_message_iter(pact)
210+
sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact)
211+
212+
# Iterate through all of them
213+
http_list = list(http_iter)
214+
async_list = list(async_iter)
215+
sync_list = list(sync_iter)
216+
217+
assert len(http_list) == 1
218+
assert len(async_list) == 1
219+
assert len(sync_list) == 1

0 commit comments

Comments
 (0)