Repository: CWorthy-ocean/C-Star

Issue:
Avoid copying and modifying a yaml on-disk
> The method first calls `InputDataset.get()` on the yaml file itself, as usual, making a local copy.

Is this a good pattern to enshrine? The whole point of separating the concept of a "case" from a "blueprint" was that the blueprint is the on-disk serialisation format for the case, but if we copy and edit files on disk we are arguably breaking that abstraction.

Might it be better to instead read the remote yaml file into an in-memory dict, edit it in-memory, then only write it out if `.persist()` is called?

_Originally posted by @TomNicholas in https://github.com/CWorthy-ocean/C-Star/issues/81#issuecomment-2351116912_
            

Discussion:

Patch:
diff --git a/cstar/base/input_dataset.py b/cstar/base/input_dataset.py
index cd96d80..b60e69d 100644
--- a/cstar/base/input_dataset.py
+++ b/cstar/base/input_dataset.py
@@ -51,11 +51,15 @@ class InputDataset(ABC):
         """
 
         self.source: DataSource = DataSource(location=location, file_hash=file_hash)
-        if (self.source.location_type == "url") and (self.source.file_hash is None):
+        if (
+            (self.source.location_type == "url")
+            and (self.source.file_hash is None)
+            and (self.source.source_type != "yaml")
+        ):
             raise ValueError(
                 f"Cannot create InputDataset for \n {self.source.location}:\n "
                 + "InputDataset.source.file_hash cannot be None if InputDataset.source.location_type is 'url'.\n"
-                + "A file hash is required to verify files downloaded from remote sources."
+                + "A file hash is required to verify non-plaintext files downloaded from remote sources."
             )
         if isinstance(start_date, str):
             start_date = dateutil.parser.parse(start_date)
diff --git a/cstar/roms/input_dataset.py b/cstar/roms/input_dataset.py
index 8128cbe..39f3e4e 100644
--- a/cstar/roms/input_dataset.py
+++ b/cstar/roms/input_dataset.py
@@ -1,6 +1,7 @@
 import yaml
-import shutil
+import tempfile
 import dateutil
+import requests
 import datetime as dt
 import roms_tools
 
@@ -149,7 +150,7 @@ class ROMSInputDataset(InputDataset, ABC):
         """
         # Ensure we're working with a Path object
         local_dir = Path(local_dir).resolve()
-
+        local_dir.mkdir(parents=True, exist_ok=True)
         # If `working_path` is set, determine we're not fetching to the same parent dir:
         if self.working_path is None:
             working_path_parent = None
@@ -162,26 +163,17 @@ class ROMSInputDataset(InputDataset, ABC):
             print(f"Input dataset already exists in {working_path_parent}, skipping.")
             return
 
-        super().get(local_dir=local_dir)
-
-        # If it's not a yaml, we're done
         if self.source.source_type != "yaml":
+            super().get(local_dir=local_dir)
             return
-
-        # Make sure that the local copy is not a symlink
-        # (as InputDataset.get() symlinks files that didn't need to be downloaded)
-        local_path = local_dir / Path(self.source.basename)
-        if local_path.is_symlink():
-            actual_path = local_path.resolve()
-            local_path.unlink()
-            shutil.copy2(actual_path, local_path)
-            local_path = actual_path
-
-        # Now modify the local copy of the yaml file as needed:
-        with open(local_path, "r") as F:
-            _, header, yaml_data = F.read().split("---", 2)
-            yaml_dict = yaml.safe_load(yaml_data)
-
+        elif self.source.location_type == "path":
+            with open(self.source.location) as F:
+                raw_yaml_text = F.read()
+        elif self.source.location_type == "url":
+            raw_yaml_text = requests.get(self.source.location).text
+        _, header, yaml_data = raw_yaml_text.split("---", 2)
+
+        yaml_dict = yaml.safe_load(yaml_data)
         yaml_keys = list(yaml_dict.keys())
         if len(yaml_keys) == 1:
             roms_tools_class_name = yaml_keys[0]
@@ -209,35 +201,36 @@ class ROMSInputDataset(InputDataset, ABC):
             if key in yaml_dict[roms_tools_class_name].keys():
                 yaml_dict[roms_tools_class_name][key] = value
 
-        with open(local_path, "w") as F:
-            F.write(f"---{header}---\n" + yaml.dump(yaml_dict))
-
-        # Finally, make a roms-tools object from the modified yaml
-        # import roms_tools
-
         roms_tools_class = getattr(roms_tools, roms_tools_class_name)
 
-        # roms-tools currently requires dask for every class except Grid
-        # in order to use wildcards in filepaths (known xarray issue):
+        # Create a temporary file that deletes itself when closed
+        with tempfile.NamedTemporaryFile(mode="w", delete=True) as temp_file:
+            temp_file.write(f"---{header}---\n" + yaml.dump(yaml_dict))
+            temp_file.flush()  # Ensure data is written to disk
 
-        if roms_tools_class_name == "Grid":
-            roms_tools_class_instance = roms_tools_class.from_yaml(local_path)
-        else:
-            roms_tools_class_instance = roms_tools_class.from_yaml(
-                local_path, use_dask=True
-            )
+            # roms-tools currently requires dask for every class except Grid
+            # in order to use wildcards in filepaths (known xarray issue):
+            if roms_tools_class_name == "Grid":
+                roms_tools_class_instance = roms_tools_class.from_yaml(temp_file.name)
+            else:
+                roms_tools_class_instance = roms_tools_class.from_yaml(
+                    temp_file.name, use_dask=True
+                )
+        ##
 
         # ... and save:
-        print(f"Saving roms-tools dataset created from {local_path}...")
+        print(f"Saving roms-tools dataset created from {self.source.location}...")
         if (np_eta is not None) and (np_xi is not None):
             savepath = roms_tools_class_instance.save(
-                local_dir / "PARTITIONED" / local_path.stem, np_xi=np_xi, np_eta=np_eta
+                local_dir / "PARTITIONED" / Path(self.source.location).stem,
+                np_xi=np_xi,
+                np_eta=np_eta,
             )
             self.partitioned_files = savepath
 
         else:
             savepath = roms_tools_class_instance.save(
-                Path(f"{local_dir/local_path.stem}.nc")
+                Path(f"{local_dir/Path(self.source.location).stem}.nc")
             )
         self.working_path = savepath[0] if len(savepath) == 1 else savepath
 


Test patch:
diff --git a/cstar/tests/unit_tests/base/test_input_dataset.py b/cstar/tests/unit_tests/base/test_input_dataset.py
index 013cf6a..ae77fb4 100644
--- a/cstar/tests/unit_tests/base/test_input_dataset.py
+++ b/cstar/tests/unit_tests/base/test_input_dataset.py
@@ -1,3 +1,4 @@
+import stat
 import pytest
 from unittest import mock
 from pathlib import Path
@@ -106,33 +107,25 @@ def remote_input_dataset():
 
 
 class TestInputDatasetInit:
-    """Unit tests for initializing InputDataset objects.
-
-    This class contains tests for initializing local and remote `InputDataset`
-    objects and verifying their attributes, as well as validating required parameters.
+    """Test class for the initialization of the InputDataset class.
 
     Tests
     -----
