diff --git a/src/pystencilssfg/__init__.py b/src/pystencilssfg/__init__.py index a888fc45ce84f318dfd51e92d74c04fda18d265a..3bea41febe6e23d9c91537a978212e26c071bc35 100644 --- a/src/pystencilssfg/__init__.py +++ b/src/pystencilssfg/__init__.py @@ -1,4 +1,3 @@ -from .configuration import SfgConfiguration, SfgOutputMode, SfgCodeStyle from .config import SfgConfig from .generator import SourceFileGenerator from .composer import SfgComposer @@ -10,9 +9,6 @@ __all__ = [ "SfgConfig", "SourceFileGenerator", "SfgComposer", - "SfgConfiguration", - "SfgOutputMode", - "SfgCodeStyle", "SfgContext", "SfgVar", "AugExpr", diff --git a/src/pystencilssfg/cli.py b/src/pystencilssfg/cli.py index e5bdb4d815f97cb5dcfb58208341ff91c26609ce..3b321c2d0be13b906384701080b8de870647d507 100644 --- a/src/pystencilssfg/cli.py +++ b/src/pystencilssfg/cli.py @@ -4,13 +4,8 @@ from os import path from argparse import ArgumentParser, BooleanOptionalAction -from .configuration import ( - SfgConfigException, - SfgConfigSource, - add_config_args_to_parser, - config_from_parser_args, - merge_configurations, -) +from .config import CommandLineParameters, SfgConfigException, OutputMode +from .emission import OutputSpec def add_newline_arg(parser): @@ -39,7 +34,7 @@ def cli_main(program="sfg-cli"): ) outfiles_parser.set_defaults(func=list_files) - add_config_args_to_parser(outfiles_parser) + CommandLineParameters.add_args_to_parser(outfiles_parser) add_newline_arg(outfiles_parser) outfiles_parser.add_argument( "--sep", type=str, default=" ", dest="sep", help="Separator for list items" @@ -79,21 +74,18 @@ def version(args): def list_files(args): - try: - project_config, cmdline_config = config_from_parser_args(args) - except SfgConfigException as exc: - abort_with_config_exception(exc) - - config = merge_configurations(project_config, cmdline_config, None) + cli_params = CommandLineParameters(args) + config = cli_params.get_config() _, scriptname = path.split(args.codegen_script) basename = path.splitext(scriptname)[0] - from .emission import HeaderImplPairEmitter - - emitter = HeaderImplPairEmitter(config.get_output_spec(basename)) + output_spec = OutputSpec.create(config, basename) + output_files = [output_spec.get_header_filepath()] + if config.output_mode != OutputMode.HEADER_ONLY: + output_files.append(output_spec.get_impl_filepath()) - print(args.sep.join(emitter.output_files), end=os.linesep if args.newline else "") + print(args.sep.join(output_files), end=os.linesep if args.newline else "") exit(0) @@ -112,18 +104,6 @@ def make_cmake_find_module(args): exit(0) -def abort_with_config_exception(exception: SfgConfigException): - def eprint(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - - match exception.config_source: - case SfgConfigSource.PROJECT: - eprint( - f"Invalid project configuration: {exception.message}\nCheck your configurator script." - ) - case SfgConfigSource.COMMANDLINE: - eprint(f"Invalid configuration on command line: {exception.message}") - case _: - assert False, "(Theoretically) unreachable code. Contact the developers." - +def abort_with_config_exception(exception: SfgConfigException, source: str): + print(f"Invalid {source} configuration: {exception.args[0]}.", file=sys.stderr) exit(1) diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py index 992233999a033b3596f722f950caba26206dacc4..9cbb5c5cd1c634ea156b253aea1b5d913dd60b5f 100644 --- a/src/pystencilssfg/config.py +++ b/src/pystencilssfg/config.py @@ -129,6 +129,19 @@ class CodeStyle(ConfigBase): indent_width: Option[int] = Option(2) """The number of spaces successively nested blocks should be indented with""" + # TODO possible future options: + # - newline before opening { + # - trailing return types + + def indent(self, s: str): + from textwrap import indent + + prefix = " " * self.get_option("indent_width") + return indent(s, prefix) + + +@dataclass +class ClangFormatOptions(ConfigBase): code_style: Option[str] = Option("file") """Code style to be used by clang-format. Passed verbatim to `--style` argument of the clang-format CLI. @@ -136,15 +149,31 @@ class CodeStyle(ConfigBase): tree will automatically be used. """ - force_clang_format: Option[bool] = Option(False) + force: Option[bool] = Option(False) """If set to True, abort code generation if ``clang-format`` binary cannot be found.""" - skip_clang_format: Option[bool] = Option(False) + skip: Option[bool] = Option(False) """If set to True, skip formatting using ``clang-format``.""" - clang_format_binary: Option[str] = Option("clang-format") + binary: Option[str] = Option("clang-format") """Path to the clang-format executable""" + @force.validate + def _validate_force(self, val: bool) -> bool: + if val and self.skip: + raise SfgConfigException( + "Cannot set both `clang_format.force` and `clang_format.skip` at the same time" + ) + return val + + @skip.validate + def _validate_skip(self, val: bool) -> bool: + if val and self.force: + raise SfgConfigException( + "Cannot set both `clang_format.force` and `clang_format.skip` at the same time" + ) + return val + class _GlobalNamespace: ... # noqa: E701 @@ -165,6 +194,9 @@ class SfgConfig(ConfigBase): (like ``a::b::c``) or `GLOBAL_NAMESPACE` if no outer namespace should be generated.""" codestyle: CodeStyle = field(default_factory=CodeStyle) + """Options affecting the code style emitted by pystencils-sfg.""" + + clang_format: ClangFormatOptions = field(default_factory=ClangFormatOptions) """Options governing the code style used by the code generator""" output_directory: Option[str] = Option(".") @@ -199,7 +231,7 @@ class CommandLineParameters: return parser - def __init__(self, args): + def __init__(self, args) -> None: self._cl_config_module_path: str | None = args.config_module_path if args.output_mode is not None: @@ -228,6 +260,7 @@ class CommandLineParameters: self._cl_header_ext = None self._cl_impl_ext = None + self._config_module: ModuleType | None if self._cl_config_module_path is not None: self._config_module = self._import_config_module( self._cl_config_module_path @@ -319,48 +352,5 @@ class CommandLineParameters: ) config_module = iutil.module_from_spec(cfg_spec) - cfg_spec.loader.exec_module(config_module) + cfg_spec.loader.exec_module(config_module) # type: ignore return config_module - - -def run_configuration_module(configurator_script: str) -> SfgConfig: - """Run a configuration module. - - A configuration module must define a function called `configure_sfg` - with the following signature in its global namespace: - - ```Python - def configure_sfg(cfg: SfgConfig) -> None: ... - ``` - - After importing the module, that function will be called. - It should populate the ``cfg`` object with the desired configuration - options. - - TODO: The configuration module may optionally define a function ``project_info``, - which takes zero arguments and should return an containing project-specific information. - This object will later be available through the `project_info` member of the - `SfgContext`. - """ - - cfg_modulename = path.splitext(path.split(configurator_script)[1])[0] - - cfg_spec = iutil.spec_from_file_location(cfg_modulename, configurator_script) - - if cfg_spec is None: - raise SfgConfigException( - f"Unable to load configurator script {configurator_script}", - ) - - configurator = iutil.module_from_spec(cfg_spec) - cfg_spec.loader.exec_module(configurator) - - if not hasattr(configurator, "configure_sfg"): - raise SfgConfigException( - "Project configurator does not define function `configure_sfg`.", - ) - - cfg = SfgConfig() - project_config = configurator.configure_sfg(cfg) - - return project_config diff --git a/src/pystencilssfg/configuration.py b/src/pystencilssfg/configuration.py deleted file mode 100644 index a76251a04cde0d875b0ba41908a0fadc5fc90553..0000000000000000000000000000000000000000 --- a/src/pystencilssfg/configuration.py +++ /dev/null @@ -1,384 +0,0 @@ -# mypy: strict_optional=False - -from __future__ import annotations - -from typing import Sequence, Any -from os import path -from enum import Enum, auto -from dataclasses import dataclass, replace, fields, InitVar -from argparse import ArgumentParser -from textwrap import indent - -from importlib import util as iutil - -from .exceptions import SfgException - - -class SfgConfigSource(Enum): - DEFAULT = auto() - PROJECT = auto() - COMMANDLINE = 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.""" - - skip_clang_format: bool = False - """If set to True, skip formatting using ``clang-format``.""" - - clang_format_binary: str = "clang-format" - """Path to the clang-format executable""" - - def indent(self, s: str): - prefix = " " * self.indent_width - return indent(s, prefix) - - -class SfgOutputMode(Enum): - STANDALONE = auto() - """Generate a header/implementation file pair (e.g. ``.hpp/.cpp``) where the implementation file will - be compiled to a standalone object.""" - - INLINE = auto() - """Generate a header/inline implementation file pair (e.g. ``.hpp/.ipp``) where all implementations - are inlined by including the implementation file at the end of the header file.""" - - HEADER_ONLY = auto() - """Generate only a header file. - - At the moment, header-only mode does not support generation of kernels and requires that all functions - and methods are marked `inline`. - """ - - -HEADER_FILE_EXTENSIONS = {"h", "hpp", "cuh"} -IMPL_FILE_EXTENSIONS: dict[SfgOutputMode, set[str]] = { - SfgOutputMode.STANDALONE: {"c", "cpp", "cu"}, - SfgOutputMode.INLINE: {".impl.h", "ipp"}, - SfgOutputMode.HEADER_ONLY: set(), -} - - -@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()) - - -@dataclass -class SfgConfiguration: - """ - Configuration for the `SfgSourceFileGenerator`. - - The source file generator 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: - - .. code-block:: Python - - from pystencilssfg import SfgConfiguration - - def sfg_config() -> SfgConfiguration: - # ... - return SfgConfiguration( - # ... - ) - - The configuration module is passed to the code generation script via the command-line argument - `--sfg-config-module`. - """ - - config_source: InitVar[SfgConfigSource | None] = None - - header_extension: str | None = None - """File extension for generated header file.""" - - impl_extension: str | None = None - """File extension for generated implementation file.""" - - output_mode: SfgOutputMode | None = None - """The generator's output mode; defines which files to generate, and the set of legal file extensions.""" - - outer_namespace: str | None = 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 = None - """Code style that should be used by the code generator.""" - - output_directory: str | None = None - """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): - 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:] - - def override(self, other: SfgConfiguration): - other_dict: dict[str, Any] = { - k: v for k, v in _shallow_dict(other).items() if v is not None - } - return replace(self, **other_dict) - - 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 - ) - - -DEFAULT_CONFIG = SfgConfiguration( - config_source=SfgConfigSource.DEFAULT, - header_extension="h", - impl_extension="cpp", - output_mode=SfgOutputMode.STANDALONE, - outer_namespace=None, - codestyle=SfgCodeStyle(), - output_directory=".", -) -"""Default configuration for the `SourceFileGenerator`.""" - - -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) - - if cfg_spec is None: - raise SfgConfigException( - SfgConfigSource.PROJECT, - f"Unable to load configurator script {configurator_script}", - ) - - configurator = iutil.module_from_spec(cfg_spec) - cfg_spec.loader.exec_module(configurator) - - if not hasattr(configurator, "sfg_config"): - 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.", - ) - - return project_config - - -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-output-mode", - type=str, - default=None, - choices=("standalone", "inline", "header-only"), - dest="output_mode", - ) - config_group.add_argument( - "--sfg-config-module", 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.output_mode is not None: - match args.output_mode.lower(): - case "standalone": - output_mode = SfgOutputMode.STANDALONE - case "inline": - output_mode = SfgOutputMode.INLINE - case "header-only": - output_mode = SfgOutputMode.HEADER_ONLY - case _: - assert False, "invalid output mode" - else: - output_mode = 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, output_mode - ) - else: - h_ext, src_ext = None, None - - cmdline_config = SfgConfiguration( - config_source=SfgConfigSource.COMMANDLINE, - header_extension=h_ext, - impl_extension=src_ext, - output_mode=output_mode, - 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 | None, - cmdline_config: SfgConfiguration | None, - script_config: SfgConfiguration | None, -): - # Project config completely overrides default config - config = DEFAULT_CONFIG - - if project_config is not None: - config = config.override(project_config) - - if cmdline_config is not None: - cmdline_dict = _shallow_dict(cmdline_config) - # Commandline config completely overrides project and default config - 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 - script_dict = _shallow_dict(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( - "Conflicting configuration:" - + f" Parameter {key} was specified both in the script and on the command line." - ) - - config = config.override(script_config) - - return config - - -def _get_file_extensions( - cfgsrc: SfgConfigSource, extensions: Sequence[str], output_mode: SfgOutputMode -): - h_ext = None - src_ext = None - - 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 IMPL_FILE_EXTENSIONS[output_mode]: - if src_ext is not None: - raise SfgConfigException( - cfgsrc, "Multiple source file extensions specified." - ) - src_ext = ext - else: - raise SfgConfigException( - cfgsrc, f"Invalid file extension '.{ext}' for output mode {output_mode}" - ) - - return h_ext, src_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)) diff --git a/src/pystencilssfg/emission/clang_format.py b/src/pystencilssfg/emission/clang_format.py index eea152a062474a6761fdbbdecfaaeb88bb63c4d3..53540d3c4698aa1e16bb846e41f8bb7f8c87a35a 100644 --- a/src/pystencilssfg/emission/clang_format.py +++ b/src/pystencilssfg/emission/clang_format.py @@ -1,11 +1,11 @@ import subprocess import shutil -from ..configuration import SfgCodeStyle +from ..config import ClangFormatOptions from ..exceptions import SfgException -def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str: +def invoke_clang_format(code: str, options: ClangFormatOptions) -> str: """Call the `clang-format` command-line tool to format the given code string according to the given style arguments. @@ -24,13 +24,15 @@ def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str: be executed (binary not found, or error during exection), the function will throw an exception. """ - if codestyle.skip_clang_format: + if options.get_option("skip"): return code - args = [codestyle.clang_format_binary, f"--style={codestyle.code_style}"] + binary = options.get_option("binary") + force = options.get_option("force") + args = [binary, f"--style={options.code_style}"] - if not shutil.which(codestyle.clang_format_binary): - if codestyle.force_clang_format: + if not shutil.which(binary): + if force: raise SfgException( "`force_clang_format` was set to true in code style, " "but clang-format binary could not be found." @@ -41,7 +43,7 @@ def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str: result = subprocess.run(args, input=code, capture_output=True, text=True) if result.returncode != 0: - if codestyle.force_clang_format: + if force: raise SfgException(f"Call to clang-format failed: \n{result.stderr}") else: return code diff --git a/src/pystencilssfg/emission/emitter.py b/src/pystencilssfg/emission/emitter.py index e96e1e187bb55fcaca892c60f751649a90e4c6a7..c32b18af351579b98477bf688c0789a394fc3732 100644 --- a/src/pystencilssfg/emission/emitter.py +++ b/src/pystencilssfg/emission/emitter.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from typing import Sequence from abc import ABC, abstractmethod from dataclasses import dataclass from os import path from ..context import SfgContext +from ..config import SfgConfig, OutputMode @dataclass @@ -36,6 +39,26 @@ class OutputSpec: def get_impl_filepath(self): return path.join(self.output_directory, self.get_impl_filename()) + @staticmethod + def create(config: SfgConfig, basename: str) -> OutputSpec: + output_mode = config.get_option("output_mode") + header_extension = config.extensions.get_option("header") + impl_extension = config.extensions.get_option("impl") + + if impl_extension is None: + match output_mode: + case OutputMode.INLINE: + impl_extension = "ipp" + case OutputMode.STANDALONE: + impl_extension = "cpp" + + return OutputSpec( + config.get_option("output_directory"), + basename, + header_extension, + impl_extension, + ) + class AbstractEmitter(ABC): @property diff --git a/src/pystencilssfg/emission/header_impl_pair.py b/src/pystencilssfg/emission/header_impl_pair.py index 9e1e69729418240844f192777bdd609a054f3614..d70cfd255e5df6d01169eea50fe4ac87bba37bdf 100644 --- a/src/pystencilssfg/emission/header_impl_pair.py +++ b/src/pystencilssfg/emission/header_impl_pair.py @@ -5,6 +5,7 @@ from ..context import SfgContext from .prepare import prepare_context from .printers import SfgHeaderPrinter, SfgImplPrinter from .clang_format import invoke_clang_format +from ..config import ClangFormatOptions from .emitter import AbstractEmitter, OutputSpec @@ -12,7 +13,12 @@ from .emitter import AbstractEmitter, OutputSpec class HeaderImplPairEmitter(AbstractEmitter): """Emits a header-implementation file pair.""" - def __init__(self, output_spec: OutputSpec, inline_impl: bool = False): + def __init__( + self, + output_spec: OutputSpec, + inline_impl: bool = False, + clang_format: ClangFormatOptions | None = None, + ): """Create a `HeaderImplPairEmitter` from an [SfgOutputSpec][pystencilssfg.configuration.SfgOutputSpec].""" self._basename = output_spec.basename self._output_directory = output_spec.output_directory @@ -21,6 +27,7 @@ class HeaderImplPairEmitter(AbstractEmitter): self._inline_impl = inline_impl self._ospec = output_spec + self._clang_format = clang_format @property def output_files(self) -> Sequence[str]: @@ -41,8 +48,9 @@ class HeaderImplPairEmitter(AbstractEmitter): header = header_printer.get_code() impl = impl_printer.get_code() - header = invoke_clang_format(header, ctx.codestyle) - impl = invoke_clang_format(impl, ctx.codestyle) + if self._clang_format is not None: + header = invoke_clang_format(header, self._clang_format) + impl = invoke_clang_format(impl, self._clang_format) makedirs(self._output_directory, exist_ok=True) diff --git a/src/pystencilssfg/emission/header_only.py b/src/pystencilssfg/emission/header_only.py index 169d2bc00240ac2898234aa657614e45ebe58857..ce6ce90f609f110153c5d5ae753d0d511540ab18 100644 --- a/src/pystencilssfg/emission/header_only.py +++ b/src/pystencilssfg/emission/header_only.py @@ -4,19 +4,23 @@ from os import path, makedirs from ..context import SfgContext from .prepare import prepare_context from .printers import SfgHeaderPrinter +from ..config import ClangFormatOptions from .clang_format import invoke_clang_format from .emitter import AbstractEmitter, OutputSpec class HeaderOnlyEmitter(AbstractEmitter): - def __init__(self, output_spec: OutputSpec): + def __init__( + self, output_spec: OutputSpec, clang_format: ClangFormatOptions | None = None + ): """Create a `HeaderImplPairEmitter` from an [SfgOutputSpec][pystencilssfg.configuration.SfgOutputSpec].""" self._basename = output_spec.basename self._output_directory = output_spec.output_directory self._header_filename = output_spec.get_header_filename() self._ospec = output_spec + self._clang_format = clang_format @property def output_files(self) -> Sequence[str]: @@ -28,7 +32,8 @@ class HeaderOnlyEmitter(AbstractEmitter): header_printer = SfgHeaderPrinter(ctx, self._ospec) header = header_printer.get_code() - header = invoke_clang_format(header, ctx.codestyle) + if self._clang_format is not None: + header = invoke_clang_format(header, self._clang_format) makedirs(self._output_directory, exist_ok=True) diff --git a/src/pystencilssfg/emission/printers.py b/src/pystencilssfg/emission/printers.py index c562bf7bca5d59365c3a1cd5b9d77b8f7e7920f5..6daccfa0c5edab18fa4c509ad8eb379a5f163204 100644 --- a/src/pystencilssfg/emission/printers.py +++ b/src/pystencilssfg/emission/printers.py @@ -7,7 +7,6 @@ from pystencils import KernelFunction from pystencils.backend.emission import emit_code from ..context import SfgContext -from ..configuration import SfgOutputSpec from ..visitors import visitor from ..exceptions import SfgException @@ -25,6 +24,8 @@ from ..ir.source_components import ( SfgVisibilityBlock, ) +from .emitter import OutputSpec + def interleave(*iters): try: @@ -71,7 +72,7 @@ class SfgGeneralPrinter: class SfgHeaderPrinter(SfgGeneralPrinter): def __init__( - self, ctx: SfgContext, output_spec: SfgOutputSpec, inline_impl: bool = False + self, ctx: SfgContext, output_spec: OutputSpec, inline_impl: bool = False ): self._output_spec = output_spec self._ctx = ctx @@ -180,7 +181,7 @@ class SfgHeaderPrinter(SfgGeneralPrinter): class SfgImplPrinter(SfgGeneralPrinter): def __init__( - self, ctx: SfgContext, output_spec: SfgOutputSpec, inline_impl: bool = False + self, ctx: SfgContext, output_spec: OutputSpec, inline_impl: bool = False ): self._output_spec = output_spec self._ctx = ctx diff --git a/src/pystencilssfg/generator.py b/src/pystencilssfg/generator.py index c2461800348c570a02b5cdc52de880d106b3a6c4..83298140d9015e48765d4a3f93750a2beae35777 100644 --- a/src/pystencilssfg/generator.py +++ b/src/pystencilssfg/generator.py @@ -1,4 +1,3 @@ -import sys import os from os import path @@ -65,29 +64,29 @@ class SourceFileGenerator: self._context.add_include(SfgHeaderInclude("cstdint", system_header=True)) self._context.add_definition("#define RESTRICT __restrict__") - output_spec = OutputSpec( - config.get_option("output_directory"), - basename, - config.extensions.get_option("header"), - config.extensions.get_option("impl"), - ) + output_mode = config.get_option("output_mode") + output_spec = OutputSpec.create(config, basename) self._emitter: AbstractEmitter - match config.get_option("output_mode"): + match output_mode: case OutputMode.HEADER_ONLY: from .emission import HeaderOnlyEmitter - self._emitter = HeaderOnlyEmitter(output_spec) + self._emitter = HeaderOnlyEmitter( + output_spec, clang_format=config.clang_format + ) case OutputMode.INLINE: from .emission import HeaderImplPairEmitter self._emitter = HeaderImplPairEmitter( - output_spec, inline_impl=True + output_spec, inline_impl=True, clang_format=config.clang_format ) case OutputMode.STANDALONE: from .emission import HeaderImplPairEmitter - self._emitter = HeaderImplPairEmitter(output_spec) + self._emitter = HeaderImplPairEmitter( + output_spec, clang_format=config.clang_format + ) def clean_files(self): for file in self._emitter.output_files: diff --git a/tests/generator/data/project_config.py b/tests/generator/data/project_config.py index 73e5e9c6d52d979542b1f31137f8f31cf5e0c637..e4e8412a5f842d06f0f9270c5417279f9d5e4bac 100644 --- a/tests/generator/data/project_config.py +++ b/tests/generator/data/project_config.py @@ -2,8 +2,9 @@ from pystencilssfg import SfgConfig def configure_sfg(cfg: SfgConfig): - cfg.codestyle.code_style = "llvm" cfg.codestyle.indent_width = 3 + cfg.clang_format.code_style = "llvm" + cfg.clang_format.force = True cfg.output_directory = "generated_sources" cfg.outer_namespace = "myproject" cfg.extensions.header = "hpp" @@ -11,3 +12,11 @@ def configure_sfg(cfg: SfgConfig): magic_string = "Spam and eggs" magic_number = 0xcafe + + +def project_info(): + return { + "use_openmp": True, + "use_cuda": True, + "float_format": "float32" + } diff --git a/tests/generator/test_config.py b/tests/generator/test_config.py index 879d47f71934c87d120a71cbb856f036e00ecbee..66b0a9f75bf8bc997e8dca04b2be9f8e454604ca 100644 --- a/tests/generator/test_config.py +++ b/tests/generator/test_config.py @@ -18,6 +18,8 @@ def test_defaults(): assert cfg.get_option("output_mode") == OutputMode.STANDALONE assert cfg.extensions.get_option("header") == "hpp" assert cfg.codestyle.get_option("indent_width") == 2 + assert cfg.clang_format.get_option("binary") == "clang-format" + assert cfg.clang_format.get_option("code_style") == "file" assert cfg.get_option("outer_namespace") is GLOBAL_NAMESPACE cfg.extensions.impl = ".cu" @@ -25,10 +27,10 @@ def test_defaults(): # Check that section subobjects of different config objects are independent # -> must use default_factory to construct them, because they are mutable! - cfg.codestyle.clang_format_binary = "bogus" + cfg.clang_format.binary = "bogus" cfg2 = SfgConfig() - assert cfg2.codestyle.clang_format_binary is None + assert cfg2.clang_format.binary is None def test_override(): @@ -36,23 +38,23 @@ def test_override(): cfg1.outer_namespace = "test" cfg1.extensions.header = "h" cfg1.extensions.impl = "c" - cfg1.codestyle.force_clang_format = True + cfg1.clang_format.force = True cfg2 = SfgConfig() cfg2.outer_namespace = GLOBAL_NAMESPACE cfg2.extensions.header = "hpp" cfg2.extensions.impl = "cpp" - cfg2.codestyle.clang_format_binary = "bogus" + cfg2.clang_format.binary = "bogus" cfg1.override(cfg2) assert cfg1.outer_namespace is GLOBAL_NAMESPACE assert cfg1.extensions.header == "hpp" assert cfg1.extensions.impl == "cpp" - assert cfg1.codestyle.force_clang_format is True assert cfg1.codestyle.indent_width is None - assert cfg1.codestyle.code_style is None - assert cfg1.codestyle.clang_format_binary == "bogus" + assert cfg1.clang_format.force is True + assert cfg1.clang_format.code_style is None + assert cfg1.clang_format.binary == "bogus" def test_from_commandline(): @@ -79,8 +81,9 @@ def test_from_commandline(): cli_args = CommandLineParameters(args) cfg = cli_args.get_config() - assert cfg.codestyle.code_style == "llvm" assert cfg.codestyle.indent_width == 3 + assert cfg.clang_format.code_style == "llvm" + assert cfg.clang_format.force is True assert ( cfg.output_directory == "gen_sources" ) # value from config module overridden by commandline @@ -114,3 +117,9 @@ def test_generator_config(monkeypatch, tmp_path): assert ctx.outer_namespace == "myproject" assert ctx.argv == ["test1", "test2"] + assert isinstance(ctx.project_info, dict) + assert ctx.project_info == { + "use_openmp": True, + "use_cuda": True, + "float_format": "float32", + } diff --git a/tests/generator_scripts/scripts/Structural.py b/tests/generator_scripts/scripts/Structural.py index e8cf2ab0d87ecf770871c6ef2d15deb7820b4561..7cfe352910b676429b97cb8f3d29bec68b74810a 100644 --- a/tests/generator_scripts/scripts/Structural.py +++ b/tests/generator_scripts/scripts/Structural.py @@ -1,9 +1,8 @@ -from pystencilssfg import SourceFileGenerator, SfgConfiguration, SfgCodeStyle +from pystencilssfg import SourceFileGenerator, SfgConfig # Do not use clang-format, since it reorders headers -cfg = SfgConfiguration( - codestyle=SfgCodeStyle(skip_clang_format=True) -) +cfg = SfgConfig() +cfg.clang_format.skip = True with SourceFileGenerator(cfg) as sfg: sfg.prelude("Expect the unexpected, and you shall never be surprised.") diff --git a/tests/generator_scripts/test_generator_scripts.py b/tests/generator_scripts/test_generator_scripts.py index 51e05c01f2cae3a637e235e4a94d1585b390a5af..d3173dce73f14458c02ad400b73f4d52d21cf947 100644 --- a/tests/generator_scripts/test_generator_scripts.py +++ b/tests/generator_scripts/test_generator_scripts.py @@ -50,10 +50,10 @@ When adding new generator scripts to the `scripts` directory, do not forget to include them here. """ SCRIPTS = [ - ScriptInfo.make("Structural", ("h", "cpp")), - ScriptInfo.make("SimpleJacobi", ("h", "cpp"), compilable_output="cpp"), - ScriptInfo.make("SimpleClasses", ("h", "cpp")), - ScriptInfo.make("Variables", ("h", "cpp"), compilable_output="cpp"), + ScriptInfo.make("Structural", ("hpp", "cpp")), + ScriptInfo.make("SimpleJacobi", ("hpp", "cpp"), compilable_output="cpp"), + ScriptInfo.make("SimpleClasses", ("hpp", "cpp")), + ScriptInfo.make("Variables", ("hpp", "cpp"), compilable_output="cpp"), ]