Repository: Toblerity/Shapely
Pull request #979 (2020-09-02 08:33:02)

CLN: remove unused doctest runners from .py files
I don't think they are used at the moment, so cleaning this up. Also, renaming tests/binascii_hex.txt to tests/test_binascii_hex.txt so it gets picked up by pytest.

BTW, with `pytest shapely --doctest-modules` we could actually run the doctests on CI (but that needs some fixes first, as there are some failures at the moment)

coveralls: 
[![Coverage Status](https://coveralls.io/builds/33170750/badge)](https://coveralls.io/builds/33170750)

Coverage increased (+0.06%) to 83.124% when pulling **53bf5a7e9df47a459affd84d7596697341d00be6 on jorisvandenbossche:remove-doctest-runners** into **0e1413ffff281c980ef711d29780cd6f170d8574 on Toblerity:master**.


Patch:
diff --git a/docs/manual.rst b/docs/manual.rst
index 895debe..68a94f0 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -392,7 +392,7 @@ copy.
   >>> LineString(line)
   <shapely.geometry.linestring.LineString object at 0x...>
 
-A `LineString` may also be constructed using a a sequence of mixed `Point`
+A `LineString` may also be constructed using a sequence of mixed `Point`
 instances or coordinate tuples. The individual coordinates are copied into
 the new object.
 
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index 342acf6..a069ea7 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -55,7 +55,7 @@ def dump_coords(geom):
         return geom.exterior.coords[:] + [i.coords[:] for i in geom.interiors]
     elif geom.type.startswith('Multi') or geom.type == 'GeometryCollection':
         # Recursive call
-        return [dump_coords(part) for part in geom]
+        return [dump_coords(part) for part in geom.geoms]
     else:
         raise ValueError('Unhandled geometry type: ' + repr(geom.type))
 
@@ -846,6 +846,16 @@ class BaseMultipartGeometry(BaseGeometry):
         return self.is_empty is False
 
     def __iter__(self):
+        """
+        .. deprecated:: 1.8
+           Iteration over multi-part geometries is deprecated and will be removed in
+           Shapely 2.0. Use the `geoms` property to access the constituent parts of
+           a multi-part geometry.
+        """
+        warn(
+            "Iteration over multi-part geometries is deprecated and will be removed in "
+            "Shapely 2.0. Use the `geoms` property to access the constituent parts of " 
+            "a multi-part geometry.", ShapelyDeprecationWarning, stacklevel=2)
         if not self.is_empty:
             return iter(self.geoms)
         else:
@@ -858,6 +868,16 @@ class BaseMultipartGeometry(BaseGeometry):
             return 0
 
     def __getitem__(self, index):
+        """
+        .. deprecated:: 1.8
+           __getitem__ for multi-part geometries is deprecated and will be removed in
+           Shapely 2.0. Use the `geoms` property to access the constituent parts of
+           a multi-part geometry.
+        """
+        warn(
+            "__getitem__ for multi-part geometries is deprecated and will be removed in "
+            "Shapely 2.0. Use the `geoms` property to access the constituent parts of " 
+            "a multi-part geometry.", ShapelyDeprecationWarning, stacklevel=2)
         if not self.is_empty:
             return self.geoms[index]
         else:
@@ -867,7 +887,7 @@ class BaseMultipartGeometry(BaseGeometry):
         return (
             type(other) == type(self) and
             len(self) == len(other) and
-            all(x == y for x, y in zip(self, other))
+            all(x == y for x, y in zip(self.geoms, other.geoms))
         )
 
     def __ne__(self, other):
@@ -891,7 +911,7 @@ class BaseMultipartGeometry(BaseGeometry):
         if color is None:
             color = "#66cc99" if self.is_valid else "#ff3333"
         return '<g>' + \
-            ''.join(p.svg(scale_factor, color) for p in self) + \
+            ''.join(p.svg(scale_factor, color) for p in self.geoms) + \
             '</g>'
 
 
@@ -991,12 +1011,3 @@ class EmptyGeometry(BaseGeometry):
     def __init__(self):
         """Create an empty geometry."""
         BaseGeometry.__init__(self)
-
-
-def _test():
-    """Test runner"""
-    import doctest
-    doctest.testmod()
-
-if __name__ == "__main__":
-    _test()
diff --git a/shapely/geometry/collection.py b/shapely/geometry/collection.py
index 3c9d333..dd1f679 100644
--- a/shapely/geometry/collection.py
+++ b/shapely/geometry/collection.py
@@ -56,6 +56,8 @@ class GeometryCollection(BaseMultipartGeometry):
 
 def geos_geometrycollection_from_py(ob):
     """Creates a GEOS GeometryCollection from a list of geometries"""
+    if isinstance(ob, BaseMultipartGeometry):
+         ob = ob.geoms
     L = len(ob)
     N = 2
     subs = (c_void_p * L)()
@@ -67,13 +69,3 @@ def geos_geometrycollection_from_py(ob):
         subs[l] = geom
     
     return (lgeos.GEOSGeom_createCollection(7, subs, L), N)
-
-# Test runner
-def _test():
-    import doctest
-    doctest.testmod()
-
-
-if __name__ == "__main__":
-    _test()
-
diff --git a/shapely/geometry/multilinestring.py b/shapely/geometry/multilinestring.py
index 1235470..18071ae 100644
--- a/shapely/geometry/multilinestring.py
+++ b/shapely/geometry/multilinestring.py
@@ -74,7 +74,7 @@ class MultiLineString(BaseMultipartGeometry):
         if stroke_color is None:
             stroke_color = "#66cc99" if self.is_valid else "#ff3333"
         return '<g>' + \
-            ''.join(p.svg(scale_factor, stroke_color) for p in self) + \
+            ''.join(p.svg(scale_factor, stroke_color) for p in self.geoms) + \
             '</g>'
 
 
@@ -142,12 +142,3 @@ def geos_multilinestring_from_py(ob):
         subs[l] = cast(geom, c_void_p)
             
     return (lgeos.GEOSGeom_createCollection(5, subs, L), N)
-
-
-# Test runner
-def _test():
-    import doctest
-    doctest.testmod()
-
-if __name__ == "__main__":
-    _test()
diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py
index 16cd04b..3614b60 100644
--- a/shapely/geometry/multipoint.py
+++ b/shapely/geometry/multipoint.py
@@ -79,7 +79,7 @@ class MultiPoint(BaseMultipartGeometry):
         if fill_color is None:
             fill_color = "#66cc99" if self.is_valid else "#ff3333"
         return '<g>' + \
-            ''.join(p.svg(scale_factor, fill_color) for p in self) + \
+            ''.join(p.svg(scale_factor, fill_color) for p in self.geoms) + \
             '</g>'
 
     @property
diff --git a/shapely/geometry/multipolygon.py b/shapely/geometry/multipolygon.py
index 2434c73..566f33a 100644
--- a/shapely/geometry/multipolygon.py
+++ b/shapely/geometry/multipolygon.py
@@ -93,7 +93,7 @@ class MultiPolygon(BaseMultipartGeometry):
         if fill_color is None:
             fill_color = "#66cc99" if self.is_valid else "#ff3333"
         return '<g>' + \
-            ''.join(p.svg(scale_factor, fill_color) for p in self) + \
+            ''.join(p.svg(scale_factor, fill_color) for p in self.geoms) + \
             '</g>'
 
 
@@ -201,11 +201,3 @@ def geos_multipolygon_from_polygons(arg):
         subs[i] = cast(geom, c_void_p)
 
     return (lgeos.GEOSGeom_createCollection(6, subs, L), N)
-
-# Test runner
-def _test():
-    import doctest
-    doctest.testmod()
-
-if __name__ == "__main__":
-    _test()
diff --git a/shapely/ops.py b/shapely/ops.py
index 7c6d15d..f86dadf 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -103,6 +103,9 @@ class CollectionOperator(object):
         source = None
         if hasattr(lines, 'type') and lines.type == 'MultiLineString':
             source = lines
+        elif hasattr(lines, 'geoms'):
+            # other Multi geometries
+            source = MultiLineString([ls.coords for ls in lines.geoms])
         elif hasattr(lines, '__iter__'):
             try:
                 source = MultiLineString([ls.coords for ls in lines])
@@ -120,6 +123,8 @@ class CollectionOperator(object):
         """
         try:
             L = len(geoms)
+            if isinstance(geoms, BaseMultipartGeometry):
+                geoms = geoms.geoms
         except TypeError:
             geoms = [geoms]
             L = 1
@@ -137,6 +142,8 @@ class CollectionOperator(object):
         """
         try:
             L = len(geoms)
+            if isinstance(geoms, BaseMultipartGeometry):
+                geoms = geoms.geoms
         except TypeError:
             geoms = [geoms]
             L = 1
diff --git a/shapely/strtree.py b/shapely/strtree.py
index 40da40b..c528837 100644
--- a/shapely/strtree.py
+++ b/shapely/strtree.py
@@ -225,7 +225,3 @@ class STRtree:
         result = ctypes.cast(item, ctypes.py_object).value
 
         return result
-
-if __name__ == "__main__":
-    import doctest
-    doctest.testmod()


Test patch:
diff --git a/tests/binascii_hex.txt b/tests/test_binascii_hex.txt
similarity index 100%
rename from tests/binascii_hex.txt
rename to tests/test_binascii_hex.txt
diff --git a/tests/test_getitem.py b/tests/test_getitem.py
index cd31770..1024132 100644
--- a/tests/test_getitem.py
+++ b/tests/test_getitem.py
@@ -1,4 +1,4 @@
-from . import unittest
+from . import unittest, shapely20_deprecated
 from shapely import geometry
 
 
@@ -48,6 +48,7 @@ class CoordsGetItemTestCase(unittest.TestCase):
 
 class MultiGeomGetItemTestCase(unittest.TestCase):
 
+    @shapely20_deprecated
     def test_index_multigeom(self):
         c = [(float(x), float(-x)) for x in range(4)]
         g = geometry.MultiPoint(c)
@@ -56,11 +57,13 @@ class MultiGeomGetItemTestCase(unittest.TestCase):
         self.assertRaises(IndexError, lambda: g[4])
         self.assertRaises(IndexError, lambda: g[-5])
 
+    @shapely20_deprecated
     def test_index_multigeom_misc(self):
         g = geometry.MultiLineString()  # empty
         self.assertRaises(IndexError, lambda: g[0])
         self.assertRaises(TypeError, lambda: g[0.0])
 
+    @shapely20_deprecated
     def test_slice_multigeom(self):
         c = [(float(x), float(-x)) for x in range(4)]
         g = geometry.MultiPoint(c)
diff --git a/tests/test_make_valid.py b/tests/test_make_valid.py
index 3d1108c..78c7fd6 100644
--- a/tests/test_make_valid.py
+++ b/tests/test_make_valid.py
@@ -9,7 +9,7 @@ def test_make_valid_invalid_input():
     geom = Polygon([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)])
     valid = make_valid(geom)
     assert len(valid) == 2
-    assert all(geom.type == 'Polygon' for geom in valid)
+    assert all(geom.type == 'Polygon' for geom in valid.geoms)
 
 
 @requires_geos_38
diff --git a/tests/test_multilinestring.py b/tests/test_multilinestring.py
index 8cc3971..6513bfa 100644
--- a/tests/test_multilinestring.py
+++ b/tests/test_multilinestring.py
@@ -32,8 +32,8 @@ class MultiLineStringTestCase(MultiGeometryTestCase):
 
         # Sub-geometry Access
         geom = MultiLineString([(((0.0, 0.0), (1.0, 2.0)))])
-        self.assertIsInstance(geom[0], LineString)
-        self.assertEqual(dump_coords(geom[0]), [(0.0, 0.0), (1.0, 2.0)])
+        self.assertIsInstance(geom.geoms[0], LineString)
+        self.assertEqual(dump_coords(geom.geoms[0]), [(0.0, 0.0), (1.0, 2.0)])
         with self.assertRaises(IndexError):  # index out of range
             geom.geoms[1]
 
@@ -42,7 +42,6 @@ class MultiLineStringTestCase(MultiGeometryTestCase):
                          {'type': 'MultiLineString',
                           'coordinates': (((0.0, 0.0), (1.0, 2.0)),)})
 
