Newer
Older
from dataclasses import dataclass, replace, fields, InitVar
from .exceptions import SfgException
class SfgConfigSource(Enum):
DEFAULT = auto()
PROJECT = 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"""
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(),
}
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
@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())
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
The configuration module is passed to the code generation script via the command-line argument
`--sfg-config-module`.
"""
config_source: InitVar[SfgConfigSource | 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."""
"""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."""
"""Code style that should be used by the code generator."""
"""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:]
other_dict: dict[str, Any] = {
k: v for k, v in _shallow_dict(other).items() if v is not None
}
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
config_source=SfgConfigSource.DEFAULT,
header_extension="h",
impl_extension="cpp",
"""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)
raise SfgConfigException(
SfgConfigSource.PROJECT,
f"Unable to load configurator script {configurator_script}",
)
cfg_spec.loader.exec_module(configurator)
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.",
)
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"
)
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
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,
output_directory=args.output_directory,
return project_config, cmdline_config
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:
# Commandline config completely overrides project and default config
else:
cmdline_dict = {}
if script_config is not None:
# User config may only set values not specified on the command line
for key, cmdline_value in cmdline_dict.items():
if cmdline_value is not None and script_dict[key] is not None:
raise SfgException(
+ f" Parameter {key} was specified both in the script and on the command line."
)
def _get_file_extensions(
cfgsrc: SfgConfigSource, extensions: Sequence[str], output_mode: SfgOutputMode
):
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."
)
raise SfgConfigException(
cfgsrc, "Multiple source file extensions specified."
)
raise SfgConfigException(
cfgsrc, f"Invalid file extension '.{ext}' for output mode {output_mode}"
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))