package_xml.py
Go to the documentation of this file.
1 import collections
2 import operator
3 import re
4 from xml.dom.minidom import parse
5 
6 DEPEND_ORDERING = ['buildtool_depend', 'depend', 'build_depend', 'build_export_depend',
7  'run_depend', 'exec_depend', 'test_depend', 'doc_depend']
8 
9 ORDERING = ['name', 'version', 'description',
10  ['maintainer', 'license', 'author', 'url']] + DEPEND_ORDERING + ['export']
11 
12 INDENT_PATTERN = re.compile('\n *')
13 
14 PEOPLE_TAGS = ['maintainer', 'author']
15 
16 FORMAT_3_HEADER = """<?xml version="1.0"?>
17 <?xml-model
18  href="http://download.ros.org/schema/package_format3.xsd"
19  schematypens="http://www.w3.org/2001/XMLSchema"?>
20 """
21 
22 
23 def get_ordering_index(name, whiny=True):
24  for i, o in enumerate(ORDERING):
25  if type(o) == list:
26  if name in o:
27  return i
28  elif name == o:
29  return i
30  if name and whiny:
31  print('\tUnsure of ordering for ' + name)
32  return len(ORDERING)
33 
34 
35 def get_package_tag_index(s, key='<package'):
36  if key not in s:
37  return 0
38  return s.index(key)
39 
40 
42  c = 0
43  while c < len(s) and s[-c - 1] == ' ':
44  c += 1
45  return c
46 
47 
48 def replace_package_set(manifest, source_tags, new_tag):
49  """Replace all the elements with tags in source_tags with new elements with new_tag."""
50  intersection = None
51  for tag in source_tags:
52  pkgs = set(manifest.get_packages_by_tag(tag))
53  if intersection is None:
54  intersection = pkgs
55  else:
56  intersection = intersection.intersection(pkgs)
57  for tag in source_tags:
58  manifest.remove_dependencies(tag, intersection)
59  manifest.insert_new_packages(new_tag, intersection)
60 
61 
62 class PackageXML:
63  def __init__(self, fn):
64  self.fn = fn
65  self.tree = parse(fn)
66  self.root = self.tree.getElementsByTagName('package')[0]
67  contents = open(fn).read()
68  self.header = contents[:get_package_tag_index(contents)]
69  self._name = None
70  self._format = None
71  self._std_tab = None
72  self.changed = False
73 
74  @property
75  def name(self):
76  if self._name is not None:
77  return self._name
78  name_tags = self.root.getElementsByTagName('name')
79  if not name_tags:
80  return
81  name_tag = name_tags[0]
82  self._name = name_tag.firstChild.nodeValue
83  return self._name
84 
85  @property
86  def format(self):
87  if self._format is not None:
88  return self._format
89  if not self.root.hasAttribute('format'):
90  self._format = 1
91  else:
92  self._format = int(self.root.attributes['format'].value)
93  return self._format
94 
95  @property
96  def std_tab(self):
97  if self._std_tab is not None:
98  return self._std_tab
99  tab_ct = collections.defaultdict(int)
100  for c in self.root.childNodes:
101  if c.nodeType == c.TEXT_NODE:
102  spaces = count_trailing_spaces(c.data)
103  tab_ct[spaces] += 1
104  if len(tab_ct) == 0:
105  self._std_tab = 4
106  else:
107  self._std_tab = max(tab_ct.items(), key=operator.itemgetter(1))[0]
108  return self._std_tab
109 
110  def get_packages_by_tag(self, tag):
111  pkgs = []
112  for el in self.root.getElementsByTagName(tag):
113  pkgs.append(el.childNodes[0].nodeValue)
114  return pkgs
115 
116  def get_packages(self, mode='build'):
117  keys = []
118  if mode == 'build':
119  keys.append('build_depend')
120  if self.format == 1 and mode == 'run':
121  keys.append('run_depend')
122  if self.format == 2 and mode != 'test':
123  keys.append('depend')
124  if mode == 'run':
125  keys.append('exec_depend')
126  if mode == 'test':
127  keys.append('test_depend')
128  pkgs = []
129  for key in keys:
130  pkgs += self.get_packages_by_tag(key)
131  return set(pkgs)
132 
133  def get_tab_element(self, tabs=1):
134  return self.tree.createTextNode('\n' + ' ' * (self.std_tab * tabs))
135 
136  def get_child_indexes(self):
137  """Return a dictionary based on which children span which indexes.
138 
139  The keys are the types of nodes in the xml (build_depend, maintainer, etc).
140  The values are arrays marking the range of elements in the xml root that match that tag.
141 
142  For example, tags[build_depend] = [(5, 9), (11, 50)] means that elements [5, 9) and [11, 50) are
143  either build_depend elements (or the strings between them)
144  """
145  tags = collections.defaultdict(list)
146  i = 0
147  current = None
148  current_start = 0
149  current_last = 0
150  while i < len(self.root.childNodes):
151  child = self.root.childNodes[i]
152  if child.nodeType == child.TEXT_NODE:
153  i += 1
154  continue
155 
156  name = child.nodeName
157  if name != current:
158  if current:
159  tags[current].append((current_start, current_last))
160  current_start = i
161  current = name
162  current_last = i
163  i += 1
164  if current:
165  tags[current].append((current_start, current_last))
166  return dict(tags)
167 
168  def get_insertion_index(self, tag, tag_value=None):
169  """Return the index where to insert a new element with the given tag type.
170 
171  If there are already elements of that type, then either insert after the last matching element,
172  or if the list is alphabetized, insert it in the correct place alphabetically using the tag_value.
173  Otherwise, look at the existing elements, and find ones that are supposed to come the closest
174  before the given tag, and insert after them. If none found, add at the end.
175  """
176  indexes = self.get_child_indexes()
177  # If there are elements of this type already
178  if tag in indexes:
179  if len(indexes[tag]) == 1 and tag in DEPEND_ORDERING:
180  start, end = indexes[tag][0]
181  tag_values = []
182  my_index = start
183  for i in range(start, end + 1):
184  child = self.root.childNodes[i]
185  if child.nodeType == child.TEXT_NODE:
186  continue
187  value = child.firstChild.data
188  tag_values.append(value)
189  if tag_value >= value:
190  my_index = i
191 
192  # If already sorted, and first_value is defined (meaning there are existing tags)
193  if tag_values and sorted(tag_values) == tag_values:
194  # If it should go before the current first tag, we XXX
195  if tag_value <= tag_values[0]:
196  return my_index - 1
197 
198  # If it should go before some existing tag
199  if tag_value <= tag_values[-1]:
200  return my_index
201 
202  # If all else fails, we insert the tag after the last matching tag
203  return indexes[tag][-1][1] # last match, end index
204 
205  # If no elements match this type, then find the right place to insert
206  else:
207  max_index = get_ordering_index(tag, whiny=False)
208  best_tag = None
209  best_index = None
210  for tag in indexes:
211  ni = get_ordering_index(tag, whiny=False)
212  if ni >= max_index:
213  # This tag should appear after our tag
214  continue
215 
216  if best_tag is None or ni > best_index or indexes[tag][-1] > indexes[best_tag][-1]:
217  best_tag = tag
218  best_index = ni
219 
220  if best_tag is None:
221  return len(self.root.childNodes)
222  else:
223  return indexes[best_tag][-1][1]
224 
225  def insert_new_tag(self, tag):
226  if tag.tagName in DEPEND_ORDERING:
227  value = tag.firstChild.data
228  else:
229  value = None
230 
231  index = self.get_insertion_index(tag.tagName, value)
232  before = self.root.childNodes[:index + 1]
233  after = self.root.childNodes[index + 1:]
234 
235  new_tab_element = self.get_tab_element()
236 
237  # if the tag immediately before where we're going to insert is a text node,
238  # then insert the new element and then the tab
239  if before and before[-1].nodeType == before[-1].TEXT_NODE:
240  new_bits = [tag, new_tab_element]
241  else:
242  # Otherwise (i.e. most cases) insert the tab then the element
243  new_bits = [new_tab_element, tag]
244 
245  self.root.childNodes = before + new_bits + after
246  self.changed = True
247 
248  def insert_new_tags(self, tags):
249  for tag in tags:
250  self.insert_new_tag(tag)
251 
252  def insert_new_tag_inside_another(self, parent, tag, depth=2):
253  all_elements = []
254  all_elements.append(self.get_tab_element(depth))
255  all_elements.append(tag)
256 
257  if len(parent.childNodes) == 0:
258  parent.childNodes = all_elements + [self.get_tab_element()]
259  else:
260  parent.childNodes = parent.childNodes[:-1] + all_elements + parent.childNodes[-1:]
261  self.changed = True
262 
263  def insert_new_packages(self, tag, values):
264  for pkg in sorted(values):
265  print('\tInserting %s: %s' % (tag, pkg))
266  node = self.tree.createElement(tag)
267  node.appendChild(self.tree.createTextNode(pkg))
268  self.insert_new_tag(node)
269 
270  def add_packages(self, build_depends, run_depends, test_depends=None, prefer_depend_tag=True):
271  if self.format == 1:
272  run_depends.update(build_depends)
273  existing_build = self.get_packages('build')
274  existing_run = self.get_packages('run')
275  build_depends = build_depends - existing_build
276  run_depends = run_depends - existing_run
277  if self.format == 1:
278  self.insert_new_packages('build_depend', build_depends)
279  self.insert_new_packages('run_depend', run_depends)
280  elif prefer_depend_tag:
281  depend_tags = build_depends.union(run_depends)
282 
283  # Remove tags that overlap with new depends
284  self.remove_dependencies('build_depend', existing_build.intersection(depend_tags))
285  self.remove_dependencies('exec_depend', existing_run.intersection(depend_tags))
286 
287  # Insert depends
288  self.insert_new_packages('depend', depend_tags)
289  else:
290  both = build_depends.intersection(run_depends)
291  self.insert_new_packages('depend', both)
292  self.insert_new_packages('build_depend', build_depends - both)
293  self.insert_new_packages('exec_depend', build_depends - both - existing_run)
294  self.insert_new_packages('exec_depend', run_depends - both)
295 
296  if test_depends is not None and len(test_depends) > 0:
297  existing_test = self.get_packages('test')
298  test_depends = set(test_depends) - existing_build - build_depends - existing_test
299  self.insert_new_packages('test_depend', test_depends)
300 
301  def remove_element(self, element):
302  """Remove the given element AND the text element before it if it is just an indentation."""
303  parent = element.parentNode
304  index = parent.childNodes.index(element)
305  if index > 0:
306  previous = parent.childNodes[index - 1]
307  if previous.nodeType == previous.TEXT_NODE and INDENT_PATTERN.match(previous.nodeValue):
308  parent.removeChild(previous)
309  parent.removeChild(element)
310  self.changed = True
311 
312  def remove_dependencies(self, name, pkgs, quiet=False):
313  for el in self.root.getElementsByTagName(name):
314  pkg = el.childNodes[0].nodeValue
315  if pkg in pkgs:
316  if not quiet:
317  print('\tRemoving %s %s' % (name, pkg))
318  self.remove_element(el)
319 
320  def get_elements_by_tags(self, tags):
321  elements = []
322  for tag in tags:
323  elements += self.root.getElementsByTagName(tag)
324  return elements
325 
326  def get_people(self):
327  people = []
328  for el in self.get_elements_by_tags(PEOPLE_TAGS):
329  name = el.childNodes[0].nodeValue
330  email = el.getAttribute('email')
331  people.append((name, email))
332  return people
333 
334  def update_people(self, target_name, target_email=None, search_name=None, search_email=None):
335  for el in self.get_elements_by_tags(PEOPLE_TAGS):
336  name = el.childNodes[0].nodeValue
337  email = el.getAttribute('email') if el.hasAttribute('email') else ''
338  if (search_name is None or name == search_name) and (search_email is None or email == search_email):
339  el.childNodes[0].nodeValue = target_name
340  if target_email:
341  el.setAttribute('email', target_email)
342  print('\tReplacing %s %s/%s with %s/%s' % (el.nodeName, name, email, target_name, target_email))
343  self.changed = True
344 
346  els = self.root.getElementsByTagName('license')
347  if len(els) == 0:
348  return None
349  return els[0]
350 
351  def get_license(self):
352  el = self.get_license_element()
353  return el.childNodes[0].nodeValue
354 
355  def set_license(self, license_str):
356  el = self.get_license_element()
357  if license != el.childNodes[0].nodeValue:
358  el.childNodes[0].nodeValue = license_str
359  self.changed = True
360 
361  def is_metapackage(self):
362  for node in self.root.getElementsByTagName('export'):
363  for child in node.childNodes:
364  if child.nodeType == child.ELEMENT_NODE:
365  if child.nodeName == 'metapackage':
366  return True
367  return False
368 
369  def get_plugin_xmls(self):
370  """Return a mapping from the package name to a list of the relative path(s) for the plugin xml(s)."""
371  xmls = collections.defaultdict(list)
372  export = self.root.getElementsByTagName('export')
373  if len(export) == 0:
374  return xmls
375  for ex in export:
376  for n in ex.childNodes:
377  if n.nodeType == self.root.ELEMENT_NODE:
378  plugin = n.getAttribute('plugin').replace('${prefix}/', '')
379  xmls[n.nodeName].append(plugin)
380  return xmls
381 
382  def get_export_tag(self):
383  """Get the export tag. Create it if it doesn't exist."""
384  export_tags = self.root.getElementsByTagName('export')
385  if len(export_tags) == 0:
386  export_tag = self.tree.createElement('export')
387  self.insert_new_tag(export_tag)
388  return export_tag
389  else:
390  return export_tags[0]
391 
392  def add_plugin_export(self, pkg_name, xml_path):
393  """Add the plugin configuration if not found. Add export tag as needed. Return the surrounding export tag."""
394  ex_tag = self.get_export_tag()
395 
396  attr = '${prefix}/' + xml_path
397  for tag in ex_tag.childNodes:
398  if tag.nodeName != pkg_name:
399  continue
400  plugin = tag.attributes.get('plugin')
401  if plugin and plugin.value == attr:
402  return
403 
404  pe = self.tree.createElement(pkg_name)
405  pe.setAttribute('plugin', attr)
406  self.insert_new_tag_inside_another(ex_tag, pe)
407  return ex_tag
408 
409  def upgrade(self, new_format=2, quiet=True):
410  if self.format == new_format:
411  if not quiet:
412  print('%s already in format %d!' % (self.name, self.format))
413  return
414 
415  if new_format not in [2, 3]:
416  raise RuntimeError('Unknown PackageXML version: ' + repr(new_format))
417 
418  if self.format == 1:
419  if not quiet:
420  print('Converting {} from version {} to 2'.format(self.name, self.format))
421  self._format = 2
422  self.root.setAttribute('format', '2')
423  replace_package_set(self, ['build_depend', 'run_depend'], 'depend')
424  replace_package_set(self, ['run_depend'], 'exec_depend')
425 
426  if new_format == 3:
427  if not quiet:
428  print('Converting {} from version {} to 3'.format(self.name, self.format))
429  self._format = 3
430  self.root.setAttribute('format', '3')
431  self.header = FORMAT_3_HEADER
432 
433  self.changed = True
434 
435  def write(self, new_fn=None):
436  if new_fn is None:
437  new_fn = self.fn
438 
439  if new_fn == self.fn and not self.changed:
440  return
441 
442  s = self.tree.toxml(self.tree.encoding)
443  index = get_package_tag_index(s)
444  s = self.header + s[index:] + '\n'
445 
446  with open(new_fn, 'wb') as f:
447  f.write(s.encode('UTF-8'))
def upgrade(self, new_format=2, quiet=True)
Definition: package_xml.py:409
def update_people(self, target_name, target_email=None, search_name=None, search_email=None)
Definition: package_xml.py:334
def replace_package_set(manifest, source_tags, new_tag)
Definition: package_xml.py:48
def get_package_tag_index(s, key='< package')
Definition: package_xml.py:35
def remove_dependencies(self, name, pkgs, quiet=False)
Definition: package_xml.py:312
def add_packages(self, build_depends, run_depends, test_depends=None, prefer_depend_tag=True)
Definition: package_xml.py:270
def insert_new_packages(self, tag, values)
Definition: package_xml.py:263
def get_insertion_index(self, tag, tag_value=None)
Definition: package_xml.py:168
def add_plugin_export(self, pkg_name, xml_path)
Definition: package_xml.py:392
def get_ordering_index(name, whiny=True)
Definition: package_xml.py:23
def get_packages(self, mode='build')
Definition: package_xml.py:116
def insert_new_tag_inside_another(self, parent, tag, depth=2)
Definition: package_xml.py:252


ros_introspection
Author(s):
autogenerated on Wed Mar 3 2021 03:56:00