[rasterio] 01/07: Imported Upstream version 0.36.0

Bas Couwenberg sebastic at debian.org
Wed Jun 15 20:48:35 UTC 2016


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

sebastic pushed a commit to branch master
in repository rasterio.

commit bf74bbb9ebedd4f8a53d0652008472c87d240a8b
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Wed Jun 15 19:18:52 2016 +0200

    Imported Upstream version 0.36.0
---
 .gitignore                        |    1 +
 .travis.yml                       |    5 +
 AUTHORS.txt                       |   47 +-
 CHANGES.txt                       |   49 +
 CITATION.txt                      |   10 +
 CONTRIBUTING.rst                  |  212 ++++
 README.rst                        |  118 ++-
 docs/Makefile                     |    6 +-
 docs/_build/html/.nojekyll        |    1 +
 docs/cli.rst                      |    7 +-
 docs/color.rst                    |    2 +-
 docs/concurrency.rst              |   10 +-
 docs/configuration.rst            |    2 +-
 docs/cookbook.rst                 |    4 +-
 docs/features.rst                 |    4 +-
 docs/georeferencing.rst           |   12 +-
 docs/image_options.rst            |    4 +-
 docs/image_processing.rst         |   24 +-
 docs/installation.rst             |    8 +-
 docs/masks.rst                    |    6 +-
 docs/osgeo_gdal_migration.rst     |    4 +-
 docs/reading.rst                  |    4 +-
 docs/recipes/reproject.py         |    2 +-
 docs/reproject.rst                |   18 +-
 docs/tags.rst                     |    2 +-
 docs/windowed-rw.rst              |    4 +-
 docs/working_with_datasets.rst    |   12 +-
 docs/writing.rst                  |   24 +-
 examples/Data visualization.ipynb | 2083 +++++++++++++++++++++++++++++++++++++
 examples/async-rasterio.py        |   22 +-
 examples/concurrent-cpu-bound.py  |   11 +-
 examples/decimate.py              |    2 +-
 examples/rasterio_polygonize.py   |   26 +-
 examples/rasterize_geometry.py    |    8 +-
 examples/reproject.py             |   20 +-
 examples/sieve.py                 |   13 +-
 examples/total.py                 |    7 +-
 pep-508-install                   |   34 +
 pyproject.toml                    |    3 +
 rasterio/__init__.py              |   10 +-
 rasterio/_base.pxd                |    2 +-
 rasterio/_base.pyx                |  178 ++--
 rasterio/_crs.pxd                 |    0
 rasterio/_crs.pyx                 |   64 ++
 rasterio/_drivers.pyx             |    2 +-
 rasterio/_err.pyx                 |   10 +-
 rasterio/_example.pyx             |    6 +-
 rasterio/_io.pyx                  |  128 ++-
 rasterio/_warp.pyx                |   47 +-
 rasterio/{five.py => compat.py}   |    2 +
 rasterio/crs.py                   |  211 ++--
 rasterio/dtypes.py                |   22 +-
 rasterio/env.py                   |   11 +-
 rasterio/plot.py                  |  260 +++--
 rasterio/rio/bounds.py            |    3 +-
 rasterio/rio/calc.py              |    3 +-
 rasterio/rio/clip.py              |    3 +-
 rasterio/rio/convert.py           |    3 +-
 rasterio/rio/edit_info.py         |   14 +-
 rasterio/rio/env.py               |    4 +-
 rasterio/rio/helpers.py           |    2 +-
 rasterio/rio/info.py              |   10 +-
 rasterio/rio/insp.py              |   20 +-
 rasterio/rio/main.py              |   41 +-
 rasterio/rio/mask.py              |    5 +-
 rasterio/rio/merge.py             |   16 +-
 rasterio/rio/options.py           |    7 +-
 rasterio/rio/overview.py          |    3 +-
 rasterio/rio/rasterize.py         |   31 +-
 rasterio/rio/sample.py            |    3 +-
 rasterio/rio/shapes.py            |   18 +-
 rasterio/rio/stack.py             |    6 +-
 rasterio/rio/transform.py         |    3 +-
 rasterio/rio/warp.py              |   46 +-
 rasterio/vfs.py                   |    6 +-
 rasterio/warp.py                  |   48 +-
 requirements-dev.txt              |    3 +-
 setup.py                          |   33 +-
 tests/__init__.py                 |    1 -
 tests/conftest.py                 |   28 +-
 tests/test_blocks.py              |   18 +-
 tests/test_colorinterp.py         |    3 +-
 tests/test_copy.py                |    8 +-
 tests/test_crs.py                 |  124 ++-
 tests/test_dataset_mask.py        |  182 ++++
 tests/test_deprecations.py        |   28 +-
 tests/test_driver_management.py   |   14 +-
 tests/test_dtypes.py              |   23 +-
 tests/test_env.py                 |   70 +-
 tests/test_err.py                 |    6 +
 tests/test_features.py            |  314 +++---
 tests/test_fillnodata.py          |   16 +-
 tests/test_image_structure.py     |    2 +-
 tests/test_indexing.py            |   12 +-
 tests/test_mask_creation.py       |   31 +-
 tests/test_pad.py                 |    4 +-
 tests/test_plot.py                |  264 +++++
 tests/test_png.py                 |    8 +-
 tests/test_read.py                |  122 ++-
 tests/test_read_boundless.py      |   12 +-
 tests/test_read_resample.py       |   12 +-
 tests/test_reshape_image.py       |   26 +
 tests/test_rio_convert.py         |    4 +-
 tests/test_rio_features.py        |   72 +-
 tests/test_rio_info.py            |   14 +-
 tests/test_rio_merge.py           |   56 +-
 tests/test_rio_options.py         |    4 +-
 tests/test_rio_warp.py            |   96 +-
 tests/test_tool.py                |   16 +-
 tests/test_update.py              |    4 +-
 tests/test_warp.py                |  300 ++++--
 tests/test_warp_transform.py      |   19 +-
 tests/test_write.py               |   58 +-
 113 files changed, 4891 insertions(+), 1195 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3be80b2..833ceed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,6 +75,7 @@ rasterio/_err.c
 rasterio/_example.c
 rasterio/_features.c
 rasterio/_io.c
+rasterio/_crs.c
 
 # vim
 .*.swp
diff --git a/.travis.yml b/.travis.yml
index 30d0c12..4db3f70 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -34,6 +34,10 @@ python:
   - "2.7"
   - "3.4"
   - "3.5"
+before_script: # configure a headless display to test matplotlib
+  - "export DISPLAY=:99.0"
+  - "sh -e /etc/init.d/xvfb start"
+  - "sleep 3"
 before_install:
   - pip install -U pip
   - pip install wheel
@@ -47,6 +51,7 @@ install:
   - "pip install --upgrade --force-reinstall --global-option=build_ext --global-option='-I$GDALINST/gdal-$GDALVERSION/include' --global-option='-L$GDALINST/gdal-$GDALVERSION/lib' --global-option='-R$GDALINST/gdal-$GDALVERSION/lib' -e ."
   - "pip install coveralls>=1.1"
   - "pip install -e .[test]"
+  - "pip install -e .[plot]"
   - "rio --version"
   - "rio --gdal-version"
 script:
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 5eaf700..efcdf27 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -1,36 +1,41 @@
 Authors
 -------
 
-* Aldo Culquicondor <alculquicondor at gmail.com>
-* Alessandro Amici <alexamici at gmail.com>
-* Alexander <spatial.hast at gmail.com>
-* Amit Kapadia <amit at planet.com>
-* AsgerPetersen <asgerpetersen at gmail.com>
-* Bas Couwenberg <sebastic at xs4all.nl>
+* Sean Gillies <sean at mapbox.com>
+* Matthew Perry <perrygeo at gmail.com>
 * Brendan Ward <bcward at consbio.org>
+* Colin Talbert <talbertc at usgs.gov>
 * Erik Seglem <erik.seglem at gmail.com>
-* Etienne B. Racine <etiennebr at gmail.com>
-* Even Rouault <even.rouault at spatialys.com>
-* Jacques Tardie <hi at jacquestardie.org>
 * James McBride <jmcbride at berkeley.edu>
-* James Seppi <james.seppi at gmail.com>
-* Jeffrey Gerard <jgerard at climate.com>
-* Johan Van de Wauw <johan.vandewauw at gmail.com>
-* Joshua Arnott <josh at snorfalorpagus.net>
-* Kelsey Jordahl <kjordahl at alum.mit.edu>
 * Kevin Wurster <wursterk at gmail.com>
-* Martijn Visser <mgvisser at gmail.com>
-* Matt Savoie <github at flamingbear.com>
-* Matthew Perry <perrygeo at gmail.com>
+* James Hiebert <hiebert at uvic.ca>
+* Amit Kapadia <amit at planet.com>
+* Kelsey Jordahl <kjordahl at alum.mit.edu>
+* dnomadb <damon at mapbox.com>
+* Even Rouault <even.rouault at spatialys.com>
+* Ryan Grout <rgrout at continuum.io>
 * Maxim Dubinin <sim at gis-lab.info>
+* Joshua Arnott <josh at snorfalorpagus.net>
 * Mike Toews <mwtoews at gmail.com>
+* Johan Van de Wauw <johan.vandewauw at gmail.com>
+* Alessandro Amici <alexamici at gmail.com>
+* mwtoews <mwtoews at gmail.com>
+* Robin Wilson <robin at rtwilson.com>
+* Jacques Tardie <hi at jacquestardie.org>
+* Jeffrey Gerard <jgerard at climate.com>
 * Nat Wilson <njwilson23 at gmail.com>
+* Martijn Visser <mgvisser at gmail.com>
+* grovduck <matt.gregory at oregonstate.edu>
 * Patrick Young <patrick.young at digitalglobe.com>
-* Robin Wilson <robin at rtwilson.com>
-* Ryan Grout <rgrout at continuum.io>
-* Sean Gillies <sean at mapbox.com>
+* Aldo Culquicondor <alculquicondor at gmail.com>
+* Etienne B. Racine <etiennebr at gmail.com>
+* Charlie Loyd <charlie at mapbox.com>
+* Talbert <talbertc at usgs.gov>
 * Trevor R.H. Clarke <tclarke at ball.com>
+* Alexander <spatial.hast at gmail.com>
 * cgohlke <cgohlke at uci.edu>
-* Rob Emmanuele
+* Bas Couwenberg <sebastic at xs4all.nl>
+* Matt Savoie <github at flamingbear.com>
+* James Seppi <james.seppi at gmail.com>
 
 See also https://github.com/mapbox/rasterio/graphs/contributors.
diff --git a/CHANGES.txt b/CHANGES.txt
index 8df10f8..3eb3cc9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,8 +1,57 @@
 Changes
 =======
 
+0.36.0 (2016-06-14)
+-------------------
+
+Bug fixes:
+
+- rio-merge now copies colormaps to output files (#774, #778).
+- The correct `--force-overwrite` and `--output` usage is now provided in the
+  case of a `FileOverwriteError` (#750).
+- Passing undefined CRS to `reproject` no longer causes a segfault (#749).
+- GDAL's invert projection check is always used by default in reprojecting
+  (#780).
+- Forward slashes are always used for GDAL VSI paths (`/vsizip/` etc) instead
+  of `os.path.sep` (#754, #789).
+
+Documentation:
+
+- Contributing guidelines have been added (#701).
+- Axis order has been corrected in image processing doc (#700).
+- A framework for comprehensive documentaton has been created (#713, #723,
+  #729, #737, #738, #739, #740, #748, #756, #760).
+
+New features:
+
+- `--src-nodata` and `--dst-nodata` options for rio-warp (#746).
+- `read()` and `read_masks()` take an `out_shape` argument for decimated reads
+  (#761).
+- Color interpretation of bands added to rio-info output (#766).
+- Dataset objects have a new per-dataset mask property: `dataset_mask` (#716).
+- Utility functions for rehaping and plotting arrays have been added to
+  `rasterio.plot` (#718, #765).
+- New `CRS` class like our old crs dicts, but with methods attached (#736,
+  #770).
+
+Packaging:
+
+- setup.py has new install extras: '[plot]', and '[all]' (#744).
+
+Refactoring:
+
+- We've standardized on `import numpy as np` (#727, #738, #740) throughout the
+  project.
+- The `five` module has been renamed to `compat` (#745).
+
+Testing:
+
+- More coverage, more xfailing tests to mark known bugs (#742, #762, #753,
+  #773, #782).
+
 0.35.1 (2016-05-06)
 -------------------
+
 - Bug fix: restore support for URI-like GDAL dataset names such as
   'NETCDF:foo:bar' (#695).
 - Bug fix: ensure GDAL environment is initialized for `transform_bounds()` as
diff --git a/CITATION.txt b/CITATION.txt
new file mode 100644
index 0000000..fe4cb63
--- /dev/null
+++ b/CITATION.txt
@@ -0,0 +1,10 @@
+If you use Rasterio for any published work, please cite it using the reference
+below:
+
+ at Misc{,
+  author =    {Sean Gillies and others},
+  organization = {Mapbox},
+  title =     {Rasterio: geospatial raster I/O for {Python} programmers},
+  year =      {2013--},
+  url = "https://github.com/mapbox/rasterio"
+}
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..a07c85f
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,212 @@
+Welcome to the Rasterio project. Here's how we work.
+
+Code of Conduct
+===============
+
+First of all: the Rasterio project has a code of conduct. Please read the
+CODE_OF_CONDUCT.txt file, it's important to all of us.
+
+Rights
+======
+
+The BSD license (see LICENSE.txt) applies to all contributions.
+
+Issue Conventions
+=================
+
+Rasterio is a relatively new project and highly active. We have bugs, both
+known and unknown.
+
+Please search existing issues, open and closed, before creating a new one.
+
+Rasterio employs C extension modules, so bug reports very often hinge on the
+following details:
+
+- Operating system type and version (Windows? Ubuntu 12.04? 14.04?)
+- The version and source of Rasterio (PyPI, Anaconda, or somewhere else?)
+- The version and source of GDAL (UbuntuGIS? Homebrew?)
+
+Please provide these details as well as tracebacks and relevant logs.  When
+using the ``$ rio`` CLI logging can be enabled with ``$ rio -v`` and verbosity
+can be increased with ``-vvv``.  Short scripts and datasets demonstrating the
+issue are especially helpful!
+
+Rasterio is not at 1.0 yet and issues proposing new features are welcome.
+
+Design Principles
+=================
+
+Rasterio's API is different from GDAL's API and this is intentional.
+
+- Rasterio is a library for reading and writing raster datasets. Rasterio uses
+  GDAL but is not a "Python binding for GDAL."
+- Rasterio always prefers Python's built-in protocols and types or Numpy
+  protocols and types over concepts from GDAL's data model.
+- Rasterio keeps I/O separate from other operations. ``rasterio.open()`` is
+  the only library function that operates on filenames and URIs.
+  ``dataset.read()``, ``dataset.write()``, and their mask counterparts are
+  the methods that perform I/O.
+- Rasterio methods and functions should be free of side-effects and hidden
+  inputs. This is challenging in practice because GDAL embraces global
+  variables.
+
+Git Conventions
+===============
+
+We use a variant of centralized workflow described in the `Git Book
+<https://git-scm.com/book/en/v2/Distributed-Git-Distributed-Workflows>`__.  We
+have no 1.0 release for Rasterio yet and we are tagging and releasing from the
+master branch. Our post-1.0 workflow is to be decided.
+
+Work on features in a new branch of the mapbox/rasterio repo or in a branch on
+a fork. Create a `GitHub pull request
+<https://help.github.com/articles/using-pull-requests/>`__ when the changes are
+ready for review.  We recommend creating a pull request as early as possible
+to give other developers a heads up and to provide an opportunity for valuable
+early feedback.
+
+Code Conventions
+================
+
+The ``rasterio`` namespace contains both Python and C extension modules. All
+C extension modules are written using `Cython <http://cython.org/>`__. The
+Cython language is a superset of Python. Cython files end with ``.pyx`` and
+``.pxd`` and are where we keep all the code that calls GDAL's C functions.
+
+Rasterio supports Python 2 and Python 3 in the same code base, which is
+aided by an internal compatibility module named ``compat.py``. It functions
+similarly to the more widely known `six <https://pythonhosted.org/six/>`__ but
+we only use a small portion of the features so it eliminates a dependency.
+
+We strongly prefer code adhering to `PEP8
+<https://www.python.org/dev/peps/pep-0008/>`__.
+
+Tests are mandatory for new features. We use `pytest <https://pytest.org>`__.
+
+We aspire to 100% coverage for Python modules but overage of the Cython code is
+a future aspiration (`#515 <https://github.com/mapbox/rasterio/issues/515>`__).
+
+Development Environment
+=======================
+
+Developing Rasterio requires Python 2.7 or any final release after and
+including 3.4.  We prefer developing with the most recent version of Python
+but recognize this is not possible for all contributors.  A C compiler is also
+required to leverage `existing protocols
+<https://docs.python.org/3.5/extending/extending.html>`__ for extending Python
+with C or C++.  See the Windows install instructions in the `readme
+<README.rst>`__ for more information about building on Windows.
+
+Initial Setup
+-------------
+
+First, clone Rasterio's ``git`` repo:
+
+.. code-block:: console
+
+    $ git clone https://github.com/mapbox/rasterio
+
+Development should occur within a `virtual environment
+<http://docs.python-guide.org/en/latest/dev/virtualenvs/>`__ to better isolate
+development work from custom environments.
+
+In some cases installing a library with an accompanying executable inside a
+virtual environment causes the shell to initially look outside the environment
+for the executable.  If this occurs try deactivating and reactivating the
+environment.
+
+Installing GDAL
+---------------
+
+The GDAL library and its headers are required to build Rasterio. We do not
+have currently have guidance for any platforms other than Linux and OS X.
+
+On Linux, GDAL and its headers should be available through your distro's
+package manager. For Ubuntu the commands are:
+
+.. code-block:: console
+
+    $ sudo add-apt-repository ppa:ubuntugis/ppa
+    $ sudo apt-get update
+    $ sudo apt-get install libgdal1h gdal-bin libgdal-dev
+
+On OS X, Homebrew is a reliable way to get GDAL.
+
+.. code-block:: console
+
+    $ brew install gdal
+
+Python build requirements
+-------------------------
+
+Provision a virtualenv with Rasterio's build requirements.  Rasterio's
+``setup.py`` script will not run unless Cython and Numpy are installed, so do
+this first from the Rasterio repo directory.
+
+Linux users may need to install some additional Numpy dependencies:
+
+.. code-block:: console
+
+    $ sudo apt-get install libatlas-dev libatlas-base-dev gfortran
+
+then:
+
+.. code-block:: console
+
+    $ pip install -U pip
+    $ pip install -r requirements-dev.txt
+
+Installing Rasterio
+-------------------
+
+Rasterio, its Cython extensions, normal dependencies, and dev dependencies can
+be installed with ``$ pip``.  Installing Rasterio in editable mode while
+developing is very but only affects the Python files.  Specifying the
+``[test]`` extra in the command below tells ``$ pip`` to also install
+Rasterio's dev dependencies.
+
+.. code-block:: console
+
+    $ pip install -e .[test]
+
+Any time a Cython (``.pyx`` or ``.pxd``) file is edited the extension modules
+need to be recompiled, which is most easily achieved with:
+
+.. code-block:: console
+
+    $ pip install -e .
+
+When switching between Python versions the extension modules must be recompiled,
+which can be forced with ``$ touch rasterio/*.pyx`` and then re-installing with
+the command above.  If this is not done an error claiming that an object ``has
+the wrong size, try recompiling`` is raised.
+
+The dependencies required to build the docs can be installed with:
+
+.. code-block:: console
+
+    $ pip install -e .[docs]
+
+Running the tests
+-----------------
+
+Rasterio's tests live in ``tests <tests/>`` and generally match the main
+package layout.
+
+To run the entire suite and the code coverage report:
+
+.. code-block:: console
+
+    $ py.test --cov rasterio --cov-report term-missing
+
+A single test file:
+
+.. code-block:: console
+
+    $ py.test tests/test_band.py
+
+A single test:
+
+.. code-block:: console
+
+    $ py.test tests/test_band.py::test_band
diff --git a/README.rst b/README.rst
index ef74520..b917047 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
 Rasterio
 ========
 
-Rasterio reads and writes geospatial raster datasets.
+Rasterio reads and writes geospatial raster data files.
 
 .. image:: https://travis-ci.org/mapbox/rasterio.png?branch=master
    :target: https://travis-ci.org/mapbox/rasterio
@@ -10,22 +10,22 @@ Rasterio reads and writes geospatial raster datasets.
 .. image:: https://coveralls.io/repos/github/mapbox/rasterio/badge.svg?branch=master
    :target: https://coveralls.io/github/mapbox/rasterio?branch=master
 
-Rasterio employs GDAL under the hood for file I/O and raster formatting. Its
-functions typically accept and return Numpy ndarrays. Rasterio is designed to
-make working with geospatial raster data more productive and more fun.
+Rasterio employs GDAL to read and writes files using GeoTIFF and many other
+formats. Its API uses familiar Python and SciPy interfaces and idioms like
+context managers, iterators, and ndarrays.
 
-Rasterio is pronounced raw-STEER-ee-oh.
+Documentation is published at https://mapbox.github.io/rasterio/.
 
 Example
 =======
 
-Here's a simple example of the basic features rasterio provides. Three bands
-are read from an image and summed to produce something like a panchromatic
+Here's an example of some basic features that Rasterio provides. Three bands
+are read from an image and averaged to produce something like a panchromatic
 band.  This new band is then written to a new single band TIFF.
 
 .. code-block:: python
 
-    import numpy
+    import numpy as np
     import rasterio
 
     # Read raster bands directly to Numpy arrays.
@@ -38,7 +38,7 @@ band.  This new band is then written to a new single band TIFF.
     # a 64-bit float (the numpy default) array. Adding other
     # arrays to it in-place converts those arrays "up" and
     # preserves the type of the total array.
-    total = numpy.zeros(r.shape)
+    total = np.zeros(r.shape)
     for band in r, g, b:
         total += band
     total /= 3
@@ -62,7 +62,7 @@ The output:
 API Overview
 ============
 
-Simple access is provided to properties of a geospatial raster file.
+Rasterio gives access to properties of a geospatial raster file.
 
 .. code-block:: python
 
@@ -81,7 +81,7 @@ Simple access is provided to properties of a geospatial raster file.
     # 3
     # [1, 2, 3]
 
-A dataset also provides methods for getting extended array slices given
+A rasterio dataset also provides methods for getting extended array slices given
 georeferenced coordinates and vice versa.
 
 
@@ -140,7 +140,7 @@ using Python.
 Rio Plugins
 -----------
 
-Rio provides the ability to create additional subcommands using plugins.  See
+Rio provides the ability to create subcommands using plugins.  See
 `cli.rst <https://github.com/mapbox/rasterio/blob/master/docs/cli.rst#rio-plugins>`__
 for more information on building plugins.
 
@@ -152,39 +152,54 @@ for a list of available plugins.
 Installation
 ============
 
+Please install Rasterio in a `virtual environment
+<https://www.python.org/dev/peps/pep-0405/>`__ so that its requirements don't
+tamper with your system's Python.
+
 Dependencies
 ------------
 
-Rasterio has one C library dependency: GDAL >=1.9. GDAL itself depends on a
+Rasterio has a C library dependency: GDAL >=1.9. GDAL itself depends on a
 number of other libraries provided by most major operating systems and also
-depends on the non standard GEOS and PROJ4 libraries.
+depends on the non standard GEOS and PROJ4 libraries. How to meet this
+requirement will be explained below.
 
-Python package dependencies (see also requirements.txt): affine, cligj (and
-click), enum34, numpy.
+Rasterio's Python dependencies are listed in its requirements.txt file.
 
 Development also requires (see requirements-dev.txt) Cython and other packages.
 
-Installing from binaries
-------------------------
+Binary Distributions
+--------------------
+
+Use a binary distributions that directly or indirectly provide GDAL if
+possible.
+
+Linux
++++++
+
+Rasterio distributions are available from UbuntuGIS and Anaconda's conda-forge
+channel.
+
+`Manylinux1 <https://github.com/pypa/manylinux>`__ distributions may be
+available in the future.
 
 OS X
-----
+++++
 
-Binary wheels with the GDAL, GEOS, and PROJ4 libraries included are available
-for OS X versions 10.7+ starting with Rasterio version 0.17. To install, just
+Binary distributions with GDAL, GEOS, and PROJ4 libraries included are available
+for OS X versions 10.7+ starting with Rasterio version 0.17. To install,
 run ``pip install rasterio``. These binary wheels are preferred by newer
-versions of pip. If you don't want these wheels and want to install from
-a source distribution, run ``pip install rasterio --no-use-wheel`` instead.
+versions of pip.
+
+If you don't want these wheels and want to install from a source distribution,
+run ``pip install rasterio --no-use-wheel`` instead.
 
 The included GDAL library is fairly minimal, providing only the format drivers
 that ship with GDAL and are enabled by default. To get access to more formats,
 you must build from a source distribution (see below).
 
-Binary wheels for other operating systems will be available in a future
-release.
-
 Windows
--------
++++++++
 
 Binary wheels for rasterio and GDAL are created by Christoph Gohlke and are
 available from his website.
@@ -196,12 +211,12 @@ this from the downloads folder:
 
 .. code-block:: console
 
-    $ pip install -U pip 
-    $ pip install GDAL-1.11.2-cp27-none-win32.whl
-    $ pip install rasterio-0.24.0-cp27-none-win32.whl
+    $ pip install -U pip
+    $ pip install GDAL-2.0.2-cp27-none-win32.whl
+    $ pip install rasterio-0.34.0-cp27-cp27m-win32.whl
 
-Installing from the source distribution
----------------------------------------
+Source Distributions
+--------------------
 
 Rasterio is a Python C extension and to build you'll need a working compiler
 (XCode on OS X etc). You'll also need Numpy preinstalled; the Numpy headers are
@@ -212,7 +227,7 @@ Travis `configuration
 guidance.
 
 Linux
------
++++++
 
 The following commands are adapted from Rasterio's Travis-CI configuration.
 
@@ -220,23 +235,31 @@ The following commands are adapted from Rasterio's Travis-CI configuration.
 
     $ sudo add-apt-repository ppa:ubuntugis/ppa
     $ sudo apt-get update
-    $ sudo apt-get install python-numpy libgdal1h gdal-bin libgdal-dev
+    $ sudo apt-get install libgdal1h gdal-bin libgdal-dev
+    $ pip install -U pip
     $ pip install rasterio
 
 Adapt them as necessary for your Linux system.
 
 OS X
-----
+++++
 
 For a Homebrew based Python environment, do the following.
 
 .. code-block:: console
 
+    $ brew upgrade
     $ brew install gdal
-    $ pip install rasterio
+    $ pip install -U pip
+    $ pip install --no-use-wheel rasterio
+
+Alternatively, you can install GDAL binaries from `kyngchaos
+<http://www.kyngchaos.com/software/frameworks#gdal_complete>`__.  You will then
+need to add the installed location ``/Library/Frameworks/GDAL.framework/Programs``
+to your system path.
 
 Windows
--------
++++++++
 
 You can download a binary distribution of GDAL from `here
 <http://www.gisinternals.com/release.php>`__.  You will also need to download
@@ -262,34 +285,27 @@ versions used `here
 Note: The GDAL dll (gdal111.dll) and gdal-data directory need to be in your
 Windows PATH otherwise rasterio will fail to work.
 
-Testing
--------
-
-From the repo directory, run py.test
-
-.. code-block:: console
-
-    $ python -m pytest
+Development and Testing
+-----------------------
 
-Note: some tests do not succeed on Windows (see
-`#66 <https://github.com/mapbox/rasterio/issues/66>`__.).
+See `CONTRIBUTING.rst <CONTRIBUTING.rst/>`__.
 
 Documentation
 -------------
 
-See https://github.com/mapbox/rasterio/tree/master/docs.
+See `docs/ <docs/>`__.
 
 License
 -------
 
-See LICENSE.txt.
+See `LICENSE.txt <LICENSE.txt>`__.
 
 Authors
 -------
 
-See AUTHORS.txt.
+See `AUTHORS.txt <AUTHORS.txt>`__.
 
 Changes
 -------
 
-See CHANGES.txt.
+See `CHANGES.txt <CHANGES.txt>`__.
diff --git a/docs/Makefile b/docs/Makefile
index d209e6f..4baff4b 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -6,6 +6,7 @@ SPHINXOPTS    =
 SPHINXBUILD   = sphinx-build
 PAPER         =
 BUILDDIR      = _build
+GITREF := $(/bin/bash git rev-parse --abbrev-ref HEAD)
 
 # User-friendly check for sphinx-build
 ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
@@ -216,8 +217,9 @@ pseudoxml:
 
 .PHONY: apidocs
 apidocs:
-	sphinx-apidoc -f -e -T -M -o . ../rasterio ../rasterio/rio ../rasterio/five.py ../rasterio/tools ../rasterio/tool.py
+	sphinx-apidoc -f -e -T -M -o . ../rasterio ../rasterio/rio ../rasterio/compat.py ../rasterio/tools ../rasterio/tool.py
 
 .PHONY: publish
 publish: html
-	aws s3 sync _build/html/ s3://mapbox/playground/perrygeo/rasterio-docs --delete --acl public-read
+	ghp-import _build/html -m "update docs at $(GITREF)"
+	git push origin gh-pages
diff --git a/docs/_build/html/.nojekyll b/docs/_build/html/.nojekyll
new file mode 100644
index 0000000..f257b09
--- /dev/null
+++ b/docs/_build/html/.nojekyll
@@ -0,0 +1 @@
+# Not a Jekyll site
diff --git a/docs/cli.rst b/docs/cli.rst
index e5c3ace..ee1ee82 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -535,8 +535,8 @@ The ``stack`` command stacks a number of bands from one or more input files
 into a multiband dataset. Input datasets must be of a kind: same data type,
 dimensions, etc. The output is cloned from the first input. By default,
 ``stack`` will take all bands from each input and write them in same order to
-the output. Optionally, bands for each input may be specified using a simple
-syntax:
+the output. Optionally, bands for each input may be specified using the
+following syntax:
 
 - ``--bidx N`` takes the Nth band from the input (first band is 1).
 - ``--bidx M,N,O`` takes bands M, N, and O.
@@ -642,9 +642,10 @@ a command ``rio mbtiles`` to export a raster to an MBTiles file.
 See `click-plugins <https://github.com/click-contrib/click-plugins>`__ for more
 information on how to build these plugins in general.
 
-In order to use these plugins with rio, add the commands to the
+To use these plugins with rio, add the commands to the
 ``rasterio.rio_plugins'`` entry point in your ``setup.py`` file, as described
 `here <https://github.com/click-contrib/click-plugins#developing-plugins>`__
+and in ``rasterio/rio/main.py``.
 
 See the
 `plugin registry <https://github.com/mapbox/rasterio/wiki/Rio-plugin-registry>`__
diff --git a/docs/color.rst b/docs/color.rst
index 10cddbb..84ee6f8 100644
--- a/docs/color.rst
+++ b/docs/color.rst
@@ -50,7 +50,7 @@ to bands using the ``write_colormap()`` method.
 
     import rasterio
 
-    with rasterio.drivers():
+    with rasterio.Env():
 
         with rasterio.open('tests/data/shade.tif') as src:
             shade = src.read(1)
diff --git a/docs/concurrency.rst b/docs/concurrency.rst
index 0b6cde7..d560eca 100644
--- a/docs/concurrency.rst
+++ b/docs/concurrency.rst
@@ -13,8 +13,8 @@ raster processing function.
 
 .. code-block:: python
 
-    import numpy
-    cimport numpy
+    import numpy as np
+    cimport numpy as np
 
     def compute(
             unsigned char[:, :, :] input, 
@@ -55,13 +55,13 @@ Here is the program in examples/concurrent-cpu-bound.py.
     import multiprocessing
     import time
 
-    import numpy
+    import numpy as np
     import rasterio
     from rasterio._example import compute
 
     def main(infile, outfile, num_workers=4):
 
-        with rasterio.drivers():
+        with rasterio.Env():
 
             # Open the source dataset.
             with rasterio.open(infile) as src:
@@ -79,7 +79,7 @@ Here is the program in examples/concurrent-cpu-bound.py.
                     def jobs():
                         for ij, window in dst.block_windows():
                             data = src.read(window=window)
-                            result = numpy.zeros(data.shape, dtype=data.dtype)
+                            result = np.zeros(data.shape, dtype=data.dtype)
                             yield data, result, window
 
                     # Submit the jobs to the thread pool executor.
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 1309ed4..d507f25 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -68,7 +68,7 @@ Rasterio
 The object returned when you call ``rasterio.Env()`` is a context manager.  It
 handles the GDAL configuration for a specific block of code and resets the
 configuration when the block exits for any reason, success or failure. The
-Rasterio ``with Env()`` pattern organizes GDAL configuration into single
+Rasterio ``with rasterio.Env()`` pattern organizes GDAL configuration into single
 statements and makes its relationship to a block of code clear.
 
 If you want to know what options are configured at any time, you could bind it
diff --git a/docs/cookbook.rst b/docs/cookbook.rst
index 66a9f5f..5034995 100644
--- a/docs/cookbook.rst
+++ b/docs/cookbook.rst
@@ -115,7 +115,7 @@ Creating a least cost path
 Using a scipy filter to smooth a raster
 ---------------------------------------
 
-This recipe demonstrates the use of scipy's `signal processing filters <http://docs.scipy.org/doc/scipy/reference/signal.html#signal-processing-scipy-signal>`_ to manipulate multi-band raster imagery
+This recipe demonstrates scipy's `signal processing filters <http://docs.scipy.org/doc/scipy/reference/signal.html#signal-processing-scipy-signal>`_ to manipulate multi-band raster imagery
 and save the results to a new GeoTIFF. Here we apply a median filter to smooth
 the image and remove small inclusions (at the expense of some sharpness and detail).
 
@@ -141,7 +141,7 @@ With median filter applied
 Using skimage to adjust the saturation of a RGB raster
 ------------------------------------------------------
 
-This recipe demonstrates the use of manipulating color with the scikit image `color module <http://scikit-image.org/docs/stable/api/skimage.color.html>`_.
+This recipe demonstrates manipulating color with the scikit image `color module <http://scikit-image.org/docs/stable/api/skimage.color.html>`_.
 
 .. literalinclude:: recipes/saturation.py
     :language: python
diff --git a/docs/features.rst b/docs/features.rst
index 7f0c342..7a0bb13 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -4,7 +4,7 @@ Vector Features
 Rasterio's ``features`` module provides functions to extract shapes of raster
 features and to create new features by "burning" shapes into rasters:
 ``shapes()`` and ``rasterize()``. These functions expose GDAL functions in
-a very general way, using iterators over GeoJSON-like Python objects instead of
+a general way, using iterators over GeoJSON-like Python objects instead of
 GIS layers.
 
 Extracting shapes of raster features
@@ -65,7 +65,7 @@ By default, only pixels whose center is within the polygon or that
 are selected by Bresenham's line algorithm will be burned in.  
 You can specify ``all_touched=True`` to burn in all pixels touched by the geometry.
 The geometries will be rasterized by the "painter's algorithm" - 
-geometries are handled in order and subsequent geometries will overwrite previous values.
+geometries are handled in order and later geometries will overwrite earlier values.
 
 Again, to burn in georeferenced shapes, pass an appropriate transform for the
 image to be created.
diff --git a/docs/georeferencing.rst b/docs/georeferencing.rst
index d905b99..bd2a85c 100644
--- a/docs/georeferencing.rst
+++ b/docs/georeferencing.rst
@@ -17,15 +17,15 @@ attribute.
     >>> import rasterio
     >>> src = rasterio.open('tests/data/RGB.byte.tif')
     >>> src.crs
-    {'init': u'epsg:32618'}
+    CRS({'init': u'epsg:32618'})
 
 Rasterio follows pyproj and uses PROJ.4 syntax in dict form as its native
-CRS syntax. If you want a WKT representation of the CRS, see the ``crs_wkt``
-attribute.
+CRS syntax. If you want a WKT representation of the CRS, see the CRS
+class's ``wkt`` attribute.
 
 .. code-block:: python
 
-    >>> src.crs_wkt
+    >>> src.crs.wkt
     u'PROJCS["UTM Zone 18, Northern Hemisphere",GEOGCS["Unknown datum based upon the WGS 84 ellipsoid",DATUM["Not_specified_based_on_WGS_84_spheroid",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northin [...]
 
 When opening a new file for writing, you may also use a CRS string as an
@@ -61,10 +61,10 @@ a pixel's image coordinates are ``x, y`` and its world coordinates are
     | y' | = | d e f | | y |
     | 1  |   | 0 0 1 | | 1 |
 
-The ``Affine`` class has a number of useful properties and methods
+The ``Affine`` class has some useful properties and methods
 described at https://github.com/sgillies/affine.
 
-Previous versions of Rasterio had a ``transform`` attribute which was a 6-element
+Earlier versions of Rasterio had a ``transform`` attribute which was a 6-element
 tuple. This usage is deprecated, please see https://github.com/mapbox/rasterio/issues/86 for details. 
 In Rasterio 1.0, the value of a  ``transform`` attribute will be an instance
 of ``Affine`` and the ``affine`` attribute will remain as an alias.
diff --git a/docs/image_options.rst b/docs/image_options.rst
index 105a847..6a48659 100644
--- a/docs/image_options.rst
+++ b/docs/image_options.rst
@@ -14,14 +14,14 @@ GDAL options are typically set as environment variables. While
 environment variables will influence the behavior of ``rasterio``, we
 highly recommended avoiding them in favor of defining behavior programatically.
 
-The preferred way to set options for rasterio is via ``rasterio.drivers()``.
+The preferred way to set options for rasterio is via ``rasterio.Env()``.
 Options set on entering the context are deleted on exit.
 
 .. code-block:: python
 
     import rasterio
 
-    with rasterio.drivers(GDAL_TIFF_INTERNAL_MASK=True):
+    with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=True):
         # GeoTIFFs written here will have internal masks, not the
         # .msk sidecars.
         ...
diff --git a/docs/image_processing.rst b/docs/image_processing.rst
index 74c98af..e097eae 100644
--- a/docs/image_processing.rst
+++ b/docs/image_processing.rst
@@ -7,21 +7,27 @@ Some python image processing software packages
 organize arrays differently than rasterio. The interpretation of a
 3-dimension array read from ``rasterio`` is::
 
-    (bands, columns, rows)
+    (bands, rows, columns)
 
-while image processing software like ``scikit-image`` is often::
+while image processing software like ``scikit-image``, ``pillow`` and ``matplotlib`` are generally ordered::
 
-    (columns, rows, bands)
+    (rows, columns, bands)
 
-Numpy provides a function to efficient swap the axis order:
+The number of rows defines the dataset's height, the columns are the dataset's width.
+
+Numpy provides a way to efficiently swap the axis order and you can use the
+following functions to convert between raster and image axis order:
 
 .. code:: python
 
-    # rasterio (bands, cols, rows) -> skimage (cols, rows, bands)
-    image_array = np.swapaxes(array, 0, 2)
+    def reshape_as_image(arr):
+        """raster order (bands, rows, cols) -> image (rows, cols, bands)
+        """
+        return np.swapaxes(np.swapaxes(arr, 0, 2), 0, 1)
 
-    # work in skimage
 
-    # skimage (cols, rows, bands) -> rasterio (bands, cols, rows)
-    array = np.swapaxes(image_array, 2, 0)
+    def reshape_as_raster(arr):
+        """image order (rows, cols, bands) -> rasterio (bands, rows, cols)
+        """
+        return np.swapaxes(np.swapaxes(arr, 2, 0), 2, 1)
 
diff --git a/docs/installation.rst b/docs/installation.rst
index d27508e..77dda7f 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -4,8 +4,8 @@ Installation
 Dependencies
 ************************
 
-Rasterio has one C library dependency: ``GDAL >=1.9``. GDAL itself depends on a
-number of other libraries provided by most major operating systems and also
+Rasterio has one C library dependency: ``GDAL >=1.9``. GDAL itself depends on
+many of other libraries provided by most major operating systems and also
 depends on the non standard GEOS and PROJ4 libraries.
 
 Python package dependencies (see also requirements.txt): ``affine, cligj, click, enum34, numpy``.
@@ -19,7 +19,7 @@ OS X
 ----
 
 Binary wheels with the GDAL, GEOS, and PROJ4 libraries included are available
-for OS X versions 10.7+ starting with Rasterio version 0.17. To install, just
+for OS X versions 10.7+ starting with Rasterio version 0.17. To install, 
 run ``pip install rasterio``. These binary wheels are preferred by newer
 versions of pip. If you don't want these wheels and want to install from
 a source distribution, run ``pip install rasterio --no-use-wheel`` instead.
@@ -37,7 +37,7 @@ Windows
 Binary wheels for rasterio and GDAL are created by Christoph Gohlke and are
 available from his website.
 
-To install rasterio, simply download both binaries for your system (`rasterio
+To install rasterio, download both binaries for your system (`rasterio
 <http://www.lfd.uci.edu/~gohlke/pythonlibs/#rasterio>`__ and `GDAL
 <http://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal>`__) and run something like
 this from the downloads folder:
diff --git a/docs/masks.rst b/docs/masks.rst
index e4ec648..05d1fde 100644
--- a/docs/masks.rst
+++ b/docs/masks.rst
@@ -17,7 +17,7 @@ trapezoid of image data within a rectangular background of 0,0,0 value pixels.
 
 .. image:: https://www.dropbox.com/s/sg7qejccih5m4ah/RGB.byte.jpg?dl=1
 
-Metadata in the dataset declares that values of 0 shall be interpreted as
+Metadata in the dataset declares that values of 0 will be interpreted as
 invalid data or *nodata* pixels. In, e.g., merging the image with adjacent
 scenes, we'd like to ignore the nodata pixels and have only valid image data in
 our final mosaic.
@@ -82,7 +82,7 @@ a problem inherent in 8-bit raster data: lack of dynamic range. The dataset
 creator has said that 0 values represent missing data (see the
 ``nodatavals`` property in the first code block of this document), but some of
 the valid data have values so low they've been rounded during processing to
-zero.  This can very easily happen in scaling 16-bit data to 8 bits.  There's
+zero.  This can happen in scaling 16-bit data to 8 bits.  There's
 no magic nodata value bullet for this. Using 16 bits per band helps, but you
 really have to be careful with 8-bit per band datasets and their nodata values.
 
@@ -196,7 +196,7 @@ If you want, you can read dataset bands as numpy masked arrays.
            [ True,  True,  True, ...,  True,  True,  True]], dtype=bool)
 
 As mentioned earlier, this mask is the inverse of the GDAL band mask. To get
-a mask conforming to GDAL RFC 15, simply do this:
+a mask conforming to GDAL RFC 15, do this:
 
 .. code-block:: python
 
diff --git a/docs/osgeo_gdal_migration.rst b/docs/osgeo_gdal_migration.rst
index 41abf69..6ca54c4 100644
--- a/docs/osgeo_gdal_migration.rst
+++ b/docs/osgeo_gdal_migration.rst
@@ -10,7 +10,7 @@ This section will discuss the differences between ``rasterio`` and ``osgeo.gdal`
 choose to use one over the other.
 
 ``osgeo.gdal`` is automatically-generated using swig. As a result, the interface and method names are
-very similar to the native C++ API.  The ``rasterio`` library is built with Cython which allows
+similar to the native C++ API.  The ``rasterio`` library is built with Cython which allows
 us to create an interface that follows the style and conventions of familiar Python code.
 
 This is best illustrated by example.  Opening a raster file with ``osgeo.gdal`` involves using gdal constants and the programmer must provide their own error handling and memory management ::
@@ -26,7 +26,7 @@ This is best illustrated by example.  Opening a raster file with ``osgeo.gdal``
 Compared to the similar code in ``rasterio``::
 
     import rasterio
-    with rasterio.drivers():
+    with rasterio.Env():
         with rasterio.open(filename, 'r') as dataset:
             # ... work with dataset
 
diff --git a/docs/reading.rst b/docs/reading.rst
index c39aaaa..08b2ebc 100644
--- a/docs/reading.rst
+++ b/docs/reading.rst
@@ -34,7 +34,7 @@ objects.
     >>> src.closed
     False
 
-If you attempt to access a nonexistent path, ``rasterio.open()`` does the same
+If you try to access a nonexistent path, ``rasterio.open()`` does the same
 thing as ``open()``, raising an exception immediately.
 
 .. code-block:: python
@@ -59,7 +59,7 @@ a file can be read like this:
     (718, 791)
 
 The returned object is a 2-dimensional Numpy ndarray. The representation of
-that array at the Python prompt is just a summary; the GeoTIFF file that
+that array at the Python prompt is a summary; the GeoTIFF file that
 Rasterio uses for testing has 0 values in the corners, but has nonzero values
 elsewhere.
 
diff --git a/docs/recipes/reproject.py b/docs/recipes/reproject.py
index 2ba260f..657314c 100644
--- a/docs/recipes/reproject.py
+++ b/docs/recipes/reproject.py
@@ -10,7 +10,7 @@ out = '/tmp/reproj.tif'
 dst_crs = crs.from_string("EPSG:3759")
 
 
-with rasterio.drivers(CHECK_WITH_INVERT_PROJ=True):
+with rasterio.Env(CHECK_WITH_INVERT_PROJ=True):
     with rasterio.open(rgb) as src:
         profile = src.profile
 
diff --git a/docs/reproject.rst b/docs/reproject.rst
index bd0a096..41dd3c8 100644
--- a/docs/reproject.rst
+++ b/docs/reproject.rst
@@ -8,7 +8,7 @@ coordinate reference system and transform to the pixels of a source image with
 a different coordinate reference system and transform. This process is known as
 reprojection.
 
-Rasterio's ``rasterio.warp.reproject()`` is a very geospatial-specific analog
+Rasterio's ``rasterio.warp.reproject()`` is a geospatial-specific analog
 to SciPy's ``scipy.ndimage.interpolation.geometric_transform()`` [1]_.
 
 The code below reprojects between two arrays, using no pre-existing GIS
@@ -18,12 +18,12 @@ transform.
 
 .. code-block:: python
 
-    import numpy
+    import numpy as np
     import rasterio
     from rasterio import Affine as A
     from rasterio.warp import reproject, RESAMPLING
 
-    with rasterio.drivers():
+    with rasterio.Env():
 
         # As source: a 512 x 512 raster centered on 0 degrees E and 0
         # degrees N, each pixel covering 15".
@@ -33,14 +33,14 @@ transform.
         # A(d, 0, -cols*d/2, 0, -d, rows*d/2).
         src_transform = A.translation(-cols*d/2, rows*d/2) * A.scale(d, -d)
         src_crs = {'init': 'EPSG:4326'}
-        source = numpy.ones(src_shape, numpy.uint8)*255
+        source = np.ones(src_shape, np.uint8)*255
 
         # Destination: a 1024 x 1024 dataset in Web Mercator (EPSG:3857)
         # with origin at 0.0, 0.0.
         dst_shape = (1024, 1024)
         dst_transform = [-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0]
         dst_crs = {'init': 'EPSG:3857'}
-        destination = numpy.zeros(dst_shape, numpy.uint8)
+        destination = np.zeros(dst_shape, np.uint8)
 
         reproject(
             source, 
@@ -58,7 +58,7 @@ transform.
 
 See `examples/reproject.py <https://github.com/mapbox/rasterio/blob/master/examples/reproject.py>`__
 for code that writes the destination array to a GeoTIFF file. I've uploaded the
-resulting file to a Mapbox map to demonstrate that the reprojection is
+resulting file to a Mapbox map to show that the reprojection is
 correct: https://a.tiles.mapbox.com/v3/sgillies.hfek2oko/page.html?secure=1#6/0.000/0.033.
 
 Reprojecting a GeoTIFF dataset
@@ -80,7 +80,7 @@ provided, and returns destination transform and dimensions.
 
 .. code-block:: python
 
-    import numpy
+    import numpy as np
     import rasterio
     from rasterio.warp import calculate_default_transform, reproject, RESAMPLING
 
@@ -123,7 +123,7 @@ the output dataset's transform matrix and, thereby, its spatial extent.
 
 .. code-block:: python
 
-    import numpy
+    import numpy as np
     import rasterio
     from rasterio import Affine as A
     from rasterio.warp import reproject, RESAMPLING
@@ -146,7 +146,7 @@ the output dataset's transform matrix and, thereby, its spatial extent.
         with rasterio.open('/tmp/zoomed-out.tif', 'w', **kwargs) as dst:
 
             for i, band in enumerate(data, 1):
-                dest = numpy.zeros_like(band)
+                dest = np.zeros_like(band)
 
                 reproject(
                     band,
diff --git a/docs/tags.rst b/docs/tags.rst
index a416d92..e51c91d 100644
--- a/docs/tags.rst
+++ b/docs/tags.rst
@@ -19,7 +19,7 @@ I'm going to use the rasterio interactive inspector in these examples below.
     >>> 
 
 Tags belong to namespaces. To get a copy of a dataset's tags from the default
-namespace, just call ``tags()`` with no arguments.
+namespace, call ``tags()`` with no arguments.
 
 .. code-block:: pycon
 
diff --git a/docs/windowed-rw.rst b/docs/windowed-rw.rst
index 436180c..09e5b3a 100644
--- a/docs/windowed-rw.rst
+++ b/docs/windowed-rw.rst
@@ -2,8 +2,8 @@ Windowed reading and writing
 ****************************
 
 Beginning in rasterio 0.3, you can read and write "windows" of raster files.
-This feature allows you to operate on rasters that are larger than your
-computers RAM or process chunks of very large rasters in parallel.
+This feature allows you to work on rasters that are larger than your
+computers RAM or process chunks of large rasters in parallel.
 
 Windows
 -------
diff --git a/docs/working_with_datasets.rst b/docs/working_with_datasets.rst
index 7f2ddb5..092953f 100644
--- a/docs/working_with_datasets.rst
+++ b/docs/working_with_datasets.rst
@@ -8,12 +8,12 @@ Working with Datasets
 Attributes
 ----------
 
-In addition to the file-like attributes shown above, a dataset has a number
-of other read-only attributes that help explain its role in spatial information
-systems. The ``driver`` attribute gives you the name of the GDAL format
-driver used. The ``height`` and ``width`` are the number of rows and columns of
-the raster dataset and ``shape`` is a ``height, width`` tuple as used by
-Numpy. The ``count`` attribute tells you the number of bands in the dataset.
+Besides the file-like attributes shown above, a dataset has some other
+read-only attributes that help explain its role in spatial information systems.
+The ``driver`` attribute gives you the name of the GDAL format driver used. The
+``height`` and ``width`` are the number of rows and columns of the raster
+dataset and ``shape`` is a ``height, width`` tuple as used by Numpy. The
+``count`` attribute tells you the number of bands in the dataset.
 
 .. code-block:: python
 
diff --git a/docs/writing.rst b/docs/writing.rst
index d0fc849..b14e8dd 100644
--- a/docs/writing.rst
+++ b/docs/writing.rst
@@ -3,7 +3,6 @@ Writing Datasets
 
 .. todo::
 
-    * supported drivers
     * appending to existing data
     * context manager
     * write 3d vs write 2d
@@ -18,14 +17,14 @@ Opening a file in writing mode is a little more complicated than opening
 a text file in Python. The dimensions of the raster dataset, the 
 data types, and the specific format must be specified.
 
-Here's a simple example of the basic rasterio functionality. 
+Here's an example of basic rasterio functionality. 
 An array is written to a new single band TIFF.
 
 .. code-block:: python
 
     # Register GDAL format drivers and configuration options with a
     # context manager.
-    with rasterio.drivers():
+    with rasterio.Env():
 
         # Write the product as a raster band to a new 8-bit file. For
         # the new file's profile, we start with the meta attributes of
@@ -40,8 +39,25 @@ An array is written to a new single band TIFF.
         with rasterio.open('example-total.tif', 'w', **profile) as dst:
             dst.write(array.astype(rasterio.uint8), 1)
 
-    # At the end of the ``with rasterio.drivers()`` block, context
+    # At the end of the ``with rasterio.Env()`` block, context
     # manager exits and all drivers are de-registered.
 
 Writing data mostly works as with a Python file. There are a few format-
 specific differences.
+
+Supported Drivers
+-----------------
+``GTiff`` is the only driver that supports writing directly to disk.
+GeoTiffs use the ``RasterUpdater`` and leverage the full capabilities 
+of the ``GDALCreate`` function. We highly recommend using GeoTiff 
+driver for writing as it is the best-tested and best-supported format.
+
+Some other formats that are writable by GDAL can also be written by
+Rasterio. These use an ``IndirectRasterUpdater`` which does not create
+directly but uses a temporary in-memory dataset and ``GDALCreateCopy``
+to produce the final output.
+
+Some formats are known to produce invalid results using the
+``IndirectRasterUpdater``. These formats will raise a ``RasterioIOError``
+if you attempt to write to the. Currently this applies to the ``netCDF``
+driver but please let us know if you experience problems writing other formats.
diff --git a/examples/Data visualization.ipynb b/examples/Data visualization.ipynb
new file mode 100644
index 0000000..e9b5455
--- /dev/null
+++ b/examples/Data visualization.ipynb	
@@ -0,0 +1,2083 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Basic data visualization"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import rasterio\n",
+    "from rasterio import plot"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "src = rasterio.open(r\"../tests/data/RGB.byte.tif\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### With a raster dataset,  (band number) or numpy array we can display the raster"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAATQAAAD7CAYAAADkSGhKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsvXmcZVV57v9919rDGWru6q6eB6ZuaGYEFCEYBkFJRCVG\nIqgYjdON+rsxP3P1Xo0azaDRqIkTUaMQEpM4xTiBIAiiAjLPY89z13zGvfda7/1j7SraqFG4JNDk\nPMCHqlPnVJ2z197PfofneZeoKj300EMPTweYJ/sN9NBDDz08UegRWg899PC0QY/Qeuihh6cNeoTW\nQw89PG3QI7QeeujhaYMeofXQQw9PG0RP9hv4z4aI9HQpPfTwNISqyr9/7GlPaE8HVNcdQ757G9HI\nAlBB8w6rl65g5+6ddLIuJAludgrNOrjpmSf77fbQw5OGHqHtB9AiQ9WjuQOjqIdHNj6CVCtIfQDJ\n29i0 [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x87f1748>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<matplotlib.axes._subplots.AxesSubplot at 0x87fbfd0>"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "%matplotlib inline\n",
+    "plot.show(src)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### We can also display a single band of a multiband image by passing a tuple (raster source, band)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXYAAADDCAYAAACWLjoXAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsvFms5el63vX7pv+0pj3XrqG7qrt6OlP7DD7HOfbRcXzs\nJCYxARnCYC64QiABN5FA3CCkRIAQQspFJBAJCCSTxDZgjOQJB8eO7TPYPvPUc3XNu/a8pv/0TVx8\n/727jexzgZDcsuqTqkq11l5rr/UNz/u8z/u8n4gx8nQ8HU/H0/F0/MUZ8s/7AzwdT8fT8XQ8Hf//\njqfA/nQ8HU/H0/EXbDwF9qfj6Xg6no6/YOMpsD8dT8fT8XT8BRtPgf3peDqejqfjL9jQf94fQAjx\n1JbzdDwdT8fT8f9hxBjFn/b4nzuwA9z+J38XLvA9AiIiZfq/EOlBIcBahe81EIlBQK9QOhADYAVY\niXCC [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x8af9ef0>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<matplotlib.axes._subplots.AxesSubplot at 0x8aeed30>"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "world = rasterio.open(r\"../tests/data/world.rgb.tif\")\n",
+    "plot.show((world, 2), cmap='viridis')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### side by side:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAABDAAAAHWCAYAAACBsXkuAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xm0p1dd5/v3d+/neX7jGWseU6mkqpLKQAYIBAiBDMxB\nlLZtRGlpFZtW1OZKt7Z9L7a2KMLtK8te7brt0CpLhb6KiCjShIgBJSTBDCQhc1JJJTWe+Tc9z7P3\n/t4/9nMq2ute7UCSCrhfa1XOqTrn/M5vzFr7+/t+P19RVZIkSZIkSZIkSZIkSZ7PzOm+AkmSJEmS\nJEmSJEmSJP+QVMBIkiRJkiRJkiRJkuR5LxUwkiRJkiRJkiRJkiR53ksFjCRJkiRJkiRJkiRJnvdS\nASNJkiRJkiRJkiRJkue9VMBIkiRJkiRJkiRJkuR5LzvdV+DZJiJpT2ySJEmSPM+oqpzu65AkSZIk\nyTeX [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x8da9cf8>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15,7))\n",
+    "plot.show(src, ax=ax1)\n",
+    "plot.show((world, 2), cmap='viridis', ax=ax2)\n",
+    "fig.tight_layout()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Multiple bands side by side"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<matplotlib.axes._subplots.AxesSubplot at 0x10c1f278>"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAABNIAAAFkCAYAAADlpBYcAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXm8JUV5979P9XLOudvsMwwzw4AMOyqIiCCCuC+YGHF7\n465EY9QYo29cYuIeicYtmihG3I3G7VU0QRRRWRQ3ZB0YlmH2/c7dz9LdVfX+UdV9zr2zQoABpr58\n5nPO6aW6us+h69avf8/ziLWWQCAQCAQCgUAgEAgEAoFAILB31IHuQCAQCAQCgUAgEAgEAoFAIPBg\nIAhpgUAgEAgEAoFAIBAIBAKBwH4QhLRAIBAIBAKBQCAQCAQCgUBgPwhCWiAQCAQCgUAgEAgEAoFA\nILAfBCEtEAgEAoFAIBAIBAKBQCAQ2A+CkBYIBAKBQCAQCAQCgUAgEAjsB0FICwTuISLybhExIvK9\ne7i/ [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x8cdc898>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "fig, (axr, axg, axb) = plt.subplots(1,3, figsize=(21,7))\n",
+    "plot.show((src, 1), ax=axr, cmap='Reds', title='red channel')\n",
+    "plot.show((src, 2), ax=axg, cmap='Greens', title='green channel')\n",
+    "plot.show((src, 3), ax=axb, cmap='Blues', title='blue channel')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### How about the same thing but with the ability to navigate simultaneously?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/javascript": [
+       "/* Put everything inside the global mpl namespace */\n",
+       "window.mpl = {};\n",
+       "\n",
+       "mpl.get_websocket_type = function() {\n",
+       "    if (typeof(WebSocket) !== 'undefined') {\n",
+       "        return WebSocket;\n",
+       "    } else if (typeof(MozWebSocket) !== 'undefined') {\n",
+       "        return MozWebSocket;\n",
+       "    } else {\n",
+       "        alert('Your browser does not have WebSocket support.' +\n",
+       "              'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+       "              'Firefox 4 and 5 are also supported but you ' +\n",
+       "              'have to enable WebSockets in about:config.');\n",
+       "    };\n",
+       "}\n",
+       "\n",
+       "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
+       "    this.id = figure_id;\n",
+       "\n",
+       "    this.ws = websocket;\n",
+       "\n",
+       "    this.supports_binary = (this.ws.binaryType != undefined);\n",
+       "\n",
+       "    if (!this.supports_binary) {\n",
+       "        var warnings = document.getElementById(\"mpl-warnings\");\n",
+       "        if (warnings) {\n",
+       "            warnings.style.display = 'block';\n",
+       "            warnings.textContent = (\n",
+       "                \"This browser does not support binary websocket messages. \" +\n",
+       "                    \"Performance may be slow.\");\n",
+       "        }\n",
+       "    }\n",
+       "\n",
+       "    this.imageObj = new Image();\n",
+       "\n",
+       "    this.context = undefined;\n",
+       "    this.message = undefined;\n",
+       "    this.canvas = undefined;\n",
+       "    this.rubberband_canvas = undefined;\n",
+       "    this.rubberband_context = undefined;\n",
+       "    this.format_dropdown = undefined;\n",
+       "\n",
+       "    this.image_mode = 'full';\n",
+       "\n",
+       "    this.root = $('<div/>');\n",
+       "    this._root_extra_style(this.root)\n",
+       "    this.root.attr('style', 'display: inline-block');\n",
+       "\n",
+       "    $(parent_element).append(this.root);\n",
+       "\n",
+       "    this._init_header(this);\n",
+       "    this._init_canvas(this);\n",
+       "    this._init_toolbar(this);\n",
+       "\n",
+       "    var fig = this;\n",
+       "\n",
+       "    this.waiting = false;\n",
+       "\n",
+       "    this.ws.onopen =  function () {\n",
+       "            fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
+       "            fig.send_message(\"send_image_mode\", {});\n",
+       "            fig.send_message(\"refresh\", {});\n",
+       "        }\n",
+       "\n",
+       "    this.imageObj.onload = function() {\n",
+       "            if (fig.image_mode == 'full') {\n",
+       "                // Full images could contain transparency (where diff images\n",
+       "                // almost always do), so we need to clear the canvas so that\n",
+       "                // there is no ghosting.\n",
+       "                fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+       "            }\n",
+       "            fig.context.drawImage(fig.imageObj, 0, 0);\n",
+       "        };\n",
+       "\n",
+       "    this.imageObj.onunload = function() {\n",
+       "        this.ws.close();\n",
+       "    }\n",
+       "\n",
+       "    this.ws.onmessage = this._make_on_message_function(this);\n",
+       "\n",
+       "    this.ondownload = ondownload;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_header = function() {\n",
+       "    var titlebar = $(\n",
+       "        '<div class=\"ui-dialog-titlebar ui-widget-header ui-corner-all ' +\n",
+       "        'ui-helper-clearfix\"/>');\n",
+       "    var titletext = $(\n",
+       "        '<div class=\"ui-dialog-title\" style=\"width: 100%; ' +\n",
+       "        'text-align: center; padding: 3px;\"/>');\n",
+       "    titlebar.append(titletext)\n",
+       "    this.root.append(titlebar);\n",
+       "    this.header = titletext[0];\n",
+       "}\n",
+       "\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
+       "\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
+       "\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_canvas = function() {\n",
+       "    var fig = this;\n",
+       "\n",
+       "    var canvas_div = $('<div/>');\n",
+       "\n",
+       "    canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
+       "\n",
+       "    function canvas_keyboard_event(event) {\n",
+       "        return fig.key_event(event, event['data']);\n",
+       "    }\n",
+       "\n",
+       "    canvas_div.keydown('key_press', canvas_keyboard_event);\n",
+       "    canvas_div.keyup('key_release', canvas_keyboard_event);\n",
+       "    this.canvas_div = canvas_div\n",
+       "    this._canvas_extra_style(canvas_div)\n",
+       "    this.root.append(canvas_div);\n",
+       "\n",
+       "    var canvas = $('<canvas/>');\n",
+       "    canvas.addClass('mpl-canvas');\n",
+       "    canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
+       "\n",
+       "    this.canvas = canvas[0];\n",
+       "    this.context = canvas[0].getContext(\"2d\");\n",
+       "\n",
+       "    var rubberband = $('<canvas/>');\n",
+       "    rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
+       "\n",
+       "    var pass_mouse_events = true;\n",
+       "\n",
+       "    canvas_div.resizable({\n",
+       "        start: function(event, ui) {\n",
+       "            pass_mouse_events = false;\n",
+       "        },\n",
+       "        resize: function(event, ui) {\n",
+       "            fig.request_resize(ui.size.width, ui.size.height);\n",
+       "        },\n",
+       "        stop: function(event, ui) {\n",
+       "            pass_mouse_events = true;\n",
+       "            fig.request_resize(ui.size.width, ui.size.height);\n",
+       "        },\n",
+       "    });\n",
+       "\n",
+       "    function mouse_event_fn(event) {\n",
+       "        if (pass_mouse_events)\n",
+       "            return fig.mouse_event(event, event['data']);\n",
+       "    }\n",
+       "\n",
+       "    rubberband.mousedown('button_press', mouse_event_fn);\n",
+       "    rubberband.mouseup('button_release', mouse_event_fn);\n",
+       "    // Throttle sequential mouse events to 1 every 20ms.\n",
+       "    rubberband.mousemove('motion_notify', mouse_event_fn);\n",
+       "\n",
+       "    rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
+       "    rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
+       "\n",
+       "    canvas_div.on(\"wheel\", function (event) {\n",
+       "        event = event.originalEvent;\n",
+       "        event['data'] = 'scroll'\n",
+       "        if (event.deltaY < 0) {\n",
+       "            event.step = 1;\n",
+       "        } else {\n",
+       "            event.step = -1;\n",
+       "        }\n",
+       "        mouse_event_fn(event);\n",
+       "    });\n",
+       "\n",
+       "    canvas_div.append(canvas);\n",
+       "    canvas_div.append(rubberband);\n",
+       "\n",
+       "    this.rubberband = rubberband;\n",
+       "    this.rubberband_canvas = rubberband[0];\n",
+       "    this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
+       "    this.rubberband_context.strokeStyle = \"#000000\";\n",
+       "\n",
+       "    this._resize_canvas = function(width, height) {\n",
+       "        // Keep the size of the canvas, canvas container, and rubber band\n",
+       "        // canvas in synch.\n",
+       "        canvas_div.css('width', width)\n",
+       "        canvas_div.css('height', height)\n",
+       "\n",
+       "        canvas.attr('width', width);\n",
+       "        canvas.attr('height', height);\n",
+       "\n",
+       "        rubberband.attr('width', width);\n",
+       "        rubberband.attr('height', height);\n",
+       "    }\n",
+       "\n",
+       "    // Set the figure to an initial 600x600px, this will subsequently be updated\n",
+       "    // upon first draw.\n",
+       "    this._resize_canvas(600, 600);\n",
+       "\n",
+       "    // Disable right mouse context menu.\n",
+       "    $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
+       "        return false;\n",
+       "    });\n",
+       "\n",
+       "    function set_focus () {\n",
+       "        canvas.focus();\n",
+       "        canvas_div.focus();\n",
+       "    }\n",
+       "\n",
+       "    window.setTimeout(set_focus, 100);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_toolbar = function() {\n",
+       "    var fig = this;\n",
+       "\n",
+       "    var nav_element = $('<div/>')\n",
+       "    nav_element.attr('style', 'width: 100%');\n",
+       "    this.root.append(nav_element);\n",
+       "\n",
+       "    // Define a callback function for later on.\n",
+       "    function toolbar_event(event) {\n",
+       "        return fig.toolbar_button_onclick(event['data']);\n",
+       "    }\n",
+       "    function toolbar_mouse_event(event) {\n",
+       "        return fig.toolbar_button_onmouseover(event['data']);\n",
+       "    }\n",
+       "\n",
+       "    for(var toolbar_ind in mpl.toolbar_items) {\n",
+       "        var name = mpl.toolbar_items[toolbar_ind][0];\n",
+       "        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+       "        var image = mpl.toolbar_items[toolbar_ind][2];\n",
+       "        var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+       "\n",
+       "        if (!name) {\n",
+       "            // put a spacer in here.\n",
+       "            continue;\n",
+       "        }\n",
+       "        var button = $('<button/>');\n",
+       "        button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
+       "                        'ui-button-icon-only');\n",
+       "        button.attr('role', 'button');\n",
+       "        button.attr('aria-disabled', 'false');\n",
+       "        button.click(method_name, toolbar_event);\n",
+       "        button.mouseover(tooltip, toolbar_mouse_event);\n",
+       "\n",
+       "        var icon_img = $('<span/>');\n",
+       "        icon_img.addClass('ui-button-icon-primary ui-icon');\n",
+       "        icon_img.addClass(image);\n",
+       "        icon_img.addClass('ui-corner-all');\n",
+       "\n",
+       "        var tooltip_span = $('<span/>');\n",
+       "        tooltip_span.addClass('ui-button-text');\n",
+       "        tooltip_span.html(tooltip);\n",
+       "\n",
+       "        button.append(icon_img);\n",
+       "        button.append(tooltip_span);\n",
+       "\n",
+       "        nav_element.append(button);\n",
+       "    }\n",
+       "\n",
+       "    var fmt_picker_span = $('<span/>');\n",
+       "\n",
+       "    var fmt_picker = $('<select/>');\n",
+       "    fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
+       "    fmt_picker_span.append(fmt_picker);\n",
+       "    nav_element.append(fmt_picker_span);\n",
+       "    this.format_dropdown = fmt_picker[0];\n",
+       "\n",
+       "    for (var ind in mpl.extensions) {\n",
+       "        var fmt = mpl.extensions[ind];\n",
+       "        var option = $(\n",
+       "            '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);\n",
+       "        fmt_picker.append(option)\n",
+       "    }\n",
+       "\n",
+       "    // Add hover states to the ui-buttons\n",
+       "    $( \".ui-button\" ).hover(\n",
+       "        function() { $(this).addClass(\"ui-state-hover\");},\n",
+       "        function() { $(this).removeClass(\"ui-state-hover\");}\n",
+       "    );\n",
+       "\n",
+       "    var status_bar = $('<span class=\"mpl-message\"/>');\n",
+       "    nav_element.append(status_bar);\n",
+       "    this.message = status_bar[0];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
+       "    // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+       "    // which will in turn request a refresh of the image.\n",
+       "    this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.send_message = function(type, properties) {\n",
+       "    properties['type'] = type;\n",
+       "    properties['figure_id'] = this.id;\n",
+       "    this.ws.send(JSON.stringify(properties));\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.send_draw_message = function() {\n",
+       "    if (!this.waiting) {\n",
+       "        this.waiting = true;\n",
+       "        this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
+       "    }\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+       "    var format_dropdown = fig.format_dropdown;\n",
+       "    var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+       "    fig.ondownload(fig, format);\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
+       "    var size = msg['size'];\n",
+       "    if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
+       "        fig._resize_canvas(size[0], size[1]);\n",
+       "        fig.send_message(\"refresh\", {});\n",
+       "    };\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
+       "    var x0 = msg['x0'];\n",
+       "    var y0 = fig.canvas.height - msg['y0'];\n",
+       "    var x1 = msg['x1'];\n",
+       "    var y1 = fig.canvas.height - msg['y1'];\n",
+       "    x0 = Math.floor(x0) + 0.5;\n",
+       "    y0 = Math.floor(y0) + 0.5;\n",
+       "    x1 = Math.floor(x1) + 0.5;\n",
+       "    y1 = Math.floor(y1) + 0.5;\n",
+       "    var min_x = Math.min(x0, x1);\n",
+       "    var min_y = Math.min(y0, y1);\n",
+       "    var width = Math.abs(x1 - x0);\n",
+       "    var height = Math.abs(y1 - y0);\n",
+       "\n",
+       "    fig.rubberband_context.clearRect(\n",
+       "        0, 0, fig.canvas.width, fig.canvas.height);\n",
+       "\n",
+       "    fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
+       "    // Updates the figure title.\n",
+       "    fig.header.textContent = msg['label'];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
+       "    var cursor = msg['cursor'];\n",
+       "    switch(cursor)\n",
+       "    {\n",
+       "    case 0:\n",
+       "        cursor = 'pointer';\n",
+       "        break;\n",
+       "    case 1:\n",
+       "        cursor = 'default';\n",
+       "        break;\n",
+       "    case 2:\n",
+       "        cursor = 'crosshair';\n",
+       "        break;\n",
+       "    case 3:\n",
+       "        cursor = 'move';\n",
+       "        break;\n",
+       "    }\n",
+       "    fig.rubberband_canvas.style.cursor = cursor;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
+       "    fig.message.textContent = msg['message'];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
+       "    // Request the server to send over a new figure.\n",
+       "    fig.send_draw_message();\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
+       "    fig.image_mode = msg['mode'];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.updated_canvas_event = function() {\n",
+       "    // Called whenever the canvas gets updated.\n",
+       "    this.send_message(\"ack\", {});\n",
+       "}\n",
+       "\n",
+       "// A function to construct a web socket function for onmessage handling.\n",
+       "// Called in the figure constructor.\n",
+       "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
+       "    return function socket_on_message(evt) {\n",
+       "        if (evt.data instanceof Blob) {\n",
+       "            /* FIXME: We get \"Resource interpreted as Image but\n",
+       "             * transferred with MIME type text/plain:\" errors on\n",
+       "             * Chrome.  But how to set the MIME type?  It doesn't seem\n",
+       "             * to be part of the websocket stream */\n",
+       "            evt.data.type = \"image/png\";\n",
+       "\n",
+       "            /* Free the memory for the previous frames */\n",
+       "            if (fig.imageObj.src) {\n",
+       "                (window.URL || window.webkitURL).revokeObjectURL(\n",
+       "                    fig.imageObj.src);\n",
+       "            }\n",
+       "\n",
+       "            fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+       "                evt.data);\n",
+       "            fig.updated_canvas_event();\n",
+       "            fig.waiting = false;\n",
+       "            return;\n",
+       "        }\n",
+       "        else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
+       "            fig.imageObj.src = evt.data;\n",
+       "            fig.updated_canvas_event();\n",
+       "            fig.waiting = false;\n",
+       "            return;\n",
+       "        }\n",
+       "\n",
+       "        var msg = JSON.parse(evt.data);\n",
+       "        var msg_type = msg['type'];\n",
+       "\n",
+       "        // Call the  \"handle_{type}\" callback, which takes\n",
+       "        // the figure and JSON message as its only arguments.\n",
+       "        try {\n",
+       "            var callback = fig[\"handle_\" + msg_type];\n",
+       "        } catch (e) {\n",
+       "            console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
+       "            return;\n",
+       "        }\n",
+       "\n",
+       "        if (callback) {\n",
+       "            try {\n",
+       "                // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+       "                callback(fig, msg);\n",
+       "            } catch (e) {\n",
+       "                console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
+       "            }\n",
+       "        }\n",
+       "    };\n",
+       "}\n",
+       "\n",
+       "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
+       "mpl.findpos = function(e) {\n",
+       "    //this section is from http://www.quirksmode.org/js/events_properties.html\n",
+       "    var targ;\n",
+       "    if (!e)\n",
+       "        e = window.event;\n",
+       "    if (e.target)\n",
+       "        targ = e.target;\n",
+       "    else if (e.srcElement)\n",
+       "        targ = e.srcElement;\n",
+       "    if (targ.nodeType == 3) // defeat Safari bug\n",
+       "        targ = targ.parentNode;\n",
+       "\n",
+       "    // jQuery normalizes the pageX and pageY\n",
+       "    // pageX,Y are the mouse positions relative to the document\n",
+       "    // offset() returns the position of the element relative to the document\n",
+       "    var x = e.pageX - $(targ).offset().left;\n",
+       "    var y = e.pageY - $(targ).offset().top;\n",
+       "\n",
+       "    return {\"x\": x, \"y\": y};\n",
+       "};\n",
+       "\n",
+       "/*\n",
+       " * return a copy of an object with only non-object keys\n",
+       " * we need this to avoid circular references\n",
+       " * http://stackoverflow.com/a/24161582/3208463\n",
+       " */\n",
+       "function simpleKeys (original) {\n",
+       "  return Object.keys(original).reduce(function (obj, key) {\n",
+       "    if (typeof original[key] !== 'object')\n",
+       "        obj[key] = original[key]\n",
+       "    return obj;\n",
+       "  }, {});\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.mouse_event = function(event, name) {\n",
+       "    var canvas_pos = mpl.findpos(event)\n",
+       "\n",
+       "    if (name === 'button_press')\n",
+       "    {\n",
+       "        this.canvas.focus();\n",
+       "        this.canvas_div.focus();\n",
+       "    }\n",
+       "\n",
+       "    var x = canvas_pos.x;\n",
+       "    var y = canvas_pos.y;\n",
+       "\n",
+       "    this.send_message(name, {x: x, y: y, button: event.button,\n",
+       "                             step: event.step,\n",
+       "                             guiEvent: simpleKeys(event)});\n",
+       "\n",
+       "    /* This prevents the web browser from automatically changing to\n",
+       "     * the text insertion cursor when the button is pressed.  We want\n",
+       "     * to control all of the cursor setting manually through the\n",
+       "     * 'cursor' event from matplotlib */\n",
+       "    event.preventDefault();\n",
+       "    return false;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+       "    // Handle any extra behaviour associated with a key event\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.key_event = function(event, name) {\n",
+       "\n",
+       "    // Prevent repeat events\n",
+       "    if (name == 'key_press')\n",
+       "    {\n",
+       "        if (event.which === this._key)\n",
+       "            return;\n",
+       "        else\n",
+       "            this._key = event.which;\n",
+       "    }\n",
+       "    if (name == 'key_release')\n",
+       "        this._key = null;\n",
+       "\n",
+       "    var value = '';\n",
+       "    if (event.ctrlKey && event.which != 17)\n",
+       "        value += \"ctrl+\";\n",
+       "    if (event.altKey && event.which != 18)\n",
+       "        value += \"alt+\";\n",
+       "    if (event.shiftKey && event.which != 16)\n",
+       "        value += \"shift+\";\n",
+       "\n",
+       "    value += 'k';\n",
+       "    value += event.which.toString();\n",
+       "\n",
+       "    this._key_event_extra(event, name);\n",
+       "\n",
+       "    this.send_message(name, {key: value,\n",
+       "                             guiEvent: simpleKeys(event)});\n",
+       "    return false;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
+       "    if (name == 'download') {\n",
+       "        this.handle_save(this, null);\n",
+       "    } else {\n",
+       "        this.send_message(\"toolbar_button\", {name: name});\n",
+       "    }\n",
+       "};\n",
+       "\n",
+       "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
+       "    this.message.textContent = tooltip;\n",
+       "};\n",
+       "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to  previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\" [...]
+       "\n",
+       "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
+       "\n",
+       "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
+       "    // Create a \"websocket\"-like object which calls the given IPython comm\n",
+       "    // object with the appropriate methods. Currently this is a non binary\n",
+       "    // socket, so there is still some room for performance tuning.\n",
+       "    var ws = {};\n",
+       "\n",
+       "    ws.close = function() {\n",
+       "        comm.close()\n",
+       "    };\n",
+       "    ws.send = function(m) {\n",
+       "        //console.log('sending', m);\n",
+       "        comm.send(m);\n",
+       "    };\n",
+       "    // Register the callback with on_msg.\n",
+       "    comm.on_msg(function(msg) {\n",
+       "        //console.log('receiving', msg['content']['data'], msg);\n",
+       "        // Pass the mpl event to the overriden (by mpl) onmessage function.\n",
+       "        ws.onmessage(msg['content']['data'])\n",
+       "    });\n",
+       "    return ws;\n",
+       "}\n",
+       "\n",
+       "mpl.mpl_figure_comm = function(comm, msg) {\n",
+       "    // This is the function which gets called when the mpl process\n",
+       "    // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+       "\n",
+       "    var id = msg.content.data.id;\n",
+       "    // Get hold of the div created by the display call when the Comm\n",
+       "    // socket was opened in Python.\n",
+       "    var element = $(\"#\" + id);\n",
+       "    var ws_proxy = comm_websocket_adapter(comm)\n",
+       "\n",
+       "    function ondownload(figure, format) {\n",
+       "        window.open(figure.imageObj.src);\n",
+       "    }\n",
+       "\n",
+       "    var fig = new mpl.figure(id, ws_proxy,\n",
+       "                           ondownload,\n",
+       "                           element.get(0));\n",
+       "\n",
+       "    // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+       "    // web socket which is closed, not our websocket->open comm proxy.\n",
+       "    ws_proxy.onopen();\n",
+       "\n",
+       "    fig.parent_element = element.get(0);\n",
+       "    fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n",
+       "    if (!fig.cell_info) {\n",
+       "        console.error(\"Failed to find cell for figure\", id, fig);\n",
+       "        return;\n",
+       "    }\n",
+       "\n",
+       "    var output_index = fig.cell_info[2]\n",
+       "    var cell = fig.cell_info[0];\n",
+       "\n",
+       "};\n",
+       "\n",
+       "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
+       "    fig.root.unbind('remove')\n",
+       "\n",
+       "    // Update the output cell to use the data from the current canvas.\n",
+       "    fig.push_to_output();\n",
+       "    var dataURL = fig.canvas.toDataURL();\n",
+       "    // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+       "    // the notebook keyboard shortcuts fail.\n",
+       "    IPython.keyboard_manager.enable()\n",
+       "    $(fig.parent_element).html('<img src=\"' + dataURL + '\">');\n",
+       "    fig.close_ws(fig, msg);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.close_ws = function(fig, msg){\n",
+       "    fig.send_message('closing', msg);\n",
+       "    // fig.ws.close()\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
+       "    // Turn the data on the canvas into data in the output cell.\n",
+       "    var dataURL = this.canvas.toDataURL();\n",
+       "    this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\">';\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.updated_canvas_event = function() {\n",
+       "    // Tell IPython that the notebook contents must change.\n",
+       "    IPython.notebook.set_dirty(true);\n",
+       "    this.send_message(\"ack\", {});\n",
+       "    var fig = this;\n",
+       "    // Wait a second, then push the new image to the DOM so\n",
+       "    // that it is saved nicely (might be nice to debounce this).\n",
+       "    setTimeout(function () { fig.push_to_output() }, 1000);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_toolbar = function() {\n",
+       "    var fig = this;\n",
+       "\n",
+       "    var nav_element = $('<div/>')\n",
+       "    nav_element.attr('style', 'width: 100%');\n",
+       "    this.root.append(nav_element);\n",
+       "\n",
+       "    // Define a callback function for later on.\n",
+       "    function toolbar_event(event) {\n",
+       "        return fig.toolbar_button_onclick(event['data']);\n",
+       "    }\n",
+       "    function toolbar_mouse_event(event) {\n",
+       "        return fig.toolbar_button_onmouseover(event['data']);\n",
+       "    }\n",
+       "\n",
+       "    for(var toolbar_ind in mpl.toolbar_items){\n",
+       "        var name = mpl.toolbar_items[toolbar_ind][0];\n",
+       "        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+       "        var image = mpl.toolbar_items[toolbar_ind][2];\n",
+       "        var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+       "\n",
+       "        if (!name) { continue; };\n",
+       "\n",
+       "        var button = $('<button class=\"btn btn-default\" href=\"#\" title=\"' + name + '\"><i class=\"fa ' + image + ' fa-lg\"></i></button>');\n",
+       "        button.click(method_name, toolbar_event);\n",
+       "        button.mouseover(tooltip, toolbar_mouse_event);\n",
+       "        nav_element.append(button);\n",
+       "    }\n",
+       "\n",
+       "    // Add the status bar.\n",
+       "    var status_bar = $('<span class=\"mpl-message\" style=\"text-align:right; float: right;\"/>');\n",
+       "    nav_element.append(status_bar);\n",
+       "    this.message = status_bar[0];\n",
+       "\n",
+       "    // Add the close button to the window.\n",
+       "    var buttongrp = $('<div class=\"btn-group inline pull-right\"></div>');\n",
+       "    var button = $('<button class=\"btn btn-mini btn-primary\" href=\"#\" title=\"Stop Interaction\"><i class=\"fa fa-power-off icon-remove icon-large\"></i></button>');\n",
+       "    button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
+       "    button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
+       "    buttongrp.append(button);\n",
+       "    var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
+       "    titlebar.prepend(buttongrp);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._root_extra_style = function(el){\n",
+       "    var fig = this\n",
+       "    el.on(\"remove\", function(){\n",
+       "\tfig.close_ws(fig, {});\n",
+       "    });\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._canvas_extra_style = function(el){\n",
+       "    // this is important to make the div 'focusable\n",
+       "    el.attr('tabindex', 0)\n",
+       "    // reach out to IPython and tell the keyboard manager to turn it's self\n",
+       "    // off when our div gets focus\n",
+       "\n",
+       "    // location in version 3\n",
+       "    if (IPython.notebook.keyboard_manager) {\n",
+       "        IPython.notebook.keyboard_manager.register_events(el);\n",
+       "    }\n",
+       "    else {\n",
+       "        // location in version 2\n",
+       "        IPython.keyboard_manager.register_events(el);\n",
+       "    }\n",
+       "\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+       "    var manager = IPython.notebook.keyboard_manager;\n",
+       "    if (!manager)\n",
+       "        manager = IPython.keyboard_manager;\n",
+       "\n",
+       "    // Check for shift+enter\n",
+       "    if (event.shiftKey && event.which == 13) {\n",
+       "        this.canvas_div.blur();\n",
+       "        event.shiftKey = false;\n",
+       "        // Send a \"J\" for go to next cell\n",
+       "        event.which = 74;\n",
+       "        event.keyCode = 74;\n",
+       "        manager.command_mode();\n",
+       "        manager.handle_keydown(event);\n",
+       "    }\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+       "    fig.ondownload(fig, null);\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.find_output_cell = function(html_output) {\n",
+       "    // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+       "    // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+       "    // IPython event is triggered only after the cells have been serialised, which for\n",
+       "    // our purposes (turning an active figure into a static one), is too late.\n",
+       "    var cells = IPython.notebook.get_cells();\n",
+       "    var ncells = cells.length;\n",
+       "    for (var i=0; i<ncells; i++) {\n",
+       "        var cell = cells[i];\n",
+       "        if (cell.cell_type === 'code'){\n",
+       "            for (var j=0; j<cell.output_area.outputs.length; j++) {\n",
+       "                var data = cell.output_area.outputs[j];\n",
+       "                if (data.data) {\n",
+       "                    // IPython >= 3 moved mimebundle to data attribute of output\n",
+       "                    data = data.data;\n",
+       "                }\n",
+       "                if (data['text/html'] == html_output) {\n",
+       "                    return [cell, data, j];\n",
+       "                }\n",
+       "            }\n",
+       "        }\n",
+       "    }\n",
+       "}\n",
+       "\n",
+       "// Register the function which deals with the matplotlib target/channel.\n",
+       "// The kernel may be null if the page has been refreshed.\n",
+       "if (IPython.notebook.kernel != null) {\n",
+       "    IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
+       "}\n"
+      ],
+      "text/plain": [
+       "<IPython.core.display.Javascript object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA8AAAAFACAYAAABz+RVqAAAgAElEQVR4XuxdB2Acxdn9Tr03W3LvvWDjgiE0U4xNx9Q0QggBQklIQgr5SUIIIZCQQIDQA4EAofdujAvNjsHY4N4tV7mo937/e7M70vqQbEvouDvpGzOcbm92dvZNffOV8YkGRUARUAQUAUVAEVAEFAFFQBFQBBQBRaALIODrAu+or6gIKAKKgCKgCCgCioAioAgoAoqAIqAIiBJgbQSKgCKgCCgCioAioAgoAoqAIqAIKAJdAgElwF2imvUlFQFFQBFQBBQBRUARUAQUAUVAEVAElABrG1AEFAFFQBFQBBQBRUARUAQUAUVAEegSCCgB7hLVrC+pCCgCioAioAgoAoqAIqAIKAKKgCKgBFjbgCKgCCgCioAioAgoAoqAIqAIKAKKQJdAQAlwl6hmfUl [...]
+      ],
+      "text/plain": [
+       "<IPython.core.display.HTML object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<matplotlib.axes._subplots.AxesSubplot at 0x11031470>"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "%matplotlib notebook\n",
+    "fig, (axr, axg, axb) = plt.subplots(1,3, figsize=(12, 4), sharex=True, sharey=True)\n",
+    "plot.show((src, 1), ax=axr, cmap='Reds', title='red channel')\n",
+    "plot.show((src, 2), ax=axg, cmap='Greens', title='green channel')\n",
+    "plot.show((src, 3), ax=axb, cmap='Blues', title='blue channel')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### If we want to see a histogram of the data we use the plot.show_hist function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaAAAAEZCAYAAADR8/HkAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3X+UXWV97/H3JwlJAPmVIhnNr0EECVYbQ8VeaUsU+aFc\ngdolgkWJQG8VbLWuK4L2klBLFZfaUBUuYjBArxcpbRUrhaBm7LJLAfkhaEAilwQSkhEFQgIh5Mf3\n/rGfCYdwzuRMss/ZZz/zea111uzznL33eT6zZ+Y7+9n77K2IwMzMrNvGVN0BMzMbnVyAzMysEi5A\nZmZWCRcgMzOrhAuQmZlVwgXIzMwq4QJktpMkbZW0RdL0qvtiVkcuQGZNSFqeCsyJDW1HpbYnUtOC\n9Hi6jfUtSste2KEum9XOuKo7YNajIj1azxDxsTLXVxZJ4yJiczfey2xXeA/IbCelPZqtQ0Nwkj4q\n6VeS [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x129de390>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "%matplotlib inline\n",
+    "plot.show_hist(src)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Any of the optional pyplot histogram parameters can be passed to tweak the representation"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZMAAAEZCAYAAABSN8jfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJztvXuYHOV15//5zoyk0V0C6wISaMRFIGFhIYKwTQhysMGX\nGLPehGAnsWVIsjb4Z7zJ+ofk2Ja9sU3wJutLNubxJUbgxQvYcQxkCRfFjG2MACEEDEhCQmh0gxnd\nR5qLZqZ7zv5R1VLPdM+oR93V3VVzPnrq6X5Pv1V1zlSpTr3nvBeZGY7jOI5TDDWVVsBxHMeJP+5M\nHMdxnKJxZ+I4juMUjTsTx3Ecp2jcmTiO4zhF487EcRzHKRp3Jk5FkbRSUp+kHw1Rp09SWtKFRZ7r\nzvBYXyrmOM7wkdQc/u1/r8D6Q14rSXOy7otJpdXWORncmTj9kPS18D/pnVmyfwpl67NkfxnKVpfg\ntEUN [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x14b3a860>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "plot.show_hist(world, bins=50, lw=0.0, stacked=False, alpha=0.3, \n",
+    "               histtype='stepfilled', title=\"World Histogram overlaid\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZYAAAEZCAYAAAC0HgObAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJztnXmYFOW1/z+HHZHFCGJkG2RxN6MR5GpQxCzghonG6C8u\n4x4VkxuzSEwEY0xujDdqxKu4hmiSi4nmCjFEkWC7RRaVEQRGQAEB2WUZFllmzu+Pqoa2nemu7q7q\n7nrnfJ6nnp636j1V59tvT5161xJVxTAMwzDColmpHTAMwzDcwgKLYRiGESoWWAzDMIxQscBiGIZh\nhIoFFsMwDCNULLAYhmEYoWKBxSg7RGSMiNSLyOMZ8tSLSJ2IHFvgtX7vn2t0IecxgiEivZJlF+I5\nx/vnvDuscxqFYYHFyIqI/NL/x/19yr7/8ffNTtl3jb9vagiXLWiCVUpw+lva/qX+/nP8XS8A9wLT\nA5zz [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x10d87b38>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "plot.show_hist(world, bins=20, lw=2.0, stacked=True,\n",
+    "               alpha=0.8, histtype='step', normed=True, \n",
+    "               title=\"World Histogram stacked\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### See matplotlib.pyplot.hist documentation at http://matplotlib.org/api/pyplot_api.html for a list of these parameters and available options"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAABDAAAAHzCAYAAADW0O2rAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3X20ZFd53/nvs/c+p6ru7ZZavIlIgMSrIxwcMAkk9iTW\nSvwWYwhDvLBxcFDMkBhwYpJZXoYZG4Y4hhCPB2wHs5wXGzzBg0k8axlmMBgtWx7jF8yw0CAbjDFB\nvNmIF6lf7606Z+/9zB/71O2mkVqCkvrevvp91rpdXeeeqjq161Td2s95nueYuyMiIiIiIiIicpCF\n/d4AEREREREREZG7owCGiIiIiIiIiBx4CmCIiIiIiIiIyIGnAIaIiIiIiIiIHHgKYIiIiIiIiIjI\ngacAhoiIiIiIiIgceApgiIiIiIjIPWZmrzCzama/cIF1qpkVM/u6DR/rF6f7evkm9yMih4MCGCIi\nIiIi [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x15b710b8>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15,7))\n",
+    "plot.show(world, ax=ax1)\n",
+    "plot.show_hist(world, bins=50, lw=0.0, stacked=False, alpha=0.3, \n",
+    "               histtype='stepfilled', title=\"World Histogram\")\n",
+    "fig.tight_layout()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Show contours from a raster"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "C:\\Anaconda\\lib\\site-packages\\matplotlib\\contour.py:370: RuntimeWarning: invalid value encountered in true_divide\n",
+      "  dist = np.add.reduce(([(abs(s)[i] / L[i]) for i in range(xsize)]), -1)\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<matplotlib.axes._subplots.AxesSubplot at 0x14c7cc88>"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAr8AAAI5CAYAAACo8FIrAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsvXd4HNd5t33PzvaKXohOohDsvYgSRVISRfVmUZKbbL9O\n/LrFjh23JK/jJP7sfP5iO4nzOpZsxXKRrUpZstUsUWInxU6wgARBgOhtd7G9zc7M9weXDEWiDEAA\nBIm9r2sv7C7Ozjk7e86Z3zznOc8jqKpKmjRp0qRJkyZNmjRTAd3VbkCaNGnSpEmTJk2aNBNFWvym\nSZMmTZo0adKkmTKkxW+aNGnSpEmTJk2aKUNa/KZJkyZNmjRp0qSZMqTFb5o0adKkSZMmTZopQ1r8\npkmTJk2aNGnSpJkyaBa/giDoBEE4KAjCq6nXmYIg/FkQhFOCILwlCIJr/JqZJk2aNGnSpEmTJs2V\nMxLL [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x14ab7550>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "fig, ax = plt.subplots(1, figsize=(12, 12))\n",
+    "plot.show((world, 1), cmap='Greys_r', interpolation='none', ax=ax)\n",
+    "ax.set_xlim(-50, 0)\n",
+    "ax.set_ylim(0, 40)\n",
+    "\n",
+    "plot.show((world, 1), contour=True, ax=ax)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### And of course there are many ways to easily customize this output ..."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/javascript": [
+       "/* Put everything inside the global mpl namespace */\n",
+       "window.mpl = {};\n",
+       "\n",
+       "mpl.get_websocket_type = function() {\n",
+       "    if (typeof(WebSocket) !== 'undefined') {\n",
+       "        return WebSocket;\n",
+       "    } else if (typeof(MozWebSocket) !== 'undefined') {\n",
+       "        return MozWebSocket;\n",
+       "    } else {\n",
+       "        alert('Your browser does not have WebSocket support.' +\n",
+       "              'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+       "              'Firefox 4 and 5 are also supported but you ' +\n",
+       "              'have to enable WebSockets in about:config.');\n",
+       "    };\n",
+       "}\n",
+       "\n",
+       "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
+       "    this.id = figure_id;\n",
+       "\n",
+       "    this.ws = websocket;\n",
+       "\n",
+       "    this.supports_binary = (this.ws.binaryType != undefined);\n",
+       "\n",
+       "    if (!this.supports_binary) {\n",
+       "        var warnings = document.getElementById(\"mpl-warnings\");\n",
+       "        if (warnings) {\n",
+       "            warnings.style.display = 'block';\n",
+       "            warnings.textContent = (\n",
+       "                \"This browser does not support binary websocket messages. \" +\n",
+       "                    \"Performance may be slow.\");\n",
+       "        }\n",
+       "    }\n",
+       "\n",
+       "    this.imageObj = new Image();\n",
+       "\n",
+       "    this.context = undefined;\n",
+       "    this.message = undefined;\n",
+       "    this.canvas = undefined;\n",
+       "    this.rubberband_canvas = undefined;\n",
+       "    this.rubberband_context = undefined;\n",
+       "    this.format_dropdown = undefined;\n",
+       "\n",
+       "    this.image_mode = 'full';\n",
+       "\n",
+       "    this.root = $('<div/>');\n",
+       "    this._root_extra_style(this.root)\n",
+       "    this.root.attr('style', 'display: inline-block');\n",
+       "\n",
+       "    $(parent_element).append(this.root);\n",
+       "\n",
+       "    this._init_header(this);\n",
+       "    this._init_canvas(this);\n",
+       "    this._init_toolbar(this);\n",
+       "\n",
+       "    var fig = this;\n",
+       "\n",
+       "    this.waiting = false;\n",
+       "\n",
+       "    this.ws.onopen =  function () {\n",
+       "            fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
+       "            fig.send_message(\"send_image_mode\", {});\n",
+       "            fig.send_message(\"refresh\", {});\n",
+       "        }\n",
+       "\n",
+       "    this.imageObj.onload = function() {\n",
+       "            if (fig.image_mode == 'full') {\n",
+       "                // Full images could contain transparency (where diff images\n",
+       "                // almost always do), so we need to clear the canvas so that\n",
+       "                // there is no ghosting.\n",
+       "                fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+       "            }\n",
+       "            fig.context.drawImage(fig.imageObj, 0, 0);\n",
+       "        };\n",
+       "\n",
+       "    this.imageObj.onunload = function() {\n",
+       "        this.ws.close();\n",
+       "    }\n",
+       "\n",
+       "    this.ws.onmessage = this._make_on_message_function(this);\n",
+       "\n",
+       "    this.ondownload = ondownload;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_header = function() {\n",
+       "    var titlebar = $(\n",
+       "        '<div class=\"ui-dialog-titlebar ui-widget-header ui-corner-all ' +\n",
+       "        'ui-helper-clearfix\"/>');\n",
+       "    var titletext = $(\n",
+       "        '<div class=\"ui-dialog-title\" style=\"width: 100%; ' +\n",
+       "        'text-align: center; padding: 3px;\"/>');\n",
+       "    titlebar.append(titletext)\n",
+       "    this.root.append(titlebar);\n",
+       "    this.header = titletext[0];\n",
+       "}\n",
+       "\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
+       "\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
+       "\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_canvas = function() {\n",
+       "    var fig = this;\n",
+       "\n",
+       "    var canvas_div = $('<div/>');\n",
+       "\n",
+       "    canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
+       "\n",
+       "    function canvas_keyboard_event(event) {\n",
+       "        return fig.key_event(event, event['data']);\n",
+       "    }\n",
+       "\n",
+       "    canvas_div.keydown('key_press', canvas_keyboard_event);\n",
+       "    canvas_div.keyup('key_release', canvas_keyboard_event);\n",
+       "    this.canvas_div = canvas_div\n",
+       "    this._canvas_extra_style(canvas_div)\n",
+       "    this.root.append(canvas_div);\n",
+       "\n",
+       "    var canvas = $('<canvas/>');\n",
+       "    canvas.addClass('mpl-canvas');\n",
+       "    canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
+       "\n",
+       "    this.canvas = canvas[0];\n",
+       "    this.context = canvas[0].getContext(\"2d\");\n",
+       "\n",
+       "    var rubberband = $('<canvas/>');\n",
+       "    rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
+       "\n",
+       "    var pass_mouse_events = true;\n",
+       "\n",
+       "    canvas_div.resizable({\n",
+       "        start: function(event, ui) {\n",
+       "            pass_mouse_events = false;\n",
+       "        },\n",
+       "        resize: function(event, ui) {\n",
+       "            fig.request_resize(ui.size.width, ui.size.height);\n",
+       "        },\n",
+       "        stop: function(event, ui) {\n",
+       "            pass_mouse_events = true;\n",
+       "            fig.request_resize(ui.size.width, ui.size.height);\n",
+       "        },\n",
+       "    });\n",
+       "\n",
+       "    function mouse_event_fn(event) {\n",
+       "        if (pass_mouse_events)\n",
+       "            return fig.mouse_event(event, event['data']);\n",
+       "    }\n",
+       "\n",
+       "    rubberband.mousedown('button_press', mouse_event_fn);\n",
+       "    rubberband.mouseup('button_release', mouse_event_fn);\n",
+       "    // Throttle sequential mouse events to 1 every 20ms.\n",
+       "    rubberband.mousemove('motion_notify', mouse_event_fn);\n",
+       "\n",
+       "    rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
+       "    rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
+       "\n",
+       "    canvas_div.on(\"wheel\", function (event) {\n",
+       "        event = event.originalEvent;\n",
+       "        event['data'] = 'scroll'\n",
+       "        if (event.deltaY < 0) {\n",
+       "            event.step = 1;\n",
+       "        } else {\n",
+       "            event.step = -1;\n",
+       "        }\n",
+       "        mouse_event_fn(event);\n",
+       "    });\n",
+       "\n",
+       "    canvas_div.append(canvas);\n",
+       "    canvas_div.append(rubberband);\n",
+       "\n",
+       "    this.rubberband = rubberband;\n",
+       "    this.rubberband_canvas = rubberband[0];\n",
+       "    this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
+       "    this.rubberband_context.strokeStyle = \"#000000\";\n",
+       "\n",
+       "    this._resize_canvas = function(width, height) {\n",
+       "        // Keep the size of the canvas, canvas container, and rubber band\n",
+       "        // canvas in synch.\n",
+       "        canvas_div.css('width', width)\n",
+       "        canvas_div.css('height', height)\n",
+       "\n",
+       "        canvas.attr('width', width);\n",
+       "        canvas.attr('height', height);\n",
+       "\n",
+       "        rubberband.attr('width', width);\n",
+       "        rubberband.attr('height', height);\n",
+       "    }\n",
+       "\n",
+       "    // Set the figure to an initial 600x600px, this will subsequently be updated\n",
+       "    // upon first draw.\n",
+       "    this._resize_canvas(600, 600);\n",
+       "\n",
+       "    // Disable right mouse context menu.\n",
+       "    $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
+       "        return false;\n",
+       "    });\n",
+       "\n",
+       "    function set_focus () {\n",
+       "        canvas.focus();\n",
+       "        canvas_div.focus();\n",
+       "    }\n",
+       "\n",
+       "    window.setTimeout(set_focus, 100);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_toolbar = function() {\n",
+       "    var fig = this;\n",
+       "\n",
+       "    var nav_element = $('<div/>')\n",
+       "    nav_element.attr('style', 'width: 100%');\n",
+       "    this.root.append(nav_element);\n",
+       "\n",
+       "    // Define a callback function for later on.\n",
+       "    function toolbar_event(event) {\n",
+       "        return fig.toolbar_button_onclick(event['data']);\n",
+       "    }\n",
+       "    function toolbar_mouse_event(event) {\n",
+       "        return fig.toolbar_button_onmouseover(event['data']);\n",
+       "    }\n",
+       "\n",
+       "    for(var toolbar_ind in mpl.toolbar_items) {\n",
+       "        var name = mpl.toolbar_items[toolbar_ind][0];\n",
+       "        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+       "        var image = mpl.toolbar_items[toolbar_ind][2];\n",
+       "        var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+       "\n",
+       "        if (!name) {\n",
+       "            // put a spacer in here.\n",
+       "            continue;\n",
+       "        }\n",
+       "        var button = $('<button/>');\n",
+       "        button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
+       "                        'ui-button-icon-only');\n",
+       "        button.attr('role', 'button');\n",
+       "        button.attr('aria-disabled', 'false');\n",
+       "        button.click(method_name, toolbar_event);\n",
+       "        button.mouseover(tooltip, toolbar_mouse_event);\n",
+       "\n",
+       "        var icon_img = $('<span/>');\n",
+       "        icon_img.addClass('ui-button-icon-primary ui-icon');\n",
+       "        icon_img.addClass(image);\n",
+       "        icon_img.addClass('ui-corner-all');\n",
+       "\n",
+       "        var tooltip_span = $('<span/>');\n",
+       "        tooltip_span.addClass('ui-button-text');\n",
+       "        tooltip_span.html(tooltip);\n",
+       "\n",
+       "        button.append(icon_img);\n",
+       "        button.append(tooltip_span);\n",
+       "\n",
+       "        nav_element.append(button);\n",
+       "    }\n",
+       "\n",
+       "    var fmt_picker_span = $('<span/>');\n",
+       "\n",
+       "    var fmt_picker = $('<select/>');\n",
+       "    fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
+       "    fmt_picker_span.append(fmt_picker);\n",
+       "    nav_element.append(fmt_picker_span);\n",
+       "    this.format_dropdown = fmt_picker[0];\n",
+       "\n",
+       "    for (var ind in mpl.extensions) {\n",
+       "        var fmt = mpl.extensions[ind];\n",
+       "        var option = $(\n",
+       "            '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);\n",
+       "        fmt_picker.append(option)\n",
+       "    }\n",
+       "\n",
+       "    // Add hover states to the ui-buttons\n",
+       "    $( \".ui-button\" ).hover(\n",
+       "        function() { $(this).addClass(\"ui-state-hover\");},\n",
+       "        function() { $(this).removeClass(\"ui-state-hover\");}\n",
+       "    );\n",
+       "\n",
+       "    var status_bar = $('<span class=\"mpl-message\"/>');\n",
+       "    nav_element.append(status_bar);\n",
+       "    this.message = status_bar[0];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
+       "    // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+       "    // which will in turn request a refresh of the image.\n",
+       "    this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.send_message = function(type, properties) {\n",
+       "    properties['type'] = type;\n",
+       "    properties['figure_id'] = this.id;\n",
+       "    this.ws.send(JSON.stringify(properties));\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.send_draw_message = function() {\n",
+       "    if (!this.waiting) {\n",
+       "        this.waiting = true;\n",
+       "        this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
+       "    }\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+       "    var format_dropdown = fig.format_dropdown;\n",
+       "    var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+       "    fig.ondownload(fig, format);\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
+       "    var size = msg['size'];\n",
+       "    if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
+       "        fig._resize_canvas(size[0], size[1]);\n",
+       "        fig.send_message(\"refresh\", {});\n",
+       "    };\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
+       "    var x0 = msg['x0'];\n",
+       "    var y0 = fig.canvas.height - msg['y0'];\n",
+       "    var x1 = msg['x1'];\n",
+       "    var y1 = fig.canvas.height - msg['y1'];\n",
+       "    x0 = Math.floor(x0) + 0.5;\n",
+       "    y0 = Math.floor(y0) + 0.5;\n",
+       "    x1 = Math.floor(x1) + 0.5;\n",
+       "    y1 = Math.floor(y1) + 0.5;\n",
+       "    var min_x = Math.min(x0, x1);\n",
+       "    var min_y = Math.min(y0, y1);\n",
+       "    var width = Math.abs(x1 - x0);\n",
+       "    var height = Math.abs(y1 - y0);\n",
+       "\n",
+       "    fig.rubberband_context.clearRect(\n",
+       "        0, 0, fig.canvas.width, fig.canvas.height);\n",
+       "\n",
+       "    fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
+       "    // Updates the figure title.\n",
+       "    fig.header.textContent = msg['label'];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
+       "    var cursor = msg['cursor'];\n",
+       "    switch(cursor)\n",
+       "    {\n",
+       "    case 0:\n",
+       "        cursor = 'pointer';\n",
+       "        break;\n",
+       "    case 1:\n",
+       "        cursor = 'default';\n",
+       "        break;\n",
+       "    case 2:\n",
+       "        cursor = 'crosshair';\n",
+       "        break;\n",
+       "    case 3:\n",
+       "        cursor = 'move';\n",
+       "        break;\n",
+       "    }\n",
+       "    fig.rubberband_canvas.style.cursor = cursor;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
+       "    fig.message.textContent = msg['message'];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
+       "    // Request the server to send over a new figure.\n",
+       "    fig.send_draw_message();\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
+       "    fig.image_mode = msg['mode'];\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.updated_canvas_event = function() {\n",
+       "    // Called whenever the canvas gets updated.\n",
+       "    this.send_message(\"ack\", {});\n",
+       "}\n",
+       "\n",
+       "// A function to construct a web socket function for onmessage handling.\n",
+       "// Called in the figure constructor.\n",
+       "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
+       "    return function socket_on_message(evt) {\n",
+       "        if (evt.data instanceof Blob) {\n",
+       "            /* FIXME: We get \"Resource interpreted as Image but\n",
+       "             * transferred with MIME type text/plain:\" errors on\n",
+       "             * Chrome.  But how to set the MIME type?  It doesn't seem\n",
+       "             * to be part of the websocket stream */\n",
+       "            evt.data.type = \"image/png\";\n",
+       "\n",
+       "            /* Free the memory for the previous frames */\n",
+       "            if (fig.imageObj.src) {\n",
+       "                (window.URL || window.webkitURL).revokeObjectURL(\n",
+       "                    fig.imageObj.src);\n",
+       "            }\n",
+       "\n",
+       "            fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+       "                evt.data);\n",
+       "            fig.updated_canvas_event();\n",
+       "            fig.waiting = false;\n",
+       "            return;\n",
+       "        }\n",
+       "        else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
+       "            fig.imageObj.src = evt.data;\n",
+       "            fig.updated_canvas_event();\n",
+       "            fig.waiting = false;\n",
+       "            return;\n",
+       "        }\n",
+       "\n",
+       "        var msg = JSON.parse(evt.data);\n",
+       "        var msg_type = msg['type'];\n",
+       "\n",
+       "        // Call the  \"handle_{type}\" callback, which takes\n",
+       "        // the figure and JSON message as its only arguments.\n",
+       "        try {\n",
+       "            var callback = fig[\"handle_\" + msg_type];\n",
+       "        } catch (e) {\n",
+       "            console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
+       "            return;\n",
+       "        }\n",
+       "\n",
+       "        if (callback) {\n",
+       "            try {\n",
+       "                // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+       "                callback(fig, msg);\n",
+       "            } catch (e) {\n",
+       "                console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
+       "            }\n",
+       "        }\n",
+       "    };\n",
+       "}\n",
+       "\n",
+       "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
+       "mpl.findpos = function(e) {\n",
+       "    //this section is from http://www.quirksmode.org/js/events_properties.html\n",
+       "    var targ;\n",
+       "    if (!e)\n",
+       "        e = window.event;\n",
+       "    if (e.target)\n",
+       "        targ = e.target;\n",
+       "    else if (e.srcElement)\n",
+       "        targ = e.srcElement;\n",
+       "    if (targ.nodeType == 3) // defeat Safari bug\n",
+       "        targ = targ.parentNode;\n",
+       "\n",
+       "    // jQuery normalizes the pageX and pageY\n",
+       "    // pageX,Y are the mouse positions relative to the document\n",
+       "    // offset() returns the position of the element relative to the document\n",
+       "    var x = e.pageX - $(targ).offset().left;\n",
+       "    var y = e.pageY - $(targ).offset().top;\n",
+       "\n",
+       "    return {\"x\": x, \"y\": y};\n",
+       "};\n",
+       "\n",
+       "/*\n",
+       " * return a copy of an object with only non-object keys\n",
+       " * we need this to avoid circular references\n",
+       " * http://stackoverflow.com/a/24161582/3208463\n",
+       " */\n",
+       "function simpleKeys (original) {\n",
+       "  return Object.keys(original).reduce(function (obj, key) {\n",
+       "    if (typeof original[key] !== 'object')\n",
+       "        obj[key] = original[key]\n",
+       "    return obj;\n",
+       "  }, {});\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.mouse_event = function(event, name) {\n",
+       "    var canvas_pos = mpl.findpos(event)\n",
+       "\n",
+       "    if (name === 'button_press')\n",
+       "    {\n",
+       "        this.canvas.focus();\n",
+       "        this.canvas_div.focus();\n",
+       "    }\n",
+       "\n",
+       "    var x = canvas_pos.x;\n",
+       "    var y = canvas_pos.y;\n",
+       "\n",
+       "    this.send_message(name, {x: x, y: y, button: event.button,\n",
+       "                             step: event.step,\n",
+       "                             guiEvent: simpleKeys(event)});\n",
+       "\n",
+       "    /* This prevents the web browser from automatically changing to\n",
+       "     * the text insertion cursor when the button is pressed.  We want\n",
+       "     * to control all of the cursor setting manually through the\n",
+       "     * 'cursor' event from matplotlib */\n",
+       "    event.preventDefault();\n",
+       "    return false;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+       "    // Handle any extra behaviour associated with a key event\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.key_event = function(event, name) {\n",
+       "\n",
+       "    // Prevent repeat events\n",
+       "    if (name == 'key_press')\n",
+       "    {\n",
+       "        if (event.which === this._key)\n",
+       "            return;\n",
+       "        else\n",
+       "            this._key = event.which;\n",
+       "    }\n",
+       "    if (name == 'key_release')\n",
+       "        this._key = null;\n",
+       "\n",
+       "    var value = '';\n",
+       "    if (event.ctrlKey && event.which != 17)\n",
+       "        value += \"ctrl+\";\n",
+       "    if (event.altKey && event.which != 18)\n",
+       "        value += \"alt+\";\n",
+       "    if (event.shiftKey && event.which != 16)\n",
+       "        value += \"shift+\";\n",
+       "\n",
+       "    value += 'k';\n",
+       "    value += event.which.toString();\n",
+       "\n",
+       "    this._key_event_extra(event, name);\n",
+       "\n",
+       "    this.send_message(name, {key: value,\n",
+       "                             guiEvent: simpleKeys(event)});\n",
+       "    return false;\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
+       "    if (name == 'download') {\n",
+       "        this.handle_save(this, null);\n",
+       "    } else {\n",
+       "        this.send_message(\"toolbar_button\", {name: name});\n",
+       "    }\n",
+       "};\n",
+       "\n",
+       "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
+       "    this.message.textContent = tooltip;\n",
+       "};\n",
+       "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to  previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\" [...]
+       "\n",
+       "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
+       "\n",
+       "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
+       "    // Create a \"websocket\"-like object which calls the given IPython comm\n",
+       "    // object with the appropriate methods. Currently this is a non binary\n",
+       "    // socket, so there is still some room for performance tuning.\n",
+       "    var ws = {};\n",
+       "\n",
+       "    ws.close = function() {\n",
+       "        comm.close()\n",
+       "    };\n",
+       "    ws.send = function(m) {\n",
+       "        //console.log('sending', m);\n",
+       "        comm.send(m);\n",
+       "    };\n",
+       "    // Register the callback with on_msg.\n",
+       "    comm.on_msg(function(msg) {\n",
+       "        //console.log('receiving', msg['content']['data'], msg);\n",
+       "        // Pass the mpl event to the overriden (by mpl) onmessage function.\n",
+       "        ws.onmessage(msg['content']['data'])\n",
+       "    });\n",
+       "    return ws;\n",
+       "}\n",
+       "\n",
+       "mpl.mpl_figure_comm = function(comm, msg) {\n",
+       "    // This is the function which gets called when the mpl process\n",
+       "    // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+       "\n",
+       "    var id = msg.content.data.id;\n",
+       "    // Get hold of the div created by the display call when the Comm\n",
+       "    // socket was opened in Python.\n",
+       "    var element = $(\"#\" + id);\n",
+       "    var ws_proxy = comm_websocket_adapter(comm)\n",
+       "\n",
+       "    function ondownload(figure, format) {\n",
+       "        window.open(figure.imageObj.src);\n",
+       "    }\n",
+       "\n",
+       "    var fig = new mpl.figure(id, ws_proxy,\n",
+       "                           ondownload,\n",
+       "                           element.get(0));\n",
+       "\n",
+       "    // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+       "    // web socket which is closed, not our websocket->open comm proxy.\n",
+       "    ws_proxy.onopen();\n",
+       "\n",
+       "    fig.parent_element = element.get(0);\n",
+       "    fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n",
+       "    if (!fig.cell_info) {\n",
+       "        console.error(\"Failed to find cell for figure\", id, fig);\n",
+       "        return;\n",
+       "    }\n",
+       "\n",
+       "    var output_index = fig.cell_info[2]\n",
+       "    var cell = fig.cell_info[0];\n",
+       "\n",
+       "};\n",
+       "\n",
+       "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
+       "    fig.root.unbind('remove')\n",
+       "\n",
+       "    // Update the output cell to use the data from the current canvas.\n",
+       "    fig.push_to_output();\n",
+       "    var dataURL = fig.canvas.toDataURL();\n",
+       "    // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+       "    // the notebook keyboard shortcuts fail.\n",
+       "    IPython.keyboard_manager.enable()\n",
+       "    $(fig.parent_element).html('<img src=\"' + dataURL + '\">');\n",
+       "    fig.close_ws(fig, msg);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.close_ws = function(fig, msg){\n",
+       "    fig.send_message('closing', msg);\n",
+       "    // fig.ws.close()\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
+       "    // Turn the data on the canvas into data in the output cell.\n",
+       "    var dataURL = this.canvas.toDataURL();\n",
+       "    this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\">';\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.updated_canvas_event = function() {\n",
+       "    // Tell IPython that the notebook contents must change.\n",
+       "    IPython.notebook.set_dirty(true);\n",
+       "    this.send_message(\"ack\", {});\n",
+       "    var fig = this;\n",
+       "    // Wait a second, then push the new image to the DOM so\n",
+       "    // that it is saved nicely (might be nice to debounce this).\n",
+       "    setTimeout(function () { fig.push_to_output() }, 1000);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._init_toolbar = function() {\n",
+       "    var fig = this;\n",
+       "\n",
+       "    var nav_element = $('<div/>')\n",
+       "    nav_element.attr('style', 'width: 100%');\n",
+       "    this.root.append(nav_element);\n",
+       "\n",
+       "    // Define a callback function for later on.\n",
+       "    function toolbar_event(event) {\n",
+       "        return fig.toolbar_button_onclick(event['data']);\n",
+       "    }\n",
+       "    function toolbar_mouse_event(event) {\n",
+       "        return fig.toolbar_button_onmouseover(event['data']);\n",
+       "    }\n",
+       "\n",
+       "    for(var toolbar_ind in mpl.toolbar_items){\n",
+       "        var name = mpl.toolbar_items[toolbar_ind][0];\n",
+       "        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+       "        var image = mpl.toolbar_items[toolbar_ind][2];\n",
+       "        var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+       "\n",
+       "        if (!name) { continue; };\n",
+       "\n",
+       "        var button = $('<button class=\"btn btn-default\" href=\"#\" title=\"' + name + '\"><i class=\"fa ' + image + ' fa-lg\"></i></button>');\n",
+       "        button.click(method_name, toolbar_event);\n",
+       "        button.mouseover(tooltip, toolbar_mouse_event);\n",
+       "        nav_element.append(button);\n",
+       "    }\n",
+       "\n",
+       "    // Add the status bar.\n",
+       "    var status_bar = $('<span class=\"mpl-message\" style=\"text-align:right; float: right;\"/>');\n",
+       "    nav_element.append(status_bar);\n",
+       "    this.message = status_bar[0];\n",
+       "\n",
+       "    // Add the close button to the window.\n",
+       "    var buttongrp = $('<div class=\"btn-group inline pull-right\"></div>');\n",
+       "    var button = $('<button class=\"btn btn-mini btn-primary\" href=\"#\" title=\"Stop Interaction\"><i class=\"fa fa-power-off icon-remove icon-large\"></i></button>');\n",
+       "    button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
+       "    button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
+       "    buttongrp.append(button);\n",
+       "    var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
+       "    titlebar.prepend(buttongrp);\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._root_extra_style = function(el){\n",
+       "    var fig = this\n",
+       "    el.on(\"remove\", function(){\n",
+       "\tfig.close_ws(fig, {});\n",
+       "    });\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._canvas_extra_style = function(el){\n",
+       "    // this is important to make the div 'focusable\n",
+       "    el.attr('tabindex', 0)\n",
+       "    // reach out to IPython and tell the keyboard manager to turn it's self\n",
+       "    // off when our div gets focus\n",
+       "\n",
+       "    // location in version 3\n",
+       "    if (IPython.notebook.keyboard_manager) {\n",
+       "        IPython.notebook.keyboard_manager.register_events(el);\n",
+       "    }\n",
+       "    else {\n",
+       "        // location in version 2\n",
+       "        IPython.keyboard_manager.register_events(el);\n",
+       "    }\n",
+       "\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+       "    var manager = IPython.notebook.keyboard_manager;\n",
+       "    if (!manager)\n",
+       "        manager = IPython.keyboard_manager;\n",
+       "\n",
+       "    // Check for shift+enter\n",
+       "    if (event.shiftKey && event.which == 13) {\n",
+       "        this.canvas_div.blur();\n",
+       "        event.shiftKey = false;\n",
+       "        // Send a \"J\" for go to next cell\n",
+       "        event.which = 74;\n",
+       "        event.keyCode = 74;\n",
+       "        manager.command_mode();\n",
+       "        manager.handle_keydown(event);\n",
+       "    }\n",
+       "}\n",
+       "\n",
+       "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+       "    fig.ondownload(fig, null);\n",
+       "}\n",
+       "\n",
+       "\n",
+       "mpl.find_output_cell = function(html_output) {\n",
+       "    // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+       "    // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+       "    // IPython event is triggered only after the cells have been serialised, which for\n",
+       "    // our purposes (turning an active figure into a static one), is too late.\n",
+       "    var cells = IPython.notebook.get_cells();\n",
+       "    var ncells = cells.length;\n",
+       "    for (var i=0; i<ncells; i++) {\n",
+       "        var cell = cells[i];\n",
+       "        if (cell.cell_type === 'code'){\n",
+       "            for (var j=0; j<cell.output_area.outputs.length; j++) {\n",
+       "                var data = cell.output_area.outputs[j];\n",
+       "                if (data.data) {\n",
+       "                    // IPython >= 3 moved mimebundle to data attribute of output\n",
+       "                    data = data.data;\n",
+       "                }\n",
+       "                if (data['text/html'] == html_output) {\n",
+       "                    return [cell, data, j];\n",
+       "                }\n",
+       "            }\n",
+       "        }\n",
+       "    }\n",
+       "}\n",
+       "\n",
+       "// Register the function which deals with the matplotlib target/channel.\n",
+       "// The kernel may be null if the page has been refreshed.\n",
+       "if (IPython.notebook.kernel != null) {\n",
+       "    IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
+       "}\n"
+      ],
+      "text/plain": [
+       "<IPython.core.display.Javascript object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA8AAAAPACAYAAAD61hCbAAAgAElEQVR4XuydB5gVRdaGSzFgVhTMYsIEihnEjGDOOa1Z17TmvOacc9Zd1/VfXXPaNa5izoqiGDFgQAyIgFnB/3zN9Nj3zr23TsNcJvR7nuc+AzN1q7vfqu6ur+rUOZMFDAIQgAAEIAABCEAAAhCAAAQgUAACkxXgGrlECEAAAhCAAAQgAAEIQAACEIBAQADTCSAAAQhAAAIQgAAEIAABCECgEAQQwIVoZi4SAhCAAAQgAAEIQAACEIAABBDA9AEIQAACEIAABCAAAQhAAAIQKAQBBHAhmpmLhAAEIAABCEAAAhCAAAQgAAEEMH0AAhCAAAQgAAEIQAACEIAABApBAAFciGbmIiEAAQhAAAIQgAAEIAABCEAAAUwfgAAEIAABCEAAAhCAAAQgAIFCEEA [...]
+      ],
+      "text/plain": [
+       "<IPython.core.display.HTML object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<matplotlib.axes._subplots.AxesSubplot at 0x1d4debe0>"
+      ]
+     },
+     "execution_count": 21,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "%matplotlib notebook\n",
+    "fig, ax = plt.subplots(1, figsize=(12, 12))\n",
+    "plot.show((world, 1), cmap='Greys_r', interpolation='none', ax=ax)\n",
+    "ax.set_xlim(-50, 0)\n",
+    "ax.set_ylim(0, 40)\n",
+    "\n",
+    "plot.show((world, 1), contour=True, ax=ax, \n",
+    "          levels=[25, 125], colors=['white', 'red'], linewidths=4,\n",
+    "         contour_label_kws=dict(fontsize=18, fmt=\"%1.0f\", inline_spacing=15, use_clabeltext=True))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Add a rasterio raster to a cartopy geoaxes"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<cartopy.mpl.feature_artist.FeatureArtist at 0x14df9ba8>"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "C:\\Anaconda\\lib\\site-packages\\matplotlib\\artist.py:221: MatplotlibDeprecationWarning: This has been deprecated in mpl 1.5, please use the\n",
+      "axes property.  A removal date has not been set.\n",
+      "  warnings.warn(_get_axes_msg, mplDeprecation, stacklevel=1)\n"
+     ]
+    },
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAABGoAAAHyCAYAAACzuiiPAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xm0p1dd5/v3Hp7xN5751JyaMlVlgIQMRBJIwiDSChFk\nsDUK6NUrfWltpRG1r7cdAMF26Dhg61JQWm2ECCY3hIQhJkBGkpCqpJKqSlVqOvM5v/kZ9973j1/B\nbV1t39XrKmF4XmvVWnX+qHN+9Qz1PPXZ3/39CucclUqlUqlUKpVKpVKpVCqV5598vj9ApVKpVCqV\nSqVSqVQqlUplrApqKpVKpVKpVCqVSqVSqVS+SVRBTaVSqVQqlUqlUqlUKpXKN4kqqKlUKpVKpVKp\nVCqVSqVS+SZRBTWVSqVSqVQqlUqlUqlUKt8k9PP9ASqVyjeOEEIC88AuoAGcBk4BG64aAVepVCqV\n/w9C [...]
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x17c2d208>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "import cartopy\n",
+    "import cartopy.crs as ccrs\n",
+    "\n",
+    "fig = plt.figure(figsize=(20, 12))\n",
+    "ax = plt.axes(projection=ccrs.InterruptedGoodeHomolosine())\n",
+    "\n",
+    "ax.set_global()\n",
+    "plot.show(world, origin='upper', transform=ccrs.PlateCarree(), interpolation=None, ax=ax)\n",
+    "\n",
+    "ax.coastlines()\n",
+    "ax.add_feature(cartopy.feature.BORDERS)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true,
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 2",
+   "language": "python",
+   "name": "python2"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 2
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython2",
+   "version": "2.7.11"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/examples/async-rasterio.py b/examples/async-rasterio.py
index 2aeb6ff..a02ef73 100644
--- a/examples/async-rasterio.py
+++ b/examples/async-rasterio.py
@@ -10,14 +10,14 @@ performance.
 import asyncio
 import time
 
-import numpy
+import numpy as np
 import rasterio
 
 from rasterio._example import compute
 
 def main(infile, outfile, with_threads=False):
-    
-    with rasterio.drivers():
+
+    with rasterio.Env():
 
         # Open the source dataset.
         with rasterio.open(infile) as src:
@@ -33,7 +33,7 @@ def main(infile, outfile, with_threads=False):
             with rasterio.open(outfile, 'w', **meta) as dst:
 
                 loop = asyncio.get_event_loop()
-                
+
                 # With the exception of the ``yield from`` statement,
                 # process_window() looks like callback-free synchronous
                 # code. With a coroutine, we can keep the read, compute,
@@ -46,10 +46,10 @@ def main(infile, outfile, with_threads=False):
 
                 @asyncio.coroutine
                 def process_window(window):
-                    
+
                     # Read a window of data.
                     data = src.read(window=window)
-                    
+
                     # We run the raster computation in a separate thread
                     # and pause until the computation finishes, letting
                     # other coroutines advance.
@@ -57,19 +57,19 @@ def main(infile, outfile, with_threads=False):
                     # The _example.compute function modifies no Python
                     # objects and releases the GIL. It can execute
                     # concurrently.
-                    result = numpy.zeros(data.shape, dtype=data.dtype)
+                    result = np.zeros(data.shape, dtype=data.dtype)
                     if with_threads:
                         yield from loop.run_in_executor(
                                             None, compute, data, result)
                     else:
                         compute(data, result)
-                    
+
                     dst.write(result, window=window)
 
                 # Queue up the loop's tasks.
-                tasks = [asyncio.Task(process_window(window)) 
+                tasks = [asyncio.Task(process_window(window))
                          for ij, window in dst.block_windows(1)]
-                
+
                 # Wait for all the tasks to finish, and close.
                 loop.run_until_complete(asyncio.wait(tasks))
                 loop.close()
@@ -93,6 +93,6 @@ if __name__ == '__main__':
         action='store_true',
         help="Run with a pool of worker threads")
     args = parser.parse_args()
-    
+
     main(args.input, args.output, args.with_workers)
 
diff --git a/examples/concurrent-cpu-bound.py b/examples/concurrent-cpu-bound.py
index 0053a05..0d62663 100644
--- a/examples/concurrent-cpu-bound.py
+++ b/examples/concurrent-cpu-bound.py
@@ -2,23 +2,23 @@
 
 Operate on a raster dataset window-by-window using a ThreadPoolExecutor.
 
-Simulates a CPU-bound thread situation where multiple threads can improve performance.
+Simulates a CPU-bound thread situation where multiple threads can improve
+performance.
 
 With -j 4, the program returns in about 1/4 the time as with -j 1.
 """
 
 import concurrent.futures
 import multiprocessing
-import time
 
-import numpy
+import numpy as np
 import rasterio
 from rasterio._example import compute
 
 
 def main(infile, outfile, num_workers=4):
 
-    with rasterio.drivers():
+    with rasterio.Env():
 
         # Open the source dataset.
         with rasterio.open(infile) as src:
@@ -36,7 +36,7 @@ def main(infile, outfile, num_workers=4):
                 def jobs():
                     for ij, window in dst.block_windows():
                         data = src.read(window=window)
-                        result = numpy.zeros(data.shape, dtype=data.dtype)
+                        result = np.zeros(data.shape, dtype=data.dtype)
                         yield data, result, window
 
                 # Submit the jobs to the thread pool executor.
@@ -87,4 +87,3 @@ if __name__ == '__main__':
     args = parser.parse_args()
 
     main(args.input, args.output, args.j)
-
diff --git a/examples/decimate.py b/examples/decimate.py
index b15ed8a..f8fbb3a 100644
--- a/examples/decimate.py
+++ b/examples/decimate.py
@@ -4,7 +4,7 @@ import tempfile
 
 import rasterio
 
-with rasterio.drivers():
+with rasterio.Env():
 
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         b, g, r = (src.read(k) for k in (1, 2, 3))
diff --git a/examples/rasterio_polygonize.py b/examples/rasterio_polygonize.py
index fbd4089..f125878 100644
--- a/examples/rasterio_polygonize.py
+++ b/examples/rasterio_polygonize.py
@@ -6,7 +6,6 @@ import subprocess
 import sys
 
 import fiona
-import numpy as np
 import rasterio
 from rasterio.features import shapes
 
@@ -16,31 +15,31 @@ logger = logging.getLogger('rasterio_polygonize')
 
 
 def main(raster_file, vector_file, driver, mask_value):
-    
-    with rasterio.drivers():
-        
+
+    with rasterio.Env():
+
         with rasterio.open(raster_file) as src:
             image = src.read(1)
-        
+
         if mask_value is not None:
             mask = image == mask_value
         else:
             mask = None
-        
+
         results = (
             {'properties': {'raster_val': v}, 'geometry': s}
-            for i, (s, v) 
+            for i, (s, v)
             in enumerate(
                 shapes(image, mask=mask, transform=src.affine)))
 
         with fiona.open(
-                vector_file, 'w', 
+                vector_file, 'w',
                 driver=driver,
                 crs=src.crs,
                 schema={'properties': [('raster_val', 'int')],
                         'geometry': 'Polygon'}) as dst:
             dst.writerecords(results)
-    
+
     return dst.name
 
 if __name__ == '__main__':
@@ -48,11 +47,11 @@ if __name__ == '__main__':
     parser = argparse.ArgumentParser(
         description="Writes shapes of raster features to a vector file")
     parser.add_argument(
-        'input', 
-        metavar='INPUT', 
+        'input',
+        metavar='INPUT',
         help="Input file name")
     parser.add_argument(
-        'output', 
+        'output',
         metavar='OUTPUT',
         help="Output file name")
     parser.add_argument(
@@ -68,7 +67,6 @@ if __name__ == '__main__':
     args = parser.parse_args()
 
     name = main(args.input, args.output, args.output_driver, args.mask_value)
-    
+
     print subprocess.check_output(
             ['ogrinfo', '-so', args.output, name])
-
diff --git a/examples/rasterize_geometry.py b/examples/rasterize_geometry.py
index 804ecd6..1b9d64f 100644
--- a/examples/rasterize_geometry.py
+++ b/examples/rasterize_geometry.py
@@ -1,5 +1,5 @@
 import logging
-import numpy
+import numpy as np
 import sys
 import rasterio
 from rasterio.features import rasterize
@@ -17,7 +17,7 @@ geometry = {
     'coordinates': [[(2, 2), (2, 4.25), (4.25, 4.25), (4.25, 2), (2, 2)]]}
 
 
-with rasterio.drivers():
+with rasterio.Env():
     result = rasterize([geometry], out_shape=(rows, cols))
     with rasterio.open(
             "test.tif",
@@ -26,8 +26,8 @@ with rasterio.drivers():
             width=cols,
             height=rows,
             count=1,
-            dtype=numpy.uint8,
+            dtype=np.uint8,
             nodata=0,
             transform=IDENTITY,
             crs={'init': "EPSG:4326"}) as out:
-        out.write(result.astype(numpy.uint8), indexes=1)
+        out.write(result.astype(np.uint8), indexes=1)
diff --git a/examples/reproject.py b/examples/reproject.py
index cadf4a5..11fb6ba 100644
--- a/examples/reproject.py
+++ b/examples/reproject.py
@@ -1,9 +1,7 @@
 import os
-import shutil
 import subprocess
-import tempfile
 
-import numpy
+import numpy as np
 import rasterio
 from rasterio import transform
 from rasterio.warp import reproject, RESAMPLING
@@ -11,27 +9,27 @@ from rasterio.warp import reproject, RESAMPLING
 tempdir = '/tmp'
 tiffname = os.path.join(tempdir, 'example.tif')
 
-with rasterio.drivers():
+with rasterio.Env():
 
     # Consider a 512 x 512 raster centered on 0 degrees E and 0 degrees N
     # with each pixel covering 15".
     rows, cols = src_shape = (512, 512)
-    dpp = 1.0/240 # decimal degrees per pixel
+    dpp = 1.0/240  # decimal degrees per pixel
     west, south, east, north = -cols*dpp/2, -rows*dpp/2, cols*dpp/2, rows*dpp/2
     src_transform = transform.from_bounds(west, south, east, north, cols, rows)
     src_crs = {'init': 'EPSG:4326'}
-    source = numpy.ones(src_shape, numpy.uint8)*255
+    source = np.ones(src_shape, np.uint8)*255
 
     # Prepare to reproject this rasters to a 1024 x 1024 dataset in
     # Web Mercator (EPSG:3857) with origin at -237481.5, 237536.4.
     dst_shape = (1024, 1024)
     dst_transform = transform.from_origin(-237481.5, 237536.4, 425.0, 425.0)
     dst_crs = {'init': 'EPSG:3857'}
-    destination = numpy.zeros(dst_shape, numpy.uint8)
+    destination = np.zeros(dst_shape, np.uint8)
 
     reproject(
-        source, 
-        destination, 
+        source,
+        destination,
         src_transform=src_transform,
         src_crs=src_crs,
         dst_transform=dst_transform,
@@ -44,13 +42,13 @@ with rasterio.drivers():
 
     # Write it out to a file.
     with rasterio.open(
-            tiffname, 
+            tiffname,
             'w',
             driver='GTiff',
             width=dst_shape[1],
             height=dst_shape[0],
             count=1,
-            dtype=numpy.uint8,
+            dtype=np.uint8,
             nodata=0,
             transform=dst_transform,
             crs=dst_crs) as dst:
diff --git a/examples/sieve.py b/examples/sieve.py
index 9b0b0d7..22b78d5 100644
--- a/examples/sieve.py
+++ b/examples/sieve.py
@@ -4,23 +4,23 @@
 
 import subprocess
 
-import numpy
+import numpy as np
 import rasterio
 from rasterio.features import sieve, shapes
 
 
 # Register GDAL and OGR drivers.
-with rasterio.drivers():
-    
+with rasterio.Env():
+
     # Read a raster to be sieved.
     with rasterio.open('tests/data/shade.tif') as src:
         shade = src.read(1)
-    
+
     # Print the number of shapes in the source raster.
     print("Slope shapes: %d" % len(list(shapes(shade))))
-    
+
     # Sieve out features 13 pixels or smaller.
-    sieved = sieve(shade, 13, out=numpy.zeros(src.shape, src.dtypes[0]))
+    sieved = sieve(shade, 13, out=np.zeros(src.shape, src.dtypes[0]))
 
     # Print the number of shapes in the sieved raster.
     print("Sieved (13) shapes: %d" % len(list(shapes(sieved))))
@@ -35,4 +35,3 @@ with rasterio.drivers():
 print(subprocess.check_output(
     ['gdalinfo', '-stats', 'example-sieved.tif']))
 subprocess.call(['open', 'example-sieved.tif'])
-
diff --git a/examples/total.py b/examples/total.py
index 8bb9900..e043ab9 100644
--- a/examples/total.py
+++ b/examples/total.py
@@ -1,8 +1,8 @@
-import numpy
+import numpy as np
 import rasterio
 import subprocess
 
-with rasterio.drivers(CPL_DEBUG=True):
+with rasterio.Env(CPL_DEBUG=True):
 
     # Read raster bands directly to Numpy arrays.
     with rasterio.open('tests/data/RGB.byte.tif') as src:
@@ -12,7 +12,7 @@ with rasterio.drivers(CPL_DEBUG=True):
     # exceed the 8-bit integer range, initialize it as 16-bit. Adding other
     # arrays to it in-place converts those arrays up and preserves the type
     # of the total array.
-    total = numpy.zeros(r.shape, dtype=rasterio.uint16)
+    total = np.zeros(r.shape, dtype=rasterio.uint16)
     for band in (r, g, b):
         total += band
     total /= 3
@@ -35,4 +35,3 @@ info = subprocess.check_output(
     ['gdalinfo', '-stats', 'example-total.tif'])
 print(info)
 subprocess.call(['open', 'example-total.tif'])
-
diff --git a/pep-508-install b/pep-508-install
new file mode 100755
index 0000000..4bfc6c1
--- /dev/null
+++ b/pep-508-install
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+"""Prototype support for PEP 518:
+
+"Specifying Minimum Build System Requirements for Python Projects".
+
+A future version of pip will do this for us and we'll remove this script.
+
+This script installs Fiona in develop mode (``pip install -e .[test]``).
+"""
+
+import subprocess
+
+
+def main():
+
+    # Parse config file for build system requirements.
+    build_system_requirements = None
+    with open('pyproject.toml') as config:
+        for line in config:
+            if line.startswith('requires'):
+                build_system_requirements = line.split('=')[-1]
+
+    # Install them if found.
+    if build_system_requirements:
+        reqs = eval(build_system_requirements)
+        subprocess.call(['pip', 'install'] +  reqs)
+
+    # Now install our package in editable mode.
+    subprocess.call(['pip', 'install', '-e', '.[test]'] +  reqs)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d128eec
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+# Minimum requirements for the build system to execute.
+requires = ["setuptools", "wheel", "cython", "numpy"]
diff --git a/rasterio/__init__.py b/rasterio/__init__.py
index 8581e8e..556e5c7 100644
--- a/rasterio/__init__.py
+++ b/rasterio/__init__.py
@@ -18,7 +18,7 @@ from rasterio.dtypes import (
     bool_, ubyte, uint8, uint16, int16, uint32, int32, float32, float64,
     complex_, check_dtype)
 from rasterio.env import ensure_env, Env
-from rasterio.five import string_types
+from rasterio.compat import string_types
 from rasterio.profiles import default_gtiff_profile
 from rasterio.transform import Affine, guard_transform
 from rasterio.vfs import parse_path
@@ -32,7 +32,7 @@ from rasterio import _err, coords, enums, vfs
 
 __all__ = [
     'band', 'open', 'copy', 'pad']
-__version__ = "0.35.1"
+__version__ = "0.36.0"
 __gdal_version__ = gdal_version()
 
 # Rasterio attaches NullHandler to the 'rasterio' logger and its
@@ -118,7 +118,7 @@ def open(path, mode='r', driver=None, width=None, height=None,
     .. code:: python
 
         >>> from affine import Affine
-        >>> Affine(0.5, 0.0, -180.0, 0.0, -0.5, 90.0)
+        >>> transform = Affine(0.5, 0.0, -180.0, 0.0, -0.5, 90.0)
 
     These coefficients are shown in the figure below.
 
@@ -298,9 +298,9 @@ def pad(array, transform, pad_width, mode=None, **kwargs):
     See numpy docs for details on mode and other kwargs:
     http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.pad.html
     """
-    import numpy
+    import numpy as np
     transform = guard_transform(transform)
-    padded_array = numpy.pad(array, pad_width, mode, **kwargs)
+    padded_array = np.pad(array, pad_width, mode, **kwargs)
     padded_trans = list(transform)
     padded_trans[2] -= pad_width * padded_trans[0]
     padded_trans[5] -= pad_width * padded_trans[4]
diff --git a/rasterio/_base.pxd b/rasterio/_base.pxd
index f8222f6..1d2a5dc 100644
--- a/rasterio/_base.pxd
+++ b/rasterio/_base.pxd
@@ -24,4 +24,4 @@ cdef class DatasetReader:
     cdef void *band(self, int bidx) except NULL
 
 
-cdef void *_osr_from_crs(object crs)
+cdef void *_osr_from_crs(object crs) except NULL
diff --git a/rasterio/_base.pyx b/rasterio/_base.pyx
index 905c545..debcba6 100644
--- a/rasterio/_base.pyx
+++ b/rasterio/_base.pyx
@@ -12,6 +12,7 @@ import warnings
 from libc.stdlib cimport malloc, free
 
 from rasterio cimport _gdal, _ogr
+from rasterio.crs import CRS
 from rasterio._err import (
     CPLErrors, GDALError, CPLE_IllegalArg, CPLE_OpenFailed, CPLE_NotSupported)
 from rasterio import dtypes
@@ -53,7 +54,6 @@ cdef class DatasetReader(object):
         self._block_shapes = None
         self._nodatavals = []
         self._crs = None
-        self._crs_wkt = None
         self._read = False
     
     def __repr__(self):
@@ -88,7 +88,6 @@ cdef class DatasetReader(object):
 
         self._transform = self.read_transform()
         self._crs = self.read_crs()
-        self._crs_wkt = self.read_crs_wkt()
 
         # touch self.meta
         _ = self.meta
@@ -124,7 +123,7 @@ cdef class DatasetReader(object):
         cdef void *osr = NULL
         if self._hds == NULL:
             raise ValueError("Null dataset")
-        crs = {}
+        crs = CRS()
         cdef const char * wkt = _gdal.GDALGetProjectionRef(self._hds)
         if wkt is NULL:
             raise ValueError("Unexpected NULL spatial reference")
@@ -182,36 +181,6 @@ cdef class DatasetReader(object):
             log.debug("GDAL dataset has no projection.")
         return crs
 
-    def read_crs_wkt(self):
-        cdef char *proj_c = NULL
-        cdef char *key_c = NULL
-        cdef void *osr = NULL
-        cdef const char * wkt = NULL
-        if self._hds == NULL:
-            raise ValueError("Null dataset")
-        wkt = _gdal.GDALGetProjectionRef(self._hds)
-        if wkt is NULL:
-            raise ValueError("Unexpected NULL spatial reference")
-        wkt_b = wkt
-        if len(wkt_b) > 0:
-            osr = _gdal.OSRNewSpatialReference(wkt)
-            log.debug("Got coordinate system")
-            if osr != NULL:
-                retval = _gdal.OSRAutoIdentifyEPSG(osr)
-                if retval > 0:
-                    log.info("Failed to auto identify EPSG: %d", retval)
-                _gdal.OSRExportToWkt(osr, &proj_c)
-                if proj_c == NULL:
-                    raise ValueError("Null projection")
-                proj_b = proj_c
-                crs_wkt = proj_b.decode('utf-8')
-                _gdal.CPLFree(proj_c)
-                _gdal.OSRDestroySpatialReference(osr)
-        else:
-            log.debug("GDAL dataset has no projection.")
-            crs_wkt = None
-        return crs_wkt
-
     def read_transform(self):
         if self._hds == NULL:
             raise ValueError("Null dataset")
@@ -455,8 +424,8 @@ cdef class DatasetReader(object):
     def window_bounds(self, window):
         """Returns the bounds of a window as x_min, y_min, x_max, y_max."""
         ((row_min, row_max), (col_min, col_max)) = window
-        x_min, y_min = (col_min, row_max) * self.affine
-        x_max, y_max = (col_max, row_min) * self.affine
+        x_min, y_min = self.affine * (col_min, row_max)
+        x_max, y_max = self.affine * (col_max, row_min)
         return x_min, y_min, x_max, y_max
 
     @property
@@ -558,15 +527,6 @@ cdef class DatasetReader(object):
         def __get__(self):
             return self.get_crs()
 
-    property crs_wkt:
-        """An OGC WKT string representation of the coordinate reference
-        system.
-        """
-        def __get__(self):
-            if not self._read and self._crs_wkt is None:
-                self._crs = self.read_crs_wkt()
-            return self._crs_wkt
-
     def get_transform(self):
         """Returns a GDAL geotransform in its native form."""
         if not self._read and self._transform is None:
@@ -874,40 +834,6 @@ def tastes_like_gdal(t):
     return t[2] == t[4] == 0.0 and t[1] > 0 and t[5] < 0
 
 
-cdef void *_osr_from_crs(object crs):
-    """Returns a reference to memory that must be deallocated
-    by the caller."""
-    cdef char *proj_c = NULL
-    cdef void *osr = _gdal.OSRNewSpatialReference(NULL)
-    params = []
-    # Normally, we expect a CRS dict.
-    if isinstance(crs, dict):
-        # EPSG is a special case.
-        init = crs.get('init')
-        if init:
-            auth, val = init.split(':')
-            if auth.upper() == 'EPSG':
-                _gdal.OSRImportFromEPSG(osr, int(val))
-        else:
-            crs['wktext'] = True
-            for k, v in crs.items():
-                if v is True or (k in ('no_defs', 'wktext') and v):
-                    params.append("+%s" % k)
-                else:
-                    params.append("+%s=%s" % (k, v))
-            proj = " ".join(params)
-            log.debug("PROJ.4 to be imported: %r", proj)
-            proj_b = proj.encode('utf-8')
-            proj_c = proj_b
-            _gdal.OSRImportFromProj4(osr, proj_c)
-    # Fall back for CRS strings like "EPSG:3857."
-    else:
-        proj_b = crs.encode('utf-8')
-        proj_c = proj_b
-        _gdal.OSRSetFromUserInput(osr, proj_c)
-    return osr
-
-
 def _transform(src_crs, dst_crs, xs, ys, zs):
     cdef double *x = NULL
     cdef double *y = NULL
@@ -965,24 +891,86 @@ def _transform(src_crs, dst_crs, xs, ys, zs):
     return retval
 
 
-def is_geographic_crs(crs):
-    cdef void *osr_crs = _osr_from_crs(crs)
-    cdef int retval = _gdal.OSRIsGeographic(osr_crs)
-    _gdal.OSRDestroySpatialReference(osr_crs)
-    return retval == 1
+cdef void *_osr_from_crs(object crs) except NULL:
+    """Returns a reference to memory that must be deallocated
+    by the caller."""
 
+    if crs is None:
+        raise CRSError('CRS cannot be None')
 
-def is_projected_crs(crs):
-    cdef void *osr_crs = _osr_from_crs(crs)
-    cdef int retval = _gdal.OSRIsProjected(osr_crs)
-    _gdal.OSRDestroySpatialReference(osr_crs)
-    return retval == 1
+    cdef char *proj_c = NULL
+    cdef void *osr = _gdal.OSRNewSpatialReference(NULL)
+    params = []
 
+    try:
+        with CPLErrors() as cple:
+            # Normally, we expect a CRS dict.
+            if isinstance(crs, dict):
+                crs = CRS(crs)
+            if isinstance(crs, CRS):
+                # EPSG is a special case.
+                init = crs.get('init')
+                if init:
+                    auth, val = init.split(':')
+                    if auth.upper() == 'EPSG':
+                        _gdal.OSRImportFromEPSG(osr, int(val))
+                else:
+                    crs['wktext'] = True
+                    for k, v in crs.items():
+                        if v is True or (k in ('no_defs', 'wktext') and v):
+                            params.append("+%s" % k)
+                        else:
+                            params.append("+%s=%s" % (k, v))
+                    proj = " ".join(params)
+                    log.debug("PROJ.4 to be imported: %r", proj)
+                    proj_b = proj.encode('utf-8')
+                    proj_c = proj_b
+                    _gdal.OSRImportFromProj4(osr, proj_c)
+            # Fall back for CRS strings like "EPSG:3857."
+            else:
+                proj_b = crs.encode('utf-8')
+                proj_c = proj_b
+                _gdal.OSRSetFromUserInput(osr, proj_c)
+            cple.check()
 
-def is_same_crs(crs1, crs2):
-    cdef void *osr_crs1 = _osr_from_crs(crs1)
-    cdef void *osr_crs2 = _osr_from_crs(crs2)
-    cdef int retval = _gdal.OSRIsSame(osr_crs1, osr_crs2)
-    _gdal.OSRDestroySpatialReference(osr_crs1)
-    _gdal.OSRDestroySpatialReference(osr_crs2)
-    return retval == 1
+    except:
+        raise CRSError('Invalid CRS')
+    
+    return osr
+
+def _can_create_osr(crs):
+    """
+    Returns True if valid OGRSpatialReference could be created from crs.
+    Specifically, it must not be NULL or empty string.
+
+    Parameters
+    ----------
+    crs: Source coordinate reference system, in rasterio dict format.
+
+    Returns
+    -------
+    out: bool
+        True if source coordinate reference appears valid.
+
+    """
+
+    cdef char *wkt = NULL
+    cdef void *osr = NULL
+
+    try:
+        osr = _osr_from_crs(crs)
+        if osr == NULL:
+            return False
+
+        _gdal.OSRExportToWkt(osr, &wkt)
+
+        # If input was empty, WKT can be too; otherwise the conversion didn't
+        # work properly and indicates an error.
+        return wkt != NULL and bool(crs) == (wkt[0] != '\0')
+
+    except CRSError:
+        return False
+
+    finally:
+        _gdal.OSRDestroySpatialReference(osr)
+        _gdal.CPLFree(wkt)
diff --git a/rasterio/_crs.pxd b/rasterio/_crs.pxd
new file mode 100644
index 0000000..e69de29
diff --git a/rasterio/_crs.pyx b/rasterio/_crs.pyx
new file mode 100644
index 0000000..fbd9526
--- /dev/null
+++ b/rasterio/_crs.pyx
@@ -0,0 +1,64 @@
+"""Coordinate reference systems, class and functions.
+"""
+
+import logging
+
+from rasterio cimport _gdal, _base
+from rasterio.compat import UserDict
+from rasterio.compat import string_types
+
+log = logging.getLogger(__name__)
+
+class _CRS(UserDict):
+    """
+    """
+    @property
+    def is_geographic(self):
+        cdef void *osr_crs = NULL
+        cdef int retval
+        try:
+            osr_crs = _base._osr_from_crs(self)
+            retval = _gdal.OSRIsGeographic(osr_crs)
+            return bool(retval == 1)
+        finally:
+            _gdal.OSRDestroySpatialReference(osr_crs)
+
+    @property
+    def is_projected(self):
+        cdef void *osr_crs = NULL
+        cdef int retval
+        try:
+            osr_crs = _base._osr_from_crs(self)
+            retval = _gdal.OSRIsProjected(osr_crs)
+            return bool(retval == 1)
+        finally:
+            _gdal.OSRDestroySpatialReference(osr_crs)
+
+    def __eq__(self, other):
+        cdef void *osr_crs1 = NULL
+        cdef void *osr_crs2 = NULL
+        cdef int retval
+        try:
+            osr_crs1 = _base._osr_from_crs(self)
+            osr_crs2 = _base._osr_from_crs(other)
+            retval = _gdal.OSRIsSame(osr_crs1, osr_crs2)
+            return bool(retval == 1)
+        finally:
+            _gdal.OSRDestroySpatialReference(osr_crs1)
+            _gdal.OSRDestroySpatialReference(osr_crs2)
+
+    @property
+    def wkt(self):
+        """An OGC WKT string representation of the coordinate reference
+        system.
+        """
+        cdef char *srcwkt = NULL
+        cdef void *osr = NULL
+        try:
+            osr = _base._osr_from_crs(self)
+            _gdal.OSRExportToWkt(osr, &srcwkt)
+            wkt = srcwkt.decode('utf-8')
+        finally:
+            _gdal.CPLFree(srcwkt)
+            _gdal.OSRDestroySpatialReference(osr)
+        return wkt
diff --git a/rasterio/_drivers.pyx b/rasterio/_drivers.pyx
index 4f0564c..2d0c723 100644
--- a/rasterio/_drivers.pyx
+++ b/rasterio/_drivers.pyx
@@ -5,7 +5,7 @@ import os
 import os.path
 import sys
 
-from rasterio.five import string_types
+from rasterio.compat import string_types
 
 
 cdef extern from "cpl_conv.h":
diff --git a/rasterio/_err.pyx b/rasterio/_err.pyx
index abe0d54..74b6acf 100644
--- a/rasterio/_err.pyx
+++ b/rasterio/_err.pyx
@@ -46,7 +46,7 @@ cdef extern from "cpl_error.h":
 
 class CPLError(Exception):
     """Base CPL error class
-    
+
     Exceptions deriving from this class are intended for use only in
     Rasterio's Cython code. Let's not expose API users to them.
     """
@@ -56,6 +56,12 @@ class CPLError(Exception):
         self.errno = errno
         self.errmsg = errmsg
 
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return "{}".format(self.errmsg)
+
     @property
     def args(self):
         return self.errtype, self.errno, self.errmsg
@@ -138,7 +144,7 @@ exception_map = {
     9: CPLE_UserInterrupt,
     10: ObjectNull,
 
-    # error numbers 11-16 are introduced in GDAL 2.1. See 
+    # error numbers 11-16 are introduced in GDAL 2.1. See
     # https://github.com/OSGeo/gdal/pull/98.
     11: CPLE_HttpResponse,
     12: CPLE_AWSBucketNotFound,
diff --git a/rasterio/_example.pyx b/rasterio/_example.pyx
index e1b4396..5eb7bb0 100644
--- a/rasterio/_example.pyx
+++ b/rasterio/_example.pyx
@@ -1,10 +1,7 @@
 # cython: boundscheck=False
 
-import numpy
-cimport numpy
-
 def compute(
-        unsigned char[:, :, :] input, 
+        unsigned char[:, :, :] input,
         unsigned char[:, :, :] output):
     # Given input and output uint8 arrays, fakes an CPU-intensive
     # computation.
@@ -23,4 +20,3 @@ def compute(
                         val += 1.0
                     val -= 2000.0
                     output[~i, j, k] = <unsigned char>val
-
diff --git a/rasterio/_io.pyx b/rasterio/_io.pyx
index 62581b8..43af44d 100644
--- a/rasterio/_io.pyx
+++ b/rasterio/_io.pyx
@@ -17,13 +17,14 @@ cimport numpy as np
 from rasterio cimport _base, _gdal, _ogr, _io
 from rasterio._base import (
     crop_window, eval_window, window_shape, window_index, tastes_like_gdal)
+from rasterio.crs import CRS
 from rasterio._drivers import driver_count, GDALEnv
 from rasterio._err import CPLErrors, GDALError, CPLE_OpenFailed
 from rasterio import dtypes
 from rasterio.coords import BoundingBox
 from rasterio.errors import (
     DriverRegistrationError, RasterioIOError, NodataShadowWarning)
-from rasterio.five import text_type, string_types
+from rasterio.compat import text_type, string_types
 from rasterio.transform import Affine
 from rasterio.enums import ColorInterp, MaskFlags, Resampling
 from rasterio.sample import sample_gen
@@ -33,6 +34,11 @@ from rasterio.vfs import parse_path, vsi_path
 log = logging.getLogger(__name__)
 
 
+# These drivers are known to produce invalid results with
+# IndirectRasterUpdater. Save users the trouble and fail fast.
+BAD_WRITE_DRIVERS = ("netCDF", )
+
+
 cdef bint in_dtype_range(value, dtype):
     """Returns True if value is in the range of dtype, else False."""
     infos = {
@@ -639,7 +645,7 @@ cdef class RasterReader(_base.DatasetReader):
 
 
     def read(self, indexes=None, out=None, window=None, masked=False,
-            boundless=False):
+            out_shape=None, boundless=False):
         """Read raster bands as a multidimensional array
 
         Parameters
@@ -648,7 +654,7 @@ cdef class RasterReader(_base.DatasetReader):
             If `indexes` is a list, the result is a 3D array, but is
             a 2D array if it is a band index number.
 
-        out: numpy ndarray, optional
+        out : numpy ndarray, optional
             As with Numpy ufuncs, this is an optional reference to an
             output array with the same dimensions and shape into which
             data will be placed.
@@ -657,6 +663,14 @@ cdef class RasterReader(_base.DatasetReader):
             array. In other words, `out` is likely to be an
             incomplete representation of the method's results.
 
+            Cannot combined with `out_shape`.
+
+        out_shape : tuple, optional
+            A tuple describing the output array's shape.  Allows for decimated
+            reads without constructing an output Numpy array.
+
+            Cannot combined with `out`.
+
         window : a pair (tuple) of pairs of ints, optional
             The optional `window` argument is a 2 item tuple. The first
             item is a tuple containing the indexes of the rows at which
@@ -753,6 +767,13 @@ cdef class RasterReader(_base.DatasetReader):
         else:
             win_shape += self.shape
 
+        if out is not None and out_shape is not None:
+            raise ValueError("out and out_shape are exclusive")
+        elif out_shape is not None:
+            if len(out_shape) == 2:
+                out_shape = (1,) + out_shape
+            out = np.empty(out_shape, dtype=dtype)
+
         if out is not None:
             if out.dtype != dtype:
                 raise ValueError(
@@ -889,7 +910,8 @@ cdef class RasterReader(_base.DatasetReader):
         return out
 
 
-    def read_masks(self, indexes=None, out=None, window=None, boundless=False):
+    def read_masks(self, indexes=None, out=None, out_shape=None, window=None,
+                   boundless=False):
         """Read raster band masks as a multidimensional array
 
         Parameters
@@ -898,7 +920,7 @@ cdef class RasterReader(_base.DatasetReader):
             If `indexes` is a list, the result is a 3D array, but is
             a 2D array if it is a band index number.
 
-        out: numpy ndarray, optional
+        out : numpy ndarray, optional
             As with Numpy ufuncs, this is an optional reference to an
             output array with the same dimensions and shape into which
             data will be placed.
@@ -907,6 +929,14 @@ cdef class RasterReader(_base.DatasetReader):
             array. In other words, `out` is likely to be an
             incomplete representation of the method's results.
 
+            Cannot combine with `out_shape`.
+
+        out_shape : tuple, optional
+            A tuple describing the output array's shape.  Allows for decimated
+            reads without constructing an output Numpy array.
+
+            Cannot combined with `out`.
+
         window : a pair (tuple) of pairs of ints, optional
             The optional `window` argument is a 2 item tuple. The first
             item is a tuple containing the indexes of the rows at which
@@ -959,6 +989,13 @@ cdef class RasterReader(_base.DatasetReader):
         
         dtype = 'uint8'
 
+        if out is not None and out_shape is not None:
+            raise ValueError("out and out_shape are exclusive")
+        elif out_shape is not None:
+            if len(out_shape) == 2:
+                out_shape = (1,) + out_shape
+            out = np.zeros(out_shape, 'uint8')
+
         if out is not None:
             if out.dtype != np.dtype(dtype):
                 raise ValueError(
@@ -968,9 +1005,10 @@ cdef class RasterReader(_base.DatasetReader):
                 raise ValueError(
                     "'out' shape %s does not match window shape %s" %
                     (out.shape, win_shape))
-        if out is None:
+        else:
             out = np.zeros(win_shape, 'uint8')
 
+
         # We can jump straight to _read() in some cases. We can ignore
         # the boundless flag if there's no given window.
         if not boundless or not window:
@@ -1122,6 +1160,54 @@ cdef class RasterReader(_base.DatasetReader):
         return out
 
 
+    def dataset_mask(self, window=None, boundless=False):
+        """Calculate the dataset's 2D mask. Derived from the individual band masks
+        provided by read_masks().
+
+        Parameters
+        ----------
+        window and boundless are passed directly to read_masks()
+        
+        Returns
+        -------
+        ndarray, shape=(self.height, self.width), dtype='uint8'
+        0 = nodata, 255 = valid data
+
+        The dataset mask is calculate based on the individual band masks according to
+        the following logic, in order of precedence:
+
+        1. If a .msk file, dataset-wide alpha or internal mask exists,
+           it will be used as the dataset mask.
+        2. If an 4-band RGBA with a shadow nodata value,
+           band 4 will be used as the dataset mask.
+        3. If a nodata value exists, use the binary OR (|) of the band masks
+        4. If no nodata value exists, return a mask filled with 255
+
+        Note that this differs from read_masks and GDAL RFC15
+        in that it applies per-dataset, not per-band
+        (see https://trac.osgeo.org/gdal/wiki/rfc15_nodatabitmask)
+        """
+        kwargs = {
+            'window': window,
+            'boundless': boundless}
+
+        # GDAL found dataset-wide alpha band or mask
+        # All band masks are equal so we can return the first
+        if self.mask_flags[0] & MaskFlags.per_dataset:
+            return self.read_masks(1, **kwargs)
+
+        # use Alpha mask if available and looks like RGB, even if nodata is shadowing
+        elif self.count == 4 and self.colorinterp(1) == ColorInterp.red:
+            return self.read_masks(4, **kwargs)
+
+        # Or use the binary OR intersection of all GDALGetMaskBands
+        else:
+            mask = self.read_masks(1, **kwargs)
+            for i in range(1, self.count):
+                mask = mask | self.read_masks(i, **kwargs)
+            return mask
+        
+
     def read_mask(self, indexes=None, out=None, window=None, boundless=False):
         """Read the mask band into an `out` array if provided, 
         otherwise return a new array containing the dataset's
@@ -1373,7 +1459,6 @@ cdef class RasterUpdater(RasterReader):
 
         self._transform = self.read_transform()
         self._crs = self.read_crs()
-        self._crs_wkt = self.read_crs_wkt()
 
         if options != NULL:
             _gdal.CSLDestroy(options)
@@ -1399,6 +1484,8 @@ cdef class RasterUpdater(RasterReader):
 
         # Normally, we expect a CRS dict.
         if isinstance(crs, dict):
+            crs = CRS(crs)
+        if isinstance(crs, CRS):
             # EPSG is a special case.
             init = crs.get('init')
             if init:
@@ -1997,7 +2084,6 @@ cdef class IndirectRasterUpdater(RasterUpdater):
 
         self._transform = self.read_transform()
         self._crs = self.read_crs()
-        self._crs_wkt = self.read_crs_wkt()
 
         # touch self.meta
         _ = self.meta
@@ -2058,6 +2144,24 @@ cdef class IndirectRasterUpdater(RasterUpdater):
 
 
 def writer(path, mode, **kwargs):
+    """Get a writer instance for path
+
+    Parameters
+    ----------
+    path: Path to new raster (for mode=='w') or existing raster to be updated
+    mode: string, open mode, one of ('w', 'r+')
+
+    Returns
+    -------
+    RasterUpdater in the case of GeoTiff
+        writes directly to disk
+    IndirectRasterUpdater for any other driver
+        using temporary MEM driver before copying data
+        to the final destination
+
+    Raises ``RasterioIOError` if driver is blacklisted from writing
+    due to known bugs in the IndirectRasterUpdater approach
+    """
     # Dispatch to direct or indirect writer/updater according to the
     # format driver's capabilities.
     cdef void *hds = NULL
@@ -2074,6 +2178,10 @@ def writer(path, mode, **kwargs):
     if mode == 'w' and 'driver' in kwargs:
         if kwargs['driver'] == 'GTiff':
             return RasterUpdater(path, mode, **kwargs)
+        elif kwargs['driver'] in BAD_WRITE_DRIVERS:
+            raise RasterioIOError(
+                "Rasterio does not support writing "
+                "with {} driver".format(kwargs['driver']))
         else:
             return IndirectRasterUpdater(path, mode, **kwargs)
     else:
@@ -2096,6 +2204,10 @@ def writer(path, mode, **kwargs):
 
         if driver == 'GTiff':
             return RasterUpdater(path, mode)
+        elif kwargs['driver'] in BAD_WRITE_DRIVERS:
+            raise RasterioIOError(
+                "Rasterio does not support updating "
+                "with {} driver".format(kwargs['driver']))
         else:
             return IndirectRasterUpdater(path, mode)
 
diff --git a/rasterio/_warp.pyx b/rasterio/_warp.pyx
index 937677f..463d127 100644
--- a/rasterio/_warp.pyx
+++ b/rasterio/_warp.pyx
@@ -24,22 +24,22 @@ cdef extern from "gdalwarper.h" nogil:
         GDALWarpOperation() except +
         int Initialize(const GDALWarpOptions *psNewOptions)
         const GDALWarpOptions *GetOptions()
-        int ChunkAndWarpImage( 
+        int ChunkAndWarpImage(
             int nDstXOff, int nDstYOff, int nDstXSize, int nDstYSize )
-        int ChunkAndWarpMulti( 
+        int ChunkAndWarpMulti(
             int nDstXOff, int nDstYOff, int nDstXSize, int nDstYSize )
-        int WarpRegion( int nDstXOff, int nDstYOff, 
+        int WarpRegion( int nDstXOff, int nDstYOff,
                         int nDstXSize, int nDstYSize,
                         int nSrcXOff=0, int nSrcYOff=0,
                         int nSrcXSize=0, int nSrcYSize=0,
                         double dfProgressBase=0.0, double dfProgressScale=1.0)
-        int WarpRegionToBuffer( int nDstXOff, int nDstYOff, 
-                                int nDstXSize, int nDstYSize, 
-                                void *pDataBuf, 
+        int WarpRegionToBuffer( int nDstXOff, int nDstYOff,
+                                int nDstXSize, int nDstYSize,
+                                void *pDataBuf,
                                 int eBufDataType,
                                 int nSrcXOff=0, int nSrcYOff=0,
                                 int nSrcXSize=0, int nSrcYSize=0,
-                                double dfProgressBase=0.0, 
+                                double dfProgressBase=0.0,
                                 double dfProgressScale=1.0)
 
 
@@ -93,7 +93,7 @@ def _transform_geom(
         _gdal.OSRDestroySpatialReference(src)
         _gdal.OSRDestroySpatialReference(dst)
         raise
-        
+
     # Transform options.
     val_b = str(antimeridian_offset).encode('utf-8')
     val_c = val_b
@@ -269,13 +269,13 @@ def _reproject(
         except:
             raise DriverRegistrationError(
                 "'MEM' driver not found. Check that this call is contained "
-                "in a `with rasterio.drivers()` or `with rasterio.open()` "
+                "in a `with rasterio.Env()` or `with rasterio.open()` "
                 "block.")
 
         try:
             with CPLErrors() as cple:
                 hdsin = _gdal.GDALCreate(
-                    hrdriver, "input", cols, rows, 
+                    hrdriver, "input", cols, rows,
                     src_count, dtypes.dtype_rev[dtype], NULL)
                 cple.check()
         except:
@@ -297,12 +297,12 @@ def _reproject(
         finally:
             _gdal.CPLFree(srcwkt)
             _gdal.OSRDestroySpatialReference(osr)
-        
+
         # Copy arrays to the dataset.
         retval = _io.io_auto(source, hdsin, 1)
         # TODO: handle errors (by retval).
         log.debug("Wrote array to temp source dataset")
-    
+
     # If the source is a rasterio Band, no copy necessary.
     elif isinstance(source, tuple):
         rdr = source.ds
@@ -312,7 +312,7 @@ def _reproject(
             src_nodata = rdr.nodata
     else:
         raise ValueError("Invalid source")
-    
+
     # Next, do the same for the destination raster.
     if dtypes.is_ndarray(destination):
         if len(destination.shape) == 2:
@@ -327,14 +327,14 @@ def _reproject(
         except:
             raise DriverRegistrationError(
                 "'MEM' driver not found. Check that this call is contained "
-                "in a `with rasterio.drivers()` or `with rasterio.open()` "
+                "in a `with rasterio.Env()` or `with rasterio.open()` "
                 "block.")
 
         _, rows, cols = destination.shape
         try:
             with CPLErrors() as cple:
                 hdsout = _gdal.GDALCreate(
-                    hrdriver, "output", cols, rows, src_count, 
+                    hrdriver, "output", cols, rows, src_count,
                     dtypes.dtype_rev[np.dtype(destination.dtype).name], NULL)
                 cple.check()
         except:
@@ -372,7 +372,7 @@ def _reproject(
             dst_nodata = udr.nodata
     else:
         raise ValueError("Invalid destination")
-    
+
     cdef void *hTransformArg = NULL
     cdef _gdal.GDALWarpOptions *psWOptions = NULL
 
@@ -398,7 +398,7 @@ def _reproject(
     val_b = str(num_threads).encode('utf-8')
     warp_extras = _gdal.CSLSetNameValue(warp_extras, "NUM_THREADS", val_b)
     log.debug("Setting NUM_THREADS option: %s", val_b)
-        
+
     for k, v in kwargs.items():
         k, v = k.upper(), str(v).upper()
         key_b = k.encode('utf-8')
@@ -540,7 +540,7 @@ def _calculate_default_transform(
     extent[:] = [0.0, 0.0, 0.0, 0.0]
     geotransform[:] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
 
-    # Make an in-memory raster dataset we can pass to 
+    # Make an in-memory raster dataset we can pass to
     # GDALCreateGenImgProjTransformer().
     transform = from_bounds(left, bottom, right, top, width, height)
     img = np.empty((height, width))
@@ -565,9 +565,14 @@ def _calculate_default_transform(
         except CPLE_NotSupported as err:
             raise CRSError(err.errmsg)
         except CPLE_AppDefined as err:
-            log.debug("Encountered points outside of valid dst crs region")
-            raise
-            #pass
+            if "Reprojection failed" in str(err):
+                # This "exception" should be treated as a debug msg, not error
+                # "Reprojection failed, err = -14, further errors will be
+                # suppressed on the transform object."
+                log.debug("Encountered points outside of valid dst crs region")
+                pass
+            else:
+                raise err
         finally:
             if wkt != NULL:
                 _gdal.CPLFree(wkt)
diff --git a/rasterio/five.py b/rasterio/compat.py
similarity index 87%
rename from rasterio/five.py
rename to rasterio/compat.py
index 6d207b9..0befc9e 100644
--- a/rasterio/five.py
+++ b/rasterio/compat.py
@@ -11,6 +11,7 @@ if sys.version_info[0] >= 3:   # pragma: no cover
     zip_longest = itertools.zip_longest
     import configparser
     from urllib.parse import urlparse
+    from collections import UserDict
 else:  # pragma: no cover
     string_types = basestring,
     text_type = unicode
@@ -18,3 +19,4 @@ else:  # pragma: no cover
     zip_longest = itertools.izip_longest
     import ConfigParser as configparser
     from urlparse import urlparse
+    from UserDict import UserDict
diff --git a/rasterio/crs.py b/rasterio/crs.py
index 45ac7d6..2b31e86 100644
--- a/rasterio/crs.py
+++ b/rasterio/crs.py
@@ -1,111 +1,131 @@
-"""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._crs import _CRS
 from rasterio.errors import CRSError
-from rasterio.five import string_types
-
+from rasterio.compat import string_types
 
-def is_valid_crs(crs):
-    """Check if valid geographic or projected coordinate reference system."""
-    return is_geographic_crs(crs) or is_projected_crs(crs)
 
+class CRS(_CRS):
+    """A container class for coordinate reference system info
 
-def to_string(crs):
-    """Turn a parameter mapping into a more conventional PROJ.4 string.
+    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
 
-    Mapping keys are tested against the ``all_proj_keys`` list. Values of
-    ``True`` are omitted, leaving the key bare: {'no_defs': True} -> "+no_defs"
-    and items where the value is otherwise not a str, int, or float are
-    omitted.
-    """
-    items = []
-    for k, v in sorted(filter(
-            lambda x: x[0] in all_proj_keys and x[1] is not False and (
-                isinstance(x[1], (bool, int, float)) or
-                isinstance(x[1], string_types)),
-            crs.items())):
-        items.append(
-            "+" + "=".join(
-                map(str, filter(
-                    lambda y: (y or y == 0) and y is not True, (k, v)))))
-    return " ".join(items)
+        +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
 
+    here we use mappings:
 
-def from_string(prjs):
-    """Turn a PROJ.4 string into a mapping of parameters.
+        {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True}
 
-    Bare parameters like "+no_defs" are given a value of ``True``. All keys
-    are checked against the ``all_proj_keys`` list.
+    One can set/get any PROJ.4 parameter using a dict-like key/value pair on the
+    object. You can instantiate the object by simply passing a dict to the
+    constructor. E.g.
 
-    EPSG:nnnn is allowed.
+        crs = CRS({'init': 'epsg:3005'})
 
-    JSON text-encoded strings are allowed.
     """
-    if '{' in prjs:
-        # may be json, try to decode it
-        try:
-            val = json.loads(prjs, strict=False)
-        except ValueError:
-            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])
-
-    parts = [o.lstrip('+') for o in prjs.strip().split()]
-
-    def parse(v):
-        if v in ('True', 'true'):
-            return True
-        elif v in ('False', 'false'):
-            return False
-        else:
-            try:
-                return int(v)
-            except ValueError:
-                pass
+
+    @property
+    def is_valid(self):
+        """Check if valid geographic or projected coordinate reference system."""
+        return self.is_geographic or self.is_projected
+
+    @property
+    def is_epsg_code(self):
+        for val in self.values():
+            if isinstance(val, string_types) and val.lower().startswith('epsg'):
+                return True
+        return False
+
+    def to_string(self):
+        """Turn a parameter mapping into a more conventional PROJ.4 string.
+
+        Mapping keys are tested against the ``all_proj_keys`` list. Values of
+        ``True`` are omitted, leaving the key bare: {'no_defs': True} -> "+no_defs"
+        and items where the value is otherwise not a str, int, or float are
+        omitted.
+        """
+        items = []
+        for k, v in sorted(filter(
+                lambda x: x[0] in all_proj_keys and x[1] is not False and (
+                    isinstance(x[1], (bool, int, float)) or
+                    isinstance(x[1], string_types)),
+                self.items())):
+            items.append("+" + "=".join(map(str, filter(
+                lambda y: (y or y == 0) and y is not True, (k, v)))))
+        return " ".join(items)
+
+    @staticmethod
+    def from_string(prjs):
+        """Turn a PROJ.4 string into a mapping of parameters.
+
+        Bare parameters like "+no_defs" are given a value of ``True``. All keys
+        are checked against the ``all_proj_keys`` list.
+
+        EPSG:nnnn is allowed.
+
+        JSON text-encoded strings are allowed.
+        """
+        if '{' in prjs:
+            # may be json, try to decode it
             try:
-                return float(v)
+                val = json.loads(prjs, strict=False)
             except ValueError:
-                return v
-
-    items = map(
-        lambda kv: len(kv) == 2 and (kv[0], parse(kv[1])) or (kv[0], True),
-        (p.split('=') for p in parts))
-
-    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):
-    """Given an integer code, returns an EPSG-like mapping.
-
-    Note: the input code is not validated against an EPSG database.
-    """
-    if int(code) <= 0:
-        raise ValueError("EPSG codes are positive integers")
-    return {'init': "epsg:%s" % code, 'no_defs': True}
-
+                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 CRS.from_epsg(prjs.split(':')[1])
+
+        parts = [o.lstrip('+') for o in prjs.strip().split()]
+
+        def parse(v):
+            if v in ('True', 'true'):
+                return True
+            elif v in ('False', 'false'):
+                return False
+            else:
+                try:
+                    return int(v)
+                except ValueError:
+                    pass
+                try:
+                    return float(v)
+                except ValueError:
+                    return v
+
+        items = map(
+            lambda kv: len(kv) == 2 and (kv[0], parse(kv[1])) or (kv[0], True),
+            (p.split('=') for p in parts))
+
+        out = CRS((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
+
+    @staticmethod
+    def from_epsg(code):
+        """Given an integer code, returns an EPSG-like mapping.
+
+        Note: the input code is not validated against an EPSG database.
+        """
+        if int(code) <= 0:
+            raise ValueError("EPSG codes are positive integers")
+        return CRS(init="epsg:%s" % code, no_defs=True)
+
+    def __repr__(self):
+        # Should use super() here, but what's the best way to be compatible
+        # between Python 2 and 3?
+        return "CRS({})".format(dict.__repr__(self.data))
+
+    def to_dict(self):
+        return self.data
 
 # Below is the big list of PROJ4 parameters from
 # http://trac.osgeo.org/proj/wiki/GenParms.
@@ -220,6 +240,5 @@ _param_data = """
 """
 
 _lines = filter(lambda x: len(x) > 1, _param_data.split("\n"))
-all_proj_keys = list(
-    set(line.split()[0].lstrip("+").strip() for line in _lines)
-    ) + ['no_mayo']
+all_proj_keys = list(set(line.split()[0].lstrip("+").strip()
+                         for line in _lines)) + ['no_mayo']
diff --git a/rasterio/dtypes.py b/rasterio/dtypes.py
index 4f6c4d4..00cb8e3 100644
--- a/rasterio/dtypes.py
+++ b/rasterio/dtypes.py
@@ -100,10 +100,10 @@ def get_minimum_dtype(values):
     -------
     rasterio dtype string
     """
-    import numpy
+    import numpy as np
 
     if not is_ndarray(values):
-        values = numpy.array(values)
+        values = np.array(values)
 
     min_value = values.min()
     max_value = values.max()
@@ -129,9 +129,9 @@ def get_minimum_dtype(values):
 
 def is_ndarray(array):
     """Check if array is a ndarray."""
-    import numpy
+    import numpy as np
 
-    return isinstance(array, numpy.ndarray) or hasattr(array, '__array__')
+    return isinstance(array, np.ndarray) or hasattr(array, '__array__')
 
 
 def can_cast_dtype(values, dtype):
@@ -147,19 +147,19 @@ def can_cast_dtype(values, dtype):
     boolean
         True if values can be cast to data type.
     """
-    import numpy
+    import numpy as np
 
     if not is_ndarray(values):
-        values = numpy.array(values)
+        values = np.array(values)
 
-    if values.dtype.name == numpy.dtype(dtype).name:
+    if values.dtype.name == np.dtype(dtype).name:
         return True
 
     elif values.dtype.kind == 'f':
-        return numpy.allclose(values, values.astype(dtype))
+        return np.allclose(values, values.astype(dtype))
 
     else:
-        return numpy.array_equal(values, values.astype(dtype))
+        return np.array_equal(values, values.astype(dtype))
 
 
 def validate_dtype(values, valid_dtypes):
@@ -176,10 +176,10 @@ def validate_dtype(values, valid_dtypes):
     boolean:
         True if dtype of values is one of valid_dtypes
     """
-    import numpy
+    import numpy as np
 
     if not is_ndarray(values):
-        values = numpy.array(values)
+        values = np.array(values)
 
     return (values.dtype.name in valid_dtypes or
             get_minimum_dtype(values) in valid_dtypes)
diff --git a/rasterio/env.py b/rasterio/env.py
index e76702f..0191a2f 100644
--- a/rasterio/env.py
+++ b/rasterio/env.py
@@ -6,7 +6,7 @@ from rasterio._drivers import (
     GDALEnv, del_gdal_config, get_gdal_config, set_gdal_config)
 from rasterio.dtypes import check_dtype
 from rasterio.errors import EnvError
-from rasterio.five import string_types
+from rasterio.compat import string_types
 from rasterio.transform import guard_transform
 from rasterio.vfs import parse_path, vsi_path
 
@@ -16,6 +16,10 @@ _env = None
 
 log = logging.getLogger(__name__)
 
+# Rasterio defaults
+default_options = {
+    'CHECK_WITH_INVERT_PROJ': True
+}
 
 class Env(object):
     """Abstraction for GDAL and AWS configuration
@@ -33,7 +37,7 @@ class Env(object):
 
     Example:
 
-        with Env(GDAL_CACHEMAX=512) as env:
+        with rasterio.Env(GDAL_CACHEMAX=512) as env:
             # All drivers are registered, GDAL's raster block cache
             # size is set to 512MB.
             # Commence processing...
@@ -56,7 +60,7 @@ class Env(object):
         """Create a new GDAL/AWS environment.
 
         Note: this class is a context manager. GDAL isn't configured
-        until the context is entered via `with Env():`
+        until the context is entered via `with rasterio.Env():`
 
         Parameters
         ----------
@@ -154,6 +158,7 @@ def defenv():
         log.debug("Environment %r exists", _env)
     else:
         _env = GDALEnv()
+        _env.update_config_options(**default_options)
         log.debug(
             "New GDAL environment %r created", _env)
 
diff --git a/rasterio/plot.py b/rasterio/plot.py
index 3646091..796fb4c 100644
--- a/rasterio/plot.py
+++ b/rasterio/plot.py
@@ -10,84 +10,197 @@ from __future__ import absolute_import
 import logging
 import warnings
 
-import rasterio
+import numpy as np
 
-try:
-    import matplotlib.pyplot as plt
-except ImportError:  # pragma: no cover
-    plt = None
-except RuntimeError as e:  # pragma: no cover
-    # Certain environment configurations can trigger a RuntimeError like:
+import rasterio
+from rasterio._io import RasterReader
 
-    # Trying to import matplotlibRuntimeError: Python is not installed as a
-    # framework. The Mac OS X backend will not be able to function correctly
-    # if Python is not installed as a framework. See the Python ...
-    warnings.warn(str(e), RuntimeWarning, stacklevel=2)
-    plt = None
+from rasterio.compat import zip_longest
 
+logger = logging.getLogger(__name__)
 
-from rasterio.five import zip_longest
 
-logger = logging.getLogger(__name__)
+def get_plt():
+    """import matplotlib.pyplot
+    raise import error if matplotlib is not installed
+    """
+    try:
+        import matplotlib.pyplot as plt
+        return plt
+    except (ImportError, RuntimeError):  # pragma: no cover
+        msg = "Could not import matplotlib\n"
+        msg += "matplotlib required for plotting functions"
+        raise ImportError(msg)
 
 
-def show(source, cmap='gray', with_bounds=True):
+def show(source, with_bounds=True, contour=False, contour_label_kws=None,
+         ax=None, title=None, **kwargs):
     """Display a raster or raster band using matplotlib.
 
     Parameters
     ----------
-    source : array-like or (raster dataset, bidx)
-        If array-like, should be of format compatible with
-        matplotlib.pyplot.imshow. If the tuple (raster dataset, bidx),
-        selects band `bidx` from raster.
-    cmap : str (opt)
-        Specifies the colormap to use in plotting. See
-        matplotlib.Colors.Colormap. Default is 'gray'.
+    source : array-like in raster axis order,
+        or (raster dataset, bidx) tuple,
+        or raster dataset,
+        If the tuple (raster dataset, bidx),
+        selects band `bidx` from raster.  If raster dataset display the rgb image
+        as defined in the colorinterp metadata, or default to first band.
     with_bounds : bool (opt)
         Whether to change the image extent to the spatial bounds of the image,
         rather than pixel coordinates. Only works when source is
-        (raster dataset, bidx).
+        (raster dataset, bidx) or raster dataset.
+    contour : bool (opt)
+        Whether to plot the raster data as contours
+    contour_label_kws : dictionary (opt)
+        Keyword arguments for labeling the contours,
+        empty dictionary for no labels.
+    ax : matplotlib axis (opt)
+        Axis to plot on, otherwise uses current axis.
+    title : str, optional
+        Title for the figure.
+    **kwargs : key, value pairings optional
+        These will be passed to the matplotlib imshow or contour method
+        depending on contour argument.
+        See full lists at:
+        http://matplotlib.org/api/axes_api.html?highlight=imshow#matplotlib.axes.Axes.imshow
+        or
+        http://matplotlib.org/api/axes_api.html?highlight=imshow#matplotlib.axes.Axes.contour
+
+    Returns
+    -------
+    ax : matplotlib Axes
+        Axes with plot.
     """
+    plt = get_plt()
+
     if isinstance(source, tuple):
         arr = source[0].read(source[1])
-        xs = source[0].res[0] / 2.
-        ys = source[0].res[1] / 2.
         if with_bounds:
-            extent = (source[0].bounds.left - xs, source[0].bounds.right - xs,
-                      source[0].bounds.bottom - ys, source[0].bounds.top - ys)
+            kwargs['extent'] = plotting_extent(source[0])
+    elif isinstance(source, RasterReader):
+        if source.count == 1:
+            arr = source.read(1, masked=True)
         else:
-            extent = None
+            try:
+                source_colorinterp = {source.colorinterp(n): n for n in source.indexes}
+                colorinterp = rasterio.enums.ColorInterp
+                rgb_indexes = [source_colorinterp[ci] for ci in
+                               (colorinterp.red, colorinterp.green, colorinterp.blue)]
+                arr = source.read(rgb_indexes, masked=True)
+                arr = reshape_as_image(arr)
+
+                if with_bounds:
+                    kwargs['extent'] = plotting_extent(source)
+            except KeyError:
+                arr = source.read(1, masked=True)
     else:
-        arr = source
-        extent = None
-    if plt is not None:
-        plt.imshow(arr, cmap=cmap, extent=extent)
-        fig = plt.gcf()
-        fig.show()
-    else:  # pragma: no cover
-        raise ImportError("matplotlib could not be imported")
+        # The source is a numpy array reshape it to image if it has 3+ bands
+        source = np.ma.squeeze(source)
+        if len(source.shape) >= 3:
+            arr = reshape_as_image(source)
+        else:
+            arr = source
+
+    show = False
+    if not ax:
+        show = True
+        ax = plt.gca()
+
+    if contour:
+        if 'cmap' not in kwargs:
+            kwargs['colors'] = kwargs.get('colors', 'red')
+        kwargs['linewidths'] = kwargs.get('linewidths', 1.5)
+        kwargs['alpha'] = kwargs.get('alpha', 0.8)
+
+        C = ax.contour(arr, origin='upper', **kwargs)
+        if contour_label_kws is None:
+            # no explicit label kws passed use defaults
+            contour_label_kws = dict(fontsize=8,
+                                     inline=True)
+        if contour_label_kws:
+            ax.clabel(C, **contour_label_kws)
+    else:
+        ax.imshow(arr, **kwargs)
+    if title:
+        ax.set_title(title, fontweight='bold')
+
+    if show:
+        plt.show()
+
+    return ax
+
+
+def plotting_extent(source):
+    """Returns an extent in the format needed
+     for matplotlib's imshow (left, right, bottom, top)
+     instead of rasterio's bounds (left, bottom, top, right)
+
+    Parameters
+    ----------
+    source : raster dataset
+    """
+    extent = (source.bounds.left, source.bounds.right,
+              source.bounds.bottom, source.bounds.top)
+    return extent
+
+
+def reshape_as_image(arr):
+    """Returns the source array reshaped into the order
+    expected by image processing and visualization software
+    (matplotlib, scikit-image, etc)
+    by swapping the axes order from (bands, rows, columns)
+    to (rows, columns, bands)
+
+    Parameters
+    ----------
+    source : array-like in a of format (bands, rows, columns)
+    """
+    # swap the axes order from (bands, rows, columns) to (rows, columns, bands)
+    im = np.ma.transpose(arr, [1,2,0])
+    return im
 
 
-def show_hist(source, bins=10, masked=True, title='Histogram'):
+
+def reshape_as_raster(arr):
+    """Returns the array in a raster order
+    by swapping the axes order from (rows, columns, bands)
+    to (bands, rows, columns)
+
+    Parameters
+    ----------
+    arr : array-like in the image form of (rows, columns, bands)
+    """
+    # swap the axes order from (rows, columns, bands) to (bands, rows, columns)
+    im = np.transpose(arr, [2,0,1])
+    return im
+
+
+def show_hist(source, bins=10, masked=True, title='Histogram', ax=None, **kwargs):
     """Easily display a histogram with matplotlib.
 
     Parameters
     ----------
-    bins : int, optional
-        Compute histogram across N bins.
-    data : np.array or rasterio.Band or tuple(dataset, bidx)
+    source : np.array or RasterReader, rasterio.Band or tuple(dataset, bidx)
         Input data to display.  The first three arrays in multi-dimensional
         arrays are plotted as red, green, and blue.
+    bins : int, optional
+        Compute histogram across N bins.
     masked : bool, optional
         When working with a `rasterio.Band()` object, specifies if the data
         should be masked on read.
     title : str, optional
         Title for the figure.
+    ax : matplotlib axes (opt)
+        The raster will be added to this axes if passed.
+    **kwargs : optional keyword arguments
+        These will be passed to the matplotlib hist method. See full list at:
+        http://matplotlib.org/api/axes_api.html?highlight=imshow#matplotlib.axes.Axes.hist
     """
-    if plt is None:  # pragma: no cover
-        raise ImportError("Could not import matplotlib")
+    plt = get_plt()
 
-    if isinstance(source, (tuple, rasterio.Band)):
+    if isinstance(source, RasterReader):
+        arr = source.read(masked=masked)
+    elif isinstance(source, (tuple, rasterio.Band)):
         arr = source[0].read(source[1], masked=masked)
     else:
         arr = source
@@ -97,10 +210,20 @@ def show_hist(source, bins=10, masked=True, title='Histogram'):
     rng = arr.min(), arr.max()
 
     if len(arr.shape) is 2:
-        arr = [arr]
+        arr = np.expand_dims(arr.flatten(), 0).T
         colors = ['gold']
     else:
-        colors = ('red', 'green', 'blue', 'violet', 'gold', 'saddlebrown')
+        arr = arr.reshape(arr.shape[0], -1).T
+        colors = ['red', 'green', 'blue', 'violet', 'gold', 'saddlebrown']
+
+    #The goal is to provide a curated set of colors for working with
+    # smaller datasets and let matplotlib define additional colors when
+    # working with larger datasets.
+    if arr.shape[-1] > len(colors):
+        n = arr.shape[-1] - len(colors)
+        colors.extend(np.ndarray.tolist(plt.get_cmap('Accent')(np.linspace(0, 1, n))))
+    else:
+        colors = colors[:arr.shape[-1]]
 
     # If a rasterio.Band() is given make sure the proper index is displayed
     # in the legend.
@@ -109,28 +232,25 @@ def show_hist(source, bins=10, masked=True, title='Histogram'):
     else:
         labels = (str(i + 1) for i in range(len(arr)))
 
-    # This loop should add a single plot each band in the input array,
-    # regardless of if the number of bands exceeds the number of colors.
-    # The colors slicing ensures that the number of iterations always
-    # matches the number of bands.
-    # The goal is to provide a curated set of colors for working with
-    # smaller datasets and let matplotlib define additional colors when
-    # working with larger datasets.
-    for bnd, color, label in zip_longest(arr, colors[:len(arr)], labels):
-
-        plt.hist(
-            bnd.flatten(),
-            bins=bins,
-            alpha=0.5,
-            color=color,
-            label=label,
-            range=rng
-        )
-
-    plt.legend(loc="upper right")
-    plt.title(title, fontweight='bold')
-    plt.grid(True)
-    plt.xlabel('DN')
-    plt.ylabel('Frequency')
-    fig = plt.gcf()
-    fig.show()
+    if ax:
+        show = False
+    else:
+        show = True
+        ax = plt.gca()
+
+    fig = ax.get_figure()
+
+    ax.hist(arr,
+             bins=bins,
+             color=colors,
+             label=labels,
+             range=rng,
+             **kwargs)
+
+    ax.legend(loc="upper right")
+    ax.set_title(title, fontweight='bold')
+    ax.grid(True)
+    ax.set_xlabel('DN')
+    ax.set_ylabel('Frequency')
+    if show:
+        plt.show()
diff --git a/rasterio/rio/bounds.py b/rasterio/rio/bounds.py
index c9b231f..6b4d1b5 100644
--- a/rasterio/rio/bounds.py
+++ b/rasterio/rio/bounds.py
@@ -10,7 +10,6 @@ from cligj import (
 
 from .helpers import write_features, to_lower
 import rasterio
-from rasterio.env import Env
 from rasterio.warp import transform_bounds
 
 logger = logging.getLogger('rio')
@@ -107,7 +106,7 @@ def bounds(ctx, input, precision, indent, compact, projection, dst_crs,
                 self._ys.extend(bbox[1::2])
 
     try:
-        with Env(CPL_DEBUG=verbosity > 2) as env:
+        with rasterio.Env(CPL_DEBUG=verbosity > 2) as env:
             write_features(
                 stdout, Collection(env), sequence=sequence,
                 geojson_type=geojson_type, use_rs=use_rs,
diff --git a/rasterio/rio/calc.py b/rasterio/rio/calc.py
index 30160a6..a04ef57 100644
--- a/rasterio/rio/calc.py
+++ b/rasterio/rio/calc.py
@@ -10,7 +10,6 @@ from cligj import files_inout_arg
 from .helpers import resolve_inout
 from . import options
 import rasterio
-from rasterio.env import Env
 from rasterio.fill import fillnodata
 from rasterio.features import sieve
 
@@ -90,7 +89,7 @@ def calc(ctx, command, files, output, name, dtype, masked, force_overwrite,
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
 
     try:
-        with Env(CPL_DEBUG=verbosity > 2) as env:
+        with rasterio.Env(CPL_DEBUG=verbosity > 2):
             output, files = resolve_inout(files=files, output=output,
                                           force_overwrite=force_overwrite)
 
diff --git a/rasterio/rio/clip.py b/rasterio/rio/clip.py
index 6bacebd..2ba6363 100644
--- a/rasterio/rio/clip.py
+++ b/rasterio/rio/clip.py
@@ -7,7 +7,6 @@ from .helpers import resolve_inout
 from . import options
 import rasterio
 from rasterio.coords import disjoint_bounds
-from rasterio.env import Env
 
 
 # Clip command
@@ -56,7 +55,7 @@ def clip(
 
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
 
-    with Env(CPL_DEBUG=verbosity > 2) as env:
+    with rasterio.Env(CPL_DEBUG=verbosity > 2):
 
         output, files = resolve_inout(files=files, output=output)
         input = files[0]
diff --git a/rasterio/rio/convert.py b/rasterio/rio/convert.py
index 49925f3..90d6041 100644
--- a/rasterio/rio/convert.py
+++ b/rasterio/rio/convert.py
@@ -9,7 +9,6 @@ import numpy as np
 from .helpers import resolve_inout
 from . import options
 import rasterio
-from rasterio.env import Env
 
 
 @click.command(short_help="Copy and convert raster dataset.")
@@ -57,7 +56,7 @@ def convert(
     """
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
 
-    with Env(CPL_DEBUG=verbosity > 2) as env:
+    with rasterio.Env(CPL_DEBUG=verbosity > 2):
 
         outputfile, files = resolve_inout(files=files, output=output)
         inputfile = files[0]
diff --git a/rasterio/rio/edit_info.py b/rasterio/rio/edit_info.py
index 3e7a188..0e6db02 100644
--- a/rasterio/rio/edit_info.py
+++ b/rasterio/rio/edit_info.py
@@ -7,9 +7,10 @@ import click
 
 from . import options
 import rasterio
-import rasterio.crs
-from rasterio.env import Env
+from rasterio.crs import CRS
+from rasterio.errors import CRSError
 from rasterio.transform import guard_transform
+from rasterio.compat import string_types
 
 
 # Handlers for info module options.
@@ -30,7 +31,12 @@ def crs_handler(ctx, param, value):
             retval = json.loads(value)
         except ValueError:
             retval = value
-        if not rasterio.crs.is_valid_crs(retval):
+        try:
+            if isinstance(retval, dict):
+                retval = CRS(retval)
+            elif isinstance(retval, string_types):
+                retval = CRS.from_string(retval)
+        except CRSError:
             raise click.BadParameter(
                 "'%s' is not a recognized CRS." % retval,
                 param=param, param_hint='crs')
@@ -126,7 +132,7 @@ def edit(ctx, input, nodata, crs, transform, tags, allmd, like):
         rng = infos[np.dtype(dtype).kind](dtype)
         return rng.min <= value <= rng.max
 
-    with Env(CPL_DEBUG=(verbosity > 2)) as env:
+    with rasterio.Env(CPL_DEBUG=(verbosity > 2)):
 
         with rasterio.open(input, 'r+') as dst:
 
diff --git a/rasterio/rio/env.py b/rasterio/rio/env.py
index 865ecde..5644e20 100644
--- a/rasterio/rio/env.py
+++ b/rasterio/rio/env.py
@@ -5,8 +5,6 @@ import logging
 import click
 
 import rasterio
-import rasterio.crs
-from rasterio.env import Env
 
 
 @click.command(short_help="Print information about the rio environment.")
@@ -19,7 +17,7 @@ def env(ctx, key):
     """
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
     stdout = click.get_text_stream('stdout')
-    with Env(CPL_DEBUG=(verbosity > 2)) as env:
+    with rasterio.Env(CPL_DEBUG=(verbosity > 2)) as env:
         if key == 'formats':
             for k, v in sorted(env.drivers().items()):
                 stdout.write("%s: %s\n" % (k, v))
diff --git a/rasterio/rio/helpers.py b/rasterio/rio/helpers.py
index 6d0700c..196e812 100644
--- a/rasterio/rio/helpers.py
+++ b/rasterio/rio/helpers.py
@@ -88,7 +88,7 @@ def resolve_inout(input=None, output=None, files=None, force_overwrite=False):
             resolved_output):
         raise FileOverwriteError(
             "file exists and won't be overwritten without use of the "
-            "`-f` or `-o` options.")
+            "`--force-overwrite` or `--output` options.")
     resolved_inputs = (
         [input] if input else [] +
         list(files[:-1 if not output else None]) if files else [])
diff --git a/rasterio/rio/info.py b/rasterio/rio/info.py
index c1184fe..073cada 100644
--- a/rasterio/rio/info.py
+++ b/rasterio/rio/info.py
@@ -5,9 +5,7 @@ import json
 import click
 
 from . import options
-import rasterio.crs
-from rasterio.env import Env
-
+import rasterio
 
 @click.command(short_help="Print information about a data file.")
 @options.file_in_arg
@@ -63,18 +61,20 @@ def info(ctx, input, aspect, indent, namespace, meta_member, verbose, bidx,
     verbosity = ctx.obj.get('verbosity')
     mode = 'r' if (verbose or meta_member == 'stats') else 'r-'
     try:
-        with Env(CPL_DEBUG=(verbosity > 2)):
+        with rasterio.Env(CPL_DEBUG=(verbosity > 2)):
             with rasterio.open(input, mode) as src:
                 info = src.profile
                 info['transform'] = info['affine'][:6]
                 del info['affine']
                 info['shape'] = info['height'], info['width']
                 info['bounds'] = src.bounds
-                proj4 = rasterio.crs.to_string(src.crs)
+                proj4 = src.crs.to_string()
                 if proj4.startswith('+init=epsg'):
                     proj4 = proj4.split('=')[1].upper()
                 info['crs'] = proj4
                 info['res'] = src.res
+                info['colorinterp'] = [src.colorinterp(i).name
+                                       for i in src.indexes]
                 if proj4 != '':
                     info['lnglat'] = src.lnglat()
                 if verbose:
diff --git a/rasterio/rio/insp.py b/rasterio/rio/insp.py
index be51df7..49993f9 100644
--- a/rasterio/rio/insp.py
+++ b/rasterio/rio/insp.py
@@ -1,5 +1,4 @@
-"""Fetch and edit raster dataset metadata from the command line.
-"""
+"""Fetch and edit raster dataset metadata from the command line."""
 from __future__ import absolute_import
 
 import code
@@ -8,12 +7,11 @@ import sys
 import collections
 import warnings
 
-import numpy
+import numpy as np
 import click
 
 from . import options
 import rasterio
-from rasterio.env import Env
 from rasterio.plot import show, show_hist
 
 try:
@@ -39,18 +37,17 @@ funcs = locals()
 
 
 def stats(source):
-    """Return a tuple with raster min, max, and mean.
-    """
+    """Return a tuple with raster min, max, and mean."""
     if isinstance(source, tuple):
         arr = source[0].read(source[1])
     else:
         arr = source
-    return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
+    return Stats(np.min(arr), np.max(arr), np.mean(arr))
 
 
 def main(banner, dataset, alt_interpreter=None):
-    """ Main entry point for use with python interpreter """
-    local = dict(funcs, src=dataset, np=numpy, rio=rasterio, plt=plt)
+    """Main entry point for use with python interpreter."""
+    local = dict(funcs, src=dataset, np=np, rio=rasterio, plt=plt)
     if not alt_interpreter:
         code.interact(banner, local=local)
     elif alt_interpreter == 'ipython':  # pragma: no cover
@@ -75,12 +72,11 @@ def main(banner, dataset, alt_interpreter=None):
     help="File mode (default 'r').")
 @click.pass_context
 def insp(ctx, input, mode, interpreter):
-    """ Open the input file in a Python interpreter.
-    """
+    """Open the input file in a Python interpreter."""
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
     logger = logging.getLogger('rio')
     try:
-        with Env(CPL_DEBUG=verbosity > 2) as env:
+        with rasterio.Env(CPL_DEBUG=verbosity > 2):
             with rasterio.open(input, mode) as src:
                 main(
                     'Rasterio %s Interactive Inspector (Python %s)\n'
diff --git a/rasterio/rio/main.py b/rasterio/rio/main.py
index a8acec4..c2605b2 100644
--- a/rasterio/rio/main.py
+++ b/rasterio/rio/main.py
@@ -1,5 +1,33 @@
 """
 Main command group for Rasterio's CLI.
+
+Subcommands developed as a part of the Rasterio package have their own
+modules under ``rasterio.rio`` (like ``rasterio/rio/info.py``) and are
+registered in the 'rasterio.rio_commands' entry point group in
+Rasterio's ``setup.py``:
+
+    entry_points='''
+        [console_scripts]
+        rio=rasterio.rio.main:main_group
+
+        [rasterio.rio_commands]
+        bounds=rasterio.rio.bounds:bounds
+        calc=rasterio.rio.calc:calc
+        ...
+
+Users may create their own ``rio`` subcommands by writing modules that
+register entry points in Rasterio's 'rasterio.rio_plugins' group. See
+for example https://github.com/sgillies/rio-plugin-example, which has
+been published to PyPI as ``rio-metasay``.
+
+There's no advantage to making a ``rio`` subcommand which doesn't
+import rasterio. But if you are using rasterio, you may profit from
+Rasterio's CLI infrastructure and the network of existing commands.
+Please add yours to the registry
+
+  https://github.com/mapbox/rasterio/wiki/Rio-plugin-registry
+
+so that other ``rio`` users may find it.
 """
 
 
@@ -32,17 +60,18 @@ def gdal_version_cb(ctx, param, value):
 @click.group()
 @cligj.verbose_opt
 @cligj.quiet_opt
- at click.option('--aws-profile', help="Use a specific profile from your shared AWS credentials file")
+ at click.option(
+    '--aws-profile',
+    help="Use a specific profile from your shared AWS credentials file")
 @click.version_option(version=rasterio.__version__, message='%(version)s')
- at click.option('--gdal-version', is_eager=True, is_flag=True, callback=gdal_version_cb)
+ at click.option(
+    '--gdal-version', is_eager=True, is_flag=True, callback=gdal_version_cb)
 @click.pass_context
 def main_group(ctx, verbose, quiet, aws_profile, gdal_version):
-
+    """Rasterio command line interface.
     """
-    Rasterio command line interface.
-    """
-
     verbosity = verbose - quiet
     configure_logging(verbosity)
     ctx.obj = {}
     ctx.obj['verbosity'] = verbosity
+    ctx.obj['aws_profile'] = aws_profile
diff --git a/rasterio/rio/mask.py b/rasterio/rio/mask.py
index 9f5e64f..1f08667 100644
--- a/rasterio/rio/mask.py
+++ b/rasterio/rio/mask.py
@@ -8,7 +8,6 @@ import cligj
 from .helpers import resolve_inout
 from . import options
 import rasterio
-from rasterio.env import Env
 
 logger = logging.getLogger('rio')
 
@@ -45,7 +44,6 @@ def mask(
         invert,
         force_overwrite,
         creation_options):
-
     """Masks in raster using GeoJSON features (masks out all areas not covered
     by features), and optionally crops the output raster to the extent of the
     features.  Features are assumed to be in the same coordinate reference
@@ -67,7 +65,6 @@ def mask(
     --crop option is not valid if features are completely outside extent of
     input raster.
     """
-
     from rasterio.mask import mask as mask_tool
     from rasterio.features import bounds as calculate_bounds
 
@@ -87,7 +84,7 @@ def mask(
         click.echo('Invert option ignored when using --crop', err=True)
         invert = False
 
-    with Env(CPL_DEBUG=verbosity > 2) as env:
+    with rasterio.Env(CPL_DEBUG=verbosity > 2):
         try:
             with click.open_file(geojson_mask) as fh:
                 geojson = json.loads(fh.read())
diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py
index 97dce03..d44c671 100644
--- a/rasterio/rio/merge.py
+++ b/rasterio/rio/merge.py
@@ -1,8 +1,6 @@
-# Merge command.
+"""Merge command."""
 
 import logging
-import math
-import os.path
 
 import click
 from cligj import files_inout_arg, format_opt
@@ -10,8 +8,6 @@ from cligj import files_inout_arg, format_opt
 from .helpers import resolve_inout
 from . import options
 import rasterio
-from rasterio.env import Env
-from rasterio.transform import Affine
 
 
 @click.command(short_help="Merge a stack of raster datasets.")
@@ -48,7 +44,6 @@ def merge(ctx, files, output, driver, bounds, res, nodata, force_overwrite,
       --res 0.1 0.1  => --res 0.1 (square)
       --res 0.1 0.2  => --res 0.1 --res 0.2  (rectangular)
     """
-
     from rasterio.merge import merge as merge_tool
 
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
@@ -56,7 +51,7 @@ def merge(ctx, files, output, driver, bounds, res, nodata, force_overwrite,
     output, files = resolve_inout(
         files=files, output=output, force_overwrite=force_overwrite)
 
-    with Env(CPL_DEBUG=verbosity > 2) as env:
+    with rasterio.Env(CPL_DEBUG=verbosity > 2):
         sources = [rasterio.open(f) for f in files]
         dest, output_transform = merge_tool(sources, bounds=bounds, res=res,
                                             nodata=nodata, precision=precision)
@@ -72,3 +67,10 @@ def merge(ctx, files, output, driver, bounds, res, nodata, force_overwrite,
 
         with rasterio.open(output, 'w', **profile) as dst:
             dst.write(dest)
+
+            # uses the colormap in the first input raster.
+            try:
+                colormap = sources[0].colormap(1)
+                dst.write_colormap(1, colormap)
+            except ValueError:
+                pass
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
index 90250d4..0b00103 100644
--- a/rasterio/rio/options.py
+++ b/rasterio/rio/options.py
@@ -83,6 +83,9 @@ def _cb_key_val(ctx, param, value):
                 out[k] = v
         return out
 
+def abspath_forward_slashes(path):
+    """Return forward-slashed version of os.path.abspath"""
+    return '/'.join(os.path.abspath(path).split(os.path.sep))
 
 def file_in_handler(ctx, param, value):
     """Normalize ordinary filesystem and VFS paths"""
@@ -95,14 +98,14 @@ def file_in_handler(ctx, param, value):
         raise click.BadParameter(
             "Input file {0} does not exist".format(path_to_check))
     if archive and scheme:
-        archive = os.path.abspath(archive)
+        archive = abspath_forward_slashes(archive)
         path = "{0}://{1}!{2}".format(scheme, archive, path)
     elif scheme and scheme.startswith('http'):
         path = "{0}://{1}".format(scheme, path)
     elif scheme == 's3':
         path = "{0}://{1}".format(scheme, path)
     else:
-        path = os.path.abspath(path)
+        path = abspath_forward_slashes(path)
     return path
 
 
diff --git a/rasterio/rio/overview.py b/rasterio/rio/overview.py
index b9990e0..60ed418 100644
--- a/rasterio/rio/overview.py
+++ b/rasterio/rio/overview.py
@@ -10,7 +10,6 @@ import click
 from . import options
 import rasterio
 from rasterio.enums import Resampling
-from rasterio.env import Env
 
 
 def build_handler(ctx, param, value):
@@ -69,7 +68,7 @@ def overview(ctx, input, build, ls, rebuild, resampling):
     """
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
 
-    with Env(CPL_DEBUG=(verbosity > 2)) as env:
+    with rasterio.Env(CPL_DEBUG=(verbosity > 2)):
         with rasterio.open(input, 'r+') as dst:
 
             if ls:
diff --git a/rasterio/rio/rasterize.py b/rasterio/rio/rasterize.py
index dec0a6f..25bc52d 100644
--- a/rasterio/rio/rasterize.py
+++ b/rasterio/rio/rasterize.py
@@ -9,9 +9,9 @@ import cligj
 from .helpers import resolve_inout
 from . import options
 import rasterio
+from rasterio.errors import CRSError
 from rasterio.transform import Affine
 from rasterio.coords import disjoint_bounds
-from rasterio.env import Env
 
 
 logger = logging.getLogger('rio')
@@ -68,7 +68,6 @@ def rasterize(
         prop,
         force_overwrite,
         creation_options):
-
     """Rasterize GeoJSON into a new or existing raster.
 
     If the output raster exists, rio-rasterize will rasterize feature values
@@ -108,8 +107,7 @@ def rasterize(
     of the output or --like rasters at this time.  This functionality may be
     added in the future.
     """
-
-    from rasterio._base import is_geographic_crs, is_same_crs
+    from rasterio.crs import CRS
     from rasterio.features import rasterize
     from rasterio.features import bounds as calculate_bounds
 
@@ -118,8 +116,13 @@ def rasterize(
     output, files = resolve_inout(
         files=files, output=output, force_overwrite=force_overwrite)
 
+    bad_param = click.BadParameter('invalid CRS.  Must be an EPSG code.',
+                                   ctx, param=src_crs, param_hint='--src_crs')
     has_src_crs = src_crs is not None
-    src_crs = src_crs or 'EPSG:4326'
+    try:
+        src_crs = CRS.from_string(src_crs) if has_src_crs else CRS.from_string('EPSG:4326')
+    except CRSError:
+        raise bad_param
 
     # If values are actually meant to be integers, we need to cast them
     # as such or rasterize creates floating point outputs
@@ -128,7 +131,7 @@ def rasterize(
     if fill == int(fill):
         fill = int(fill)
 
-    with Env(CPL_DEBUG=verbosity > 2):
+    with rasterio.Env(CPL_DEBUG=verbosity > 2):
 
         def feature_value(feature):
             if prop and 'properties' in feature:
@@ -151,7 +154,7 @@ def rasterize(
 
         if os.path.exists(output):
             with rasterio.open(output, 'r+') as out:
-                if has_src_crs and not is_same_crs(src_crs, out.crs):
+                if has_src_crs and src_crs != out.crs:
                     raise click.BadParameter('GeoJSON does not match crs of '
                                              'existing output raster',
                                              param='input', param_hint='input')
@@ -171,7 +174,7 @@ def rasterize(
                     all_touched=all_touched,
                     dtype=meta.get('dtype', None),
                     default_value=default_value,
-                    fill = fill)
+                    fill=fill)
 
                 for bidx in range(1, meta['count'] + 1):
                     data = out.read(bidx, masked=True)
@@ -185,7 +188,7 @@ def rasterize(
             if like is not None:
                 template_ds = rasterio.open(like)
 
-                if has_src_crs and not is_same_crs(src_crs, template_ds.crs):
+                if has_src_crs and src_crs != template_ds.crs:
                     raise click.BadParameter('GeoJSON does not match crs of '
                                              '--like raster',
                                              param='input', param_hint='input')
@@ -209,7 +212,7 @@ def rasterize(
             else:
                 bounds = bounds or geojson_bounds
 
-                if is_geographic_crs(src_crs):
+                if src_crs.is_geographic:
                     if (bounds[0] < -180 or bounds[2] > 180 or
                             bounds[1] < -80 or bounds[3] > 80):
                         raise click.BadParameter(
@@ -238,12 +241,6 @@ def rasterize(
                     height = max(int(ceil((bounds[3] - bounds[1]) /
                                  float(res[1]))), 1)
 
-                src_crs = src_crs.upper()
-                if not src_crs.count('EPSG:'):
-                    raise click.BadParameter(
-                        'invalid CRS.  Must be an EPSG code.',
-                        ctx, param=src_crs, param_hint='--src_crs')
-
                 kwargs = {
                     'count': 1,
                     'crs': src_crs,
@@ -262,7 +259,7 @@ def rasterize(
                 all_touched=all_touched,
                 dtype=kwargs.get('dtype', None),
                 default_value=default_value,
-                fill = fill)
+                fill=fill)
 
             if 'dtype' not in kwargs:
                 kwargs['dtype'] = result.dtype
diff --git a/rasterio/rio/sample.py b/rasterio/rio/sample.py
index 65656cc..6e3ffc4 100644
--- a/rasterio/rio/sample.py
+++ b/rasterio/rio/sample.py
@@ -4,7 +4,6 @@ import logging
 import click
 
 import rasterio
-from rasterio.env import Env
 
 
 @click.command(short_help="Sample a dataset.")
@@ -67,7 +66,7 @@ def sample(ctx, files, bidx):
         points = [input]
 
     try:
-        with Env(CPL_DEBUG=verbosity > 2) as env:
+        with rasterio.Env(CPL_DEBUG=verbosity > 2):
             with rasterio.open(source) as src:
                 if bidx is None:
                     indexes = src.indexes
diff --git a/rasterio/rio/shapes.py b/rasterio/rio/shapes.py
index 993eef8..6848dcb 100644
--- a/rasterio/rio/shapes.py
+++ b/rasterio/rio/shapes.py
@@ -7,8 +7,8 @@ import cligj
 from .helpers import coords, write_features
 from . import options
 import rasterio
-from rasterio.env import Env
 from rasterio.transform import Affine
+from rasterio.crs import CRS
 
 logger = logging.getLogger('rio')
 
@@ -88,7 +88,7 @@ def shapes(
       $ rio shapes --as-mask --bidx 1 tests/data/RGB.byte.tif
     """
     # These import numpy, which we don't want to do unless it's needed.
-    import numpy
+    import numpy as np
     import rasterio.features
     import rasterio.warp
 
@@ -145,14 +145,14 @@ def shapes(
                         msk_shape = (
                             src.height // sampling, src.width // sampling)
                         if bidx is None:
-                            msk = numpy.zeros(
+                            msk = np.zeros(
                                 (src.count,) + msk_shape, 'uint8')
                         else:
-                            msk = numpy.zeros(msk_shape, 'uint8')
+                            msk = np.zeros(msk_shape, 'uint8')
                         msk = src.read_masks(bidx, msk)
 
                     if bidx is None:
-                        msk = numpy.logical_or.reduce(msk).astype('uint8')
+                        msk = np.logical_or.reduce(msk).astype('uint8')
 
                     # Possibly overridden below.
                     img = msk
@@ -162,7 +162,7 @@ def shapes(
                     if sampling == 1:
                         img = src.read(bidx, masked=False)
                     else:
-                        img = numpy.zeros(
+                        img = np.zeros(
                             (src.height // sampling, src.width // sampling),
                             dtype=src.dtypes[src.indexes.index(bidx)])
                         img = src.read(bidx, img, masked=False)
@@ -172,7 +172,7 @@ def shapes(
                 # categories to 2 and likely reduces the number of
                 # shapes.
                 if as_mask:
-                    tmp = numpy.ones_like(img, 'uint8') * 255
+                    tmp = np.ones_like(img, 'uint8') * 255
                     tmp[img == 0] = 0
                     img = tmp
                     if not with_nodata:
@@ -184,7 +184,7 @@ def shapes(
                 ys = [bounds[1], bounds[3]]
                 if projection == 'geographic':
                     xs, ys = rasterio.warp.transform(
-                        src.crs, {'init': 'epsg:4326'}, xs, ys)
+                        src.crs, CRS({'init': 'epsg:4326'}), xs, ys)
                 if precision >= 0:
                     xs = [round(v, precision) for v in xs]
                     ys = [round(v, precision) for v in ys]
@@ -220,7 +220,7 @@ def shapes(
         geojson_type = 'collection'
 
     try:
-        with Env(CPL_DEBUG=(verbosity > 2)) as env:
+        with rasterio.Env(CPL_DEBUG=(verbosity > 2)) as env:
             write_features(
                 stdout, Collection(env), sequence=sequence,
                 geojson_type=geojson_type, use_rs=use_rs,
diff --git a/rasterio/rio/stack.py b/rasterio/rio/stack.py
index fc3e44d..71cf2d3 100644
--- a/rasterio/rio/stack.py
+++ b/rasterio/rio/stack.py
@@ -8,8 +8,7 @@ from cligj import files_inout_arg, format_opt
 from .helpers import resolve_inout
 from . import options
 import rasterio
-from rasterio.env import Env
-from rasterio.five import zip_longest
+from rasterio.compat import zip_longest
 
 
 # Stack command.
@@ -55,11 +54,10 @@ def stack(ctx, files, output, driver, bidx, photometric, force_overwrite,
       rio stack RGB.byte.tif --bidx ..2 RGB.byte.tif --bidx 3.. -o stacked.tif
 
     """
-
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 2
     logger = logging.getLogger('rio')
     try:
-        with Env(CPL_DEBUG=verbosity > 2) as env:
+        with rasterio.Env(CPL_DEBUG=verbosity > 2):
             output, files = resolve_inout(files=files, output=output,
                                           force_overwrite=force_overwrite)
             output_count = 0
diff --git a/rasterio/rio/transform.py b/rasterio/rio/transform.py
index af4fa18..3d3f7f1 100644
--- a/rasterio/rio/transform.py
+++ b/rasterio/rio/transform.py
@@ -8,7 +8,6 @@ import click
 from cligj import precision_opt
 
 import rasterio
-from rasterio.env import Env
 
 
 @click.command(short_help="Transform coordinates.")
@@ -32,7 +31,7 @@ def transform(ctx, input, src_crs, dst_crs, precision):
         src = [input]
 
     try:
-        with Env(CPL_DEBUG=verbosity > 2) as env:
+        with rasterio.Env(CPL_DEBUG=verbosity > 2):
             if src_crs.startswith('EPSG'):
                 src_crs = {'init': src_crs}
             elif os.path.exists(src_crs):
diff --git a/rasterio/rio/warp.py b/rasterio/rio/warp.py
index 874d5e5..456a2cc 100644
--- a/rasterio/rio/warp.py
+++ b/rasterio/rio/warp.py
@@ -8,8 +8,7 @@ from cligj import files_inout_arg, format_opt
 from .helpers import resolve_inout
 from . import options
 import rasterio
-from rasterio import crs
-from rasterio.env import Env
+from rasterio.crs import CRS
 from rasterio.errors import CRSError
 from rasterio.transform import Affine
 from rasterio.warp import (
@@ -48,7 +47,11 @@ def x_dst_bounds_handler(ctx, param, value):
 @files_inout_arg
 @options.output_opt
 @format_opt
- at options.like_file_opt
+ at click.option(
+    '--like',
+    type=click.Path(exists=True),
+    help='Raster dataset to use as a template for obtaining affine '
+         'transform (bounds and resolution), and crs.')
 @click.option('--dst-crs', default=None,
               help='Target coordinate reference system.')
 @options.dimensions_opt
@@ -72,6 +75,10 @@ def x_dst_bounds_handler(ctx, param, value):
 @click.option('--resampling', type=click.Choice([r.name for r in Resampling]),
               default='nearest', help="Resampling method.",
               show_default=True)
+ at click.option('--src-nodata', default=None, show_default=True,
+              type=float, help="Manually override source nodata")
+ at click.option('--dst-nodata', default=None, show_default=True,
+              type=float, help="Manually override destination nodata")
 @click.option('--threads', type=int, default=1,
               help='Number of processing threads.')
 @click.option('--check-invert-proj', type=bool, default=True,
@@ -80,7 +87,7 @@ def x_dst_bounds_handler(ctx, param, value):
 @options.creation_options
 @click.pass_context
 def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
-         x_dst_bounds, bounds, res, resampling, threads, check_invert_proj,
+         x_dst_bounds, bounds, res, resampling, src_nodata, dst_nodata, threads, check_invert_proj,
          force_overwrite, creation_options):
     """
     Warp a raster dataset.
@@ -122,7 +129,6 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
         > --bounds -78 22 -76 24 --res 0.1 --dst-crs EPSG:4326
 
     """
-
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
 
     output, files = resolve_inout(
@@ -137,8 +143,8 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
         # Expand one value to two if needed
         res = (res[0], res[0]) if len(res) == 1 else res
 
-    with Env(CPL_DEBUG=verbosity > 2,
-             CHECK_WITH_INVERT_PROJ=check_invert_proj) as env:
+    with rasterio.Env(CPL_DEBUG=verbosity > 2,
+                      CHECK_WITH_INVERT_PROJ=check_invert_proj):
         with rasterio.open(files[0]) as src:
             l, b, r, t = src.bounds
             out_kwargs = src.meta.copy()
@@ -161,7 +167,7 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
 
             elif dst_crs is not None:
                 try:
-                    dst_crs = crs.from_string(dst_crs)
+                    dst_crs = CRS.from_string(dst_crs)
                 except ValueError as err:
                     raise click.BadParameter(
                         str(err), param='dst_crs', param_hint='dst_crs')
@@ -248,6 +254,26 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
                 dst_width = src.width
                 dst_height = src.height
 
+            # If src_nodata is not None, update the dst metadata NODATA
+            # value to src_nodata (will be overridden by dst_nodata if it is not None
+            if src_nodata is not None:
+                # Update the dst nodata value
+                out_kwargs.update({
+                    'nodata': src_nodata
+                })
+
+            # Validate a manually set destination NODATA value
+            # against the input datatype.
+            if dst_nodata is not None:
+                if src_nodata is None and src.meta['nodata'] is None:
+                    raise click.BadParameter(
+                        "--src-nodata must be provided because dst-nodata is not None")
+                else:
+                    # Update the dst nodata value
+                    out_kwargs.update({
+                        'nodata': dst_nodata
+                        })
+
             # When the bounds option is misused, extreme values of
             # destination width and height may result.
             if (dst_width < 0 or dst_height < 0 or
@@ -275,9 +301,9 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
                         destination=rasterio.band(dst, i),
                         src_transform=src.affine,
                         src_crs=src.crs,
-                        # src_nodata=#TODO
+                        src_nodata=src_nodata,
                         dst_transform=out_kwargs['transform'],
                         dst_crs=out_kwargs['crs'],
-                        # dst_nodata=#TODO
+                        dst_nodata=dst_nodata,
                         resampling=resampling,
                         num_threads=threads)
diff --git a/rasterio/vfs.py b/rasterio/vfs.py
index c5f057f..40ef1fe 100644
--- a/rasterio/vfs.py
+++ b/rasterio/vfs.py
@@ -2,7 +2,7 @@
 
 import os
 
-from rasterio.five import urlparse
+from rasterio.compat import urlparse
 
 
 # NB: As not to propagate fallacies of distributed computing, Rasterio
@@ -59,8 +59,8 @@ def vsi_path(path, archive=None, scheme=None):
     elif scheme and scheme == 's3':
         result = "/vsis3/{0}".format(path)
     elif scheme and scheme != 'file':
-        path = path.strip(os.path.sep)
-        result = os.path.sep.join(
+        path = path.strip('/')
+        result = '/'.join(
             ['/vsi{0}'.format(scheme), archive, path])
     else:
         result = path
diff --git a/rasterio/warp.py b/rasterio/warp.py
index d824d5c..a5d037a 100644
--- a/rasterio/warp.py
+++ b/rasterio/warp.py
@@ -31,10 +31,10 @@ def transform(src_crs, dst_crs, xs, ys, zs=None):
 
     Parameters
     ------------
-    src_crs: dict
-        Source coordinate reference system, in rasterio dict format.
-        Example: {'init': 'EPSG:4326'}
-    dst_crs: dict
+    src_crs: CRS or dict
+        Source coordinate reference system, as a rasterio CRS object.
+        Example: CRS({'init': 'EPSG:4326'})
+    dst_crs: CRS or dict
         Target coordinate reference system.
     xs: array_like
         Contains x values.  Will be cast to double floating point values.
@@ -64,10 +64,10 @@ def transform_geom(
 
     Parameters
     ------------
-    src_crs: dict
+    src_crs: CRS or dict
         Source coordinate reference system, in rasterio dict format.
-        Example: {'init': 'EPSG:4326'}
-    dst_crs: dict
+        Example: CRS({'init': 'EPSG:4326'})
+    dst_crs: CRS or dict
         Target coordinate reference system.
     geom: GeoJSON like dict object
     antimeridian_cutting: bool, optional
@@ -114,10 +114,10 @@ def transform_bounds(
 
     Parameters
     ----------
-    src_crs: dict
+    src_crs: CRS or dict
         Source coordinate reference system, in rasterio dict format.
-        Example: {'init': 'EPSG:4326'}
-    dst_crs: dict
+        Example: CRS({'init': 'EPSG:4326'})
+    dst_crs: CRS or dict
         Target coordinate reference system.
     left, bottom, right, top: float
         Bounding coordinates in src_crs, from the bounds property of a raster.
@@ -195,11 +195,11 @@ def reproject(
     src_transform: affine transform object, optional
         Source affine transformation.  Required if source and destination
         are ndarrays.  Will be derived from source if it is a rasterio Band.
-    src_crs: dict, optional
+    src_crs: CRS or dict, optional
         Source coordinate reference system, in rasterio dict format.
         Required if source and destination are ndarrays.
         Will be derived from source if it is a rasterio Band.
-        Example: {'init': 'EPSG:4326'}
+        Example: CRS({'init': 'EPSG:4326'})
     src_nodata: int or float, optional
         The source nodata value.  Pixels with this value will not be used
         for interpolation.  If not set, it will be default to the
@@ -208,7 +208,7 @@ def reproject(
     dst_transform: affine transform object, optional
         Target affine transformation.  Required if source and destination
         are ndarrays.  Will be derived from target if it is a rasterio Band.
-    dst_crs: dict, optional
+    dst_crs: CRS or dict, optional
         Target coordinate reference system.  Required if source and destination
         are ndarrays.  Will be derived from target if it is a rasterio Band.
     dst_nodata: int or float, optional
@@ -244,11 +244,25 @@ def reproject(
                 ['Resampling.{0}'.format(k) for k in
                  Resampling.__members__.keys() if k != 'gauss'])))
 
+    # If working with identity transform, assume it is crs-less data
+    # and that translating the matrix very slightly will avoid #674
+    eps = 1e-100
+    if src_transform and guard_transform(src_transform).is_identity:
+        src_transform = src_transform.translation(eps, eps)
+    if dst_transform and guard_transform(dst_transform).is_identity:
+        dst_transform = dst_transform.translation(eps, eps)
+
     if src_transform:
         src_transform = guard_transform(src_transform).to_gdal()
     if dst_transform:
         dst_transform = guard_transform(dst_transform).to_gdal()
 
+    # Passing None can cause segfault, use empty dict
+    if src_crs is None:
+        src_crs = {}
+    if dst_crs is None:
+        dst_crs = {}
+
     _reproject(
         source,
         destination,
@@ -286,10 +300,10 @@ def calculate_default_transform(
 
     Parameters
     ----------
-    src_crs: dict
+    src_crs: CRS or dict
         Source coordinate reference system, in rasterio dict format.
-        Example: {'init': 'EPSG:4326'}
-    dst_crs: dict
+        Example: CRS({'init': 'EPSG:4326'})
+    dst_crs: CRS or dict
         Target coordinate reference system.
     width: int
         Source raster width.
@@ -306,7 +320,7 @@ def calculate_default_transform(
 
     Note
     ----
-    Should be called within a raster.env.Env() context
+    Should be called within a rasterio.Env() context
 
     Some behavior of this function is determined by the
     CHECK_WITH_INVERT_PROJ environment variable
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 8651db1..c6aa55e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,6 +4,7 @@ cligj
 cython>=0.23.4
 delocate
 enum34
+ghp-import
 numpy>=1.10
 snuggs>=1.2
 packaging
@@ -13,4 +14,4 @@ setuptools>=0.9.8
 wheel
 sphinx
 sphinx-rtd-theme
-numpydoc
+numpydoc
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 40ea6f9..b06fc94 100755
--- a/setup.py
+++ b/setup.py
@@ -9,6 +9,7 @@
 # source or binary distribution. This is essential when creating self-contained
 # binary wheels.
 
+import itertools
 import logging
 import os
 import pprint
@@ -78,8 +79,8 @@ gdal2plus = False
 gdal_output = [None] * 4
 
 try:
-    import numpy
-    include_dirs.append(numpy.get_include())
+    import numpy as np
+    include_dirs.append(np.get_include())
 except ImportError:
     log.critical("Numpy and its headers are required to run setup(). Exiting.")
     sys.exit(1)
@@ -186,7 +187,9 @@ if os.path.exists("MANIFEST.in") and "clean" not in sys.argv:
         Extension(
             'rasterio._err', ['rasterio/_err.pyx'], **ext_options),
         Extension(
-            'rasterio._example', ['rasterio/_example.pyx'], **ext_options)],
+            'rasterio._example', ['rasterio/_example.pyx'], **ext_options),
+        Extension(
+            'rasterio._crs', ['rasterio/_crs.pyx'], **ext_options)],
         quiet=True, **cythonize_options)
 
 # If there's no manifest template, as in an sdist, we just specify .c files.
@@ -209,7 +212,9 @@ else:
         Extension(
             'rasterio._err', ['rasterio/_err.c'], **ext_options),
         Extension(
-            'rasterio._example', ['rasterio/_example.c'], **ext_options)]
+            'rasterio._example', ['rasterio/_example.c'], **ext_options),
+        Extension(
+            'rasterio._crs', ['rasterio/_crs.c'], **ext_options)]
 
 with open('README.rst') as f:
     readme = f.read()
@@ -220,6 +225,17 @@ inst_reqs = ['affine', 'cligj', 'numpy', 'snuggs', 'click-plugins']
 if sys.version_info < (3, 4):
     inst_reqs.append('enum34')
 
+extra_reqs = {
+    'ipython': ['ipython>=2.0'],
+    's3': ['boto3>=1.2.4'],
+    'plot': ['matplotlib'],
+    'test': [
+        'pytest>=2.8.2', 'pytest-cov>=2.2.0', 'boto3>=1.2.4', 'packaging'],
+    'docs': ['ghp-import', 'numpydoc', 'sphinx', 'sphinx-rtd-theme']}
+
+# Add all extra requirements
+extra_reqs['all'] = list(set(itertools.chain(*extra_reqs.values())))
+
 setup_args = dict(
     name='rasterio',
     version=version,
@@ -232,10 +248,14 @@ setup_args = dict(
         'Intended Audience :: Science/Research',
         'License :: OSI Approved :: BSD License',
         'Programming Language :: C',
+        'Programming Language :: Cython',
         'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 3',
         'Topic :: Multimedia :: Graphics :: Graphics Conversion',
         'Topic :: Scientific/Engineering :: GIS'],
     keywords='raster gdal',
@@ -272,10 +292,7 @@ setup_args = dict(
     ext_modules=ext_modules,
     zip_safe=False,
     install_requires=inst_reqs,
-    extras_require={
-        'ipython': ['ipython>=2.0'],
-        's3': ['boto3>=1.2.4'],
-        'test': ['boto3>=1.2.4', 'packaging']})
+    extras_require=extra_reqs)
 
 if os.environ.get('PACKAGE_DATA'):
     setup_args['package_data'] = {'rasterio': ['gdal_data/*', 'proj_data/*']}
diff --git a/tests/__init__.py b/tests/__init__.py
index 792d600..e69de29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/tests/conftest.py b/tests/conftest.py
index cdc1770..513ed55 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -7,8 +7,9 @@ import sys
 from click.testing import CliRunner
 import py
 import pytest
-import numpy
+import numpy as np
 
+from rasterio.crs import CRS
 
 DEFAULT_SHAPE = (10, 10)
 
@@ -107,10 +108,10 @@ def basic_image():
     Returns
     -------
 
-    numpy ndarray
+    np ndarray
     """
 
-    image = numpy.zeros(DEFAULT_SHAPE, dtype=numpy.uint8)
+    image = np.zeros(DEFAULT_SHAPE, dtype=np.uint8)
     image[2:5, 2:5] = 1
 
     return image
@@ -126,10 +127,10 @@ def basic_image_2x2():
     Returns
     -------
 
-    numpy ndarray
+    np ndarray
     """
 
-    image = numpy.zeros(DEFAULT_SHAPE, dtype=numpy.uint8)
+    image = np.zeros(DEFAULT_SHAPE, dtype=np.uint8)
     image[2:4, 2:4] = 1
 
     return image
@@ -144,7 +145,7 @@ def pixelated_image(basic_image):
     Returns
     -------
 
-    numpy ndarray
+    np ndarray
     """
 
     image = basic_image.copy()
@@ -162,11 +163,11 @@ def diagonal_image():
     Returns
     -------
 
-    numpy ndarray
+    np ndarray
     """
 
-    image = numpy.zeros(DEFAULT_SHAPE, dtype=numpy.uint8)
-    numpy.fill_diagonal(image, 1)
+    image = np.zeros(DEFAULT_SHAPE, dtype=np.uint8)
+    np.fill_diagonal(image, 1)
     return image
 
 
@@ -190,7 +191,7 @@ def basic_image_file(tmpdir, basic_image):
 
     outfilename = str(tmpdir.join('basic_image.tif'))
     kwargs = {
-        "crs": {'init': 'epsg:4326'},
+        "crs": CRS({'init': 'epsg:4326'}),
         "transform": Affine.identity(),
         "count": 1,
         "dtype": rasterio.uint8,
@@ -225,7 +226,7 @@ def pixelated_image_file(tmpdir, pixelated_image):
 
     outfilename = str(tmpdir.join('pixelated_image.tif'))
     kwargs = {
-        "crs": {'init': 'epsg:4326'},
+        "crs": CRS({'init': 'epsg:4326'}),
         "transform": Affine.identity(),
         "count": 1,
         "dtype": rasterio.uint8,
@@ -249,3 +250,8 @@ def gdalenv(request):
             rasterio.env.delenv()
             rasterio.env._env = None
     request.addfinalizer(fin)
+
+
+ at pytest.fixture(scope='module')
+def path_rgb_byte_tif():
+    return os.path.join('tests', 'data', 'RGB.byte.tif')
diff --git a/tests/test_blocks.py b/tests/test_blocks.py
index c9c6504..c2cbb17 100644
--- a/tests/test_blocks.py
+++ b/tests/test_blocks.py
@@ -6,7 +6,7 @@ import sys
 import tempfile
 import unittest
 
-import numpy
+import numpy as np
 
 import rasterio
 
@@ -18,11 +18,11 @@ class WindowTest(unittest.TestCase):
         # Positive height and width are needed when stop is None.
         self.assertRaises(
             ValueError,
-            rasterio.window_shape, 
+            rasterio.window_shape,
             (((10, 20),(10, None)),) )
         self.assertRaises(
             ValueError,
-            rasterio.window_shape, 
+            rasterio.window_shape,
             (((None, 10),(10, 20)),) )
     def test_window_shape_None_start(self):
         self.assertEqual(
@@ -62,7 +62,7 @@ def test_window_index():
     assert r.stop == 4
     assert c.start == 1
     assert c.stop == 12
-    arr = numpy.ones((20,20))
+    arr = np.ones((20,20))
     assert arr[idx].shape == (4, 11)
 
 class RasterBlocksTest(unittest.TestCase):
@@ -88,7 +88,7 @@ class RasterBlocksTest(unittest.TestCase):
         with rasterio.open('tests/data/RGB.byte.tif') as s:
             self.assertEqual(
                 s.width*s.height,
-                sum((w[0][1]-w[0][0])*(w[1][1]-w[1][0]) 
+                sum((w[0][1]-w[0][0])*(w[1][1]-w[1][0])
                     for ji, w in s.block_windows(1)))
 
 class WindowReadTest(unittest.TestCase):
@@ -99,7 +99,7 @@ class WindowReadTest(unittest.TestCase):
             first_block = s.read(1, window=first_window)
             self.assertEqual(first_block.dtype, rasterio.ubyte)
             self.assertEqual(
-                first_block.shape, 
+                first_block.shape,
                 rasterio.window_shape(first_window))
 
 class WindowWriteTest(unittest.TestCase):
@@ -109,10 +109,10 @@ class WindowWriteTest(unittest.TestCase):
         shutil.rmtree(self.tempdir)
     def test_write_window(self):
         name = os.path.join(self.tempdir, "test_write_window.tif")
-        a = numpy.ones((50, 50), dtype=rasterio.ubyte) * 127
+        a = np.ones((50, 50), dtype=rasterio.ubyte) * 127
         with rasterio.open(
-                name, 'w', 
-                driver='GTiff', width=100, height=100, count=1, 
+                name, 'w',
+                driver='GTiff', width=100, height=100, count=1,
                 dtype=a.dtype) as s:
             s.write(a, indexes=1, window=((30, 80), (10, 60)))
         # subprocess.call(["open", name])
diff --git a/tests/test_colorinterp.py b/tests/test_colorinterp.py
index 19b8644..fb623b8 100644
--- a/tests/test_colorinterp.py
+++ b/tests/test_colorinterp.py
@@ -1,7 +1,7 @@
 import pytest
 
 import rasterio
-from rasterio.enums import ColorInterp, PhotometricInterp
+from rasterio.enums import ColorInterp
 
 
 def test_cmyk_interp(tmpdir):
@@ -19,6 +19,7 @@ def test_cmyk_interp(tmpdir):
         assert dst.colorinterp(4) == ColorInterp.black
 
 
+ at pytest.mark.skip(reason="crashing on OS X with Homebrew's GDAL")
 def test_ycbcr_interp(tmpdir):
     """A YCbCr TIFF has red, green, blue bands."""
     with rasterio.open('tests/data/RGB.byte.tif') as src:
diff --git a/tests/test_copy.py b/tests/test_copy.py
index 4d2b4dc..7e54cc2 100644
--- a/tests/test_copy.py
+++ b/tests/test_copy.py
@@ -6,21 +6,23 @@ import subprocess
 import sys
 import tempfile
 
-import numpy
-
 import rasterio
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
+
 class CopyTest(unittest.TestCase):
+
     def setUp(self):
         self.tempdir = tempfile.mkdtemp()
+
     def tearDown(self):
         shutil.rmtree(self.tempdir)
+
     def test_copy(self):
         name = os.path.join(self.tempdir, 'test_copy.tif')
         rasterio.copy(
-            'tests/data/RGB.byte.tif', 
+            'tests/data/RGB.byte.tif',
             name)
         info = subprocess.check_output(["gdalinfo", name])
         self.assert_("GTiff" in info.decode('utf-8'))
diff --git a/tests/test_crs.py b/tests/test_crs.py
index ccead15..d368371 100644
--- a/tests/test_crs.py
+++ b/tests/test_crs.py
@@ -5,9 +5,8 @@ import sys
 import json
 
 import rasterio
-from rasterio import crs
-from rasterio.crs import (
-    is_geographic_crs, is_projected_crs, is_same_crs, is_valid_crs)
+from rasterio._base import _can_create_osr
+from rasterio.crs import CRS
 from rasterio.errors import CRSError
 
 
@@ -17,7 +16,7 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 # 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 == {'init': 'epsg:32618'}
+        assert src.crs.to_dict() == {'init': 'epsg:32618'}
 
 def test_read_epsg3857(tmpdir):
     tiffname = str(tmpdir.join('lol.tif'))
@@ -25,7 +24,7 @@ def test_read_epsg3857(tmpdir):
         'gdalwarp', '-t_srs', 'EPSG:3857',
         'tests/data/RGB.byte.tif', tiffname])
     with rasterio.open(tiffname) as src:
-        assert src.crs == {'init': 'epsg:3857'}
+        assert src.crs.to_dict() == {'init': 'epsg:3857'}
 
 # Ensure that CRS sticks when we write a file.
 def test_write_3857(tmpdir):
@@ -36,7 +35,7 @@ def test_write_3857(tmpdir):
     dst_path = str(tmpdir.join('wut.tif'))
     with rasterio.open(src_path) as src:
         with rasterio.open(dst_path, 'w', **src.meta) as dst:
-            assert dst.crs == {'init': 'epsg:3857'}
+            assert dst.crs.to_dict() == {'init': 'epsg:3857'}
     info = subprocess.check_output([
         'gdalinfo', dst_path])
     # WKT string may vary a bit w.r.t GDAL versions
@@ -45,39 +44,39 @@ def test_write_3857(tmpdir):
 
 def test_from_proj4_json():
     json_str = '{"proj": "longlat", "ellps": "WGS84", "datum": "WGS84"}'
-    crs_dict = crs.from_string(json_str)
+    crs_dict = CRS.from_string(json_str)
     assert crs_dict == json.loads(json_str)
 
     # Test with invalid JSON code
     with pytest.raises(ValueError):
-        assert crs.from_string('{foo: bar}')
+        assert CRS.from_string('{foo: bar}')
 
 
 def test_from_epsg():
-    crs_dict = crs.from_epsg(4326)
+    crs_dict = CRS.from_epsg(4326)
     assert crs_dict['init'].lower() == 'epsg:4326'
 
     # Test with invalid EPSG code
     with pytest.raises(ValueError):
-        assert crs.from_epsg(0)
+        assert CRS.from_epsg(0)
 
 
 def test_from_epsg_string():
-    crs_dict = crs.from_string('epsg:4326')
+    crs_dict = CRS.from_string('epsg:4326')
     assert crs_dict['init'].lower() == 'epsg:4326'
 
     # Test with invalid EPSG code
     with pytest.raises(ValueError):
-        assert crs.from_string('epsg:xyz')
+        assert CRS.from_string('epsg:xyz')
 
 
 def test_from_string():
-    wgs84_crs = crs.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
-    assert wgs84_crs == {'no_defs': True, 'ellps': 'WGS84', 'datum': 'WGS84', 'proj': 'longlat'}
+    wgs84_crs = CRS.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+    assert wgs84_crs.to_dict() == {'no_defs': True, 'ellps': 'WGS84', 'datum': 'WGS84', 'proj': 'longlat'}
 
     # Make sure this doesn't get handled using the from_epsg() even though 'epsg' is in the string
-    epsg_init_crs = crs.from_string('+units=m +init=epsg:26911 +no_defs=True')
-    assert epsg_init_crs == {'units': 'm', 'init': 'epsg:26911', 'no_defs': True}
+    epsg_init_crs = CRS.from_string('+units=m +init=epsg:26911 +no_defs=True')
+    assert epsg_init_crs.to_dict() == {'units': 'm', 'init': 'epsg:26911', 'no_defs': True}
 
 
 def test_bare_parameters():
@@ -86,70 +85,101 @@ def test_bare_parameters():
     which makes presents bare parameters as key=<bool>."""
 
     # Example produced by pyproj
-    crs_dict = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
+    crs_dict = CRS.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
     assert crs_dict.get('no_defs', False) is True
 
-    crs_dict = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=False +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
+    crs_dict = CRS.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=False +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
     assert crs_dict.get('no_defs', True) is False
 
 
 def test_is_geographic():
-    assert is_geographic_crs({'init': 'EPSG:4326'}) is True
-    assert is_geographic_crs({'init': 'EPSG:3857'}) is False
+    assert CRS({'init': 'EPSG:4326'}).is_geographic is True
+    assert CRS({'init': 'EPSG:3857'}).is_geographic is False
 
-    wgs84_crs = crs.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
-    assert is_geographic_crs(wgs84_crs) is True
+    wgs84_crs = CRS.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+    assert wgs84_crs.is_geographic is True
 
-    nad27_crs = crs.from_string('+proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs')
-    assert is_geographic_crs(nad27_crs) is True
+    nad27_crs = CRS.from_string('+proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs')
+    assert nad27_crs.is_geographic is True
 
-    lcc_crs = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
-    assert is_geographic_crs(lcc_crs) is False
+    lcc_crs = CRS.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
+    assert lcc_crs.is_geographic is False
 
 
 def test_is_projected():
-    assert is_projected_crs({'init': 'EPSG:3857'}) is True
-    assert is_projected_crs({'init': 'EPSG:4326'}) is False
+    assert CRS({'init': 'EPSG:3857'}).is_projected is True
+    assert CRS({'INIT': 'EPSG:4326'}).is_projected is False
 
-    lcc_crs = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
-    assert is_projected_crs(lcc_crs) is True
+    lcc_crs = CRS.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
+    assert CRS(lcc_crs).is_projected is True
 
-    wgs84_crs = crs.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
-    assert is_projected_crs(wgs84_crs) is False
+    wgs84_crs = CRS.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+    assert CRS(wgs84_crs).is_projected is False
 
 
 def test_is_same_crs():
-    crs1 = {'init': 'EPSG:4326'}
-    crs2 = {'init': 'EPSG:3857'}
+    crs1 = CRS({'init': 'EPSG:4326'})
+    crs2 = CRS({'init': 'EPSG:3857'})
 
-    assert is_same_crs(crs1, crs1) is True
-    assert is_same_crs(crs1, crs2) is False
+    assert crs1 == crs1
+    assert crs1 != crs2
 
-    wgs84_crs = crs.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
-    assert is_same_crs(crs1, wgs84_crs) is True
+    wgs84_crs = CRS.from_string('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+    assert crs1 == wgs84_crs
 
     # Make sure that same projection with different parameter are not equal
-    lcc_crs1 = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
-    lcc_crs2 = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=45 +lat_0=0')
-    assert is_same_crs(lcc_crs1, lcc_crs2) is False
+    lcc_crs1 = CRS.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
+    lcc_crs2 = CRS.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=45 +lat_0=0')
+    assert lcc_crs1 != lcc_crs2
 
 
 def test_to_string():
-    assert crs.to_string({'init': 'EPSG:4326'}) == "+init=EPSG:4326"
+    assert CRS({'init': 'EPSG:4326'}).to_string() == "+init=EPSG:4326"
 
 
 def test_is_valid_false():
-    assert not is_valid_crs('EPSG:432600')
+    with pytest.raises(CRSError):
+        CRS(init='EPSG:432600').is_valid
 
 
 def test_is_valid():
-    assert is_valid_crs('EPSG:4326')
+    assert CRS(init='EPSG:4326').is_valid
 
 
 def test_empty_json():
     with pytest.raises(CRSError):
-        crs.from_string('{}')
+        CRS.from_string('{}')
     with pytest.raises(CRSError):
-        crs.from_string('[]')
+        CRS.from_string('[]')
     with pytest.raises(CRSError):
-        crs.from_string('')
+        CRS.from_string('')
+
+
+def test_can_create_osr():
+    assert _can_create_osr({'init': 'EPSG:4326'})
+    assert _can_create_osr('EPSG:4326')
+
+
+def test_can_create_osr_empty():
+    assert _can_create_osr({})
+    assert _can_create_osr('')
+
+
+def test_can_create_osr_invalid():
+    assert not _can_create_osr(None)
+    assert not _can_create_osr('EPSG:-1')
+    assert not _can_create_osr('EPSG:')
+    assert not _can_create_osr('foo')
+
+
+def test_has_wkt_property():
+    assert CRS({'init': 'EPSG:4326'}).wkt.startswith('GEOGCS["WGS 84",DATUM')
+
+
+def test_repr():
+    assert repr(CRS({'init': 'EPSG:4326'})).startswith("CRS({'init'")
+
+
+def test_epsg_code():
+    assert CRS({'init': 'EPSG:4326'}).is_epsg_code
+    assert not CRS({'proj': 'latlon'}).is_epsg_code
diff --git a/tests/test_dataset_mask.py b/tests/test_dataset_mask.py
new file mode 100644
index 0000000..d14e6a6
--- /dev/null
+++ b/tests/test_dataset_mask.py
@@ -0,0 +1,182 @@
+import logging
+import sys
+
+import numpy as np
+import pytest
+
+try:
+    import matplotlib as mpl
+    mpl.use('agg')
+    import matplotlib.pyplot as plt
+except ImportError:
+    plt = None
+
+from affine import Affine
+import rasterio
+from rasterio.enums import MaskFlags
+from rasterio.errors import NodataShadowWarning
+from rasterio.crs import CRS
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+# Setup test arrays
+red = np.array([[0, 0, 0],
+                [0, 1, 1],
+                [1, 0, 1]]).astype('uint8') * 255
+
+grn = np.array([[0, 0, 0],
+                [1, 0, 1],
+                [1, 0, 1]]).astype('uint8') * 255
+
+blu = np.array([[0, 0, 0],
+                [1, 1, 0],
+                [1, 0, 1]]).astype('uint8') * 255
+
+# equivalent to alp = red | grn | blu
+# valid data anywhere there is at least one R, G or B value
+alp = np.array([[0, 0, 0],
+                [1, 1, 1],
+                [1, 0, 1]]).astype('uint8') * 255
+
+# mask might be constructed using different tools
+# and differ from a strict interpretation of rgb values
+msk = np.array([[0, 0, 0],
+                [1, 1, 1],
+                [1, 1, 1]]).astype('uint8') * 255
+
+alldata = np.array([[1, 1, 1],
+                    [1, 1, 1],
+                    [1, 1, 1]]).astype('uint8') * 255
+
+# boundless window ((1, 4, (1, 4))
+alp_shift_lr = np.array([[1, 1, 0],
+                         [0, 1, 0],
+                         [0, 0, 0]]).astype('uint8') * 255
+
+ at pytest.fixture(scope='function')
+def tiffs(tmpdir):
+
+    _profile = {
+        'affine': Affine(5.0, 0.0, 0.0, 0.0, -5.0, 0.0),
+        'transform': Affine(5.0, 0.0, 0.0, 0.0, -5.0, 0.0),
+        'crs': CRS({'init': 'epsg:4326'}),
+        'driver': 'GTiff',
+        'dtype': 'uint8',
+        'height': 3,
+        'width': 3}
+
+    # 1. RGB without nodata value
+    prof = _profile.copy()
+    prof['count'] = 3
+    prof['nodata'] = None
+    with rasterio.open(str(tmpdir.join('rgb_no_ndv.tif')), 'w', **prof) as dst:
+        dst.write(red, 1)
+        dst.write(grn, 2)
+        dst.write(blu, 3)
+
+    # 2. RGB with nodata value
+    prof = _profile.copy()
+    prof['count'] = 3
+    prof['nodata'] = 0
+    with rasterio.open(str(tmpdir.join('rgb_ndv.tif')), 'w', **prof) as dst:
+        dst.write(red, 1)
+        dst.write(grn, 2)
+        dst.write(blu, 3)
+
+    # 3. RGBA without nodata value
+    prof = _profile.copy()
+    prof['count'] = 4
+    prof['nodata'] = None
+    with rasterio.open(str(tmpdir.join('rgba_no_ndv.tif')), 'w', **prof) as dst:
+        dst.write(red, 1)
+        dst.write(grn, 2)
+        dst.write(blu, 3)
+        dst.write(alp, 4)
+
+    # 4. RGBA with nodata value
+    prof = _profile.copy()
+    prof['count'] = 4
+    prof['nodata'] = 0
+    with rasterio.open(str(tmpdir.join('rgba_ndv.tif')), 'w', **prof) as dst:
+        dst.write(red, 1)
+        dst.write(grn, 2)
+        dst.write(blu, 3)
+        dst.write(alp, 4)
+
+    # 5. RGB with msk
+    prof = _profile.copy()
+    prof['count'] = 3
+    with rasterio.open(str(tmpdir.join('rgb_msk.tif')), 'w', **prof) as dst:
+        dst.write(red, 1)
+        dst.write(grn, 2)
+        dst.write(blu, 3)
+        dst.write_mask(msk)
+
+    # 6. RGB with msk (internal)
+    prof = _profile.copy()
+    prof['count'] = 3
+    with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=True) as env:
+        with rasterio.open(str(tmpdir.join('rgb_msk_internal.tif')),
+                           'w', **prof) as dst:
+            dst.write(red, 1)
+            dst.write(grn, 2)
+            dst.write(blu, 3)
+            dst.write_mask(msk)
+
+    # 7. RGBA with msk
+    prof = _profile.copy()
+    prof['count'] = 4
+    with rasterio.open(str(tmpdir.join('rgba_msk.tif')), 'w', **prof) as dst:
+        dst.write(red, 1)
+        dst.write(grn, 2)
+        dst.write(blu, 3)
+        dst.write(alp, 4)
+        dst.write_mask(msk)
+
+    return tmpdir
+
+
+def test_no_ndv(tiffs):
+    with rasterio.open(str(tiffs.join('rgb_no_ndv.tif'))) as src:
+        assert np.array_equal(src.dataset_mask(), alldata)
+
+def test_rgb_ndv(tiffs):
+    with rasterio.open(str(tiffs.join('rgb_ndv.tif'))) as src:
+        assert np.array_equal(src.dataset_mask(), alp)
+
+def test_rgba_no_ndv(tiffs):
+    with rasterio.open(str(tiffs.join('rgba_no_ndv.tif'))) as src:
+        assert np.array_equal(src.dataset_mask(), alp)
+
+def test_rgba_ndv(tiffs):
+    with rasterio.open(str(tiffs.join('rgba_ndv.tif'))) as src:
+        with pytest.warns(NodataShadowWarning):
+            res = src.dataset_mask()
+        assert np.array_equal(res, alp)
+
+def test_rgb_msk(tiffs):
+    with rasterio.open(str(tiffs.join('rgb_msk.tif'))) as src:
+        assert np.array_equal(src.dataset_mask(), msk)
+        # each band's mask is also equal
+        for bmask in src.read_masks():
+            assert np.array_equal(bmask, msk)
+
+def test_rgb_msk_int(tiffs):
+    with rasterio.open(str(tiffs.join('rgb_msk_internal.tif'))) as src:
+        assert np.array_equal(src.dataset_mask(), msk)
+
+def test_rgba_msk(tiffs):
+    with rasterio.open(str(tiffs.join('rgba_msk.tif'))) as src:
+        # mask takes precendent over alpha
+        assert np.array_equal(src.dataset_mask(), msk)
+
+def test_kwargs(tiffs):
+    with rasterio.open(str(tiffs.join('rgb_ndv.tif'))) as src:
+        # window and boundless are passed along
+        other = src.dataset_mask(window=((1, 4), (1, 4)), boundless=True)
+        assert np.array_equal(alp_shift_lr, other)
+
+        # band indexes are not supported
+        with pytest.raises(TypeError):
+            src.dataset_mask(indexes=1)
diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py
index 6f87545..a720288 100644
--- a/tests/test_deprecations.py
+++ b/tests/test_deprecations.py
@@ -3,7 +3,7 @@
 # on the way to stabilizing the API for 1.0
 import warnings
 
-import numpy
+import numpy as np
 import pytest
 
 # New modules
@@ -15,13 +15,20 @@ from rasterio import (
     get_data_window, window_intersection, window_union, windows_intersect
 )
 
+try:
+    import matplotlib as mpl
+    mpl.use('agg')
+    import matplotlib.pyplot as plt
+    plt.show = lambda :None
+except:
+    pass
 
 DATA_WINDOW = ((3, 5), (2, 6))
 
 
 @pytest.fixture
 def data():
-    data = numpy.zeros((10, 10), dtype='uint8')
+    data = np.zeros((10, 10), dtype='uint8')
     data[slice(*DATA_WINDOW[0]), slice(*DATA_WINDOW[1])] = 1
     return data
 
@@ -85,7 +92,7 @@ def test_stats(recwarn):
         assert recwarn.pop(DeprecationWarning)
         new = stats_new((src, 1))
         assert len(recwarn) == 0
-        assert numpy.allclose(numpy.array(new), numpy.array(old))
+        assert np.allclose(np.array(new), np.array(old))
 
 
 # xfail because for unknown reasons, travis fails with matplotlib errors
@@ -135,7 +142,7 @@ def test_mask(recwarn, basic_image_file, basic_geometry):
                        nodata=nodata_val, invert=True)
         assert len(recwarn) == nwarn
         for parts in zip(new, old):
-            assert numpy.allclose(parts[0], parts[1])
+            assert np.allclose(parts[0], parts[1])
 
 
 def test_merge(recwarn, tmpdir):
@@ -154,4 +161,15 @@ def test_merge(recwarn, tmpdir):
     new = merge_new(in_sources)
     assert len(recwarn) == nwarn
     for parts in zip(new, old):
-        assert numpy.allclose(parts[0], parts[1])
+        assert np.allclose(parts[0], parts[1])
+
+
+def test_driver_deprec(recwarn):
+    """Test that drivers() still works but raises deprecation
+    """
+    warnings.simplefilter('always')
+    with rasterio.drivers():
+        pass
+    assert len(recwarn) == 1
+    recwarn.pop(DeprecationWarning)
+    assert len(recwarn) == 0
diff --git a/tests/test_driver_management.py b/tests/test_driver_management.py
index 6eac978..b40fdd8 100644
--- a/tests/test_driver_management.py
+++ b/tests/test_driver_management.py
@@ -1,22 +1,20 @@
 import logging
-import sys
 
 import rasterio
 from rasterio._drivers import driver_count
-from rasterio.env import Env
 
 
 def test_drivers():
-    with Env() as m:
+    with rasterio.Env() as m:
         assert driver_count() > 0
-        assert type(m) == Env
+        assert type(m) == rasterio.Env
     assert driver_count() > 0
 
 
 def test_drivers_bwd_compat():
-    with rasterio.drivers() as m:
+    with rasterio.Env() as m:
         assert driver_count() > 0
-        assert type(m) == Env
+        assert type(m) == rasterio.Env
     assert driver_count() > 0
 
 
@@ -28,7 +26,7 @@ def test_cpl_debug_true(tmpdir):
     fh = logging.FileHandler(logfile)
     log.addHandler(fh)
 
-    with Env(CPL_DEBUG=True):
+    with rasterio.Env(CPL_DEBUG=True):
         with rasterio.open("tests/data/RGB.byte.tif"):
             pass
 
@@ -44,7 +42,7 @@ def test_cpl_debug_false(tmpdir):
     fh = logging.FileHandler(logfile)
     log.addHandler(fh)
 
-    with Env(CPL_DEBUG=False):
+    with rasterio.Env(CPL_DEBUG=False):
         with rasterio.open("tests/data/RGB.byte.tif"):
             pass
 
diff --git a/tests/test_dtypes.py b/tests/test_dtypes.py
index 9c0310b..0cd587c 100644
--- a/tests/test_dtypes.py
+++ b/tests/test_dtypes.py
@@ -1,7 +1,9 @@
 import numpy as np
+import pytest
 
+import rasterio
 from rasterio import (
-    ubyte, uint8, uint16, uint32, int16, int32, float32, float64)
+    ubyte, uint8, uint16, uint32, int16, int32, float32, float64, complex_)
 from rasterio.dtypes import (
     _gdal_typename, is_ndarray, check_dtype, get_minimum_dtype, can_cast_dtype,
     validate_dtype
@@ -55,4 +57,21 @@ def test_validate_dtype():
     assert validate_dtype([1, 2, 3], ('uint8', 'uint16')) == True
     assert validate_dtype(np.array([1, 2, 3]), ('uint8', 'uint16')) == True
     assert validate_dtype(np.array([1.4, 2.1, 3.65]), ('float32',)) == True
-    assert validate_dtype(np.array([1.4, 2.1, 3.65]),('uint8',)) == False
+    assert validate_dtype(np.array([1.4, 2.1, 3.65]), ('uint8',)) == False
+
+
+# Roundtrip to complex type failing for unknown reasons
+# see https://github.com/mapbox/rasterio/issues/714
+ at pytest.mark.xfail
+def test_complex(tmpdir):
+    name = str(tmpdir.join("complex.tif"))
+    arr1 = np.ones((2, 2), dtype=complex_)
+    profile = dict(driver='GTiff', width=2, height=2, count=1, dtype=complex_)
+
+    with rasterio.open(name, 'w', **profile) as dst:
+        dst.write(arr1, 1)
+
+    with rasterio.open(name) as src:
+        arr2 = src.read(1)
+
+    assert np.array_equal(arr1, arr2)
diff --git a/tests/test_env.py b/tests/test_env.py
index fffc8a7..05b6d76 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -9,9 +9,10 @@ from packaging.version import parse
 import pytest
 
 import rasterio
-from rasterio._drivers import (
-    GDALEnv, del_gdal_config, get_gdal_config, set_gdal_config, driver_count)
-from rasterio.env import defenv, delenv, getenv, setenv, ensure_env, Env
+from rasterio._drivers import (del_gdal_config, get_gdal_config,
+                               set_gdal_config)
+from rasterio.env import defenv, delenv, getenv, setenv, ensure_env
+from rasterio.env import default_options
 from rasterio.errors import EnvError
 from rasterio.rio.main import main_group
 
@@ -45,10 +46,13 @@ def test_gdal_config_accessers():
 # at the end of the test, making tests as isolates as GDAL allows.
 
 def test_env_accessors(gdalenv):
-    """High level GDAL env access"""
+    """High level GDAL env access."""
     defenv()
     setenv(foo='1', bar='2')
-    assert getenv() == rasterio.env._env.options == {'foo': '1', 'bar': '2'}
+    expected = default_options.copy()
+    expected.update({'foo': '1', 'bar': '2'})
+    assert getenv() == rasterio.env._env.options
+    assert getenv() == expected
     assert get_gdal_config('foo') == '1'
     assert get_gdal_config('bar') == '2'
     delenv()
@@ -74,20 +78,23 @@ def test_ensure_env_decorator(gdalenv):
 def test_no_aws_gdal_config(gdalenv):
     """Trying to set AWS-specific GDAL config options fails."""
     with pytest.raises(EnvError):
-        Env(AWS_ACCESS_KEY_ID='x')
+        rasterio.Env(AWS_ACCESS_KEY_ID='x')
     with pytest.raises(EnvError):
-        Env(AWS_SECRET_ACCESS_KEY='y')
+        rasterio.Env(AWS_SECRET_ACCESS_KEY='y')
 
 
 def test_env_options(gdalenv):
     """Test env options."""
-    env = Env(foo='x')
+    env = rasterio.Env(foo='x')
     assert env.options == {'foo': 'x'}
     assert not env.previous_options
-    assert getenv() == rasterio.env._env.options == {}
+    expected = default_options.copy()
+    assert getenv() == rasterio.env._env.options == expected
+    expected.update({'foo': 'x'})
     with env:
-        assert getenv() == rasterio.env._env.options == {'foo': 'x'}
-    assert getenv() == rasterio.env._env.options == {}
+        assert getenv() == rasterio.env._env.options == expected
+    del expected['foo']
+    assert getenv() == rasterio.env._env.options == expected
 
 
 def test_aws_session(gdalenv):
@@ -108,22 +115,26 @@ def test_aws_session_credentials(gdalenv):
         aws_access_key_id='id', aws_secret_access_key='key',
         aws_session_token='token', region_name='null-island-1')
     s = rasterio.env.Env(aws_session=aws_session)
-    assert getenv() == rasterio.env._env.options == {}
+    expected = default_options.copy()
+    assert getenv() == rasterio.env._env.options == expected
     s.get_aws_credentials()
-    assert getenv() == rasterio.env._env.options == {
+    expected.update({
         'aws_access_key_id': 'id', 'aws_region': 'null-island-1',
-        'aws_secret_access_key': 'key', 'aws_session_token': 'token'}
+        'aws_secret_access_key': 'key', 'aws_session_token': 'token'})
+    assert getenv() == rasterio.env._env.options == expected
 
 
 def test_with_aws_session_credentials(gdalenv):
     """Create an Env with a boto3 session."""
-    with Env(aws_access_key_id='id', aws_secret_access_key='key',
+    with rasterio.Env(aws_access_key_id='id', aws_secret_access_key='key',
              aws_session_token='token', region_name='null-island-1') as s:
-        assert getenv() == rasterio.env._env.options == {}
+        expected = default_options.copy()
+        assert getenv() == rasterio.env._env.options == expected
         s.get_aws_credentials()
-        assert getenv() == rasterio.env._env.options == {
+        expected.update({
             'aws_access_key_id': 'id', 'aws_region': 'null-island-1',
-            'aws_secret_access_key': 'key', 'aws_session_token': 'token'}
+            'aws_secret_access_key': 'key', 'aws_session_token': 'token'})
+        assert getenv() == rasterio.env._env.options == expected
 
 
 def test_session_env_lazy(monkeypatch, gdalenv):
@@ -131,11 +142,16 @@ def test_session_env_lazy(monkeypatch, gdalenv):
     monkeypatch.setenv('AWS_ACCESS_KEY_ID', 'id')
     monkeypatch.setenv('AWS_SECRET_ACCESS_KEY', 'key')
     monkeypatch.setenv('AWS_SESSION_TOKEN', 'token')
-    with Env() as s:
+    with rasterio.Env() as s:
         s.get_aws_credentials()
-        assert getenv() == rasterio.env._env.options == {
-            'aws_access_key_id': 'id', 'aws_secret_access_key': 'key',
+        assert getenv() == rasterio.env._env.options
+        expected = {
+            'aws_access_key_id': 'id',
+            'aws_secret_access_key': 'key',
             'aws_session_token': 'token'}
+        for k, v in expected.items():
+            assert getenv()[k] == v
+
     monkeypatch.undo()
 
 
@@ -147,7 +163,7 @@ def test_open_with_default_env(gdalenv):
 
 def test_open_with_env(gdalenv):
     """Read from a dataset with an explicit env."""
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as dataset:
             assert dataset.count == 3
 
@@ -156,7 +172,7 @@ def test_open_with_env(gdalenv):
 @credentials
 def test_s3_open_with_session(gdalenv):
     """Read from S3 demonstrating lazy credentials."""
-    with Env():
+    with rasterio.Env():
         with rasterio.open(L8TIF) as dataset:
             assert dataset.count == 1
 
@@ -171,8 +187,8 @@ def test_s3_open_with_default_session(gdalenv):
 
 @mingdalversion
 def test_open_https_vsicurl(gdalenv):
-    """Read from HTTPS URL"""
-    with Env():
+    """Read from HTTPS URL."""
+    with rasterio.Env():
         with rasterio.open(httpstif) as dataset:
             assert dataset.count == 1
 
@@ -182,7 +198,7 @@ def test_open_https_vsicurl(gdalenv):
 @mingdalversion
 @credentials
 def test_s3_rio_info(runner):
-    """S3 is supported by rio-info"""
+    """S3 is supported by rio-info."""
     result = runner.invoke(main_group, ['info', L8TIF])
     assert result.exit_code == 0
     assert '"crs": "EPSG:32645"' in result.output
@@ -191,7 +207,7 @@ def test_s3_rio_info(runner):
 @mingdalversion
 @credentials
 def test_https_rio_info(runner):
-    """HTTPS is supported by rio-info"""
+    """HTTPS is supported by rio-info."""
     result = runner.invoke(main_group, ['info', httpstif])
     assert result.exit_code == 0
     assert '"crs": "EPSG:32645"' in result.output
diff --git a/tests/test_err.py b/tests/test_err.py
index fa1d3d5..8c8b4ad 100644
--- a/tests/test_err.py
+++ b/tests/test_err.py
@@ -4,6 +4,7 @@ import pytest
 
 import rasterio
 from rasterio.env import Env
+from rasterio._err import CPLError
 from rasterio.errors import RasterioIOError
 
 
@@ -24,3 +25,8 @@ def test_io_error_env(tmpdir):
 def test_bogus_band_error():
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         assert src._has_band(4) is False
+
+
+def test_cplerror_str():
+    err = CPLError(1, 1, "test123")
+    assert str(err) == "test123"
diff --git a/tests/test_features.py b/tests/test_features.py
index e54ebc6..d804a59 100644
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -1,11 +1,10 @@
 import logging
 import sys
-import numpy
+import numpy as np
 import pytest
 
 from affine import Affine
 import rasterio
-from rasterio.env import Env
 from rasterio.features import bounds, geometry_mask, rasterize, sieve, shapes
 
 
@@ -45,12 +44,11 @@ def test_feature_collection(basic_featurecollection):
 
 
 def test_bounds_existing_bbox(basic_featurecollection):
-    """
-    Test with existing bbox in geojson, similar to that produced by
-    rasterio.  Values specifically modified here for testing, bboxes are not
-    valid as written.
-    """
+    """Test with existing bbox in geojson.
 
+    Similar to that produced by rasterio.  Values specifically modified here
+    for testing, bboxes are not valid as written.
+    """
     fc = basic_featurecollection
     fc['bbox'] = [0, 10, 10, 20]
     fc['features'][0]['bbox'] = [0, 100, 10, 200]
@@ -60,8 +58,8 @@ def test_bounds_existing_bbox(basic_featurecollection):
 
 
 def test_geometry_mask(basic_geometry, basic_image_2x2):
-    with Env():
-        assert numpy.array_equal(
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image_2x2 == 0,
             geometry_mask(
                 [basic_geometry],
@@ -72,8 +70,8 @@ def test_geometry_mask(basic_geometry, basic_image_2x2):
 
 
 def test_geometry_mask_invert(basic_geometry, basic_image_2x2):
-    with Env():
-        assert numpy.array_equal(
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image_2x2,
             geometry_mask(
                 [basic_geometry],
@@ -85,49 +83,44 @@ def test_geometry_mask_invert(basic_geometry, basic_image_2x2):
 
 
 def test_rasterize(basic_geometry, basic_image_2x2):
-    """ Rasterize operation should succeed for both an out_shape and out """
-
-    with Env():
-        assert numpy.array_equal(
+    """Rasterize operation should succeed for both an out_shape and out."""
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image_2x2,
             rasterize([basic_geometry], out_shape=DEFAULT_SHAPE)
         )
 
-        out = numpy.zeros(DEFAULT_SHAPE)
+        out = np.zeros(DEFAULT_SHAPE)
         rasterize([basic_geometry], out=out)
-        assert numpy.array_equal(basic_image_2x2, out)
+        assert np.array_equal(basic_image_2x2, out)
 
 
 def test_rasterize_invalid_out_dtype(basic_geometry):
-    """ A non-supported data type for out should raise an exception """
-
-    out = numpy.zeros(DEFAULT_SHAPE, dtype=numpy.int64)
-    with Env():
+    """A non-supported data type for out should raise an exception."""
+    out = np.zeros(DEFAULT_SHAPE, dtype=np.int64)
+    with rasterio.Env():
         with pytest.raises(ValueError):
             rasterize([basic_geometry], out=out)
 
 
 def test_rasterize_shapes_out_dtype_mismatch(basic_geometry):
-    """ Shape values must be able to fit in data type for out """
-
-    out = numpy.zeros(DEFAULT_SHAPE, dtype=numpy.uint8)
-    with Env():
+    """Shape values must be able to fit in data type for out."""
+    out = np.zeros(DEFAULT_SHAPE, dtype=np.uint8)
+    with rasterio.Env():
         with pytest.raises(ValueError):
             rasterize([(basic_geometry, 10000000)], out=out)
 
 
 def test_rasterize_missing_out(basic_geometry):
-    """ If both out and out_shape are missing, should raise exception """
-
-    with Env():
+    """If both out and out_shape are missing, should raise exception."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             rasterize([basic_geometry], out=None, out_shape=None)
 
 
 def test_rasterize_missing_shapes():
-    """ Shapes are required for this operation """
-
-    with Env():
+    """Shapes are required for this operation."""
+    with rasterio.Env():
         with pytest.raises(ValueError) as ex:
             rasterize([], out_shape=DEFAULT_SHAPE)
 
@@ -135,9 +128,8 @@ def test_rasterize_missing_shapes():
 
 
 def test_rasterize_invalid_shapes():
-    """ Invalid shapes should raise an exception rather than be skipped """
-
-    with Env():
+    """Invalid shapes should raise an exception rather than be skipped."""
+    with rasterio.Env():
         with pytest.raises(ValueError) as ex:
             rasterize([{'foo': 'bar'}], out_shape=DEFAULT_SHAPE)
 
@@ -145,13 +137,12 @@ def test_rasterize_invalid_shapes():
 
 
 def test_rasterize_default_value(basic_geometry, basic_image_2x2):
-    """ All shapes should rasterize to the default value """
-
+    """All shapes should rasterize to the default value."""
     default_value = 2
     truth = basic_image_2x2 * default_value
 
-    with Env():
-        assert numpy.array_equal(
+    with rasterio.Env():
+        assert np.array_equal(
             truth,
             rasterize(
                 [basic_geometry], out_shape=DEFAULT_SHAPE,
@@ -161,9 +152,8 @@ def test_rasterize_default_value(basic_geometry, basic_image_2x2):
 
 
 def test_rasterize_invalid_default_value(basic_geometry):
-    """ A default value that requires an int64 should raise an exception """
-
-    with Env():
+    """A default value that requires an int64 should raise an exception."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             rasterize(
                 [basic_geometry], out_shape=DEFAULT_SHAPE,
@@ -172,11 +162,10 @@ def test_rasterize_invalid_default_value(basic_geometry):
 
 
 def test_rasterize_fill_value(basic_geometry, basic_image_2x2):
-    """ All pixels not covered by shapes should be given fill value """
-
+    """All pixels not covered by shapes should be given fill value."""
     default_value = 2
-    with Env():
-        assert numpy.array_equal(
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image_2x2 + 1,
             rasterize(
                 [basic_geometry], out_shape=DEFAULT_SHAPE, fill=1,
@@ -186,9 +175,8 @@ def test_rasterize_fill_value(basic_geometry, basic_image_2x2):
 
 
 def test_rasterize_invalid_fill_value(basic_geometry):
-    """ A fill value that requires an int64 should raise an exception """
-
-    with Env():
+    """A fill value that requires an int64 should raise an exception."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             rasterize(
                 [basic_geometry], out_shape=DEFAULT_SHAPE, fill=1000000000000,
@@ -197,19 +185,18 @@ def test_rasterize_invalid_fill_value(basic_geometry):
 
 
 def test_rasterize_fill_value_dtype_mismatch(basic_geometry):
-    """ A fill value that doesn't match dtype should fail """
-
-    with Env():
+    """A fill value that doesn't match dtype should fail."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             rasterize(
                 [basic_geometry], out_shape=DEFAULT_SHAPE, fill=1000000,
-                default_value=2, dtype=numpy.uint8
+                default_value=2, dtype=np.uint8
             )
 
 
 def test_rasterize_all_touched(basic_geometry, basic_image):
-    with Env():
-        assert numpy.array_equal(
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image,
             rasterize(
                 [basic_geometry], out_shape=DEFAULT_SHAPE, all_touched=True
@@ -222,10 +209,9 @@ def test_rasterize_value(basic_geometry, basic_image_2x2):
     All shapes should rasterize to the value passed in a tuple alongside
     each shape
     """
-
     value = 5
-    with Env():
-        assert numpy.array_equal(
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image_2x2 * value,
             rasterize(
                 [(basic_geometry, value)], out_shape=DEFAULT_SHAPE
@@ -234,9 +220,8 @@ def test_rasterize_value(basic_geometry, basic_image_2x2):
 
 
 def test_rasterize_invalid_value(basic_geometry):
-    """ A shape value that requires an int64 should raise an exception """
-
-    with Env():
+    """A shape value that requires an int64 should raise an exception."""
+    with rasterio.Env():
         with pytest.raises(ValueError) as ex:
             rasterize(
                 [(basic_geometry, 1000000000000)], out_shape=DEFAULT_SHAPE
@@ -246,9 +231,8 @@ def test_rasterize_invalid_value(basic_geometry):
 
 
 def test_rasterize_supported_dtype(basic_geometry):
-    """ Supported data types should return valid results """
-
-    with Env():
+    """Supported data types should return valid results."""
+    with rasterio.Env():
         supported_types = (
             ('int16', -32768),
             ('int32', -2147483648),
@@ -260,7 +244,7 @@ def test_rasterize_supported_dtype(basic_geometry):
         )
 
         for dtype, default_value in supported_types:
-            truth = numpy.zeros(DEFAULT_SHAPE, dtype=dtype)
+            truth = np.zeros(DEFAULT_SHAPE, dtype=dtype)
             truth[2:4, 2:4] = default_value
 
             result = rasterize(
@@ -269,24 +253,23 @@ def test_rasterize_supported_dtype(basic_geometry):
                 default_value=default_value,
                 dtype=dtype
             )
-            assert numpy.array_equal(result, truth)
-            assert numpy.dtype(result.dtype) == numpy.dtype(truth.dtype)
+            assert np.array_equal(result, truth)
+            assert np.dtype(result.dtype) == np.dtype(truth.dtype)
 
             result = rasterize(
                 [(basic_geometry, default_value)],
                 out_shape=DEFAULT_SHAPE
             )
-            if numpy.dtype(dtype).kind == 'f':
-                assert numpy.allclose(result, truth)
+            if np.dtype(dtype).kind == 'f':
+                assert np.allclose(result, truth)
             else:
-                assert numpy.array_equal(result, truth)
+                assert np.array_equal(result, truth)
             # Since dtype is auto-detected, it may not match due to upcasting
 
 
 def test_rasterize_unsupported_dtype(basic_geometry):
-    """ Unsupported types should all raise exceptions """
-
-    with Env():
+    """Unsupported types should all raise exceptions."""
+    with rasterio.Env():
         unsupported_types = (
             ('int8', -127),
             ('int64', 20439845334323),
@@ -311,9 +294,8 @@ def test_rasterize_unsupported_dtype(basic_geometry):
 
 
 def test_rasterize_mismatched_dtype(basic_geometry):
-    """ Mismatched values and dtypes should raise exceptions """
-
-    with Env():
+    """Mismatched values and dtypes should raise exceptions."""
+    with rasterio.Env():
         mismatched_types = (('uint8', 3.2423), ('uint8', -2147483648))
         for dtype, default_value in mismatched_types:
             with pytest.raises(ValueError):
@@ -333,27 +315,24 @@ def test_rasterize_mismatched_dtype(basic_geometry):
 
 
 def test_rasterize_geometries_symmetric():
-    """ Make sure that rasterize is symmetric with shapes """
-
+    """Make sure that rasterize is symmetric with shapes."""
     transform = (1.0, 0.0, 0.0, 0.0, -1.0, 0.0)
-    truth = numpy.zeros(DEFAULT_SHAPE, dtype=rasterio.ubyte)
+    truth = np.zeros(DEFAULT_SHAPE, dtype=rasterio.ubyte)
     truth[2:5, 2:5] = 1
-    with Env():
+    with rasterio.Env():
         s = shapes(truth, transform=transform)
         result = rasterize(s, out_shape=DEFAULT_SHAPE, transform=transform)
-        assert numpy.array_equal(result, truth)
+        assert np.array_equal(result, truth)
 
 
 def test_rasterize_internal_driver_manager(basic_geometry):
-    """ Rasterize should work without explicitly calling driver manager """
-
+    """Rasterize should work without explicitly calling driver manager."""
     assert rasterize([basic_geometry], out_shape=DEFAULT_SHAPE).sum() == 4
 
 
 def test_shapes(basic_image):
-    """ Test creation of shapes from pixel values """
-
-    with Env():
+    """Test creation of shapes from pixel values."""
+    with rasterio.Env():
         results = list(shapes(basic_image))
 
         assert len(results) == 2
@@ -379,9 +358,8 @@ def test_shapes(basic_image):
 
 
 def test_shapes_band(pixelated_image, pixelated_image_file):
-    """ Shapes from a band should match shapes from an array """
-
-    with Env():
+    """Shapes from a band should match shapes from an array."""
+    with rasterio.Env():
         truth = list(shapes(pixelated_image))
 
         with rasterio.open(pixelated_image_file) as src:
@@ -397,8 +375,7 @@ def test_shapes_connectivity_rook(diagonal_image):
     Diagonals are not connected, so there will be 1 feature per pixel plus
     background.
     """
-
-    with Env():
+    with rasterio.Env():
         assert len(list(shapes(diagonal_image, connectivity=4))) == 12
 
 
@@ -407,26 +384,23 @@ def test_shapes_connectivity_queen(diagonal_image):
     Diagonals are connected, so there will be 1 feature for all pixels plus
     background.
     """
-
-    with Env():
+    with rasterio.Env():
         assert len(list(shapes(diagonal_image, connectivity=8))) == 2
 
 
 def test_shapes_connectivity_invalid(diagonal_image):
-    """ Invalid connectivity should raise exception """
-
-    with Env():
+    """Invalid connectivity should raise exception."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             assert next(shapes(diagonal_image, connectivity=12))
 
 
 def test_shapes_mask(basic_image):
-    """ Only pixels not masked out should be converted to features """
-
-    mask = numpy.ones(basic_image.shape, dtype=rasterio.bool_)
+    """Only pixels not masked out should be converted to features."""
+    mask = np.ones(basic_image.shape, dtype=rasterio.bool_)
     mask[4:5, 4:5] = False
 
-    with Env():
+    with rasterio.Env():
         results = list(shapes(basic_image, mask=mask))
 
         assert len(results) == 2
@@ -442,26 +416,24 @@ def test_shapes_mask(basic_image):
 
 
 def test_shapes_blank_mask(basic_image):
-    """ Mask is blank so results should mask shapes without mask """
-
-    with Env():
-        assert numpy.array_equal(
+    """Mask is blank so results should mask shapes without mask."""
+    with rasterio.Env():
+        assert np.array_equal(
             list(shapes(
                 basic_image,
-                mask=numpy.ones(basic_image.shape, dtype=rasterio.bool_))
+                mask=np.ones(basic_image.shape, dtype=rasterio.bool_))
             ),
             list(shapes(basic_image))
         )
 
 
 def test_shapes_invalid_mask_shape(basic_image):
-    """ A mask that is the wrong shape should fail """
-
-    with Env():
+    """A mask that is the wrong shape should fail."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             next(shapes(
                 basic_image,
-                mask=numpy.ones(
+                mask=np.ones(
                     (basic_image.shape[0] + 10, basic_image.shape[1] + 10),
                     dtype=rasterio.bool_
                 )
@@ -469,20 +441,18 @@ def test_shapes_invalid_mask_shape(basic_image):
 
 
 def test_shapes_invalid_mask_dtype(basic_image):
-    """ A mask that is the wrong dtype should fail """
-
-    with Env():
+    """A mask that is the wrong dtype should fail."""
+    with rasterio.Env():
         for dtype in ('int8', 'int16', 'int32'):
             with pytest.raises(ValueError):
                 next(shapes(
                     basic_image,
-                    mask=numpy.ones(basic_image.shape, dtype=dtype)
+                    mask=np.ones(basic_image.shape, dtype=dtype)
                 ))
 
 
 def test_shapes_supported_dtypes(basic_image):
-    """ Supported data types should return valid results """
-
+    """Supported data types should return valid results."""
     supported_types = (
         ('int16', -32768),
         ('int32', -2147483648),
@@ -491,15 +461,14 @@ def test_shapes_supported_dtypes(basic_image):
         ('float32', 1.434532)
     )
 
-    with Env():
+    with rasterio.Env():
         for dtype, test_value in supported_types:
             shape, value = next(shapes(basic_image.astype(dtype) * test_value))
-            assert numpy.allclose(value, test_value)
+            assert np.allclose(value, test_value)
 
 
 def test_shapes_unsupported_dtypes(basic_image):
-    """ Unsupported data types should raise exceptions """
-
+    """Unsupported data types should raise exceptions."""
     unsupported_types = (
         ('int8', -127),
         ('uint32', 4294967295),
@@ -508,15 +477,14 @@ def test_shapes_unsupported_dtypes(basic_image):
         ('float64', -98332.133422114)
     )
 
-    with Env():
+    with rasterio.Env():
         for dtype, test_value in unsupported_types:
             with pytest.raises(ValueError):
                 next(shapes(basic_image.astype(dtype) * test_value))
 
 
 def test_shapes_internal_driver_manager(basic_image):
-    """ Shapes should work without explicitly calling driver manager """
-
+    """Shapes should work without explicitly calling driver manager."""
     assert next(shapes(basic_image))[0]['type'] == 'Polygon'
 
 
@@ -525,9 +493,8 @@ def test_sieve_small(basic_image, pixelated_image):
     Setting the size smaller than or equal to the size of the feature in the
     image should not change the image.
     """
-
-    with Env():
-        assert numpy.array_equal(
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image,
             sieve(pixelated_image, basic_image.sum())
         )
@@ -537,30 +504,27 @@ def test_sieve_large(basic_image):
     """
     Setting the size larger than size of feature should leave us an empty image.
     """
-
-    with Env():
-        assert not numpy.any(sieve(basic_image, basic_image.sum() + 1))
+    with rasterio.Env():
+        assert not np.any(sieve(basic_image, basic_image.sum() + 1))
 
 
 def test_sieve_invalid_size(basic_image):
-    with Env():
+    with rasterio.Env():
         for invalid_size in (0, 45.1234, basic_image.size + 1):
             with pytest.raises(ValueError):
                 sieve(basic_image, invalid_size)
 
 
 def test_sieve_connectivity_rook(diagonal_image):
-    """ Diagonals are not connected, so feature is removed """
-
-    assert not numpy.any(
+    """Diagonals are not connected, so feature is removed."""
+    assert not np.any(
         sieve(diagonal_image, diagonal_image.sum(), connectivity=4)
     )
 
 
 def test_sieve_connectivity_queen(diagonal_image):
-    """ Diagonals are connected, so feature is retained """
-
-    assert numpy.array_equal(
+    """Diagonals are connected, so feature is retained."""
+    assert np.array_equal(
         diagonal_image,
         sieve(diagonal_image, diagonal_image.sum(), connectivity=8)
     )
@@ -572,30 +536,28 @@ def test_sieve_connectivity_invalid(basic_image):
 
 
 def test_sieve_out(basic_image):
-    """ Output array passed in should match the returned array """
-
-    with Env():
-        output = numpy.zeros_like(basic_image)
+    """Output array passed in should match the returned array."""
+    with rasterio.Env():
+        output = np.zeros_like(basic_image)
         output[1:3, 1:3] = 5
         sieved_image = sieve(basic_image, basic_image.sum(), out=output)
-        assert numpy.array_equal(basic_image, sieved_image)
-        assert numpy.array_equal(output, sieved_image)
+        assert np.array_equal(basic_image, sieved_image)
+        assert np.array_equal(output, sieved_image)
 
 
 def test_sieve_invalid_out(basic_image):
-    """ Output with different dtype or shape should fail """
-
-    with Env():
+    """Output with different dtype or shape should fail."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             sieve(
                 basic_image, basic_image.sum(),
-                out=numpy.zeros(basic_image.shape, dtype=rasterio.int32)
+                out=np.zeros(basic_image.shape, dtype=rasterio.int32)
             )
 
         with pytest.raises(ValueError):
             sieve(
                 basic_image, basic_image.sum(),
-                out=numpy.zeros(
+                out=np.zeros(
                     (basic_image.shape[0] + 10, basic_image.shape[1] + 10),
                     dtype=rasterio.ubyte
                 )
@@ -607,45 +569,42 @@ def test_sieve_mask(basic_image):
     Only areas within the overlap of mask and input will be kept, so long
     as mask is a bool or uint8 dtype.
     """
-
-    mask = numpy.ones(basic_image.shape, dtype=rasterio.bool_)
+    mask = np.ones(basic_image.shape, dtype=rasterio.bool_)
     mask[4:5, 4:5] = False
-    truth = basic_image * numpy.invert(mask)
+    truth = basic_image * np.invert(mask)
 
-    with Env():
+    with rasterio.Env():
         sieved_image = sieve(basic_image, basic_image.sum(), mask=mask)
         assert sieved_image.sum() > 0
 
-        assert numpy.array_equal(
+        assert np.array_equal(
             truth,
             sieved_image
         )
 
-        assert numpy.array_equal(
+        assert np.array_equal(
             truth.astype(rasterio.uint8),
             sieved_image
         )
 
 
 def test_sieve_blank_mask(basic_image):
-    """ A blank mask should have no effect """
-
-    mask = numpy.ones(basic_image.shape, dtype=rasterio.bool_)
-    with Env():
-        assert numpy.array_equal(
+    """A blank mask should have no effect."""
+    mask = np.ones(basic_image.shape, dtype=rasterio.bool_)
+    with rasterio.Env():
+        assert np.array_equal(
             basic_image,
             sieve(basic_image, basic_image.sum(), mask=mask)
         )
 
 
 def test_sieve_invalid_mask_shape(basic_image):
-    """ A mask that is the wrong shape should fail """
-
-    with Env():
+    """A mask that is the wrong shape should fail."""
+    with rasterio.Env():
         with pytest.raises(ValueError):
             sieve(
                 basic_image, basic_image.sum(),
-                mask=numpy.ones(
+                mask=np.ones(
                     (basic_image.shape[0] + 10, basic_image.shape[1] + 10),
                     dtype=rasterio.bool_
                 )
@@ -653,20 +612,18 @@ def test_sieve_invalid_mask_shape(basic_image):
 
 
 def test_sieve_invalid_mask_dtype(basic_image):
-    """ A mask that is the wrong dtype should fail """
-
-    with Env():
+    """A mask that is the wrong dtype should fail."""
+    with rasterio.Env():
         for dtype in ('int8', 'int16', 'int32'):
             with pytest.raises(ValueError):
                 sieve(
                     basic_image, basic_image.sum(),
-                    mask=numpy.ones(basic_image.shape, dtype=dtype)
+                    mask=np.ones(basic_image.shape, dtype=dtype)
                 )
 
 
 def test_sieve_supported_dtypes(basic_image):
-    """ Supported data types should return valid results """
-
+    """Supported data types should return valid results."""
     supported_types = (
         ('int16', -32768),
         ('int32', -2147483648),
@@ -674,17 +631,16 @@ def test_sieve_supported_dtypes(basic_image):
         ('uint16', 65535)
     )
 
-    with Env():
+    with rasterio.Env():
         for dtype, test_value in supported_types:
             truth = (basic_image).astype(dtype) * test_value
             sieved_image = sieve(truth, basic_image.sum())
-            assert numpy.array_equal(truth, sieved_image)
-            assert numpy.dtype(sieved_image.dtype) == numpy.dtype(dtype)
+            assert np.array_equal(truth, sieved_image)
+            assert np.dtype(sieved_image.dtype) == np.dtype(dtype)
 
 
 def test_sieve_unsupported_dtypes(basic_image):
-    """ Unsupported data types should raise exceptions """
-
+    """Unsupported data types should raise exceptions."""
     unsupported_types = (
         ('int8', -127),
         ('uint32', 4294967295),
@@ -694,7 +650,7 @@ def test_sieve_unsupported_dtypes(basic_image):
         ('float64', -98332.133422114)
     )
 
-    with Env():
+    with rasterio.Env():
         for dtype, test_value in unsupported_types:
             with pytest.raises(ValueError):
                 sieve(
@@ -704,26 +660,24 @@ def test_sieve_unsupported_dtypes(basic_image):
 
 
 def test_sieve_band(pixelated_image, pixelated_image_file):
-    """ Sieving a band from a raster file should match sieve of array """
-
-    with Env():
+    """Sieving a band from a raster file should match sieve of array."""
+    with rasterio.Env():
         truth = sieve(pixelated_image, 9)
 
         with rasterio.open(pixelated_image_file) as src:
             band = rasterio.band(src, 1)
-            assert numpy.array_equal(truth, sieve(band, 9))
+            assert np.array_equal(truth, sieve(band, 9))
 
             # Mask band should also work but will be a no-op
-            assert numpy.array_equal(
+            assert np.array_equal(
                 pixelated_image,
                 sieve(band, 9, mask=band)
             )
 
 
 def test_sieve_internal_driver_manager(basic_image, pixelated_image):
-    """ Sieve should work without explicitly calling driver manager """
-
-    assert numpy.array_equal(
+    """Sieve should work without explicitly calling driver manager."""
+    assert np.array_equal(
         basic_image,
         sieve(pixelated_image, basic_image.sum())
     )
diff --git a/tests/test_fillnodata.py b/tests/test_fillnodata.py
index 2ccee1e..d2ab80f 100644
--- a/tests/test_fillnodata.py
+++ b/tests/test_fillnodata.py
@@ -1,6 +1,6 @@
 import logging
 import sys
-import numpy
+import numpy as np
 import pytest
 
 import rasterio
@@ -11,16 +11,16 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 def test_fillnodata():
     """Test filling nodata values in an ndarray"""
     # create a 5x5 array, with some missing data
-    a = numpy.ones([3, 3]) * 42
+    a = np.ones([3, 3]) * 42
     a[1][1] = 0
     # find the missing data
     mask = ~(a == 0)
     # fill the missing data using interpolation from the edges
     result = fillnodata(a, mask)
-    assert(numpy.all((numpy.ones([3, 3]) * 42) == result))
+    assert(np.all((np.ones([3, 3]) * 42) == result))
 
 def test_fillnodata_invalid_types():
-    a = numpy.ones([3, 3])
+    a = np.ones([3, 3])
     with pytest.raises(ValueError):
         fillnodata(None, a)
     with pytest.raises(ValueError):
@@ -28,15 +28,15 @@ def test_fillnodata_invalid_types():
 
 def test_fillnodata_mask_ones():
     # when mask is all ones, image should be unmodified
-    a = numpy.ones([3, 3]) * 42
+    a = np.ones([3, 3]) * 42
     a[1][1] = 0
-    mask = numpy.ones([3, 3])
+    mask = np.ones([3, 3])
     result = fillnodata(a, mask)
-    assert(numpy.all(a == result))
+    assert(np.all(a == result))
 
 '''
 def test_fillnodata_smooth():
-    a = numpy.array([[1,3,3,1],[2,0,0,2],[2,0,0,2],[1,3,3,1]], dtype=numpy.float64)
+    a = np.array([[1,3,3,1],[2,0,0,2],[2,0,0,2],[1,3,3,1]], dtype=np.float64)
     mask = ~(a == 0)
     result = fillnodata(a, mask, max_search_distance=1, smoothing_iterations=0)
     assert(result[1][1] == 3)
diff --git a/tests/test_image_structure.py b/tests/test_image_structure.py
index ed43973..69f3372 100644
--- a/tests/test_image_structure.py
+++ b/tests/test_image_structure.py
@@ -67,7 +67,7 @@ def test_interleaving_pixel():
         assert src.profile['interleave'] == 'pixel'
 
 
-def test_interleaving_pixel():
+def test_interleaving_band():
     with rasterio.open('tests/data/rgb_deflate.tif') as src:
         assert src.interleaving.name == 'band'
         assert src.interleaving.value == 'BAND'
diff --git a/tests/test_indexing.py b/tests/test_indexing.py
index 0a659ae..d1b15d7 100644
--- a/tests/test_indexing.py
+++ b/tests/test_indexing.py
@@ -1,4 +1,4 @@
-import numpy
+import numpy as np
 import pytest
 
 import rasterio
@@ -77,7 +77,7 @@ def test_window_full_cover():
 
 @pytest.fixture
 def data():
-    data = numpy.zeros((10, 10), dtype='uint8')
+    data = np.zeros((10, 10), dtype='uint8')
     data[slice(*DATA_WINDOW[0]), slice(*DATA_WINDOW[1])] = 1
     return data
 
@@ -88,7 +88,7 @@ def test_data_window_unmasked(data):
 
 
 def test_data_window_masked(data):
-    data = numpy.ma.masked_array(data, data == 0)
+    data = np.ma.masked_array(data, data == 0)
     window = windows.get_data_window(data)
     assert window == DATA_WINDOW
 
@@ -97,12 +97,12 @@ def test_data_window_nodata(data):
     window = windows.get_data_window(data, nodata=0)
     assert window == DATA_WINDOW
 
-    window = windows.get_data_window(numpy.ones_like(data), nodata=0)
+    window = windows.get_data_window(np.ones_like(data), nodata=0)
     assert window == ((0, data.shape[0]), (0, data.shape[1]))
 
 
 def test_data_window_nodata_disjunct():
-    data = numpy.zeros((3, 10, 10), dtype='uint8')
+    data = np.zeros((3, 10, 10), dtype='uint8')
     data[0, :4, 1:4] = 1
     data[1, 2:5, 2:8] = 1
     data[2, 1:6, 1:6] = 1
@@ -111,7 +111,7 @@ def test_data_window_nodata_disjunct():
 
 
 def test_data_window_empty_result():
-    data = numpy.zeros((3, 10, 10), dtype='uint8')
+    data = np.zeros((3, 10, 10), dtype='uint8')
     window = windows.get_data_window(data, nodata=0)
     assert window == ((0, 0), (0, 0))
 
diff --git a/tests/test_mask_creation.py b/tests/test_mask_creation.py
index a8dcec8..5ae0824 100644
--- a/tests/test_mask_creation.py
+++ b/tests/test_mask_creation.py
@@ -1,17 +1,15 @@
 """
 Tests of band mask creation, both .msk sidecar and internal.
-
-See https://github.com/mapbox/rasterio/issues/293 for bug report.
 """
 
+import pytest
 import rasterio
 from rasterio.enums import MaskFlags
-from rasterio.env import Env
 
 
 def test_create_internal_mask(data):
     """Write an internal mask to the fixture's RGB.byte.tif."""
-    with Env(GDAL_TIFF_INTERNAL_MASK=True):
+    with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=True):
         with rasterio.open(str(data.join('RGB.byte.tif')), 'r+') as dst:
             blue = dst.read(1, masked=False)
             mask = 255 * (blue == 0).astype('uint8')
@@ -52,3 +50,28 @@ def test_create_sidecar_mask(data):
     # Check the .msk file, too.
     with rasterio.open(str(data.join('RGB.byte.tif.msk'))) as msk:
         assert (mask == msk.read(1, masked=False)).all()
+
+
+def test_create_mask_windowed_sidecar(data):
+    """Writing masks by window succeeds with sidecar mask
+    """
+    with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=False):
+        with rasterio.open(str(data.join('RGB.byte.tif')), 'r+') as dst:
+            for ij, window in dst.block_windows():
+                blue = dst.read(1, window=window, masked=False)
+                mask = 255 * (blue == 0).astype('uint8')
+                dst.write_mask(mask, window=window)
+
+
+ at pytest.mark.xfail(reason="https://github.com/mapbox/rasterio/issues/781")
+def test_create_mask_windowed_internal(data):
+    """Writing masks by window with internal mask
+    Currently fails with
+        rasterio.errors.RasterioIOError: Failed to get mask.
+    """
+    with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=True):
+        with rasterio.open(str(data.join('RGB.byte.tif')), 'r+') as dst:
+            for ij, window in dst.block_windows():
+                blue = dst.read(1, window=window, masked=False)
+                mask = 255 * (blue == 0).astype('uint8')
+                dst.write_mask(mask, window=window)
diff --git a/tests/test_pad.py b/tests/test_pad.py
index 8cd894c..d8a7eba 100644
--- a/tests/test_pad.py
+++ b/tests/test_pad.py
@@ -1,12 +1,12 @@
 
 import affine
-import numpy
+import numpy as np
 
 import rasterio
 
 
 def test_pad():
-    arr = numpy.ones((10, 10))
+    arr = np.ones((10, 10))
     trans = affine.Affine(1.0, 0.0, 0.0, 0.0, -1.0, 10.0)
     arr2, trans2 = rasterio.pad(arr, trans, 2, 'edge')
     assert arr2.shape == (14, 14)
diff --git a/tests/test_plot.py b/tests/test_plot.py
new file mode 100644
index 0000000..1227afe
--- /dev/null
+++ b/tests/test_plot.py
@@ -0,0 +1,264 @@
+import numpy as np
+import pytest
+
+try:
+    import matplotlib as mpl
+    mpl.use('agg')
+    import matplotlib.pyplot as plt
+    plt.show = lambda :None
+except ImportError:
+    plt = None
+
+import rasterio
+from rasterio.plot import show, show_hist, get_plt
+from rasterio.enums import ColorInterp
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_raster():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show((src, 1))
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            try:
+                show(src)
+                fig = plt.gcf()
+                plt.close(fig)
+            except ImportError:
+                pass
+
+        with rasterio.open('tests/data/float.tif') as src:
+            try:
+                show(src)
+                fig = plt.gcf()
+                plt.close(fig)
+            except ImportError:
+                pass
+
+def test_show_cmyk_interp(tmpdir):
+    """A CMYK TIFF has cyan, magenta, yellow, black bands."""
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        meta = src.meta
+    meta['photometric'] = 'CMYK'
+    meta['count'] = 4
+    tiffname = str(tmpdir.join('foo.tif'))
+    with rasterio.open(tiffname, 'w', **meta) as dst:
+        assert dst.profile['photometric'] == 'cmyk'
+        assert dst.colorinterp(1) == ColorInterp.cyan
+        assert dst.colorinterp(2) == ColorInterp.magenta
+        assert dst.colorinterp(3) == ColorInterp.yellow
+        assert dst.colorinterp(4) == ColorInterp.black
+
+    with rasterio.open(tiffname) as src:
+        try:
+            show(src)
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_raster_no_bounds():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show((src, 1), with_bounds=False)
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_raster_title():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show((src, 1), title="insert title here")
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_hist_large():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    try:
+        rand_arr = np.random.randn(10, 718, 791)
+        show_hist(rand_arr)
+        fig = plt.gcf()
+        plt.close(fig)
+    except ImportError:
+        pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_raster_cmap():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show((src, 1), cmap='jet')
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_raster_ax():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            fig, ax = plt.subplots(1)
+            show((src, 1), ax=ax)
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_array():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show(src.read(1))
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_array3D():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show(src.read((1, 2, 3)))
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_hist():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show_hist((src, 1), bins=256)
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+        try:
+            show_hist(src.read(), bins=256)
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+        try:
+            fig, ax = plt.subplots(1)
+            show_hist(src.read(), bins=256, ax=ax)
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_hist_mplargs():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show_hist(src, bins=50, lw=0.0, stacked=False, alpha=0.3, 
+               histtype='stepfilled', title="World Histogram overlaid")
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_contour():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show((src, 1), contour=True)
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_show_contour_mplargs():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            show((src, 1), contour=True, 
+                levels=[25, 125], colors=['white', 'red'], linewidths=4,
+                contour_label_kws=dict(fontsize=18, fmt="%1.0f", inline_spacing=15, use_clabeltext=True))
+            fig = plt.gcf()
+            plt.close(fig)
+        except ImportError:
+            pass
+
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
+def test_get_plt():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        try:
+            assert plt == get_plt()
+        except ImportError:
+            pass
\ No newline at end of file
diff --git a/tests/test_png.py b/tests/test_png.py
index 2a31ac5..3029fc0 100644
--- a/tests/test_png.py
+++ b/tests/test_png.py
@@ -2,7 +2,7 @@ import logging
 import subprocess
 import sys
 import re
-import numpy
+import numpy as np
 import rasterio
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
@@ -10,10 +10,10 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
 def test_write_ubyte(tmpdir):
     name = str(tmpdir.mkdir("sub").join("test_write_ubyte.png"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     with rasterio.open(
-            name, 'w', 
-            driver='PNG', width=100, height=100, count=1, 
+            name, 'w',
+            driver='PNG', width=100, height=100, count=1,
             dtype=a.dtype) as s:
         s.write(a, indexes=1)
     info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
diff --git a/tests/test_read.py b/tests/test_read.py
index 7298e02..6a57f61 100644
--- a/tests/test_read.py
+++ b/tests/test_read.py
@@ -1,9 +1,10 @@
+from hashlib import md5
 import logging
 import sys
 import unittest
 
-import numpy
-from hashlib import md5
+import numpy as np
+import pytest
 
 import rasterio
 
@@ -11,6 +12,15 @@ import rasterio
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
 
+# Find out if we've got HDF support (needed below).
+try:
+    with rasterio.open('tests/data/no_band.h5') as s:
+        pass
+    has_hdf = True
+except:
+    has_hdf = False
+
+
 class ReaderContextTest(unittest.TestCase):
 
     def test_context(self):
@@ -26,17 +36,17 @@ class ReaderContextTest(unittest.TestCase):
             self.assertEqual(s.nodatavals, (0, 0, 0))
             self.assertEqual(s.indexes, (1, 2, 3))
             self.assertEqual(s.crs['init'], 'epsg:32618')
-            self.assert_(s.crs_wkt.startswith('PROJCS'), s.crs_wkt)
+            self.assert_(s.crs.wkt.startswith('PROJCS'), s.crs.wkt)
             for i, v in enumerate((101985.0, 2611485.0, 339315.0, 2826915.0)):
                 self.assertAlmostEqual(s.bounds[i], v)
             self.assertEqual(
-                s.affine, 
+                s.affine,
                 (300.0379266750948, 0.0, 101985.0,
                  0.0, -300.041782729805, 2826915.0,
                  0, 0, 1.0))
             self.assertEqual(s.meta['crs'], s.crs)
             self.assertEqual(
-                repr(s), 
+                repr(s),
                 "<open RasterReader name='tests/data/RGB.byte.tif' "
                 "mode='r'>")
         self.assertEqual(s.closed, True)
@@ -48,7 +58,7 @@ class ReaderContextTest(unittest.TestCase):
         self.assertEqual(s.nodatavals, (0, 0, 0))
         self.assertEqual(s.crs['init'], 'epsg:32618')
         self.assertEqual(
-            s.affine, 
+            s.affine,
             (300.0379266750948, 0.0, 101985.0,
              0.0, -300.041782729805, 2826915.0,
              0, 0, 1.0))
@@ -59,7 +69,7 @@ class ReaderContextTest(unittest.TestCase):
 
     def test_derived_spatial(self):
         with rasterio.open('tests/data/RGB.byte.tif') as s:
-            self.assert_(s.crs_wkt.startswith('PROJCS'), s.crs_wkt)
+            self.assert_(s.crs.wkt.startswith('PROJCS'), s.crs.wkt)
             for i, v in enumerate((101985.0, 2611485.0, 339315.0, 2826915.0)):
                 self.assertAlmostEqual(s.bounds[i], v)
             for a, b in zip(s.ul(0, 0), (101985.0, 2826915.0)):
@@ -76,17 +86,18 @@ class ReaderContextTest(unittest.TestCase):
 
     def test_read_ubyte_out(self):
         with rasterio.open('tests/data/RGB.byte.tif') as s:
-            a = numpy.zeros((718, 791), dtype=rasterio.ubyte)
+            a = np.zeros((718, 791), dtype=rasterio.ubyte)
             a = s.read(1, a)
             self.assertEqual(a.dtype, rasterio.ubyte)
 
     def test_read_out_dtype_fail(self):
         with rasterio.open('tests/data/RGB.byte.tif') as s:
-            a = numpy.zeros((718, 791), dtype=rasterio.float32)
+            a = np.zeros((718, 791), dtype=rasterio.float32)
             try:
                 s.read(1, a)
             except ValueError as e:
-                assert "the array's dtype 'float32' does not match the file's dtype" in str(e)
+                assert ("the array's dtype 'float32' does not match the "
+                        "file's dtype") in str(e)
             except:
                 assert "failed to catch exception" is False
 
@@ -167,18 +178,18 @@ class ReaderContextTest(unittest.TestCase):
             self.assertTrue(hasattr(a, 'mask'))
             self.assertEqual(a.mask.sum((1, 2)).tolist(), [0, 0, 1])
             self.assertEqual([md5(x.tostring()).hexdigest() for x in a],
-                              ['1df719040daa9dfdb3de96d6748345e8',
-                               'ec8fb3659f40c4a209027231bef12bdb',
-                               '5a9c12aebc126ec6f27604babd67a4e2'])
+                             ['1df719040daa9dfdb3de96d6748345e8',
+                              'ec8fb3659f40c4a209027231bef12bdb',
+                              '5a9c12aebc126ec6f27604babd67a4e2'])
             # window without any missing data, but still is masked result
             a = s.read(window=((310, 330), (320, 330)), masked=True)
             self.assertEqual(a.ndim, 3)
             self.assertEqual(a.shape, (3, 20, 10))
             self.assertTrue(hasattr(a, 'mask'))
             self.assertEqual([md5(x.tostring()).hexdigest() for x in a[:]],
-                              ['9e3000d60b4b6fb956f10dc57c4dc9b9',
-                               '6a675416a32fcb70fbcf601d01aeb6ee',
-                               '94fd2733b534376c273a894f36ad4e0b'])
+                             ['9e3000d60b4b6fb956f10dc57c4dc9b9',
+                              '6a675416a32fcb70fbcf601d01aeb6ee',
+                              '94fd2733b534376c273a894f36ad4e0b'])
 
     def test_read_window_overflow(self):
         """Test graceful Numpy-like handling of windows that overflow
@@ -192,46 +203,46 @@ class ReaderContextTest(unittest.TestCase):
         the dataset's bounds."""
         with rasterio.open('tests/data/RGB.byte.tif') as s:
             a = s.read(window=((10000, 20000), (10000, 20000)))
-            self.assertEqual(a.shape, (3,0,0))
+            self.assertEqual(a.shape, (3, 0, 0))
 
     def test_read_window_overlap(self):
         """Test graceful Numpy-like handling of windows beyond
         the dataset's bounds."""
         with rasterio.open('tests/data/RGB.byte.tif') as s:
             a = s.read(window=((-100, 20000), (-100, 20000)))
-            self.assertEqual(a.shape, (3,100,100))
+            self.assertEqual(a.shape, (3, 100, 100))
 
     def test_read_out(self):
         with rasterio.open('tests/data/RGB.byte.tif') as s:
             # regular array, without mask
-            a = numpy.empty((3, 718, 791), numpy.ubyte)
+            a = np.empty((3, 718, 791), np.ubyte)
             b = s.read(out=a)
             self.assertFalse(hasattr(a, 'mask'))
             self.assertFalse(hasattr(b, 'mask'))
             # with masked array
-            a = numpy.ma.empty((3, 718, 791), numpy.ubyte)
+            a = np.ma.empty((3, 718, 791), np.ubyte)
             b = s.read(out=a)
             self.assertEqual(id(a.data), id(b.data))
             # TODO: is there a way to id(a.mask)?
             self.assertTrue(hasattr(a, 'mask'))
             self.assertTrue(hasattr(b, 'mask'))
             # use all parameters
-            a = numpy.empty((1, 20, 10), numpy.ubyte)
+            a = np.empty((1, 20, 10), np.ubyte)
             b = s.read([2], a, ((310, 330), (320, 330)), False)
             self.assertEqual(id(a), id(b))
             # pass 2D array with index
-            a = numpy.empty((20, 10), numpy.ubyte)
+            a = np.empty((20, 10), np.ubyte)
             b = s.read(2, a, ((310, 330), (320, 330)), False)
             self.assertEqual(id(a), id(b))
             self.assertEqual(a.ndim, 2)
             # different data types
-            a = numpy.empty((3, 718, 791), numpy.float64)
+            a = np.empty((3, 718, 791), np.float64)
             self.assertRaises(ValueError, s.read, out=a)
             # different number of array dimensions
-            a = numpy.empty((20, 10), numpy.ubyte)
+            a = np.empty((20, 10), np.ubyte)
             self.assertRaises(ValueError, s.read, [2], out=a)
             # different number of array shape in 3D
-            a = numpy.empty((2, 20, 10), numpy.ubyte)
+            a = np.empty((2, 20, 10), np.ubyte)
             self.assertRaises(ValueError, s.read, [2], out=a)
 
     def test_read_nan_nodata(self):
@@ -240,22 +251,75 @@ class ReaderContextTest(unittest.TestCase):
             self.assertEqual(a.ndim, 3)
             self.assertEqual(a.shape, (1, 2, 3))
             self.assertTrue(hasattr(a, 'mask'))
-            self.assertNotEqual(a.fill_value, numpy.nan)
-            self.assertEqual(str(list(set(s.nodatavals))), str([numpy.nan]))
+            self.assertNotEqual(a.fill_value, np.nan)
+            self.assertEqual(str(list(set(s.nodatavals))), str([np.nan]))
             self.assertEqual(a.dtype, rasterio.float32)
-            self.assertFalse(numpy.isnan(a).any())
+            self.assertFalse(np.isnan(a).any())
             a = s.read(masked=False)
             self.assertFalse(hasattr(a, 'mask'))
-            self.assertTrue(numpy.isnan(a).any())
+            self.assertTrue(np.isnan(a).any())
             # Window does not contain a nodatavalue, result is still masked
             a = s.read(window=((0, 2), (0, 2)), masked=True)
             self.assertEqual(a.ndim, 3)
             self.assertEqual(a.shape, (1, 2, 2))
             self.assertTrue(hasattr(a, 'mask'))
 
+    @pytest.mark.skipif(not has_hdf, reason="HDF driver not available")
     def test_read_no_band(self):
         with rasterio.open('tests/data/no_band.h5') as s:
             self.assertEqual(s.count, 0)
             self.assertEqual(s.meta['dtype'], 'float_')
             self.assertIsNone(s.meta['nodata'])
             self.assertRaises(ValueError, s.read)
+
+
+ at pytest.mark.parametrize("shape,indexes", [
+    ((72, 80), 1),          # Single band
+    ((2, 72, 80), (1, 3)),  # Multiband
+    ((3, 72, 80), None)     # All bands
+])
+def test_out_shape(path_rgb_byte_tif, shape, indexes):
+
+    """Test read(out_shape) and read_masks(out_shape).  The tests are identical
+    aside from the method call.
+
+    The pytest parameters are:
+
+        * shape - tuple passed to out_shape
+        * indexes - The bands to read
+
+    The resulting images have been decimated by a factor of 10.
+    """
+
+    with rasterio.open(path_rgb_byte_tif) as src:
+
+        for attr in 'read', 'read_masks':
+
+            reader = getattr(src, attr)
+
+            out_shape = reader(indexes, out_shape=shape)
+            out = reader(indexes, out=np.empty(shape, dtype=src.dtypes[0]))
+
+            assert out_shape.shape == out.shape
+            assert (out_shape == out).all()
+
+            # Sanity check fo the test itself
+            assert shape[-2:] == (72, 80)
+
+
+def test_out_shape_exceptions(path_rgb_byte_tif):
+
+    with rasterio.open(path_rgb_byte_tif) as src:
+
+        for attr in 'read', 'read_masks':
+
+            reader = getattr(src, attr)
+
+            with pytest.raises(ValueError):
+                out = np.empty((src.count, src.height, src.width))
+                out_shape = (src.count, src.height, src.width)
+                reader(out=out, out_shape=out_shape)
+
+            with pytest.raises(ValueError):
+                out_shape = (5, src.height, src.width)
+                reader(1, out_shape=out_shape)
diff --git a/tests/test_read_boundless.py b/tests/test_read_boundless.py
index 9fdb593..ab12554 100644
--- a/tests/test_read_boundless.py
+++ b/tests/test_read_boundless.py
@@ -1,7 +1,7 @@
 import logging
 import sys
 
-import numpy
+import numpy as np
 import pytest
 
 import rasterio
@@ -42,7 +42,7 @@ def test_read_boundless_overlap():
 
 def test_read_boundless_resample():
     with rasterio.open('tests/data/RGB.byte.tif') as src:
-        out = numpy.zeros((3, 800, 800), dtype=numpy.uint8)
+        out = np.zeros((3, 800, 800), dtype=np.uint8)
         data = src.read(
                 out=out,
                 window=((-200, 200), (-200, 200)),
@@ -94,19 +94,19 @@ def test_read_boundless_noshift():
                       window=((100, 101), (-1, src.shape[1])))[0, 0, 0:9]
         c2 = src.read(boundless=True,
                       window=((100, 101), (-1, src.shape[1] + 1)))[0, 0, 0:9]
-        assert numpy.array_equal(c1, c2)
+        assert np.array_equal(c1, c2)
 
         # when row stop exceeds image height
         r1 = src.read(boundless=True,
                       window=((-1, src.shape[0]), (100, 101)))[0, 0, 0:9]
         r2 = src.read(boundless=True,
                       window=((-1, src.shape[0] + 1), (100, 101)))[0, 0, 0:9]
-        assert numpy.array_equal(r1, r2)
+        assert np.array_equal(r1, r2)
 
 
-def test_numpy_warning(recwarn):
+def test_np_warning(recwarn):
     """Ensure no deprecation warnings
-    On numpy 1.11 and previous versions of rasterio you might see:
+    On np 1.11 and previous versions of rasterio you might see:
         VisibleDeprecationWarning: using a non-integer number
         instead of an integer will result in an error in the future
     """
diff --git a/tests/test_read_resample.py b/tests/test_read_resample.py
index 3198421..fc62ea2 100644
--- a/tests/test_read_resample.py
+++ b/tests/test_read_resample.py
@@ -1,4 +1,4 @@
-import numpy
+import numpy as np
 
 import rasterio
 
@@ -10,9 +10,9 @@ import rasterio
 
 def test_read_out_shape_resample_down():
     with rasterio.open('tests/data/RGB.byte.tif') as s:
-        out = numpy.zeros((8, 8), dtype=rasterio.ubyte)
+        out = np.zeros((8, 8), dtype=rasterio.ubyte)
         data = s.read(1, out=out)
-        expected = numpy.array([
+        expected = np.array([
             [  0,   0,  20,  15,   0,   0,   0,   0],
             [  0,   6, 193,   9, 255, 127,  23,  39],
             [  0,   7,  27, 255, 193,  14,  28,  34],
@@ -20,7 +20,7 @@ def test_read_out_shape_resample_down():
             [  0,   9,  69,  49,  17,  22, 255,   0],
             [ 11,   7,  13,  25,  13,  29,  33,   0],
             [  8,  10,  88,  27,  20,  33,  25,   0],
-            [  0,   0,   0,   0,  98,  23,   0,   0]], dtype=numpy.uint8)
+            [  0,   0,   0,   0,  98,  23,   0,   0]], dtype=np.uint8)
         assert (data == expected).all() # all True.
 
 
@@ -28,7 +28,7 @@ def test_read_out_shape_resample_up():
     # Instead of testing array items, test statistics. Upsampling by an even
     # constant factor shouldn't change the mean.
     with rasterio.open('tests/data/RGB.byte.tif') as s:
-        out = numpy.zeros((7180, 7910), dtype=rasterio.ubyte)
+        out = np.zeros((7180, 7910), dtype=rasterio.ubyte)
         data = s.read(1, out=out, masked=True)
         assert data.shape == (7180, 7910)
         assert data.mean() == s.read(1, masked=True).mean()
@@ -37,7 +37,7 @@ def test_read_out_shape_resample_up():
 def test_read_downsample_alpha():
     with rasterio.Env(GTIFF_IMPLICIT_JPEG_OVR=False):
         with rasterio.open('tests/data/alpha.tif') as src:
-            out = numpy.zeros((100, 100), dtype=rasterio.ubyte)
+            out = np.zeros((100, 100), dtype=rasterio.ubyte)
             assert src.width == 1223
             assert src.height == 1223
             assert src.count == 4
diff --git a/tests/test_reshape_image.py b/tests/test_reshape_image.py
new file mode 100644
index 0000000..24c41bc
--- /dev/null
+++ b/tests/test_reshape_image.py
@@ -0,0 +1,26 @@
+import numpy as np
+
+from rasterio import plot
+import rasterio
+
+try:
+    import matplotlib as mpl
+    mpl.use('agg')
+except:
+    pass
+
+def test_reshape():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        im_data = plot.reshape_as_image(src.read())
+        assert im_data.shape == (718, 791, 3)
+
+def test_roundtrip_reshape():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        data = src.read()
+        im_data = plot.reshape_as_image(data)
+        assert np.array_equal(data, rasterio.plot.reshape_as_raster(im_data))
+
+def test_reshape_as_raster():
+    img_arr = np.random.randn(718, 791, 3)
+    rast_arr = plot.reshape_as_raster(img_arr)
+    assert img_arr.shape[-1] == rast_arr.shape[0]
diff --git a/tests/test_rio_convert.py b/tests/test_rio_convert.py
index 6bce7df..b2446a5 100644
--- a/tests/test_rio_convert.py
+++ b/tests/test_rio_convert.py
@@ -1,7 +1,7 @@
 import sys
 import os
 import logging
-import numpy
+import numpy as np
 from click.testing import CliRunner
 
 import rasterio
@@ -38,7 +38,7 @@ def test_clip_like(runner, tmpdir):
     with rasterio.open('tests/data/shade.tif') as template_ds:
         with rasterio.open(output) as out:
             assert out.shape == template_ds.shape
-            assert numpy.allclose(out.bounds, template_ds.bounds)
+            assert np.allclose(out.bounds, template_ds.bounds)
 
 
 def test_clip_missing_params(runner, tmpdir):
diff --git a/tests/test_rio_features.py b/tests/test_rio_features.py
index 69f2200..efb04aa 100644
--- a/tests/test_rio_features.py
+++ b/tests/test_rio_features.py
@@ -2,7 +2,7 @@ import logging
 import os
 import re
 import sys
-import numpy
+import numpy as np
 import json
 from affine import Affine
 
@@ -11,7 +11,7 @@ from rasterio.rio.mask import mask
 from rasterio.rio.shapes import shapes
 from rasterio.rio.rasterize import rasterize
 from rasterio.rio.main import main_group
-
+from rasterio.crs import CRS
 
 DEFAULT_SHAPE = (10, 10)
 
@@ -33,7 +33,7 @@ def test_mask(runner, tmpdir, basic_feature, basic_image_2x2,
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
-        assert numpy.array_equal(
+        assert np.array_equal(
             basic_image_2x2,
             out.read(1, masked=True).filled(0)
         )
@@ -54,7 +54,7 @@ def test_mask_all_touched(runner, tmpdir, basic_feature, basic_image,
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
-        assert numpy.array_equal(
+        assert np.array_equal(
             basic_image,
             out.read(1, masked=True).filled(0)
         )
@@ -77,7 +77,7 @@ def test_mask_invert(runner, tmpdir, basic_feature, pixelated_image,
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
-        assert numpy.array_equal(
+        assert np.array_equal(
             truth,
             out.read(1, masked=True).filled(0))
 
@@ -95,7 +95,7 @@ def test_mask_featurecollection(runner, tmpdir, basic_featurecollection,
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
-        assert numpy.array_equal(
+        assert np.array_equal(
             basic_image_2x2,
             out.read(1, masked=True).filled(0))
 
@@ -107,7 +107,7 @@ def test_mask_out_of_bounds(runner, tmpdir, basic_feature,
     blank image.
     """
 
-    coords = numpy.array(basic_feature['geometry']['coordinates']) - 10
+    coords = np.array(basic_feature['geometry']['coordinates']) - 10
     basic_feature['geometry']['coordinates'] = coords.tolist()
 
     output = str(tmpdir.join('test.tif'))
@@ -121,7 +121,7 @@ def test_mask_out_of_bounds(runner, tmpdir, basic_feature,
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
-        assert not numpy.any(out.read(1, masked=True).filled(0))
+        assert not np.any(out.read(1, masked=True).filled(0))
 
 
 def test_mask_no_geojson(runner, tmpdir, pixelated_image, pixelated_image_file):
@@ -136,7 +136,7 @@ def test_mask_no_geojson(runner, tmpdir, pixelated_image, pixelated_image_file):
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
-        assert numpy.array_equal(
+        assert np.array_equal(
             pixelated_image,
             out.read(1, masked=True).filled(0))
 
@@ -172,7 +172,7 @@ def test_mask_crop(runner, tmpdir, basic_feature, pixelated_image):
     image = pixelated_image
     outfilename = str(tmpdir.join('pixelated_image.tif'))
     kwargs = {
-        "crs": {'init': 'epsg:4326'},
+        "crs": CRS({'init': 'epsg:4326'}),
         "transform": Affine(1, 0, 0, 0, -1, 0),
         "count": 1,
         "dtype": rasterio.uint8,
@@ -185,7 +185,7 @@ def test_mask_crop(runner, tmpdir, basic_feature, pixelated_image):
 
     output = str(tmpdir.join('test.tif'))
 
-    truth = numpy.zeros((4, 3))
+    truth = np.zeros((4, 3))
     truth[1:3, 0:2] = 1
 
     result = runner.invoke(
@@ -195,7 +195,7 @@ def test_mask_crop(runner, tmpdir, basic_feature, pixelated_image):
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.array_equal(
+        assert np.array_equal(
             truth,
             out.read(1, masked=True).filled(0))
 
@@ -208,7 +208,7 @@ def test_mask_crop_inverted_y(runner, tmpdir, basic_feature, pixelated_image_fil
 
     output = str(tmpdir.join('test.tif'))
 
-    truth = numpy.zeros((4, 3))
+    truth = np.zeros((4, 3))
     truth[1:3, 0:2] = 1
 
     result = runner.invoke(
@@ -220,7 +220,7 @@ def test_mask_crop_inverted_y(runner, tmpdir, basic_feature, pixelated_image_fil
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.array_equal(
+        assert np.array_equal(
             truth,
             out.read(1, masked=True).filled(0))
 
@@ -232,7 +232,7 @@ def test_mask_crop_out_of_bounds(runner, tmpdir, basic_feature,
     --crop option.
     """
 
-    coords = numpy.array(basic_feature['geometry']['coordinates']) - 10
+    coords = np.array(basic_feature['geometry']['coordinates']) - 10
     basic_feature['geometry']['coordinates'] = coords.tolist()
 
     output = str(tmpdir.join('test.tif'))
@@ -267,7 +267,7 @@ def test_shapes(runner, pixelated_image_file):
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 4
-    assert numpy.allclose(
+    assert np.allclose(
         json.loads(result.output)['features'][0]['geometry']['coordinates'],
         [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]])
 
@@ -393,7 +393,7 @@ def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 1
 
-    assert numpy.allclose(
+    assert np.allclose(
         json.loads(result.output)['features'][0]['geometry']['coordinates'],
         [[[3, 5], [3, 10], [8, 10], [8, 8], [9, 8], [10, 8], [10, 5], [3, 5]]])
 
@@ -418,7 +418,7 @@ def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 1
 
-    assert numpy.allclose(
+    assert np.allclose(
         json.loads(result.output)['features'][0]['geometry']['coordinates'],
         [[[5, 5], [5, 10], [10, 10], [10, 5], [5, 5]]])
 
@@ -441,7 +441,7 @@ def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 3
-    assert numpy.allclose(
+    assert np.allclose(
         json.loads(result.output)['features'][1]['geometry']['coordinates'],
         [[[2, 2], [2, 5], [5, 5], [5, 2], [2, 2]]])
 
@@ -457,10 +457,10 @@ def test_rasterize(tmpdir, runner, basic_feature):
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.allclose(out.bounds, (2, 2, 4.25, 4.25))
+        assert np.allclose(out.bounds, (2, 2, 4.25, 4.25))
         data = out.read(1, masked=False)
         assert data.shape == DEFAULT_SHAPE
-        assert numpy.all(data)
+        assert np.all(data)
 
 
 def test_rasterize_bounds(tmpdir, runner, basic_feature, basic_image_2x2):
@@ -474,9 +474,9 @@ def test_rasterize_bounds(tmpdir, runner, basic_feature, basic_image_2x2):
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.allclose(out.bounds, (0, 10, 10, 0))
+        assert np.allclose(out.bounds, (0, 10, 10, 0))
         data = out.read(1, masked=False)
-        assert numpy.array_equal(basic_image_2x2, data)
+        assert np.array_equal(basic_image_2x2, data)
         assert data.shape == DEFAULT_SHAPE
 
 
@@ -490,10 +490,10 @@ def test_rasterize_resolution(tmpdir, runner, basic_feature):
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.allclose(out.bounds, (2, 2, 4.25, 4.25))
+        assert np.allclose(out.bounds, (2, 2, 4.25, 4.25))
         data = out.read(1, masked=False)
         assert data.shape == (15, 15)
-        assert numpy.all(data)
+        assert np.all(data)
 
 
 def test_rasterize_multiresolution(tmpdir, runner, basic_feature):
@@ -507,10 +507,10 @@ def test_rasterize_multiresolution(tmpdir, runner, basic_feature):
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.allclose(out.bounds, (2, 2, 4.25, 4.25))
+        assert np.allclose(out.bounds, (2, 2, 4.25, 4.25))
         data = out.read(1, masked=False)
         assert data.shape == (15, 15)
-        assert numpy.all(data)
+        assert np.all(data)
 
 
 def test_rasterize_src_crs(tmpdir, runner, basic_feature):
@@ -533,7 +533,7 @@ def test_rasterize_mismatched_src_crs(tmpdir, runner, basic_feature):
     world bounds should fail.
     """
 
-    coords = numpy.array(basic_feature['geometry']['coordinates']) * 100000
+    coords = np.array(basic_feature['geometry']['coordinates']) * 100000
     basic_feature['geometry']['coordinates'] = coords.tolist()
 
     output = str(tmpdir.join('test.tif'))
@@ -565,7 +565,7 @@ def test_rasterize_existing_output(tmpdir, runner, basic_feature):
     The final result should include rasterized pixels from both
     """
 
-    truth = numpy.zeros(DEFAULT_SHAPE)
+    truth = np.zeros(DEFAULT_SHAPE)
     truth[2:4, 2:4] = 1
     truth[4:6, 4:6] = 1
 
@@ -580,7 +580,7 @@ def test_rasterize_existing_output(tmpdir, runner, basic_feature):
     assert result.exit_code == 0
     assert os.path.exists(output)
 
-    coords = numpy.array(basic_feature['geometry']['coordinates']) + 2
+    coords = np.array(basic_feature['geometry']['coordinates']) + 2
     basic_feature['geometry']['coordinates'] = coords.tolist()
 
     result = runner.invoke(
@@ -592,7 +592,7 @@ def test_rasterize_existing_output(tmpdir, runner, basic_feature):
     assert result.exit_code == 0
 
     with rasterio.open(output) as out:
-        assert numpy.array_equal(truth, out.read(1, masked=False))
+        assert np.array_equal(truth, out.read(1, masked=False))
 
 
 def test_rasterize_like_raster(tmpdir, runner, basic_feature, basic_image_2x2,
@@ -608,7 +608,7 @@ def test_rasterize_like_raster(tmpdir, runner, basic_feature, basic_image_2x2,
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.array_equal(basic_image_2x2, out.read(1, masked=False))
+        assert np.array_equal(basic_image_2x2, out.read(1, masked=False))
 
         with rasterio.open(pixelated_image_file) as src:
             assert out.crs == src.crs
@@ -683,10 +683,10 @@ def test_rasterize_property_value(tmpdir, runner, basic_feature):
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert numpy.allclose(out.bounds, (2, 2, 4.25, 4.25))
+        assert np.allclose(out.bounds, (2, 2, 4.25, 4.25))
         data = out.read(1, masked=False)
         assert data.shape == DEFAULT_SHAPE
-        assert numpy.all(data == basic_feature['properties']['val'])
+        assert np.all(data == basic_feature['properties']['val'])
 
 
 def test_rasterize_like_raster_outside_bounds(tmpdir, runner, basic_feature,
@@ -696,7 +696,7 @@ def test_rasterize_like_raster_outside_bounds(tmpdir, runner, basic_feature,
     in a blank image
     """
 
-    coords = numpy.array(basic_feature['geometry']['coordinates']) + 100
+    coords = np.array(basic_feature['geometry']['coordinates']) + 100
     basic_feature['geometry']['coordinates'] = coords.tolist()
 
     output = str(tmpdir.join('test.tif'))
@@ -710,7 +710,7 @@ def test_rasterize_like_raster_outside_bounds(tmpdir, runner, basic_feature,
     assert 'outside bounds' in result.output
     assert os.path.exists(output)
     with rasterio.open(output) as out:
-        assert not numpy.any(out.read(1, masked=False))
+        assert not np.any(out.read(1, masked=False))
 
 
 def test_rasterize_invalid_stdin(tmpdir, runner):
diff --git a/tests/test_rio_info.py b/tests/test_rio_info.py
index 2cbbc76..12b1dfd 100644
--- a/tests/test_rio_info.py
+++ b/tests/test_rio_info.py
@@ -65,7 +65,7 @@ def test_edit_crs_obj(data):
         edit, [inputfile, '--crs', '{"init": "epsg:32618"}'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
-        assert src.crs == {'init': 'epsg:32618'}
+        assert src.crs.to_dict() == {'init': 'epsg:32618'}
 
 
 def test_edit_transform_err_not_json(data):
@@ -132,7 +132,7 @@ class MockOption:
 def test_all_callback_pass(data):
     ctx = MockContext()
     ctx.obj['like'] = {'transform': 'foo'}
-    assert all_handler(ctx, None, None) == None
+    assert all_handler(ctx, None, None) is None
 
 
 def test_all_callback(data):
@@ -167,7 +167,7 @@ def test_transform_callback(data):
 
 
 def test_crs_callback_pass(data):
-    """Always return None if the value is None"""
+    """Always return None if the value is None."""
     ctx = MockContext()
     ctx.obj['like'] = {'crs': 'foo'}
     assert crs_handler(ctx, MockOption('crs'), None) is None
@@ -376,6 +376,13 @@ def test_info_stats_only():
     assert result.output.startswith('1.000000 255.000000 66.02')
 
 
+def test_info_colorinterp():
+    runner = CliRunner()
+    result = runner.invoke(main_group, ['info', 'tests/data/alpha.tif'])
+    assert result.exit_code == 0
+    assert '"colorinterp": ["red", "green", "blue", "alpha"]' in result.output
+
+
 def test_transform_err():
     runner = CliRunner()
     result = runner.invoke(main_group, [
@@ -591,6 +598,7 @@ def test_bounds_seq_rs():
     assert result.output == (
         '\x1e[-78.96, 23.56, -76.57, 25.55]\n\x1e[-78.96, 23.56, -76.57, 25.55]\n')
 
+
 def test_insp():
     runner = CliRunner()
     result = runner.invoke(main_group, ['insp', 'tests/data/RGB.byte.tif'])
diff --git a/tests/test_rio_merge.py b/tests/test_rio_merge.py
index ffd9442..d643bb9 100644
--- a/tests/test_rio_merge.py
+++ b/tests/test_rio_merge.py
@@ -2,7 +2,7 @@ import sys
 import os
 import logging
 import click
-import numpy
+import numpy as np
 from click.testing import CliRunner
 from pytest import fixture
 
@@ -29,12 +29,12 @@ def test_data_dir_1(tmpdir):
     }
 
     with rasterio.open(str(tmpdir.join('b.tif')), 'w', **kwargs) as dst:
-        data = numpy.ones((10, 10), dtype=rasterio.uint8)
+        data = np.ones((10, 10), dtype=rasterio.uint8)
         data[0:6, 0:6] = 255
         dst.write(data, indexes=1)
 
     with rasterio.open(str(tmpdir.join('a.tif')), 'w', **kwargs) as dst:
-        data = numpy.ones((10, 10), dtype=rasterio.uint8)
+        data = np.ones((10, 10), dtype=rasterio.uint8)
         data[4:8, 4:8] = 254
         dst.write(data, indexes=1)
 
@@ -55,18 +55,38 @@ def test_data_dir_2(tmpdir):
     }
 
     with rasterio.open(str(tmpdir.join('b.tif')), 'w', **kwargs) as dst:
-        data = numpy.zeros((10, 10), dtype=rasterio.uint8)
+        data = np.zeros((10, 10), dtype=rasterio.uint8)
         data[0:6, 0:6] = 255
         dst.write(data, indexes=1)
 
     with rasterio.open(str(tmpdir.join('a.tif')), 'w', **kwargs) as dst:
-        data = numpy.zeros((10, 10), dtype=rasterio.uint8)
+        data = np.zeros((10, 10), dtype=rasterio.uint8)
         data[4:8, 4:8] = 254
         dst.write(data, indexes=1)
 
     return tmpdir
 
 
+def test_merge_with_colormap(test_data_dir_1):
+    outputname = str(test_data_dir_1.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_1.listdir()]
+    inputs.sort()
+
+    # Add a colormap to the first input prior merge
+    with rasterio.open(inputs[0], 'r+') as src:
+        src.write_colormap(1, {0: (255, 0, 0, 255), 255: (0, 0, 0, 0)})
+
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(outputname) as out:
+        cmap = out.colormap(1)
+        assert cmap[0] == (255, 0, 0, 255)
+        assert cmap[255] == (0, 0, 0, 255)
+
+
 def test_merge_with_nodata(test_data_dir_1):
     outputname = str(test_data_dir_1.join('merged.tif'))
     inputs = [str(x) for x in test_data_dir_1.listdir()]
@@ -78,10 +98,10 @@ def test_merge_with_nodata(test_data_dir_1):
     with rasterio.open(outputname) as out:
         assert out.count == 1
         data = out.read(1, masked=False)
-        expected = numpy.ones((10, 10), dtype=rasterio.uint8)
+        expected = np.ones((10, 10), dtype=rasterio.uint8)
         expected[0:6, 0:6] = 255
         expected[4:8, 4:8] = 254
-        assert numpy.all(data == expected)
+        assert np.all(data == expected)
 
 
 def test_merge_warn(test_data_dir_1):
@@ -106,10 +126,10 @@ def test_merge_without_nodata(test_data_dir_2):
     with rasterio.open(outputname) as out:
         assert out.count == 1
         data = out.read(1, masked=False)
-        expected = numpy.zeros((10, 10), dtype=rasterio.uint8)
+        expected = np.zeros((10, 10), dtype=rasterio.uint8)
         expected[0:6, 0:6] = 255
         expected[4:8, 4:8] = 254
-        assert numpy.all(data == expected)
+        assert np.all(data == expected)
 
 
 def test_merge_output_exists(tmpdir):
@@ -181,12 +201,12 @@ def test_data_dir_overlapping(tmpdir):
     }
 
     with rasterio.open(str(tmpdir.join('se.tif')), 'w', **kwargs) as dst:
-        data = numpy.ones((10, 10), dtype=rasterio.uint8)
+        data = np.ones((10, 10), dtype=rasterio.uint8)
         dst.write(data, indexes=1)
 
     kwargs['transform'] = (-113, 0.2, 0, 45, 0, -0.2)
     with rasterio.open(str(tmpdir.join('nw.tif')), 'w', **kwargs) as dst:
-        data = numpy.ones((10, 10), dtype=rasterio.uint8) * 2
+        data = np.ones((10, 10), dtype=rasterio.uint8) * 2
         dst.write(data, indexes=1)
 
     return tmpdir
@@ -205,10 +225,10 @@ def test_merge_overlapping(test_data_dir_overlapping):
         assert out.shape == (15, 15)
         assert out.bounds == (-114, 43, -111, 46)
         data = out.read(1, masked=False)
-        expected = numpy.zeros((15, 15), dtype=rasterio.uint8)
+        expected = np.zeros((15, 15), dtype=rasterio.uint8)
         expected[0:10, 0:10] = 1
         expected[5:, 5:] = 2
-        assert numpy.all(data == expected)
+        assert np.all(data == expected)
 
 
 # Fixture to create test datasets within temporary directory
@@ -226,12 +246,12 @@ def test_data_dir_float(tmpdir):
     }
 
     with rasterio.open(str(tmpdir.join('two.tif')), 'w', **kwargs) as dst:
-        data = numpy.zeros((10, 10), dtype=rasterio.float64)
+        data = np.zeros((10, 10), dtype=rasterio.float64)
         data[0:6, 0:6] = 255
         dst.write(data, indexes=1)
 
     with rasterio.open(str(tmpdir.join('one.tif')), 'w', **kwargs) as dst:
-        data = numpy.zeros((10, 10), dtype=rasterio.float64)
+        data = np.zeros((10, 10), dtype=rasterio.float64)
         data[4:8, 4:8] = 254
         dst.write(data, indexes=1)
     return tmpdir
@@ -248,10 +268,10 @@ def test_merge_float(test_data_dir_float):
     with rasterio.open(outputname) as out:
         assert out.count == 1
         data = out.read(1, masked=False)
-        expected = numpy.ones((10, 10), dtype=rasterio.float64) * -1.5
+        expected = np.ones((10, 10), dtype=rasterio.float64) * -1.5
         expected[0:6, 0:6] = 255
         expected[4:8, 4:8] = 254
-        assert numpy.all(data == expected)
+        assert np.all(data == expected)
 
 
 # Test below comes from issue #288. There was an off-by-one error in
@@ -260,7 +280,7 @@ def test_merge_float(test_data_dir_float):
 @fixture(scope='function')
 def tiffs(tmpdir):
 
-    data = numpy.ones((1, 1, 1), 'uint8')
+    data = np.ones((1, 1, 1), 'uint8')
 
     kwargs = {
         'count': '1',
diff --git a/tests/test_rio_options.py b/tests/test_rio_options.py
index 43a52a4..023366c 100644
--- a/tests/test_rio_options.py
+++ b/tests/test_rio_options.py
@@ -28,9 +28,10 @@ def test_file_in_handler_no_vfs_nonexistent():
 
 def test_file_in_handler_no_vfs():
     """file path is expanded to abspath"""
+    from rasterio.rio.options import abspath_forward_slashes
     ctx = MockContext()
     retval = file_in_handler(ctx, 'INPUT', 'tests/data/RGB.byte.tif')
-    assert retval == os.path.abspath('tests/data/RGB.byte.tif')
+    assert retval == abspath_forward_slashes('tests/data/RGB.byte.tif')
 
 
 def test_file_in_handler_bad_scheme():
@@ -52,7 +53,6 @@ def test_file_in_handler_with_vfs():
     """vfs file path path is expanded"""
     ctx = MockContext()
     retval = file_in_handler(ctx, 'INPUT', 'zip://tests/data/files.zip!/inputs/RGB.byte.tif')
-    assert retval.startswith('zip:///')
     assert retval.endswith('tests/data/files.zip!/inputs/RGB.byte.tif')
 
 
diff --git a/tests/test_rio_warp.py b/tests/test_rio_warp.py
index 94e01fe..f7f14b1 100644
--- a/tests/test_rio_warp.py
+++ b/tests/test_rio_warp.py
@@ -2,11 +2,10 @@ import logging
 import os
 import sys
 
-import numpy
+import numpy as np
 import pytest
 
 import rasterio
-import rasterio.crs
 from rasterio.rio import warp
 from rasterio.rio.main import main_group
 
@@ -57,6 +56,60 @@ def test_dst_crs_error_epsg_2(runner, tmpdir):
     assert 'for dst_crs: EPSG codes are positive integers' in result.output
 
 
+def test_dst_nodata_float_no_src_nodata_err(runner, tmpdir):
+    """Valid integer destination nodata dtype"""
+    srcname = 'tests/data/float.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--dst-nodata', '0.0'])
+    assert result.exit_code == 2
+    assert 'src-nodata must be provided because dst-nodata is not None' in result.output
+
+
+def test_src_nodata_int_ok(runner, tmpdir):
+    """Check if input nodata is overridden"""
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--src-nodata', '1'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.meta['nodata'] == 1
+
+
+def test_dst_nodata_int_ok(runner, tmpdir):
+    """Check if input nodata is overridden"""
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--dst-nodata', '255'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.meta['nodata'] == 255
+
+
+def test_src_nodata_float_ok(runner, tmpdir):
+    """Check if input nodata is overridden"""
+    srcname = 'tests/data/float.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--src-nodata', '1.5'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.meta['nodata'] == 1.5
+
+
+def test_dst_nodata_float_override_src_ok(runner, tmpdir):
+    """Check if srcnodata is overridden"""
+    srcname = 'tests/data/float.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--src-nodata', '1.5', '--dst-nodata', '2.5'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.meta['nodata'] == 2.5
+
+
 def test_warp_no_reproject(runner, tmpdir):
     """ When called without parameters, output should be same as source """
     srcname = 'tests/data/shade.tif'
@@ -70,9 +123,9 @@ def test_warp_no_reproject(runner, tmpdir):
             assert output.count == src.count
             assert output.crs == src.crs
             assert output.nodata == src.nodata
-            assert numpy.allclose(output.bounds, src.bounds)
+            assert np.allclose(output.bounds, src.bounds)
             assert output.affine.almost_equals(src.affine)
-            assert numpy.allclose(output.read(1), src.read(1))
+            assert np.allclose(output.read(1), src.read(1))
 
 
 def test_warp_no_reproject_dimensions(runner, tmpdir):
@@ -88,7 +141,7 @@ def test_warp_no_reproject_dimensions(runner, tmpdir):
             assert output.crs == src.crs
             assert output.width == 100
             assert output.height == 100
-            assert numpy.allclose([97.839396, 97.839396],
+            assert np.allclose([97.839396, 97.839396],
                                   [output.affine.a, -output.affine.e])
 
 
@@ -103,7 +156,7 @@ def test_warp_no_reproject_res(runner, tmpdir):
     with rasterio.open(srcname) as src:
         with rasterio.open(outputname) as output:
             assert output.crs == src.crs
-            assert numpy.allclose([30, 30], [output.affine.a, -output.affine.e])
+            assert np.allclose([30, 30], [output.affine.a, -output.affine.e])
             assert output.width == 327
             assert output.height == 327
 
@@ -120,8 +173,8 @@ def test_warp_no_reproject_bounds(runner, tmpdir):
     with rasterio.open(srcname) as src:
         with rasterio.open(outputname) as output:
             assert output.crs == src.crs
-            assert numpy.allclose(output.bounds, out_bounds)
-            assert numpy.allclose([src.affine.a, src.affine.e],
+            assert np.allclose(output.bounds, out_bounds)
+            assert np.allclose([src.affine.a, src.affine.e],
                                   [output.affine.a, output.affine.e])
             assert output.width == 105
             assert output.height == 210
@@ -140,8 +193,8 @@ def test_warp_no_reproject_bounds_res(runner, tmpdir):
     with rasterio.open(srcname) as src:
         with rasterio.open(outputname) as output:
             assert output.crs == src.crs
-            assert numpy.allclose(output.bounds, out_bounds)
-            assert numpy.allclose([30, 30], [output.affine.a, -output.affine.e])
+            assert np.allclose(output.bounds, out_bounds)
+            assert np.allclose([30, 30], [output.affine.a, -output.affine.e])
             assert output.width == 34
             assert output.height == 67
 
@@ -160,7 +213,7 @@ def test_warp_reproject_dst_crs(runner, tmpdir):
             assert output.crs == {'init': 'epsg:4326'}
             assert output.width == 835
             assert output.height == 696
-            assert numpy.allclose(output.bounds,
+            assert np.allclose(output.bounds,
                                   [-78.95864996545055, 23.564787976164418,
                                    -76.5759177302349, 25.550873767433984])
 
@@ -189,7 +242,7 @@ def test_warp_reproject_res(runner, tmpdir):
 
     with rasterio.open(outputname) as output:
         assert output.crs == {'init': 'epsg:4326'}
-        assert numpy.allclose([0.01, 0.01], [output.affine.a, -output.affine.e])
+        assert np.allclose([0.01, 0.01], [output.affine.a, -output.affine.e])
         assert output.width == 9
         assert output.height == 7
 
@@ -208,7 +261,7 @@ def test_warp_reproject_dimensions(runner, tmpdir):
             assert output.crs == {'init': 'epsg:4326'}
             assert output.width == 100
             assert output.height == 100
-            assert numpy.allclose([0.0008789062498762235, 0.0006771676143921468],
+            assert np.allclose([0.0008789062498762235, 0.0006771676143921468],
                                   [output.affine.a, -output.affine.e])
 
 
@@ -271,9 +324,9 @@ def test_warp_reproject_src_bounds_res(runner, tmpdir):
     with rasterio.open(srcname) as src:
         with rasterio.open(outputname) as output:
             assert output.crs == {'init': 'epsg:4326'}
-            assert numpy.allclose(output.bounds[:],
+            assert np.allclose(output.bounds[:],
                                   [-106.45036, 39.6138, -106.44136, 39.6278])
-            assert numpy.allclose([0.001, 0.001],
+            assert np.allclose([0.001, 0.001],
                                   [output.affine.a, -output.affine.e])
             assert output.width == 9
             assert output.height == 14
@@ -293,15 +346,15 @@ def test_warp_reproject_dst_bounds(runner, tmpdir):
     with rasterio.open(srcname) as src:
         with rasterio.open(outputname) as output:
             assert output.crs == {'init': 'epsg:4326'}
-            assert numpy.allclose(output.bounds[0::3],
+            assert np.allclose(output.bounds[0::3],
                                   [-106.45036, 39.6278])
-            assert numpy.allclose([0.001, 0.001],
+            assert np.allclose([0.001, 0.001],
                                   [output.affine.a, -output.affine.e])
 
             # XXX: an extra row and column is produced in the dataset
             # because we're using ceil instead of floor internally.
             # Not necessarily a bug, but may change in the future.
-            assert numpy.allclose([output.bounds[2]-0.001, output.bounds[1]+0.001],
+            assert np.allclose([output.bounds[2]-0.001, output.bounds[1]+0.001],
                                   [-106.44136, 39.6138])
             assert output.width == 10
             assert output.height == 15
@@ -320,9 +373,9 @@ def test_warp_reproject_like(runner, tmpdir):
         "nodata": 0
     }
 
-    with rasterio.drivers():
+    with rasterio.Env():
         with rasterio.open(likename, 'w', **kwargs) as dst:
-            data = numpy.zeros((10, 10), dtype=rasterio.uint8)
+            data = np.zeros((10, 10), dtype=rasterio.uint8)
             dst.write(data, indexes=1)
 
     srcname = 'tests/data/shade.tif'
@@ -334,7 +387,7 @@ def test_warp_reproject_like(runner, tmpdir):
 
     with rasterio.open(outputname) as output:
         assert output.crs == {'init': 'epsg:4326'}
-        assert numpy.allclose([0.001, 0.001], [output.affine.a, -output.affine.e])
+        assert np.allclose([0.001, 0.001], [output.affine.a, -output.affine.e])
         assert output.width == 10
         assert output.height == 10
 
@@ -395,7 +448,6 @@ def test_warp_badcrs_src_bounds(runner, tmpdir):
     assert "Invalid value for dst_crs" in result.output
 
 
- at pytest.mark.xfail
 def test_warp_reproject_check_invert(runner, tmpdir):
     srcname = 'tests/data/world.rgb.tif'
     outputname = str(tmpdir.join('test.tif'))
diff --git a/tests/test_tool.py b/tests/test_tool.py
index da5acdc..c0f8471 100644
--- a/tests/test_tool.py
+++ b/tests/test_tool.py
@@ -1,7 +1,11 @@
 import numpy as np
+import pytest
 
 try:
+    import matplotlib as mpl
+    mpl.use('agg')
     import matplotlib.pyplot as plt
+    plt.show = lambda :None
 except ImportError:
     plt = None
 
@@ -19,7 +23,8 @@ def test_stats():
         results2 = stats(src.read(1))
         assert np.allclose(np.array(results), np.array(results2))
 
-
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
 def test_show_raster():
     """
     This test only verifies that code up to the point of plotting with
@@ -33,7 +38,8 @@ def test_show_raster():
         except ImportError:
             pass
 
-
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
 def test_show_raster_no_bounds():
     """
     This test only verifies that code up to the point of plotting with
@@ -47,7 +53,8 @@ def test_show_raster_no_bounds():
         except ImportError:
             pass
 
-
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
 def test_show_array():
     """
     This test only verifies that code up to the point of plotting with
@@ -61,7 +68,8 @@ def test_show_array():
         except ImportError:
             pass
 
-
+ at pytest.mark.skipif(plt is None,
+                    reason="requires matplotlib")
 def test_show_hist():
     """
     This test only verifies that code up to the point of plotting with
diff --git a/tests/test_update.py b/tests/test_update.py
index 1e05b36..4d7aa19 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -4,7 +4,7 @@ import subprocess
 import re
 
 import affine
-import numpy
+import numpy as np
 import pytest
 
 import rasterio
@@ -25,7 +25,7 @@ def test_update_tags(data):
 def test_update_band(data):
     tiffname = str(data.join('RGB.byte.tif'))
     with rasterio.open(tiffname, 'r+') as f:
-        f.write(numpy.zeros(f.shape, dtype=f.dtypes[0]), indexes=1)
+        f.write(np.zeros(f.shape, dtype=f.dtypes[0]), indexes=1)
     with rasterio.open(tiffname) as f:
         assert not f.read(1).any()
 
diff --git a/tests/test_warp.py b/tests/test_warp.py
index 8336d60..5dff688 100644
--- a/tests/test_warp.py
+++ b/tests/test_warp.py
@@ -3,11 +3,11 @@ import logging
 import sys
 import pytest
 from affine import Affine
-import numpy
+import numpy as np
+from packaging.version import parse
 
 import rasterio
 from rasterio.enums import Resampling
-from rasterio.env import Env
 from rasterio.warp import (
     reproject, transform_geom, transform, transform_bounds,
     calculate_default_transform)
@@ -19,8 +19,30 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 DST_TRANSFORM = Affine.from_gdal(-8789636.708, 300.0, 0.0, 2943560.235, 0.0, -300.0)
 
 
+def supported_resampling(method):
+    if method == Resampling.gauss:
+        return False
+    gdal110plus_only = (
+        Resampling.mode, Resampling.average)
+    gdal2plus_only = (
+        Resampling.max, Resampling.min, Resampling.med,
+        Resampling.q1, Resampling.q3)
+    version = parse(rasterio.__gdal_version__)
+    if version < parse('1.10'):
+        return method not in gdal2plus_only and method not in gdal110plus_only
+    if version < parse('2.0'):
+        return method not in gdal2plus_only
+    return True
+
+
+reproj_expected = (
+    ({'CHECK_WITH_INVERT_PROJ': False}, 6215),
+    ({'CHECK_WITH_INVERT_PROJ': True}, 4005))
+
+
 class ReprojectParams(object):
-    """ Class to assist testing reprojection by encapsulating parameters """
+    """Class to assist testing reprojection by encapsulating parameters."""
+
     def __init__(self, left, bottom, right, top, width, height, src_crs,
                  dst_crs):
         self.width = width
@@ -30,7 +52,7 @@ class ReprojectParams(object):
         self.src_crs = src_crs
         self.dst_crs = dst_crs
 
-        with Env():
+        with rasterio.Env():
             dt, dw, dh = calculate_default_transform(
                 src_crs, dst_crs, width, height, left, bottom, right, top)
             self.dst_transform = dt
@@ -51,25 +73,25 @@ def default_reproject_params():
 
 
 def test_transform():
-    """2D and 3D"""
+    """2D and 3D."""
     WGS84_crs = {'init': 'EPSG:4326'}
     WGS84_points = ([12.492269], [41.890169], [48.])
     ECEF_crs = {'init': 'EPSG:4978'}
     ECEF_points = ([4642610.], [1028584.], [4236562.])
     ECEF_result = transform(WGS84_crs, ECEF_crs, *WGS84_points)
-    assert numpy.allclose(numpy.array(ECEF_result), numpy.array(ECEF_points))
+    assert np.allclose(np.array(ECEF_result), np.array(ECEF_points))
 
     UTM33_crs = {'init': 'EPSG:32633'}
     UTM33_points = ([291952], [4640623])
     UTM33_result = transform(WGS84_crs, UTM33_crs, *WGS84_points[:2])
-    assert numpy.allclose(numpy.array(UTM33_result), numpy.array(UTM33_points))
+    assert np.allclose(np.array(UTM33_result), np.array(UTM33_points))
 
 
 def test_transform_bounds():
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             l, b, r, t = src.bounds
-            assert numpy.allclose(
+            assert np.allclose(
                 transform_bounds(src.crs, {'init': 'EPSG:4326'}, l, b, r, t),
                 (
                     -78.95864996545055, 23.564991210854686,
@@ -83,7 +105,7 @@ def test_transform_bounds_densify():
     # a different result than otherwise
     src_crs = {'init': 'EPSG:4326'}
     dst_crs = {'init': 'EPSG:32610'}
-    assert numpy.allclose(
+    assert np.allclose(
         transform_bounds(
             src_crs,
             dst_crs,
@@ -96,7 +118,7 @@ def test_transform_bounds_densify():
         )
     )
 
-    assert numpy.allclose(
+    assert np.allclose(
         transform_bounds(
             src_crs,
             dst_crs,
@@ -111,11 +133,11 @@ def test_transform_bounds_densify():
 
 
 def test_transform_bounds_no_change():
-    """ Make sure that going from and to the same crs causes no change """
-    with Env():
+    """Make sure that going from and to the same crs causes no change."""
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             l, b, r, t = src.bounds
-            assert numpy.allclose(
+            assert np.allclose(
                 transform_bounds(src.crs, src.crs, l, b, r, t),
                 src.bounds
             )
@@ -136,7 +158,7 @@ def test_calculate_default_transform():
         0.0028535715391804096, 0.0, -78.95864996545055,
         0.0, -0.0028535715391804096, 25.550873767433984)
 
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             wgs84_crs = {'init': 'EPSG:4326'}
             dst_transform, width, height = calculate_default_transform(
@@ -148,7 +170,7 @@ def test_calculate_default_transform():
 
 
 def test_calculate_default_transform_single_resolution():
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             target_resolution = 0.1
             target_transform = Affine(
@@ -166,7 +188,7 @@ def test_calculate_default_transform_single_resolution():
 
 
 def test_calculate_default_transform_multiple_resolutions():
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             target_resolution = (0.2, 0.1)
             target_transform = Affine(
@@ -185,7 +207,7 @@ def test_calculate_default_transform_multiple_resolutions():
 
 
 def test_reproject_ndarray():
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             source = src.read(1)
 
@@ -202,7 +224,7 @@ def test_reproject_ndarray():
             nadgrids='@null',
             wktext=True,
             no_defs=True)
-        out = numpy.empty(src.shape, dtype=numpy.uint8)
+        out = np.empty(src.shape, dtype=np.uint8)
         reproject(
             source,
             out,
@@ -215,12 +237,12 @@ def test_reproject_ndarray():
 
 
 def test_reproject_epsg():
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             source = src.read(1)
 
         dst_crs = {'init': 'EPSG:3857'}
-        out = numpy.empty(src.shape, dtype=numpy.uint8)
+        out = np.empty(src.shape, dtype=np.uint8)
         reproject(
             source,
             out,
@@ -233,13 +255,16 @@ def test_reproject_epsg():
 
 
 def test_reproject_out_of_bounds():
-    # using EPSG code not appropriate for the transform should return blank image
-    with Env():
+    """Using EPSG code is not appropriate for the transform.
+
+    Should return blank image.
+    """
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             source = src.read(1)
 
         dst_crs = {'init': 'EPSG:32619'}
-        out = numpy.empty(src.shape, dtype=numpy.uint8)
+        out = np.empty(src.shape, dtype=np.uint8)
         reproject(
             source,
             out,
@@ -251,13 +276,14 @@ def test_reproject_out_of_bounds():
         assert not out.any()
 
 
-def test_reproject_nodata():
+ at pytest.mark.parametrize("options, expected", reproj_expected)
+def test_reproject_nodata(options, expected):
     params = default_reproject_params()
     nodata = 215
 
-    with Env():
-        source = numpy.ones((params.width, params.height), dtype=numpy.uint8)
-        out = numpy.zeros((params.dst_width, params.dst_height),
+    with rasterio.Env(**options):
+        source = np.ones((params.width, params.height), dtype=np.uint8)
+        out = np.zeros((params.dst_width, params.dst_height),
                           dtype=source.dtype)
         out.fill(120)  # Fill with arbitrary value
 
@@ -272,17 +298,18 @@ def test_reproject_nodata():
             dst_nodata=nodata
         )
 
-        assert (out == 1).sum() == 6215
+        assert (out == 1).sum() == expected
         assert (out == nodata).sum() == (params.dst_width *
-                                         params.dst_height - 6215)
+                                         params.dst_height - expected)
 
 
-def test_reproject_nodata_nan():
+ at pytest.mark.parametrize("options, expected", reproj_expected)
+def test_reproject_nodata_nan(options, expected):
     params = default_reproject_params()
 
-    with Env():
-        source = numpy.ones((params.width, params.height), dtype=numpy.float32)
-        out = numpy.zeros((params.dst_width, params.dst_height),
+    with rasterio.Env(**options):
+        source = np.ones((params.width, params.height), dtype=np.float32)
+        out = np.zeros((params.dst_width, params.dst_height),
                           dtype=source.dtype)
         out.fill(120)  # Fill with arbitrary value
 
@@ -291,28 +318,25 @@ def test_reproject_nodata_nan():
             out,
             src_transform=params.src_transform,
             src_crs=params.src_crs,
-            src_nodata=numpy.nan,
+            src_nodata=np.nan,
             dst_transform=params.dst_transform,
             dst_crs=params.dst_crs,
-            dst_nodata=numpy.nan
+            dst_nodata=np.nan
         )
 
-        assert (out == 1).sum() == 6215
-        assert numpy.isnan(out).sum() == (params.dst_width *
-                                          params.dst_height - 6215)
-
+        assert (out == 1).sum() == expected
+        assert np.isnan(out).sum() == (params.dst_width *
+                                       params.dst_height - expected)
 
-def test_reproject_dst_nodata_default():
-    """
-    If nodata is not provided, destination will be filled with 0
-    instead of nodata
-    """
 
+ at pytest.mark.parametrize("options, expected", reproj_expected)
+def test_reproject_dst_nodata_default(options, expected):
+    """If nodata is not provided, destination will be filled with 0."""
     params = default_reproject_params()
 
-    with Env():
-        source = numpy.ones((params.width, params.height), dtype=numpy.uint8)
-        out = numpy.zeros((params.dst_width, params.dst_height),
+    with rasterio.Env(**options):
+        source = np.ones((params.width, params.height), dtype=np.uint8)
+        out = np.zeros((params.dst_width, params.dst_height),
                           dtype=source.dtype)
         out.fill(120)  # Fill with arbitrary value
 
@@ -325,17 +349,17 @@ def test_reproject_dst_nodata_default():
             dst_crs=params.dst_crs
         )
 
-        assert (out == 1).sum() == 6215
+        assert (out == 1).sum() == expected
         assert (out == 0).sum() == (params.dst_width *
-                                    params.dst_height - 6215)
+                                    params.dst_height - expected)
 
 
 def test_reproject_invalid_dst_nodata():
-    """ dst_nodata must be in value range of data type """
+    """dst_nodata must be in value range of data type."""
     params = default_reproject_params()
 
-    with Env():
-        source = numpy.ones((params.width, params.height), dtype=numpy.uint8)
+    with rasterio.Env():
+        source = np.ones((params.width, params.height), dtype=np.uint8)
         out = source.copy()
 
         with pytest.raises(ValueError):
@@ -352,11 +376,11 @@ def test_reproject_invalid_dst_nodata():
 
 
 def test_reproject_missing_src_nodata():
-    """ src_nodata is required if dst_nodata is not None """
+    """src_nodata is required if dst_nodata is not None."""
     params = default_reproject_params()
 
-    with Env():
-        source = numpy.ones((params.width, params.height), dtype=numpy.uint8)
+    with rasterio.Env():
+        source = np.ones((params.width, params.height), dtype=np.uint8)
         out = source.copy()
 
         with pytest.raises(ValueError):
@@ -372,11 +396,11 @@ def test_reproject_missing_src_nodata():
 
 
 def test_reproject_invalid_src_nodata():
-    """ src_nodata must be in range for data type """
+    """src_nodata must be in range for data type."""
     params = default_reproject_params()
 
-    with Env():
-        source = numpy.ones((params.width, params.height), dtype=numpy.uint8)
+    with rasterio.Env():
+        source = np.ones((params.width, params.height), dtype=np.uint8)
         out = source.copy()
 
         with pytest.raises(ValueError):
@@ -393,8 +417,8 @@ def test_reproject_invalid_src_nodata():
 
 
 def test_reproject_multi():
-    """Ndarry to ndarray"""
-    with Env():
+    """Ndarry to ndarray."""
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             source = src.read()
         dst_crs = dict(
@@ -410,7 +434,7 @@ def test_reproject_multi():
             nadgrids='@null',
             wktext=True,
             no_defs=True)
-        destin = numpy.empty(source.shape, dtype=numpy.uint8)
+        destin = np.empty(source.shape, dtype=np.uint8)
         reproject(
             source,
             destin,
@@ -423,7 +447,7 @@ def test_reproject_multi():
 
 
 def test_warp_from_file():
-    """File to ndarray"""
+    """File to ndarray."""
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         dst_crs = dict(
             proj='merc',
@@ -438,7 +462,7 @@ def test_warp_from_file():
             nadgrids='@null',
             wktext=True,
             no_defs=True)
-        destin = numpy.empty(src.shape, dtype=numpy.uint8)
+        destin = np.empty(src.shape, dtype=np.uint8)
         reproject(
             rasterio.band(src, 1),
             destin,
@@ -448,7 +472,7 @@ def test_warp_from_file():
 
 
 def test_warp_from_to_file(tmpdir):
-    """File to file"""
+    """File to file."""
     tiffname = str(tmpdir.join('foo.tif'))
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         dst_crs = dict(
@@ -474,7 +498,7 @@ def test_warp_from_to_file(tmpdir):
 
 
 def test_warp_from_to_file_multi(tmpdir):
-    """File to file"""
+    """File to file."""
     tiffname = str(tmpdir.join('foo.tif'))
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         dst_crs = dict(
@@ -561,13 +585,13 @@ def test_transform_geom():
 
 
 def test_reproject_unsupported_resampling():
-    """Values not in enums.Resampling are not supported."""
-    with Env():
+    """Values not in enums. Resampling are not supported."""
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             source = src.read(1)
 
         dst_crs = {'init': 'EPSG:32619'}
-        out = numpy.empty(src.shape, dtype=numpy.uint8)
+        out = np.empty(src.shape, dtype=np.uint8)
         with pytest.raises(ValueError):
             reproject(
                 source,
@@ -581,12 +605,12 @@ def test_reproject_unsupported_resampling():
 
 def test_reproject_unsupported_resampling_guass():
     """Resampling.gauss is unsupported."""
-    with Env():
+    with rasterio.Env():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             source = src.read(1)
 
         dst_crs = {'init': 'EPSG:32619'}
-        out = numpy.empty(src.shape, dtype=numpy.uint8)
+        out = np.empty(src.shape, dtype=np.uint8)
         with pytest.raises(ValueError):
             reproject(
                 source,
@@ -596,3 +620,137 @@ def test_reproject_unsupported_resampling_guass():
                 dst_transform=DST_TRANSFORM,
                 dst_crs=dst_crs,
                 resampling=Resampling.gauss)
+
+
+ at pytest.mark.parametrize("method", Resampling)
+def test_resample_default_invert_proj(method):
+    """Nearest and bilinear should produce valid results
+    with the default Env
+    """
+    if not supported_resampling(method):
+        pytest.skip()
+
+    with rasterio.Env():
+        with rasterio.open('tests/data/world.rgb.tif') as src:
+            source = src.read(1)
+            profile = src.profile.copy()
+
+        dst_crs = {'init': 'EPSG:32619'}
+
+        # Calculate the ideal dimensions and transformation in the new crs
+        dst_affine, dst_width, dst_height = calculate_default_transform(
+            src.crs, dst_crs, src.width, src.height, *src.bounds)
+
+        profile['height'] = dst_height
+        profile['width'] = dst_width
+
+        out = np.empty(shape=(dst_height, dst_width), dtype=np.uint8)
+
+        out = np.empty(src.shape, dtype=np.uint8)
+        reproject(
+            source,
+            out,
+            src_transform=src.transform,
+            src_crs=src.crs,
+            dst_transform=dst_affine,
+            dst_crs=dst_crs,
+            resampling=method)
+
+        assert out.mean() > 0
+
+
+ at pytest.mark.xfail()
+ at pytest.mark.parametrize("method", Resampling)
+def test_resample_no_invert_proj(method):
+    """Nearest and bilinear should produce valid results with
+    CHECK_WITH_INVERT_PROJ = False
+    """
+    if not supported_resampling(method):
+        pytest.skip()
+
+    with rasterio.Env(CHECK_WITH_INVERT_PROJ=False):
+        with rasterio.open('tests/data/world.rgb.tif') as src:
+            source = src.read(1)
+            profile = src.profile.copy()
+
+        dst_crs = {'init': 'EPSG:32619'}
+
+        # Calculate the ideal dimensions and transformation in the new crs
+        dst_affine, dst_width, dst_height = calculate_default_transform(
+            src.crs, dst_crs, src.width, src.height, *src.bounds)
+
+        profile['height'] = dst_height
+        profile['width'] = dst_width
+
+        out = np.empty(shape=(dst_height, dst_width), dtype=np.uint8)
+
+        # see #614, some resamplin methods succeed but produce blank images
+        out = np.empty(src.shape, dtype=np.uint8)
+        reproject(
+            source,
+            out,
+            src_transform=src.transform,
+            src_crs=src.crs,
+            dst_transform=dst_affine,
+            dst_crs=dst_crs,
+            resampling=method)
+
+        assert out.mean() > 0
+
+
+def test_reproject_crs_none():
+    """Reproject with crs is None should not cause segfault"""
+    src = np.random.random(25).reshape((1, 5, 5))
+    srcaff = Affine(1.1, 0.0, 0.0, 0.0, 1.1, 0.0)
+    srccrs = None
+    dst = np.empty(shape=(1, 11, 11))
+    dstaff = Affine(0.5, 0.0, 0.0, 0.0, 0.5, 0.0)
+    dstcrs = None
+
+    with rasterio.Env():
+        reproject(
+            src, dst,
+            src_transform=srcaff,
+            src_crs=srccrs,
+            dst_transform=dstaff,
+            dst_crs=dstcrs,
+            resampling=Resampling.nearest)
+
+
+def test_reproject_identity():
+    """Reproject with an identity matrix."""
+    # note the affines are both positive e, src is identity
+    src = np.random.random(25).reshape((1, 5, 5))
+    srcaff = Affine(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)  # Identity
+    srccrs = {'init': 'epsg:3857'}
+
+    dst = np.empty(shape=(1, 10, 10))
+    dstaff = Affine(0.5, 0.0, 0.0, 0.0, 0.5, 0.0)
+    dstcrs = {'init': 'epsg:3857'}
+
+    with rasterio.Env():
+        reproject(
+            src, dst,
+            src_transform=srcaff,
+            src_crs=srccrs,
+            dst_transform=dstaff,
+            dst_crs=dstcrs,
+            resampling=Resampling.nearest)
+
+    # note the affines are both positive e, dst is identity
+    src = np.random.random(100).reshape((1, 10, 10))
+    srcaff = Affine(0.5, 0.0, 0.0, 0.0, 0.5, 0.0)
+    srccrs = {'init': 'epsg:3857'}
+
+    dst = np.empty(shape=(1, 5, 5))
+    dstaff = Affine(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)  # Identity
+    dstcrs = {'init': 'epsg:3857'}
+
+    with rasterio.Env():
+        reproject(
+            src, dst,
+            src_transform=srcaff,
+            src_crs=srccrs,
+            dst_transform=dstaff,
+            dst_crs=dstcrs,
+            resampling=Resampling.nearest)
diff --git a/tests/test_warp_transform.py b/tests/test_warp_transform.py
index d8daf84..3a53b7c 100644
--- a/tests/test_warp_transform.py
+++ b/tests/test_warp_transform.py
@@ -4,16 +4,14 @@ import pytest
 
 import rasterio
 from rasterio._warp import _calculate_default_transform
-from rasterio.env import Env
 from rasterio.errors import CRSError
-from rasterio.transform import Affine, from_bounds
+from rasterio.transform import from_bounds
 from rasterio.warp import transform_bounds
 
 
 def test_identity():
     """Get the same transform and dimensions back for same crs."""
     # Tile: [53, 96, 8]
-    # [-11740727.544603072, 4852834.0517692715, -11584184.510675032, 5009377.085697309]
     src_crs = dst_crs = 'EPSG:3857'
     width = height = 1000
     left, bottom, right, top = (
@@ -21,7 +19,7 @@ def test_identity():
         5009377.085697309)
     transform = from_bounds(left, bottom, right, top, width, height)
 
-    with Env():
+    with rasterio.Env():
         res_transform, res_width, res_height = _calculate_default_transform(
             src_crs, dst_crs, width, height, left, bottom, right, top)
 
@@ -33,7 +31,7 @@ def test_identity():
 
 def test_transform_bounds():
     """CRSError is raised."""
-    with Env():
+    with rasterio.Env():
         left, bottom, right, top = (
             -11740727.544603072, 4852834.0517692715, -11584184.510675032,
             5009377.085697309)
@@ -44,7 +42,7 @@ def test_transform_bounds():
 
 
 def test_gdal_transform_notnull():
-    with Env():
+    with rasterio.Env():
         dt, dw, dh = _calculate_default_transform(
             src_crs={'init': 'EPSG:4326'},
             dst_crs={'init': 'EPSG:32610'},
@@ -58,7 +56,7 @@ def test_gdal_transform_notnull():
 
 
 def test_gdal_transform_fail_dst_crs():
-    with Env():
+    with rasterio.Env():
         dt, dw, dh = _calculate_default_transform(
             {'init': 'EPSG:4326'},
             '+proj=foobar',
@@ -69,8 +67,9 @@ def test_gdal_transform_fail_dst_crs():
             right=-80,
             top=70)
 
+
 def test_gdal_transform_fail_src_crs():
-    with Env():
+    with rasterio.Env():
         dt, dw, dh = _calculate_default_transform(
             '+proj=foobar',
             {'init': 'EPSG:32610'},
@@ -85,8 +84,8 @@ def test_gdal_transform_fail_src_crs():
 @pytest.mark.xfail(
     os.environ.get('GDALVERSION', 'a.b.c').startswith('1.9'),
                    reason="GDAL 1.9 doesn't catch this error")
-def test_gdal_transform_fail_src_crs():
-    with Env():
+def test_gdal_transform_fail_dst_crs_xfail():
+    with rasterio.Env():
         with pytest.raises(CRSError):
             dt, dw, dh = _calculate_default_transform(
                 {'init': 'EPSG:4326'},
diff --git a/tests/test_write.py b/tests/test_write.py
index d0e543e..081be9e 100644
--- a/tests/test_write.py
+++ b/tests/test_write.py
@@ -3,10 +3,12 @@ import re
 import subprocess
 import sys
 
-import numpy
+import numpy as np
 import pytest
 
 import rasterio
+from rasterio.errors import RasterioIOError
+from rasterio._io import BAD_WRITE_DRIVERS
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
@@ -42,7 +44,7 @@ def test_no_crs(tmpdir):
     with rasterio.open(
             name, 'w', driver='GTiff', width=100, height=100, count=1,
             dtype=rasterio.uint8) as dst:
-        dst.write(numpy.ones((100, 100), dtype=rasterio.uint8), indexes=1)
+        dst.write(np.ones((100, 100), dtype=rasterio.uint8), indexes=1)
 
 def test_context(tmpdir):
     name = str(tmpdir.join("test_context.tif"))
@@ -73,7 +75,7 @@ def test_context(tmpdir):
 
 def test_write_ubyte(tmpdir):
     name = str(tmpdir.mkdir("sub").join("test_write_ubyte.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     with rasterio.open(
             name, 'w',
             driver='GTiff', width=100, height=100, count=1,
@@ -83,7 +85,7 @@ def test_write_ubyte(tmpdir):
     assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
 def test_write_ubyte_multi(tmpdir):
     name = str(tmpdir.mkdir("sub").join("test_write_ubyte_multi.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     with rasterio.open(
             name, 'w',
             driver='GTiff', width=100, height=100, count=1,
@@ -93,7 +95,7 @@ def test_write_ubyte_multi(tmpdir):
     assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
 def test_write_ubyte_multi_list(tmpdir):
     name = str(tmpdir.mkdir("sub").join("test_write_ubyte_multi_list.tif"))
-    a = numpy.array([numpy.ones((100, 100), dtype=rasterio.ubyte) * 127])
+    a = np.array([np.ones((100, 100), dtype=rasterio.ubyte) * 127])
     with rasterio.open(
             name, 'w',
             driver='GTiff', width=100, height=100, count=1,
@@ -103,7 +105,7 @@ def test_write_ubyte_multi_list(tmpdir):
     assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
 def test_write_ubyte_multi_3(tmpdir):
     name = str(tmpdir.mkdir("sub").join("test_write_ubyte_multi_list.tif"))
-    arr = numpy.array(3 * [numpy.ones((100, 100), dtype=rasterio.ubyte) * 127])
+    arr = np.array(3 * [np.ones((100, 100), dtype=rasterio.ubyte) * 127])
     with rasterio.open(
             name, 'w',
             driver='GTiff', width=100, height=100, count=3,
@@ -114,7 +116,7 @@ def test_write_ubyte_multi_3(tmpdir):
 
 def test_write_float(tmpdir):
     name = str(tmpdir.join("test_write_float.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.float32) * 42.0
+    a = np.ones((100, 100), dtype=rasterio.float32) * 42.0
     with rasterio.open(
             name, 'w',
             driver='GTiff', width=100, height=100, count=2,
@@ -127,7 +129,7 @@ def test_write_float(tmpdir):
 
 def test_write_crs_transform(tmpdir):
     name = str(tmpdir.join("test_write_crs_transform.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     transform = [101985.0, 300.0379266750948, 0.0,
                  2826915.0, 0.0, -300.041782729805]
     with rasterio.open(
@@ -138,7 +140,7 @@ def test_write_crs_transform(tmpdir):
             transform=transform,
             dtype=rasterio.ubyte) as s:
         s.write(a, indexes=1)
-    assert s.crs == {'init': 'epsg:32618'}
+    assert s.crs.to_dict() == {'init': 'epsg:32618'}
     info = subprocess.check_output(["gdalinfo", name]).decode('utf-8')
     assert 'PROJCS["UTM Zone 18, Northern Hemisphere",' in info
     # make sure that pixel size is nearly the same as transform
@@ -147,7 +149,7 @@ def test_write_crs_transform(tmpdir):
 
 def test_write_crs_transform_affine(tmpdir):
     name = str(tmpdir.join("test_write_crs_transform.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     transform = [101985.0, 300.0379266750948, 0.0,
                  2826915.0, 0.0, -300.041782729805]
     with rasterio.open(
@@ -158,7 +160,7 @@ def test_write_crs_transform_affine(tmpdir):
             affine=transform,
             dtype=rasterio.ubyte) as s:
         s.write(a, indexes=1)
-    assert s.crs == {'init': 'epsg:32618'}
+    assert s.crs.to_dict() == {'init': 'epsg:32618'}
     info = subprocess.check_output(["gdalinfo", name]).decode('utf-8')
     assert 'PROJCS["UTM Zone 18, Northern Hemisphere",' in info
     # make sure that pixel size is nearly the same as transform
@@ -168,7 +170,7 @@ def test_write_crs_transform_affine(tmpdir):
 def test_write_crs_transform_2(tmpdir):
     """Using 'EPSG:32618' as CRS."""
     name = str(tmpdir.join("test_write_crs_transform.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     transform = [101985.0, 300.0379266750948, 0.0,
                  2826915.0, 0.0, -300.041782729805]
     with rasterio.open(
@@ -178,7 +180,7 @@ def test_write_crs_transform_2(tmpdir):
             transform=transform,
             dtype=rasterio.ubyte) as s:
         s.write(a, indexes=1)
-    assert s.crs == {'init': 'epsg:32618'}
+    assert s.crs.to_dict() == {'init': 'epsg:32618'}
     info = subprocess.check_output(["gdalinfo", name]).decode('utf-8')
     assert 'PROJCS["WGS 84 / UTM zone 18N",' in info
     # make sure that pixel size is nearly the same as transform
@@ -188,18 +190,18 @@ def test_write_crs_transform_2(tmpdir):
 def test_write_crs_transform_3(tmpdir):
     """Using WKT as CRS."""
     name = str(tmpdir.join("test_write_crs_transform.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     transform = [101985.0, 300.0379266750948, 0.0,
                  2826915.0, 0.0, -300.041782729805]
-    crs_wkt = 'PROJCS["UTM Zone 18, Northern Hemisphere",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]'
+    wkt = 'PROJCS["UTM Zone 18, Northern Hemisphere",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]'
     with rasterio.open(
             name, 'w',
             driver='GTiff', width=100, height=100, count=1,
-            crs=crs_wkt,
+            crs=wkt,
             transform=transform,
             dtype=rasterio.ubyte) as s:
         s.write(a, indexes=1)
-    assert s.crs == {'init': 'epsg:32618'}
+    assert s.crs.to_dict() == {'init': 'epsg:32618'}
     info = subprocess.check_output(["gdalinfo", name]).decode('utf-8')
     assert 'PROJCS["UTM Zone 18, Northern Hemisphere",' in info
     # make sure that pixel size is nearly the same as transform
@@ -208,7 +210,7 @@ def test_write_crs_transform_3(tmpdir):
 
 def test_write_meta(tmpdir):
     name = str(tmpdir.join("test_write_meta.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     meta = dict(driver='GTiff', width=100, height=100, count=1)
     with rasterio.open(name, 'w', dtype=a.dtype, **meta) as s:
         s.write(a, indexes=1)
@@ -217,7 +219,7 @@ def test_write_meta(tmpdir):
 
 def test_write_nodata(tmpdir):
     name = str(tmpdir.join("test_write_nodata.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     with rasterio.open(
             name, 'w',
             driver='GTiff', width=100, height=100, count=2,
@@ -230,7 +232,7 @@ def test_write_nodata(tmpdir):
 
 def test_guard_nodata(tmpdir):
     name = str(tmpdir.join("test_guard_nodata.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     with pytest.raises(ValueError):
         rasterio.open(
             name, 'w',
@@ -240,7 +242,7 @@ def test_guard_nodata(tmpdir):
 
 def test_write_lzw(tmpdir):
     name = str(tmpdir.join("test_write_lzw.tif"))
-    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    a = np.ones((100, 100), dtype=rasterio.ubyte) * 127
     with rasterio.open(
             name, 'w',
             driver='GTiff',
@@ -259,9 +261,9 @@ def test_write_noncontiguous(tmpdir):
     BANDS = 6
     # Create a 3-D random int array (rows, columns, bands)
     total = ROWS * COLS * BANDS
-    arr = numpy.random.randint(
+    arr = np.random.randint(
         0, 10, size=total).reshape(
-            (ROWS, COLS, BANDS), order='F').astype(numpy.int32)
+            (ROWS, COLS, BANDS), order='F').astype(np.int32)
     kwargs = {
         'driver': 'GTiff',
         'width': COLS,
@@ -272,3 +274,13 @@ def test_write_noncontiguous(tmpdir):
     with rasterio.open(name, 'w', **kwargs) as dst:
         for i in range(BANDS):
             dst.write(arr[:, :, i], indexes=i + 1)
+
+
+ at pytest.mark.parametrize("driver", BAD_WRITE_DRIVERS)
+def test_write_blacklist(tmpdir, driver):
+    name = str(tmpdir.join("data.test"))
+    with pytest.raises(RasterioIOError) as exc_info:
+        rasterio.open(name, 'w', driver=driver, width=100, height=100,
+                      count=1, dtype='uint8')
+    exc = str(exc_info.value)
+    assert "Rasterio does not support writing with {} driver".format(driver) in exc

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



More information about the Pkg-grass-devel mailing list