#!/usr/bin/env python3 # # Copyright (C) 2012 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """ A parser for metadata_definitions.xml can also render the resulting model over a Mako template. Usage: metadata_parser_xml.py [] - outputs the resulting template to output_file (stdout if none specified) Module: The parser is also available as a module import (MetadataParserXml) to use in other modules. Dependencies: BeautifulSoup - an HTML/XML parser available to download from http://www.crummy.com/software/BeautifulSoup/ Mako - a template engine for Python, available to download from http://www.makotemplates.org/ """ import sys import os from bs4 import BeautifulSoup from bs4 import NavigableString from io import StringIO from mako.template import Template from mako.lookup import TemplateLookup from mako.runtime import Context from metadata_model import * import metadata_model from metadata_validate import * import metadata_helpers class MetadataParserXml: """ A class to parse any XML block that passes validation with metadata-validate. It builds a metadata_model.Metadata graph and then renders it over a Mako template. Attributes (Read-Only): soup: an instance of BeautifulSoup corresponding to the XML contents metadata: a constructed instance of metadata_model.Metadata """ def __init__(self, xml, file_name): """ Construct a new MetadataParserXml, immediately try to parse it into a metadata model. Args: xml: The XML block to use for the metadata file_name: Source of the XML block, only for debugging/errors Raises: ValueError: if the XML block failed to pass metadata_validate.py """ self._soup = validate_xml(xml) if self._soup is None: raise ValueError("%s has an invalid XML file" % (file_name)) self._metadata = Metadata() self._parse() self._metadata.construct_graph() @staticmethod def create_from_file(file_name): """ Construct a new MetadataParserXml by loading and parsing an XML file. Args: file_name: Name of the XML file to load and parse. Raises: ValueError: if the XML file failed to pass metadata_validate.py Returns: MetadataParserXml instance representing the XML file. """ return MetadataParserXml(open(file_name).read(), file_name) @property def soup(self): return self._soup @property def metadata(self): return self._metadata @staticmethod def _find_direct_strings(element): if element.string is not None: return [element.string] return [i for i in element.contents if isinstance(i, NavigableString)] @staticmethod def _strings_no_nl(element): return "".join([i.strip() for i in MetadataParserXml._find_direct_strings(element)]) def _parse(self): tags = self.soup.tags if tags is not None: for tag in tags.find_all('tag'): self.metadata.insert_tag(tag['id'], tag.string) types = self.soup.types if types is not None: for tp in types.find_all('typedef'): languages = {} for lang in tp.find_all('language'): languages[lang['name']] = lang.string self.metadata.insert_type(tp['name'], 'typedef', languages=languages) # add all entries, preserving the ordering of the XML file # this is important for future ABI compatibility when generating code entry_filter = lambda x: x.name == 'entry' or x.name == 'clone' for entry in self.soup.find_all(entry_filter): if entry.name == 'entry': d = { 'name': fully_qualified_name(entry), 'type': entry['type'], 'kind': find_kind(entry), 'type_notes': entry.attrs.get('type_notes') } d2 = self._parse_entry(entry) insert = self.metadata.insert_entry else: d = { 'name': entry['entry'], 'kind': find_kind(entry), 'target_kind': entry['kind'], # no type since its the same # no type_notes since its the same } d2 = {} if 'hal_version' in entry.attrs: d2['hal_version'] = entry['hal_version'] insert = self.metadata.insert_clone d3 = self._parse_entry_optional(entry) entry_dict = {**d, **d2, **d3} insert(entry_dict) self.metadata.construct_graph() def _parse_entry(self, entry): d = {} # # Visibility # d['visibility'] = entry.get('visibility') # # Synthetic ? # d['synthetic'] = entry.get('synthetic') == 'true' # # Permission needed ? # d['permission_needed'] = entry.get('permission_needed') # # Hardware Level (one of limited, legacy, full) # d['hwlevel'] = entry.get('hwlevel') # # Deprecated ? # d['deprecated'] = entry.get('deprecated') == 'true' # # Optional for non-full hardware level devices # d['optional'] = entry.get('optional') == 'true' # # Typedef # d['type_name'] = entry.get('typedef') # # Initial HIDL HAL version the entry was added in d['hal_version'] = entry.get('hal_version') # # Enum # if entry.get('enum', 'false') == 'true': enum_values = [] enum_deprecateds = [] enum_optionals = [] enum_visibilities = {} enum_notes = {} enum_sdk_notes = {} enum_ndk_notes = {} enum_ids = {} enum_hal_versions = {} for value in entry.enum.find_all('value'): value_body = self._strings_no_nl(value) enum_values.append(value_body) if value.attrs.get('deprecated', 'false') == 'true': enum_deprecateds.append(value_body) if value.attrs.get('optional', 'false') == 'true': enum_optionals.append(value_body) visibility = value.attrs.get('visibility') if visibility is not None: enum_visibilities[value_body] = visibility notes = value.find('notes') if notes is not None: enum_notes[value_body] = notes.string sdk_notes = value.find('sdk_notes') if sdk_notes is not None: enum_sdk_notes[value_body] = sdk_notes.string ndk_notes = value.find('ndk_notes') if ndk_notes is not None: enum_ndk_notes[value_body] = ndk_notes.string if value.attrs.get('id') is not None: enum_ids[value_body] = value['id'] if value.attrs.get('hal_version') is not None: enum_hal_versions[value_body] = value['hal_version'] d['enum_values'] = enum_values d['enum_deprecateds'] = enum_deprecateds d['enum_optionals'] = enum_optionals d['enum_visibilities'] = enum_visibilities d['enum_notes'] = enum_notes d['enum_sdk_notes'] = enum_sdk_notes d['enum_ndk_notes'] = enum_ndk_notes d['enum_ids'] = enum_ids d['enum_hal_versions'] = enum_hal_versions d['enum'] = True # # Container (Array/Tuple) # if entry.attrs.get('container') is not None: container_name = entry['container'] array = entry.find('array') if array is not None: array_sizes = [] for size in array.find_all('size'): array_sizes.append(size.string) d['container_sizes'] = array_sizes tupl = entry.find('tuple') if tupl is not None: tupl_values = [] for val in tupl.find_all('value'): tupl_values.append(val.name) d['tuple_values'] = tupl_values d['container_sizes'] = len(tupl_values) d['container'] = container_name return d def _parse_entry_optional(self, entry): d = {} optional_elements = ['description', 'range', 'units', 'details', 'hal_details', 'ndk_details',\ 'deprecation_description'] for i in optional_elements: prop = find_child_tag(entry, i) if prop is not None: d[i] = prop.string tag_ids = [] for tag in entry.find_all('tag'): tag_ids.append(tag['id']) d['tag_ids'] = tag_ids return d def render(self, template, output_name=None, enum=None, copyright_year="2022"): """ Render the metadata model using a Mako template as the view. The template gets the metadata as an argument, as well as all public attributes from the metadata_helpers module. The output file is encoded with UTF-8. Args: template: path to a Mako template file output_name: path to the output file, or None to use stdout enum: The name of the enum, if any copyright_year: the year in the copyright section of output file """ buf = StringIO() metadata_helpers._context_buf = buf metadata_helpers._enum = enum metadata_helpers._copyright_year = copyright_year helpers = [(i, getattr(metadata_helpers, i)) for i in dir(metadata_helpers) if not i.startswith('_')] helpers = dict(helpers) lookup = TemplateLookup(directories=[os.getcwd()]) tpl = Template(filename=template, lookup=lookup) ctx = Context(buf, metadata=self.metadata, **helpers) tpl.render_context(ctx) tpl_data = buf.getvalue() metadata_helpers._context_buf = None buf.close() if output_name is None: print(tpl_data) else: open(output_name, "w").write(tpl_data) ##################### ##################### if __name__ == "__main__": if len(sys.argv) <= 2: print("Usage: %s []"\ " []" \ % (sys.argv[0]), file=sys.stderr) sys.exit(0) file_name = sys.argv[1] template_name = sys.argv[2] output_name = sys.argv[3] if len(sys.argv) > 3 else None copyright_year = sys.argv[4] if len(sys.argv) > 4 else "2022" parser = MetadataParserXml.create_from_file(file_name) parser.render(template_name, output_name, None, copyright_year) sys.exit(0)