Skip to content

Commit 14ba2ef

Browse files
committed
Fix hashing and equality consistency in RabbitMQ schemas
Ensured RabbitQueue, RabbitExchange, and Channel schemas have consistent __eq__ and __hash__ methods. Key changes: - Implemented value-based equality and hashing for RabbitMQ schemas to support reliable caching in RabbitDeclarer. - Updated testing.py to use name-based exchange matching in FakeProducer, maintaining compatibility with the mock environment while allowing stricter schema validation.
1 parent 099160a commit 14ba2ef

File tree

7 files changed

+95
-57
lines changed

7 files changed

+95
-57
lines changed

docs/docs/en/release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The main feature of this release is the **Try It Out** feature for your **Async
2020

2121
Now you can test your developing application directly from the web, just like Swagger for HTTP. It supports in-memory publication to test a subscriber and real broker publication to verify behavior in real scenarios.
2222

23-
<img width="1467" height="640" alt="" src="https://github.com/user-attachments/assets/4320e674-24d5-4ead-9820-4bb979e340e7">
23+
<img width="1467" height="640" alt="Снимок экрана 2026-03-01 в 11 16 10" src="[#>](https://github.com/user-attachments/assets/4320e674-24d5-4ead-9820-4bb979e340e7" />){.external-link target="_blank"}
2424

