diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5ca7ae3ba9f695c20a0ea0e8292fa10773a7aa18..5331eca41f6a1ef142a74f923f3a79007e18d025 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -39,7 +39,7 @@ testsuite:
     - pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev"
     - pip install -e .
   script:
-    - pytest -v
+    - pytest -v --cov=src/pystencilssfg --cov-report=term
     - coverage html
     - coverage xml
   coverage: '/TOTAL.*\s+(\d+%)$/'
diff --git a/conftest.py b/conftest.py
index e1d0cdda1b64ec9eff93367589fe3a08d975deb7..1c85902fdb05c4799fd18c2b4ac1710fca3ae933 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,4 +1,5 @@
 import pytest
+from os import path
 
 
 @pytest.fixture(autouse=True)
@@ -9,3 +10,11 @@ def prepare_composer(doctest_namespace):
 
     sfg = SfgComposer(SfgContext())
     doctest_namespace["sfg"] = sfg
+
+
+DATA_DIR = path.join(path.split(__file__)[0], "tests/data")
+
+
+@pytest.fixture
+def sample_config_module():
+    return path.join(DATA_DIR, "project_config.py")
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..09b697c6051fd22dc5893980ac87e6a0d978cf50
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1 @@
+**/generated
diff --git a/docs/source/_templates/autosummary/entire_class.rst b/docs/source/_templates/autosummary/entire_class.rst
new file mode 100644
index 0000000000000000000000000000000000000000..8643ade9ab822e8b1ffebc5f3f7ba96db5d0824f
--- /dev/null
+++ b/docs/source/_templates/autosummary/entire_class.rst
@@ -0,0 +1,6 @@
+{{ fullname | escape | underline}}
+
+.. currentmodule:: {{ module }}
+
+.. autoclass:: {{ objname }}
+    :members:
diff --git a/docs/source/_templates/autosummary/recursive_class.rst b/docs/source/_templates/autosummary/recursive_class.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4a8af04df02869e7af736e61f79fa9faa607c6c8
--- /dev/null
+++ b/docs/source/_templates/autosummary/recursive_class.rst
@@ -0,0 +1,26 @@
+{% extends "!autosummary/class.rst" %}
+
+   {% block methods %}
+
+   {% if methods %}
+   .. rubric:: {{ _('Methods') }}
+
+   .. autosummary::
+      :toctree:
+   {% for item in methods %}
+      ~{{ name }}.{{ item }}
+   {%- endfor %}
+   {% endif %}
+   {% endblock %}
+
+   {% block attributes %}
+   {% if attributes %}
+   .. rubric:: {{ _('Attributes') }}
+
+   .. autosummary::
+      :toctree:
+   {% for item in attributes %}
+      ~{{ name }}.{{ item }}
+   {%- endfor %}
+   {% endif %}
+   {% endblock %}
diff --git a/docs/source/api/generation.rst b/docs/source/api/generation.rst
index f15b1170099cfb10c9f333cb6f4540137e162654..36b5ca03e52dabb2301a2ff0b72f5b9624d32fbb 100644
--- a/docs/source/api/generation.rst
+++ b/docs/source/api/generation.rst
@@ -5,13 +5,32 @@ Generator Script Interface
 .. autoclass:: pystencilssfg.SourceFileGenerator
     :members:
 
-.. autoclass:: pystencilssfg.SfgConfiguration
+Configuration
+=============
+
+.. module:: pystencilssfg.config
+
+.. autoclass:: SfgConfig
     :members:
+    :exclude-members: __init__
+    
+
+Categories, Parameter Types, and Special Values
+-----------------------------------------------
+
+.. autoclass:: _GlobalNamespace
+.. autodata:: GLOBAL_NAMESPACE
 
-.. autoclass:: pystencilssfg.SfgOutputMode
+.. autoclass:: OutputMode
     :members:
 
-.. autoclass:: pystencilssfg.SfgCodeStyle
+.. autoclass:: CodeStyle
     :members:
 
