#!/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 datetime import datetime 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') # Aconfig flag gating this entry ? d['aconfig_flag'] = entry.get('aconfig_flag') # # 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') # # HAL version from which this entry became a session characteristic ? d['session_characteristics_key_since'] = entry.get('session_characteristics_key_since') # # 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 = {} enum_aconfig_flags = {} 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'] if value.attrs.get('aconfig_flag') is not None: enum_aconfig_flags[value_body] = value['aconfig_flag'] 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_aconfig_flags'] = enum_aconfig_flags 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=None): """ 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 copyright_year = copyright_year \ if copyright_year is not None \ else str(datetime.now().year) metadata_helpers._copyright_year = \ metadata_helpers.infer_copyright_year_from_source(output_name, 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 str(datetime.now().year) parser = MetadataParserXml.create_from_file(file_name) parser.render(template_name, output_name, None, copyright_year) sys.exit(0)