Added lots of features
This commit is contained in:
parent
818956f0a2
commit
2e8498aa90
421
gen_enum_str.py
421
gen_enum_str.py
@ -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)
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user