This is an automated email from the git hooks/post-receive script. sebastic pushed a commit to branch master in repository python-geojson.
commit 01ccef171bf5bdba3be268a5f601e114324b62be Author: Bas Couwenberg <[email protected]> Date: Sun Sep 17 18:25:43 2017 +0200 New upstream version 2.2.0 --- CHANGELOG.rst | 7 +++++ README.rst | 12 ++++++-- geojson/_version.py | 2 +- geojson/codec.py | 5 ++-- geojson/geometry.py | 54 +++++++++++++++++++++++++--------- geojson/utils.py | 8 ++---- tests/test_constructor.py | 33 +++++++++++++++++++++ tests/test_coords.py | 4 +++ tests/test_features.py | 7 +++-- tests/test_geo_interface.py | 15 ++++++++-- tests/test_strict_json.py | 34 ++++++++++++---------- tests/test_validation.py | 70 +++++++++++++++++++++++++++++++++------------ 12 files changed, 188 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d863f6d..1449f32 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changes ======= +2.2.0 (2017-09-17) +------------------ + +- Allow constructing geojson objects from geojson objects + + - https://github.com/frewsxcv/python-geojson/pull/104 + 2.1.0 (2017-08-29) ------------------ diff --git a/README.rst b/README.rst index f1be0e7..d71aaa1 100644 --- a/README.rst +++ b/README.rst @@ -194,9 +194,14 @@ FeatureCollection >>> my_other_feature = Feature(geometry=Point((-80.234, -22.532))) - >>> FeatureCollection([my_feature, my_other_feature]) # doctest: +ELLIPSIS + >>> feature_collection = FeatureCollection([my_feature, my_other_feature]) + + >>> feature_collection # doctest: +ELLIPSIS {"features": [{"geometry": {"coordinates": [1.643..., -19.12...], "type": "Point"}, "properties": {}, "type": "Feature"}, {"geometry": {"coordinates": [-80.23..., -22.53...], "type": "Point"}, "properties": {}, "type": "Feature"}], "type": "FeatureCollection"} + >>> feature_collection.errors() + [] + Visualize the result of the example above `here <https://gist.github.com/frewsxcv/34513be6fb492771ef7b>`__. General information about FeatureCollection can be found in `Section 3.3`_ within `The GeoJSON Format Specification`_. .. _Section 3.3: https://tools.ietf.org/html/rfc7946#section-3.3 @@ -300,7 +305,7 @@ validation >>> obj = geojson.Point((-3.68,40.41,25.14,10.34)) >>> obj.errors() - 'the "coordinates" member must be a single position' + 'a position must have exactly 2 or 3 values' generate_random ~~~~~~~~~~~~~~~ @@ -314,6 +319,9 @@ generate_random >>> geojson.utils.generate_random("LineString") # doctest: +ELLIPSIS {"coordinates": [...], "type": "LineString"} + >>> geojson.utils.generate_random("Polygon") # doctest: +ELLIPSIS + {"coordinates": [...], "type": "Polygon"} + Development ----------- diff --git a/geojson/_version.py b/geojson/_version.py index 4edb268..4bde90e 100644 --- a/geojson/_version.py +++ b/geojson/_version.py @@ -1,2 +1,2 @@ -__version__ = "2.1.0" +__version__ = "2.2.0" __version_info__ = tuple(map(int, __version__.split("."))) diff --git a/geojson/codec.py b/geojson/codec.py index eb8808b..703db08 100644 --- a/geojson/codec.py +++ b/geojson/codec.py @@ -11,7 +11,7 @@ from geojson.mapping import to_mapping class GeoJSONEncoder(json.JSONEncoder): def default(self, obj): - return geojson.factory.GeoJSON.to_instance(obj) + return geojson.factory.GeoJSON.to_instance(obj) # NOQA # Wrap the functions from json, providing encoder, decoders, and @@ -19,8 +19,7 @@ class GeoJSONEncoder(json.JSONEncoder): # Here the defaults are set to only permit valid JSON as per RFC 4267 def _enforce_strict_numbers(obj): - if isinstance(obj, (int, float)): - raise ValueError("Number %r is not JSON compliant" % obj) + raise ValueError("Number %r is not JSON compliant" % obj) def dump(obj, fp, cls=GeoJSONEncoder, allow_nan=False, **kwargs): diff --git a/geojson/geometry.py b/geojson/geometry.py index 8226c9c..1bcc006 100644 --- a/geojson/geometry.py +++ b/geojson/geometry.py @@ -4,17 +4,18 @@ from decimal import Decimal from geojson.base import GeoJSON +if sys.version_info[0] == 3: + # Python 3.x has no long type + _JSON_compliant_types = (float, int, Decimal) +else: + _JSON_compliant_types = (float, int, Decimal, long) # noqa + + class Geometry(GeoJSON): """ Represents an abstract base class for a WGS84 geometry. """ - if sys.version_info[0] == 3: - # Python 3.x has no long type - __JSON_compliant_types = (float, int, Decimal) - else: - __JSON_compliant_types = (float, int, Decimal, long) # noqa - def __init__(self, coordinates=None, crs=None, validate=False, **extra): """ Initialises a Geometry object. @@ -26,8 +27,8 @@ class Geometry(GeoJSON): """ super(Geometry, self).__init__(**extra) - self["coordinates"] = coordinates or [] - self.clean_coordinates(self["coordinates"]) + self["coordinates"] = self.clean_coordinates(coordinates or []) + if validate: errors = self.errors() if errors: @@ -37,11 +38,22 @@ class Geometry(GeoJSON): @classmethod def clean_coordinates(cls, coords): + if isinstance(coords, cls): + return coords['coordinates'] + + new_coords = [] + if isinstance(coords, Geometry): + coords = [coords] for coord in coords: if isinstance(coord, (list, tuple)): - cls.clean_coordinates(coord) - elif not isinstance(coord, cls.__JSON_compliant_types): - raise ValueError("%r is not JSON compliant number" % coord) + new_coords.append(cls.clean_coordinates(coord)) + elif isinstance(coord, Geometry): + new_coords.append(coord['coordinates']) + elif isinstance(coord, _JSON_compliant_types): + new_coords.append(coord) + else: + raise ValueError("%r is not a JSON compliant number" % coord) + return new_coords class GeometryCollection(GeoJSON): @@ -61,8 +73,10 @@ class GeometryCollection(GeoJSON): # Marker classes. def check_point(coord): + if not isinstance(coord, list): + return 'each position must be a list' if len(coord) not in (2, 3): - return 'the "coordinates" member must be a single position' + return 'a position must have exactly 2 or 3 values' class Point(Geometry): @@ -76,9 +90,15 @@ class MultiPoint(Geometry): def check_line_string(coord): + if not isinstance(coord, list): + return 'each line must be a list of positions' if len(coord) < 2: return ('the "coordinates" member must be an array of ' 'two or more positions') + for pos in coord: + error = check_point(pos) + if error: + return error class LineString(MultiPoint): @@ -92,13 +112,19 @@ class MultiLineString(Geometry): def check_polygon(coord): + if not isinstance(coord, list): + return 'Each polygon must be a list of linear rings' + + if not all(isinstance(elem, list) for elem in coord): + return "Each element of a polygon's coordinates must be a list" + lengths = all(len(elem) >= 4 for elem in coord) if lengths is False: - return 'LinearRing must contain with 4 or more positions' + return 'Each linear ring must contain at least 4 positions' isring = all(elem[0] == elem[-1] for elem in coord) if isring is False: - return 'The first and last positions in LinearRing must be equivalent' + return 'Each linear ring must end where it started' class Polygon(Geometry): diff --git a/geojson/utils.py b/geojson/utils.py index 15ff320..e6acb21 100644 --- a/geojson/utils.py +++ b/geojson/utils.py @@ -5,7 +5,7 @@ def coords(obj): """ Yields the coordinates from a Feature or Geometry. - :param obj: A geometry or feature to extract the coordinates from." + :param obj: A geometry or feature to extract the coordinates from. :type obj: Feature, Geometry :return: A generator with coordinate tuples from the geometry or feature. :rtype: generator @@ -78,6 +78,7 @@ def generate_random(featureType, numberVertices=3, :rtype: object :raises ValueError: if there is no featureType provided. """ + from geojson import Point, LineString, Polygon import random import math @@ -98,10 +99,7 @@ def generate_random(featureType, numberVertices=3, return Point((randomLon(), randomLat())) def createLine(): - coords = [] - for i in range(0, numberVertices): - coords.append((randomLon(), randomLat())) - return LineString(coords) + return LineString([createPoint() for unused in range(numberVertices)]) def createPoly(): aveRadius = 60 diff --git a/tests/test_constructor.py b/tests/test_constructor.py new file mode 100644 index 0000000..aab36bd --- /dev/null +++ b/tests/test_constructor.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Tests for geojson object constructor +""" + +import unittest + +import geojson + + +class TestGeoJSONConstructor(unittest.TestCase): + + def test_copy_construction(self): + coords = [1, 2] + pt = geojson.Point(coords) + self.assertEquals(geojson.Point(pt), pt) + + def test_nested_constructors(self): + a = [5, 6] + b = [9, 10] + c = [-5, 12] + mp = geojson.MultiPoint([geojson.Point(a), b]) + self.assertEquals(mp.coordinates, [a, b]) + + mls = geojson.MultiLineString([geojson.LineString([a, b]), [a, c]]) + self.assertEquals(mls.coordinates, [[a, b], [a, c]]) + + outer = [a, b, c, a] + poly = geojson.Polygon(geojson.MultiPoint(outer)) + other = [[1, 1], [1, 2], [2, 1], [1, 1]] + poly2 = geojson.Polygon([outer, other]) + self.assertEquals(geojson.MultiPolygon([poly, poly2]).coordinates, + [[outer], [outer, other]]) diff --git a/tests/test_coords.py b/tests/test_coords.py index 3435786..fe242bd 100644 --- a/tests/test_coords.py +++ b/tests/test_coords.py @@ -57,3 +57,7 @@ class CoordsTestCase(unittest.TestCase): self.assertEqual(result['type'], 'MultiPolygon') self.assertEqual(result['coordinates'][0][0][0], (3.78, 9.28)) self.assertEqual(result['coordinates'][-1][-1][-1], (23.18, -34.29)) + + def test_map_invalid(self): + with self.assertRaises(ValueError): + map_coords(lambda x: x, {"type": ""}) diff --git a/tests/test_features.py b/tests/test_features.py index fe295d4..f735a0b 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,4 +1,7 @@ -from io import BytesIO # NOQA +try: + from StringIO import StringIO +except ImportError: + from io import StringIO import unittest import geojson @@ -36,7 +39,7 @@ class FeaturesTest(unittest.TestCase): def test_unicode_properties(self): with open("tests/data.geojson") as file_: obj = geojson.load(file_) - geojson.dumps(obj) + geojson.dump(obj, StringIO()) def test_feature_class(self): """ diff --git a/tests/test_geo_interface.py b/tests/test_geo_interface.py index 7e7cb54..42aff28 100644 --- a/tests/test_geo_interface.py +++ b/tests/test_geo_interface.py @@ -4,6 +4,7 @@ Encoding/decoding custom objects with __geo_interface__ import unittest import geojson +from geojson.mapping import to_mapping class EncodingDecodingTest(unittest.TestCase): @@ -96,8 +97,8 @@ class EncodingDecodingTest(unittest.TestCase): self.assertEqual(actual, self.restaurant_str) # Classes that don't implement geo interface should raise TypeError - self.assertRaises( - TypeError, geojson.dumps, self.restaurant_nogeo, sort_keys=True) + with self.assertRaises(TypeError): + geojson.dumps(self.restaurant_nogeo) def test_encode_nested(self): """ @@ -129,3 +130,13 @@ class EncodingDecodingTest(unittest.TestCase): nested = expected['geometry'] expected = self.restaurant1.__geo_interface__ self.assertEqual(nested, expected) + + def test_invalid(self): + with self.assertRaises(ValueError) as cm: + geojson.loads('{"type":"Point", "coordinates":[[-Infinity, 4]]}') + + self.assertIn('is not JSON compliant', str(cm.exception)) + + def test_mapping(self): + self.assertEquals(to_mapping(geojson.Point([1, 2])), + {"coordinates": [1, 2], "type": "Point"}) diff --git a/tests/test_strict_json.py b/tests/test_strict_json.py index 2dfdfd9..1aeb8f5 100644 --- a/tests/test_strict_json.py +++ b/tests/test_strict_json.py @@ -12,41 +12,45 @@ class StrictJsonTest(unittest.TestCase): """ Ensure Error is raised when encoding nan """ - unstrict = { + self._raises_on_dump({ "type": "Point", "coordinates": (float("nan"), 1.0), - } - self.assertRaises(ValueError, geojson.dumps, unstrict) + }) def test_encode_inf(self): """ Ensure Error is raised when encoding inf or -inf """ - unstrict = { + self._raises_on_dump({ "type": "Point", "coordinates": (float("inf"), 1.0), - } - self.assertRaises(ValueError, geojson.dumps, unstrict) + }) - unstrict = { + self._raises_on_dump({ "type": "Point", "coordinates": (float("-inf"), 1.0), - } - self.assertRaises(ValueError, geojson.dumps, unstrict) + }) + + def _raises_on_dump(self, unstrict): + with self.assertRaises(ValueError): + geojson.dumps(unstrict) def test_decode_nan(self): """ Ensure Error is raised when decoding NaN """ - unstrict = '{"type": "Point", "coordinates": [1.0, NaN]}' - self.assertRaises(ValueError, geojson.loads, unstrict) + self._raises_on_load('{"type": "Point", "coordinates": [1.0, NaN]}') def test_decode_inf(self): """ Ensure Error is raised when decoding Infinity or -Infinity """ - unstrict = '{"type": "Point", "coordinates": [1.0, Infinity]}' - self.assertRaises(ValueError, geojson.loads, unstrict) + self._raises_on_load( + '{"type": "Point", "coordinates": [1.0, Infinity]}') + + self._raises_on_load( + '{"type": "Point", "coordinates": [1.0, -Infinity]}') - unstrict = '{"type": "Point", "coordinates": [1.0, -Infinity]}' - self.assertRaises(ValueError, geojson.loads, unstrict) + def _raises_on_load(self, unstrict): + with self.assertRaises(ValueError): + geojson.loads(unstrict) diff --git a/tests/test_validation.py b/tests/test_validation.py index d104d89..d3abf4c 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -7,33 +7,38 @@ import unittest import geojson +INVALID_POS = (10, 20, 30, 40) +VALID_POS = (10, 20) +VALID_POS_WITH_ELEVATION = (10, 20, 30) + class TestValidationGeometry(unittest.TestCase): def test_invalid_geometry_with_validate(self): - self.assertRaises( - ValueError, geojson.Point, (10, 20, 30, 40), validate=True) + with self.assertRaises(ValueError): + geojson.Point(INVALID_POS, validate=True) def test_invalid_geometry_without_validate(self): - try: - geojson.Point((10, 20, 30)) - geojson.Point((10, 20, 30), validate=False) - except ValueError: - self.fail("Point raised ValueError unexpectedly") + geojson.Point(INVALID_POS) + geojson.Point(INVALID_POS, validate=False) def test_valid_geometry(self): - try: - geojson.Point((10, 20), validate=True) - geojson.Point((10, 20), validate=False) - except ValueError: - self.fail("Point raised ValueError unexpectedly") + geojson.Point(VALID_POS, validate=True) def test_valid_geometry_with_elevation(self): - try: - geojson.Point((10, 20, 30), validate=True) - geojson.Point((10, 20, 30), validate=False) - except ValueError: - self.fail("Point raised ValueError unexpectedly") + geojson.Point(VALID_POS_WITH_ELEVATION, validate=True) + + def test_not_sequence(self): + with self.assertRaises(ValueError) as cm: + geojson.MultiPoint([5], validate=True) + + self.assertIn('each position must be a list', str(cm.exception)) + + def test_not_number(self): + with self.assertRaises(ValueError) as cm: + geojson.MultiPoint([[1, '?']], validate=False) + + self.assertIn('is not a JSON compliant number', str(cm.exception)) class TestValidationGeoJSONObject(unittest.TestCase): @@ -78,8 +83,17 @@ class TestValidationMultipoint(unittest.TestCase): class TestValidationLineString(unittest.TestCase): def test_invalid_linestring(self): - ls = geojson.LineString([(8.919, 44.4074)]) - self.assertEqual(ls.is_valid, False) + with self.assertRaises(ValueError) as cm: + geojson.LineString([(8.919, 44.4074)], validate=True) + + self.assertIn('must be an array of two or more positions', + str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + geojson.LineString([(8.919, 44.4074), [3]], validate=True) + + self.assertIn('a position must have exactly 2 or 3 values', + str(cm.exception)) def test_valid_linestring(self): ls = geojson.LineString([(10, 5), (4, 3)]) @@ -89,6 +103,12 @@ class TestValidationLineString(unittest.TestCase): class TestValidationMultiLineString(unittest.TestCase): def test_invalid_multilinestring(self): + with self.assertRaises(ValueError) as cm: + geojson.MultiLineString([1], validate=True) + + self.assertIn('each line must be a list of positions', + str(cm.exception)) + mls = geojson.MultiLineString([[(10, 5), (20, 1)], []]) self.assertEqual(mls.is_valid, False) @@ -102,6 +122,12 @@ class TestValidationMultiLineString(unittest.TestCase): class TestValidationPolygon(unittest.TestCase): def test_invalid_polygon(self): + with self.assertRaises(ValueError) as cm: + geojson.Polygon([1], validate=True) + + self.assertIn("Each element of a polygon's coordinates must be a list", + str(cm.exception)) + poly1 = geojson.Polygon( [[(2.38, 57.322), (23.194, -20.28), (-120.43, 19.15)]]) self.assertEqual(poly1.is_valid, False) @@ -120,6 +146,12 @@ class TestValidationPolygon(unittest.TestCase): class TestValidationMultiPolygon(unittest.TestCase): def test_invalid_multipolygon(self): + with self.assertRaises(ValueError) as cm: + geojson.MultiPolygon([1], validate=True) + + self.assertIn("Each polygon must be a list of linear rings", + str(cm.exception)) + poly1 = [(2.38, 57.322), (23.194, -20.28), (-120.43, 19.15), (25.44, -17.91)] poly2 = [(2.38, 57.322), (23.194, -20.28), -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-geojson.git _______________________________________________ Pkg-grass-devel mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-grass-devel

