Repository: ufs-community/uwtools
Pull request #728: Output methods for shave, make_orog, sfc_climo components
Author: login: maddenp-cu
Created: 2025-04-28T20:38:30Z | Merged: 2025-04-30T14:56:46Z

**Synopsis**

Implement `.output` property methods in `filter_topo`, `orog`, `orog_gsl`, `sfc_climo_gen`, and `shave` drivers.

Fixes #633 #634 #635 

**Type**

- [x] Documentation
- [x] Enhancement (adds new functionality)

**Impact**

- [x] This is a breaking change (changes existing functionality)

**Checklist**

- [x] I have added myself and any co-authors to the PR's _Assignees_ list.
- [x] I have reviewed the documentation and have made any updates necessitated by this change.


Linked issue #633: Output method for sfc_climo driver
# AC

- [ ] Add an interface for a public method task (if not already done)
- [ ] Provide the output files in an appropriate data structure (dict, list) as a public helper method on the object. 


# Description

Best guess at appropriate data structures for output of sfc_climo_gen.

No need to hook into CLI.


Diff:
diff --git a/docs/sections/user_guide/cli/drivers/mpas_init/show-schema.out b/docs/sections/user_guide/cli/drivers/mpas_init/show-schema.out
index 6a711600f..974cc8107 100644
--- a/docs/sections/user_guide/cli/drivers/mpas_init/show-schema.out
+++ b/docs/sections/user_guide/cli/drivers/mpas_init/show-schema.out
@@ -11,7 +11,7 @@
               "type": "integer"
             },
             "length": {
-              "minimum": 1,
+              "minimum": 0,
               "type": "integer"
             },
             "offset": {
diff --git a/docs/sections/user_guide/cli/drivers/orog/index.rst b/docs/sections/user_guide/cli/drivers/orog/index.rst
index 77f16009c..33b2fd966 100644
--- a/docs/sections/user_guide/cli/drivers/orog/index.rst
+++ b/docs/sections/user_guide/cli/drivers/orog/index.rst
@@ -3,7 +3,7 @@
 
 .. include:: ../shared/idempotent.rst
 
-The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``orog``.
+The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``orog``. Documentation for this UFS Utils component is :ufs-utils:`here <orog>`.
 
 .. include:: help.rst
 
diff --git a/docs/sections/user_guide/cli/drivers/sfc_climo_gen/index.rst b/docs/sections/user_guide/cli/drivers/sfc_climo_gen/index.rst
index 4743dfa67..186c05c74 100644
--- a/docs/sections/user_guide/cli/drivers/sfc_climo_gen/index.rst
+++ b/docs/sections/user_guide/cli/drivers/sfc_climo_gen/index.rst
@@ -3,7 +3,7 @@
 
 .. include:: ../shared/idempotent.rst
 
-The ``uw`` mode for configuring and running the :sfc-climo-gen:`sfc_climo_gen<>` component.
+The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``sfc_climo_gen``. Documentation for this UFS Utils component is :ufs-utils:`here <sfc-climo-gen>`.
 
 .. include:: help.rst
 
diff --git a/src/uwtools/drivers/chgres_cube.py b/src/uwtools/drivers/chgres_cube.py
index fbedc8a21..07046b603 100644
--- a/src/uwtools/drivers/chgres_cube.py
+++ b/src/uwtools/drivers/chgres_cube.py
@@ -114,13 +114,12 @@ def driver_name(cls) -> str:
         return STR.chgrescube
 
     @property
-    def output(self) -> dict[str, list[str]]:
+    def output(self) -> dict[str, list[Path]]:
         """
         Returns a description of the file(s) created when this component runs.
         """
-        output_types = ["atm", "sfc"]
-        paths = [f"{self.rundir}/out.{t}.tile7.nc" for t in output_types]
-        return {"netcdffiles": paths}
+        outfile = lambda x: self.rundir / f"out.{x}.tile7.nc"
+        return {"atm": [outfile("atm")], "sfc": [outfile("sfc")]}
 
 
 set_driver_docstring(ChgresCube)
diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py
index c9ce4054a..b8f72a421 100644
--- a/src/uwtools/drivers/driver.py
+++ b/src/uwtools/drivers/driver.py
@@ -428,7 +428,7 @@ def _run_via_local_execution(self):
     # Public methods
 
     @property
-    def output(self) -> dict[str, str] | dict[str, list[str]]:
+    def output(self) -> dict[str, Path] | dict[str, list[Path]]:
         """
         Returns a description of the file(s) created when this component runs.
         """
diff --git a/src/uwtools/drivers/filter_topo.py b/src/uwtools/drivers/filter_topo.py
index a99a9d1b6..1fd0bd364 100644
--- a/src/uwtools/drivers/filter_topo.py
+++ b/src/uwtools/drivers/filter_topo.py
@@ -37,7 +37,7 @@ def filtered_output_file(self):
         The filtered output file staged from raw input.
         """
         src = Path(self.config["config"]["input_raw_orog"])
-        dst = self.rundir / self.config["config"]["filtered_orog"]
+        dst = self.output["path"]
         yield self.taskname(f"Raw orog input {dst!s}")
         yield asset(dst, dst.is_file)
         yield filecopy(src=src, dst=dst)
@@ -81,5 +81,12 @@ def driver_name(cls) -> str:
         """
         return STR.filtertopo
 
+    @property
+    def output(self) -> dict[str, Path]:
+        """
+        Returns a description of the file(s) created when this component runs.
+        """
+        return {"path": self.rundir / self.config["config"]["filtered_orog"]}
+
 
 set_driver_docstring(FilterTopo)
diff --git a/src/uwtools/drivers/orog.py b/src/uwtools/drivers/orog.py
index 6a1d58d96..a496765c0 100644
--- a/src/uwtools/drivers/orog.py
+++ b/src/uwtools/drivers/orog.py
@@ -108,6 +108,13 @@ def driver_name(cls) -> str:
         """
         return STR.orog
 
+    @property
+    def output(self) -> dict[str, Path]:
+        """
+        Returns a description of the file(s) created when this component runs.
+        """
+        return {"path": self.rundir / "out.oro.nc"}
+
     # Private helper methods
 
     @property
diff --git a/src/uwtools/drivers/orog_gsl.py b/src/uwtools/drivers/orog_gsl.py
index 95547976c..13da21222 100644
--- a/src/uwtools/drivers/orog_gsl.py
+++ b/src/uwtools/drivers/orog_gsl.py
@@ -94,6 +94,16 @@ def driver_name(cls) -> str:
         """
         return STR.oroggsl
 
+    @property
+    def output(self) -> dict[str, Path]:
+        """
+        Returns a description of the file(s) created when this component runs.
+        """
+        vals = tuple(self.config["config"][k] for k in ["resolution", "tile", "halo"])
+        template = "C%s_oro_data_{x}.tile%s.halo%s.nc" % vals
+        outfile = lambda x: self.rundir / template.format(x=x)
+        return {"ls": outfile("ls"), "ss": outfile("ss")}
+
     # Private helper methods
 
     @property
diff --git a/src/uwtools/drivers/sfc_climo_gen.py b/src/uwtools/drivers/sfc_climo_gen.py
index de78a50d8..c64abfa10 100644
--- a/src/uwtools/drivers/sfc_climo_gen.py
+++ b/src/uwtools/drivers/sfc_climo_gen.py
@@ -2,6 +2,7 @@
 A driver for sfc_climo_gen.
 """
 
+import re
 from pathlib import Path
 
 from iotaa import asset, task, tasks
@@ -52,6 +53,17 @@ def provisioned_rundir(self):
             self.runscript(),
         ]
 
+    @property
+    def output(self) -> dict[str, list[Path]]:
+        """
+        Returns a description of the file(s) created when this component runs.
+        """
+        cfg = self.config["namelist"]["update_values"]["config"]
+        halo = cfg.get("halo")
+        ns = [0, halo] if halo else [0]
+        keys = [m[1] for key in cfg if (m := re.match(r"^input_(.*)_file$", key))]
+        return {key: [self.rundir / f"{key}.tile7.halo{n}.nc" for n in ns] for key in keys}
+
     # Public helper methods
 
     @classmethod
diff --git a/src/uwtools/drivers/shave.py b/src/uwtools/drivers/shave.py
index 1f809e714..d68d0205c 100644
--- a/src/uwtools/drivers/shave.py
+++ b/src/uwtools/drivers/shave.py
@@ -58,6 +58,13 @@ def driver_name(cls) -> str:
         """
         return STR.shave
 
+    @property
+    def output(self) -> dict[str, Path]:
+        """
+        Returns a description of the file(s) created when this component runs.
+        """
+        return {"path": Path(self.config["config"]["output_grid_file"])}
+
     # Private helper methods
 
     @property
diff --git a/src/uwtools/drivers/upp.py b/src/uwtools/drivers/upp.py
index b0426f96a..4568cb9e3 100644
--- a/src/uwtools/drivers/upp.py
+++ b/src/uwtools/drivers/upp.py
@@ -100,7 +100,7 @@ def driver_name(cls) -> str:
         return STR.upp
 
     @property
-    def output(self) -> dict[str, list[str]]:
+    def output(self) -> dict[str, list[Path]]:
         """
         Returns a description of the file(s) created when this component runs.
         """
@@ -119,7 +119,7 @@ def output(self) -> dict[str, list[str]]:
         paths = []
         for _ in range(nblocks):
             identifier = lines[0]
-            paths.append(str(self.rundir / (identifier + suffix)))
+            paths.append(self.rundir / (identifier + suffix))
             fields, lines = lines[: self.NFIELDS], lines[self.NFIELDS :]
             _, lines = (
                 (lines[0], lines[1:])
@@ -127,7 +127,7 @@ def output(self) -> dict[str, list[str]]:
                 else (None, lines)
             )
             lines = lines[self.NPARAMS * nvars.pop() :]
-        return {"gribfiles": paths}
+        return {"paths": paths}
 
     # Private helper methods
 
diff --git a/src/uwtools/tests/drivers/test_chgres_cube.py b/src/uwtools/tests/drivers/test_chgres_cube.py
index bc3287e70..ceccf8104 100644
--- a/src/uwtools/tests/drivers/test_chgres_cube.py
+++ b/src/uwtools/tests/drivers/test_chgres_cube.py
@@ -138,9 +138,10 @@ def test_ChgresCube_namelist_file_missing_base_file(driverobj, logged):
 
 
 def test_ChgresCube_output(driverobj):
-    files = ["out.atm.tile7.nc", "out.sfc.tile7.nc"]
-    expected = {"netcdffiles": [str(driverobj.rundir / file) for file in files]}
-    assert driverobj.output == expected
+    assert driverobj.output == {
+        "atm": [driverobj.rundir / "out.atm.tile7.nc"],
+        "sfc": [driverobj.rundir / "out.sfc.tile7.nc"],
+    }
 
 
 def test_ChgresCube_provisioned_rundir(driverobj, ready_task):
diff --git a/src/uwtools/tests/drivers/test_filter_topo.py b/src/uwtools/tests/drivers/test_filter_topo.py
index 9d8e255ce..d7d830263 100644
--- a/src/uwtools/tests/drivers/test_filter_topo.py
+++ b/src/uwtools/tests/drivers/test_filter_topo.py
@@ -6,12 +6,11 @@
 from unittest.mock import patch
 
 import f90nml  # type: ignore[import-untyped]
-from pytest import fixture, mark, raises
+from pytest import fixture, mark
 
 from uwtools.config.support import from_od
 from uwtools.drivers.driver import Driver
 from uwtools.drivers.filter_topo import FilterTopo
-from uwtools.exceptions import UWNotImplementedError
 
 # Fixtures
 
@@ -73,7 +72,6 @@ def driverobj(config):
         "_scheduler",
         "_validate",
         "_write_runscript",
-        "output",
         "run",
         "runscript",
         "taskname",
@@ -109,9 +107,9 @@ def test_FilterTopo_namelist_file(driverobj):
 
 
 def test_FilterTopo_output(driverobj):
-    with raises(UWNotImplementedError) as e:
-        assert driverobj.output
-    assert str(e.value) == "The output() method is not yet implemented for this driver"
+    assert driverobj.output == {
+        "path": driverobj.rundir / driverobj.config["config"]["filtered_orog"]
+    }
 
 
 def test_FilterTopo_provisioned_rundir(driverobj, ready_task):
diff --git a/src/uwtools/tests/drivers/test_orog.py b/src/uwtools/tests/drivers/test_orog.py
index ef7ec8d6c..744435d1f 100644
--- a/src/uwtools/tests/drivers/test_orog.py
+++ b/src/uwtools/tests/drivers/test_orog.py
@@ -5,11 +5,10 @@
 from pathlib import Path
 from unittest.mock import patch
 
-from pytest import fixture, mark, raises
+from pytest import fixture, mark
 
 from uwtools.drivers.driver import Driver
 from uwtools.drivers.orog import Orog
-from uwtools.exceptions import UWNotImplementedError
 from uwtools.scheduler import Slurm
 
 # Fixtures
@@ -73,7 +72,6 @@ def driverobj(config):
         "_scheduler",
         "_validate",
         "_write_runscript",
-        "output",
         "run",
         "taskname",
     ],