-.. autoattribute:: pystencilssfg.configuration.DEFAULT_CONFIG
+.. autoclass:: ClangFormatOptions
+    :members:
+
+Option Descriptors
+------------------
+
+.. autoclass:: Option
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
deleted file mode 100644
index 1ea987fdc32cc22a18c423c970d7bdfd0c8ca0cc..0000000000000000000000000000000000000000
--- a/docs/source/api/index.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-#############
-API Reference
-#############
-
-These pages provide a reference for the public API of *pystencils-sfg*.
-
-.. toctree::
-    :maxdepth: 1
-    
-    generation
-    composer
-    lang
-    ir
-    errors
diff --git a/docs/source/conf.py b/docs/source/conf.py
index c9e2cc3b0f04aa70619b8e10e7e0407c00f0145f..40d5a0fbccc11bddd40d0da96a3dc883b64cdc6e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -27,6 +27,7 @@ extensions = [
     "myst_parser",
     "sphinx.ext.autodoc",
     "sphinx.ext.napoleon",
+    "sphinx.ext.autosummary",
     "sphinx.ext.doctest",
     "sphinx.ext.intersphinx",
     "sphinx_autodoc_typehints",
@@ -42,6 +43,10 @@ source_suffix = {
 }
 master_doc = "index"
 nitpicky = True
+myst_enable_extensions = [
+    "colon_fence",
+    "dollarmath"
+]
 
 
 # -- Options for HTML output -------------------------------------------------
@@ -71,6 +76,7 @@ intersphinx_mapping = {
 
 autodoc_member_order = "bysource"
 autodoc_typehints = "description"
+# autodoc_class_signature = "separated"
 
 #   Doctest Setup
 
diff --git a/docs/source/index.md b/docs/source/index.md
index c65d57700d041c31de6b950d7d5b7a703b27d73d..4e93f0ce207f948be923a24d7943892bf4a48f7f 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -3,9 +3,24 @@
 ```{toctree}
 :maxdepth: 1
 :hidden:
+:caption: User Guide
 
-usage/index
-api/index
+usage/generator_scripts
+usage/project_integration
+usage/tips_n_tricks
+```
+
+
+```{toctree}
+:maxdepth: 1
+:hidden:
+:caption: API Reference
+
+api/generation
+api/composer
+api/lang
+api/ir
+api/errors
 ```
 
 [![pipeline](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/badges/master/pipeline.svg)](https://i10git.cs.fau.de/pycodegen-/pystencils-sfg/commits/master)
@@ -210,4 +225,34 @@ To `#include` them, add the prefix `gen/<target name>`:
 ```
 
 For details on how to add *pystencils-sfg* to your CMake project, refer to
-[CLI and Build System Integration](usage/cli_and_build_system.md).
+[the project integration guide](#guide_project_integration).
+
+## Learn To Use pystencils-sfg
+
+Here is an overview of user guides for pystencils-sfg available on this site.
+A basic understanding of [pystencils](https://pycodegen.pages.i10git.cs.fau.de/pystencils/index.html)
+is required.
+
+```{card} Writing Generator Scripts
+:link: guide:generator_scripts
+:link-type: ref
+
+Learn about *generator scripts*, the primary usage idiom of *pystencils-sfg*:
+Embedd *pystencils*-generated kernels into C++ source files and augment them with
+arbitrary C++ glue code.
+```
+
+```{card} CLI and Build System Integration
+:link: guide_project_integration
+:link-type: ref
+
+Learn how to control code generation from the command line
+and how to embedd *pystencils-sfg* into your build system.
+```
+
+```{card} Tips and Tricks
+:link: guide:tips_n_tricks
+:link-type: ref
+
+A collection of various tricks that might come in handy when working with *pystencils-sfg*.
+```
diff --git a/docs/source/usage/cli_and_build_system.md b/docs/source/usage/cli_and_build_system.md
deleted file mode 100644
index 910b6b630e4f7b4501dd2f4ca0958042f4d43eda..0000000000000000000000000000000000000000
--- a/docs/source/usage/cli_and_build_system.md
+++ /dev/null
@@ -1,78 +0,0 @@
-(guide:cli)=
-# CLI and Build System
-
-## Command Line Interface
-
-*pystencils-sfg* exposes not one, but two command line interfaces:
-The *global CLI* offers a few tools meant to be used by build systems,
-while the *generator script* command line interface is meant for a build system to communicate
-with the code generator during on-the-fly generation.
-
-### Global CLI
-
-The global CLI may be accessed either through the `sfg-cli` shell command, or using `python -m pystencilssfg`.
-
-### Generator Script CLI
-
-The [SourceFileGenerator][pystencilssfg.SourceFileGenerator] evaluates a generator script's command line arguments,
-which can be supplied by the user, but more frequently by the build system.
-
-## CMake Integration
-
-*pystencils-sfg* is shipped with a CMake module for on-the-fly code generation during the CMake build process.
-
-### Add the module
-
-To include the module in your CMake source tree, a separate find module is provided.
-You can use the global CLI to obtain the find module; simply run
-
-```shell
-sfg-cli cmake make-find-module
-```
-
-to create the file `FindPystencilsSfg.cmake` in the current directory.
-Add it to the CMake module path, and load the *pystencils-sfg* module via *find_package*:
-
-```CMake
-find_package( PystencilsSfg )
-```
-
-Make sure to set the `Python_ROOT_DIR` cache variable to point to the correct Python interpreter
-(i.e. the virtual environment you have installed *pystencils-sfg* into).
-
-### Add generator scripts
-
-The primary interaction point in CMake is the function `pystencilssfg_generate_target_sources`,
-with the following signature:
-
-```CMake
-pystencilssfg_generate_target_sources( <target> 
-    SCRIPTS script1.py [script2.py ...]
-    [DEPENDS dependency1.py [dependency2.py...]]
-    [FILE_EXTENSIONS <header-extension> <impl-extension>]
-    [HEADER_ONLY])
-```
-
-It registers the generator scripts `script1.py [script2.py ...]` to be executed at compile time using `add_custom_command`
-and adds their output files to the specified `<target>`.
-Any changes in the generator scripts, or any listed dependency, will trigger regeneration.
-The function takes the following options:
-
- - `SCRIPTS`: A list of generator scripts
- - `DEPENDS`: A list of dependencies for the generator scripts
- - `FILE_EXTENSION`: The desired extensions for the generated files
- - `HEADER_ONLY`: Toggles header-only code generation
-
-### Include generated files
-
-The `pystencils-sfg` CMake module creates a subfolder `sfg_sources/gen` at the root of the build tree
-and writes all generated source files into it. The directory `sfg_sources` is added to the project's include
-path, such that generated header files for a target `<target>` may be included via:
-```C++
-#include "gen/<target>/kernels.h"
-```
-
-### Project Configuration
-
-The *pystencils-sfg* CMake module reads the scoped variable `PystencilsSfg_CONFIGURATOR_SCRIPT` to find
-the *configuration module* that should be passed to the generator scripts.
diff --git a/docs/source/usage/examples/.gitignore b/docs/source/usage/examples/.gitignore
index 26a5736a94d1260044161ac4963b05b3f3407025..97dbcc9ef5b156c49d997a8606b4a3e5ac3b8dd6 100644
--- a/docs/source/usage/examples/.gitignore
+++ b/docs/source/usage/examples/.gitignore
@@ -1,2 +1,3 @@
 *.cpp
-*.h
\ No newline at end of file
+*.h
+*.hpp
\ No newline at end of file
diff --git a/docs/source/usage/examples/guide_generator_scripts/custom_cmdline_args/kernels.py b/docs/source/usage/examples/guide_generator_scripts/custom_cmdline_args/kernels.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5f2f2e4535c12fdbeceb3a9d8e5670480c982a6
--- /dev/null
+++ b/docs/source/usage/examples/guide_generator_scripts/custom_cmdline_args/kernels.py
@@ -0,0 +1,9 @@
+from pystencilssfg import SourceFileGenerator
+from argparse import ArgumentParser
+
+parser = ArgumentParser()
+# set up parser ...
+
+with SourceFileGenerator(keep_unknown_argv=True) as sfg:
+    args = parser.parse_args(sfg.context.argv)
+    ...
diff --git a/docs/source/usage/examples/guide_generator_scripts/inline_config/kernels.py b/docs/source/usage/examples/guide_generator_scripts/inline_config/kernels.py
new file mode 100644
index 0000000000000000000000000000000000000000..f8d3f6d224cebb9ddf8cfaf7ba293aa9ecad07a3
--- /dev/null
+++ b/docs/source/usage/examples/guide_generator_scripts/inline_config/kernels.py
@@ -0,0 +1,8 @@
+from pystencilssfg import SourceFileGenerator, SfgConfig
+
+cfg = SfgConfig()
+cfg.output_directory = "gen_src"
+cfg.codestyle.indent_width = 4
+
+with SourceFileGenerator(cfg) as sfg:
+    ...
diff --git a/docs/source/usage/generator_scripts.md b/docs/source/usage/generator_scripts.md
index 57b9bcdf3e2aa56a942da0d884265cd669d445cc..d4086027ee9e2ec9f4b0bad226aad363f814e040 100644
--- a/docs/source/usage/generator_scripts.md
+++ b/docs/source/usage/generator_scripts.md
@@ -3,7 +3,7 @@
 
 Writing generator scripts is the primary usage idiom of *pystencils-sfg*.
 A generator script is a Python script, say `kernels.py`, which contains *pystencils-sfg*
-code at the top level that, when executed, emits source code to a pair of files `kernels.h`
+code at the top level that, when executed, emits source code to a pair of files `kernels.hpp`
 and `kernels.cpp`. This guide describes how to write such a generator script, its structure, and how
 it can be used to generate code.
 
@@ -23,14 +23,14 @@ To start, place the following code in a Python script, e.g. `kernels.py`:
 
 The source file is constructed within the context manager's managed region.
 During execution of the script, when the region ends, a header/source file pair
-`kernels.h` and `kernels.cpp` will be written to disk next to your script.
+`kernels.hpp` and `kernels.cpp` will be written to disk next to your script.
 Execute the script as-is and inspect the generated files, which will of course still be empty:
 
 ``````{dropdown} Generated Files
 `````{tab-set}
 
-````{tab-item} kernels.h
-```{literalinclude} examples/guide_generator_scripts/01/kernels.h
+````{tab-item} kernels.hpp
+```{literalinclude} examples/guide_generator_scripts/01/kernels.hpp
 ```
 ````
 
@@ -41,31 +41,110 @@ Execute the script as-is and inspect the generated files, which will of course s
 `````
 ``````
 
-<!-- A few notes on configuration:
-
- - The [SourceFileGenerator](#pystencilssfg.SourceFileGenerator) parses the script's command line arguments
-   for configuration options (refer to [CLI and Build System Integration](cli_and_build_system.md)).
-   If you intend to evaluate command-line parameters inside your
-   generator script, read them from `sfg.context.argv` instead of `sys.argv`.
-   There, all arguments meant for the code generator are already removed.
- - The code generator's configuration is consolidated from a global project configuration which may
-   be provided by the build system; a number of command line arguments; and the
-   [SfgConfiguration](#pystencilssfg.SfgConfiguration) provided in the script.
-   The project configuration may safely be overridden by the latter two; however, conflicts
-   between command-line arguments and the configuration defined in the script will cause
-   an exception to be thrown. -->
-
 ## Using the Composer
 
 The object `sfg` constructed in above snippet is an instance of [SfgComposer](#pystencilssfg.composer.SfgComposer).
 The composer is the central part of the user front-end of *pystencils-sfg*.
 It provides an interface for constructing source files that closely mimics
 C++ syntactic structures within Python.
-Here is an overview of its various functions:
+
+::::{dropdown} Composer API Overview
+```{eval-rst}
+.. currentmodule:: pystencilssfg.composer
+```
+
+Structure and Verbatim Code:
+
+```{eval-rst}
+
+.. autosummary::
+  :nosignatures:
+
+  SfgBasicComposer.prelude
+  SfgBasicComposer.include
+  SfgBasicComposer.namespace
+  SfgBasicComposer.code
+  SfgBasicComposer.define_once
+```
+
+Kernels and Kernel Namespaces:
+
+```{eval-rst}
+
+.. autosummary::
+  :nosignatures:
+
+  SfgBasicComposer.kernels
+  SfgBasicComposer.kernel_namespace
+  SfgBasicComposer.kernel_function
+```
+
+Function definition, parameters, and header inclusion:
+
+```{eval-rst}
+
+.. autosummary::
+  :nosignatures:
+
+  SfgBasicComposer.function
+  SfgBasicComposer.params
+  SfgBasicComposer.require
+```
+
+Variables, expressions, and variable initialization:
+
+```{eval-rst}
+
+.. autosummary::
+  :nosignatures:
+
+  SfgBasicComposer.var
+  SfgBasicComposer.vars
+  SfgBasicComposer.expr
+  SfgBasicComposer.init
+  
+  SfgBasicComposer.map_field
+  SfgBasicComposer.set_param
+```
+
+Parameter mappings:
+
+```{eval-rst}
+
+.. autosummary::
+  :nosignatures:
+
+  SfgBasicComposer.set_param
+  SfgBasicComposer.map_field
+  SfgBasicComposer.map_vector
+```
+
+Control Flow:
+
+```{eval-rst}
+
+.. autosummary::
+  :nosignatures:
+
+  SfgBasicComposer.branch
+  SfgBasicComposer.switch
+```
+
+Kernel Invocation:
+
+```{eval-rst}
+
+.. autosummary::
+  :nosignatures:
+
+  SfgBasicComposer.call
+  SfgBasicComposer.cuda_invoke
+```
+::::
 
 ### Includes and Definitions
 
-With [`SfgComposer.include`](#pystencilssfg.composer.SfgBasicComposer.include), the code generator can be instructed to include header files.
+With {any}`include <SfgBasicComposer.include>`, the code generator can be instructed to include header files.
 As in C++, you can use the `<>` delimiters for system headers, and omit them for project headers.
 
 `````{tab-set}
@@ -75,8 +154,8 @@ As in C++, you can use the `<>` delimiters for system headers, and omit them for
 ```
 ````
 
-````{tab-item} kernels.h
-```{literalinclude} examples/guide_generator_scripts/02/kernels.h
+````{tab-item} kernels.hpp
+```{literalinclude} examples/guide_generator_scripts/02/kernels.hpp
 ```
 ````
 
@@ -112,8 +191,8 @@ the [`sfg.kernel_namespace`](#pystencilssfg.composer.SfgBasicComposer.kernel_nam
 ```
 ````
 
-````{tab-item} kernels.h
-```{literalinclude} examples/guide_generator_scripts/03/kernels.h
+````{tab-item} kernels.hpp
+```{literalinclude} examples/guide_generator_scripts/03/kernels.hpp
 ```
 ````
 
@@ -141,8 +220,8 @@ Use `sfg.function` to create a function, and `sfg.call` to call a kernel:
 ```
 ````
 
-````{tab-item} kernels.h
-```{literalinclude} examples/guide_generator_scripts/04/kernels.h
+````{tab-item} kernels.hpp
+```{literalinclude} examples/guide_generator_scripts/04/kernels.hpp
 ```
 ````
 
@@ -171,8 +250,8 @@ such as, for example, `std::span` or `std::vector`, like this:
 ```
 ````
 
-````{tab-item} kernels.h
-```{literalinclude} examples/guide_generator_scripts/05/kernels.h
+````{tab-item} kernels.hpp
+```{literalinclude} examples/guide_generator_scripts/05/kernels.hpp
 ```
 ````
 
@@ -192,3 +271,77 @@ The pystencils-sfg provides modelling support for a number of C++ standard libra
 (see {any}`pystencilssfg.lang.cpp.std`).
 It also provides the necessary infrastructure for modelling the data structures of any C++ framework
 in a similar manner.
+
+
+## Configuration and Invocation
+
+There are several ways to affect the behavior and output of a generator script.
+For one, the `SourceFileGenerator` itself may be configured from the combination of three
+different configuration sources:
+
+- **Inline Configuration:** The generator script may set up an {any}`SfgConfig` object,
+  which is passed to the `SourceFileGenerator` at its creation; see [Inline Configuration](#inline_config)
+- **Command-Line Options:** The `SourceFileGenerator` parses the command line arguments of
+  the generator script to set some of its configuration options; see [Command-Line Options](#cmdline_options)
+- **Project Configuration:** When embedded into a larger project, using a build system such as CMake, generator scripts
+  may be configured globally within that project by the use of a *configuration module*.
+  Settings specified inside that configuration module are always overridden by the former to configuration sources.
+  For details on configuration modules, refer to the guide on [Project and Build System Integration](#guide_project_integration).
+
+(inline_config)=
+### Inline Configuration
+
+To configure the source file generator within your generator script, import the {any}`SfgConfig` from `pystencilssfg`.
+You may then set up the configuration object before passing it to the `SourceFileGenerator` constructor.
+To illustrate, the following snippet alters the code indentation width and changes the output directory
+of the generator script to `gen_src`:
+
+```{literalinclude} examples/guide_generator_scripts/inline_config/kernels.py
+```
+
+(cmdline_options)=
+### Command-Line Options
+
+The `SourceFileGenerator` consumes a number of command-line parameters that may be passed to the script
+on invocation. These include:
+
+- `--sfg-output-dir <path>`: Set the output directory of the generator script. This corresponds to {any}`SfgConfig.output_directory`.
+- `--sfg-file-extensions <exts>`: Set the file extensions used for the generated files;
+  `exts` must be a comma-separated list not containing any spaces. Corresponds to {any}`SfgConfig.extensions`.
+- `--sfg-output-mode <mode>`: Set the output mode of the generator script. Corresponds to {any}`SfgConfig.output_mode`.
+
+If any configuration option is set to conflicting values on the command line and in the inline configuration,
+the generator script will terminate with an error.
+
+You may examine the full set of possible command line parameters by invoking a generator script
+with the `--help` flag:
+
+```bash
+$ python kernels.py --help
+```
+
+## Adding Custom Command-Line Options
+
+Sometimes, you might want to add your own command-line options to a generator script
+in order to affect its behavior from the shell,
+for instance by using {any}`argparse` to set up an argument parser.
+If you parse your options directly from {any}`sys.argv`,
+as {any}`parse_args <argparse.ArgumentParser.parse_args>` does by default,
+your parser will also receive any options meant for the `SourceFileGenerator`.
+To filter these out of the argument list,
+pass the additional option `keep_unknown_argv=True` to your `SourceFileGenerator`.
+This will instruct it to store any unknown command line arguments into `sfg.context.argv`,
+where you can then retrieve them from and pass on to your custom parser:
+
+```{literalinclude} examples/guide_generator_scripts/custom_cmdline_args/kernels.py
+```
+
+Any SFG-specific arguments will already have been filtered out of this argument list.
+As a consequence of the above, if the generator script is invoked with a typo in some SFG-specific argument,
+which the `SourceFileGenerator` therefore does not recognize,
+that argument will be passed on to your downstream parser instead.
+
+:::{important}
+If you do *not* pass on `sfg.context.argv` to a downstream parser, make sure that `keep_unknown_argv` is set to
+`False` (which is the default), such that typos or illegal arguments will not be ignored.
+:::
diff --git a/docs/source/usage/index.md b/docs/source/usage/index.md
deleted file mode 100644
index 5783e9ebe71faade1b69dd233595eb8dcb3710ce..0000000000000000000000000000000000000000
--- a/docs/source/usage/index.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Usage Guides
-
-```{toctree}
-:maxdepth: 1
-:hidden:
-
-generator_scripts
-cli_and_build_system
-tips_n_tricks
-```
-
-These pages provide an overview of how to use the pystencils Source File Generator.
-A basic understanding of [pystencils](https://pycodegen.pages.i10git.cs.fau.de/pystencils/index.html)
-is required.
-
-```{card} Writing Generator Scripts
-:link: guide:generator_scripts
-:link-type: ref
-
-Learn about *generator scripts*, the primary usage idiom of *pystencils-sfg*:
-Embedd *pystencils*-generated kernels into C++ source files and augment them with
-arbitrary C++ glue code.
-```
-
-```{card} CLI and Build System Integration
-:link: guide:cli
-:link-type: ref
-
-Learn how to control code generation from the command line
-and how to embedd *pystencils-sfg* into your build system.
-```
-
-```{card} Tips and Tricks
-:link: guide:tips_n_tricks
-:link-type: ref
-
-A collection of various tricks that might come in handy when working with *pystencils-sfg*.
-```
diff --git a/docs/source/usage/project_integration.md b/docs/source/usage/project_integration.md
new file mode 100644
index 0000000000000000000000000000000000000000..19809a616175bee796a435b440951dffc07199b2
--- /dev/null
+++ b/docs/source/usage/project_integration.md
@@ -0,0 +1,120 @@
+(guide_project_integration)=
+# Project and Build System Integration
+
+(config_module)=
+## Project-Wide Settings using Configuration Modules
+
+When embedding *pystencils-sfg* into a C++ project or build system,
+you might want to set a project-wide base configuration for all generator scripts.
+In addition, it might be necessary to pass various details about the project
+and build setup to the generator scripts.
+Both can be achieved by the use of a *configuration module*.
+
+A configuration module is a Python file that defines up to two functions:
+- `def configure_sfg(cfg: SfgConfig)` is called to set up the project-wide base configuration.
+   It takes an {any}`SfgConfig` object which it may modify to establish the project-wide option set.
+- `def project_info() -> Any` is called by *pystencils-sfg* to retrieve an object that encapsulates
+  any custom project-specific information.
+  This information is passed on to the generator scripts through
+  the {any}`sfg.context.project_info <SfgContext.project_info>` attribute.
+
+An example configuration module might look like this:
+
+```Python
+from pystencilssfg import SfgConfig
+
+def configure_sfg(cfg: SfgConfig):
+    cfg.extensions.header = "h++"
+    cfg.extensions.impl = "c++"
+    cfg.clang_format.code_style = "llvm"
+    ...
+
+def project_info():
+    return {
+        "project_name": "my-project",
+        "float_precision": "float32",
+        "use_cuda": False,
+        ...
+    }
+```
+
+Here, `project_info` returns a dictionary, but this is just for illustration;
+the function may return any type of arbitrarily complex objects.
+For improved API safety, {any}`dataclasses` might be a good tool for setting up
+project info objects.
+
+When invoking a generator script, the path to the current configuration module must be passed to it
+using the `--sfg-config-module` command-line parameter.
+This can be automated by an adequately set up build system, such as GNU Make or CMake.
+
+If you are using pystencils-sfg with CMake through the provided CMake module,
+[see below](#cmake_set_config_module) on how to specify a configuration module for your project.
+
+(cmake_integration)=
+## CMake Integration
+
+*pystencils-sfg* is shipped with a CMake module for on-the-fly code generation during the CMake build process.
+
+### Add the module
+
+To include the module in your CMake source tree, a separate find module is provided.
+You can use the global CLI to obtain the find module; simply run
+
+```shell
+sfg-cli cmake make-find-module
+```
+
+to create the file `FindPystencilsSfg.cmake` in the current directory.
+Add it to the CMake module path, and load the *pystencils-sfg* module via *find_package*:
+
+```CMake
+find_package( PystencilsSfg )
+```
+
+Make sure to set the `Python_ROOT_DIR` cache variable to point to the correct Python interpreter
+(i.e. the virtual environment you have installed *pystencils-sfg* into).
+
+### Add generator scripts
+
+The primary interaction point in CMake is the function `pystencilssfg_generate_target_sources`,
+with the following signature:
+
+```CMake
+pystencilssfg_generate_target_sources( <target> 
+    SCRIPTS script1.py [script2.py ...]
+    [DEPENDS dependency1.py [dependency2.py...]]
+    [FILE_EXTENSIONS <header-extension> <impl-extension>]
+    [OUTPUT_MODE <standalone|inline|header-only>]
+)
+```
+
+It registers the generator scripts `script1.py [script2.py ...]` to be executed at compile time using `add_custom_command`
+and adds their output files to the specified `<target>`.
+Any changes in the generator scripts, or any listed dependency, will trigger regeneration.
+The function takes the following options:
+
+ - `SCRIPTS`: A list of generator scripts
+ - `DEPENDS`: A list of dependencies for the generator scripts
+ - `FILE_EXTENSION`: The desired extensions for the generated files
+ - `OUTPUT_MODE`: Sets the output mode of the code generator; see {any}`SfgConfig.output_mode`.
+
+### Include generated files
+
+The `pystencils-sfg` CMake module creates a subfolder `sfg_sources/gen` at the root of the build tree
+and writes all generated source files into it. The directory `sfg_sources` is added to the project's include
+path, such that generated header files for a target `<target>` may be included via:
+```C++
+#include "gen/<target>/kernels.h"
+```
+
+(cmake_set_config_module)=
+### Set a Configuration Module
+
+To specify a [configuration module](#config_module) for your project,
+set the scoped variable `PystencilsSfg_CONFIG_MODULE` to point at the respective Python file.
+The pystencils-sfg CMake system will then pass that module to each generator script invocation.
+
+You might want to populate your configuration module with information about the current
+build setup and environment.
+For this purpose, take a look at the
+[configure_file](https://cmake.org/cmake/help/latest/command/configure_file.html) CMake function.
diff --git a/integration/CMakeDemo/CMakeLists.txt b/integration/CMakeDemo/CMakeLists.txt
index 6642f40e63368d7ad60b4dd602eaab8e737c1c71..b69ff9aec9fe93af6d5364edb80cff67a51a5ba0 100644
--- a/integration/CMakeDemo/CMakeLists.txt
+++ b/integration/CMakeDemo/CMakeLists.txt
@@ -11,7 +11,7 @@ execute_process( COMMAND sfg-cli cmake make-find-module
 
 find_package( PystencilsSfg REQUIRED )
 
-set( PystencilsSfg_CONFIGURATOR_SCRIPT codegen_config.py )
+set( PystencilsSfg_CONFIG_MODULE codegen_config.py )
 
 add_library( genlib )
 pystencilssfg_generate_target_sources( genlib SCRIPTS kernels.py FILE_EXTENSIONS .h .cpp )
diff --git a/integration/CMakeDemo/codegen_config.py b/integration/CMakeDemo/codegen_config.py
index 2f1231a95d4050b994235b4dd56e964819d774c0..8162ae460328ab0229f422c3ec9d63522106a78b 100644
--- a/integration/CMakeDemo/codegen_config.py
+++ b/integration/CMakeDemo/codegen_config.py
@@ -1,16 +1,6 @@
-from sys import stderr
-from pystencilssfg import SfgConfiguration
+from pystencilssfg import SfgConfig
 
-def sfg_config():
-    print("sfg_config() called!", file=stderr)
 
-    project_info = {
-        'B': 'A'
-    }
-
-    return SfgConfiguration(
-        header_extension='hpp',
-        impl_extension='cpp',
-        outer_namespace='cmake_demo',
-        project_info=project_info
-    )
+def configure(cfg: SfgConfig):
+    cfg.extensions.header = "h++"
+    cfg.extensions.impl = "c++"
diff --git a/pyproject.toml b/pyproject.toml
index 48795fa3ab5b83ba7bd54b191a2531c00031ddeb..0a3cde6b11c7e282bb05bff349cb75c21f7bec80 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,9 +23,6 @@ requires = [
 build-backend = "setuptools.build_meta"
 
 [project.optional-dependencies]
-interactive = [
-    "ipython>=8.17.2",
-]
 testing = [
     "flake8>=6.1.0",
     "mypy>=1.7.0",
@@ -39,7 +36,8 @@ docs = [
     "sphinx_design",
     "sphinx_autodoc_typehints",
     "sphinx-copybutton",
-    "packaging"
+    "packaging",
+    "clang-format"
 ]
 
 [tool.versioneer]
@@ -51,4 +49,8 @@ tag_prefix = "v"
 parentdir_prefix = "pystencilssfg-"
 
 [tool.coverage.run]
-include = ["src/pystencilssfg/*"]
+omit = [
+    "setup.py",
+    "src/pystencilssfg/_version.py",
+    "integration/*"
+]
diff --git a/pytest.ini b/pytest.ini
index 8eb047cc9f4e18d995e09941d9e5d30a2ef0ff41..7dc0d3812dee41d0511ea766ec4e8b032760bf60 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -3,6 +3,10 @@ testpaths = src/pystencilssfg tests/
 python_files = "test_*.py"
 #   Need to ignore the generator scripts, otherwise they would be executed
 #   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
+    --ignore=tests/generator_scripts/config
+    --ignore=tests/data
 
 doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
diff --git a/src/pystencilssfg/__init__.py b/src/pystencilssfg/__init__.py
index b5ac38f9c9487c6b8caf77a72cda55ea7fc1e792..b2def3b84ba3eab0706b095878a9c165322c1d8b 100644
--- a/src/pystencilssfg/__init__.py
+++ b/src/pystencilssfg/__init__.py
@@ -1,16 +1,16 @@
-from .configuration import SfgConfiguration, SfgOutputMode, SfgCodeStyle
-from .generator import SourceFileGenerator
+from .config import SfgConfig
+from .generator import SourceFileGenerator, GLOBAL_NAMESPACE, OutputMode
 from .composer import SfgComposer
 from .context import SfgContext
 from .lang import SfgVar, AugExpr
 from .exceptions import SfgException
 
 __all__ = [
+    "SfgConfig",
+    "GLOBAL_NAMESPACE",
+    "OutputMode",
     "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/cmake/modules/PystencilsSfg.cmake b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
index deb97ce841f394ddf4ff31f2e326ce18f91706cd..c42a251fda1c4201023e8150f9e35c4427e532c8 100644
--- a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
+++ b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
@@ -54,11 +54,22 @@ function(pystencilssfg_generate_target_sources TARGET)
     endif()
 
     if(DEFINED PystencilsSfg_CONFIGURATOR_SCRIPT)
+        message(AUTHOR_WARNING "The variable PystencilsSfg_CONFIGURATOR_SCRIPT is deprecated. Set PystencilsSfg_CONFIG_MODULE instead.")
         cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIGURATOR_SCRIPT OUTPUT_VARIABLE configscript)
         list(APPEND generatorArgs "--sfg-config-module=${configscript}")
         list(APPEND _pssfg_DEPENDS ${configscript})
     endif()
 
+    if(DEFINED PystencilsSfg_CONFIG_MODULE)
+        if(DEFINED PystencilsSfg_CONFIGURATOR_SCRIPT)
+            message(FATAL_ERROR "At most one of PystencilsSfg_CONFIGURATOR_SCRIPT and PystencilsSfg_CONFIG_MODULE may be set.")
+        endif()
+
+        cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIG_MODULE OUTPUT_VARIABLE config_module)
+        list(APPEND generatorArgs "--sfg-config-module=${config_module}")
+        list(APPEND _pssfg_DEPENDS ${config_module})
+    endif()
+
     if(DEFINED _pssfg_FILE_EXTENSIONS)
         string(JOIN "," extensionsString ${_pssfg_FILE_EXTENSIONS})
 
diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py
index 15177e6d768e3e568d8f4d30785d87344520b4b4..d54659626d6c23f36d24b62ea59bdb5e04c7470b 100644
--- a/src/pystencilssfg/composer/basic_composer.py
+++ b/src/pystencilssfg/composer/basic_composer.py
@@ -319,6 +319,7 @@ class SfgBasicComposer(SfgIComposer):
         return SfgFunctionParams([x.as_variable() for x in args])
 
     def require(self, *includes: str | SfgHeaderInclude) -> SfgRequireIncludes:
+        """Use inside a function body to require the inclusion of headers."""
         return SfgRequireIncludes(
             list(SfgHeaderInclude.parse(incl) for incl in includes)
         )
@@ -443,6 +444,7 @@ class SfgBasicComposer(SfgIComposer):
         return SfgBranchBuilder()
 
     def switch(self, switch_arg: ExprLike) -> SfgSwitchBuilder:
+        """Use inside a function to construct a switch-case statement."""
         return SfgSwitchBuilder(switch_arg)
 
     def map_field(
diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..aef3e78e1ab1488bb4cab3da1e3822e26d2c080e
--- /dev/null
+++ b/src/pystencilssfg/config.py
@@ -0,0 +1,398 @@
+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 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.
+
+    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)
+
+
+@dataclass
+class FileExtensions(ConfigBase):
+    """Option category containing output file extensions."""
+
+    header: Option[str] = Option("hpp")
+    """File extension for generated header file."""
+
+    impl: Option[str] = Option()
+    """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 OutputMode(Enum):
+    """Output mode of the source file generator."""
+
+    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 CodeStyle(ConfigBase):
+    """Options affecting the code style used by the source file generator."""
+
+    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):
+    """Options affecting the invocation of ``clang-format`` for automatic code formatting."""
+
+    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: Option[bool] = Option(False)
+    """If set to ``True``, abort code generation if ``clang-format`` binary cannot be found."""
+
+    skip: Option[bool] = Option(False)
+    """If set to ``True``, skip formatting using ``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
+
+
+GLOBAL_NAMESPACE = _GlobalNamespace()
+"""Indicates the C++ global namespace."""
+
+
+@dataclass
+class SfgConfig(ConfigBase):
+    """Configuration options for the `SourceFileGenerator`."""
+
+    extensions: FileExtensions = field(default_factory=FileExtensions)
+    """File extensions of the generated files
+
+    Options in this category:
+        .. autosummary::
+            FileExtensions.header
+            FileExtensions.impl
+    """
+
+    output_mode: Option[OutputMode] = Option(OutputMode.STANDALONE)
+    """The generator's output mode; defines which files to generate, and the set of legal file extensions.
+
+    Possible parameters:
+        .. autosummary::
+            OutputMode.STANDALONE
+            OutputMode.INLINE
+            OutputMode.HEADER_ONLY
+    """
+
+    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.
+
+    .. autosummary::
+        GLOBAL_NAMESPACE
+    """
+
+    codestyle: CodeStyle = field(default_factory=CodeStyle)
+    """Options affecting the code style emitted by pystencils-sfg.
+
+    Options in this category:
+        .. autosummary::
+            CodeStyle.indent_width
+    """
+
+    clang_format: ClangFormatOptions = field(default_factory=ClangFormatOptions)
+    """Options governing the code style used by the code generator
+
+    Options in this category:
+        .. autosummary::
+            ClangFormatOptions.code_style
+            ClangFormatOptions.force
+            ClangFormatOptions.skip
+            ClangFormatOptions.binary
+    """
+
+    output_directory: Option[str] = Option(".")
+    """Directory to which the generated files should be written."""
+
+
+class CommandLineParameters:
+    @staticmethod
+    def add_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="config_module_path"
+        )
+
+        return parser
+
+    def __init__(self, args) -> None:
+        self._cl_config_module_path: str | None = args.config_module_path
+
+        if args.output_mode is not None:
+            match args.output_mode.lower():
+                case "standalone":
+                    output_mode = OutputMode.STANDALONE
+                case "inline":
+                    output_mode = OutputMode.INLINE
+                case "header-only":
+                    output_mode = OutputMode.HEADER_ONLY
+                case _:
+                    assert False, "invalid output mode"
+        else:
+            output_mode = None
+
+        self._cl_output_mode = output_mode
+
+        self._cl_output_dir: str | None = args.output_directory
+
+        if args.file_extensions is not None:
+            file_extentions = list(args.file_extensions.split(","))
+            h_ext, impl_ext = self._get_file_extensions(file_extentions)
+            self._cl_header_ext = h_ext
+            self._cl_impl_ext = impl_ext
+        else:
+            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
+            )
+        else:
+            self._config_module = None
+
+    @property
+    def configuration_module(self) -> ModuleType | None:
+        return self._config_module
+
+    def get_config(self) -> SfgConfig:
+        cfg = SfgConfig()
+        if self._config_module is not None and hasattr(
+            self._config_module, "configure_sfg"
+        ):
+            self._config_module.configure_sfg(cfg)
+
+        if self._cl_output_mode is not None:
+            cfg.output_mode = self._cl_output_mode
+        if self._cl_header_ext is not None:
+            cfg.extensions.header = self._cl_header_ext
+        if self._cl_impl_ext is not None:
+            cfg.extensions.impl = self._cl_impl_ext
+        if self._cl_output_dir is not None:
+            cfg.output_directory = self._cl_output_dir
+
+        return cfg
+
+    def find_conflicts(self, cfg: SfgConfig):
+        for name, mine, theirs in (
+            ("output_mode", self._cl_output_mode, cfg.output_mode),
+            ("extensions.header", self._cl_header_ext, cfg.extensions.header),
+            ("extensions.impl", self._cl_impl_ext, cfg.extensions.impl),
+            ("output_directory", self._cl_output_dir, cfg.output_directory),
+        ):
+            if mine is not None and theirs is not None and mine != theirs:
+                raise SfgConfigException(
+                    f"Conflicting values given for option {name} on command line and inside generator script.\n"
+                    f"    Value on command-line: {name}",
+                    f"    Value in script: {name}",
+                )
+
+    def get_project_info(self) -> Any:
+        if self._config_module is not None and hasattr(
+            self._config_module, "project_info"
+        ):
+            return self._config_module.project_info()
+        else:
+            return None
+
+    def _get_file_extensions(self, extensions: Sequence[str]):
+        h_ext = None
+        src_ext = None
+
+        extensions = tuple(ext.strip() for ext in extensions)
+        extensions = tuple((ext[1:] if ext[0] == "." else ext) for ext in extensions)
+
+        HEADER_FILE_EXTENSIONS = {"h", "hpp", "hxx", "h++", "cuh"}
+        IMPL_FILE_EXTENSIONS = {"c", "cpp", "cxx", "c++", "cu", ".impl.h", "ipp"}
+
+        for ext in extensions:
+            if ext in HEADER_FILE_EXTENSIONS:
+                if h_ext is not None:
+                    raise SfgConfigException(
+                        "Multiple header file extensions specified."
+                    )
+                h_ext = ext
+            elif ext in IMPL_FILE_EXTENSIONS:
+                if src_ext is not None:
+                    raise SfgConfigException(
+                        "Multiple source file extensions specified."
+                    )
+                src_ext = ext
+            else:
+                raise SfgConfigException(
+                    f"Invalid file extension: Don't know what to do with '.{ext}'"
+                )
+
+        return h_ext, src_ext
+
+    def _import_config_module(self, module_path: str) -> ModuleType:
+        cfg_modulename = path.splitext(path.split(module_path)[1])[0]
+
+        cfg_spec = iutil.spec_from_file_location(cfg_modulename, module_path)
+
+        if cfg_spec is None:
+            raise SfgConfigException(
+                f"Unable to import configuration module {module_path}",
+            )
+
+        config_module = iutil.module_from_spec(cfg_spec)
+        cfg_spec.loader.exec_module(config_module)  # type: ignore
+        return config_module
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/context.py b/src/pystencilssfg/context.py
index bd3591889cbed6ab2a4bc023787944857585a601..17537a26706be5e5a9c93bc9cc5c09bb13c48dff 100644
--- a/src/pystencilssfg/context.py
+++ b/src/pystencilssfg/context.py
@@ -1,6 +1,6 @@
 from typing import Generator, Sequence, Any
 
-from .configuration import SfgCodeStyle
+from .config import CodeStyle
 from .ir.source_components import (
     SfgHeaderInclude,
     SfgKernelNamespace,
@@ -45,7 +45,7 @@ class SfgContext:
     def __init__(
         self,
         outer_namespace: str | None = None,
-        codestyle: SfgCodeStyle = SfgCodeStyle(),
+        codestyle: CodeStyle | None = None,
         argv: Sequence[str] | None = None,
         project_info: Any = None,
     ):
@@ -65,7 +65,7 @@ class SfgContext:
         self._outer_namespace = outer_namespace
         self._inner_namespace: str | None = None
 
-        self._codestyle = codestyle
+        self._codestyle = codestyle if codestyle is not None else CodeStyle()
 
         #   Source Components
         self._prelude: str = ""
@@ -121,7 +121,7 @@ class SfgContext:
                 assert False
 
     @property
-    def codestyle(self) -> SfgCodeStyle:
+    def codestyle(self) -> CodeStyle:
         """The code style object for this generation context."""
         return self._codestyle
 
diff --git a/src/pystencilssfg/emission/__init__.py b/src/pystencilssfg/emission/__init__.py
index 3280e459ff72a23bea06e711e5fb0aa73989cc74..fd666283edd12d49c02eb42eee90a5ae8f9756dd 100644
--- a/src/pystencilssfg/emission/__init__.py
+++ b/src/pystencilssfg/emission/__init__.py
@@ -1,5 +1,5 @@
-from .emitter import AbstractEmitter
+from .emitter import AbstractEmitter, OutputSpec
 from .header_impl_pair import HeaderImplPairEmitter
 from .header_only import HeaderOnlyEmitter
 
-__all__ = ["AbstractEmitter", "HeaderImplPairEmitter", "HeaderOnlyEmitter"]
+__all__ = ["AbstractEmitter", "OutputSpec", "HeaderImplPairEmitter", "HeaderOnlyEmitter"]
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 55fe43c337069e49d38ed27a85e2f33929dfdc54..c32b18af351579b98477bf688c0789a394fc3732 100644
--- a/src/pystencilssfg/emission/emitter.py
+++ b/src/pystencilssfg/emission/emitter.py
@@ -1,7 +1,63 @@
+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
+class OutputSpec:
+    """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())
+
+    @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):
diff --git a/src/pystencilssfg/emission/header_impl_pair.py b/src/pystencilssfg/emission/header_impl_pair.py
index 8d2cd2cf437e0dfbfa8527d6f288d291c27e0a83..d70cfd255e5df6d01169eea50fe4ac87bba37bdf 100644
--- a/src/pystencilssfg/emission/header_impl_pair.py
+++ b/src/pystencilssfg/emission/header_impl_pair.py
@@ -1,19 +1,24 @@
 from typing import Sequence
 from os import path, makedirs
 
-from ..configuration import SfgOutputSpec
 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
+from .emitter import AbstractEmitter, OutputSpec
 
 
 class HeaderImplPairEmitter(AbstractEmitter):
     """Emits a header-implementation file pair."""
 
-    def __init__(self, output_spec: SfgOutputSpec, 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
@@ -22,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]:
@@ -42,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 7347a61059ac48ad2f5b7c320aa3a2965c5b8452..ce6ce90f609f110153c5d5ae753d0d511540ab18 100644
--- a/src/pystencilssfg/emission/header_only.py
+++ b/src/pystencilssfg/emission/header_only.py
@@ -1,23 +1,26 @@
 from typing import Sequence
 from os import path, makedirs
 
-from ..configuration import SfgOutputSpec
 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
+from .emitter import AbstractEmitter, OutputSpec
 
 
 class HeaderOnlyEmitter(AbstractEmitter):
-    def __init__(self, output_spec: SfgOutputSpec):
+    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]:
@@ -29,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 aa8396c4ecae7471c333a14587435ac3f35eec3a..cc9cd6252604cd9bedb42a995c7c242a61e117d4 100644
--- a/src/pystencilssfg/generator.py
+++ b/src/pystencilssfg/generator.py
@@ -1,25 +1,17 @@
-# TODO
-# mypy strict_optional=False
-
-import sys
 import os
 from os import path
 
-from .configuration import (
-    SfgConfiguration,
-    SfgOutputMode,
-    config_from_commandline,
-    merge_configurations,
-)
+from .config import SfgConfig, CommandLineParameters, OutputMode, GLOBAL_NAMESPACE
 from .context import SfgContext
 from .composer import SfgComposer
-from .emission import AbstractEmitter
+from .emission import AbstractEmitter, OutputSpec
+from .exceptions import SfgException
 
 
 class SourceFileGenerator:
     """Context manager that controls the code generation process in generator scripts.
 
-    **Usage:** The `SourceFileGenerator` must be used as a context manager by calling it within
+    The `SourceFileGenerator` must be used as a context manager by calling it within
     a ``with`` statement in the top-level code of a generator script (see :ref:`guide:generator_scripts`).
     Upon entry to its context, it creates an :class:`SfgComposer` which can be used to populate the generated files.
     When the managed region finishes, the code files are generated and written to disk at the locations
@@ -27,35 +19,59 @@ class SourceFileGenerator:
     Existing copies of the target files are deleted on entry to the managed region,
     and if an exception occurs within the managed region, no files are exported.
 
-    **Configuration:** The `SourceFileGenerator` optionally takes a user-defined configuration
-    object which is merged with configuration obtained from the build system; for details
-    on configuration sources, refer to :class:`SfgConfiguration`.
-
     Args:
-        sfg_config: User configuration for the code generator
+        sfg_config: Inline configuration for the code generator
+        keep_unknown_argv: If `True`, any command line arguments given to the generator script
+            that the `SourceFileGenerator` does not understand are stored in
+            `sfg.context.argv`.
     """
 
-    def __init__(self, sfg_config: SfgConfiguration | None = None):
-        if sfg_config and not isinstance(sfg_config, SfgConfiguration):
+    def __init__(
+        self, sfg_config: SfgConfig | None = None, keep_unknown_argv: bool = False
+    ):
+        if sfg_config and not isinstance(sfg_config, SfgConfig):
             raise TypeError("sfg_config is not an SfgConfiguration.")
 
         import __main__
 
+        if not hasattr(__main__, "__file__"):
+            raise SfgException(
+                "Invalid execution environment: "
+                "It seems that you are trying to run the `SourceFileGenerator` in an environment "
+                "without a valid entry point, such as a REPL or a multiprocessing fork."
+            )
+
         scriptpath = __main__.__file__
         scriptname = path.split(scriptpath)[1]
         basename = path.splitext(scriptname)[0]
 
-        project_config, cmdline_config, script_args = config_from_commandline(sys.argv)
+        from argparse import ArgumentParser
+
+        parser = ArgumentParser(
+            scriptname,
+            description="Generator script using pystencils-sfg",
+            allow_abbrev=False,
+        )
+        CommandLineParameters.add_args_to_parser(parser)
+
+        if keep_unknown_argv:
+            sfg_args, script_args = parser.parse_known_args()
+        else:
+            sfg_args = parser.parse_args()
+            script_args = []
 
-        config = merge_configurations(project_config, cmdline_config, sfg_config)
+        cli_params = CommandLineParameters(sfg_args)
 
-        assert config.codestyle is not None
+        config = cli_params.get_config()
+        if sfg_config is not None:
+            cli_params.find_conflicts(sfg_config)
+            config.override(sfg_config)
 
         self._context = SfgContext(
-            config.outer_namespace,
+            None if config.outer_namespace is GLOBAL_NAMESPACE else config.outer_namespace,  # type: ignore
             config.codestyle,
             argv=script_args,
-            project_info=config.project_info,
+            project_info=cli_params.get_project_info(),
         )
 
         from pystencilssfg.ir import SfgHeaderInclude
@@ -63,22 +79,29 @@ class SourceFileGenerator:
         self._context.add_include(SfgHeaderInclude("cstdint", system_header=True))
         self._context.add_definition("#define RESTRICT __restrict__")
 
+        output_mode = config.get_option("output_mode")
+        output_spec = OutputSpec.create(config, basename)
+
         self._emitter: AbstractEmitter
-        match config.output_mode:
-            case SfgOutputMode.HEADER_ONLY:
+        match output_mode:
+            case OutputMode.HEADER_ONLY:
                 from .emission import HeaderOnlyEmitter
 
-                self._emitter = HeaderOnlyEmitter(config.get_output_spec(basename))
-            case SfgOutputMode.INLINE:
+                self._emitter = HeaderOnlyEmitter(
+                    output_spec, clang_format=config.clang_format
+                )
+            case OutputMode.INLINE:
                 from .emission import HeaderImplPairEmitter
 
                 self._emitter = HeaderImplPairEmitter(
-                    config.get_output_spec(basename), inline_impl=True
+                    output_spec, inline_impl=True, clang_format=config.clang_format
                 )
-            case SfgOutputMode.STANDALONE:
+            case OutputMode.STANDALONE:
                 from .emission import HeaderImplPairEmitter
 
-                self._emitter = HeaderImplPairEmitter(config.get_output_spec(basename))
+                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/data/project_config.py b/tests/data/project_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..03c11b60868e9231e14eff2bba5fa88765564019
--- /dev/null
+++ b/tests/data/project_config.py
@@ -0,0 +1,22 @@
+from pystencilssfg import SfgConfig
+
+
+def configure_sfg(cfg: SfgConfig):
+    cfg.codestyle.indent_width = 3
+    cfg.clang_format.code_style = "llvm"
+    cfg.clang_format.skip = True
+    cfg.output_directory = "generated_sources"
+    cfg.outer_namespace = "myproject"
+    cfg.extensions.header = "hpp"
+
+
+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
new file mode 100644
index 0000000000000000000000000000000000000000..4485dc22e639b185b8b0756ae6d69f92af5e45e8
--- /dev/null
+++ b/tests/generator/test_config.py
@@ -0,0 +1,110 @@
+import pytest
+
+from pystencilssfg.config import (
+    SfgConfig,
+    OutputMode,
+    GLOBAL_NAMESPACE,
+    CommandLineParameters,
+    SfgConfigException
+)
+
+
+def test_defaults():
+    cfg = SfgConfig()
+
+    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"
+    assert cfg.extensions.get_option("impl") == "cu"
+
+    #   Check that section subobjects of different config objects are independent
+    #   -> must use default_factory to construct them, because they are mutable!
+    cfg.clang_format.binary = "bogus"
+
+    cfg2 = SfgConfig()
+    assert cfg2.clang_format.binary is None
+
+
+def test_override():
+    cfg1 = SfgConfig()
+    cfg1.outer_namespace = "test"
+    cfg1.extensions.header = "h"
+    cfg1.extensions.impl = "c"
+    cfg1.clang_format.force = True
+
+    cfg2 = SfgConfig()
+    cfg2.outer_namespace = GLOBAL_NAMESPACE
+    cfg2.extensions.header = "hpp"
+    cfg2.extensions.impl = "cpp"
+    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.indent_width is None
+    assert cfg1.clang_format.force is True
+    assert cfg1.clang_format.code_style is None
+    assert cfg1.clang_format.binary == "bogus"
+
+
+def test_sanitation():
+    cfg = SfgConfig()
+
+    cfg.extensions.header = ".hxx"
+    assert cfg.extensions.header == "hxx"
+
+    cfg.extensions.header = ".cxx"
+    assert cfg.extensions.header == "cxx"
+
+    cfg.clang_format.force = True
+    with pytest.raises(SfgConfigException):
+        cfg.clang_format.skip = True
+
+    cfg.clang_format.force = False
+    cfg.clang_format.skip = True
+    with pytest.raises(SfgConfigException):
+        cfg.clang_format.force = True
+
+
+def test_from_commandline(sample_config_module):
+    from argparse import ArgumentParser
+
+    parser = ArgumentParser()
+    CommandLineParameters.add_args_to_parser(parser)
+
+    args = parser.parse_args(
+        ["--sfg-output-dir", ".out", "--sfg-file-extensions", ".h++,c++"]
+    )
+
+    cli_args = CommandLineParameters(args)
+    cfg = cli_args.get_config()
+
+    assert cfg.output_directory == ".out"
+    assert cfg.extensions.header == "h++"
+    assert cfg.extensions.impl == "c++"
+
+    args = parser.parse_args(
+        ["--sfg-output-dir", "gen_sources", "--sfg-config-module", sample_config_module]
+    )
+    cli_args = CommandLineParameters(args)
+    cfg = cli_args.get_config()
+
+    assert cfg.codestyle.indent_width == 3
+    assert cfg.clang_format.code_style == "llvm"
+    assert cfg.clang_format.skip is True
+    assert (
+        cfg.output_directory == "gen_sources"
+    )  # value from config module overridden by commandline
+    assert cfg.outer_namespace == "myproject"
+    assert cfg.extensions.header == "hpp"
+
+    assert cli_args.configuration_module is not None
+    assert cli_args.configuration_module.magic_string == "Spam and eggs"
+    assert cli_args.configuration_module.magic_number == 0xCAFE
diff --git a/tests/generator_scripts/config/TestConfigModule_cfg.py b/tests/generator_scripts/config/TestConfigModule_cfg.py
new file mode 100644
index 0000000000000000000000000000000000000000..e118b43e3a10ff131d63accb748400b8298af85b
--- /dev/null
+++ b/tests/generator_scripts/config/TestConfigModule_cfg.py
@@ -0,0 +1,14 @@
+from pystencilssfg import SfgConfig
+
+
+def configure_sfg(cfg: SfgConfig):
+    cfg.outer_namespace = "myproject"
+    cfg.codestyle.indent_width = 3
+
+
+def project_info():
+    return {
+        "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/scripts/TestConfigModule.py b/tests/generator_scripts/scripts/TestConfigModule.py
new file mode 100644
index 0000000000000000000000000000000000000000..35b41923ac7a2defd5dc5b459939e046f48f1ce4
--- /dev/null
+++ b/tests/generator_scripts/scripts/TestConfigModule.py
@@ -0,0 +1,15 @@
+from pystencilssfg import SourceFileGenerator
+
+with SourceFileGenerator() as sfg:
+    ctx = sfg.context
+
+    assert ctx.outer_namespace == "myproject"
+    assert ctx.codestyle.indent_width == 3
+
+    assert not ctx.argv
+    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/TestExtraCommandLineArgs.py b/tests/generator_scripts/scripts/TestExtraCommandLineArgs.py
new file mode 100644
index 0000000000000000000000000000000000000000..2cf989321a160c714ee6ecb972061359d40d1439
--- /dev/null
+++ b/tests/generator_scripts/scripts/TestExtraCommandLineArgs.py
@@ -0,0 +1,6 @@
+from pystencilssfg import SourceFileGenerator
+
+with SourceFileGenerator(keep_unknown_argv=True) as sfg:
+    ctx = sfg.context
+
+    assert ctx.argv == ["--precision", "float32", "test1", "test2"]
diff --git a/tests/generator_scripts/scripts/TestIllegalArgs.py b/tests/generator_scripts/scripts/TestIllegalArgs.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce0a0cbf4dd29f36e508d33b48c3e68e4adaaf58
--- /dev/null
+++ b/tests/generator_scripts/scripts/TestIllegalArgs.py
@@ -0,0 +1,4 @@
+from pystencilssfg import SourceFileGenerator
+
+with SourceFileGenerator() as sfg:
+    ...
diff --git a/tests/generator_scripts/scripts/Variables.py b/tests/generator_scripts/scripts/Variables.py
index 9fd4e0027104451738abea72e15084047cf4465e..bcc85e4cba1b75364b500628b8b10fdb24a7470c 100644
--- a/tests/generator_scripts/scripts/Variables.py
+++ b/tests/generator_scripts/scripts/Variables.py
@@ -1,7 +1,6 @@
-import sympy as sp
 from pystencils import TypedSymbol, fields, kernel
 
-from pystencilssfg import SourceFileGenerator, SfgConfiguration
+from pystencilssfg import SourceFileGenerator
 
 with SourceFileGenerator() as sfg:
     α = TypedSymbol("alpha", "float32")
diff --git a/tests/generator_scripts/test_generator_scripts.py b/tests/generator_scripts/test_generator_scripts.py
index 51e05c01f2cae3a637e235e4a94d1585b390a5af..16adba2ec1ff9731f799609a5635cc49f3de5a0f 100644
--- a/tests/generator_scripts/test_generator_scripts.py
+++ b/tests/generator_scripts/test_generator_scripts.py
@@ -9,6 +9,7 @@ import subprocess
 
 THIS_DIR = path.split(__file__)[0]
 SCRIPTS_DIR = path.join(THIS_DIR, "scripts")
+CONFIG_DIR = path.join(THIS_DIR, "config")
 EXPECTED_DIR = path.join(THIS_DIR, "expected")
 
 
@@ -30,6 +31,12 @@ class ScriptInfo:
     Output files will all be placed in the ``out`` folder.
     """
 
+    args: tuple[str, ...] = ()
+    """Command-line arguments to be passed to the generator script"""
+
+    should_fail: bool = False
+    """Whether the exeuction of this script should fail."""
+
     compilable_output: str | None = None
     """File extension of the output file that can be compiled.
 
@@ -50,10 +57,41 @@ 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(
+        "TestConfigModule",
+        ("h++", "c++"),
+        args=(
+            "--sfg-file-extensions",
+            ".c++,.h++",
+            "--sfg-config-module",
+            path.join(CONFIG_DIR, "TestConfigModule_cfg.py"),
+        ),
+    ),
+    ScriptInfo.make(
+        "TestIllegalArgs",
+        ("h++", "c++"),
+        args=(
+            "--sfg-file-extensionss",
+            ".c++,.h++",
+        ),
+        should_fail=True
+    ),
+    ScriptInfo.make(
+        "TestExtraCommandLineArgs",
+        ("h++", "c++"),
+        args=(
+            "--sfg-file-extensions",
+            ".c++,.h++",
+            "--precision",
+            "float32",
+            "test1",
+            "test2"
+        ),
+    ),
+    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"),
 ]
 
 
@@ -75,12 +113,17 @@ def test_generator_script(script_info: ScriptInfo):
         shutil.rmtree(output_dir)
     os.makedirs(output_dir, exist_ok=True)
 
-    args = ["python", script_file, "--sfg-output-dir", output_dir]
+    args = ["python", script_file, "--sfg-output-dir", output_dir] + list(script_info.args)
 
     result = subprocess.run(args)
 
+    if script_info.should_fail:
+        if result.returncode == 0:
+            pytest.fail(f"Generator script {script_name} was supposed to fail, but didn't.")
+        return
+
     if result.returncode != 0:
-        raise AssertionError(f"Generator script {script_name} failed.")
+        pytest.fail(f"Generator script {script_name} failed.")
 
     #   Check generated files
     expected_files = set(
diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3c2585453627e7dc706083f4b1508160ea2f09c
--- /dev/null
+++ b/tests/integration/test_cli.py
@@ -0,0 +1,68 @@
+import subprocess
+
+
+def test_list_files():
+    output_dir = "/my/output/directory"
+    args = [
+        "sfg-cli",
+        "list-files",
+        "--sfg-output-dir",
+        output_dir,
+        "--sfg-file-extensions",
+        "cu, cuh",
+        "genscript.py",
+    ]
+
+    result = subprocess.run(args, capture_output=True, text=True)
+
+    assert result.returncode == 0
+    assert (
+        result.stdout
+        == "/my/output/directory/genscript.cuh /my/output/directory/genscript.cu\n"
+    )
+
+
+def test_list_files_headeronly():
+    output_dir = "/my/output/directory"
+    args = [
+        "python", "-m", "pystencilssfg",
+        "list-files",
+        "--sfg-output-dir",
+        output_dir,
+        "--sfg-output-mode",
+        "header-only",
+        "genscript.py",
+    ]
+
+    result = subprocess.run(args, capture_output=True, text=True)
+
+    assert result.returncode == 0
+    assert result.stdout == "/my/output/directory/genscript.hpp\n"
+
+
+def test_list_files_with_config_module(sample_config_module):
+    args = [
+        "sfg-cli",
+        "list-files",
+        "--sfg-config-module",
+        sample_config_module,
+        "genscript.py",
+    ]
+
+    result = subprocess.run(args, capture_output=True, text=True)
+
+    assert result.returncode == 0
+    assert (
+        result.stdout
+        == "generated_sources/genscript.hpp generated_sources/genscript.cpp\n"
+    )
+
+
+def test_make_find_module(tmp_path):
+    args = ["sfg-cli", "cmake", "make-find-module"]
+
+    result = subprocess.run(args, cwd=str(tmp_path))
+    assert result.returncode == 0
+
+    expected_path = tmp_path / "FindPystencilsSfg.cmake"
+    assert expected_path.exists()