1
0
Fork 0
mirror of https://github.com/iNavFlight/inav.git synced 2025-07-26 09:45:33 +03:00
inav/src/utils/update_cli_docs.py
2021-04-24 15:45:40 +02:00

193 lines
8.4 KiB
Python
Executable file

#!/usr/bin/env python3
import optparse
import os
import re
import yaml # pyyaml / python-yaml
SETTINGS_MD_PATH = "docs/Settings.md"
SETTINGS_YAML_PATH = "src/main/fc/settings.yaml"
CODE_DEFAULTS_PATH = "src/main"
DEFAULTS_BLACKLIST = [
'baro_hardware',
'dterm_lpf_type',
'dterm_lpf2_type',
'failsafe_procedure',
'flaperon_throw_offset',
'heading_hold_rate_limit',
'mag_hardware',
'pitot_hardware',
'rx_min_usec',
'serialrx_provider',
]
MIN_MAX_REPLACEMENTS = {
'INT16_MIN': -32768,
'INT16_MAX': 32767,
'INT32_MIN': -2147483648,
'INT32_MAX': 2147483647,
'UINT8_MAX': 255,
'UINT16_MAX': 65535,
'UINT32_MAX': 4294967295,
}
def parse_settings_yaml():
"""Parse the YAML settings specs"""
with open(SETTINGS_YAML_PATH, "r") as settings_yaml:
return yaml.load(settings_yaml, Loader=yaml.Loader)
def generate_md_table_from_yaml(settings_yaml):
"""Generate a sorted markdown table with description & default value for each setting"""
params = {}
# Extract description, default/min/max values of each setting from the YAML specs (if present)
for group in settings_yaml['groups']:
for member in group['members']:
if not any(key in member for key in ["description", "default_value", "min", "max"]) and not options.quiet:
print("Setting \"{}\" has an incomplete specification".format(member['name']))
# Handle default/min/max fields for each setting
for key in ["default_value", "min", "max"]:
# Basing on the check above, not all fields may be present
if key in member:
### Fetch actual values from the `constants` block if present
if ('constants' in settings_yaml) and (member[key] in settings_yaml['constants']):
member[key] = settings_yaml['constants'][member[key]]
### Fetch actual values from hardcoded min/max replacements
elif member[key] in MIN_MAX_REPLACEMENTS:
member[key] = MIN_MAX_REPLACEMENTS[member[key]]
### Handle edge cases of YAML autogeneration and prettify some values
# Replace booleans with "ON"/"OFF"
if type(member[key]) == bool:
member[key] = "ON" if member[key] else "OFF"
# Replace zero placeholder with actual zero
elif member[key] == ":zero":
member[key] = 0
# Replace target-default placeholder with extended definition
elif member[key] == ":target":
member[key] = "_target default_"
# Replace empty strings with more evident marker
elif member[key] == "":
member[key] = "_empty_"
# Reformat direct code references
elif str(member[key])[0] == ":":
member[key] = f'`{member[key][1:]}`'
params[member['name']] = {
"description": member["description"] if "description" in member else "",
"default": member["default_value"] if "default_value" in member else "",
"min": member["min"] if "min" in member else "",
"max": member["max"] if "max" in member else ""
}
# MD table header
md_table_lines = [
"| Variable Name | Default Value | Min | Max | Description |\n",
"| ------------- | ------------- | --- | --- | ----------- |\n",
]
# Sort the settings by name and build the rows of the table
for param in sorted(params.items()):
md_table_lines.append("| {} | {} | {} | {} | {} |\n".format(
param[0], param[1]['default'], param[1]['min'], param[1]['max'], param[1]['description']
))
# Return the assembled table
return md_table_lines
def write_settings_md(lines):
"""Write the contents of the CLI settings docs"""
with open(SETTINGS_MD_PATH, "w") as settings_md:
settings_md.writelines(lines)
# Return all matches of a compiled regex in a list of files
def regex_search(regex, files):
for f in files:
with open(f, 'r') as _f:
for _, line in enumerate(_f.readlines()):
matches = regex.search(line)
if matches:
yield matches
# Return plausible default values for a given setting found by scraping the relative source files
def find_default(setting_name, headers):
regex = re.compile(rf'^\s*\.{setting_name}\s=\s([A-Za-z0-9_\-]+)(?:,)?(?:\s+//.+$)?')
files_to_search_in = []
for header in headers:
header = f'{CODE_DEFAULTS_PATH}/{header}'
files_to_search_in.append(header)
if header.endswith('.h'):
header_c = re.sub(r'\.h$', '.c', header)
if os.path.isfile(header_c):
files_to_search_in.append(header_c)
defaults = []
for matches in regex_search(regex, files_to_search_in):
defaults.append(matches.group(1))
return defaults
# Try to map default values in the YAML spec back to the actual C code and check for mismatches (defaults updated in the code but not in the YAML)
# Done by scraping the source files, prone to false negatives. Settings in `DEFAULTS_BLACKLIST` are ignored for this
# reason (values that refer to other source files are too complex to parse this way).
def check_defaults(settings_yaml):
retval = True
for group in settings_yaml['groups']:
if 'headers' in group:
headers = group['headers']
for member in group['members']:
# Ignore blacklisted settings
if member['name'] in DEFAULTS_BLACKLIST:
continue
default_from_code = find_default(member['name'], headers)
if len(default_from_code) == 0: # No default value found (no direct code mapping)
continue
elif len(default_from_code) > 1: # Duplicate default values found (regexes are a quick but poor solution)
if not options.quiet:
print(f"Duplicate default values found for {member['name']}: {default_from_code}, consider adding to blacklist")
continue
# Extract the only matched value, guarded by the previous checks
default_from_code = default_from_code[0]
# Map C values to their equivalents used in the YAML spec
code_values_map = { 'true': 'ON', 'false': 'OFF' }
if default_from_code in code_values_map:
default_from_code = code_values_map[default_from_code]
default_from_yaml = member["default_value"] if "default_value" in member else ""
# Remove eventual Markdown formatting
default_from_yaml = default_from_yaml.replace('`', '').replace('*', '').replace('__', '')
# Allow specific C-YAML matches that coudln't be replaced in the previous steps
extra_allowed_matches = { '1': 'ON', '0': 'OFF' }
if default_from_yaml not in default_from_code: # Equal or substring
if default_from_code in extra_allowed_matches and default_from_yaml in extra_allowed_matches[default_from_code]:
continue
if not options.quiet:
print(f"{member['name']} has mismatched default values. Code reports '{default_from_code}' and YAML reports '{default_from_yaml}'")
retval = False
return retval
if __name__ == "__main__":
global options, args
parser = optparse.OptionParser()
parser.add_option('-q', '--quiet', action="store_true", default=False, help="do not write anything to stdout")
parser.add_option('-d', '--defaults', action="store_true", default=False, help="check for mismatched default values")
options, args = parser.parse_args()
settings_yaml = parse_settings_yaml()
if options.defaults:
defaults_match = check_defaults(settings_yaml)
quit(0 if defaults_match else 1)
md_table_lines = generate_md_table_from_yaml(settings_yaml)
settings_md_lines = \
["# CLI Variable Reference\n", "\n" ] + \
md_table_lines + \
["\n", "> Note: this table is autogenerated. Do not edit it manually."]
write_settings_md(settings_md_lines)