[py] conform the internal BiDi layer to ADR 17786 and move four modules onto it - #18052
AutomatedTester wants to merge 1 commit into
Conversation
|
Sorry, yes, I created the generic tracking issue with #18020 but then didn't update it to reflect current state. The Python row is already completely covered by work done in #17761, #17942, #17966, #17952 #17786 scopes event subscription and routing to a separate orchestration layer, which should(?) be language-specific implementation details that aren't surfaced directly to the user. So, I don't think there's anything put into another ADR for that layer beyond the kind of things we're specifying for Script & Network. Actually, let me dig into what additional behavior we need to define for the other events we haven't discussed, yet to see if there's something more here. I brought it up in the last TLC meeting and you and I probably should have a chat about how we're going to transition from the .bidi implementation to the ._bidi implementation, and move things off of the driver object per #17670. I don't think we want to continue extending the current implementation, but I don't have a clear idea of what all needs to be done for the transition to work the way we need it to. |
…es onto it ADR 17786 fixes the behavior the low-level BiDi layer must exhibit at the wire boundary. The generated `_bidi` layer already met most of it for command results; this closes the gaps and starts moving the supported modules onto it. Events now reach their generated payload type. Every domain emitted EVENTS and EVENT_TYPES tables that nothing consumed, so decisions 1 and 7-10 held for command results and not for the other half of the inbound surface. Domain gains event()/on()/off() and an Event descriptor shaped for the connection's existing add_callback contract. Deserialization failures are logged as well as raised, because callbacks run on a daemon thread where a raise would otherwise be invisible. Outbound integers are narrowed the way inbound ones already are: a whole float is a valid integer either way, but it now reaches the wire as an integer rather than as 5.0. The rule that an extras key may not shadow a declared field is enforced at construction rather than only at serialization, since it is an invariant of the representation. as_json keeps checking, because a frozen record can still be mutated through object.__setattr__. A new conformance sweep walks every generated type from the registry and checks the contract mechanically -- wire metadata, extensibility, union dispatch, required fields, undeclared fields, round-trip stability. It is what makes generator drift loud rather than silent. permissions, webExtension, browsingContext and browser.setDownloadBehavior now build their wire frames with the generated types instead of by hand. Public signatures, documented exceptions and return shapes are unchanged, except that permissions.set_permission now rejects a missing origin locally rather than letting the remote end reject it. Two of these had been hand-rolling what the layer models directly: setViewport's `...` sentinel is the omitted-vs-null distinction, and setDownloadBehavior carried a field opted out of the facade's blanket None-stripping. generate_bidi.py gains an extra_imports manifest hook, and only imports command_builder when something still uses it. Refs #18020
ec35e41 to
3dbb0ec
Compare
User description
Implements ADR 17786 — the behavioral contract for the low-level WebDriver BiDi layer — for Python, and starts moving the supported modules onto the layer that satisfies it. Tracking issue: #18020.
The generated
_bidilayer already met most of the contract for command results (#17761, #17942, #17966, #17952). This closes the remaining gaps and puts four supported modules on it.Contract gaps closed
Events never reached their typed payload. Every generated domain emitted
EVENTSandEVENT_TYPEStables that nothing consumed —Transport.executeonly deserialized command results, so decisions 1 and 7–10 held for half the inbound surface and not the other half.Domaingainsevent()/on()/off()and anEventdescriptor shaped for the connection's existingadd_callbackcontract. A deserialization failure is logged as well as raised, because callbacks run on a daemon thread where a raise would otherwise be invisible.on()deliberately does not subscribe — that's orchestration, which the ADR puts out of scope. Callers still usesession.subscribe; this governs only how what arrives is typed.Outbound integers weren't narrowed.
_read_scalarnormalized a whole float inbound;as_jsonsent5.0. A whole float is a valid integer either way (decision 4), but it now reaches the wire as one.The extras-shadowing rule fired late. Decision 1 says a declared key must never appear in the extras map — an invariant of the representation, so it's now checked at construction.
as_jsonkeeps checking, because a frozen record can still be mutated throughobject.__setattr__.Nothing checked the contract across the whole schema. A new sweep walks every generated type from the registry and asserts it mechanically: wire metadata,
_EXTENSIBLEconsistency, union dispatch, required fields, undeclared fields, round-trip stability. 2200 assertions over 300+ types. I mutation-tested it — deleting the undeclared-field warning turns 253 of them red.Supported modules moved onto the layer
permissions,webExtension,browsingContextandbrowser.setDownloadBehaviornow build their wire frames with the generated types instead of by hand. Public signatures, documented exceptions and return shapes are unchanged, with one exception noted below.Two of these had been hand-rolling what the layer models directly:
browsingContext.setViewportused an...sentinel so an explicitNoneserialized but an omitted argument didn't — that is exactly the omitted-vs-null distinction the contract's baseline provides.browser.setDownloadBehaviorcarried the comment "downloadBehavior is a REQUIRED field in the BiDi spec (can be null but must be present). Do NOT use a generic None-filter on it" — a field opted out of the facade's blanketNone-stripping by hand.generate_bidi.pygains anextra_importsmanifest hook, and now only importscommand_builderwhen something still uses it.Behavior change
permissions.set_permissionrejects a missingoriginlocally instead of sending an incomplete frame for the remote end to reject.originis required by the spec; the signature had it optional. Same outcome, without the round trip, per decision 5.Not in this PR
browser.setClientWindowState— itsstateargument folds the spec's named-state and rect-state variants together and adds a"normal"the spec's enum doesn't have. It also appears to send the rect state asstaterather than as siblingwidth/height/x/y, which looks wrong against the schema; I'd rather check that on its own than move a possible bug.storage(3 methods) andemulation(7) — same mechanical pattern, not yet done.sessionandinputhave no hand-written frames at all.CddlCommand.to_python_methodand applyextract_field/deserializerules to a raw result dict — the inbound path where users are most exposed. Delegating them generically is blocked on the two generators being independent projections (generate_bidi.pyreads CDDL;generate_bidi_protocol.pyreads the JS-projected schema), so there's no shared identity to map a facade type onto its_bidicounterpart. Closing that likely means pointing the CDDL generator at the same projected schema, which wants its own decision.Testing
//py:unit//py:test-chrome-bidi//py:mypy./scripts/format.sh --pre-pushNew unit coverage: 15 event-dispatch tests, 2200 conformance assertions, and per-module tests for permissions, webExtension, browsingContext and browser.
protocol_tests.pygains two real-browser event round-trips — a Chrome-pushedlog.entryAddedarriving asConsoleLogEntrywith its enum restored, itsjs-uinttimestamp intact past 2^31, and its nestedscript.RemoteValueargs dispatched to their variants.One flaky test surfaced during verification and is not related to this change:
test_activate_browsing_context(a window-focus test) failed then passed on two consecutive identical runs, in a file this PR doesn't touch. Worth a separate look.Cross-binding
The event gap is not Python-specific — Ruby emits
EVENTS/EVENT_TYPESinrb/lib/selenium/webdriver/bidi/protocol/*.rbwith no typed-event consumer either. Worth agreeing one event seam across bindings rather than inventing two. The conformance sweep is a pattern the other bindings could copy, since the schema is shared.AI assistance disclosure
Per CONTRIBUTING.md: substantial parts of this PR were written with Claude Code (Claude Opus). It produced the
Eventdispatch seam, the conformance sweep, the manifest migrations and the tests, working from the ADR and the existing layer. I've reviewed the result and own it. Opened as a draft for that reason — happy to split it (the_bidicontract work and the module migrations are separable) if that reads better for review.Co-Authored-By: Copse noreply@copse.dev
Copse-Models: acp:claude-agent-acp#opus[1m]