Source code for csnake.codewriter

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

from ._version import __version__
from .cconstructs import CExtendedLiteral
from .cconstructs import Enum
from .cconstructs import Function
from .cconstructs import Struct
from .cconstructs import Variable
from .cconstructs import VariableValue
from .cconstructs import generate_c_value_initializer
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: Optional[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: Optional[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!r}, 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: Optional[Iterable[Mapping[str, str]]] = None, intro: Optional[str] = None, year: Optional[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() year = int(year or date.today().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: Optional[Union[CExtendedLiteral, VariableValue]] = None, comment: Optional[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 is not None else "", ) self.add_line(line, comment=comment, ignore_indent=True)
[docs] def start_if_def( self, define: str, invert: bool = False, comment: Optional[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 self.add(f" /* ~switch ({switch_name}) */")
[docs] def add_switch_case( self, case=Union[CExtendedLiteral, VariableValue], comment: Optional[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: Optional[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: Optional[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: Optional[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: Optional[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: Union[str, Path]) -> None: """Write code to filename. Args: filename: name of the file to write code into """ with Path(filename).open("w") as openfile: openfile.write(self.code)