Repository: PlasmaPy/PlasmaPy

Issue:
Flaky Failure in `test_floating_potential`
The `test_floating_potential` module occasionally emits a `JSONDecodeError`. See https://github.com/PlasmaPy/PlasmaPy/actions/runs/9352991385/job/25742427527#step:6:129

It is unclear which of the tests this error is originating from.. usually the console mentions this information?


Discussion:

Resolving patch:
diff --git a/src/plasmapy/particles/atomic.py b/src/plasmapy/particles/atomic.py
--- a/src/plasmapy/particles/atomic.py
+++ b/src/plasmapy/particles/atomic.py
@@ -46,8 +46,6 @@
 
 __all__.sort()
 
-_DEFAULT_DOWNLOADER = Downloader()
-
 
 @particle_input
 def atomic_number(element: ParticleLike) -> int:
@@ -1182,7 +1180,8 @@ def stopping_power(
 
     """
 
-    nist_data_path = _DEFAULT_DOWNLOADER.get_file("NIST_STAR.hdf5")
+    # TODO: figure out a better way of handling the Downloader() here
+    nist_data_path = Downloader().get_file("NIST_STAR.hdf5")
 
     # Validate particle input. Currently, the only supported particles are protons and electrons.
     with h5py.File(nist_data_path, "r") as nist_data:
diff --git a/src/plasmapy/utils/data/__init__.py b/src/plasmapy/utils/data/__init__.py
--- a/src/plasmapy/utils/data/__init__.py
+++ b/src/plasmapy/utils/data/__init__.py
@@ -3,6 +3,6 @@
 downloading and retrieving data files.
 """
 
-__all__ = ["Downloader"]
+__all__ = ["_API_CONNECTION_ESTABLISHED", "Downloader"]
 
-from plasmapy.utils.data.downloader import Downloader
+from plasmapy.utils.data.downloader import _API_CONNECTION_ESTABLISHED, Downloader
diff --git a/src/plasmapy/utils/data/downloader.py b/src/plasmapy/utils/data/downloader.py
--- a/src/plasmapy/utils/data/downloader.py
+++ b/src/plasmapy/utils/data/downloader.py
@@ -13,14 +13,27 @@
 
 import requests
 
-__all__ = ["Downloader"]
+__all__ = ["_API_CONNECTION_ESTABLISHED", "Downloader"]
 
+_API_CONNECTION_ESTABLISHED = False
 _IS_CI = "GH_TOKEN" in os.environ
 
 
 # TODO: use a config file variable to allow users to set a location
 # for the data download folder?
 
+try:
+    response = requests.get("https://api.github.com/", timeout=10)
+
+    _API_CONNECTION_ESTABLISHED = response.status_code == 200
+except (
+    requests.exceptions.ConnectionError,
+    requests.exceptions.ReadTimeout,
+) as e:  # coverage: ignore
+    # TODO: logging library when??
+    print(f"Failed to connect to GitHub API:\n{e}")  # noqa: T201
+    _API_CONNECTION_ESTABLISHED = False
+
 
 class Downloader:
     """


Test patch:
diff --git a/tests/diagnostics/charged_particle_radiography/test_detector_stacks.py b/tests/diagnostics/charged_particle_radiography/test_detector_stacks.py
--- a/tests/diagnostics/charged_particle_radiography/test_detector_stacks.py
+++ b/tests/diagnostics/charged_particle_radiography/test_detector_stacks.py
@@ -14,7 +14,11 @@
     Layer,
     Stack,
 )
-from plasmapy.utils.data.downloader import Downloader
+from plasmapy.utils.data.downloader import _API_CONNECTION_ESTABLISHED, Downloader
+
+check_database_connection = pytest.mark.skipif(
+    not _API_CONNECTION_ESTABLISHED, reason="failed to connect to data repository"
+)
 
 
 @pytest.fixture()
@@ -62,6 +66,7 @@ def hdv2_stack(downloader):
     return Stack(layers)
 
 
+@check_database_connection
 def test_create_layer_with_different_stopping_powers(downloader) -> None:
     """
     Tests the input validation for creating a Layer with either the linear
@@ -94,21 +99,25 @@ def test_create_layer_with_different_stopping_powers(downloader) -> None:
         Layer(100 * u.um, eaxis, mass_stopping_power, mass_density=None)
 
 
+@check_database_connection
 def test_film_stack_num_layers(hdv2_stack) -> None:
     # Test num_layers property
     assert hdv2_stack.num_layers == 21
 
 
+@check_database_connection
 def test_film_stack_num_active(hdv2_stack) -> None:
     # Test num_active property
     assert hdv2_stack.num_active == 10
 
 
+@check_database_connection
 def test_film_stack_thickness(hdv2_stack) -> None:
     # Test thickness property
     assert np.isclose(hdv2_stack.thickness.to(u.mm).value, 1.19)
 
 
+@check_database_connection
 def test_film_stack_deposition_curves(hdv2_stack) -> None:
     energies = np.arange(1, 60, 1) * u.MeV
     deposition_curves = hdv2_stack.deposition_curves(energies, return_only_active=False)
@@ -119,6 +128,7 @@ def test_film_stack_deposition_curves(hdv2_stack) -> None:
     ), "The integral over all layers for each particle species is not unity."
 
 
