# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# 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.
import os.path
from robot.errors import DataError
from robot.running import ArgInfo, TypeInfo
from robot.utils import ET, ETSource
from .datatypes import EnumMember, TypedDictItem, TypeDoc
from .model import LibraryDoc, KeywordDoc
[docs]
class XmlDocBuilder:
[docs]
def build(self, path):
spec = self._parse_spec(path)
libdoc = LibraryDoc(name=spec.get('name'),
type=spec.get('type').upper(),
version=spec.find('version').text or '',
doc=spec.find('doc').text or '',
scope=spec.get('scope'),
doc_format=spec.get('format') or 'ROBOT',
source=spec.get('source'),
lineno=int(spec.get('lineno')) or -1)
libdoc.inits = self._create_keywords(spec, 'inits/init', libdoc.source)
libdoc.keywords = self._create_keywords(spec, 'keywords/kw', libdoc.source)
# RF >= 5 have 'typedocs', RF >= 4 have 'datatypes', older/custom may have neither.
if spec.find('typedocs') is not None:
libdoc.type_docs = self._parse_type_docs(spec)
else:
libdoc.type_docs = self._parse_data_types(spec)
return libdoc
def _parse_spec(self, path):
if not os.path.isfile(path):
raise DataError(f"Spec file '{path}' does not exist.")
with ETSource(path) as source:
root = ET.parse(source).getroot()
if root.tag != 'keywordspec':
raise DataError(f"Invalid spec file '{path}'.")
version = root.get('specversion')
if version not in ('3', '4', '5', '6'):
raise DataError(f"Invalid spec file version '{version}'. "
f"Supported versions are 3, 4, 5, and 6.")
return root
def _create_keywords(self, spec, path, lib_source):
return [self._create_keyword(elem, lib_source) for elem in spec.findall(path)]
def _create_keyword(self, elem, lib_source):
kw = KeywordDoc(name=elem.get('name', ''),
doc=elem.find('doc').text or '',
short_doc=elem.find('shortdoc').text or '',
tags=[t.text for t in elem.findall('tags/tag')],
private=elem.get('private', 'false') == 'true',
deprecated=elem.get('deprecated', 'false') == 'true',
source=elem.get('source') or lib_source,
lineno=int(elem.get('lineno', -1)))
self._create_arguments(elem, kw)
self._add_return_type(elem.find('returntype'), kw)
return kw
def _create_arguments(self, elem, kw: KeywordDoc):
spec = kw.args
spec = kw.args
positional_only = []
positional_or_named = []
named_only = []
for arg in elem.findall('arguments/arg'):
name_elem = arg.find('name')
if name_elem is None:
continue
name = name_elem.text
kind = arg.get('kind')
if kind == ArgInfo.POSITIONAL_ONLY:
positional_only.append(name)
elif kind == ArgInfo.POSITIONAL_OR_NAMED:
positional_or_named.append(name)
elif kind == ArgInfo.VAR_POSITIONAL:
spec.var_positional = name
elif kind == ArgInfo.NAMED_ONLY:
named_only.append(name)
elif kind == ArgInfo.VAR_NAMED:
spec.var_named = name
default_elem = arg.find('default')
if default_elem is not None:
spec.defaults[name] = default_elem.text or ''
if not spec.types:
spec.types = {}
type_docs = {}
type_elems = arg.findall('type')
if len(type_elems) == 1 and 'name' in type_elems[0].attrib:
type_info = self._parse_type_info(type_elems[0], type_docs)
else:
type_info = self._parse_legacy_type_info(type_elems, type_docs)
if type_info:
spec.types[name] = type_info
kw.type_docs[name] = type_docs
spec.positional_only = positional_only
spec.positional_or_named = positional_or_named
spec.named_only = named_only
def _parse_type_info(self, type_elem, type_docs):
name = type_elem.get('name')
if type_elem.get('typedoc'):
type_docs[name] = type_elem.get('typedoc')
nested = [self._parse_type_info(child, type_docs)
for child in type_elem.findall('type')]
return TypeInfo(name, None, nested=nested or None)
def _parse_legacy_type_info(self, type_elems, type_docs):
types = []
for elem in type_elems:
name = elem.text
types.append(name)
if elem.get('typedoc'):
type_docs[name] = elem.get('typedoc')
return TypeInfo.from_sequence(types) if types else None
def _add_return_type(self, elem, kw):
if elem is not None:
type_docs = {}
kw.args.return_type = self._parse_type_info(elem, type_docs)
kw.type_docs['return'] = type_docs
def _parse_type_docs(self, spec):
for elem in spec.findall('typedocs/type'):
doc = TypeDoc(elem.get('type'), elem.get('name'), elem.find('doc').text,
[e.text for e in elem.findall('accepts/type')],
[e.text for e in elem.findall('usages/usage')])
if doc.type == TypeDoc.ENUM:
doc.members = self._parse_members(elem)
if doc.type == TypeDoc.TYPED_DICT:
doc.items = self._parse_items(elem)
yield doc
def _parse_members(self, elem):
return [EnumMember(member.get('name'), member.get('value'))
for member in elem.findall('members/member')]
def _parse_items(self, elem):
def get_required(item):
required = item.get('required', None)
return None if required is None else required == 'true'
return [TypedDictItem(item.get('key'), item.get('type'), get_required(item))
for item in elem.findall('items/item')]
# Code below used for parsing legacy 'datatypes'.
def _parse_data_types(self, spec):
for elem in spec.findall('datatypes/enums/enum'):
yield self._create_enum_doc(elem)
for elem in spec.findall('datatypes/typeddicts/typeddict'):
yield self._create_typed_dict_doc(elem)
def _create_enum_doc(self, elem):
return TypeDoc(TypeDoc.ENUM, elem.get('name'), elem.find('doc').text,
members=self._parse_members(elem))
def _create_typed_dict_doc(self, elem):
return TypeDoc(TypeDoc.TYPED_DICT, elem.get('name'), elem.find('doc').text,
items=self._parse_items(elem))