Repository: gammapy/gammapy

Issue:
Use pytest fixtures properly
pytest fixtures should be injected into tests, not called as functions.

To quote from https://docs.pytest.org/en/latest/changelog.html#pytest-3-7-0-2018-07-30

> Calling a fixture function directly, as opposed to request them in a test function, now issues a RemovedInPytest4Warning. It will be changed into an error in pytest 4.0.

We have a few cases where we call fixtures.

I tried to remove one here:
https://github.com/gammapy/gammapy/commit/783f66042057cbea2c3b4714a65ecaa18dede73a#diff-0debc01ce3d7be3a9f0278816f236183R74
but this leads to fails e.g. in `gammapy/spectrum/tests/test_extract.py` because those fixtures are imported there.

Therefore I've reverted that change in master just now in 28852004864da88731bff96f3e2b3ee891759b05 .

This is a reminder issue to resolve that properly. For sure we shouldn't call fixtures.
The question is how to share that fixture code.
Either we move the fixtures to `conftest.py`, so that they are visible from multiple test places.
Or we change those "fixtures" into normal functions.

@adonath @registerrier - Let's discuss and find a solution tomorrow.


Discussion:
I noticed again that `gammapy/background/tests/test_reflected.py` and `gammapy/spectrum/tests/test_extract.py` should be cleaned up to have sane use of setup code. One option would be to use `make_something` helper functions instead of fixtures if that helps.

Specifically I was puzzled by a fail that showed up in `test_compute_energy_threshold` because `extraction` was used without `extraction.run()` having been called. For now I added it here:
https://github.com/gammapy/gammapy/commit/2a4b688ba2c4b7d193e408465d148a8bedfe66f2#diff-8a3a149acc144cd2c63f9bffb70e6a84R112
But now the extraction is running multiple times, so it's slow at the least, and possibly there's still errors that will appear if tests are run in a different order, because the global `extract` fixture is used and modified in several tests.

Resolving patch:
diff --git a/gammapy/catalog/hess.py b/gammapy/catalog/hess.py
--- a/gammapy/catalog/hess.py
+++ b/gammapy/catalog/hess.py
@@ -632,10 +632,7 @@ class SourceCatalogHGPS(SourceCatalog):
 
     def __init__(self, filename=None, hdu="HGPS_SOURCES"):
         if not filename:
-            filename = (
-                Path(os.environ["HGPS_ANALYSIS"])
-                / "data/catalogs/HGPS3/release/HGPS_v0.4.fits"
-            )
+            filename = "$GAMMAPY_EXTRA/datasets/catalogs/hgps_catalog_v1.fits.gz"
 
         filename = str(make_path(filename))
         table = Table.read(filename, hdu=hdu)


Test patch:
diff --git a/gammapy/background/tests/test_phase.py b/gammapy/background/tests/test_phase.py
--- a/gammapy/background/tests/test_phase.py
+++ b/gammapy/background/tests/test_phase.py
@@ -8,7 +8,7 @@
 from .. import BackgroundEstimate, PhaseBackgroundEstimator
 
 
-@pytest.fixture
+@pytest.fixture(scope="session")
 def on_region():
     """Example on_region for testing."""
     pos = SkyCoord("08h35m20.65525s", "-45d10m35.1545s", frame="icrs")
@@ -16,7 +16,7 @@ def on_region():
     return CircleSkyRegion(pos, radius)
 
 
-@pytest.fixture
+@pytest.fixture(scope="session")
 def observations():
     """Example observation list for testing."""
     datastore = DataStore.from_dir("$GAMMAPY_EXTRA/datasets/cta-1dc/index/gps")
@@ -24,11 +24,11 @@ def observations():
 
 
 @pytest.fixture(scope="session")
-def phase_bkg_estimator():
+def phase_bkg_estimator(on_region, observations):
     """Example background estimator for testing."""
     return PhaseBackgroundEstimator(
-        observations=observations(),
-        on_region=on_region(),
+        observations=observations,
+        on_region=on_region,
         on_phase=(0.5, 0.6),
         off_phase=(0.7, 1),
     )
