Skip to content
Snippets Groups Projects
configuration.py 6.25 KiB
Newer Older
Frederik Hennig's avatar
Frederik Hennig committed
from __future__ import annotations

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):
        if self.header_only:
            raise SfgConfigException(cfg_src, "Header-only code generation is not implemented yet.")
Frederik Hennig's avatar
Frederik Hennig committed

        if self.header_extension and self.header_extension[0] == '.':
            self.header_extension = self.header_extension[1:]

Frederik Hennig's avatar
Frederik Hennig committed
        if self.source_extension and self.source_extension[0] == '.':
            self.source_extension = self.source_extension[1:]

Frederik Hennig's avatar
Frederik Hennig committed
    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)


DEFAULT_CONFIG = SfgConfiguration(
    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:
Frederik Hennig's avatar
Frederik Hennig committed
        config = config.override(project_config)

    if cmdline_config is not None:
Frederik Hennig's avatar
Frederik Hennig committed
        cmdline_dict = asdict(cmdline_config)
        #   Commandline config completely overrides project and default config
Frederik Hennig's avatar
Frederik Hennig committed
        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
Frederik Hennig's avatar
Frederik Hennig committed
        script_dict = asdict(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(
                    f"Conflicting configuration: Parameter {key} was specified both in the script and on the command line.")

Frederik Hennig's avatar
Frederik Hennig committed
        config = config.override(script_config)

    return config
def _get_file_extensions(cfgsrc: SfgConfigSource, extensions: Sequence[str]):
Frederik Hennig's avatar
Frederik Hennig committed
    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.")
Frederik Hennig's avatar
Frederik Hennig committed
            h_ext = ext
        elif ext in SOURCE_FILE_EXTENSIONS:
            if src_ext is not None:
                raise SfgConfigException(cfgsrc, "Multiple source file extensions specified.")
Frederik Hennig's avatar
Frederik Hennig committed
            src_ext = ext
        else:
            raise SfgConfigException(cfgsrc, f"Don't know how to interpret file extension '.{ext}'")
Frederik Hennig's avatar
Frederik Hennig committed

    return h_ext, src_ext