diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 93ad218..a226fa1 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -34,4 +34,7 @@ jobs: - name: Run test run: | python release_package.py --mode install - python tests/test_cpp_generator.py + python tests/test_cpp_file.py + python tests/test_cpp_function_string.py + python tests/test_cpp_variable_string.py + python tests/test_html_generator.py diff --git a/release_package.py b/release_package.py index 80bf023..bae927b 100644 --- a/release_package.py +++ b/release_package.py @@ -230,6 +230,23 @@ def tmp_release_notes(): return os.path.abspath(release_md) +def increment_minor_version(file_path: str): + """ + Increment minor package version number, e.g. 2.2.9 -> 2.2.10 + :return: New version number + """ + config = ConfigParser() + config.read(file_path) + version = config['metadata']['version'] + major, minor, patch = [int(v) for v in version.split('.')] + patch += 1 + new_version = f"{major}.{minor}.{patch}" + config['metadata']['version'] = new_version + with open(file_path, 'w') as f: + config.write(f) + return new_version + + def main(): parser = argparse.ArgumentParser(description='Command-line params') parser.add_argument('--mode', @@ -246,16 +263,28 @@ def main(): action='store_true', required=False) parser.add_argument('--publish-pypi', - help='Publish the package to PyPI', + help='Publish the package to PyPI server', action='store_true', + default=False, + required=False) + parser.add_argument('--increment-version', + help='Increment minor version number, e.g. 2.2.9 -> 2.2.10', + action='store_true', + default=False, required=False) args = parser.parse_args() + global VERSION print(f'Package name: {PACKAGE_NAME}') print(f'Package name2: {PACKAGE_NAME_DASH}') print(f'Version: {VERSION}') sanity_check(args) + if args.increment_version: + new_version = increment_minor_version('setup.cfg') + VERSION = new_version + print(f'Increment version to {new_version}') + if args.mode == "build": build_wheel() elif args.mode == "install": diff --git a/setup.cfg b/setup.cfg index 467b44e..3fcf3c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,11 @@ [metadata] name = code_generation -version = 2.1.1 +version = 2.2.44 [options] packages = find: -python_requires = >=3.4 +python_requires = >=3.8 [options.packages.find] where = src + diff --git a/src/code_generation/__init__.py b/src/code_generation/__init__.py index 7bc1524..f9b58b0 100644 --- a/src/code_generation/__init__.py +++ b/src/code_generation/__init__.py @@ -1,2 +1,9 @@ +from . import cpp_array +from . import cpp_class +from . import cpp_enum +from . import cpp_function +from . import cpp_generator +from . import cpp_variable +from . import html_generator from pathlib import Path PACKAGE_ROOT = Path(__file__).parent diff --git a/src/code_generation/code_generator.py b/src/code_generation/code_generator.py index 338fefc..ac1b96c 100644 --- a/src/code_generation/code_generator.py +++ b/src/code_generation/code_generator.py @@ -1,13 +1,16 @@ +import sys +from code_generation.code_style import ANSICodeStyle + __doc__ = """ Simple and straightforward code generator that could be used for generating code on any programming language and to be a 'building block' for creating more complicated code generator. Thanks to Eric Reynolds, the code mainly based on his article published on -http://www.codeproject.com/Articles/571645/Really-simple-Cplusplus-code-generation-in-Python -However, it was both simplified and extended +https://www.codeproject.com/Articles/571645/Really-simple-Cplusplus-code-generation-in-Python +However, it was both significantly extended since then and also simplified. Used under the Code Project Open License -http://www.codeproject.com/info/cpol10.aspx +https://www.codeproject.com/info/cpol10.aspx Examples of usage: @@ -23,7 +26,7 @@ # Python code cpp = CppFile('example.cpp') with cpp.block('class A', ';'): - cpp.label('public:') + cpp.label('public') cpp('int m_classMember1;') cpp('double m_classMember2;') @@ -41,56 +44,6 @@ class A """ -class ANSICodeStyle: - """ - Class represents C++ {} close and its formatting style. - It supports ANSI C style with braces on the new lines, like that: - // C++ code - { - // C++ code - }; - finishing postfix is optional (e.g. necessary for classes, unnecessary for namespaces) - """ - - # EOL symbol - endline = "\n" - - # Tab (indentation) symbol - indent = "\t" - - def __init__(self, owner, text, postfix): - """ - @param: owner - CodeFile where text is written to - @param: text - text opening C++ close - @param: postfix - optional terminating symbol (e.g. ; for classes) - """ - self.owner = owner - if self.owner.last is not None: - with self.owner.last: - pass - self.owner.write("".join(text)) - self.owner.last = self - self.postfix = postfix - - def __enter__(self): - """ - Open code block - """ - self.owner.write("{") - self.owner.current_indent += 1 - self.owner.last = None - - def __exit__(self, *_): - """ - Close code block - """ - if self.owner.last is not None: - with self.owner.last: - pass - self.owner.current_indent -= 1 - self.owner.write("}" + self.postfix) - - class CodeFile: """ The class is a main instrument of code generation @@ -147,27 +100,27 @@ def close(self): self.out.close() self.out = None - def write(self, text, indent=0): + def write(self, text, indent=0, endline=True): """ Write a new line with line ending """ - self.out.write('{0}{1}{2}'.format(CodeFile.Formatter.indent * (self.current_indent+indent), + self.out.write('{0}{1}{2}'.format(self.Formatter.indent * (self.current_indent+indent), text, - CodeFile.Formatter.endline)) + self.Formatter.endline if endline else '')) def append(self, x): """ Append to the existing line without line ending """ - self.out.write(x) + self.out.write(x) - def __call__(self, text): + def __call__(self, text, indent=0, endline=True): """ Supports 'object()' semantic, i.e. cpp('#include ') inserts appropriate line """ - self.write(text) + self.write(text, indent, endline) def block(self, text, postfix=''): """ @@ -176,7 +129,13 @@ def block(self, text, postfix=''): cpp.block(class_name, ';'): """ return CodeFile.Formatter(self, text, postfix) - + + def endline(self, count=1): + """ + Insert an endline + """ + self.write(CodeFile.Formatter.endline * count, endline=False) + def newline(self, n=1): """ Insert one or several empty lines @@ -202,3 +161,21 @@ def label(self, text): a: """ self.write('{0}:'.format(text), -1) + + +def cpp_example(): + cpp = CppFile('example.cpp') + with cpp.block('class A', ';'): + cpp.label('public') + cpp('int m_classMember1;') + cpp('double m_classMember2;') + + +def main(): + cpp_example() + return 0 + + +if __name__ == '__main__': + sys.exit(main()) + diff --git a/src/code_generation/code_style.py b/src/code_generation/code_style.py new file mode 100644 index 0000000..50f4172 --- /dev/null +++ b/src/code_generation/code_style.py @@ -0,0 +1,103 @@ +__doc__ = """Formatters for different styles of code generation +""" + + +class ANSICodeStyle: + """ + Class represents C++ {} close and its formatting style. + It supports ANSI C style with braces on the new lines, like that: + // C++ code + { + // C++ code + }; + finishing postfix is optional (e.g. necessary for classes, unnecessary for namespaces) + """ + + # EOL symbol + endline = "\n" + + # Tab (indentation) symbol + indent = "\t" + + def __init__(self, owner, text, postfix): + """ + @param: owner - CodeFile where text is written to + @param: text - text opening C++ close + @param: postfix - optional terminating symbol (e.g. ; for classes) + """ + self.owner = owner + if self.owner.last is not None: + with self.owner.last: + pass + self.owner.write("".join(text)) + self.owner.last = self + self.postfix = postfix + + def __enter__(self): + """ + Open code block + """ + self.owner.write("{") + self.owner.current_indent += 1 + self.owner.last = None + + def __exit__(self, *_): + """ + Close code block + """ + if self.owner.last is not None: + with self.owner.last: + pass + self.owner.current_indent -= 1 + self.owner.write("}" + self.postfix) + + +class HTMLStyle: + """ + Class representing HTML close and its formatting style. + It supports HTML DOM-tree style, like that: + // HTML code + + // HTML content + + """ + # EOL symbol + endline = "\n" + + # Tab (indentation) symbol is 2 spaces + indent = " " + + def __init__(self, owner, element, *attrs, **kwattrs): + """ + @param: owner - CodeFile where text is written to + @param: element - HTML element name + @param: attrs - optional opening tag content, like attributes ['class="class1"', 'id="id1"'] + @param: kwattrs - optional opening tag attributes, like class="class1", id="id1" + """ + self.owner = owner + if self.owner.last is not None: + with self.owner.last: + pass + self.element = element + attributes = "".join(f' {attr}' for attr in attrs) + attributes += "".join(f' {key}="{value}"' for key, value in kwattrs.items()) + self.attributes = attributes + self.owner.last = self + + def __enter__(self): + """ + Open code block + """ + self.owner.write(f"<{self.element}{self.attributes}>") + self.owner.current_indent += 1 + self.owner.last = None + + def __exit__(self, *_): + """ + Close code block + """ + if self.owner.last is not None: + with self.owner.last: + pass + self.owner.current_indent -= 1 + self.owner.write(f"") diff --git a/src/code_generation/cpp_array.py b/src/code_generation/cpp_array.py new file mode 100644 index 0000000..179a4af --- /dev/null +++ b/src/code_generation/cpp_array.py @@ -0,0 +1,185 @@ +from code_generation.cpp_generator import CppLanguageElement, CppDeclaration, CppImplementation + + +# noinspection PyUnresolvedReferences +class CppArray(CppLanguageElement): + """ + The Python class that generates string representation for C++ array (automatic or class member) + For example: + + int arr[] = {1,2,2}; + double doubles[5] = {1.0,2.0}; + + class MyClass + { + int m_arr1[10]; + static double m_arr2[]; + ... + } + Available properties: + + type - string, variable type + (is_)static - boolean, 'static' prefix + (is_)const - boolean, 'const' prefix + (is_)class_member - boolean, for appropriate definition/declaration rendering + array_size - integer, size of array if required + newline_align - in the array definition rendering place every item on the new string + + NOTE: versions 2.0+ of CodeGenerator support boolean properties without "is_" suffix, + but old versions preserved for backward compatibility + """ + availablePropertiesNames = {'type', + 'is_static', + 'static', + 'is_const', + 'const', + 'is_class_member', + 'class_member', + 'array_size', + 'newline_align'} | CppLanguageElement.availablePropertiesNames + + def __init__(self, **properties): + self.is_static = False + self.is_const = False + self.is_class_member = False + self.array_size = 0 + self.newline_align = None + + # array elements + self.items = [] + + input_property_names = set(properties.keys()) + self.check_input_properties_names(input_property_names) + super(CppArray, self).__init__(properties) + self.init_class_properties(current_class_properties=self.availablePropertiesNames, + input_properties_dict=properties) + + def _render_static(self): + """ + @return: 'static' prefix if required + """ + return 'static ' if self.is_static else '' + + def _render_const(self): + """ + @return: 'const' prefix if required + """ + return 'const ' if self.is_const else '' + + def _render_size(self): + """ + @return: array size + """ + return self.array_size if self.array_size else '' + + def _render_content(self): + """ + @return: array items if any + """ + return ', '.join(self.items) if self.items else 'nullptr' + + def _render_value(self, cpp): + """ + Render to string array items + """ + if not self.items: + raise RuntimeError('Empty arrays do not supported') + for item in self.items[:-1]: + cpp('{0},'.format(item)) + cpp('{0}'.format(self.items[-1])) + + def declaration(self): + """ + @return: CppDeclaration wrapper, that could be used + for declaration rendering using render_to_string(cpp) interface + """ + return CppDeclaration(self) + + def definition(self): + """ + @return: CppImplementation wrapper, that could be used + for definition rendering using render_to_string(cpp) interface + """ + return CppImplementation(self) + + def add_array_item(self, item): + """ + If variable is an array it could contain a number of items + @param: item - string + """ + self.items.append(item) + + def add_array_items(self, items): + """ + If variable is an array it could contain a number of items + @param: items - list of strings + """ + self.items.extend(items) + + def render_to_string(self, cpp): + """ + Generates definition for the C++ array. + Output depends on the array type + + Generates something like + int my_array[5] = {1, 2, 0}; + const char* my_array[] = {"Hello", "World"}; + + That method is used for generating automatic (non-class members) arrays + For class members use render_to_string_declaration/render_to_string_implementation methods + """ + if self.is_class_member and not (self.is_static and self.is_const): + raise RuntimeError('For class member variables use definition() and declaration() methods') + + # newline-formatting of array elements makes sense only if array is not empty + if self.newline_align and self.items: + with cpp.block(f'{self._render_static()}{self._render_const()}{self.type} ' + f'{self.name}[{self._render_size()}] = ', ';'): + # render array items + self._render_value(cpp) + else: + cpp(f'{self._render_static()}{self._render_const()}{self.type} ' + f'{self.name}[{self._render_size()}] = {{{self._render_content()}}};') + + def render_to_string_declaration(self, cpp): + """ + Generates declaration for the C++ array. + Non-static arrays-class members do not supported + + Example: + static int my_class_member_array[]; + """ + if not self.is_class_member: + raise RuntimeError('For automatic variable use its render_to_string() method') + cpp(f'{self._render_static()}{self._render_const()}{self.type} {self.name}[{self._render_size()}];') + + def render_to_string_implementation(self, cpp): + """ + Generates definition for the C++ array. + Output depends on the array type + + Example: + int MyClass::m_my_static_array[] = + { + ... + }; + + Non-static arrays-class members do not supported + """ + if not self.is_class_member: + raise RuntimeError('For automatic variable use its render_to_string() method') + + # generate definition for the static class member arrays only + # other types are not supported + if not self.is_static: + raise RuntimeError('Only static arrays as class members are supported') + + # newline-formatting of array elements makes sense only if array is not empty + if self.newline_align and self.items: + with cpp.block(f'{self._render_static()}{self._render_const()}{self.type} ' + f'{self.name}[{self._render_size()}] = ', ';'): + # render array items + self._render_value(cpp) + else: + cpp(f'{self._render_static()}{self._render_const()}{self.type} ' + f'{self.name}[{self._render_size()}] = {{{self._render_content()}}};') diff --git a/src/code_generation/cpp_class.py b/src/code_generation/cpp_class.py new file mode 100644 index 0000000..e056f49 --- /dev/null +++ b/src/code_generation/cpp_class.py @@ -0,0 +1,549 @@ +from code_generation.cpp_generator import CppLanguageElement, CppDeclaration, CppImplementation +from code_generation.cpp_function import CppFunction +from textwrap import dedent + + +class CppClass(CppLanguageElement): + """ + The Python class that generates string representation for C++ class or struct. + Usually contains a number of child elements - internal classes, enums, methods and variables. + Available properties: + is_struct - boolean, use 'struct' keyword for class declaration, 'class' otherwise + documentation - string, '/// Example doxygen' + + Example of usage: + + # Python code + cpp_class = CppClass(name = 'MyClass', is_struct = True) + cpp_class.add_variable(CppVariable(name = "m_var", + type = 'size_t', + is_static = True, + is_const = True, + initialization_value = 255)) + + def handle(cpp): cpp('return m_var;') + + cpp_class.add_method(CppFunction(name = "GetVar", + ret_type = 'size_t', + is_static = True, + implementation_handle = handle)) + + // Generated C++ declaration + struct MyClass + { + static const size_t m_var; + static size_t GetVar(); + } + + // Generated C++ definition + const size_t MyClass::m_var = 255; + + size_t MyClass::GetVar() + { + return m_var; + } + """ + availablePropertiesNames = {'is_struct', + 'documentation', + 'parent_class'} | CppLanguageElement.availablePropertiesNames + + class CppMethod(CppFunction): + """ + The Python class that generates string representation for C++ method + Parameters are passed as plain strings('int a', 'void p = NULL' etc.) + Available properties: + ret_type - string, return value for the method ('void', 'int'). Could not be set for constructors + is_static - boolean, static method prefix + is_const - boolean, const method prefix, could not be static + is_virtual - boolean, virtual method postfix, could not be static + is_pure_virtual - boolean, ' = 0' method postfix, could not be static + documentation - string, '/// Example doxygen' + implementation_handle - reference to a function that receives 'self' and C++ code generator handle + (see code_generator.cpp) and generates method body without braces + Ex. + #Python code + def functionBody(self, cpp): cpp('return 42;') + f1 = CppFunction(name = 'GetAnswer', + ret_type = 'int', + documentation = '// Generated code', + implementation_handle = functionBody) + + // Generated code + int MyClass::GetAnswer() + { + return 42; + } + """ + availablePropertiesNames = {'ret_type', + 'is_static', + 'is_constexpr', + 'is_virtual', + 'is_inline', + 'is_pure_virtual', + 'is_const', + 'is_override', + 'is_final', + 'implementation_handle', + 'documentation'} | CppLanguageElement.availablePropertiesNames + + def __init__(self, **properties): + # arguments are plain strings + # e.g. 'int* a', 'const string& s', 'size_t sz = 10' + self.is_static = False + self.is_constexpr = False + self.is_virtual = False + self.is_inline = False + self.is_pure_virtual = False + self.is_const = False + self.is_override = False + self.is_final = False + self.arguments = [] + self.implementation_handle = properties.get('implementation_handle') + self.documentation = properties.get('documentation') + + # check properties + input_property_names = set(properties.keys()) + self.check_input_properties_names(input_property_names) + super().__init__(**properties) + self.init_class_properties(current_class_properties=self.availablePropertiesNames, + input_properties_dict=properties) + + def _render_static(self): + """ + Before function name, declaration only + Static functions can't be const, virtual or pure virtual + """ + return 'static' if self.is_static else '' + + def _render_constexpr(self): + """ + Before function name, declaration only + Constexpr functions can't be const, virtual or pure virtual + """ + return 'constexpr ' if self.is_constexpr else '' + + def _render_virtual(self): + """ + Before function name, could be in declaration or definition + Virtual functions can't be static or constexpr + """ + return 'virtual ' if self.is_virtual else '' + + def _render_inline(self): + """ + Before function name, could be in declaration or definition + Inline functions can't be static, virtual or constexpr + """ + return 'inline ' if self.is_inline else '' + + def _render_ret_type(self): + """ + Return type, could be in declaration or definition + """ + return self.ret_type if self.ret_type else '' + + def _render_pure(self): + """ + After function name, declaration only + Pure virtual functions must be virtual + """ + return ' = 0' if self.is_pure_virtual else '' + + def _render_const(self): + """ + After function name, could be in declaration or definition + Const functions can't be static, virtual or constexpr + """ + return ' const' if self.is_const else '' + + def _render_override(self): + """ + After function name, could be in declaration or definition + Override functions must be virtual + """ + return ' override' if self.is_override else '' + + def _render_final(self): + """ + After function name, could be in declaration or definition + Final functions must be virtual + """ + return ' final' if self.is_final else '' + + def _sanity_check(self): + """ + Check whether attributes compose a correct C++ code + """ + if self.is_inline and (self.is_virtual or self.is_pure_virtual): + raise ValueError(f'Inline method {self.name} could not be virtual') + if self.is_constexpr and (self.is_virtual or self.is_pure_virtual): + raise ValueError(f'Constexpr method {self.name} could not be virtual') + if self.is_const and self.is_static: + raise ValueError(f'Static method {self.name} could not be const') + if self.is_const and self.is_virtual: + raise ValueError(f'Virtual method {self.name} could not be const') + if self.is_const and self.is_pure_virtual: + raise ValueError(f'Pure virtual method {self.name} could not be const') + if self.is_override and not self.is_virtual: + raise ValueError(f'Override method {self.name} should be virtual') + if self.is_inline and (self.is_virtual or self.is_pure_virtual): + raise ValueError(f'Inline method {self.name} could not be virtual') + if self.is_final and not self.is_virtual: + raise ValueError(f'Final method {self.name} should be virtual') + if self.is_static and self.is_virtual: + raise ValueError(f'Static method {self.name} could not be virtual') + if self.is_pure_virtual and not self.is_virtual: + raise ValueError(f'Pure virtual method {self.name} is also a virtual method') + if not self.ref_to_parent: + raise ValueError(f'Method {self.name} object must be a child of CppClass') + if self.is_constexpr and self.implementation_handle is None: + raise ValueError(f'Method {self.name} object must be initialized when "constexpr"') + if self.is_pure_virtual and self.implementation_handle is not None: + raise ValueError(f'Pure virtual method {self.name} could not be implemented') + + def add_argument(self, argument): + """ + @param: argument string representation of the C++ function argument ('int a', 'void p = NULL' etc) + """ + self.arguments.append(argument) + + def args(self): + """ + @return: string arguments + """ + return ", ".join(self.arguments) + + def implementation(self, cpp): + """ + The method calls Python function that creates C++ method body if handle exists + """ + if self.implementation_handle is not None: + self.implementation_handle(self, cpp) + + def declaration(self): + """ + @return: CppDeclaration wrapper, that could be used + for declaration rendering using render_to_string(cpp) interface + """ + return CppDeclaration(self) + + def definition(self): + """ + @return: CppImplementation wrapper, that could be used + for definition rendering using render_to_string(cpp) interface + """ + return CppImplementation(self) + + def render_to_string(self, cpp): + """ + By default, method is rendered as a declaration together with implementation, + like the method is implemented within the C++ class body, e.g. + class A + { + void f() + { + ... + } + } + """ + # check all properties for the consistency + self._sanity_check() + if self.documentation: + cpp(dedent(self.documentation)) + with cpp.block(f'{self._render_virtual()}{self._render_constexpr()}{self._render_inline()}' + f'{self._render_ret_type()} {self.fully_qualified_name()}({self.args()})' + f'{self._render_const()}' + f'{self._render_override()}' + f'{self._render_final()}' + f'{self._render_pure()}'): + self.implementation(cpp) + + def render_to_string_declaration(self, cpp): + """ + Special case for a method declaration string representation. + Generates just a function signature terminated by ';' + Example: + int GetX() const; + """ + # check all properties for the consistency + self._sanity_check() + if self.is_constexpr: + if self.documentation: + cpp(dedent(self.documentation)) + self.render_to_string(cpp) + else: + cpp(f'{self._render_virtual()}{self._render_inline()}' + f'{self._render_ret_type()} {self.name}({self.args()})' + f'{self._render_const()}' + f'{self._render_override()}' + f'{self._render_final()}' + f'{self._render_pure()};') + + def render_to_string_implementation(self, cpp): + """ + Special case for a method implementation string representation. + Generates method string in the form + Example: + int MyClass::GetX() const + { + ... + } + Generates method body if self.implementation_handle property exists + """ + # check all properties for the consistency + self._sanity_check() + + if self.implementation_handle is None: + raise RuntimeError(f'No implementation handle for the method {self.name}') + + if self.documentation and not self.is_constexpr: + cpp(dedent(self.documentation)) + with cpp.block(f'{self._render_virtual()}{self._render_constexpr()}{self._render_inline()}' + f'{self._render_ret_type()} {self.fully_qualified_name()}({self.args()})' + f'{self._render_const()}' + f'{self._render_override()}' + f'{self._render_final()}' + f'{self._render_pure()}'): + self.implementation(cpp) + + def __init__(self, **properties): + self.is_struct = False + self.documentation = None + self.parent_class = None + input_property_names = set(properties.keys()) + self.check_input_properties_names(input_property_names) + super(CppClass, self).__init__(properties) + self.init_class_properties(current_class_properties=self.availablePropertiesNames, + input_properties_dict=properties) + + # aggregated classes + self.internal_class_elements = [] + + # class members + self.internal_variable_elements = [] + + # array class members + self.internal_array_elements = [] + + # class methods + self.internal_method_elements = [] + + # class enums + self.internal_enum_elements = [] + + def _parent_class(self): + """ + @return: parent class object + """ + return self.parent_class if self.parent_class else "" + + def inherits(self): + """ + @return: string representation of the inheritance + """ + return f' : public {self._parent_class()}' + + ######################################## + # ADD CLASS MEMBERS + def add_enum(self, enum): + """ + @param: enum CppEnum instance + """ + enum.ref_to_parent = self + self.internal_enum_elements.append(enum) + + def add_variable(self, cpp_variable): + """ + @param: cpp_variable CppVariable instance + """ + cpp_variable.ref_to_parent = self + cpp_variable.is_class_member = True + self.internal_variable_elements.append(cpp_variable) + + def add_array(self, cpp_variable): + """ + @param: cpp_variable CppVariable instance + """ + cpp_variable.ref_to_parent = self + cpp_variable.is_class_member = True + self.internal_array_elements.append(cpp_variable) + + def add_internal_class(self, cpp_class): + """ + Add nested class + @param: cpp_class CppClass instance + """ + cpp_class.ref_to_parent = self + self.internal_class_elements.append(cpp_class) + + def add_method(self, method): + """ + @param: method CppFunction instance + """ + method.ref_to_parent = self + method.is_method = True + self.internal_method_elements.append(method) + + ######################################## + # RENDER CLASS MEMBERS + def _render_internal_classes_declaration(self, cpp): + """ + Generates section of nested classes + Could be placed both in 'private:' or 'public:' sections + Method is protected as it is used by CppClass only + """ + for classItem in self.internal_class_elements: + classItem.declaration().render_to_string(cpp) + cpp.newline() + + def _render_enum_section(self, cpp): + """ + Render to string all contained enums + Method is protected as it is used by CppClass only + """ + for enumItem in self.internal_enum_elements: + enumItem.render_to_string(cpp) + cpp.newline() + + def _render_variables_declaration(self, cpp): + """ + Render to string all contained variable class members + Method is protected as it is used by CppClass only + """ + for varItem in self.internal_variable_elements: + varItem.declaration().render_to_string(cpp) + cpp.newline() + + def _render_array_declaration(self, cpp): + """ + Render to string all contained array class members + Method is protected as it is used by CppClass only + """ + for arrItem in self.internal_array_elements: + arrItem.declaration().render_to_string(cpp) + cpp.newline() + + def _render_methods_declaration(self, cpp): + """ + Generates all class methods declaration + Should be placed in 'public:' section + Method is protected as it is used by CppClass only + """ + for funcItem in self.internal_method_elements: + funcItem.render_to_string_declaration(cpp) + cpp.newline() + + def render_static_members_implementation(self, cpp): + """ + Generates definition for all static class variables + Method is public, as it could be used for nested classes + int MyClass::my_static_array[] = {} + """ + # generate definition for static variables + static_vars = [variable for variable in self.internal_variable_elements if variable.is_static] + for varItem in static_vars: + varItem.definition().render_to_string(cpp) + cpp.newline() + for arrItem in self.internal_array_elements: + arrItem.definition().render_to_string(cpp) + cpp.newline() + + # do the same for nested classes + for classItem in self.internal_class_elements: + classItem.render_static_members_implementation(cpp) + cpp.newline() + + def render_methods_implementation(self, cpp): + """ + Generates all class methods declaration + Should be placed in 'public:' section + Method is public, as it could be used for nested classes + """ + # generate methods implementation section + for funcItem in self.internal_method_elements: + if not funcItem.is_pure_virtual: + funcItem.render_to_string_implementation(cpp) + cpp.newline() + # do the same for nested classes + for classItem in self.internal_class_elements: + classItem.render_static_members_implementation(cpp) + cpp.newline() + + ######################################## + # GROUP GENERATED SECTIONS + def class_interface(self, cpp): + """ + Generates section that generally used as an 'open interface' + Generates string representation for enums, internal classes and methods + Should be placed in 'public:' section + """ + self._render_enum_section(cpp) + self._render_internal_classes_declaration(cpp) + self._render_methods_declaration(cpp) + + def private_class_members(self, cpp): + """ + Generates section of class member variables. + Should be placed in 'private:' section + """ + self._render_variables_declaration(cpp) + self._render_array_declaration(cpp) + + def render_to_string(self, cpp): + """ + Render to string both declaration and definition. + A rare case enough, because the only code generator handle is used. + Typically class declaration is rendered to *.h file, and definition to *.cpp + """ + self.render_to_string_declaration(cpp) + self.render_to_string_implementation(cpp) + + def render_to_string_declaration(self, cpp): + """ + Render to string class declaration. + Typically handle to header should be passed as 'cpp' param + """ + if self.documentation: + cpp(dedent(self.documentation)) + + with cpp.block(f'{self._render_class_type()} {self.name} {self.inherits()}', postfix=';'): + + # in case of struct all members meant to be public + if not self.is_struct: + cpp.label('public') + self.class_interface(cpp) + cpp.newline() + + # in case of struct all members meant to be public + if not self.is_struct: + cpp.label('private') + self.private_class_members(cpp) + + def _render_class_type(self): + """ + @return: 'class' or 'struct' keyword + """ + return 'struct' if self.is_struct else 'class' + + def render_to_string_implementation(self, cpp): + """ + Render to string class definition. + Typically handle to *.cpp file should be passed as 'cpp' param + """ + cpp.newline(2) + self.render_static_members_implementation(cpp) + self.render_methods_implementation(cpp) + + def declaration(self): + """ + @return: CppDeclaration wrapper, that could be used + for declaration rendering using render_to_string(cpp) interface + """ + return CppDeclaration(self) + + def definition(self): + """ + @return: CppImplementation wrapper, that could be used + for definition rendering using render_to_string(cpp) interface + """ + return CppImplementation(self) diff --git a/src/code_generation/cpp_enum.py b/src/code_generation/cpp_enum.py new file mode 100644 index 0000000..d84c0d7 --- /dev/null +++ b/src/code_generation/cpp_enum.py @@ -0,0 +1,133 @@ +from code_generation.cpp_generator import CppLanguageElement + +__doc__ = """The module encapsulates C++ code generation logics for main C++ language primitives: +classes, methods and functions, variables, enums. +Every C++ element could render its current state to a string that could be evaluated as +a legal C++ construction. + +Some elements could be rendered to a pair of representations (i.e. declaration and definition) + +Example: +# Python code +cpp_class = CppClass(name = 'MyClass', is_struct = True) +cpp_class.add_variable(CppVariable(name = "m_var", + type = 'size_t', + is_static = True, + is_const = True, + initialization_value = 255)) + +// Generated C++ declaration +struct MyClass +{ + static const size_t m_var; +} + +// Generated C++ definition +const size_t MyClass::m_var = 255; + + +That module uses and highly depends on code_generator.py as it uses +code generating and formatting primitives implemented there. + +The main object referenced from code_generator.py is CppFile, +which is passed as a parameter to render_to_string(cpp) Python method + +It could also be used for composing more complicated C++ code, +that does not supported by cpp_generator + +It support: + +- functional calls: +cpp('int a = 10;') + +- 'with' semantic: +with cpp.block('class MyClass', ';') + class_definition(cpp) + +- append code to the last string without EOL: +cpp.append(', p = NULL);') + +- empty lines: +cpp.newline(2) + +For detailed information see code_generator.py documentation. +""" + + +class CppEnum(CppLanguageElement): + """ + The Python class that generates string representation for C++ enum + All enum elements are explicitly initialized with incremented values + + Available properties: + prefix - string, prefix added to every enum element, 'e' by default ('eItem1') + add_counter - boolean, terminating value that shows count of enum elements added, 'True' by default. + + Example of usage: + # Python code + enum_elements = CppEnum(name = 'Items') + for item in ['Chair', 'Table', 'Shelve']: + enum_elements.add_item(enum_elements.name) + + // Generated C++ code + enum Items + { + eChair = 0, + eTable = 1, + eShelve = 2, + eItemsCount = 3 + } + """ + availablePropertiesNames = {'prefix', + 'enum_class', + 'add_counter'} | CppLanguageElement.availablePropertiesNames + + def __init__(self, **properties): + self.enum_class = False + # check properties + input_property_names = set(properties.keys()) + self.check_input_properties_names(input_property_names) + super(CppEnum, self).__init__(properties) + + self.init_class_properties(current_class_properties=self.availablePropertiesNames, + input_properties_dict=properties) + + # place enum items here + self.enum_items = [] + + def _render_class(self): + return 'class ' if self.enum_class else '' + + def add_item(self, item): + """ + @param: item - string representation for the enum element + """ + self.enum_items.append(item) + + def add_items(self, items): + """ + @param: items - list of strings + """ + self.enum_items.extend(items) + + # noinspection PyUnresolvedReferences + def render_to_string(self, cpp): + """ + Generates a string representation for the enum + It always contains a terminating value that shows count of enum elements + enum MyEnum + { + eItem1 = 0, + eItem2 = 1, + eMyEnumCount = 2 + } + """ + counter = 0 + final_prefix = self.prefix if self.prefix is not None else 'e' + with cpp.block(f'enum {self._render_class()}{self.name}', postfix=';'): + for item in self.enum_items: + cpp(f'{final_prefix}{item} = {counter},') + counter += 1 + if self.add_counter in [None, True]: + last_element = f'{final_prefix}{self.name}Count = {counter}' + cpp(last_element) diff --git a/src/code_generation/cpp_function.py b/src/code_generation/cpp_function.py new file mode 100644 index 0000000..f94a555 --- /dev/null +++ b/src/code_generation/cpp_function.py @@ -0,0 +1,145 @@ +from code_generation.cpp_generator import CppLanguageElement, CppDeclaration, CppImplementation +from textwrap import dedent + + +class CppFunction(CppLanguageElement): + """ + The Python class that generates string representation for C++ function (not method!) + Parameters are passed as plain strings('int a', 'void p = NULL' etc.) + Available properties: + ret_type - string, return value for the method ('void', 'int'). Could not be set for constructors + is_constexpr - boolean, const method prefix + documentation - string, '/// Example doxygen' + implementation_handle - reference to a function that receives 'self' and C++ code generator handle + (see code_generator.cpp) and generates method body without braces + Ex. + #Python code + def functionBody(self, cpp): cpp('return 42;') + f1 = CppFunction(name = 'GetAnswer', + ret_type = 'int', + documentation = '// Generated code', + implementation_handle = functionBody) + + // Generated code + int GetAnswer() + { + return 42; + } + """ + availablePropertiesNames = {'ret_type', + 'is_constexpr', + 'implementation_handle', + 'documentation'} | CppLanguageElement.availablePropertiesNames + + def __init__(self, **properties): + # arguments are plain strings + # e.g. 'int* a', 'const string& s', 'size_t sz = 10' + self.arguments = [] + self.ret_type = None + self.implementation_handle = None + self.documentation = None + self.is_constexpr = False + + # check properties + input_property_names = set(properties.keys()) + self.check_input_properties_names(input_property_names) + super(CppFunction, self).__init__(properties) + self.init_class_properties(current_class_properties=self.availablePropertiesNames, + input_properties_dict=properties) + + def _sanity_check(self): + """ + Check whether attributes compose a correct C++ code + """ + if self.is_constexpr and self.implementation_handle is None: + raise ValueError(f'Constexpr function {self.name} must have implementation') + + def _render_constexpr(self): + """ + Before function name, declaration only + Constexpr functions can't be const, virtual or pure virtual + """ + return 'constexpr ' if self.is_constexpr else '' + + def args(self): + """ + @return: string arguments + """ + return ", ".join(self.arguments) + + def add_argument(self, argument): + """ + @param: argument string representation of the C++ function argument ('int a', 'void p = NULL' etc) + """ + self.arguments.append(argument) + + def implementation(self, cpp): + """ + The method calls Python function that creates C++ method body if handle exists + """ + if self.implementation_handle is not None: + self.implementation_handle(self, cpp) + + def declaration(self): + """ + @return: CppDeclaration wrapper, that could be used + for declaration rendering using render_to_string(cpp) interface + """ + return CppDeclaration(self) + + def definition(self): + """ + @return: CppImplementation wrapper, that could be used + for definition rendering using render_to_string(cpp) interface + """ + return CppImplementation(self) + + def render_to_string(self, cpp): + """ + By function method is rendered as a declaration together with implementation + void f() + { + ... + } + """ + # check all properties for the consistency + self._sanity_check() + if self.documentation: + cpp(dedent(self.documentation)) + with cpp.block(f'{self._render_constexpr()}{self.ret_type} {self.name}({self.args()})'): + self.implementation(cpp) + + def render_to_string_declaration(self, cpp): + """ + Special case for a function declaration string representation. + Generates just a function signature terminated by ';' + Example: + int GetX(); + """ + # check all properties for the consistency + if self.is_constexpr: + if self.documentation: + cpp(dedent(self.documentation)) + self.render_to_string(cpp) + else: + cpp(f'{self._render_constexpr()}{self.ret_type} {self.name}({self.args()});') + + def render_to_string_implementation(self, cpp): + """ + Special case for a function implementation string representation. + Generates function string in the form + Example: + int GetX() const + { + ... + } + Generates method body if self.implementation_handle property exists + """ + if self.implementation_handle is None: + raise RuntimeError(f'No implementation handle for the function {self.name}') + + # check all properties for the consistency + if self.documentation and not self.is_constexpr: + cpp(dedent(self.documentation)) + with cpp.block(f'{self._render_constexpr()}{self.ret_type} {self.name}({self.args()})'): + self.implementation(cpp) diff --git a/src/code_generation/cpp_generator.py b/src/code_generation/cpp_generator.py index b01ec02..fbebe32 100644 --- a/src/code_generation/cpp_generator.py +++ b/src/code_generation/cpp_generator.py @@ -1,6 +1,3 @@ -from textwrap import dedent - - __doc__ = """The module encapsulates C++ code generation logics for main C++ language primitives: classes, methods and functions, variables, enums. Every C++ element could render its current state to a string that could be evaluated as @@ -137,6 +134,28 @@ def init_class_properties(self, current_class_properties, input_properties_dict, if propertyName not in CppLanguageElement.availablePropertiesNames: setattr(self, propertyName, propertyValue) + def process_boolean_properties(self, properties): + """ + For every boolean property starting from 'is_' prefix generate a property without 'is_' prefix + """ + res = {**properties} + for prop in self.availablePropertiesNames: + if prop.startswith("is_"): + res[prop.replace("is_", "")] = properties.get(prop, False) + return res + + def init_boolean_properties(self, current_class_properties, input_properties_dict): + """ + Check if input properties contain either 'is_' prefixed properties or non-prefixed properties + If so, initialize prefixed properties with non-prefixed values + """ + for prop in self.availablePropertiesNames: + if prop.startswith("is_"): + non_prefixed = prop.replace("is_", "") + if non_prefixed in input_properties_dict: + setattr(self, prop, input_properties_dict[non_prefixed]) + current_class_properties.update(input_properties_dict) + def render_to_string(self, cpp): """ @param: cpp - handle that supports code generation interface (see code_generator.py) @@ -153,814 +172,20 @@ def parent_qualifier(self): int MyClass::m_staticVar = 0; Supports for nested classes, e.g. - void MyClass::NestedClass::Method() + void MyClass::NestedClass:: """ - full_qualified_name = '' + full_parent_qualifier = '' parent = self.ref_to_parent # walk though all existing parents while parent: - full_qualified_name = f'{parent.name}::{full_qualified_name}' + full_parent_qualifier = f'{parent.name}::{full_parent_qualifier}' parent = parent.ref_to_parent - return full_qualified_name - - -class CppFunction(CppLanguageElement): - """ - The Python class that generates string representation for C++ function or method - Parameters are passed as plain strings('int a', 'void p = NULL' etc) - Available properties: - ret_type - string, return value for the method ('void', 'int'). Could not be set for constructors - is_static - boolean, static method prefix - is_const - boolean, const method prefix, could not be static - is_virtual - boolean, virtual method postfix, could not be static - is_pure_virtual - boolean, ' = 0' method postfix, could not be static - documentation - string, '/// Example doxygen' - implementation_handle - reference to a function that receives 'self' and C++ code generator handle - (see code_generator.cpp) and generates method body without braces - Ex. - #Python code - def functionBody(self, cpp): cpp('return 42;') - f1 = CppFunction(name = 'GetAnswer', - ret_type = 'int', - documentation = '// Generated code', - implementation_handle = functionBody) - - // Generated code - int GetAnswer() - { - return 42; - } - """ - availablePropertiesNames = {'ret_type', - 'is_static', - 'is_const', - 'is_constexpr', - 'is_virtual', - 'is_pure_virtual', - 'implementation_handle', - 'documentation', - 'is_method'} | CppLanguageElement.availablePropertiesNames - - def __init__(self, **properties): - - # check properties - input_property_names = set(properties.keys()) - self.check_input_properties_names(input_property_names) - super(CppFunction, self).__init__(properties) - self.init_class_properties(current_class_properties=self.availablePropertiesNames, - input_properties_dict=properties) - - # arguments are plain strings - # e.g. 'int* a', 'const string& s', 'size_t sz = 10' - self.arguments = [] - - # noinspection PyUnresolvedReferences - def _sanity_check(self): - """ - Check whether attributes compose a correct C++ code - """ - if not self.is_method and (self.is_static or self.is_const or self.is_virtual or self.is_pure_virtual): - raise RuntimeError('Non-member function could not be static, const or (pure)virtual') - if self.is_method and self.is_static and self.is_virtual: - raise RuntimeError('Static method could not be virtual') - if self.is_method and self.is_pure_virtual and not self.is_virtual: - raise RuntimeError('Pure virtual method should have attribute is_virtual=True') - if self.is_method and not self.ref_to_parent: - raise RuntimeError('Method object could be a child of a CppClass only. Use CppClass.add_method()') - if self.is_constexpr and not self.implementation_handle: - raise RuntimeError("Method object must be initialized when 'constexpr'") - - def add_argument(self, argument): - """ - @param: argument string representation of the C++ function argument ('int a', 'void p = NULL' etc) - """ - self.arguments.append(argument) - - def implementation(self, cpp): - """ - The method calls Python function that creates C++ method body if handle exists - """ - if self.implementation_handle is not None: - self.implementation_handle(self, cpp) - - def declaration(self): - """ - @return: CppDeclaration wrapper, that could be used - for declaration rendering using render_to_string(cpp) interface - """ - return CppDeclaration(self) - - def definition(self): - """ - @return: CppImplementation wrapper, that could be used - for definition rendering using render_to_string(cpp) interface - """ - return CppImplementation(self) - - def render_to_string(self, cpp): - """ - By default method is rendered as a declaration mutual with implementation, - like the method is implemented within the C++ class body, e.g. - class A - { - void f() - { - ... - } - } - """ - # check all properties for the consistency - self._sanity_check() - if self.documentation and self.is_constexpr: - cpp(dedent(self.documentation)) - with cpp.block('{0}{1}{2} {3}({4}){5}{6}'.format( - 'virtual ' if self.is_virtual else '', - 'constexpr ' if self.is_constexpr else '', - self.ret_type if self.ret_type else '', - self.name, - ', '.join(self.arguments), - ' const ' if self.is_const else '', - ' = 0' if self.is_pure_virtual else '')): - self.implementation(cpp) - - def render_to_string_declaration(self, cpp): - """ - Special case for a method declaration string representation. - Generates just a function signature terminated by ';' - Example: - int GetX() const; - """ - # check all properties for the consistency - self._sanity_check() - if self.is_constexpr: - if self.documentation: - cpp(dedent(self.documentation)) - with cpp.block('{0}constexpr {1} {2}({3}){4}{5}'.format( - 'virtual ' if self.is_virtual else '', - self.ret_type if self.ret_type else '', - self.name, - ', '.join(self.arguments), - ' const ' if self.is_const else '', - ' = 0' if self.is_pure_virtual else '')): - self.implementation(cpp) - else: - cpp('{0}{1} {2}({3}){4}{5};'.format('virtual ' if self.is_virtual else '', - self.ret_type if self.ret_type else '', - self.name, - ', '.join(self.arguments), - ' const ' if self.is_const else '', - ' = 0' if self.is_pure_virtual else '')) - - def render_to_string_implementation(self, cpp): - """ - Special case for a method implementation string representation. - Generates method string in the form - Example: - int MyClass::GetX() const - { - ... - } - Generates method body if self.implementation_handle property exists - """ - # check all properties for the consistency - self._sanity_check() - if self.documentation and not self.is_constexpr: - cpp(dedent(self.documentation)) - with cpp.block('{0}{1} {2}{3}({4}){5}{6}'.format( - '/*virtual*/' if self.is_virtual else '', - self.ret_type if self.ret_type else '', - '{0}'.format(self.parent_qualifier()) if self.is_method else '', - self.name, - ', '.join(self.arguments), - ' const ' if self.is_const else '', - ' = 0' if self.is_pure_virtual else '')): - self.implementation(cpp) - - -class CppEnum(CppLanguageElement): - """ - The Python class that generates string representation for C++ enum - All enum elements are explicitly initialized with incremented values - - Available properties: - prefix - string, prefix added to every enum element, 'e' by default ('eItem1') - add_counter - boolean, terminating value that shows count of enum elements added, 'True' by default. - - Example of usage: - # Python code - enum_elements = CppEnum(name = 'Items') - for item in ['Chair', 'Table', 'Shelve']: - enum_elements.add_item(enum_elements.name) - - // Generated C++ code - enum Items - { - eChair = 0, - eTable = 1, - eShelve = 2, - eItemsCount = 3 - } - """ - availablePropertiesNames = {'prefix', - 'add_counter'} | CppLanguageElement.availablePropertiesNames - - def __init__(self, **properties): - # check properties - input_property_names = set(properties.keys()) - self.check_input_properties_names(input_property_names) - super(CppEnum, self).__init__(properties) - - self.init_class_properties(current_class_properties=self.availablePropertiesNames, - input_properties_dict=properties) - - # place enum items here - self.enum_items = [] - - def add_item(self, item): - """ - @param: item - string representation for the enum element - """ - self.enum_items.append(item) - - def add_items(self, items): - """ - @param: items - list of strings - """ - self.enum_items.extend(items) - - # noinspection PyUnresolvedReferences - def render_to_string(self, cpp): - """ - Generates a string representation for the enum - It always contains a terminating value that shows count of enum elements - enum MyEnum - { - eItem1 = 0, - eItem2 = 1, - eMyEnumCount = 2 - } - """ - counter = 0 - finalPrefix = self.prefix if self.prefix != None else 'e' - with cpp.block('enum {0}'.format(self.name), ';'): - for item in self.enum_items: - cpp('{0}{1} = {2},'.format(finalPrefix, item, counter)) - counter += 1 - if self.add_counter in [None, True]: - last_element = '{0}{1}Count = {2}'.format(finalPrefix, self.name, counter) - cpp(last_element) - - -# noinspection PyUnresolvedReferences -class CppVariable(CppLanguageElement): - """ - The Python class that generates string representation for C++ variable (automatic or class member) - For example: - class MyClass - { - int m_var1; - double m_var2; - ... - } - Available properties: - type - string, variable type - is_static - boolean, 'static' prefix - is_extern - boolean, 'extern' prefix - is_const - boolean, 'const' prefix - initialization_value - string, value to be initialized with. - 'a = value;' for automatic variables, 'a(value)' for the class member - documentation - string, '/// Example doxygen' - is_class_member - boolean, for appropriate definition/declaration rendering - """ - availablePropertiesNames = {'type', - 'is_static', - 'is_extern', - 'is_const', - 'is_constexpr', - 'initialization_value', - 'documentation', - 'is_class_member'} | CppLanguageElement.availablePropertiesNames - - def __init__(self, **properties): - input_property_names = set(properties.keys()) - self.check_input_properties_names(input_property_names) - super(CppVariable, self).__init__(properties) - self.init_class_properties(current_class_properties=self.availablePropertiesNames, - input_properties_dict=properties) - if self.is_const and self.is_constexpr: - raise RuntimeError("Variable object can be either 'const' or 'constexpr', not both") - if self.is_constexpr and not self.initialization_value: - raise RuntimeError("Variable object must be initialized when 'constexpr'") - if self.is_static and self.is_extern: - raise RuntimeError("Variable object can be either 'extern' or 'static', not both") - - def declaration(self): - """ - @return: CppDeclaration wrapper, that could be used - for declaration rendering using render_to_string(cpp) interface - """ - return CppDeclaration(self) - - def definition(self): - """ - @return: CppImplementation wrapper, that could be used - for definition rendering using render_to_string(cpp) interface - """ - return CppImplementation(self) - - def render_to_string(self, cpp): - """ - Only automatic variables or static const class members could be rendered using this method - Generates complete variable definition, e.g. - int a = 10; - const double b = M_PI; - """ - if self.is_class_member and not (self.is_static and self.is_const): - raise RuntimeError('For class member variables use definition() and declaration() methods') - else: - if self.documentation: - cpp(dedent(self.documentation)) - cpp('{0}{1}{2} {3}{4};'.format('static ' if self.is_static else 'extern ' if self.is_extern else '', - 'const ' if self.is_const else 'constexpr ' if self.is_constexpr else '', - self.type, - self.name, - ' = {0}'.format( - self.initialization_value) if self.initialization_value else '')) - - def render_to_string_declaration(self, cpp): - """ - Generates declaration for the class member variables, for example - int m_var; - """ - if not self.is_class_member: - raise RuntimeError('For automatic variable use its render_to_string() method') - - if self.documentation and self.is_class_member: - cpp(dedent(self.documentation)) - cpp('{0}{1}{2} {3};'.format( - 'static ' if self.is_static else '', - 'const ' if self.is_const else 'constexpr ' if self.is_constexpr else '', - self.type, - self.name if not self.is_constexpr else '{} = {}'.format(self.name, self.initialization_value))) - - def render_to_string_implementation(self, cpp): - """ - Generates definition for the class member variable. - Output depends on the variable type - - Generates something like - int MyClass::m_my_static_var = 0; - - for static class members, and - m_var(0) - for non-static class members. - That string could be used in constructor initialization string - """ - if not self.is_class_member: - raise RuntimeError('For automatic variable use its render_to_string() method') - - # generate definition for the static class member - if not self.is_constexpr: - if self.is_static: - cpp('{0}{1} {2}{3} {4};'.format('const ' if self.is_const else '', - self.type, - '{0}'.format(self.parent_qualifier()), - self.name, - ' = {0}'.format( - self.initialization_value if self.initialization_value else ''))) - - # generate definition for non-static static class member - # (string for the constructor initialization list) - else: - cpp('{0}({1})'.format(self.name, self.initialization_value if self.initialization_value else '')) - - -# noinspection PyUnresolvedReferences -class CppArray(CppLanguageElement): - """ - The Python class that generates string representation for C++ array (automatic or class member) - For example: - - int arr[] = {1,2,2}; - double doubles[5] = {1.0,2.0}; - - class MyClass - { - int m_arr1[10]; - static double m_arr2[]; - ... - } - Available properties: - - type - string, variable type - is_static - boolean, 'static' prefix - is_const - boolean, 'const' prefix - arraySize - integer, size of array if required - is_class_member - boolean, for appropriate definition/declaration rendering - newline_align - in the array definition rendering place every item on the new string - """ - availablePropertiesNames = {'type', - 'is_static', - 'is_const', - 'arraySize', - 'is_class_member', - 'newline_align'} | CppLanguageElement.availablePropertiesNames - - def __init__(self, **properties): - input_property_names = set(properties.keys()) - self.check_input_properties_names(input_property_names) - super(CppArray, self).__init__(properties) - self.init_class_properties(current_class_properties=self.availablePropertiesNames, - input_properties_dict=properties) - # array elements - self.items = [] - - def declaration(self): - """ - @return: CppDeclaration wrapper, that could be used - for declaration rendering using render_to_string(cpp) interface - """ - return CppDeclaration(self) - - def definition(self): - """ - @return: CppImplementation wrapper, that could be used - for definition rendering using render_to_string(cpp) interface - """ - return CppImplementation(self) + return full_parent_qualifier - def add_array_item(self, item): + def fully_qualified_name(self): """ - If variable is an array it could contain a number of items - @param: item - string - """ - self.items.append(item) - - def add_array_items(self, items): - """ - If variable is an array it could contain a number of items - @param: items - list of strings - """ - self.items.extend(items) - - def __render_value(self, cpp): - """ - Render to string array items - """ - if not self.items: - raise RuntimeError('Empty arrays do not supported') - for item in self.items[:-1]: - cpp('{0},'.format(item)) - cpp('{0}'.format(self.items[-1])) - - def render_to_string(self, cpp): - """ - Generates definition for the C++ array. - Output depends on the array type - - Generates something like - int my_array[5] = {1, 2, 0}; - const char* my_array[] = {"Hello", "World"}; - - That method is used for generating automatic (non-class members) arrays - For class members use render_to_string_declaration/render_to_string_implementation methods - """ - if self.is_class_member and not (self.is_static and self.is_const): - raise RuntimeError('For class member variables use definition() and declaration() methods') - - # newline-formatting of array elements makes sense only if array is not empty - if self.newline_align and self.items: - with cpp.block('{0}{1}{2} {3}{4} = '.format('static ' if self.is_static else '', - 'const ' if self.is_const else '', - self.type, - self.name, - '[{0}]'.format(self.arraySize if self.arraySize else '')), ';'): - # iterate over array items - self.__render_value(cpp) - else: - cpp('{0}{1}{2} {3}{4} = {5};'.format('static ' if self.is_static else '', - 'const ' if self.is_const else '', - self.type, - self.name, - '[{0}]'.format(self.arraySize if self.arraySize else ''), - '{{{0}}}'.format(', '.join(self.items)) if self.items else 'NULL')) - - def render_to_string_declaration(self, cpp): - """ - Generates declaration for the C++ array. - Non-static arrays-class members do not supported - - Example: - static int my_class_member_array[]; - """ - if not self.is_class_member: - raise RuntimeError('For automatic variable use its render_to_string() method') - - cpp('{0}{1}{2} {3}{4};'.format('static ' if self.is_static else '', - 'const ' if self.is_const else '', - self.type, - self.name, - '[{0}]'.format(self.arraySize if self.arraySize else ''))) - - def render_to_string_implementation(self, cpp): - """ - Generates definition for the C++ array. - Output depends on the array type - - Example: - int MyClass::m_my_static_array[] = - { - ... - }; - - Non-static arrays-class members do not supported - """ - if not self.is_class_member: - raise RuntimeError('For automatic variable use its render_to_string() method') - - # generate definition for the static class member arrays only - # other types does not supported - if not self.is_static: - raise RuntimeError('Only static arrays as class members are supported') - - # newline-formatting of array elements makes sense only if array is not empty - if self.newline_align and self.items: - with cpp.block('{0}{1}{2} {3}{4}{5} = '.format('static ' if self.is_static else '', - 'const ' if self.is_const else '', - self.type, - '{0}'.format(self.parent_qualifier()), - self.name, - '[{0}]'.format(self.arraySize if self.arraySize else '')), - ';'): - # iterate over array items - self.__render_value(cpp) - else: - cpp('{0}{1}{2} {3}{4}{5} = {6};'.format('static ' if self.is_static else '', - 'const ' if self.is_const else '', - self.type, - '{0}'.format(self.parent_qualifier()), - self.name, - '[{0}]'.format(self.arraySize if self.arraySize else ''), - '{{{0}}}'.format(', '.join(self.items)) if self.items else 'NULL')) - - -class CppClass(CppLanguageElement): - """ - The Python class that generates string representation for C++ class or struct. - Usually contains a number of child elements - internal classes, enums, methods and variables. - Available properties: - is_struct - boolean, use 'struct' keyword for class declaration, 'class' otherwise - documentation - string, '/// Example doxygen' - - Example of usage: - - # Python code - cpp_class = CppClass(name = 'MyClass', is_struct = True) - cpp_class.add_variable(CppVariable(name = "m_var", - type = 'size_t', - is_static = True, - is_const = True, - initialization_value = 255)) - - def handle(cpp): cpp('return m_var;') - - cpp_class.add_method(CppFunction(name = "GetVar", - ret_type = 'size_t', - is_static = True, - implementation_handle = handle)) - - // Generated C++ declaration - struct MyClass - { - static const size_t m_var; - static size_t GetVar(); - } - - // Generated C++ definition - const size_t MyClass::m_var = 255; - - size_t MyClass::GetVar() - { - return m_var; - } - """ - availablePropertiesNames = {'is_struct', - 'documentation', - 'parent_class'} | CppLanguageElement.availablePropertiesNames - - def __init__(self, **properties): - input_property_names = set(properties.keys()) - self.check_input_properties_names(input_property_names) - super(CppClass, self).__init__(properties) - self.init_class_properties(current_class_properties=self.availablePropertiesNames, - input_properties_dict=properties) - - # aggregated classes - self.internal_class_elements = [] - - # class members - self.internal_variable_elements = [] - - # array class members - self.internal_array_elements = [] - - # class methods - self.internal_method_elements = [] - - # class enums - self.internal_enum_elements = [] - - ######################################## - # ADD CLASS MEMBERS - def add_enum(self, enum): - """ - @param: enum CppEnum instance - """ - enum.ref_to_parent = self - self.internal_enum_elements.append(enum) - - def add_variable(self, cpp_variable): - """ - @param: cpp_variable CppVariable instance - """ - cpp_variable.ref_to_parent = self - cpp_variable.is_class_member = True - self.internal_variable_elements.append(cpp_variable) - - def add_array(self, cpp_variable): - """ - @param: cpp_variable CppVariable instance - """ - cpp_variable.ref_to_parent = self - cpp_variable.is_class_member = True - self.internal_array_elements.append(cpp_variable) - - def add_internal_class(self, cpp_class): - """ - Add nested class - @param: cpp_class CppClass instance - """ - cpp_class.ref_to_parent = self - self.internal_class_elements.append(cpp_class) - - def add_method(self, method): - """ - @param: method CppFunction instance - """ - method.ref_to_parent = self - method.is_method = True - self.internal_method_elements.append(method) - - ######################################## - # RENDER CLASS MEMBERS - def render_internal_classes_declaration(self, cpp): - """ - Generates section of nested classes - Could be placed both in 'private:' or 'public:' sections - """ - for classItem in self.internal_class_elements: - classItem.declaration().render_to_string(cpp) - cpp.newline() - - def render_enum_section(self, cpp): - """ - Render to string all contained enums - """ - for enumItem in self.internal_enum_elements: - enumItem.render_to_string(cpp) - cpp.newline() - - def render_variables_declaration(self, cpp): - """ - Render to string all contained variable class members - """ - for varItem in self.internal_variable_elements: - varItem.declaration().render_to_string(cpp) - cpp.newline() - - def render_array_declaration(self, cpp): - """ - Render to string all contained array class members - """ - for arrItem in self.internal_array_elements: - arrItem.declaration().render_to_string(cpp) - cpp.newline() - - def render_methods_declaration(self, cpp): - """ - Generates all class methods declaration - Should be placed in 'public:' section - """ - for funcItem in self.internal_method_elements: - funcItem.render_to_string_declaration(cpp) - cpp.newline() - - def render_static_members_implementation(self, cpp): - """ - Generates definition for all static class variables - int MyClass::my_static_array[] = {} - """ - # generate definition for static variables - static_vars = [variable for variable in self.internal_variable_elements if variable.is_static] - for varItem in static_vars: - varItem.definition().render_to_string(cpp) - cpp.newline() - for arrItem in self.internal_array_elements: - arrItem.definition().render_to_string(cpp) - cpp.newline() - - # do the same for nested classes - for classItem in self.internal_class_elements: - classItem.render_static_members_implementation(cpp) - cpp.newline() - - def render_methods_implementation(self, cpp): - """ - Generates all class methods declaration - Should be placed in 'public:' section - """ - # generate methods implementation section - for funcItem in self.internal_method_elements: - funcItem.render_to_string_implementation(cpp) - cpp.newline() - # do the same for nested classes - for classItem in self.internal_class_elements: - classItem.render_static_members_implementation(cpp) - cpp.newline() - - ######################################## - # GROUP GENERATED SECTIONS - def class_interface(self, cpp): - """ - Generates section that generally used as an 'open interface' - Generates string representation for enums, internal classes and methods - Should be placed in 'public:' section - """ - self.render_enum_section(cpp) - self.render_internal_classes_declaration(cpp) - self.render_methods_declaration(cpp) - - def private_class_members(self, cpp): - """ - Generates section of class member variables. - Should be placed in 'private:' section - """ - self.render_variables_declaration(cpp) - self.render_array_declaration(cpp) - - def render_to_string(self, cpp): - """ - Render to string both declaration and definition. - A rare case enough, because the only code generator handle is used. - Typically class declaration is rendered to *.h file, and definition to *.cpp - """ - self.render_to_string_declaration(cpp) - self.render_to_string_implementation(cpp) - - # noinspection PyUnresolvedReferences - def render_to_string_declaration(self, cpp): - """ - Render to string class declaration. - Typically handle to header should be passed as 'cpp' param - """ - if self.documentation: - cpp(dedent(self.documentation)) - class_type = 'struct' if self.is_struct else 'class' - with cpp.block('{0} {1} {2}'.format(class_type, - self.name, - ' : public {0}'.format(self.parent_class) if self.parent_class else ''), - ';'): - - # in case of struct all members meant to be public - if not self.is_struct: - cpp.label('public') - self.class_interface(cpp) - cpp.newline() - - # in case of struct all members meant to be public - if not self.is_struct: - cpp.label('private') - self.private_class_members(cpp) - - def render_to_string_implementation(self, cpp): - """ - Render to string class definition. - Typically handle to *.cpp file should be passed as 'cpp' param - """ - cpp.newline(2) - self.render_static_members_implementation(cpp) - self.render_methods_implementation(cpp) - - def declaration(self): - """ - @return: CppDeclaration wrapper, that could be used - for declaration rendering using render_to_string(cpp) interface - """ - return CppDeclaration(self) - - def definition(self): - """ - @return: CppImplementation wrapper, that could be used - for definition rendering using render_to_string(cpp) interface + Generate string for fully qualified name of the element + Ex. + MyClass::NestedClass::Method() """ - return CppImplementation(self) + return f'{self.parent_qualifier()}{self.name}' diff --git a/src/code_generation/cpp_variable.py b/src/code_generation/cpp_variable.py new file mode 100644 index 0000000..145e832 --- /dev/null +++ b/src/code_generation/cpp_variable.py @@ -0,0 +1,214 @@ +from code_generation.cpp_generator import CppLanguageElement, CppDeclaration, CppImplementation +from textwrap import dedent + +__doc__ = """The module encapsulates C++ code generation logics for main C++ language primitives: +classes, methods and functions, variables, enums. +Every C++ element could render its current state to a string that could be evaluated as +a legal C++ construction. + +Some elements could be rendered to a pair of representations (i.e. declaration and definition) + +Example: +# Python code +cpp_class = CppClass(name = 'MyClass', is_struct = True) +cpp_class.add_variable(CppVariable(name = "m_var", + type = 'size_t', + is_static = True, + is_const = True, + initialization_value = 255)) + +// Generated C++ declaration +struct MyClass +{ + static const size_t m_var; +} + +// Generated C++ definition +const size_t MyClass::m_var = 255; + + +That module uses and highly depends on code_generator.py as it uses +code generating and formatting primitives implemented there. + +The main object referenced from code_generator.py is CppFile, +which is passed as a parameter to render_to_string(cpp) Python method + +It could also be used for composing more complicated C++ code, +that does not supported by cpp_generator + +It support: + +- functional calls: +cpp('int a = 10;') + +- 'with' semantic: +with cpp.block('class MyClass', ';') + class_definition(cpp) + +- append code to the last string without EOL: +cpp.append(', p = NULL);') + +- empty lines: +cpp.newline(2) + +For detailed information see code_generator.py documentation. +""" + + +# noinspection PyUnresolvedReferences +class CppVariable(CppLanguageElement): + """ + The Python class that generates string representation for C++ variable (automatic or class member) + For example: + class MyClass + { + int m_var1; + double m_var2; + ... + } + Available properties: + type - string, variable type + is_static - boolean, 'static' prefix + is_extern - boolean, 'extern' prefix + is_const - boolean, 'const' prefix + is_constexpr - boolean, 'constexpr' prefix + initialization_value - string, initialization_value to be initialized with. + 'a = initialization_value;' for automatic variables, 'a(initialization_value)' for the class member + documentation - string, '/// Example doxygen' + is_class_member - boolean, for appropriate definition/declaration rendering + """ + availablePropertiesNames = {'type', + 'is_static', + 'is_extern', + 'is_const', + 'is_constexpr', + 'initialization_value', + 'documentation', + 'is_class_member'} | CppLanguageElement.availablePropertiesNames + + def __init__(self, **properties): + input_property_names = set(properties.keys()) + self.check_input_properties_names(input_property_names) + super(CppVariable, self).__init__(properties) + self.init_class_properties(current_class_properties=self.availablePropertiesNames, + input_properties_dict=properties) + + def _sanity_check(self): + """ + @raise: ValueError, if some properties are not valid + """ + if self.is_const and self.is_constexpr: + raise ValueError("Variable object can be either 'const' or 'constexpr', not both") + if self.is_constexpr and not self.initialization_value: + raise ValueError("Variable object must be initialized when 'constexpr'") + if self.is_static and self.is_extern: + raise ValueError("Variable object can be either 'extern' or 'static', not both") + + def _render_static(self): + """ + @return: 'static' prefix, can't be used with 'extern' + """ + return 'static ' if self.is_static else '' + + def _render_extern(self): + """ + @return: 'extern' prefix, can't be used with 'static' + """ + return 'extern ' if self.is_extern else '' + + def _render_const(self): + """ + @return: 'const' prefix, can't be used with 'constexpr' + """ + return 'const ' if self.is_const else '' + + def _render_constexpr(self): + """ + @return: 'constexpr' prefix, can't be used with 'const' + """ + return 'constexpr ' if self.is_constexpr else '' + + def _render_init_value(self): + """ + @return: string, initialization_value to be initialized with + """ + return self.initialization_value if self.initialization_value else '' + + def assignment(self, value): + """ + Generates assignment statement for the variable, e.g. + a = 10; + b = 20; + """ + return f'{self.name} = {value}' + + def declaration(self): + """ + @return: CppDeclaration wrapper, that could be used + for declaration rendering using render_to_string(cpp) interface + """ + return CppDeclaration(self) + + def definition(self): + """ + @return: CppImplementation wrapper, that could be used + for definition rendering using render_to_string(cpp) interface + """ + return CppImplementation(self) + + def render_to_string(self, cpp): + """ + Only automatic variables or static const class members could be rendered using this method + Generates complete variable definition, e.g. + int a = 10; + const double b = M_PI; + """ + self._sanity_check() + if self.is_class_member and not (self.is_static and self.is_const): + raise RuntimeError('For class member variables use definition() and declaration() methods') + elif self.is_extern: + cpp(f'{self._render_extern()}{self.type} {self.name};') + else: + if self.documentation: + cpp(dedent(self.documentation)) + cpp(f'{self._render_static()}{self._render_const()}{self._render_constexpr()}' + f'{self.type} {self.assignment(self._render_init_value())};') + + def render_to_string_declaration(self, cpp): + """ + Generates declaration for the class member variables, for example + int m_var; + """ + if not self.is_class_member: + raise RuntimeError('For automatic variable use its render_to_string() method') + + if self.documentation and self.is_class_member: + cpp(dedent(self.documentation)) + cpp(f'{self._render_static()}{self._render_extern()}{self._render_const()}{self._render_constexpr()}' + f'{self.type} {self.name if not self.is_constexpr else self.assignment(self._render_init_value())};') + + def render_to_string_implementation(self, cpp): + """ + Generates definition for the class member variable. + Output depends on the variable type + + Generates something like + int MyClass::m_my_static_var = 0; + + for static class members, and + m_var(0) + for non-static class members. + That string could be used in constructor initialization string + """ + if not self.is_class_member: + raise RuntimeError('For automatic variable use its render_to_string() method') + + # generate definition for the static class member + if not self.is_constexpr: + if self.is_static: + cpp(f'{self._render_static()}{self._render_const()}{self._render_constexpr()}' + f'{self.type} {self.fully_qualified_name()} = {self._render_init_value()};') + # generate definition for non-static static class member, e.g. m_var(0) + # (string for the constructor initialization list) + else: + cpp(f'{self.name}({self._render_init_value()})') diff --git a/src/code_generation/html_generator.py b/src/code_generation/html_generator.py new file mode 100644 index 0000000..2dbc6ae --- /dev/null +++ b/src/code_generation/html_generator.py @@ -0,0 +1,81 @@ +import sys +from code_generation.code_style import HTMLStyle + + +class HtmlFile: + + Formatter = HTMLStyle + + def __init__(self, filename, writer=None): + self.current_indent = 0 + self.last = None + self.filename = filename + if writer: + self.out = writer + else: + self.out = open(filename, "w") + + def close(self): + """ + File created, just close the handle + """ + self.out.close() + self.out = None + + def write(self, text, indent=0, endline=True): + """ + Write a new line with line ending + """ + self.out.write('{0}{1}{2}'.format(self.Formatter.indent * (self.current_indent+indent), + text, + self.Formatter.endline if endline else '')) + + def append(self, x): + """ + Append to the existing line without line ending + """ + self.out.write(x) + + def __call__(self, text, indent=0, endline=True): + """ + Supports 'object()' semantic, i.e. + cpp('#include ') + inserts appropriate line + """ + self.write(text, indent, endline) + + def block(self, element, **attributes): + """ + Returns a stub for HTML element + Supports 'with' semantic, i.e. + html.block(element='p', id='id1', name='name1'): + """ + return self.Formatter(self, element=element, **attributes) + + def endline(self, count=1): + """ + Insert an endline + """ + self.write(self.Formatter.endline * count, endline=False) + + def newline(self, n=1): + """ + Insert one or several empty lines + """ + for _ in range(n): + self.write('') + + +def html_example(): + html = HtmlFile('ex.html') + with html.block(element='p', id='id1', name='name1'): + html('Text') + + +def main(): + html_example() + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/test_cpp_generator.py b/tests/create_assets.py similarity index 52% rename from tests/test_cpp_generator.py rename to tests/create_assets.py index c25271f..3ee1248 100644 --- a/tests/test_cpp_generator.py +++ b/tests/create_assets.py @@ -1,174 +1,18 @@ -import unittest -import filecmp import os -import io -import sys -sys.path.append('src') -sys.path.append('../src') -from code_generation.code_generator import * -from code_generation.cpp_generator import * +from code_generation.code_generator import CppFile +from code_generation.cpp_variable import CppVariable +from code_generation.cpp_enum import CppEnum +from code_generation.cpp_array import CppArray +from code_generation.cpp_function import CppFunction +from code_generation.cpp_class import CppClass __doc__ = """ -Unit tests for C++ code generator """ +PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -def handle_to_factorial(_, cpp): - cpp('return n < 1 ? 1 : (n * factorial(n - 1));') - - -class TestCppFunctionGenerator(unittest.TestCase): - - @staticmethod - def handle_to_factorial(_, cpp): - cpp('return n < 1 ? 1 : (n * factorial(n - 1));') - - def test_is_constexpr_raises_error_when_implementation_value_is_none(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - func = CppFunction(name="factorial", ret_type="int", is_constexpr=True) - self.assertRaises(RuntimeError, func.render_to_string, cpp) - - def test_is_constexpr_render_to_string(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - func = CppFunction(name="factorial", ret_type="int", - implementation_handle=TestCppFunctionGenerator.handle_to_factorial, is_constexpr=True) - func.add_argument('int n') - func.render_to_string(cpp) - self.assertIn(dedent("""\ - constexpr int factorial(int n) - { - \treturn n < 1 ? 1 : (n * factorial(n - 1)); - }"""), writer.getvalue()) - - def test_is_constexpr_render_to_string_declaration(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - func = CppFunction(name="factorial", ret_type="int", - implementation_handle=TestCppFunctionGenerator.handle_to_factorial, is_constexpr=True) - func.add_argument('int n') - func.render_to_string_declaration(cpp) - self.assertIn(dedent("""\ - constexpr int factorial(int n) - { - \treturn n < 1 ? 1 : (n * factorial(n - 1)); - }"""), writer.getvalue()) - - def test_README_example(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - factorial_function = CppFunction(name='factorial', ret_type='int', is_constexpr=True, - implementation_handle=handle_to_factorial, - documentation='/// Calculates and returns the factorial of p n.') - factorial_function.add_argument('int n') - factorial_function.render_to_string(cpp) - self.assertIn(dedent("""\ - /// Calculates and returns the factorial of p n. - constexpr int factorial(int n) - { - \treturn n < 1 ? 1 : (n * factorial(n - 1)); - }"""), writer.getvalue()) - - -class TestCppVariableGenerator(unittest.TestCase): - - def test_cpp_var_via_writer(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - variables = CppVariable(name="var1", - type="char*", - is_class_member=False, - is_static=False, - is_const=True, - initialization_value='0') - variables.render_to_string(cpp) - self.assertEqual('const char* var1 = 0;\n', writer.getvalue()) - - def test_is_constexpr_raises_error_when_is_const_true(self): - self.assertRaises(RuntimeError, CppVariable, name="COUNT", type="int", is_class_member=True, is_const=True, - is_constexpr=True, initialization_value='0') - - def test_is_constexpr_raises_error_when_initialization_value_is_none(self): - self.assertRaises(RuntimeError, CppVariable, name="COUNT", type="int", is_class_member=True, is_constexpr=True) - - def test_is_constexpr_render_to_string(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - variables = CppVariable(name="COUNT", - type="int", - is_class_member=False, - is_constexpr=True, - initialization_value='0') - variables.render_to_string(cpp) - self.assertIn('constexpr int COUNT = 0;', writer.getvalue()) - - def test_is_constexpr_render_to_string_declaration(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - variables = CppVariable(name="COUNT", - type="int", - is_class_member=True, - is_constexpr=True, - initialization_value='0') - variables.render_to_string_declaration(cpp) - self.assertIn('constexpr int COUNT = 0;', writer.getvalue()) - - def test_is_extern_raises_error_when_is_static_is_true(self): - self.assertRaises(RuntimeError, CppVariable, name="var1", type="char*", is_static=True, is_extern=True) - - def test_is_extern_render_to_string(self): - writer = io.StringIO() - cpp = CppFile(None, writer=writer) - v = CppVariable(name="var1", type="char*", is_extern=True) - v.render_to_string(cpp) - self.assertIn('extern char* var1;', writer.getvalue()) - - -class TestCppGenerator(unittest.TestCase): - """ - Test C++ code generation - """ - def test_cpp_variables(self): - generate_var(output_dir='.') - expected_cpp = ['var.cpp'] - self.assertEqual(filecmp.cmpfiles('.', 'tests/test_assets', expected_cpp)[0], expected_cpp) - os.remove(expected_cpp[0]) - - def test_cpp_arrays(self): - generate_array(output_dir='.') - expected_cpp = ['array.cpp'] - self.assertEqual(filecmp.cmpfiles('.', 'tests/test_assets', expected_cpp)[0], expected_cpp) - os.remove(expected_cpp[0]) - - def test_cpp_function(self): - generate_func(output_dir='.') - expected_cpp = ['func.cpp'] - expected_h = ['func.h'] - self.assertEqual(filecmp.cmpfiles('.', 'tests/test_assets', expected_cpp)[0], expected_cpp) - self.assertEqual(filecmp.cmpfiles('.', 'tests/test_assets', expected_h)[0], expected_h) - os.remove(expected_cpp[0]) - os.remove(expected_h[0]) - - def test_cpp_enum(self): - generate_enum(output_dir='.') - expected_cpp = ['enum.cpp'] - self.assertEqual(filecmp.cmpfiles('.', 'tests/test_assets', expected_cpp)[0], expected_cpp) - os.remove(expected_cpp[0]) - - def test_cpp_class(self): - generate_class(output_dir='.') - expected_cpp = ['class.cpp'] - expected_h = ['class.h'] - self.assertEqual(filecmp.cmpfiles('.', 'tests/test_assets', expected_cpp)[0], expected_cpp) - self.assertEqual(filecmp.cmpfiles('.', 'tests/test_assets', expected_h)[0], expected_h) - os.remove(expected_cpp[0]) - os.remove(expected_h[0]) - - -# Generate test data def generate_enum(output_dir='.'): """ Generate model data (C++ enum) @@ -243,7 +87,7 @@ def generate_var(output_dir='.'): def generate_array(output_dir='.'): cpp = CppFile(os.path.join(output_dir, 'array.cpp')) arrays = [] - a1 = CppArray(name='array1', type='int', is_const=True, arraySize=5) + a1 = CppArray(name='array1', type='int', is_const=True, array_size=5) a1.add_array_items(['1', '2', '3']) a2 = CppArray(name='array2', type='const char*', is_const=True) a2.add_array_item('"Item1"') @@ -328,9 +172,9 @@ def generate_class(output_dir='.'): my_class.add_variable(CppVariable(name="m_var3", type="int", is_constexpr=True, - is_static = True, + is_static=True, is_class_member=True, - initialization_value = 42)) + initialization_value=42)) example_class.add_variable(CppVariable( name="m_var1", @@ -385,17 +229,36 @@ def method_body(_, cpp1): my_class_h.close() +def generate_factorial(output_dir='.'): + cpp = CppFile(os.path.join(output_dir, 'factorial.cpp')) + h = CppFile(os.path.join(output_dir, 'factorial.h')) + + def handle_to_factorial(_, cpp_file): + cpp_file('return n < 1 ? 1 : (n * factorial(n - 1));') + + func = CppFunction(name="factorial", ret_type="int", + implementation_handle=handle_to_factorial, + is_constexpr=True) + func.add_argument('int n') + func.render_to_string(cpp) + func.render_to_string_declaration(h) + cpp.close() + h.close() + + def generate_reference_code(): """ Generate model data for C++ generator Do not call unless generator logic is changed """ - generate_enum(output_dir='test_assets') - generate_var(output_dir='test_assets') - generate_array(output_dir='test_assets') - generate_func(output_dir='test_assets') - generate_class(output_dir='test_assets') + asset_dir = os.path.join(PROJECT_DIR, 'new_assets') + generate_enum(output_dir=asset_dir) + generate_var(output_dir=asset_dir) + generate_array(output_dir=asset_dir) + generate_func(output_dir=asset_dir) + generate_class(output_dir=asset_dir) + generate_factorial(output_dir=asset_dir) if __name__ == "__main__": - unittest.main() + generate_reference_code() diff --git a/tests/test_assets/factorial.cpp b/tests/test_assets/factorial.cpp new file mode 100644 index 0000000..ee7af84 --- /dev/null +++ b/tests/test_assets/factorial.cpp @@ -0,0 +1,4 @@ +constexpr int factorial(int n) +{ + return n < 1 ? 1 : (n * factorial(n - 1)); +} diff --git a/tests/test_assets/factorial.h b/tests/test_assets/factorial.h new file mode 100644 index 0000000..ee7af84 --- /dev/null +++ b/tests/test_assets/factorial.h @@ -0,0 +1,4 @@ +constexpr int factorial(int n) +{ + return n < 1 ? 1 : (n * factorial(n - 1)); +} diff --git a/tests/test_cpp_file.py b/tests/test_cpp_file.py new file mode 100644 index 0000000..cdfa501 --- /dev/null +++ b/tests/test_cpp_file.py @@ -0,0 +1,206 @@ +import os +import unittest +import filecmp + +from code_generation.code_generator import CppFile +from code_generation.cpp_variable import CppVariable +from code_generation.cpp_enum import CppEnum +from code_generation.cpp_array import CppArray +from code_generation.cpp_function import CppFunction +from code_generation.cpp_class import CppClass + +__doc__ = """ +Unit tests for C++ code generator +""" + + +class TestCppFileIo(unittest.TestCase): + """ + Test C++ code generation by writing to file + """ + + def test_cpp_array(self): + """ + Test C++ variables generation + """ + cpp = CppFile('array.cpp') + arrays = [] + a1 = CppArray(name='array1', type='int', is_const=True, array_size=5) + a1.add_array_items(['1', '2', '3']) + a2 = CppArray(name='array2', type='const char*', is_const=True) + a2.add_array_item('"Item1"') + a2.add_array_item('"Item2"') + a3 = CppArray(name='array3', type='const char*', is_const=True, newline_align=True) + a3.add_array_item('"ItemNewline1"') + a3.add_array_item('"ItemNewline2"') + + arrays.append(a1) + arrays.append(a2) + arrays.append(a3) + + for arr in arrays: + arr.render_to_string(cpp) + self.assertTrue(filecmp.cmpfiles('.', 'tests', 'array.cpp')) + cpp.close() + if os.path.exists('array.cpp'): + os.remove('array.cpp') + + def test_cpp_class(self): + """ + Test C++ classes generation + """ + my_class_cpp = CppFile('class.cpp') + my_class_h = CppFile('class.h') + my_class = CppClass(name='MyClass') + + enum_elements = CppEnum(name='Items', prefix='wd') + for item in ['One', 'Two', 'Three']: + enum_elements.add_item(item) + my_class.add_enum(enum_elements) + + nested_class = CppClass(name='Nested', is_struct=True) + nested_class.add_variable(CppVariable(name="m_gcAnswer", + type="size_t", + is_class_member=True, + is_static=True, + is_const=True, + initialization_value='42')) + my_class.add_internal_class(nested_class) + + my_class.add_variable(CppVariable(name="m_var1", + type="int", + initialization_value='1')) + + my_class.add_variable(CppVariable(name="m_var2", + type="int*")) + + a2 = CppArray(name='array2', type='char*', is_const=True, is_static=True, ) + a2.add_array_item('"Item1"') + a2.add_array_item('"Item2"') + a3 = CppArray(name='array3', type='char*', is_static=True, is_const=True, newline_align=True) + a3.add_array_item('"ItemNewline1"') + a3.add_array_item('"ItemNewline2"') + + my_class.add_array(a2) + my_class.add_array(a3) + + def const_method_body(_, cpp): + cpp('return m_var1;') + + def virtual_method_body(_, cpp): + cpp('return 0;') + + my_class.add_method(CppClass.CppMethod(name="GetParam", + ret_type="int", + is_const=True, + implementation_handle=const_method_body)) + + my_class.add_method(CppClass.CppMethod(name="VirtualMethod", + ret_type="int", + is_virtual=True, + implementation_handle=virtual_method_body)) + + my_class.add_method(CppClass.CppMethod(name="PureVirtualMethod", + ret_type="void", + is_virtual=True, + is_pure_virtual=True)) + + my_class.declaration().render_to_string(my_class_h) + my_class.definition().render_to_string(my_class_cpp) + + self.assertTrue(filecmp.cmpfiles('.', 'tests', 'class.cpp')) + self.assertTrue(filecmp.cmpfiles('.', 'tests', 'class.h')) + my_class_cpp.close() + my_class_h.close() + if os.path.exists('class.cpp'): + os.remove('class.cpp') + if os.path.exists('class.h'): + os.remove('class.h') + + def test_cpp_enum(self): + """ + Test C++ enums generation + """ + cpp = CppFile('enum.cpp') + enum_elements = CppEnum(name='Items') + for item in ['Chair', 'Table', 'Shelve']: + enum_elements.add_item(item) + enum_elements.render_to_string(cpp) + + enum_elements_custom = CppEnum(name='Items', prefix='it') + for item in ['Chair', 'Table', 'Shelve']: + enum_elements_custom.add_item(item) + enum_elements_custom.render_to_string(cpp) + + self.assertTrue(filecmp.cmpfiles('.', 'tests', 'enum.cpp')) + cpp.close() + if os.path.exists('enum.cpp'): + os.remove('enum.cpp') + + def test_cpp_function(self): + """ + Test C++ functions generation + """ + cpp = CppFile('func.cpp') + hpp = CppFile('func.h') + + def function_body(_, cpp1): + cpp1('return 42;') + + functions = [CppFunction(name='GetParam', ret_type='int'), + CppFunction(name='Calculate', ret_type='void'), + CppFunction(name='GetAnswer', ret_type='int', implementation_handle=function_body)] + + for func in functions: + func.render_to_string(hpp) + for func in functions: + func.render_to_string_declaration(hpp) + functions[2].render_to_string_implementation(cpp) + + self.assertTrue(filecmp.cmpfiles('.', 'tests', 'func.cpp')) + self.assertTrue(filecmp.cmpfiles('.', 'tests', 'func.h')) + cpp.close() + hpp.close() + if os.path.exists('func.cpp'): + os.remove('func.cpp') + if os.path.exists('func.h'): + os.remove('func.h') + + def test_cpp_variable(self): + """ + Test C++ variables generation + """ + cpp = CppFile('var.cpp') + variables = [CppVariable(name="var1", + type="char*", + is_class_member=False, + is_static=False, + is_const=True, + initialization_value='0'), + CppVariable(name="var2", + type="int", + is_class_member=False, + is_static=True, + is_const=False, + initialization_value='0'), + CppVariable(name="var3", + type="std::string", + is_class_member=False, + is_static=False, + is_const=False), + CppVariable(name="var3", + type="std::string", + is_class_member=False, + is_static=False, + is_const=False)] + + for var in variables: + var.render_to_string(cpp) + self.assertTrue(filecmp.cmpfiles('.', 'tests', 'var.cpp')) + cpp.close() + if os.path.exists('var.cpp'): + os.remove('var.cpp') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cpp_function_string.py b/tests/test_cpp_function_string.py new file mode 100644 index 0000000..14d4bbd --- /dev/null +++ b/tests/test_cpp_function_string.py @@ -0,0 +1,71 @@ +import unittest +import io +from textwrap import dedent + +from code_generation.code_generator import CppFile +from code_generation.cpp_function import CppFunction + +__doc__ = """ +Unit tests for C++ code generator +""" + + +def handle_to_factorial(_, cpp): + cpp('return n < 1 ? 1 : (n * factorial(n - 1));') + + +class TestCppFunctionStringIo(unittest.TestCase): + """ + Test C++ function generation by writing to StringIO + """ + + def test_is_constexpr_no_implementation_raises(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + func = CppFunction(name="factorial", ret_type="int", is_constexpr=True) + self.assertRaises(ValueError, func.render_to_string, cpp) + + def test_is_constexpr_render_to_string(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + func = CppFunction(name="factorial", ret_type="int", + implementation_handle=handle_to_factorial, is_constexpr=True) + func.add_argument('int n') + func.render_to_string(cpp) + self.assertIn(dedent("""\ + constexpr int factorial(int n) + { + \treturn n < 1 ? 1 : (n * factorial(n - 1)); + }"""), writer.getvalue()) + + def test_is_constexpr_render_to_string_declaration(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + func = CppFunction(name="factorial", ret_type="int", + implementation_handle=handle_to_factorial, is_constexpr=True) + func.add_argument('int n') + func.render_to_string_declaration(cpp) + self.assertIn(dedent("""\ + constexpr int factorial(int n) + { + \treturn n < 1 ? 1 : (n * factorial(n - 1)); + }"""), writer.getvalue()) + + def test_docstring_example(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + factorial_function = CppFunction(name='factorial', ret_type='int', is_constexpr=True, + implementation_handle=handle_to_factorial, + documentation='/// Calculates and returns the factorial of p @n.') + factorial_function.add_argument('int n') + factorial_function.render_to_string(cpp) + self.assertIn(dedent("""\ + /// Calculates and returns the factorial of p @n. + constexpr int factorial(int n) + { + \treturn n < 1 ? 1 : (n * factorial(n - 1)); + }"""), writer.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cpp_variable_string.py b/tests/test_cpp_variable_string.py new file mode 100644 index 0000000..196841b --- /dev/null +++ b/tests/test_cpp_variable_string.py @@ -0,0 +1,79 @@ +import unittest +import io + +from code_generation.code_generator import CppFile +from code_generation.cpp_variable import CppVariable + +__doc__ = """Unit tests for C++ code generator +""" + + +class TestCppVariableStringIo(unittest.TestCase): + """ + Test C++ variable generation by writing to StringIO + """ + + def test_cpp_var(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + variables = CppVariable(name="var1", + type="char*", + is_class_member=False, + is_static=False, + is_const=True, + initialization_value='0') + variables.render_to_string(cpp) + print(writer.getvalue()) + self.assertEqual('const char* var1 = 0;\n', writer.getvalue()) + + def test_is_constexpr_const_raises(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + var = CppVariable(name="COUNT", type="int", is_class_member=True, is_const=True, + is_constexpr=True, initialization_value='0') + self.assertRaises(ValueError, var.render_to_string, cpp) + + def test_is_constexpr_no_implementation_raises(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + var = CppVariable(name="COUNT", type="int", is_class_member=True, is_constexpr=True) + self.assertRaises(ValueError, var.render_to_string, cpp) + + def test_is_constexpr_render_to_string(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + variables = CppVariable(name="COUNT", + type="int", + is_class_member=False, + is_constexpr=True, + initialization_value='0') + variables.render_to_string(cpp) + self.assertIn('constexpr int COUNT = 0;', writer.getvalue()) + + def test_is_constexpr_render_to_string_declaration(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + variables = CppVariable(name="COUNT", + type="int", + is_class_member=True, + is_constexpr=True, + initialization_value='0') + variables.render_to_string_declaration(cpp) + self.assertIn('constexpr int COUNT = 0;', writer.getvalue()) + + def test_is_extern_static_raises(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + var = CppVariable(name="var1", type="char*", is_static=True, is_extern=True) + self.assertRaises(ValueError, var.render_to_string, cpp) + + def test_is_extern_render_to_string(self): + writer = io.StringIO() + cpp = CppFile(None, writer=writer) + v = CppVariable(name="var1", type="char*", is_extern=True) + v.render_to_string(cpp) + self.assertIn('extern char* var1;', writer.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_html_generator.py b/tests/test_html_generator.py new file mode 100644 index 0000000..9e0a21f --- /dev/null +++ b/tests/test_html_generator.py @@ -0,0 +1,24 @@ +import unittest +import io + +from code_generation.html_generator import * + +__doc__ = """ +Unit tests for HTML code generator +""" + + +class TestHTMLFunctionGenerator(unittest.TestCase): + + def test_is_constexpr_render_to_string(self): + writer = io.StringIO() + html = HtmlFile(None, writer=writer) + with html.block(element='p', id='id1', name='name1'): + html('Text') + print(writer.getvalue()) + result = """

\n Text\n

\n""" + self.assertIn(result, writer.getvalue()) + + +if __name__ == "__main__": + unittest.main()