Skip to content

Module

module

Base class for defining flepimop2 modules.

ModuleBase

Bases: BaseModel

Base class for all flepimop2 modules.

Combines configuration parsing via pydantic with the module-naming conventions required by the flepimop2 loader. Every concrete module class must end up with a non-empty module string - either declared explicitly as a Literal[...] field or resolved from the module="..." class-keyword shortcut.

Attributes:

Name Type Description
module_namespace str | None

The flepimop2 namespace used to resolve short module names such as "csv" into fully-qualified paths like "flepimop2.backend.csv". Set by the ABC subclass, not by individual module implementations.

module str

The fully-qualified module name. Concrete subclasses should specialize this to a Literal[...] type via the module="..." class-keyword shortcut.

options dict[str, Any] | None

Optional grab-bag of extra information the module exposes for flepimop2 to take advantage of.

__init_subclass__(**kwargs)

Process module= and module_namespace= class-keyword arguments.

Only namespace resolution and validation happen here. The actual Pydantic field specialization for module is deferred to __pydantic_init_subclass__ so that it runs after Pydantic has finished building the model and all field defaults are in place.

Parameters:

Name Type Description Default
**kwargs Any

Additional keyword arguments passed to parent classes.

{}
Source code in src/flepimop2/module.py
def __init_subclass__(cls, **kwargs: Any) -> None:
    """
    Process `module=` and `module_namespace=` class-keyword arguments.

    Only namespace resolution and validation happen here.  The actual
    Pydantic field specialization for `module` is deferred to
    `__pydantic_init_subclass__` so that it runs after Pydantic has
    finished building the model and all field defaults are in place.

    Args:
        **kwargs: Additional keyword arguments passed to parent classes.

    """
    module_namespace = kwargs.pop("module_namespace", None)
    module = kwargs.pop("module", None)
    super().__init_subclass__(**kwargs)
    if module_namespace is not None:
        cls._apply_module_namespace(module_namespace)
    if module is not None:
        module_full_name = cls._resolve_module_name(module)
        # Store for __pydantic_init_subclass__ to consume.
        cls._pending_module = module_full_name
        # Also set the class attribute so plain access works.
        cls.__annotations__ = {
            **getattr(cls, "__annotations__", {}),
            "module": Literal[module_full_name],
        }
    if inspect.isabstract(cls) or cls.__name__.endswith("ABC"):
        return
    # For non-pydantic paths (shouldn't exist anymore) validate at class time.
    if cls._pending_module is None:
        cls._validate_module_definition()

__pydantic_init_subclass__(**kwargs) classmethod

Finalize the module field specialization after Pydantic builds the model.

This hook runs after Pydantic has completed model construction, so all field defaults inherited from parent classes are stable.

Parameters:

Name Type Description Default
**kwargs Any

Additional keyword arguments passed to parent classes.

{}
Source code in src/flepimop2/module.py
@classmethod
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:  # noqa: PLW3201
    """
    Finalize the `module` field specialization after Pydantic builds the model.

    This hook runs after Pydantic has completed model construction, so all
    field defaults inherited from parent classes are stable.

    Args:
        **kwargs: Additional keyword arguments passed to parent classes.

    """
    super().__pydantic_init_subclass__(**kwargs)
    pending = cls.__dict__.get("_pending_module")
    if pending is None:
        return
    field = cls.model_fields.get("module")
    if field is not None:
        field.annotation = cast("Any", Literal[pending])
        field.default = pending
        cls.model_rebuild(force=True)
    # Clear the pending marker so subclasses don't inherit it.
    cls._pending_module = None

from_shorthand(shorthand) classmethod

Build an instance from shorthand configuration text.

Concrete modules may override this optional hook to support configuration values shaped like module_name(...). The provided shorthand is the text inside the parentheses.

Parameters:

Name Type Description Default
shorthand str

The text contained within the shorthand parentheses.

required

Raises:

Type Description
NotImplementedError

If the module does not support shorthand syntax.

Source code in src/flepimop2/module.py
@classmethod
def from_shorthand(cls, shorthand: str) -> Self:
    """
    Build an instance from shorthand configuration text.

    Concrete modules may override this optional hook to support
    configuration values shaped like `module_name(...)`.  The provided
    `shorthand` is the text inside the parentheses.

    Args:
        shorthand: The text contained within the shorthand parentheses.

    Raises:
        NotImplementedError: If the module does not support shorthand syntax.

    """
    msg = "Shorthand syntax is not supported by this module."
    raise NotImplementedError(msg)

option(name, default=RaiseOnMissing)

Retrieve an option value by name, with an optional default.

Parameters:

Name Type Description Default
name str

The name of the option to retrieve.

required
default Any

The default value to return if the option is not found. Omitting this argument causes a KeyError when the option is missing.

RaiseOnMissing

Returns:

Type Description
Any

The value of the option if found, otherwise the default value.

Raises:

Type Description
KeyError

If the option is missing and default is not provided.

Examples:

