From 975cf8a3b30bf06bde33d1c69f37127eabbb5006 Mon Sep 17 00:00:00 2001 From: Vincent Untz Date: Wed, 14 Apr 2010 17:11:30 -0400 Subject: [PATCH] Add gsettings-schema-convert script It can be used to convert a gconf schema to either a simple gsettings schema format or the full xml gsettings schema format. It also converts from the simple format to the full xml format. --- gio/gsettings-schema-convert | 673 +++++++++++++++++++++++++++++++++++ 1 file changed, 673 insertions(+) create mode 100644 gio/gsettings-schema-convert diff --git a/gio/gsettings-schema-convert b/gio/gsettings-schema-convert new file mode 100644 index 000000000..698cdfbc2 --- /dev/null +++ b/gio/gsettings-schema-convert @@ -0,0 +1,673 @@ +#!/usr/bin/env python +# vim: set ts=4 sw=4 et: coding=UTF-8 +# +# Copyright (c) 2010, Novell, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +# USA. +# +# Authors: Vincent Untz + +import os +import sys + +import optparse + +try: + from lxml import etree as ET +except ImportError: + try: + from xml.etree import cElementTree as ET + except ImportError: + import cElementTree as ET + + +GSETTINGS_SIMPLE_SCHEMA_INDENT = ' ' + + +###################################### + + +class GSettingsSchemaConvertException(Exception): + pass + + +###################################### + + +class GSettingsSchemaRoot: + + def __init__(self): + self.schemas = [] + + def simplify(self): + for schema in self.schemas: + schema.simplify() + + def get_simple_string(self): + need_empty_line = False + result = '' + + for schema in self.schemas: + if need_empty_line: + result += '\n' + result += schema.get_simple_string() + if result: + need_empty_line = True + + return result + + def get_xml_node(self): + schemalist_node = ET.Element('schemalist') + for schema in self.schemas: + schema_node = schema.get_xml_node() + if schema_node is not None: + schemalist_node.append(schema_node) + return schemalist_node + + +###################################### + + +# Note: defined before GSettingsSchema because GSettingsSchema is a subclass. +# But from a schema point of view, GSettingsSchema is a parent of +# GSettingsSchemaDir. +class GSettingsSchemaDir: + + def __init__(self): + self.name = None + self.dirs = [] + self.keys = [] + + def get_simple_string(self, current_indent): + content = self._get_simple_string_for_content(current_indent) + if not content: + return '' + + result = '' + result += '%schild %s:\n' % (current_indent, self.name) + result += content + return result + + def _get_simple_string_for_content(self, current_indent): + need_empty_line = False + result = '' + + for key in self.keys: + result += key.get_simple_string(current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT) + need_empty_line = True + + for dir in self.dirs: + if need_empty_line: + result += '\n' + result += dir.get_simple_string(current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT) + if result: + need_empty_line = True + + return result + + def get_xml_node(self): + node = self._get_xml_node_for_content() + if node is not None: + node.set('id', self.name) + return node + + def _get_xml_node_for_content(self): + if not self.keys and not self.dirs: + return None + + schema_node = ET.Element('schema') + for key in self.keys: + key_node = key.get_xml_node() + schema_node.append(key_node) + for dir in self.dirs: + dir_node = dir.get_xml_node() + schema_node.append(dir_node) + + return schema_node + + +###################################### + + +class GSettingsSchema(GSettingsSchemaDir): + + def __init__(self): + self.id = None + self.path = None + self.dirs = [] + self.keys = [] + + def simplify(self): + while len(self.dirs) == 1 and not self.keys: + dir = self.dirs[0] + self.dirs = dir.dirs + self.keys = dir.keys + if self.path: + self.path += dir.name + '/' + + def get_simple_string(self): + if not self.dirs and not self.keys: + return '' + + result = '' + result += 'schema %s:\n' % self.id + if self.path: + result += '%spath %s\n' % (GSETTINGS_SIMPLE_SCHEMA_INDENT, self.path) + result += '\n' + result += self._get_simple_string_for_content('') + + return result + + def get_xml_node(self): + if not self.dirs and not self.keys: + return None + + node = self._get_xml_node_for_content() + node.set('id', self.id) + if self.path: + node.set('path', self.path) + + return node + + +###################################### + + +class GSettingsSchemaKey: + + def __init__(self): + self.name = None + self.type = None + self.default = None + self.typed_default = None + self.localized = None + self.summary = None + self.description = None + + def fill(self, name, type, default, typed_default, localized, summary, description): + self.name = name + self.type = type + self.default = default + self.typed_default = typed_default + self.localized = localized + self.summary = summary + self.description = description + + def get_simple_string(self, current_indent): + result = '' + result += '%skey %s = %s\n' % (current_indent, self.name, self.typed_default or self.default) + current_indent += GSETTINGS_SIMPLE_SCHEMA_INDENT + if self.summary: + result += '%sSummary: %s\n' % (current_indent, self.summary) + if self.description: + result += '%sDescription: %s\n' % (current_indent, self.description) + return result + + def get_xml_node(self): + key_node = ET.Element('key') + key_node.set('name', self.name) + key_node.set('type', self.type) + default_node = ET.SubElement(key_node, 'default') + default_node.text = self.default + if self.summary: + summary_node = ET.SubElement(key_node, 'summary') + summary_node.text = self.summary + if self.description: + description_node = ET.SubElement(key_node, 'description') + description_node.text = self.description + return key_node + + +###################################### + + +def map_gconf_type_to_variant_type(gconftype, gconfsubtype): + typemap = { 'string': 's', 'int': 'i', 'float': 'f', 'bool': 'b', 'list': 'a' } + result = typemap[gconftype] + if gconftype == 'list': + result = result + typemap[gconfsubtype] + return result + + +class GConfSchema: + + def __init__(self, node): + locale_node = node.find('locale') + + self.key = node.find('key').text + self.type = node.find('type').text + if self.type == 'list': + self.list_type = node.find('list_type').text + else: + self.list_type = None + self.varianttype = map_gconf_type_to_variant_type(self.type, self.list_type) + + applyto_node = node.find('applyto') + if applyto_node is not None: + self.applyto = node.find('applyto').text + self.applyto.strip() + self.keyname = self.applyto[self.applyto.rfind('/')+1:] + self.prefix = self.applyto[:self.applyto.rfind('/')+1] + else: + self.applyto = None + self.key.strip() + self.keyname = self.key[self.key.rfind('/')+1:] + self.prefix = self.key[:self.key.rfind('/')+1] + self.prefix = os.path.normpath(self.prefix) + + try: + self.default = locale_node.find('default').text + self.localized = True + except: + self.default = node.find('default').text + self.localized = False + self.typed_default = None + + self.short = self._get_value_with_locale(node, locale_node, 'short') + self.long = self._get_value_with_locale(node, locale_node, 'long') + + self.short = self._oneline(self.short) + self.long = self._oneline(self.long) + + # Fix the default to be parsable by GVariant + if self.type == 'string': + if not self.default: + self.default = '\'\'' + else: + self.default.replace('\'', '\\\'') + self.default = '\'%s\'' % self.default + elif self.type == 'bool': + self.default = self.default.lower() + elif self.type == 'list': + l = self.default.strip() + if not (l[0] == '[' and l[-1] == ']'): + raise GSettingsSchemaConvertException('Cannot parse default value for list: %s' % self.default) + values = l[1:-1].strip() + if not values: + self.typed_default = '@%s []' % self.varianttype + elif self.list_type == 'string': + items = [ item.strip() for item in values.split(',') ] + items = [ item.replace('\'', '\\\'') for item in items ] + values = ', '.join([ '\'%s\'' % item for item in items ]) + self.default = '[ %s ]' % values + + # FIXME: kill this when we'll have python bindings for GVariant. Right + # now, every simple format schema we'll generate has to have an + # explicit type since we can't guess the type later on when converting + # to XML. + if not self.typed_default: + self.typed_default = '@%s %s' % (self.varianttype, self.default) + + def _get_value_with_locale(self, node, locale_node, element): + element_node = None + if locale_node is not None: + element_node = locale_node.find(element) + if element_node is None: + element_node = node.find(element) + if element_node is not None: + return element_node.text + else: + return None + + def _oneline(self, s): + lines = s.splitlines() + result = '' + for line in lines: + result += ' ' + line.lstrip() + return result.strip() + + def get_gsettings_schema_key(self): + key = GSettingsSchemaKey() + key.fill(self.keyname, self.varianttype, self.default, self.typed_default, self.localized, self.short, self.long) + return key + + +###################################### + +allowed_tokens = { + '' : [ 'schema' ], + 'schema' : [ 'path', 'child', 'key' ], + 'path' : [ ], + 'child' : [ 'child', 'key' ], + 'key' : [ 'summary', 'description' ], + 'summary' : [ ], + 'description' : [ ] +} + +def _eat_indent(line, indent_stack): + i = 0 + buf = '' + previous_max_index = len(indent_stack) - 1 + index = -1 + + while i < len(line) - 1 and line[i].isspace(): + buf += line[i] + i += 1 + if previous_max_index > index: + if buf == indent_stack[index + 1]: + buf = '' + index += 1 + continue + elif indent_stack[index + 1].startswith(buf): + continue + else: + raise GSettingsSchemaConvertException('Indentation not consistent.') + else: + continue + + if buf and previous_max_index <= index: + indent_stack.append(buf) + elif previous_max_index > index: + indent_stack = indent_stack[:index + 1] + + return (indent_stack, line[i:]) + +def _eat_word(line): + i = 0 + while i < len(line) - 1 and not line[i].isspace(): + i += 1 + return (line[:i], line[i:]) + +def _word_to_token(word): + if word == 'schema': + return 'schema' + if word == 'path': + return 'path' + if word == 'child': + return 'child' + if word == 'key': + return 'key' + if word == 'Summary:': + return 'summary' + if word == 'Description:': + return 'description' + raise GSettingsSchemaConvertException('\'%s\' is not a valid token.' % word) + +def _get_name_without_colon(line): + if line[-1] != ':': + raise GSettingsSchemaConvertException('\'%s\' has no trailing colon.' % line) + line = line[:-1].strip() + # FIXME: we could check there's no space + return line + +def _parse_key(line): + items = line.split('=') + if len(items) != 2: + raise GSettingsSchemaConvertException('Cannot parse key \'%s\'.' % line) + name = items[0].strip() + type = '' + value = items[1].strip() + if value[0] == '@': + i = 1 + while not value[i].isspace(): + i += 1 + type = value[1:i] + value = value[i:].strip() + if not value: + raise GSettingsSchemaConvertException('No value specified for key \'%s\'.' % line) + return (name, type, value) + +def read_simple_schema(simple_schema_file): + root = GSettingsSchemaRoot() + + f = open(simple_schema_file, 'r') + lines = f.readlines() + f.close() + lines = [ line[:-1] for line in lines ] + + leading_indent = None + indent_stack = [] + token_stack = [] + object_stack = [ root ] + + for line in lines: + # make sure that lines with only spaces are ignored and considered as + # empty lines + line = line.rstrip() + + # ignore empty line + if not line: + continue + + # look at the indentation to know where we should be + (indent_stack, line) = _eat_indent(line, indent_stack) + if leading_indent is None: + leading_indent = len(indent_stack) + + # ignore comments + if line[0] == '#': + continue + + (word, line) = _eat_word(line) + token = _word_to_token(word) + line = line.lstrip() + + new_level = len(indent_stack) - leading_indent + old_level = len(token_stack) + + if new_level > old_level + 1: + raise GSettingsSchemaConvertException('Internal error: stacks not in sync.') + elif new_level <= old_level: + token_stack = token_stack[:new_level] + # we always have the root + object_stack = object_stack[:new_level + 1] + + if new_level == 0: + parent_token = '' + else: + parent_token = token_stack[-1] + + if not token in allowed_tokens[parent_token]: + raise GSettingsSchemaConvertException('Token \'%s\' not allowed after token \'%s\'.' % (token, parent_token)) + + current_object = object_stack[-1] + + new_object = None + if token == 'schema': + name = _get_name_without_colon(line) + new_object = GSettingsSchema() + new_object.id = name + current_object.schemas.append(new_object) + elif token == 'path': + current_object.path = line + elif token == 'child': + name = _get_name_without_colon(line) + new_object = GSettingsSchemaDir() + new_object.name = name + current_object.dirs.append(new_object) + elif token == 'key': + new_object = GSettingsSchemaKey() + (name, type, value) = _parse_key(line) + new_object.name = name + new_object.type = type + new_object.default = value + current_object.keys.append(new_object) + elif token == 'summary': + current_object.summary = line + elif token == 'description': + current_object.description = line + + if new_object: + token_stack.append(token) + object_stack.append(new_object) + + return root + + +###################################### + + +def read_gconf_schema(gconf_schema_file): + gsettings_schema_root = GSettingsSchemaRoot() + + gconfschemafile_node = ET.parse(gconf_schema_file).getroot() + for schemalist_node in gconfschemafile_node.findall('schemalist'): + for schema_node in schemalist_node.findall('schema'): + gconf_schema = GConfSchema(schema_node) + + schemas_only = (gconf_schema.applyto is not None) + + dirpath = gconf_schema.prefix + # remove leading slash because there's none in gsettings, and trailing + # slash because we'll split the string + if dirpath[0] == '/': + dirpath = dirpath[1:] + if dirpath[-1] == '/': + dirpath = dirpath[:-1] + # remove leading 'schemas/' for schemas-only keys + if schemas_only and dirpath.startswith('schemas/'): + dirpath = dirpath[len('schemas/'):] + + if not dirpath: + raise GSettingsSchemaConvertException('Toplevel keys are not accepted: %s' % gconf_schema.applyto) + + hierarchy = dirpath.split('/') + + # we don't want to put apps/ and desktop/ keys in the same schema, + # so we have a first step where we make sure to create a new schema + # to avoid this case if necessary + gsettings_schema = None + for schema in gsettings_schema_root.schemas: + if schemas_only: + schema_path = schema.path + else: + schema_path = schema._hacky_path + if dirpath.startswith(schema_path): + gsettings_schema = schema + break + if not gsettings_schema: + gsettings_schema = GSettingsSchema() + gsettings_schema.id = 'FIXME' + if schemas_only: + gsettings_schema.path = hierarchy[0] + '/' + else: + gsettings_schema._hacky_path = hierarchy[0] + '/' + gsettings_schema_root.schemas.append(gsettings_schema) + + # we create all the subdirs that lead to this key + gsettings_dir = gsettings_schema + for item in hierarchy[1:]: + subdir = None + for dir in gsettings_dir.dirs: + if dir.name == item: + subdir = dir + break + if not subdir: + subdir = GSettingsSchemaDir() + subdir.name = item + gsettings_dir.dirs.append(subdir) + gsettings_dir = subdir + + # we have the final directory, so we can put the key there + gsettings_dir.keys.append(gconf_schema.get_gsettings_schema_key()) + + gsettings_schema_root.simplify() + + return gsettings_schema_root + + +###################################### + + +def main(args): + parser = optparse.OptionParser() + + parser.add_option("-o", "--output", dest="output", + help="output file") + parser.add_option("-g", "--gconf", action="store_true", dest="gconf", + default=False, help="convert a gconf schema file") + parser.add_option("-s", "--simple", action="store_true", dest="simple", + default=False, help="use the simple schema format as output (only for gconf schema conversion)") + parser.add_option("-x", "--xml", action="store_true", dest="xml", + default=False, help="use the xml schema format as output") + parser.add_option("-f", "--force", action="store_true", dest="force", + default=False, help="overwrite output file if already existing") + + (options, args) = parser.parse_args() + + if len(args) < 1: + print >> sys.stderr, 'Need a filename to work on.' + return 1 + elif len(args) > 1: + print >> sys.stderr, 'Too many arguments.' + return 1 + + if options.simple and options.xml: + print >> sys.stderr, 'Too many output formats requested.' + return 1 + if not options.simple and not options.xml: + if options.gconf: + options.simple = True + else: + options.xml = True + + argfile = os.path.expanduser(args[0]) + if not os.path.exists(argfile): + print >> sys.stderr, '%s does not exist.' % argfile + return 1 + + if options.output: + options.output = os.path.expanduser(options.output) + + try: + if options.output and not options.force and os.path.exists(options.output): + raise GSettingsSchemaConvertException('%s already exists.' % options.output) + + if options.gconf: + try: + schema_root = read_gconf_schema(argfile) + except SyntaxError, e: + raise GSettingsSchemaConvertException('%s does not look like a gconf schema file: %s' % (argfile, e)) + else: + schema_root = read_simple_schema(argfile) + + if options.xml: + node = schema_root.get_xml_node() + tree = ET.ElementTree(node) + try: + output = ET.tostring(tree, pretty_print = True) + except TypeError: + # pretty_print only works with lxml + output = ET.tostring(tree) + else: + output = schema_root.get_simple_string() + + if not options.output: + sys.stdout.write(output) + else: + try: + fout = open(options.output, 'w') + fout.write(output) + fout.close() + except GSettingsSchemaConvertException, e: + fout.close() + if os.path.exists(options.output): + os.unlink(options.output) + raise e + + except GSettingsSchemaConvertException, e: + print >> sys.stderr, '%s' % e + return 1 + + return 0 + + +if __name__ == '__main__': + try: + res = main(sys.argv) + sys.exit(res) + except KeyboardInterrupt: + pass