Repository: rasterio/rasterio

Issue:
Semantics of --dst-crs {} are underdefined
From https://github.com/mapbox/rasterio/pull/621#issuecomment-204564340:

> or using "[]" instead of {}? Currently an output raster is created without a CRS since this is valid JSON.

``` bash
$ rio warp --dst-crs {} tests/data/RGB.byte.tif OUT.tif
```



Discussion:

Resolving patch:
diff --git a/rasterio/crs.py b/rasterio/crs.py
--- a/rasterio/crs.py
+++ b/rasterio/crs.py
@@ -1,17 +1,20 @@
-# Coordinate reference systems and functions.
-#
-# PROJ.4 is the law of this land: http://proj.osgeo.org/. But whereas PROJ.4
-# coordinate reference systems are described by strings of parameters such as
-#
-#   +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
-#
-# here we use mappings:
-#
-#   {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True}
-#
+"""Coordinate reference systems and functions.
+
+PROJ.4 is the law of this land: http://proj.osgeo.org/. But whereas PROJ.4
+coordinate reference systems are described by strings of parameters such as
+
+  +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
+
+here we use mappings:
+
+  {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True}
+"""
+
 
 import json
+
 from rasterio._base import is_geographic_crs, is_projected_crs, is_same_crs
+from rasterio.errors import CRSError
 from rasterio.five import string_types
 
 
@@ -54,9 +57,14 @@ def from_string(prjs):
     if '{' in prjs:
         # may be json, try to decode it
         try:
-            return json.loads(prjs, strict=False)
+            val = json.loads(prjs, strict=False)
         except ValueError:
-            raise ValueError('crs appears to be JSON but is not valid')
+            raise CRSError('crs appears to be JSON but is not valid')
+
+        if not val:
+            raise CRSError("crs is empty JSON")
+        else:
+            return val
 
     if prjs.strip().upper().startswith('EPSG:'):
         return from_epsg(prjs.split(':')[1])
@@ -82,7 +90,12 @@ def parse(v):
         lambda kv: len(kv) == 2 and (kv[0], parse(kv[1])) or (kv[0], True),
         (p.split('=') for p in parts))
 
-    return dict((k, v) for k, v in items if k in all_proj_keys)
+    out = dict((k, v) for k, v in items if k in all_proj_keys)
+
+    if not out:
+        raise CRSError("crs is empty or invalid: {}".format(prjs))
+
+    return out
 
 
 def from_epsg(code):
diff --git a/rasterio/rio/warp.py b/rasterio/rio/warp.py
--- a/rasterio/rio/warp.py
+++ b/rasterio/rio/warp.py
@@ -157,7 +157,7 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
                     dst_height = template_ds.height
                     dst_width = template_ds.width
 
-            elif dst_crs:
+            elif dst_crs is not None:
                 try:
                     dst_crs = crs.from_string(dst_crs)
                 except ValueError as err:


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,6 +8,7 @@
 from rasterio import crs
 from rasterio.crs import (
     is_geographic_crs, is_projected_crs, is_same_crs, is_valid_crs)
+from rasterio.errors import CRSError
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
@@ -146,3 +147,12 @@ def test_is_valid_false():
 
 def test_is_valid():
     assert is_valid_crs('EPSG:4326')
+
+
+def test_empty_json():
+    with pytest.raises(CRSError):
+        crs.from_string('{}')
+    with pytest.raises(CRSError):
+        crs.from_string('[]')
+    with pytest.raises(CRSError):
+        crs.from_string('')
diff --git a/tests/test_rio_warp.py b/tests/test_rio_warp.py
--- a/tests/test_rio_warp.py
+++ b/tests/test_rio_warp.py
@@ -1,12 +1,12 @@
 import logging
 import os
-import re
 import sys
 
 import numpy
 import pytest
 
 import rasterio
+import rasterio.crs
 from rasterio.rio import warp
 from rasterio.rio.main import main_group
 
@@ -352,3 +352,22 @@ def test_warp_reproject_nolostdata(runner, tmpdir):
         # 50 column swath on the right edge should have some ones (gdalwarped has 7223)
         assert arr[0, :, -50:].sum() > 7000
         assert output.crs == {'init': 'epsg:3857'}
+
+
+def test_warp_dst_crs_empty_string(runner, tmpdir):
+    """`$ rio warp --dst-crs ''` used to perform a falsey check that would treat
+    `--dst-crs ''` as though `--dst-crs` was not supplied at all.  If the user
+    gives any value we should let `rasterio.crs.from_string()` handle the
+    validation.
+    """
+
+    infile = 'tests/data/RGB.byte.tif'
+    outfile = str(tmpdir.mkdir('empty_warp_dst_crs.tif').join('test.tif'))
+
+    result = runner.invoke(warp.warp, [
+        infile,
+        outfile,
+        '--dst-crs', ''])
+
+    assert result.exit_code != 0
+    assert 'empty or invalid' in result.output