Repository: xCDAT/xcdat

Issue:
[Bug]: `create_grid` breaks if an arg is a tuple with the second element being `None`
### What happened?

I ran into this issue during e3sm_diags development while testing a function to reconstruct pressure levels. 
I passed a single pressure level (e.g., `plevs=[800]`) to `create_axis("lev", plevs, generate_bounds=True)` and it threw an error about generating bounds requiring  > 1 data points.

To workaround this, I tried setting `generate_bounds=False`. However, I ran into the issue of `create_grid` breaking because of a bug in the conditional for copying bounds as described below.

Steps to reproduce (refer to MVCE):

1. Use `z_axis = xc.create_axis(...generate_bounds=False)`, which will return a tuple consisting of `(xr.DataArray, None)`. 
2. Pass this tuple as an argument to `xc.create_grid(z=z_axis)`
3. `xc.create_grid` breaks on line 544 -- `AttributeError: 'NoneType' object has no attribute 'copy'`
   - This line attempts to deep copy bounds (the second element of the tuple), which breaks since it is `None`.
   - https://github.com/xCDAT/xcdat/blob/08cfea37cf47454abea83de66cf7677ab157708b/xcdat/regridder/grid.py#L544

### What did you expect to happen? Are there are possible answers you came across?

The conditional to check the argument type for `create_grid` should also check if the second element is `None` and execute the appropriate logic

https://github.com/xCDAT/xcdat/blob/08cfea37cf47454abea83de66cf7677ab157708b/xcdat/regridder/grid.py#L532-L553

### Minimal Complete Verifiable Example (MVCE)

```python
import xcdat as xc

plevs = [800, 200]

# Create the output pressure grid to regrid to using the `plevs` array.
z_axis = xc.create_axis("lev", plevs, generate_bounds=False)
pressure_grid = xc.create_grid(z=z_axis)
```


### Relevant log output

```python
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/global/homes/v/vo13/mambaforge/envs/e3sm_diags_dev_658/lib/python3.10/site-packages/xcdat/regridder/grid.py", line 544, in create_grid
    axis, bnds = item[0].copy(deep=True), item[1].copy(deep=True)  # type: ignore[union-attr]
AttributeError: 'NoneType' object has no attribute 'copy'
```


### Anything else we need to know?

_No response_

### Environment

xcdat=0.6.0rc1 and main branch

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/xCDAT/xcdat/pull/539?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xCDAT) Report
Patch coverage: **`100.00%`** and no project coverage change.
> Comparison is base [(`08cfea3`)](https://app.codecov.io/gh/xCDAT/xcdat/commit/08cfea37cf47454abea83de66cf7677ab157708b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xCDAT) 100.00% compared to head [(`fd530aa`)](https://app.codecov.io/gh/xCDAT/xcdat/pull/539?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xCDAT) 100.00%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #539   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           15        15           
  Lines         1590      1592    +2     
=========================================
+ Hits          1590      1592    +2     
```


| [Files Changed](https://app.codecov.io/gh/xCDAT/xcdat/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xCDAT) | Coverage Δ | |
|---|---|---|
| [xcdat/regridder/grid.py](https://app.codecov.io/gh/xCDAT/xcdat/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xCDAT#diff-eGNkYXQvcmVncmlkZGVyL2dyaWQucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/xCDAT/xcdat/pull/539?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xCDAT).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xCDAT).


Patch:
diff --git a/xcdat/regridder/grid.py b/xcdat/regridder/grid.py
index 5b53af6..5feb082 100644
--- a/xcdat/regridder/grid.py
+++ b/xcdat/regridder/grid.py
@@ -529,28 +529,30 @@ def create_grid(
     axes = {"x": x, "y": y, "z": z}
     ds = xr.Dataset(attrs={} if attrs is None else attrs.copy())
 
-    for key, item in axes.items():
+    for axis, item in axes.items():
         if item is None:
             continue
 
         if isinstance(item, (tuple, list)):
             if len(item) != 2:
                 raise ValueError(
-                    f"Argument {key!r} should be an xr.DataArray representing "
+                    f"Argument {axis!r} should be an xr.DataArray representing "
                     "coordinates or a tuple (xr.DataArray, xr.DataArray) representing "
                     "coordinates and bounds."
                 )
 
-            axis, bnds = item[0].copy(deep=True), item[1].copy(deep=True)  # type: ignore[union-attr]
+            coords = item[0].copy(deep=True)
 
-            # ensure bnds attribute is set
-            axis.attrs["bounds"] = bnds.name
+            if item[1] is not None:
+                bnds = item[1].copy(deep=True)
 
-            ds = ds.assign({bnds.name: bnds})
+                coords.attrs["bounds"] = bnds.name
+
+                ds = ds.assign({bnds.name: bnds})
         else:
-            axis = item.copy(deep=True)
+            coords = item.copy(deep=True)
 
-        ds = ds.assign_coords({axis.name: axis})
+        ds = ds.assign_coords({coords.name: coords})
 
     return ds
 


Test patch:
diff --git a/tests/test_regrid.py b/tests/test_regrid.py
index 4a23aa1..b996097 100644
--- a/tests/test_regrid.py
+++ b/tests/test_regrid.py
@@ -885,13 +885,21 @@ class TestGrid:
         ):
             grid.create_grid()
 
-    def test_create_grid(self):
+    def test_create_grid_with_coords(self):
         new_grid = grid.create_grid(x=self.lon, y=self.lat)
 
         assert np.array_equal(new_grid.lat, self.lat)
         assert np.array_equal(new_grid.lon, self.lon)
 
-    def test_create_grid_with_bounds(self):
+    def test_create_grid_with_tuple_of_coords_and_no_bounds(self):
+        # This case happens if `create_axis` is used without creating bounds,
+        # which will return a tuple of (xr.DataArray, None).
+        new_grid = grid.create_grid(x=(self.lon, None), y=(self.lat, None))
+
+        assert np.array_equal(new_grid.lat, self.lat)
+        assert np.array_equal(new_grid.lon, self.lon)
+
+    def test_create_grid_with_tuple_of_coords_and_bounds(self):
         new_grid = grid.create_grid(
             x=(self.lon, self.lon_bnds), y=(self.lat, self.lat_bnds)
         )