Repository: rasterio/rasterio

Issue:
Return None when a dataset has an undefined CRS
Instead of `CRS()`. Returning `CRS()` means that application-level concerns about what an undefined CRS means become Rasterio's concerns. `None` is what we have for undefined things in Python, and using it will help us tackle the concerns at the proper level.


Discussion:

Resolving patch:
diff --git a/rasterio/rio/edit_info.py b/rasterio/rio/edit_info.py
--- a/rasterio/rio/edit_info.py
+++ b/rasterio/rio/edit_info.py
@@ -171,7 +171,7 @@ def in_dtype_range(value, dtype):
             dst.nodata = nodata
 
         if unset_crs:
-            dst.crs = CRS()
+            dst.crs = None  # CRS()
         elif crs:
             dst.crs = crs
 
@@ -190,7 +190,7 @@ def in_dtype_range(value, dtype):
     # Post check - ensure that crs was unset properly
     if unset_crs:
         with ctx.obj['env'], rasterio.open(input, 'r') as src:
-            if dict(src.crs) != {}:
+            if src.crs:
                 warnings.warn(
                     'CRS was not unset. Availability of his functionality '
                     'differs depending on GDAL version and driver')
diff --git a/rasterio/rio/info.py b/rasterio/rio/info.py
--- a/rasterio/rio/info.py
+++ b/rasterio/rio/info.py
@@ -67,10 +67,15 @@ def info(ctx, input, aspect, indent, namespace, meta_member, verbose, bidx,
             info = dict(src.profile)
             info['shape'] = (info['height'], info['width'])
             info['bounds'] = src.bounds
-            proj4 = src.crs.to_string()
-            if proj4.startswith('+init=epsg'):
-                proj4 = proj4.split('=')[1].upper()
-            info['crs'] = proj4
+
+            if src.crs:
+                proj4 = src.crs.to_string()
+                if proj4.startswith('+init=epsg'):
+                    proj4 = proj4.split('=')[1].upper()
+                info['crs'] = proj4
+            else:
+                info['crs'] = None
+
             info['res'] = src.res
             info['colorinterp'] = [src.colorinterp(i).name
                                    for i in src.indexes]


Test patch:
diff --git a/tests/test_crs.py b/tests/test_crs.py
--- a/tests/test_crs.py
+++ b/tests/test_crs.py
@@ -8,16 +8,24 @@
 from rasterio._base import _can_create_osr
 from rasterio.crs import CRS
 from rasterio.errors import CRSError
+from rasterio.io import DatasetWriter
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
 
+@pytest.fixture(scope='session')
+def profile_rgb_byte_tif(path_rgb_byte_tif):
+    with rasterio.open(path_rgb_byte_tif) as src:
+        return src.profile
+
+
 # When possible, Rasterio gives you the CRS in the form of an EPSG code.
 def test_read_epsg(tmpdir):
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         assert src.crs.to_dict() == {'init': 'epsg:32618'}
 
+
 def test_read_epsg3857(tmpdir):
     tiffname = str(tmpdir.join('lol.tif'))
     subprocess.call([
@@ -26,6 +34,7 @@ def test_read_epsg3857(tmpdir):
     with rasterio.open(tiffname) as src:
         assert src.crs.to_dict() == {'init': 'epsg:3857'}
 
+
 # Ensure that CRS sticks when we write a file.
 def test_write_3857(tmpdir):
     src_path = str(tmpdir.join('lol.tif'))
@@ -42,6 +51,15 @@ def test_write_3857(tmpdir):
     assert 'PROJCS["WGS 84 / Pseudo-Mercator"' in info.decode('utf-8')
 
 
+def test_write_bogus_fails(tmpdir, profile_rgb_byte_tif):
+    src_path = str(tmpdir.join('lol.tif'))
+    profile = profile_rgb_byte_tif.copy()
+    profile['crs'] = ['foo']
+    with pytest.raises(CRSError):
+        rasterio.open(src_path, 'w', **profile)
+        # TODO: switch to DatasetWriter here and don't require a .start().
+
+
 def test_from_proj4_json():
     json_str = '{"proj": "longlat", "ellps": "WGS84", "datum": "WGS84"}'
     crs_dict = CRS.from_string(json_str)
@@ -165,8 +183,7 @@ def test_empty_json():
 @pytest.mark.parametrize('arg', [None, {}, ''])
 def test_can_create_osr_none_err(arg):
     """Passing None or empty fails"""
-    with pytest.raises(ValueError):
-        _can_create_osr(arg)
+    assert not _can_create_osr(arg)
 
 
 def test_can_create_osr():
diff --git a/tests/test_rio_info.py b/tests/test_rio_info.py
--- a/tests/test_rio_info.py
+++ b/tests/test_rio_info.py
@@ -44,7 +44,8 @@ def test_unset_crs(data):
                            ['edit-info', inputfile, '--unset-crs'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
-        assert dict(src.crs) == {}
+        assert src.crs is None
+
 
 @pytest.mark.skip(
     Version(rasterio.__gdal_version__) >= Version('1.10'),
diff --git a/tests/test_warp.py b/tests/test_warp.py
--- a/tests/test_warp.py
+++ b/tests/test_warp.py
@@ -10,7 +10,7 @@
 import rasterio
 from rasterio.control import GroundControlPoint
 from rasterio.enums import Resampling
-from rasterio.errors import GDALBehaviorChangeException
+from rasterio.errors import GDALBehaviorChangeException, CRSError
 from rasterio.warp import (
     reproject, transform_geom, transform, transform_bounds,
     calculate_default_transform)
@@ -98,6 +98,53 @@ def uninvertable_reproject_params():
         dst_crs={'init': 'EPSG:26836'})
 
 
+WGS84_crs = {'init': 'EPSG:4326'}
+
+
+def test_transform_src_crs_none():
+    with pytest.raises(CRSError):
+        transform(None, WGS84_crs, [], [])
+
+
+def test_transform_dst_crs_none():
+    with pytest.raises(CRSError):
+        transform(WGS84_crs, None, [], [])
+
+
+def test_transform_bounds_src_crs_none():
+    with pytest.raises(CRSError):
+        transform_bounds(None, WGS84_crs, 0, 0, 0, 0)
+
+
+def test_transform_bounds_dst_crs_none():
+    with pytest.raises(CRSError):
+        transform_bounds(WGS84_crs, None, 0, 0, 0, 0)
+
+
+def test_transform_geom_src_crs_none():
+    with pytest.raises(CRSError):
+        transform_geom(None, WGS84_crs, None)
+
+
+def test_transform_geom_dst_crs_none():
+    with pytest.raises(CRSError):
+        transform_geom(WGS84_crs, None, None)
+
+
+def test_reproject_src_crs_none():
+    with pytest.raises(CRSError):
+        reproject(np.ones((2, 2)), np.zeros((2, 2)),
+                  src_transform=Affine.identity(),
+                  dst_transform=Affine.identity(), dst_crs=WGS84_crs)
+
+
+def test_reproject_dst_crs_none():
+    with pytest.raises(CRSError):
+        reproject(np.ones((2, 2)), np.zeros((2, 2)),
+                  src_transform=Affine.identity(),
+                  dst_transform=Affine.identity(), src_crs=WGS84_crs)
+
+
 def test_transform():
     """2D and 3D."""
     WGS84_crs = {'init': 'EPSG:4326'}