-
     def test_from_multilinestring_z(self):
         coords1 = [(0.0, 1.0, 2.0), (3.0, 4.0, 5.0)]
         coords2 = [(6.0, 7.0, 8.0), (9.0, 10.0, 11.0)]
@@ -85,6 +84,7 @@ class MultiLineStringTestCase(MultiGeometryTestCase):
 
         # TODO: is there an inverse?
 
+    @shapely20_deprecated
     def test_subgeom_access(self):
         line0 = LineString([(0.0, 1.0), (2.0, 3.0)])
         line1 = LineString([(4.0, 5.0), (6.0, 7.0)])
@@ -106,5 +106,18 @@ def test_multilinestring_adapter_deprecated():
         asMultiLineString(coords)
 
 
+def test_iteration_deprecated():
+    geom = MultiLineString([[[5.0, 6.0], [7.0, 8.0]]])
+    with pytest.warns(ShapelyDeprecationWarning, match="Iteration"):
+        for g in geom:
+            pass
+
+
+def test_getitem_deprecated():
+    geom = MultiLineString([[[5.0, 6.0], [7.0, 8.0]]])
+    with pytest.warns(ShapelyDeprecationWarning, match="__getitem__"):
+        part = geom[0]
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(MultiLineStringTestCase)
diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py
index 55f4e9e..5fed9a4 100644
--- a/tests/test_multipoint.py
+++ b/tests/test_multipoint.py
@@ -89,6 +89,7 @@ class MultiPointTestCase(MultiGeometryTestCase):
         pas = asarray(geoma)
         assert_array_equal(pas, array([[1., 2.], [3., 4.]]))
 
