Newer
Older
from typing import List, Sequence
from enum import Enum, auto
from dataclasses import dataclass, replace, asdict, InitVar
from argparse import ArgumentParser
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, 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
header_extension: str = None
"""File extension for generated header files."""
source_extension: str = None
"""File extension for generated source files."""
header_only: bool = None
"""If set to `True`, generate only a header file without accompaning source file."""
base_namespace: str = 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
"""Code style that should be used by the code generator."""
output_directory: str = None
"""Directory to which the generated files should be written."""
def __post_init__(self, cfg_src: SfgConfigSource = 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):
raise NotImplementedError()
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-configurator", 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.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
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,
cmdline_config: SfgConfiguration,
script_config: SfgConfiguration):
# 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"Conflicting configuration: Parameter {key} was specified both in the script and on the command line.")
def _get_file_extensions(cfgsrc: SfgConfigSource, extensions: Sequence[str]):
h_ext = None
src_ext = None
extensions = ((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}'")