-    test_local_init:
-        Tests the initialization of a local `InputDataset` instance.
-    test_remote_init:
-        Tests the initialization of a remote `InputDataset` instance.
-    test_remote_requires_file_hash:
-        Verifies that a remote `InputDataset` raises a `ValueError` if the file hash is not provided.
+    test_local_init
+       Test initialization of an InputDataset with a local source
+    test_remote_init
+       Test initialization of an InputDataset with a remote source.
+    test_remote_requires_file_hash
+       Test that a remote InputDataset raises an error when the file hash is missing
     """
 
     def test_local_init(self, local_input_dataset):
-        """Test initialization of a local InputDataset.
+        """Test initialization of an InputDataset with a local source.
 
         Fixtures
         --------
         local_input_dataset: MockInputDataset instance for local files.
 
-        Mocks
-        -----
-        - Mocked `location_type` attribute of `DataSource`, returning 'path'.
-        - Mocked `basename` attribute of `DataSource`, returning 'local_file.nc'.
-
         Asserts
         -------
         - The `location_type` is "path".
@@ -151,18 +144,12 @@ class TestInputDatasetInit:
         ), "Expected an instance of MockInputDataset"
 
     def test_remote_init(self, remote_input_dataset):
-        """Test initialization of a remote InputDataset.
+        """Test initialization of an InputDataset with a remote source.
 
         Fixtures
         --------
         remote_input_dataset: MockInputDataset instance for remote files.
 
-        Mocks
-        -----
-        - Mocked `location_type` attribute of `DataSource`, returning 'url'.
-        - Mocked `basename` attribute of `DataSource`, returning 'remote_file.nc'.
-        - Mocked `file_hash` attribute of `DataSource`, returning 'abc123'.
-
         Asserts
         -------
         - The `location_type` is "url".
@@ -193,10 +180,6 @@ class TestInputDatasetInit:
         --------
         remote_input_dataset: MockInputDataset instance for remote files.
 
-        Mocks
-        -----
-        - Mocked remote InputDataset without a file hash.
-
         Asserts
         -------
         - A ValueError is raised if the `file_hash` is missing for a remote dataset.
@@ -208,25 +191,596 @@ class TestInputDatasetInit:
         expected_message = (
             "Cannot create InputDataset for \n http://example.com/remote_file.nc:\n "
             + "InputDataset.source.file_hash cannot be None if InputDataset.source.location_type is 'url'.\n"
-            + "A file hash is required to verify files downloaded from remote sources."
+            + "A file hash is required to verify non-plaintext files downloaded from remote sources."
         )
 
         assert str(exception_info.value) == expected_message
 
 
+class TestStrAndRepr:
+    """Test class for the __str__ and __repr__ methods on an InputDataset.
+
+    Tests
+    -----
+    test_local_str
+       Test the string representation of an InputDataset with a local source
+    test_local_repr
+       Test the repr representation of an InputDataset with a local source
+    test_remote_repr
+       Test the repr representation of an InputDataset with a remote source
+    test_remote_str
+       Test the string representation of an InputDataset with a remote source
+    test_str_with_working_path
+       Test the string representation when the InputDataset.working_path attribute is defined
+    test_repr_with_working_path
+       Test the repr representation when the InputDataset.working_path attribute is defined
+    """
+
+    def test_local_str(self, local_input_dataset):
+        """Test the string representation of a local InputDataset."""
+        expected_str = dedent("""\
+    ----------------
+    MockInputDataset
+    ----------------
+    Source location: some/local/source/path/local_file.nc
+    start_date: 2024-10-22 12:34:56
+    end_date: 2024-12-31 23:59:59
+    Working path: None ( does not yet exist. Call InputDataset.get() )""")
+        assert str(local_input_dataset) == expected_str
+
+    def test_local_repr(self, local_input_dataset):
+        """Test the repr representation of a local InputDataset."""
+        expected_repr = dedent("""\
+    MockInputDataset(
+    location = 'some/local/source/path/local_file.nc',
+    file_hash = None,
+    start_date = datetime.datetime(2024, 10, 22, 12, 34, 56),
+    end_date = datetime.datetime(2024, 12, 31, 23, 59, 59)
+    )""")
+        assert repr(local_input_dataset) == expected_repr
+
+    def test_remote_repr(self, remote_input_dataset):
+        """Test the repr representation of a remote InputDataset."""
+        expected_repr = dedent("""\
+    MockInputDataset(
+    location = 'http://example.com/remote_file.nc',
+    file_hash = 'abc123',
+    start_date = datetime.datetime(2024, 10, 22, 12, 34, 56),
+    end_date = datetime.datetime(2024, 12, 31, 23, 59, 59)
+    )""")
+        assert repr(remote_input_dataset) == expected_repr
+
+    def test_remote_str(self, remote_input_dataset):
+        """Test the string representation of a remote InputDataset."""
+        expected_str = dedent("""\
+    ----------------
+    MockInputDataset
+    ----------------
+    Source location: http://example.com/remote_file.nc
+    Source file hash: abc123
+    start_date: 2024-10-22 12:34:56
+    end_date: 2024-12-31 23:59:59
+    Working path: None ( does not yet exist. Call InputDataset.get() )""")
+        assert str(remote_input_dataset) == expected_str
+
+    @mock.patch.object(
+        MockInputDataset, "local_hash", new_callable=mock.PropertyMock
+    )  # Mock local_hash
+    @mock.patch.object(
+        MockInputDataset, "exists_locally", new_callable=mock.PropertyMock
+    )  # Mock exists_locally
+    def test_str_with_working_path(
+        self, mock_exists_locally, mock_local_hash, local_input_dataset
+    ):
+        """Test the string output when the working_path attribute is defined.
+
+        This test verifies that the string output includes the correct working path
+        and whether the path exists or not, mocking the `exists_locally` and `local_hash`
+        properties to simulate both cases.
+
+        Fixtures
+        --------
+        local_input_dataset: MockInputDataset instance for local files.
+
+        Asserts
+        -------
+        - The string output includes the working path when it is set.
+        - If the working path exists, the string includes "(exists)".
+        - If the working path does not exist, the string includes a message indicating the path does not yet exist.
+        """
+        local_input_dataset.working_path = Path("/some/local/path")
+
+        # Mock local_hash to prevent triggering _get_sha256_hash
+        mock_local_hash.return_value = {"mocked_path": "mocked_hash"}
+
+        # Simulate exists_locally being True
+        mock_exists_locally.return_value = True
+        assert "Working path: /some/local/path" in str(local_input_dataset)
+        assert "(exists)" in str(local_input_dataset)
+
+        # Simulate exists_locally being False
+        mock_exists_locally.return_value = False
+        assert "Working path: /some/local/path" in str(local_input_dataset)
+        assert " ( does not yet exist. Call InputDataset.get() )" in str(
+            local_input_dataset
+        )
+
+    @mock.patch.object(
+        MockInputDataset, "local_hash", new_callable=mock.PropertyMock
+    )  # Mock local_hash
+    @mock.patch.object(
+        MockInputDataset, "exists_locally", new_callable=mock.PropertyMock
+    )  # Mock exists_locally
+    def test_repr_with_working_path(
+        self, mock_exists_locally, mock_local_hash, local_input_dataset
+    ):
+        """Test the repr output when the working_path attribute is defined.
+
+        This test verifies that the repr output correctly includes the working path and indicates
+        whether or not the path exists, mocking the `exists_locally` and `local_hash` properties
+        to simulate both cases.
+
+        Fixtures
+        --------
+        local_input_dataset: MockInputDataset instance for local files.
+
+        Asserts
+        -------
+        - If the working path exists, the repr includes the path with no additional notes.
+        - If the working path does not exist, the repr includes a note indicating the path does not exist.
+        """
+        local_input_dataset.working_path = Path("/some/local/path")
+
+        # Mock local_hash to prevent triggering _get_sha256_hash
+        mock_local_hash.return_value = {"mocked_path": "mocked_hash"}
+
+        # Simulate exists_locally being True
+        mock_exists_locally.return_value = True
+        assert (
+            "State: <working_path = /some/local/path, local_hash = {'mocked_path': 'mocked_hash'}>"
+            in repr(local_input_dataset)
+        )
+
+        # Simulate exists_locally being False
+        mock_exists_locally.return_value = False
+        mock_local_hash.return_value = None
+        assert "State: <working_path = /some/local/path (does not exist)>" in repr(
+            local_input_dataset
+        )
+
+
+class TestExistsLocally:
+    """Test class for the 'exists_locally' property.
+
+    Tests
+    -----
+    test_no_working_path_or_stat_cache
+       Test exists_locally when no working path or stat cache is defined
+    test_file_does_not_exist
+       Test exists_locally when the file does not exist
+    test_no_cached_stats
+       Test exists_locally when no cached stats are available
+    test_size_mismatch
+       Test exists_locally when the file size does not match the cached value
+    test_modification_time_mismatch_with_hash_match
+       Test exists_locally when the modification time does not match but the hash
+    test_modification_time_and_hash_mismatch
+       Test exists_locally when both modification time and hash do not match.
+    test_all_checks_pass
+       Test exists_locally when all checks pass
+    """
+
+    def test_no_working_path_or_stat_cache(self, local_input_dataset):
+        """Test exists_locally when no working path or stat cache is defined.
+
+        Asserts:
+        - exists_locally is False when `working_path` or `_local_file_stat_cache` is None.
+        """
+        local_input_dataset.working_path = None
+        local_input_dataset._local_file_stat_cache = None
+        assert (
+            not local_input_dataset.exists_locally
+        ), "Expected exists_locally to be False when working_path or stat cache is None"
+
+    def test_file_does_not_exist(self, local_input_dataset):
+        """Test exists_locally when the file does not exist.
+
+        Asserts:
+        - exists_locally is False when any file in `working_path` does not exist.
+        """
+        local_input_dataset.working_path = Path("/some/nonexistent/path")
+        local_input_dataset._local_file_stat_cache = {
+            Path("/some/nonexistent/path"): None
+        }
+
+        with mock.patch.object(Path, "exists", return_value=False):
+            assert (
+                not local_input_dataset.exists_locally
+            ), "Expected exists_locally to be False when the file does not exist"
+
+    def test_no_cached_stats(self, local_input_dataset):
+        """Test exists_locally when no cached stats are available.
+
+        Asserts:
+        - exists_locally is False when no stats are cached for a file.
+        """
+        local_input_dataset.working_path = Path("/some/local/path")
+        local_input_dataset._local_file_stat_cache = {}
+
+        with mock.patch.object(Path, "exists", return_value=True):
+            assert (
+                not local_input_dataset.exists_locally
+            ), "Expected exists_locally to be False when no cached stats are available"
+
+    def test_size_mismatch(self, local_input_dataset):
+        """Test exists_locally when the file size does not match the cached value.
+
+        Asserts:
+        - exists_locally is False when the file size does not match.
+        """
+        local_input_dataset.working_path = Path("/some/local/path")
+        local_input_dataset._local_file_stat_cache = {
+            Path("/some/local/path"): mock.Mock(st_size=100)
+        }
+
+        with mock.patch.object(Path, "exists", return_value=True):
+            with mock.patch.object(Path, "stat", return_value=mock.Mock(st_size=200)):
+                assert not local_input_dataset.exists_locally, "Expected exists_locally to be False when file size does not match cached stats"
+
+    def test_modification_time_mismatch_with_hash_match(self, local_input_dataset):
+        """Test exists_locally when the modification time does not match but the hash
+        matches.
+
+        Asserts:
+        - exists_locally is True when the modification time does not match but the hash matches.
+        """
+        local_input_dataset.working_path = Path("/some/local/path")
+        local_input_dataset._local_file_stat_cache = {
+            Path("/some/local/path"): mock.Mock(st_size=100, st_mtime=12345)
+        }
+        local_input_dataset._local_file_hash_cache = {
+            Path("/some/local/path"): "mocked_hash"
+        }
+
+        with mock.patch.object(Path, "exists", return_value=True):
+            with mock.patch.object(
+                Path,
+                "stat",
+                return_value=mock.Mock(
+                    st_size=100, st_mtime=54321, st_mode=stat.S_IFREG
+                ),
+            ):
+                with mock.patch(
+                    "cstar.base.input_dataset._get_sha256_hash",
+                    return_value="mocked_hash",
+                ):
+                    assert local_input_dataset.exists_locally, "Expected exists_locally to be True when modification time mismatches but hash matches"
+
+    def test_modification_time_and_hash_mismatch(self, local_input_dataset):
+        """Test exists_locally when both modification time and hash do not match.
+
+        Asserts:
+        - exists_locally is False when both modification time and hash do not match.
+        """
+        local_input_dataset.working_path = Path("/some/local/path")
+        local_input_dataset._local_file_stat_cache = {
+            Path("/some/local/path"): mock.Mock(st_size=100, st_mtime=12345)
+        }
+        local_input_dataset._local_file_hash_cache = {
+            Path("/some/local/path"): "mocked_hash"
+        }
+
+        with mock.patch.object(Path, "exists", return_value=True):
+            with mock.patch.object(
+                Path,
+                "stat",
+                return_value=mock.Mock(
+                    st_size=100, st_mtime=54321, st_mode=stat.S_IFREG
+                ),
+            ):
+                with mock.patch(
+                    "cstar.base.input_dataset._get_sha256_hash",
+                    return_value="different_hash",
+                ):
+                    assert not local_input_dataset.exists_locally, "Expected exists_locally to be False when both modification time and hash do not match"
+
+    def test_all_checks_pass(self, local_input_dataset):
+        """Test exists_locally when all checks pass.
+
+        Asserts:
+        - exists_locally is True when the file exists, size matches, and hash matches (if needed).
+        """
+        local_input_dataset.working_path = Path("/some/local/path")
+        local_input_dataset._local_file_stat_cache = {
+            Path("/some/local/path"): mock.Mock(st_size=100, st_mtime=12345)
+        }
+        local_input_dataset._local_file_hash_cache = {
+            Path("/some/local/path"): "mocked_hash"
+        }
+
+        with mock.patch.object(Path, "exists", return_value=True):
+            with mock.patch.object(
+                Path, "stat", return_value=mock.Mock(st_size=100, st_mtime=12345)
+            ):
+                assert (
+                    local_input_dataset.exists_locally
+                ), "Expected exists_locally to be True when all checks pass"
+
+
+def test_to_dict(remote_input_dataset):
+    """Test the InputDataset.to_dict method, using a remote InputDataset as an example.
+
+    Fixtures
+    --------
+    remote_input_dataset: MockInputDataset instance for remote files.
+
+    Asserts
+    -------
+    - The dictionary returned matches a known expected dictionary
+    """
+    assert remote_input_dataset.to_dict() == {
+        "location": "http://example.com/remote_file.nc",
+        "file_hash": "abc123",
+        "start_date": "2024-10-22 12:34:56",
+        "end_date": "2024-12-31 23:59:59",
+    }
+
+
+class TestInputDatasetGet:
+    """Test class for the InputDataset.get method.
+
+    This test class covers scenarios for both local and remote datasets and verifies the
+    behavior of the InputDataset.get method, including handling of existing files,
+    file downloading, and symbolic link creation.
+
+    Attributes
+    ----------
+    - target_dir: Simulated directory for storing files.
+    - target_filepath_local: Path for local files in the target directory.
+    - target_filepath_remote: Path for remote files in the target directory.
+
+    Tests
+    -----
+    - test_get_when_filename_exists
+    - test_get_with_local_source
+    - test_get_with_remote_source
+    - test_get_remote_with_no_file_hash
+    """
+
+    # Common attributes
+    target_dir = Path("/some/local/target/dir")
+    target_filepath_local = target_dir / "local_file.nc"
+    target_filepath_remote = target_dir / "remote_file.nc"
+
+    def setup_method(self, local_input_dataset):
+        """Setup method to patch various file system operations used in the get method.
+
+        This method mocks file system interactions to prevent actual disk operations during testing.
+
+        Mocks
+        -----
+        - Path.mkdir: Mocks directory creation to avoid creating real directories.
+        - Path.symlink_to: Mocks symbolic link creation to avoid modifying the file system.
+        - Path.resolve: Mocks path resolution, allowing the test to control what paths are "resolved" to.
+        - Path.exists: Mocks file existence checks to simulate whether files or directories already exist.
+        """
+        # Patch Path.mkdir globally for all tests in this class to avoid file system interaction
+        self.patch_mkdir = mock.patch.object(Path, "mkdir")
+        self.mock_mkdir = self.patch_mkdir.start()
+
+        # Patch Path.symlink_to globally for all tests
+        self.patch_symlink_to = mock.patch.object(Path, "symlink_to")
+        self.mock_symlink_to = self.patch_symlink_to.start()
+
+        # Patch Path.resolve globally for all tests but let each test set the side_effect
+        self.patcher_resolve = mock.patch.object(Path, "resolve")
+        self.mock_resolve = self.patcher_resolve.start()
+
+        # Patch Path.exists globally for all tests but let each test set the return_value
+        self.patcher_exists = mock.patch.object(Path, "exists")
+        self.mock_exists = self.patcher_exists.start()
+
+    def teardown_method(self):
+        """Stops all patches started in setup_method."""
+        mock.patch.stopall()
+
+    @mock.patch("cstar.base.input_dataset._get_sha256_hash", return_value="mocked_hash")
+    @mock.patch.object(
+        MockInputDataset, "exists_locally", new_callable=mock.PropertyMock
+    )
+    def test_get_when_file_exists(
+        self, mock_exists_locally, mock_get_hash, local_input_dataset
+    ):
+        """Test the InputDataset.get method when the target file already exists."""
+        # Hardcode the resolved path for local_dir
+        local_dir_resolved = Path("/resolved/local/dir")
+        target_path = local_dir_resolved / "local_file.nc"
+
+        # Set the side effect of the mocked resolve
+        self.mock_resolve.return_value = local_dir_resolved
+
+        # Mock `exists_locally` to return True
+        mock_exists_locally.return_value = True
+
+        # Set `working_path` to match `target_path`
+        local_input_dataset.working_path = target_path
+
+        # Call the `get` method
+        local_input_dataset.get(self.target_dir)
+
+        # Ensure `_get_sha256_hash` was not called
+        mock_get_hash.assert_not_called()
+
+        # Assert `working_path` remains unchanged
+        assert local_input_dataset.working_path == target_path, (
+            f"Expected working_path to remain as {target_path}, "
+            f"but got {local_input_dataset.working_path}"
+        )
+
+    @mock.patch("cstar.base.input_dataset._get_sha256_hash", return_value="mocked_hash")
+    def test_get_with_local_source(self, mock_get_hash, local_input_dataset):
+        """Test the InputDataset.get method with a local source file.
+
+        This test verifies that when the source file is local, a symbolic link is
+        created in the target directory and the working_path is updated accordingly.
+        """
+        # Define resolved paths for local_dir and source file
+        local_dir_resolved = Path("/resolved/local/dir")
+        source_filepath_local = Path("/resolved/source/local_file.nc")
+
+        # Set the side effect of the mocked resolve to return the correct paths
+        self.mock_resolve.side_effect = [local_dir_resolved, source_filepath_local]
+
+        # Mock Path.exists to simulate that the file doesn't exist yet in local_dir
+        self.mock_exists.return_value = False
+
+        # Mock Path.stat to simulate valid file stats for target_path
+        mock_stat_result = mock.Mock(
+            st_size=12345, st_mtime=1678901234, st_mode=0o100644
+        )
+        with mock.patch.object(Path, "stat", return_value=mock_stat_result):
+            # Call the get method
+            local_input_dataset.get(self.target_dir)
+
+            # Assert that a symbolic link was created with the resolved path
+            self.mock_symlink_to.assert_called_once_with(source_filepath_local)
+
+            # Assert that working_path is updated to the resolved target path
+            expected_target_path = local_dir_resolved / "local_file.nc"
+            assert (
+                local_input_dataset.working_path == expected_target_path
+            ), f"Expected working_path to be {expected_target_path}, but got {local_input_dataset.working_path}"
+
+    @mock.patch("cstar.base.input_dataset._get_sha256_hash", return_value="mocked_hash")
+    def test_get_local_wrong_hash(self, mock_get_hash, local_input_dataset):
+        """Test the `get` method with a bogus file_hash for local sources."""
+        # Assign a bogus file hash
+        local_input_dataset.source.file_hash = "bogus_hash"
+
+        # Define the resolved source and target paths
+        source_filepath_local = Path("/resolved/source/local_file.nc")
+        target_filepath_local = Path("/resolved/local/target/dir/local_file.nc")
+
+        # Mock Path.resolve to return the correct paths
+        self.mock_resolve.side_effect = [target_filepath_local, source_filepath_local]
+
+        # Mock Path.exists to simulate that the file doesn't yet exist
+        self.mock_exists.return_value = False
+
+        # Call `get` and assert it raises a ValueError
+        with pytest.raises(
+            ValueError, match="The provided file hash.*does not match.*"
+        ):
+            local_input_dataset.get(self.target_dir)
+
+        # Ensure `_get_sha256_hash` was called with the source path
+        mock_get_hash.assert_called_once_with(source_filepath_local)
+
+    @mock.patch("pooch.create")
+    @mock.patch("pooch.HTTPDownloader")
+    @mock.patch("cstar.base.input_dataset._get_sha256_hash", return_value="mocked_hash")
+    def test_get_with_remote_source(
+        self, mock_get_hash, mock_downloader, mock_pooch_create, remote_input_dataset
+    ):
+        """Test the InputDataset.get method with a remote source file.
+
+        This test verifies that when the source file is remote, the file is downloaded
+        correctly using pooch, and the working_path is updated to the downloaded file
+        path.
+        """
+        # Define resolved paths
+        target_filepath_remote = self.target_dir / "remote_file.nc"
+
+        # Mock Path.stat to simulate file stats for target_path
+        mock_stat_result = mock.Mock(
+            st_size=12345, st_mtime=1678901234, st_mode=0o100644
+        )
+        with mock.patch.object(Path, "stat", return_value=mock_stat_result):
+            # Mock Path.exists to simulate the target file does not yet exist
+            self.mock_exists.return_value = False
+
+            # Mock Path.resolve to return the correct target directory
+            with mock.patch.object(Path, "resolve", return_value=self.target_dir):
+                # Create a mock Pooch instance and mock the fetch method
+                mock_pooch_instance = mock.Mock()
+                mock_pooch_create.return_value = mock_pooch_instance
+                mock_fetch = mock.Mock()
+                mock_pooch_instance.fetch = mock_fetch
+
+                # Call the get method
+                remote_input_dataset.get(self.target_dir)
+
+                # Ensure pooch.create was called correctly
+                mock_pooch_create.assert_called_once_with(
+                    path=self.target_dir,
+                    base_url="http://example.com/",
+                    registry={"remote_file.nc": "abc123"},
+                )
+
+                # Ensure fetch was called with the mocked downloader
+                mock_fetch.assert_called_once_with(
+                    "remote_file.nc", downloader=mock_downloader.return_value
+                )
+
+                # Assert that working_path is updated to the expected target path
+                assert (
+                    remote_input_dataset.working_path == target_filepath_remote
+                ), f"Expected working_path to be {target_filepath_remote}, but got {remote_input_dataset.working_path}"
+
+    def test_get_remote_with_no_file_hash(self, remote_input_dataset):
+        """Test the InputDataset.get method when no file_hash is provided for a remote
+        source.
+
+        This test verifies that the get method raises a ValueError when a remote source file is
+        attempted to be fetched without a defined file_hash, as file verification is necessary.
+
+        Fixtures
+        --------
+        remote_input_dataset: MockInputDataset instance for remote files.
+        mock_exists: Mock for Path.exists to simulate that the target file does not yet exist.
+        mock_resolve: Mock for Path.resolve to simulate resolving the target directory.
+
+        Asserts
+        -------
+        - A ValueError is raised when no file_hash is provided for a remote file.
+        - The error message matches the expected message regarding the missing file_hash.
+        """
+        remote_input_dataset.source.file_hash = None
+        self.mock_exists.return_value = False
+        self.mock_resolve.return_value = self.target_dir
+        expected_message = (
+            "InputDataset.source.source_type is 'url' "
+            + "but no InputDataset.source.file_hash is not defined. "
+            + "Cannot proceed."
+        )
+
+        with pytest.raises(ValueError) as exception_info:
+            remote_input_dataset.get(self.target_dir)
+        assert str(exception_info.value) == expected_message
+
+
 class TestLocalHash:
-    """Tests for the `local_hash` property of the `InputDataset`.
+    """Test class for the `local_hash` property.
 
-    This test suite verifies the behavior of the `local_hash` property, ensuring
-    it correctly computes, caches, and handles edge cases such as multiple files
-    or a missing `working_path` attribute.
+    Mocks
+    -----
+    Path.resolve()
+       Mocks calls to resolve any mocked paths with pathlib
+    cstar.utils._get_sha256_hash
+       Mocks calls to compute sha256 checksums using cstar.utils
+    InputDataset.exists_locally
+       Mocks calls to the boolean `exists_locally` property of InputDataset
 
     Tests
     -----
-    - `test_local_hash_single_file`: Verifies the calculation of `local_hash` for a single file.
-    - `test_local_hash_cached`: Ensures cached hash values are used when available.
-    - `test_local_hash_no_working_path`: Confirms that `local_hash` returns `None` when no working path is set.
-    - `test_local_hash_multiple_files`: Validates `local_hash` computation for multiple files.
+    test_local_hash_single_file
+       Test `local_hash` calculation for a single file.
+    test_local_hash_cached
+       Test `local_hash` when the hash is cached
+    test_local_hash_no_working_path
+       Test `local_hash` when no working path is set.
+    test_local_hash_multiple_files
+       Test `local_hash` calculation for multiple files
     """
 
     def setup_method(self):
