Repository: LSSTDESC/healsparse

Issue:
healsparse won't read a `pathlib.Path`
When trying to read from a `pathlib.Path` object, healsparse will throw an error about only supporting fits and parquet files; e.g.,
```
(Pdb) hsp_plus
PosixPath('/pscratch/sd/s/smau/y6-image-sims/debug-cosmos-rand-piff-median_color-seed/DES0608-5414/16200/plus/des-pizza-slices-y6/DES0608-5414/metadetect/DES0608-5414_metadetect-config_mdetcat_part0000-healsparse-mask.hs')
(Pdb) healsparse.HealSparseMap.read(hsp_plus)
*** NotImplementedError: HealSparse only supports fits and parquet files.
(Pdb) healsparse.HealSparseMap.read(hsp_plus.as_posix())
HealSparseMap: nside_coverage = 128, nside_sparse = 131072, int32, 2551130 valid pixels
```
the error is also slightly confusing as the file format is not a problem.

This is happening because a `Path` doesn't match as a `str` (https://github.com/LSSTDESC/healsparse/blob/833509256873ae92db52af8e15e17ff9d9559a01/healsparse/io_coverage.py#L33-L46)

One possible solution would be checking 
```
isinstance(filename_or_fits, (str, Path))
```
instead of 
```
isinstance(filename_or_fits, str)
```

I haven't checked if any of the code downstream would complain about a `Path`...

Discussion:

Patch:
diff --git a/healsparse/io_coverage.py b/healsparse/io_coverage.py
index 2129ef8..8d492b4 100644
--- a/healsparse/io_coverage.py
+++ b/healsparse/io_coverage.py
@@ -1,4 +1,5 @@
 import os
+import pathlib
 
 from .fits_shim import HealSparseFits
 from .io_coverage_fits import _read_coverage_fits
@@ -30,6 +31,9 @@ def _read_coverage(coverage_class, filename_or_fits, use_threads=False):
     is_fits = False
     is_parquet_file = False
 
+    if isinstance(filename_or_fits, pathlib.PurePath):
+        filename_or_fits = os.fspath(filename_or_fits)
+
     if isinstance(filename_or_fits, str):
         try:
             fits = HealSparseFits(filename_or_fits)


Test patch:
diff --git a/tests/test_healSparseCoverage.py b/tests/test_healSparseCoverage.py
index a7bd6cf..2d8aca6 100644
--- a/tests/test_healSparseCoverage.py
+++ b/tests/test_healSparseCoverage.py
@@ -6,6 +6,7 @@ import tempfile
 import shutil
 import os
 import pytest
+import pathlib
 
 import healsparse
 
@@ -40,15 +41,16 @@ class HealSparseCoverageTestCase(unittest.TestCase):
         ipnest = np.unique(hpg.angle_to_pixel(nside_coverage, ra, dec))
         cov_mask_test[ipnest] = True
 
-        cov_map = healsparse.HealSparseCoverage.read(fname)
+        for readfile in (fname, pathlib.Path(fname)):
+            cov_map = healsparse.HealSparseCoverage.read(readfile)
 
-        # Ensure that the coverage mask is what we think it should be
-        testing.assert_array_equal(cov_map.coverage_mask, cov_mask_test)
+            # Ensure that the coverage mask is what we think it should be
+            testing.assert_array_equal(cov_map.coverage_mask, cov_mask_test)
 
-        # Ensure that we can address the cov_map by index
-        testing.assert_array_equal(cov_map[:], cov_map._cov_index_map)
-        testing.assert_array_equal(cov_map[0: 100], cov_map._cov_index_map[0: 100])
-        testing.assert_array_equal([cov_map[0]], [cov_map._cov_index_map[0]])
+            # Ensure that we can address the cov_map by index
+            testing.assert_array_equal(cov_map[:], cov_map._cov_index_map)
+            testing.assert_array_equal(cov_map[0: 100], cov_map._cov_index_map[0: 100])
+            testing.assert_array_equal([cov_map[0]], [cov_map._cov_index_map[0]])
 
         if not has_healpy:
             return
@@ -84,15 +86,16 @@ class HealSparseCoverageTestCase(unittest.TestCase):
         ipnest = np.unique(hpg.angle_to_pixel(nside_coverage, ra, dec))
         cov_mask_test[ipnest] = True
 
-        cov_map = healsparse.HealSparseCoverage.read(fname)
+        for readfile in (fname, pathlib.Path(fname)):
+            cov_map = healsparse.HealSparseCoverage.read(fname)
 
-        # Ensure that the coverage mask is what we think it should be
-        testing.assert_array_equal(cov_map.coverage_mask, cov_mask_test)
+            # Ensure that the coverage mask is what we think it should be
+            testing.assert_array_equal(cov_map.coverage_mask, cov_mask_test)
 
