Fix five correctness bugs in the Python wrapper - #1
Merged
Conversation
Terminal layout, both examples:
- meters._default_width() left the bar six columns too wide, so every
meter row overflowed the terminal and wrapped, tearing the
full-screen frame apart on any window from ~57 to ~110 columns. The
magic 26 is now a derived ROW_CHROME constant.
- meters_tui._bar_width() was over-generous by the same kind of margin.
Rich crops an over-wide grid cell with an ellipsis rather than
wrapping, so the right-hand end of every bar — including the
peak-hold marker — was silently chopped off at 80 and 100 columns.
- The TUI's meters table hard-coded a 16-column label, but the --all
labels run to 18 ("v2 strobe seg high"), so three of the eleven raw
rows wrapped onto a second line. The label column is now derived from
the widest label the table can show.
Messages and errors:
- nrpn.sysex() masked its header bytes to seven bits, so a caller
mistake such as DeviceModel.set_param(0x80, 4, v) was silently
retargeted at page 0 — the string/morph page — instead of surfacing.
It now raises ValueError, matching protocol.push_field.
- ModuleSlot.parse() raised a bare ValueError for an unknown slot, so
slot_enable_cc("nope") and SlotEnable("nope", True).message() escaped
an `except LibKPError`, while DeviceModel.set_effect_enabled raised
UnknownSlotError for the same input. All four paths now agree.
- libkp.__init__ exported every error class except TimeoutErrorLibKP,
which Session.connect and Session.handshake both raise.
Robustness:
- meters.run() built its view and took over the terminal outside the
try, so a write failure after connect leaked the socket and the
ingest/writer tasks.
Tests cover each fix; the TUI layout test skips without the tui extra.
test_public_api guards the export surface and test_conformance now
rejects an empty vector case list, which would otherwise collect zero
tests and pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016BcyRYArvAWDXdS8KZjVju
gotwalt
added a commit
that referenced
this pull request
Aug 24, 2026
Fix five correctness bugs in the Python wrapper
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.
Review of the hand-written Python wrapper around
src/libkp/_generated.py(the generated module itself is out of scope). Five real bugs, each with a regression test that fails before the fix.Fixed
Terminal layout — both examples render wider than they claim
examples/meters.py::_default_width()returnedcolumns - LABEL_WIDTH - 26, but a meter row spendsLABEL_WIDTH + 31columns on chrome (indent, label, brackets, value,range …readout). Every row came out six columns too wide, so on any terminal from ~57 to ~110 columns every meter row wrapped and the full-screen frame — drawn withHOME+ per-lineCLEAR_EOL— tore apart. Measured before the fix: an 80-column terminal produced 85-column rows. The magic26is now a derivedROW_CHROMEconstant.examples/meters_tui.py::_bar_width()had the same class of bug withself.size.width - 34. Rich crops an over-wide grid cell with an ellipsis instead of wrapping, so the right-hand end of every bar — including the peak-hold marker — was silently chopped at 80 and 100 columns.The TUI meters table hard-coded
width=16for its label column, but the--alllabels reach 18 characters ("v2 strobe seg high"), so three of the eleven raw rows wrapped onto a second line with the bar misaligned below the label. The column width is now derived from the widest label the table can show.nrpn.sysex()silently retargeted out-of-range addressesIt masked every header byte with
& 0x7F.DeviceModel.set_paramis documented as "the escape hatch for any address", soset_param(0x80, 4, value)built a perfectly valid message aimed at page 0 — the string/morph page — rather than surfacing the mistake. It now raisesValueError, consistent withprotocol.push_field. (Rust/Swift takeu8/UInt8and emit the raw byte, producing a corrupt SysEx; raising is better than either.) Value masking inset_single/control_change/program_changeis deliberate and unchanged.Unknown effect slots raised two different exception types
ModuleSlot.parse()raised a bareValueError, soslot_enable_cc("nope")andSlotEnable("nope", True).message()escaped anexcept LibKPError, whileDeviceModel.set_effect_enabled("nope", True)raisedUnknownSlotErrorfor the same input. All four entry points now raiseUnknownSlotError. The# pragma: no cover - defensivewas wrong — the branch is reachable from two public functions.TimeoutErrorLibKPwas not exportedEvery other member of
libkp.errors.__all__is re-exported from the package root, but this one — raised by bothSession.connectandSession.handshake— was missing.meters.run()leaked the session on a setup failureThe view, the event subscription, and the terminal takeover all happened outside the
try, so a write failure afterDeviceModel.connect(a closed pipe, say) left the socket and the ingest/writer tasks running.Tests
test_meters_example.pyand the newtest_meters_tui.pyassert that no row overflows or gets truncated at 72/80/100/120/160 columns; both fail against the old constants. The TUI file's helper tests run unconditionally and the layout testimportorskipstextual, so it skips under CI'spip install -e ".[dev]"— it passes locally with thetuiextra.New
test_public_api.pyguards the export surface, andtest_conformance.pynow rejects an empty vector case list, which would otherwise make itsparametrizecollect zero tests and pass green (Swift already guards this).331 passed, 5 skipped;ruff checkandruff format --checkclean.Looked at, deliberately not changed
broadcast_targetsbuildsx.y.z.255, while Rust and Swift read the real netmask viaif_addrs/getifaddrsand compute the true subnet broadcast for every interface. On a /16 or /22 LAN a Profiler outside the client's /24 never sees the directed broadcast. A stdlib-only fix needsfcntl/SIOCGIFNETMASK, which is Unix-specific; the docstring already flags the compromise. Worth a follow-up.Cabinet.onis never populated. Page0x0C/2 falls through to the genericParamChanged, so the TUI's cab on/off dot can never light. Rust and Swift declare the same field and also never fill it, and there is no spec constant for the address — this is a shared spec gap, not a Python bug, so fixing it here alone would break tri-language parity.discover()sorts reply IPs as strings, sofind_first()returns the lexicographically lowest address rather than the first one seen. Swift does the same; only Rust sorts numerically. Changing Python alone would trade one divergence for another.NrpnHeader.parsedoes not verify the trailing0xF7and unconditionally drops the last byte. Matches Rust/Swift, and every path that consumes the result already length-checks.DeviceModel.connectnever consultsHandshakeOutcome.accepted. Tightening it would reject devices whose ack misses the 30 ms idle window, which seems worse than the status quo.🤖 Generated with Claude Code
https://claude.ai/code/session_016BcyRYArvAWDXdS8KZjVju