@@ -254,21 +808,7 @@ class TestLocalHash:
         mock.patch.stopall()
 
     def test_local_hash_single_file(self, local_input_dataset):
-        """Test `local_hash` calculation for a single file.
-
-        This test ensures that the `local_hash` property calculates the hash correctly
-        for a single file in the `working_path`.
-
-        Mocks
-        -----
-        - Mocked `Path.resolve` to simulate resolved paths.
-        - Mocked `_get_sha256_hash` to simulate hash computation.
-
-        Asserts
-        -------
-        - The `local_hash` matches the expected hash for the file.
-        - `_get_sha256_hash` is called with the resolved path.
-        """
+        """Test `local_hash` calculation for a single file."""
         local_input_dataset._local_file_hash_cache = None
         local_input_dataset.working_path = Path("/some/local/path")
 
@@ -289,17 +829,7 @@ class TestLocalHash:
         self.mock_get_hash.assert_called_once_with(Path("/some/local/path"))
 
     def test_local_hash_cached(self, local_input_dataset):
-        """Test `local_hash` when the hash is cached.
-
-        This test ensures that if the `_local_file_hash_cache` is already set,
-        the `local_hash` property uses the cached value without recomputing.
-
-        Asserts
-        -------
-        - The `local_hash` property returns the cached value.
-        - `_get_sha256_hash` is not called.
-        """
-
+        """Test `local_hash` when the hash is cached."""
         cached_hash = {Path("/resolved/local/path"): "cached_hash"}
         local_input_dataset._local_file_hash_cache = cached_hash
 
