Merge pull request #4402 from bojidar-bg/docteam-add-helper
Add a python script to check the current doc status
This commit is contained in:
commit
7d89a8b748
|
@ -0,0 +1,430 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import sys
|
||||
import re
|
||||
import math
|
||||
import platform
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
################################################################################
|
||||
# Config #
|
||||
################################################################################
|
||||
|
||||
flags = {
|
||||
'c': platform.platform() != 'Windows', # Disable by default on windows, since we use ANSI escape codes
|
||||
'b': False,
|
||||
'g': False,
|
||||
'u': False,
|
||||
'h': False,
|
||||
'p': False,
|
||||
'o': True,
|
||||
'i': False,
|
||||
}
|
||||
flag_descriptions = {
|
||||
'c': 'Toggle colors when outputting.',
|
||||
'b': 'Toggle showing only not fully described classes.',
|
||||
'g': 'Toggle showing only completed classes.',
|
||||
'u': 'Toggle URLs to docs.',
|
||||
'h': 'Show help and exit.',
|
||||
'p': 'Toggle showing percentage as well as counts.',
|
||||
'o': 'Toggle overall column.',
|
||||
'i': 'Toggle collapse of class items columns.',
|
||||
}
|
||||
long_flags = {
|
||||
'colors': 'c',
|
||||
'color': 'c',
|
||||
'use-colors': 'c',
|
||||
|
||||
'bad': 'b',
|
||||
'only-bad': 'b',
|
||||
|
||||
'good': 'g',
|
||||
'only-good': 'g',
|
||||
|
||||
'urls': 'u',
|
||||
'url': 'u',
|
||||
'generate-urls': 'u',
|
||||
'gen-url': 'u',
|
||||
|
||||
'help': 'h',
|
||||
|
||||
'percent': 'p',
|
||||
'percentages': 'p',
|
||||
'use-percentages': 'p',
|
||||
|
||||
'overall': 'o',
|
||||
'use-overall': 'o',
|
||||
|
||||
'items': 'i',
|
||||
'collapse-items': 'i',
|
||||
'collapse': 'i',
|
||||
'narrow': 'i',
|
||||
}
|
||||
table_columns = ['name', 'brief_description', 'description', 'methods', 'constants', 'members', 'signals']
|
||||
table_column_names = ['Name', 'Brief Description', 'Description', 'Methods', 'Constants', 'Members', 'Signals']
|
||||
colors = {
|
||||
'name': [34], # blue
|
||||
'part_big_problem': [4, 31], # underline, red
|
||||
'part_problem': [31], # red
|
||||
'part_mostly_good': [33], # yellow
|
||||
'part_good': [32], # green
|
||||
'url': [4, 34], # underline, blue
|
||||
'section': [1, 4], # bold, underline
|
||||
'state_off': [36], # cyan
|
||||
'state_on': [1, 35], # bold, magenta/plum
|
||||
}
|
||||
overall_progress_description_weigth = 10
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# Utils #
|
||||
################################################################################
|
||||
|
||||
def validate_tag(elem, tag):
|
||||
if elem.tag != tag:
|
||||
print('Tag mismatch, expected "' + tag + '", got ' + elem.tag)
|
||||
sys.exit(255)
|
||||
|
||||
def color(color, string):
|
||||
if flags['c']:
|
||||
color_format = ''
|
||||
for code in colors[color]:
|
||||
color_format += '\033[' + str(code) + 'm'
|
||||
return color_format + string + '\033[0m'
|
||||
else:
|
||||
return string
|
||||
|
||||
ansi_escape = re.compile(r'\x1b[^m]*m')
|
||||
def nonescape_len(s):
|
||||
return len(ansi_escape.sub('', s))
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# Classes #
|
||||
################################################################################
|
||||
|
||||
class ClassStatusProgress:
|
||||
def __init__(self, described = 0, total = 0):
|
||||
self.described = described
|
||||
self.total = total
|
||||
|
||||
def __add__(self, other):
|
||||
return ClassStatusProgress(self.described + other.described, self.total + other.total)
|
||||
|
||||
def increment(self, described):
|
||||
if described:
|
||||
self.described += 1
|
||||
self.total += 1
|
||||
|
||||
def is_ok(self):
|
||||
return self.described >= self.total
|
||||
|
||||
def to_configured_colored_string(self):
|
||||
if flags['p']:
|
||||
return self.to_colored_string('{percent}% ({has}/{total})', '{pad_percent}{pad_described}{s}{pad_total}')
|
||||
else:
|
||||
return self.to_colored_string()
|
||||
|
||||
def to_colored_string(self, format='{has}/{total}', pad_format='{pad_described}{s}{pad_total}'):
|
||||
ratio = self.described/self.total if self.total != 0 else 1
|
||||
percent = round(100*ratio)
|
||||
s = format.format(has = str(self.described), total = str(self.total), percent = str(percent))
|
||||
if self.described >= self.total:
|
||||
s = color('part_good', s)
|
||||
elif self.described >= self.total/4*3:
|
||||
s = color('part_mostly_good', s)
|
||||
elif self.described > 0:
|
||||
s = color('part_problem', s)
|
||||
else:
|
||||
s = color('part_big_problem', s)
|
||||
pad_size = max(len(str(self.described)), len(str(self.total)))
|
||||
pad_described = ''.ljust(pad_size - len(str(self.described)))
|
||||
pad_percent = ''.ljust(3 - len(str(percent)))
|
||||
pad_total = ''.ljust(pad_size - len(str(self.total)))
|
||||
return pad_format.format(pad_described = pad_described, pad_total = pad_total, pad_percent = pad_percent, s = s)
|
||||
|
||||
|
||||
class ClassStatus:
|
||||
def __init__(self, name=''):
|
||||
self.name = name
|
||||
self.has_brief_description = True
|
||||
self.has_description = True
|
||||
self.progresses = {
|
||||
'methods': ClassStatusProgress(),
|
||||
'constants': ClassStatusProgress(),
|
||||
'members': ClassStatusProgress(),
|
||||
'signals': ClassStatusProgress()
|
||||
}
|
||||
|
||||
def __add__(self, other):
|
||||
new_status = ClassStatus()
|
||||
new_status.name = self.name
|
||||
new_status.has_brief_description = self.has_brief_description and other.has_brief_description
|
||||
new_status.has_description = self.has_description and other.has_description
|
||||
for k in self.progresses:
|
||||
new_status.progresses[k] = self.progresses[k] + other.progresses[k]
|
||||
return new_status
|
||||
|
||||
def is_ok(self):
|
||||
ok = True
|
||||
ok = ok and self.has_brief_description
|
||||
ok = ok and self.has_description
|
||||
for k in self.progresses:
|
||||
ok = ok and self.progresses[k].is_ok()
|
||||
return ok
|
||||
|
||||
def make_output(self):
|
||||
output = {}
|
||||
output['name'] = color('name', self.name)
|
||||
|
||||
ok_string = color('part_good', 'OK')
|
||||
missing_string = color('part_big_problem', 'MISSING')
|
||||
|
||||
output['brief_description'] = ok_string if self.has_brief_description else missing_string
|
||||
output['description'] = ok_string if self.has_description else missing_string
|
||||
|
||||
description_progress = ClassStatusProgress(
|
||||
(self.has_brief_description + self.has_description) * overall_progress_description_weigth,
|
||||
2 * overall_progress_description_weigth
|
||||
)
|
||||
items_progress = ClassStatusProgress()
|
||||
|
||||
for k in ['methods', 'constants', 'members', 'signals']:
|
||||
items_progress += self.progresses[k]
|
||||
output[k] = self.progresses[k].to_configured_colored_string()
|
||||
|
||||
output['items'] = items_progress.to_configured_colored_string()
|
||||
|
||||
output['overall'] = (description_progress + items_progress).to_colored_string('{percent}%', '{pad_percent}{s}')
|
||||
|
||||
if self.name.startswith('Total'):
|
||||
output['url'] = color('url', 'http://docs.godotengine.org/en/latest/classes/_classes.html')
|
||||
output['comment'] = color('part_good', 'ALL OK')
|
||||
else:
|
||||
output['url'] = color('url', 'http://docs.godotengine.org/en/latest/classes/class_{name}.html'.format(name=self.name.lower()))
|
||||
|
||||
if not flags['g'] and self.is_ok():
|
||||
output['comment'] = color('part_good', 'ALL OK')
|
||||
|
||||
return output
|
||||
|
||||
def generate_for_class(c):
|
||||
status = ClassStatus()
|
||||
status.name = c.attrib['name']
|
||||
for tag in list(c):
|
||||
|
||||
if tag.tag == 'brief_description':
|
||||
status.has_brief_description = len(tag.text.strip()) > 0
|
||||
|
||||
elif tag.tag == 'description':
|
||||
status.has_description = len(tag.text.strip()) > 0
|
||||
|
||||
elif tag.tag in ['methods', 'signals']:
|
||||
for sub_tag in list(tag):
|
||||
descr = sub_tag.find('description')
|
||||
status.progresses[tag.tag].increment(len(descr.text.strip()) > 0)
|
||||
|
||||
elif tag.tag in ['constants', 'members']:
|
||||
for sub_tag in list(tag):
|
||||
status.progresses[tag.tag].increment(len(sub_tag.text.strip()) > 0)
|
||||
|
||||
elif tag.tag in ['theme_items']:
|
||||
pass #Ignore those tags, since they seem to lack description at all
|
||||
|
||||
else:
|
||||
print(tag.tag, tag.attrib)
|
||||
|
||||
return status
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# Arguments #
|
||||
################################################################################
|
||||
|
||||
input_file_list = []
|
||||
input_class_list = []
|
||||
|
||||
for arg in sys.argv[1:]:
|
||||
if arg.startswith('--'):
|
||||
flags[long_flags[arg[2:]]] = not flags[long_flags[arg[2:]]]
|
||||
elif arg.startswith('-'):
|
||||
for f in arg[1:]:
|
||||
flags[f] = not flags[f]
|
||||
elif arg.endswith('.xml'):
|
||||
input_file_list.append(arg)
|
||||
else:
|
||||
input_class_list.append(arg)
|
||||
|
||||
if flags['i']:
|
||||
for r in ['methods', 'constants', 'members', 'signals']:
|
||||
index = table_columns.index(r)
|
||||
del table_column_names[index]
|
||||
del table_columns[index]
|
||||
table_column_names.append('Items')
|
||||
table_columns.append('items')
|
||||
|
||||
if flags['o'] == (not flags['i']):
|
||||
table_column_names.append('Overall')
|
||||
table_columns.append('overall')
|
||||
|
||||
if flags['u']:
|
||||
table_column_names.append('Docs URL')
|
||||
table_columns.append('url')
|
||||
|
||||
|
||||
################################################################################
|
||||
# Help #
|
||||
################################################################################
|
||||
|
||||
if len(input_file_list) < 1 or flags['h']:
|
||||
if not flags['h']:
|
||||
print(color('section', 'Invalid usage') + ': At least one classes.xml file is required')
|
||||
print(color('section', 'Usage') + ': doc_status.py [flags] <classes.xml> [class names]')
|
||||
print('\t< and > signify required parameters, while [ and ] signify optional parameters.')
|
||||
print('\tNote that you can give more than one classes file, in which case they will be merged on-the-fly.')
|
||||
print(color('section', 'Available flags') + ':')
|
||||
possible_synonym_list = list(long_flags)
|
||||
possible_synonym_list.sort()
|
||||
flag_list = list(flags)
|
||||
flag_list.sort()
|
||||
for flag in flag_list:
|
||||
synonyms = [color('name', '-' + flag)]
|
||||
for synonym in possible_synonym_list:
|
||||
if long_flags[synonym] == flag:
|
||||
synonyms.append(color('name', '--' + synonym))
|
||||
|
||||
print(('{synonyms} (Currently '+color('state_'+('on' if flags[flag] else 'off'), '{value}')+')\n\t{description}').format(
|
||||
synonyms = ', '.join(synonyms),
|
||||
value = ('on' if flags[flag] else 'off'),
|
||||
description = flag_descriptions[flag]
|
||||
))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# Parse class list #
|
||||
################################################################################
|
||||
|
||||
class_names = []
|
||||
classes = {}
|
||||
|
||||
for file in input_file_list:
|
||||
tree = ET.parse(file)
|
||||
doc = tree.getroot()
|
||||
|
||||
if 'version' not in doc.attrib:
|
||||
print('Version missing from "doc"')
|
||||
sys.exit(255)
|
||||
|
||||
version = doc.attrib['version']
|
||||
|
||||
for c in list(doc):
|
||||
if c.attrib['name'] in class_names:
|
||||
continue
|
||||
class_names.append(c.attrib['name'])
|
||||
classes[c.attrib['name']] = c
|
||||
|
||||
class_names.sort()
|
||||
|
||||
if len(input_class_list) < 1:
|
||||
input_class_list = class_names
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# Make output table #
|
||||
################################################################################
|
||||
|
||||
table = [table_column_names]
|
||||
table_row_chars = '+- '
|
||||
table_column_chars = '|'
|
||||
|
||||
total_status = ClassStatus('Total')
|
||||
|
||||
for cn in input_class_list:
|
||||
if not cn in classes:
|
||||
print('Cannot find class ' + cn + '!')
|
||||
sys.exit(255)
|
||||
|
||||
c = classes[cn]
|
||||
validate_tag(c, 'class')
|
||||
status = ClassStatus.generate_for_class(c)
|
||||
|
||||
if flags['b'] and status.is_ok():
|
||||
continue
|
||||
if flags['g'] and not status.is_ok():
|
||||
continue
|
||||
|
||||
total_status = total_status + status
|
||||
out = status.make_output()
|
||||
row = []
|
||||
for column in table_columns:
|
||||
if column in out:
|
||||
row.append(out[column])
|
||||
else:
|
||||
row.append('')
|
||||
|
||||
if 'comment' in out and out['comment'] != '':
|
||||
row.append(out['comment'])
|
||||
|
||||
table.append(row)
|
||||
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# Print output table #
|
||||
################################################################################
|
||||
|
||||
if len(table) == 1:
|
||||
print(color('part_big_problem', 'No classes suitable for printing!'))
|
||||
sys.exit(0)
|
||||
|
||||
if len(table) > 2:
|
||||
total_status.name = 'Total = {0}'.format(len(table) - 1)
|
||||
out = total_status.make_output()
|
||||
row = []
|
||||
for column in table_columns:
|
||||
if column in out:
|
||||
row.append(out[column])
|
||||
else:
|
||||
row.append('')
|
||||
table.append(row)
|
||||
|
||||
table_column_sizes = []
|
||||
for row in table:
|
||||
for cell_i, cell in enumerate(row):
|
||||
if cell_i >= len(table_column_sizes):
|
||||
table_column_sizes.append(0)
|
||||
|
||||
table_column_sizes[cell_i] = max(nonescape_len(cell), table_column_sizes[cell_i])
|
||||
|
||||
divider_string = table_row_chars[0]
|
||||
for cell_i in range(len(table[0])):
|
||||
divider_string += table_row_chars[1] * (table_column_sizes[cell_i] + 2) + table_row_chars[0]
|
||||
print(divider_string)
|
||||
|
||||
for row_i, row in enumerate(table):
|
||||
row_string = table_column_chars
|
||||
for cell_i, cell in enumerate(row):
|
||||
padding_needed = table_column_sizes[cell_i] - nonescape_len(cell) + 2
|
||||
if cell_i == 0:
|
||||
row_string += table_row_chars[2] + cell + table_row_chars[2]*(padding_needed-1)
|
||||
else:
|
||||
row_string += table_row_chars[2]*math.floor(padding_needed/2) + cell + table_row_chars[2]*math.ceil((padding_needed/2))
|
||||
row_string += table_column_chars
|
||||
|
||||
print(row_string)
|
||||
|
||||
if row_i == 0 or row_i == len(table) - 2:
|
||||
print(divider_string)
|
||||
|
||||
print(divider_string)
|
||||
|
||||
if total_status.is_ok() and not flags['g']:
|
||||
print('All listed classes are ' + color('part_good', 'OK') + '!')
|
||||
|
Loading…
Reference in New Issue