@@ -140,9 +138,7 @@ def test_Orog_input_config_file_old(driverobj):
 
 
 def test_Orog_output(driverobj):
-    with raises(UWNotImplementedError) as e:
-        assert driverobj.output
-    assert str(e.value) == "The output() method is not yet implemented for this driver"
+    assert driverobj.output == {"path": driverobj.rundir / "out.oro.nc"}
 
 
 def test_Orog_provisioned_rundir(driverobj, ready_task):
diff --git a/src/uwtools/tests/drivers/test_orog_gsl.py b/src/uwtools/tests/drivers/test_orog_gsl.py
index 80c5aab8e..0ef515fdd 100644
--- a/src/uwtools/tests/drivers/test_orog_gsl.py
+++ b/src/uwtools/tests/drivers/test_orog_gsl.py
@@ -5,11 +5,10 @@
 from pathlib import Path
 from unittest.mock import patch
 
-from pytest import fixture, mark, raises
+from pytest import fixture, mark
 
 from uwtools.drivers.driver import Driver
 from uwtools.drivers.orog_gsl import OrogGSL
-from uwtools.exceptions import UWNotImplementedError
 
 # Fixtures
 
@@ -63,7 +62,6 @@ def driverobj(config):
         "_scheduler",
         "_validate",
         "_write_runscript",
