diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b89453ec319c92bf5a7048f6b787c621771525d0..41e3a70865bdfbd60e238b7911ce99a4c977ec18 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,12 +23,10 @@ typechecker: script: - nox --session typecheck -testsuite: +.testsuite-base: extends: .nox-base stage: "Tests" needs: [] - script: - - nox --session testsuite coverage: '/TOTAL.*\s+(\d+%)$/' artifacts: when: always @@ -40,6 +38,16 @@ testsuite: coverage_format: cobertura path: coverage.xml +"testsuite-py3.10": + extends: .testsuite-base + script: + - nox --session testsuite-3.10 + +"testsuite-py3.13": + extends: .testsuite-base + script: + - nox --session testsuite-3.13 + build-documentation: extends: .nox-base stage: "Documentation" diff --git a/noxfile.py b/noxfile.py index 915ab84dcd70663a7c2c7644212e6aded5484d50..a725b7bafcb43caee4f90e60bc509c1413a061a9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -11,7 +11,14 @@ def add_pystencils_git(session: nox.Session): cache_dir = session.cache_dir pystencils_dir = cache_dir / "pystencils" - if not pystencils_dir.exists(): + if pystencils_dir.exists(): + with session.chdir(pystencils_dir): + session.run_install( + "git", + "pull", + external=True + ) + else: session.run_install( "git", "clone", @@ -50,7 +57,7 @@ def typecheck(session: nox.Session): session.run("mypy", "src/pystencilssfg") -@nox.session(python=["3.10"], tags=["tests"]) +@nox.session(python=["3.10", "3.11", "3.12", "3.13"], tags=["tests"]) def testsuite(session: nox.Session): """Run the testsuite and measure coverage.""" editable_install(session, ["testsuite"]) diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py index 0376145bad86270cc1356603631ab160bee0e6a9..3b858a5d668713e0c433b87d8f893047feade0cf 100644 --- a/src/pystencilssfg/config.py +++ b/src/pystencilssfg/config.py @@ -3,101 +3,27 @@ from __future__ import annotations from argparse import ArgumentParser from types import ModuleType -from typing import Generic, TypeVar, Callable, Any, Sequence -from abc import ABC -from dataclasses import dataclass, fields, field +from typing import Any, Sequence +from dataclasses import dataclass from enum import Enum, auto from os import path from importlib import util as iutil -class SfgConfigException(Exception): ... # noqa: E701 - - -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. +from pystencils.codegen.config import ConfigBase, BasicOption, Category - 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 | None = None, - 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 | None: - return self._default - - def get(self, obj) -> Option_T | None: - 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) - - def __delete__(self, obj): - delattr(obj, self._lookup) - - -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 f in fields(self): # type: ignore - fvalue = getattr(self, f.name) - if isinstance(fvalue, ConfigBase): # type: ignore - fvalue.override(getattr(other, f.name)) - else: - new_val = getattr(other, f.name) - if new_val is not None: - setattr(self, f.name, new_val) +class SfgConfigException(Exception): ... # noqa: E701 @dataclass class FileExtensions(ConfigBase): - """Option category containing output file extensions.""" + """BasicOption category containing output file extensions.""" - header: Option[str] = Option("hpp") + header: BasicOption[str] = BasicOption("hpp") """File extension for generated header file.""" - impl: Option[str] = Option() + impl: BasicOption[str] = BasicOption() """File extension for generated implementation file.""" @header.validate @@ -132,7 +58,7 @@ class OutputMode(Enum): class CodeStyle(ConfigBase): """Options affecting the code style used by the source file generator.""" - indent_width: Option[int] = Option(2) + indent_width: BasicOption[int] = BasicOption(2) """The number of spaces successively nested blocks should be indented with""" # TODO possible future options: @@ -150,20 +76,20 @@ class CodeStyle(ConfigBase): class ClangFormatOptions(ConfigBase): """Options affecting the invocation of ``clang-format`` for automatic code formatting.""" - code_style: Option[str] = Option("file") + code_style: BasicOption[str] = BasicOption("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: Option[bool] = Option(False) + force: BasicOption[bool] = BasicOption(False) """If set to ``True``, abort code generation if ``clang-format`` binary cannot be found.""" - skip: Option[bool] = Option(False) + skip: BasicOption[bool] = BasicOption(False) """If set to ``True``, skip formatting using ``clang-format``.""" - binary: Option[str] = Option("clang-format") + binary: BasicOption[str] = BasicOption("clang-format") """Path to the clang-format executable""" @force.validate @@ -194,7 +120,7 @@ GLOBAL_NAMESPACE = _GlobalNamespace() class SfgConfig(ConfigBase): """Configuration options for the `SourceFileGenerator`.""" - extensions: FileExtensions = field(default_factory=FileExtensions) + extensions: Category[FileExtensions] = Category(FileExtensions()) """File extensions of the generated files Options in this category: @@ -203,7 +129,7 @@ class SfgConfig(ConfigBase): FileExtensions.impl """ - output_mode: Option[OutputMode] = Option(OutputMode.STANDALONE) + output_mode: BasicOption[OutputMode] = BasicOption(OutputMode.STANDALONE) """The generator's output mode; defines which files to generate, and the set of legal file extensions. Possible parameters: @@ -213,7 +139,7 @@ class SfgConfig(ConfigBase): OutputMode.HEADER_ONLY """ - outer_namespace: Option[str | _GlobalNamespace] = Option(GLOBAL_NAMESPACE) + outer_namespace: BasicOption[str | _GlobalNamespace] = BasicOption(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. @@ -221,7 +147,7 @@ class SfgConfig(ConfigBase): GLOBAL_NAMESPACE """ - codestyle: CodeStyle = field(default_factory=CodeStyle) + codestyle: Category[CodeStyle] = Category(CodeStyle()) """Options affecting the code style emitted by pystencils-sfg. Options in this category: @@ -229,7 +155,7 @@ class SfgConfig(ConfigBase): CodeStyle.indent_width """ - clang_format: ClangFormatOptions = field(default_factory=ClangFormatOptions) + clang_format: Category[ClangFormatOptions] = Category(ClangFormatOptions()) """Options governing the code style used by the code generator Options in this category: @@ -240,7 +166,7 @@ class SfgConfig(ConfigBase): ClangFormatOptions.binary """ - output_directory: Option[str] = Option(".") + output_directory: BasicOption[str] = BasicOption(".") """Directory to which the generated files should be written."""