|  | # 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. | 
|  |  | 
|  | # flake8: noqa | 
|  |  | 
|  | 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, Iterator | 
|  |  | 
|  | import inflection  # type: ignore | 
|  |  | 
|  |  | 
|  | log_level = getattr(logging, os.environ.get('LOG_LEVEL', 'warning').upper()) | 
|  | logging.basicConfig(level=log_level) | 
|  | logger = logging.getLogger('generate') | 
|  |  | 
|  | SHARED_HEADER = '''# DO NOT EDIT THIS FILE! | 
|  | # | 
|  | # This file is generated from the CDP specification. If you need to make | 
|  | # changes, edit the generator and regenerate all of the modules.''' | 
|  |  | 
|  | INIT_HEADER = '''{} | 
|  | '''.format(SHARED_HEADER) | 
|  |  | 
|  | MODULE_HEADER = '''{} | 
|  | # | 
|  | # CDP domain: {{}}{{}} | 
|  | from __future__ import annotations | 
|  | from .util import event_class, T_JSON_DICT | 
|  | from dataclasses import dataclass | 
|  | import enum | 
|  | import typing | 
|  | '''.format(SHARED_HEADER) | 
|  |  | 
|  | current_version = '' | 
|  |  | 
|  | 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 | 
|  | cls.event_class = method | 
|  | 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']) | 
|  | """ | 
|  |  | 
|  |  | 
|  | def indent(s, n): | 
|  | ''' 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. | 
|  | 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. | 
|  | ''' | 
|  | def replace_one(match): | 
|  | if match.group(2) == 's': | 
|  | return f"``{match.group(1)}``'s" | 
|  | if match.group(2): | 
|  | # This case (some trailer other than "s") doesn't currently exist | 
|  | # in the CDP definitions, but it's here just to be safe. | 
|  | return f'``{match.group(1)}`` {match.group(2)}' | 
|  | return f'``{match.group(1)}``' | 
|  |  | 
|  | # Sometimes pipes are used where backticks should have been used. | 
|  | docstr = docstr.replace('|', '`') | 
|  | return BACKTICK_RE.sub(replace_one, docstr) | 
|  |  | 
|  |  | 
|  | def inline_doc(description): | 
|  | ''' 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')] | 
|  | return '\n'.join(lines) | 
|  |  | 
|  |  | 
|  | def docstring(description): | 
|  | ''' Generate a docstring from a description. ''' | 
|  | if not description: | 
|  | return '' | 
|  |  | 
|  | description = escape_backticks(description) | 
|  | return dedent("'''\n{}\n'''").format(description) | 
|  |  | 
|  |  | 
|  | def is_builtin(name): | 
|  | ''' Return True if ``name`` would shadow a builtin. ''' | 
|  | try: | 
|  | getattr(builtins, name) | 
|  | return True | 
|  | except AttributeError: | 
|  | return False | 
|  |  | 
|  |  | 
|  | def snake_case(name): | 
|  | ''' 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. | 
|  | For a dotted ref, the part before the dot is snake cased. | 
|  | ''' | 
|  | if '.' in ref: | 
|  | domain, subtype = ref.split('.') | 
|  | ref = f'{snake_case(domain)}.{subtype}' | 
|  | return f"{ref}" | 
|  |  | 
|  |  | 
|  | class CdpPrimitiveType(Enum): | 
|  | ''' All of the CDP types that map directly to a Python type. ''' | 
|  | boolean = 'bool' | 
|  | integer = 'int' | 
|  | number = 'float' | 
|  | object = 'dict' | 
|  | string = 'str' | 
|  |  | 
|  | @classmethod | 
|  | def get_annotation(cls, cdp_type): | 
|  | ''' Return a type annotation for the CDP type. ''' | 
|  | if cdp_type == 'any': | 
|  | return 'typing.Any' | 
|  | return cls[cdp_type].value | 
|  |  | 
|  | @classmethod | 
|  | def get_constructor(cls, cdp_type, val): | 
|  | ''' Return the code to construct a value for a given CDP type. ''' | 
|  | if cdp_type == 'any': | 
|  | return val | 
|  | cons = cls[cdp_type].value | 
|  | return f'{cons}({val})' | 
|  |  | 
|  |  | 
|  | @dataclass | 
|  | class CdpItems: | 
|  | ''' Represents the type of a repeated item. ''' | 
|  | type: str | 
|  | ref: str | 
|  |  | 
|  | @classmethod | 
|  | def from_json(cls, type): | 
|  | ''' Generate code to instantiate an item from a JSON object. ''' | 
|  | return cls(type.get('type'), type.get('$ref')) | 
|  |  | 
|  |  | 
|  | @dataclass | 
|  | class CdpProperty: | 
|  | ''' A property belonging to a non-primitive CDP type. ''' | 
|  | name: str | 
|  | description: Optional[str] | 
|  | type: Optional[str] | 
|  | ref: Optional[str] | 
|  | enum: List[str] | 
|  | items: Optional[CdpItems] | 
|  | optional: bool | 
|  | experimental: bool | 
|  | deprecated: bool | 
|  |  | 
|  | @property | 
|  | def py_name(self): | 
|  | ''' Get this property's Python name. ''' | 
|  | return snake_case(self.name) | 
|  |  | 
|  | @property | 
|  | def py_annotation(self): | 
|  | ''' This property's Python type annotation. ''' | 
|  | if self.items: | 
|  | if self.items.ref: | 
|  | py_ref = ref_to_python(self.items.ref) | 
|  | ann = f"typing.List[{py_ref}]" | 
|  | else: | 
|  | ann = 'typing.List[{}]'.format( | 
|  | CdpPrimitiveType.get_annotation(self.items.type)) | 
|  | else: | 
|  | if self.ref: | 
|  | py_ref = ref_to_python(self.ref) | 
|  | ann = py_ref | 
|  | else: | 
|  | ann = CdpPrimitiveType.get_annotation( | 
|  | cast(str, self.type)) | 
|  | if self.optional: | 
|  | ann = f'typing.Optional[{ann}]' | 
|  | return ann | 
|  |  | 
|  | @classmethod | 
|  | def from_json(cls, property): | 
|  | ''' Instantiate a CDP property from a JSON object. ''' | 
|  | return cls( | 
|  | 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), | 
|  | ) | 
|  |  | 
|  | def generate_decl(self): | 
|  | ''' Generate the code that declares this property. ''' | 
|  | code = inline_doc(self.description) | 
|  | if code: | 
|  | code += '\n' | 
|  | code += f'{self.py_name}: {self.py_annotation}' | 
|  | if self.optional: | 
|  | code += ' = None' | 
|  | return code | 
|  |  | 
|  | def generate_to_json(self, dict_, use_self=True): | 
|  | ''' 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: | 
|  | 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: | 
|  | code = dedent(f'''\ | 
|  | if {self_ref}{self.py_name} is not None: | 
|  | {assign}''') | 
|  | else: | 
|  | code = assign | 
|  | return code | 
|  |  | 
|  | def generate_from_json(self, 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}']]" | 
|  | else: | 
|  | if self.ref: | 
|  | py_ref = ref_to_python(self.ref) | 
|  | expr = f"{py_ref}.from_json({dict_}['{self.name}'])" | 
|  | else: | 
|  | expr = CdpPrimitiveType.get_constructor(self.type, | 
|  | f"{dict_}['{self.name}']") | 
|  | if self.optional: | 
|  | expr = f"{expr} if '{self.name}' in {dict_} else None" | 
|  | return expr | 
|  |  | 
|  |  | 
|  | @dataclass | 
|  | class CdpType: | 
|  | ''' A top-level CDP type. ''' | 
|  | id: str | 
|  | description: Optional[str] | 
|  | type: str | 
|  | items: Optional[CdpItems] | 
|  | enum: List[str] | 
|  | properties: List[CdpProperty] | 
|  |  | 
|  | @classmethod | 
|  | def from_json(cls, type_): | 
|  | ''' Instantiate a CDP type from a JSON object. ''' | 
|  | return cls( | 
|  | 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', [])], | 
|  | ) | 
|  |  | 
|  | def generate_code(self): | 
|  | ''' Generate Python code for this type. ''' | 
|  | logger.debug('Generating type %s: %s', self.id, self.type) | 
|  | if self.enum: | 
|  | return self.generate_enum_code() | 
|  | if self.properties: | 
|  | return self.generate_class_code() | 
|  | return self.generate_primitive_code() | 
|  |  | 
|  | def generate_primitive_code(self): | 
|  | ''' Generate code for a primitive type. ''' | 
|  | if self.items: | 
|  | if self.items.ref: | 
|  | nested_type = ref_to_python(self.items.ref) | 
|  | else: | 
|  | nested_type = CdpPrimitiveType.get_annotation(self.items.type) | 
|  | py_type = f'typing.List[{nested_type}]' | 
|  | superclass = 'list' | 
|  | else: | 
|  | # A primitive type cannot have a ref, so there is no branch here. | 
|  | py_type = CdpPrimitiveType.get_annotation(self.type) | 
|  | superclass = py_type | 
|  |  | 
|  | code = f'class {self.id}({superclass}):\n' | 
|  | doc = docstring(self.description) | 
|  | if doc: | 
|  | code += indent(doc, 4) + '\n' | 
|  |  | 
|  | def_to_json = dedent(f'''\ | 
|  | def to_json(self) -> {py_type}: | 
|  | return self''') | 
|  | code += indent(def_to_json, 4) | 
|  |  | 
|  | def_from_json = dedent(f'''\ | 
|  | @classmethod | 
|  | def from_json(cls, json: {py_type}) -> {self.id}: | 
|  | return cls(json)''') | 
|  | code += '\n\n' + indent(def_from_json, 4) | 
|  |  | 
|  | def_repr = dedent(f'''\ | 
|  | def __repr__(self): | 
|  | return '{self.id}({{}})'.format(super().__repr__())''') | 
|  | code += '\n\n' + indent(def_repr, 4) | 
|  |  | 
|  | return code | 
|  |  | 
|  | def generate_enum_code(self): | 
|  | ''' | 
|  | 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 | 
|  | CDP metadata. | 
|  | ''' | 
|  | def_to_json = dedent('''\ | 
|  | def to_json(self): | 
|  | return self.value''') | 
|  |  | 
|  | def_from_json = dedent('''\ | 
|  | @classmethod | 
|  | def from_json(cls, json): | 
|  | return cls(json)''') | 
|  |  | 
|  | code = f'class {self.id}(enum.Enum):\n' | 
|  | doc = docstring(self.description) | 
|  | if doc: | 
|  | code += indent(doc, 4) + '\n' | 
|  | 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) | 
|  | code += '\n' + indent(def_to_json, 4) | 
|  | code += '\n\n' + indent(def_from_json, 4) | 
|  |  | 
|  | 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 | 
|  | dataclasses. | 
|  | ''' | 
|  | # children = set() | 
|  | code = dedent(f'''\ | 
|  | @dataclass | 
|  | class {self.id}:\n''') | 
|  | doc = docstring(self.description) | 
|  | if doc: | 
|  | code += indent(doc, 4) + '\n' | 
|  |  | 
|  | # 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) | 
|  | props.sort(key=operator.attrgetter('optional')) | 
|  | code += '\n\n'.join(indent(p.generate_decl(), 4) for p in props) | 
|  | code += '\n\n' | 
|  |  | 
|  | # Emit to_json() method. The properties are sorted in the same order as | 
|  | # above for readability. | 
|  | def_to_json = dedent('''\ | 
|  | def to_json(self): | 
|  | json = dict() | 
|  | ''') | 
|  | 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' | 
|  |  | 
|  | # Emit from_json() method. The properties are sorted in the same order | 
|  | # as above for readability. | 
|  | def_from_json = dedent('''\ | 
|  | @classmethod | 
|  | def from_json(cls, json): | 
|  | return cls( | 
|  | ''') | 
|  | from_jsons = [] | 
|  | for p in props: | 
|  | 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) | 
|  | code += indent(def_from_json, 4) | 
|  |  | 
|  | return code | 
|  |  | 
|  | def get_refs(self): | 
|  | ''' Return all refs for this type. ''' | 
|  | 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): | 
|  | ''' A parameter to a CDP command. ''' | 
|  |  | 
|  | def generate_code(self): | 
|  | ''' Generate the code for a parameter in a function call. ''' | 
|  | 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) | 
|  | py_type = f'typing.List[{nested_type}]' | 
|  | else: | 
|  | if self.ref: | 
|  | py_type = f"{ref_to_python(self.ref)}" | 
|  | else: | 
|  | py_type = CdpPrimitiveType.get_annotation( | 
|  | cast(str, self.type)) | 
|  | if self.optional: | 
|  | py_type = f'typing.Optional[{py_type}]' | 
|  | code = f"{self.py_name}: {py_type}" | 
|  | if self.optional: | 
|  | code += ' = None' | 
|  | return code | 
|  |  | 
|  | def generate_decl(self): | 
|  | ''' Generate the declaration for this parameter. ''' | 
|  | if self.description: | 
|  | code = inline_doc(self.description) | 
|  | code += '\n' | 
|  | else: | 
|  | code = '' | 
|  | code += f'{self.py_name}: {self.py_annotation}' | 
|  | return code | 
|  |  | 
|  | def generate_doc(self): | 
|  | ''' Generate the docstring for this parameter. ''' | 
|  | doc = f':param {self.py_name}:' | 
|  |  | 
|  | if self.experimental: | 
|  | doc += ' **(EXPERIMENTAL)**' | 
|  |  | 
|  | if self.optional: | 
|  | doc += ' *(Optional)*' | 
|  |  | 
|  | if self.description: | 
|  | desc = self.description.replace('`', '``').replace('\n', ' ') | 
|  | doc += f' {desc}' | 
|  | return doc | 
|  |  | 
|  | def generate_from_json(self, dict_): | 
|  | ''' | 
|  | Generate the code to instantiate this parameter from a JSON dict. | 
|  | ''' | 
|  | code = super().generate_from_json(dict_) | 
|  | return f'{self.py_name}={code}' | 
|  |  | 
|  |  | 
|  | class CdpReturn(CdpProperty): | 
|  | ''' A return value from a CDP command. ''' | 
|  | @property | 
|  | def py_annotation(self): | 
|  | ''' Return the Python type annotation for this return. ''' | 
|  | 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) | 
|  | ann = f'typing.List[{py_type}]' | 
|  | 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: | 
|  | ann = f'typing.Optional[{ann}]' | 
|  | return ann | 
|  |  | 
|  | def generate_doc(self): | 
|  | ''' Generate the docstring for this return. ''' | 
|  | if self.description: | 
|  | doc = self.description.replace('\n', ' ') | 
|  | if self.optional: | 
|  | doc = f'*(Optional)* {doc}' | 
|  | else: | 
|  | doc = '' | 
|  | return doc | 
|  |  | 
|  | def generate_return(self, dict_): | 
|  | ''' Generate code for returning this value. ''' | 
|  | return super().generate_from_json(dict_) | 
|  |  | 
|  |  | 
|  | @dataclass | 
|  | class CdpCommand: | 
|  | ''' A CDP command. ''' | 
|  | name: str | 
|  | description: str | 
|  | experimental: bool | 
|  | deprecated: bool | 
|  | parameters: List[CdpParameter] | 
|  | returns: List[CdpReturn] | 
|  | domain: str | 
|  |  | 
|  | @property | 
|  | def py_name(self): | 
|  | ''' Get a Python name for this command. ''' | 
|  | return snake_case(self.name) | 
|  |  | 
|  | @classmethod | 
|  | def from_json(cls, command, domain) -> 'CdpCommand': | 
|  | ''' Instantiate a CDP command from a JSON object. ''' | 
|  | parameters = command.get('parameters', []) | 
|  | returns = command.get('returns', []) | 
|  |  | 
|  | return cls( | 
|  | command['name'], | 
|  | command.get('description'), | 
|  | command.get('experimental', False), | 
|  | command.get('deprecated', False), | 
|  | [cast(CdpParameter, CdpParameter.from_json(p)) for p in parameters], | 
|  | [cast(CdpReturn, CdpReturn.from_json(r)) for r in returns], | 
|  | domain, | 
|  | ) | 
|  |  | 
|  | def generate_code(self): | 
|  | ''' Generate code for a CDP command. ''' | 
|  | global current_version | 
|  | # Generate the function header | 
|  | if len(self.returns) == 0: | 
|  | ret_type = 'None' | 
|  | elif len(self.returns) == 1: | 
|  | ret_type = self.returns[0].py_annotation | 
|  | else: | 
|  | nested_types = ', '.join(r.py_annotation for r in self.returns) | 
|  | ret_type = f'typing.Tuple[{nested_types}]' | 
|  | ret_type = f"typing.Generator[T_JSON_DICT,T_JSON_DICT,{ret_type}]" | 
|  |  | 
|  | code = '' | 
|  |  | 
|  | code += f'def {self.py_name}(' | 
|  | ret = f') -> {ret_type}:\n' | 
|  | if self.parameters: | 
|  | 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: | 
|  | para += ' = None' | 
|  | clean_params.append(para) | 
|  | code += '\n' | 
|  | code += indent( | 
|  | ',\n'.join(clean_params), 8) | 
|  | code += '\n' | 
|  | code += indent(ret, 4) | 
|  | else: | 
|  | code += ret | 
|  |  | 
|  | # Generate the docstring | 
|  | doc = '' | 
|  | if self.description: | 
|  | doc = self.description | 
|  | if self.experimental: | 
|  | doc += '\n\n**EXPERIMENTAL**' | 
|  | if self.parameters and doc: | 
|  | doc += '\n\n' | 
|  | elif not self.parameters and self.returns: | 
|  | doc += '\n' | 
|  | doc += '\n'.join(p.generate_doc() for p in self.parameters) | 
|  | if len(self.returns) == 1: | 
|  | doc += '\n' | 
|  | ret_doc = self.returns[0].generate_doc() | 
|  | doc += f':returns: {ret_doc}' | 
|  | elif len(self.returns) > 1: | 
|  | 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)) | 
|  | doc += indent(ret_docs, 4) | 
|  | if doc: | 
|  | code += indent(docstring(doc), 4) | 
|  |  | 
|  | # Generate the function body | 
|  | if self.parameters: | 
|  | 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) | 
|  | code += indent(f"'method': '{self.domain}.{self.name}',\n", 8) | 
|  | if self.parameters: | 
|  | code += indent("'params': params,\n", 8) | 
|  | code += indent('}\n', 4) | 
|  | code += indent('json = yield cmd_dict', 4) | 
|  | if len(self.returns) == 0: | 
|  | pass | 
|  | elif len(self.returns) == 1: | 
|  | ret = self.returns[0].generate_return(dict_='json') | 
|  | code += indent(f'\nreturn {ret}', 4) | 
|  | else: | 
|  | ret = '\nreturn (\n' | 
|  | expr = ',\n'.join(r.generate_return(dict_='json') for r in self.returns) | 
|  | ret += indent(expr, 4) | 
|  | ret += '\n)' | 
|  | code += indent(ret, 4) | 
|  | return code | 
|  |  | 
|  | def get_refs(self): | 
|  | ''' Get all refs for this command. ''' | 
|  | 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: | 
|  | ''' A CDP event object. ''' | 
|  | name: str | 
|  | description: Optional[str] | 
|  | deprecated: bool | 
|  | experimental: bool | 
|  | parameters: List[CdpParameter] | 
|  | domain: str | 
|  |  | 
|  | @property | 
|  | def py_name(self): | 
|  | ''' Return the Python class name for this event. ''' | 
|  | return inflection.camelize(self.name, uppercase_first_letter=True) | 
|  |  | 
|  | @classmethod | 
|  | def from_json(cls, json: dict, domain: str): | 
|  | ''' Create a new CDP event instance from a JSON dict. ''' | 
|  | return cls( | 
|  | 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 | 
|  | ) | 
|  |  | 
|  | def generate_code(self): | 
|  | ''' Generate code for a CDP event. ''' | 
|  | global current_version | 
|  | code = dedent(f'''\ | 
|  | @event_class('{self.domain}.{self.name}') | 
|  | @dataclass | 
|  | class {self.py_name}:''') | 
|  |  | 
|  | code += '\n' | 
|  | desc = '' | 
|  | if self.description or self.experimental: | 
|  | if self.experimental: | 
|  | desc += '**EXPERIMENTAL**\n\n' | 
|  |  | 
|  | if self.description: | 
|  | desc += self.description | 
|  |  | 
|  | code += indent(docstring(desc), 4) | 
|  | code += '\n' | 
|  | code += indent( | 
|  | '\n'.join(p.generate_decl() for p in self.parameters), 4) | 
|  | code += '\n\n' | 
|  | def_from_json = dedent(f'''\ | 
|  | @classmethod | 
|  | def from_json(cls, json: T_JSON_DICT) -> {self.py_name}: | 
|  | return cls( | 
|  | ''') | 
|  | code += indent(def_from_json, 4) | 
|  | from_json = ',\n'.join(p.generate_from_json(dict_='json') | 
|  | for p in self.parameters) | 
|  | code += indent(from_json, 12) | 
|  | code += '\n' | 
|  | code += indent(')', 8) | 
|  | return code | 
|  |  | 
|  | def get_refs(self): | 
|  | ''' Get all refs for this event. ''' | 
|  | 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: | 
|  | ''' A CDP domain contains metadata, types, commands, and events. ''' | 
|  | domain: str | 
|  | description: Optional[str] | 
|  | experimental: bool | 
|  | dependencies: List[str] | 
|  | types: List[CdpType] | 
|  | commands: List[CdpCommand] | 
|  | events: List[CdpEvent] | 
|  |  | 
|  | @property | 
|  | def module(self): | 
|  | ''' The name of the Python module for this CDP domain. ''' | 
|  | return snake_case(self.domain) | 
|  |  | 
|  | @classmethod | 
|  | def from_json(cls, domain: dict): | 
|  | ''' Instantiate a CDP domain from a JSON object. ''' | 
|  | types = domain.get('types', []) | 
|  | commands = domain.get('commands', []) | 
|  | events = domain.get('events', []) | 
|  | domain_name = domain['domain'] | 
|  |  | 
|  | return cls( | 
|  | domain_name, | 
|  | domain.get('description'), | 
|  | domain.get('experimental', False), | 
|  | domain.get('dependencies', []), | 
|  | [CdpType.from_json(type) for type in types], | 
|  | [CdpCommand.from_json(command, domain_name) | 
|  | for command in commands], | 
|  | [CdpEvent.from_json(event, domain_name) for event in events] | 
|  | ) | 
|  |  | 
|  | def generate_code(self): | 
|  | ''' Generate the Python module code for a given CDP domain. ''' | 
|  | exp = ' (experimental)' if self.experimental else '' | 
|  | code = MODULE_HEADER.format(self.domain, exp) | 
|  | import_code = self.generate_imports() | 
|  | if import_code: | 
|  | code += import_code | 
|  | code += '\n\n' | 
|  | code += '\n' | 
|  | item_iter_t = Union[CdpEvent, CdpCommand, CdpType] | 
|  | item_iter: Iterator[item_iter_t] = itertools.chain( | 
|  | iter(self.types), | 
|  | iter(self.commands), | 
|  | iter(self.events), | 
|  | ) | 
|  | code += '\n\n\n'.join(item.generate_code() for item in item_iter) | 
|  | code += '\n' | 
|  | 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 | 
|  | 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. | 
|  | ''' | 
|  | 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: | 
|  | domain, _ = ref.split('.') | 
|  | except ValueError: | 
|  | continue | 
|  | if domain != self.domain: | 
|  | dependencies.add(snake_case(domain)) | 
|  | code = '\n'.join(f'from . import {d}' for d in sorted(dependencies)) | 
|  |  | 
|  | return code | 
|  |  | 
|  | def generate_sphinx(self): | 
|  | ''' | 
|  | Generate a Sphinx document for this domain. | 
|  | ''' | 
|  | docs = self.domain + '\n' | 
|  | docs += '=' * len(self.domain) + '\n\n' | 
|  | if self.description: | 
|  | docs += f'{self.description}\n\n' | 
|  | if self.experimental: | 
|  | docs += '*This CDP domain is experimental.*\n\n' | 
|  | docs += f'.. module:: cdp.{self.module}\n\n' | 
|  | docs += '* Types_\n* Commands_\n* Events_\n\n' | 
|  |  | 
|  | docs += 'Types\n-----\n\n' | 
|  | if self.types: | 
|  | docs += dedent('''\ | 
|  | 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. | 
|  | ''') | 
|  | else: | 
|  | docs += '*There are no types in this module.*\n' | 
|  | for type in self.types: | 
|  | docs += f'\n.. autoclass:: {type.id}\n' | 
|  | docs += '      :members:\n' | 
|  | docs += '      :undoc-members:\n' | 
|  | docs += '      :exclude-members: from_json, to_json\n' | 
|  |  | 
|  | docs += '\nCommands\n--------\n\n' | 
|  | if self.commands: | 
|  | docs += dedent('''\ | 
|  | 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>`. | 
|  | ''') | 
|  | else: | 
|  | 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' | 
|  |  | 
|  | docs += '\nEvents\n------\n\n' | 
|  | if self.events: | 
|  | docs += dedent('''\ | 
|  | 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. | 
|  | ''') | 
|  | else: | 
|  | docs += '*There are no events in this module.*\n' | 
|  | for event in self.events: | 
|  | docs += f'\n.. autoclass:: {event.py_name}\n' | 
|  | docs += '      :members:\n' | 
|  | docs += '      :undoc-members:\n' | 
|  | docs += '      :exclude-members: from_json, to_json\n' | 
|  |  | 
|  | return docs | 
|  |  | 
|  |  | 
|  | def parse(json_path, output_path): | 
|  | ''' | 
|  | 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 | 
|  | ''' | 
|  | global current_version | 
|  | with open(json_path, encoding="utf-8") as json_file: | 
|  | schema = json.load(json_file) | 
|  | version = schema['version'] | 
|  | assert (version['major'], version['minor']) == ('1', '3') | 
|  | current_version = f'{version["major"]}.{version["minor"]}' | 
|  | domains = [] | 
|  | for domain in schema['domains']: | 
|  | domains.append(CdpDomain.from_json(domain)) | 
|  | return domains | 
|  |  | 
|  |  | 
|  | def generate_init(init_path, domains): | 
|  | ''' | 
|  | 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) | 
|  | ''' | 
|  | with open(init_path, "w", encoding="utf-8") as init_file: | 
|  | init_file.write(INIT_HEADER) | 
|  | for domain in domains: | 
|  | init_file.write(f'from . import {domain.module}\n') | 
|  | init_file.write('from . import util\n\n') | 
|  |  | 
|  |  | 
|  | def generate_docs(docs_path, domains): | 
|  | ''' | 
|  | Generate Sphinx documents for each domain. | 
|  | ''' | 
|  | logger.info('Generating Sphinx documents') | 
|  |  | 
|  | # Remove generated documents | 
|  | for subpath in docs_path.iterdir(): | 
|  | subpath.unlink() | 
|  |  | 
|  | # Generate document for each domain | 
|  | for domain in domains: | 
|  | doc = docs_path / f'{domain.module}.rst' | 
|  | with doc.open('w') as f: | 
|  | f.write(domain.generate_sphinx()) | 
|  |  | 
|  |  | 
|  | def main(browser_protocol_path, js_protocol_path, output_path): | 
|  | ''' Main entry point. ''' | 
|  | output_path = Path(output_path).resolve() | 
|  | json_paths = [ | 
|  | browser_protocol_path, | 
|  | js_protocol_path, | 
|  | ] | 
|  |  | 
|  | # Generate util.py | 
|  | util_path = output_path / "util.py" | 
|  | with util_path.open('w') as util_file: | 
|  | util_file.write(UTIL_PY) | 
|  |  | 
|  | # Remove generated code | 
|  | for subpath in output_path.iterdir(): | 
|  | if subpath.is_file() and subpath.name not in ('py.typed', 'util.py'): | 
|  | subpath.unlink() | 
|  |  | 
|  | # Parse domains | 
|  | domains = [] | 
|  | for json_path in json_paths: | 
|  | logger.info('Parsing JSON file %s', json_path) | 
|  | domains.extend(parse(json_path, output_path)) | 
|  | domains.sort(key=operator.attrgetter('domain')) | 
|  |  | 
|  | # 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: | 
|  | if domain.domain == 'DOM': | 
|  | for cmd in domain.commands: | 
|  | if cmd.name == 'resolveNode': | 
|  | # Patch 1 | 
|  | cmd.parameters[1].ref = 'BackendNodeId' | 
|  | elif domain.domain == 'Page': | 
|  | for event in domain.events: | 
|  | if event.name == 'screencastVisibilityChanged': | 
|  | # Patch 2 | 
|  | event.description = event.description.replace('`', '') | 
|  |  | 
|  | for domain in domains: | 
|  | 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: | 
|  | module_file.write(domain.generate_code()) | 
|  |  | 
|  | init_path = output_path / '__init__.py' | 
|  | 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) | 
|  |  | 
|  | py_typed_path = output_path / 'py.typed' | 
|  | py_typed_path.touch() | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | import sys | 
|  | assert sys.version_info >= (3, 7), "To generate the CDP code requires python 3.7 or later" | 
|  | args = sys.argv[1:] | 
|  | main(*args) |