-        "output",
         "run",
         "runscript",
         "taskname",
@@ -93,9 +91,8 @@ def test_OrogGSL_input_grid_file(driverobj):
 
 
 def test_OrogGSL_output(driverobj):
-    with raises(UWNotImplementedError) as e:
-        assert driverobj.output
-    assert str(e.value) == "The output() method is not yet implemented for this driver"
+    outfile = lambda x: driverobj.rundir / f"C403_oro_data_{x}.tile7.halo4.nc"
+    assert driverobj.output == {"ls": outfile("ls"), "ss": outfile("ss")}
 
 
 def test_OrogGSL_provisioned_rundir(driverobj, ready_task):
diff --git a/src/uwtools/tests/drivers/test_sfc_climo_gen.py b/src/uwtools/tests/drivers/test_sfc_climo_gen.py
index dff696277..3189bc139 100644
--- a/src/uwtools/tests/drivers/test_sfc_climo_gen.py
+++ b/src/uwtools/tests/drivers/test_sfc_climo_gen.py
@@ -6,12 +6,11 @@
 from unittest.mock import patch
 
 import f90nml  # type: ignore[import-untyped]
-from pytest import fixture, mark, raises
+from pytest import fixture, mark
 
 from uwtools.drivers import sfc_climo_gen
 from uwtools.drivers.driver import Driver
 from uwtools.drivers.sfc_climo_gen import SfcClimoGen
