Newer
Older
from dataclasses import dataclass, replace, asdict, InitVar
from jinja2.filters import do_indent
from .exceptions import SfgException
HEADER_FILE_EXTENSIONS = {'h', 'hpp'}
SOURCE_FILE_EXTENSIONS = {'c', 'cpp'}
class SfgConfigSource(Enum):
DEFAULT = auto()
PROJECT = auto()
COMMANDLINE = auto
SCRIPT = auto()
class SfgConfigException(Exception):
def __init__(self, cfg_src: SfgConfigSource | None, message: str):
assert cfg_src != SfgConfigSource.DEFAULT, "Invalid default config. Contact a developer."
super().__init__(cfg_src, message)
self.message = message
self.config_source = cfg_src
@dataclass
class SfgCodeStyle:
indent_width: int = 2
def indent(self, s: str):
return do_indent(s, self.indent_width, first=True)
@dataclass
class SfgConfiguration:
config_source: InitVar[SfgConfigSource | None] = None
"""File extension for generated header files."""
"""File extension for generated source files."""
"""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.source_extension and self.source_extension[0] == '.':
self.source_extension = self.source_extension[1:]
def override(self, other: SfgConfiguration):
other_dict = asdict(other)
other_dict = {k: v for k, v in other_dict.items() if v is not None}
return replace(self, **other_dict)
config_source=SfgConfigSource.DEFAULT,
header_extension='h',
source_extension='cpp',
header_only=False,
base_namespace=None,
codestyle=SfgCodeStyle(),
output_directory=""
)
def run_configurator(configurator_script: str):
cfg_spec = iutil.spec_from_file_location(configurator_script)
if cfg_spec is None:
raise SfgConfigException(SfgConfigSource.PROJECT,
f"Unable to load configurator script {configurator_script}")
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",
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-configurator", 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,
header_extension=h_ext,
source_extension=src_ext,
header_only=args.header_only,
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(
"Conflicting configuration:"
+ 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.")
h_ext = ext
elif ext in SOURCE_FILE_EXTENSIONS:
if src_ext is not None:
raise SfgConfigException(cfgsrc, "Multiple source file extensions specified.")
raise SfgConfigException(cfgsrc, f"Don't know how to interpret file extension '.{ext}'")