Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion py/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -619,16 +619,25 @@ generate_devtools_latest(
browser_versions = BROWSER_VERSIONS,
)

# Generate BiDi source files from CDDL specification
# Generate BiDi source files from CDDL specification.
# extra_cddl_files are merged with the primary WebDriver BiDi spec before
# generation so that end users get the full set of BiDi-adjacent modules.
generate_bidi(
name = "create-bidi-src",
cddl_file = "@webdriver_bidi_all_cddl//file:spec.cddl",
enhancements_manifest = "//py/private:bidi_enhancements_manifest.py",
extra_cddl_files = [
"@permissions_all_cddl//file:spec.cddl",
"@prefetch_all_cddl//file:spec.cddl",
"@ua_client_hints_all_cddl//file:spec.cddl",
"@web_bluetooth_all_cddl//file:spec.cddl",
],
extra_srcs = [
"//py/private:_event_manager.py",
"//py/private:cdp.py",
],
generator = ":generate_bidi",
merge_tool = "//py/private:merge_cddl",
module_name = "selenium/webdriver/common/bidi",
spec_version = "1.0",
)
Expand Down
120 changes: 0 additions & 120 deletions py/generate_bidi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,123 +1549,6 @@ def generate_console_file(output_path: Path) -> None:
logger.info(f"Generated: {console_path}")


def generate_permissions_file(output_path: Path) -> None:
"""Generate permissions.py file with permission-related classes."""
permissions_path = output_path / "permissions.py"

code = (
"# Licensed to the Software Freedom Conservancy (SFC) under one\n"
"# or more contributor license agreements. See the NOTICE file\n"
"# distributed with this work for additional information\n"
"# regarding copyright ownership. The SFC licenses this file\n"
"# to you under the Apache License, Version 2.0 (the\n"
'# "License"); you may not use this file except in compliance\n'
"# with the License. You may obtain a copy of the License at\n"
"#\n"
"# http://www.apache.org/licenses/LICENSE-2.0\n"
"#\n"
"# Unless required by applicable law or agreed to in writing,\n"
"# software distributed under the License is distributed on an\n"
'# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n'
"# KIND, either express or implied. See the License for the\n"
"# specific language governing permissions and limitations\n"
"# under the License.\n"
"\n"
'"""WebDriver BiDi Permissions module."""\n'
"\n"
"from __future__ import annotations\n"
"\n"
"from enum import Enum\n"
"from typing import Any\n"
"\n"
"from selenium.webdriver.common.bidi.common import command_builder\n"
"\n"
'_VALID_PERMISSION_STATES = {"granted", "denied", "prompt"}\n'
"\n"
"\n"
"class PermissionState(str, Enum):\n"
' """Permission state enumeration."""\n'
"\n"
' GRANTED = "granted"\n'
' DENIED = "denied"\n'
' PROMPT = "prompt"\n'
"\n"
"\n"
"class PermissionDescriptor:\n"
' """Descriptor for a permission."""\n'
"\n"
" def __init__(self, name: str) -> None:\n"
' """Initialize a PermissionDescriptor.\n'
"\n"
" Args:\n"
" name: The name of the permission (e.g., 'geolocation', 'microphone', 'camera')\n"
' """\n'
" self.name = name\n"
"\n"
" def __repr__(self) -> str:\n"
" return f\"PermissionDescriptor('{self.name}')\"\n"
"\n"
"\n"
"class Permissions:\n"
' """WebDriver BiDi Permissions module."""\n'
"\n"
" def __init__(self, websocket_connection: Any) -> None:\n"
' """Initialize the Permissions module.\n'
"\n"
" Args:\n"
" websocket_connection: The WebSocket connection for sending BiDi commands\n"
' """\n'
" self._conn = websocket_connection\n"
"\n"
" def set_permission(\n"
" self,\n"
" descriptor: PermissionDescriptor | str,\n"
" state: PermissionState | str,\n"
" origin: str | None = None,\n"
" user_context: str | None = None,\n"
" ) -> None:\n"
' """Set a permission for a given origin.\n'
"\n"
" Args:\n"
" descriptor: The permission descriptor or permission name as a string\n"
" state: The desired permission state\n"
" origin: The origin for which to set the permission\n"
" user_context: Optional user context ID to scope the permission\n"
"\n"
" Raises:\n"
" ValueError: If the state is not a valid permission state\n"
' """\n'
" state_value = state.value if isinstance(state, PermissionState) else state\n"
" if state_value not in _VALID_PERMISSION_STATES:\n"
" raise ValueError(\n"
' f"Invalid permission state: {state_value!r}. "\n'
' f"Must be one of {sorted(_VALID_PERMISSION_STATES)}"\n'
" )\n"
"\n"
" if isinstance(descriptor, str):\n"
' descriptor_dict = {"name": descriptor}\n'
" else:\n"
' descriptor_dict = {"name": descriptor.name}\n'
"\n"
" params: dict[str, Any] = {\n"
' "descriptor": descriptor_dict,\n'
' "state": state_value,\n'
" }\n"
" if origin is not None:\n"
' params["origin"] = origin\n'
" if user_context is not None:\n"
' params["userContext"] = user_context\n'
"\n"
' cmd = command_builder("permissions.setPermission", params)\n'
" self._conn.execute(cmd)\n"
)