+    @shapely20_deprecated
     def test_subgeom_access(self):
         p0 = Point(1.0, 2.0)
         p1 = Point(3.0, 4.0)
@@ -128,5 +129,18 @@ def test_multipoint_array_interface_numpy_deprecated():
         np.array(geom)
 
 
+def test_iteration_deprecated():
+    geom = MultiPoint([[5.0, 6.0], [7.0, 8.0]])
+    with pytest.warns(ShapelyDeprecationWarning, match="Iteration"):
+        for g in geom:
+            pass
+
+
+def test_getitem_deprecated():
+    geom = MultiPoint([[5.0, 6.0], [7.0, 8.0]])
+    with pytest.warns(ShapelyDeprecationWarning, match="__getitem__"):
+        part = geom[0]
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(MultiPointTestCase)
diff --git a/tests/test_multipolygon.py b/tests/test_multipolygon.py
index d1c58dc..143e4a4 100644
--- a/tests/test_multipolygon.py
+++ b/tests/test_multipolygon.py
@@ -74,6 +74,7 @@ class MultiPolygonTestCase(MultiGeometryTestCase):
         self.assertEqual(len(mpa.geoms[0].interiors), 1)
         self.assertEqual(len(mpa.geoms[0].interiors[0].coords), 5)
 
+    @shapely20_deprecated
     def test_subgeom_access(self):
         poly0 = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)])
         poly1 = Polygon([(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)])
