pax_global_header00006660000000000000000000000064126346333620014522gustar00rootroot0000000000000052 comment=f0e247dbea09c6217463e50ae041ec06d86ec7f5 cligj-0.4.0/000077500000000000000000000000001263463336200126135ustar00rootroot00000000000000cligj-0.4.0/.gitignore000077500000000000000000000012771263463336200146150ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ venv/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # PyCharm IDE .idea/ cligj-0.4.0/.travis.yml000077500000000000000000000010171263463336200147260ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.3" - "3.4" - "3.5" install: - "pip install coveralls" - "pip install -e .[test]" script: - py.test --cov cligj --cov-report term-missing after_success: - coveralls deploy: on: tags: true provider: pypi distributions: "sdist bdist_wheel" user: seang password: secure: "dB3c7Ha9wYvdbpFn/FOb1sIDI0N/qvU5RKRFSLMsgp7rPuD0Vt4T8GMMWeiN+NmbgubAbe1sFhUUzXjh6Y7y/5Lolbd7lUTJLp4G+8v27ES6/9rVjMOZwoJFeRLOzF9Sl/ZONPo7zyI/fQS7x1BXfVaJKUhnSassyPABDU9dxw8=" cligj-0.4.0/CHANGES.txt000066400000000000000000000013671263463336200144330ustar00rootroot000000000000000.4.0 (2015-12-17) ------------------ - Introduces a click argument, `features_in_arg`, which utilizes a click callback to normalize the input of geojson features (#9). - Release from tagged Travis CI builds (#10). 0.3.0 (2015-08-12) ------------------ - Deprecation of the cligj.plugins module (#6). Please switch to the click-plugins module: https://github.com/click-contrib/click-plugins. The cligj.plugins module will be removed from cligj at version 1.0. 0.2.0 (2015-05-28) ------------------ - Addition of a pluggable command group class and a corresponding click-style decorator (#2, #3). 0.1.0 (2015-01-06) ------------------ - Initial release: a collection of GeoJSON-related command line arguments and options for use with Click (#1). cligj-0.4.0/LICENSE000077500000000000000000000026751263463336200136350ustar00rootroot00000000000000Copyright (c) 2014, Mapbox All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of cligj nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cligj-0.4.0/README.rst000077500000000000000000000110761263463336200143120ustar00rootroot00000000000000cligj ====== .. image:: https://travis-ci.org/mapbox/cligj.svg :target: https://travis-ci.org/mapbox/cligj .. image:: https://coveralls.io/repos/mapbox/cligj/badge.png?branch=master :target: https://coveralls.io/r/mapbox/cligj?branch=master Common arguments and options for GeoJSON processing commands, using Click. `cligj` is for Python developers who create command line interfaces for geospatial data. `cligj` allows you to quickly build consistent, well-tested and interoperable CLIs for handling GeoJSON. Arguments --------- ``files_in_arg`` Multiple files ``files_inout_arg`` Multiple files, last of which is an output file. ``features_in_arg`` GeoJSON Features input which accepts multiple representations of GeoJSON features and returns the input data as an iterable of GeoJSON Feature-like dictionaries Options -------- ``verbose_opt`` ``quiet_opt`` ``format_opt`` JSON formatting options ~~~~~~~~~~~~~~~~~~~~~~~ ``indent_opt`` ``compact_opt`` Coordinate precision option ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``precision_opt`` Geographic (default), projected, or Mercator switch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``projection_geographic_opt`` ``projection_projected_opt`` ``projection_mercator_opt`` Feature collection or feature sequence switch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``sequence_opt`` ``use_rs_opt`` GeoJSON output mode option ~~~~~~~~~~~~~~~~~~~~~~~~~~ ``geojson_type_collection_opt`` ``geojson_type_feature_opt`` ``def geojson_type_bbox_opt`` Example ------- Here's an example of a command that writes out GeoJSON features as a collection or, optionally, a sequence of individual features. Since most software that reads and writes GeoJSON expects a text containing a single feature collection, that's the default, and a LF-delimited sequence of texts containing one GeoJSON feature each is a feature that is turned on using the ``--sequence`` option. To write sequences of feature texts that conform to the `JSON Text Sequences proposed standard `__ (and might contain pretty-printed JSON) with the ASCII Record Separator (0x1e) as a delimiter, use the ``--rs`` option .. code-block:: python import click import cligj import json def process_features(features): for feature in features: # TODO process feature here yield feature @click.command() @cligj.features_in_arg @cligj.sequence_opt @cligj.use_rs_opt def pass_features(features, sequence, use_rs): if sequence: for feature in process_features(features): if use_rs: click.echo(b'\x1e', nl=False) click.echo(json.dumps(feature)) else: click.echo(json.dumps( {'type': 'FeatureCollection', 'features': list(process_features(features))})) On the command line, the generated help text explains the usage .. code-block:: console Usage: pass_features [OPTIONS] FEATURES... Options: --sequence / --no-sequence Write a LF-delimited sequence of texts containing individual objects or write a single JSON text containing a feature collection object (the default). --rs / --no-rs Use RS (0x1E) as a prefix for individual texts in a sequence as per http://tools.ietf.org/html /draft-ietf-json-text-sequence-13 (default is False). --help Show this message and exit. And can be used like this .. code-block:: console $ cat data.geojson {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'id': '1'}, {'type': 'Feature', 'id': '2'}]} $ pass_features data.geojson {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'id': '1'}, {'type': 'Feature', 'id': '2'}]} $ cat data.geojson | pass_features {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'id': '1'}, {'type': 'Feature', 'id': '2'}]} $ cat data.geojson | pass_features --sequence {'type': 'Feature', 'id': '1'} {'type': 'Feature', 'id': '2'} $ cat data.geojson | pass_features --sequence --rs ^^{'type': 'Feature', 'id': '1'} ^^{'type': 'Feature', 'id': '2'} In this example, ``^^`` represents 0x1e. Plugins ------- .. warning:: The cligj.plugins module is deprecated and will be removed at version 1.0. Use `click-plugins `_ instead. cligj-0.4.0/cligj/000077500000000000000000000000001263463336200137035ustar00rootroot00000000000000cligj-0.4.0/cligj/__init__.py000077500000000000000000000065061263463336200160260ustar00rootroot00000000000000# cligj # Shared arguments and options. import click from .features import normalize_feature_inputs # Arguments. # Multiple input files. files_in_arg = click.argument( 'files', nargs=-1, type=click.Path(resolve_path=True), required=True, metavar="INPUTS...") # Multiple files, last of which is an output file. files_inout_arg = click.argument( 'files', nargs=-1, type=click.Path(resolve_path=True), required=True, metavar="INPUTS... OUTPUT") # Features input # Accepts multiple representations of GeoJSON features # Returns the input data as an iterable of GeoJSON Feature-like dictionaries features_in_arg = click.argument( 'features', nargs=-1, callback=normalize_feature_inputs, metavar="FEATURES...") # Options. verbose_opt = click.option( '--verbose', '-v', count=True, help="Increase verbosity.") quiet_opt = click.option( '--quiet', '-q', count=True, help="Decrease verbosity.") # Format driver option. format_opt = click.option( '-f', '--format', '--driver', default='GTiff', help="Output format driver") # JSON formatting options. indent_opt = click.option( '--indent', type=int, default=None, help="Indentation level for JSON output") compact_opt = click.option( '--compact/--not-compact', default=False, help="Use compact separators (',', ':').") # Coordinate precision option. precision_opt = click.option( '--precision', type=int, default=-1, help="Decimal precision of coordinates.") # Geographic (default), projected, or Mercator switch. projection_geographic_opt = click.option( '--geographic', 'projection', flag_value='geographic', default=True, help="Output in geographic coordinates (the default).") projection_projected_opt = click.option( '--projected', 'projection', flag_value='projected', help="Output in dataset's own, projected coordinates.") projection_mercator_opt = click.option( '--mercator', 'projection', flag_value='mercator', help="Output in Web Mercator coordinates.") # Feature collection or feature sequence switch. sequence_opt = click.option( '--sequence/--no-sequence', default=False, help="Write a LF-delimited sequence of texts containing individual " "objects or write a single JSON text containing a feature " "collection object (the default).") use_rs_opt = click.option( '--rs/--no-rs', 'use_rs', default=False, help="Use RS (0x1E) as a prefix for individual texts in a sequence " "as per http://tools.ietf.org/html/draft-ietf-json-text-sequence-13 " "(default is False).") # GeoJSON output mode option. def geojson_type_collection_opt(default=False): return click.option( '--collection', 'geojson_type', flag_value='collection', default=default, help="Output as GeoJSON feature collection(s).") def geojson_type_feature_opt(default=False): return click.option( '--feature', 'geojson_type', flag_value='feature', default=default, help="Output as GeoJSON feature(s).") def geojson_type_bbox_opt(default=False): return click.option( '--bbox', 'geojson_type', flag_value='bbox', default=default, help="Output as GeoJSON bounding box array(s).") cligj-0.4.0/cligj/features.py000066400000000000000000000070751263463336200161040ustar00rootroot00000000000000from itertools import chain import json import re import click def normalize_feature_inputs(ctx, param, features_like): """ Click callback which accepts the following values: * Path to file(s), each containing single FeatureCollection or Feature * Coordinate pair(s) of the form "[0, 0]" or "0, 0" or "0 0" * if not specified or '-', process STDIN stream containing - line-delimited features - ASCII Record Separator (0x1e) delimited features - FeatureCollection or Feature object and yields GeoJSON Features. """ if len(features_like) == 0: features_like = ('-',) for flike in features_like: try: # It's a file/stream with GeoJSON src = iter(click.open_file(flike, mode='r')) for feature in iter_features(src): yield feature except IOError: # It's a coordinate string coords = list(coords_from_query(flike)) feature = { 'type': 'Feature', 'properties': {}, 'geometry': { 'type': 'Point', 'coordinates': coords}} yield feature def iter_features(src): """Yield features from a src that may be either a GeoJSON feature text sequence or GeoJSON feature collection.""" first_line = next(src) # If input is RS-delimited JSON sequence. if first_line.startswith(u'\x1e'): buffer = first_line.strip(u'\x1e') for line in src: if line.startswith(u'\x1e'): if buffer: feat = json.loads(buffer) yield feat buffer = line.strip(u'\x1e') else: buffer += line else: feat = json.loads(buffer) yield feat else: try: feat = json.loads(first_line) assert feat['type'] == 'Feature' yield feat for line in src: feat = json.loads(line) yield feat except (TypeError, KeyError, AssertionError, ValueError): text = "".join(chain([first_line], src)) feats = json.loads(text) if feats['type'] == 'Feature': yield feats elif feats['type'] == 'FeatureCollection': for feat in feats['features']: yield feat def iter_query(query): """Accept a filename, stream, or string. Returns an iterator over lines of the query.""" try: itr = click.open_file(query).readlines() except IOError: itr = [query] return itr def coords_from_query(query): """Transform a query line into a (lng, lat) pair of coordinates.""" try: coords = json.loads(query) except ValueError: vals = re.split(r"\,*\s*", query.strip()) coords = [float(v) for v in vals] return tuple(coords[:2]) def normalize_feature_objects(feature_objs): """Takes an iterable of GeoJSON-like Feature mappings or an iterable of objects with a geo interface and normalizes it to the former.""" for obj in feature_objs: if hasattr(obj, "__geo_interface__") and \ 'type' in obj.__geo_interface__.keys() and \ obj.__geo_interface__['type'] == 'Feature': yield obj.__geo_interface__ elif isinstance(obj, dict) and 'type' in obj and \ obj['type'] == 'Feature': yield obj else: raise ValueError("Did not recognize object {0}" "as GeoJSON Feature".format(obj)) cligj-0.4.0/cligj/plugins.py000066400000000000000000000151741263463336200157460ustar00rootroot00000000000000""" Common components required to enable setuptools plugins. In general the components defined here are slightly modified or subclassed versions of core click components. This is required in order to insert code that loads entry points when necessary while still maintaining a simple API is only slightly different from the click API. Here's how it works: When defining a main commandline group: >>> import click >>> @click.group() ... def cli(): ... '''A commandline interface.''' ... pass The `click.group()` decorator turns `cli()` into an instance of `click.Group()`. Subsequent commands hang off of this group: >>> @cli.command() ... @click.argument('val') ... def printer(val): ... '''Print a value.''' ... click.echo(val) At this point the entry points, which are just instances of `click.Command()`, can be added to the main group with: >>> from pkg_resources import iter_entry_points >>> for ep in iter_entry_points('module.commands'): ... cli.add_command(ep.load()) This works but its not very Pythonic, is vulnerable to typing errors, must be manually updated if a better method is discovered, and most importantly, if an entry point throws an exception on completely crashes the group the command is attached to. A better time to load the entry points is when the group they will be attached to is instantiated. This requires slight modifications to the `click.group()` decorator and `click.Group()` to let them load entry points as needed. If the modified `group()` decorator is used on the same group like this: >>> from pkg_resources import iter_entry_points >>> import cligj.plugins >>> @cligj.plugins.group(plugins=iter_entry_points('module.commands')) ... def cli(): ... '''A commandline interface.''' ... pass Now the entry points are loaded before the normal `click.group()` decorator is called, except it returns a modified `Group()` so if we hang another group off of `cli()`: >>> @cli.group(plugins=iter_entry_points('other_module.commands')) ... def subgroup(): ... '''A subgroup with more plugins''' ... pass We can register additional plugins in a sub-group. Catching broken plugins is done in the modified `group()` which attaches instances of `BrokenCommand()` to the group instead of instances of `click.Command()`. The broken commands have special help messages and override `click.Command.invoke()` so the user gets a useful error message with a traceback if they attempt to run the command or use `--help`. """ import os import sys import traceback import warnings import click warnings.warn( "cligj.plugins has been deprecated in favor of click-plugins: " "https://github.com/click-contrib/click-plugins. The plugins " "module will be removed in cligj 1.0.", FutureWarning, stacklevel=2) class BrokenCommand(click.Command): """ Rather than completely crash the CLI when a broken plugin is loaded, this class provides a modified help message informing the user that the plugin is broken and they should contact the owner. If the user executes the plugin or specifies `--help` a traceback is reported showing the exception the plugin loader encountered. """ def __init__(self, name): """ Define the special help messages after instantiating `click.Command()`. Parameters ---------- name : str Name of command. """ click.Command.__init__(self, name) util_name = os.path.basename(sys.argv and sys.argv[0] or __file__) if os.environ.get('CLIGJ_HONESTLY'): # pragma no cover icon = u'\U0001F4A9' else: icon = u'\u2020' self.help = ( "\nWarning: entry point could not be loaded. Contact " "its author for help.\n\n\b\n" + traceback.format_exc()) self.short_help = ( icon + " Warning: could not load plugin. See `%s %s --help`." % (util_name, self.name)) def invoke(self, ctx): """ Print the error message instead of doing nothing. Parameters ---------- ctx : click.Context Required for click. """ click.echo(self.help, color=ctx.color) ctx.exit(1) # Defaults to 0 but we want an error code class Group(click.Group): """ A subclass of `click.Group()` that returns the modified `group()` decorator when `Group.group()` is called. Used by the modified `group()` decorator. So many groups... See the main docstring in this file for a full explanation. """ def __init__(self, **kwargs): click.Group.__init__(self, **kwargs) def group(self, *args, **kwargs): """ Return the modified `group()` rather than `click.group()`. This gives the user an opportunity to assign entire groups of plugins to their own subcommand group. See the main docstring in this file for a full explanation. """ def decorator(f): cmd = group(*args, **kwargs)(f) self.add_command(cmd) return cmd return decorator def group(plugins=None, **kwargs): """ A special group decorator that behaves exactly like `click.group()` but allows for additional plugins to be loaded. Example: >>> import cligj.plugins >>> from pkg_resources import iter_entry_points >>> plugins = iter_entry_points('module.entry_points') >>> @cligj.plugins.group(plugins=plugins) ... def cli(): ... '''A CLI aplication''' ... pass Plugins that raise an exception on load are caught and converted to an instance of `BrokenCommand()`, which has better error handling and prevents broken plugins from taking crashing the CLI. See the main docstring in this file for a full explanation. Parameters ---------- plugins : iter An iterable that produces one entry point per iteration. kwargs : **kwargs Additional arguments for `click.Group()`. """ def decorator(f): kwargs.setdefault('cls', Group) grp = click.group(**kwargs)(f) if plugins is not None: for entry_point in plugins: try: grp.add_command(entry_point.load()) except Exception: # Catch this so a busted plugin doesn't take down the CLI. # Handled by registering a dummy command that does nothing # other than explain the error. grp.add_command(BrokenCommand(entry_point.name)) return grp return decorator cligj-0.4.0/setup.py000077500000000000000000000014511263463336200143310ustar00rootroot00000000000000from codecs import open as codecs_open from setuptools import setup, find_packages # Get the long description from the relevant file with codecs_open('README.rst', encoding='utf-8') as f: long_description = f.read() setup(name='cligj', version='0.4.0', description=u"Click params for commmand line interfaces to GeoJSON", long_description=long_description, classifiers=[], keywords='', author=u"Sean Gillies", author_email='sean@mapbox.com', url='https://github.com/mapbox/cligj', license='BSD', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=False, install_requires=[ 'click>=4.0' ], extras_require={ 'test': ['pytest-cov'], }) cligj-0.4.0/tests/000077500000000000000000000000001263463336200137555ustar00rootroot00000000000000cligj-0.4.0/tests/__init__.py000066400000000000000000000002201263463336200160600ustar00rootroot00000000000000# Do not delete this file. It makes the tests directory behave like a Python # module, which is required to manually register and test plugins.cligj-0.4.0/tests/broken_plugins.py000066400000000000000000000004331263463336200173500ustar00rootroot00000000000000""" We detect plugins that throw an exception on import, so just throw an exception to mimic a problem. """ import click @click.command() def something(arg): click.echo('passed') raise Exception('I am a broken plugin. Send help.') @click.command() def after(): pass cligj-0.4.0/tests/conftest.py000066400000000000000000000002031263463336200161470ustar00rootroot00000000000000from click.testing import CliRunner import pytest @pytest.fixture(scope='function') def runner(request): return CliRunner() cligj-0.4.0/tests/onepoint.geojson000066400000000000000000000001651263463336200172000ustar00rootroot00000000000000{"geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"}, "id": "0", "properties": {}, "type": "Feature"} cligj-0.4.0/tests/test_cli.py000077500000000000000000000131221263463336200161370ustar00rootroot00000000000000import os import os.path import click import cligj def test_files_in(runner): @click.command() @cligj.files_in_arg def cmd(files): for f in files: click.echo(f) result = runner.invoke(cmd, ['1.tif', '2.tif']) assert not result.exception assert result.output.splitlines() == [ os.path.join(os.getcwd(), '1.tif'), os.path.join(os.getcwd(), '2.tif'), ] def test_files_inout(runner): @click.command() @cligj.files_inout_arg def cmd(files): for f in files: click.echo(f) result = runner.invoke(cmd, ['1.tif', '2.tif']) assert not result.exception assert result.output.splitlines() == [ os.path.join(os.getcwd(), '1.tif'), os.path.join(os.getcwd(), '2.tif'), ] def test_verbose(runner): @click.command() @cligj.verbose_opt def cmd(verbose): click.echo("%s" % verbose) result = runner.invoke(cmd, ['-vv']) assert not result.exception assert result.output.splitlines() == ['2'] def test_quiet(runner): @click.command() @cligj.quiet_opt def cmd(quiet): click.echo("%s" % quiet) result = runner.invoke(cmd, ['-qq']) assert not result.exception assert result.output.splitlines() == ['2'] def test_format(runner): @click.command() @cligj.format_opt def cmd(driver): click.echo("%s" % driver) result = runner.invoke(cmd, ['--driver', 'lol']) assert not result.exception assert result.output.splitlines() == ['lol'] result = runner.invoke(cmd, ['--format', 'lol']) assert not result.exception assert result.output.splitlines() == ['lol'] result = runner.invoke(cmd, ['-f', 'lol']) assert not result.exception assert result.output.splitlines() == ['lol'] result = runner.invoke(cmd) assert not result.exception assert result.output.splitlines() == ['GTiff'] def test_indent(runner): @click.command() @cligj.indent_opt def cmd(indent): click.echo("%s" % indent) result = runner.invoke(cmd, ['--indent', '2']) assert not result.exception assert result.output.splitlines() == ['2'] result = runner.invoke(cmd) assert not result.exception assert result.output.splitlines() == ['None'] def test_compact(runner): @click.command() @cligj.compact_opt def cmd(compact): click.echo("%s" % compact) result = runner.invoke(cmd, ['--compact']) assert not result.exception assert result.output.splitlines() == ['True'] result = runner.invoke(cmd, ['--not-compact']) assert not result.exception assert result.output.splitlines() == ['False'] result = runner.invoke(cmd) assert not result.exception assert result.output.splitlines() == ['False'] def test_precision(runner): @click.command() @cligj.precision_opt def cmd(precision): click.echo("%s" % precision) result = runner.invoke(cmd, ['--precision', '2']) assert not result.exception assert result.output.splitlines() == ['2'] result = runner.invoke(cmd) assert not result.exception assert result.output.splitlines() == ['-1'] def test_projection(runner): @click.command() @cligj.projection_geographic_opt @cligj.projection_projected_opt @cligj.projection_mercator_opt def cmd(projection): click.echo("%s" % projection) result = runner.invoke(cmd, ['--geographic']) assert not result.exception assert result.output.splitlines() == ['geographic'] result = runner.invoke(cmd, ['--projected']) assert not result.exception assert result.output.splitlines() == ['projected'] result = runner.invoke(cmd, ['--mercator']) assert not result.exception assert result.output.splitlines() == ['mercator'] result = runner.invoke(cmd) assert not result.exception assert result.output.splitlines() == ['geographic'] def test_sequence(runner): @click.command() @cligj.sequence_opt def cmd(sequence): click.echo("%s" % sequence) result = runner.invoke(cmd) assert not result.exception assert result.output.splitlines() == ['False'] result = runner.invoke(cmd, ['--sequence']) assert not result.exception assert result.output.splitlines() == ['True'] result = runner.invoke(cmd, ['--no-sequence']) assert not result.exception assert result.output.splitlines() == ['False'] def test_sequence_rs(runner): @click.command() @cligj.sequence_opt @cligj.use_rs_opt def cmd(sequence, use_rs): click.echo("%s" % sequence) click.echo("%s" % use_rs) result = runner.invoke(cmd, ['--sequence', '--rs']) assert not result.exception assert result.output.splitlines() == ['True', 'True'] result = runner.invoke(cmd, ['--sequence']) assert not result.exception assert result.output.splitlines() == ['True', 'False'] def test_geojson_type(runner): @click.command() @cligj.geojson_type_collection_opt(True) @cligj.geojson_type_feature_opt(False) @cligj.geojson_type_bbox_opt(False) def cmd(geojson_type): click.echo("%s" % geojson_type) result = runner.invoke(cmd) assert not result.exception assert result.output.splitlines() == ['collection'] result = runner.invoke(cmd, ['--collection']) assert not result.exception assert result.output.splitlines() == ['collection'] result = runner.invoke(cmd, ['--feature']) assert not result.exception assert result.output.splitlines() == ['feature'] result = runner.invoke(cmd, ['--bbox']) assert not result.exception assert result.output.splitlines() == ['bbox'] cligj-0.4.0/tests/test_features.py000066400000000000000000000076151263463336200172150ustar00rootroot00000000000000import json import sys import pytest from cligj.features import \ coords_from_query, iter_query, \ normalize_feature_inputs, normalize_feature_objects def test_iter_query_string(): assert iter_query("lolwut") == ["lolwut"] def test_iter_query_file(tmpdir): filename = str(tmpdir.join('test.txt')) with open(filename, 'w') as f: f.write("lolwut") assert iter_query(filename) == ["lolwut"] def test_coords_from_query_json(): assert coords_from_query("[-100, 40]") == (-100, 40) def test_coords_from_query_csv(): assert coords_from_query("-100, 40") == (-100, 40) def test_coords_from_query_ws(): assert coords_from_query("-100 40") == (-100, 40) @pytest.fixture def expected_features(): with open("tests/twopoints.geojson") as src: fc = json.loads(src.read()) return fc['features'] def _geoms(features): geoms = [] for feature in features: geoms.append(feature['geometry']) return geoms def test_featurecollection_file(expected_features): features = normalize_feature_inputs(None, 'features', ["tests/twopoints.geojson"]) assert _geoms(features) == _geoms(expected_features) def test_featurecollection_stdin(expected_features): sys.stdin = open("tests/twopoints.geojson", 'r') features = normalize_feature_inputs(None, 'features', []) assert _geoms(features) == _geoms(expected_features) def test_featuresequence(expected_features): features = normalize_feature_inputs(None, 'features', ["tests/twopoints_seq.txt"]) assert _geoms(features) == _geoms(expected_features) # TODO test path to sequence files fail def test_featuresequence_stdin(expected_features): sys.stdin = open("tests/twopoints_seq.txt", 'r') features = normalize_feature_inputs(None, 'features', []) assert _geoms(features) == _geoms(expected_features) def test_singlefeature(expected_features): features = normalize_feature_inputs(None, 'features', ["tests/onepoint.geojson"]) assert _geoms(features) == _geoms([expected_features[0]]) def test_singlefeature_stdin(expected_features): sys.stdin = open("tests/onepoint.geojson", 'r') features = normalize_feature_inputs(None, 'features', []) assert _geoms(features) == _geoms([expected_features[0]]) def test_featuresequencers(expected_features): features = normalize_feature_inputs(None, 'features', ["tests/twopoints_seqrs.txt"]) assert _geoms(features) == _geoms(expected_features) def test_featuresequencers_stdin(expected_features): sys.stdin = open("tests/twopoints_seqrs.txt", 'r') features = normalize_feature_inputs(None, 'features', []) assert _geoms(features) == _geoms(expected_features) def test_coordarrays(expected_features): inputs = ["[-122.7282, 45.5801]", "[-121.3153, 44.0582]"] features = normalize_feature_inputs(None, 'features', inputs) assert _geoms(features) == _geoms(expected_features) def test_coordpairs_comma(expected_features): inputs = ["-122.7282, 45.5801", "-121.3153, 44.0582"] features = normalize_feature_inputs(None, 'features', inputs) assert _geoms(features) == _geoms(expected_features) def test_coordpairs_space(expected_features): inputs = ["-122.7282 45.5801", "-121.3153 44.0582"] features = normalize_feature_inputs(None, 'features', inputs) assert _geoms(features) == _geoms(expected_features) class MockGeo(object): def __init__(self, feature): self.__geo_interface__ = feature def test_normalize_feature_objects(expected_features): objs = [MockGeo(f) for f in expected_features] assert expected_features == list(normalize_feature_objects(objs)) assert expected_features == list(normalize_feature_objects(expected_features)) def test_normalize_feature_objects_bad(expected_features): objs = [MockGeo(f) for f in expected_features] objs.append(MockGeo(dict())) with pytest.raises(ValueError): list(normalize_feature_objects(objs)) cligj-0.4.0/tests/test_plugins.py000066400000000000000000000102451263463336200170510ustar00rootroot00000000000000"""Unittests for ``cligj.plugins``.""" import os from pkg_resources import EntryPoint from pkg_resources import iter_entry_points from pkg_resources import working_set import click import cligj.plugins # Create a few CLI commands for testing @click.command() @click.argument('arg') def cmd1(arg): """Test command 1""" click.echo('passed') @click.command() @click.argument('arg') def cmd2(arg): """Test command 2""" click.echo('passed') # Manually register plugins in an entry point and put broken plugins in a # different entry point. # The `DistStub()` class gets around an exception that is raised when # `entry_point.load()` is called. By default `load()` has `requires=True` # which calls `dist.requires()` and the `cligj.plugins.group()` decorator # doesn't allow us to change this. Because we are manually registering these # plugins the `dist` attribute is `None` so we can just create a stub that # always returns an empty list since we don't have any requirements. A full # `pkg_resources.Distribution()` instance is not needed because there isn't # a package installed anywhere. class DistStub(object): def requires(self, *args): return [] working_set.by_key['cligj']._ep_map = { 'cligj.test_plugins': { 'cmd1': EntryPoint.parse( 'cmd1=tests.test_plugins:cmd1', dist=DistStub()), 'cmd2': EntryPoint.parse( 'cmd2=tests.test_plugins:cmd2', dist=DistStub()) }, 'cligj.broken_plugins': { 'before': EntryPoint.parse( 'before=tests.broken_plugins:before', dist=DistStub()), 'after': EntryPoint.parse( 'after=tests.broken_plugins:after', dist=DistStub()), 'do_not_exist': EntryPoint.parse( 'do_not_exist=tests.broken_plugins:do_not_exist', dist=DistStub()) } } # Main CLI groups - one with good plugins attached and the other broken @cligj.plugins.group(plugins=iter_entry_points('cligj.test_plugins')) def good_cli(): """Good CLI group.""" pass @cligj.plugins.group(plugins=iter_entry_points('cligj.broken_plugins')) def broken_cli(): """Broken CLI group.""" pass def test_registered(): # Make sure the plugins are properly registered. If this test fails it # means that some of the for loops in other tests may not be executing. assert len([ep for ep in iter_entry_points('cligj.test_plugins')]) > 1 assert len([ep for ep in iter_entry_points('cligj.broken_plugins')]) > 1 def test_register_and_run(runner): result = runner.invoke(good_cli) assert result.exit_code is 0 for ep in iter_entry_points('cligj.test_plugins'): cmd_result = runner.invoke(good_cli, [ep.name, 'something']) assert cmd_result.exit_code is 0 assert cmd_result.output.strip() == 'passed' def test_broken_register_and_run(runner): result = runner.invoke(broken_cli) assert result.exit_code is 0 assert u'\U0001F4A9' in result.output or u'\u2020' in result.output for ep in iter_entry_points('cligj.broken_plugins'): cmd_result = runner.invoke(broken_cli, [ep.name]) assert cmd_result.exit_code is not 0 assert 'Traceback' in cmd_result.output def test_group_chain(runner): # Attach a sub-group to a CLI and get execute it without arguments to make # sure both the sub-group and all the parent group's commands are present @good_cli.group() def sub_cli(): """Sub CLI.""" pass result = runner.invoke(good_cli) assert result.exit_code is 0 assert sub_cli.name in result.output for ep in iter_entry_points('cligj.test_plugins'): assert ep.name in result.output # Same as above but the sub-group has plugins @good_cli.group(plugins=iter_entry_points('cligj.test_plugins')) def sub_cli_plugins(): """Sub CLI with plugins.""" pass result = runner.invoke(good_cli, ['sub_cli_plugins']) assert result.exit_code is 0 for ep in iter_entry_points('cligj.test_plugins'): assert ep.name in result.output # Execute one of the sub-group's commands result = runner.invoke(good_cli, ['sub_cli_plugins', 'cmd1', 'something']) assert result.exit_code is 0 assert result.output.strip() == 'passed' cligj-0.4.0/tests/twopoints.geojson000066400000000000000000000020661263463336200174150ustar00rootroot00000000000000{"features": [{"bbox": [-122.9292140099711, 45.37948199034149, -122.44106199104115, 45.858097009742835], "center": [-122.7282, 45.5801], "context": [{"id": "postcode.2503633822", "text": "97203"}, {"id": "region.3470299826", "text": "Oregon"}, {"id": "country.4150104525", "short_code": "us", "text": "United States"}], "geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"}, "id": "place.42767", "place_name": "Portland, Oregon, United States", "properties": {}, "relevance": 0.999, "text": "Portland", "type": "Feature"}, {"bbox": [-121.9779540096568, 43.74737999114854, -120.74788099000016, 44.32812500969035], "center": [-121.3153, 44.0582], "context": [{"id": "postcode.3332732485", "text": "97701"}, {"id": "region.3470299826", "text": "Oregon"}, {"id": "country.4150104525", "short_code": "us", "text": "United States"}], "geometry": {"coordinates": [-121.3153, 44.0582], "type": "Point"}, "id": "place.3965", "place_name": "Bend, Oregon, United States", "properties": {}, "relevance": 0.999, "text": "Bend", "type": "Feature"}], "type": "FeatureCollection"} cligj-0.4.0/tests/twopoints_seq.txt000066400000000000000000000003521263463336200174340ustar00rootroot00000000000000{"geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"}, "id": "0", "properties": {}, "type": "Feature"} {"geometry": {"coordinates": [-121.3153, 44.0582], "type": "Point"}, "id": "1", "properties": {}, "type": "Feature"} cligj-0.4.0/tests/twopoints_seqrs.txt000066400000000000000000000003761263463336200200070ustar00rootroot00000000000000{"geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"}, "id": "0", "properties": {}, "type": "Feature"} {"geometry": { "coordinates": [-121.3153, 44.0582], "type": "Point"}, "id": "1", "properties": {}, "type": "Feature"}