gsettings-schema-convert 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. #!/usr/bin/env python
  2. # vim: set ts=4 sw=4 et: coding=UTF-8
  3. #
  4. # Copyright (c) 2010, Novell, Inc.
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public License
  8. # as published by the Free Software Foundation; either version 2
  9. # of the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
  19. # USA.
  20. #
  21. # Authors: Vincent Untz <vuntz@gnome.org>
  22. # TODO: add alias support for choices
  23. # choices: 'this-is-an-alias' = 'real', 'other', 'real'
  24. # TODO: we don't support migrating a pair from a gconf schema. It has yet to be
  25. # seen in real-world usage, though.
  26. import os
  27. import sys
  28. import optparse
  29. try:
  30. from lxml import etree as ET
  31. except ImportError:
  32. try:
  33. from xml.etree import cElementTree as ET
  34. except ImportError:
  35. import cElementTree as ET
  36. GSETTINGS_SIMPLE_SCHEMA_INDENT = ' '
  37. TYPES_FOR_CHOICES = [ 's' ]
  38. TYPES_FOR_RANGE = [ 'y', 'n', 'q', 'i', 'u', 'x', 't', 'h', 'd' ]
  39. ######################################
  40. def is_schema_id_valid(id):
  41. # FIXME: there's currently no restriction on what an id should contain,
  42. # but there might be some later on
  43. return True
  44. def is_key_name_valid(name):
  45. # FIXME: we could check that name is valid ([-a-z0-9], no leading/trailing
  46. # -, no leading digit, 32 char max). Note that we don't want to validate
  47. # the key when converting from gconf, though, since gconf keys use
  48. # underscores.
  49. return True
  50. def are_choices_valid(choices):
  51. # FIXME: we could check that all values have the same type with GVariant
  52. return True
  53. def is_range_valid(minmax):
  54. # FIXME: we'll be able to easily check min < max once we can convert the
  55. # values with GVariant
  56. return True
  57. ######################################
  58. class GSettingsSchemaConvertException(Exception):
  59. pass
  60. ######################################
  61. class GSettingsSchemaRoot:
  62. def __init__(self):
  63. self.gettext_domain = None
  64. self.schemas = []
  65. def get_simple_string(self):
  66. need_empty_line = False
  67. result = ''
  68. for schema in self.schemas:
  69. if need_empty_line:
  70. result += '\n'
  71. result += schema.get_simple_string()
  72. if result:
  73. need_empty_line = True
  74. # Only put the gettext domain if we have some content
  75. if result and self.gettext_domain:
  76. result = 'gettext-domain: %s\n\n%s' % (self.gettext_domain, result)
  77. return result
  78. def get_xml_node(self):
  79. schemalist_node = ET.Element('schemalist')
  80. if self.gettext_domain:
  81. schemalist_node.set('gettext-domain', self.gettext_domain)
  82. for schema in self.schemas:
  83. for schema_node in schema.get_xml_nodes():
  84. schemalist_node.append(schema_node)
  85. return schemalist_node
  86. ######################################
  87. class GSettingsSchema:
  88. def __init__(self):
  89. self.id = None
  90. self.path = None
  91. # only set when this schema is a child
  92. self.name = None
  93. self.gettext_domain = None
  94. self.children = []
  95. self.keys = []
  96. def get_simple_string(self, current_indent = '', parent_path = ''):
  97. if not self.children and not self.keys:
  98. return ''
  99. content = self._get_simple_string_for_content(current_indent)
  100. if not content:
  101. return ''
  102. if self.name:
  103. id = 'child %s' % self.name
  104. force_empty_line = False
  105. else:
  106. id = 'schema %s' % self.id
  107. force_empty_line = True
  108. result = ''
  109. result += '%s%s:\n' % (current_indent, id)
  110. result += self._get_simple_string_for_attributes(current_indent, parent_path, force_empty_line)
  111. result += content
  112. return result
  113. def _get_simple_string_for_attributes(self, current_indent, parent_path, force_empty_line):
  114. need_empty_line = force_empty_line
  115. result = ''
  116. if self.gettext_domain:
  117. result += '%sgettext-domain: %s\n' % (current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT, self.gettext_domain)
  118. need_empty_line = True
  119. if self.path and (not parent_path or (self.path != '%s%s/' % (parent_path, self.name))):
  120. result += '%spath: %s\n' % (current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT, self.path)
  121. need_empty_line = True
  122. if need_empty_line:
  123. result += '\n'
  124. return result
  125. def _get_simple_string_for_content(self, current_indent):
  126. need_empty_line = False
  127. result = ''
  128. for key in self.keys:
  129. result += key.get_simple_string(current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT)
  130. need_empty_line = True
  131. for child in self.children:
  132. if need_empty_line:
  133. result += '\n'
  134. result += child.get_simple_string(current_indent + GSETTINGS_SIMPLE_SCHEMA_INDENT, self.path)
  135. if result:
  136. need_empty_line = True
  137. return result
  138. def get_xml_nodes(self):
  139. if not self.children and not self.keys:
  140. return []
  141. (node, children_nodes) = self._get_xml_nodes_for_content()
  142. if node is None:
  143. return []
  144. node.set('id', self.id)
  145. if self.path:
  146. node.set('path', self.path)
  147. nodes = [ node ]
  148. nodes.extend(children_nodes)
  149. return nodes
  150. def _get_xml_nodes_for_content(self):
  151. if not self.keys and not self.children:
  152. return (None, None)
  153. children_nodes = []
  154. schema_node = ET.Element('schema')
  155. if self.gettext_domain:
  156. schema_node.set('gettext-domain', self.gettext_domain)
  157. for key in self.keys:
  158. key_node = key.get_xml_node()
  159. schema_node.append(key_node)
  160. for child in self.children:
  161. child_nodes = child.get_xml_nodes()
  162. children_nodes.extend(child_nodes)
  163. child_node = ET.SubElement(schema_node, 'child')
  164. if not child.name:
  165. raise GSettingsSchemaConvertException('Internal error: child being processed with no schema id.')
  166. child_node.set('name', child.name)
  167. child_node.set('schema', '%s' % child.id)
  168. return (schema_node, children_nodes)
  169. ######################################
  170. class GSettingsSchemaKey:
  171. def __init__(self):
  172. self.name = None
  173. self.type = None
  174. self.default = None
  175. self.typed_default = None
  176. self.l10n = None
  177. self.l10n_context = None
  178. self.summary = None
  179. self.description = None
  180. self.choices = None
  181. self.range = None
  182. def fill(self, name, type, default, typed_default, l10n, l10n_context, summary, description, choices, range):
  183. self.name = name
  184. self.type = type
  185. self.default = default
  186. self.typed_default = typed_default
  187. self.l10n = l10n
  188. self.l10n_context = l10n_context
  189. self.summary = summary
  190. self.description = description
  191. self.choices = choices
  192. self.range = range
  193. def _has_range_choices(self):
  194. return self.choices is not None and self.type in TYPES_FOR_CHOICES
  195. def _has_range_minmax(self):
  196. return self.range is not None and len(self.range) == 2 and self.type in TYPES_FOR_RANGE
  197. def get_simple_string(self, current_indent):
  198. # FIXME: kill this when we'll have python bindings for GVariant. Right
  199. # now, every simple format schema we'll generate has to have an
  200. # explicit type since we can't guess the type later on when converting
  201. # to XML.
  202. self.typed_default = '@%s %s' % (self.type, self.default)
  203. result = ''
  204. result += '%skey %s = %s\n' % (current_indent, self.name, self.typed_default or self.default)
  205. current_indent += GSETTINGS_SIMPLE_SCHEMA_INDENT
  206. if self.l10n:
  207. l10n = self.l10n
  208. if self.l10n_context:
  209. l10n += ' %s' % self.l10n_context
  210. result += '%sl10n: %s\n' % (current_indent, l10n)
  211. if self.summary:
  212. result += '%ssummary: %s\n' % (current_indent, self.summary)
  213. if self.description:
  214. result += '%sdescription: %s\n' % (current_indent, self.description)
  215. if self._has_range_choices():
  216. result += '%schoices: %s\n' % (current_indent, ', '.join(self.choices))
  217. elif self._has_range_minmax():
  218. result += '%srange: %s\n' % (current_indent, '%s..%s' % (self.range[0] or '', self.range[1] or ''))
  219. return result
  220. def get_xml_node(self):
  221. key_node = ET.Element('key')
  222. key_node.set('name', self.name)
  223. key_node.set('type', self.type)
  224. default_node = ET.SubElement(key_node, 'default')
  225. default_node.text = self.default
  226. if self.l10n:
  227. default_node.set('l10n', self.l10n)
  228. if self.l10n_context:
  229. default_node.set('context', self.l10n_context)
  230. if self.summary:
  231. summary_node = ET.SubElement(key_node, 'summary')
  232. summary_node.text = self.summary
  233. if self.description:
  234. description_node = ET.SubElement(key_node, 'description')
  235. description_node.text = self.description
  236. if self._has_range_choices():
  237. choices_node = ET.SubElement(key_node, 'choices')
  238. for choice in self.choices:
  239. choice_node = ET.SubElement(choices_node, 'choice')
  240. choice_node.set('value', choice)
  241. elif self._has_range_minmax():
  242. (min, max) = self.range
  243. range_node = ET.SubElement(key_node, 'range')
  244. min_node = ET.SubElement(range_node, 'min')
  245. if min:
  246. min_node.text = min
  247. max_node = ET.SubElement(range_node, 'max')
  248. if max:
  249. max_node.text = max
  250. return key_node
  251. ######################################
  252. class SimpleSchemaParser:
  253. allowed_tokens = {
  254. '' : [ 'gettext-domain', 'schema' ],
  255. 'gettext-domain' : [ ],
  256. 'schema' : [ 'gettext-domain', 'path', 'child', 'key' ],
  257. 'path' : [ ],
  258. 'child' : [ 'gettext-domain', 'child', 'key' ],
  259. 'key' : [ 'l10n', 'summary', 'description', 'choices', 'range' ],
  260. 'l10n' : [ ],
  261. 'summary' : [ ],
  262. 'description' : [ ],
  263. 'choices' : [ ],
  264. 'range' : [ ]
  265. }
  266. allowed_separators = [ ':', '=' ]
  267. def __init__(self, file):
  268. self.file = file
  269. self.root = GSettingsSchemaRoot()
  270. # this is just a convenient helper to remove the leading indentation
  271. # that should be common to all lines
  272. self.leading_indent = None
  273. self.indent_stack = []
  274. self.token_stack = []
  275. self.object_stack = [ self.root ]
  276. self.previous_token = None
  277. self.current_token = None
  278. self.unparsed_line = ''
  279. def _eat_indent(self):
  280. line = self.unparsed_line
  281. i = 0
  282. buf = ''
  283. previous_max_index = len(self.indent_stack) - 1
  284. index = -1
  285. while i < len(line) - 1 and line[i].isspace():
  286. buf += line[i]
  287. i += 1
  288. if previous_max_index > index:
  289. if buf == self.indent_stack[index + 1]:
  290. buf = ''
  291. index += 1
  292. continue
  293. elif self.indent_stack[index + 1].startswith(buf):
  294. continue
  295. else:
  296. raise GSettingsSchemaConvertException('Inconsistent indentation.')
  297. else:
  298. continue
  299. if buf and previous_max_index > index:
  300. raise GSettingsSchemaConvertException('Inconsistent indentation.')
  301. elif buf and previous_max_index <= index:
  302. self.indent_stack.append(buf)
  303. elif previous_max_index > index:
  304. self.indent_stack = self.indent_stack[:index + 1]
  305. self.unparsed_line = line[i:]
  306. def _parse_word(self):
  307. line = self.unparsed_line
  308. i = 0
  309. while i < len(line) and not line[i].isspace() and not line[i] in self.allowed_separators:
  310. i += 1
  311. self.unparsed_line = line[i:]
  312. return line[:i]
  313. def _word_to_token(self, word):
  314. lower = word.lower()
  315. if lower and lower in self.allowed_tokens.keys():
  316. return lower
  317. raise GSettingsSchemaConvertException('\'%s\' is not a valid token.' % lower)
  318. def _token_allow_separator(self):
  319. return self.current_token in [ 'gettext-domain', 'path', 'l10n', 'summary', 'description', 'choices', 'range' ]
  320. def _parse_id_without_separator(self):
  321. line = self.unparsed_line
  322. if line[-1] in self.allowed_separators:
  323. line = line[:-1].strip()
  324. if not is_schema_id_valid(line):
  325. raise GSettingsSchemaConvertException('\'%s\' is not a valid schema id.' % line)
  326. self.unparsed_line = ''
  327. return line
  328. def _parse_key(self):
  329. line = self.unparsed_line
  330. split = False
  331. for separator in self.allowed_separators:
  332. items = line.split(separator)
  333. if len(items) == 2:
  334. split = True
  335. break
  336. if not split:
  337. raise GSettingsSchemaConvertException('Key \'%s\' cannot be parsed.' % line)
  338. name = items[0].strip()
  339. if not is_key_name_valid(name):
  340. raise GSettingsSchemaConvertException('\'%s\' is not a valid key name.' % name)
  341. type = ''
  342. value = items[1].strip()
  343. if value[0] == '@':
  344. i = 1
  345. while not value[i].isspace():
  346. i += 1
  347. type = value[1:i]
  348. value = value[i:].strip()
  349. if not value:
  350. raise GSettingsSchemaConvertException('No value specified for key \'%s\' (\'%s\').' % (name, line))
  351. self.unparsed_line = ''
  352. object = GSettingsSchemaKey()
  353. object.name = name
  354. object.type = type
  355. object.default = value
  356. return object
  357. def _parse_l10n(self):
  358. line = self.unparsed_line
  359. items = [ item.strip() for item in line.split(' ', 1) if item.strip() ]
  360. if not items:
  361. self.unparsed_line = ''
  362. return (None, None)
  363. if len(items) == 1:
  364. self.unparsed_line = ''
  365. return (items[0], None)
  366. if len(items) == 2:
  367. self.unparsed_line = ''
  368. return (items[0], items[1])
  369. raise GSettingsSchemaConvertException('Internal error: more items than expected for localization \'%s\'.' % line)
  370. def _parse_choices(self, object):
  371. if object.type not in TYPES_FOR_CHOICES:
  372. raise GSettingsSchemaConvertException('Key \'%s\' of type \'%s\' cannot have choices.' % (object.name, object.type))
  373. line = self.unparsed_line
  374. choices = [ item.strip() for item in line.split(',') ]
  375. if not are_choices_valid(choices):
  376. raise GSettingsSchemaConvertException('\'%s\' is not a valid choice.' % line)
  377. self.unparsed_line = ''
  378. return choices
  379. def _parse_range(self, object):
  380. if object.type not in TYPES_FOR_RANGE:
  381. raise GSettingsSchemaConvertException('Key \'%s\' of type \'%s\' cannot have a range.' % (object.name, object.type))
  382. line = self.unparsed_line
  383. minmax = [ item.strip() for item in line.split('..') ]
  384. if len(minmax) != 2:
  385. raise GSettingsSchemaConvertException('Range \'%s\' cannot be parsed.' % line)
  386. if not is_range_valid(minmax):
  387. raise GSettingsSchemaConvertException('\'%s\' is not a valid range.' % line)
  388. self.unparsed_line = ''
  389. return tuple(minmax)
  390. def parse_line(self, line):
  391. # make sure that lines with only spaces are ignored and considered as
  392. # empty lines
  393. self.unparsed_line = line.rstrip()
  394. # ignore empty line
  395. if not self.unparsed_line:
  396. return
  397. # look at the indentation to know where we should be
  398. self._eat_indent()
  399. if self.leading_indent is None:
  400. self.leading_indent = len(self.indent_stack)
  401. # ignore comments
  402. if self.unparsed_line[0] == '#':
  403. return
  404. word = self._parse_word()
  405. if self.current_token:
  406. self.previous_token = self.current_token
  407. self.current_token = self._word_to_token(word)
  408. self.unparsed_line = self.unparsed_line.lstrip()
  409. allow_separator = self._token_allow_separator()
  410. if len(self.unparsed_line) > 0 and self.unparsed_line[0] in self.allowed_separators:
  411. if allow_separator:
  412. self.unparsed_line = self.unparsed_line[1:].lstrip()
  413. else:
  414. raise GSettingsSchemaConvertException('Separator \'%s\' is not allowed after \'%s\'.' % (self.unparsed_line[0], self.current_token))
  415. new_level = len(self.indent_stack) - self.leading_indent
  416. old_level = len(self.token_stack)
  417. if new_level > old_level + 1:
  418. raise GSettingsSchemaConvertException('Internal error: stacks not in sync.')
  419. elif new_level <= old_level:
  420. self.token_stack = self.token_stack[:new_level]
  421. # we always have the root
  422. self.object_stack = self.object_stack[:new_level + 1]
  423. if new_level == 0:
  424. parent_token = ''
  425. else:
  426. parent_token = self.token_stack[-1]
  427. # there's new indentation, but no token is allowed under the previous
  428. # one
  429. if new_level == old_level + 1 and self.previous_token != parent_token:
  430. raise GSettingsSchemaConvertException('\'%s\' is not allowed under \'%s\'.' % (self.current_token, self.previous_token))
  431. if not self.current_token in self.allowed_tokens[parent_token]:
  432. if parent_token:
  433. error = '\'%s\' is not allowed under \'%s\'.' % (self.current_token, parent_token)
  434. else:
  435. error = '\'%s\' is not allowed at the root level.' % self.current_token
  436. raise GSettingsSchemaConvertException(error)
  437. current_object = self.object_stack[-1]
  438. new_object = None
  439. if self.current_token == 'gettext-domain':
  440. current_object.gettext_domain = self.unparsed_line
  441. elif self.current_token == 'schema':
  442. name = self._parse_id_without_separator()
  443. new_object = GSettingsSchema()
  444. new_object.id = name
  445. current_object.schemas.append(new_object)
  446. elif self.current_token == 'path':
  447. current_object.path = self.unparsed_line
  448. elif self.current_token == 'child':
  449. if not isinstance(current_object, GSettingsSchema):
  450. raise GSettingsSchemaConvertException('Internal error: child being processed with no parent schema.')
  451. name = self._parse_id_without_separator()
  452. new_object = GSettingsSchema()
  453. new_object.id = '%s.%s' % (current_object.id, name)
  454. if current_object.path:
  455. new_object.path = '%s%s/' % (current_object.path, name)
  456. new_object.name = name
  457. current_object.children.append(new_object)
  458. elif self.current_token == 'key':
  459. new_object = self._parse_key()
  460. current_object.keys.append(new_object)
  461. elif self.current_token == 'l10n':
  462. (current_object.l10n, current_object.l10n_context) = self._parse_l10n()
  463. elif self.current_token == 'summary':
  464. current_object.summary = self.unparsed_line
  465. elif self.current_token == 'description':
  466. current_object.description = self.unparsed_line
  467. elif self.current_token == 'choices':
  468. current_object.choices = self._parse_choices(current_object)
  469. elif self.current_token == 'range':
  470. current_object.range = self._parse_range(current_object)
  471. if new_object:
  472. self.token_stack.append(self.current_token)
  473. self.object_stack.append(new_object)
  474. def parse(self):
  475. f = open(self.file, 'r')
  476. lines = [ line[:-1] for line in f.readlines() ]
  477. f.close()
  478. try:
  479. current_line_nb = 0
  480. for line in lines:
  481. current_line_nb += 1
  482. self.parse_line(line)
  483. except GSettingsSchemaConvertException, e:
  484. raise GSettingsSchemaConvertException('%s:%s: %s' % (os.path.basename(self.file), current_line_nb, e))
  485. return self.root
  486. ######################################
  487. class XMLSchemaParser:
  488. def __init__(self, file):
  489. self.file = file
  490. self.root = None
  491. def _parse_key(self, key_node, schema):
  492. key = GSettingsSchemaKey()
  493. key.name = key_node.get('name')
  494. if not key.name:
  495. raise GSettingsSchemaConvertException('A key in schema \'%s\' has no name.' % schema.id)
  496. key.type = key_node.get('type')
  497. if not key.type:
  498. raise GSettingsSchemaConvertException('Key \'%s\' in schema \'%s\' has no type.' % (key.name, schema.id))
  499. default_node = key_node.find('default')
  500. if default_node is None or not default_node.text.strip():
  501. raise GSettingsSchemaConvertException('Key \'%s\' in schema \'%s\' has no default value.' % (key.name, schema.id))
  502. key.l10n = default_node.get('l10n')
  503. key.l10n_context = default_node.get('context')
  504. key.default = default_node.text.strip()
  505. summary_node = key_node.find('summary')
  506. if summary_node is not None:
  507. key.summary = summary_node.text.strip()
  508. description_node = key_node.find('description')
  509. if description_node is not None:
  510. key.description = description_node.text.strip()
  511. range_node = key_node.find('range')
  512. if range_node is not None:
  513. min = None
  514. max = None
  515. min_node = range_node.find('min')
  516. if min_node is not None:
  517. min = min_node.text.strip()
  518. max_node = range_node.find('max')
  519. if max_node is not None:
  520. max = max_node.text.strip()
  521. if min or max:
  522. self.range = (min, max)
  523. choices_node = key_node.find('choices')
  524. if choices_node is not None:
  525. self.choices = []
  526. for choice_node in choices_node.findall('choice'):
  527. value = choice_node.get('value')
  528. if value:
  529. self.choices.append(value)
  530. else:
  531. raise GSettingsSchemaConvertException('A choice for key \'%s\' in schema \'%s\' has no value.' % (key.name, schema.id))
  532. return key
  533. def _parse_schema(self, schema_node):
  534. schema = GSettingsSchema()
  535. schema._children = []
  536. schema.id = schema_node.get('id')
  537. if not schema.id:
  538. raise GSettingsSchemaConvertException('A schema has no id.')
  539. schema.path = schema_node.get('path')
  540. schema.gettext_domain = schema_node.get('gettext-domain')
  541. for key_node in schema_node.findall('key'):
  542. key = self._parse_key(key_node, schema)
  543. schema.keys.append(key)
  544. for child_node in schema_node.findall('child'):
  545. child_name = child_node.get('name')
  546. if not child_name:
  547. raise GSettingsSchemaConvertException('A child of schema \'%s\' has no name.' % schema.id)
  548. child_schema = child_node.get('schema')
  549. if not child_schema:
  550. raise GSettingsSchemaConvertException('Child \'%s\' of schema \'%s\' has no schema.' % (child_name, schema.id))
  551. expected_id = schema.id + '.' + child_name
  552. if child_schema != expected_id:
  553. raise GSettingsSchemaConvertException('\'%s\' is too complex for this tool: child \'%s\' of schema \'%s\' has a schema that is not the expected one (\'%s\' vs \'%s\').' % (os.path.basename(self.file), child_name, schema.id, child_schema, expected_id))
  554. schema._children.append((child_schema, child_name))
  555. return schema
  556. def parse(self):
  557. self.root = GSettingsSchemaRoot()
  558. schemas = []
  559. parent = {}
  560. schemalist_node = ET.parse(self.file).getroot()
  561. self.root.gettext_domain = schemalist_node.get('gettext-domain')
  562. for schema_node in schemalist_node.findall('schema'):
  563. schema = self._parse_schema(schema_node)
  564. for (child_schema, child_name) in schema._children:
  565. if parent.has_key(child_schema):
  566. raise GSettingsSchemaConvertException('Child \'%s\' is declared by two different schemas: \'%s\' and \'%s\'.' % (child_schema, parent[child_schema], schema.id))
  567. parent[child_schema] = schema
  568. schemas.append(schema)
  569. # now let's move all schemas where they should leave
  570. for schema in schemas:
  571. if parent.has_key(schema.id):
  572. parent_schema = parent[schema.id]
  573. # check that the paths of parent and child are supported by
  574. # this tool
  575. found = False
  576. for (child_schema, child_name) in parent_schema._children:
  577. if child_schema == schema.id:
  578. found = True
  579. break
  580. if not found:
  581. raise GSettingsSchemaConvertException('Internal error: child not found in parent\'s children.')
  582. schema.name = child_name
  583. parent_schema.children.append(schema)
  584. else:
  585. self.root.schemas.append(schema)
  586. return self.root
  587. ######################################
  588. def map_gconf_type_to_variant_type(gconftype, gconfsubtype):
  589. typemap = { 'string': 's', 'int': 'i', 'float': 'd', 'bool': 'b', 'list': 'a' }
  590. try:
  591. result = typemap[gconftype]
  592. except KeyError:
  593. raise GSettingsSchemaConvertException('Type \'%s\' is not a known gconf type.' % gconftype)
  594. if gconftype == 'list':
  595. try:
  596. result = result + typemap[gconfsubtype]
  597. except KeyError:
  598. raise GSettingsSchemaConvertException('Type \'%s\' is not a known gconf type.' % gconfsubtype)
  599. return result
  600. def fix_value_for_simple_gconf_type(gconftype, gconfvalue):
  601. '''If there is no value, then we choose a 'neutral' value (false, 0, empty
  602. string).
  603. '''
  604. if gconftype == 'string':
  605. if not gconfvalue:
  606. return '\'\''
  607. return '\'' + gconfvalue.replace('\'', '\\\'') + '\''
  608. elif gconftype == 'int':
  609. if not gconfvalue:
  610. return '0'
  611. try:
  612. int(gconfvalue)
  613. except ValueError:
  614. raise GSettingsSchemaConvertException()
  615. return gconfvalue
  616. elif gconftype == 'float':
  617. if not gconfvalue:
  618. return '0.0'
  619. try:
  620. float(gconfvalue)
  621. except ValueError:
  622. raise GSettingsSchemaConvertException()
  623. return gconfvalue
  624. elif gconftype == 'bool':
  625. if not gconfvalue:
  626. return 'false'
  627. value = gconfvalue.lower()
  628. # gconf schemas can have 0/1 for false/true
  629. if value == '0':
  630. return 'false'
  631. elif value == '1':
  632. return 'true'
  633. elif value in ['false', 'true']:
  634. return value
  635. else:
  636. raise GSettingsSchemaConvertException()
  637. else:
  638. return gconfvalue
  639. class GConfSchema:
  640. def __init__(self, node):
  641. locale_node = node.find('locale')
  642. self.key = node.find('key').text
  643. self.type = node.find('type').text
  644. if self.type == 'list':
  645. self.list_type = node.find('list_type').text
  646. else:
  647. self.list_type = None
  648. self.varianttype = map_gconf_type_to_variant_type(self.type, self.list_type)
  649. applyto_node = node.find('applyto')
  650. if applyto_node is not None:
  651. self.applyto = node.find('applyto').text
  652. self.applyto.strip()
  653. self.keyname = self.applyto[self.applyto.rfind('/')+1:]
  654. self.prefix = self.applyto[:self.applyto.rfind('/')+1]
  655. else:
  656. self.applyto = None
  657. self.key.strip()
  658. self.keyname = self.key[self.key.rfind('/')+1:]
  659. self.prefix = self.key[:self.key.rfind('/')+1]
  660. self.prefix = os.path.normpath(self.prefix)
  661. try:
  662. self.default = locale_node.find('default').text
  663. self.localized = 'messages'
  664. except:
  665. try:
  666. self.default = node.find('default').text
  667. except:
  668. self.default = ''
  669. self.localized = None
  670. self.typed_default = None
  671. self.short = self._get_value_with_locale(node, locale_node, 'short')
  672. self.long = self._get_value_with_locale(node, locale_node, 'long')
  673. if self.short:
  674. self.short = self._oneline(self.short)
  675. if self.long:
  676. self.long = self._oneline(self.long)
  677. # Fix the default value to be parsable by GVariant
  678. if self.type == 'list':
  679. l = self.default.strip()
  680. if not l:
  681. l = '[]'
  682. elif not (l[0] == '[' and l[-1] == ']'):
  683. raise GSettingsSchemaConvertException('Cannot parse default list value \'%s\' for key \'%s\'.' % (self.default, self.applyto or self.key))
  684. values = l[1:-1].strip()
  685. if not values:
  686. self.default = '[]'
  687. self.typed_default = '@%s []' % self.varianttype
  688. else:
  689. items = [ item.strip() for item in values.split(',') ]
  690. try:
  691. items = [ fix_value_for_simple_gconf_type(self.list_type, item) for item in items ]
  692. except GSettingsSchemaConvertException:
  693. raise GSettingsSchemaConvertException('Invalid item(s) of type \'%s\' in default list \'%s\' for key \'%s\'.' % (self.list_type, self.default, self.applyto or self.key))
  694. values = ', '.join(items)
  695. self.default = '[ %s ]' % values
  696. else:
  697. try:
  698. self.default = fix_value_for_simple_gconf_type(self.type, self.default)
  699. except GSettingsSchemaConvertException:
  700. raise GSettingsSchemaConvertException('Invalid default value \'%s\' of type \'%s\' for key \'%s\'.' % (self.default, self.type, self.applyto or self.key))
  701. def _get_value_with_locale(self, node, locale_node, element):
  702. element_node = None
  703. if locale_node is not None:
  704. element_node = locale_node.find(element)
  705. if element_node is None:
  706. element_node = node.find(element)
  707. if element_node is not None:
  708. return element_node.text
  709. else:
  710. return None
  711. def _oneline(self, s):
  712. lines = s.splitlines()
  713. result = ''
  714. for line in lines:
  715. result += ' ' + line.lstrip()
  716. return result.strip()
  717. def convert_underscores(self):
  718. self.prefix = self.prefix.replace('_', '-')
  719. self.keyname = self.keyname.replace('_', '-')
  720. def get_gsettings_schema_key(self):
  721. key = GSettingsSchemaKey()
  722. key.fill(self.keyname, self.varianttype, self.default, self.typed_default, self.localized, self.keyname, self.short, self.long, None, None)
  723. return key
  724. ######################################
  725. class GConfSchemaParser:
  726. def __init__(self, file, default_gettext_domain, default_schema_id, keep_underscores):
  727. self.file = file
  728. self.default_gettext_domain = default_gettext_domain
  729. self.default_schema_id = default_schema_id
  730. self.keep_underscores = keep_underscores
  731. self.root = None
  732. self.default_schema_id_count = 0
  733. def _insert_schema(self, gconf_schema):
  734. if not self.keep_underscores:
  735. gconf_schema.convert_underscores()
  736. schemas_only = (gconf_schema.applyto is None)
  737. dirpath = gconf_schema.prefix
  738. if dirpath[0] != '/':
  739. raise GSettingsSchemaConvertException('Key \'%s\' has a relative path. There is no relative path in GSettings schemas.' % gconf_schema.applyto or gconf_schema.key)
  740. # remove leading 'schemas/' for schemas-only keys
  741. if schemas_only and dirpath.startswith('/schemas/'):
  742. dirpath = dirpath[len('/schemas'):]
  743. if len(dirpath) == 1:
  744. raise GSettingsSchemaConvertException('Key \'%s\' is a toplevel key. Toplevel keys are not accepted in GSettings schemas.' % gconf_schema.applyto or gconf_schema.key)
  745. # remove trailing slash because we'll split the string
  746. if dirpath[-1] == '/':
  747. dirpath = dirpath[:-1]
  748. # and also remove leading slash when splitting
  749. hierarchy = dirpath[1:].split('/')
  750. # we don't want to put apps/ and desktop/ keys in the same schema,
  751. # so we have a first step where we make sure to create a new schema
  752. # to avoid this case if necessary
  753. gsettings_schema = None
  754. for schema in self.root.schemas:
  755. if schemas_only:
  756. schema_path = schema._hacky_path
  757. else:
  758. schema_path = schema.path
  759. if dirpath.startswith(schema_path):
  760. gsettings_schema = schema
  761. break
  762. if not gsettings_schema:
  763. gsettings_schema = GSettingsSchema()
  764. if schemas_only:
  765. gsettings_schema._hacky_path = '/' + hierarchy[0] + '/'
  766. else:
  767. gsettings_schema.path = '/' + hierarchy[0] + '/'
  768. self.root.schemas.append(gsettings_schema)
  769. # we create the schema hierarchy that leads to this key
  770. gsettings_dir = gsettings_schema
  771. for item in hierarchy[1:]:
  772. subdir = None
  773. for child in gsettings_dir.children:
  774. if child.name == item:
  775. subdir = child
  776. break
  777. if not subdir:
  778. subdir = GSettingsSchema()
  779. # note: the id will be set later on
  780. if gsettings_dir.path:
  781. subdir.path = '%s%s/' % (gsettings_dir.path, item)
  782. subdir.name = item
  783. gsettings_dir.children.append(subdir)
  784. gsettings_dir = subdir
  785. # we have the final directory, so we can put the key there
  786. gsettings_dir.keys.append(gconf_schema.get_gsettings_schema_key())
  787. def _set_children_id(self, schema):
  788. for child in schema.children:
  789. child.id = '%s.%s' % (schema.id, child.name)
  790. self._set_children_id(child)
  791. def _fix_hierarchy(self):
  792. for schema in self.root.schemas:
  793. # we created one schema per level, starting at the root level;
  794. # however, we don't need to go that far and we can simplify the
  795. # hierarchy
  796. while len(schema.children) == 1 and not schema.keys:
  797. child = schema.children[0]
  798. schema.children = child.children
  799. schema.keys = child.keys
  800. if schema.path:
  801. schema.path += child.name + '/'
  802. # now that we have a toplevel schema, set the id
  803. if self.default_schema_id:
  804. schema.id = self.default_schema_id
  805. if self.default_schema_id_count > 0:
  806. schema.id += '.FIXME-%s' % self.default_schema_id_count
  807. self.default_schema_id_count += 1
  808. else:
  809. schema.id = 'FIXME'
  810. self._set_children_id(schema)
  811. def parse(self):
  812. # reset the state of the parser
  813. self.root = GSettingsSchemaRoot()
  814. if self.default_gettext_domain:
  815. self.root.gettext_domain = self.default_gettext_domain
  816. self.default_schema_id_count = 0
  817. gconfschemafile_node = ET.parse(self.file).getroot()
  818. for schemalist_node in gconfschemafile_node.findall('schemalist'):
  819. for schema_node in schemalist_node.findall('schema'):
  820. gconf_schema = GConfSchema(schema_node)
  821. if gconf_schema.localized and not self.root.gettext_domain:
  822. self.root.gettext_domain = 'FIXME'
  823. self._insert_schema(gconf_schema)
  824. self._fix_hierarchy()
  825. return self.root
  826. ######################################
  827. def main(args):
  828. parser = optparse.OptionParser()
  829. parser.add_option("-o", "--output", dest="output",
  830. help="output file")
  831. parser.add_option("-g", "--gconf", action="store_true", dest="gconf",
  832. default=False, help="convert a gconf schema file")
  833. parser.add_option("-d", "--gettext-domain", dest="gettext_domain",
  834. help="default gettext domain to use when converting gconf schema file")
  835. parser.add_option("-i", "--schema-id", dest="schema_id",
  836. help="default schema ID to use when converting gconf schema file")
  837. parser.add_option("-u", "--keep-underscores", action="store_true", dest="keep_underscores",
  838. help="keep underscores in key names instead of replacing them with dashes when converting gconf schema file")
  839. parser.add_option("-s", "--simple", action="store_true", dest="simple",
  840. default=False, help="use the simple schema format as output")
  841. parser.add_option("-x", "--xml", action="store_true", dest="xml",
  842. default=False, help="use the xml schema format as output")
  843. parser.add_option("-f", "--force", action="store_true", dest="force",
  844. default=False, help="overwrite output file if already existing")
  845. (options, args) = parser.parse_args()
  846. if len(args) < 1:
  847. print >> sys.stderr, 'Need a filename to work on.'
  848. return 1
  849. elif len(args) > 1:
  850. print >> sys.stderr, 'Too many arguments.'
  851. return 1
  852. if options.simple and options.xml:
  853. print >> sys.stderr, 'Too many output formats requested.'
  854. return 1
  855. if not options.gconf and options.gettext_domain:
  856. print >> sys.stderr, 'Default gettext domain can only be specified when converting a gconf schema.'
  857. return 1
  858. if not options.gconf and options.schema_id:
  859. print >> sys.stderr, 'Default schema ID can only be specified when converting a gconf schema.'
  860. return 1
  861. if not options.gconf and options.keep_underscores:
  862. print >> sys.stderr, 'The --keep-underscores option can only be specified when converting a gconf schema.'
  863. return 1
  864. argfile = os.path.expanduser(args[0])
  865. if not os.path.exists(argfile):
  866. print >> sys.stderr, '\'%s\' does not exist.' % argfile
  867. return 1
  868. if options.output:
  869. options.output = os.path.expanduser(options.output)
  870. try:
  871. if options.output and not options.force and os.path.exists(options.output):
  872. raise GSettingsSchemaConvertException('\'%s\' already exists. Use --force to overwrite it.' % options.output)
  873. if options.gconf:
  874. if not options.simple and not options.xml:
  875. options.xml = True
  876. try:
  877. parser = GConfSchemaParser(argfile, options.gettext_domain, options.schema_id, options.keep_underscores)
  878. schema_root = parser.parse()
  879. except SyntaxError, e:
  880. raise GSettingsSchemaConvertException('\'%s\' does not look like a valid gconf schema file: %s' % (argfile, e))
  881. else:
  882. # autodetect if file is XML or not
  883. try:
  884. parser = XMLSchemaParser(argfile)
  885. schema_root = parser.parse()
  886. if not options.simple and not options.xml:
  887. options.simple = True
  888. except SyntaxError, e:
  889. parser = SimpleSchemaParser(argfile)
  890. schema_root = parser.parse()
  891. if not options.simple and not options.xml:
  892. options.xml = True
  893. if options.xml:
  894. node = schema_root.get_xml_node()
  895. try:
  896. output = ET.tostring(node, pretty_print = True)
  897. except TypeError:
  898. # pretty_print only works with lxml
  899. output = ET.tostring(node)
  900. else:
  901. output = schema_root.get_simple_string()
  902. if not options.output:
  903. sys.stdout.write(output)
  904. else:
  905. try:
  906. fout = open(options.output, 'w')
  907. fout.write(output)
  908. fout.close()
  909. except GSettingsSchemaConvertException, e:
  910. fout.close()
  911. if os.path.exists(options.output):
  912. os.unlink(options.output)
  913. raise e
  914. except GSettingsSchemaConvertException, e:
  915. print >> sys.stderr, '%s' % e
  916. return 1
  917. return 0
  918. if __name__ == '__main__':
  919. try:
  920. res = main(sys.argv)
  921. sys.exit(res)
  922. except KeyboardInterrupt:
  923. pass