with open(permissions_path, "w", encoding="utf-8") as f:
f.write(code)

logger.info(f"Generated: {permissions_path}")


def main(
cddl_file: str,
output_dir: str,
Expand Down Expand Up @@ -1733,9 +1616,6 @@ def main(
# Generate common.py
generate_common_file(output_path)

# Generate permissions.py
generate_permissions_file(output_path)

# Generate console.py
generate_console_file(output_path)

Expand Down
7 changes: 7 additions & 0 deletions py/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ exports_files([
"cdp.py",
])

py_binary(
name = "merge_cddl",
srcs = ["merge_cddl.py"],
legacy_create_init = False,
visibility = ["//py:__pkg__"],
)

py_binary(
name = "untar",
srcs = [
Expand Down
63 changes: 63 additions & 0 deletions py/private/bidi_enhancements_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,69 @@ def remove_file_dialog_handler(self, handler_id: int) -> None:
return self._event_manager.remove_event_handler("file_dialog_opened", handler_id)''',
],
},
"permissions": {
"extra_dataclasses": [
'''class PermissionDescriptor:
"""Descriptor identifying a permission by name.

Args:
name: The permission name (e.g. 'geolocation', 'microphone', 'camera').
"""

def __init__(self, name: str) -> None:
self.name = name

def __repr__(self) -> str:
return f"PermissionDescriptor(name={self.name!r})"''',
],
"extra_methods": [
''' def set_permission(
self,
descriptor: "PermissionDescriptor | str",
state: "PermissionState | str",
origin: str | None = None,
user_context: str | None = None,
*,
embedded_origin: str | None = None,
) -> None:
"""Set a browser permission.

Args:
descriptor: The permission descriptor or permission name as a string.
state: The desired permission state (granted, denied, or prompt).
origin: The origin to scope the permission to.
user_context: Optional user context ID to scope the permission.
embedded_origin: Keyword-only. Embedded origin for cross-origin
iframes; scopes the permission to that iframe's origin.

Raises:
ValueError: If *state* is not a valid permission state.
"""
state_value = state.value if isinstance(state, PermissionState) else state
valid_states = {"granted", "denied", "prompt"}
if state_value not in valid_states:
raise ValueError(
f"Invalid permission state: {state_value!r}. "
f"Must be one of {sorted(valid_states)}"
)

descriptor_dict = {"name": descriptor} if isinstance(descriptor, str) else {"name": descriptor.name}

params: dict = {
"descriptor": descriptor_dict,
"state": state_value,
}
if origin is not None:
params["origin"] = origin
if embedded_origin is not None:
params["embeddedOrigin"] = embedded_origin
if user_context is not None:
params["userContext"] = user_context
Comment thread
AutomatedTester marked this conversation as resolved.

cmd = command_builder("permissions.setPermission", params)
self._conn.execute(cmd)''',
],
},
}


Expand Down
41 changes: 38 additions & 3 deletions py/private/generate_bidi.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ def _generate_bidi_impl(ctx):
"""Implementation of the generate_bidi rule."""

cddl_file = ctx.file.cddl_file
extra_cddl_files = ctx.files.extra_cddl_files
manifest_file = ctx.file.enhancements_manifest
generator = ctx.executable.generator
output_dir = ctx.attr.module_name
Expand All @@ -12,6 +13,7 @@ def _generate_bidi_impl(ctx):
# The generator creates BiDi modules from the CDDL spec
# Using snake_case naming convention for Python files
module_names = [
"bluetooth",
"browser",
"browsing_context",
"common",
Expand All @@ -23,10 +25,30 @@ def _generate_bidi_impl(ctx):
"permissions",
"script",
"session",
"speculation",
"storage",
"user_agent_client_hints",
"webextension",
]

# Merge extra CDDL files into the primary spec before generation.
# Bazel requires inputs to be declared upfront, so we concatenate into a
# single file and pass that to the generator instead of the raw primary.
all_cddl = [cddl_file] + extra_cddl_files
if extra_cddl_files:
if not ctx.executable.merge_tool:
fail("merge_tool is required when extra_cddl_files is non-empty")
merged_cddl = ctx.actions.declare_file("merged_bidi.cddl")
ctx.actions.run(
inputs = all_cddl,
outputs = [merged_cddl],
executable = ctx.executable.merge_tool,
arguments = [merged_cddl.path] + [f.path for f in all_cddl],
)
input_cddl = merged_cddl
else:
input_cddl = cddl_file

# Declare all output files
module_files = [
ctx.actions.declare_file(output_dir + "/" + name + ".py")
Expand All @@ -51,13 +73,13 @@ def _generate_bidi_impl(ctx):

# Build the command to run the generator
args = [
cddl_file.path,
input_cddl.path,
output_base,
spec_version,
]

# Add enhancement manifest if provided
inputs = [cddl_file]
inputs = [input_cddl]
if manifest_file:
args.extend(["--enhancements-manifest", manifest_file.path])
inputs.append(manifest_file)
Expand All @@ -78,13 +100,19 @@ generate_bidi = rule(
"cddl_file": attr.label(
allow_single_file = [".cddl"],
mandatory = True,
doc = "CDDL specification file",
doc = "Primary CDDL specification file",
),
"enhancements_manifest": attr.label(
allow_single_file = [".py"],
mandatory = False,
doc = "Enhancement manifest Python file (optional)",
),
"extra_cddl_files": attr.label_list(
allow_files = [".cddl"],
mandatory = False,
default = [],
doc = "Additional CDDL files merged into the primary spec before generation",
),
"extra_srcs": attr.label_list(
allow_files = [".py"],
mandatory = False,
Expand All @@ -97,6 +125,13 @@ generate_bidi = rule(
mandatory = True,
doc = "Generator script (e.g., generate_bidi.py)",
),
"merge_tool": attr.label(
executable = True,
cfg = "exec",
mandatory = False,
default = None,
doc = "Tool that concatenates multiple CDDL files into one (e.g., merge_cddl.py). Required when extra_cddl_files is non-empty.",
),
Comment thread
AutomatedTester marked this conversation as resolved.
"module_name": attr.string(
mandatory = True,
doc = "Name of the module being generated (e.g., 'selenium/webdriver/common/bidi')",
Expand Down
46 changes: 46 additions & 0 deletions py/private/merge_cddl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Concatenate multiple CDDL files into a single output file.

Usage:
merge_cddl.py <output> <input1> [<input2> ...]
"""

import sys


def main() -> None:
if len(sys.argv) < 3:
usage = (__doc__ or "Usage:\n merge_cddl.py <output> <input1> [<input2> ...]\n").strip()
print(usage, file=sys.stderr)
raise SystemExit(1)

out_path = sys.argv[1]
input_paths = sys.argv[2:]
with open(out_path, "wb") as out_f:
for index, input_path in enumerate(input_paths):
if index > 0:
# Ensure files that lack a trailing newline don't accidentally
# join their last and first tokens across the boundary.
out_f.write(b"\n")
with open(input_path, "rb") as in_f:
out_f.write(in_f.read())


if __name__ == "__main__":
main()
Loading
Loading