@@ -96,3 +97,20 @@ def test_multipolygon_adapter_deprecated():
     holes_coords = [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))]
     with pytest.warns(ShapelyDeprecationWarning, match="proxy geometries"):
         asMultiPolygon([(coords, holes_coords)])
+
+
+def test_iteration_deprecated():
+    geom = MultiPolygon(
+        [(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
+          [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))])])
+    with pytest.warns(ShapelyDeprecationWarning, match="Iteration"):
+        for g in geom:
+            pass
+
+
+def test_getitem_deprecated():
+    geom = MultiPolygon(
+        [(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
+          [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))])])
+    with pytest.warns(ShapelyDeprecationWarning, match="__getitem__"):
+        part = geom[0]
diff --git a/tests/test_ndarrays.py b/tests/test_ndarrays.py
index 4dc6fae..e850c66 100644
--- a/tests/test_ndarrays.py
+++ b/tests/test_ndarrays.py
@@ -21,7 +21,7 @@ class TransposeTestCase(unittest.TestCase):
         arr = numpy.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]])
         tarr = arr.T
         shape = geometry.asMultiPoint(tarr)
-        coords = reduce(lambda x, y: x + y, [list(g.coords) for g in shape])
+        coords = reduce(lambda x, y: x + y, [list(g.coords) for g in shape.geoms])
         self.assertEqual(
             coords,
             [(1.0, 3.0), (1.0, 4.0), (2.0, 4.0), (2.0, 3.0), (1.0, 3.0)]
@@ -54,7 +54,7 @@ class TransposeTestCase(unittest.TestCase):
         arr = numpy.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]])
         tarr = arr.T
         shape = geometry.MultiPoint(tarr)
-        coords = reduce(lambda x, y: x + y, [list(g.coords) for g in shape])
+        coords = reduce(lambda x, y: x + y, [list(g.coords) for g in shape.geoms])
         self.assertEqual(
             coords,
             [(1.0, 3.0), (1.0, 4.0), (2.0, 4.0), (2.0, 3.0), (1.0, 3.0)]
diff --git a/tests/test_polygonize.py b/tests/test_polygonize.py
index cb6feda..a6ae46d 100755
--- a/tests/test_polygonize.py
+++ b/tests/test_polygonize.py
@@ -35,7 +35,7 @@ class PolygonizeTestCase(unittest.TestCase):
 
         result2, dangles, cuts, invalids = polygonize_full(lines2)
         self.assertEqual(len(result2), 2)
-        self.assertTrue(all([isinstance(x, Polygon) for x in result2]))
+        self.assertTrue(all([isinstance(x, Polygon) for x in result2.geoms]))
         self.assertEqual(list(dangles.geoms), [])
         self.assertTrue(all([isinstance(x, LineString) for x in cuts.geoms]))
 
diff --git a/tests/test_shared_paths.py b/tests/test_shared_paths.py
index 7ab2d54..24d6f51 100644
--- a/tests/test_shared_paths.py
+++ b/tests/test_shared_paths.py
@@ -14,23 +14,23 @@ class SharedPaths(unittest.TestCase):
         
         self.assertTrue(isinstance(result, GeometryCollection))
         self.assertTrue(len(result) == 2)
-        a, b = result
+        a, b = result.geoms
         self.assertTrue(isinstance(a, MultiLineString))
         self.assertTrue(len(a) == 1)
-        self.assertEqual(a[0].coords[:], [(5, 0), (10, 0)])
+        self.assertEqual(a.geoms[0].coords[:], [(5, 0), (10, 0)])
         self.assertTrue(b.is_empty)
 
-    def test_shared_paths_forward(self):
+    def test_shared_paths_forward2(self):
         g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)])
         g2 = LineString([(15, 0), (5, 0)])
         result = shared_paths(g1, g2)
         
         self.assertTrue(isinstance(result, GeometryCollection))
         self.assertTrue(len(result) == 2)
