mirror of
https://github.com/acemod/ACE3.git
synced 2024-08-30 18:23:18 +00:00
0cd904bdc3
* Remove obsolete event rename script * Add initial function documenting python script - Currently only finds all public/invalid function files - Method "document_public" is in place to handle the data taken from a function header, but currently unimplemented * Add author/description/arguments processing * Improve console logging and add return processing * Use class based approach for better data handling * Add argument processing with support for notes * Implement rudimentary doc output * Add return and example processing * Fix example variable * Fix documenting no arguments/return * Fix malformed return handling * Improve control flow
307 lines
10 KiB
Python
307 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Author: SilentSpike
|
|
Crawl function headers to produce appropriate documentation of public functions.
|
|
|
|
Supported header sections:
|
|
- Author(s) (with description below)
|
|
- Arguments
|
|
- Return Value
|
|
- Example(s)
|
|
- Public (by default function will only be documented if set to "Yes")
|
|
|
|
EXAMPLES
|
|
document_functions common --debug
|
|
Crawl only functions in addons/common and only reports debug messages.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import re
|
|
import argparse
|
|
|
|
class FunctionFile:
|
|
def __init__(self, directory="."):
|
|
self.directory = directory
|
|
|
|
# False unless specified in processing
|
|
self.debug = False
|
|
|
|
# Empty until imported from file
|
|
self.path = ""
|
|
self.header = ""
|
|
|
|
# Defaults until header is processed
|
|
self.public = False
|
|
self.authors = []
|
|
self.description = ""
|
|
self.arguments = []
|
|
self.return_value = []
|
|
self.example = ""
|
|
|
|
# Filepath should only be logged once
|
|
self.logged = False
|
|
|
|
def import_header(self, file_path):
|
|
self.path = file_path
|
|
|
|
with open(file_path) as file:
|
|
code = file.read()
|
|
|
|
header_match = re.match(r"\s*/\*.+?\*/", code, re.S)
|
|
if header_match:
|
|
self.header = header_match.group(0)
|
|
else:
|
|
self.feedback("Missing header", 3)
|
|
|
|
def has_header(self):
|
|
return bool(self.header)
|
|
|
|
def process_header(self, debug=False):
|
|
# Detailed debugging occurs here so value is set
|
|
self.debug = debug
|
|
|
|
# Preemptively cut away the comment characters (and leading/trailing whitespace)
|
|
self.header_text = "\n".join([x[3:].strip() for x in self.header.splitlines()])
|
|
|
|
# Split the header into expected sections
|
|
self.sections = re.split(r"^(Author|Argument|Return Value|Example|Public)s?:\s?", self.header_text, 0, re.M)
|
|
|
|
# If public section is missing we can't continue
|
|
public_raw = self.get_section("Public")
|
|
if not public_raw:
|
|
self.feedback("Public value undefined", 3)
|
|
return
|
|
|
|
# Determine whether the header is public
|
|
self.public = self.process_public(public_raw)
|
|
|
|
# Don't bother to process the rest if private
|
|
# Unless in debug mode
|
|
if not self.public and not self.debug:
|
|
return
|
|
|
|
# Retrieve the raw sections text for processing
|
|
author_raw = self.get_section("Author")
|
|
arguments_raw = self.get_section("Argument")
|
|
return_value_raw = self.get_section("Return Value")
|
|
example_raw = self.get_section("Example")
|
|
|
|
# Author and description are stored in first section
|
|
if author_raw:
|
|
self.authors = self.process_author(author_raw)
|
|
self.description = self.process_description(author_raw)
|
|
|
|
if arguments_raw:
|
|
self.arguments = self.process_arguments(arguments_raw)
|
|
|
|
# Process return
|
|
if return_value_raw:
|
|
self.return_value = self.process_return_value(return_value_raw)
|
|
|
|
# Process example
|
|
if example_raw:
|
|
self.example = example_raw.strip()
|
|
|
|
def get_section(self, section_name):
|
|
try:
|
|
section_text = self.sections[self.sections.index(section_name) + 1]
|
|
return section_text
|
|
except ValueError:
|
|
self.feedback("Missing \"{}\" header section".format(section_name), 2)
|
|
return ""
|
|
|
|
def process_public(self, raw):
|
|
# Raw just includes an EOL character
|
|
public_text = raw[:-1]
|
|
|
|
if not re.match(r"(Yes|No)", public_text, re.I):
|
|
self.feedback("Invalid public value \"{}\"".format(public_text), 2)
|
|
|
|
return public_text.capitalize() == "Yes"
|
|
|
|
def is_public(self):
|
|
return self.public
|
|
|
|
def process_author(self, raw):
|
|
# Authors are listed on the first line
|
|
authors_text = raw.splitlines()[0]
|
|
|
|
# Seperate authors are divided by commas
|
|
return authors_text.split(", ")
|
|
|
|
def process_description(self, raw):
|
|
# Just use all the lines after the authors line
|
|
description_text = "".join(raw.splitlines(1)[1:])
|
|
|
|
return description_text
|
|
|
|
def process_arguments(self, raw):
|
|
lines = raw.splitlines()
|
|
|
|
if lines[0] == "None":
|
|
return []
|
|
|
|
if lines.count("") == len(lines):
|
|
self.feedback("No arguments provided (use \"None\" where appropriate)", 2)
|
|
return []
|
|
|
|
if lines[-1] == "":
|
|
lines.pop()
|
|
else:
|
|
self.feedback("No blank line after arguments list", 1)
|
|
|
|
arguments = []
|
|
for argument in lines:
|
|
valid = re.match(r"^(\d+):\s(.+?)\<([\s\w]+?)\>(\s\(default: (.+)\))?$", argument)
|
|
|
|
if valid:
|
|
arg_index = valid.group(1)
|
|
arg_name = valid.group(2)
|
|
arg_types = valid.group(3)
|
|
arg_default = valid.group(5)
|
|
arg_notes = []
|
|
|
|
if arg_index != str(len(arguments)):
|
|
self.feedback("Argument index {} does not match listed order".format(arg_index), 1)
|
|
|
|
if arg_default == None:
|
|
arg_default = ""
|
|
|
|
arguments.append([arg_index, arg_name, arg_types, arg_default, arg_notes])
|
|
else:
|
|
# Notes about the above argument won't start with an index
|
|
# Only applies if there exists an above argument
|
|
if re.match(r"^(\d+):", argument) or not arguments:
|
|
self.feedback("Malformed argument \"{}\"".format(argument), 2)
|
|
arguments.append(["?", "Malformed", "?", "?", []])
|
|
else:
|
|
arguments[-1][-1].append(argument)
|
|
|
|
return arguments
|
|
|
|
def process_return_value(self, raw):
|
|
return_value = raw.strip()
|
|
|
|
if return_value == "None":
|
|
return []
|
|
|
|
valid = re.match(r"^(.+?)\<([\s\w]+?)\>", return_value)
|
|
|
|
if valid:
|
|
return_name = valid.group(1)
|
|
return_types = valid.group(2)
|
|
else:
|
|
self.feedback("Malformed return value \"{}\"".format(return_value), 2)
|
|
return ["Malformed",""]
|
|
|
|
return [return_name, return_types]
|
|
|
|
def document(self, component):
|
|
str_list = []
|
|
|
|
# Title
|
|
str_list.append("\n## ace_{}_fnc_{}\n".format(component,os.path.basename(self.path)[4:-4]))
|
|
# Description
|
|
str_list.append("__Description__\n\n" + self.description)
|
|
# Arguments
|
|
if self.arguments:
|
|
str_list.append("__Parameters__\n\nIndex | Description | Datatype(s) | Default Value\n--- | --- | --- | ---\n")
|
|
for argument in self.arguments:
|
|
str_list.append("{} | {} | {} | {}\n".format(*argument))
|
|
str_list.append("\n")
|
|
else:
|
|
str_list.append("__Parameters__\n\nNone\n\n")
|
|
# Return Value
|
|
if self.return_value:
|
|
str_list.append("__Return Value__\n\nDescription | Datatype(s)\n--- | ---\n{} | {}\n\n".format(*self.return_value))
|
|
else:
|
|
str_list.append("__Return Value__\n\nNone\n\n")
|
|
# Example
|
|
str_list.append("__Example__\n\n```sqf\n{}\n```\n\n".format(self.example))
|
|
# Authors
|
|
str_list.append("\n__Authors__\n\n")
|
|
for author in self.authors:
|
|
str_list.append("- {}\n".format(author))
|
|
# Horizontal rule
|
|
str_list.append("\n---\n")
|
|
|
|
return ''.join(str_list)
|
|
|
|
def log_file(self, error=False):
|
|
# When in debug mode we only want to see the files with errors
|
|
if not self.debug or error:
|
|
if not self.logged:
|
|
rel_path = os.path.relpath(self.path, self.directory)
|
|
|
|
self.write("Processing... {}".format(rel_path), 1)
|
|
self.logged = True
|
|
|
|
def feedback(self, message, level=0):
|
|
priority_str = ["Info","Warning","Error","Aborted"][level]
|
|
|
|
self.log_file(level > 0)
|
|
self.write("{0}: {1}".format(priority_str, message))
|
|
|
|
def write(self, message, indent=2):
|
|
to_print = [" "]*indent
|
|
to_print.append(message)
|
|
print("".join(to_print))
|
|
|
|
def document_functions(components):
|
|
os.makedirs('../wiki/functions/', exist_ok=True)
|
|
|
|
for component in components:
|
|
output = os.path.join('../wiki/functions/',component) + ".md"
|
|
with open(output, "w") as file:
|
|
for function in components[component]:
|
|
file.write(function.document(component))
|
|
|
|
def crawl_dir(directory, debug=False):
|
|
components = {}
|
|
|
|
for root, dirs, files in os.walk(directory):
|
|
for file in files:
|
|
if file.endswith(".sqf") and file.startswith("fnc_"):
|
|
file_path = os.path.join(root, file)
|
|
|
|
# Attempt to import the header from file
|
|
function = FunctionFile(directory)
|
|
function.import_header(file_path)
|
|
|
|
# Undergo data extraction and detailed debug
|
|
if function.has_header():
|
|
function.process_header(debug)
|
|
|
|
if function.is_public() and not debug:
|
|
# Add functions to component key (initalise key if necessary)
|
|
component = os.path.basename(os.path.dirname(root))
|
|
components.setdefault(component,[]).append(function)
|
|
|
|
function.feedback("Publicly documented")
|
|
|
|
document_functions(components)
|
|
|
|
def main():
|
|
print("""
|
|
#########################
|
|
# Documenting Functions #
|
|
#########################
|
|
""")
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('directory', nargs="?", type=str, default=".", help='only crawl specified module addon folder')
|
|
parser.add_argument('--debug', action="store_true", help='only check for header debug messages')
|
|
args = parser.parse_args()
|
|
|
|
# abspath is just used for the terminal output
|
|
prospective_dir = os.path.abspath(os.path.join('../../addons/',args.directory))
|
|
if os.path.isdir(prospective_dir):
|
|
print("Directory: {}".format(prospective_dir))
|
|
crawl_dir(prospective_dir, args.debug)
|
|
else:
|
|
print("Invalid directory: {}".format(prospective_dir))
|
|
|
|
if __name__ == "__main__":
|
|
main()
|