Skip to content
Snippets Groups Projects
Commit fcfb59f1 authored by Frederik Hennig's avatar Frederik Hennig
Browse files

Introduce CppTypeFactory. Extend documentation on cpptype. Write user guide on C++ API modelling.

parent 719c6daa
No related branches found
No related tags found
1 merge request!12Improve versatility and robustness of `cpptype`, and document it in the user guide
Pipeline #71947 passed
...@@ -3,13 +3,15 @@ from os import path ...@@ -3,13 +3,15 @@ from os import path
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def prepare_composer(doctest_namespace): def prepare_doctest_namespace(doctest_namespace):
from pystencilssfg import SfgContext, SfgComposer from pystencilssfg import SfgContext, SfgComposer
from pystencilssfg import lang
# Place a composer object in the environment for doctests # Place a composer object in the environment for doctests
sfg = SfgComposer(SfgContext()) sfg = SfgComposer(SfgContext())
doctest_namespace["sfg"] = sfg doctest_namespace["sfg"] = sfg
doctest_namespace["lang"] = lang
DATA_DIR = path.join(path.split(__file__)[0], "tests/data") DATA_DIR = path.join(path.split(__file__)[0], "tests/data")
......
...@@ -24,7 +24,7 @@ html_title = f"pystencils-sfg v{version} Documentation" ...@@ -24,7 +24,7 @@ html_title = f"pystencils-sfg v{version} Documentation"
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [ extensions = [
"myst_parser", "myst_nb",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.napoleon", "sphinx.ext.napoleon",
"sphinx.ext.autosummary", "sphinx.ext.autosummary",
...@@ -37,16 +37,8 @@ extensions = [ ...@@ -37,16 +37,8 @@ extensions = [
templates_path = ["_templates"] templates_path = ["_templates"]
exclude_patterns = [] exclude_patterns = []
source_suffix = {
".rst": "restructuredtext",
".md": "markdown",
}
master_doc = "index" master_doc = "index"
nitpicky = True nitpicky = True
myst_enable_extensions = [
"colon_fence",
"dollarmath"
]
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
...@@ -89,6 +81,15 @@ sfg = SfgComposer(SfgContext()) ...@@ -89,6 +81,15 @@ sfg = SfgComposer(SfgContext())
''' '''
# -- Options for MyST / MyST-NB ----------------------------------------------
nb_execution_mode = "cache" # do not execute notebooks by default
myst_enable_extensions = [
"dollarmath",
"colon_fence",
]
# Prepare code generation examples # Prepare code generation examples
def build_examples(): def build_examples():
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
:caption: User Guide :caption: User Guide
usage/generator_scripts usage/generator_scripts
C++ API Modelling <usage/api_modelling>
usage/project_integration usage/project_integration
usage/tips_n_tricks usage/tips_n_tricks
``` ```
......
---
file_format: mystnb
kernelspec:
name: python3
---
# Modelling C++ APIs in pystencils-sfg
Pystencils-SFG is designed to help you generate C++ code that interfaces with pystencils on the one side,
and with your handwritten code on the other side.
This requires that the C++ classes and APIs of your framework or application be represented within the SFG system.
This guide shows how you can use the facilities of the {any}`pystencilssfg.lang` module to model your C++ interfaces
for use with the code generator.
To begin, import the `lang` module:
```{code-cell} ipython3
from pystencilssfg import lang
```
## Defining C++ Types and Type Templates
The first C++ entities that need to be mirrored for the SFGs are the types and type templates a library
or application uses or exposes.
### Non-Templated Types
To define a C++ type, we use {any}`pystencilssfg.lang.cpptype <pystencilssfg.lang.types.cpptype>`:
```{code-cell} ipython3
MyClassTypeFactory = lang.cpptype("my_namespace::MyClass", "MyClass.hpp")
MyClassTypeFactory
```
This defines two properties of the type: its fully qualified name, and the set of headers
that need to be included when working with the type.
Now, whenever this type occurs as the type of a variable given to pystencils-sfg,
the code generator will make sure that `MyClass.hpp` is included into the respective
generated code file.
The object returned by `cpptype` is not the type itself, but a factory for instances of the type.
Even as `MyClass` does not have any template parameters, we can create different instances of it:
`const` and non-`const`, as well as references and non-references.
We do this by calling the factory:
```{code-cell} ipython3
MyClass = MyClassTypeFactory()
str(MyClass)
```
To produce a `const`-qualified version of the type:
```{code-cell} ipython3
MyClassConst = MyClassTypeFactory(const=True)
str(MyClassConst)
```
And finally, to produce a reference instead:
```{code-cell} ipython3
MyClassRef = MyClassTypeFactory(ref=True)
str(MyClassRef)
```
Of course, `const` and `ref` can also be combined to create a reference-to-const.
### Types with Template Parameters
We can add template parameters to our type by the use of
[Python format strings](https://docs.python.org/3/library/string.html#formatstrings):
```{code-cell} ipython3
MyClassTemplate = lang.cpptype("my_namespace::MyClass< {T1}, {T2} >", "MyClass.hpp")
MyClassTemplate
```
Here, the type parameters `T1` and `T2` are specified in braces.
For them, values must be provided when calling the factory to instantiate the type:
```{code-cell} ipython3
MyClassIntDouble = MyClassTemplate(T1="int", T2="double")
str(MyClassIntDouble)
```
The way type parameters are passed to the factory is identical to the behavior of {any}`str.format`,
except that it does not support attribute or element accesses.
In particular, this means that we can also use unnamed, implicit positional parameters:
```{code-cell} ipython3
MyClassTemplate = lang.cpptype("my_namespace::MyClass< {}, {} >", "MyClass.hpp")
MyClassIntDouble = MyClassTemplate("int", "double")
str(MyClassIntDouble)
```
\ No newline at end of file
...@@ -40,7 +40,7 @@ docs = [ ...@@ -40,7 +40,7 @@ docs = [
"sphinx", "sphinx",
"pydata-sphinx-theme==0.15.4", "pydata-sphinx-theme==0.15.4",
"sphinx-book-theme==1.1.3", # workaround for https://github.com/executablebooks/sphinx-book-theme/issues/865 "sphinx-book-theme==1.1.3", # workaround for https://github.com/executablebooks/sphinx-book-theme/issues/865
"myst-parser", "myst-nb",
"sphinx_design", "sphinx_design",
"sphinx_autodoc_typehints", "sphinx_autodoc_typehints",
"sphinx-copybutton", "sphinx-copybutton",
......
from __future__ import annotations from __future__ import annotations
from typing import Any, Iterable, Sequence, Mapping, Callable from typing import Any, Iterable, Sequence, Mapping, TypeVar, Generic
from abc import ABC from abc import ABC
from dataclasses import dataclass from dataclasses import dataclass
from itertools import chain from itertools import chain
...@@ -105,9 +105,87 @@ class CppType(PsCustomType, ABC): ...@@ -105,9 +105,87 @@ class CppType(PsCustomType, ABC):
return set(str(h) for h in self.class_includes) return set(str(h) for h in self.class_includes)
TypeClass_T = TypeVar("TypeClass_T", bound=CppType)
"""Python type variable bound to `CppType`."""
class CppTypeFactory(Generic[TypeClass_T]):
"""Type Factory returned by `cpptype`."""
def __init__(self, tclass: type[TypeClass_T]) -> None:
self._type_class = tclass
@property
def includes(self) -> frozenset[HeaderFile]:
"""Set of headers required by this factory's type"""
return self._type_class.class_includes
@property
def template_string(self) -> str:
"""Template string of this factory's type"""
return self._type_class.template_string
def __str__(self) -> str:
return f"Factory for {self.template_string}` defined in {self.includes}"
def __repr__(self) -> str:
return f"CppTypeFactory({self.template_string}, includes={{ {', '.join(str(i) for i in self.includes)} }})"
def __call__(self, *args, ref: bool = False, **kwargs) -> TypeClass_T | Ref:
"""Create a type object of this factory's C++ type template.
Args:
args, kwargs: Positional and keyword arguments are forwarded to the template string formatter
ref: If ``True``, return a reference type
Returns:
An instantiated type object
"""
obj = self._type_class(*args, **kwargs)
if ref:
return Ref(obj)
else:
return obj
def cpptype( def cpptype(
typestr: str, include: str | HeaderFile | Iterable[str | HeaderFile] = () template_str: str, include: str | HeaderFile | Iterable[str | HeaderFile] = ()
) -> Callable[..., CppType | Ref]: ) -> CppTypeFactory:
"""Describe a C++ type template, associated with a set of required header files.
This function allows users to define C++ type templates using
`Python format string syntax <https://docs.python.org/3/library/string.html#formatstrings>`_.
The types may furthermore be annotated with a set of header files that must be included
in order to use the type.
>>> opt_template = lang.cpptype("std::optional< {T} >", "<optional>")
>>> opt_template.template_string
'std::optional< {T} >'
This function returns a `CppTypeFactory` object, which in turn can be called to create
an instance of the C++ type template.
Therein, the ``template_str`` argument is treated as a Python format string:
The positional and keyword arguments passed to the returned type factory are passed
through machinery that is based on `str.format` to produce the actual type name.
>>> int_option = opt_template(T="int")
>>> int_option.c_string().strip()
'std::optional< int >'
The factory may also create reference types when the ``ref=True`` is specified.
>>> int_option_ref = opt_template(T="int", ref=True)
>>> int_option_ref.c_string().strip()
'std::optional< int >&'
Args:
template_str: Format string defining the type template
include: Either the name of a header file, or a sequence of names of header files
Returns:
CppTypeFactory: A factory used to instantiate the type template
"""
headers: list[str | HeaderFile] headers: list[str | HeaderFile]
if isinstance(include, (str, HeaderFile)): if isinstance(include, (str, HeaderFile)):
...@@ -118,17 +196,10 @@ def cpptype( ...@@ -118,17 +196,10 @@ def cpptype(
headers = list(include) headers = list(include)
class TypeClass(CppType): class TypeClass(CppType):
template_string = typestr template_string = template_str
class_includes = frozenset(HeaderFile.parse(h) for h in headers) class_includes = frozenset(HeaderFile.parse(h) for h in headers)
def factory(*args, ref: bool = False, **kwargs): return CppTypeFactory[TypeClass](TypeClass)
obj = TypeClass(*args, **kwargs)
if ref:
return Ref(obj)
else:
return obj
return staticmethod(factory)
class Ref(PsType): class Ref(PsType):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment