Repository: watts-dev/watts (language: python)

Issue:
Extra templated input files
Some codes (such as SAS4A) may use templated files in both the main input file and the associated files. We need to provide the option for the user to expend templated supplementary files. Maybe this could be done with `extra_inputs_tmpl` that would contain the list of extra inputs that would need to be extended. 


Discussion / hints:
What about just having the first argument (`template_file`) accept one or more template files? i.e.
```Python
plugin = watts.PluginX('template1')
```
if you only have a single template, or
```Python
plugin = watts.PluginX(['template1', 'template2'])
```
if you have multiple
@paulromano , I am not sure about that because we would not know which is the expended input file to execute - we need to be able to specify the following:
- main input file to execute (can be a template or not, but must be a text file)
- extra files - those are not templates and will just be passed along
- extra input templates - those are additional input files that we will want to expend prior to passing along

We could have several options:
`watts.PluginX(main_input = 'main.tmpl', supp_inputs=['main_in.e', 'sub.i'], supp_tmpl = ['supp1.tmpl'])` % heavier but more explicit
or:
`watts.PluginX('main.tmpl', supp_inputs=['main_in.e', 'sub.i', 'supp1.tmpl'])` % in this case, we force templated files to use '.tmpl' extension



Another thought would be to adopt a convention of the first template listed being the primary one, i.e.
```Python
watts.PluginX(['main.tmpl', 'supp1.tmpl'], extra_inputs=['main_in.e', 'sub.i'])
```

I had the same thought by I personally prefer it to be more explicit. Otherwise it will be error prone. 
You're right — [explicit is better than implicit](https://www.python.org/dev/peps/pep-0020/#the-zen-of-python) :+1:

Fix patch:
diff --git a/src/watts/plugin.py b/src/watts/plugin.py
index 07646bbe..7231bf79 100644
--- a/src/watts/plugin.py
+++ b/src/watts/plugin.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: MIT
 
 from abc import ABC, abstractmethod
+from datetime import datetime
 from pathlib import Path
 import shutil
 from typing import Optional, List
@@ -107,11 +108,37 @@ class TemplatePlugin(Plugin):
         Path to template file
     extra_inputs
         Extra (non-templated) input files
+    extra_template_inputs
+        Extra templated input files
 
     """
-    def __init__(self, template_file: PathLike, extra_inputs: Optional[List[PathLike]] = None):
+    def __init__(self, template_file: PathLike, extra_inputs: Optional[List[PathLike]] = None,
+                 extra_template_inputs: Optional[List[PathLike]] = None):
         super().__init__(extra_inputs)
         self.render_template = TemplateRenderer(template_file)
+        self.extra_render_templates = []
+        if extra_template_inputs is not None:
+            self.extra_render_templates = [TemplateRenderer(f, '') for f in extra_template_inputs]
+
+    def _get_result_input(self, input_filename: str):
+        """Get the data needed to create the postrun results object
+
+        Parameters
+        ----------
+        input_filename
+            Name of the input file for the plugin code
+
+        Returns
+        -------
+        tuple of data used to create the results object
+        """
+        time = datetime.fromtimestamp(self._run_time * 1e-9)
+        inputs = [p.name for p in self.extra_inputs]
+        inputs.append(input_filename)
+        for renderer in self.extra_render_templates:
+            inputs.append(renderer.template_file.name)
+        outputs = [p for p in Path.cwd().iterdir() if p.name not in inputs]
+        return time, inputs, outputs
 
     def prerun(self, params: Parameters, filename: Optional[str] = None):
         """Render the template based on model parameters
@@ -125,3 +152,5 @@ def prerun(self, params: Parameters, filename: Optional[str] = None):
         """
         # Render the template
         self.render_template(params, filename=filename)
+        for render_template in self.extra_render_templates:
+            render_template(params)
diff --git a/src/watts/plugin_moose.py b/src/watts/plugin_moose.py
index 9a0bf186..00908ef9 100644
--- a/src/watts/plugin_moose.py
+++ b/src/watts/plugin_moose.py
@@ -182,9 +182,5 @@ def postrun(self, params: Parameters) -> ResultsMOOSE:
         """
         print("Post-run for MOOSE Plugin")
 
-        time = datetime.fromtimestamp(self._run_time * 1e-9)
-        # Start with non-templated input files
-        inputs = [p.name for p in self.extra_inputs]
-        inputs.append(self.moose_inp_name)
-        outputs = [p for p in Path.cwd().iterdir() if p.name not in inputs]
+        time, inputs, outputs = self._get_result_input(self.moose_inp_name)
         return ResultsMOOSE(params, time, inputs, outputs)
