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