diff --git a/gammapy/background/tests/test_reflected.py b/gammapy/background/tests/test_reflected.py
--- a/gammapy/background/tests/test_reflected.py
+++ b/gammapy/background/tests/test_reflected.py
@@ -14,8 +14,8 @@
 from ..reflected import ReflectedRegionsFinder, ReflectedRegionsBackgroundEstimator
 
 
-@pytest.fixture
-def mask():
+@pytest.fixture(scope="session")
+def exclusion_mask():
     """Example mask for testing."""
     pos = SkyCoord(83.63, 22.01, unit="deg", frame="icrs")
     exclusion_region = CircleSkyRegion(pos, Angle(0.3, "deg"))
@@ -24,7 +24,7 @@ def mask():
     return WcsNDMap(geom, data=mask)
 
 
-@pytest.fixture
+@pytest.fixture(scope="session")
 def on_region():
     """Example on_region for testing."""
     pos = SkyCoord(83.63, 22.01, unit="deg", frame="icrs")
@@ -33,7 +33,7 @@ def on_region():
     return region
 
 
-@pytest.fixture
+@pytest.fixture(scope="session")
 def observations():
     """Example observation list for testing."""
     datastore = DataStore.from_dir("$GAMMAPY_EXTRA/datasets/hess-dl3-dr1")
@@ -41,22 +41,23 @@ def observations():
     return datastore.get_observations(obs_ids)
 
 
-@pytest.fixture
-def bkg_estimator():
+@pytest.fixture(scope="session")
+def bkg_estimator(observations, exclusion_mask, on_region):
     """Example background estimator for testing."""
     return ReflectedRegionsBackgroundEstimator(
-        observations=observations(), on_region=on_region(), exclusion_mask=mask()
+        observations=observations, on_region=on_region, exclusion_mask=exclusion_mask,
+        min_distance_input="0.2 deg"
     )
 
 
 @requires_data("gammapy-extra")
-def test_find_reflected_regions(mask, on_region):
+def test_find_reflected_regions(exclusion_mask, on_region):
     pointing = SkyCoord(83.2, 22.5, unit="deg")
     fregions = ReflectedRegionsFinder(
         center=pointing,
         region=on_region,
-        exclusion_mask=mask,
-        min_distance_input=Angle("0 deg"),
+        exclusion_mask=exclusion_mask,
+        min_distance_input="0 deg",
     )
     fregions.run()
     regions = fregions.reflected_regions
@@ -70,7 +71,7 @@ def test_find_reflected_regions(mask, on_region):
     assert len(regions) == 16
 
     # Test with too small exclusion
-    small_mask = mask.cutout(pointing, Angle("0.2 deg"))
+    small_mask = exclusion_mask.cutout(pointing, Angle("0.2 deg"))
     fregions.exclusion_mask = small_mask
     fregions.run()
     regions = fregions.reflected_regions
@@ -86,26 +87,16 @@ def test_find_reflected_regions(mask, on_region):
 
 @requires_data("gammapy-extra")
 class TestReflectedRegionBackgroundEstimator:
-    def setup(self):
-        self.bg_maker = bkg_estimator()
+    def test_basic(self, bkg_estimator):
+        assert "ReflectedRegionsBackgroundEstimator" in str(bkg_estimator)
 
-    def test_basic(self):
-        assert "ReflectedRegionsBackgroundEstimator" in str(self.bg_maker)
-
-    def test_run(self):
-        self.bg_maker.finder.min_distance = Angle("0.2 deg")
-        self.bg_maker.run()
-        assert len(self.bg_maker.result[1].off_region) == 11
-        assert "Reflected" in str(self.bg_maker.result[1])
+    def test_run(self, bkg_estimator):
+        bkg_estimator.run()
+        assert len(bkg_estimator.result[1].off_region) == 11
+        assert "Reflected" in str(bkg_estimator.result[1])
 
     @requires_dependency("matplotlib")
-    def test_plot(self):
-        # The following line can be removed once we drop support for regions 0.2
-        # See https://github.com/gammapy/gammapy/issues/1758
-        pytest.importorskip("regions", minversion="0.3")
-        self.bg_maker.run()
+    def test_plot(self, bkg_estimator):
         with mpl_plot_check():
