Repository: rasterio/rasterio

Issue:
Add support for netcdf to CI instances
Though we have the netcdf apt package we don't seem to be configuring it when we build our GDAL library artifacts. We need this to test the subdatasets of #1127.


Discussion:

Resolving patch:
diff --git a/rasterio/rio/info.py b/rasterio/rio/info.py
--- a/rasterio/rio/info.py
+++ b/rasterio/rio/info.py
@@ -50,6 +50,8 @@
 @click.option('--checksum', 'meta_member', flag_value='checksum',
               help="Print integer checksum of a single band "
                    "(use --bidx).")
+@click.option('--subdatasets', 'meta_member', flag_value='subdatasets',
+              help="Print subdataset identifiers.")
 @click.option('-v', '--tell-me-more', '--verbose', is_flag=True,
               help="Output extra information.")
 @options.bidx_opt
@@ -106,7 +108,10 @@ def info(ctx, input, aspect, indent, namespace, meta_member, verbose, bidx,
                     'crs': proj4, 'points': [p.asdict() for p in gcps]}
 
         if aspect == 'meta':
-            if meta_member == 'stats':
+            if meta_member == 'subdatasets':
+                for name in src.subdatasets:
+                    click.echo(name)
+            elif meta_member == 'stats':
                 band = src.read(bidx, masked=masked)
                 click.echo('%f %f %f' % (
                     float(band.min()),
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
--- a/rasterio/rio/options.py
+++ b/rasterio/rio/options.py
@@ -53,7 +53,7 @@
 import click
 
 import rasterio
-from rasterio.vfs import parse_path
+from rasterio.vfs import FILE_SCHEMES, parse_path
 
 
 class IgnoreOptionMarker(object):
@@ -111,10 +111,12 @@ def file_in_handler(ctx, param, value):
     try:
         path, archive, scheme = parse_path(value)
         path_to_check = archive or path
-        if (scheme not in ['http', 'https', 's3'] and not
-                os.path.exists(path_to_check)):
+
+        # Validate existence of files.
+        if scheme in FILE_SCHEMES and not os.path.exists(path_to_check):
             raise IOError(
                 "Input file {0} does not exist".format(path_to_check))
+
         if archive and scheme:
             archive = abspath_forward_slashes(archive)
             path = "{0}://{1}!{2}".format(scheme, archive, path)
@@ -122,8 +124,10 @@ def file_in_handler(ctx, param, value):
             path = "{0}://{1}".format(scheme, path)
         elif scheme == 's3':
             path = "{0}://{1}".format(scheme, path)
-        else:
+        elif scheme in FILE_SCHEMES:
             path = abspath_forward_slashes(path)
+        else:
+            path = path
         return path
     except Exception:
         raise click.BadParameter("{} is not a valid input file".format(value))
diff --git a/rasterio/vfs.py b/rasterio/vfs.py
--- a/rasterio/vfs.py
+++ b/rasterio/vfs.py
@@ -8,8 +8,19 @@
 # NB: As not to propagate fallacies of distributed computing, Rasterio
 # does not support HTTP or FTP URLs via GDAL's vsicurl handler. Only
 # the following local filesystem schemes are supported.
-SCHEMES = {'gzip': 'gzip', 'zip': 'zip', 'tar': 'tar', 'https': 'curl',
-           'http': 'curl', 's3': 's3'}
+SCHEMES = {
+    'gzip': 'gzip',
+    'gzip+file': 'gzip',
+    'zip': 'zip',
+    'zip+file': 'zip',
+    'tar': 'tar',
+    'tar+file': 'tar',
+    'https': 'curl',
+    'http': 'curl',
+    's3': 's3'}
+
+FILE_SCHEMES = [
+    '', 'file', 'gzip', 'gzip+file', 'zip', 'zip+file', 'tar', 'tar+file']
 
 
 def parse_path(uri, vfs=None):
@@ -38,10 +49,10 @@ def parse_path(uri, vfs=None):
             path = parts.pop() if parts else None
             archive = parts.pop() if parts else None
         # For filesystem paths.
-        elif scheme in (None, '', 'file'):
+        elif scheme.lower() in FILE_SCHEMES:
             pass
         # We permit GDAL's idiosyncratic URI-like dataset paths such as
-        # 'NETCDF:...' to fall right through with no parsed archive
+        # 'netcdf':... to fall right through with no parsed archive
         # or scheme.
         else:
             archive = scheme = None
@@ -60,7 +71,8 @@ def vsi_path(path, archive=None, scheme=None):
         result = "/vsis3/{0}".format(path)
     elif scheme and scheme != 'file':
         if archive:
-            result = '/vsi{0}/{1}/{2}'.format(scheme, archive, path.lstrip('/'))
+            result = '/vsi{0}/{1}/{2}'.format(
+                scheme, archive, path.lstrip('/'))
         else:
             result = '/vsi{0}/{1}'.format(scheme, path.lstrip('/'))
     else:


Test patch:
diff --git a/tests/data/RGB.nc b/tests/data/RGB.nc
new file mode 100644
Binary files /dev/null and b/tests/data/RGB.nc differ
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
@@ -390,7 +390,7 @@ def test_info():
     info = json.loads(result.output)
     assert info['count'] == 1
     assert info['dtype'] == 'float64'
-    assert info['crs'] == None
+    assert info['crs'] is None
 
 
 def test_info_units():
@@ -767,3 +767,15 @@ def test_info_checksums_only():
         ['info', 'tests/data/RGB.byte.tif', '--checksum', '--bidx', '2'])
     assert result.exit_code == 0
     assert result.output.strip() == '29131'
+
+
+@pytest.mark.skipif(parse(rasterio.__gdal_version__) < parse('2.1'),
+                    reason='netCDF requires GDAL 2.1+')
+def test_info_subdatasets():
+    runner = CliRunner()
+    result = runner.invoke(
+        main_group,
+        ['info', 'netcdf:tests/data/RGB.nc', '--subdatasets'])
+    assert result.exit_code == 0
+    assert len(result.output) == 93
+    assert result.output.startswith('netcdf:tests/data/RGB.nc:Band1')
diff --git a/tests/test_rio_options.py b/tests/test_rio_options.py
--- a/tests/test_rio_options.py
+++ b/tests/test_rio_options.py
@@ -72,13 +72,6 @@ def test_file_in_handler_no_vfs():
     assert retval == abspath_forward_slashes('tests/data/RGB.byte.tif')
 
 
-def test_file_in_handler_bad_scheme():
-    """lolwut scheme is not supported"""
-    ctx = MockContext()
-    with pytest.raises(click.BadParameter):
-        file_in_handler(ctx, 'INPUT', 'lolwut://bogus')
-
-
 def test_file_in_handler_with_vfs_nonexistent():
     """archive does not exist"""
     ctx = MockContext()
diff --git a/tests/test_subdatasets.py b/tests/test_subdatasets.py
new file mode 100644
--- /dev/null
+++ b/tests/test_subdatasets.py
@@ -0,0 +1,15 @@
+from packaging.version import parse
+import pytest
+
+import rasterio
+
+
+@pytest.mark.skipif(parse(rasterio.__gdal_version__) < parse('2.1'),
+                    reason="netcdf driver not available before GDAL 2.1")
+def test_subdatasets():
+    """Get subdataset names and descriptions"""
+    with rasterio.open('netcdf:tests/data/RGB.nc') as src:
+        subs = src.subdatasets
+        assert len(subs) == 3
+        for name in subs:
+            assert name.startswith('netcdf')
diff --git a/tests/test_vfs.py b/tests/test_vfs.py
--- a/tests/test_vfs.py
+++ b/tests/test_vfs.py
@@ -18,7 +18,7 @@ def test_parse_path_with_vfs():
 
 
 def test_parse_path_zip():
-    """Correctly parse VFS scheme URL"""
+    """Correctly parse zip scheme URL"""
     assert parse_path('zip://tests/data/files.zip!foo.tif') == (
         'foo.tif', 'tests/data/files.zip', 'zip')
 
@@ -35,10 +35,10 @@ def test_parse_path_file():
         '/foo.tif', None, '')
 
 
-def test_parse_netcdf():
-    """Annoying URI-like GDAL dataset names fall through properly"""
-    assert parse_path('NETCDF:filepath:varname') == (
-        'NETCDF:filepath:varname', None, None)
+def test_parse_gdal():
+    """GDAL dataset identifiers fall through properly"""
+    assert parse_path('GDAL:filepath:varname') == (
+        'GDAL:filepath:varname', None, None)
 
 
 def test_vsi_path_scheme():