From 2b4b01d72837e1c67da83e3a22231be5ffb46861 Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Wed, 15 Nov 2023 14:30:49 +0900
Subject: [PATCH] Added configuration module

---
 pystencilssfg/configuration.py | 152 +++++++++++++++++++++++++++++++++
 pystencilssfg/context.py       |  44 +++-------
 2 files changed, 165 insertions(+), 31 deletions(-)
 create mode 100644 pystencilssfg/configuration.py

diff --git a/pystencilssfg/configuration.py b/pystencilssfg/configuration.py
new file mode 100644
index 0000000..18eabe8
--- /dev/null
+++ b/pystencilssfg/configuration.py
@@ -0,0 +1,152 @@
+from typing import List, Sequence
+from enum import Enum, auto
+from dataclasses import dataclass, replace
+from argparse import ArgumentParser
+
+from jinja2.filters import do_indent
+
+from .exceptions import SfgException
+
+HEADER_FILE_EXTENSIONS = {'h', 'hpp'}
+SOURCE_FILE_EXTENSIONS = {'c', 'cpp'}
+
+
+@dataclass
+class SfgCodeStyle:
+    indent_width: int = 2
+
+    def indent(self, s: str):
+        return do_indent(s, self.indent_width, first=True)
+
+
+@dataclass
+class SfgConfiguration:
+    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):
+        if self.header_only:
+            raise SfgException(
+                "Header-only code generation is not implemented yet.")
+        
+        if self.header_extension[0] == '.':
+            self.header_extension = self.header_extension[1:]
+
+        if self.source_extension[0] == '.':
+            self.source_extension = self.source_extension[1:]
+
+
+DEFAULT_CONFIG = SfgConfiguration(
+    header_extension='h',
+    source_extension='cpp',
+    header_only=False,
+    base_namespace=None,
+    codestyle=SfgCodeStyle(),
+    output_directory=""
+)
+
+
+def get_file_extensions(self, 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 ValueError("Multiple header file extensions found.")
+            h_ext = ext
+        elif ext in SOURCE_FILE_EXTENSIONS:
+            if src_ext is not None:
+                raise ValueError("Multiple source file extensions found.")
+            src_ext = ext
+        else:
+            raise ValueError(f"Don't know how to interpret extension '.{ext}'")
+
+    return h_ext, src_ext
+
+
+def run_configurator(configurator_script: str):
+    raise NotImplementedError()
+
+
+def config_from_commandline(self, argv: List[str]):
+    parser = ArgumentParser("pystencilssfg",
+                            description="pystencils Source File Generator",
+                            allow_abbrev=False)
+
+    parser.add_argument("-sfg-d", "--sfg-output-dir",
+                        type=str, default='.', dest='output_directory')
+    parser.add_argument("-sfg-e", "--sfg-file-extensions",
+                        type=str, default=None, nargs='*', dest='file_extensions')
+    parser.add_argument("--sfg-header-only",
+                        type=str, default=None, nargs='*', dest='header_only')
+    parser.add_argument("--sfg-configurator", type=str,
+                        default=None, nargs='*', dest='configurator_script')
+
+    args, script_args = parser.parse_known_args(argv)
+
+    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:
+        h_ext, src_ext = get_file_extensions(args.file_extensions)
+    else:
+        h_ext, src_ext = None, None
+
+    cmdline_config = SfgConfiguration(
+        header_extension=h_ext,
+        source_extension=src_ext,
+        header_only=args.header_only,
+        output_directory=args.output_directory
+    )
+
+    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:
+        config = replace(DEFAULT_CONFIG, **(project_config.asdict()))
+
+    if cmdline_config is not None:
+        cmdline_dict = cmdline_config.asdict()
+        #   Commandline config completely overrides project and default config
+        config = replace(config, **cmdline_dict)
+    else:
+        cmdline_dict = {}
+
+    if script_config is not None:
+        #   User config may only set values not specified on the command line
+        script_dict = script_config.asdict()
+        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.")
+
+        config = replace(config, **script_dict)
+
+    return config
diff --git a/pystencilssfg/context.py b/pystencilssfg/context.py
index f2d968d..feece85 100644
--- a/pystencilssfg/context.py
+++ b/pystencilssfg/context.py
@@ -5,13 +5,11 @@ import sys
 import os
 from os import path
 
-from argparse import ArgumentParser
-
-from jinja2.filters import do_indent
 
 from pystencils import Field
 from pystencils.astnodes import KernelFunction
 
+from .configuration import SfgConfiguration, config_from_commandline, merge_configurations, SfgCodeStyle
 from .kernel_namespace import SfgKernelNamespace, SfgKernelHandle
 from .tree import SfgCallTreeNode, SfgSequence, SfgKernelCallNode, SfgStatements
 from .tree.deferred_nodes import SfgDeferredFieldMapping
@@ -21,37 +19,22 @@ from .source_concepts import SrcField, TypedSymbolOrObject
 from .source_components import SfgFunction, SfgHeaderInclude
 
 
-@dataclass
-class SfgCodeStyle:
-    indent_width: int = 2
-
-    def indent(self, s: str):
-        return do_indent(s, self.indent_width, first=True)
-
 
 class SourceFileGenerator:
-    def __init__(self,
-                 namespace: str = "pystencils",
-                 codestyle: SfgCodeStyle = SfgCodeStyle()):
-        
-        parser = ArgumentParser(
-            "pystencilssfg",
-            description="pystencils Source File Generator",
-            allow_abbrev=False)
-        
-        parser.add_argument("-d", "--sfg-output-dir", type=str, default='.', dest='output_directory')
-
-        generator_args, script_args = parser.parse_known_args(sys.argv)
-
+    def __init__(self, sfg_config: SfgConfiguration):
         import __main__
         scriptpath = __main__.__file__
         scriptname = path.split(scriptpath)[1]
-        basename = path.splitext(scriptname)[0]        
+        basename = path.splitext(scriptname)[0]
+
+        project_config, cmdline_config, script_args = config_from_commandline(sys.argv)
+
+        config = merge_configurations(project_config, cmdline_config, sfg_config)
 
-        self._context = SfgContext(script_args, namespace, codestyle)
+        self._context = SfgContext(script_args, config)
 
         from .emitters.cpu.basic_cpu import BasicCpuEmitter
-        self._emitter = BasicCpuEmitter(self._context, basename, generator_args.output_directory)
+        self._emitter = BasicCpuEmitter(self._context, basename, config.output_directory)
 
     def clean_files(self):
         for file in self._emitter.output_files:
@@ -68,10 +51,9 @@ class SourceFileGenerator:
 
 
 class SfgContext:
-    def __init__(self, argv, root_namespace: str, codestyle: SfgCodeStyle):
+    def __init__(self, argv, config: SfgConfiguration):
         self._argv = argv
-        self._root_namespace = root_namespace
-        self._codestyle = codestyle
+        self._config = config
         self._default_kernel_namespace = SfgKernelNamespace(self, "kernels")
 
         #   Source Components
@@ -85,11 +67,11 @@ class SfgContext:
 
     @property
     def root_namespace(self) -> str:
-        return self._root_namespace
+        return self._config.base_namespace
     
     @property
     def codestyle(self) -> SfgCodeStyle:
-        return self._codestyle
+        return self._config.codestyle
 
     #----------------------------------------------------------------------------------------------
     #   Source Component Getters
-- 
GitLab