-            self.bg_maker.plot()
-            self.bg_maker.plot(idx=1)
-            self.bg_maker.plot(idx=[0, 1])
-            self.bg_maker.plot(add_legend=True)
+            bkg_estimator.plot(idx=1, add_legend=True)
+            bkg_estimator.plot(idx=[0, 1])
diff --git a/gammapy/catalog/tests/test_fermi.py b/gammapy/catalog/tests/test_fermi.py
--- a/gammapy/catalog/tests/test_fermi.py
+++ b/gammapy/catalog/tests/test_fermi.py
@@ -23,24 +23,28 @@
 SOURCES_3FGL = [
     dict(
         idx=0,
+        name="3FGL J0000.1+6545",
         spec_type=PowerLaw,
         dnde=u.Quantity(1.4351261e-9, "cm-2 s-1 GeV-1"),
         dnde_err=u.Quantity(2.1356270e-10, "cm-2 s-1 GeV-1"),
     ),
     dict(
         idx=4,
+        name="3FGL J0001.4+2120",
         spec_type=LogParabola,
         dnde=u.Quantity(8.3828599e-10, "cm-2 s-1 GeV-1"),
         dnde_err=u.Quantity(2.6713238e-10, "cm-2 s-1 GeV-1"),
     ),
     dict(
         idx=55,
+        name="3FGL J0023.4+0923",
         spec_type=ExponentialCutoffPowerLaw3FGL,
         dnde=u.Quantity(1.8666925e-09, "cm-2 s-1 GeV-1"),
         dnde_err=u.Quantity(2.2068837e-10, "cm-2 s-1 GeV-1"),
     ),
     dict(
         idx=960,
+        name="3FGL J0835.3-4510",
         spec_type=PLSuperExpCutoff3FGL,
         dnde=u.Quantity(1.6547128794756733e-06, "cm-2 s-1 GeV-1"),
         dnde_err=u.Quantity(1.6621504e-11, "cm-2 s-1 MeV-1"),
@@ -50,12 +54,14 @@
 SOURCES_3FHL = [
     dict(
         idx=352,
+        name="3FHL J0534.5+2201",
         spec_type=PowerLaw,
         dnde=u.Quantity(6.3848912826152664e-12, "cm-2 s-1 GeV-1"),
         dnde_err=u.Quantity(2.679593524691324e-13, "cm-2 s-1 GeV-1"),
     ),
     dict(
         idx=1442,
+        name="3FHL J2158.8-3013",
         spec_type=LogParabola,
         dnde=u.Quantity(2.056998292908196e-12, "cm-2 s-1 GeV-1"),
         dnde_err=u.Quantity(4.219030630302381e-13, "cm-2 s-1 GeV-1"),
diff --git a/gammapy/catalog/tests/test_gammacat.py b/gammapy/catalog/tests/test_gammacat.py
--- a/gammapy/catalog/tests/test_gammacat.py
+++ b/gammapy/catalog/tests/test_gammacat.py
@@ -79,12 +79,12 @@ def test_w28_alias_names(self, gammacat):
         for name in names:
             assert str(gammacat[name]) == str(gammacat["W28"])
 
-    def test_sort_table(self):
+    def test_sort_table(self, gammacat):
         name = "HESS J1848-018"
         sort_keys = ["ra", "dec", "reference_id"]
         for sort_key in sort_keys:
             # this test modifies the catalog, so we make a copy
-            cat = gammacat()
+            cat = gammacat.copy()
             cat.table.sort(sort_key)
             assert cat[name].name == name
 
diff --git a/gammapy/cube/tests/test_models.py b/gammapy/cube/tests/test_models.py
--- a/gammapy/cube/tests/test_models.py
+++ b/gammapy/cube/tests/test_models.py
@@ -82,10 +82,20 @@ def diffuse_evaluator(diffuse_model, exposure, background, psf, edisp):
     return MapEvaluator(diffuse_model, exposure, background, psf=psf, edisp=edisp)
 
 
+@pytest.fixture(scope="session")
+def sky_models(sky_model):
+    return SkyModels([sky_model, sky_model])
+
+
+@pytest.fixture(scope="session")
+def compound_model(sky_model):
+    return sky_model + sky_model
+
+
 def test_background_model(background):
     bkg1 = BackgroundModel(background, norm=2.0).evaluate()
     assert_allclose(bkg1[0][0][0], background.data[0][0][0] * 2.0, rtol=1e-3)
-    assert_allclose(bkg1.sum(), background.data.sum()*2.0, rtol=1e-3)
+    assert_allclose(bkg1.sum(), background.data.sum() * 2.0, rtol=1e-3)
 
     bkg2 = BackgroundModel(background, norm=2.0, tilt=0.2, reference="1000 GeV").evaluate()
     assert_allclose(bkg2[0][0][0], 2.254e-07, rtol=1e-3)
@@ -93,11 +103,8 @@ def test_background_model(background):
 
 
 class TestSkyModels:
-    def setup(self):
-        self.sky_models = SkyModels([sky_model(), sky_model()])
-
-    def test_to_compound_model(self):
-        sky_models = self.sky_models
+    @staticmethod
+    def test_to_compound_model(sky_models):
         model = sky_models.to_compound_model()
         assert isinstance(model, CompoundSkyModel)
         pars = model.parameters.parameters
@@ -105,8 +112,8 @@ def test_to_compound_model(self):
         assert pars[0].name == "lon_0"
         assert pars[-1].name == "reference"
 
-    def test_parameters(self):
-        sky_models = self.sky_models
+    @staticmethod
+    def test_parameters(sky_models):
         parnames = ["lon_0", "lat_0", "sigma", "index", "amplitude", "reference"] * 2
         assert sky_models.parameters.names == parnames
 
@@ -115,13 +122,8 @@ def test_parameters(self):
         p2 = sky_models.skymodels[0].parameters["lon_0"]
         assert p1 is p2
 
-        # Check that parameter assignment works
-        assert sky_models.parameters.parameters[-1].value == 1
-        sky_models.parameters = sky_models.parameters.copy()
-        assert sky_models.parameters.parameters[-1].value == 1
-
-    def test_evaluate(self):
-        sky_models = self.sky_models
+    @staticmethod
+    def test_evaluate(sky_models):
         lon = 3 * u.deg * np.ones(shape=(3, 4))
         lat = 4 * u.deg * np.ones(shape=(3, 4))
         energy = [1, 1, 1, 1, 1] * u.TeV
@@ -178,11 +180,6 @@ def test_evaluate_array(sky_model):
 
 
 class TestCompoundSkyModel:
-    @staticmethod
-    @pytest.fixture()
-    def compound_model(sky_model):
-        return sky_model + sky_model
-
     @staticmethod
     def test_parameters(compound_model):
         parnames = ["lon_0", "lat_0", "sigma", "index", "amplitude", "reference"] * 2
diff --git a/gammapy/scripts/tests/test_spectrum_pipe.py b/gammapy/scripts/tests/test_spectrum_pipe.py
--- a/gammapy/scripts/tests/test_spectrum_pipe.py
+++ b/gammapy/scripts/tests/test_spectrum_pipe.py
@@ -1,23 +1,39 @@
 # Licensed under a 3-clause BSD style license - see LICENSE.rst
 from __future__ import absolute_import, division, print_function, unicode_literals
+import pytest
 from numpy.testing import assert_allclose
 import astropy.units as u
+from astropy.coordinates import SkyCoord, Angle
+from regions import CircleSkyRegion
 from ...utils.energy import EnergyBounds
 from ...utils.testing import requires_dependency, requires_data
-from ...background.tests.test_reflected import observations, on_region
 from ...spectrum.models import PowerLaw
 from ..spectrum_pipe import SpectrumAnalysisIACT
+from ...data import DataStore
 
 
-def get_config():
+@pytest.fixture(scope="session")
+def observations():
+    """Example observation list for testing."""
+    datastore = DataStore.from_dir("$GAMMAPY_EXTRA/datasets/hess-dl3-dr1")
+    obs_ids = [23523, 23526]
+    return datastore.get_observations(obs_ids)
+
+
+@pytest.fixture(scope="session")
+def config():
     """Get test config, extend to several scenarios"""
     model = PowerLaw(
         index=2, amplitude=1e-11 * u.Unit("cm-2 s-1 TeV-1"), reference=1 * u.TeV
     )
+
+    pos = SkyCoord(83.63, 22.01, unit="deg", frame="icrs")
+    radius = Angle(0.11, "deg")
+    on_region = CircleSkyRegion(pos, radius)
     fp_binning = EnergyBounds.equal_log_spacing(1, 50, 4, "TeV")
     return dict(
         outdir=None,
-        background=dict(on_region=on_region()),
+        background=dict(on_region=on_region),
         extraction=dict(),
         fit=dict(model=model),
         fp_binning=fp_binning,
@@ -26,11 +42,10 @@ def get_config():
 
 @requires_data("gammapy-extra")
 @requires_dependency("sherpa")
-def test_spectrum_analysis_iact(tmpdir):
-    config = get_config()
+def test_spectrum_analysis_iact(tmpdir, config, observations):
     config["outdir"] = tmpdir
 
-    analysis = SpectrumAnalysisIACT(observations=observations(), config=config)
+    analysis = SpectrumAnalysisIACT(observations=observations, config=config)
     analysis.run()
     flux_points = analysis.flux_points
 
diff --git a/gammapy/spectrum/tests/test_extract.py b/gammapy/spectrum/tests/test_extract.py
--- a/gammapy/spectrum/tests/test_extract.py
+++ b/gammapy/spectrum/tests/test_extract.py
@@ -4,28 +4,62 @@
 import numpy as np
 from numpy.testing import assert_allclose
 import astropy.units as u
+from astropy.coordinates import SkyCoord, Angle
+from regions import CircleSkyRegion
 from ...utils.testing import assert_quantity_allclose
 from ...utils.testing import requires_dependency, requires_data
 from ...spectrum import SpectrumExtraction, SpectrumObservation
-from ...background.tests.test_reflected import bkg_estimator, observations
+from ...background import ReflectedRegionsBackgroundEstimator
+from ...maps import WcsGeom, WcsNDMap
+from ...data import DataStore
 
 
 @pytest.fixture(scope="session")
-def bkg_estimate():
+def exclusion_mask():
+    """Example mask for testing."""
+    pos = SkyCoord(83.63, 22.01, unit="deg", frame="icrs")
+    exclusion_region = CircleSkyRegion(pos, Angle(0.3, "deg"))
+    geom = WcsGeom.create(skydir=pos, binsz=0.02, width=10.0)
+    mask = geom.region_mask([exclusion_region], inside=False)
+    return WcsNDMap(geom, data=mask)
+
+
+@pytest.fixture(scope="session")
+def on_region():
+    """Example on_region for testing."""
+    pos = SkyCoord(83.63, 22.01, unit="deg", frame="icrs")
+    radius = Angle(0.11, "deg")
+    region = CircleSkyRegion(pos, radius)
+    return region
+
+
+@pytest.fixture
+def observations():
+    """Example observation list for testing."""
+    datastore = DataStore.from_dir("$GAMMAPY_EXTRA/datasets/hess-dl3-dr1")
+    obs_ids = [23523, 23526]
+    return datastore.get_observations(obs_ids)
+
+
+@pytest.fixture
+def bkg_estimate(observations, on_region, exclusion_mask):
     """An example background estimate"""
-    est = bkg_estimator()
+    est = ReflectedRegionsBackgroundEstimator(
+        observations=observations, on_region=on_region, exclusion_mask=exclusion_mask,
+        min_distance_input="0.2 deg"
+    )
     est.run()
     return est.result
 
 
-@pytest.fixture(scope="session")
-def extraction():
+@pytest.fixture
+def extraction(bkg_estimate, observations):
     """An example SpectrumExtraction for tests."""
     # Restrict true energy range covered by HAP exporter
     e_true = np.logspace(-1, 1.9, 70) * u.TeV
 
     return SpectrumExtraction(
-        bkg_estimate=bkg_estimate(), observations=observations(), e_true=e_true
+        bkg_estimate=bkg_estimate, observations=observations, e_true=e_true
     )
 
 
@@ -38,7 +72,7 @@ class TestSpectrumExtraction:
                 dict(containment_correction=False),
                 dict(
                     n_on=192,
-                    sigma=20.971149,
+                    sigma=20.941125,
                     aeff=580254.9 * u.m ** 2,
                     edisp=0.236176,
                     containment=1,
@@ -48,7 +82,7 @@ class TestSpectrumExtraction:
                 dict(containment_correction=True),
                 dict(
                     n_on=192,
-                    sigma=20.971149,
+                    sigma=20.941125,
                     aeff=373237.8 * u.m ** 2,
                     edisp=0.236176,
                     containment=0.661611,
@@ -80,7 +114,8 @@ def test_extract(self, pars, results, observations, bkg_estimate):
         assert_allclose(sigma_actual, results["sigma"], atol=1e-2)
         assert_allclose(containment_actual, results["containment"], rtol=1e-3)
 
-    def test_alpha(self, observations, bkg_estimate):
+    @staticmethod
+    def test_alpha(observations, bkg_estimate):
         bkg_estimate[0].a_off = 0
         bkg_estimate[1].a_off = 2
         extraction = SpectrumExtraction(
@@ -89,7 +124,8 @@ def test_alpha(self, observations, bkg_estimate):
         extraction.run()
         assert len(extraction.spectrum_observations) == 0
 
-    def test_run(self, tmpdir, extraction):
+    @staticmethod
+    def test_run(tmpdir, extraction):
         """Test the run method and check if files are written correctly"""
         extraction.run()
         extraction.write(outdir=tmpdir, overwrite=True)
@@ -124,4 +160,4 @@ def test_compute_energy_threshold(self, extraction):
         extraction.run()
         extraction.compute_energy_threshold(method_lo="area_max", area_percent_lo=10)
         actual = extraction.spectrum_observations[0].lo_threshold
-        assert_quantity_allclose(actual, 0.879923 * u.TeV, rtol=1e-3)
+        assert_quantity_allclose(actual, 0.8799225 * u.TeV, rtol=1e-3)
diff --git a/gammapy/time/tests/test_lightcurve.py b/gammapy/time/tests/test_lightcurve.py
--- a/gammapy/time/tests/test_lightcurve.py
+++ b/gammapy/time/tests/test_lightcurve.py
@@ -201,13 +201,12 @@ def spec_extraction():
 
 
 @requires_data("gammapy-extra")
-def test_lightcurve_estimator():
-    spec_extract = spec_extraction()
-    lc_estimator = LightCurveEstimator(spec_extract)
+def test_lightcurve_estimator(spec_extraction):
+    lc_estimator = LightCurveEstimator(spec_extraction)
 
     # param
     intervals = []
-    for obs in spec_extract.observations:
+    for obs in spec_extraction.observations:
         intervals.append([obs.events.time[0], obs.events.time[-1]])
 
     model = PowerLaw(
@@ -249,10 +248,8 @@ def test_lightcurve_estimator():
 
 
 @requires_data("gammapy-extra")
-def test_lightcurve_interval_maker():
-    spec_extract = spec_extraction()
-
-    table = LightCurveEstimator.make_time_intervals_fixes(500, spec_extract)
+def test_lightcurve_interval_maker(spec_extraction):
+    table = LightCurveEstimator.make_time_intervals_fixes(500, spec_extraction)
     intervals = list(zip(table["t_start"], table["t_stop"]))
 
     assert len(intervals) == 9
@@ -261,9 +258,8 @@ def test_lightcurve_interval_maker():
 
 
 @requires_data("gammapy-extra")
-def test_lightcurve_adaptative_interval_maker():
-    spec_extract = spec_extraction()
-    lc_estimator = LightCurveEstimator(spec_extract)
+def test_lightcurve_adaptative_interval_maker(spec_extraction):
+    lc_estimator = LightCurveEstimator(spec_extraction)
     separator = [
         Time((53343.94050200008 + 53343.952979345195) / 2, scale="tt", format="mjd")
     ]
@@ -271,7 +267,7 @@ def test_lightcurve_adaptative_interval_maker():
         significance=3,
         significance_method="lima",
         energy_range=[0.2, 100] * u.TeV,
-        spectrum_extraction=spec_extract,
+        spectrum_extraction=spec_extraction,
         separators=separator,
     )
     assert_allclose(table["significance"] >= 3, True)