Repository: yt-project/yt

Issue:
BUG: testing utility `@requires_ds` decorator doesn't work with pytest
### Bug report

**Bug summary**

`@requires_ds` doesn't play nicely with pytest

**Code for reproduction**

running this example test module with pytest, and within an env without `xarray` installed should not produce an error, but it does
```python
# t.py
from yt.testing import requires_module
from yt.utilities.answer_testing.framework import requires_ds

@requires_module("xarray")
@requires_ds("CfRadialGrid/grid1.nc")
def test_foo():
    pass
```

run with
```shell
$ pytest t.py
```

**Actual outcome**

```
============================================== test session starts ==============================================
platform darwin -- Python 3.10.3, pytest-7.1.1, pluggy-1.0.0 -- /Users/robcleme/.pyenv/versions/3.10.3/envs/yt-dev/bin/python3.10
cachedir: .pytest_cache
Matplotlib: 3.5.2
Freetype: 2.6.1
rootdir: /Users/robcleme/dev/yt-project/yt, configfile: pyproject.toml
plugins: anyio-3.5.0, forked-1.4.0, mpl-0.16.1, xdist-2.1.0, durations-1.1.0
collected 0 items / 1 error                                                                                     

==================================================== ERRORS =====================================================
_____________________________________________ ERROR collecting t.py _____________________________________________
ImportError while importing test module '/Users/robcleme/dev/yt-project/yt/t.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.pyenv/versions/3.10.3/lib/python3.10/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
t.py:5: in <module>
    @requires_ds("CfRadialGrid/grid1.nc")
yt/utilities/answer_testing/framework.py:1133: in requires_ds
    condition = (big_data and not run_big_data) or not can_run_ds(ds_fn, file_check)
yt/utilities/answer_testing/framework.py:309: in can_run_ds
    load(ds_fn)
yt/loaders.py:107: in load
    return candidates[0](fn, *args, **kwargs)
yt/frontends/cf_radial/data_structures.py:127: in __init__
    with self._handle(filename=filename) as xr_ds_handle:
../../../.pyenv/versions/3.10.3/lib/python3.10/contextlib.py:135: in __enter__
    return next(self.gen)
yt/frontends/cf_radial/data_structures.py:203: in _handle
    with xr.open_dataset(filename) as xrds:
yt/utilities/on_demand_imports.py:24: in __call__
    raise self.error
E   ImportError: This functionality requires the xarray package to be installed.
```

Note that the result is identical if I change the order of the decorators or simply remove the `@requires_module` decorator.
In contrast, replacing `@requires_ds` with `@yt.testing.requires_file` works as intended.


Discussion:
an even simpler reproducer is
```python
# t.py
from yt.utilities.answer_testing.framework import can_run_ds
can_run_ds("CfRadialGrid/grid1.nc")
```

Resolving patch:
diff --git a/yt/frontends/cf_radial/data_structures.py b/yt/frontends/cf_radial/data_structures.py
--- a/yt/frontends/cf_radial/data_structures.py
+++ b/yt/frontends/cf_radial/data_structures.py
@@ -294,6 +294,9 @@ def _is_valid(cls, filename, *args, **kwargs):
         # This accepts a filename or a set of arguments and returns True or
         # False depending on if the file is of the type requested.
 
+        if not xr.__is_available__:
+            return False
+
         warn_netcdf(filename)
         is_cfrad = False
         try:
diff --git a/yt/frontends/fits/data_structures.py b/yt/frontends/fits/data_structures.py
--- a/yt/frontends/fits/data_structures.py
+++ b/yt/frontends/fits/data_structures.py
@@ -540,7 +540,11 @@ def _determine_axes(self):
 
     @classmethod
     def _is_valid(cls, filename, *args, **kwargs):
-        fileh = check_fits_valid(filename)
+        try:
+            fileh = check_fits_valid(filename)
+        except Exception:
+            return False
+
         if fileh is None:
             return False
         else:
@@ -638,7 +642,11 @@ def _determine_bbox(self):
 
     @classmethod
     def _is_valid(cls, filename, *args, **kwargs):
-        fileh = check_fits_valid(filename)
+        try:
+            fileh = check_fits_valid(filename)
+        except Exception:
+            return False
+
         if fileh is None:
             return False
         else:
@@ -711,7 +719,10 @@ def _set_code_unit_attributes(self):
 
     @classmethod
     def _is_valid(cls, filename, *args, **kwargs):
