[fiona] 01/02: Imported Upstream version 1.6.0

Johan Van de Wauw johanvdw-guest at moszumanska.debian.org
Sat Aug 1 23:03:01 UTC 2015


This is an automated email from the git hooks/post-receive script.

johanvdw-guest pushed a commit to branch master
in repository fiona.

commit 021f3000994a4e5de7715d0c95b806ecbfb1efc1
Author: Johan Van de Wauw <johan.vandewauw at gmail.com>
Date:   Thu Jul 23 22:17:24 2015 +0200

    Imported Upstream version 1.6.0
---
 CHANGES.txt                   |   9 ++
 CREDITS.txt                   |  44 +++++----
 README.rst                    |  19 ++++
 fiona/__init__.py             |   8 +-
 fiona/_drivers.pyx            |   1 +
 fiona/_geometry.pxd           |  20 ++--
 fiona/collection.py           |  40 +++++++-
 fiona/fio/bounds.py           |   9 +-
 fiona/fio/cat.py              | 138 ++++++++++++++++++++++-----
 fiona/fio/cli.py              |  61 ------------
 fiona/fio/fio.py              | 202 ----------------------------------------
 fiona/fio/helpers.py          |  33 +++++++
 fiona/fio/info.py             |  99 ++++++++++++++++++++
 fiona/fio/main.py             |  36 +++++++
 fiona/fio/options.py          |   8 ++
 fiona/inspector.py            |  15 +--
 fiona/ograpi.pxd              |  10 ++
 fiona/ogrext.pyx              |  20 ++++
 requirements-dev.txt          |   2 +-
 setup.py                      |  80 +++++++++++-----
 tests/fixtures.py             |   7 ++
 tests/test_bytescollection.py | 212 ++++++++++++++++++++++++++++++++++++++++++
 tests/test_cli.py             |  17 ++--
 tests/test_fio_cat.py         |   4 +-
 tests/test_fio_load.py        |  69 ++++++++++++--
 25 files changed, 794 insertions(+), 369 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 23f4b97..b6b043b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,15 @@
 Changes
 =======
 
