Skip to content
Snippets Groups Projects
Commit 9c00a711 authored by Frederik Hennig's avatar Frederik Hennig
Browse files

start rebuilding configuration system

parent ba2ff160
No related branches found
No related tags found
1 merge request!2Refactor Configuration System & Extend Documentation
Pipeline #70647 passed
...@@ -39,7 +39,7 @@ testsuite: ...@@ -39,7 +39,7 @@ testsuite:
- pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev" - pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev"
- pip install -e . - pip install -e .
script: script:
- pytest -v - pytest -v --cov=src/pystencilssfg --cov-report=term
- coverage html - coverage html
- coverage xml - coverage xml
coverage: '/TOTAL.*\s+(\d+%)$/' coverage: '/TOTAL.*\s+(\d+%)$/'
......
...@@ -3,6 +3,6 @@ testpaths = src/pystencilssfg tests/ ...@@ -3,6 +3,6 @@ testpaths = src/pystencilssfg tests/
python_files = "test_*.py" python_files = "test_*.py"
# Need to ignore the generator scripts, otherwise they would be executed # Need to ignore the generator scripts, otherwise they would be executed
# during test collection # during test collection
addopts = --doctest-modules --ignore=tests/generator_scripts/scripts --cov=src/pystencilssfg --cov-report=term addopts = --doctest-modules --ignore=tests/generator_scripts/scripts
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
from __future__ import annotations
from typing import Generic, TypeVar, Callable, Any
from abc import ABC
from dataclasses import dataclass, fields
from enum import Enum, auto
Option_T = TypeVar("Option_T")
class Option(Generic[Option_T]):
"""Option descriptor.
This descriptor is used to model configuration options.
It maintains a default value for the option that is used when no value
was specified by the user.
In configuration options, the value `None` stands for `unset`.
It can therefore not be used to set an option to the meaning "not any", or "empty"
- for these, special values need to be used.
"""
def __init__(
self,
default: Option_T,
validator: Callable[[Any, Option_T | None], Option_T | None] | None = None,
) -> None:
self._default = default
self._validator = validator
self._name: str
self._lookup: str
def validate(self, validator: Callable[[Any, Any], Any] | None):
self._validator = validator
return validator
@property
def default(self) -> Option_T:
return self._default
def get(self, obj) -> Option_T:
print("get called")
val = getattr(obj, self._lookup, None)
if val is None:
return self._default
else:
return val
def __set_name__(self, owner, name: str):
self._name = name
self._lookup = f"_{name}"
def __get__(self, obj, objtype=None) -> Option_T | None:
if obj is None:
return None
return getattr(obj, self._lookup, None)
def __set__(self, obj, value: Option_T | None):
if self._validator is not None:
value = self._validator(obj, value)
setattr(obj, self._lookup, value)
class ConfigBase(ABC):
def get_option(self, name: str) -> Any:
"""Get the value set for the specified option, or the option's default value if none has been set."""
descr: Option = type(self).__dict__[name]
return descr.get(self)
def override(self, other: ConfigBase):
for field in fields(self): # type: ignore
fvalue = getattr(self, field.name)
if isinstance(fvalue, ConfigBase): # type: ignore
fvalue.override(getattr(other, field.name))
else:
setattr(self, field.name, getattr(other, field.name))
@dataclass
class FileExtensions(ConfigBase):
header: Option[str] = Option("hpp")
"""File extension for generated header file."""
impl: Option[str | None] = Option(None)
"""File extension for generated implementation file."""
@header.validate
@impl.validate
def _validate_extension(self, ext: str | None) -> str | None:
if ext is not None and ext[0] == ".":
return ext[1:]
return ext
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`.
"""
@dataclass
class SfgCodeStyle(ConfigBase):
indent_width: Option[int] = Option(2)
code_style: Option[str] = Option("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: 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)
"""If set to True, skip formatting using ``clang-format``."""
clang_format_binary: Option[str] = Option("clang-format")
"""Path to the clang-format executable"""
class _GlobalNamespace: ... # noqa: E701
GLOBAL_NAMESPACE = _GlobalNamespace()
@dataclass
class SfgConfig(ConfigBase):
extensions: FileExtensions = FileExtensions()
"""File extensions of the generated files"""
output_mode: Option[SfgOutputMode] = Option(SfgOutputMode.STANDALONE)
"""The generator's output mode; defines which files to generate, and the set of legal file extensions."""
outer_namespace: Option[str | _GlobalNamespace] = Option(GLOBAL_NAMESPACE)
"""The outermost namespace in the generated file. May be a valid C++ nested namespace qualifier
(like ``a::b::c``) or `GLOBAL_NAMESPACE` if no outer namespace should be generated."""
codestyle: SfgCodeStyle = SfgCodeStyle()
"""Options governing the code style used by the code generator"""
output_directory: Option[str] = Option(".")
"""Directory to which the generated files should be written."""
from pystencilssfg.config import SfgConfig, SfgOutputMode, GLOBAL_NAMESPACE
def test_defaults():
cfg = SfgConfig()
assert cfg.get_option("output_mode") == SfgOutputMode.STANDALONE
assert cfg.extensions.get_option("header") == "hpp"
assert cfg.codestyle.get_option("indent_width") == 2
assert cfg.get_option("outer_namespace") is GLOBAL_NAMESPACE
cfg.extensions.impl = ".cu"
assert cfg.extensions.get_option("impl") == "cu"
def test_override():
cfg1 = SfgConfig()
cfg1.outer_namespace = "test"
cfg1.extensions.header = "h"
cfg1.extensions.impl = "c"
cfg1.codestyle.force_clang_format = True
cfg2 = SfgConfig()
cfg2.outer_namespace = GLOBAL_NAMESPACE
cfg2.extensions.header = "hpp"
cfg2.extensions.impl = "cpp"
cfg2.codestyle.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"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment