From 8f5cce90d3e3e817025a97051396ecf8e9e1076f Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 18:23:16 -0500 Subject: [PATCH 1/8] perf(ws): adopt orderbook snapshot dicts by identity in _apply_snapshot_inplace _apply_snapshot_inplace wrapped msg.msg.yes / msg.msg.no in dict(...), silently nullifying the ~5x speedup CHANGELOG v2.5.0 #263 advertises. For a 200-level book that's 400 needless re-hashes and reallocs per snapshot on the recv hot path. The OrderbookSnapshotPayload._levels_to_dict BeforeValidator already produces a typed dict[Decimal, Decimal]; the model is discarded immediately after this call so adopting the dicts by identity is safe and matches the original v2.5 design intent (see comment in kalshi/ws/models/orderbook_delta.py). Adds a regression test (TestSnapshotIdentityAdoption) that pins: - state.yes is msg.msg.yes (no defensive copy on adoption) - subsequent _apply_delta_inplace mutates the adopted dict in place AND invalidates the #244 cached Orderbook Closes #296. --- kalshi/ws/orderbook.py | 8 +++-- tests/ws/test_orderbook.py | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/kalshi/ws/orderbook.py b/kalshi/ws/orderbook.py index a62214d6..192e0f03 100644 --- a/kalshi/ws/orderbook.py +++ b/kalshi/ws/orderbook.py @@ -125,8 +125,12 @@ def _apply_snapshot_inplace( """ ticker = msg.msg.market_ticker sid_val = sid if sid is not None else msg.sid - yes_levels = dict(msg.msg.yes) - no_levels = dict(msg.msg.no) + # #263/#296: the validator returns these dicts already typed as + # ``dict[Decimal, Decimal]``; adopt them by identity. The model is + # discarded immediately after this call so there are no external + # references that would observe later in-place delta mutations. + yes_levels = msg.msg.yes + no_levels = msg.msg.no # If a prior book existed under a different sid (e.g. server moved # the ticker between subs), unindex it from the old sid first so diff --git a/tests/ws/test_orderbook.py b/tests/ws/test_orderbook.py index 8d4606ba..24277a27 100644 --- a/tests/ws/test_orderbook.py +++ b/tests/ws/test_orderbook.py @@ -362,3 +362,74 @@ def test_public_apply_snapshot_returns_orderbook(self) -> None: assert book.ticker == "T" assert len(book.yes) == 1 assert len(book.no) == 1 + +class TestSnapshotIdentityAdoption: + """Regression for #296: ``_apply_snapshot_inplace`` must adopt the + validator-built ``dict[Decimal, Decimal]`` by identity (no rebuild). + + The CHANGELOG v2.5.0 #263 claim is "snapshot apply collapses to a single + dict walk"; if a future refactor reintroduces a ``dict(...)`` copy, this + test fails immediately rather than silently regressing recv-loop perf. + """ + + def test_snapshot_adopts_validated_dicts_by_identity(self) -> None: + mgr = OrderbookManager() + # Build with Decimal-keyed dicts directly so the validator's + # identity fast-path engages (see ``_levels_to_dict``). + yes_in: dict[Decimal, Decimal] = {Decimal("0.50"): Decimal("100.00")} + no_in: dict[Decimal, Decimal] = {Decimal("0.45"): Decimal("150.00")} + msg = OrderbookSnapshotMessage.model_validate( + { + "type": "orderbook_snapshot", + "sid": 1, + "seq": 1, + "msg": { + "market_ticker": "T", + "market_id": "id", + "yes": yes_in, + "no": no_in, + }, + } + ) + # NOTE: Pydantic may re-bind the validator output to a fresh model + # field — we don't control that and shouldn't assert it. What this + # test pins down is the SDK-side contract: whatever dict Pydantic + # hands us through ``msg.msg.yes`` / ``.no``, ``_apply_snapshot_inplace`` + # adopts by identity without an extra ``dict(...)`` copy (#296). + mgr._apply_snapshot_inplace(msg) + state = mgr._books["T"] + assert state.yes is msg.msg.yes + assert state.no is msg.msg.no + + def test_delta_mutates_in_place_and_invalidates_cache(self) -> None: + mgr = OrderbookManager() + yes_in: dict[Decimal, Decimal] = {Decimal("0.50"): Decimal("100.00")} + msg = OrderbookSnapshotMessage.model_validate( + { + "type": "orderbook_snapshot", + "sid": 1, + "seq": 1, + "msg": { + "market_ticker": "T", + "market_id": "id", + "yes": yes_in, + "no": {}, + }, + } + ) + mgr._apply_snapshot_inplace(msg) + state = mgr._books["T"] + adopted_yes = state.yes + # Materialize once to populate the #244 cache. + cached = state.to_orderbook() + assert state._cached is cached + + applied = mgr._apply_delta_inplace( + make_delta(price="0.50", delta="25", side="yes") + ) + assert applied is True + # In-place mutation: the dict instance is unchanged, the value moved. + assert state.yes is adopted_yes + assert state.yes[Decimal("0.50")] == Decimal("125.00") + # The cache was invalidated so the next read re-materializes. + assert state._cached is None From 77cf67317cf62a8eb2aaf1fca124117bdeacd3b7 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 18:39:53 -0500 Subject: [PATCH 2/8] fix-up(ws): trim snapshot-adopt comment - Drop issue refs (#263/#296) from inline comment; PR/commit metadata is the appropriate place for them - Tighten three-line block to retain safety rationale - Ruff E302 flagged by bot review was a false positive: tests/ws/test_orderbook.py is already clean --- kalshi/ws/orderbook.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kalshi/ws/orderbook.py b/kalshi/ws/orderbook.py index 192e0f03..8f743970 100644 --- a/kalshi/ws/orderbook.py +++ b/kalshi/ws/orderbook.py @@ -125,10 +125,9 @@ def _apply_snapshot_inplace( """ ticker = msg.msg.market_ticker sid_val = sid if sid is not None else msg.sid - # #263/#296: the validator returns these dicts already typed as - # ``dict[Decimal, Decimal]``; adopt them by identity. The model is - # discarded immediately after this call so there are no external - # references that would observe later in-place delta mutations. + # Adopt by identity — validator already produced dict[Decimal, Decimal], + # and the model is discarded immediately after this call so nothing + # outside _BookState can observe later in-place delta mutations. yes_levels = msg.msg.yes no_levels = msg.msg.no From 876da84b4af07ee8ef7bb28fc3e2b4d91da4622b Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 19:00:02 -0500 Subject: [PATCH 3/8] fix-up(ws): clarify snapshot ownership in comment + docstring - Reframe inline comment as ownership contract instead of caller behavior - Add ownership-transfer note to _apply_snapshot_inplace docstring Addresses round-2 review on #306 --- kalshi/ws/orderbook.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/kalshi/ws/orderbook.py b/kalshi/ws/orderbook.py index 8f743970..73c372a9 100644 --- a/kalshi/ws/orderbook.py +++ b/kalshi/ws/orderbook.py @@ -122,12 +122,15 @@ def _apply_snapshot_inplace( ticker can be torn down later via :meth:`remove_by_sid` — required for all-markets subscriptions that don't pin tickers up front. If omitted, defaults to ``msg.sid`` from the envelope. + + Ownership note: ``msg.msg.yes`` / ``.no`` are adopted by identity + into the new ``_BookState``; callers must treat them as consumed. """ ticker = msg.msg.market_ticker sid_val = sid if sid is not None else msg.sid - # Adopt by identity — validator already produced dict[Decimal, Decimal], - # and the model is discarded immediately after this call so nothing - # outside _BookState can observe later in-place delta mutations. + # Adopt msg.msg.yes / .no by identity. _BookState takes ownership and + # mutates them in place on every delta — callers must not access these + # dicts on `msg` after this returns. yes_levels = msg.msg.yes no_levels = msg.msg.no From b548a5be44290f2d249e4326d6dcb215e706f666 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 19:10:40 -0500 Subject: [PATCH 4/8] fix-up(ws): document orderbook snapshot aliasing on raw subscribers; trim redundant comment - Document on subscribe_orderbook_delta and OrderbookSnapshotPayload that msg.yes/.no are live dicts owned by OrderbookManager after dispatch (raw subscribers must copy or use client.orderbook() for immutability). - Drop the inline ownership comment in _apply_snapshot_inplace; the docstring covers it. - Rephrase the snapshot-identity test comment so it no longer reads as contradicting its own identity assertion. Addresses round-3 review on #306. --- kalshi/ws/client.py | 7 +++++++ kalshi/ws/models/orderbook_delta.py | 5 +++++ kalshi/ws/orderbook.py | 3 --- tests/ws/test_orderbook.py | 8 +++----- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/kalshi/ws/client.py b/kalshi/ws/client.py index 5ac03623..0bfd6f46 100644 --- a/kalshi/ws/client.py +++ b/kalshi/ws/client.py @@ -643,6 +643,13 @@ async def subscribe_ticker( async def subscribe_orderbook_delta( self, *, tickers: list[str] | None = None, maxsize: int = 1000, ) -> AsyncIterator[OrderbookSnapshotMessage | OrderbookDeltaMessage]: + """Subscribe to ``orderbook_delta`` for the given tickers. + + Note: ``OrderbookSnapshotMessage.msg.yes`` and ``.no`` are live dicts + owned by the :class:`OrderbookManager` after this dispatch — they + mutate on every delta. Use :meth:`orderbook` if you need an immutable + snapshot view. + """ params: dict[str, Any] = {"send_initial_snapshot": True} if tickers: params["market_tickers"] = tickers diff --git a/kalshi/ws/models/orderbook_delta.py b/kalshi/ws/models/orderbook_delta.py index b3d918fe..cf803e84 100644 --- a/kalshi/ws/models/orderbook_delta.py +++ b/kalshi/ws/models/orderbook_delta.py @@ -69,6 +69,11 @@ class OrderbookSnapshotPayload(BaseModel): ``ValidationError`` lets the recv loop's malformed-frame handler log it and roll back the seq watermark for #241 rather than silently resetting the local book to empty. + + Aliasing note: when delivered through + :meth:`KalshiWSClient.subscribe_orderbook_delta`, ``yes`` / ``no`` are live + dicts owned by the :class:`OrderbookManager` after dispatch — they + mutate on every subsequent delta. """ market_ticker: str diff --git a/kalshi/ws/orderbook.py b/kalshi/ws/orderbook.py index 73c372a9..4197c65d 100644 --- a/kalshi/ws/orderbook.py +++ b/kalshi/ws/orderbook.py @@ -128,9 +128,6 @@ def _apply_snapshot_inplace( """ ticker = msg.msg.market_ticker sid_val = sid if sid is not None else msg.sid - # Adopt msg.msg.yes / .no by identity. _BookState takes ownership and - # mutates them in place on every delta — callers must not access these - # dicts on `msg` after this returns. yes_levels = msg.msg.yes no_levels = msg.msg.no diff --git a/tests/ws/test_orderbook.py b/tests/ws/test_orderbook.py index 24277a27..972d35ef 100644 --- a/tests/ws/test_orderbook.py +++ b/tests/ws/test_orderbook.py @@ -391,11 +391,9 @@ def test_snapshot_adopts_validated_dicts_by_identity(self) -> None: }, } ) - # NOTE: Pydantic may re-bind the validator output to a fresh model - # field — we don't control that and shouldn't assert it. What this - # test pins down is the SDK-side contract: whatever dict Pydantic - # hands us through ``msg.msg.yes`` / ``.no``, ``_apply_snapshot_inplace`` - # adopts by identity without an extra ``dict(...)`` copy (#296). + # We don't assert state.yes is yes_in (Pydantic may copy the input during + # field binding), but we DO assert state.yes is msg.msg.yes: the SDK must + # adopt the already-validated dict without adding another dict() copy. mgr._apply_snapshot_inplace(msg) state = mgr._books["T"] assert state.yes is msg.msg.yes From 6fb6115ce809452853602948dfd05bb358091312 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 19:20:04 -0500 Subject: [PATCH 5/8] fix-up(ws): add subscriber-side aliasing test for snapshot identity adoption - New TestSnapshotIdentityAdoption.test_subscriber_held_snapshot_dict_mutates_on_delta documents the live-dict contract for raw subscribe_orderbook_delta consumers: a subscriber who stores msg.msg.yes observes future delta mutations because _BookState adopts the validator-built dict by identity. - Pins the contract called out in the docstrings on subscribe_orderbook_delta and OrderbookSnapshotPayload so a future defensive-copy refactor breaks the test instead of silently regressing perf or the documented behavior. Addresses round-4 review on #306. --- tests/ws/test_orderbook.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/ws/test_orderbook.py b/tests/ws/test_orderbook.py index 972d35ef..4320e80a 100644 --- a/tests/ws/test_orderbook.py +++ b/tests/ws/test_orderbook.py @@ -431,3 +431,33 @@ def test_delta_mutates_in_place_and_invalidates_cache(self) -> None: assert state.yes[Decimal("0.50")] == Decimal("125.00") # The cache was invalidated so the next read re-materializes. assert state._cached is None + + def test_subscriber_held_snapshot_dict_mutates_on_delta(self) -> None: + """Documents the live-dict contract for raw ``subscribe_orderbook_delta`` consumers.""" + mgr = OrderbookManager() + yes_in: dict[Decimal, Decimal] = {Decimal("0.50"): Decimal("100.00")} + no_in: dict[Decimal, Decimal] = {Decimal("0.45"): Decimal("150.00")} + msg = OrderbookSnapshotMessage.model_validate( + { + "type": "orderbook_snapshot", + "sid": 1, + "seq": 1, + "msg": { + "market_ticker": "T", + "market_id": "id", + "yes": yes_in, + "no": no_in, + }, + } + ) + mgr._apply_snapshot_inplace(msg) + # A raw subscriber who stored ``msg.msg.yes`` after receiving the snapshot + # frame is holding the live ``_BookState`` dict; the next delta mutates it. + held_yes = msg.msg.yes + applied = mgr._apply_delta_inplace( + make_delta(price="0.60", delta="25", side="yes") + ) + assert applied is True + assert Decimal("0.60") in held_yes + assert held_yes[Decimal("0.60")] == Decimal("25.00") + assert held_yes is mgr._books["T"].yes From c3a7fbb1a5bc8768d36026816bbf676166b62db2 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 19:29:32 -0500 Subject: [PATCH 6/8] fix-up(ws): document aliasing on public apply_snapshot + OrderbookManager class doc Addresses round-5 review on #306: - apply_snapshot docstring now flags that msg.msg.yes/.no are adopted by identity (public callers hit the same aliasing as raw subscribers). - OrderbookManager class doc clarifies that the "no mutations leak in" safety applies to returned Orderbook objects only; input snapshot fields are adopted by reference. --- kalshi/ws/orderbook.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/kalshi/ws/orderbook.py b/kalshi/ws/orderbook.py index 4197c65d..a1b76995 100644 --- a/kalshi/ws/orderbook.py +++ b/kalshi/ws/orderbook.py @@ -89,6 +89,9 @@ class OrderbookManager: Each call to :meth:`apply_snapshot`, :meth:`apply_delta`, or :meth:`get` returns a fresh :class:`Orderbook` instance. Consumers may safely retain references; subsequent updates will not mutate previously-returned books. + (This safety guarantee applies to returned ``Orderbook`` objects; + ``msg.msg.yes`` / ``.no`` fields on input snapshot messages are + adopted by reference — see :class:`OrderbookSnapshotPayload`.) Per-sid tracking (:meth:`tickers_for_sid`, :meth:`remove_by_sid`) lets callers tear down all books owned by a subscription without enumerating @@ -200,6 +203,11 @@ def apply_snapshot(self, msg: OrderbookSnapshotMessage, sid: int | None = None) resulting :class:`Orderbook`. The recv loop bypasses this and calls :meth:`_apply_snapshot_inplace` directly to avoid the unused O(n log n) materialization (#199). + + Ownership note: ``msg.msg.yes`` / ``.no`` are adopted by identity + into the new ``_BookState`` and mutate on every subsequent delta; + callers must treat them as consumed after this returns. The + returned :class:`Orderbook` is a fresh, immutable snapshot. """ self._apply_snapshot_inplace(msg, sid=sid) ticker = msg.msg.market_ticker From 53393c3f93582a8f71daca68f2a02b56e3d8a7b7 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 19:37:28 -0500 Subject: [PATCH 7/8] fix-up(ws): keep public apply_snapshot non-aliasing; defer identity adoption to _apply_snapshot_inplace Addresses round-5 review on #306: - Restore the no-aliasing contract on the public apply_snapshot() by defensively copying state.yes/no after the inplace adoption. The recv loop still benefits from identity adoption via the existing _apply_snapshot_inplace bypass (per #199), where the perf win actually matters; the public wrapper was never on the hot path. - Drop the aliasing warnings from apply_snapshot() and OrderbookManager class docstrings since the public API no longer leaks. - Keep the raw-subscriber aliasing note on subscribe_orderbook_delta and OrderbookSnapshotPayload (those paths still see live dicts). - New test_public_apply_snapshot_does_not_alias_input_msg pins the restored contract: a held msg.msg.yes is unaffected by subsequent deltas through the public apply_snapshot/apply_delta entry points. --- kalshi/ws/orderbook.py | 28 ++++++++++++-------- tests/ws/test_orderbook.py | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/kalshi/ws/orderbook.py b/kalshi/ws/orderbook.py index a1b76995..3ae16c4b 100644 --- a/kalshi/ws/orderbook.py +++ b/kalshi/ws/orderbook.py @@ -89,9 +89,10 @@ class OrderbookManager: Each call to :meth:`apply_snapshot`, :meth:`apply_delta`, or :meth:`get` returns a fresh :class:`Orderbook` instance. Consumers may safely retain references; subsequent updates will not mutate previously-returned books. - (This safety guarantee applies to returned ``Orderbook`` objects; - ``msg.msg.yes`` / ``.no`` fields on input snapshot messages are - adopted by reference — see :class:`OrderbookSnapshotPayload`.) + Input ``OrderbookSnapshotMessage`` arguments are also safe to reuse on + the public API — see :meth:`apply_snapshot`. Raw + ``subscribe_orderbook_delta`` consumers, which bypass this wrapper, see + the live ``_BookState`` dicts; see :class:`OrderbookSnapshotPayload`. Per-sid tracking (:meth:`tickers_for_sid`, :meth:`remove_by_sid`) lets callers tear down all books owned by a subscription without enumerating @@ -199,19 +200,26 @@ def _apply_delta_inplace(self, msg: OrderbookDeltaMessage) -> bool: def apply_snapshot(self, msg: OrderbookSnapshotMessage, sid: int | None = None) -> Orderbook: """Initialize (or reset) a book from a full snapshot. - Public wrapper that mutates in place then materializes the - resulting :class:`Orderbook`. The recv loop bypasses this and - calls :meth:`_apply_snapshot_inplace` directly to avoid the + Public wrapper: mutates in place via :meth:`_apply_snapshot_inplace` + and materializes a fresh :class:`Orderbook`. The recv loop bypasses + this and calls :meth:`_apply_snapshot_inplace` directly to avoid the unused O(n log n) materialization (#199). - Ownership note: ``msg.msg.yes`` / ``.no`` are adopted by identity - into the new ``_BookState`` and mutate on every subsequent delta; - callers must treat them as consumed after this returns. The - returned :class:`Orderbook` is a fresh, immutable snapshot. + ``msg`` is safe to reuse after this call: the manager defensively + copies the adopted dicts into ``_BookState`` so subsequent delta + mutations do not leak back through ``msg.msg.yes`` / ``.no``. The + recv loop, which never holds ``msg`` past dispatch, skips this + defensive copy via the direct inplace path. """ self._apply_snapshot_inplace(msg, sid=sid) ticker = msg.msg.market_ticker state = self._books[ticker] + # Defensive copy on the public path so a caller-supplied ``msg`` keeps + # its original ``.yes`` / ``.no`` dicts and is not aliased to the + # live ``_BookState``. The recv-loop bypass keeps the identity-adopt + # perf win where it matters (#296). + state.yes = dict(state.yes) + state.no = dict(state.no) return state.to_orderbook() def apply_delta(self, msg: OrderbookDeltaMessage) -> Orderbook | None: diff --git a/tests/ws/test_orderbook.py b/tests/ws/test_orderbook.py index 4320e80a..118171c3 100644 --- a/tests/ws/test_orderbook.py +++ b/tests/ws/test_orderbook.py @@ -461,3 +461,55 @@ def test_subscriber_held_snapshot_dict_mutates_on_delta(self) -> None: assert Decimal("0.60") in held_yes assert held_yes[Decimal("0.60")] == Decimal("25.00") assert held_yes is mgr._books["T"].yes + + def test_public_apply_snapshot_does_not_alias_input_msg(self) -> None: + """Public apply_snapshot must defensively copy so the caller's msg is not aliased.""" + from decimal import Decimal + + from kalshi.ws.models.orderbook_delta import ( + OrderbookDeltaMessage, + OrderbookSnapshotMessage, + ) + from kalshi.ws.orderbook import OrderbookManager + + snapshot_payload = { + "type": "orderbook_snapshot", + "sid": 7, + "seq": 1, + "msg": { + "market_ticker": "PUB-T", + "market_id": "id", + "yes": {Decimal("0.30"): Decimal("5")}, + "no": {Decimal("0.70"): Decimal("3")}, + }, + } + msg = OrderbookSnapshotMessage.model_validate(snapshot_payload) + held_yes = msg.msg.yes + + mgr = OrderbookManager() + mgr.apply_snapshot(msg) + + # Public path: state must NOT share identity with the caller's msg. + state = mgr._books["PUB-T"] + assert state.yes is not held_yes + assert state.no is not msg.msg.no + assert state.yes == {Decimal("0.30"): Decimal("5")} + + # A subsequent delta mutates state but leaves the caller's msg dict untouched. + delta = OrderbookDeltaMessage.model_validate({ + "type": "orderbook_delta", + "sid": 7, + "seq": 2, + "msg": { + "market_ticker": "PUB-T", + "market_id": "id", + "price": Decimal("0.30"), + "delta": Decimal("4"), + "side": "yes", + }, + }) + mgr.apply_delta(delta) + assert state.yes[Decimal("0.30")] == Decimal("9") + # Caller's held reference is untouched — the public-API safety contract. + assert held_yes[Decimal("0.30")] == Decimal("5") + From 5250428b0626c943cfd2e460f0791084b106789a Mon Sep 17 00:00:00 2001 From: Jeff West Date: Thu, 21 May 2026 19:45:29 -0500 Subject: [PATCH 8/8] fix-up(ws): drop redundant inline imports; clarify defensive-copy intent Addresses round-6 review on #306: - Inline imports inside test_public_apply_snapshot_does_not_alias_input_msg are already module-level; drop them. - state.yes = dict(state.yes) reads as a self-copy; rewrite as dict(msg.msg.yes) to make "copy the caller's input back out" the visible intent. - Skipped blank-line nit (5th false positive; ruff clean for the sixth time). - CHANGELOG entry deferred to v2.6.0 release packaging (handled by orchestrator per the audit-closure plan, not in-PR per project convention). --- kalshi/ws/orderbook.py | 13 +++++++------ tests/ws/test_orderbook.py | 8 -------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/kalshi/ws/orderbook.py b/kalshi/ws/orderbook.py index 3ae16c4b..a70f5e92 100644 --- a/kalshi/ws/orderbook.py +++ b/kalshi/ws/orderbook.py @@ -214,12 +214,13 @@ def apply_snapshot(self, msg: OrderbookSnapshotMessage, sid: int | None = None) self._apply_snapshot_inplace(msg, sid=sid) ticker = msg.msg.market_ticker state = self._books[ticker] - # Defensive copy on the public path so a caller-supplied ``msg`` keeps - # its original ``.yes`` / ``.no`` dicts and is not aliased to the - # live ``_BookState``. The recv-loop bypass keeps the identity-adopt - # perf win where it matters (#296). - state.yes = dict(state.yes) - state.no = dict(state.no) + # Defensive copy on the public path so the caller-supplied ``msg`` + # keeps its original ``.yes`` / ``.no`` dicts; ``_apply_snapshot_inplace`` + # adopted them by identity above, so we copy via ``msg.msg`` to make + # the "copy the caller's input" intent explicit. The recv-loop bypass + # keeps the identity-adopt perf win where it matters. + state.yes = dict(msg.msg.yes) + state.no = dict(msg.msg.no) return state.to_orderbook() def apply_delta(self, msg: OrderbookDeltaMessage) -> Orderbook | None: diff --git a/tests/ws/test_orderbook.py b/tests/ws/test_orderbook.py index 118171c3..4ca1753b 100644 --- a/tests/ws/test_orderbook.py +++ b/tests/ws/test_orderbook.py @@ -464,14 +464,6 @@ def test_subscriber_held_snapshot_dict_mutates_on_delta(self) -> None: def test_public_apply_snapshot_does_not_alias_input_msg(self) -> None: """Public apply_snapshot must defensively copy so the caller's msg is not aliased.""" - from decimal import Decimal - - from kalshi.ws.models.orderbook_delta import ( - OrderbookDeltaMessage, - OrderbookSnapshotMessage, - ) - from kalshi.ws.orderbook import OrderbookManager - snapshot_payload = { "type": "orderbook_snapshot", "sid": 7,