gz-cpp-util/gen_enum_str.py
2022-09-28 15:31:13 +02:00

474 lines
18 KiB
Python
Executable File

#!/bin/python3
"""
A python script to generate toString functions for all enumerations in a cpp file.
Copyright © 2022 Matthias Quintern.
This software comes with no warranty.
This software is licensed under the GPL3
"""
from os import path, listdir, chdir
from sys import argv, exit
import re
# search in header files for enums
header_filetypes = [".hpp"]
# definition are written to source file having the same name as the header
source_filetype = ".cpp"
def error(s):
print(s)
exit(1)
class Enum:
# settings
include_docstrings = True
"""
include a doxygen docstring before the declarations
"""
docstring_include_generated = True
"""
include a list of the enum values in @details
"""
docstring_include_names = True
"""
throw exceptions on invalid arguments
if false:
toString: return empty string
fromString: return the last enum value
"""
throw_exceptions = True
# generate fromString for string_view
string_view = True
def __init__(self, startLine:int, name:str, numbers:list[int], names:list[str], namespace:str=""):
self.startLine = startLine
self.name = name
self.numbers = numbers
self.names = names
self.namespace = namespace
def is_range(self) -> tuple[bool, int, int]:
"""
check if the enum is a continous range
returns: bool, min, max
"""
sorted_numbers = self.numbers.copy()
sorted_numbers.sort()
if sorted_numbers == range(sorted_numbers[0], sorted_numbers[-1]+1, 1):
return True, sorted_numbers[0], sorted_numbers[-1]
else:
return False, -1, -1
def get_name(self):
if self.namespace:
return self.namespace + "::" + self.name
else:
return self.name
def get_struct_name(self) -> str:
return "EnumStringConversion_" + self.name
def get_names_for_doc(self, qoutes="") -> str:
"""
returns all names in quotes, separated with a ,
"""
s = ""
for name in self.names:
s += qoutes + name + qoutes + ", "
if len(s) > 2: return s[:-1]
else: return s
def get_dec_toString(self) -> str:
"""
return the declaration for the toString method
"""
s = ""
if Enum.include_docstrings:
s += "/**\n"
s += f' * @brief Convert @ref {self.get_name()} "an enumeration value" to std::string\n'
s += f' * @details\n'
if Enum.docstring_include_generated:
s += f' * This function was generated by gen_enum_str.py\\n\n'
if Enum.throw_exceptions:
s += f' * Throws gz::InvalidArgument if v is invalid.\n'
s += f' * @throws gz::InvalidArgument if v is invalid.\n'
else:
s += f' * Returns an empty string if v is invalid.\n'
s += f' */\n'
s += f"std::string toString(const {self.get_name()}& v);\n"
return s
def get_dec_fromString(self) -> str:
"""
return the declaration(s) for the fromString method(s)
"""
s = f"template<std::same_as<{self.get_name()}> T>\n"
s += f"{self.get_name()} fromString(const std::string& s);\n"
if Enum.string_view:
s += f"template<std::same_as<{self.get_name()}> T>\n"
s += f"{self.get_name()} fromString(const std::string_view& sv);\n"
if Enum.include_docstrings:
s += "/**\n"
s += f' * @brief Convert a std::string to @ref {self.get_name()} "an enumeration value"\n'
s += f' * @details\n'
if Enum.docstring_include_generated:
s += f' * This function was generated by gen_enum_str.py\\n\n'
if Enum.throw_exceptions:
s += f' * Throws gz::InvalidArgument if s is invalid.\n'
s += f' * @throws gz::InvalidArgument if s is invalid.\n'
else:
s += f' * Returns {self.get_name()}::{self.names[-1]} if s is invalid.\n'
if Enum.docstring_include_names:
s += f' * @param v one of: {self.get_names_for_doc()}\n'
s += f' */\n'
s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string& s);\n"
if Enum.string_view:
if Enum.include_docstrings:
s += '/// @brief Convert a std::string_view to @ref {self.get_name()} "an enumeration value"\n'
s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string_view& sv);\n"
return s
def get_def_struct(self) -> str:
"""
return the definition of the EnumStringConversion struct
"""
s = ""
s += f"// Holds maps used by fromString and toString for conversion of {self.name} values\n"
s += f"struct {self.get_struct_name()}" + " {\n"
s += f" static gz::util::unordered_string_map<{self.get_name()}> name2type;\n"
s += f" static std::map<{self.get_name()}, std::string> type2name;\n"
s += "}; // generated by gen_enum_str\n\n"
# name2type
s += f"gz::util::unordered_string_map<{self.get_name()}> {self.get_struct_name()}::name2type " + "{\n"
for i in range(len(self.names)):
# { "ENUM_VAL", namespace::ENUM_NAME::ENUM_VAL },
s += '\t{ "' + self.names[i] + '", ' + self.get_name() + "::" + self.names[i] + " },\n"
s += "}; // generated by gen_enum_str\n\n"
# type2name
s += f"std::map<{self.get_name()}, std::string> {self.get_struct_name()}::type2name " + "{\n"
for i in range(len(self.names)):
# { namespace::ENUM_NAME::ENUM_VAL, "ENUM_VAL", },
s += '\t{ ' + self.get_name() + "::" + self.names[i] + ', "' + self.names[i] + '" },\n'
s += "}; // generated by gen_enum_str\n\n"
return s
def get_def_toString(self) -> str:
s = ""
s += f"std::string toString(const {self.get_name()}& v) " + "{\n"
s += f"\tif ({self.get_struct_name()}::type2name.contains(v)) " + "{\n"
s += f"\t\treturn {self.get_struct_name()}::type2name.at(v);\n"
s += "\t}\n"
s += "\telse {\n"
if Enum.throw_exceptions:
s += f'\t\tthrow gz::InvalidArgument("InvalidArgument: \'" + std::to_string(static_cast<int>(v)) + "\'", "toString({self.name})");\n'
else:
s += f'\t\treturn "";\n'
s += "\t}\n"
s += "} // generated by gen_enum_str\n\n"
return s
def get_def_fromString(self) -> str:
s = ""
# string_view
s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string_view& sv) " + "{\n"
s += f"\tif ({self.get_struct_name()}::name2type.contains(sv)) " + "{\n"
s += f"\t\treturn {self.get_struct_name()}::name2type.find(sv)->second;\n"
s += "\t}\n"
s += "\telse {\n"
if Enum.throw_exceptions:
s += f'\t\tthrow gz::InvalidArgument("InvalidArgument: \'" + std::string(sv) + "\'", "fromString<{self.name}>");\n'
else:
s += f'\t\treturn {self.get_name()}::{self.names[-1]};\n'
s += "\t}\n"
s += "} // generated by gen_enum_str\n\n"
# string
s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string& s) " + "{\n"
s += f"\treturn fromString<{self.get_name()}>(std::string_view(s));\n"
s += "} // generated by gen_enum_str\n\n"
return s
def append_enums_to_files(header_file:str, source_file:str, enums: list[Enum], interactive:bool=False):
"""
put declaration at the end of the source
put definition at the end of the header
"""
if len(enums) == 0:
return
print("append_enums_to_files:", header_file, source_file)
if not path.isfile(header_file): error("File does not exist:" + header_file)
if not path.isfile(source_file):
print("Creating source_file: " + source_file)
with open(source_file, "w") as file:
file.write(f'// generated by gen_enum_str\n#include "{header_file}"\n\n#include <gz-util/util/string.hpp>\n\n')
with open(header_file, "r") as file:
header = file.read()
with open(source_file, "r") as file:
source = file.read()
# delete everything between the two comments
comment_gen_begin = "// ENUM - STRING CONVERSION BEGIN\n"
comment_gen_end = "// ENUM - STRING CONVERSION END\n"
gen_begin = header.find(comment_gen_begin)
gen_end = header.find(comment_gen_end)
if gen_begin > 0 and gen_end > gen_begin:
header = header[:gen_begin - 1] + header[gen_end + len(comment_gen_end):]
gen_begin = source.find(comment_gen_begin)
gen_end = source.find(comment_gen_end)
if gen_begin > 0 and gen_end > gen_begin:
source = source[:gen_begin - 1] + source[gen_end + len(comment_gen_end):]
header += "\n" + comment_gen_begin
header += "// do not write your own code between these comment blocks - it will be overwritten when you run gen_enum_str.py again\n"
source += "\n" + comment_gen_begin
source += "// do not write your own code between these comment blocks - it will be overwritten when you run gen_enum_str.py again\n"
for enum in enums:
if interactive:
answer = input(f"Generate conversion for: {header_file}:{enum.startLine} - enum {enum.name}? (y/n): ")
if answer not in "yY":
continue
header += f"//\n// {enum.name}\n//\n"
header += enum.get_dec_toString()
header += enum.get_dec_fromString() + "\n"
source += f"//\n// {enum.name}\n//\n"
source += enum.get_def_struct()
source += enum.get_def_toString()
source += enum.get_def_fromString()
header += "\n" + comment_gen_end
source += "\n" + comment_gen_end
with open(header_file, "w") as file:
file.write(header)
with open(source_file, "w") as file:
file.write(source)
def get_enums(headerfile: str) -> list[Enum]:
"""
find enums in headerfile
namespaces are not properly supported, the script can not detect when a namespace is closed.
so it only works when you dont open namespaces in namespaces
"""
# print("get_enums:", headerfile)
if not path.isfile(headerfile): error("File does not exist:" + headerfile)
with open(headerfile, "r") as file:
lines = file.readlines()
# FIND ENUMS
enums_start = []
enums_stop = []
enums_namespace = []
current_namespace = ""
in_enum = False
for i in range(len(lines)):
match = re.match(r" *namespace ([a-zA-Z0-9_:]+) +{.*", lines[i])
if match:
current_namespace = match.groups()[0]
if re.match(r" *enum [a-zA-Z0-9_-]+ {.*", lines[i]) and not "gen_enum_str.py" in lines[i]:
if in_enum: error("process_file: could not find end of enum starting at line " + str(enums_start.pop()))
in_enum = True
enums_start.append(i)
enums_namespace.append(current_namespace)
if in_enum:
if re.match(r" *[^/*]?.*}.*", lines[i]):
enums_stop.append(i)
in_enum = False
if len(enums_start) != len(enums_stop): error("process_file: could not find end of enum starting at line " + str(enums_start.pop()))
# create a single string with the enum
enums = []
for i in range(len(enums_start)):
enums.append("")
j = enums_start[i]
while j <= enums_stop[i]:
enums[i] += lines[j]
j += 1
# get name and vars
enum_names = []
enum_dicts = []
spaces = []
var = "[a-zA-Z0-9_-]+"
enumvar = "(" + var + r"(=*-?\d+)?)"
restring = "enum (" + var + "){(" + enumvar + ",)*" + enumvar + ",?}"
for i in range(len(enums)):
enums[i] = enums[i].replace("\n", "").replace("enum ", "enumü").replace(" ", "").replace("enumü", "enum ")
match = re.match(restring, enums[i])
if not match: error("Invalid enum at line " + str(enums_start[i]))
spaces.append(lines[enums_start[i]].find("e"))
enum_names.append(match.groups()[0])
enum_val = 0
enum = {}
for m in re.finditer(enumvar, enums[i][len("enum " + enum_names[i]):]):
if "=" in m.group():
enum_val = int(m.group()[m.group().find("=")+1:])
var_end = m.group().find("=")
else:
var_end = len(m.group())
enum[m.group()[0:var_end]] = enum_val
enum_val += 1
# print("enum:", enum_names[i], enum)
enum_dicts.append(enum)
enums = []
for i in range(len(enum_names)):
enums.append(Enum(enums_start[i], enum_names[i], list(enum_dicts[i].values()), list(enum_dicts[i].keys()), enums_namespace[i]))
return enums
# def process_file(inputfile: str, outputfile: str):
# print("process_file:", inputfile)
# if not path.isfile(inputfile): error("File does not exist:" + inputfile)
# with open(inputfile, "r") as file:
# lines = file.readlines()
# # FIND ENUMS
# enums_start = []
# enums_stop = []
# in_enum = False
# for i in range(len(lines)):
# if re.match(r" *enum [a-zA-Z0-9_-]+ {.*", lines[i]) and not "gen_enum_str.py" in lines[i]:
# if in_enum: error("process_file: could not find end of enum starting at line " + str(enums_start.pop()))
# in_enum = True
# enums_start.append(i)
# if in_enum:
# if re.match(r" *[^/*]?.*}.*", lines[i]):
# enums_stop.append(i)
# in_enum = False
# if len(enums_start) != len(enums_stop): error("process_file: could not find end of enum starting at line " + str(enums_start.pop()))
# # create a single string with the enum
# enums = []
# for i in range(len(enums_start)):
# enums.append("")
# j = enums_start[i]
# while j <= enums_stop[i]:
# enums[i] += lines[j]
# j += 1
# # get name and vars
# enum_names = []
# enum_dicts = []
# spaces = []
# var = "[a-zA-Z0-9_-]+"
# enumvar = "(" + var + r"(=*-?\d+)?)"
# restring = "enum (" + var + "){(" + enumvar + ",)*" + enumvar + ",?}"
# for i in range(len(enums)):
# enums[i] = enums[i].replace("\n", "").replace("enum ", "enumü").replace(" ", "").replace("enumü", "enum ")
# match = re.match(restring, enums[i])
# if not match: error("Invalid enum at line " + str(enums_start[i]))
# spaces.append(lines[enums_start[i]].find("e"))
# enum_names.append(match.groups()[0])
# enum_val = 0
# enum = {}
# for m in re.finditer(enumvar, enums[i][len("enum " + enum_names[i]):]):
# if "=" in m.group():
# enum_val = int(m.group()[m.group().find("=")+1:])
# var_end = m.group().find("=")
# else:
# var_end = len(m.group())
# enum[m.group()[0:var_end]] = enum_val
# enum_val += 1
# print("enum:", enum_names[i], enum)
# enum_dicts.append(enum)
# for i in reversed(range(len(enum_names))):
# lines.insert(enums_stop[i] + 1, getEnumStrF(enum_names[i], enum_dicts[i], spaces[i]))
# lines[enums_start[i]] = lines[enums_start[i]].strip("\n") + " // processed by gen_enum_str.py\n"
# with open(outputfile, "w") as file:
# file.writelines(lines)
def process_path(path_, interactive:bool=False, recurse:bool=False):
# print("Processing path:", path_)
if path.isfile(path_) and path.splitext(path_)[-1] in header_filetypes:
enums = get_enums(path_)
append_enums_to_files(path_, path_.split('.')[0] + source_filetype, enums, interactive)
elif recurse:
if path.isdir(path_):
for p in listdir(path_):
process_path(path_ + "/" + p, interactive, recurse)
def print_help():
print("""
Synposis: get_enum_str.py <Options>... <file>...
get_enum_str.py <Options>... -r <path>...
-h --help help
-r recurse: Recurse through given paths process all files.
-i interactive: prompt for every enumeration
--no-docs turn off docstring generation
--docs-no-names do not list names of enumeration values in docstring
--docs-no-gen do not put "generated by gen_enum_str" in docstring
--no-throw return empty string/last enum value if the argument for to/fromString is invalid.
This would normaly throw gz::InvalidArgument
This option does not make the functions noexcept!
If the generated code produces errors:
- check that necessary headers are included in the source file:
- gz-util/string.hpp
- gz-util/exceptions.hpp (unless you use --no-throw)
- check the namespaces of the enumerations, the generated code should be in global namespace
nested namespace are not supported, you will have to correct that if you use them
""")
def missing_arg(arg):
print("Missing argument for", arg)
exit(1)
if __name__ == "__main__":
input_files = []
recurse = False
interactive = False
i = 1
while i in range(1, len(argv)):
if argv[i] == "--help" or argv[i] == "-h":
print_help()
exit(0)
elif argv[i] == "-r":
recurse = True
elif argv[i] == "-i":
interactive = True
elif argv[i] == "--no-throw":
Enum.throw_exceptions = False
elif argv[i] == "--no-docs":
Enum.include_docstrings = False
elif argv[i] == "--docs-no-names":
Enum.docstring_include_names = False
elif argv[i] == "--docs-no-gen":
Enum.docstring_include_generated = False
else:
input_files.append(argv[i])
i += 1
if len(input_files) == 0:
error("Missing input files/paths.")
for f in input_files:
process_path(path.abspath(f), recurse=recurse, interactive=interactive)