# -*- 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_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)