@@ -309,17 +839,7 @@ class TestLocalHash:
         self.mock_get_hash.assert_not_called()
 
     def test_local_hash_no_working_path(self, local_input_dataset):
-        """Test `local_hash` when no working path is set.
-
-        This test ensures that the `local_hash` property returns `None` when the
-        `working_path` attribute is not defined, indicating no valid local file exists.
-
-        Asserts
-        -------
-        - The `local_hash` property returns `None` when `working_path` is `None`.
-        - `_get_sha256_hash` is not called.
-        """
-
+        """Test `local_hash` when no working path is set."""
         local_input_dataset.working_path = None
 
         result = local_input_dataset.local_hash
@@ -330,23 +850,7 @@ class TestLocalHash:
         self.mock_get_hash.assert_not_called()
 
     def test_local_hash_multiple_files(self, local_input_dataset):
-        """Test `local_hash` calculation for multiple files.
-
-        This test ensures that the `local_hash` property correctly computes and returns
-        SHA256 hashes for multiple files when `working_path` is a list of paths.
-
-        Mocks
-        -----
-        - `Path.resolve`: Mocked to return predefined resolved paths for each file.
-        - `_get_sha256_hash`: Mocked to return a consistent hash value for testing.
-
-        Asserts
-        -------
-        - The `local_hash` property returns a dictionary mapping each file path to its
-          corresponding hash.
-        - `_get_sha256_hash` is called for each resolved path in `working_path`.
-        """
-
+        """Test `local_hash` calculation for multiple files."""
         local_input_dataset._local_file_hash_cache = None
         local_input_dataset.working_path = [
             Path("/some/local/path1"),
@@ -372,136 +876,3 @@ class TestLocalHash:
             ],
             any_order=True,
         )