-        a, b = result
+        a, b = result.geoms
         self.assertTrue(isinstance(b, MultiLineString))
         self.assertTrue(len(b) == 1)
-        self.assertEqual(b[0].coords[:], [(5, 0), (10, 0)])
+        self.assertEqual(b.geoms[0].coords[:], [(5, 0), (10, 0)])
         self.assertTrue(a.is_empty)
     
     def test_wrong_type(self):
diff --git a/tests/test_split.py b/tests/test_split.py
index 9e3a253..58f759a 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -23,7 +23,7 @@ class TestSplitGeometry(unittest.TestCase):
 				raise ValueError
 		elif expected_chunks == 1:
 			# not split --> expected equal to line
-			self.assertTrue(s[0].equals(geom))
+			self.assertTrue(s.geoms[0].equals(geom))
 
 	def test_split_closed_line_with_point(self):
 		# point at start/end of closed ring -> return equal
@@ -175,15 +175,15 @@ class TestSplitClosedRing(TestSplitGeometry):
 		splitter = Point([0.0, 0.5])
 		self.helper(self.ls, splitter, 2)
 		result = split(self.ls, splitter)
-		assert result[0].coords[:] == [(0, 0), (0.0, 0.5)]
-		assert result[1].coords[:] == [(0.0, 0.5), (0, 1), (1, 1), (1, 0), (0, 0)]
+		assert result.geoms[0].coords[:] == [(0, 0), (0.0, 0.5)]
+		assert result.geoms[1].coords[:] == [(0.0, 0.5), (0, 1), (1, 1), (1, 0), (0, 0)]
 
 		# previously failed, see GH#585
 		splitter = Point([0.5, 0.0])
 		self.helper(self.ls, splitter, 2)
 		result = split(self.ls, splitter)
-		assert result[0].coords[:] == [(0, 0), (0, 1), (1, 1), (1, 0), (0.5, 0)]
-		assert result[1].coords[:] == [(0.5, 0), (0, 0)]
+		assert result.geoms[0].coords[:] == [(0, 0), (0, 1), (1, 1), (1, 0), (0.5, 0)]
+		assert result.geoms[1].coords[:] == [(0.5, 0), (0, 0)]
 
 		splitter = Point([2.0, 2.0])
 		self.helper(self.ls, splitter, 1)
diff --git a/tests/test_voronoi_diagram.py b/tests/test_voronoi_diagram.py
index d3fd902..0d3ede6 100644
--- a/tests/test_voronoi_diagram.py
+++ b/tests/test_voronoi_diagram.py
@@ -39,7 +39,7 @@ def test_edges():
     regions = voronoi_diagram(mp, edges=True)
 
     assert len(regions) == 1
-    assert all(r.type == 'LineString' for r in regions)
+    assert all(r.type == 'LineString' for r in regions.geoms)
 
 
 @requires_geos_35
@@ -50,7 +50,7 @@ def test_smaller_envelope():
     regions = voronoi_diagram(mp, envelope=poly)
 
     assert len(regions) == 2
-    assert sum(r.area for r in regions) > poly.area
+    assert sum(r.area for r in regions.geoms) > poly.area
 
 
 @requires_geos_35
@@ -64,7 +64,7 @@ def test_larger_envelope():
     regions = voronoi_diagram(mp, envelope=poly)
 
     assert len(regions) == 2
-    assert sum(r.area for r in regions) == poly.area
+    assert sum(r.area for r in regions.geoms) == poly.area
 
 
 @requires_geos_35