# -*- coding: utf-8 -*-
#
# Copyright (c), 2015-2016, Quantum Espresso Foundation and SISSA (Scuola
# Internazionale Superiore di Studi Avanzati). All rights reserved.
# This file is distributed under the terms of the MIT License. See the
# file 'LICENSE' in the root directory of the present distribution, or
# http://opensource.org/licenses/MIT.
# Authors: Davide Brunato
#
"""
This module contains XMLDocument class for xsdtypes package
"""
import logging
from xml.etree import ElementTree
from .exceptions import FileFormatError
from .exceptions import XMLSchemaValidationError
from .xmlschema import XMLSchema
logger = logging.getLogger('qespresso')
[docs]class XmlDocument(object):
"""
Generic XML schema based document.
A XSD schema is needed for checking types, validation of the configuration
and for lookup of default values of the attributes. Full schema's validation
is available only if lxml library is installed.
Supported files data format for configuration are XML, YAML and JSON.
"""
[docs] def __init__(self, xsd_file):
self._document = None
self._config_file = None
self._file_format = None
self._xsd_file = xsd_file
try:
self.schema = XMLSchema(xsd_file)
except (IOError, FileFormatError) as err:
logger.error('XML Schema not available: %s' % err)
self.schema = None
raise
self.namespaces = self.schema.namespaces
[docs] def read(self, filename, data_format='XML'):
"""
Read configuration from a text file in a specific data format.
:param filename: Name of the text file containing the configuration
:param data_format: Input file data format (XML, JSON or YAML)
"""
data_format = data_format.upper()
old_config = (self._document, self._config_file)
try:
if data_format == 'XML':
self._document = self.parse_xml(filename)
elif data_format == 'YAML':
self._document = self.parse_yaml(filename)
elif data_format == 'JSON':
self._document = self.parse_json(filename)
else:
raise ValueError(
"'input_format' argument must be 'XML', 'YAML' or 'JSON'")
except (FileFormatError, XMLSchemaValidationError) as e:
raise
else:
self._config_file = filename
# Validation of the new ElementTree structure (valid
try:
self.validate()
except XMLSchemaValidationError as e:
self._document, self._config_file = old_config
raise
[docs] def parse_xml(self, filename):
"""
Return an ElementTree object representing an XML file
"""
try:
return ElementTree.parse(filename)
except ElementTree.ParseError as e:
raise FileFormatError('XML', filename, e)
[docs] def parse_json(self, filename):
"""
Build an ElementTree object representing a YAML file
"""
logger.warning("JSON read is a TODO!")
[docs] def parse_yaml(self, filename):
"""
Build an ElementTree object representing a YAML file
"""
logger.warning("YAML read is a TODO!")
[docs] def from_dict(self):
"""
Build an ElementTree object from a dictionary
"""
[docs] def validate(self, filename=None):
if filename is not None:
try:
self.validate()
except XMLSchemaValidationError as e:
e.message = "Invalid XML file '%s': %s" % (filename, e.message)
raise
else:
self._config_file = filename
self.schema.validate(self._document)
self.extra_validations(self._document)
[docs] def write(self, filename, output_format='XML'):
"""
Write configuration to a text file in a specific data format.
:param filename:
:param output_format:
:return:
"""
if self._document is None:
logger.error("No configuration loaded!")
return
output_format = output_format.upper()
if output_format == 'XML':
pass
elif output_format == 'YAML':
logger.warning("YAML write is a TODO!")
elif output_format == 'JSON':
logger.warning("JSON write is a TODO!")
else:
raise ValueError(
"Accepted output_format are: 'XML'(default), 'YAML' and 'JSON'!"
)
[docs] def read_string(self, text):
self._document = ElementTree.fromstring(text)
[docs] def get(self, qualified_name):
section, _, item = qualified_name.partition(".")
query = "./{0}/{1}".format(section, item)
print(query)
node = self._document.find(query)
if node is None:
return
return node.text
def __getitem__(self, section):
query = "./{0}".format(section)
parent = self._document.find(query)
return dict((item.tag, item.text) for item in parent)
[docs] def to_dict(self):
from .etree import etree_to_dict
return etree_to_dict(self._document, self.schema)
[docs] def to_json(self):
"""Converts the configuration to to json."""
import json
return json.dumps(self.to_dict(), sort_keys=True, indent=4)
#def to_yaml(self):
# """Converts the configuration to to json."""
# import yaml
# return yaml.dump(self.to_dict(), default_flow_style=False)
# ElementTree API wrappers
[docs] def iter(self, tag=None):
return self._document.iter(tag)
[docs] def find(self, path, namespaces=None):
"""
Find first matching element by tag name or path.
:param path: is a string having either an element tag or an XPath,
:param namespaces: is an optional mapping from namespace prefix to full name.
:return: the first matching element, or None if no element was found
"""
namespaces = namespaces or {}
namespaces.update(self.namespaces)
# Disable namespaces for python 3.8 (MATSCI-8809), not sure why this is
# needed at all. Will ask QE support.
namespaces = None
return self._document.find(path, namespaces)
[docs] def findall(self, path, namespaces=None):
"""
Find all matching subelements by tag name or path.
:param path: is a string having either an element tag or an XPath,
:param namespaces: is an optional mapping from namespace prefix to full name.
:return: the first matching element, or None if no element was found
"""
namespaces = namespaces or {}
namespaces.update(self.namespaces)
# Disable namespaces for python 3.8 (MATSCI-8809), not sure why this is
# needed at all. Will ask QE support.
namespaces = None
return self._document.findall(path, namespaces)