-
-
-class TestStrAndRepr:
-    """Tests for string and representation methods of the `InputDataset` class.
-
-    This test class verifies the correctness of the `__str__` and `__repr__` methods
-    for both local and remote datasets, as well as scenarios where the `working_path` is
-    defined or missing.
-
-    Tests
-    -----
-    - `test_local_str`: Ensures the `__str__` method for a local dataset produces the
-      expected string output.
-    - `test_local_repr`: Ensures the `__repr__` method for a local dataset produces the
-      expected representation string.
-    - `test_remote_str`: Ensures the `__str__` method for a remote dataset produces the
-      expected string output.
-    - `test_remote_repr`: Ensures the `__repr__` method for a remote dataset produces the
-      expected representation string.
-    - `test_str_with_working_path`: Verifies the `__str__` output when the `working_path`
-      attribute is defined.
-    - `test_repr_with_working_path`: Verifies the `__repr__` output when the `working_path`
-      attribute is defined.
-    """
-
-    def test_local_str(self, local_input_dataset):
-        """Test the string representation of a local InputDataset."""
-        expected_str = dedent("""\
-            ----------------
-            MockInputDataset
-            ----------------
-            Source location: some/local/source/path/local_file.nc
-            start_date: 2024-10-22 12:34:56
-            end_date: 2024-12-31 23:59:59
-            Working path: None ( does not yet exist. Call InputDataset.get() )
-        """).strip()
-        assert str(local_input_dataset) == expected_str
-
-    def test_local_repr(self, local_input_dataset):
-        """Test the repr representation of a local InputDataset."""
-        expected_repr = dedent("""\
-        MockInputDataset(
-        location = 'some/local/source/path/local_file.nc',
-        file_hash = None,
-        start_date = datetime.datetime(2024, 10, 22, 12, 34, 56),
-        end_date = datetime.datetime(2024, 12, 31, 23, 59, 59)
-        )
-        """).strip()
-        actual_repr = repr(local_input_dataset)
-        assert (
-            actual_repr == expected_repr
-        ), f"Expected:\n{expected_repr}\nBut got:\n{actual_repr}"
-
-    def test_remote_repr(self, remote_input_dataset):
-        """Test the repr representation of a remote InputDataset."""
-        expected_repr = dedent("""\
-        MockInputDataset(
-        location = 'http://example.com/remote_file.nc',
-        file_hash = 'abc123',
-        start_date = datetime.datetime(2024, 10, 22, 12, 34, 56),
-        end_date = datetime.datetime(2024, 12, 31, 23, 59, 59)
-        )
-        """).strip()
-        actual_repr = repr(remote_input_dataset)
-        assert (
-            actual_repr == expected_repr
-        ), f"Expected:\n{expected_repr}\nBut got:\n{actual_repr}"
-
-    def test_remote_str(self, remote_input_dataset):
-        """Test the string representation of a remote InputDataset."""
-        expected_str = dedent("""\
-            ----------------
-            MockInputDataset
-            ----------------
-            Source location: http://example.com/remote_file.nc
-            Source file hash: abc123
-            start_date: 2024-10-22 12:34:56
-            end_date: 2024-12-31 23:59:59
-            Working path: None ( does not yet exist. Call InputDataset.get() )
-        """).strip()
-        assert str(remote_input_dataset) == expected_str
-
-    @mock.patch.object(
-        MockInputDataset, "local_hash", new_callable=mock.PropertyMock
-    )  # Mock local_hash
-    @mock.patch.object(
-        MockInputDataset, "exists_locally", new_callable=mock.PropertyMock
-    )  # Mock exists_locally
-    def test_str_with_working_path(
-        self, mock_exists_locally, mock_local_hash, local_input_dataset
-    ):
-        """Test the string output when the working_path attribute is defined."""
-        local_input_dataset.working_path = Path("/some/local/path")
-        mock_local_hash.return_value = {"mocked_path": "mocked_hash"}
-
-        # Simulate exists_locally being True
-        mock_exists_locally.return_value = True
-        assert "Working path: /some/local/path" in str(local_input_dataset)
-        assert "(exists)" in str(local_input_dataset)
-
-        # Simulate exists_locally being False
-        mock_exists_locally.return_value = False
-        assert "Working path: /some/local/path" in str(local_input_dataset)
-        assert " ( does not yet exist. Call InputDataset.get() )" in str(
-            local_input_dataset
-        )
-
-    @mock.patch.object(
-        MockInputDataset, "local_hash", new_callable=mock.PropertyMock
-    )  # Mock local_hash
-    @mock.patch.object(
-        MockInputDataset, "exists_locally", new_callable=mock.PropertyMock
-    )  # Mock exists_locally
-    def test_repr_with_working_path(
-        self, mock_exists_locally, mock_local_hash, local_input_dataset
-    ):
-        """Test the repr output when the working_path attribute is defined."""
-        local_input_dataset.working_path = Path("/some/local/path")
-        mock_local_hash.return_value = {"mocked_path": "mocked_hash"}
-
-        # Simulate exists_locally being True
-        mock_exists_locally.return_value = True
-        assert (
-            "State: <working_path = /some/local/path, local_hash = {'mocked_path': 'mocked_hash'}>"
-            in repr(local_input_dataset)
-        )
-
-        # Simulate exists_locally being False
-        mock_exists_locally.return_value = False
-        mock_local_hash.return_value = None
-        assert "State: <working_path = /some/local/path (does not exist)>" in repr(
-            local_input_dataset
-        )
diff --git a/cstar/tests/unit_tests/roms/test_roms_input_dataset.py b/cstar/tests/unit_tests/roms/test_roms_input_dataset.py
index ca13852..a2d835b 100644
--- a/cstar/tests/unit_tests/roms/test_roms_input_dataset.py
+++ b/cstar/tests/unit_tests/roms/test_roms_input_dataset.py
@@ -89,6 +89,44 @@ def local_roms_yaml_dataset():
         yield dataset
 
 
