carla/PythonAPI/docs/doc_gen.py

735 lines
28 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2017 Computer Vision Center (CVC) at the Universitat Autonoma de
# Barcelona (UAB).
#
# This work is licensed under the terms of the MIT license.
# For a copy, see <https://opensource.org/licenses/MIT>.
import os
import yaml
import re
import doc_gen_snipets
COLOR_METHOD = '#7fb800'
COLOR_PARAM = '#00a6ed'
COLOR_INSTANCE_VAR = '#f8805a'
COLOR_NOTE = '#8E8E8E'
COLOR_WARNING = '#ED2F2F'
QUERY = re.compile(r'([cC]arla(\.[a-zA-Z0-9_]+)+)')
def create_hyperlinks(text):
return re.sub(QUERY, r'[\1](#\1)', text)
def create_getter_setter_hyperlinks(text):
return re.sub(QUERY, r'[\1](#\1)', text)
def join(elem, separator=''):
return separator.join(elem)
class MarkdownFile:
def __init__(self):
self._data = ""
self._list_depth = 0
self.endl = ' \n'
def data(self):
return self._data
def list_push(self, buf=''):
if buf:
self.text(join([
' ' * self._list_depth if self._list_depth != 0 else '', '- ', buf]))
self._list_depth = (self._list_depth + 1)
def list_pushn(self, buf):
self.list_push(join([buf, self.endl]))
def list_pop(self):
self._list_depth = max(self._list_depth - 1, 0)
def list_popn(self):
self.list_pop()
self._data = join([self._data, '\n'])
def list_depth(self):
if self._data.strip()[-1:] != '\n' or self._list_depth == 0:
return ''
return join([' ' * self._list_depth])
def separator(self):
self._data = join([self._data, '\n---\n'])
def new_line(self):
self._data = join([self._data, self.endl])
def text(self, buf):
self._data = join([self._data, buf])
def textn(self, buf):
self._data = join([self._data, self.list_depth(), buf, self.endl])
def first_title(self):
self._data = join([
self._data, '# Python API reference\n'])
def title(self, strongness, buf):
self._data = join([
self._data, '\n', self.list_depth(), '#' * strongness, ' ', buf, '\n'])
def title_html(self, strongness, buf):
if strongness == 5:
self._data = join([
self._data, '\n', self.list_depth(), '<h', str(strongness), ' style="margin-top: -20px">', buf, '</h', str(strongness),'>\n','<div style="padding-left:30px;margin-top:-25px"></div>'])
else:
self._data = join([
self._data, '\n', self.list_depth(), '<h', str(strongness), '>', buf, '</h', str(strongness), '>\n'])
def inherit_join(self, inh):
self._data = join([
self._data, '<small style="display:block;margin-top:-20px;">Inherited from ', inh, '</small></br>\n'])
def note(self, buf):
self._data = join([self._data, buf])
def code_block(self, buf, language=''):
return join(['```', language, '\n', self.list_depth(), buf, '\n', self.list_depth(), '```\n'])
def prettify_doc(self, doc):
punctuation_marks = ['.', '!', '?']
doc = doc.strip()
doc += '' if doc[-1:] in punctuation_marks else '.'
return doc
def italic(buf):
return join(['_', buf, '_'])
def bold(buf):
return join(['**', buf, '**'])
def snipet(name,class_key):
return join(["<button class=\"SnipetButton\" id=\"",class_key,".",name,"-snipet_button\">", "snippet &rarr;", '</button>'])
def code(buf):
return join(['`', buf, '`'])
def brackets(buf):
return join(['[', buf, ']'])
def parentheses(buf):
return join(['(', buf, ')'])
def small_html(buf):
return join(['<small>', buf, '</small>'])
def small(buf):
return join(['<sub><sup>', buf, '</sup></sub>'])
def sub(buf):
return join(['<sub>', buf, '</sub>'])
def html_key(buf):
return join(['<a name="', buf, '"></a>'])
def color(col, buf):
return join(['<font color="', col, '">', buf, '</font>'])
def valid_dic_val(dic, value):
return value in dic and dic[value]
class YamlFile:
"""Yaml file class"""
def __init__(self, path):
self._path = path
with open(path) as yaml_file:
self.data = yaml.safe_load(yaml_file)
self.validate()
def validate(self):
# print('Validating ' + str(self._path.replace('\\', '/').split('/')[-1:][0]))
if self.data is None:
print('\n[ERROR] File: ' + self._path)
print("This file has no data:")
exit(0)
for module in self.data:
if 'module_name' in module and module['module_name'] is None:
print('\n[ERROR] File: ' + self._path)
print("'module_name' is empty in:")
exit(0)
if 'classes' in module:
if not module['classes']:
print('\n[ERROR] File: ' + self._path)
print("'classes' is empty in:")
exit(0)
for cl in module['classes']:
if 'class_name' in cl and cl['class_name'] is None:
print('\n[ERROR] File: ' + self._path)
print("'class_name' is empty in:")
exit(0)
if 'instance_variables' in cl and cl['instance_variables']:
for iv in cl['instance_variables']:
if 'var_name' not in iv:
print('\n[ERROR] File: ' + self._path)
print("'var_name' not found inside 'instance_variables' of class: " + cl['class_name'])
exit(0)
if 'var_name' in iv and iv['var_name'] is None:
print('\n[ERROR] File: ' + self._path)
print("'var_name' is empty in:")
exit(0)
if 'methods' in cl and cl['methods']:
for met in cl['methods']:
if 'def_name' not in met:
print('\n[ERROR] File: ' + self._path)
print("'def_name' not found inside 'methods' of class: " + cl['class_name'])
exit(0)
if 'def_name' in met and met['def_name'] is None:
print('\n[ERROR] File: ' + self._path)
print("'def_name' is empty in:")
exit(0)
if 'params' in met and met['params']:
for param in met['params']:
if 'param_name' not in param:
print('\n[ERROR] File: ' + self._path)
print("'param_name' not found inside 'params' of class: " + cl['class_name'])
exit(0)
if 'param_name' in param and param['param_name'] is None:
print('\n[ERROR] File: ' + self._path)
print("'param_name' is empty in:")
exit(0)
if 'type' in param and param['type'] is None:
print('\n[ERROR] File: ' + self._path)
print("'type' is empty in:")
exit(0)
def get_modules(self):
return [module for module in self.data]
def append_snipet_button_script(md):
md.textn("\n\n<script>\n"+
"function ButtonAction(container_name){\n"+
"if(window_big){\n"+
"snipet_name = container_name.replace('-snipet_button','-snipet');\n"+
"document.getElementById(\"snipets-container\").innerHTML = document.getElementById(snipet_name).innerHTML;\n"+
"}\n"+
"else{\n"+
"document.getElementById(\"snipets-container\").innerHTML = null;"+
"code_name = container_name.replace('-snipet_button','-code');\n"+
"var range = document.createRange();\n"+
"range.selectNode(document.getElementById(code_name));\n"+
"alert(range);\n"+
"}\n"+
"}\n"+
"function WindowResize(){\n"+
"if(window.innerWidth > 1200){\n"+
"window_big = true;\n"+
"}\n"+
"else{\n"+
"window_big = false;\n"+
"}\n"+
"}\n"+
"var window_big;\n"+
"if(window.innerWidth > 1200){\n"+
"window_big = true;\n"+
"}\n"+
"else{\n"+
"window_big = false;\n"+
"}\n"+
"buttons = document.getElementsByClassName('SnipetButton')\n"+
"for (let i = 0; i < buttons.length; i++) {\n"+
"buttons[i].addEventListener(\"click\",function(){ButtonAction(buttons[i].id);},true);\n"+
"}\n"+
"window.onresize = WindowResize;\n"+
"</script>\n")
def append_code_snipets(md):
current_folder = os.path.dirname(os.path.abspath(__file__))
snipets_path = os.path.join(current_folder, '../../Docs/python_api_snipets.md')
snipets = open(snipets_path, 'r')
md.text(snipets.read())
snipets.close()
os.remove(snipets_path)
def gen_stub_method_def(method):
"""Return python def as it should be written in stub files"""
param = ''
method_name = method['def_name']
for p in method['params']:
p_type = join([': ', str(p['type'])]) if 'type' in p else ''
default = join([' = ', str(p['default'])]) if 'default' in p else ''
param = join([param, p['param_name'], p_type, default, ', '])
param = param[:-2] # delete the last ', '
return_type = join([' -> ', method['return']]) if 'return' in method else ''
return join([method_name, parentheses(param), return_type])
def gen_doc_method_def(method, class_key, is_indx=False, with_self=True):
"""Return python def as it should be written in docs"""
param = ''
snipet_link = ''
method_name = method['def_name']
full_method_name = method_name
if valid_dic_val(method, 'static'):
with_self = False
# to correctly render methods like __init__ in md
if method_name[0] == '_':
method_name = '\\' + method_name
if is_indx:
method_name = bold(method_name)
else:
method_name = bold(color(COLOR_METHOD, method_name))
if with_self:
if not 'params' in method or method['params'] is None:
method['params'] = []
method['params'].insert(0, {'param_name': 'self'})
if valid_dic_val(method, 'params'):
for p in method['params']:
default = join(['=', str(p['default'])]) if 'default' in p else ''
if is_indx:
param = join([param, bold(p['param_name']), default, ', '])
else:
param = join([param, color(COLOR_PARAM, bold(p['param_name']) + create_hyperlinks(default)), ', '])
if with_self:
method['params'] = method['params'][1:]
if not method['params']: # if is empty delete it
del method['params']
param = param[:-2] # delete the last ', '
# Add snipet
current_folder = os.path.dirname(os.path.abspath(__file__))
snipets_path = os.path.join(current_folder, '../../Docs/python_api_snipets.md')
snipets = open(snipets_path, 'r')
if class_key+'.'+full_method_name+'-snipet' in snipets.read():
snipet_link = snipet(full_method_name, class_key)
return join([method_name, parentheses(param),snipet_link])
def gen_doc_dunder_def(dunder, is_indx=False, with_self=True):
"""Return python def as it should be written in docs"""
param = ''
dunder_name = dunder['def_name']
if valid_dic_val(dunder, 'static'):
with_self = False
# to correctly render methods like __init__ in md
if dunder_name[0] == '_':
dunder_name = '\\' + dunder_name
if is_indx:
dunder_name = bold(dunder_name)
else:
dunder_name = bold(color(COLOR_METHOD, dunder_name))
if with_self:
if not 'params' in dunder or dunder['params'] is None:
dunder['params'] = []
dunder['params'].insert(0, {'param_name': 'self'})
if valid_dic_val(dunder, 'params'):
for p in dunder['params']:
default = join(['=', str(p['type'])]) if 'type' in p else ''
if is_indx:
param = join([param, bold(p['param_name']), default, ', '])
else:
param = join([param, color(COLOR_PARAM, bold(p['param_name']) + create_hyperlinks(default)), ', '])
if with_self:
dunder['params'] = dunder['params'][1:]
if not dunder['params']: # if is empty delete it
del dunder['params']
param = param[:-2] # delete the last ', '
return join([dunder_name, parentheses(param)])
def gen_inst_var_indx(inst_var, class_key):
inst_var_name = inst_var['var_name']
inst_var_key = join([class_key, inst_var_name], '.')
return join([
brackets(bold(inst_var_name)),
parentheses(inst_var_key), ' ',
sub(italic('Instance variable'))])
def gen_method_indx(method, class_key):
method_name = method['def_name']
method_key = join([class_key, method_name], '.')
method_def = gen_doc_method_def(method, class_key, True)
return join([
brackets(method_def),
parentheses(method_key), ' ',
sub(italic('Method'))])
def add_doc_method_param(md, param):
param_name = param['param_name']
param_type = ''
param_doc = ''
param_units = ''
if valid_dic_val(param, 'type'):
param_type = create_hyperlinks(param['type'])
if valid_dic_val(param, 'doc'):
param_doc = create_hyperlinks(md.prettify_doc(param['doc']))
if valid_dic_val(param, 'param_units'):
param_units = small_html(' - '+param['param_units'])
param_type = '' if not param_type else parentheses(italic(param_type+param_units))
md.list_push(code(param_name))
if param_type:
md.text(' ' + param_type)
if param_doc:
md.textn(' - ' + param_doc)
else:
md.new_line()
md.list_pop()
def add_doc_method(md, method, class_key):
method_name = method['def_name']
method_key = join([class_key, method_name], '.')
method_def = gen_doc_method_def(method, class_key, False)
md.list_pushn(join([html_key(method_key), method_def]))
# Method doc
if valid_dic_val(method, 'doc'):
md.textn(create_hyperlinks(md.prettify_doc(method['doc'])))
printed_title = False
if valid_dic_val(method, 'params'):
for param in method['params']:
# is_self = valid_dic_val(param, 'param_name') and param['param_name'] == 'self'
have_doc = valid_dic_val(param, 'doc')
have_type = valid_dic_val(param, 'type')
if not have_doc and not have_type:
continue
# Print the 'Parameters' title once
if not printed_title:
printed_title = True
md.list_push(bold('Parameters:') + '\n')
add_doc_method_param(md, param)
if printed_title:
md.list_pop()
# Return doc
if valid_dic_val(method, 'return'):
md.list_push(bold('Return:') + ' ')
return_units = ''
if valid_dic_val(method, 'return_units'):
return_units = small_html(' - '+method['return_units'])
md.textn(italic(create_hyperlinks(method['return'])+return_units))
md.list_pop()
# Note doc
if valid_dic_val(method, 'note'):
md.list_push(bold('Note:') + ' ')
md.textn(color(COLOR_NOTE, italic(create_hyperlinks(method['note']))))
md.list_pop()
# Warning doc
if valid_dic_val(method, 'warning'):
md.list_push(bold('Warning:') + ' ')
md.textn(color(COLOR_WARNING, italic(create_hyperlinks(method['warning']))))
md.list_pop()
# Raises error doc
if valid_dic_val(method, 'raises'):
md.list_pushn(bold('Raises:') + ' ' + method['raises'])
md.list_pop()
md.list_pop()
def add_doc_getter_setter(md, method, class_key, is_getter, other_list):
method_name = method['def_name']
method_key = join([class_key, method_name], '.')
method_def = gen_doc_method_def(method, class_key, False)
md.list_pushn(join([html_key(method_key), method_def]))
# Method doc
if valid_dic_val(method, 'doc'):
md.textn(create_hyperlinks(md.prettify_doc(method['doc'])))
printed_title = False
if valid_dic_val(method, 'params'):
for param in method['params']:
# is_self = valid_dic_val(param, 'param_name') and param['param_name'] == 'self'
have_doc = valid_dic_val(param, 'doc')
have_type = valid_dic_val(param, 'type')
if not have_doc and not have_type:
continue
# Print the 'Parameters' title once
if not printed_title:
printed_title = True
md.list_push(bold('Parameters:') + '\n')
add_doc_method_param(md, param)
if printed_title:
md.list_pop()
# Return doc
if valid_dic_val(method, 'return'):
md.list_push(bold('Return:') + ' ')
return_units = ''
if valid_dic_val(method, 'return_units'):
return_units = small_html(' - '+method['return_units'])
md.textn(italic(create_hyperlinks(method['return'])+return_units))
md.list_pop()
# If setter/getter
for element in other_list:
el_name = element['def_name']
if el_name[4:] == method_name[4:]:
if is_getter:
md.list_push(bold('Setter:') + ' ')
else:
md.list_push(bold('Getter:') + ' ')
md.textn(italic(create_hyperlinks(class_key+'.'+el_name)))
md.list_pop()
# Note doc
if valid_dic_val(method, 'note'):
md.list_push(bold('Note:') + ' ')
md.textn(color(COLOR_NOTE, italic(create_hyperlinks(method['note']))))
md.list_pop()
# Warning doc
if valid_dic_val(method, 'warning'):
md.list_push(bold('Warning:') + ' ')
md.textn(color(COLOR_WARNING, italic(create_hyperlinks(method['warning']))))
md.list_pop()
# Raises error doc
if valid_dic_val(method, 'raises'):
md.list_pushn(bold('Raises:') + ' ' + method['raises'])
md.list_pop()
md.list_pop()
def add_doc_dunder(md, dunder, class_key):
dunder_name = dunder['def_name']
dunder_key = join([class_key, dunder_name], '.')
dunder_def = gen_doc_dunder_def(dunder, False)
md.list_pushn(join([html_key(dunder_key), dunder_def]))
# Dunder doc
if valid_dic_val(dunder, 'doc'):
md.textn(create_hyperlinks(md.prettify_doc(dunder['doc'])))
# Return doc
if valid_dic_val(dunder, 'return'):
md.list_push(bold('Return:') + ' ')
md.textn(italic(create_hyperlinks(dunder['return'])))
md.list_pop()
md.list_pop()
def add_doc_dunder_param(md, param):
param_name = param['param_name']
param_type = ''
if valid_dic_val(param, 'type'):
param_type = create_hyperlinks(param['type'])
param_type = '' if not param_type else parentheses(italic(param_type))
md.list_push(code(param_name))
if param_type:
md.text(' ' + param_type)
md.new_line()
else:
md.new_line()
md.list_pop()
def add_doc_inst_var(md, inst_var, class_key):
var_name = inst_var['var_name']
var_key = join([class_key, var_name], '.')
var_type = ''
var_units = ''
# Instance variable type
if valid_dic_val(inst_var, 'type'):
if valid_dic_val(inst_var, 'var_units'):
var_units = small_html(' - '+inst_var['var_units'])
var_type = ' ' + parentheses(italic(create_hyperlinks(inst_var['type']+var_units)))
md.list_pushn(
html_key(var_key) +
bold(color(COLOR_INSTANCE_VAR, var_name)) +
var_type)
# Instance variable doc
if valid_dic_val(inst_var, 'doc'):
md.textn(create_hyperlinks(md.prettify_doc(inst_var['doc'])))
# Note doc
if valid_dic_val(inst_var, 'note'):
md.list_push(bold('Note:') + ' ')
md.textn(color(COLOR_NOTE, italic(create_hyperlinks(inst_var['note']))))
md.list_pop()
# Warning doc
if valid_dic_val(inst_var, 'warning'):
md.list_push(bold('Warning:') + ' ')
md.textn(color(COLOR_WARNING, italic(create_hyperlinks(inst_var['warning']))))
md.list_pop()
md.list_pop()
class Documentation:
"""Main documentation class"""
def __init__(self, path):
self._path = path
self._files = [f for f in os.listdir(path) if f.endswith('.yml')]
self._yamls = list()
for yaml_file in self._files:
self._yamls.append(YamlFile(os.path.join(path, yaml_file)))
# Merge same modules of different files
self.master_dict = dict()
for yaml_file in self._yamls:
for module in yaml_file.get_modules():
module_name = module['module_name']
if module_name not in self.master_dict:
self.master_dict[module_name] = module
elif valid_dic_val(module, 'classes'):
for new_module in module['classes']:
# Create the 'classes' key if does not exist already
if not valid_dic_val(self.master_dict[module_name], 'classes'):
self.master_dict[module_name]['classes'] = []
self.master_dict[module_name]['classes'].append(new_module)
def gen_overview(self):
"""Generates a referenced index for markdown file"""
md = MarkdownFile()
md.title(3, 'Overview')
for module_name in sorted(self.master_dict):
module = self.master_dict[module_name]
module_key = '#' + module_name
md.list_pushn(
brackets(bold(module_key[1:])) +
parentheses(module_key) + ' ' +
sub(italic('Module')))
# Generate class overview (if any)
if 'classes' in module and module['classes']:
for cl in sorted(module['classes']):
class_name = cl['class_name']
class_key = join([module_key, class_name], '.')
md.list_pushn(join([
brackets(bold(class_name)),
parentheses(class_key), ' ',
sub(italic('Class'))]))
# Generate class instance variables overview (if any)
if 'instance_variables' in cl and cl['instance_variables']:
for inst_var in cl['instance_variables']:
md.list_push(gen_inst_var_indx(inst_var, class_key))
md.list_popn()
# Generate class methods overview (if any)
if 'methods' in cl and cl['methods']:
for method in sorted(cl['methods'], key = lambda i: i['def_name']):
md.list_push(gen_method_indx(method, class_key))
md.list_popn()
md.list_pop()
md.list_pop()
return md.data()
def gen_body(self):
"""Generates the documentation body"""
md = MarkdownFile()
md.first_title()
md.textn(
"This reference contains all the details the Python API. To consult a previous reference for a specific CARLA release, change the documentation version using the panel in the bottom right corner.<br>"
+"This will change the whole documentation to a previous state. Remember that the <i>latest</i> version is the `dev` branch and may show features not available in any packaged versions of CARLA.<hr>")
for module_name in sorted(self.master_dict):
module = self.master_dict[module_name]
module_key = module_name
# Generate class doc (if any)
if valid_dic_val(module, 'classes'):
for cl in sorted(module['classes'], key = lambda i: i['class_name']):
class_name = cl['class_name']
class_key = join([module_key, class_name], '.')
current_title = module_name+'.'+class_name
md.title(2, join([current_title,'<a name="',current_title,'"></a>']))
# Inheritance
if valid_dic_val(cl, 'parent'):
inherits = italic(create_hyperlinks(cl['parent']))
md.inherit_join(inherits)
# Class main doc
if valid_dic_val(cl, 'doc'):
md.textn(create_hyperlinks(md.prettify_doc(cl['doc'])))
# Generate instance variable doc (if any)
if valid_dic_val(cl, 'instance_variables'):
md.title(3, 'Instance Variables')
for inst_var in cl['instance_variables']:
add_doc_inst_var(md, inst_var, class_key)
# Generate method doc (if any)
if valid_dic_val(cl, 'methods'):
method_list = list()
dunder_list = list()
get_list = list()
set_list = list()
for method in sorted(cl['methods'], key = lambda i: i['def_name']):
method_name = method['def_name']
if method_name[0] == '_' and method_name != '__init__':
dunder_list.append(method)
elif method_name[:4] == 'get_':
get_list.append(method)
elif method_name[:4] == 'set_':
set_list.append(method)
else:
method_list.append(method)
md.title(3, 'Methods')
for method in method_list:
add_doc_method(md, method, class_key)
if len(get_list)>0:
md.title(5, 'Getters')
for method in get_list:
add_doc_getter_setter(md, method, class_key, True, set_list)
if len(set_list)>0:
md.title(5, 'Setters')
for method in set_list:
add_doc_getter_setter(md, method, class_key, False, get_list)
if len(dunder_list)>0:
md.title(5, 'Dunder methods')
for method in dunder_list:
add_doc_dunder(md, method, class_key)
md.separator()
append_code_snipets(md)
append_snipet_button_script(md)
return md.data().strip()
def gen_markdown(self):
"""Generates the whole markdown file"""
return join([self.gen_body()], '\n').strip()
def main():
"""Main function"""
print("Generating PythonAPI documentation...")
script_path = os.path.dirname(os.path.abspath(__file__))
doc_gen_snipets.main()
docs = Documentation(script_path)
with open(os.path.join(script_path, '../../Docs/python_api.md'), 'w') as md_file:
md_file.write(docs.gen_markdown())
print("Done!")
if __name__ == "__main__":
main()