Added lots of features

This commit is contained in:
matthias@arch 2022-09-28 15:31:13 +02:00
parent 818956f0a2
commit 2e8498aa90

View File

@ -1,6 +1,6 @@
#!/bin/python3
"""
A python script to generate to_string functions for all enumerations in a cpp file.
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
@ -10,40 +10,280 @@ from sys import argv, exit
import re
filetypes = [".hpp", ".cpp", ".tpp"]
# 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)
def getEnumStrF(name: str, enum: dict, spaces: int):
s = " " * spaces
s += f"const std::map<{name}, std::string> {name}ToStringMap" + " { // generated by gen_enum_str.py\n"
for name_, val in enum.items():
s += " " * (spaces + 4)
s += "{ " + name_ + ", \"" + name_ + "\" },\n"
s += " " * spaces + "};\n"
s += " " * spaces + "const std::string& to_string(" + name + " v) { return " + f"{name}ToStringMap.at(v);" + " }; // generated by gen_enum_str.py\n\n"
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 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:
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]):
@ -84,84 +324,149 @@ def process_file(inputfile: str, outputfile: str):
var_end = len(m.group())
enum[m.group()[0:var_end]] = enum_val
enum_val += 1
print("enum:", enum_names[i], enum)
# 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)
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_path(path_):
if path.isfile(path_) and path.splitext(path_)[-1] in filetypes:
process_file(path_, path_)
# def process_file(inputfile: str, outputfile: str):
# print("process_file:", inputfile)
# if not path.isfile(inputfile): error("File does not exist:" + inputfile)
elif path.isdir(path_):
# 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_):
if path.isfile(p) and path.splitext(p)[-1] in filetypes:
process_file(p, p)
elif path.isdir(p):
chdir(p)
process_path(p)
chdir("..")
else:
print("Invalid path:", path_)
process_path(path_ + "/" + p, interactive, recurse)
def print_help():
"""
print("""
Synposis: get_enum_str.py <Options>... <file>...
-o <path> output: file to path. Only when 1 input file.
get_enum_str.py <Options>... -r <path>...
-h --help help
-r recurse: Recurse through the directory and process all cpp/hpp files. Implies -i
-i in place: Edit the file in place
"""
-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__":
output_file = None
input_files = []
in_place = False
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)
if argv[i] == "-o":
if len(argv) > i + 1: output_file = argv[i+1]
else: missing_arg(argv[i])
i += 1
elif argv[i] == "-r":
recurse = True
elif argv[i] == "-i":
in_place = True
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 output_file is None and not in_place and not recurse:
error("Missing output path or -i")
if output_file and (recurse or in_place or len(input_files) > 1):
error("Error: -o does not work with -r, -i or multiple input paths.")
if len(input_files) == 0:
error("Missing input files.")
if in_place:
output_file = input_files[0]
if (recurse):
error("Missing input files/paths.")
for f in input_files:
process_path(f)
else:
process_file(input_files[0], output_file)
process_path(path.abspath(f), recurse=recurse, interactive=interactive)