+@pytest.fixture
+def remote_roms_yaml_dataset():
+    """Fixture to provide a ROMSInputDataset with a remote YAML source.
+
+    Mocks:
+    ------
+    - DataSource.location_type: Property mocked as 'url'
+    - DataSource.source_type: Property mocked as 'yaml'
+    - DataSource.basename: Property mocked as 'remote_file.yaml'
+
+    Yields:
+    -------
+        MockROMSInputDataset: A mock dataset pointing to a local YAML file.
+    """
+    with (
+        mock.patch.object(
+            DataSource, "location_type", new_callable=mock.PropertyMock
+        ) as mock_location_type,
+        mock.patch.object(
+            DataSource, "source_type", new_callable=mock.PropertyMock
+        ) as mock_source_type,
+        mock.patch.object(
+            DataSource, "basename", new_callable=mock.PropertyMock
+        ) as mock_basename,
+    ):
+        mock_location_type.return_value = "url"
+        mock_source_type.return_value = "yaml"
+        mock_basename.return_value = "remote_file.yaml"
+
+        dataset = MockROMSInputDataset(
+            location="https://dodgyfakeyamlfiles.ru/all/remote_file.yaml",
+            start_date="2024-10-22 12:34:56",
+            end_date="2024-12-31 23:59:59",
+        )
+
+        yield dataset
+
+
 ################################################################################
 
 
@@ -247,10 +285,6 @@ class TestROMSInputDatasetGet:
         Mocks:
         ------
         - `InputDataset.get`: Mocks the parent class' `get` method to simulate dataset retrieval.
-        - `Path.is_symlink`: Mocks `is_symlink` to control symlink behavior.
-        - `Path.resolve`: Mocks `resolve` to simulate resolving symlinks.
-        - `Path.unlink`: Mocks `unlink` to simulate file unlinking.
-        - `shutil.copy2`: Mocks file copying to avoid modifying the file system.
         - `builtins.open`: Mocks the `open` function to simulate reading YAML files.
         - `yaml.safe_load`: Mocks YAML parsing to return a test-specific dictionary.
         - `yaml.dump`: Mocks YAML dumping to simulate saving modified YAML files.
@@ -269,18 +303,11 @@ class TestROMSInputDatasetGet:
         self.mock_get = self.patch_get.start()
 
         # Mocking Path methods
-        self.patch_is_symlink = mock.patch("pathlib.Path.is_symlink", autospec=True)
-        self.mock_is_symlink = self.patch_is_symlink.start()
-
         self.patch_resolve = mock.patch("pathlib.Path.resolve", autospec=True)
         self.mock_resolve = self.patch_resolve.start()
 
-        self.patch_unlink = mock.patch("pathlib.Path.unlink", autospec=True)
-        self.mock_unlink = self.patch_unlink.start()
-
-        # Mock shutil.copy2
-        self.patch_copy2 = mock.patch("shutil.copy2", autospec=True)
-        self.mock_copy2 = self.patch_copy2.start()
+        self.patch_mkdir = mock.patch("pathlib.Path.mkdir", autospec=True)
+        self.mock_mkdir = self.patch_mkdir.start()
 
         # Mock open for reading YAML
         # When we call 'read' in the tested method, we split header and data,
@@ -332,8 +359,9 @@ class TestROMSInputDatasetGet:
         source.
 
         This test ensures the `get` method correctly processes a `roms-tools` YAML file to
-        create partitioned ROMS grid files. It covers steps including resolving symlinks,
-        copying and modifying YAML files, and saving the Grid object with proper partitioning.
+        create partitioned ROMS grid files. It covers opening and reading a YAML file,
+        editing it in memory, creating a Grid object,
+        and saving the Grid object with proper partitioning.
 
         Fixtures:
         ---------
