Source code for csnake.codewriter

# -*- coding: utf-8 -*-
import re
from datetime import date
from typing import Iterable
from typing import List
from typing import Mapping
from typing import Union

from ._version import __version__
from .cconstructs import CExtendedLiteral
from .cconstructs import Enum
from .cconstructs import Function
from .cconstructs import generate_c_value_initializer
from .cconstructs import Struct
from .cconstructs import Variable
from .cconstructs import VariableValue
from .codewriterlite import CodeWriterLite
from .utils import assure_str

SOURCE_URL = "https://gitlab.com/andrejr/csnake"
PYPI_URL = "https://pypi.org/project/csnake"


class DefStackEmptyError(IndexError):
    "Ended a if[n]def that wasn't started earlier. To ignore, use \
    ignore_ifdef_stack=True as parameter"


class SwitchStackEmptyError(IndexError):
    "Ended a switch that wasn't started earlier. To ignore, use \
    ignore_switch_stack=True as parameter"


[docs]class CodeWriter(CodeWriterLite): """Container class for C code, with methods to add C constructs. :class:`CodeWriter` (abbreviated `CW` or `cw`) is a code container that allows you to add code (both manually written and generated), access it as a :obj:`str`, iterate over the lines of code and output the code to a file. It also helps you to indent and dedent code, open or close code blocks consistently. :class:`CodeWriter` is iterable (iterates over lines of code), and also has methods like :meth:`__contains__` and :meth:`__len__`. Here's an overview of abc's :class:`CodeWriter` is a virtual subclass of: >>> from csnake import CodeWriter >>> from collections.abc import Container, Iterable, Sized, Collection >>> cwr = CodeWriter() >>> assert isinstance(cwr, Container) >>> assert isinstance(cwr, Iterable) >>> assert isinstance(cwr, Sized) >>> assert isinstance(cwr, Collection) Args: lf: linefeed character used to separate lines of code when rendering code with :func:`str` or :attr:`code`. indent: indentation unit string or :class:`int` number of spaces, used to indent code. Attributes: lf(str): linefeed character used to separate lines of code when rendering code with :func:`str` or :attr:`code`. lines(list(str)): lines of code. """ __slots__ = ("_def_stack", "_switch_stack") _CPP = "__cplusplus" def __init__(self, lf: str = None, indent: Union[int, str] = 4) -> None: super().__init__(lf, indent) # initialize values self._def_stack: List[str] = [] self._switch_stack: List[str] = []
[docs] def add_autogen_comment(self, source_file_name: str = None) -> None: """Add a comment denoting the code was autogenerated by a script. File name of the script the file is generated from is optional. Args: source_file_name: name of the script the code was generated from Examples: Without a script file name: >>> from csnake import CodeWriter >>> cwr1 = CodeWriter() >>> cwr1.add_autogen_comment() >>> print(cwr1) # doctest: +SKIP /* * This file was automatically generated using csnake v0.2.1. * * This file should not be edited directly, any changes will be * overwritten next time the script is run. * * Source code for csnake is available at: * https://gitlab.com/andrejr/csnake */ With a script name: >>> from csnake import CodeWriter >>> cwr2 = CodeWriter() >>> cwr2.add_autogen_comment('test.py') >>> print(cwr2) # doctest: +SKIP /* * This file was automatically generated using csnake v0.2.1. * * This file should not be edited directly, any changes will be * overwritten next time the script is run. * Make any changes to the file 'test.py', not this file. * * Source code for csnake is available at: * https://gitlab.com/andrejr/csnake */ """ self.start_comment() self.add_line( f"This file was automatically generated using csnake v{__version__}." ) self.add_line() self.add_lines( ( "This file should not be edited directly, any changes will be", "overwritten next time the script is run.", ) ) if source_file_name: source_file_name = assure_str(source_file_name) if source_file_name: self.add_line( f"Make any changes to the file '{source_file_name}', not this file." ) self.add_line() self.add_line("Source code for csnake is available at:") self.add_line(SOURCE_URL) if PYPI_URL: self.add_line() self.add_line("csnake is also available on PyPI, at :") self.add_line(f"{PYPI_URL}") self.end_comment()
[docs] def add_license_comment( self, license_: str, authors: Iterable[Mapping[str, str]] = None, intro: str = None, year: int = None, ) -> None: """Add a comment with the license and authors. The comment also adds the year to the copyright. Args: license\\_: :obj:`str` containing the text of the license authors: optional iterable containing mappings (dict-likes, one per author) with key-value pairs for keys 'name' (author's name) and email (author's email, optional) intro: introductory text added before the author list, optional year: year of the copyright (optional). If it is left out, current year is assumed. Raises: ValueError: if any of the arguments is of wrong type Examples: Just license: >>> from csnake import CodeWriter >>> license_text = 'license\\ntext\\nlines' >>> cw1 = CodeWriter() >>> cw1.add_license_comment(license_text) >>> print(cw1) /* * license * text * lines */ With introduction: >>> intro_text = 'intro\\ntext' >>> cw2 = CodeWriter() >>> cw2.add_license_comment(license_text, intro=intro_text) >>> print(cw2) /* * intro * text * * license * text * lines */ With authors (and year; year defaults to current year): >>> authors = [ ... {'name': 'Author Surname'}, ... {'name': 'Other Surname', 'email': 'test@email'}, ... ] >>> cw3 = CodeWriter() >>> cw3.add_license_comment( ... license_text, authors=authors, intro=intro_text, year=2019 ... ) >>> print(cw3) /* * intro * text * * Copyright © 2019 Author Surname * Copyright © 2019 Other Surname <test@email> * * license * text * lines */ """ self.start_comment() if intro: for line in intro.splitlines(): if line == "": self.add_line() else: self.add_line(line) self.add_line() if not year: year = date.today().year else: year = int(year) if authors: for author in authors: self.add_line( "Copyright © {year} {name}{email}".format( year=year, name=assure_str(author["name"]), email=" <{}>".format(assure_str(author["email"])) if author.get("email", None) else "", ) ) self.add_line() if not isinstance(license_, str): raise TypeError("license_ must be a string.") for line in license_.splitlines(): self.add_line(line) self.end_comment()
[docs] def add_define( self, name: str, value: Union[CExtendedLiteral, VariableValue] = None, comment: str = None, ) -> None: """Add a define directive for macros. Macros may or may not have a value. Args: name: macro name value: literal or variable assigned to the macro (optional) comment: comment accompanying macro definition Examples: >>> from csnake import CodeWriter, Variable, Subscript >>> cwr = CodeWriter() >>> cwr.add_define('PI', 3.14) >>> cwr.add_define('LOG') >>> somearr = Variable('some_array', 'int', value=range(0, 5)) >>> cwr.add_define('DEST', Subscript(somearr, 2)) >>> print(cwr) #define PI 3.14 #define LOG #define DEST some_array[2] Todo: - Make a class (within the :mod:`cconstructs`) representing a macro, so that object-type defines may be used as initializers. - Add support for function-like macros. """ name = assure_str(name) line = "#define {name}{value}".format( name=str(name), value=" " + generate_c_value_initializer(value) if value else "", ) self.add_line(line, comment=comment, ignore_indent=True)
[docs] def start_if_def( self, define: str, invert: bool = False, comment: str = None ) -> None: """ Start an :ccode:`#ifdef` or :ccode:`#ifndef` (preprocessor) block. :ccode:`#ifdef` (or :ccode:`#ifndef`) blocks can be nested. :ccode:`#endif` always ends the innermost block. :ccode:`endif` statements are added by :meth:`end_if_def`. Args: define: name of the macro whose existence we're checking. invert: (optional) whether this block is an :ccode:`#ifndef` (:code:`True`) or :ccode:`#ifdef` (:code:`False`, default). comment: (optional) comment accompanying the statement. Raises: ValueError: if one of the arguments is of the wrong type. Examples: :meth:`start_if_def` and :meth:`end_if_def` in action, including nested ifdefs: >>> from csnake import CodeWriter >>> cwr = CodeWriter() >>> cwr.start_if_def('DEBUG') >>> cwr.start_if_def('ARM', invert=True) >>> cwr.add_define('LOG') >>> cwr.end_if_def() >>> cwr.end_if_def() >>> print(cwr) #ifdef DEBUG #ifndef ARM #define LOG #endif /* ARM */ #endif /* DEBUG */ """ self._def_stack.append(define) define = assure_str(define) if invert: self.add_line( f"#ifndef {define}", comment=comment, ignore_indent=True ) else: self.add_line( f"#ifdef {define}", comment=comment, ignore_indent=True )
[docs] def end_if_def(self, ignore_ifdef_stack: bool = False) -> None: """ Insert an :ccode:`#endif` to end a :ccode:`#ifdef` (preprocessor) block. :ccode:`#ifdef` (or :ccode:`#ifndef`) blocks can be nested. :ccode:`#endif` always ends the innermost block. :ccode:`endif` statements are added by :meth:`end_if_def`. Args: ignore_ifdef_stack: (optional) don't throw an exception :ccode:`#endif` if is unmatched. Raises: ValueError: if one of the arguments is of the wrong type. DefStackEmptyError: if there isn't a matching :code:`#ifdef` and :obj:`ignore_ifdef_stack` isn't set. Examples: See :meth:`start_if_def`. """ try: def_name = self._def_stack.pop() except IndexError as e: if ignore_ifdef_stack: def_name = "" else: raise DefStackEmptyError from e self.add_line("#endif", comment=def_name, ignore_indent=True)
[docs] def cpp_entry(self) -> None: """Start a conditional :ccode:`extern "C"` for use CPP compilers. Examples: :meth:`cpp_entry` and :meth:`cpp_exit` in action: >>> from csnake import CodeWriter >>> cwr = CodeWriter() >>> cwr.cpp_entry() >>> cwr.add_line('some_code();') >>> cwr.cpp_exit() >>> print(cwr) #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ some_code(); #ifdef __cplusplus } #endif /* __cplusplus */ """ self.start_if_def(self._CPP) self.add_line('extern "C" {', ignore_indent=True) self.end_if_def()
[docs] def cpp_exit(self) -> None: """End a conditional :ccode:`extern "C"` for use CPP compilers. Examples: See :meth:`cpp_entry`. """ self.start_if_def(self._CPP) self.add_line("}", ignore_indent=True) self.end_if_def()
[docs] def start_switch( self, switch: Union[CExtendedLiteral, VariableValue] ) -> None: """Start a switch statement. Used with :meth:`end_switch`, :meth:`add_switch_case`, :meth:`add_switch_default`, :meth:`add_switch_break`, :meth:`add_switch_return`, to form C switch statements. Switch statements can be nested, so :meth:`end_switch` closes the innermost switch. Args: switch: literal or variable the choice depends on. Examples: Demonstration of switch-related methods: >>> from csnake import CodeWriter, Variable >>> cw = CodeWriter() >>> var = Variable("somevar", "int") >>> case_var = Variable("case_var", "int") >>> cw.start_switch(var) >>> cw.add_switch_case(2) >>> cw.add_line('do_something();') >>> cw.add_switch_break() >>> cw.add_switch_case(case_var) >>> cw.add_switch_return(5) >>> cw.add_switch_default() >>> cw.add_switch_return(8) >>> cw.end_switch() >>> print(cw) switch (somevar) { case 2: do_something(); break; case case_var: return 5; default: return 8; } /* ~switch (somevar) */ """ switch_str = generate_c_value_initializer(switch) self._switch_stack.append(switch_str) self.add_line(f"switch ({switch_str})") self.open_brace()
[docs] def end_switch(self, ignore_switch_stack: bool = False) -> None: """End a switch statement. Used with :meth:`start_switch`, :meth:`add_switch_case`, :meth:`add_switch_default`, :meth:`add_switch_break`, :meth:`add_switch_return`, to form C switch statements. Switch statements can be nested, so :meth:`end_switch` closes the innermost switch. Args: ignore_switch_stack: don't throw an exception if a switch start is missing Raises: SwitchStackEmptyError: if :meth:`end_switch` is called outside of a switch statement, and :obj:`ignore_switch_stack` is :code:`False`. Examples: See :meth:`start_switch`. """ self.close_brace() try: switch_name = self._switch_stack.pop() except IndexError as e: if ignore_switch_stack: switch_name = "" else: raise SwitchStackEmptyError from e else: switch_name = switch_name self.add(f" /* ~switch ({switch_name}) */")
[docs] def add_switch_case( self, case=Union[CExtendedLiteral, VariableValue], comment: str = None ) -> None: """Add a switch case statement. Used with :meth:`start_switch`, :meth:`end_switch`, :meth:`add_switch_default`, :meth:`add_switch_break`, :meth:`add_switch_return`, to form C switch statements. Args: case: literal or variable representing the current case's value comment: accompanying inline comment Examples: See :meth:`start_switch`. """ self.add_line( f"case {generate_c_value_initializer(case)}:", comment=comment ) self.indent()
[docs] def add_switch_default(self, comment: str = None) -> None: """Add a switch default statement. Used with :meth:`start_switch`, :meth:`end_switch`, :meth:`add_switch_case`, :meth:`add_switch_break`, :meth:`add_switch_return`, to form C switch statements. Switch statements can be nested, so :meth:`end_switch` closes the innermost switch. Examples: See :meth:`start_switch`. """ self.add_line("default:", comment=comment) self.indent()
[docs] def add_switch_break(self) -> None: """Break a switch case. Used with :meth:`start_switch`, :meth:`end_switch`, :meth:`add_switch_case`, :meth:`add_switch_default`, :meth:`add_switch_return`, to form C switch statements. Examples: See :meth:`start_switch`. """ self.add_line("break;") self.dedent()
[docs] def add_switch_return( self, value: Union[CExtendedLiteral, VariableValue] = None ) -> None: """Return inside of a switch statement Used with :meth:`start_switch`, :meth:`end_switch`, :meth:`add_switch_case`, :meth:`add_switch_default`, :meth:`add_switch_break`, to form C switch statements. Args: value: literal or variable representing the value to return Examples: See :meth:`start_switch`. """ self.add_line( "return{val};".format( val=" " + generate_c_value_initializer(value) if value else "" ) ) self.dedent()
[docs] def include(self, name: str, comment: str = None) -> None: """Add an :ccode:`#include` directive. System headers should be surrounded with brackets `(<>)`, while local headers may or may not be surrounded with quotation marks `("")` (the resulting code will have quotation marks surrounding the header's name) Args: name: name of header to include, with or without brackets/quotes. If no brackets/quotes surround the name, quotes are used by default. comment: accompanying inline comment Examples: All types of includes. >>> from csnake import CodeWriter >>> cw = CodeWriter() >>> cw.include('"some_local_header.h"') >>> cw.include('other_local_header.h') >>> cw.include("<string.h>") >>> print(cw) #include "some_local_header.h" #include "other_local_header.h" #include <string.h> """ name = str(name) if re.search(r'^(<.*>|".*")$', name): pass else: name = f'"{name}"' self.add_line( "#include {name}".format(name=name), comment=comment, ignore_indent=True, )
[docs] def add_enum(self, enum: Enum) -> None: """Add an enumeration definition. Args: enum: enum in question See Also: :class:`Enum` for details on the `enum` class Examples: >>> from csnake import ( ... CodeWriter, Enum, Variable, Dereference, AddressOf ... ) >>> cw = CodeWriter() >>> name = "somename" >>> pfx = "pfx" >>> typedef = False >>> enum = Enum(name, prefix=pfx, typedef=typedef) >>> cval1 = Variable("varname", "int") >>> enum.add_value("val1", 1) >>> enum.add_value("val2", Dereference(1000)) >>> enum.add_value("val3", cval1) >>> enum.add_value("val4", AddressOf(cval1), "some comment") >>> cw.add_enum(enum) >>> print(cw) enum somename { pfxval1 = 1, pfxval2 = *1000, pfxval3 = varname, pfxval4 = &varname /* some comment */ }; """ if not isinstance(enum, Enum): raise TypeError('enum must be of type "Enum"') self.add_lines(enum.generate_declaration(self._indent_unit).lines)
[docs] def add_variable_declaration( self, variable: Variable, extern: bool = False ) -> None: """Add a variable's declaration. Args: variable: variable in question extern: wheter to add the :ccode:`extern` qualifier to the declaration (`True`) or not (`False`, default) See Also: :class:`Variable` for details on the `Variable` class Examples: >>> import numpy as np >>> from csnake import CodeWriter, Variable >>> cw = CodeWriter() >>> var = Variable( ... "test", ... primitive="int", ... value=np.arange(24).reshape((2, 3, 4)) ... ) >>> cw.add_variable_declaration(var) >>> print(cw) int test[2][3][4]; """ if not isinstance(variable, Variable): raise TypeError("variable must be of type 'Variable'") self.add_line( variable.generate_declaration(extern) + ";", comment=variable.comment, )
[docs] def add_variable_initialization(self, variable: Variable) -> None: """Add a variable's initialization. Args: variable: variable in question See Also: :class:`Variable` for details on the `Variable` class Example: >>> import numpy as np >>> from csnake import CodeWriter, Variable >>> cw = CodeWriter() >>> var = Variable( ... "test", ... primitive="int", ... value=np.arange(24).reshape((2, 3, 4)) ... ) >>> cw.add_variable_initialization(var) >>> print(cw) int test[2][3][4] = { { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }, { {12, 13, 14, 15}, {16, 17, 18, 19}, {20, 21, 22, 23} } }; """ if not isinstance(variable, Variable): raise TypeError("variable must be of type 'Variable'") init_cwr = variable.generate_initialization(self._indent_unit) assert isinstance(init_cwr, Iterable) self.add_lines(init_cwr)
[docs] def add_struct(self, struct: Struct) -> None: """Add a struct declaration. Args: struct: struct in question See Also: :class:`Struct` for details on the `Struct` class. Example: >>> from csnake import CodeWriter, Variable, Struct >>> cw = CodeWriter() >>> strct = Struct("strname", typedef=False) >>> var1 = Variable("var1", "int") >>> var2 = Variable("var2", "int", value=range(10)) >>> strct.add_variable(var1) >>> strct.add_variable(var2) >>> strct.add_variable(("var3", "int")) >>> strct.add_variable({"name": "var4", "primitive": "int"}) >>> cw.add_struct(strct) >>> print(cw) struct strname { int var1; int var2[10]; int var3; int var4; }; """ if not isinstance(struct, Struct): raise TypeError("struct must be of type 'Struct'") declaration = struct.generate_declaration(indent=self._indent_unit) assert isinstance(declaration, Iterable) # mypy self.add_lines(declaration)
[docs] def add_function_prototype( self, func: Function, extern: bool = False, comment: str = None ) -> None: """Add a functions's prototype. Args: func: function in question extern: wheter to add the :ccode:`extern` qualifier to the prototype (`True`) or not (`False`, default) comment: accompanying inline comment See Also: :class:`Function` for details on the `Function` class Examples: >>> from csnake import CodeWriter, Variable, Function >>> cw = CodeWriter() >>> arg1 = Variable("arg1", "int") >>> arg2 = Variable("arg2", "int", value=range(10)) >>> arg3 = ("arg3", "int") >>> arg4 = {"name": "arg4", "primitive": "int"} >>> fun = Function( ... "testfunct", "void", arguments=(arg1, arg2, arg3, arg4) ... ) >>> fun.add_code(("code;", "more_code;")) >>> cw.add_function_prototype(fun) >>> print(cw) void testfunct(int arg1, int arg2[10], int arg3, int arg4); """ if not isinstance(func, Function): raise TypeError("func must be of type 'Function'") self.add_line(func.generate_prototype(extern) + ";", comment=comment)
[docs] def add_function_definition(self, func: Function) -> None: """Add a functions's definition / implementation. Args: func: function in question See Also: :class:`Function` for details on the `Function` class Examples: >>> from csnake import CodeWriter, Variable, Function >>> cw = CodeWriter() >>> arg1 = Variable("arg1", "int") >>> arg2 = Variable("arg2", "int", value=range(10)) >>> arg3 = ("arg3", "int") >>> arg4 = {"name": "arg4", "primitive": "int"} >>> fun = Function( ... "testfunct", "void", arguments=(arg1, arg2, arg3, arg4) ... ) >>> fun.add_code(("code;", "more_code;")) >>> cw.add_function_definition(fun) >>> print(cw) void testfunct(int arg1, int arg2[10], int arg3, int arg4) { code; more_code; } """ if not isinstance(func, Function): raise TypeError("Argument func must be of type 'Function'") definition = func.generate_definition(self._indent_unit) assert isinstance(definition, Iterable) # mypy self.add_lines(definition)
[docs] def add_function_call(self, func: Function, *arg) -> None: """Add a call to a function with listed arguments. Args: func: function in question \\*arg: (rest of the args) function's args in sequence See Also: :class:`Function` for details on the `Function` class Examples: >>> from csnake import CodeWriter, Variable, Function >>> cw = CodeWriter() >>> arg1 = Variable("arg1", "int") >>> arg2 = Variable("arg2", "int", value=range(10)) >>> arg3 = ("arg3", "int") >>> arg4 = {"name": "arg4", "primitive": "int"} >>> fun = Function( ... "testfunct", "void", arguments=(arg1, arg2, arg3, arg4) ... ) >>> fun.add_code(("code;", "more_code;")) >>> cw.add_function_call(fun, 1, 2, 3, 4) >>> print(cw) testfunct(1, 2, 3, 4); """ if not isinstance(func, Function): raise TypeError("func must be of type 'Function'") self.add_line(func.generate_call(*arg) + ";")
[docs] def write_to_file(self, filename: str) -> None: """Write code to filename. Args: filename: name of the file to write code into """ with open(filename, "w") as openfile: openfile.write(self.code)