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


ros_introspection
Author(s):
autogenerated on Wed Jun 22 2022 02:45:33