Repository navigation
feat(testing): RabbitMQ, NATS, Redis and MQTT handlers and publishers assert on their own fields - #3120
Draft
Lancetnik wants to merge 11 commits into
Draft
feat(testing): RabbitMQ, NATS, Redis and MQTT handlers and publishers assert on their own fields#3120Lancetnik wants to merge 11 commits into
Lancetnik wants to merge 11 commits into
Conversation
Lancetnik
requested review from
IvanKirpichnikov,
borisalekseev and
powersemmi
as code owners
September 12, 2026 12:26
Lancetnik
marked this pull request as draft
September 12, 2026 12:44
6 tasks done
Lancetnik
force-pushed
the
feat/557-broker-fields
branch
from
September 12, 2026 14:49
60b0e7f to
43ead48
Compare
Lancetnik
marked this pull request as ready for review
September 12, 2026 14:50
Lancetnik
marked this pull request as draft
September 12, 2026 14:55
…tition` `Endpoint` reads the wrapper class it builds from `_call_wrapper_class`, and `ensure_call_wrapper` takes that class while still returning an already wrapped handler untouched, so the first decorator on a shared handler decides. Both Kafka packages set `KafkaHandlerCallWrapper` on their `LogicSubscriber` and `LogicPublisher`, and repeat the `__call__` overloads with the return type narrowed to it. `KafkaCallAssertions` in `_internal/kafka` declares the three Call assertions with `key=` and `partition=` and forwards them as Broker fields. `ExpectedCall` carries the fields with a reader `(name, message) -> value` that each package supplies from its `call_wrapper` module: attributes on an aiokafka record, method calls on a confluent message, a `SetupError` naming Kafka for a raw message of another broker. A batch refuses the two fields as it refuses `headers`. The in-memory confluent message is now a `confluent_kafka.Message` subclass, so the reader takes it for the client's own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The AIOKafka and Confluent tabs gain a `test_kafka_fields` snippet, the context example no longer reaches for the key by path, and the testing page says each broker's handlers and publishers take that broker's own fields. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eir reader `ExpectedCall.broker_fields` is one `BrokerFields`: the values asked for, already stripped of the ones left EMPTY, and the reader that answers them. The recorder no longer filters EMPTY twice or guards a reader that may be missing, and the Kafka mixin's helper is named for what it returns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The confluent reader's `isinstance` guard had no test going red on its own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyright spells an `async def`'s return as `CoroutineType[...]` where mypy says `Coroutine[...]`, so an `assert_type` over an async handler cannot satisfy both checkers; a sync handler reads the same in both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…der` Both Kafka packages built the same reader by hand: an isinstance guard, the same refusal text, and one line that differs (an attribute on aiokafka, a method on confluent). `field_reader(record, read)` in `_internal/kafka` builds it once, so each `call_wrapper` module opens with its class and binds the reader inline. The mixin declares the reader as a `ClassVar` and looks it up on the class, replacing a staticmethod stub that only raised; a `LogicPublisher` takes the reader from its wrapper class instead of restating the import. The refusal names the message class a shared handler got (`RabbitMessage`), not the raw client type. `ExpectedCall.message_fields()` introspects the dataclass again instead of listing its fields by hand. The shared-handler tests carry `require_aiopika`, as the other cross-broker tests do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he recorder Every broker's wrapper guards its reader the same way: the raw message must be the client's own class, or the assertion refuses it naming the broker. The guard lived in `_internal/kafka` with "Kafka" spelled into its text; it now takes the broker's name and lives with `BrokerFields`, so the next broker is a wrapper class and a `read` callable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… `routing_key`, `message_id`, `priority` `RabbitCallAssertions` in `faststream/rabbit/call_wrapper.py` declares the three Call assertions with the four fields, under the names `publish()` takes, and forwards them as Broker fields; `RabbitHandlerCallWrapper` beside it binds the reader over the aio_pika `IncomingMessage`, which the in-memory `PatchedMessage` subclasses, so a raw message of another broker is refused with a `SetupError` naming RabbitMQ. `RabbitSubscriber` and `RabbitPublisher` set the wrapper class and narrow their `__call__` return types to it, and the publisher inherits the mixin with the same reader. The Rabbit `advanced_testing.py` snippet publishes with `routing_key=` and asserts it; the testing page gains the RabbitMQ tab and says which fields a RabbitMQ handler or publisher takes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`NatsHandlerCallWrapper` and the NATS `LogicPublisher` take `subject=` on the three Call assertions: the whole address a core or JetStream message arrived on, which `path` does not carry. Every subscriber variant returns the wrapper through the overloads on `LogicSubscriber`. A key-value or object-store entry has no subject: asking for it raises `SetupError` naming the kind; another broker's message is refused as a non-NATS one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…st`, `stream` `RedisCallAssertions` gives the three Call assertions the names Redis's `publish()` takes; `RedisHandlerCallWrapper` binds a reader that answers the one matching the raw message's `type`, so `list=` on a pub/sub message is refused naming the kind, and another broker's message is refused naming Redis. Every subscriber variant inherits the wrapper class and the narrowed `__call__` from `LogicSubscriber`; `LogicPublisher` inherits the mixin, so every publisher variant takes the same names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… `retain` `MqttCallAssertions` declares the three Call assertions with `topic=`, `qos=` and `retain=`, and `MqttHandlerCallWrapper` binds their reader to the zmqtt message: the concrete topic the message arrived on (`path` holds only the template's captures), the `QoS` enum and the retain flag, as delivered. Both live in `faststream/mqtt/call_wrapper.py`, since MQTT is one package. The subscriber base and `MQTTPublisher` hand out that wrapper and narrow their `__call__` to it; a raw message of another broker is refused naming MQTT. The MQTT `advanced_testing.py` snippet asserts `topic=` and `qos=`, and the testing page shows it beside the Kafka tabs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IvanKirpichnikov
force-pushed
the
feat/557-broker-fields
branch
from
September 14, 2026 21:35
43ead48 to
4bfa491
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #3113. Tickets 03–06 of the Call assertions plan, one broker per commit.
#3113 gave Kafka handlers and publishers
keyandpartitionas named arguments of the three Call assertions. The other four brokers now take their own fields the same way, under the names theirpublish()uses:exchange,routing_key,message_id,prioritysubjectchannel,list,streamtopic,qos,retainEvery field compares exactly, as the client delivered it. A batch refuses them the way it refuses
headers, and a field asked of a message another broker delivered raisesSetupErrornaming the broker.What changes
A preparatory commit moves
field_readerfrom_internal/kafkato_internal/testing/calls.py, besideBrokerFields, and gives it the broker's name as a parameter. The guard and its refusal text were Kafka's; now every wrapper shares them, and a broker is a wrapper class and areadcallable.Each broker commit adds
faststream/<broker>/call_wrapper.pywith the<Broker>CallAssertionsmixin and the<Broker>HandlerCallWrapper, sets_call_wrapper_classon the subscriber base and the publisher, and narrows their__call__to the wrapper, as Kafka does. The classes are internal and not exported.IncomingMessagewithgetattr; the in-memoryPatchedMessageis a subclass, so the guard holds underTestRabbitBroker.subjectfor a core or JetStream message; a key-value or object-store entry has none, sosubject=on one raisesSetupErrornaming the kind. The attribute and the overloads sit once onLogicSubscriber, inherited by all eight variants.channel, and itstypesays the kind. Naming the wrong kind (list=on a pub/sub message) raisesSetupErrorsaying which kind the message is and which name answers for it.topic,qosandretainoff the zmqtt message. Themax_workersoverloads onMQTTRegistrator.subscriberthis needs landed in fix(mqtt):subscriber()is typed bymax_workers, so a decorated handler stays typed #3123.Neither RabbitMQ nor MQTT has batch consumption, so those two have no batch refusal test; the generic guard stays covered by Kafka and Redis.
Tests and docs
test_test_client.py: the three methods with the fields on a handler and on a publisher, the mismatch text naming the field, the batch refusal where the broker has batches, the wrong-kind refusal (NATS stores, Redis kinds), and a handler shared with another broker refusing the field for that broker's message.tests/mypy/<broker>.pypins a decorated handler and a published handler as the broker's wrapper and calls the three methods on a publisher.advanced_testing.pysnippet gains atest_<broker>_fields, the docs runner calls it, and the "Validates Message Fields" section names every broker's fields with a tab per broker.🤖 Generated with Claude Code