From fdc9ed0d0e67945c476d6684af00bea7f21796a9 Mon Sep 17 00:00:00 2001
From: jlaura <jlaura@usgs.gov>
Date: Wed, 22 Aug 2018 20:48:58 -0700
Subject: [PATCH] Adds tests for GDAL not installed (#71)

* Adds tests for GDAL not installed

* typo in travis else

* proper matrix build

* semicolons...I write python

* updates test with conditional osr

* Proper pytest skipping enabled for conditional imports

* missed pytest import

* Typos

* Mixing pytest and unittest

* typos and imports

* typos city

* removes hasosr

* fixes wrapper for conditional gdal

* Removes test for non-available gdal
---
 .travis.yml                                |  14 ++-
 plio/camera/__init__.py                    |  20 ++++
 plio/camera/csm.py                         |  13 +--
 plio/camera/tests/test_csm.py              |  10 +-
 plio/io/__init__.py                        |  23 ++++
 plio/io/extract_metadata.py                |  21 ++--
 plio/io/io_gdal.py                         |  25 ++---
 plio/io/io_spectral_profiler.py            |   3 +-
 plio/io/tests/test_io_gdal.py              |  15 +--
 plio/io/tests/test_io_spectral_profiler.py |   6 +-
 plio/io/tests/test_metadata.py             | 121 +++++++++++----------
 11 files changed, 155 insertions(+), 116 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index f4d6990..618e0fa 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,8 +10,10 @@ os:
   - osx
 
 env:
-  - PYTHON_VERSION=3.5
-  - PYTHON_VERSION=3.6
+  - PYTHON_VERSION=3.5 HAS_GDAL=true
+  - PYTHON_VERSION=3.6 HAS_GDAL=true
+  - PYTHON_VERSION=3.5 HAS_GDAL=false
+  - PYTHON_VERSION=3.6 HAS_GDAL=false
 
 before_install:
   # We do this conditionally because it saves us some downloading if the
@@ -36,8 +38,12 @@ before_install:
 
 install:
   - conda config --add channels usgs-astrogeology
-  - conda config --add channels conda-forge 
-  - conda install libgdal gdal numpy pyproj h5py pvl scipy protobuf affine jinja2 networkx pandas sqlalchemy pyyaml ncurses usgscam pytest pytest-cov sh coveralls nbsphinx
+  - conda config --add channels conda-forge
+  - if $HAS_GDAL; then
+      conda install libgdal gdal numpy pyproj h5py pvl scipy protobuf affine jinja2 networkx pandas sqlalchemy pyyaml ncurses pytest pytest-cov sh coveralls nbsphinx;
+    else
+      conda install numpy pyproj h5py pvl scipy protobuf affine jinja2 networkx pandas sqlalchemy pyyaml ncurses pytest pytest-cov sh coveralls nbsphinx;
+    fi
 
 script:
   - pytest --cov=plio
diff --git a/plio/camera/__init__.py b/plio/camera/__init__.py
index e69de29..3f4336c 100644
--- a/plio/camera/__init__.py
+++ b/plio/camera/__init__.py
@@ -0,0 +1,20 @@
+import importlib
+import warnings
+
+cam = importlib.find_loader('usgscam')
+cycsm_isd = importlib.find_loader('cycsm.isd')
+
+if cam:
+    cam = cam.load_module()
+
+if cycsm_isd:
+    cycsm_isd = cycsm_isd.load_module()
+
+def conditional_cameras(func):
+    def cam_check(*args, **kwargs):
+        if cam:
+            return func(*args, **kwargs)
+        else:
+            warning.warn('Trying to call a camera method, but usgscam is not installed.')
+            return None
+        return cam_check
\ No newline at end of file
diff --git a/plio/camera/csm.py b/plio/camera/csm.py
index b555efc..6e20e79 100644
--- a/plio/camera/csm.py
+++ b/plio/camera/csm.py
@@ -1,17 +1,11 @@
 import datetime
 import json
 
-try:
-    import usgscam as cam
-    from cycsm.isd import Isd
-    camera_support = True
-except:
-    camera_support = False
 import requests
 
 from plio.utils.utils import find_in_dict
 from plio.io.io_json import NumpyEncoder
-
+from plio.camera import conditional_cameras, cam, cycsm_isd
 
 def data_from_cube(header):
     data = {}
@@ -28,16 +22,15 @@ def data_from_cube(header):
     data['SPACECRAFT_CLOCK_START_COUNT'] = find_in_dict(header, 'SpacecraftClockCount')
     return data
 
+@conditional_cameras
 def create_camera(obj, url='http://smalls:8002/api/1.0/missions/mars_reconnaissance_orbiter/csm_isd'):
-    if not camera_support:
-        print("Usgscam library not installed. Camera capabilities are disabled")
     
     data = json.dumps(data_from_cube(obj.metadata), cls=NumpyEncoder)
     r = requests.post(url, data=data)
 
     # Get the ISD back and instantiate a local ISD for the image
     isd = r.json()['data']['isd']
-    i = Isd.loads(isd)
+    i = cycsm_isd.Isd.loads(isd)
 
     # Create the plugin and camera as usual
     plugin = cam.genericls.Plugin()
diff --git a/plio/camera/tests/test_csm.py b/plio/camera/tests/test_csm.py
index ce6cf12..7f2cd90 100644
--- a/plio/camera/tests/test_csm.py
+++ b/plio/camera/tests/test_csm.py
@@ -5,9 +5,7 @@ from unittest import mock
 import pytest
 import pvl
 
-import usgscam
-
-from plio.camera import csm
+from plio.camera import csm, cam, conditional_cameras
 from plio.examples import get_path
 
 def mock_requests_post(*args, **kwargs):
@@ -35,11 +33,13 @@ def header():
 def req_obj():
     return 
 
+@pytest.mark.skipif(cam is None, reason="Cameras not installed")
 def test_data_from_cube(header):
     data = csm.data_from_cube(header)
     assert data['START_TIME'] == datetime.datetime(2008, 9, 17, 5, 8, 10, 820000)
 
+@pytest.mark.skipif(cam is None, reason="Cameras not installed")
 @mock.patch('requests.post', side_effect=mock_requests_post)
 def test_create_camera(header):
-    cam = csm.create_camera(header)
-    assert isinstance(cam, usgscam.genericls.SensorModel)
+    created_camera = csm.create_camera(header)
+    assert isinstance(create_camera, cam.genericls.SensorModel)
diff --git a/plio/io/__init__.py b/plio/io/__init__.py
index f040a7c..ce170f1 100644
--- a/plio/io/__init__.py
+++ b/plio/io/__init__.py
@@ -1,3 +1,26 @@
+# Conditional imports for GDAL
+import importlib
+import warnings
+
+gdal = importlib.find_loader('gdal')
+ogr = importlib.find_loader('osgeo.ogr')
+osr = importlib.find_loader('osr')
+
+if gdal:
+    gdal = gdal.load_module()
+    ogr = ogr.load_module()
+    osr = osr.load_module()
+    gdal.UseExceptions() 
+
+def conditional_gdal(func):
+    def has_gdal(*args, **kwargs):
+        if gdal:
+            return func(*args, **kwargs)
+        else:
+            warning.warn('Trying to call a GDAL method, but GDAL is not installed.')
+        return None
+    return has_gdal
+
 from . import io_autocnetgraph
 from . import io_controlnetwork
 from . import io_db
diff --git a/plio/io/extract_metadata.py b/plio/io/extract_metadata.py
index 547b578..6d94c2a 100644
--- a/plio/io/extract_metadata.py
+++ b/plio/io/extract_metadata.py
@@ -1,14 +1,10 @@
-try:
-    from osgeo import osr
-    hasosr = True
-except:
-    hasosr = False
-    
+from plio.io import osr, conditional_gdal
     
 import_options = ['ImportFromWkt', 'ImportFromProj4',
                   'ImportFromEPSG', 'ImportFromUSGS',
                   'ImportFromXML']
 
+@conditional_gdal
 def extract_projstring(proj_string):
     """
     Import an OSR supported projection string into
@@ -25,10 +21,7 @@ def extract_projstring(proj_string):
           OSR spatial reference object
 
     """
-    if hasosr:
-        srs = osr.SpatialReference()
-    else:
-        return
+    srs = osr.SpatialReference()
     for import_option in import_options:
         try:
             func = getattr(srs, import_option)
@@ -42,6 +35,7 @@ def extract_projstring(proj_string):
     srs.MorphFromESRI()
     return srs
 
+@conditional_gdal
 def get_standard_parallels(srs):
     """
     Get all standard parallels for a given map projection
@@ -62,6 +56,7 @@ def get_standard_parallels(srs):
         parallels[i] = srs.GetProjParm('Standard_Parallel_{}'.format(i+1), 0.0)
     return parallels
 
+@conditional_gdal
 def get_central_meridian(srs):
     """
     Get the central meridian of the projection
@@ -79,6 +74,7 @@ def get_central_meridian(srs):
 
     return srs.GetProjParm('central_meridian', 0.0)
 
+@conditional_gdal
 def get_spheroid(srs):
     """
     Get the semi-major, semi-minor, and inverse flattening
@@ -100,6 +96,7 @@ def get_spheroid(srs):
     invflattening = srs.GetInvFlattening()
     return semimajor, semiminor, invflattening
 
+@conditional_gdal
 def get_projection_name(srs):
     """
     Extract the projection name from a
@@ -118,6 +115,7 @@ def get_projection_name(srs):
     proj_name = srs.GetAttrValue("PROJECTION", 0)
     return proj_name
 
+@conditional_gdal
 def get_false_easting(srs):
     """
     Extract the false easting parameter from a
@@ -136,6 +134,7 @@ def get_false_easting(srs):
 
     return srs.GetProjParm('False_Easting', 0)
 
+@conditional_gdal
 def get_false_northing(srs):
     """
     Extract the false northing parameter from a
@@ -154,6 +153,7 @@ def get_false_northing(srs):
 
     return srs.GetProjParm('False_Northing', 0)
 
+@conditional_gdal
 def get_scale_factor(srs):
     """
     Extract the scale factor, k, from a spatial reference system (if present)
@@ -171,6 +171,7 @@ def get_scale_factor(srs):
 
     return srs.GetProjParm('scale_factor', 1.0)
 
+@conditional_gdal
 def get_latitude_of_origin(srs):
     """
     Extract the latitude of origin from
diff --git a/plio/io/io_gdal.py b/plio/io/io_gdal.py
index 249511d..a890015 100644
--- a/plio/io/io_gdal.py
+++ b/plio/io/io_gdal.py
@@ -6,20 +6,12 @@ import warnings
 import affine
 import numpy as np
 import pvl
-try:
-    # Try the full GDAL stack
-    import gdal
-    from osgeo import ogr
-    import osr
-    gdal.UseExceptions()
-    has_gdal = True
-except:
-    has_gdal = False
-
-from plio.io import extract_metadata
+
+
+from plio.io import extract_metadata, conditional_gdal
 from plio.geofuncs import geofuncs
 from plio.utils.utils import find_in_dict
-
+from plio.io import gdal, ogr, osr
 
 NP2GDAL_CONVERSION = {
   "uint8": 1,
@@ -158,7 +150,7 @@ class GeoDataset(object):
 
         """
         self.file_name = file_name
-        if not has_gdal:
+        if not gdal:
             raise ImportError('No module name gdal.')
         self.dataset = gdal.Open(file_name)
         if self.dataset is None:
@@ -581,7 +573,7 @@ def array_to_raster(array, file_name, projection=None,
               A GDAL supported bittype, e.g. GDT_Int32
               Default: GDT_Float64
     """
-    if not has_gdal:
+    if not gdal:
         raise ImportError('No module named gdal.')
     driver = gdal.GetDriverByName(outformat)
     try:
@@ -617,7 +609,7 @@ def array_to_raster(array, file_name, projection=None,
             bnd.WriteArray(array[:,:,i - 1])
             dataset.FlushCache()
 
-
+@conditional_gdal
 def match_rasters(match_to, match_from, destination,
                   resampling_method='GRA_Bilinear', ndv=0):
     """
@@ -658,9 +650,6 @@ def match_rasters(match_to, match_from, destination,
     match_from__srs = match_from.dataset.GetProjection()
     match_from__gt = match_from.geotransform
 
-    if not has_gdal:
-        raise ImportError('No module named gdal.')
-
     dst = gdal.GetDriverByName('GTiff').Create(destination, width, height, 1,
                                                gdalconst.GDT_Float64)
 
diff --git a/plio/io/io_spectral_profiler.py b/plio/io/io_spectral_profiler.py
index a82db13..dc31aa0 100755
--- a/plio/io/io_spectral_profiler.py
+++ b/plio/io/io_spectral_profiler.py
@@ -4,7 +4,6 @@ import pvl
 import numpy as np
 
 from plio.utils.utils import find_in_dict
-from plio.io.io_gdal import GeoDataset
 
 class Spectral_Profiler(object):
 
@@ -169,5 +168,7 @@ class Spectral_Profiler(object):
         -------
 
         """
+
+        from plio.io.io_gdal import GeoDataset
         path, ext = os.path.splitext(self.input_data)
         self.browse = GeoDataset(path + extension)
diff --git a/plio/io/tests/test_io_gdal.py b/plio/io/tests/test_io_gdal.py
index f3d5255..38ef04f 100644
--- a/plio/io/tests/test_io_gdal.py
+++ b/plio/io/tests/test_io_gdal.py
@@ -3,14 +3,17 @@ import sys
 import unittest
 
 import numpy as np
+import pytest
 
 from plio.examples import get_path
 
 sys.path.insert(0, os.path.abspath('..'))
 
 from plio.io import io_gdal
+from plio.io import gdal
 
 
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
 class TestMercator(unittest.TestCase):
     def setUp(self):
         self.dataset = io_gdal.GeoDataset(get_path('Mars_MGS_MOLA_ClrShade_MAP2_0.0N0.0_MERC.tif'))
@@ -101,7 +104,7 @@ class TestMercator(unittest.TestCase):
         self.assertEqual(arr.dtype, np.int8)
         self.assertAlmostEqual(np.mean(arr), 10.10353227, 6)
 
-
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
 class TestLambert(unittest.TestCase):
     def setUp(self):
         self.dataset = io_gdal.GeoDataset(get_path('Lunar_LRO_LOLA_Shade_MAP2_90.0N20.0_LAMB.tif'))
@@ -143,6 +146,7 @@ class TestLambert(unittest.TestCase):
         self.assertEqual(sp, [73.0, 42.0])
 
 
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
 class TestPolar(unittest.TestCase):
     def setUp(self):
         self.dataset = io_gdal.GeoDataset(get_path('Mars_MGS_MOLA_ClrShade_MAP2_90.0N0.0_POLA.tif'))
@@ -171,6 +175,7 @@ class TestPolar(unittest.TestCase):
         self.assertAlmostEqual(pixel[0], 0.0, 6)
         self.assertAlmostEqual(pixel[1], 0.0, 6)
 
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
 class TestWriter(unittest.TestCase):
     def setUp(self):
         self.arr = np.random.random((100,100))
@@ -240,11 +245,3 @@ class TestWriter(unittest.TestCase):
         except:
             pass
 
-class TestWithoutGdal(unittest.TestCase):
-    def test_without_gdal(self):
-        io_gdal.has_gdal = False
-        with self.assertRaises(ImportError):
-            io_gdal.GeoDataset('foo')
-
-    def tearDown(self):
-        io_gdal.has_gdal = True
diff --git a/plio/io/tests/test_io_spectral_profiler.py b/plio/io/tests/test_io_spectral_profiler.py
index 0446607..6b5b390 100644
--- a/plio/io/tests/test_io_spectral_profiler.py
+++ b/plio/io/tests/test_io_spectral_profiler.py
@@ -3,12 +3,15 @@ import sys
 import unittest
 
 import pandas as pd
+import pytest
 
 sys.path.insert(0, os.path.abspath('..'))
 
 from plio.examples import get_path
 from plio.io import io_spectral_profiler
 from plio.io.io_gdal import GeoDataset
+from plio.io import gdal
+
 
 class Test_Spectral_Profiler_IO(unittest.TestCase):
 
@@ -19,7 +22,8 @@ class Test_Spectral_Profiler_IO(unittest.TestCase):
         ds = io_spectral_profiler.Spectral_Profiler(self.examplefile)
         self.assertEqual(ds.nspectra, 38)
         self.assertEqual(ds.spectra[0].columns.tolist(), ['RAW', 'REF1', 'REF2', 'QA', 'RAD'])
-
+    
+    @pytest.mark.skipif(gdal is None, reason="GDAL not installed")
     def test_read_browse(self):
         ds = io_spectral_profiler.Spectral_Profiler(self.examplefile)
         ds.open_browse()
diff --git a/plio/io/tests/test_metadata.py b/plio/io/tests/test_metadata.py
index 5b28ff5..df63a00 100644
--- a/plio/io/tests/test_metadata.py
+++ b/plio/io/tests/test_metadata.py
@@ -1,60 +1,65 @@
-import unittest
-
-from osgeo import osr
-
-osr.UseExceptions()
+import pytest
 
 from plio.io import extract_metadata as em
-
-
-class TestSRSProjectionExtraction(unittest.TestCase):
-
-    def setUp(self):
-        self.wktsrs = 'PROJCS["Moon2000_Mercator180",GEOGCS["GCS_Moon_2000",DATUM["D_Moon_2000",SPHEROID["Moon_2000_IAU_IAG",1737400.0,0.0]],PRIMEM["Reference_Meridian",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",180.0],PARAMETER["Standard_Parallel_1",0.0],UNIT["Meter",1.0]]'
-        self.wktsrs = 'PROJCS["Mercator",GEOGCS["GCS_Moon_2000",DATUM["D_Moon_2000",SPHEROID["Moon_2000_IAU_IAG",1737400.0,0.0]],PRIMEM["Reference_Meridian",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",180.0],PARAMETER["Standard_Parallel_1",0.0],UNIT["Meter",1.0]]'
-        self.srs = em.extract_projstring(self.wktsrs)
-
-    def test_generate_srs(self):
-        self.srs = em.extract_projstring(self.wktsrs)
-
-    def test_false_easting(self):
-        e = em.get_false_easting(self.srs)
-        self.assertEqual(e, 0.0)
-
-    def test_false_northing(self):
-        n = em.get_false_northing(self.srs)
-        self.assertEqual(n, 0.0)
-
-    def test_projection_name(self):
-        name = em.get_projection_name(self.srs)
-        self.assertEqual(name, 'Mercator_2SP')
-
-    def test_axes_extract(self):
-        smajor, sminor, invflattening = em.get_spheroid(self.srs)
-        self.assertEqual(smajor, 1737400.0)
-        self.assertEqual(sminor, 1737400.0)
-        self.assertEqual(invflattening, 0.0)
-
-    def test_get_standard_parallels(self):
-        parallels = em.get_standard_parallels(self.srs)
-        self.assertEqual(parallels[0], 0.0)
-        self.assertEqual(parallels[1], 0.0)
-
-    def test_get_central_meridian(self):
-        clon = em.get_central_meridian(self.srs)
-        self.assertEqual(clon, 180.0)
-
-    def test_export_to_proj4(self):
-        """
-        Check that proj4 is not supporting Moon2000_Mercator
-        """
-        proj4 = self.srs.ExportToProj4()
-        self.assertEqual(proj4, '+proj=merc +lon_0=180 +lat_ts=0 +x_0=0 +y_0=0 +a=1737400 +b=1737400 +units=m +no_defs ')
-
-    def test_scale_factor(self):
-        k = em.get_scale_factor(self.srs)
-        self.assertEqual(k, 1.0)
-
-    def test_latitude_of_origin(self):
-        lo = em.get_latitude_of_origin(self.srs)
-        self.assertEqual(lo, 90.0)
+from plio.io import gdal
+
+@pytest.fixture
+def wkt_moon():
+    wktsrs = 'PROJCS["Moon2000_Mercator180",GEOGCS["GCS_Moon_2000",DATUM["D_Moon_2000",SPHEROID["Moon_2000_IAU_IAG",1737400.0,0.0]],PRIMEM["Reference_Meridian",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",180.0],PARAMETER["Standard_Parallel_1",0.0],UNIT["Meter",1.0]]'
+    return em.extract_projstring(wktsrs)
+
+@pytest.fixture
+def srs_mars():
+    wktsrs = 'PROJCS["Mercator",GEOGCS["GCS_Moon_2000",DATUM["D_Moon_2000",SPHEROID["Moon_2000_IAU_IAG",1737400.0,0.0]],PRIMEM["Reference_Meridian",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",180.0],PARAMETER["Standard_Parallel_1",0.0],UNIT["Meter",1.0]]'
+    return em.extract_projstring(wktsrs)
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_false_easting(srs_mars):
+    e = em.get_false_easting(srs_mars)
+    assert e == 0.0
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_false_northing(srs_mars):
+    n = em.get_false_northing(srs_mars)
+    assert n == 0.0
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_projection_name(srs_mars):
+    name = em.get_projection_name(srs_mars)
+    assert name == 'Mercator_2SP'
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_axes_extract(srs_mars):
+    smajor, sminor, invflattening = em.get_spheroid(srs_mars)
+    assert smajor == 1737400.0
+    assert sminor == 1737400.0
+    assert invflattening == 0.0
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_get_standard_parallels(srs_mars):
+    parallels = em.get_standard_parallels(srs_mars)
+    assert parallels[0] == 0.0
+    assert parallels[1] == 0.0
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_get_central_meridian(srs_mars):
+    clon = em.get_central_meridian(srs_mars)
+    assert clon == 180.0
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_export_to_proj4(srs_mars):
+    """
+    Check that proj4 is not supporting Moon2000_Mercator
+    """
+    proj4 = srs_mars.ExportToProj4()
+    assert proj4 == '+proj=merc +lon_0=180 +lat_ts=0 +x_0=0 +y_0=0 +a=1737400 +b=1737400 +units=m +no_defs '
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_scale_factor(srs_mars):
+    k = em.get_scale_factor(srs_mars)
+    assert k == 1.0
+
+@pytest.mark.skipif(gdal is None, reason="GDAL not installed")
+def test_latitude_of_origin(srs_mars):
+    lo = em.get_latitude_of_origin(srs_mars)
+    assert lo == 90.0
-- 
GitLab