chromium/components/exo/wayland/compatibility_test/wayland_protocol_data_classes.py

# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import dataclasses
import enum
import os.path
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple
import xml.etree.ElementTree

# Short aliases for typing
Element = xml.etree.ElementTree.Element


@dataclasses.dataclass(frozen=True)
class Description:
    """Holds the data from a Wayland protocol <description> subtree."""

    # The value of the name summary of the <description>.
    summary: str

    # The text content of the <description>.
    description: str

    @staticmethod
    def parse_xml(e: Element) -> Optional['Description']:
        return Description(summary=e.get('summary'),
                           description=e.text.strip()
                           if e.text else '') if e is not None else None


class MessageArgType(enum.Enum):
    """The valid types of an <arg>."""
    INT = 'int'
    UINT = 'uint'
    FIXED = 'fixed'
    STRING = 'string'
    FD = 'fd'  # File descriptor
    ARRAY = 'array'  # untyped Array
    NEW_ID = 'new_id'  # Special type for constructing
    OBJECT = 'object'  # An interface


@dataclasses.dataclass(frozen=True)
class MessageArg:
    """Holds the data from a Wayland protocol <arg> subtree."""

    # The containing Message, for context.
    message: 'Message' = dataclasses.field(repr=False, hash=False)

    # The value of the name attribute of the <arg>.
    name: str

    # The value of the type attribute of the <arg>.
    type: MessageArgType

    # The value of the optional summary attribute of the <arg>.
    summary: Optional[str]

    # The value of the optional interface attribute of the <arg>.
    interface: Optional[str]

    # The value of the optional nullable attribute of the <arg>.
    nullable: Optional[bool]

    # The value of the optional enum attribute of the <arg>.
    enum: Optional[str]

    # The optional <description> child element of the <arg>.
    description: Optional[Description]

    @staticmethod
    def parse_xml(message: 'Message', e: Element) -> 'MessageArg':
        return MessageArg(message=message,
                          name=e.get('name'),
                          type=e.get('type'),
                          summary=e.get('summary'),
                          interface=e.get('interface'),
                          nullable=e.get('allow-null') == 'true'
                          if e.get('allow-null') else None,
                          enum=e.get('enum'),
                          description=Description.parse_xml(
                              e.find('description')))


class RequestType(enum.Enum):
    """The valid types of a <request> message."""
    DESTRUCTOR = 'destructor'


@dataclasses.dataclass(frozen=True)
class Message:
    """Holds the data from a Wayland protocol <request> OR <event> subtree."""

    # The containing Interface, for context.
    interface: 'Interface' = dataclasses.field(repr=False, hash=False)

    # If true, this message is an <event> in the containing interface.
    # Otherwise it is a <request>.
    is_event: bool

    # The value of the name attribute of the <request> or <event>.
    name: str

    # The value of the optional type attribute of the <request> Always empty
    # for <events>.
    request_type: Optional[RequestType]

    # The value of the optional since attribute of the <request> or <event>.
    since: Optional[int]

    # The optional <description> child element of the <request> or <event>.
    description: Optional[Description]

    # The child <arg> elements of the <request> or <event>
    args: Tuple[MessageArg, ...] = dataclasses.field(init=False)

    @staticmethod
    def parse_xml(interface: 'Interface', is_event: bool,
                  e: Element) -> 'Message':
        message = Message(
            interface=interface,
            is_event=is_event,
            name=e.get('name'),
            request_type=e.get('type'),
            since=int(e.get('since')) if e.get('since') else None,
            description=Description.parse_xml(e.find('description')))

        # Note: This is needed to finish up since the instance is frozen.
        object.__setattr__(
            message, 'args',
            tuple(MessageArg.parse_xml(message, c) for c in e.findall('arg')))

        return message


@dataclasses.dataclass(frozen=True)
class EnumEntry:
    """Holds the data from a Wayland protocol <entry> subtree."""

    # The containing Enum, for context.
    enum: 'Enum' = dataclasses.field(repr=False, hash=False)

    # The value of the name attribute of the <entry>.
    name: str

    # The value of the value attribute of the <entry>.
    value: int

    # The value of the optional summary attribute of the <entry>.
    summary: Optional[str]

    # The value of the optional since attribute of the <entry>.
    since: Optional[int]

    # The optional <description> child element of the <request> or <event>.
    description: Optional[Description]

    @staticmethod
    def parse_xml(enum: 'Enum', e: Element) -> 'EnumEntry':
        return EnumEntry(
            enum=enum,
            name=e.get('name'),
            value=int(e.get('value'), 0),
            summary=e.get('summary'),
            since=int(e.get('since'), 0) if e.get('since') else None,
            description=Description.parse_xml(e.find('description')))


@dataclasses.dataclass(frozen=True)
class Enum:
    """Holds the data from a Wayland protocol <enum> subtree."""

    # The containing Interface, for context.
    interface: 'Interface' = dataclasses.field(repr=False, hash=False)

    # The value of the name attribute of the <enum>.
    name: str

    # The value of the optional since attribute of the <enum>.
    since: Optional[int]

    # The value of the optional bitfield attribute of the <enum>.
    bitfield: Optional[bool]

    # The optional <description> child element of the <request> or <event>.
    description: Optional[Description]

    # The child <entry> elements for the <enum>.
    entries: Tuple[EnumEntry, ...] = dataclasses.field(init=False)

    @staticmethod
    def parse_xml(interface: 'Interface', e: Element) -> 'Enum':
        enum = Enum(interface=interface,
                    name=e.get('name'),
                    since=int(e.get('since'), 0) if e.get('since') else None,
                    bitfield=e.get('bitfield') == 'true'
                    if e.get('bitfield') else None,
                    description=Description.parse_xml(e.find('description')))

        # Note: This is needed to finish up since the instance is frozen.
        object.__setattr__(
            enum, 'entries',
            tuple(EnumEntry.parse_xml(enum, c) for c in e.findall('entry')))

        return enum


@dataclasses.dataclass(frozen=True)
class Interface:
    """Holds the data from a Wayland protocol <interface> subtree."""

    # The containing Protocol, for context.
    protocol: 'Protocol' = dataclasses.field(repr=False, hash=False)

    # The value of the name attribute of the <interface>.
    name: str

    # The value of the version attribute of the <interface>.
    version: int

    # The optional <description> child element of the <interface>.
    description: Optional[Description]

    # The child <request> elements for the <interface>.
    requests: Tuple[Message, ...] = dataclasses.field(init=False)

    # The child <event> elements for the <interface>.
    events: Tuple[Message, ...] = dataclasses.field(init=False)

    # The child <enum> elements for the <interface>.
    enums: Tuple[Enum, ...] = dataclasses.field(init=False)

    @staticmethod
    def parse_xml(protocol: 'Protocol', e: Element) -> 'Interface':
        interface = Interface(protocol=protocol,
                              name=e.get('name'),
                              version=int(e.get('version'), 0),
                              description=Description.parse_xml(
                                  e.find('description')))

        # Note: This is needed to finish up since the instance is frozen.
        object.__setattr__(
            interface, 'requests',
            tuple(
                Message.parse_xml(interface, False, c)
                for c in e.findall('request')))
        object.__setattr__(
            interface, 'events',
            tuple(
                Message.parse_xml(interface, True, c)
                for c in e.findall('event')))
        object.__setattr__(
            interface, 'enums',
            tuple(Enum.parse_xml(interface, c) for c in e.findall('enum')))

        return interface


@dataclasses.dataclass(frozen=True)
class Copyright:
    """Holds the data from a Wayland protocol <copyright> subtree."""

    # The text content of the <copyright>.
    text: str

    @staticmethod
    def parse_xml(e: Element) -> Optional['Copyright']:
        return Copyright(text=e.text.strip()) if e is not None else None


@dataclasses.dataclass(frozen=True)
class Protocol:
    """Holds the data from a Wayland protocol <protocol> subtree."""

    # The universe of known protocols this is part of.
    protocols: 'Protocols' = dataclasses.field(repr=False, hash=False)

    # The containing base filename (no path, no extension), for context.
    filename: str

    # The value of the name attribute of the <protocol>.
    name: str

    # The optional <copyright> child element of the <protocol>.
    copyright: Optional[Copyright]

    # The optional <description> child element of the <protocol>.
    description: Optional[Description]

    # The child <interface> elements for the <protocol>.
    interfaces: Tuple[Interface, ...] = dataclasses.field(init=False)

    @staticmethod
    def parse_xml(protocols: 'Protocols', filename: str,
                  e: Element) -> 'Protocol':
        protocol = Protocol(protocols,
                            filename=filename,
                            name=e.get('name'),
                            copyright=Copyright.parse_xml(e.find('copyright')),
                            description=Description.parse_xml(
                                e.find('description')))

        # Note: This is needed to finish up since the instance is frozen.
        object.__setattr__(
            protocol, 'interfaces',
            tuple(
                Interface.parse_xml(protocol, i)
                for i in e.findall('interface')))

        return protocol


@dataclasses.dataclass(frozen=True)
class Protocols:
    """Holds the data from multiple Wayland protocol files."""

    # The parsed protocol dataclasses
    protocols: Tuple[Protocol, ...] = dataclasses.field(init=False)

    @staticmethod
    def parse_xml_files(filenames: Iterable[str]) -> 'Protocols':
        protocols = Protocols()

        # Note: This is needed to finish up since the instance is frozen.
        object.__setattr__(
            protocols, 'protocols',
            tuple(
                Protocol.parse_xml(
                    protocols,
                    os.path.splitext(os.path.basename(filename))[0],
                    xml.etree.ElementTree.parse(filename).getroot())
                for filename in filenames))

        return protocols