>>> from flepimop2.module import ModuleBase
>>> class MyModule(ModuleBase, module="flepimop2.test.mymodule"):
...     pass
>>> mod = MyModule.model_validate({"options": {"option1": 42}})
>>> mod.option("option1")
42
>>> mod.option("option2", default="default_value")
'default_value'
>>> mod.option("option2")
Traceback (most recent call last):
    ...
KeyError: "Option 'option2' not found in module 'flepimop2.test.mymodule'."
>>> class MyModuleWithMissingOption(
...     ModuleBase, module="flepimop2.test.noopts"
... ):
...     pass
>>> mod = MyModuleWithMissingOption()
>>> mod.option("option1", default="default_value")
'default_value'
>>> mod.option("option1")
Traceback (most recent call last):
    ...
KeyError: "Option 'option1' not found in module 'flepimop2.test.noopts'."
Source code in src/flepimop2/module.py
def option(self, name: str, default: Any = RaiseOnMissing) -> Any:  # noqa: ANN401
    """
    Retrieve an option value by name, with an optional default.

    Args:
        name: The name of the option to retrieve.
        default: The default value to return if the option is not found.
            Omitting this argument causes a `KeyError` when the option
            is missing.

    Returns:
        The value of the option if found, otherwise the default value.

    Raises:
        KeyError: If the option is missing and `default` is not provided.

    Examples:
        >>> from flepimop2.module import ModuleBase
        >>> class MyModule(ModuleBase, module="flepimop2.test.mymodule"):
        ...     pass
        >>> mod = MyModule.model_validate({"options": {"option1": 42}})
        >>> mod.option("option1")
        42
        >>> mod.option("option2", default="default_value")
        'default_value'
        >>> mod.option("option2")
        Traceback (most recent call last):
            ...
        KeyError: "Option 'option2' not found in module 'flepimop2.test.mymodule'."
        >>> class MyModuleWithMissingOption(
        ...     ModuleBase, module="flepimop2.test.noopts"
        ... ):
        ...     pass
        >>> mod = MyModuleWithMissingOption()
        >>> mod.option("option1", default="default_value")
        'default_value'
        >>> mod.option("option1")
        Traceback (most recent call last):
            ...
        KeyError: "Option 'option1' not found in module 'flepimop2.test.noopts'."

    """
    opts = self.options or {}
    if name not in opts and isinstance(default, RaiseOnMissingType):
        msg = f"Option '{name}' not found in module '{self.module}'."
        raise KeyError(msg)
    return opts.get(name, default)

patch(other, *, conflict)

Patch this module configuration with another module configuration.

This method treats other as the incoming patch. The default implementation is intentionally simple: replace wholesale for replace, and deep-merge dumped model dictionaries for merge.

Module developers can override this method to implement more complex patching logic if needed (e.g. merging certain subsections or sets of fields while replacing others). However, they should still try to respect the semantics of the conflict argument as much as possible to avoid surprising users.

Parameters:

Name Type Description Default
other Self

The patch to apply to this module.

required
conflict Literal[MERGE, REPLACE]

How to handle overlapping fields.

required

Returns:

Type Description
Self

The patched module configuration.

Raises:

Type Description
TypeError

If self and other are different concrete model types.

Source code in src/flepimop2/module.py
def patch(
    self,
    other: Self,
    *,
    conflict: Literal[PatchConflictMode.MERGE, PatchConflictMode.REPLACE],
) -> Self:
    """
    Patch this module configuration with another module configuration.

    This method treats `other` as the incoming patch. The default
    implementation is intentionally simple: replace wholesale for `replace`,
    and deep-merge dumped model dictionaries for `merge`.

    Module developers can override this method to implement more complex patching
    logic if needed (e.g. merging certain subsections or sets of fields while
    replacing others). However, they should still try to respect the semantics of
    the `conflict` argument as much as possible to avoid surprising users.

    Args:
        other: The patch to apply to this module.
        conflict: How to handle overlapping fields.

    Returns:
        The patched module configuration.

    Raises:
        TypeError: If `self` and `other` are different concrete model types.
    """
    if type(self) is not type(other):
        msg = (
            f"Cannot patch {type(self).__name__} with {type(other).__name__}; "
            "module patching requires matching concrete types."
        )
        raise TypeError(msg)
    if conflict is PatchConflictMode.REPLACE:
        return other.model_copy(deep=True)
    return type(self).model_validate(
        _deep_merge_dicts(self.model_dump(), other.model_dump())
    )

to_yaml_data()

Convert the module configuration into YAML-ready Python objects.

Subclasses can override this to customize just their serialized configuration block without changing patch semantics.

Returns:

Type Description
object

A YAML-ready representation of the module configuration.

Source code in src/flepimop2/module.py
def to_yaml_data(self) -> object:
    """
    Convert the module configuration into YAML-ready Python objects.

    Subclasses can override this to customize just their serialized
    configuration block without changing patch semantics.

    Returns:
        A YAML-ready representation of the module configuration.
    """
    data = _model_to_yaml_mapping(self)
    if not data.get("options"):
        data.pop("options", None)
    return data