Source code for rofunc.utils.robolab.formatter.mjcf_parser.parser

# Copyright 2018 The dm_control Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or  implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================

"""Functions for parsing XML into an MJCF object model."""

import os
import sys

from lxml import etree

from rofunc.utils.robolab.formatter.mjcf_parser import constants
from rofunc.utils.robolab.formatter.mjcf_parser import debugging
from rofunc.utils.robolab.formatter.mjcf_parser import element
# Copybara placeholder for internal file handling dependency.
from rofunc.utils.robolab.formatter.mjcf_parser import io as resources


[docs]def from_xml_string(xml_string, escape_separators=False, model_dir='', resolve_references=True, assets=None): """Parses an XML string into an MJCF object model. Args: xml_string: An XML string representing an MJCF model. escape_separators: (optional) A boolean, whether to replace '/' characters in element identifiers. If `False`, any '/' present in the XML causes a ValueError to be raised. model_dir: (optional) Path to the directory containing the model XML file. This is used to prefix the paths of all asset files. resolve_references: (optional) A boolean indicating whether the parser should attempt to resolve reference attributes to a corresponding element. assets: (optional) A dictionary of pre-loaded assets, of the form `{filename: bytestring}`. If present, PyMJCF will search for assets in this dictionary before attempting to load them from the filesystem. Returns: An `mjcf.RootElement`. """ xml_root = etree.fromstring(xml_string) return _parse(xml_root, escape_separators, model_dir=model_dir, resolve_references=resolve_references, assets=assets)
[docs]def from_file(file_handle, escape_separators=False, model_dir='', resolve_references=True, assets=None): """Parses an XML file into an MJCF object model. Args: file_handle: A Python file-like handle. escape_separators: (optional) A boolean, whether to replace '/' characters in element identifiers. If `False`, any '/' present in the XML causes a ValueError to be raised. model_dir: (optional) Path to the directory containing the model XML file. This is used to prefix the paths of all asset files. resolve_references: (optional) A boolean indicating whether the parser should attempt to resolve reference attributes to a corresponding element. assets: (optional) A dictionary of pre-loaded assets, of the form `{filename: bytestring}`. If present, PyMJCF will search for assets in this dictionary before attempting to load them from the filesystem. Returns: An `mjcf.RootElement`. """ xml_root = etree.parse(file_handle).getroot() return _parse(xml_root, escape_separators, model_dir=model_dir, resolve_references=resolve_references, assets=assets)
[docs]def from_path(path, escape_separators=False, resolve_references=True, assets=None): """Parses an XML file into an MJCF object model. Args: path: A path to an XML file. This path should be loadable using `resources.GetResource`. escape_separators: (optional) A boolean, whether to replace '/' characters in element identifiers. If `False`, any '/' present in the XML causes a ValueError to be raised. resolve_references: (optional) A boolean indicating whether the parser should attempt to resolve reference attributes to a corresponding element. assets: (optional) A dictionary of pre-loaded assets, of the form `{filename: bytestring}`. If present, PyMJCF will search for assets in this dictionary before attempting to load them from the filesystem. Returns: An `mjcf.RootElement`. """ model_dir, _ = os.path.split(path) contents = resources.GetResource(path) xml_root = etree.fromstring(contents) return _parse(xml_root, escape_separators, model_dir=model_dir, resolve_references=resolve_references, assets=assets)
def _parse(xml_root, escape_separators=False, model_dir='', resolve_references=True, assets=None): """Parses a complete MJCF model from an XML. Args: xml_root: An `etree.Element` object. escape_separators: (optional) A boolean, whether to replace '/' characters in element identifiers. If `False`, any '/' present in the XML causes a ValueError to be raised. model_dir: (optional) Path to the directory containing the model XML file. This is used to prefix the paths of all asset files. resolve_references: (optional) A boolean indicating whether the parser should attempt to resolve reference attributes to a corresponding element. assets: (optional) A dictionary of pre-loaded assets, of the form `{filename: bytestring}`. If present, PyMJCF will search for assets in this dictionary before attempting to load them from the filesystem. Returns: An `mjcf.RootElement`. Raises: ValueError: If `xml_root`'s tag is not 'mujoco.*'. """ assets = assets or {} if not xml_root.tag.startswith('mujoco'): raise ValueError('Root element of the XML should be <mujoco.*>: got <{}>' .format(xml_root.tag)) with debugging.freeze_current_stack_trace(): # Recursively parse any included XML files. to_include = [] for include_tag in xml_root.findall('include'): try: # First look for the path to the included XML file in the assets dict. path_or_xml_string = assets[include_tag.attrib['file']] parsing_func = from_xml_string except KeyError: # If it's not present in the assets dict then attempt to load the XML # from the filesystem. path_or_xml_string = os.path.join(model_dir, include_tag.attrib['file']) parsing_func = from_path included_mjcf = parsing_func( path_or_xml_string, escape_separators=escape_separators, resolve_references=resolve_references, assets=assets) to_include.append(included_mjcf) # We must remove <include/> tags before parsing the main XML file, since # these are a schema violation. xml_root.remove(include_tag) # Parse the main XML file. try: model = xml_root.attrib.pop('model') except KeyError: model = None mjcf_root = element.RootElement( model=model, model_dir=model_dir, assets=assets) _parse_children(xml_root, mjcf_root, escape_separators) # Merge in the included XML files. for included_mjcf in to_include: # The included MJCF might have been automatically assigned a model name # that conficts with that of `mjcf_root`, so we override it here. included_mjcf.model = mjcf_root.model mjcf_root.include_copy(included_mjcf) if resolve_references: mjcf_root.resolve_references() return mjcf_root def _parse_children(xml_element, mjcf_element, escape_separators=False): """Parses all children of a given XML element into an MJCF element. Args: xml_element: The source `etree.Element` object. mjcf_element: The target `mjcf.Element` object. escape_separators: (optional) A boolean, whether to replace '/' characters in element identifiers. If `False`, any '/' present in the XML causes a ValueError to be raised. """ for xml_child in xml_element: if xml_child.tag is etree.Comment or xml_child.tag is etree.PI: continue try: child_spec = mjcf_element.spec.children[xml_child.tag] if escape_separators: attributes = {} for name, value in xml_child.attrib.items(): # skip flipping the slash for fields that may contain paths, like # custom text and asset file. if name in ['data', 'file', 'meshdir', 'assetdir', 'texturedir', 'content_type', 'fileleft', 'fileright', 'fileback', 'filefront', 'plugin', 'key', 'value']: attributes[name] = value else: new_value = value.replace( constants.PREFIX_SEPARATOR_ESCAPE, constants.PREFIX_SEPARATOR_ESCAPE * 2) new_value = new_value.replace( constants.PREFIX_SEPARATOR, constants.PREFIX_SEPARATOR_ESCAPE) attributes[name] = new_value else: attributes = dict(xml_child.attrib) if child_spec.repeated or child_spec.on_demand: mjcf_child = mjcf_element.add(xml_child.tag, **attributes) else: mjcf_child = getattr(mjcf_element, xml_child.tag) mjcf_child.set_attributes(**attributes) except: # pylint: disable=bare-except err_type, err, traceback = sys.exc_info() raise err_type( # pylint: disable=raise-missing-from f'Line {xml_child.sourceline}: error while parsing element ' f'<{xml_child.tag}>: {err}').with_traceback(traceback) _parse_children(xml_child, mjcf_child, escape_separators)