Repository: wtbarnes/fiasco

Issue:
Generalize the decorator @needs_dataset, which assumes that the input is a fiasco.Ion
While testing the Gaunt factor objects in #293, I'm running into missing datasets because of the database version.  While doing so, I noticed that the decorator `@needs_dataset` assumes that the input is a `fiasco.Ion` object, which results in errors similar to this: 

`FAILED fiasco/tests/test_gaunt.py::test_gaunt_factor_free_free_total_itoh[3-True-False-75-1.34662286] - AttributeError: 'GauntFactor' object has no attribute 'ion_name'`

The real error is that it hasn't found `itoh_integrated_gaunt_nonrel.txt` when parsing in the data files, so this is a bit misleading.  

Anyway, just a note that this decorator ought to be generalized, or a new decorator should be created for other types of objects.


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/wtbarnes/fiasco/pull/316?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Will+Barnes) Report
Attention: Patch coverage is `95.91837%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 93.14%. Comparing base [(`7256ed3`)](https://app.codecov.io/gh/wtbarnes/fiasco/commit/7256ed33089c72878dcf94ed7022618d57ebb025?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Will+Barnes) to head [(`25a4f37`)](https://app.codecov.io/gh/wtbarnes/fiasco/commit/25a4f3785533d8f578bb8005fb44135a0b2b16b4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Will+Barnes).

| [Files with missing lines](https://app.codecov.io/gh/wtbarnes/fiasco/pull/316?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Will+Barnes) | Patch % | Lines |
|---|---|---|
| [fiasco/util/tests/test\_decorator.py](https://app.codecov.io/gh/wtbarnes/fiasco/pull/316?src=pr&el=tree&filepath=fiasco%2Futil%2Ftests%2Ftest_decorator.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Will+Barnes#diff-Zmlhc2NvL3V0aWwvdGVzdHMvdGVzdF9kZWNvcmF0b3IucHk=) | 95.65% | [2 Missing :warning: ](https://app.codecov.io/gh/wtbarnes/fiasco/pull/316?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Will+Barnes) |

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


```diff
@@            Coverage Diff             @@
##             main     #316      +/-   ##
==========================================
+ Coverage   92.65%   93.14%   +0.48%     
==========================================
  Files          37       38       +1     
  Lines        3119     3165      +46     
==========================================
+ Hits         2890     2948      +58     
+ Misses        229      217      -12     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/wtbarnes/fiasco/pull/316?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Will+Barnes).   
: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=Will+Barnes).


Patch:
diff --git a/fiasco/util/decorators.py b/fiasco/util/decorators.py
index a4b6c01..c3e919c 100644
--- a/fiasco/util/decorators.py
+++ b/fiasco/util/decorators.py
@@ -17,16 +17,18 @@ def needs_dataset(*names):
 
     def decorator(func):
         """
-        func is a method of `fiasco.Ion`.
+        func is a method of an object that requires a dataset, e.g Ion.
         """
         @wraps(func)
         def func_wrapper(*args, **kwargs):
-            ion = args[0]
+            obj = args[0]
             for n in names:
                 try:
-                    _ = ion.__getattribute__(n)
+                    _ = obj.__getattribute__(n)
                 except KeyError:
-                    raise MissingDatasetException(f'{n} dataset missing for {ion.ion_name}.')
+                    raise MissingDatasetException(
+                        f"{n} dataset missing for {getattr(obj, 'ion_name', obj)}."
+                    )
 
             return func(*args, **kwargs)
         return func_wrapper


Test patch:
diff --git a/fiasco/util/tests/test_decorator.py b/fiasco/util/tests/test_decorator.py
new file mode 100644
index 0000000..2b9fba4
--- /dev/null
+++ b/fiasco/util/tests/test_decorator.py
@@ -0,0 +1,64 @@
+"""
+Tests for any decorators
+"""
+import astropy.units as u
+import pytest
+
+from fiasco import Ion
+from fiasco.util import needs_dataset
+from fiasco.util.exceptions import MissingDatasetException
+
+
+@pytest.fixture()
+def ion_like_object(hdf5_dbase_root):
+    class NewIon(Ion):
+        @needs_dataset('elvlc')
+        def elvlc_method(self):
+            pass
+        @needs_dataset('fake')
+        def fake_method(self):
+            pass
+        @property
+        def _fake(self):
+            raise KeyError
+    return NewIon('Fe XI', 1*u.MK, hdf5_dbase_root=hdf5_dbase_root)
+
+
+@pytest.fixture()
+def non_ion_like_object():
+    class NonIon:
+        @needs_dataset('elvlc')
+        def elvlc_method(self):
+            pass
+        @property
+        def _elvlc(self):
+            pass
+        @needs_dataset('fake')
+        def fake_method(self):
+            pass
+        @property
+        def _fake(self):
+            raise KeyError
+        def __str__(self):
+            return 'non-ion'
+    return NonIon()
+
+
+def test_decorator_ion_like_has_data(ion_like_object):
+    # Just test that this does not throw an exception
+    assert ion_like_object.elvlc_method() is None
+
+
+def test_decorator_ion_like_has_no_data(ion_like_object):
+    with pytest.raises(MissingDatasetException, match='fake dataset missing for Fe 11'):
+        ion_like_object.fake_method()
+
+
+def test_decorator_non_ion_like_has_data(non_ion_like_object):
+    # Just test that this does not throw an exception
+    assert non_ion_like_object.elvlc_method() is None
+
+
+def test_decorator_non_ion_like_has_no_data(non_ion_like_object):
+    with pytest.raises(MissingDatasetException, match='fake dataset missing for non-ion'):
+        non_ion_like_object.fake_method()