+@check_database_connection
 def test_film_stack_energy_bands_active(hdv2_stack) -> None:
     # Test energy bands
     ebands = hdv2_stack.energy_bands([0.1, 60] * u.MeV, 0.1 * u.MeV, dx=1 * u.um)
@@ -129,6 +139,7 @@ def test_film_stack_energy_bands_active(hdv2_stack) -> None:
     assert np.allclose(ebands.to(u.MeV).value[:5, :], expected, atol=0.15)
 
 
+@check_database_connection
 def test_film_stack_energy_bands_inum_active(hdv2_stack) -> None:
     # Test including inum_active layers
     ebands = hdv2_stack.energy_bands(
diff --git a/tests/utils/data/test_downloader.py b/tests/utils/data/test_downloader.py
--- a/tests/utils/data/test_downloader.py
+++ b/tests/utils/data/test_downloader.py
@@ -5,7 +5,11 @@
 import numpy as np
 import pytest
 
-from plasmapy.utils.data.downloader import Downloader
+from plasmapy.utils.data.downloader import _API_CONNECTION_ESTABLISHED, Downloader
+
+check_database_connection = pytest.mark.skipif(
+    not _API_CONNECTION_ESTABLISHED, reason="failed to connect to data repository"
+)
 
 
 def in_ci() -> bool:
@@ -17,6 +21,7 @@ def in_ci() -> bool:
 
 
 @pytest.fixture(scope="module")
+@check_database_connection
 def downloader_validated(tmpdir_factory) -> Downloader:
     api_token = os.environ["GH_TOKEN"] if in_ci() else None
 
@@ -31,6 +36,7 @@ def downloader_validated(tmpdir_factory) -> Downloader:
     return Downloader(directory=path, api_token=api_token)
 
 
+@check_database_connection
 @pytest.mark.skipif(
     not in_ci(), reason="Tests only use authenticated API calls when run in CI."
 )
@@ -44,6 +50,7 @@ def test_api_token(downloader_validated: Downloader) -> None:
 
 
 @pytest.fixture(scope="module")
+@check_database_connection
 def downloader_unvalidated(tmpdir_factory) -> Downloader:
     path = tmpdir_factory.mktemp("unvalidated")
 
@@ -58,6 +65,7 @@ def downloader_unvalidated(tmpdir_factory) -> Downloader:
 ]
 
 
+@check_database_connection
 @pytest.mark.parametrize(("url", "expected"), test_urls)
 def test_http_request(
     downloader_validated: Downloader, url: str, expected: None | Exception
@@ -72,6 +80,7 @@ def test_http_request(
             downloader_validated._http_request(url)
 
 
+@check_database_connection
 def test_blob_file(downloader_validated: Downloader) -> None:
     """
     Test the read and write blob file routines
@@ -90,6 +99,7 @@ def test_blob_file(downloader_validated: Downloader) -> None:
     assert downloader_validated._blob_dict["test_key"] == test_str
 
 
+@check_database_connection
 def test_update_blob_entry(downloader_validated):
     """
     Test the logic in the _update_blob_entry function
@@ -127,6 +137,7 @@ def test_update_blob_entry(downloader_validated):
     "downloader", ["downloader_validated", "downloader_unvalidated"]
 )
 @pytest.mark.parametrize(("filename", "expected"), test_files)
+@check_database_connection
 def test_get_file(
     filename: str, expected: Exception | None, downloader: Downloader, request
 ) -> None:
@@ -154,6 +165,7 @@ def test_get_file(
 @pytest.mark.parametrize(
     "downloader", ["downloader_validated", "downloader_unvalidated"]
 )
+@check_database_connection
 def test_get_local_only_file(downloader: Downloader, request):
     """
     Test various file retrieval modes
@@ -190,6 +202,7 @@ def test_get_local_only_file(downloader: Downloader, request):
         dl.get_file("not_anywhere.txt")
 
 
+@check_database_connection
 def test_get_file_NIST_PSTAR_datafile(downloader_validated) -> None:
     """Test getting a particular file and checking for known contents"""
 
@@ -204,6 +217,7 @@ def test_get_file_NIST_PSTAR_datafile(downloader_validated) -> None:
 
 
 @pytest.mark.flaky(reruns=2)
+@check_database_connection
 def test_at_most_one_api_call(downloader_validated) -> None:
     """
     Test that at most one API call is made over multiple queries
@@ -223,6 +237,7 @@ def test_at_most_one_api_call(downloader_validated) -> None:
     assert used1 <= used0 + 1
 
 
+@check_database_connection
 def test_creating_another_downloader(downloader_validated) -> None:
     """
     Test creating a second downloader in the same directory.
@@ -237,6 +252,7 @@ def test_creating_another_downloader(downloader_validated) -> None:
     assert dl2.get_file(filename) == filepath
 
 
+@check_database_connection
 def test_ensure_update_blob_dict_runs(downloader_validated: Downloader) -> None:
     """
     Ensure the _update_blob_dict method gets run if it hasn't already.