Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/rompy/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,23 @@ class BaseConfig(RompyBaseModel):
# noop call for config objects
def __call__(self, *args, **kwargs):
return self

def render(self, context: dict, output_dir: Path | str):
"""Render the configuration template to the output directory.

This method orchestrates the template rendering process. The default implementation
uses cookiecutter rendering with the template and checkout defined on this config.
Subclasses can override this method to implement alternative rendering strategies
(e.g., direct file writing, Jinja2, custom logic).

Args:
context: Full context dictionary. Expected to contain at least 'runtime' and 'config' keys.
output_dir: Target directory for rendered output.

Returns:
str: Path to the staging directory (workspace) containing rendered files.
"""
# Import locally to avoid potential circular imports at module import time
from rompy.core.render import render as cookiecutter_render

cookiecutter_render(context, self.template, output_dir, self.checkout)
15 changes: 5 additions & 10 deletions src/rompy/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from rompy.backends import BackendConfig
from rompy.backends.config import BaseBackendConfig
from rompy.core.config import BaseConfig
from rompy.core.render import render
from rompy.core.time import TimeRange
from rompy.core.types import RompyBaseModel
from rompy.logging import get_logger
Expand Down Expand Up @@ -255,10 +254,8 @@ def generate(self) -> str:
cc_full["config"] = self.config

# Render templates
logger.info(f"Rendering model templates to {self.output_dir}/{self.run_id}...")
staging_dir = render(
cc_full, self.config.template, self.output_dir, self.config.checkout
)
logger.info(f"Rendering model configurations to {self.staging_dir}...")
self.config.render(cc_full, self.output_dir)

logger.info("")
# Use the log_box utility function
Expand All @@ -269,8 +266,8 @@ def generate(self) -> str:
logger=logger,
add_empty_line=False,
)
logger.info(f"Model files generated at: {staging_dir}")
return staging_dir
logger.info(f"Model files generated at: {self.staging_dir}")
return self.staging_dir

def zip(self) -> str:
"""Zip the input files for the model run
Expand Down Expand Up @@ -366,9 +363,7 @@ def run(self, backend: BackendConfig, workspace_dir: Optional[str] = None) -> bo
# Pass the config object and workspace_dir to the backend
return backend_instance.run(self, config=backend, workspace_dir=workspace_dir)

def postprocess(
self, processor: "BasePostprocessorConfig", **kwargs
) -> Dict[str, Any]:
def postprocess(self, processor, **kwargs) -> Dict[str, Any]:
"""
Postprocess the model outputs using the specified processor configuration.

Expand Down
46 changes: 46 additions & 0 deletions tests/test_config_render_delegation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest.mock

from rompy.model import ModelRun
from rompy.core.config import BaseConfig


def test_modelrun_delegates_to_config_render(tmp_path):
"""ModelRun.generate() delegates rendering to config.render()."""

# Setup
class TestConfig(BaseConfig):
def render(self, context: dict, output_dir):
# real implementation replaced by mock in the test
pass

config = TestConfig(template="../rompy/templates/base", checkout=None)
model_run = ModelRun(run_id="test", output_dir=str(tmp_path), config=config)

# Patch the class method so the instance call is intercepted. Inspect
# positional args to find the context and output_dir regardless of whether
# the mock was bound to the instance or the class.
# Patch by import path to avoid binding issues with Pydantic-generated classes
with unittest.mock.patch.object(
TestConfig, "render", return_value=None
) as mock_render:
# Execute
result = model_run.generate()

# Verify delegation
mock_render.assert_called_once()
call_args = mock_render.call_args
pos_args = call_args[0]

# Locate the context dict (contains 'runtime') and the output_dir arg
context_arg = next(
(a for a in pos_args if isinstance(a, dict) and "runtime" in a), None
)
output_dir_arg = next(
(a for a in pos_args if str(a) == str(model_run.output_dir)), None
)

assert isinstance(context_arg, dict)
assert "runtime" in context_arg
assert "config" in context_arg
assert output_dir_arg == model_run.output_dir
assert str(result) == str(tmp_path / "test")