diff --git a/src/watts/plugin_pyarc.py b/src/watts/plugin_pyarc.py
index 9648d3cd..5cc9adf2 100644
--- a/src/watts/plugin_pyarc.py
+++ b/src/watts/plugin_pyarc.py
@@ -136,8 +136,5 @@ def postrun(self, params: Parameters) -> ResultsPyARC:
         """
         print("Post-run for PyARC Plugin")
 
-        time = datetime.fromtimestamp(self._run_time * 1e-9)
-        inputs = [p.name for p in self.extra_inputs]
-        inputs.append(self.pyarc_inp_name)
-        outputs = [p for p in Path.cwd().iterdir() if p.name not in inputs]
+        time, inputs, outputs = self._get_result_input(self.pyarc_inp_name)
         return ResultsPyARC(params, time, inputs, outputs, self.pyarc.user_object.results)
diff --git a/src/watts/plugin_sas.py b/src/watts/plugin_sas.py
index 11a6015d..1eeade24 100644
--- a/src/watts/plugin_sas.py
+++ b/src/watts/plugin_sas.py
@@ -194,9 +194,5 @@ def postrun(self, params: Parameters) -> ResultsSAS:
             with open("PRIMAR4.dat", "r") as file_in, open("PRIMAR4.csv", "w") as file_out:
                 subprocess.run(str(self.conv_primar4), stdin=file_in, stdout=file_out)
 
-        time = datetime.fromtimestamp(self._run_time * 1e-9)
-        # Start with non-templated input files
-        inputs = [p.name for p in self.extra_inputs]
-        inputs.append(self.sas_inp_name)
-        outputs = [p for p in Path.cwd().iterdir() if p.name not in inputs]
+        time, inputs, outputs = self._get_result_input(self.sas_inp_name)
         return ResultsSAS(params, time, inputs, outputs)
diff --git a/src/watts/template.py b/src/watts/template.py
index a20a389b..e6ccd14e 100644
--- a/src/watts/template.py
+++ b/src/watts/template.py
@@ -20,13 +20,14 @@ class TemplateRenderer:
     **template_kwargs
         Keywork arguments passed to :class:`jinja2.Template`
     """
-    def __init__(self, template_file: PathLike, **template_kwargs):
+    def __init__(self, template_file: PathLike, suffix: str = '.rendered', **template_kwargs):
         self.template_file = Path(template_file)
         self.template = jinja2.Template(
             self.template_file.read_text(),
             undefined=jinja2.StrictUndefined,
             **template_kwargs
         )
+        self.suffix = suffix
 
     def __call__(self, params: Parameters, filename: Optional[PathLike] = None):
         """Render the template
@@ -43,7 +44,7 @@ def __call__(self, params: Parameters, filename: Optional[PathLike] = None):
         # Default rendered template filename
         if filename is None:
             name = self.template_file.name
-            out_path = self.template_file.with_name(f'{name}.rendered')
+            out_path = self.template_file.with_name(f'{name}{self.suffix}')
         else:
             out_path = Path(filename)
 


Test patch:
diff --git a/tests/test_template.py b/tests/test_template.py
index 6cecc29f..eb63faa5 100644
--- a/tests/test_template.py
+++ b/tests/test_template.py
@@ -20,14 +20,34 @@ def test_undefined_template(run_in_tmpdir):
     # Create plugin to build template
     plugin = TemplatePlugin('example_template')
 
-    # Passing model with 'variable' set should work
-    model = watts.Parameters(variable=1)
-    plugin.prerun(model)
+    # Passing parameters with 'variable' set should work
+    params = watts.Parameters(variable=1)
+    plugin.prerun(params)
     with open('example_template.rendered', 'r') as fh:
         rendered = fh.read()
     assert rendered == '1'
 
-    # Passing empty model should raise jinja2.UndefinedError
-    empty_model = watts.Parameters()
+    # Passing empty parameters should raise jinja2.UndefinedError
+    empty_params = watts.Parameters()
     with pytest.raises(jinja2.UndefinedError) as e:
-        plugin.prerun(empty_model)
+        plugin.prerun(empty_params)
+
+def test_extra_template_inputs(run_in_tmpdir):
+    # Create templates expecting a variable
+    with open('main_template', 'w') as fh:
+        fh.write("{{ x }}")
+    with open('extra_template', 'w') as fh:
+        fh.write("{{ y }}")
+
+    # Create plugin to build template
+    plugin = TemplatePlugin('main_template', extra_template_inputs=['extra_template'])
+
+    # Pass parameters with 'x' and 'y' set
+    params = watts.Parameters(x=1, y=2)
+    plugin.prerun(params)
+    with open('main_template.rendered', 'r') as fh:
+        rendered = fh.read()
+    assert rendered == '1'
+    with open('extra_template', 'r') as fh:
+        rendered = fh.read()
+    assert rendered == '2'