diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aff8963..0b9714a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,8 @@ Changelog for package urdfdom_py ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* Deprecate internal details of `xml_reflection` classes. + 0.4.0 (2018-02-21) ------------------ * Add Link.visual and Link.collision properties (`#28 `_) diff --git a/README.md b/README.md index 5d2f20c..80782bc 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,37 @@ # urdf_parser_py -## Development Setup +## Authors -You must manually run `setup.py`. For catkin development, you can install to $ws/../build/lib/pythonX.Y/dist-packages via +* Thomas Moulard - `urdfpy` implementation, integration +* David Lu - `urdf_python` implementation, integration +* Kelsey Hawkins - `urdf_parser_python` implementation, integration +* Antonio El Khoury - bugfixes +* Eric Cousineau - reflection +* Ioan Sucan +* Jackie Kay +* Chris LaLancette (maintainer) +* Shane Loretz (maintainer) - devel_prefix=$(cd $(catkin_find --first-only)/.. && pwd) - cd ../urdf_parser_py - python setup.py install --install-layout deb --prefix $devel_prefix +## Features -## Authors +* URDF +* SDF (very basic coverage) +* XML Saving / Loading + * Some attempts to preserve original ordering; comments are stripped out, + however. + +## Todo + +1. Deprecate public access to `xml_reflection`. +2. Make a direct, two-way URDF <-> SDF converter when kinematics are not an + issue. + +## Development Setup + +For ease of developing, you may manually run `setup.py`. +For catkin development, you can install to +`${ws}/../build/lib/pythonX.Y/dist-packages` via: -* Thomas Moulard - `urdfpy` implementation, integration -* David Lu - `urdf_python` implementation, integration -* Kelsey Hawkins - `urdf_parser_python` implementation, integration -* Antonio El Khoury - bugfixes -* Eric Cousineau - reflection update - -## Reflection - -This an attempt to generalize the structure of the URDF via reflection to make it easier to extend. This concept is taken from Gazebo's SDF structure, and was done with SDF in mind to a) make an SDF parser and b) make a simple converter between URDF and SDF. - -### Changes - -* Features: - * Transmission and basic Gazebo nodes. - * General aggregate types, preserving order - * Dumping to YAML, used for printing to string (dictionaries do not preserve attribute ordering) -* XML Parsing: minidom has been swapped out with lxml.etree, but it should not be hard to change that back. Maybe Sax could be used for event-driven parsing. -* API: - * Loading methods rely primarily on instance methods rather than static methods, mirroring Gazebo's SDF construct-then-load method - * Renamed static `parse_xml()` to `from_xml()`, and renamed `load_*` methods to `from_*` if they are static - -### Todo - -1. Support additional formats (SDF, drakeURDF, etc.) - * Parse Gazebo's SDF definition files at some point? For speed's sake, parse it and have it generate code to use? - * Consider auto-generating modules from schemas such as [urdf.xsd](https://github.com/ros/urdfdom/blob/master/xsd/urdf.xsd). This can extend to [SDF](http://sdformat.org/schemas/model.xsd), [drakeURDF](https://github.com/RobotLocomotion/drake/blob/master/drake/doc/drakeURDF.xsd). -2. Make a direct, two-way URDF <-> SDF converter. - * Gazebo has the ability to load URDFs and save SDFs, but it lumps everything together -3. Consider a cleaner implementation for reflection. - * Make the names a little clearer, especially the fact that `from_xml` and `to_xml` write to a node, but do not create a new one. - * Abstraction layer is not clear. Should explicitly use abstract classes, and try to really clarify the dispatch order (`xmlr.Element`, `xmlr.Param`, `xmlr.Object`, etc.) -4. Figure out good policy for handling default methods. If saving to XML, write out default values, or leave them out for brevity (and to leave it open for change)? Might be best to add that as an option. -5. Find a lightweight package that can handle the reflection aspect more elegantly. Enthought traits? IPython's spinoff of traits? + devel_prefix=$(cd $(catkin_find --first-only)/.. && pwd) + cd ../urdf_parser_py + python setup.py install --install-layout deb --prefix ${devel_prefix} diff --git a/package.xml b/package.xml index ac6218e..d396400 100644 --- a/package.xml +++ b/package.xml @@ -10,7 +10,7 @@ David Lu Kelsey Hawkins Antonio El Khoury - Eric Cousineau + Eric Cousineau Ioan Sucan Jackie Kay diff --git a/src/urdf_parser_py/__init__.py b/src/urdf_parser_py/__init__.py index e69de29..859622d 100644 --- a/src/urdf_parser_py/__init__.py +++ b/src/urdf_parser_py/__init__.py @@ -0,0 +1,68 @@ +""" +Python implementation of the URDF parser. +""" + +import functools +import warnings + + +class _DeprecatedDescriptor(object): + def __init__(self, attr): + self._attr = attr + + def _warn(self): + raise NotImplemented + + def __get__(self, obj, objtype): + self._warn() + if obj is None: + return getattr(objtype, self._attr) + else: + return getattr(obj, self._attr) + + def __set__(self, obj, value): + self._warn() + setattr(obj, self._attr, value) + + def __del__(self, obj): + self._warn() + delattr(obj, self._attr) + + +class _NowPrivateDescriptor(_DeprecatedDescriptor): + # Implements the descriptor interface to warn about deprecated access. + def __init__(self, private): + _DeprecatedDescriptor.__init__(self, private) + self._private = private + self._old_public = self._private.lstrip('_') + self.__doc__ = "Deprecated propery '{}'".format(self._old_public) + + def _warn(self): + warnings.warn( + "'{}' is deprecated, and will be removed in future releases." + .format(self._old_public), + category=DeprecationWarning, stacklevel=1) + + +def _now_private_property(private): + # Indicates that a property (or method) is now private. + return _NowPrivateDescriptor(private) + + +class _RenamedDescriptor(_DeprecatedDescriptor): + # Implements the descriptor interface to warn about deprecated access. + def __init__(self, old, new): + _DeprecatedDescriptor.__init__(self, new) + self._old = old + self._new = new + self.__doc__ = "Deprecated propery '{}'".format(self._old) + + def _warn(self): + warnings.warn( + "'{}' is deprecated, please use '{}' instead.".format( + self._old, self._new), + category=DeprecationWarning, stacklevel=1) + + +def _renamed_property(old, new): + return _RenamedDescriptor(old, new) diff --git a/src/urdf_parser_py/_xml_reflection.py b/src/urdf_parser_py/_xml_reflection.py new file mode 100644 index 0000000..29d1796 --- /dev/null +++ b/src/urdf_parser_py/_xml_reflection.py @@ -0,0 +1,5 @@ +# TODO(eacousineau): Move all symbols from `.xml_reflection` into here. +from urdf_parser_py.xml_reflection.basics import * +# Import full module so that tests can easily monkey patch `on_error`. +from urdf_parser_py.xml_reflection import core +from urdf_parser_py.xml_reflection.core import * diff --git a/src/urdf_parser_py/sdf.py b/src/urdf_parser_py/sdf.py index 67d25bd..9c9b5e9 100644 --- a/src/urdf_parser_py/sdf.py +++ b/src/urdf_parser_py/sdf.py @@ -1,12 +1,10 @@ -from urdf_parser_py.xml_reflection.basics import * -import urdf_parser_py.xml_reflection as xmlr +from urdf_parser_py import _now_private_property +import urdf_parser_py._xml_reflection as _xmlr -# What is the scope of plugins? Model, World, Sensor? +_xmlr.start_namespace('sdf') -xmlr.start_namespace('sdf') - -class Pose(xmlr.Object): +class Pose(_xmlr.Object): def __init__(self, vec=None, extra=None): self.xyz = None self.rpy = None @@ -32,37 +30,37 @@ def as_vec(self): rpy = self.rpy if self.rpy else [0, 0, 0] return xyz + rpy - def read_xml(self, node): + def _read_xml(self, node): # Better way to do this? Define type? - vec = get_type('vector6').read_xml(node) - self.load_vec(vec) + vec = _xmlr.get_type('vector6').read_xml_value(node) + self.from_vec(vec) - def write_xml(self, node): + def _write_xml(self, node): vec = self.as_vec() - get_type('vector6').write_xml(node, vec) + _xmlr.get_type('vector6').write_xml_value(node, vec) - def check_valid(self): + def _check_valid(self): assert self.xyz is not None or self.rpy is not None -name_attribute = xmlr.Attribute('name', str) -pose_element = xmlr.Element('pose', Pose, False) +_name_attribute = _xmlr.Attribute('name', str) +_pose_element = _xmlr.Element('pose', Pose, required=False) -class Entity(xmlr.Object): +class Entity(_xmlr.Object): def __init__(self, name=None, pose=None): self.name = name self.pose = pose -xmlr.reflect(Entity, params=[ - name_attribute, - pose_element +_xmlr.reflect(Entity, params=[ + _name_attribute, + _pose_element ]) -class Inertia(xmlr.Object): - KEYS = ['ixx', 'ixy', 'ixz', 'iyy', 'iyz', 'izz'] +class Inertia(_xmlr.Object): + _KEYS = ['ixx', 'ixy', 'ixz', 'iyy', 'iyz', 'izz'] def __init__(self, ixx=0.0, ixy=0.0, ixz=0.0, iyy=0.0, iyz=0.0, izz=0.0): self.ixx = ixx @@ -79,24 +77,21 @@ def to_matrix(self): [self.ixz, self.iyz, self.izz]] -xmlr.reflect(Inertia, - params=[xmlr.Element(key, float) for key in Inertia.KEYS]) - -# Pretty much copy-paste... Better method? -# Use multiple inheritance to separate the objects out so they are unique? +_xmlr.reflect(Inertia, tag='inertia', + params=[_xmlr.Element(key, float) for key in Inertia._KEYS]) -class Inertial(xmlr.Object): +class Inertial(_xmlr.Object): def __init__(self, mass=0.0, inertia=None, pose=None): self.mass = mass self.inertia = inertia self.pose = pose -xmlr.reflect(Inertial, params=[ - xmlr.Element('mass', float), - xmlr.Element('inertia', Inertia), - pose_element +_xmlr.reflect(Inertial, tag='inertial', params=[ + _xmlr.Element('mass', float), + _xmlr.Element('inertia', Inertia), + _pose_element ]) @@ -107,14 +102,21 @@ def __init__(self, name=None, pose=None, inertial=None, kinematic=False): self.kinematic = kinematic -xmlr.reflect(Link, parent_cls=Entity, params=[ - xmlr.Element('inertial', Inertial), - xmlr.Attribute('kinematic', bool, False), - xmlr.AggregateElement('visual', Visual, var='visuals'), - xmlr.AggregateElement('collision', Collision, var='collisions') +_xmlr.reflect(Link, tag='link', parent_cls=Entity, params=[ + _xmlr.Element('inertial', Inertial), + _xmlr.Attribute('kinematic', bool, False), + _xmlr.AggregateElement('visual', Visual, var='visuals'), + _xmlr.AggregateElement('collision', Collision, var='collisions') ]) +class Joint(Entity): + pass + + +_xmlr.reflect(Joint, tag='joint', parent_cls=Entity, params=[]) + + class Model(Entity): def __init__(self, name=None, pose=None): Entity.__init__(self, name, pose) @@ -123,10 +125,10 @@ def __init__(self, name=None, pose=None): self.plugins = [] -xmlr.reflect(Model, parent_cls=Entity, params=[ - xmlr.AggregateElement('link', Link, var='links'), - xmlr.AggregateElement('joint', Joint, var='joints'), - xmlr.AggregateElement('plugin', Plugin, var='plugins') +_xmlr.reflect(Model, parent_cls=Entity, params=[ + _xmlr.AggregateElement('link', Link, var='links'), + _xmlr.AggregateElement('joint', Joint, var='joints'), + _xmlr.AggregateElement('plugin', Plugin, var='plugins') ]) -xmlr.end_namespace('sdf') +_xmlr.end_namespace('sdf') diff --git a/src/urdf_parser_py/urdf.py b/src/urdf_parser_py/urdf.py index 2b08437..642bc61 100644 --- a/src/urdf_parser_py/urdf.py +++ b/src/urdf_parser_py/urdf.py @@ -1,26 +1,20 @@ -from urdf_parser_py.xml_reflection.basics import * -import urdf_parser_py.xml_reflection as xmlr +from urdf_parser_py import _now_private_property +import urdf_parser_py._xml_reflection as _xmlr -# Add a 'namespace' for names to avoid a conflict between URDF and SDF? -# A type registry? How to scope that? Just make a 'global' type pointer? -# Or just qualify names? urdf.geometric, sdf.geometric +_xmlr.start_namespace('urdf') -xmlr.start_namespace('urdf') +_xmlr.add_type('element_link', _xmlr.SimpleElementType('link', str)) +_xmlr.add_type('element_xyz', _xmlr.SimpleElementType('xyz', 'vector3')) -xmlr.add_type('element_link', xmlr.SimpleElementType('link', str)) -xmlr.add_type('element_xyz', xmlr.SimpleElementType('xyz', 'vector3')) -verbose = True - - -class Pose(xmlr.Object): +class Pose(_xmlr.Object): def __init__(self, xyz=None, rpy=None): self.xyz = xyz self.rpy = rpy - def check_valid(self): - assert (self.xyz is None or len(self.xyz) == 3) and \ - (self.rpy is None or len(self.rpy) == 3) + def _check_valid(self): + assert (self.xyz is None or len(self.xyz) == 3 and + self.rpy is None or len(self.rpy) == 3) # Aliases for backwards compatibility @property @@ -36,18 +30,18 @@ def position(self): return self.xyz def position(self, value): self.xyz = value -xmlr.reflect(Pose, tag='origin', params=[ - xmlr.Attribute('xyz', 'vector3', False, default=[0, 0, 0]), - xmlr.Attribute('rpy', 'vector3', False, default=[0, 0, 0]) +_xmlr.reflect(Pose, tag='origin', params=[ + _xmlr.Attribute('xyz', 'vector3', required=False, default=[0, 0, 0]), + _xmlr.Attribute('rpy', 'vector3', required=False, default=[0, 0, 0]) ]) # Common stuff -name_attribute = xmlr.Attribute('name', str) -origin_element = xmlr.Element('origin', Pose, False) +_name_attribute = _xmlr.Attribute('name', str) +_origin_element = _xmlr.Element('origin', Pose, required=False) -class Color(xmlr.Object): +class Color(_xmlr.Object): def __init__(self, *args): # What about named colors? count = len(args) @@ -64,150 +58,154 @@ def __init__(self, *args): raise Exception('Invalid color argument count') -xmlr.reflect(Color, tag='color', params=[ - xmlr.Attribute('rgba', 'vector4') +_xmlr.reflect(Color, tag='color', params=[ + _xmlr.Attribute('rgba', 'vector4') ]) -class JointDynamics(xmlr.Object): +class JointDynamics(_xmlr.Object): def __init__(self, damping=None, friction=None): self.damping = damping self.friction = friction -xmlr.reflect(JointDynamics, tag='dynamics', params=[ - xmlr.Attribute('damping', float, False), - xmlr.Attribute('friction', float, False) +_xmlr.reflect(JointDynamics, tag='dynamics', params=[ + _xmlr.Attribute('damping', float, required=False), + _xmlr.Attribute('friction', float, required=False) ]) -class Box(xmlr.Object): +class Box(_xmlr.Object): def __init__(self, size=None): self.size = size -xmlr.reflect(Box, tag='box', params=[ - xmlr.Attribute('size', 'vector3') +_xmlr.reflect(Box, tag='box', params=[ + _xmlr.Attribute('size', 'vector3') ]) -class Cylinder(xmlr.Object): +class Cylinder(_xmlr.Object): def __init__(self, radius=0.0, length=0.0): self.radius = radius self.length = length -xmlr.reflect(Cylinder, tag='cylinder', params=[ - xmlr.Attribute('radius', float), - xmlr.Attribute('length', float) +_xmlr.reflect(Cylinder, tag='cylinder', params=[ + _xmlr.Attribute('radius', float), + _xmlr.Attribute('length', float) ]) -class Sphere(xmlr.Object): +class Sphere(_xmlr.Object): def __init__(self, radius=0.0): self.radius = radius -xmlr.reflect(Sphere, tag='sphere', params=[ - xmlr.Attribute('radius', float) +_xmlr.reflect(Sphere, tag='sphere', params=[ + _xmlr.Attribute('radius', float) ]) -class Mesh(xmlr.Object): +class Mesh(_xmlr.Object): def __init__(self, filename=None, scale=None): self.filename = filename self.scale = scale -xmlr.reflect(Mesh, tag='mesh', params=[ - xmlr.Attribute('filename', str), - xmlr.Attribute('scale', 'vector3', required=False) +_xmlr.reflect(Mesh, tag='mesh', params=[ + _xmlr.Attribute('filename', str), + _xmlr.Attribute('scale', 'vector3', required=False) ]) -class GeometricType(xmlr.ValueType): +class _GeometricType(_xmlr.ValueType): def __init__(self): - self.factory = xmlr.FactoryType('geometric', { + self.factory = _xmlr.FactoryType('geometric', { 'box': Box, 'cylinder': Cylinder, 'sphere': Sphere, 'mesh': Mesh }) - def from_xml(self, node, path): - children = xml_children(node) + def read_xml_value(self, node, path): + children = _xmlr.xml_children(node) assert len(children) == 1, 'One element only for geometric' - return self.factory.from_xml(children[0], path=path) + return self.factory.read_xml_value(children[0], path=path) - def write_xml(self, node, obj): + def write_xml_value(self, node, obj): name = self.factory.get_name(obj) - child = node_add(node, name) - obj.write_xml(child) + child = _xmlr.node_add(node, name) + obj._write_xml(child) + + +# TODO(eacousineau): Deprecate public access. +GeometricType = _GeometricType -xmlr.add_type('geometric', GeometricType()) +_xmlr.add_type('geometric', _GeometricType()) -class Collision(xmlr.Object): +class Collision(_xmlr.Object): def __init__(self, geometry=None, origin=None): self.geometry = geometry self.origin = origin -xmlr.reflect(Collision, tag='collision', params=[ - origin_element, - xmlr.Element('geometry', 'geometric') +_xmlr.reflect(Collision, tag='collision', params=[ + _origin_element, + _xmlr.Element('geometry', 'geometric') ]) -class Texture(xmlr.Object): +class Texture(_xmlr.Object): def __init__(self, filename=None): self.filename = filename -xmlr.reflect(Texture, tag='texture', params=[ - xmlr.Attribute('filename', str) +_xmlr.reflect(Texture, tag='texture', params=[ + _xmlr.Attribute('filename', str) ]) -class Material(xmlr.Object): +class Material(_xmlr.Object): def __init__(self, name=None, color=None, texture=None): self.name = name self.color = color self.texture = texture - def check_valid(self): + def _check_valid(self): if self.color is None and self.texture is None: - xmlr.on_error("Material has neither a color nor texture.") + _xmlr.on_error("Material has neither a color nor texture.") -xmlr.reflect(Material, tag='material', params=[ - name_attribute, - xmlr.Element('color', Color, False), - xmlr.Element('texture', Texture, False) +_xmlr.reflect(Material, tag='material', params=[ + _name_attribute, + _xmlr.Element('color', Color, False), + _xmlr.Element('texture', Texture, False) ]) class LinkMaterial(Material): - def check_valid(self): + def _check_valid(self): pass -class Visual(xmlr.Object): +class Visual(_xmlr.Object): def __init__(self, geometry=None, material=None, origin=None): self.geometry = geometry self.material = material self.origin = origin -xmlr.reflect(Visual, tag='visual', params=[ - origin_element, - xmlr.Element('geometry', 'geometric'), - xmlr.Element('material', LinkMaterial, False) +_xmlr.reflect(Visual, tag='visual', params=[ + _origin_element, + _xmlr.Element('geometry', 'geometric'), + _xmlr.Element('material', LinkMaterial, False) ]) -class Inertia(xmlr.Object): +class Inertia(_xmlr.Object): KEYS = ['ixx', 'ixy', 'ixz', 'iyy', 'iyz', 'izz'] def __init__(self, ixx=0.0, ixy=0.0, ixz=0.0, iyy=0.0, iyz=0.0, izz=0.0): @@ -225,38 +223,38 @@ def to_matrix(self): [self.ixz, self.iyz, self.izz]] -xmlr.reflect(Inertia, tag='inertia', - params=[xmlr.Attribute(key, float) for key in Inertia.KEYS]) +_xmlr.reflect(Inertia, tag='inertia', + params=[_xmlr.Attribute(key, float) for key in Inertia.KEYS]) -class Inertial(xmlr.Object): +class Inertial(_xmlr.Object): def __init__(self, mass=0.0, inertia=None, origin=None): self.mass = mass self.inertia = inertia self.origin = origin -xmlr.reflect(Inertial, tag='inertial', params=[ - origin_element, - xmlr.Element('mass', 'element_value'), - xmlr.Element('inertia', Inertia, False) +_xmlr.reflect(Inertial, tag='inertial', params=[ + _origin_element, + _xmlr.Element('mass', 'element_value'), + _xmlr.Element('inertia', Inertia, required=False) ]) # FIXME: we are missing the reference position here. -class JointCalibration(xmlr.Object): +class JointCalibration(_xmlr.Object): def __init__(self, rising=None, falling=None): self.rising = rising self.falling = falling -xmlr.reflect(JointCalibration, tag='calibration', params=[ - xmlr.Attribute('rising', float, False, 0), - xmlr.Attribute('falling', float, False, 0) +_xmlr.reflect(JointCalibration, tag='calibration', params=[ + _xmlr.Attribute('rising', float, required=False, default=0), + _xmlr.Attribute('falling', float, required=False, default=0) ]) -class JointLimit(xmlr.Object): +class JointLimit(_xmlr.Object): def __init__(self, effort=None, velocity=None, lower=None, upper=None): self.effort = effort self.velocity = velocity @@ -264,31 +262,31 @@ def __init__(self, effort=None, velocity=None, lower=None, upper=None): self.upper = upper -xmlr.reflect(JointLimit, tag='limit', params=[ - xmlr.Attribute('effort', float), - xmlr.Attribute('lower', float, False, 0), - xmlr.Attribute('upper', float, False, 0), - xmlr.Attribute('velocity', float) +_xmlr.reflect(JointLimit, tag='limit', params=[ + _xmlr.Attribute('effort', float), + _xmlr.Attribute('lower', float, required=False, default=0), + _xmlr.Attribute('upper', float, required=False, default=0), + _xmlr.Attribute('velocity', float) ]) # FIXME: we are missing __str__ here. -class JointMimic(xmlr.Object): +class JointMimic(_xmlr.Object): def __init__(self, joint_name=None, multiplier=None, offset=None): self.joint = joint_name self.multiplier = multiplier self.offset = offset -xmlr.reflect(JointMimic, tag='mimic', params=[ - xmlr.Attribute('joint', str), - xmlr.Attribute('multiplier', float, False), - xmlr.Attribute('offset', float, False) +_xmlr.reflect(JointMimic, tag='mimic', params=[ + _xmlr.Attribute('joint', str), + _xmlr.Attribute('multiplier', float, required=False), + _xmlr.Attribute('offset', float, required=False) ]) -class SafetyController(xmlr.Object): +class SafetyController(_xmlr.Object): def __init__(self, velocity=None, position=None, lower=None, upper=None): self.k_velocity = velocity self.k_position = position @@ -296,15 +294,15 @@ def __init__(self, velocity=None, position=None, lower=None, upper=None): self.soft_upper_limit = upper -xmlr.reflect(SafetyController, tag='safety_controller', params=[ - xmlr.Attribute('k_velocity', float), - xmlr.Attribute('k_position', float, False, 0), - xmlr.Attribute('soft_lower_limit', float, False, 0), - xmlr.Attribute('soft_upper_limit', float, False, 0) +_xmlr.reflect(SafetyController, tag='safety_controller', params=[ + _xmlr.Attribute('k_velocity', float), + _xmlr.Attribute('k_position', float, required=False, default=0), + _xmlr.Attribute('soft_lower_limit', float, required=False, default=0), + _xmlr.Attribute('soft_upper_limit', float, required=False, default=0) ]) -class Joint(xmlr.Object): +class Joint(_xmlr.Object): TYPES = ['unknown', 'revolute', 'continuous', 'prismatic', 'floating', 'planar', 'fixed'] @@ -324,7 +322,7 @@ def __init__(self, name=None, parent=None, child=None, joint_type=None, self.calibration = calibration self.mimic = mimic - def check_valid(self): + def _check_valid(self): assert self.type in self.TYPES, "Invalid joint type: {}".format(self.type) # noqa # Aliases @@ -334,25 +332,25 @@ def joint_type(self): return self.type @joint_type.setter def joint_type(self, value): self.type = value -xmlr.reflect(Joint, tag='joint', params=[ - name_attribute, - xmlr.Attribute('type', str), - origin_element, - xmlr.Element('axis', 'element_xyz', False), - xmlr.Element('parent', 'element_link'), - xmlr.Element('child', 'element_link'), - xmlr.Element('limit', JointLimit, False), - xmlr.Element('dynamics', JointDynamics, False), - xmlr.Element('safety_controller', SafetyController, False), - xmlr.Element('calibration', JointCalibration, False), - xmlr.Element('mimic', JointMimic, False), +_xmlr.reflect(Joint, tag='joint', params=[ + _name_attribute, + _xmlr.Attribute('type', str), + _origin_element, + _xmlr.Element('axis', 'element_xyz', required=False), + _xmlr.Element('parent', 'element_link'), + _xmlr.Element('child', 'element_link'), + _xmlr.Element('limit', JointLimit, required=False), + _xmlr.Element('dynamics', JointDynamics, required=False), + _xmlr.Element('safety_controller', SafetyController, required=False), + _xmlr.Element('calibration', JointCalibration, required=False), + _xmlr.Element('mimic', JointMimic, required=False), ]) -class Link(xmlr.Object): +class Link(_xmlr.Object): def __init__(self, name=None, visual=None, inertial=None, collision=None, origin=None): - self.aggregate_init() + self._aggregate_init() self.name = name self.visuals = [] self.inertial = inertial @@ -388,16 +386,16 @@ def __set_collision(self, collision): collision = property(__get_collision, __set_collision) -xmlr.reflect(Link, tag='link', params=[ - name_attribute, - origin_element, - xmlr.AggregateElement('visual', Visual), - xmlr.AggregateElement('collision', Collision), - xmlr.Element('inertial', Inertial, False), +_xmlr.reflect(Link, tag='link', params=[ + _name_attribute, + _origin_element, + _xmlr.AggregateElement('visual', Visual), + _xmlr.AggregateElement('collision', Collision), + _xmlr.Element('inertial', Inertial, required=False), ]) -class PR2Transmission(xmlr.Object): +class PR2Transmission(_xmlr.Object): def __init__(self, name=None, joint=None, actuator=None, type=None, mechanicalReduction=1): self.name = name @@ -407,72 +405,72 @@ def __init__(self, name=None, joint=None, actuator=None, type=None, self.mechanicalReduction = mechanicalReduction -xmlr.reflect(PR2Transmission, tag='pr2_transmission', params=[ - name_attribute, - xmlr.Attribute('type', str), - xmlr.Element('joint', 'element_name'), - xmlr.Element('actuator', 'element_name'), - xmlr.Element('mechanicalReduction', float) +_xmlr.reflect(PR2Transmission, tag='pr2_transmission', params=[ + _name_attribute, + _xmlr.Attribute('type', str), + _xmlr.Element('joint', 'element_name'), + _xmlr.Element('actuator', 'element_name'), + _xmlr.Element('mechanicalReduction', float) ]) -class Actuator(xmlr.Object): +class Actuator(_xmlr.Object): def __init__(self, name=None, mechanicalReduction=1): self.name = name self.mechanicalReduction = None -xmlr.reflect(Actuator, tag='actuator', params=[ - name_attribute, - xmlr.Element('mechanicalReduction', float, required=False) +_xmlr.reflect(Actuator, tag='actuator', params=[ + _name_attribute, + _xmlr.Element('mechanicalReduction', float, required=False) ]) -class TransmissionJoint(xmlr.Object): +class TransmissionJoint(_xmlr.Object): def __init__(self, name=None): - self.aggregate_init() + self._aggregate_init() self.name = name self.hardwareInterfaces = [] - def check_valid(self): + def _check_valid(self): assert len(self.hardwareInterfaces) > 0, "no hardwareInterface defined" -xmlr.reflect(TransmissionJoint, tag='joint', params=[ - name_attribute, - xmlr.AggregateElement('hardwareInterface', str), +_xmlr.reflect(TransmissionJoint, tag='joint', params=[ + _name_attribute, + _xmlr.AggregateElement('hardwareInterface', str), ]) -class Transmission(xmlr.Object): +class Transmission(_xmlr.Object): """ New format: http://wiki.ros.org/urdf/XML/Transmission """ def __init__(self, name=None): - self.aggregate_init() + self._aggregate_init() self.name = name self.joints = [] self.actuators = [] - def check_valid(self): + def _check_valid(self): assert len(self.joints) > 0, "no joint defined" assert len(self.actuators) > 0, "no actuator defined" -xmlr.reflect(Transmission, tag='new_transmission', params=[ - name_attribute, - xmlr.Element('type', str), - xmlr.AggregateElement('joint', TransmissionJoint), - xmlr.AggregateElement('actuator', Actuator) +_xmlr.reflect(Transmission, tag='new_transmission', params=[ + _name_attribute, + _xmlr.Element('type', str), + _xmlr.AggregateElement('joint', TransmissionJoint), + _xmlr.AggregateElement('actuator', Actuator) ]) -xmlr.add_type('transmission', - xmlr.DuckTypedFactory('transmission', +_xmlr.add_type('transmission', + _xmlr.DuckTypedFactory('transmission', [Transmission, PR2Transmission])) -class Robot(xmlr.Object): +class Robot(_xmlr.Object): def __init__(self, name=None): - self.aggregate_init() + self._aggregate_init() self.name = name self.joints = [] @@ -487,8 +485,8 @@ def __init__(self, name=None): self.parent_map = {} self.child_map = {} - def add_aggregate(self, typeName, elem): - xmlr.Object.add_aggregate(self, typeName, elem) + def _add_aggregate(self, typeName, elem): + _xmlr.Object._add_aggregate(self, typeName, elem) if typeName == 'joint': joint = elem @@ -503,10 +501,10 @@ def add_aggregate(self, typeName, elem): self.link_map[link.name] = link def add_link(self, link): - self.add_aggregate('link', link) + self._add_aggregate('link', link) def add_joint(self, joint): - self.add_aggregate('joint', joint) + self._add_aggregate('joint', joint) def get_chain(self, root, tip, joints=True, links=True, fixed=True): chain = [] @@ -546,16 +544,16 @@ def from_parameter_server(cls, key='robot_description'): return cls.from_xml_string(rospy.get_param(key)) -xmlr.reflect(Robot, tag='robot', params=[ - xmlr.Attribute('name', str, False), # Is 'name' a required attribute? - xmlr.AggregateElement('link', Link), - xmlr.AggregateElement('joint', Joint), - xmlr.AggregateElement('gazebo', xmlr.RawType()), - xmlr.AggregateElement('transmission', 'transmission'), - xmlr.AggregateElement('material', Material) +_xmlr.reflect(Robot, tag='robot', params=[ + _xmlr.Attribute('name', str, required=False), # Is 'name' a required attribute? + _xmlr.AggregateElement('link', Link), + _xmlr.AggregateElement('joint', Joint), + _xmlr.AggregateElement('gazebo', _xmlr.RawType()), + _xmlr.AggregateElement('transmission', 'transmission'), + _xmlr.AggregateElement('material', Material) ]) # Make an alias URDF = Robot -xmlr.end_namespace() +_xmlr.end_namespace() diff --git a/src/urdf_parser_py/xml_reflection/__init__.py b/src/urdf_parser_py/xml_reflection/__init__.py index 6685d18..93e9148 100644 --- a/src/urdf_parser_py/xml_reflection/__init__.py +++ b/src/urdf_parser_py/xml_reflection/__init__.py @@ -1 +1,2 @@ +# TODO(eacousineau): Deprecate public access. from urdf_parser_py.xml_reflection.core import * diff --git a/src/urdf_parser_py/xml_reflection/basics.py b/src/urdf_parser_py/xml_reflection/basics.py index fa71b59..1027a2c 100644 --- a/src/urdf_parser_py/xml_reflection/basics.py +++ b/src/urdf_parser_py/xml_reflection/basics.py @@ -1,7 +1,28 @@ -import string -import yaml import collections +import string +# TODO(eacousineau): Leverage tfoote's PR. +from xml.etree.ElementTree import ElementTree + from lxml import etree +import yaml + +# TODO(eacousineau): Deprecate public access. +from urdf_parser_py import _now_private_property + +__all__ = [ + "xml_string", + "dict_sub", + "node_add", + "pfloat", + "xml_children", + "isstring", + "to_yaml", + "SelectiveReflection", + "YamlReflection", + # Backwards compatibility. + "etree", +] + def xml_string(rootXml, addHeader=True): # Meh @@ -74,13 +95,15 @@ def to_yaml(obj): class SelectiveReflection(object): - def get_refl_vars(self): + def _get_refl_vars(self): return list(vars(self).keys()) + get_refl_vars = _now_private_property('_get_refl_vars') + class YamlReflection(SelectiveReflection): def to_yaml(self): - raw = dict((var, getattr(self, var)) for var in self.get_refl_vars()) + raw = dict((var, getattr(self, var)) for var in self._get_refl_vars()) return to_yaml(raw) def __str__(self): diff --git a/src/urdf_parser_py/xml_reflection/core.py b/src/urdf_parser_py/xml_reflection/core.py index 8939e77..92aa8bd 100644 --- a/src/urdf_parser_py/xml_reflection/core.py +++ b/src/urdf_parser_py/xml_reflection/core.py @@ -1,31 +1,59 @@ -from urdf_parser_py.xml_reflection.basics import * -import sys import copy +import sys -# @todo Get rid of "import *" -# @todo Make this work with decorators - -# Is this reflection or serialization? I think it's serialization... -# Rename? +# Backwards compatibility. +# TODO(eacousineau): Deprecate public access. +from urdf_parser_py import _now_private_property, _renamed_property +from urdf_parser_py.xml_reflection.basics import * -# Do parent operations after, to allow child to 'override' parameters? -# Need to make sure that duplicate entires do not get into the 'unset*' lists +__all__ = [ + "reflect", + "on_error", + "skip_default", + "value_types", + "value_type_prefix", + "start_namespace", + "end_namespace", + "add_type", + "get_type", + "make_type", + "Path", + "ParseError", + "ValueType", + "BasicType", + "ListType", + "VectorType", + "RawType", + "SimpleElementType", + "ObjectType", + "FactoryType", + "DuckTypedFactory", + "Param", + "Attribute", + "Element", + "AggregateElement", + "Info", + "Reflection", + "Object", +] + +# Unless otherwise stated, all functions and classes are not intedned to be +# user-visible. Once deprecation is complete and public access is removed, then +# these implementation details should not need to worry about backwards +# compatibility. def reflect(cls, *args, **kwargs): """ Simple wrapper to add XML reflection to an xml_reflection.Object class """ - cls.XML_REFL = Reflection(*args, **kwargs) + cls._XML_REFL = Reflection(*args, **kwargs) -# Rename 'write_xml' to 'write_xml' to have paired 'load/dump', and make -# 'pre_dump' and 'post_load'? -# When dumping to yaml, include tag name? -# How to incorporate line number and all that jazz? def on_error_stderr(message): - """ What to do on an error. This can be changed to raise an exception. """ sys.stderr.write(message + '\n') + +# What to do on an error. This can be changed to raise an exception. on_error = on_error_stderr @@ -52,6 +80,7 @@ def end_namespace(): def add_type(key, value): + """Adds a type to the regsitry.""" if isinstance(key, str): key = value_type_prefix + key assert key not in value_types @@ -59,7 +88,9 @@ def add_type(key, value): def get_type(cur_type): - """ Can wrap value types if needed """ + """Retrieves type from registry. + If this is not registered, it will be implicitly registered.""" + # TODO(eacousineau): Remove confusing implicit behavior. if value_type_prefix and isinstance(cur_type, str): # See if it exists in current 'namespace' curKey = value_type_prefix + cur_type @@ -76,6 +107,8 @@ def get_type(cur_type): def make_type(cur_type): + """Creates a wrapping `ValueType` instance for `cur_type`.""" + # TODO(eacousineau): Remove this, and use direct instances. if isinstance(cur_type, ValueType): return cur_type elif isinstance(cur_type, str): @@ -99,11 +132,13 @@ def make_type(cur_type): class Path(object): + """Records path information for producing XPath-like references in errors. + """ def __init__(self, tag, parent=None, suffix="", tree=None): - self.parent = parent - self.tag = tag - self.suffix = suffix - self.tree = tree # For validating general path (getting true XML path) + self.parent = parent + self.tag = tag + self.suffix = suffix + self.tree = tree # For validating general path (getting true XML path) def __str__(self): if self.parent is not None: @@ -114,30 +149,37 @@ def __str__(self): else: return self.suffix + class ParseError(Exception): + """Indicates a parser error at a given path.""" def __init__(self, e, path): - self.e = e - self.path = path - message = "ParseError in {}:\n{}".format(self.path, self.e) - super(ParseError, self).__init__(message) + self.e = e + self.path = path + message = "ParseError in {}:\n{}".format(self.path, self.e) + super(ParseError, self).__init__(message) class ValueType(object): - """ Primitive value type """ - - def from_xml(self, node, path): + """Primitive value type. Default semantics based on string parsing.""" + # TODO(eacousineau): Delegate string semantics to child class, so that this + # can be a pure ABC. + def read_xml_value(self, node, path): + """Reads value from a node and returns the value. + Can be overridden in child classes.""" return self.from_string(node.text) - def write_xml(self, node, value): - """ - If type has 'write_xml', this function should expect to have it's own - XML already created i.e., In Axis.to_sdf(self, node), 'node' would be - the 'axis' element. - @todo Add function that makes an XML node completely independently? + def write_xml_value(self, node, value): + """Writes value to a node (that must already exist). + Can be overridden in child classes. """ node.text = self.to_string(value) - def equals(self, a, b): + from_xml = _renamed_property('from_xml', 'read_xml_value') + write_xml = _renamed_property('write_xml', 'write_xml_value') + equals = _now_private_property('_equals') + + def _equals(self, a, b): + # TODO(eacousineau): Remove this. return a == b @@ -159,8 +201,9 @@ def to_string(self, values): def from_string(self, text): return text.split() - def equals(self, aValues, bValues): - return len(aValues) == len(bValues) and all(a == b for (a, b) in zip(aValues, bValues)) # noqa + def _equals(self, aValues, bValues): + return (len(aValues) == len(bValues) and + all(a == b for (a, b) in zip(aValues, bValues))) class VectorType(ListType): @@ -187,11 +230,11 @@ class RawType(ValueType): Simple, raw XML value. Need to bugfix putting this back into a document """ - def from_xml(self, node, path): + def read_xml_value(self, node, path): return node - def write_xml(self, node, value): - # @todo rying to insert an element at root level seems to screw up + def write_xml_value(self, node, value): + # @todo Trying to insert an element at root level seems to screw up # pretty printing children = xml_children(value) list(map(node.append, children)) @@ -200,6 +243,7 @@ def write_xml(self, node, value): node.set(attrib_key, attrib_value) + class SimpleElementType(ValueType): """ Extractor that retrieves data from an element, given a @@ -210,26 +254,28 @@ def __init__(self, attribute, value_type): self.attribute = attribute self.value_type = get_type(value_type) - def from_xml(self, node, path): + def read_xml_value(self, node, path): text = node.get(self.attribute) return self.value_type.from_string(text) - def write_xml(self, node, value): + def write_xml_value(self, node, value): text = self.value_type.to_string(value) node.set(self.attribute, text) class ObjectType(ValueType): + # Wraps an `Object` def __init__(self, cur_type): + assert issubclass(cur_type, Object) self.type = cur_type - def from_xml(self, node, path): + def read_xml_value(self, node, path): obj = self.type() - obj.read_xml(node, path) + obj._read_xml(node, path) return obj - def write_xml(self, node, obj): - obj.write_xml(node) + def write_xml_value(self, node, obj): + obj._write_xml(node) class FactoryType(ValueType): @@ -241,12 +287,12 @@ def __init__(self, name, typeMap): # Reverse lookup self.nameMap[value] = key - def from_xml(self, node, path): + def read_xml_value(self, node, path): cur_type = self.typeMap.get(node.tag) if cur_type is None: raise Exception("Invalid {} tag: {}".format(self.name, node.tag)) value_type = get_type(cur_type) - return value_type.from_xml(node, path) + return value_type.read_xml_value(node, path) def get_name(self, obj): cur_type = type(obj) @@ -255,21 +301,21 @@ def get_name(self, obj): raise Exception("Invalid {} type: {}".format(self.name, cur_type)) return name - def write_xml(self, node, obj): - obj.write_xml(node) + def write_xml_value(self, node, obj): + obj._write_xml(node) class DuckTypedFactory(ValueType): def __init__(self, name, typeOrder): self.name = name assert len(typeOrder) > 0 - self.type_order = typeOrder + self.type_order = [get_type(x) for x in typeOrder] - def from_xml(self, node, path): + def read_xml_value(self, node, path): error_set = [] for value_type in self.type_order: try: - return value_type.from_xml(node, path) + return value_type.read_xml_value(node, path) except Exception as e: error_set.append((value_type, e)) # Should have returned, we encountered errors @@ -278,12 +324,13 @@ def from_xml(self, node, path): out += "\nValue Type: {}\nException: {}\n".format(value_type, e) raise ParseError(Exception(out), path) - def write_xml(self, node, obj): - obj.write_xml(node) + def write_xml_value(self, node, obj): + assert isinstance(obj, Object) + obj._write_xml(node) class Param(object): - """ Mirroring Gazebo's SDF api + """XML reflected parameter; serves as base class for Attribute and Element. @param xml_var: Xml name @todo If the value_type is an object with a tag defined in it's @@ -294,13 +341,14 @@ class Param(object): def __init__(self, xml_var, value_type, required=True, default=None, var=None): + self.value_type = get_type(value_type) + assert isinstance(self.value_type, ValueType), self.value_type self.xml_var = xml_var if var is None: self.var = xml_var else: self.var = var self.type = None - self.value_type = get_type(value_type) self.default = default if required: assert default is None, "Default does not make sense for a required field" # noqa @@ -315,6 +363,7 @@ def set_default(self, obj): class Attribute(Param): + """Value stored in an XML attribute.""" def __init__(self, xml_var, value_type, required=True, default=None, var=None): Param.__init__(self, xml_var, value_type, required, default, var) @@ -346,6 +395,7 @@ def add_to_xml(self, obj, node): class Element(Param): + """Value stored in an XML element.""" def __init__(self, xml_var, value_type, required=True, default=None, var=None, is_raw=False): Param.__init__(self, xml_var, value_type, required, default, var) @@ -353,7 +403,7 @@ def __init__(self, xml_var, value_type, required=True, default=None, self.is_raw = is_raw def set_from_xml(self, obj, node, path): - value = self.value_type.from_xml(node, path) + value = self.value_type.read_xml_value(node, path) setattr(obj, self.var, value) def add_to_xml(self, obj, parent): @@ -371,10 +421,11 @@ def add_scalar_to_xml(self, parent, value): node = parent else: node = node_add(parent, self.xml_var) - self.value_type.write_xml(node, value) + self.value_type.write_xml_value(node, value) class AggregateElement(Element): + """Indicates an element is an aggregate.""" def __init__(self, xml_var, value_type, var=None, is_raw=False): if var is None: var = xml_var + 's' @@ -383,22 +434,23 @@ def __init__(self, xml_var, value_type, var=None, is_raw=False): self.is_aggregate = True def add_from_xml(self, obj, node, path): - value = self.value_type.from_xml(node, path) - obj.add_aggregate(self.xml_var, value) + value = self.value_type.read_xml_value(node, path) + obj._add_aggregate(self.xml_var, value) def set_default(self, obj): pass class Info: - """ Small container for keeping track of what's been consumed """ - + """Small container for keeping track of what's been consumed.""" + # TODO(eacousineau): Rename to `Memo`. def __init__(self, node): self.attributes = list(node.attrib.keys()) self.children = xml_children(node) class Reflection(object): + """Stores reflection information for an `Object` derived class.""" def __init__(self, params=[], parent_cls=None, tag=None): """ Construct a XML reflection thing @param parent_cls: Parent class, to use it's reflection as well. @@ -407,7 +459,7 @@ def __init__(self, params=[], parent_cls=None, tag=None): definition thing. """ if parent_cls is not None: - self.parent = parent_cls.XML_REFL + self.parent = parent_cls._XML_REFL else: self.parent = None self.tag = tag @@ -474,7 +526,7 @@ def get_element_path(element): element_path = Path(element.xml_var, parent = path) # Add an index (allow this to be overriden) if element.is_aggregate: - values = obj.get_aggregate_list(element.xml_var) + values = obj._get_aggregate_list(element.xml_var) index = 1 + len(values) # 1-based indexing for W3C XPath element_path.suffix = "[{}]".format(index) return element_path @@ -515,10 +567,10 @@ def get_element_path(element): on_error("Scalar element defined multiple times: {}".format(tag)) # noqa info.children.remove(child) - # For unset attributes and scalar elements, we should not pass the attribute - # or element path, as those paths will implicitly not exist. - # If we do supply it, then the user would need to manually prune the XPath to try - # and find where the problematic parent element. + # For unset attributes and scalar elements, we should not pass the + # attribute or element path, as those paths will implicitly not exist. + # If we do supply it, then the user would need to manually prune the + # XPath to try and find where the problematic parent element. for attribute in map(self.attribute_map.get, unset_attributes): try: attribute.set_default(obj) @@ -552,122 +604,137 @@ def add_to_xml(self, obj, node): element.add_to_xml(obj, node) # Now add in aggregates if self.aggregates: - obj.add_aggregates_to_xml(node) + obj._add_aggregates_to_xml(node) class Object(YamlReflection): - """ Raw python object for yaml / xml representation """ - XML_REFL = None + """Base for user-visible classes which leverage XML reflection.""" + # TODO(eacousineau): Remove most of the reflection-specific code to a + # separate instance, if possible. + _XML_REFL = None + XML_REFL = _now_private_property('_XML_REFL') - def get_refl_vars(self): - return self.XML_REFL.vars + def _get_refl_vars(self): + return self._XML_REFL.vars - def check_valid(self): + def _check_valid(self): pass - def pre_write_xml(self): + check_valid = _now_private_property('_check_valid') + pre_write_xml = _now_private_property('_pre_write_xml') + post_read_xml = _now_private_property('_post_read_xml') + write_xml = _now_private_property('_write_xml') + read_xml = _now_private_property('_read_xml') + from_xml = _now_private_property('_from_xml') + + def _pre_write_xml(self): """ If anything needs to be converted prior to dumping to xml i.e., getting the names of objects and such """ pass - def write_xml(self, node): + def _write_xml(self, node): """ Adds contents directly to XML node """ - self.check_valid() - self.pre_write_xml() - self.XML_REFL.add_to_xml(self, node) + self._check_valid() + self._pre_write_xml() + self._XML_REFL.add_to_xml(self, node) def to_xml(self): """ Creates an overarching tag and adds its contents to the node """ - tag = self.XML_REFL.tag + tag = self._XML_REFL.tag assert tag is not None, "Must define 'tag' in reflection to use this function" # noqa doc = etree.Element(tag) - self.write_xml(doc) + self._write_xml(doc) return doc def to_xml_string(self, addHeader=True): return xml_string(self.to_xml(), addHeader) - def post_read_xml(self): + def _post_read_xml(self): pass - def read_xml(self, node, path): - self.XML_REFL.set_from_xml(self, node, path) - self.post_read_xml() + def _read_xml(self, node, path): + self._XML_REFL.set_from_xml(self, node, path) + self._post_read_xml() try: - self.check_valid() + self._check_valid() except ParseError: raise except Exception, e: raise ParseError(e, path) @classmethod - def from_xml(cls, node, path): + def _from_xml(cls, node, path): cur_type = get_type(cls) - return cur_type.from_xml(node, path) + return cur_type.read_xml_value(node, path) @classmethod def from_xml_string(cls, xml_string): node = etree.fromstring(xml_string) - path = Path(cls.XML_REFL.tag, tree=etree.ElementTree(node)) - return cls.from_xml(node, path) + path = Path(cls._XML_REFL.tag, tree=etree.ElementTree(node)) + return cls._from_xml(node, path) @classmethod def from_xml_file(cls, file_path): xml_string = open(file_path, 'r').read() return cls.from_xml_string(xml_string) - # Confusing distinction between loading code in object and reflection - # registry thing... + get_aggregate_list = _now_private_property('_get_aggregate_list') + aggregate_init = _now_private_property('_aggregate_init') + add_aggregate = _now_private_property('_add_aggregate') + add_aggregates_to_xml = _now_private_property('_add_aggregates_to_xml') + remove_aggregate = _now_private_property('_remove_aggregate') + lump_aggregates = _now_private_property('_lump_aggregates') - def get_aggregate_list(self, xml_var): - var = self.XML_REFL.paramMap[xml_var].var + def _get_aggregate_list(self, xml_var): + var = self._XML_REFL.paramMap[xml_var].var values = getattr(self, var) assert isinstance(values, list) return values - def aggregate_init(self): + def _aggregate_init(self): """ Must be called in constructor! """ - self.aggregate_order = [] + self._aggregate_order = [] # Store this info in the loaded object??? Nah - self.aggregate_type = {} - - def add_aggregate(self, xml_var, obj): - """ NOTE: One must keep careful track of aggregate types for this system. - Can use 'lump_aggregates()' before writing if you don't care. """ - self.get_aggregate_list(xml_var).append(obj) - self.aggregate_order.append(obj) - self.aggregate_type[obj] = xml_var - - def add_aggregates_to_xml(self, node): - for value in self.aggregate_order: - typeName = self.aggregate_type[value] - element = self.XML_REFL.element_map[typeName] + self._aggregate_type = {} + + def _add_aggregate(self, xml_var, obj): + """ NOTE: One must keep careful track of aggregate types for this + system. + Can use '_lump_aggregates()' before writing if you don't care.""" + self._get_aggregate_list(xml_var).append(obj) + self._aggregate_order.append(obj) + self._aggregate_type[obj] = xml_var + + def _add_aggregates_to_xml(self, node): + for value in self._aggregate_order: + typeName = self._aggregate_type[value] + element = self._XML_REFL.element_map[typeName] element.add_scalar_to_xml(node, value) - def remove_aggregate(self, obj): - self.aggregate_order.remove(obj) - xml_var = self.aggregate_type[obj] - del self.aggregate_type[obj] - self.get_aggregate_list(xml_var).remove(obj) + def _remove_aggregate(self, obj): + self._aggregate_order.remove(obj) + xml_var = self._aggregate_type[obj] + del self._aggregate_type[obj] + self._get_aggregate_list(xml_var).remove(obj) - def lump_aggregates(self): + def _lump_aggregates(self): """ Put all aggregate types together, just because """ - self.aggregate_init() - for param in self.XML_REFL.aggregates: - for obj in self.get_aggregate_list(param.xml_var): - self.add_aggregate(param.var, obj) - - """ Compatibility """ + self._aggregate_init() + for param in self._XML_REFL.aggregates: + for obj in self._get_aggregate_list(param.xml_var): + self._add_aggregate(param.var, obj) def parse(self, xml_string): + """ Backwards compatibility """ node = etree.fromstring(xml_string) - path = Path(self.XML_REFL.tag, tree=etree.ElementTree(node)) - self.read_xml(node, path) + path = Path(self._XML_REFL.tag, tree=etree.ElementTree(node)) + self._read_xml(node, path) return self # Really common types -# Better name: element_with_name? Attributed element? +# TODO(eacousineau): Make this objects, not string names with weird implicit +# rules. add_type('element_name', SimpleElementType('name', str)) add_type('element_value', SimpleElementType('value', float)) diff --git a/test/test_base.py b/test/test_base.py new file mode 100644 index 0000000..9a092f3 --- /dev/null +++ b/test/test_base.py @@ -0,0 +1,17 @@ +from contextlib import contextmanager +import unittest +import warnings + + +class TestBase(unittest.TestCase): + def setUp(self): + # Ensure that all deprecations are seen as errors. + warnings.simplefilter('error', DeprecationWarning) + + @contextmanager + def catch_warnings(self): + """Wraps nominal catch warnings to reset filters.""" + with warnings.catch_warnings(record=True) as w: + # Do not error; instead, only warn once. + warnings.simplefilter('once', DeprecationWarning) + yield w diff --git a/test/test_urdf.py b/test/test_urdf.py index 8dce198..8dc5b41 100644 --- a/test/test_urdf.py +++ b/test/test_urdf.py @@ -1,27 +1,32 @@ from __future__ import print_function +from os.path import abspath, dirname, join import unittest -import mock -import os import sys +import warnings +from xml.dom import minidom # noqa + +import mock # Add path to import xml_matching -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))) -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), - '../src'))) +# TODO(eacousineau): Can CTest somehow provide this? +TEST_DIR = dirname(abspath(__file__)) +sys.path.append(TEST_DIR) +sys.path.append(join(dirname(TEST_DIR), 'src')) -from xml.dom import minidom # noqa -from xml_matching import xml_matches # noqa from urdf_parser_py import urdf # noqa -import urdf_parser_py.xml_reflection as xmlr +import urdf_parser_py._xml_reflection as _xmlr +from xml_matching import xml_matches # noqa +from test_base import TestBase -class ParseException(xmlr.core.ParseError): + +class ParseException(_xmlr.ParseError): def __init__(self, e = "", path = ""): super(ParseException, self).__init__(e, path) -class TestURDFParser(unittest.TestCase): - @mock.patch('urdf_parser_py.xml_reflection.on_error', +class TestURDFParser(TestBase): + @mock.patch('urdf_parser_py._xml_reflection.on_error', mock.Mock(side_effect=ParseException)) def parse(self, xml): return urdf.Robot.from_xml_string(xml) @@ -183,8 +188,8 @@ def test_link_multiple_collision(self): self.parse_and_compare(xml) -class LinkOriginTestCase(unittest.TestCase): - @mock.patch('urdf_parser_py.xml_reflection.on_error', +class LinkOriginTestCase(TestBase): + @mock.patch('urdf_parser_py._xml_reflection.on_error', mock.Mock(side_effect=ParseException)) def parse(self, xml): return urdf.Robot.from_xml_string(xml) @@ -220,7 +225,7 @@ def test_robot_link_defaults_xyz_set(self): self.assertEquals(origin.rpy, [0, 0, 0]) -class LinkMultiVisualsAndCollisionsTest(unittest.TestCase): +class LinkMultiVisualsAndCollisionsTest(TestBase): xml = ''' @@ -276,5 +281,26 @@ def test_multi_collision_access(self): self.assertEquals(id(dummyObject), id(robot.links[0].collisions[0])) +class TestDeprecation(TestBase): + """Tests deprecated interfaces.""" + def test_deprecated_properties(self): + with self.catch_warnings() as w: + urdf.Robot.XML_REFL + urdf.Pose().check_valid() + self.assertEqual(len(w), 2) + self.assertIn("'XML_REFL'", str(w[0].message)) + self.assertIn("'check_valid'", str(w[1].message)) + + +class TestExampleRobots(TestBase): + """Tests that some samples files can be parsed without error.""" + @unittest.skip("Badly formatted transmissions") + def test_calvin_urdf(self): + urdf.Robot.from_xml_file(join(TEST_DIR, 'calvin/calvin.urdf')) + + def test_romeo_urdf(self): + urdf.Robot.from_xml_file(join(TEST_DIR, 'romeo/romeo.urdf')) + + if __name__ == '__main__': unittest.main() diff --git a/test/test_urdf_error.py b/test/test_urdf_error.py index 5cd61ee..06da8c0 100644 --- a/test/test_urdf_error.py +++ b/test/test_urdf_error.py @@ -1,21 +1,33 @@ from __future__ import print_function +from os.path import abspath, dirname, join +import sys import unittest +import warnings + +# TODO(eacousineau): Can CTest somehow provide this? +TEST_DIR = dirname(abspath(__file__)) +sys.path.append(TEST_DIR) +sys.path.append(join(dirname(TEST_DIR), 'src')) + from urdf_parser_py import urdf -import urdf_parser_py.xml_reflection as xmlr +import urdf_parser_py._xml_reflection as _xmlr +from test_base import TestBase -ParseError = xmlr.core.ParseError +ParseError = _xmlr.ParseError -class TestURDFParserError(unittest.TestCase): + +class TestURDFParserError(TestBase): def setUp(self): + TestBase.setUp(self) # Manually patch "on_error" to capture errors self.errors = [] def add_error(message): self.errors.append(message) - xmlr.core.on_error = add_error + _xmlr.core.on_error = add_error def tearDown(self): - xmlr.core.on_error = xmlr.core.on_error_stderr + _xmlr.core.on_error = _xmlr.core.on_error_stderr def assertLoggedErrors(self, errors, func, *args, **kwds): func(*args, **kwds) @@ -146,5 +158,6 @@ def test_bad_ducktype(self): for func in funcs: self.assertParseErrorPath("/robot[@name='test']/transmission[@name='simple_trans_bad']", func) + if __name__ == '__main__': unittest.main()