@@ -343,19 +371,13 @@ class TestROMSInputDatasetGet:
         ------
         - `Path.stat`: Simulates retrieving file metadata for partitioned files.
         - `_get_sha256_hash`: Simulates computing the hash of each partitioned file.
-        - `Path.is_symlink`: Simulates symlink detection for the source YAML file.
-        - `Path.resolve`: Simulates resolving paths for symlinks and partitioned files.
-        - `Path.unlink`: Simulates unlinking symlinks.
-        - `shutil.copy2`: Simulates copying the YAML file to the target directory.
         - `yaml.safe_load`: Simulates loading YAML content from a file.
         - `roms_tools.Grid.from_yaml`: Simulates creating a Grid object from the YAML file.
         - `roms_tools.Grid.save`: Simulates saving Grid data as partitioned NetCDF files.
 
         Asserts:
         --------
-        - Ensures `get` is called on the YAML file with correct arguments.
-        - Verifies symlink handling operations such as `is_symlink`, `unlink`, and `copy2`.
-        - Confirms `resolve` is called for the directory, YAML file, and partitioned files.
+        - Confirms `resolve` is called for the directory, and partitioned files.
         - Ensures `yaml.safe_load` processes the YAML content as expected.
         - Validates `roms_tools.Grid.from_yaml` creates the Grid object from the YAML file.
         - Verifies `roms_tools.Grid.save` saves files with correct partitioning parameters.
@@ -369,14 +391,9 @@ class TestROMSInputDatasetGet:
         )
         mock_stat.return_value = mock_stat_result
 
-        # Mock the is_symlink method to return True
-        self.mock_is_symlink.return_value = True
-
         # Mock resolve to return a resolved path