-        return check_sky_coords(filename, ndim=2)
+        try:
+            return check_sky_coords(filename, ndim=2)
+        except Exception:
+            return False
 
 
 class SpectralCubeFITSHierarchy(FITSHierarchy):
@@ -821,7 +832,10 @@ def pixel2spec(self, pixel_value):
 
     @classmethod
     def _is_valid(cls, filename, *args, **kwargs):
-        return check_sky_coords(filename, ndim=3)
+        try:
+            return check_sky_coords(filename, ndim=3)
+        except Exception:
+            return False
 
 
 class EventsFITSHierarchy(FITSHierarchy):
@@ -920,7 +934,10 @@ def _determine_wcs(self):
 
     @classmethod
     def _is_valid(cls, filename, *args, **kwargs):
-        fileh = check_fits_valid(filename)
+        try:
+            fileh = check_fits_valid(filename)
+        except Exception:
+            return False
         if fileh is not None:
             try:
                 valid = fileh[1].name == "EVENTS"


Test patch:
diff --git a/yt/frontends/cf_radial/tests/test_outputs.py b/yt/frontends/cf_radial/tests/test_outputs.py
--- a/yt/frontends/cf_radial/tests/test_outputs.py
+++ b/yt/frontends/cf_radial/tests/test_outputs.py
@@ -39,14 +39,14 @@
 }
 
 
-@requires_file(cf)
 @requires_module("xarray")
+@requires_file(cf)
 def test_units_override():
     units_override_check(cf)
 
 
-@requires_file(cf)
 @requires_module("xarray")
+@requires_file(cf)
 def test_cf_radial_gridded():
     ds = data_dir_load(cf)
     assert isinstance(ds, CFRadialDataset)
@@ -89,8 +89,8 @@ def check_domain(ds):
     assert_equal(ds.domain_right_edge, domain_right_array)
 
 
-@requires_file(cf_nongridded)
 @requires_module("xarray")
+@requires_file(cf_nongridded)
 def test_auto_gridding():
     # loads up a radial dataset, which triggers the gridding.
 
@@ -123,8 +123,8 @@ def test_auto_gridding():
     shutil.rmtree(tempdir)
 
 
-@requires_file(cf_nongridded)
 @requires_module("xarray")
+@requires_file(cf_nongridded)
 def test_grid_parameters():
     # checks that the gridding parameters are used and that conflicts in parameters
     # are resolved as expected.
@@ -172,8 +172,8 @@ def test_grid_parameters():
     shutil.rmtree(tempdir)
 
 
-@requires_ds(cf)
 @requires_module("xarray")
+@requires_ds(cf)
 def test_cfradial_grid_field_values():
     ds = data_dir_load(cf)
     fields_to_check = [("cf_radial", field) for field in _fields_cfradial]
diff --git a/yt/frontends/fits/tests/test_outputs.py b/yt/frontends/fits/tests/test_outputs.py
--- a/yt/frontends/fits/tests/test_outputs.py
+++ b/yt/frontends/fits/tests/test_outputs.py
@@ -1,4 +1,9 @@
-from yt.testing import assert_equal, requires_file, units_override_check
+from yt.testing import (
+    assert_equal,
+    requires_file,
+    requires_module,
+    units_override_check,
+)
 from yt.utilities.answer_testing.framework import (
     data_dir_load,
     requires_ds,
@@ -31,6 +36,7 @@ def test_grs():
 vf = "UnigridData/velocity_field_20.fits"
 
 
+@requires_module("astropy")
 @requires_ds(vf)
 def test_velocity_field():
     ds = data_dir_load(vf, cls=FITSDataset)
diff --git a/yt/utilities/answer_testing/framework.py b/yt/utilities/answer_testing/framework.py
--- a/yt/utilities/answer_testing/framework.py
+++ b/yt/utilities/answer_testing/framework.py
@@ -34,7 +34,13 @@
     assert_rel_equal,
     skipif,
 )
-from yt.utilities.exceptions import YTCloudError, YTNoAnswerNameSpecified, YTNoOldAnswer
+from yt.utilities.exceptions import (
+    YTAmbiguousDataType,
+    YTCloudError,
+    YTNoAnswerNameSpecified,
+    YTNoOldAnswer,
+    YTUnidentifiedDataType,
+)
 from yt.utilities.logger import disable_stream_logging
 from yt.visualization import (
     image_writer as image_writer,
@@ -313,6 +319,9 @@ def can_run_ds(ds_fn, file_check=False):
                 result_storage["tainted"] = True
             raise
         return False
+    except (YTUnidentifiedDataType, YTAmbiguousDataType):
+        return False
+
     return result_storage is not None