2020-05-18 11:32:52 +01:00
|
|
|
# The MIT License(MIT)
|
|
|
|
|
#
|
|
|
|
|
# Copyright(c) 2018 Hyperion Gray
|
|
|
|
|
#
|
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
# of this software and associated documentation files(the "Software"), to deal
|
|
|
|
|
# in the Software without restriction, including without limitation the rights
|
|
|
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
|
|
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
|
#
|
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
|
#
|
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
|
# THE SOFTWARE.
|
|
|
|
|
|
|
|
|
|
# This is a copy of https://github.com/HyperionGray/python-chrome-devtools-protocol/blob/master/generator/generate.py
|
|
|
|
|
# The license above is theirs and MUST be preserved.
|
|
|
|
|
|
|
|
|
|
import builtins
|
|
|
|
|
import itertools
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import operator
|
|
|
|
|
import os
|
|
|
|
|
import re
|
2025-11-16 18:50:49 -05:00
|
|
|
from collections.abc import Iterator
|
2025-11-30 19:15:56 -05:00
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from enum import Enum
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from textwrap import dedent
|
|
|
|
|
from textwrap import indent as tw_indent
|
2025-12-29 10:49:17 -05:00
|
|
|
from typing import cast
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
import inflection # type: ignore
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
log_level = getattr(logging, os.environ.get("LOG_LEVEL", "warning").upper())
|
2020-05-18 11:32:52 +01:00
|
|
|
logging.basicConfig(level=log_level)
|
2025-11-18 13:59:15 +05:30
|
|
|
logger = logging.getLogger("generate")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
SHARED_HEADER = """# DO NOT EDIT THIS FILE!
|
2020-05-18 11:32:52 +01:00
|
|
|
#
|
|
|
|
|
# This file is generated from the CDP specification. If you need to make
|
2025-11-18 13:59:15 +05:30
|
|
|
# changes, edit the generator and regenerate all of the modules."""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-12-29 10:49:17 -05:00
|
|
|
INIT_HEADER = f"""{SHARED_HEADER}
|
|
|
|
|
"""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-12-29 10:49:17 -05:00
|
|
|
MODULE_HEADER = f"""{SHARED_HEADER}
|
2020-05-18 11:32:52 +01:00
|
|
|
#
|
|
|
|
|
# CDP domain: {{}}{{}}
|
|
|
|
|
from __future__ import annotations
|
2020-09-24 15:36:47 +01:00
|
|
|
from .util import event_class, T_JSON_DICT
|
2020-05-18 11:32:52 +01:00
|
|
|
from dataclasses import dataclass
|
|
|
|
|
import enum
|
|
|
|
|
import typing
|
2025-12-29 10:49:17 -05:00
|
|
|
"""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
current_version = ""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2020-09-24 15:36:47 +01:00
|
|
|
UTIL_PY = """
|
|
|
|
|
import typing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
T_JSON_DICT = typing.Dict[str, typing.Any]
|
|
|
|
|
_event_parsers = dict()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def event_class(method):
|
|
|
|
|
''' A decorator that registers a class as an event class. '''
|
|
|
|
|
def decorate(cls):
|
|
|
|
|
_event_parsers[method] = cls
|
2024-06-06 21:00:42 -07:00
|
|
|
cls.event_class = method
|
2020-09-24 15:36:47 +01:00
|
|
|
return cls
|
|
|
|
|
return decorate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_json_event(json: T_JSON_DICT) -> typing.Any:
|
|
|
|
|
''' Parse a JSON dictionary into a CDP event. '''
|
|
|
|
|
return _event_parsers[json['method']].from_json(json['params'])
|
|
|
|
|
"""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
def indent(s, n):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""A shortcut for `textwrap.indent` that always uses spaces."""
|
2025-11-18 13:59:15 +05:30
|
|
|
return tw_indent(s, n * " ")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
BACKTICK_RE = re.compile(r"`([^`]+)`(\w+)?")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def escape_backticks(docstr):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Escape backticks in a docstring by doubling them up.
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
This is a little tricky because RST requires a non-letter character after
|
|
|
|
|
the closing backticks, but some CDPs docs have things like "`AxNodeId`s".
|
|
|
|
|
If we double the backticks in that string, then it won't be valid RST. The
|
|
|
|
|
fix is to insert an apostrophe if an "s" trails the backticks.
|
2025-11-18 13:59:15 +05:30
|
|
|
"""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
def replace_one(match):
|
2025-11-18 13:59:15 +05:30
|
|
|
if match.group(2) == "s":
|
2020-05-18 11:32:52 +01:00
|
|
|
return f"``{match.group(1)}``'s"
|
2023-01-25 00:39:48 +13:00
|
|
|
if match.group(2):
|
2020-05-18 11:32:52 +01:00
|
|
|
# This case (some trailer other than "s") doesn't currently exist
|
|
|
|
|
# in the CDP definitions, but it's here just to be safe.
|
2025-11-18 13:59:15 +05:30
|
|
|
return f"``{match.group(1)}`` {match.group(2)}"
|
|
|
|
|
return f"``{match.group(1)}``"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
# Sometimes pipes are used where backticks should have been used.
|
2025-11-18 13:59:15 +05:30
|
|
|
docstr = docstr.replace("|", "`")
|
2020-05-18 11:32:52 +01:00
|
|
|
return BACKTICK_RE.sub(replace_one, docstr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def inline_doc(description):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate an inline doc, e.g. `#: This type is a ...`."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if not description:
|
2025-11-18 13:59:15 +05:30
|
|
|
return ""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
description = escape_backticks(description)
|
2025-11-30 19:15:56 -05:00
|
|
|
lines = [f"#: {line}" for line in description.split("\n")]
|
2025-11-18 13:59:15 +05:30
|
|
|
return "\n".join(lines)
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def docstring(description):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate a docstring from a description."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if not description:
|
2025-11-18 13:59:15 +05:30
|
|
|
return ""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
description = escape_backticks(description)
|
|
|
|
|
return dedent("'''\n{}\n'''").format(description)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_builtin(name):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Return True if `name` would shadow a builtin."""
|
2020-05-18 11:32:52 +01:00
|
|
|
try:
|
|
|
|
|
getattr(builtins, name)
|
|
|
|
|
return True
|
|
|
|
|
except AttributeError:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def snake_case(name):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Convert a camel case name to snake case.
|
|
|
|
|
|
|
|
|
|
If the name would shadow a Python builtin, then append an underscore.
|
|
|
|
|
"""
|
2020-05-18 11:32:52 +01:00
|
|
|
name = inflection.underscore(name)
|
|
|
|
|
if is_builtin(name):
|
2025-11-18 13:59:15 +05:30
|
|
|
name += "_"
|
2020-05-18 11:32:52 +01:00
|
|
|
return name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ref_to_python(ref):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Convert a CDP `$ref` to the name of a Python type.
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
For a dotted ref, the part before the dot is snake cased.
|
2025-11-18 13:59:15 +05:30
|
|
|
"""
|
|
|
|
|
if "." in ref:
|
|
|
|
|
domain, subtype = ref.split(".")
|
|
|
|
|
ref = f"{snake_case(domain)}.{subtype}"
|
2020-05-18 11:32:52 +01:00
|
|
|
return f"{ref}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CdpPrimitiveType(Enum):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""All of the CDP types that map directly to a Python type."""
|
|
|
|
|
|
|
|
|
|
boolean = "bool"
|
|
|
|
|
integer = "int"
|
|
|
|
|
number = "float"
|
|
|
|
|
object = "dict"
|
|
|
|
|
string = "str"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_annotation(cls, cdp_type):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Return a type annotation for the CDP type."""
|
|
|
|
|
if cdp_type == "any":
|
|
|
|
|
return "typing.Any"
|
2023-01-25 00:39:48 +13:00
|
|
|
return cls[cdp_type].value
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_constructor(cls, cdp_type, val):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Return the code to construct a value for a given CDP type."""
|
|
|
|
|
if cdp_type == "any":
|
2020-05-18 11:32:52 +01:00
|
|
|
return val
|
2023-01-25 00:39:48 +13:00
|
|
|
cons = cls[cdp_type].value
|
2025-11-18 13:59:15 +05:30
|
|
|
return f"{cons}({val})"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CdpItems:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Represents the type of a repeated item."""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
type: str
|
|
|
|
|
ref: str
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, type):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate code to instantiate an item from a JSON object."""
|
|
|
|
|
return cls(type.get("type"), type.get("$ref"))
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CdpProperty:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""A property belonging to a non-primitive CDP type."""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
name: str
|
2025-11-16 18:50:49 -05:00
|
|
|
description: str | None
|
|
|
|
|
type: str | None
|
|
|
|
|
ref: str | None
|
|
|
|
|
enum: list[str]
|
|
|
|
|
items: CdpItems | None
|
2020-05-18 11:32:52 +01:00
|
|
|
optional: bool
|
|
|
|
|
experimental: bool
|
|
|
|
|
deprecated: bool
|
|
|
|
|
|
|
|
|
|
@property
|
2025-11-18 13:59:15 +05:30
|
|
|
def py_name(self) -> str:
|
|
|
|
|
"""Get this property's Python name."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return snake_case(self.name)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def py_annotation(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""This property's Python type annotation."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.items:
|
|
|
|
|
if self.items.ref:
|
|
|
|
|
py_ref = ref_to_python(self.items.ref)
|
2022-06-13 19:05:38 +01:00
|
|
|
ann = f"typing.List[{py_ref}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-26 09:08:15 -05:00
|
|
|
ann = f"typing.List[{CdpPrimitiveType.get_annotation(self.items.type)}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
|
|
|
|
if self.ref:
|
|
|
|
|
py_ref = ref_to_python(self.ref)
|
|
|
|
|
ann = py_ref
|
|
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
ann = CdpPrimitiveType.get_annotation(cast(str, self.type))
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
ann = f"typing.Optional[{ann}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
return ann
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, property):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Instantiate a CDP property from a JSON object."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return cls(
|
2025-11-18 13:59:15 +05:30
|
|
|
property["name"],
|
|
|
|
|
property.get("description"),
|
|
|
|
|
property.get("type"),
|
|
|
|
|
property.get("$ref"),
|
|
|
|
|
property.get("enum"),
|
|
|
|
|
CdpItems.from_json(property["items"]) if "items" in property else None,
|
|
|
|
|
property.get("optional", False),
|
|
|
|
|
property.get("experimental", False),
|
|
|
|
|
property.get("deprecated", False),
|
2020-05-18 11:32:52 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def generate_decl(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate the code that declares this property."""
|
2020-05-18 11:32:52 +01:00
|
|
|
code = inline_doc(self.description)
|
|
|
|
|
if code:
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n"
|
|
|
|
|
code += f"{self.py_name}: {self.py_annotation}"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
code += " = None"
|
2020-05-18 11:32:52 +01:00
|
|
|
return code
|
|
|
|
|
|
2020-09-22 11:40:14 +01:00
|
|
|
def generate_to_json(self, dict_, use_self=True):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate the code that exports this property to the specified JSON dict."""
|
2025-11-18 13:59:15 +05:30
|
|
|
self_ref = "self." if use_self else ""
|
2020-05-18 11:32:52 +01:00
|
|
|
assign = f"{dict_}['{self.name}'] = "
|
|
|
|
|
if self.items:
|
|
|
|
|
if self.items.ref:
|
|
|
|
|
assign += f"[i.to_json() for i in {self_ref}{self.py_name}]"
|
|
|
|
|
else:
|
|
|
|
|
assign += f"[i for i in {self_ref}{self.py_name}]"
|
|
|
|
|
else:
|
|
|
|
|
if self.ref:
|
|
|
|
|
assign += f"{self_ref}{self.py_name}.to_json()"
|
|
|
|
|
else:
|
|
|
|
|
assign += f"{self_ref}{self.py_name}"
|
|
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
code = dedent(f"""\
|
2020-05-18 11:32:52 +01:00
|
|
|
if {self_ref}{self.py_name} is not None:
|
2025-11-18 13:59:15 +05:30
|
|
|
{assign}""")
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
|
|
|
|
code = assign
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def generate_from_json(self, dict_):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate the code that creates an instance from a JSON dict named `dict_`."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.items:
|
|
|
|
|
if self.items.ref:
|
|
|
|
|
py_ref = ref_to_python(self.items.ref)
|
|
|
|
|
expr = f"[{py_ref}.from_json(i) for i in {dict_}['{self.name}']]"
|
|
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
cons = CdpPrimitiveType.get_constructor(self.items.type, "i")
|
2020-05-18 11:32:52 +01:00
|
|
|
expr = f"[{cons} for i in {dict_}['{self.name}']]"
|
|
|
|
|
else:
|
|
|
|
|
if self.ref:
|
|
|
|
|
py_ref = ref_to_python(self.ref)
|
|
|
|
|
expr = f"{py_ref}.from_json({dict_}['{self.name}'])"
|
|
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
expr = CdpPrimitiveType.get_constructor(self.type, f"{dict_}['{self.name}']")
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.optional:
|
|
|
|
|
expr = f"{expr} if '{self.name}' in {dict_} else None"
|
|
|
|
|
return expr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CdpType:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""A top-level CDP type."""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
id: str
|
2025-11-16 18:50:49 -05:00
|
|
|
description: str | None
|
2020-05-18 11:32:52 +01:00
|
|
|
type: str
|
2025-11-16 18:50:49 -05:00
|
|
|
items: CdpItems | None
|
|
|
|
|
enum: list[str]
|
|
|
|
|
properties: list[CdpProperty]
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, type_):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Instantiate a CDP type from a JSON object."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return cls(
|
2025-11-18 13:59:15 +05:30
|
|
|
type_["id"],
|
|
|
|
|
type_.get("description"),
|
|
|
|
|
type_["type"],
|
|
|
|
|
CdpItems.from_json(type_["items"]) if "items" in type_ else None,
|
|
|
|
|
type_.get("enum"),
|
|
|
|
|
[CdpProperty.from_json(p) for p in type_.get("properties", [])],
|
2020-05-18 11:32:52 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def generate_code(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate Python code for this type."""
|
|
|
|
|
logger.debug("Generating type %s: %s", self.id, self.type)
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.enum:
|
|
|
|
|
return self.generate_enum_code()
|
2023-01-25 00:39:48 +13:00
|
|
|
if self.properties:
|
2020-05-18 11:32:52 +01:00
|
|
|
return self.generate_class_code()
|
2023-01-25 00:39:48 +13:00
|
|
|
return self.generate_primitive_code()
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
def generate_primitive_code(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate code for a primitive type."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.items:
|
|
|
|
|
if self.items.ref:
|
|
|
|
|
nested_type = ref_to_python(self.items.ref)
|
|
|
|
|
else:
|
|
|
|
|
nested_type = CdpPrimitiveType.get_annotation(self.items.type)
|
2025-11-18 13:59:15 +05:30
|
|
|
py_type = f"typing.List[{nested_type}]"
|
|
|
|
|
superclass = "list"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
|
|
|
|
# A primitive type cannot have a ref, so there is no branch here.
|
|
|
|
|
py_type = CdpPrimitiveType.get_annotation(self.type)
|
|
|
|
|
superclass = py_type
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
code = f"class {self.id}({superclass}):\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
doc = docstring(self.description)
|
|
|
|
|
if doc:
|
2025-11-18 13:59:15 +05:30
|
|
|
code += indent(doc, 4) + "\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
def_to_json = dedent(f"""\
|
2020-05-18 11:32:52 +01:00
|
|
|
def to_json(self) -> {py_type}:
|
2025-11-18 13:59:15 +05:30
|
|
|
return self""")
|
2020-05-18 11:32:52 +01:00
|
|
|
code += indent(def_to_json, 4)
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
def_from_json = dedent(f"""\
|
2020-05-18 11:32:52 +01:00
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, json: {py_type}) -> {self.id}:
|
2025-11-18 13:59:15 +05:30
|
|
|
return cls(json)""")
|
|
|
|
|
code += "\n\n" + indent(def_from_json, 4)
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
def_repr = dedent(f"""\
|
2020-05-18 11:32:52 +01:00
|
|
|
def __repr__(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
return '{self.id}({{}})'.format(super().__repr__())""")
|
|
|
|
|
code += "\n\n" + indent(def_repr, 4)
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def generate_enum_code(self):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate an "enum" type.
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
Enums are handled by making a python class that contains only class
|
|
|
|
|
members. Each class member is upper snaked case, e.g.
|
2025-11-30 19:15:56 -05:00
|
|
|
`MyTypeClass.MY_ENUM_VALUE` and is assigned a string value from the
|
2020-05-18 11:32:52 +01:00
|
|
|
CDP metadata.
|
2025-11-18 13:59:15 +05:30
|
|
|
"""
|
|
|
|
|
def_to_json = dedent("""\
|
2020-05-18 11:32:52 +01:00
|
|
|
def to_json(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
return self.value""")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
def_from_json = dedent("""\
|
2020-05-18 11:32:52 +01:00
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, json):
|
2025-11-18 13:59:15 +05:30
|
|
|
return cls(json)""")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
code = f"class {self.id}(enum.Enum):\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
doc = docstring(self.description)
|
|
|
|
|
if doc:
|
2025-11-18 13:59:15 +05:30
|
|
|
code += indent(doc, 4) + "\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
for enum_member in self.enum:
|
|
|
|
|
snake_name = snake_case(enum_member).upper()
|
|
|
|
|
enum_code = f'{snake_name} = "{enum_member}"\n'
|
|
|
|
|
code += indent(enum_code, 4)
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n" + indent(def_to_json, 4)
|
|
|
|
|
code += "\n\n" + indent(def_from_json, 4)
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def generate_class_code(self):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate a class type.
|
|
|
|
|
|
|
|
|
|
Top-level types that are defined as a CDP `object` are turned into Python
|
2020-05-18 11:32:52 +01:00
|
|
|
dataclasses.
|
2025-11-18 13:59:15 +05:30
|
|
|
"""
|
|
|
|
|
code = dedent(f"""\
|
2020-05-18 11:32:52 +01:00
|
|
|
@dataclass
|
2025-11-18 13:59:15 +05:30
|
|
|
class {self.id}:\n""")
|
2020-05-18 11:32:52 +01:00
|
|
|
doc = docstring(self.description)
|
|
|
|
|
if doc:
|
2025-11-18 13:59:15 +05:30
|
|
|
code += indent(doc, 4) + "\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
# Emit property declarations. These are sorted so that optional
|
|
|
|
|
# properties come after required properties, which is required to make
|
|
|
|
|
# the dataclass constructor work.
|
|
|
|
|
props = list(self.properties)
|
2025-11-18 13:59:15 +05:30
|
|
|
props.sort(key=operator.attrgetter("optional"))
|
|
|
|
|
code += "\n\n".join(indent(p.generate_decl(), 4) for p in props)
|
|
|
|
|
code += "\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
# Emit to_json() method. The properties are sorted in the same order as
|
|
|
|
|
# above for readability.
|
2025-11-18 13:59:15 +05:30
|
|
|
def_to_json = dedent("""\
|
2020-05-18 11:32:52 +01:00
|
|
|
def to_json(self):
|
|
|
|
|
json = dict()
|
2025-11-18 13:59:15 +05:30
|
|
|
""")
|
|
|
|
|
assigns = (p.generate_to_json(dict_="json") for p in props)
|
|
|
|
|
def_to_json += indent("\n".join(assigns), 4)
|
|
|
|
|
def_to_json += "\n"
|
|
|
|
|
def_to_json += indent("return json", 4)
|
|
|
|
|
code += indent(def_to_json, 4) + "\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
# Emit from_json() method. The properties are sorted in the same order
|
|
|
|
|
# as above for readability.
|
2025-11-18 13:59:15 +05:30
|
|
|
def_from_json = dedent("""\
|
2020-05-18 11:32:52 +01:00
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, json):
|
|
|
|
|
return cls(
|
2025-11-18 13:59:15 +05:30
|
|
|
""")
|
2023-01-25 00:39:48 +13:00
|
|
|
from_jsons = []
|
2020-05-18 11:32:52 +01:00
|
|
|
for p in props:
|
2025-11-18 13:59:15 +05:30
|
|
|
from_json = p.generate_from_json(dict_="json")
|
|
|
|
|
from_jsons.append(f"{p.py_name}={from_json},")
|
|
|
|
|
def_from_json += indent("\n".join(from_jsons), 8)
|
|
|
|
|
def_from_json += "\n"
|
|
|
|
|
def_from_json += indent(")", 4)
|
2020-05-18 11:32:52 +01:00
|
|
|
code += indent(def_from_json, 4)
|
|
|
|
|
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def get_refs(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Return all refs for this type."""
|
2020-05-18 11:32:52 +01:00
|
|
|
refs = set()
|
|
|
|
|
if self.enum:
|
|
|
|
|
# Enum types don't have refs.
|
|
|
|
|
pass
|
|
|
|
|
elif self.properties:
|
|
|
|
|
# Enumerate refs for a class type.
|
|
|
|
|
for prop in self.properties:
|
|
|
|
|
if prop.items and prop.items.ref:
|
|
|
|
|
refs.add(prop.items.ref)
|
|
|
|
|
elif prop.ref:
|
|
|
|
|
refs.add(prop.ref)
|
|
|
|
|
else:
|
|
|
|
|
# A primitive type can't have a direct ref, but it can have an items
|
|
|
|
|
# which contains a ref.
|
|
|
|
|
if self.items and self.items.ref:
|
|
|
|
|
refs.add(self.items.ref)
|
|
|
|
|
return refs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CdpParameter(CdpProperty):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""A parameter to a CDP command."""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
def generate_code(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate the code for a parameter in a function call."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.items:
|
|
|
|
|
if self.items.ref:
|
|
|
|
|
nested_type = ref_to_python(self.items.ref)
|
|
|
|
|
py_type = f"typing.List[{nested_type}]"
|
|
|
|
|
else:
|
|
|
|
|
nested_type = CdpPrimitiveType.get_annotation(self.items.type)
|
2025-11-18 13:59:15 +05:30
|
|
|
py_type = f"typing.List[{nested_type}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
|
|
|
|
if self.ref:
|
2022-06-13 19:05:38 +01:00
|
|
|
py_type = f"{ref_to_python(self.ref)}"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
py_type = CdpPrimitiveType.get_annotation(cast(str, self.type))
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
py_type = f"typing.Optional[{py_type}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
code = f"{self.py_name}: {py_type}"
|
|
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
code += " = None"
|
2020-05-18 11:32:52 +01:00
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def generate_decl(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate the declaration for this parameter."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.description:
|
|
|
|
|
code = inline_doc(self.description)
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
code = ""
|
|
|
|
|
code += f"{self.py_name}: {self.py_annotation}"
|
2020-05-18 11:32:52 +01:00
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def generate_doc(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate the docstring for this parameter."""
|
|
|
|
|
doc = f":param {self.py_name}:"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
if self.experimental:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += " **(EXPERIMENTAL)**"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += " *(Optional)*"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
if self.description:
|
2025-11-18 13:59:15 +05:30
|
|
|
desc = self.description.replace("`", "``").replace("\n", " ")
|
|
|
|
|
doc += f" {desc}"
|
2020-05-18 11:32:52 +01:00
|
|
|
return doc
|
|
|
|
|
|
|
|
|
|
def generate_from_json(self, dict_):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate the code to instantiate this parameter from a JSON dict."""
|
2020-05-18 11:32:52 +01:00
|
|
|
code = super().generate_from_json(dict_)
|
2025-11-18 13:59:15 +05:30
|
|
|
return f"{self.py_name}={code}"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class CdpReturn(CdpProperty):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""A return value from a CDP command."""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
@property
|
|
|
|
|
def py_annotation(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Return the Python type annotation for this return."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.items:
|
|
|
|
|
if self.items.ref:
|
|
|
|
|
py_ref = ref_to_python(self.items.ref)
|
|
|
|
|
ann = f"typing.List[{py_ref}]"
|
|
|
|
|
else:
|
|
|
|
|
py_type = CdpPrimitiveType.get_annotation(self.items.type)
|
2025-11-18 13:59:15 +05:30
|
|
|
ann = f"typing.List[{py_type}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
|
|
|
|
if self.ref:
|
|
|
|
|
py_ref = ref_to_python(self.ref)
|
|
|
|
|
ann = f"{py_ref}"
|
|
|
|
|
else:
|
|
|
|
|
ann = CdpPrimitiveType.get_annotation(self.type)
|
|
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
ann = f"typing.Optional[{ann}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
return ann
|
|
|
|
|
|
|
|
|
|
def generate_doc(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate the docstring for this return."""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.description:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc = self.description.replace("\n", " ")
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.optional:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc = f"*(Optional)* {doc}"
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc = ""
|
2020-05-18 11:32:52 +01:00
|
|
|
return doc
|
|
|
|
|
|
|
|
|
|
def generate_return(self, dict_):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate code for returning this value."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return super().generate_from_json(dict_)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CdpCommand:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""A CDP command."""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
name: str
|
|
|
|
|
description: str
|
|
|
|
|
experimental: bool
|
|
|
|
|
deprecated: bool
|
2025-11-16 18:50:49 -05:00
|
|
|
parameters: list[CdpParameter]
|
|
|
|
|
returns: list[CdpReturn]
|
2020-05-18 11:32:52 +01:00
|
|
|
domain: str
|
|
|
|
|
|
|
|
|
|
@property
|
2025-11-04 21:09:01 +07:00
|
|
|
def py_name(self) -> str:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Get a Python name for this command."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return snake_case(self.name)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-11-18 13:59:15 +05:30
|
|
|
def from_json(cls, command, domain) -> "CdpCommand":
|
|
|
|
|
"""Instantiate a CDP command from a JSON object."""
|
|
|
|
|
parameters = command.get("parameters", [])
|
|
|
|
|
returns = command.get("returns", [])
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
return cls(
|
2025-11-18 13:59:15 +05:30
|
|
|
command["name"],
|
|
|
|
|
command.get("description"),
|
|
|
|
|
command.get("experimental", False),
|
|
|
|
|
command.get("deprecated", False),
|
2024-12-04 12:51:00 +07:00
|
|
|
[cast(CdpParameter, CdpParameter.from_json(p)) for p in parameters],
|
|
|
|
|
[cast(CdpReturn, CdpReturn.from_json(r)) for r in returns],
|
2020-05-18 11:32:52 +01:00
|
|
|
domain,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def generate_code(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate code for a CDP command."""
|
2020-05-18 11:32:52 +01:00
|
|
|
global current_version
|
|
|
|
|
# Generate the function header
|
|
|
|
|
if len(self.returns) == 0:
|
2025-11-18 13:59:15 +05:30
|
|
|
ret_type = "None"
|
2020-05-18 11:32:52 +01:00
|
|
|
elif len(self.returns) == 1:
|
|
|
|
|
ret_type = self.returns[0].py_annotation
|
|
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
nested_types = ", ".join(r.py_annotation for r in self.returns)
|
|
|
|
|
ret_type = f"typing.Tuple[{nested_types}]"
|
2020-05-18 11:32:52 +01:00
|
|
|
ret_type = f"typing.Generator[T_JSON_DICT,T_JSON_DICT,{ret_type}]"
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
code = ""
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
code += f"def {self.py_name}("
|
|
|
|
|
ret = f") -> {ret_type}:\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.parameters:
|
2021-10-05 11:00:38 +01:00
|
|
|
params = [p.generate_code() for p in self.parameters]
|
|
|
|
|
optional = False
|
|
|
|
|
clean_params = []
|
|
|
|
|
for para in params:
|
|
|
|
|
if "= None" in para:
|
|
|
|
|
optional = True
|
|
|
|
|
if optional and "= None" not in para:
|
2025-11-18 13:59:15 +05:30
|
|
|
para += " = None"
|
2021-10-05 11:00:38 +01:00
|
|
|
clean_params.append(para)
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n"
|
|
|
|
|
code += indent(",\n".join(clean_params), 8)
|
|
|
|
|
code += "\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
code += indent(ret, 4)
|
|
|
|
|
else:
|
|
|
|
|
code += ret
|
|
|
|
|
|
|
|
|
|
# Generate the docstring
|
2025-11-18 13:59:15 +05:30
|
|
|
doc = ""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.description:
|
|
|
|
|
doc = self.description
|
|
|
|
|
if self.experimental:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += "\n\n**EXPERIMENTAL**"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.parameters and doc:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += "\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
elif not self.parameters and self.returns:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += "\n"
|
|
|
|
|
doc += "\n".join(p.generate_doc() for p in self.parameters)
|
2020-05-18 11:32:52 +01:00
|
|
|
if len(self.returns) == 1:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += "\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
ret_doc = self.returns[0].generate_doc()
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += f":returns: {ret_doc}"
|
2020-05-18 11:32:52 +01:00
|
|
|
elif len(self.returns) > 1:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc += "\n"
|
|
|
|
|
doc += ":returns: A tuple with the following items:\n\n"
|
|
|
|
|
ret_docs = "\n".join(f"{i}. **{r.name}** - {r.generate_doc()}" for i, r in enumerate(self.returns))
|
2020-05-18 11:32:52 +01:00
|
|
|
doc += indent(ret_docs, 4)
|
|
|
|
|
if doc:
|
|
|
|
|
code += indent(docstring(doc), 4)
|
|
|
|
|
|
|
|
|
|
# Generate the function body
|
|
|
|
|
if self.parameters:
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n"
|
|
|
|
|
code += indent("params: T_JSON_DICT = dict()", 4)
|
|
|
|
|
code += "\n"
|
|
|
|
|
assigns = (p.generate_to_json(dict_="params", use_self=False) for p in self.parameters)
|
|
|
|
|
code += indent("\n".join(assigns), 4)
|
|
|
|
|
code += "\n"
|
|
|
|
|
code += indent("cmd_dict: T_JSON_DICT = {\n", 4)
|
2020-05-18 11:32:52 +01:00
|
|
|
code += indent(f"'method': '{self.domain}.{self.name}',\n", 8)
|
|
|
|
|
if self.parameters:
|
|
|
|
|
code += indent("'params': params,\n", 8)
|
2025-11-18 13:59:15 +05:30
|
|
|
code += indent("}\n", 4)
|
|
|
|
|
code += indent("json = yield cmd_dict", 4)
|
2020-05-18 11:32:52 +01:00
|
|
|
if len(self.returns) == 0:
|
|
|
|
|
pass
|
|
|
|
|
elif len(self.returns) == 1:
|
2025-11-18 13:59:15 +05:30
|
|
|
ret = self.returns[0].generate_return(dict_="json")
|
|
|
|
|
code += indent(f"\nreturn {ret}", 4)
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
ret = "\nreturn (\n"
|
|
|
|
|
expr = ",\n".join(r.generate_return(dict_="json") for r in self.returns)
|
2020-05-18 11:32:52 +01:00
|
|
|
ret += indent(expr, 4)
|
2025-11-18 13:59:15 +05:30
|
|
|
ret += "\n)"
|
2020-05-18 11:32:52 +01:00
|
|
|
code += indent(ret, 4)
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def get_refs(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Get all refs for this command."""
|
2020-05-18 11:32:52 +01:00
|
|
|
refs = set()
|
|
|
|
|
for type_ in itertools.chain(self.parameters, self.returns):
|
|
|
|
|
if type_.items and type_.items.ref:
|
|
|
|
|
refs.add(type_.items.ref)
|
|
|
|
|
elif type_.ref:
|
|
|
|
|
refs.add(type_.ref)
|
|
|
|
|
return refs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CdpEvent:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""A CDP event object."""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
name: str
|
2025-11-16 18:50:49 -05:00
|
|
|
description: str | None
|
2020-05-18 11:32:52 +01:00
|
|
|
deprecated: bool
|
|
|
|
|
experimental: bool
|
2025-11-16 18:50:49 -05:00
|
|
|
parameters: list[CdpParameter]
|
2020-05-18 11:32:52 +01:00
|
|
|
domain: str
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def py_name(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Return the Python class name for this event."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return inflection.camelize(self.name, uppercase_first_letter=True)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, json: dict, domain: str):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Create a new CDP event instance from a JSON dict."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return cls(
|
2025-11-18 13:59:15 +05:30
|
|
|
json["name"],
|
|
|
|
|
json.get("description"),
|
|
|
|
|
json.get("deprecated", False),
|
|
|
|
|
json.get("experimental", False),
|
|
|
|
|
[cast(CdpParameter, CdpParameter.from_json(p)) for p in json.get("parameters", [])],
|
|
|
|
|
domain,
|
2020-05-18 11:32:52 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def generate_code(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate code for a CDP event."""
|
2020-05-18 11:32:52 +01:00
|
|
|
global current_version
|
2025-11-18 13:59:15 +05:30
|
|
|
code = dedent(f"""\
|
2020-05-18 11:32:52 +01:00
|
|
|
@event_class('{self.domain}.{self.name}')
|
|
|
|
|
@dataclass
|
2025-11-18 13:59:15 +05:30
|
|
|
class {self.py_name}:""")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n"
|
|
|
|
|
desc = ""
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.description or self.experimental:
|
|
|
|
|
if self.experimental:
|
2025-11-18 13:59:15 +05:30
|
|
|
desc += "**EXPERIMENTAL**\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
if self.description:
|
|
|
|
|
desc += self.description
|
|
|
|
|
|
|
|
|
|
code += indent(docstring(desc), 4)
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n"
|
|
|
|
|
code += indent("\n".join(p.generate_decl() for p in self.parameters), 4)
|
|
|
|
|
code += "\n\n"
|
|
|
|
|
def_from_json = dedent(f"""\
|
2020-05-18 11:32:52 +01:00
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, json: T_JSON_DICT) -> {self.py_name}:
|
|
|
|
|
return cls(
|
2025-11-18 13:59:15 +05:30
|
|
|
""")
|
2020-05-18 11:32:52 +01:00
|
|
|
code += indent(def_from_json, 4)
|
2025-11-18 13:59:15 +05:30
|
|
|
from_json = ",\n".join(p.generate_from_json(dict_="json") for p in self.parameters)
|
2020-05-18 11:32:52 +01:00
|
|
|
code += indent(from_json, 12)
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n"
|
|
|
|
|
code += indent(")", 8)
|
2020-05-18 11:32:52 +01:00
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def get_refs(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Get all refs for this event."""
|
2020-05-18 11:32:52 +01:00
|
|
|
refs = set()
|
|
|
|
|
for param in self.parameters:
|
|
|
|
|
if param.items and param.items.ref:
|
|
|
|
|
refs.add(param.items.ref)
|
|
|
|
|
elif param.ref:
|
|
|
|
|
refs.add(param.ref)
|
|
|
|
|
return refs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CdpDomain:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""A CDP domain contains metadata, types, commands, and events."""
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
domain: str
|
2025-11-16 18:50:49 -05:00
|
|
|
description: str | None
|
2020-05-18 11:32:52 +01:00
|
|
|
experimental: bool
|
2025-11-16 18:50:49 -05:00
|
|
|
dependencies: list[str]
|
|
|
|
|
types: list[CdpType]
|
|
|
|
|
commands: list[CdpCommand]
|
|
|
|
|
events: list[CdpEvent]
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
@property
|
2025-11-04 21:09:01 +07:00
|
|
|
def module(self) -> str:
|
2025-11-18 13:59:15 +05:30
|
|
|
"""The name of the Python module for this CDP domain."""
|
2020-05-18 11:32:52 +01:00
|
|
|
return snake_case(self.domain)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_json(cls, domain: dict):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Instantiate a CDP domain from a JSON object."""
|
|
|
|
|
types = domain.get("types", [])
|
|
|
|
|
commands = domain.get("commands", [])
|
|
|
|
|
events = domain.get("events", [])
|
|
|
|
|
domain_name = domain["domain"]
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
domain_name,
|
2025-11-18 13:59:15 +05:30
|
|
|
domain.get("description"),
|
|
|
|
|
domain.get("experimental", False),
|
|
|
|
|
domain.get("dependencies", []),
|
2020-05-18 11:32:52 +01:00
|
|
|
[CdpType.from_json(type) for type in types],
|
2025-11-18 13:59:15 +05:30
|
|
|
[CdpCommand.from_json(command, domain_name) for command in commands],
|
|
|
|
|
[CdpEvent.from_json(event, domain_name) for event in events],
|
2020-05-18 11:32:52 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def generate_code(self):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Generate the Python module code for a given CDP domain."""
|
|
|
|
|
exp = " (experimental)" if self.experimental else ""
|
2020-05-18 11:32:52 +01:00
|
|
|
code = MODULE_HEADER.format(self.domain, exp)
|
|
|
|
|
import_code = self.generate_imports()
|
|
|
|
|
if import_code:
|
|
|
|
|
code += import_code
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n\n"
|
|
|
|
|
code += "\n"
|
2025-12-29 10:49:17 -05:00
|
|
|
item_iter: Iterator[CdpEvent | CdpCommand | CdpType] = itertools.chain(
|
2020-05-18 11:32:52 +01:00
|
|
|
iter(self.types),
|
|
|
|
|
iter(self.commands),
|
|
|
|
|
iter(self.events),
|
|
|
|
|
)
|
2025-11-18 13:59:15 +05:30
|
|
|
code += "\n\n\n".join(item.generate_code() for item in item_iter)
|
|
|
|
|
code += "\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def generate_imports(self):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Determine which modules this module depends on and emit the code to import those modules.
|
|
|
|
|
|
|
|
|
|
Notice that CDP defines a `dependencies` field for each domain, but
|
2020-05-18 11:32:52 +01:00
|
|
|
these dependencies are a subset of the modules that we actually need to
|
|
|
|
|
import to make our Python code work correctly and type safe. So we
|
|
|
|
|
ignore the CDP's declared dependencies and compute them ourselves.
|
2025-11-18 13:59:15 +05:30
|
|
|
"""
|
2020-05-18 11:32:52 +01:00
|
|
|
refs = set()
|
|
|
|
|
for type_ in self.types:
|
|
|
|
|
refs |= type_.get_refs()
|
|
|
|
|
for command in self.commands:
|
|
|
|
|
refs |= command.get_refs()
|
|
|
|
|
for event in self.events:
|
|
|
|
|
refs |= event.get_refs()
|
|
|
|
|
dependencies = set()
|
|
|
|
|
for ref in refs:
|
|
|
|
|
try:
|
2025-11-18 13:59:15 +05:30
|
|
|
domain, _ = ref.split(".")
|
2020-05-18 11:32:52 +01:00
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
if domain != self.domain:
|
|
|
|
|
dependencies.add(snake_case(domain))
|
2025-11-18 13:59:15 +05:30
|
|
|
code = "\n".join(f"from . import {d}" for d in sorted(dependencies))
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
def generate_sphinx(self):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate a Sphinx document for this domain."""
|
2025-11-18 13:59:15 +05:30
|
|
|
docs = self.domain + "\n"
|
|
|
|
|
docs += "=" * len(self.domain) + "\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.description:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += f"{self.description}\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.experimental:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += "*This CDP domain is experimental.*\n\n"
|
|
|
|
|
docs += f".. module:: cdp.{self.module}\n\n"
|
|
|
|
|
docs += "* Types_\n* Commands_\n* Events_\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += "Types\n-----\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.types:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += dedent("""\
|
2020-05-18 11:32:52 +01:00
|
|
|
Generally, you do not need to instantiate CDP types
|
|
|
|
|
yourself. Instead, the API creates objects for you as return
|
|
|
|
|
values from commands, and then you can use those objects as
|
|
|
|
|
arguments to other commands.
|
2025-11-18 13:59:15 +05:30
|
|
|
""")
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += "*There are no types in this module.*\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
for type in self.types:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += f"\n.. autoclass:: {type.id}\n"
|
|
|
|
|
docs += " :members:\n"
|
|
|
|
|
docs += " :undoc-members:\n"
|
|
|
|
|
docs += " :exclude-members: from_json, to_json\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += "\nCommands\n--------\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.commands:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += dedent("""\
|
2020-05-18 11:32:52 +01:00
|
|
|
Each command is a generator function. The return
|
|
|
|
|
type ``Generator[x, y, z]`` indicates that the generator
|
|
|
|
|
*yields* arguments of type ``x``, it must be resumed with
|
|
|
|
|
an argument of type ``y``, and it returns type ``z``. In
|
|
|
|
|
this library, types ``x`` and ``y`` are the same for all
|
|
|
|
|
commands, and ``z`` is the return type you should pay attention
|
|
|
|
|
to. For more information, see
|
|
|
|
|
:ref:`Getting Started: Commands <getting-started-commands>`.
|
2025-11-18 13:59:15 +05:30
|
|
|
""")
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += "*There are no types in this module.*\n"
|
|
|
|
|
for command in sorted(self.commands, key=operator.attrgetter("py_name")):
|
|
|
|
|
docs += f"\n.. autofunction:: {command.py_name}\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += "\nEvents\n------\n\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
if self.events:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += dedent("""\
|
2020-05-18 11:32:52 +01:00
|
|
|
Generally, you do not need to instantiate CDP events
|
|
|
|
|
yourself. Instead, the API creates events for you and then
|
|
|
|
|
you use the event\'s attributes.
|
2025-11-18 13:59:15 +05:30
|
|
|
""")
|
2020-05-18 11:32:52 +01:00
|
|
|
else:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += "*There are no events in this module.*\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
for event in self.events:
|
2025-11-18 13:59:15 +05:30
|
|
|
docs += f"\n.. autoclass:: {event.py_name}\n"
|
|
|
|
|
docs += " :members:\n"
|
|
|
|
|
docs += " :undoc-members:\n"
|
|
|
|
|
docs += " :exclude-members: from_json, to_json\n"
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
return docs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse(json_path, output_path):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Parse JSON protocol description and return domain objects.
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
:param Path json_path: path to a JSON CDP schema
|
|
|
|
|
:param Path output_path: a directory path to create the modules in
|
|
|
|
|
:returns: a list of CDP domain objects
|
2025-11-18 13:59:15 +05:30
|
|
|
"""
|
2020-05-18 11:32:52 +01:00
|
|
|
global current_version
|
2023-01-25 00:39:48 +13:00
|
|
|
with open(json_path, encoding="utf-8") as json_file:
|
2020-05-18 11:32:52 +01:00
|
|
|
schema = json.load(json_file)
|
2025-11-18 13:59:15 +05:30
|
|
|
version = schema["version"]
|
|
|
|
|
assert (version["major"], version["minor"]) == ("1", "3")
|
|
|
|
|
current_version = f"{version['major']}.{version['minor']}"
|
2023-01-25 00:39:48 +13:00
|
|
|
domains = []
|
2025-11-18 13:59:15 +05:30
|
|
|
for domain in schema["domains"]:
|
2020-05-18 11:32:52 +01:00
|
|
|
domains.append(CdpDomain.from_json(domain))
|
|
|
|
|
return domains
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_init(init_path, domains):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate an `__init__.py` that exports the specified modules.
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
:param Path init_path: a file path to create the init file in
|
|
|
|
|
:param list[tuple] modules: a list of modules each represented as tuples
|
|
|
|
|
of (name, list_of_exported_symbols)
|
2025-11-18 13:59:15 +05:30
|
|
|
"""
|
2023-01-25 00:39:48 +13:00
|
|
|
with open(init_path, "w", encoding="utf-8") as init_file:
|
2020-05-18 11:32:52 +01:00
|
|
|
init_file.write(INIT_HEADER)
|
|
|
|
|
for domain in domains:
|
2025-11-18 13:59:15 +05:30
|
|
|
init_file.write(f"from . import {domain.module}\n")
|
|
|
|
|
init_file.write("from . import util\n\n")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_docs(docs_path, domains):
|
2025-11-30 19:15:56 -05:00
|
|
|
"""Generate Sphinx documents for each domain."""
|
2025-11-18 13:59:15 +05:30
|
|
|
logger.info("Generating Sphinx documents")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
# Remove generated documents
|
|
|
|
|
for subpath in docs_path.iterdir():
|
|
|
|
|
subpath.unlink()
|
|
|
|
|
|
|
|
|
|
# Generate document for each domain
|
|
|
|
|
for domain in domains:
|
2025-11-18 13:59:15 +05:30
|
|
|
doc = docs_path / f"{domain.module}.rst"
|
|
|
|
|
with doc.open("w") as f:
|
2020-05-18 11:32:52 +01:00
|
|
|
f.write(domain.generate_sphinx())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(browser_protocol_path, js_protocol_path, output_path):
|
2025-11-18 13:59:15 +05:30
|
|
|
"""Main entry point."""
|
2020-05-18 11:32:52 +01:00
|
|
|
output_path = Path(output_path).resolve()
|
|
|
|
|
json_paths = [
|
|
|
|
|
browser_protocol_path,
|
|
|
|
|
js_protocol_path,
|
|
|
|
|
]
|
|
|
|
|
|
2020-09-24 15:36:47 +01:00
|
|
|
# Generate util.py
|
|
|
|
|
util_path = output_path / "util.py"
|
2025-11-18 13:59:15 +05:30
|
|
|
with util_path.open("w") as util_file:
|
2020-09-24 15:36:47 +01:00
|
|
|
util_file.write(UTIL_PY)
|
|
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
# Remove generated code
|
|
|
|
|
for subpath in output_path.iterdir():
|
2025-11-18 13:59:15 +05:30
|
|
|
if subpath.is_file() and subpath.name not in ("py.typed", "util.py"):
|
2020-05-18 11:32:52 +01:00
|
|
|
subpath.unlink()
|
|
|
|
|
|
|
|
|
|
# Parse domains
|
2023-01-25 00:39:48 +13:00
|
|
|
domains = []
|
2020-05-18 11:32:52 +01:00
|
|
|
for json_path in json_paths:
|
2025-11-18 13:59:15 +05:30
|
|
|
logger.info("Parsing JSON file %s", json_path)
|
2020-05-18 11:32:52 +01:00
|
|
|
domains.extend(parse(json_path, output_path))
|
2025-11-18 13:59:15 +05:30
|
|
|
domains.sort(key=operator.attrgetter("domain"))
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
# Patch up CDP errors. It's easier to patch that here than it is to modify
|
|
|
|
|
# the generator code.
|
|
|
|
|
# 1. DOM includes an erroneous $ref that refers to itself.
|
|
|
|
|
# 2. Page includes an event with an extraneous backtick in the description.
|
|
|
|
|
for domain in domains:
|
2025-11-18 13:59:15 +05:30
|
|
|
if domain.domain == "DOM":
|
2020-05-18 11:32:52 +01:00
|
|
|
for cmd in domain.commands:
|
2025-11-18 13:59:15 +05:30
|
|
|
if cmd.name == "resolveNode":
|
2020-05-18 11:32:52 +01:00
|
|
|
# Patch 1
|
2025-11-18 13:59:15 +05:30
|
|
|
cmd.parameters[1].ref = "BackendNodeId"
|
|
|
|
|
elif domain.domain == "Page":
|
2020-05-18 11:32:52 +01:00
|
|
|
for event in domain.events:
|
2025-11-18 13:59:15 +05:30
|
|
|
if event.name == "screencastVisibilityChanged":
|
2020-05-18 11:32:52 +01:00
|
|
|
# Patch 2
|
2025-11-18 13:59:15 +05:30
|
|
|
event.description = event.description.replace("`", "")
|
2020-05-18 11:32:52 +01:00
|
|
|
|
|
|
|
|
for domain in domains:
|
2025-11-18 13:59:15 +05:30
|
|
|
logger.info("Generating module: %s → %s.py", domain.domain, domain.module)
|
|
|
|
|
module_path = output_path / f"{domain.module}.py"
|
|
|
|
|
with module_path.open("w") as module_file:
|
2020-05-18 11:32:52 +01:00
|
|
|
module_file.write(domain.generate_code())
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
init_path = output_path / "__init__.py"
|
2020-05-18 11:32:52 +01:00
|
|
|
generate_init(init_path, domains)
|
|
|
|
|
|
|
|
|
|
# Not generating the docs as we don't want people to directly
|
|
|
|
|
# Use the CDP APIs
|
|
|
|
|
# docs_path = here.parent / 'docs' / 'api'
|
|
|
|
|
# generate_docs(docs_path, domains)
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
py_typed_path = output_path / "py.typed"
|
2020-05-18 11:32:52 +01:00
|
|
|
py_typed_path.touch()
|
|
|
|
|
|
|
|
|
|
|
2025-11-18 13:59:15 +05:30
|
|
|
if __name__ == "__main__":
|
2020-05-18 11:32:52 +01:00
|
|
|
import sys
|
2025-11-18 13:59:15 +05:30
|
|
|
|
2020-05-18 11:32:52 +01:00
|
|
|
assert sys.version_info >= (3, 7), "To generate the CDP code requires python 3.7 or later"
|
|
|
|
|
args = sys.argv[1:]
|
|
|
|
|
main(*args)
|