+1.6.0 (2015-07-21)
+------------------
+- Upgrade Cython requirement to 0.22 (#214).
+- New BytesCollection class (#215).
+- Add GDAL's OpenFileGDB driver to registered drivers (#221).
+- Implement CLI commands as plugins (#228).
+- Raise click.abort instead of calling sys.exit, preventing suprising exits
+  (#236).
+
 1.5.1 (2015-03-19)
 ------------------
 - Restore test data to sdists by fixing MANIFEST.in (#216).
diff --git a/CREDITS.txt b/CREDITS.txt
index 4fcf276..03a2998 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -3,26 +3,30 @@ Credits
 
 Fiona is written by:
 
-* Sean Gillies (https://github.com/sgillies)
-
-With contributions by:
-
-* Joshua Arnott (https://github.com/snorfalorpagus)
-* René Buffat (https://github.com/rbuffat)
-* Michele Citterio (https://github.com/citterio)
-* Stefano Costa (https://github.com/steko)
-* Ludovic Delauné (https://github.com/ldgeo)
-* Kelsey Jordahl (https://github.com/kjordahl)
-* Frédéric Junod (https://github.com/fredj)
-* jwass (https://github.com/jwass)
-* Brandon Liu (https://github.com/bdon)
-* lordi (https://github.com/lordi)
-* Ariel Núñez (https://github.com/ingenieroariel)
-* Oliver Tonnhofer (https://github.com/olt)
-* Brendan Wards (https://github.com/brendan-ward)
-* Michael Weisman (https://github.com/mweisman)
-* Andy Wilson (https://github.com/wilsaj)
-* Martijn Visser (https://github.com/visr)
+* Sean Gillies <sean.gillies at gmail.com>
+* Rene Buffat <buffat at gmail.com>
+* Kevin Wurster <wursterk at gmail.com>
+* Kelsey Jordahl <kjordahl at enthought.com>
+* Patrick Young <patrick.mckendree.young at gmail.com>
+* Hannes Gräuler <graeuler at geoplex.de>
+* Ryan Grout <rgrout at continuum.io>
+* Michael Weisman <mweisman at gmail.com>
+* Joshua Arnott <josh at snorfalorpagus.net>
+* Jacob Wasserman <jwasserman at gmail.com>
+* Miro Hrončok <miro at hroncok.cz>
+* Michele Citterio <michele at citterio.net>
+* Johan Van de Wauw <johan.vandewauw at gmail.com>
+* fredj <frederic.junod at camptocamp.com>
+* Brendan Ward <bcward at consbio.org>
+* wilsaj <wilson.andrew.j+github at gmail.com>
+* Stefano Costa <steko at iosa.it>
+* Oliver Tonnhofer <olt at bogosoft.com>
+* Martijn Visser <mgvisser at gmail.com>
+* Ludovic Delauné <ludotux at gmail.com>
+* Hannes Gräuler <hgraeule at uos.de>
+* dimlev <dimlev at gmail.com>
+* Brandon Liu <bdon at bdon.org>
+* Ariel Nunez <ingenieroariel at gmail.com>
 
 Fiona would not be possible without the great work of Frank Warmerdam and other
 GDAL/OGR developers.
diff --git a/README.rst b/README.rst
index 546a4b5..54cce6d 100644
--- a/README.rst
+++ b/README.rst
@@ -286,6 +286,25 @@ Windows
 Binary installers are available at
 http://www.lfd.uci.edu/~gohlke/pythonlibs/#fiona and coming eventually to PyPI.
 
+You can download a binary distribution of GDAL from `here
+<http://www.gisinternals.com/release.php>`_.  You will also need to download
+the compiled libraries and headers (include files).
+
+When building from source on Windows, it is important to know that setup.py
+cannot rely on gdal-config, which is only present on UNIX systems, to discover 
+the locations of header files and libraries that Fiona needs to compile its 
+C extensions. On Windows, these paths need to be provided by the user. 
+You will need to find the include files and the library files for gdal and 
+use setup.py as follows.
+
+.. code-block:: console
+
+    $ python setup.py build_ext -I<path to gdal include files> -lgdal_i -L<path to gdal library>
+    $ python setup.py install
+
+Note: The GDAL dll (gdal111.dll) and gdal-data directory need to be in your 
+Windows PATH otherwise Fiona will fail to work.
+
 Development and testing
 =======================
 
diff --git a/fiona/__init__.py b/fiona/__init__.py
index 70ba9ef..5eac18c 100644
--- a/fiona/__init__.py
+++ b/fiona/__init__.py
@@ -63,17 +63,21 @@ writing modes) flush contents to disk when their ``with`` blocks end.
 """
 
 __all__ = ['bounds', 'listlayers', 'open', 'prop_type', 'prop_width']
-__version__ = "1.5.1"
+__version__ = "1.6.0"
 
 import logging
 import os
 from six import string_types
 
-from fiona.collection import Collection, vsi_path
+from fiona.collection import Collection, BytesCollection, vsi_path
 from fiona._drivers import driver_count, GDALEnv, supported_drivers
 from fiona.odict import OrderedDict
 from fiona.ogrext import _bounds, _listlayers, FIELD_TYPES_MAP
 
+# These modules are imported by fiona.ogrext, but are also import here to
+# help tools like cx_Freeze find them automatically
+from fiona import _geometry, _err, rfc3339
+import uuid
 
 log = logging.getLogger('Fiona')
 class NullHandler(logging.Handler):
diff --git a/fiona/_drivers.pyx b/fiona/_drivers.pyx
index 604ce22..c320455 100644
--- a/fiona/_drivers.pyx
+++ b/fiona/_drivers.pyx
@@ -186,6 +186,7 @@ supported_drivers = dict([
 #ESRI FileGDB 	FileGDB 	Yes 	Yes 	No, needs FileGDB API library
 # multi-layer
     ("FileGDB", "raw"),
+    ("OpenFileGDB", "r"),
 #ESRI Personal GeoDatabase 	PGeo 	No 	Yes 	No, needs ODBC library
 #ESRI ArcSDE 	SDE 	No 	Yes 	No, needs ESRI SDE
 #ESRI Shapefile 	ESRI Shapefile 	Yes 	Yes 	Yes
diff --git a/fiona/_geometry.pxd b/fiona/_geometry.pxd
index 1ff29cf..2bf2cc1 100644
--- a/fiona/_geometry.pxd
+++ b/fiona/_geometry.pxd
@@ -19,14 +19,14 @@ cdef class GeomBuilder:
 
 
 cdef class OGRGeomBuilder:
-    cdef void * _createOgrGeometry(self, int geom_type)
+    cdef void * _createOgrGeometry(self, int geom_type) except NULL
     cdef _addPointToGeometry(self, void *cogr_geometry, object coordinate)
-    cdef void * _buildPoint(self, object coordinates)
-    cdef void * _buildLineString(self, object coordinates)
-    cdef void * _buildLinearRing(self, object coordinates)
-    cdef void * _buildPolygon(self, object coordinates)
-    cdef void * _buildMultiPoint(self, object coordinates)
-    cdef void * _buildMultiLineString(self, object coordinates)
-    cdef void * _buildMultiPolygon(self, object coordinates)
-    cdef void * _buildGeometryCollection(self, object coordinates)
-    cdef void * build(self, object geom)
+    cdef void * _buildPoint(self, object coordinates) except NULL
+    cdef void * _buildLineString(self, object coordinates) except NULL
+    cdef void * _buildLinearRing(self, object coordinates) except NULL
+    cdef void * _buildPolygon(self, object coordinates) except NULL
+    cdef void * _buildMultiPoint(self, object coordinates) except NULL
+    cdef void * _buildMultiLineString(self, object coordinates) except NULL
+    cdef void * _buildMultiPolygon(self, object coordinates) except NULL
+    cdef void * _buildGeometryCollection(self, object coordinates) except NULL
+    cdef void * build(self, object geom) except NULL
diff --git a/fiona/collection.py b/fiona/collection.py
index 3478b7b..e284242 100644
--- a/fiona/collection.py
+++ b/fiona/collection.py
@@ -8,9 +8,10 @@ from fiona.ogrext import Iterator, ItemsIterator, KeysIterator
 from fiona.ogrext import Session, WritingSession
 from fiona.ogrext import (
     calc_gdal_version_num, get_gdal_version_num, get_gdal_release_name)
+from fiona.ogrext import buffer_to_virtual_file, remove_virtual_file
 from fiona.errors import DriverError, SchemaError, CRSError
 from fiona._drivers import driver_count, GDALEnv, supported_drivers
-from six import string_types
+from six import string_types, binary_type
 
 class Collection(object):
 
@@ -406,6 +407,43 @@ class Collection(object):
         self.__exit__(None, None, None)
 
 
+class BytesCollection(Collection):
+    """BytesCollection takes a buffer of bytes and maps that to
+    a virtual file that can then be opened by fiona.
+    """
+    def __init__(self, bytesbuf):
+        """Takes buffer of bytes whose contents is something we'd like
+        to open with Fiona and maps it to a virtual file.
+        """
+        if not isinstance(bytesbuf, binary_type):
+            raise ValueError("input buffer must be bytes")
+
+        # Hold a reference to the buffer, as bad things will happen if
+        # it is garbage collected while in use.
+        self.bytesbuf = bytesbuf
+
+        # Map the buffer to a file.
+        self.virtual_file = buffer_to_virtual_file(self.bytesbuf)
+
+        # Instantiate the parent class.
+        super(BytesCollection, self).__init__(self.virtual_file)
+
+    def close(self):
+        """Removes the virtual file associated with the class."""
+        super(BytesCollection, self).close()
+        if self.virtual_file:
+            remove_virtual_file(self.virtual_file)
+            self.virtual_file = None
+            self.bytesbuf = None
+
+    def __repr__(self):
+        return "<%s BytesCollection '%s', mode '%s' at %s>" % (
+            self.closed and "closed" or "open",
+            self.path + ":" + str(self.name),
+            self.mode,
+            hex(id(self)))
+
+
 def vsi_path(path, vsi=None, archive=None):
     # If a VSF and archive file are specified, we convert the path to
     # an OGR VSI path (see cpl_vsi.h).
diff --git a/fiona/fio/bounds.py b/fiona/fio/bounds.py
index f35398f..2ba1074 100644
--- a/fiona/fio/bounds.py
+++ b/fiona/fio/bounds.py
@@ -6,11 +6,11 @@ import click
 from cligj import precision_opt, use_rs_opt
 
 import fiona
-from fiona.fio.cli import cli, obj_gen
+from .helpers import obj_gen
 
 
 # Bounds command
- at cli.command(short_help="Print the extent of GeoJSON objects")
+ at click.command(short_help="Print the extent of GeoJSON objects")
 @precision_opt
 @click.option('--explode/--no-explode', default=False,
               help="Explode collections into features (default: no).")
@@ -79,7 +79,6 @@ def bounds(ctx, precision, explode, with_id, with_obj, use_rs):
                     click.echo(u'\u001e', nl=False)
                 click.echo(json.dumps(rec))
 
-        sys.exit(0)
     except Exception:
-        logger.exception("Failed. Exception caught")
-        sys.exit(1)
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
diff --git a/fiona/fio/cat.py b/fiona/fio/cat.py
index b97d1ba..a2ed042 100644
--- a/fiona/fio/cat.py
+++ b/fiona/fio/cat.py
@@ -1,15 +1,24 @@
 from functools import partial
+import itertools
 import json
 import logging
 import sys
+import warnings
 
 import click
-from cligj import files_in_arg
-from cligj import precision_opt, indent_opt, compact_opt, use_rs_opt
+from cligj import (
+    compact_opt, files_in_arg, indent_opt,
+    sequence_opt, precision_opt, use_rs_opt)
 
 import fiona
 from fiona.transform import transform_geom
-from fiona.fio.cli import cli, obj_gen
+from .helpers import obj_gen
+from . import options
+
+
+FIELD_TYPES_MAP_REV = dict([(v, k) for k, v in fiona.FIELD_TYPES_MAP.items()])
+
+warnings.simplefilter('default')
 
 
 def make_ld_context(context_items):
@@ -62,15 +71,14 @@ def id_record(rec):
 
 
 # Cat command
- at cli.command(short_help="Concatenate and print the features of datasets")
+ at click.command(short_help="Concatenate and print the features of datasets")
 @files_in_arg
 @precision_opt
 @indent_opt
 @compact_opt
 @click.option('--ignore-errors/--no-ignore-errors', default=False,
               help="log errors but do not stop serialization.")
- at click.option('--dst_crs', default=None, metavar="EPSG:NNNN",
-              help="Destination CRS.")
+ at options.dst_crs_opt
 @use_rs_opt
 @click.option('--bbox', default=None, metavar="w,s,e,n",
               help="filter for features intersecting a bounding box")
@@ -109,14 +117,14 @@ def cat(ctx, files, precision, indent, compact, ignore_errors, dst_crs,
                         if use_rs:
                             click.echo(u'\u001e', nl=False)
                         click.echo(json.dumps(feat, **dump_kwds))
-        sys.exit(0)
+
     except Exception:
-        logger.exception("Failed. Exception caught")
-        sys.exit(1)
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
 
 
 # Collect command
- at cli.command(short_help="Collect a sequence of features.")
+ at click.command(short_help="Collect a sequence of features.")
 @precision_opt
 @indent_opt
 @compact_opt
@@ -125,8 +133,7 @@ def cat(ctx, files, precision, indent, compact, ignore_errors, dst_crs,
               "(default), level.")
 @click.option('--ignore-errors/--no-ignore-errors', default=False,
               help="log errors but do not stop serialization.")
- at click.option('--src_crs', default=None, metavar="EPSG:NNNN",
-              help="Source CRS.")
+ at options.src_crs_opt
 @click.option('--with-ld-context/--without-ld-context', default=False,
               help="add a JSON-LD context to JSON output.")
 @click.option('--add-ld-context-item', multiple=True,
@@ -285,14 +292,13 @@ def collect(ctx, precision, indent, compact, record_buffered, ignore_errors,
             json.dump(collection, sink, **dump_kwds)
             sink.write("\n")
 
-        sys.exit(0)
     except Exception:
-        logger.exception("Failed. Exception caught")
-        sys.exit(1)
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
 
 
 # Distribute command
- at cli.command(short_help="Distribute features from a collection")
+ at click.command(short_help="Distribute features from a collection")
 @use_rs_opt
 @click.pass_context
 def distrib(ctx, use_rs):
@@ -314,13 +320,13 @@ def distrib(ctx, use_rs):
                 if use_rs:
                     click.echo(u'\u001e', nl=False)
                 click.echo(json.dumps(feat))
-        sys.exit(0)
     except Exception:
-        raise
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
 
 
 # Dump command
- at cli.command(short_help="Dump a dataset to GeoJSON.")
+ at click.command(short_help="Dump a dataset to GeoJSON.")
 @click.argument('input', type=click.Path(), required=True)
 @click.option('--encoding', help="Specify encoding of the input file.")
 @precision_opt
@@ -475,7 +481,95 @@ def dump(ctx, input, encoding, precision, indent, compact, record_buffered,
                         collection['features'] = [transformer(source.crs, rec) for rec in source]
                     json.dump(collection, sink, **dump_kwds)
 
-        sys.exit(0)
     except Exception:
-        logger.exception("Failed. Exception caught")
-        sys.exit(1)
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
+
+
+# Load command.
+ at click.command(short_help="Load GeoJSON to a dataset in another format.")
+ at click.argument('output', type=click.Path(), required=True)
+ at click.option('-f', '--format', '--driver', required=True,
+              help="Output format driver name.")
+ at options.src_crs_opt
+ at click.option(
+    '--dst-crs', '--dst_crs',
+    help="Destination CRS.  Defaults to --src-crs when not given.")
+ at click.option(
+    '--sequence / --no-sequence', default=False,
+    help="Specify whether the input stream is a LF-delimited sequence of GeoJSON "
+         "features (the default) or a single GeoJSON feature collection.")
+ at click.pass_context
+def load(ctx, output, driver, src_crs, dst_crs, sequence):
+    """Load features from JSON to a file in another format.
+
+    The input is a GeoJSON feature collection or optionally a sequence of
+    GeoJSON feature objects."""
+    verbosity = (ctx.obj and ctx.obj['verbosity']) or 2
+    logger = logging.getLogger('fio')
+    stdin = click.get_text_stream('stdin')
+
+    dst_crs = dst_crs or src_crs
+
+    if src_crs and dst_crs and src_crs != dst_crs:
+        transformer = partial(transform_geom, src_crs, dst_crs,
+                              antimeridian_cutting=True, precision=-1)
+    else:
+        transformer = lambda x: x
+
+    first_line = next(stdin)
+
+    # If input is RS-delimited JSON sequence.
+    if first_line.startswith(u'\x1e'):
+        def feature_gen():
+            buffer = first_line.strip(u'\x1e')
+            for line in stdin:
+                if line.startswith(u'\x1e'):
+                    if buffer:
+                        feat = json.loads(buffer)
+                        feat['geometry'] = transformer(feat['geometry'])
+                        yield feat
+                    buffer = line.strip(u'\x1e')
+                else:
+                    buffer += line
+            else:
+                feat = json.loads(buffer)
+                feat['geometry'] = transformer(feat['geometry'])
+                yield feat
+    elif sequence:
+        def feature_gen():
+            yield json.loads(first_line)
+            for line in stdin:
+                feat = json.loads(line)
+                feat['geometry'] = transformer(feat['geometry'])
+                yield feat
+    else:
+        def feature_gen():
+            text = "".join(itertools.chain([first_line], stdin))
+            for feat in json.loads(text)['features']:
+                feat['geometry'] = transformer(feat['geometry'])
+                yield feat
+
+    try:
+        source = feature_gen()
+
+        # Use schema of first feature as a template.
+        # TODO: schema specified on command line?
+        first = next(source)
+        schema = {'geometry': first['geometry']['type']}
+        schema['properties'] = dict([
+            (k, FIELD_TYPES_MAP_REV.get(type(v)) or 'str')
+            for k, v in first['properties'].items()])
+
+        with fiona.drivers(CPL_DEBUG=verbosity>2):
+            with fiona.open(
+                    output, 'w',
+                    driver=driver,
+                    crs=dst_crs,
+                    schema=schema) as dst:
+                dst.write(first)
+                dst.writerecords(source)
+
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
diff --git a/fiona/fio/cli.py b/fiona/fio/cli.py
deleted file mode 100644
index bb120e8..0000000
--- a/fiona/fio/cli.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import json
-import logging
-import sys
-import warnings
-
-import click
-from cligj import verbose_opt, quiet_opt
-
-from fiona import __version__ as fio_version
-
-
-warnings.simplefilter('default')
-
-def configure_logging(verbosity):
-    log_level = max(10, 30 - 10*verbosity)
-    logging.basicConfig(stream=sys.stderr, level=log_level)
-
-
-def print_version(ctx, param, value):
-    if not value or ctx.resilient_parsing:
-        return
-    click.echo(fio_version)
-    ctx.exit()
-
-
-# The CLI command group.
- at click.group(help="Fiona command line interface.")
- at verbose_opt
- at quiet_opt
- at click.option('--version', is_flag=True, callback=print_version,
-              expose_value=False, is_eager=True,
-              help="Print Fiona version.")
- at click.pass_context
-def cli(ctx, verbose, quiet):
-    verbosity = verbose - quiet
-    configure_logging(verbosity)
-    ctx.obj = {}
-    ctx.obj['verbosity'] = verbosity
-
-
-def obj_gen(lines):
-    """Return a generator of JSON objects loaded from ``lines``."""
-    first_line = next(lines)
-    if first_line.startswith(u'\x1e'):
-        def gen():
-            buffer = first_line.strip(u'\x1e')
-            for line in lines:
-                if line.startswith(u'\x1e'):
-                    if buffer:
-                        yield json.loads(buffer)
-                    buffer = line.strip(u'\x1e')
-                else:
-                    buffer += line
-            else:
-                yield json.loads(buffer)
-    else:
-        def gen():
-            yield json.loads(first_line)
-            for line in lines:
-                yield json.loads(line)
-    return gen()
diff --git a/fiona/fio/fio.py b/fiona/fio/fio.py
deleted file mode 100644
index 989c16a..0000000
--- a/fiona/fio/fio.py
+++ /dev/null
@@ -1,202 +0,0 @@
-#!/usr/bin/env python
-
-"""Fiona command line interface"""
-
-import code
-from functools import partial
-import itertools
-import json
-import logging
-import pprint
-import sys
-import warnings
-
-import click
-from cligj import indent_opt, sequence_opt
-import six.moves
-
-import fiona
-import fiona.crs
-from fiona.transform import transform_geom
-from fiona.fio.cli import cli
-from fiona.fio.cat import cat, collect, dump, distrib
-from fiona.fio.bounds import bounds
-
-
-FIELD_TYPES_MAP_REV = dict([(v, k) for k, v in fiona.FIELD_TYPES_MAP.items()])
-
-warnings.simplefilter('default')
-
-# Commands are below.
-
- at cli.command(short_help="Print information about the fio environment.")
- at click.option('--formats', 'key', flag_value='formats', default=True,
-              help="Enumerate the available formats.")
- at click.pass_context
-def env(ctx, key):
-    """Print information about the Fiona environment: available
-    formats, etc.
-    """
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('fio')
-    stdout = click.get_text_stream('stdout')
-    with fiona.drivers(CPL_DEBUG=(verbosity > 2)) as env:
-        if key == 'formats':
-            for k, v in sorted(fiona.supported_drivers.items()):
-                modes = ', '.join("'" + m + "'" for m in v)
-                stdout.write("%s (modes %s)\n" % (k, modes))
-            stdout.write('\n')
-
-
-# Info command.
- at cli.command(short_help="Print information about a dataset.")
-# One or more files.
- at click.argument('input', type=click.Path(exists=True))
- at indent_opt
-# Options to pick out a single metadata item and print it as
-# a string.
- at click.option('--count', 'meta_member', flag_value='count',
-              help="Print the count of features.")
- at click.option('-f', '--format', '--driver', 'meta_member', flag_value='driver',
-              help="Print the format driver.")
- at click.option('--crs', 'meta_member', flag_value='crs',
-              help="Print the CRS as a PROJ.4 string.")
- at click.option('--bounds', 'meta_member', flag_value='bounds',
-              help="Print the boundary coordinates "
-                   "(left, bottom, right, top).")
- at click.pass_context
-def info(ctx, input, indent, meta_member):
-    verbosity = (ctx.obj and ctx.obj['verbosity']) or 2
-    logger = logging.getLogger('rio')
-    try:
-        with fiona.drivers(CPL_DEBUG=verbosity>2):
-            with fiona.open(input) as src:
-                info = src.meta
-                info.update(bounds=src.bounds, count=len(src))
-                proj4 = fiona.crs.to_string(src.crs)
-                if proj4.startswith('+init=epsg'):
-                    proj4 = proj4.split('=')[1].upper()
-                info['crs'] = proj4
-                if meta_member:
-                    if isinstance(info[meta_member], (list, tuple)):
-                        click.echo(" ".join(map(str, info[meta_member])))
-                    else:
-                        click.echo(info[meta_member])
-                else:
-                    click.echo(json.dumps(info, indent=indent))
-        sys.exit(0)
-    except Exception:
-        logger.exception("Failed. Exception caught")
-        sys.exit(1)
-
-# Insp command.
- at cli.command(short_help="Open a dataset and start an interpreter.")
- at click.argument('src_path', type=click.Path(exists=True))
- at click.pass_context
-def insp(ctx, src_path):
-    verbosity = (ctx.obj and ctx.obj['verbosity']) or 2
-    logger = logging.getLogger('fio')
-    try:
-        with fiona.drivers(CPL_DEBUG=verbosity>2):
-            with fiona.open(src_path) as src:
-                code.interact(
-                    'Fiona %s Interactive Inspector (Python %s)\n'
-                    'Type "src.schema", "next(src)", or "help(src)" '
-                    'for more information.' %  (
-                        fiona.__version__, '.'.join(
-                            map(str, sys.version_info[:3]))),
-                    local=locals())
-            sys.exit(0)
-    except Exception:
-        logger.exception("Failed. Exception caught")
-        sys.exit(1)
-
-
-# Load command.
- at cli.command(short_help="Load GeoJSON to a dataset in another format.")
- at click.argument('output', type=click.Path(), required=True)
- at click.option('-f', '--format', '--driver', required=True,
-              help="Output format driver name.")
- at click.option('--src_crs', default=None, help="Source CRS.")
- at click.option('--dst_crs', default=None, help="Destination CRS.")
- at sequence_opt
- at click.pass_context
-
-def load(ctx, output, driver, src_crs, dst_crs, sequence):
-    """Load features from JSON to a file in another format.
-
-    The input is a GeoJSON feature collection or optionally a sequence of
-    GeoJSON feature objects."""
-    verbosity = (ctx.obj and ctx.obj['verbosity']) or 2
-    logger = logging.getLogger('fio')
-    stdin = click.get_text_stream('stdin')
-
-    if src_crs and dst_crs:
-        transformer = partial(transform_geom, src_crs, dst_crs,
-                              antimeridian_cutting=True, precision=-1)
-    else:
-        transformer = lambda x: x
-
-    first_line = next(stdin)
-
-    # If input is RS-delimited JSON sequence.
-    if first_line.startswith(u'\x1e'):
-        def feature_gen():
-            buffer = first_line.strip(u'\x1e')
-            for line in stdin:
-                if line.startswith(u'\x1e'):
-                    if buffer:
-                        feat = json.loads(buffer)
-                        feat['geometry'] = transformer(feat['geometry'])
-                        yield feat
-                    buffer = line.strip(u'\x1e')
-                else:
-                    buffer += line
-            else:
-                feat = json.loads(buffer)
-                feat['geometry'] = transformer(feat['geometry'])
-                yield feat
-    elif sequence:
-        def feature_gen():
-            yield json.loads(first_line)
-            for line in stdin:
-                feat = json.loads(line)
-                feat['geometry'] = transformer(feat['geometry'])
-                yield feat
-    else:
-        def feature_gen():
-            text = "".join(itertools.chain([first_line], stdin))
-            for feat in json.loads(text)['features']:
-                feat['geometry'] = transformer(feat['geometry'])
-                yield feat
-
-    try:
-        source = feature_gen()
-
-        # Use schema of first feature as a template.
-        # TODO: schema specified on command line?
-        first = next(source)
-        schema = {'geometry': first['geometry']['type']}
-        schema['properties'] = dict([
-            (k, FIELD_TYPES_MAP_REV.get(type(v)) or 'str')
-            for k, v in first['properties'].items()])
-
-        with fiona.drivers(CPL_DEBUG=verbosity>2):
-            with fiona.open(
-                    output, 'w',
-                    driver=driver,
-                    crs=dst_crs,
-                    schema=schema) as dst:
-                dst.write(first)
-                dst.writerecords(source)
-        sys.exit(0)
-    except IOError:
-        logger.info("IOError caught")
-        sys.exit(0)
-    except Exception:
-        logger.exception("Failed. Exception caught")
-        sys.exit(1)
-
-
-if __name__ == '__main__':
-    cli()
diff --git a/fiona/fio/helpers.py b/fiona/fio/helpers.py
new file mode 100644
index 0000000..76f261b
--- /dev/null
+++ b/fiona/fio/helpers.py
@@ -0,0 +1,33 @@
+"""
+Helper objects needed by multiple CLI commands.
+"""
+
+
+import json
+import warnings
+
+
+warnings.simplefilter('default')
+
+
+def obj_gen(lines):
+    """Return a generator of JSON objects loaded from ``lines``."""
+    first_line = next(lines)
+    if first_line.startswith(u'\x1e'):
+        def gen():
+            buffer = first_line.strip(u'\x1e')
+            for line in lines:
+                if line.startswith(u'\x1e'):
+                    if buffer:
+                        yield json.loads(buffer)
+                    buffer = line.strip(u'\x1e')
+                else:
+                    buffer += line
+            else:
+                yield json.loads(buffer)
+    else:
+        def gen():
+            yield json.loads(first_line)
+            for line in lines:
+                yield json.loads(line)
+    return gen()
diff --git a/fiona/fio/info.py b/fiona/fio/info.py
new file mode 100644
index 0000000..96080f4
--- /dev/null
+++ b/fiona/fio/info.py
@@ -0,0 +1,99 @@
+"""
+Commands to get info about datasources and the Fiona environment
+"""
+
+
+import code
+import logging
+import json
+import sys
+
+import click
+from cligj import indent_opt
+
+import fiona
+import fiona.crs
+
+
+ at click.command(short_help="Print information about the fio environment.")
+ at click.option('--formats', 'key', flag_value='formats', default=True,
+              help="Enumerate the available formats.")
+ at click.pass_context
+def env(ctx, key):
+    """Print information about the Fiona environment: available
+    formats, etc.
+    """
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('fio')
+    stdout = click.get_text_stream('stdout')
+    with fiona.drivers(CPL_DEBUG=(verbosity > 2)) as env:
+        if key == 'formats':
+            for k, v in sorted(fiona.supported_drivers.items()):
+                modes = ', '.join("'" + m + "'" for m in v)
+                stdout.write("%s (modes %s)\n" % (k, modes))
+            stdout.write('\n')
+
+
+# Info command.
+ at click.command(short_help="Print information about a dataset.")
+# One or more files.
+ at click.argument('input', type=click.Path(exists=True))
+ at indent_opt
+# Options to pick out a single metadata item and print it as
+# a string.
+ at click.option('--count', 'meta_member', flag_value='count',
+              help="Print the count of features.")
+ at click.option('-f', '--format', '--driver', 'meta_member', flag_value='driver',
+              help="Print the format driver.")
+ at click.option('--crs', 'meta_member', flag_value='crs',
+              help="Print the CRS as a PROJ.4 string.")
+ at click.option('--bounds', 'meta_member', flag_value='bounds',
+              help="Print the boundary coordinates "
+                   "(left, bottom, right, top).")
+ at click.pass_context
+def info(ctx, input, indent, meta_member):
+    verbosity = (ctx.obj and ctx.obj['verbosity']) or 2
+    logger = logging.getLogger('rio')
+    try:
+        with fiona.drivers(CPL_DEBUG=verbosity>2):
+            with fiona.open(input) as src:
+                info = src.meta
+                info.update(bounds=src.bounds, count=len(src))
+                proj4 = fiona.crs.to_string(src.crs)
+                if proj4.startswith('+init=epsg'):
+                    proj4 = proj4.split('=')[1].upper()
+                info['crs'] = proj4
+                if meta_member:
+                    if isinstance(info[meta_member], (list, tuple)):
+                        click.echo(" ".join(map(str, info[meta_member])))
+                    else:
+                        click.echo(info[meta_member])
+                else:
+                    click.echo(json.dumps(info, indent=indent))
+
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
+
+
+# Insp command.
+ at click.command(short_help="Open a dataset and start an interpreter.")
+ at click.argument('src_path', type=click.Path(exists=True))
+ at click.pass_context
+def insp(ctx, src_path):
+    verbosity = (ctx.obj and ctx.obj['verbosity']) or 2
+    logger = logging.getLogger('fio')
+    try:
+        with fiona.drivers(CPL_DEBUG=verbosity>2):
+            with fiona.open(src_path) as src:
+                code.interact(
+                    'Fiona %s Interactive Inspector (Python %s)\n'
+                    'Type "src.schema", "next(src)", or "help(src)" '
+                    'for more information.' %  (
+                        fiona.__version__, '.'.join(
+                            map(str, sys.version_info[:3]))),
+                    local=locals())
+
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
diff --git a/fiona/fio/main.py b/fiona/fio/main.py
new file mode 100644
index 0000000..758ff42
--- /dev/null
+++ b/fiona/fio/main.py
@@ -0,0 +1,36 @@
+"""
+Main click group for the CLI.  Needs to be isolated for entry-point loading.
+"""
+
+
+import logging
+from pkg_resources import iter_entry_points
+import warnings
+import sys
+
+import click
+from cligj import verbose_opt, quiet_opt
+import cligj.plugins
+
+from fiona import __version__ as fio_version
+
+
+def configure_logging(verbosity):
+    log_level = max(10, 30 - 10*verbosity)
+    logging.basicConfig(stream=sys.stderr, level=log_level)
+
+
+ at cligj.plugins.group(plugins=(
+        ep for ep in list(iter_entry_points('fiona.fio_commands')) +
+                     list(iter_entry_points('fiona.fio_plugins'))))
+ at verbose_opt
+ at quiet_opt
+ at click.version_option(fio_version)
+ at click.pass_context
+def main_group(ctx, verbose, quiet):
+
+    """Fiona command line interface."""
+
+    verbosity = verbose - quiet
+    configure_logging(verbosity)
+    ctx.obj = {'verbosity': verbosity}
diff --git a/fiona/fio/options.py b/fiona/fio/options.py
new file mode 100644
index 0000000..47fb1ae
--- /dev/null
+++ b/fiona/fio/options.py
@@ -0,0 +1,8 @@
+"""Common commandline options for `fio`"""
+
+
+import click
+
+
+src_crs_opt = click.option('--src-crs', '--src_crs', help="Source CRS.")
+dst_crs_opt = click.option('--dst-crs', '--dst_crs', help="Destination CRS.")
diff --git a/fiona/inspector.py b/fiona/inspector.py
index e5e44c1..669f866 100644
--- a/fiona/inspector.py
+++ b/fiona/inspector.py
@@ -12,14 +12,15 @@ logger = logging.getLogger('fiona.inspector')
 
 def main(srcfile):
     
-    with fiona.drivers(), fiona.open(srcfile) as src:
+    with fiona.drivers():
+        with fiona.open(srcfile) as src:
             
-        code.interact(
-            'Fiona %s Interactive Inspector (Python %s)\n'
-            'Type "src.schema", "next(src)", or "help(src)" '
-            'for more information.' %  (
-                fiona.__version__, '.'.join(map(str, sys.version_info[:3]))),
-            local=locals())
+            code.interact(
+                'Fiona %s Interactive Inspector (Python %s)\n'
+                'Type "src.schema", "next(src)", or "help(src)" '
+                'for more information.' %  (
+                    fiona.__version__, '.'.join(map(str, sys.version_info[:3]))),
+                local=locals())
 
     return 1
 
diff --git a/fiona/ograpi.pxd b/fiona/ograpi.pxd
index de1abfe..c84ca7d 100644
--- a/fiona/ograpi.pxd
+++ b/fiona/ograpi.pxd
@@ -18,6 +18,16 @@ cdef extern from "cpl_string.h":
     char ** CSLSetNameValue (char **list, char *name, char *value)
     void    CSLDestroy (char **list)
 
+cdef extern from "cpl_vsi.h":
+    ctypedef struct VSILFILE:
+        pass
+    int VSIFCloseL (VSILFILE *)
+    VSILFILE * VSIFileFromMemBuffer (const char * filename,
+                                     unsigned char * data,
+                                     int data_len,
+                                     int take_ownership)
+    int VSIUnlink (const char * pathname)
+
 ctypedef int OGRErr
 ctypedef struct OGREnvelope:
     double MinX
diff --git a/fiona/ogrext.pyx b/fiona/ogrext.pyx
index fb125c4..3434553 100644
--- a/fiona/ogrext.pyx
+++ b/fiona/ogrext.pyx
@@ -7,6 +7,7 @@ import os
 import sys
 import warnings
 import math
+import uuid
 
 from six import integer_types, string_types, text_type
 
@@ -1165,3 +1166,22 @@ def _listlayers(path):
 
     return layer_names
 
+def buffer_to_virtual_file(bytesbuf):
+    """Maps a bytes buffer to a virtual file.
+    """
+    vsi_filename = os.path.join('/vsimem', uuid.uuid4().hex)
+    vsi_cfilename = vsi_filename if not isinstance(vsi_filename, string_types) else vsi_filename.encode('utf-8')
+
+    vsi_handle = ograpi.VSIFileFromMemBuffer(vsi_cfilename, bytesbuf, len(bytesbuf), 0)
+    if vsi_handle == NULL:
+        raise OSError('failed to map buffer to file')
+    if ograpi.VSIFCloseL(vsi_handle) != 0:
+        raise OSError('failed to close mapped file handle')
+
+    return vsi_filename
+
+def remove_virtual_file(vsi_filename):
+    vsi_cfilename = vsi_filename if not isinstance(vsi_filename, string_types) else vsi_filename.encode('utf-8')
+    return ograpi.VSIUnlink(vsi_cfilename)
+
+
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 2a82b05..3012d81 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,4 +1,4 @@
-cython==0.21.2
+cython>=0.21.2
 nose
 pytest
 setuptools
diff --git a/setup.py b/setup.py
index 02950da..0deb1fd 100644
--- a/setup.py
+++ b/setup.py
@@ -19,6 +19,26 @@ log = logging.getLogger()
 if 'all' in sys.warnoptions:
     log.level = logging.DEBUG
 
+def check_output(cmd):
+    # since subprocess.check_output doesn't exist in 2.6
+    # we wrap it here.
+    try:
+        out = subprocess.check_output(cmd)
+        return out.decode('utf')
+    except AttributeError:
+        # For some reasone check_output doesn't exist
+        # So fall back on Popen
+        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+        out, err = p.communicate()
+        return out
+
+def copy_data_tree(datadir, destdir):
+    try:
+        shutil.rmtree(destdir)
+    except OSError:
+        pass
+    shutil.copytree(datadir, destdir)
+
 # Parse the version from the fiona module.
 with open('fiona/__init__.py', 'r') as f:
     for line in f:
@@ -52,21 +72,17 @@ include_dirs = []
 library_dirs = []
 libraries = []
 extra_link_args = []
+gdal_output = [None]*3
 
 try:
     gdal_config = os.environ.get('GDAL_CONFIG', 'gdal-config')
-    with open("gdal-config.txt", "w") as gcfg:
-        subprocess.call([gdal_config, "--cflags"], stdout=gcfg)
-        subprocess.call([gdal_config, "--libs"], stdout=gcfg)
-        subprocess.call([gdal_config, "--datadir"], stdout=gcfg)
-    with open("gdal-config.txt", "r") as gcfg:
-        cflags = gcfg.readline().strip()
-        libs = gcfg.readline().strip()
-        datadir = gcfg.readline().strip()
-    for item in cflags.split():
+    for i, flag in enumerate(("--cflags", "--libs", "--datadir")):
+        gdal_output[i] = check_output([gdal_config, flag]).strip()
+
+    for item in gdal_output[0].split():
         if item.startswith("-I"):
             include_dirs.extend(item[2:].split(":"))
-    for item in libs.split():
+    for item in gdal_output[1].split():
         if item.startswith("-L"):
             library_dirs.extend(item[2:].split(":"))
         elif item.startswith("-l"):
@@ -75,6 +91,13 @@ try:
             # e.g. -framework GDAL
             extra_link_args.append(item)
 
+except Exception as e:
+    if os.name == "nt":
+        log.info(("Building on Windows requires extra options to setup.py to locate needed GDAL files.\n"
+                   "More information is available in the README."))
+    else:
+        log.warning("Failed to get options via gdal-config: %s", str(e))
+
     # Conditionally copy the GDAL data. To be used in conjunction with
     # the bdist_wheel command to make self-contained binary wheels.
     if os.environ.get('PACKAGE_DATA'):
@@ -83,19 +106,23 @@ try:
         except OSError:
             pass
         shutil.copytree(datadir, 'fiona/gdal_data')
-
-except Exception as e:
-    log.warning("Failed to get options via gdal-config: %s", str(e))
-
-# Conditionally copy PROJ.4 data.
 if os.environ.get('PACKAGE_DATA'):
+    destdir = 'fiona/gdal_data'
+    if gdal_output[2]:
+        log.info("Copying gdal data from %s" % gdal_output[2])
+        copy_data_tree(gdal_output[2], destdir)
+    else:
+        # check to see if GDAL_DATA is defined
+        gdal_data = os.environ.get('GDAL_DATA', None)
+        if gdal_data:
+            log.info("Copying gdal data from %s" % gdal_data)
+            copy_data_tree(gdal_data, destdir)
+
+    # Conditionally copy PROJ.4 data. 
     projdatadir = os.environ.get('PROJ_LIB', '/usr/local/share/proj')
     if os.path.exists(projdatadir):
-        try:
-            shutil.rmtree('fiona/proj_data')
-        except OSError:
-            pass
-        shutil.copytree(projdatadir, 'fiona/proj_data')
+        log.info("Copying proj data from %s" % projdatadir)
+        copy_data_tree(projdatadir, 'finoa/proj_data')
 
 ext_options = dict(
     include_dirs=include_dirs,
@@ -150,7 +177,18 @@ setup_args = dict(
     packages=['fiona', 'fiona.fio'],
     entry_points='''
         [console_scripts]
-        fio=fiona.fio.fio:cli
+        fio=fiona.fio.main:main_group
+
+        [fiona.fio_commands]
+        bounds=fiona.fio.bounds:bounds
+        cat=fiona.fio.cat:cat
+        collect=fiona.fio.cat:collect
+        distrib=fiona.fio.cat:distrib
+        dump=fiona.fio.cat:dump
+        env=fiona.fio.info:env
+        info=fiona.fio.info:info
+        insp=fiona.fio.info:insp
+        load=fiona.fio.cat:load
         ''',
     install_requires=requirements,
     tests_require=['nose'],
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 1a38cab..2b6722d 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -4,7 +4,14 @@ import os.path
 def read_file(name):
     return open(os.path.join(os.path.dirname(__file__), name)).read()
 
+# GeoJSON feature collection on a single line
 feature_collection = read_file('data/collection.txt')
+
+# Same as above but with pretty-print styling applied
 feature_collection_pp = read_file('data/collection-pp.txt')
+
+# One feature per line
 feature_seq = read_file('data/sequence.txt')
+
+# Same as above but each feature has pretty-print styling
 feature_seq_pp_rs = read_file('data/sequence-pp.txt')
diff --git a/tests/test_bytescollection.py b/tests/test_bytescollection.py
new file mode 100644
index 0000000..e9eaccf
--- /dev/null
+++ b/tests/test_bytescollection.py
@@ -0,0 +1,212 @@
+# Testing BytesCollection
+import unittest
+
+import six
+
+import fiona
+
+class ReadingTest(unittest.TestCase):
+
+    def setUp(self):
+        with open('tests/data/coutwildrnp.json') as src:
+            bytesbuf = src.read().encode('utf-8')
+        self.c = fiona.BytesCollection(bytesbuf)
+
+    def tearDown(self):
+        self.c.close()
+
+    @unittest.skipIf(six.PY2, 'string are bytes in Python 2')
+    def test_construct_with_str(self):
+        with open('tests/data/coutwildrnp.json') as src:
+            strbuf = src.read()
+        self.assertRaises(ValueError, fiona.BytesCollection, strbuf)
+
+    def test_open_repr(self):
+        # I'm skipping checking the name of the virtual file as it produced by uuid.
+        self.failUnless(
+            repr(self.c).startswith("<open BytesCollection '/vsimem/") and
+            repr(self.c).endswith(":OGRGeoJSON', mode 'r' at %s>" % hex(id(self.c))))
+
+    def test_closed_repr(self):
+        # I'm skipping checking the name of the virtual file as it produced by uuid.
+        self.c.close()
+        self.failUnless(
+            repr(self.c).startswith("<closed BytesCollection '/vsimem/") and
+            repr(self.c).endswith(":OGRGeoJSON', mode 'r' at %s>" % hex(id(self.c))))
+
+    def test_path(self):
+        self.failUnlessEqual(self.c.path, self.c.virtual_file)
+
+    def test_closed_virtual_file(self):
+        self.c.close()
+        self.failUnless(self.c.virtual_file is None)
+
+    def test_closed_buf(self):
+        self.c.close()
+        self.failUnless(self.c.bytesbuf is None)
+
+    def test_name(self):
+        self.failUnlessEqual(self.c.name, 'OGRGeoJSON')
+
+    def test_mode(self):
+        self.failUnlessEqual(self.c.mode, 'r')
+
+    def test_collection(self):
+        self.failUnlessEqual(self.c.encoding, 'utf-8')
+
+    def test_iter(self):
+        self.failUnless(iter(self.c))
+
+    def test_closed_no_iter(self):
+        self.c.close()
+        self.assertRaises(ValueError, iter, self.c)
+
+    def test_len(self):
+        self.failUnlessEqual(len(self.c), 67)
+
+    def test_closed_len(self):
+        # Len is lazy, it's never computed in this case. TODO?
+        self.c.close()
+        self.failUnlessEqual(len(self.c), 0)
+
+    def test_len_closed_len(self):
+        # Lazy len is computed in this case and sticks.
+        len(self.c)
+        self.c.close()
+        self.failUnlessEqual(len(self.c), 67)
+
+    def test_driver(self):
+        self.failUnlessEqual(self.c.driver, "GeoJSON")
+
+    def test_closed_driver(self):
+        self.c.close()
+        self.failUnlessEqual(self.c.driver, None)
+
+    def test_driver_closed_driver(self):
+        self.c.driver
+        self.c.close()
+        self.failUnlessEqual(self.c.driver, "GeoJSON")
+
+    def test_schema(self):
+        s = self.c.schema['properties']
+        self.failUnlessEqual(s['PERIMETER'], "float")
+        self.failUnlessEqual(s['NAME'], "str")
+        self.failUnlessEqual(s['URL'], "str")
+        self.failUnlessEqual(s['STATE_FIPS'], "str")
+        self.failUnlessEqual(s['WILDRNP020'], "int")
+
+    def test_closed_schema(self):
+        # Schema is lazy too, never computed in this case. TODO?
+        self.c.close()
+        self.failUnlessEqual(self.c.schema, None)
+
+    def test_schema_closed_schema(self):
+        self.c.schema
+        self.c.close()
+        self.failUnlessEqual(
+            sorted(self.c.schema.keys()),
+            ['geometry', 'properties'])
+
+    def test_crs(self):
+        crs = self.c.crs
+        self.failUnlessEqual(crs['init'], 'epsg:4326')
+
+    def test_crs_wkt(self):
+        crs = self.c.crs_wkt
+        self.failUnless(crs.startswith('GEOGCS["WGS 84"'))
+
+    def test_closed_crs(self):
+        # Crs is lazy too, never computed in this case. TODO?
+        self.c.close()
+        self.failUnlessEqual(self.c.crs, None)
+
+    def test_crs_closed_crs(self):
+        self.c.crs
+        self.c.close()
+        self.failUnlessEqual(
+            sorted(self.c.crs.keys()),
+            ['init'])
+
+    def test_meta(self):
+        self.failUnlessEqual(
+            sorted(self.c.meta.keys()),
+            ['crs', 'driver', 'schema'])
+
+    def test_bounds(self):
+        self.failUnlessAlmostEqual(self.c.bounds[0], -113.564247, 6)
+        self.failUnlessAlmostEqual(self.c.bounds[1], 37.068981, 6)
+        self.failUnlessAlmostEqual(self.c.bounds[2], -104.970871, 6)
+        self.failUnlessAlmostEqual(self.c.bounds[3], 41.996277, 6)
+
+    def test_iter_one(self):
+        itr = iter(self.c)
+        f = next(itr)
+        self.failUnlessEqual(f['id'], "0")
+        self.failUnlessEqual(f['properties']['STATE'], 'UT')
+
+    def test_iter_list(self):
+        f = list(self.c)[0]
+        self.failUnlessEqual(f['id'], "0")
+        self.failUnlessEqual(f['properties']['STATE'], 'UT')
+
+    def test_re_iter_list(self):
+        f = list(self.c)[0] # Run through iterator
+        f = list(self.c)[0] # Run through a new, reset iterator
+        self.failUnlessEqual(f['id'], "0")
+        self.failUnlessEqual(f['properties']['STATE'], 'UT')
+
+    def test_getitem_one(self):
+        f = self.c[0]
+        self.failUnlessEqual(f['id'], "0")
+        self.failUnlessEqual(f['properties']['STATE'], 'UT')
+
+    def test_no_write(self):
+        self.assertRaises(IOError, self.c.write, {})
+
+    def test_iter_items_list(self):
+        i, f = list(self.c.items())[0]
+        self.failUnlessEqual(i, 0)
+        self.failUnlessEqual(f['id'], "0")
+        self.failUnlessEqual(f['properties']['STATE'], 'UT')
+
+    def test_iter_keys_list(self):
+        i = list(self.c.keys())[0]
+        self.failUnlessEqual(i, 0)
+
+    def test_in_keys(self):
+        self.failUnless(0 in self.c.keys())
+        self.failUnless(0 in self.c)
+
+class FilterReadingTest(unittest.TestCase):
+
+    def setUp(self):
+        with open('tests/data/coutwildrnp.json') as src:
+            bytesbuf = src.read().encode('utf-8')
+        self.c = fiona.BytesCollection(bytesbuf)
+
+    def tearDown(self):
+        self.c.close()
+
+    def test_filter_1(self):
+        results = list(self.c.filter(bbox=(-120.0, 30.0, -100.0, 50.0)))
+        self.failUnlessEqual(len(results), 67)
+        f = results[0]
+        self.failUnlessEqual(f['id'], "0")
+        self.failUnlessEqual(f['properties']['STATE'], 'UT')
+
+    def test_filter_reset(self):
+        results = list(self.c.filter(bbox=(-112.0, 38.0, -106.0, 40.0)))
+        self.failUnlessEqual(len(results), 26)
+        results = list(self.c.filter())
+        self.failUnlessEqual(len(results), 67)
+
+    def test_filter_mask(self):
+        mask = {
+            'type': 'Polygon',
+            'coordinates': (
+                ((-112, 38), (-112, 40), (-106, 40), (-106, 38), (-112, 38)),)}
+        results = list(self.c.filter(mask=mask))
+        self.failUnlessEqual(len(results), 26)
+
+
+
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 0a9bfb1..ab659f1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,10 +1,9 @@
-import json
+from pkg_resources import iter_entry_points
 import re
 
-import click
 from click.testing import CliRunner
 
-from fiona.fio import fio
+from fiona.fio.main import main_group
 
 
 WILDSHP = 'tests/data/coutwildrnp.shp'
@@ -12,7 +11,7 @@ WILDSHP = 'tests/data/coutwildrnp.shp'
 
 def test_info_json():
     runner = CliRunner()
-    result = runner.invoke(fio.info, [WILDSHP])
+    result = runner.invoke(main_group, ['info', WILDSHP])
     assert result.exit_code == 0
     assert '"count": 67' in result.output
     assert '"crs": "EPSG:4326"' in result.output
@@ -21,13 +20,19 @@ def test_info_json():
 
 def test_info_count():
     runner = CliRunner()
-    result = runner.invoke(fio.info, ['--count', WILDSHP])
+    result = runner.invoke(main_group, ['info', '--count', WILDSHP])
     assert result.exit_code == 0
     assert result.output == "67\n"
 
 
 def test_info_bounds():
     runner = CliRunner()
-    result = runner.invoke(fio.info, ['--bounds', WILDSHP])
+    result = runner.invoke(main_group, ['info', '--bounds', WILDSHP])
     assert result.exit_code == 0
     assert len(re.findall(r'\d*\.\d*', result.output)) == 4
+
+
+def test_all_registered():
+    # Make sure all the subcommands are actually registered to the main CLI group
+    for ep in iter_entry_points('fiona.fio_commands'):
+        assert ep.name in main_group.commands
diff --git a/tests/test_fio_cat.py b/tests/test_fio_cat.py
index 7ba5b4e..94f5d1f 100644
--- a/tests/test_fio_cat.py
+++ b/tests/test_fio_cat.py
@@ -60,7 +60,7 @@ def test_collect_rs():
     runner = CliRunner()
     result = runner.invoke(
         cat.collect,
-        ['--src_crs', 'EPSG:3857'],
+        ['--src-crs', 'EPSG:3857'],
         feature_seq_pp_rs,
         catch_exceptions=False)
     assert result.exit_code == 0
@@ -71,7 +71,7 @@ def test_collect_no_rs():
     runner = CliRunner()
     result = runner.invoke(
         cat.collect,
-        ['--src_crs', 'EPSG:3857'],
+        ['--src-crs', 'EPSG:3857'],
         feature_seq,
         catch_exceptions=False)
     assert result.exit_code == 0
diff --git a/tests/test_fio_load.py b/tests/test_fio_load.py
index 5702576..cfe254f 100644
--- a/tests/test_fio_load.py
+++ b/tests/test_fio_load.py
@@ -1,21 +1,19 @@
-import json
 import os
 import tempfile
 
-import click
 from click.testing import CliRunner
 
 import fiona
-from fiona.fio import fio
+from fiona.fio.main import main_group
 
 from .fixtures import (
-    feature_collection, feature_collection_pp, feature_seq, feature_seq_pp_rs)
+    feature_collection, feature_seq, feature_seq_pp_rs)
 
 
 def test_err():
     runner = CliRunner()
     result = runner.invoke(
-        fio.load, [], '', catch_exceptions=False)
+        main_group, ['load'], '', catch_exceptions=False)
     assert result.exit_code == 2
 
 
@@ -27,7 +25,7 @@ def test_exception(tmpdir=None):
         tmpfile = str(tmpdir.join('test.shp'))
     runner = CliRunner()
     result = runner.invoke(
-        fio.load, ['-f', 'Shapefile', tmpfile], '42', catch_exceptions=False)
+        main_group, ['load', '-f', 'Shapefile', tmpfile], '42', catch_exceptions=False)
     assert result.exit_code == 1
 
 
@@ -39,7 +37,7 @@ def test_collection(tmpdir=None):
         tmpfile = str(tmpdir.join('test.shp'))
     runner = CliRunner()
     result = runner.invoke(
-        fio.load, ['-f', 'Shapefile', tmpfile], feature_collection)
+        main_group, ['load', '-f', 'Shapefile', tmpfile], feature_collection)
     assert result.exit_code == 0
     assert len(fiona.open(tmpfile)) == 2
 
@@ -52,7 +50,7 @@ def test_seq_rs(tmpdir=None):
         tmpfile = str(tmpdir.join('test.shp'))
     runner = CliRunner()
     result = runner.invoke(
-        fio.load, ['-f', 'Shapefile', tmpfile], feature_seq_pp_rs)
+        main_group, ['load', '-f', 'Shapefile', tmpfile], feature_seq_pp_rs)
     assert result.exit_code == 0
     assert len(fiona.open(tmpfile)) == 2
 
@@ -65,6 +63,59 @@ def test_seq_no_rs(tmpdir=None):
         tmpfile = str(tmpdir.join('test.shp'))
     runner = CliRunner()
     result = runner.invoke(
-        fio.load, ['-f', 'Shapefile', '--sequence', tmpfile], feature_seq)
+        main_group, ['load', '-f', 'Shapefile', '--sequence', tmpfile], feature_seq)
     assert result.exit_code == 0
     assert len(fiona.open(tmpfile)) == 2
+
+
+def test_dst_crs_default_to_src_crs(tmpdir=None):
+    # When --dst-crs is not given default to --src-crs.
+    if tmpdir is None:
+        tmpdir = tempfile.mkdtemp()
+        tmpfile = os.path.join(tmpdir, 'test.shp')
+    else:
+        tmpfile = str(tmpdir.join('test.shp'))
+    runner = CliRunner()
+    result = runner.invoke(
+        main_group, [
+            'load', '--src-crs', 'EPSG:32617', '-f', 'Shapefile', '--sequence', tmpfile
+        ], feature_seq)
+    assert result.exit_code == 0
+    with fiona.open(tmpfile) as src:
+        assert src.crs == {'init': 'epsg:32617'}
+        assert len(src) == len(feature_seq.splitlines())
+
+
+def test_different_crs(tmpdir=None):
+    if tmpdir is None:
+        tmpdir = tempfile.mkdtemp()
+        tmpfile = os.path.join(tmpdir, 'test.shp')
+    else:
+        tmpfile = str(tmpdir.join('test.shp'))
+    runner = CliRunner()
+    result = runner.invoke(
+        main_group, [
+            'load', '--src-crs', 'EPSG:32617', '--dst-crs', 'EPSG:32610',
+            '-f', 'Shapefile', '--sequence', tmpfile
+        ], feature_seq)
+    assert result.exit_code == 0
+    with fiona.open(tmpfile) as src:
+        assert src.crs == {'init': 'epsg:32610'}
+        assert len(src) == len(feature_seq.splitlines())
+
+
+def test_dst_crs_no_src(tmpdir=None):
+    if tmpdir is None:
+        tmpdir = tempfile.mkdtemp()
+        tmpfile = os.path.join(tmpdir, 'test.shp')
+    else:
+        tmpfile = str(tmpdir.join('test.shp'))
+    runner = CliRunner()
+    result = runner.invoke(
+        main_group, [
+            'load', '--dst-crs', 'EPSG:32610', '-f', 'Shapefile', '--sequence', tmpfile
+        ], feature_seq)
+    assert result.exit_code == 0
+    with fiona.open(tmpfile) as src:
+        assert src.crs == {'init': 'epsg:32610'}
+        assert len(src) == len(feature_seq.splitlines())

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/fiona.git



More information about the Pkg-grass-devel mailing list