core.py
Go to the documentation of this file.
1 import sys
2 import copy
3 
4 from urdf_parser_py.xml_reflection.basics import node_add
5 from urdf_parser_py.xml_reflection.basics import xml_children
6 from urdf_parser_py.xml_reflection.basics import xml_string
7 from urdf_parser_py.xml_reflection.basics import YamlReflection
8 from xml.etree import ElementTree as ET
9 
10 # @todo Make this work with decorators
11 
12 # Is this reflection or serialization? I think it's serialization...
13 # Rename?
14 
15 # Do parent operations after, to allow child to 'override' parameters?
16 # Need to make sure that duplicate entires do not get into the 'unset*' lists
17 
18 
19 def reflect(cls, *args, **kwargs):
20  """
21  Simple wrapper to add XML reflection to an xml_reflection.Object class
22  """
23  cls.XML_REFL = Reflection(*args, **kwargs)
24 
25 # Rename 'write_xml' to 'write_xml' to have paired 'load/dump', and make
26 # 'pre_dump' and 'post_load'?
27 # When dumping to yaml, include tag name?
28 
29 # How to incorporate line number and all that jazz?
30 def on_error_stderr(message):
31  """ What to do on an error. This can be changed to raise an exception. """
32  sys.stderr.write(message + '\n')
33 on_error = on_error_stderr
34 
35 
36 skip_default = False
37 # defaultIfMatching = True # Not implemeneted yet
38 
39 # Registering Types
40 value_types = {}
41 value_type_prefix = ''
42 
43 
44 def start_namespace(namespace):
45  """
46  Basic mechanism to prevent conflicts for string types for URDF and SDF
47  @note Does not handle nesting!
48  """
49  global value_type_prefix
50  value_type_prefix = namespace + '.'
51 
52 
54  global value_type_prefix
55  value_type_prefix = ''
56 
57 
58 def add_type(key, value):
59  if isinstance(key, str):
60  key = value_type_prefix + key
61  assert key not in value_types
62  value_types[key] = value
63 
64 
65 def get_type(cur_type):
66  """ Can wrap value types if needed """
67  if value_type_prefix and isinstance(cur_type, str):
68  # See if it exists in current 'namespace'
69  curKey = value_type_prefix + cur_type
70  value_type = value_types.get(curKey)
71  else:
72  value_type = None
73  if value_type is None:
74  # Try again, in 'global' scope
75  value_type = value_types.get(cur_type)
76  if value_type is None:
77  value_type = make_type(cur_type)
78  add_type(cur_type, value_type)
79  return value_type
80 
81 
82 def make_type(cur_type):
83  if isinstance(cur_type, ValueType):
84  return cur_type
85  elif isinstance(cur_type, str):
86  if cur_type.startswith('vector'):
87  extra = cur_type[6:]
88  if extra:
89  count = float(extra)
90  else:
91  count = None
92  return VectorType(count)
93  else:
94  raise Exception("Invalid value type: {}".format(cur_type))
95  elif cur_type == list:
96  return ListType()
97  elif issubclass(cur_type, Object):
98  return ObjectType(cur_type)
99  elif cur_type in [str, float]:
100  return BasicType(cur_type)
101  else:
102  raise Exception("Invalid type: {}".format(cur_type))
103 
104 
105 class Path(object):
106  def __init__(self, tag, parent=None, suffix="", tree=None):
107  self.parent = parent
108  self.tag = tag
109  self.suffix = suffix
110  self.tree = tree # For validating general path (getting true XML path)
111 
112  def __str__(self):
113  if self.parent is not None:
114  return "{}/{}{}".format(self.parent, self.tag, self.suffix)
115  else:
116  if self.tag is not None and len(self.tag) > 0:
117  return "/{}{}".format(self.tag, self.suffix)
118  else:
119  return self.suffix
120 
121 class ParseError(Exception):
122  def __init__(self, e, path):
123  self.e = e
124  self.path = path
125  message = "ParseError in {}:\n{}".format(self.path, self.e)
126  super(ParseError, self).__init__(message)
127 
128 
129 class ValueType(object):
130  """ Primitive value type """
131 
132  def from_xml(self, node, path):
133  return self.from_string(node.text)
134 
135  def write_xml(self, node, value):
136  """
137  If type has 'write_xml', this function should expect to have it's own
138  XML already created i.e., In Axis.to_sdf(self, node), 'node' would be
139  the 'axis' element.
140  @todo Add function that makes an XML node completely independently?
141  """
142  node.text = self.to_string(value)
143 
144  def equals(self, a, b):
145  return a == b
146 
147 
149  def __init__(self, cur_type):
150  self.type = cur_type
151 
152  def to_string(self, value):
153  return str(value)
154 
155  def from_string(self, value):
156  return self.type(value)
157 
158 
160  def to_string(self, values):
161  return ' '.join(values)
162 
163  def from_string(self, text):
164  return text.split()
165 
166  def equals(self, aValues, bValues):
167  return len(aValues) == len(bValues) and all(a == b for (a, b) in zip(aValues, bValues)) # noqa
168 
169 
171  def __init__(self, count=None):
172  self.count = count
173 
174  def check(self, values):
175  if self.count is not None:
176  assert len(values) == self.count, "Invalid vector length"
177 
178  def to_string(self, values):
179  self.check(values)
180  raw = list(map(str, values))
181  return ListType.to_string(self, raw)
182 
183  def from_string(self, text):
184  raw = ListType.from_string(self, text)
185  self.check(raw)
186  return list(map(float, raw))
187 
188 
190  """
191  Simple, raw XML value. Need to bugfix putting this back into a document
192  """
193 
194  def from_xml(self, node, path):
195  return node
196 
197  def write_xml(self, node, value):
198  # @todo rying to insert an element at root level seems to screw up
199  # pretty printing
200  children = xml_children(value)
201  list(map(node.append, children))
202  # Copy attributes
203  for (attrib_key, attrib_value) in value.attrib.items():
204  node.set(attrib_key, attrib_value)
205 
206 
208  """
209  Extractor that retrieves data from an element, given a
210  specified attribute, casted to value_type.
211  """
212 
213  def __init__(self, attribute, value_type):
214  self.attribute = attribute
215  self.value_type = get_type(value_type)
216 
217  def from_xml(self, node, path):
218  text = node.get(self.attribute)
219  return self.value_type.from_string(text)
220 
221  def write_xml(self, node, value):
222  text = self.value_type.to_string(value)
223  node.set(self.attribute, text)
224 
225 
227  def __init__(self, cur_type):
228  self.type = cur_type
229 
230  def from_xml(self, node, path):
231  obj = self.type()
232  obj.read_xml(node, path)
233  return obj
234 
235  def write_xml(self, node, obj):
236  obj.write_xml(node)
237 
238 
240  def __init__(self, name, typeMap):
241  self.name = name
242  self.typeMap = typeMap
243  self.nameMap = {}
244  for (key, value) in typeMap.items():
245  # Reverse lookup
246  self.nameMap[value] = key
247 
248  def from_xml(self, node, path):
249  cur_type = self.typeMap.get(node.tag)
250  if cur_type is None:
251  raise Exception("Invalid {} tag: {}".format(self.name, node.tag))
252  value_type = get_type(cur_type)
253  return value_type.from_xml(node, path)
254 
255  def get_name(self, obj):
256  cur_type = type(obj)
257  name = self.nameMap.get(cur_type)
258  if name is None:
259  raise Exception("Invalid {} type: {}".format(self.name, cur_type))
260  return name
261 
262  def write_xml(self, node, obj):
263  obj.write_xml(node)
264 
265 
267  def __init__(self, name, typeOrder):
268  self.name = name
269  assert len(typeOrder) > 0
270  self.type_order = typeOrder
271 
272  def from_xml(self, node, path):
273  error_set = []
274  for value_type in self.type_order:
275  try:
276  return value_type.from_xml(node, path)
277  except Exception as e:
278  error_set.append((value_type, e))
279  # Should have returned, we encountered errors
280  out = "Could not perform duck-typed parsing."
281  for (value_type, e) in error_set:
282  out += "\nValue Type: {}\nException: {}\n".format(value_type, e)
283  raise ParseError(Exception(out), path)
284 
285  def write_xml(self, node, obj):
286  obj.write_xml(node)
287 
288 
289 class Param(object):
290  """ Mirroring Gazebo's SDF api
291 
292  @param xml_var: Xml name
293  @todo If the value_type is an object with a tag defined in it's
294  reflection, allow it to act as the default tag name?
295  @param var: Python class variable name. By default it's the same as the
296  XML name
297  """
298 
299  def __init__(self, xml_var, value_type, required=True, default=None,
300  var=None):
301  self.xml_var = xml_var
302  if var is None:
303  self.var = xml_var
304  else:
305  self.var = var
306  self.type = None
307  self.value_type = get_type(value_type)
308  self.default = default
309  if required:
310  assert default is None, "Default does not make sense for a required field" # noqa
311  self.required = required
312  self.is_aggregate = False
313 
314  def set_default(self, obj):
315  if self.required:
316  raise Exception("Required {} not set in XML: {}".format(self.type, self.xml_var)) # noqa
317  elif not skip_default:
318  setattr(obj, self.var, self.default)
319 
320 
322  def __init__(self, xml_var, value_type, required=True, default=None,
323  var=None):
324  Param.__init__(self, xml_var, value_type, required, default, var)
325  self.type = 'attribute'
326 
327  def set_from_string(self, obj, value):
328  """ Node is the parent node in this case """
329  # Duplicate attributes cannot occur at this point
330  setattr(obj, self.var, self.value_type.from_string(value))
331 
332  def get_value(self, obj):
333  return getattr(obj, self.var)
334 
335  def add_to_xml(self, obj, node):
336  value = getattr(obj, self.var)
337  # Do not set with default value if value is None
338  if value is None:
339  if self.required:
340  raise Exception("Required attribute not set in object: {}".format(self.var)) # noqa
341  elif not skip_default:
342  value = self.default
343  # Allow value type to handle None?
344  if value is not None:
345  node.set(self.xml_var, self.value_type.to_string(value))
346 
347 # Add option if this requires a header?
348 # Like <joints> <joint/> .... </joints> ???
349 # Not really... This would be a specific list type, not really aggregate
350 
351 
352 class Element(Param):
353  def __init__(self, xml_var, value_type, required=True, default=None,
354  var=None, is_raw=False):
355  Param.__init__(self, xml_var, value_type, required, default, var)
356  self.type = 'element'
357  self.is_raw = is_raw
358 
359  def set_from_xml(self, obj, node, path):
360  value = self.value_type.from_xml(node, path)
361  setattr(obj, self.var, value)
362 
363  def add_to_xml(self, obj, parent):
364  value = getattr(obj, self.xml_var)
365  if value is None:
366  if self.required:
367  raise Exception("Required element not defined in object: {}".format(self.var)) # noqa
368  elif not skip_default:
369  value = self.default
370  if value is not None:
371  self.add_scalar_to_xml(parent, value)
372 
373  def add_scalar_to_xml(self, parent, value):
374  if self.is_raw:
375  node = parent
376  else:
377  node = node_add(parent, self.xml_var)
378  self.value_type.write_xml(node, value)
379 
380 
382  def __init__(self, xml_var, value_type, var=None, is_raw=False):
383  if var is None:
384  var = xml_var + 's'
385  Element.__init__(self, xml_var, value_type, required=False, var=var,
386  is_raw=is_raw)
387  self.is_aggregate = True
388 
389  def add_from_xml(self, obj, node, path):
390  value = self.value_type.from_xml(node, path)
391  obj.add_aggregate(self.xml_var, value)
392 
393  def set_default(self, obj):
394  pass
395 
396 
397 class Info:
398  """ Small container for keeping track of what's been consumed """
399 
400  def __init__(self, node):
401  self.attributes = list(node.attrib.keys())
402  self.children = xml_children(node)
403 
404 
405 class Reflection(object):
406  def __init__(self, params=[], parent_cls=None, tag=None):
407  """ Construct a XML reflection thing
408  @param parent_cls: Parent class, to use it's reflection as well.
409  @param tag: Only necessary if you intend to use Object.write_xml_doc()
410  This does not override the name supplied in the reflection
411  definition thing.
412  """
413  if parent_cls is not None:
414  self.parent = parent_cls.XML_REFL
415  else:
416  self.parent = None
417  self.tag = tag
418 
419  # Laziness for now
420  attributes = []
421  elements = []
422  for param in params:
423  if isinstance(param, Element):
424  elements.append(param)
425  else:
426  attributes.append(param)
427 
428  self.vars = []
429  self.paramMap = {}
430 
431  self.attributes = attributes
432  self.attribute_map = {}
434  for attribute in attributes:
435  self.attribute_map[attribute.xml_var] = attribute
436  self.paramMap[attribute.xml_var] = attribute
437  self.vars.append(attribute.var)
438  if attribute.required:
439  self.required_attribute_names.append(attribute.xml_var)
440 
441  self.elements = []
442  self.element_map = {}
444  self.aggregates = []
445  self.scalars = []
446  self.scalarNames = []
447  for element in elements:
448  self.element_map[element.xml_var] = element
449  self.paramMap[element.xml_var] = element
450  self.vars.append(element.var)
451  if element.required:
452  self.required_element_names.append(element.xml_var)
453  if element.is_aggregate:
454  self.aggregates.append(element)
455  else:
456  self.scalars.append(element)
457  self.scalarNames.append(element.xml_var)
458 
459  def set_from_xml(self, obj, node, path, info=None):
460  is_final = False
461  if info is None:
462  is_final = True
463  info = Info(node)
464 
465  if self.parent:
466  path = self.parent.set_from_xml(obj, node, path, info)
467 
468  # Make this a map instead? Faster access? {name: isSet} ?
469  unset_attributes = list(self.attribute_map.keys())
470  unset_scalars = copy.copy(self.scalarNames)
471 
472  def get_attr_path(attribute):
473  attr_path = copy.copy(path)
474  attr_path.suffix += '[@{}]'.format(attribute.xml_var)
475  return attr_path
476 
477  def get_element_path(element):
478  element_path = Path(element.xml_var, parent = path)
479  # Add an index (allow this to be overriden)
480  if element.is_aggregate:
481  values = obj.get_aggregate_list(element.xml_var)
482  index = 1 + len(values) # 1-based indexing for W3C XPath
483  element_path.suffix = "[{}]".format(index)
484  return element_path
485 
486  id_var = "name"
487  # Better method? Queues?
488  for xml_var in copy.copy(info.attributes):
489  attribute = self.attribute_map.get(xml_var)
490  if attribute is not None:
491  value = node.attrib[xml_var]
492  attr_path = get_attr_path(attribute)
493  try:
494  attribute.set_from_string(obj, value)
495  if attribute.xml_var == id_var:
496  # Add id_var suffix to current path (do not copy so it propagates)
497  path.suffix = "[@{}='{}']".format(id_var, attribute.get_value(obj))
498  except ParseError:
499  raise
500  except Exception as e:
501  raise ParseError(e, attr_path)
502  unset_attributes.remove(xml_var)
503  info.attributes.remove(xml_var)
504 
505  # Parse unconsumed nodes
506  for child in copy.copy(info.children):
507  tag = child.tag
508  element = self.element_map.get(tag)
509  if element is not None:
510  # Name will have been set
511  element_path = get_element_path(element)
512  if element.is_aggregate:
513  element.add_from_xml(obj, child, element_path)
514  else:
515  if tag in unset_scalars:
516  element.set_from_xml(obj, child, element_path)
517  unset_scalars.remove(tag)
518  else:
519  on_error("Scalar element defined multiple times: {}".format(tag)) # noqa
520  info.children.remove(child)
521 
522  # For unset attributes and scalar elements, we should not pass the attribute
523  # or element path, as those paths will implicitly not exist.
524  # If we do supply it, then the user would need to manually prune the XPath to try
525  # and find where the problematic parent element.
526  for attribute in map(self.attribute_map.get, unset_attributes):
527  try:
528  attribute.set_default(obj)
529  except ParseError:
530  raise
531  except Exception as e:
532  raise ParseError(e, path) # get_attr_path(attribute.xml_var)
533 
534  for element in map(self.element_map.get, unset_scalars):
535  try:
536  element.set_default(obj)
537  except ParseError:
538  raise
539  except Exception as e:
540  raise ParseError(e, path) # get_element_path(element)
541 
542  if is_final:
543  for xml_var in info.attributes:
544  on_error('Unknown attribute "{}" in {}'.format(xml_var, path))
545  for node in info.children:
546  on_error('Unknown tag "{}" in {}'.format(node.tag, path))
547  # Allow children parsers to adopt this current path (if modified with id_var)
548  return path
549 
550  def add_to_xml(self, obj, node):
551  if self.parent:
552  self.parent.add_to_xml(obj, node)
553  for attribute in self.attributes:
554  attribute.add_to_xml(obj, node)
555  for element in self.scalars:
556  element.add_to_xml(obj, node)
557  # Now add in aggregates
558  if self.aggregates:
559  obj.add_aggregates_to_xml(node)
560 
561 
563  """ Raw python object for yaml / xml representation """
564  XML_REFL = None
565 
566  def get_refl_vars(self):
567  return self.XML_REFL.vars
568 
569  def check_valid(self):
570  pass
571 
572  def pre_write_xml(self):
573  """ If anything needs to be converted prior to dumping to xml
574  i.e., getting the names of objects and such """
575  pass
576 
577  def write_xml(self, node):
578  """ Adds contents directly to XML node """
579  self.check_valid()
580  self.pre_write_xml()
581  self.XML_REFL.add_to_xml(self, node)
582 
583  def to_xml(self):
584  """ Creates an overarching tag and adds its contents to the node """
585  tag = self.XML_REFL.tag
586  assert tag is not None, "Must define 'tag' in reflection to use this function" # noqa
587  doc = ET.Element(tag)
588  self.write_xml(doc)
589  return doc
590 
591  def to_xml_string(self, addHeader=True):
592  return xml_string(self.to_xml(), addHeader)
593 
594  def post_read_xml(self):
595  pass
596 
597  def read_xml(self, node, path):
598  self.XML_REFL.set_from_xml(self, node, path)
599  self.post_read_xml()
600  try:
601  self.check_valid()
602  except ParseError:
603  raise
604  except Exception as e:
605  raise ParseError(e, path)
606 
607  @classmethod
608  def from_xml(cls, node, path):
609  cur_type = get_type(cls)
610  return cur_type.from_xml(node, path)
611 
612  @classmethod
613  def from_xml_string(cls, xml_string):
614  node = ET.fromstring(xml_string)
615  path = Path(cls.XML_REFL.tag, tree=ET.ElementTree(node))
616  return cls.from_xml(node, path)
617 
618  @classmethod
619  def from_xml_file(cls, file_path):
620  xml_string = open(file_path, 'r').read()
621  return cls.from_xml_string(xml_string)
622 
623  # Confusing distinction between loading code in object and reflection
624  # registry thing...
625 
626  def get_aggregate_list(self, xml_var):
627  var = self.XML_REFL.paramMap[xml_var].var
628  values = getattr(self, var)
629  assert isinstance(values, list)
630  return values
631 
632  def aggregate_init(self):
633  """ Must be called in constructor! """
634  self.aggregate_order = []
635  # Store this info in the loaded object??? Nah
636  self.aggregate_type = {}
637 
638  def add_aggregate(self, xml_var, obj):
639  """ NOTE: One must keep careful track of aggregate types for this system.
640  Can use 'lump_aggregates()' before writing if you don't care. """
641  self.get_aggregate_list(xml_var).append(obj)
642  self.aggregate_order.append(obj)
643  self.aggregate_type[obj] = xml_var
644 
645  def add_aggregates_to_xml(self, node):
646  for value in self.aggregate_order:
647  typeName = self.aggregate_type[value]
648  element = self.XML_REFL.element_map[typeName]
649  element.add_scalar_to_xml(node, value)
650 
651  def remove_aggregate(self, obj):
652  self.aggregate_order.remove(obj)
653  xml_var = self.aggregate_type[obj]
654  del self.aggregate_type[obj]
655  self.get_aggregate_list(xml_var).remove(obj)
656 
657  def lump_aggregates(self):
658  """ Put all aggregate types together, just because """
659  self.aggregate_init()
660  for param in self.XML_REFL.aggregates:
661  for obj in self.get_aggregate_list(param.xml_var):
662  self.add_aggregate(param.var, obj)
663 
664  """ Compatibility """
665 
666  def parse(self, xml_string):
667  node = ET.fromstring(xml_string)
668  path = Path(self.XML_REFL.tag, tree=ET.ElementTree(node))
669  self.read_xml(node, path)
670  return self
671 
672 
673 # Really common types
674 # Better name: element_with_name? Attributed element?
675 add_type('element_name', SimpleElementType('name', str))
676 add_type('element_value', SimpleElementType('value', float))
677 
678 # Add in common vector types so they aren't absorbed into the namespaces
679 get_type('vector3')
680 get_type('vector4')
681 get_type('vector6')
def on_error_stderr(message)
Definition: core.py:30
def set_from_string(self, obj, value)
Definition: core.py:327
def start_namespace(namespace)
Definition: core.py:44
def from_xml_file(cls, file_path)
Definition: core.py:619
def get_aggregate_list(self, xml_var)
Definition: core.py:626
def __init__(self, xml_var, value_type, required=True, default=None, var=None)
Definition: core.py:323
def add_to_xml(self, obj, parent)
Definition: core.py:363
def __init__(self, params=[], parent_cls=None, tag=None)
Definition: core.py:406
def __init__(self, xml_var, value_type, var=None, is_raw=False)
Definition: core.py:382
def __init__(self, xml_var, value_type, required=True, default=None, var=None)
Definition: core.py:300
def read_xml(self, node, path)
Definition: core.py:597
def add_aggregate(self, xml_var, obj)
Definition: core.py:638
def add_type(key, value)
Definition: core.py:58
def write_xml(self, node, value)
Definition: core.py:135
def equals(self, aValues, bValues)
Definition: core.py:166
def set_from_xml(self, obj, node, path)
Definition: core.py:359
def reflect(cls, args, kwargs)
Definition: core.py:19
def __init__(self, tag, parent=None, suffix="", tree=None)
Definition: core.py:106
def set_from_xml(self, obj, node, path, info=None)
Definition: core.py:459
def add_scalar_to_xml(self, parent, value)
Definition: core.py:373
def add_from_xml(self, obj, node, path)
Definition: core.py:389
def from_xml_string(cls, xml_string)
Definition: core.py:613
def xml_string(rootXml, addHeader=True)
Definition: basics.py:9
def __init__(self, attribute, value_type)
Definition: core.py:213
def from_xml(self, node, path)
Definition: core.py:194
def __init__(self, xml_var, value_type, required=True, default=None, var=None, is_raw=False)
Definition: core.py:354
def __init__(self, name, typeMap)
Definition: core.py:240
def to_xml_string(self, addHeader=True)
Definition: core.py:591
def parse(self, xml_string)
Definition: core.py:666
def from_xml(cls, node, path)
Definition: core.py:608
def write_xml(self, node, value)
Definition: core.py:197


urdfdom_py
Author(s): Thomas Moulard, David Lu, Kelsey Hawkins, Antonio El Khoury, Eric Cousineau, Ioan Sucan , Jackie Kay
autogenerated on Mon Feb 28 2022 23:58:25