Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 746 additions and 476 deletions
......@@ -4,13 +4,8 @@ from os import path
from argparse import ArgumentParser, BooleanOptionalAction
from .configuration import (
SfgConfigException,
SfgConfigSource,
add_config_args_to_parser,
config_from_parser_args,
merge_configurations,
)
from .config import CommandLineParameters, SfgConfigException, OutputMode
from .emission import OutputSpec
def add_newline_arg(parser):
......@@ -39,7 +34,7 @@ def cli_main(program="sfg-cli"):
)
outfiles_parser.set_defaults(func=list_files)
add_config_args_to_parser(outfiles_parser)
CommandLineParameters.add_args_to_parser(outfiles_parser)
add_newline_arg(outfiles_parser)
outfiles_parser.add_argument(
"--sep", type=str, default=" ", dest="sep", help="Separator for list items"
......@@ -79,21 +74,18 @@ def version(args):
def list_files(args):
try:
project_config, cmdline_config = config_from_parser_args(args)
except SfgConfigException as exc:
abort_with_config_exception(exc)
config = merge_configurations(project_config, cmdline_config, None)
cli_params = CommandLineParameters(args)
config = cli_params.get_config()
_, scriptname = path.split(args.codegen_script)
basename = path.splitext(scriptname)[0]
from .emission import HeaderImplPairEmitter
emitter = HeaderImplPairEmitter(config.get_output_spec(basename))
output_spec = OutputSpec.create(config, basename)
output_files = [output_spec.get_header_filepath()]
if config.output_mode != OutputMode.HEADER_ONLY:
output_files.append(output_spec.get_impl_filepath())
print(args.sep.join(emitter.output_files), end=os.linesep if args.newline else "")
print(args.sep.join(output_files), end=os.linesep if args.newline else "")
exit(0)
......@@ -112,18 +104,6 @@ def make_cmake_find_module(args):
exit(0)
def abort_with_config_exception(exception: SfgConfigException):
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
match exception.config_source:
case SfgConfigSource.PROJECT:
eprint(
f"Invalid project configuration: {exception.message}\nCheck your configurator script."
)
case SfgConfigSource.COMMANDLINE:
eprint(f"Invalid configuration on command line: {exception.message}")
case _:
assert False, "(Theoretically) unreachable code. Contact the developers."
def abort_with_config_exception(exception: SfgConfigException, source: str):
print(f"Invalid {source} configuration: {exception.args[0]}.", file=sys.stderr)
exit(1)
......@@ -54,11 +54,22 @@ function(pystencilssfg_generate_target_sources TARGET)
endif()
if(DEFINED PystencilsSfg_CONFIGURATOR_SCRIPT)
message(AUTHOR_WARNING "The variable PystencilsSfg_CONFIGURATOR_SCRIPT is deprecated. Set PystencilsSfg_CONFIG_MODULE instead.")
cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIGURATOR_SCRIPT OUTPUT_VARIABLE configscript)
list(APPEND generatorArgs "--sfg-config-module=${configscript}")
list(APPEND _pssfg_DEPENDS ${configscript})
endif()
if(DEFINED PystencilsSfg_CONFIG_MODULE)
if(DEFINED PystencilsSfg_CONFIGURATOR_SCRIPT)
message(FATAL_ERROR "At most one of PystencilsSfg_CONFIGURATOR_SCRIPT and PystencilsSfg_CONFIG_MODULE may be set.")
endif()
cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIG_MODULE OUTPUT_VARIABLE config_module)
list(APPEND generatorArgs "--sfg-config-module=${config_module}")
list(APPEND _pssfg_DEPENDS ${config_module})
endif()
if(DEFINED _pssfg_FILE_EXTENSIONS)
string(JOIN "," extensionsString ${_pssfg_FILE_EXTENSIONS})
......
......@@ -319,6 +319,7 @@ class SfgBasicComposer(SfgIComposer):
return SfgFunctionParams([x.as_variable() for x in args])
def require(self, *includes: str | SfgHeaderInclude) -> SfgRequireIncludes:
"""Use inside a function body to require the inclusion of headers."""
return SfgRequireIncludes(
list(SfgHeaderInclude.parse(incl) for incl in includes)
)
......@@ -443,6 +444,7 @@ class SfgBasicComposer(SfgIComposer):
return SfgBranchBuilder()
def switch(self, switch_arg: ExprLike) -> SfgSwitchBuilder:
"""Use inside a function to construct a switch-case statement."""
return SfgSwitchBuilder(switch_arg)
def map_field(
......
from __future__ import annotations
from argparse import ArgumentParser
from types import ModuleType
from typing import Generic, TypeVar, Callable, Any, Sequence
from abc import ABC
from dataclasses import dataclass, fields, field
from enum import Enum, auto
from os import path
from importlib import util as iutil
class SfgConfigException(Exception): ... # noqa: E701
Option_T = TypeVar("Option_T")
class Option(Generic[Option_T]):
"""Option descriptor.
This descriptor is used to model configuration options.
It maintains a default value for the option that is used when no value
was specified by the user.
In configuration options, the value `None` stands for `unset`.
It can therefore not be used to set an option to the meaning "not any", or "empty"
- for these, special values need to be used.
"""
def __init__(
self,
default: Option_T | None = None,
validator: Callable[[Any, Option_T | None], Option_T | None] | None = None,
) -> None:
self._default = default
self._validator = validator
self._name: str
self._lookup: str
def validate(self, validator: Callable[[Any, Any], Any] | None):
self._validator = validator
return validator
@property
def default(self) -> Option_T | None:
return self._default
def get(self, obj) -> Option_T | None:
val = getattr(obj, self._lookup, None)
if val is None:
return self._default
else:
return val
def __set_name__(self, owner, name: str):
self._name = name
self._lookup = f"_{name}"
def __get__(self, obj, objtype=None) -> Option_T | None:
if obj is None:
return None
return getattr(obj, self._lookup, None)
def __set__(self, obj, value: Option_T | None):
if self._validator is not None:
value = self._validator(obj, value)
setattr(obj, self._lookup, value)
def __delete__(self, obj):
delattr(obj, self._lookup)
class ConfigBase(ABC):
def get_option(self, name: str) -> Any:
"""Get the value set for the specified option, or the option's default value if none has been set."""
descr: Option = type(self).__dict__[name]
return descr.get(self)
def override(self, other: ConfigBase):
for f in fields(self): # type: ignore
fvalue = getattr(self, f.name)
if isinstance(fvalue, ConfigBase): # type: ignore
fvalue.override(getattr(other, f.name))
else:
new_val = getattr(other, f.name)
if new_val is not None:
setattr(self, f.name, new_val)
@dataclass
class FileExtensions(ConfigBase):
"""Option category containing output file extensions."""
header: Option[str] = Option("hpp")
"""File extension for generated header file."""
impl: Option[str] = Option()
"""File extension for generated implementation file."""
@header.validate
@impl.validate
def _validate_extension(self, ext: str | None) -> str | None:
if ext is not None and ext[0] == ".":
return ext[1:]
return ext
class OutputMode(Enum):
"""Output mode of the source file generator."""
STANDALONE = auto()
"""Generate a header/implementation file pair (e.g. ``.hpp/.cpp``) where the implementation file will
be compiled to a standalone object."""
INLINE = auto()
"""Generate a header/inline implementation file pair (e.g. ``.hpp/.ipp``) where all implementations
are inlined by including the implementation file at the end of the header file."""
HEADER_ONLY = auto()
"""Generate only a header file.
At the moment, header-only mode does not support generation of kernels and requires that all functions
and methods are marked `inline`.
"""
@dataclass
class CodeStyle(ConfigBase):
"""Options affecting the code style used by the source file generator."""
indent_width: Option[int] = Option(2)
"""The number of spaces successively nested blocks should be indented with"""
# TODO possible future options:
# - newline before opening {
# - trailing return types
def indent(self, s: str):
from textwrap import indent
prefix = " " * self.get_option("indent_width")
return indent(s, prefix)
@dataclass
class ClangFormatOptions(ConfigBase):
"""Options affecting the invocation of ``clang-format`` for automatic code formatting."""
code_style: Option[str] = Option("file")
"""Code style to be used by clang-format. Passed verbatim to `--style` argument of the clang-format CLI.
Similar to clang-format itself, the default value is `file`, such that a `.clang-format` file found in the build
tree will automatically be used.
"""
force: Option[bool] = Option(False)
"""If set to ``True``, abort code generation if ``clang-format`` binary cannot be found."""
skip: Option[bool] = Option(False)
"""If set to ``True``, skip formatting using ``clang-format``."""
binary: Option[str] = Option("clang-format")
"""Path to the clang-format executable"""
@force.validate
def _validate_force(self, val: bool) -> bool:
if val and self.skip:
raise SfgConfigException(
"Cannot set both `clang_format.force` and `clang_format.skip` at the same time"
)
return val
@skip.validate
def _validate_skip(self, val: bool) -> bool:
if val and self.force:
raise SfgConfigException(
"Cannot set both `clang_format.force` and `clang_format.skip` at the same time"
)
return val
class _GlobalNamespace: ... # noqa: E701
GLOBAL_NAMESPACE = _GlobalNamespace()
"""Indicates the C++ global namespace."""
@dataclass
class SfgConfig(ConfigBase):
"""Configuration options for the `SourceFileGenerator`."""
extensions: FileExtensions = field(default_factory=FileExtensions)
"""File extensions of the generated files
Options in this category:
.. autosummary::
FileExtensions.header
FileExtensions.impl
"""
output_mode: Option[OutputMode] = Option(OutputMode.STANDALONE)
"""The generator's output mode; defines which files to generate, and the set of legal file extensions.
Possible parameters:
.. autosummary::
OutputMode.STANDALONE
OutputMode.INLINE
OutputMode.HEADER_ONLY
"""
outer_namespace: Option[str | _GlobalNamespace] = Option(GLOBAL_NAMESPACE)
"""The outermost namespace in the generated file. May be a valid C++ nested namespace qualifier
(like ``a::b::c``) or `GLOBAL_NAMESPACE` if no outer namespace should be generated.
.. autosummary::
GLOBAL_NAMESPACE
"""
codestyle: CodeStyle = field(default_factory=CodeStyle)
"""Options affecting the code style emitted by pystencils-sfg.
Options in this category:
.. autosummary::
CodeStyle.indent_width
"""
clang_format: ClangFormatOptions = field(default_factory=ClangFormatOptions)
"""Options governing the code style used by the code generator
Options in this category:
.. autosummary::
ClangFormatOptions.code_style
ClangFormatOptions.force
ClangFormatOptions.skip
ClangFormatOptions.binary
"""
output_directory: Option[str] = Option(".")
"""Directory to which the generated files should be written."""
class CommandLineParameters:
@staticmethod
def add_args_to_parser(parser: ArgumentParser):
config_group = parser.add_argument_group("Configuration")
config_group.add_argument(
"--sfg-output-dir", type=str, default=None, dest="output_directory"
)
config_group.add_argument(
"--sfg-file-extensions",
type=str,
default=None,
dest="file_extensions",
help="Comma-separated list of file extensions",
)
config_group.add_argument(
"--sfg-output-mode",
type=str,
default=None,
choices=("standalone", "inline", "header-only"),
dest="output_mode",
)
config_group.add_argument(
"--sfg-config-module", type=str, default=None, dest="config_module_path"
)
return parser
def __init__(self, args) -> None:
self._cl_config_module_path: str | None = args.config_module_path
if args.output_mode is not None:
match args.output_mode.lower():
case "standalone":
output_mode = OutputMode.STANDALONE
case "inline":
output_mode = OutputMode.INLINE
case "header-only":
output_mode = OutputMode.HEADER_ONLY
case _:
assert False, "invalid output mode"
else:
output_mode = None
self._cl_output_mode = output_mode
self._cl_output_dir: str | None = args.output_directory
if args.file_extensions is not None:
file_extentions = list(args.file_extensions.split(","))
h_ext, impl_ext = self._get_file_extensions(file_extentions)
self._cl_header_ext = h_ext
self._cl_impl_ext = impl_ext
else:
self._cl_header_ext = None
self._cl_impl_ext = None
self._config_module: ModuleType | None
if self._cl_config_module_path is not None:
self._config_module = self._import_config_module(
self._cl_config_module_path
)
else:
self._config_module = None
@property
def configuration_module(self) -> ModuleType | None:
return self._config_module
def get_config(self) -> SfgConfig:
cfg = SfgConfig()
if self._config_module is not None and hasattr(
self._config_module, "configure_sfg"
):
self._config_module.configure_sfg(cfg)
if self._cl_output_mode is not None:
cfg.output_mode = self._cl_output_mode
if self._cl_header_ext is not None:
cfg.extensions.header = self._cl_header_ext
if self._cl_impl_ext is not None:
cfg.extensions.impl = self._cl_impl_ext
if self._cl_output_dir is not None:
cfg.output_directory = self._cl_output_dir
return cfg
def find_conflicts(self, cfg: SfgConfig):
for name, mine, theirs in (
("output_mode", self._cl_output_mode, cfg.output_mode),
("extensions.header", self._cl_header_ext, cfg.extensions.header),
("extensions.impl", self._cl_impl_ext, cfg.extensions.impl),
("output_directory", self._cl_output_dir, cfg.output_directory),
):
if mine is not None and theirs is not None and mine != theirs:
raise SfgConfigException(
f"Conflicting values given for option {name} on command line and inside generator script.\n"
f" Value on command-line: {name}",
f" Value in script: {name}",
)
def get_project_info(self) -> Any:
if self._config_module is not None and hasattr(
self._config_module, "project_info"
):
return self._config_module.project_info()
else:
return None
def _get_file_extensions(self, extensions: Sequence[str]):
h_ext = None
src_ext = None
extensions = tuple(ext.strip() for ext in extensions)
extensions = tuple((ext[1:] if ext[0] == "." else ext) for ext in extensions)
HEADER_FILE_EXTENSIONS = {"h", "hpp", "hxx", "h++", "cuh"}
IMPL_FILE_EXTENSIONS = {"c", "cpp", "cxx", "c++", "cu", ".impl.h", "ipp"}
for ext in extensions:
if ext in HEADER_FILE_EXTENSIONS:
if h_ext is not None:
raise SfgConfigException(
"Multiple header file extensions specified."
)
h_ext = ext
elif ext in IMPL_FILE_EXTENSIONS:
if src_ext is not None:
raise SfgConfigException(
"Multiple source file extensions specified."
)
src_ext = ext
else:
raise SfgConfigException(
f"Invalid file extension: Don't know what to do with '.{ext}'"
)
return h_ext, src_ext
def _import_config_module(self, module_path: str) -> ModuleType:
cfg_modulename = path.splitext(path.split(module_path)[1])[0]
cfg_spec = iutil.spec_from_file_location(cfg_modulename, module_path)
if cfg_spec is None:
raise SfgConfigException(
f"Unable to import configuration module {module_path}",
)
config_module = iutil.module_from_spec(cfg_spec)
cfg_spec.loader.exec_module(config_module) # type: ignore
return config_module
# mypy: strict_optional=False
from __future__ import annotations
from typing import Sequence, Any
from os import path
from enum import Enum, auto
from dataclasses import dataclass, replace, fields, InitVar
from argparse import ArgumentParser
from textwrap import indent
from importlib import util as iutil
from .exceptions import SfgException
class SfgConfigSource(Enum):
DEFAULT = auto()
PROJECT = auto()
COMMANDLINE = auto()
SCRIPT = auto()
class SfgConfigException(Exception):
def __init__(self, cfg_src: SfgConfigSource | None, message: str):
super().__init__(cfg_src, message)
self.message = message
self.config_source = cfg_src
@dataclass
class SfgCodeStyle:
indent_width: int = 2
code_style: str = "file"
"""Code style to be used by clang-format. Passed verbatim to `--style` argument of the clang-format CLI.
Similar to clang-format itself, the default value is `file`, such that a `.clang-format` file found in the build
tree will automatically be used.
"""
force_clang_format: bool = False
"""If set to True, abort code generation if ``clang-format`` binary cannot be found."""
skip_clang_format: bool = False
"""If set to True, skip formatting using ``clang-format``."""
clang_format_binary: str = "clang-format"
"""Path to the clang-format executable"""
def indent(self, s: str):
prefix = " " * self.indent_width
return indent(s, prefix)
class SfgOutputMode(Enum):
STANDALONE = auto()
"""Generate a header/implementation file pair (e.g. ``.hpp/.cpp``) where the implementation file will
be compiled to a standalone object."""
INLINE = auto()
"""Generate a header/inline implementation file pair (e.g. ``.hpp/.ipp``) where all implementations
are inlined by including the implementation file at the end of the header file."""
HEADER_ONLY = auto()
"""Generate only a header file.
At the moment, header-only mode does not support generation of kernels and requires that all functions
and methods are marked `inline`.
"""
HEADER_FILE_EXTENSIONS = {"h", "hpp", "cuh"}
IMPL_FILE_EXTENSIONS: dict[SfgOutputMode, set[str]] = {
SfgOutputMode.STANDALONE: {"c", "cpp", "cu"},
SfgOutputMode.INLINE: {".impl.h", "ipp"},
SfgOutputMode.HEADER_ONLY: set(),
}
@dataclass
class SfgOutputSpec:
"""Name and path specification for files output by the code generator.
Filenames are constructed as `<output_directory>/<basename>.<extension>`."""
output_directory: str
"""Directory to which the generated files should be written."""
basename: str
"""Base name for output files."""
header_extension: str
"""File extension for generated header file."""
impl_extension: str
"""File extension for generated implementation file."""
def get_header_filename(self):
return f"{self.basename}.{self.header_extension}"
def get_impl_filename(self):
return f"{self.basename}.{self.impl_extension}"
def get_header_filepath(self):
return path.join(self.output_directory, self.get_header_filename())
def get_impl_filepath(self):
return path.join(self.output_directory, self.get_impl_filename())
@dataclass
class SfgConfiguration:
"""
Configuration for the `SfgSourceFileGenerator`.
The source file generator draws configuration from a total of four sources:
- The default configuration (`pystencilssfg.configuration.DEFAULT_CONFIG`);
- The project configuration;
- Command-line arguments;
- The user configuration passed to the constructor of `SourceFileGenerator`.
They take precedence in the following way:
- Project configuration overrides the default configuration
- Command line arguments override the project configuration
- User configuration overrides default and project configuration,
and must not conflict with command-line arguments; otherwise, an error is thrown.
**Project Configuration via Configurator Script**
Currently, the only way to define the project configuration is via a configuration module.
A configurator module is a Python file defining the following function at the top-level:
.. code-block:: Python
from pystencilssfg import SfgConfiguration
def sfg_config() -> SfgConfiguration:
# ...
return SfgConfiguration(
# ...
)
The configuration module is passed to the code generation script via the command-line argument
`--sfg-config-module`.
"""
config_source: InitVar[SfgConfigSource | None] = None
header_extension: str | None = None
"""File extension for generated header file."""
impl_extension: str | None = None
"""File extension for generated implementation file."""
output_mode: SfgOutputMode | None = None
"""The generator's output mode; defines which files to generate, and the set of legal file extensions."""
outer_namespace: str | None = None
"""The outermost namespace in the generated file. May be a valid C++ nested namespace qualifier
(like ``a::b::c``) or `None` if no outer namespace should be generated."""
codestyle: SfgCodeStyle | None = None
"""Code style that should be used by the code generator."""
output_directory: str | None = None
"""Directory to which the generated files should be written."""
project_info: Any = None
"""Object for managing project-specific information. To be set by the configurator script."""
def __post_init__(self, cfg_src: SfgConfigSource | None = None):
if self.header_extension and self.header_extension[0] == ".":
self.header_extension = self.header_extension[1:]
if self.impl_extension and self.impl_extension[0] == ".":
self.impl_extension = self.impl_extension[1:]
def override(self, other: SfgConfiguration):
other_dict: dict[str, Any] = {
k: v for k, v in _shallow_dict(other).items() if v is not None
}
return replace(self, **other_dict)
def get_output_spec(self, basename: str) -> SfgOutputSpec:
assert self.header_extension is not None
assert self.impl_extension is not None
assert self.output_directory is not None
return SfgOutputSpec(
self.output_directory, basename, self.header_extension, self.impl_extension
)
DEFAULT_CONFIG = SfgConfiguration(
config_source=SfgConfigSource.DEFAULT,
header_extension="h",
impl_extension="cpp",
output_mode=SfgOutputMode.STANDALONE,
outer_namespace=None,
codestyle=SfgCodeStyle(),
output_directory=".",
)
"""Default configuration for the `SourceFileGenerator`."""
def run_configurator(configurator_script: str):
cfg_modulename = path.splitext(path.split(configurator_script)[1])[0]
cfg_spec = iutil.spec_from_file_location(cfg_modulename, configurator_script)
if cfg_spec is None:
raise SfgConfigException(
SfgConfigSource.PROJECT,
f"Unable to load configurator script {configurator_script}",
)
configurator = iutil.module_from_spec(cfg_spec)
cfg_spec.loader.exec_module(configurator)
if not hasattr(configurator, "sfg_config"):
raise SfgConfigException(
SfgConfigSource.PROJECT,
"Project configurator does not define function `sfg_config`.",
)
project_config = configurator.sfg_config()
if not isinstance(project_config, SfgConfiguration):
raise SfgConfigException(
SfgConfigSource.PROJECT,
"sfg_config did not return a SfgConfiguration object.",
)
return project_config
def add_config_args_to_parser(parser: ArgumentParser):
config_group = parser.add_argument_group("Configuration")
config_group.add_argument(
"--sfg-output-dir", type=str, default=None, dest="output_directory"
)
config_group.add_argument(
"--sfg-file-extensions",
type=str,
default=None,
dest="file_extensions",
help="Comma-separated list of file extensions",
)
config_group.add_argument(
"--sfg-output-mode",
type=str,
default=None,
choices=("standalone", "inline", "header-only"),
dest="output_mode",
)
config_group.add_argument(
"--sfg-config-module", type=str, default=None, dest="configurator_script"
)
return parser
def config_from_parser_args(args):
if args.configurator_script is not None:
project_config = run_configurator(args.configurator_script)
else:
project_config = None
if args.output_mode is not None:
match args.output_mode.lower():
case "standalone":
output_mode = SfgOutputMode.STANDALONE
case "inline":
output_mode = SfgOutputMode.INLINE
case "header-only":
output_mode = SfgOutputMode.HEADER_ONLY
case _:
assert False, "invalid output mode"
else:
output_mode = None
if args.file_extensions is not None:
file_extentions = list(args.file_extensions.split(","))
h_ext, src_ext = _get_file_extensions(
SfgConfigSource.COMMANDLINE, file_extentions, output_mode
)
else:
h_ext, src_ext = None, None
cmdline_config = SfgConfiguration(
config_source=SfgConfigSource.COMMANDLINE,
header_extension=h_ext,
impl_extension=src_ext,
output_mode=output_mode,
output_directory=args.output_directory,
)
return project_config, cmdline_config
def config_from_commandline(argv: list[str]):
parser = ArgumentParser(
"pystencilssfg",
description="pystencils Source File Generator",
allow_abbrev=False,
)
add_config_args_to_parser(parser)
args, script_args = parser.parse_known_args(argv)
project_config, cmdline_config = config_from_parser_args(args)
return project_config, cmdline_config, script_args
def merge_configurations(
project_config: SfgConfiguration | None,
cmdline_config: SfgConfiguration | None,
script_config: SfgConfiguration | None,
):
# Project config completely overrides default config
config = DEFAULT_CONFIG
if project_config is not None:
config = config.override(project_config)
if cmdline_config is not None:
cmdline_dict = _shallow_dict(cmdline_config)
# Commandline config completely overrides project and default config
config = config.override(cmdline_config)
else:
cmdline_dict = {}
if script_config is not None:
# User config may only set values not specified on the command line
script_dict = _shallow_dict(script_config)
for key, cmdline_value in cmdline_dict.items():
if cmdline_value is not None and script_dict[key] is not None:
raise SfgException(
"Conflicting configuration:"
+ f" Parameter {key} was specified both in the script and on the command line."
)
config = config.override(script_config)
return config
def _get_file_extensions(
cfgsrc: SfgConfigSource, extensions: Sequence[str], output_mode: SfgOutputMode
):
h_ext = None
src_ext = None
extensions = tuple((ext[1:] if ext[0] == "." else ext) for ext in extensions)
for ext in extensions:
if ext in HEADER_FILE_EXTENSIONS:
if h_ext is not None:
raise SfgConfigException(
cfgsrc, "Multiple header file extensions specified."
)
h_ext = ext
elif ext in IMPL_FILE_EXTENSIONS[output_mode]:
if src_ext is not None:
raise SfgConfigException(
cfgsrc, "Multiple source file extensions specified."
)
src_ext = ext
else:
raise SfgConfigException(
cfgsrc, f"Invalid file extension '.{ext}' for output mode {output_mode}"
)
return h_ext, src_ext
def _shallow_dict(obj):
"""Workaround to create a shallow dict of a dataclass object, see
https://docs.python.org/3/library/dataclasses.html#dataclasses.asdict."""
return dict((field.name, getattr(obj, field.name)) for field in fields(obj))
from typing import Generator, Sequence, Any
from .configuration import SfgCodeStyle
from .config import CodeStyle
from .ir.source_components import (
SfgHeaderInclude,
SfgKernelNamespace,
......@@ -45,7 +45,7 @@ class SfgContext:
def __init__(
self,
outer_namespace: str | None = None,
codestyle: SfgCodeStyle = SfgCodeStyle(),
codestyle: CodeStyle | None = None,
argv: Sequence[str] | None = None,
project_info: Any = None,
):
......@@ -65,7 +65,7 @@ class SfgContext:
self._outer_namespace = outer_namespace
self._inner_namespace: str | None = None
self._codestyle = codestyle
self._codestyle = codestyle if codestyle is not None else CodeStyle()
# Source Components
self._prelude: str = ""
......@@ -121,7 +121,7 @@ class SfgContext:
assert False
@property
def codestyle(self) -> SfgCodeStyle:
def codestyle(self) -> CodeStyle:
"""The code style object for this generation context."""
return self._codestyle
......
from .emitter import AbstractEmitter
from .emitter import AbstractEmitter, OutputSpec
from .header_impl_pair import HeaderImplPairEmitter
from .header_only import HeaderOnlyEmitter
__all__ = ["AbstractEmitter", "HeaderImplPairEmitter", "HeaderOnlyEmitter"]
__all__ = ["AbstractEmitter", "OutputSpec", "HeaderImplPairEmitter", "HeaderOnlyEmitter"]
import subprocess
import shutil
from ..configuration import SfgCodeStyle
from ..config import ClangFormatOptions
from ..exceptions import SfgException
def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str:
def invoke_clang_format(code: str, options: ClangFormatOptions) -> str:
"""Call the `clang-format` command-line tool to format the given code string
according to the given style arguments.
......@@ -24,13 +24,15 @@ def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str:
be executed (binary not found, or error during exection), the function will
throw an exception.
"""
if codestyle.skip_clang_format:
if options.get_option("skip"):
return code
args = [codestyle.clang_format_binary, f"--style={codestyle.code_style}"]
binary = options.get_option("binary")
force = options.get_option("force")
args = [binary, f"--style={options.code_style}"]
if not shutil.which(codestyle.clang_format_binary):
if codestyle.force_clang_format:
if not shutil.which(binary):
if force:
raise SfgException(
"`force_clang_format` was set to true in code style, "
"but clang-format binary could not be found."
......@@ -41,7 +43,7 @@ def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str:
result = subprocess.run(args, input=code, capture_output=True, text=True)
if result.returncode != 0:
if codestyle.force_clang_format:
if force:
raise SfgException(f"Call to clang-format failed: \n{result.stderr}")
else:
return code
......
from __future__ import annotations
from typing import Sequence
from abc import ABC, abstractmethod
from dataclasses import dataclass
from os import path
from ..context import SfgContext
from ..config import SfgConfig, OutputMode
@dataclass
class OutputSpec:
"""Name and path specification for files output by the code generator.
Filenames are constructed as `<output_directory>/<basename>.<extension>`."""
output_directory: str
"""Directory to which the generated files should be written."""
basename: str
"""Base name for output files."""
header_extension: str
"""File extension for generated header file."""
impl_extension: str
"""File extension for generated implementation file."""
def get_header_filename(self):
return f"{self.basename}.{self.header_extension}"
def get_impl_filename(self):
return f"{self.basename}.{self.impl_extension}"
def get_header_filepath(self):
return path.join(self.output_directory, self.get_header_filename())
def get_impl_filepath(self):
return path.join(self.output_directory, self.get_impl_filename())
@staticmethod
def create(config: SfgConfig, basename: str) -> OutputSpec:
output_mode = config.get_option("output_mode")
header_extension = config.extensions.get_option("header")
impl_extension = config.extensions.get_option("impl")
if impl_extension is None:
match output_mode:
case OutputMode.INLINE:
impl_extension = "ipp"
case OutputMode.STANDALONE:
impl_extension = "cpp"
return OutputSpec(
config.get_option("output_directory"),
basename,
header_extension,
impl_extension,
)
class AbstractEmitter(ABC):
......
from typing import Sequence
from os import path, makedirs
from ..configuration import SfgOutputSpec
from ..context import SfgContext
from .prepare import prepare_context
from .printers import SfgHeaderPrinter, SfgImplPrinter
from .clang_format import invoke_clang_format
from ..config import ClangFormatOptions
from .emitter import AbstractEmitter
from .emitter import AbstractEmitter, OutputSpec
class HeaderImplPairEmitter(AbstractEmitter):
"""Emits a header-implementation file pair."""
def __init__(self, output_spec: SfgOutputSpec, inline_impl: bool = False):
def __init__(
self,
output_spec: OutputSpec,
inline_impl: bool = False,
clang_format: ClangFormatOptions | None = None,
):
"""Create a `HeaderImplPairEmitter` from an [SfgOutputSpec][pystencilssfg.configuration.SfgOutputSpec]."""
self._basename = output_spec.basename
self._output_directory = output_spec.output_directory
......@@ -22,6 +27,7 @@ class HeaderImplPairEmitter(AbstractEmitter):
self._inline_impl = inline_impl
self._ospec = output_spec
self._clang_format = clang_format
@property
def output_files(self) -> Sequence[str]:
......@@ -42,8 +48,9 @@ class HeaderImplPairEmitter(AbstractEmitter):
header = header_printer.get_code()
impl = impl_printer.get_code()
header = invoke_clang_format(header, ctx.codestyle)
impl = invoke_clang_format(impl, ctx.codestyle)
if self._clang_format is not None:
header = invoke_clang_format(header, self._clang_format)
impl = invoke_clang_format(impl, self._clang_format)
makedirs(self._output_directory, exist_ok=True)
......
from typing import Sequence
from os import path, makedirs
from ..configuration import SfgOutputSpec
from ..context import SfgContext
from .prepare import prepare_context
from .printers import SfgHeaderPrinter
from ..config import ClangFormatOptions
from .clang_format import invoke_clang_format
from .emitter import AbstractEmitter
from .emitter import AbstractEmitter, OutputSpec
class HeaderOnlyEmitter(AbstractEmitter):
def __init__(self, output_spec: SfgOutputSpec):
def __init__(
self, output_spec: OutputSpec, clang_format: ClangFormatOptions | None = None
):
"""Create a `HeaderImplPairEmitter` from an [SfgOutputSpec][pystencilssfg.configuration.SfgOutputSpec]."""
self._basename = output_spec.basename
self._output_directory = output_spec.output_directory
self._header_filename = output_spec.get_header_filename()
self._ospec = output_spec
self._clang_format = clang_format
@property
def output_files(self) -> Sequence[str]:
......@@ -29,7 +32,8 @@ class HeaderOnlyEmitter(AbstractEmitter):
header_printer = SfgHeaderPrinter(ctx, self._ospec)
header = header_printer.get_code()
header = invoke_clang_format(header, ctx.codestyle)
if self._clang_format is not None:
header = invoke_clang_format(header, self._clang_format)
makedirs(self._output_directory, exist_ok=True)
......
......@@ -7,7 +7,6 @@ from pystencils import KernelFunction
from pystencils.backend.emission import emit_code
from ..context import SfgContext
from ..configuration import SfgOutputSpec
from ..visitors import visitor
from ..exceptions import SfgException
......@@ -25,6 +24,8 @@ from ..ir.source_components import (
SfgVisibilityBlock,
)
from .emitter import OutputSpec
def interleave(*iters):
try:
......@@ -71,7 +72,7 @@ class SfgGeneralPrinter:
class SfgHeaderPrinter(SfgGeneralPrinter):
def __init__(
self, ctx: SfgContext, output_spec: SfgOutputSpec, inline_impl: bool = False
self, ctx: SfgContext, output_spec: OutputSpec, inline_impl: bool = False
):
self._output_spec = output_spec
self._ctx = ctx
......@@ -180,7 +181,7 @@ class SfgHeaderPrinter(SfgGeneralPrinter):
class SfgImplPrinter(SfgGeneralPrinter):
def __init__(
self, ctx: SfgContext, output_spec: SfgOutputSpec, inline_impl: bool = False
self, ctx: SfgContext, output_spec: OutputSpec, inline_impl: bool = False
):
self._output_spec = output_spec
self._ctx = ctx
......
# TODO
# mypy strict_optional=False
import sys
import os
from os import path
from .configuration import (
SfgConfiguration,
SfgOutputMode,
config_from_commandline,
merge_configurations,
)
from .config import SfgConfig, CommandLineParameters, OutputMode, GLOBAL_NAMESPACE
from .context import SfgContext
from .composer import SfgComposer
from .emission import AbstractEmitter
from .emission import AbstractEmitter, OutputSpec
from .exceptions import SfgException
class SourceFileGenerator:
"""Context manager that controls the code generation process in generator scripts.
**Usage:** The `SourceFileGenerator` must be used as a context manager by calling it within
The `SourceFileGenerator` must be used as a context manager by calling it within
a ``with`` statement in the top-level code of a generator script (see :ref:`guide:generator_scripts`).
Upon entry to its context, it creates an :class:`SfgComposer` which can be used to populate the generated files.
When the managed region finishes, the code files are generated and written to disk at the locations
......@@ -27,35 +19,59 @@ class SourceFileGenerator:
Existing copies of the target files are deleted on entry to the managed region,
and if an exception occurs within the managed region, no files are exported.
**Configuration:** The `SourceFileGenerator` optionally takes a user-defined configuration
object which is merged with configuration obtained from the build system; for details
on configuration sources, refer to :class:`SfgConfiguration`.
Args:
sfg_config: User configuration for the code generator
sfg_config: Inline configuration for the code generator
keep_unknown_argv: If `True`, any command line arguments given to the generator script
that the `SourceFileGenerator` does not understand are stored in
`sfg.context.argv`.
"""
def __init__(self, sfg_config: SfgConfiguration | None = None):
if sfg_config and not isinstance(sfg_config, SfgConfiguration):
def __init__(
self, sfg_config: SfgConfig | None = None, keep_unknown_argv: bool = False
):
if sfg_config and not isinstance(sfg_config, SfgConfig):
raise TypeError("sfg_config is not an SfgConfiguration.")
import __main__
if not hasattr(__main__, "__file__"):
raise SfgException(
"Invalid execution environment: "
"It seems that you are trying to run the `SourceFileGenerator` in an environment "
"without a valid entry point, such as a REPL or a multiprocessing fork."
)
scriptpath = __main__.__file__
scriptname = path.split(scriptpath)[1]
basename = path.splitext(scriptname)[0]
project_config, cmdline_config, script_args = config_from_commandline(sys.argv)
from argparse import ArgumentParser
parser = ArgumentParser(
scriptname,
description="Generator script using pystencils-sfg",
allow_abbrev=False,
)
CommandLineParameters.add_args_to_parser(parser)
if keep_unknown_argv:
sfg_args, script_args = parser.parse_known_args()
else:
sfg_args = parser.parse_args()
script_args = []
config = merge_configurations(project_config, cmdline_config, sfg_config)
cli_params = CommandLineParameters(sfg_args)
assert config.codestyle is not None
config = cli_params.get_config()
if sfg_config is not None:
cli_params.find_conflicts(sfg_config)
config.override(sfg_config)
self._context = SfgContext(
config.outer_namespace,
None if config.outer_namespace is GLOBAL_NAMESPACE else config.outer_namespace, # type: ignore
config.codestyle,
argv=script_args,
project_info=config.project_info,
project_info=cli_params.get_project_info(),
)
from pystencilssfg.ir import SfgHeaderInclude
......@@ -63,22 +79,29 @@ class SourceFileGenerator:
self._context.add_include(SfgHeaderInclude("cstdint", system_header=True))
self._context.add_definition("#define RESTRICT __restrict__")
output_mode = config.get_option("output_mode")
output_spec = OutputSpec.create(config, basename)
self._emitter: AbstractEmitter
match config.output_mode:
case SfgOutputMode.HEADER_ONLY:
match output_mode:
case OutputMode.HEADER_ONLY:
from .emission import HeaderOnlyEmitter
self._emitter = HeaderOnlyEmitter(config.get_output_spec(basename))
case SfgOutputMode.INLINE:
self._emitter = HeaderOnlyEmitter(
output_spec, clang_format=config.clang_format
)
case OutputMode.INLINE:
from .emission import HeaderImplPairEmitter
self._emitter = HeaderImplPairEmitter(
config.get_output_spec(basename), inline_impl=True
output_spec, inline_impl=True, clang_format=config.clang_format
)
case SfgOutputMode.STANDALONE:
case OutputMode.STANDALONE:
from .emission import HeaderImplPairEmitter
self._emitter = HeaderImplPairEmitter(config.get_output_spec(basename))
self._emitter = HeaderImplPairEmitter(
output_spec, clang_format=config.clang_format
)
def clean_files(self):
for file in self._emitter.output_files:
......
from pystencilssfg import SfgConfig
def configure_sfg(cfg: SfgConfig):
cfg.codestyle.indent_width = 3
cfg.clang_format.code_style = "llvm"
cfg.clang_format.skip = True
cfg.output_directory = "generated_sources"
cfg.outer_namespace = "myproject"
cfg.extensions.header = "hpp"
magic_string = "Spam and eggs"
magic_number = 0xcafe
def project_info():
return {
"use_openmp": True,
"use_cuda": True,
"float_format": "float32"
}
import pytest
from pystencilssfg.config import (
SfgConfig,
OutputMode,
GLOBAL_NAMESPACE,
CommandLineParameters,
SfgConfigException
)
def test_defaults():
cfg = SfgConfig()
assert cfg.get_option("output_mode") == OutputMode.STANDALONE
assert cfg.extensions.get_option("header") == "hpp"
assert cfg.codestyle.get_option("indent_width") == 2
assert cfg.clang_format.get_option("binary") == "clang-format"
assert cfg.clang_format.get_option("code_style") == "file"
assert cfg.get_option("outer_namespace") is GLOBAL_NAMESPACE
cfg.extensions.impl = ".cu"
assert cfg.extensions.get_option("impl") == "cu"
# Check that section subobjects of different config objects are independent
# -> must use default_factory to construct them, because they are mutable!
cfg.clang_format.binary = "bogus"
cfg2 = SfgConfig()
assert cfg2.clang_format.binary is None
def test_override():
cfg1 = SfgConfig()
cfg1.outer_namespace = "test"
cfg1.extensions.header = "h"
cfg1.extensions.impl = "c"
cfg1.clang_format.force = True
cfg2 = SfgConfig()
cfg2.outer_namespace = GLOBAL_NAMESPACE
cfg2.extensions.header = "hpp"
cfg2.extensions.impl = "cpp"
cfg2.clang_format.binary = "bogus"
cfg1.override(cfg2)
assert cfg1.outer_namespace is GLOBAL_NAMESPACE
assert cfg1.extensions.header == "hpp"
assert cfg1.extensions.impl == "cpp"
assert cfg1.codestyle.indent_width is None
assert cfg1.clang_format.force is True
assert cfg1.clang_format.code_style is None
assert cfg1.clang_format.binary == "bogus"
def test_sanitation():
cfg = SfgConfig()
cfg.extensions.header = ".hxx"
assert cfg.extensions.header == "hxx"
cfg.extensions.header = ".cxx"
assert cfg.extensions.header == "cxx"
cfg.clang_format.force = True
with pytest.raises(SfgConfigException):
cfg.clang_format.skip = True
cfg.clang_format.force = False
cfg.clang_format.skip = True
with pytest.raises(SfgConfigException):
cfg.clang_format.force = True
def test_from_commandline(sample_config_module):
from argparse import ArgumentParser
parser = ArgumentParser()
CommandLineParameters.add_args_to_parser(parser)
args = parser.parse_args(
["--sfg-output-dir", ".out", "--sfg-file-extensions", ".h++,c++"]
)
cli_args = CommandLineParameters(args)
cfg = cli_args.get_config()
assert cfg.output_directory == ".out"
assert cfg.extensions.header == "h++"
assert cfg.extensions.impl == "c++"
args = parser.parse_args(
["--sfg-output-dir", "gen_sources", "--sfg-config-module", sample_config_module]
)
cli_args = CommandLineParameters(args)
cfg = cli_args.get_config()
assert cfg.codestyle.indent_width == 3
assert cfg.clang_format.code_style == "llvm"
assert cfg.clang_format.skip is True
assert (
cfg.output_directory == "gen_sources"
) # value from config module overridden by commandline
assert cfg.outer_namespace == "myproject"
assert cfg.extensions.header == "hpp"
assert cli_args.configuration_module is not None
assert cli_args.configuration_module.magic_string == "Spam and eggs"
assert cli_args.configuration_module.magic_number == 0xCAFE
from pystencilssfg import SfgConfig
def configure_sfg(cfg: SfgConfig):
cfg.outer_namespace = "myproject"
cfg.codestyle.indent_width = 3
def project_info():
return {
"use_openmp": True,
"use_cuda": True,
"float_format": "float32",
}
from pystencilssfg import SourceFileGenerator, SfgConfiguration, SfgCodeStyle
from pystencilssfg import SourceFileGenerator, SfgConfig
# Do not use clang-format, since it reorders headers
cfg = SfgConfiguration(
codestyle=SfgCodeStyle(skip_clang_format=True)
)
cfg = SfgConfig()
cfg.clang_format.skip = True
with SourceFileGenerator(cfg) as sfg:
sfg.prelude("Expect the unexpected, and you shall never be surprised.")
......
from pystencilssfg import SourceFileGenerator
with SourceFileGenerator() as sfg:
ctx = sfg.context
assert ctx.outer_namespace == "myproject"
assert ctx.codestyle.indent_width == 3
assert not ctx.argv
assert isinstance(ctx.project_info, dict)
assert ctx.project_info == {
"use_openmp": True,
"use_cuda": True,
"float_format": "float32",
}
from pystencilssfg import SourceFileGenerator
with SourceFileGenerator(keep_unknown_argv=True) as sfg:
ctx = sfg.context
assert ctx.argv == ["--precision", "float32", "test1", "test2"]
from pystencilssfg import SourceFileGenerator
with SourceFileGenerator() as sfg:
...