Skip to content

Commit 138a865

Browse files
Annotate module-level constants in wire/constants.py as Final
Per PEP 591, immutable module-level constants should be annotated as Final so type checkers and readers see the binding intent. Module constants in the wire layer were bare assignments; mypy treated them as mutable module-level variables. Sweep the wire constants module: PROTOCOL_VERSION{,_LEGACY}, WORD_SIZE, HEADER_SIZE, ROW_DONE/PART_BYTE/MARKER, SQLITE_* primary codes, SQLITE_IOERR_NOT_LEADER / LEADERSHIP_LOST, DQLITE_PROTO/NOTFOUND/PARSE, SQLITE_PRIMARY_CODE_MASK, the auto-rollback set members, and the default cap constants. Tests and mypy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 03f15bb commit 138a865

1 file changed

Lines changed: 29 additions & 28 deletions

File tree

src/dqlitewire/constants.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"""Protocol constants for dqlite wire protocol."""
22

33
from enum import IntEnum
4+
from typing import Final
45

56
# Protocol versions
6-
PROTOCOL_VERSION = 1
7-
PROTOCOL_VERSION_LEGACY = 0x86104DD760433FE5 # Pre-1.0 dqlite servers
7+
PROTOCOL_VERSION: Final[int] = 1
8+
PROTOCOL_VERSION_LEGACY: Final[int] = 0x86104DD760433FE5 # Pre-1.0 dqlite servers
89

910
# Word size in bytes (all messages are padded to 8-byte boundaries)
10-
WORD_SIZE = 8
11+
WORD_SIZE: Final[int] = 8
1112

1213
# Header size in bytes
13-
HEADER_SIZE = 8
14+
HEADER_SIZE: Final[int] = 8
1415

1516
# Row markers — written as full uint64 words on the wire. Detection
1617
# validates all 8 bytes of the sentinel (via ``_ROW_DONE_MARKER`` /
@@ -20,10 +21,10 @@
2021
# or 0xEE could be confused with a marker. Use the uint64 constants
2122
# below for encoding and the single-byte constants to build the detection
2223
# byte sequences.
23-
ROW_DONE_BYTE = 0xFF
24-
ROW_PART_BYTE = 0xEE
25-
ROW_DONE_MARKER = 0xFFFFFFFFFFFFFFFF
26-
ROW_PART_MARKER = 0xEEEEEEEEEEEEEEEE
24+
ROW_DONE_BYTE: Final[int] = 0xFF
25+
ROW_PART_BYTE: Final[int] = 0xEE
26+
ROW_DONE_MARKER: Final[int] = 0xFFFFFFFFFFFFFFFF
27+
ROW_PART_MARKER: Final[int] = 0xEEEEEEEEEEEEEEEE
2728

2829

2930
class RequestType(IntEnum):
@@ -108,10 +109,10 @@ class NodeRole(IntEnum):
108109
# downstream callers can ``code == SQLITE_BUSY`` instead of
109110
# ``code == 5``. Source: ``dqlite-upstream/src/gateway.c`` emit
110111
# sites, ``include/dqlite.h`` (extended IOERR variants).
111-
SQLITE_ERROR = 1 # generic SQL error (e.g. nonempty statement tail)
112-
SQLITE_BUSY = 5 # busy retry-or-fail — engine-side OR Raft-side
113-
SQLITE_NOTFOUND = 12 # gateway.c LOOKUP_DB / LOOKUP_STMT
114-
SQLITE_PROTOCOL = 15 # gateway.c "bad format version" / wire mismatch
112+
SQLITE_ERROR: Final[int] = 1 # generic SQL error (e.g. nonempty statement tail)
113+
SQLITE_BUSY: Final[int] = 5 # busy retry-or-fail — engine-side OR Raft-side
114+
SQLITE_NOTFOUND: Final[int] = 12 # gateway.c LOOKUP_DB / LOOKUP_STMT
115+
SQLITE_PROTOCOL: Final[int] = 15 # gateway.c "bad format version" / wire mismatch
115116

116117
# SQLite extended error codes that signal leader changes in a dqlite
117118
# cluster. Upstream definitions in ``dqlite-upstream/include/dqlite.h``:
@@ -122,9 +123,9 @@ class NodeRole(IntEnum):
122123
# where ``SQLITE_IOERR = 10``. Callers (``dqliteclient`` and
123124
# ``sqlalchemy-dqlite``) import these to decide whether to invalidate a
124125
# connection and retry against a fresh leader.
125-
SQLITE_IOERR = 10
126-
SQLITE_IOERR_NOT_LEADER = SQLITE_IOERR | (40 << 8) # 10250
127-
SQLITE_IOERR_LEADERSHIP_LOST = SQLITE_IOERR | (41 << 8) # 10506
126+
SQLITE_IOERR: Final[int] = 10
127+
SQLITE_IOERR_NOT_LEADER: Final[int] = SQLITE_IOERR | (40 << 8) # 10250
128+
SQLITE_IOERR_LEADERSHIP_LOST: Final[int] = SQLITE_IOERR | (41 << 8) # 10506
128129
LEADER_ERROR_CODES: frozenset[int] = frozenset(
129130
{SQLITE_IOERR_NOT_LEADER, SQLITE_IOERR_LEADERSHIP_LOST}
130131
)
@@ -136,20 +137,20 @@ class NodeRole(IntEnum):
136137
# ``dqlite-upstream/src/protocol.h`` (DQLITE_PROTO),
137138
# ``dqlite-upstream/src/lib/registry.h`` (DQLITE_NOTFOUND), and
138139
# ``dqlite-upstream/src/lib/serialize.h`` (DQLITE_PARSE).
139-
DQLITE_PROTO = 1001 # protocol.h:9 — Raft FSM-internal protocol error.
140+
DQLITE_PROTO: Final[int] = 1001 # protocol.h:9 — Raft FSM-internal protocol error.
140141
# Currently emitted only inside command.c / fsm.c
141142
# apply paths and never reaches gateway.c::failure(),
142143
# but included here for namespace completeness so a
143144
# future change that surfaces it through the gateway
144145
# passes through ``primary_sqlite_code`` cleanly.
145-
DQLITE_NOTFOUND = 1002 # registry lookup miss (server-side scratch)
146-
DQLITE_PARSE = 1005 # gateway.c unrecognized request type / schema
146+
DQLITE_NOTFOUND: Final[int] = 1002 # registry lookup miss (server-side scratch)
147+
DQLITE_PARSE: Final[int] = 1005 # gateway.c unrecognized request type / schema
147148

148149
# Bit mask for extracting the primary (low-byte) code from an
149150
# extended SQLite error code. Upstream encodes extended codes as
150151
# ``primary | (sub << 8)``; ``code & SQLITE_PRIMARY_CODE_MASK``
151152
# recovers the primary. Use via :func:`primary_sqlite_code`.
152-
SQLITE_PRIMARY_CODE_MASK = 0xFF
153+
SQLITE_PRIMARY_CODE_MASK: Final[int] = 0xFF
153154

154155

155156
_DQLITE_NAMESPACE_CODES: frozenset[int] = frozenset({DQLITE_PROTO, DQLITE_NOTFOUND, DQLITE_PARSE})
@@ -209,11 +210,11 @@ def primary_sqlite_code(code: int) -> int:
209210
# without inspecting the message text; the client-side handler at
210211
# ``dqliteclient.connection`` carves out the Raft-side
211212
# "checkpoint in progress" case explicitly.
212-
SQLITE_ABORT = 4 # operation aborted (e.g., sqlite3_interrupt) — defensive
213-
SQLITE_NOMEM = 7 # out of memory
214-
SQLITE_INTERRUPT = 9 # query interrupted via INTERRUPT
215-
SQLITE_CORRUPT = 11 # database disk image malformed — defensive
216-
SQLITE_FULL = 13 # database/disk full
213+
SQLITE_ABORT: Final[int] = 4 # operation aborted (e.g., sqlite3_interrupt) — defensive
214+
SQLITE_NOMEM: Final[int] = 7 # out of memory
215+
SQLITE_INTERRUPT: Final[int] = 9 # query interrupted via INTERRUPT
216+
SQLITE_CORRUPT: Final[int] = 11 # database disk image malformed — defensive
217+
SQLITE_FULL: Final[int] = 13 # database/disk full
217218
# Auxiliary database-file format errors. Both surface from the engine
218219
# when the file on disk cannot be read as a SQLite database — schema
219220
# version mismatch, header magic mismatch, or a non-database file
@@ -223,8 +224,8 @@ def primary_sqlite_code(code: int) -> int:
223224
# in the wire layer alongside the other SQLite primaries lets both
224225
# the dialect and the dbapi import them by name instead of inlining
225226
# magic literals.
226-
SQLITE_FORMAT = 24
227-
SQLITE_NOTADB = 26
227+
SQLITE_FORMAT: Final[int] = 24
228+
SQLITE_NOTADB: Final[int] = 26
228229

229230
TX_AUTO_ROLLBACK_PRIMARY_CODES: frozenset[int] = frozenset(
230231
{SQLITE_ABORT, SQLITE_NOMEM, SQLITE_INTERRUPT, SQLITE_IOERR, SQLITE_CORRUPT, SQLITE_FULL}
@@ -237,10 +238,10 @@ def primary_sqlite_code(code: int) -> int:
237238
# any realistic legitimate result set. Forwarded to every
238239
# :class:`DqliteConnection` the public ``connect()`` /
239240
# ``ConnectionPool`` / dbapi ``connect()`` entry points hand out.
240-
DEFAULT_MAX_TOTAL_ROWS = 10_000_000
241+
DEFAULT_MAX_TOTAL_ROWS: Final[int] = 10_000_000
241242

242243
# Per-query continuation-frame cap. Complements
243244
# ``DEFAULT_MAX_TOTAL_ROWS``: a server sending one row per frame can
244245
# inflict O(n) Python decode work where n is the row cap; the frame
245246
# cap bounds that work even when the row cap is large.
246-
DEFAULT_MAX_CONTINUATION_FRAMES = 100_000
247+
DEFAULT_MAX_CONTINUATION_FRAMES: Final[int] = 100_000

0 commit comments

Comments
 (0)