kiwi-el8/helper/schema_parser.py
Alberto Planas 0c095a98b1 Replace @staticmethod with @classmethod when needed
@classmethod are used in Python to represent methods that can
query and update the class (cls parameter). Is expected to be
used for metaprograming, or advanced techniques that require the
access to the class itself, before we have an instance.

@staticmethod are used to associate a function to a class. It will
not be have access to the instance (self) not the class (cls). In
other programming languages are known as class methods.

This patch replace all the @classmethod with @staticmethod when
there is not need to access to the cls parameter, because the
intention is to be used as normal functions.
2019-03-26 16:27:43 +01:00

375 lines
12 KiB
Python
Executable File

#!/usr/bin/env python
"""
Usage: schema_parser.py SCHEMA [--output=ARG]
Arguments:
SCHEMA Kiwi RelaxNG schema file to parse
Options:
--output This is output file (stdout used if not present)
"""
import docopt
from lxml import etree
from collections import namedtuple
import os.path
import logging
import sys
logging.basicConfig(level=logging.WARNING)
class SchemaNode(object):
Child = namedtuple('Child', ['node', 'properties'])
@staticmethod
def is_type(node):
pass
def __init__(self, node, namespaces):
self.logger = logging.getLogger(self.__class__.__name__)
self.node = node
self.namespaces = namespaces
ref = node.xpath(
'(./parent::rng:define[@name])[1]',
namespaces=namespaces
)
if len(ref):
self.reference = ref[0].attrib['name']
else:
self.reference = None
self.documentation = None
self.paths = []
element_tree = node.getroottree()
path = element_tree.getelementpath(node)
self.x_path = path.replace('{%s}' % namespaces['rng'], 'rng:')
self.properties = ['optional', 'oneOrMore', 'zeroOrMore']
def get_documentation(self):
for candidate in self.node:
if candidate.tag.endswith('documentation'):
self.documentation = candidate.text.replace('\n', ' ')
if self.documentation is None:
self.documentation = ''
self.logger.warning(
'Node %s has no documentation in schema', self.name
)
return self.documentation
def query(self, query, ret_type=None):
nodes = []
for node in self.node.xpath(query, namespaces=self.namespaces):
if ret_type is None:
nodes.append(SchemaNode(node, self.namespaces))
else:
nodes.append(ret_type(node, self.namespaces))
return nodes
def get_children_by_type(self, ret_type, stop_types=None):
if stop_types is None:
stop_types = []
# Gets direct children of this node in its tree
children = self.children_in_tree(ret_type, stop_types)
# Follow references that are defined somewhere else in tree
s_types = [ret_type] + stop_types
for reference in self.children_in_tree(Reference, s_types):
if not reference.node.name.endswith('any'):
children += reference.node.resolve_reference(
ret_type, stop_types, reference.properties
)
return children
def children_in_tree(self, ret_type, stop_types, properties=None):
if properties is None:
properties = []
children = []
for child in self.node:
if self._is_in_types(child, stop_types):
continue
if ret_type.is_type(child):
children.append(SchemaNode.Child(
node=ret_type(child, self.namespaces),
properties=self._get_properties(child, properties)
))
else:
children += SchemaNode(child, self.namespaces).children_in_tree(
ret_type, stop_types, self._get_properties(child, properties)
)
return children
def _get_properties(self, node, properties):
props = []
for prop in self.properties:
if node.tag.endswith(prop):
props = [prop]
break
return props + properties
def _is_in_types(self, node, types):
for t in types:
if t.is_type(node):
return True
return False
class Reference(SchemaNode):
@staticmethod
def is_type(node):
if 'name' in node.attrib and node.tag.endswith('ref'):
return True
return False
def __init__(self, node, namespaces):
super(Reference, self).__init__(node, namespaces)
if not self.is_type(node):
raise Exception('The given node is not a reference')
self.name = self.node.attrib['name']
def resolve_reference(self, ret_type, stop_types, properties):
define = self._find_define()
if define is None:
return []
# Gets direct children of this node in its tree
nodes = define.children_in_tree(ret_type, stop_types, properties)
# Follow references that are defined somewhere else in tree
s_types = stop_types + [ret_type]
for reference in define.children_in_tree(
Reference, s_types, properties
):
nodes += reference.node.resolve_reference(
ret_type, stop_types, reference.properties
)
return nodes
def _find_define(self):
node = self.query(
'//rng:define[@name=\'%s\']' % self.name
)
if len(node):
return node[0]
class Attribute(SchemaNode):
@staticmethod
def is_type(node):
if 'name' in node.attrib and node.tag.endswith('attribute'):
return True
return False
def __init__(self, node, namespaces):
super(Attribute, self).__init__(node, namespaces)
if not self.is_type(node):
raise Exception('The given node is not an attribute')
self.documentation = None
self.name = self.node.attrib['name']
class Element(SchemaNode):
Child = namedtuple('Child', ['x_path', 'properties'])
@staticmethod
def is_type(node):
if 'name' in node.attrib and node.tag.endswith('element'):
return True
return False
def __init__(self, node, namespaces):
super(Element, self).__init__(node, namespaces)
if not self.is_type(node):
raise Exception('The given node is not an element')
self.children = None
self.name = self.node.attrib['name']
self.attributes = self.get_children_by_type(Attribute, [Element])
def get_parent_paths(self):
parents = []
for path in filter(lambda x: '.' in x, self.paths):
parent = path.rsplit('.' + self.get_name(), 1)[0]
if len(parent):
parents.append(parent)
return parents
def get_children(self):
if self.children is None:
self.find_children()
return self.children
def find_children(self):
self.children = []
elements = []
for child in self.get_children_by_type(Element, [Attribute]):
self.children.append(Element.Child(
x_path=child.node.x_path,
properties=child.properties
))
elements.append(child.node)
return elements
class RNGSchemaParser(object):
def __init__(self, schema):
self.logger = logging.getLogger(self.__class__.__name__)
self.schema_tree = etree.parse(schema)
etree.strip_tags(self.schema_tree, etree.Comment)
self.nspaces = self.schema_tree.getroot().nsmap
self.nspaces.pop(None)
if 'rng' not in self.nspaces:
raise Exception('\'rng:\' namespace must be defined in namespaces map')
self.root_nodes = []
for start in self.schema_tree.xpath('//rng:start', namespaces=self.nspaces):
self.root_nodes += SchemaNode(start, self.nspaces).get_children_by_type(
Element, [Attribute]
)
if not self.root_nodes:
raise Exception('Not valid RelaxNG start tag')
self.elements = dict()
for root_node in self.root_nodes:
self._find_elements(root_node.node, None)
def x_path_query(self, query):
return self.schema_tree.xpath(query, namespaces=self.nspaces)
def write_element_doc(self, element, current_path, output):
if current_path is None:
current_path = []
element_path = '.'.join(current_path + [element.name])
level = element_path.count('.')
titles = '-_.,:;#~<>^*'
doc = '.. _k.%s:\n\n' % element_path
doc += '%s\n' % element.name
doc += titles[level] * len(element.name) + '\n\n'
if len(element.get_documentation()):
doc += '%s\n\n' % element.get_documentation()
parents = []
for path in [x for x in element.paths if len(x) > 1]:
parents.append(':ref:`k.%s`' % '.'.join(path[:-1]))
if len(parents):
doc += 'Parents:\n'
doc += ' These elements contain ``%s``: ' % element.name
doc += ', '.join(parents)
doc += '\n\n'
children = []
for child in element.get_children():
children.append(':ref:`%s <k.%s>` %s' % (
self.elements[child.x_path].name,
'.'.join([element_path, self.elements[child.x_path].name]),
self._properties_to_doc(child.properties)
))
if len(children):
doc += 'Children:\n'
doc += ' The following elements occur in ``%s``: ' % element.name
doc += ', '.join(children)
doc += '\n\n'
if len(element.attributes):
doc += 'List of attributes for ``%s``:\n\n' % element.name
for attribute in element.attributes:
doc += '* ``%s`` %s: %s\n' % (
attribute.node.name,
self._properties_to_doc(attribute.properties),
attribute.node.get_documentation()
)
doc += '\n'
if output:
with open(output, 'a') as f:
f.write(doc)
else:
sys.stdout.write(doc)
for child in element.get_children():
self.write_element_doc(
self.elements[child.x_path],
current_path + [element.name], output
)
def legend(self):
doc = '.. hint:: **Element quantifiers**\n\n'\
' * **Optional** elements are qualified with _`[?]`\n'\
' * Elements that occur **one or more** times are qualified with _`[+]`\n'\
' * Elements that occur **zero or more** times are qualified with _`[*]`\n'\
' * Required elements are not qualified\n\n'
return doc
def _properties_to_doc(self, properties):
if not properties:
return ''
if properties[0] == 'optional':
return '`[?]`_'
elif properties[0] == 'oneOrMore':
return '`[+]`_'
elif properties[0] == 'zeroOrMore':
return '`[*]`_'
return ''
def _find_elements(self, element, path):
if path is None:
path = []
if element.x_path not in self.elements:
self.elements[element.x_path] = element
for child in element.find_children():
self._find_elements(child, path + [element.name])
self.elements[element.x_path].paths.append(path + [element.name])
if __name__ == "__main__":
try:
logger = logging.getLogger(__name__)
arguments = docopt.docopt(__doc__)
ofile = arguments['--output']
if os.path.splitext(arguments['SCHEMA'])[-1] == '.rnc':
raise OSError("Need RNG instead of RNC")
if ofile and os.path.isfile(ofile):
logger.warning('Output file %s will be overwritten', ofile)
if os.path.isfile(arguments['SCHEMA']):
parser = RNGSchemaParser(arguments['SCHEMA'])
value = parser.x_path_query(
'(//rng:attribute[@name=\'schemaversion\']/rng:value)[1]'
)
if value:
version = value[0].text
else:
raise Exception('Schema version not found!')
title_label = '.. _schema-docs:\n\n'
title = 'Schema Documentation %s\n' % version
title += '=' * len(title) + '\n\n'
title = title_label + title
if ofile:
with open(ofile, 'w') as f:
f.write(title)
f.write(parser.legend())
else:
sys.stdout.write(title)
for element in parser.root_nodes:
parser.write_element_doc(
parser.elements[element.node.x_path], None, ofile
)
else:
raise Exception('File not found: %s' % arguments['SCHEMA'])
except Exception as e:
logger.error('Schema parser failed', exc_info=True)
exit(2)