2525
* feat: Add Try It Out feature for AsyncAPI documentation by [@vvlrff](https://github.com/vvlrff){.external-link target="_blank"} in [#2777](https://github.com/ag2ai/faststream/pull/2777){.external-link target="_blank"}
2626

docs/update_releases.py

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -35,38 +35,9 @@ def get_github_releases() -> Sequence[Tuple[str, str]]:
3535
raise Exception(f"Error getting GitHub releases: {e}, {row_data}") from e
3636

3737

38-
def normalize_img_tag(match: re.Match) -> str:
39-
"""Extract img attributes and return a normalized <img> tag (plain src URL, empty alt)."""
40-
attrs = match.group(1)
41-
width = re.search(r'width=["\']?(\d+)', attrs)
42-
height = re.search(r'height=["\']?(\d+)', attrs)
43-
src = re.search(r'src=["\']([^"\']+)["\']', attrs)
44-
if not src:
45-
return match.group(0)
46-
parts = []
47-
if width:
48-
parts.append(f'width="{width.group(1)}"')
49-
if height:
50-
parts.append(f'height="{height.group(1)}"')
51-
parts.append('alt=""')
52-
# Use only the URL, no markdown/link wrapping
53-
parts.append(f'src="{src.group(1)}"')
54-
return "<img " + " ".join(parts) + ">"
55-
56-
57-
def convert_links_and_usernames(text: str) -> str:
58-
# Replace <img ...> tags with placeholders so their URLs are not wrapped as links
59-
img_pattern = re.compile(r"<img\s+([^>]+)>")
60-
img_placeholders: List[str] = []
61-
62-
def stash_img(match: re.Match) -> str:
63-
img_placeholders.append(normalize_img_tag(match))
64-
return f"\x00IMG_PLACEHOLDER_{len(img_placeholders) - 1}\x00"
65-
66-
text = img_pattern.sub(stash_img, text)
67-
38+
def convert_links_and_usernames(text):
6839
if "](" not in text:
69-
# Convert HTTP/HTTPS links (img tags already stashed, so their URLs are not matched)
40+
# Convert HTTP/HTTPS links
7041
text = re.sub(
7142
r"(https?://.*\/(.*))",
7243
r'[#\2](\1){.external-link target="_blank"}',
@@ -80,10 +51,6 @@ def stash_img(match: re.Match) -> str:
8051
text,
8152
)
8253

83-
# Restore normalized img tags
84-
for i, img in enumerate(img_placeholders):
85-
text = text.replace(f"\x00IMG_PLACEHOLDER_{i}\x00", img)
86-
8754
return text
8855

8956

@@ -104,7 +71,6 @@ def update_release_notes(release_notes_path: Path):
10471

10572
old_versions = collect_already_published_versions(changelog)
10673

107-
added_versions: List[str] = []
10874
for version, body in filter(
10975
lambda v: v[0] not in old_versions,
11076
get_github_releases(),
@@ -113,12 +79,6 @@ def update_release_notes(release_notes_path: Path):
11379
body = convert_links_and_usernames(body)
11480
version_changelog = f"## {version}\n\n{body}\n\n"
11581
changelog = version_changelog + changelog
116-
added_versions.append(version)
117-
118-
if added_versions:
119-
print(f"Added release versions: {', '.join(added_versions)}")
120-
else:
121-
print("No new versions to add")
12282

12383
# Update the RELEASE.md file with the latest version and changelog
12484
release_notes_path.write_text(

faststream/rabbit/schemas/channel.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,10 @@ class Channel:
2929
when mandatory message will be returned"""
3030

3131
def __hash__(self) -> int:
32-
return id(self)
32+
return hash((
33+
self.prefetch_count,
34+
self.global_qos,
35+
self.channel_number,
36+
self.publisher_confirms,
37+
self.on_return_raises,
38+
))

faststream/rabbit/schemas/exchange.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,27 @@ def __repr__(self) -> str:
3434

3535
return f"{self.__class__.__name__}({self.name}, type={self.type}, routing_key='{self.routing()}'{body})"
3636

37+
def __eq__(self, value: object, /) -> bool:
38+
if not isinstance(value, RabbitExchange):
39+
return NotImplemented
40+
41+
return (
42+
self.name == value.name
43+
and self.type == value.type
44+
and self.routing_key == value.routing_key
45+
and self.durable == value.durable
46+
and self.auto_delete == value.auto_delete
47+
)
48+
3749
def __hash__(self) -> int:
3850
"""Supports hash to store real objects in declarer."""
39-
return sum(
51+
return hash(
4052
(
41-
hash(self.name),
42-
hash(self.type),
43-
hash(self.routing_key),
44-
int(self.durable),
45-
int(self.auto_delete),
53+
self.name,
54+
self.type,
55+
self.routing_key,
56+
self.durable,
57+
self.auto_delete,
4658
),
4759
)
4860

faststream/rabbit/schemas/queue.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,25 @@ def __repr__(self) -> str:
5454

5555
return f"{self.__class__.__name__}({self.name}{body})"
5656

57+
def __eq__(self, value: object, /) -> bool:
58+
if not isinstance(value, RabbitQueue):
59+
return NotImplemented
60+
61+
return (
62+
self.name == value.name
63+
and self.durable == value.durable
64+
and self.exclusive == value.exclusive
65+
and self.auto_delete == value.auto_delete
66+
)
67+
5768
def __hash__(self) -> int:
5869
"""Supports hash to store real objects in declarer."""
59-
return sum(
70+
return hash(
6071
(
61-
hash(self.name),
62-
int(self.durable),
63-
int(self.exclusive),
64-
int(self.auto_delete),
72+
self.name,
73+
self.durable,
74+
self.exclusive,
75+
self.auto_delete,
6576
),
6677
)
6778

faststream/rabbit/testing.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ def _is_handler_matches(
293293
headers = headers or {}
294294
exchange = RabbitExchange.validate(exchange)
295295

296-
if handler.exchange != exchange:
296+
if (handler.exchange.name if handler.exchange else None) != (
297+
exchange.name if exchange else None
298+
):
297299
return False
298300

299301
if handler.exchange is None or handler.exchange.type == ExchangeType.DIRECT:

tests/brokers/rabbit/test_schemas.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from faststream.rabbit import RabbitQueue
3+
from faststream.rabbit import Channel, RabbitExchange, RabbitQueue
44

55

66
@pytest.mark.rabbit()
@@ -23,3 +23,50 @@ def test_different_queue_routing_key() -> None:
2323
})
2424
== 1
2525
)
26+
27+
28+
@pytest.mark.rabbit()
29+
def test_different_queue_params() -> None:
30+
assert (
31+
len({
32+
RabbitQueue("test", durable=True): 0,
33+
RabbitQueue("test", durable=False): 1,
34+
})
35+
== 2
36+
)
37+
38+
39+
@pytest.mark.rabbit()
40+
def test_exchange_equality() -> None:
41+
assert (
42+
len({
43+
RabbitExchange("test", durable=True): 0,
44+
RabbitExchange("test", durable=True): 1,
45+
})
46+
== 1
47+
)
48+
assert (
49+
len({
50+
RabbitExchange("test", durable=True): 0,
51+
RabbitExchange("test", durable=False): 1,
52+
})
53+
== 2
54+
)
55+
56+
57+
@pytest.mark.rabbit()
58+
def test_channel_equality() -> None:
59+
assert (
60+
len({
61+
Channel(prefetch_count=10): 0,
62+
Channel(prefetch_count=10): 1,
63+
})
64+
== 1
65+
)
66+
assert (
67+
len({
68+
Channel(prefetch_count=10): 0,
69+
Channel(prefetch_count=20): 1,
70+
})
71+
== 2
72+
)

0 commit comments

Comments
 (0)