-        resolved_path = Path("/resolved/path/to/local_file.yaml")
         self.mock_resolve.side_effect = [
-            Path("some/local/dir"),  # First resolve: local_dir
-            resolved_path,  # Second resolve: symlink target
+            Path("some/local/dir"),  # First resolve: local_dir passed to 'get'
             *(
                 Path(f"some/local/dir/PARTITIONED/local_file.{i:02d}.nc")
                 for i in range(1, 13)
@@ -394,26 +411,15 @@ class TestROMSInputDatasetGet:
             for i in range(1, 13)
         ]
         self.mock_rt_grid_instance.save.return_value = partitioned_paths
+        self.mock_yaml_dump.return_value = "mocked_yaml_content"
 
         # Call the method under test
         local_roms_yaml_dataset.get(local_dir=Path("some/local/dir"), np_xi=3, np_eta=4)
 
-        # Assert "get" was called on the yaml file itself
-        self.mock_get.assert_called_once_with(
-            local_roms_yaml_dataset, local_dir=Path("some/local/dir")
-        )
-
-        # Assert that symlink handling code was triggered
-        self.mock_is_symlink.assert_called_once()
-        self.mock_unlink.assert_called_once()
-        self.mock_copy2.assert_called_once_with(
-            resolved_path, Path("some/local/dir/local_file.yaml")
-        )
-
         # Assert resolve calls
         expected_resolve_calls = [
             mock.call(Path("some/local/dir")),
-            mock.call(Path("some/local/dir/local_file.yaml")),
+            # mock.call(Path("some/local/dir/local_file.yaml")),
             *(
                 mock.call(Path(f"some/local/dir/PARTITIONED/local_file.{i:02d}.nc"))
                 for i in range(1, 13)
@@ -428,7 +434,7 @@ class TestROMSInputDatasetGet:
         self.mock_yaml_load.assert_called_once()
 
         # Assert that roms_tools.Grid.from_yaml was called
-        self.mock_rt_grid.from_yaml.assert_called_once_with(resolved_path)
+        self.mock_rt_grid.from_yaml.assert_called_once()
 
         # Finally, ensure the save method is called
         self.mock_rt_grid_instance.save.assert_called_once_with(
@@ -444,6 +450,95 @@ class TestROMSInputDatasetGet:
             f"but got {mock_stat.call_count} calls."
         )
 
+    @mock.patch("cstar.roms.input_dataset._get_sha256_hash", return_value="mocked_hash")
+    @mock.patch("pathlib.Path.stat", autospec=True)
+    @mock.patch("requests.get", autospec=True)
+    def test_get_grid_from_remote_yaml_partitioned(
+        self, mock_request, mock_stat, mock_get_hash, remote_roms_yaml_dataset
+    ):
+        """Test the `get` method for unpartitioned ROMS grid files from a remote YAML
+        source.
+
+        This test ensures the `get` method correctly processes a `roms-tools` YAML file to
+        create partitioned ROMS grid files. It covers requesting yaml data from a URL,
+        editing it in memory, creating a Grid object,
+        and saving the Grid object with proper partitioning.
+
+        Fixtures:
+        ---------
+        - `remote_roms_yaml_dataset`: Provides a ROMSInputDataset instance with a remote YAML source.
+
+        Mocks:
+        ------
+        - `Path.stat`: Simulates retrieving file metadata for partitioned files.
+        - `_get_sha256_hash`: Simulates computing the hash of each partitioned file.
+        - `yaml.safe_load`: Simulates loading YAML content from a file.
+        - `roms_tools.Grid.from_yaml`: Simulates creating a Grid object from the YAML file.
+        - `roms_tools.Grid.save`: Simulates saving Grid data as partitioned NetCDF files.
+
+        Asserts:
+        --------
+        - Confirms `resolve` is called for the directory, and saved file.
+        - Ensures `yaml.safe_load` processes the YAML content as expected.
+        - Validates `roms_tools.Grid.from_yaml` creates the Grid object from the YAML file.
+        - Verifies `roms_tools.Grid.save` saves files with correct partitioning parameters.
+        - Ensures metadata and checksums for partitioned files are cached via `stat` and `_get_sha256_hash`.
+        """
+
+        # Mock the stat result
+        mock_stat_result = mock.Mock(
+            st_size=12345, st_mtime=1678901234, st_mode=0o100644
+        )
+        mock_stat.return_value = mock_stat_result
+
+        # Mock the call to requests.get on the remote yaml file
+        mock_request.return_value.text = "---\nheader---\ndata"
+
+        self.mock_resolve.side_effect = [
+            Path("some/local/dir"),  # First resolve: local_dir
+            Path("some/local/dir/remote_file.nc"),  # Second resolve: during caching
+        ]
+
+        # Mock the list of paths returned by roms_tools.save
+        self.mock_rt_grid_instance.save.return_value = [
+            Path("some/local/dir/remote_file.nc"),
+        ]
+
+        # Mock yaml loading
+        self.mock_yaml_load.return_value = {
+            "Grid": {"source": "ETOPO5", "fake": "entry"}
+        }
+        self.mock_yaml_dump.return_value = "mocked_yaml_content"
+
+        # Call the method under test
+        remote_roms_yaml_dataset.get(local_dir=Path("some/local/dir"))
+
+        # Assert resolve calls
+        expected_resolve_calls = [
+            mock.call(Path("some/local/dir")),
+            mock.call(Path("some/local/dir/remote_file.nc")),
+        ]
+        assert self.mock_resolve.call_args_list == expected_resolve_calls, (
+            f"Expected resolve calls:\n{expected_resolve_calls}\n"
+            f"But got:\n{self.mock_resolve.call_args_list}"
+        )
+
+        # Check that the yaml.safe_load was called properly
+        self.mock_yaml_load.assert_called_once()
+
+        # Assert that roms_tools.Grid.from_yaml was called
+        self.mock_rt_grid.from_yaml.assert_called_once()
+
+        # Finally, ensure the save method is called
+        self.mock_rt_grid_instance.save.assert_called_once_with(
+            Path("some/local/dir/remote_file.nc")
+        )
+
+        # Ensure stat was called for the saved file
+        assert (
+            mock_stat.call_count == 1
+        ), f"Expected stat to be called 1 time, but got {mock_stat.call_count} calls."
+
     @mock.patch("pathlib.Path.stat", autospec=True)
     @mock.patch("cstar.roms.input_dataset._get_sha256_hash", return_value="mocked_hash")
     def test_get_surface_forcing_from_local_yaml_unpartitioned(
@@ -464,10 +559,6 @@ class TestROMSInputDatasetGet:
         ------
         - `Path.stat`: Simulates retrieving file metadata for the generated file.
         - `_get_sha256_hash`: Simulates computing the hash of the saved file.
-        - `Path.is_symlink`: Simulates symlink detection for the source YAML file.
-        - `Path.resolve`: Simulates resolving paths for symlinks and the saved file.
-        - `Path.unlink`: Simulates unlinking symlinks.
-        - `shutil.copy2`: Simulates copying the YAML file to the target directory.
         - `yaml.safe_load`: Simulates loading YAML content from a file.
         - `roms_tools.SurfaceForcing.from_yaml`: Simulates creating a SurfaceForcing object from the YAML file.
         - `roms_tools.SurfaceForcing.save`: Simulates saving SurfaceForcing data as an unpartitioned NetCDF file.
@@ -476,22 +567,16 @@ class TestROMSInputDatasetGet:
         --------
         - Ensures the start and end times in the YAML dictionary are updated correctly.
         - Validates that `get` is called on the YAML file with the correct arguments.
-        - Confirms symlink handling operations such as `is_symlink`, `unlink`, and `copy2`.
         - Verifies `yaml.safe_load` processes the YAML content correctly.
         - Confirms `roms_tools.SurfaceForcing.from_yaml` creates the SurfaceForcing object from the YAML file.
         - Ensures `roms_tools.SurfaceForcing.save` saves the file with the correct parameters.
         - Verifies file metadata and checksum caching via `stat` and `_get_sha256_hash`.
         """
 
-        # Mock the is_symlink method to return True
-        self.mock_is_symlink.return_value = True
-
         # Mock resolve to return a resolved path
-        resolved_path = Path("/resolved/path/to/local_file.yaml")
         self.mock_resolve.side_effect = [
             Path("some/local/dir"),  # First resolve: local_dir
-            resolved_path,  # Second resolve: symlink target
-            Path("some/local/dir/local_file.nc"),  # Third resolve: during caching
+            Path("some/local/dir/local_file.nc"),  # Second resolve: during caching
         ]
 
         # Mock yaml loading for a more complex YAML with both Grid and SurfaceForcing
@@ -507,7 +592,8 @@ class TestROMSInputDatasetGet:
 
         # Mock yaml.safe_load to return this dictionary
         self.mock_yaml_load.return_value = yaml_dict
-
+        # Mock yaml.dump to return something 'write' can handle:
+        self.mock_yaml_dump.return_value = "mocked_yaml_content"
         # Configure from_yaml mock to return the SurfaceForcing instance
         self.mock_rt_surface_forcing.from_yaml.return_value = (
             self.mock_rt_surface_forcing_instance
@@ -538,25 +624,11 @@ class TestROMSInputDatasetGet:
             == dt.datetime(2022, 1, 31).isoformat()
         )
 
-        # Assertions to ensure everything worked as expected
-        self.mock_get.assert_called_once_with(
-            local_roms_yaml_dataset, Path("some/local/dir")
-        )
-
-        # Assert that symlink handling code was triggered
-        self.mock_is_symlink.assert_called_once()
-        self.mock_unlink.assert_called_once()
-        self.mock_copy2.assert_called_once_with(
-            resolved_path, Path("some/local/dir/local_file.yaml")
-        )
-
         # Check that the yaml.safe_load was called properly
         self.mock_yaml_load.assert_called_once()
 
         # Assert that roms_tools.SurfaceForcing was instantiated
-        self.mock_rt_surface_forcing.from_yaml.assert_called_once_with(
-            resolved_path, use_dask=True
-        )
+        self.mock_rt_surface_forcing.from_yaml.assert_called_once()
 
         # Ensure the save method was called for the SurfaceForcing instance
         self.mock_rt_surface_forcing_instance.save.assert_called_once_with(
@@ -584,7 +656,7 @@ class TestROMSInputDatasetGet:
 
         Mocks:
         ------
-        - `Path.resolve`: Simulates resolving a symlink to the actual file path.
+        - `Path.resolve`: Simulates resolving the path to the source yaml file
         - `yaml.safe_load`: Simulates loading YAML content from a file with too many sections.
 
         Asserts:
@@ -599,7 +671,7 @@ class TestROMSInputDatasetGet:
         resolved_path = Path("/resolved/path/to/local_file.yaml")
         self.mock_resolve.side_effect = [
             Path("some/local/dir"),  # First resolve: local_dir
-            resolved_path,  # Second resolve: symlink target
+            resolved_path,  # Second resolve: source location
         ]
 
         # Mock yaml loading for a YAML with too many sections
@@ -685,9 +757,6 @@ class TestROMSInputDatasetGet:
 
         # Ensure no further operations were performed
         self.mock_get.assert_not_called()
-        self.mock_is_symlink.assert_not_called()
-        self.mock_unlink.assert_not_called()
-        self.mock_copy2.assert_not_called()
         self.mock_yaml_load.assert_not_called()
 
     @mock.patch(
@@ -743,9 +812,6 @@ class TestROMSInputDatasetGet:
 
         # Ensure no further operations were performed
         self.mock_get.assert_not_called()
-        self.mock_is_symlink.assert_not_called()
-        self.mock_unlink.assert_not_called()
-        self.mock_copy2.assert_not_called()
         self.mock_yaml_load.assert_not_called()
 
     @mock.patch(
@@ -767,7 +833,7 @@ class TestROMSInputDatasetGet:
         Asserts:
         --------
         - Ensures the parent class `get` method (`self.mock_get`) is called with the correct arguments.
-        - Ensures no further actions (e.g., resolving symlinks, modifying YAML, or saving files) occur.
+        - Ensures no further actions (e.g., loading, modifying YAML, or saving files) occur.
         - Ensures no messages are printed during the method's execution.
         """
         # Mock the `source` attribute and its `source_type` property
@@ -796,15 +862,6 @@ class TestROMSInputDatasetGet:
 
             # Ensure no further processing happened
             mock_print.assert_not_called()
-            assert (
-                not self.mock_is_symlink.called
-            ), "Expected no calls to is_symlink, but some occurred."
-            assert (
-                not self.mock_unlink.called
-            ), "Expected no calls to unlink, but some occurred."
-            assert (
-                not self.mock_copy2.called
-            ), "Expected no calls to copy2, but some occurred."
             assert (
                 not self.mock_yaml_load.called
             ), "Expected no calls to yaml.safe_load, but some occurred."
@@ -825,6 +882,14 @@ class TestROMSInputDatasetPartition:
         Validates that an error is raised if files span multiple directories.
     """
 
+    def setup_method(self):
+        self.patch_mkdir = mock.patch("pathlib.Path.mkdir", autospec=True)
+        self.mock_mkdir = self.patch_mkdir.start()
+
+    def teardown_method(self):
+        """Stop all patches."""
+        mock.patch.stopall()
+
     @mock.patch("cstar.roms.input_dataset.roms_tools.utils.partition_netcdf")
     def test_partition_single_file(
         self, mock_partition_netcdf, local_roms_netcdf_dataset