# 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
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'):
raise DataError(f"Invalid spec file version '{version}'. "
f"Supported versions are 3 and 4.")
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 '',
shortdoc=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)
return kw
def _create_arguments(self, elem, kw: KeywordDoc):
spec = kw.args
setters = {
ArgInfo.POSITIONAL_ONLY: spec.positional_only.append,
ArgInfo.POSITIONAL_ONLY_MARKER: lambda value: None,
ArgInfo.POSITIONAL_OR_NAMED: spec.positional_or_named.append,
ArgInfo.VAR_POSITIONAL: lambda value: setattr(spec, 'var_positional', value),
ArgInfo.NAMED_ONLY_MARKER: lambda value: None,
ArgInfo.NAMED_ONLY: spec.named_only.append,
ArgInfo.VAR_NAMED: lambda value: setattr(spec, 'var_named', value),
}
for arg in elem.findall('arguments/arg'):
name_elem = arg.find('name')
if name_elem is None:
continue
name = name_elem.text
setters[arg.get('kind')](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 = {}
types = []
type_docs = {}
for typ in arg.findall('type'):
types.append(typ.text)
if typ.get('typedoc'):
type_docs[typ.text] = typ.get('typedoc')
spec.types[name] = tuple(types)
kw.type_docs[name] = 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))