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
83 changes: 36 additions & 47 deletions py/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,23 @@
# 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
from dataclasses import dataclass
from enum import Enum
import itertools
import json
import logging
import operator
import os
from pathlib import Path
import re
from textwrap import dedent, indent as tw_indent
from typing import Optional, cast, List, Union

from collections.abc import Iterator
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from textwrap import dedent
from textwrap import indent as tw_indent
from typing import Union, cast

import inflection # type: ignore


log_level = getattr(logging, os.environ.get("LOG_LEVEL", "warning").upper())
logging.basicConfig(level=log_level)
logger = logging.getLogger("generate")
Expand Down Expand Up @@ -90,16 +88,16 @@ def parse_json_event(json: T_JSON_DICT) -> typing.Any:


def indent(s, n):
"""A shortcut for ``textwrap.indent`` that always uses spaces."""
"""A shortcut for `textwrap.indent` that always uses spaces."""
return tw_indent(s, n * " ")


BACKTICK_RE = re.compile(r"`([^`]+)`(\w+)?")


def escape_backticks(docstr):
"""
Escape backticks in a docstring by doubling them up.
"""Escape backticks in a docstring by doubling them up.

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
Expand All @@ -121,12 +119,12 @@ def replace_one(match):


def inline_doc(description):
"""Generate an inline doc, e.g. ``#: This type is a ...``"""
"""Generate an inline doc, e.g. `#: This type is a ...`."""
if not description:
return ""

description = escape_backticks(description)
lines = [f"#: {l}" for l in description.split("\n")]
lines = [f"#: {line}" for line in description.split("\n")]
return "\n".join(lines)


Expand All @@ -140,7 +138,7 @@ def docstring(description):


def is_builtin(name):
"""Return True if ``name`` would shadow a builtin."""
"""Return True if `name` would shadow a builtin."""
try:
getattr(builtins, name)
return True
Expand All @@ -149,17 +147,19 @@ def is_builtin(name):


def snake_case(name):
"""Convert a camel case name to snake case. If the name would shadow a
Python builtin, then append an underscore."""
"""Convert a camel case name to snake case.

If the name would shadow a Python builtin, then append an underscore.
"""
name = inflection.underscore(name)
if is_builtin(name):
name += "_"
return name


def ref_to_python(ref):
"""
Convert a CDP ``$ref`` to the name of a Python type.
"""Convert a CDP `$ref` to the name of a Python type.

For a dotted ref, the part before the dot is snake cased.
"""
if "." in ref:
Expand Down Expand Up @@ -270,8 +270,7 @@ def generate_decl(self):
return code

def generate_to_json(self, dict_, use_self=True):
"""Generate the code that exports this property to the specified JSON
dict."""
"""Generate the code that exports this property to the specified JSON dict."""
self_ref = "self." if use_self else ""
assign = f"{dict_}['{self.name}'] = "
if self.items:
Expand All @@ -293,13 +292,11 @@ def generate_to_json(self, dict_, use_self=True):
return code

def generate_from_json(self, dict_):
"""Generate the code that creates an instance from a JSON dict named
``dict_``."""
"""Generate the code that creates an instance from a JSON dict named `dict_`."""
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}']]"
expr
else:
cons = CdpPrimitiveType.get_constructor(self.items.type, "i")
expr = f"[{cons} for i in {dict_}['{self.name}']]"
Expand Down Expand Up @@ -384,11 +381,11 @@ def __repr__(self):
return code

def generate_enum_code(self):
"""
Generate an "enum" type.
"""Generate an "enum" type.

Enums are handled by making a python class that contains only class
members. Each class member is upper snaked case, e.g.
``MyTypeClass.MY_ENUM_VALUE`` and is assigned a string value from the
`MyTypeClass.MY_ENUM_VALUE` and is assigned a string value from the
CDP metadata.
"""
def_to_json = dedent("""\
Expand All @@ -414,12 +411,11 @@ def from_json(cls, json):
return code

def generate_class_code(self):
"""
Generate a class type.
Top-level types that are defined as a CDP ``object`` are turned into Python
"""Generate a class type.

Top-level types that are defined as a CDP `object` are turned into Python
dataclasses.
"""
# children = set()
code = dedent(f"""\
@dataclass
class {self.id}:\n""")
Expand Down Expand Up @@ -536,9 +532,7 @@ def generate_doc(self):
return doc

def generate_from_json(self, dict_):
"""
Generate the code to instantiate this parameter from a JSON dict.
"""
"""Generate the code to instantiate this parameter from a JSON dict."""
code = super().generate_from_json(dict_)
return f"{self.py_name}={code}"

Expand Down Expand Up @@ -836,10 +830,9 @@ def generate_code(self):
return code

def generate_imports(self):
"""
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
"""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
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.
Expand All @@ -864,9 +857,7 @@ def generate_imports(self):
return code

def generate_sphinx(self):
"""
Generate a Sphinx document for this domain.
"""
"""Generate a Sphinx document for this domain."""
docs = self.domain + "\n"
docs += "=" * len(self.domain) + "\n\n"
if self.description:
Expand Down Expand Up @@ -928,8 +919,8 @@ def generate_sphinx(self):


def parse(json_path, output_path):
"""
Parse JSON protocol description and return domain objects.
"""Parse JSON protocol description and return domain objects.

: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
Expand All @@ -947,8 +938,8 @@ def parse(json_path, output_path):


def generate_init(init_path, domains):
"""
Generate an ``__init__.py`` that exports the specified modules.
"""Generate an `__init__.py` that exports the specified modules.

: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)
Expand All @@ -961,9 +952,7 @@ def generate_init(init_path, domains):


def generate_docs(docs_path, domains):
"""
Generate Sphinx documents for each domain.
"""
"""Generate Sphinx documents for each domain."""
logger.info("Generating Sphinx documents")

# Remove generated documents
Expand Down
3 changes: 1 addition & 2 deletions py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,10 @@ ignore_missing_imports = true
[tool.ruff]
extend-exclude = [
"selenium/webdriver/common/devtools/",
"generate.py",
]
line-length = 120
respect-gitignore = true
target-version = "py39"
target-version = "py310"

[tool.ruff.lint]
extend-select = ["D", "E4", "E7", "E9", "F", "I", "E501", "RUF022", "TID252"]
Expand Down