-from uwtools.exceptions import UWNotImplementedError
 
 # Fixtures
 
@@ -84,7 +83,6 @@ def driverobj(config):
         "_scheduler",
         "_validate",
         "_write_runscript",
-        "output",
         "run",
         "runscript",
         "taskname",
@@ -117,10 +115,23 @@ def test_SfcClimoGen_namelist_file_fails_validation(driverobj, logged, ready_tas
     assert logged("  'string' is not of type 'integer'")
 
 
-def test_SfcClimoGen_output(driverobj):
-    with raises(UWNotImplementedError) as e:
-        assert driverobj.output
-    assert str(e.value) == "The output() method is not yet implemented for this driver"
+@mark.parametrize("halo", [1, None])
+def test_SfcClimoGen_output(driverobj, halo):
+    driverobj._config["namelist"]["update_values"]["config"]["halo"] = halo
+    keys = [
+        "facsf",
+        "maximum_snow_albedo",
+        "slope_type",
+        "snowfree_albedo",
+        "soil_type",
+        "substrate_temperature",
+        "vegetation_greenness",
+        "vegetation_type",
+    ]
+    ns = [0, halo] if halo else [0]
+    assert driverobj.output == {
+        key: [driverobj.rundir / f"{key}.tile7.halo{n}.nc" for n in ns] for key in keys
+    }
 
 
 def test_SfcClimoGen_provisioned_rundir(driverobj, ready_task):
diff --git a/src/uwtools/tests/drivers/test_shave.py b/src/uwtools/tests/drivers/test_shave.py
index 182828372..5e6d92350 100644
--- a/src/uwtools/tests/drivers/test_shave.py
+++ b/src/uwtools/tests/drivers/test_shave.py
@@ -5,11 +5,10 @@
 from pathlib import Path
 from unittest.mock import patch
 
-from pytest import fixture, mark, raises
+from pytest import fixture, mark
 
 from uwtools.drivers.driver import Driver
 from uwtools.drivers.shave import Shave
-from uwtools.exceptions import UWNotImplementedError
 
 # Fixtures
 
@@ -63,7 +62,6 @@ def driverobj(config):
         "_scheduler",
         "_validate",
         "_write_runscript",
-        "output",
         "run",
         "runscript",
         "taskname",
@@ -83,7 +81,7 @@ def test_Shave_input_config_file(driverobj):
     nhalo = driverobj.config["config"]["nhalo"]
     input_file_path = driverobj._config["config"]["input_grid_file"]
     Path(input_file_path).touch()
-    output_file_path = driverobj._config["config"]["output_grid_file"]
+    output_file_path = driverobj.config["config"]["output_grid_file"]
     driverobj.input_config_file()
     content = Path(driverobj._input_config_path).read_text().strip().split("\n")
     assert len(content) == 1
@@ -91,9 +89,7 @@ def test_Shave_input_config_file(driverobj):
 
 
 def test_Shave_output(driverobj):
-    with raises(UWNotImplementedError) as e:
-        assert driverobj.output
-    assert str(e.value) == "The output() method is not yet implemented for this driver"
+    assert driverobj.output == {"path": Path(driverobj.config["config"]["output_grid_file"])}
 
 
 def test_Shave_provisioned_rundir(driverobj, ready_task):
diff --git a/src/uwtools/tests/drivers/test_upp.py b/src/uwtools/tests/drivers/test_upp.py
index 31695f4db..cf20ad785 100644
--- a/src/uwtools/tests/drivers/test_upp.py
+++ b/src/uwtools/tests/drivers/test_upp.py
@@ -171,7 +171,7 @@ def test_UPP_output(driverobj, tmp_path):
     control_file = tmp_path / "postxconfig-NT.txt"
     control_file.write_text("\n".join(control_data))
     driverobj._config["control_file"] = str(control_file)
-    expected = {"gribfiles": [str(driverobj.rundir / ("%s.GrbF24" % x)) for x in ("FOO", "BAR")]}
+    expected = {"paths": [driverobj.rundir / ("%s.GrbF24" % x) for x in ("FOO", "BAR")]}
     assert driverobj.output == expected
 
 


Reviews:
user: login: maddenp-cu
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T20:49:49Z
user: login: elcarpenterNOAA
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T21:25:01Z
user: login: nbharwani-cu
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T12:56:10Z
user: login: christinaholtNOAA
body: This looks great.

I think any time we see `tile7` show up in one of these output files, we will expect size output files there for the global configuration (one each for tiles 1-6). How big a breaking change would we see in the future if we needed to create lists where the single items currently exist? Would we want to consider extending that now to be closer to futureproof for global support?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T16:46:15Z
user: login: maddenp-cu
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T20:09:52Z

Review comments:
diff_hunk: @@ -11,7 +11,7 @@
               "type": "integer"
             },
             "length": {
-              "minimum": 1,
+              "minimum": 0,
path: docs/sections/user_guide/cli/drivers/mpas_init/show-schema.out
user: login: maddenp-cu
body: Updated from `make docs` stemming from a previous PR.
created_at: 2025-04-28T20:39:16Z
author_association: COLLABORATOR
diff_hunk: @@ -3,7 +3,7 @@
 
 .. include:: ../shared/idempotent.rst
 
-The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``orog``.
+The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``orog``. Documentation for this UFS Utils component is :ufs-utils:`here <orog>`.
path: docs/sections/user_guide/cli/drivers/orog/index.rst
user: login: maddenp-cu
body: Add a missing link to upstream docs.
created_at: 2025-04-28T20:39:42Z
author_association: COLLABORATOR
diff_hunk: @@ -3,7 +3,7 @@
 
 .. include:: ../shared/idempotent.rst
 
-The ``uw`` mode for configuring and running the :sfc-climo-gen:`sfc_climo_gen<>` component.
+The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``sfc_climo_gen``. Documentation for this UFS Utils component is :ufs-utils:`here <sfc-climo-gen>`.
path: docs/sections/user_guide/cli/drivers/sfc_climo_gen/index.rst
user: login: maddenp-cu
body: For consistency with upstream-docs links for other components.
created_at: 2025-04-28T20:40:03Z
author_association: COLLABORATOR
diff_hunk: @@ -114,13 +114,12 @@ def driver_name(cls) -> str:
         return STR.chgrescube
 
     @property
-    def output(self) -> dict[str, list[str]]:
+    def output(self) -> dict[str, Path]:
path: src/uwtools/drivers/chgres_cube.py
user: login: maddenp-cu
body: I'm not sure why we went with `str` instead of `Path` previously, as we generally prefer the latter wherever possible. Please let me know if anyone thinks `Path` isn't the right choice here.
created_at: 2025-04-28T20:41:13Z
author_association: COLLABORATOR
diff_hunk: @@ -114,13 +114,12 @@ def driver_name(cls) -> str:
         return STR.chgrescube
 
     @property
-    def output(self) -> dict[str, list[str]]:
+    def output(self) -> dict[str, Path]:
         """
         Returns a description of the file(s) created when this component runs.
         """
-        output_types = ["atm", "sfc"]
-        paths = [f"{self.rundir}/out.{t}.tile7.nc" for t in output_types]
-        return {"netcdffiles": paths}
path: src/uwtools/drivers/chgres_cube.py
user: login: maddenp-cu
body: Simplified slightly.
created_at: 2025-04-28T20:41:40Z
author_association: COLLABORATOR
diff_hunk: @@ -119,15 +119,15 @@ def output(self) -> dict[str, list[str]]:
         paths = []
         for _ in range(nblocks):
             identifier = lines[0]
-            paths.append(str(self.rundir / (identifier + suffix)))
+            paths.append(self.rundir / (identifier + suffix))
             fields, lines = lines[: self.NFIELDS], lines[self.NFIELDS :]
             _, lines = (
                 (lines[0], lines[1:])
                 if fields[self.GENPROCTYPE_IDX] == "ens_fcst"
                 else (None, lines)
             )
             lines = lines[self.NPARAMS * nvars.pop() :]
-        return {"gribfiles": paths}
+        return {"paths": paths}
path: src/uwtools/drivers/upp.py
user: login: maddenp-cu
body: I'm really not sure what's best for the keys in these values, but started to think that lots of different names might not be useful, and might be a least-surprise violation. So, where it seemed like there were meaningful keys to use, I've used those; elsewhere I've just used `path` for a single item and `paths` for a list. Happy to hear other ideas!
created_at: 2025-04-28T20:48:51Z
author_association: COLLABORATOR
diff_hunk: @@ -94,6 +94,16 @@ def driver_name(cls) -> str:
         """
         return STR.oroggsl
 
+    @property
+    def output(self) -> dict[str, Path]:
+        """
+        Returns a description of the file(s) created when this component runs.
+        """
+        vals = tuple(self.config["config"][k] for k in ["resolution", "tile", "halo"])
path: src/uwtools/drivers/orog_gsl.py
user: login: christinaholtNOAA
body: TIL that Python doesn't have explicit tuple comprehensions. 
created_at: 2025-04-29T16:40:50Z
author_association: COLLABORATOR
diff_hunk: @@ -114,13 +114,12 @@ def driver_name(cls) -> str:
         return STR.chgrescube
 
     @property
-    def output(self) -> dict[str, list[str]]:
+    def output(self) -> dict[str, list[Path]]:
         """
         Returns a description of the file(s) created when this component runs.
         """
-        output_types = ["atm", "sfc"]
-        paths = [f"{self.rundir}/out.{t}.tile7.nc" for t in output_types]
-        return {"netcdffiles": paths}
+        outfile = lambda x: self.rundir / f"out.{x}.tile7.nc"
+        return {"atm": [outfile("atm")], "sfc": [outfile("sfc")]}
path: src/uwtools/drivers/chgres_cube.py
user: login: maddenp-cu
body: Updated so that the values are `list[Path]` instead of just `Path`, to support multiple values for global configurations later without requiring breaking downstream changes.
created_at: 2025-04-29T20:08:07Z
author_association: COLLABORATOR
diff_hunk: @@ -52,6 +53,17 @@ def provisioned_rundir(self):
             self.runscript(),
         ]
 
+    @property
+    def output(self) -> dict[str, list[Path]]:
+        """
+        Returns a description of the file(s) created when this component runs.
+        """
+        cfg = self.config["namelist"]["update_values"]["config"]
+        halo = cfg.get("halo")
+        ns = [0, halo] if halo else [0]
+        keys = [m[1] for key in cfg if (m := re.match(r"^input_(.*)_file$", key))]
+        return {key: [self.rundir / f"{key}.tile7.halo{n}.nc" for n in ns] for key in keys}
path: src/uwtools/drivers/sfc_climo_gen.py
user: login: maddenp-cu
body: Updated so that the values are `list[Path]` instead of just `Path`, to support multiple values for global configurations later without requiring breaking downstream changes, and accounted for the fact that both `halo0` and e.g. `halo4` when `halo` is `4` output files are created.
created_at: 2025-04-29T20:09:34Z
author_association: COLLABORATOR

PR comments:
user: login: maddenp-cu
created_at: 2025-04-29T19:53:26Z
body: > I think any time we see `tile7` show up in one of these output files, we will expect size output files there for the global configuration (one each for tiles 1-6). How big a breaking change would we see in the future if we needed to create lists where the single items currently exist? Would we want to consider extending that now to be closer to futureproof for global support?

@christinaholtNOAA I think it should be generalized, but also that the limitation is currently widespread in `uwtools` drivers and needs to be dealt with holistically, in tandem with a global-model use case that lets us validate what we're doing. We knew that we were making assumptions/guesses in the initial implementation of the drivers, but IIRC intended to integrate them into SRW and then fix any issues that arose; then, once we had the regional case locked down with SRW as a sort of integration test for the drivers in regional mode, implement global behavior -- with _something_ as a use case, but I'm not sure what. I don't think it makes much sense to try to implement global behavior that we have no way to test, when we have only partial integration with SRW to even test regional behavior. I'm not happy about any of this, but feel like this piecemeal approach is the only sane way forward. Is any of that inaccurate?

That said, I do think that I should go back and make sure that the values are _lists_ where they should be, even if currently they're lists of 1 item with `time7` hardcoded. (And I see that `sfc_climo_gen` actually creates `halo0` files _plus_ files for whatever `halo` is specified, if it's specified, which it doesn't have to be but _can_ be for regional mode... Ugh. So much additional validation should be done that isn't implemented in JSON Schema at the moment, and maybe can't reasonably be given how complex it could be.)
author_association: COLLABORATOR
user: login: maddenp-cu
created_at: 2025-04-30T14:56:20Z
body: I'm going to go ahead and merge this, as I have another PR waiting in the wings. We can and should circle back on the regional-vs-global treatment of tiled outputs soon.
author_association: COLLABORATOR