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