Newer
Older
"""
The [source file generator][pystencilssfg.SourceFileGenerator] 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:
```Python
from pystencilssfg import SfgConfiguration
def sfg_config() -> SfgConfiguration:
```
The configuration module is passed to the code generation script via the command-line argument
`--sfg-config-module`.
"""
from dataclasses import dataclass, replace, fields, InitVar
HEADER_FILE_EXTENSIONS = {"h", "hpp"}
IMPL_FILE_EXTENSIONS = {"c", "cpp", ".impl.h"}
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."""
clang_format_binary: str = "clang-format"
"""Path to the clang-format executable"""
prefix = " " * self.indent_width
return indent(s, prefix)
def __post__init__(self):
if self.force_clang_format:
import shutil
if not shutil.which(self.clang_format_binary):
raise SfgException(
"`force_clang_format` set to true in code style, but clang-format binary not found."
)
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
@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())
config_source: InitVar[SfgConfigSource | None] = None
"""File extension for generated header file."""
impl_extension: str | None = None
"""File extension for generated implementation file."""
"""If set to `True`, generate only a header file without accompaning source file."""
"""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):
raise SfgConfigException(
cfg_src, "Header-only code generation is not implemented yet."
)
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",
)
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-header-only", default=None, action="store_true", dest="header_only"
)
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.file_extensions is not None:
file_extentions = list(args.file_extensions.split(","))
h_ext, src_ext = _get_file_extensions(
SfgConfigSource.COMMANDLINE, file_extentions
)
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]):
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."
)
elif ext in IMPL_FILE_EXTENSIONS:
raise SfgConfigException(
cfgsrc, "Multiple source file extensions specified."
)
raise SfgConfigException(
cfgsrc, f"Don't know how to interpret file extension '.{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))