-        # Ensure that we can address the cov_map by index
-        testing.assert_array_equal(cov_map[:], cov_map._cov_index_map)
-        testing.assert_array_equal(cov_map[0: 100], cov_map._cov_index_map[0: 100])
-        testing.assert_array_equal([cov_map[0]], [cov_map._cov_index_map[0]])
+            # Ensure that we can address the cov_map by index
+            testing.assert_array_equal(cov_map[:], cov_map._cov_index_map)
+            testing.assert_array_equal(cov_map[0: 100], cov_map._cov_index_map[0: 100])
+            testing.assert_array_equal([cov_map[0]], [cov_map._cov_index_map[0]])
 
     def test_read_non_fits(self):
         """Test reading coverage from a file that isn't fits or parquet."""
@@ -103,6 +106,7 @@ class HealSparseCoverageTestCase(unittest.TestCase):
             f.write('some text.')
 
         self.assertRaises(NotImplementedError, healsparse.HealSparseCoverage.read, fname)
+        self.assertRaises(NotImplementedError, healsparse.HealSparseCoverage.read, pathlib.Path(fname))
 
     def test_read_missing_file(self):
         """Test reading coverage from a file that isn't there."""
@@ -111,6 +115,7 @@ class HealSparseCoverageTestCase(unittest.TestCase):
         fname = os.path.join(self.test_dir, 'NOT_A_FILE')
 
         self.assertRaises(IOError, healsparse.HealSparseCoverage.read, fname)
+        self.assertRaises(IOError, healsparse.HealSparseCoverage.read, pathlib.Path(fname))
 
     def setUp(self):
         self.test_dir = None
diff --git a/tests/test_io.py b/tests/test_io.py
index 4909c6c..7b82eef 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -6,6 +6,7 @@ from numpy import random
 import tempfile
 import shutil
 import os
+import pathlib
 
 import healsparse
 
@@ -39,46 +40,48 @@ class HealsparseFitsIoTestCase(unittest.TestCase):
 
         sparse_map[30000: 30005] = np.zeros(5, dtype=np.float64)
 
-        # Write it to healsparse format
-        sparse_map.write(os.path.join(self.test_dir, 'healsparse_map.hs'))
+        for mode in ("str", "path"):
+            if mode == "str":
+                readfile = os.path.join(self.test_dir, "healsparse_map.hsp")
+            else:
+                readfile = self.test_dir / pathlib.Path("healsparse_map2.hsp")
 
-        # Read in healsparse format (full map)
-        sparse_map = healsparse.HealSparseMap.read(os.path.join(self.test_dir, 'healsparse_map.hs'))
+            # Write it to healsparse format
+            sparse_map.write(readfile)
 
-        # Check that we can do a basic lookup
-        testing.assert_almost_equal(sparse_map.get_values_pix(ipnest), test_values)
+            # Read in healsparse format (full map)
+            sparse_map = healsparse.HealSparseMap.read(readfile)
 
-        # Check that we can do a basic set
-        sparse_map[30000: 30005] = np.zeros(5, dtype=np.float64)
+            # Check that we can do a basic lookup
+            testing.assert_almost_equal(sparse_map.get_values_pix(ipnest), test_values)
 
-        # Try to read in healsparse format, non-unique pixels
-        self.assertRaises(RuntimeError, healsparse.HealSparseMap.read,
-                          os.path.join(self.test_dir, 'healsparse_map.hs'), pixels=[0, 0])
+            # Check that we can do a basic set
+            sparse_map[30000: 30005] = np.zeros(5, dtype=np.float64)
 
-        # Read in healsparse format (two pixels)
-        sparse_map_small = healsparse.HealSparseMap.read(os.path.join(self.test_dir,
-                                                                      'healsparse_map.hs'),
-                                                         pixels=[0, 1])
+            # Try to read in healsparse format, non-unique pixels
+            self.assertRaises(RuntimeError, healsparse.HealSparseMap.read, readfile, pixels=[0, 0])
 
-        # Test the coverage map only has two pixels
-        cov_mask = sparse_map_small.coverage_mask
-        self.assertEqual(cov_mask.sum(), 2)
+            # Read in healsparse format (two pixels)
+            sparse_map_small = healsparse.HealSparseMap.read(readfile, pixels=[0, 1])
 
-        # Test lookup of values in those two pixels
-        ipnestCov = np.right_shift(ipnest, sparse_map_small._cov_map.bit_shift)
-        outside_small, = np.where(ipnestCov > 1)
-        test_values2 = test_values.copy()
-        test_values2[outside_small] = hpg.UNSEEN
+            # Test the coverage map only has two pixels
+            cov_mask = sparse_map_small.coverage_mask
+            self.assertEqual(cov_mask.sum(), 2)
 
-        testing.assert_almost_equal(sparse_map_small.get_values_pix(ipnest), test_values2)
+            # Test lookup of values in those two pixels
+            ipnestCov = np.right_shift(ipnest, sparse_map_small._cov_map.bit_shift)
+            outside_small, = np.where(ipnestCov > 1)
+            test_values2 = test_values.copy()
+            test_values2[outside_small] = hpg.UNSEEN
 
-        # Read in healsparse format (all pixels)
-        sparse_map_full = healsparse.HealSparseMap.read(
-            os.path.join(self.test_dir,
-                         'healsparse_map.hs'),
-            pixels=np.arange(hpg.nside_to_npixel(nside_coverage))
-        )
-        testing.assert_almost_equal(sparse_map_full.get_values_pix(ipnest), test_values)
+            testing.assert_almost_equal(sparse_map_small.get_values_pix(ipnest), test_values2)
+
+            # Read in healsparse format (all pixels)
+            sparse_map_full = healsparse.HealSparseMap.read(
+                readfile,
+                pixels=np.arange(hpg.nside_to_npixel(nside_coverage)),
+            )
+            testing.assert_almost_equal(sparse_map_full.get_values_pix(ipnest), test_values)
 
     def test_fits_read_outoforder(self):
         """
diff --git a/tests/test_io_parquet.py b/tests/test_io_parquet.py
index 100ab70..8fbadb5 100644
--- a/tests/test_io_parquet.py
+++ b/tests/test_io_parquet.py
@@ -7,6 +7,7 @@ import tempfile
 import shutil
 import os
 import pytest
+import pathlib
 
 import healsparse
 
@@ -45,41 +46,45 @@ class ParquetIoTestCase(unittest.TestCase):
         u, = np.where(full_map > hpg.UNSEEN)
         sparse_map[u] = full_map[u]
 
-        fname = os.path.join(self.test_dir, 'healsparse_map.hsparquet')
-
-        # Write it in healsparse parquet format
-        sparse_map.write(fname, format='parquet')
-
-        # Read in healsparse format (full map)
-        sparse_map = healsparse.HealSparseMap.read(fname)
-        # Check that we can do a basic lookup
-        testing.assert_almost_equal(sparse_map.get_values_pix(ipnest), test_values)
-
-        # Try to read in healsparse format, non-unique pixels
-        self.assertRaises(RuntimeError, healsparse.HealSparseMap.read,
-                          fname, pixels=[0, 0])
-
-        # Read in healsparse (two pixels)
-        sparse_map_small = healsparse.HealSparseMap.read(fname, pixels=[0, 1])
-
-        # Test the coverage map only has two pixels
-        cov_mask = sparse_map_small.coverage_mask
-        self.assertEqual(cov_mask.sum(), 2)
-
-        # Test lookup of values in those two pixels
-        ipnestCov = np.right_shift(ipnest, sparse_map_small._cov_map.bit_shift)
-        outside_small, = np.where(ipnestCov > 1)
-        test_values2 = test_values.copy()
-        test_values2[outside_small] = hpg.UNSEEN
-
-        testing.assert_almost_equal(sparse_map_small.get_values_pix(ipnest), test_values2)
-
-        # Read in healsparse format (all pixels)
-        sparse_map_full = healsparse.HealSparseMap.read(
-            fname,
-            pixels=np.arange(hpg.nside_to_npixel(nside_coverage))
-        )
-        testing.assert_almost_equal(sparse_map_full.get_values_pix(ipnest), test_values)
+        for mode in ("str", "path"):
+            if mode == "str":
+                readfile = os.path.join(self.test_dir, "healsparse_map.hsparquet")
+            else:
+                readfile = self.test_dir / pathlib.Path("healsparse_map2.hsparquet")
+
+                # Write it in healsparse parquet format
+                sparse_map.write(readfile, format="parquet")
+
+                # Read in healsparse format (full map)
+                sparse_map = healsparse.HealSparseMap.read(readfile)
+                # Check that we can do a basic lookup
+                testing.assert_almost_equal(sparse_map.get_values_pix(ipnest), test_values)
+
+                # Try to read in healsparse format, non-unique pixels
+                self.assertRaises(RuntimeError, healsparse.HealSparseMap.read,
+                                  readfile, pixels=[0, 0])
+
+                # Read in healsparse (two pixels)
+                sparse_map_small = healsparse.HealSparseMap.read(readfile, pixels=[0, 1])
+
+                # Test the coverage map only has two pixels
+                cov_mask = sparse_map_small.coverage_mask
+                self.assertEqual(cov_mask.sum(), 2)
+
+                # Test lookup of values in those two pixels
+                ipnestCov = np.right_shift(ipnest, sparse_map_small._cov_map.bit_shift)
+                outside_small, = np.where(ipnestCov > 1)
+                test_values2 = test_values.copy()
+                test_values2[outside_small] = hpg.UNSEEN
+
+                testing.assert_almost_equal(sparse_map_small.get_values_pix(ipnest), test_values2)
+
+                # Read in healsparse format (all pixels)
+                sparse_map_full = healsparse.HealSparseMap.read(
+                    readfile,
+                    pixels=np.arange(hpg.nside_to_npixel(nside_coverage)),
+                )
+                testing.assert_almost_equal(sparse_map_full.get_values_pix(ipnest), test_values)
 
     def test_parquet_read_outoforder(self):
         """