Repository: Toblerity/Shapely
Pull request #1071 (2021-01-23 04:35:22)

fix the multi libgeos for pyinstaller
If we have installed the `finoa`, `shapely`, `rasterio`, and package them all use Pyinstaller. 

There are multi `libgeos_c-*.so.*`, not only one. 

So, we should check the length of geos_pyinstaller_so greater and equal to 1.

![image](https://user-images.githubusercontent.com/5787131/105569030-159d7a00-5d79-11eb-9575-23d1f12f5bbc.png)


![image](https://user-images.githubusercontent.com/5787131/105569013-fef72300-5d78-11eb-9422-087ea3893e13.png)



Patch:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 745913d..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,126 +0,0 @@
-language: python
-dist: xenial
-
-cache:
-  directories:
-    - $HOME/geosinstall
-    - ~/.cache/pip
-
-matrix:
-  include:
-    - python: "3.5"
-      env:
-        GEOS_VERSION="3.5.2"
-        SPEEDUPS=1
-        NUMPY=1
-    - python: "3.5"
-      env:
-        GEOS_VERSION="3.5.2"
-        SPEEDUPS=0
-        NUMPY=1
-    - python: "3.5"
-      env:
-        GEOS_VERSION="3.5.2"
-        SPEEDUPS=0
-        NUMPY=0
-    - python: "3.6"
-      env:
-        GEOS_VERSION="3.6.4"
-        SPEEDUPS=1
-        NUMPY=1
-    - python: "3.6"
-      env:
-        GEOS_VERSION="3.6.4"
-        SPEEDUPS=0
-        NUMPY=1
-    - python: "3.6"
-      env:
-        GEOS_VERSION="3.6.4"
-        SPEEDUPS=0
-        NUMPY=0
-    - python: "3.7"
-      env:
-        GEOS_VERSION="3.7.3"
-        SPEEDUPS=1
-        NUMPY=1
-    - python: "3.7"
-      env:
-        GEOS_VERSION="3.7.3"
-        SPEEDUPS=0
-        NUMPY=1
-    - python: "3.7"
-      env:
-        GEOS_VERSION="3.7.3"
-        SPEEDUPS=0
-        NUMPY=0
-    - python: "3.8"
-      env:
-        GEOS_VERSION="3.8.1"
-        SPEEDUPS=1
-        NUMPY=1
-    - python: "3.8"
-      env:
-        GEOS_VERSION="3.8.1"
-        SPEEDUPS=0
-        NUMPY=1
-    - python: "3.8"
-      env:
-        GEOS_VERSION="3.8.1"
-        SPEEDUPS=0
-        NUMPY=0
-    - python: "3.9"
-      env:
-        GEOS_VERSION="3.9.0beta2"
-        SPEEDUPS=1
-        NUMPY=1
-    - python: "3.9"
-      env:
-        GEOS_VERSION="3.9.0beta2"
-        SPEEDUPS=0
-        NUMPY=1
-    - python: "3.9"
-      env:
-        GEOS_VERSION="3.9.0beta2"
-        SPEEDUPS=0
-        NUMPY=0
-    - python: "3.9-dev"
-      env:
-        GEOS_VERSION="master"
-        SPEEDUPS=1
-        NUMPY=1
-  allow_failures:
-    - python: "3.9-dev"
-      env:
-        GEOS_VERSION="master"
-        SPEEDUPS=1
-        NUMPY=1
-
-before_install:
-  - export GEOS_INSTALL=$HOME/geosinstall/geos-$GEOS_VERSION
-  - ./ci/install_geos.sh
-  - pip install --disable-pip-version-check --upgrade pip
-  - pip install --upgrade wheel
-  # if building with speedups install cython
-  - if [ "$SPEEDUPS" == "1" ]; then pip install --install-option="--no-cython-compile" cython; fi
-  # if testing without numpy explicitly remove it
-  - if [ "$NUMPY" == "0" ]; then pip uninstall --yes numpy; fi
-  # convert SPEEDUPS to --with-speedups/--without-speedups
-  - if [ "$SPEEDUPS" == "1" ]; then SPEEDUPS_FLAG=--with-speedups; else SPEEDUPS_FLAG=--without-speedups; fi
-  - pip install --upgrade coveralls pytest-cov pytest>=3.8
-
-install:
-  - export GEOS_CONFIG=$HOME/geosinstall/geos-$GEOS_VERSION/bin/geos-config
-  - pip install -v -e .[all]
-
-script:
-  - export LD_LIBRARY_PATH=$HOME/geosinstall/geos-$GEOS_VERSION/lib
-  - export DYLD_LIBRARY_PATH=$HOME/geosinstall/geos-$GEOS_VERSION/lib
-  - python -c "from shapely.geos import geos_version; print(geos_version)"
-  - python -m pytest --cov shapely --cov-report term-missing "${SPEEDUPS_FLAG}"
-  - if [[ "$TRAVIS_PYTHON_VERSION" == "3.8" && "$NUMPY" == "1" && "$SPEEDUPS" == "1" ]]; then python -m pytest shapely --doctest-modules; fi
-
-after_success:
-  - coveralls || echo "!! intermittent coveralls failure"
-
-notifications:
-    email: false
diff --git a/CHANGES.txt b/CHANGES.txt
index 6e996c2..5339dce 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,17 +1,30 @@
 Changes
 =======
 
-1.8.0 (TBD)
+1.8a1 (TBD)
 -----------
 
+Deprecations:
+
+- ops.cascaded_union will be removed in version 2.0.0.
+- More ...
+
 New features:
 
+- Add a normalize() method to geometry classes, exposing the GEOSNormalize
+  algorithm (#1090).
+- Initialize STRtree with a capacity of 10 items per node (#1070).
 - Load libraries relocated to shapely/.libs by auditwheel versions < 3.1 or
   relocated to Shapely.libs by auditwheel versions >= 3.1.
 - shapely.ops.voronoi_diagram() computes the Voronoi Diagram of a geometry or
   geometry collection (#833, #851).
 - shapely.validation.make_valid() fixes invalid geometries (#883)
 
+Bug fixes:
+
+- Several issues with duplication and order of vertices in ops.substring have
+  been fixed (#1008).
+
 1.7.1 (2020-08-20)
 ------------------
 
diff --git a/CITATION.txt b/CITATION.txt
index b3ea848..d3c3946 100644
--- a/CITATION.txt
+++ b/CITATION.txt
@@ -1,7 +1,7 @@
 If you use Shapely for any published work, please cite it using the reference
 below:
 
-@Misc{,
+@Misc{shapely,
   author =    {Sean Gillies and others},
   organization = {toblerity.org},
   title =     {Shapely: manipulation and analysis of geometric objects},
diff --git a/README.rst b/README.rst
index 4adbb71..3005d66 100644
--- a/README.rst
+++ b/README.rst
@@ -2,10 +2,10 @@
 Shapely
 =======
 
-|travis| |appveyor| |coveralls|
+|github-actions| |appveyor| |coveralls|
 
-.. |travis| image:: https://travis-ci.org/Toblerity/Shapely.svg?branch=master
-   :target: https://travis-ci.org/Toblerity/Shapely
+.. |github-actions| image:: https://github.com/Toblerity/Shapely/workflows/Tests%20(Linux)/badge.svg
+   :target: https://github.com/Toblerity/Shapely/actions
 
 .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/Toblerity/Shapely?branch=master&svg=true
    :target: https://ci.appveyor.com/project/frsci/shapely?branch=master
diff --git a/appveyor.yml b/appveyor.yml
index 1e2cf23..77365bf 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -22,7 +22,7 @@ environment:
       GEOS_VERSION: "3.8.1"
     - PYTHON: "C:\\Python39-x64"
       ARCH: x64
-      GEOS_VERSION: "3.9.0beta2"
+      GEOS_VERSION: "3.9.1"
 
 
 install:
diff --git a/ci/install_geos.cmd b/ci/install_geos.cmd
index 966ec30..24f78dd 100644
--- a/ci/install_geos.cmd
+++ b/ci/install_geos.cmd
@@ -21,7 +21,7 @@ if exist %GEOS_INSTALL% (
   cd build
   cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=%GEOS_INSTALL% .. || exit /B 2
   cmake --build . || exit /B 3
-  :: ctest . || exit /B 4
+  ctest . || exit /B 4
   ctest .
   cmake --install . || exit /B 5
   cd ..
diff --git a/docs/manual.rst b/docs/manual.rst
index e043806..0282ad6 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -207,7 +207,9 @@ General Attributes and Methods
   This can be thought of as a measure of the robustness of a geometry, where larger values of
   minimum clearance indicate a more robust geometry. If no minimum clearance exists for a geometry,
   such as a point, this will return `math.infinity`.
-
+  
+  `New in Shapely 1.7.1`
+  
   Requires GEOS 3.6 or higher.
 
 .. code-block:: pycon
@@ -775,14 +777,12 @@ Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple.
   (-1.0, -1.0, 2.0, 2.0)
 
 Its members are instances of `Polygon` and are accessed via the ``geoms``
-property or via the iterator protocol using ``in`` or ``list()``.
+property.
 
 .. code-block:: pycon
 
   >>> len(polygons.geoms)
   3
-  >>> len(polygons)
-  3
 
 .. _empties:
 
@@ -1184,6 +1184,20 @@ A line's endpoints are part of its `boundary` and are therefore not contained.
   >>> [p.wkt for p in contained]
   ['POINT (0.5000000000000000 0.5000000000000000)']
 
+.. method:: object.covers(other)
+
+  Returns ``True`` if every point of `other` is a point on the interior or
+  boundary of `object`. This is similar to ``object.contains(other)`` except
+  that this does not require any interior points of `other` to lie in the 
+  interior of `object`.
+
+.. method:: object.covered_by(other)
+
+  Returns ``True`` if every point of `object` is a point on the interior or
+  boundary of `other`. This is equivalent to ``other.covers(object)``.
+  
+  `New in version 1.8`.
+
 .. method:: object.crosses(other)
 
   Returns ``True`` if the `interior` of the object intersects the `interior` of
@@ -1649,11 +1663,11 @@ mileage may vary when cleaning surfaces.
   True
   >>> clean
   <shapely.geometry.multipolygon.MultiPolygon object at ...>
-  >>> len(clean)
+  >>> len(clean.geoms)
   2
-  >>> list(clean[0].exterior.coords)
+  >>> list(clean.geoms[0].exterior.coords)
   [(0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (0.0, 0.0)]
-  >>> list(clean[1].exterior.coords)
+  >>> list(clean.geoms[1].exterior.coords)
   [(1.0, 1.0), (2.0, 2.0), (2.0, 0.0), (1.0, 1.0)]
 
 Buffering splits the polygon in two at the point where they touch.
@@ -2147,7 +2161,7 @@ using functions in the :mod:`shapely.ops` module.
     ...     ((1, 1), (100, 100)),
     ...     ]
     >>> result, dangles, cuts, invalids = polygonize_full(lines)
-    >>> len(result)
+    >>> len(result.geoms)
     2
     >>> list(result.geoms)
     [<shapely.geometry.polygon.Polygon object at ...>, <shapely.geometry.polygon.Polygon object at ...>]
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index 51ee01e..a0a4f00 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -388,6 +388,9 @@ def prototype(lgeos, geos_version):
     lgeos.GEOSGeom_getDimensions.restype = c_int
     lgeos.GEOSGeom_getDimensions.argtypes = [c_void_p]
 
+    lgeos.GEOSNormalize.restype = c_int
+    lgeos.GEOSNormalize.argtypes = [c_void_p]
+
     # Misc functions
 
     lgeos.GEOSArea.restype = c_double
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index ac4ada5..b97de9a 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -637,6 +637,27 @@ class BaseGeometry(object):
             op = self.impl['simplify']
         return geom_factory(op(self, tolerance))
 
+    def normalize(self):
+        """Converts geometry to normal form (or canonical form).
+
+        This method orders the coordinates, rings of a polygon and parts of
+        multi geometries consistently. Typically useful for testing purposes
+        (for example in combination with `equals_exact`).
+
+        Examples
+        --------
+        >>> from shapely.wkt import loads
+        >>> p = loads("MULTILINESTRING((0 0, 1 1), (3 3, 2 2))")
+        >>> p.normalize().wkt
+        'MULTILINESTRING ((2 2, 3 3), (0 0, 1 1))'
+        """
+        # self.impl['normalize'](self)
+        if self._geom is None:
+            raise ValueError("Null geometry supports no operations")
+        geom_cloned = lgeos.GEOSGeom_clone(self._geom)
+        lgeos.GEOSNormalize(geom_cloned)
+        return geom_factory(geom_cloned)
+
     # Binary operations
     # -----------------
 
@@ -870,6 +891,11 @@ class BaseMultipartGeometry(BaseGeometry):
             return iter([])
 
     def __len__(self):
+        warn(
+            "__len__ for multi-part geometries is deprecated and will be removed in "
+            "Shapely 2.0. Check the length of the `geoms` property instead to get the "
+            " number of parts of a multi-part geometry.",
+            ShapelyDeprecationWarning, stacklevel=2)
         if not self.is_empty:
             return len(self.geoms)
         else:
@@ -894,7 +920,7 @@ class BaseMultipartGeometry(BaseGeometry):
     def __eq__(self, other):
         return (
             type(other) == type(self) and
-            len(self) == len(other) and
+            len(self.geoms) == len(other.geoms) and
             all(x == y for x, y in zip(self.geoms, other.geoms))
         )
 
diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py
index bf13f85..6464e9f 100644
--- a/shapely/geometry/multipoint.py
+++ b/shapely/geometry/multipoint.py
@@ -48,7 +48,7 @@ class MultiPoint(BaseMultipartGeometry):
         """
         super(MultiPoint, self).__init__()
 
-        if points is None or len(points) == 0:
+        if points is None or (not isinstance(points, MultiPoint) and len(points) == 0):
             # allow creation of empty multipoints, to support unpickling
             pass
         else:
diff --git a/shapely/geos.py b/shapely/geos.py
index cb8b232..3e65340 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -85,7 +85,7 @@ if sys.platform.startswith('linux'):
 
     elif hasattr(sys, 'frozen'):
         geos_pyinstaller_so = glob.glob(os.path.join(sys.prefix, 'libgeos_c-*.so.*'))
-        if len(geos_pyinstaller_so) == 1:
+        if len(geos_pyinstaller_so) >= 1:
             _lgeos = CDLL(geos_pyinstaller_so[0])
             LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
     elif exists_conda_env():
@@ -699,6 +699,7 @@ class LGEOS330(LGEOSBase):
         self.methods['simplify'] = self.GEOSSimplify
         self.methods['topology_preserve_simplify'] = \
             self.GEOSTopologyPreserveSimplify
+        self.methods['normalize'] = self.GEOSNormalize
         self.methods['cascaded_union'] = self.GEOSUnionCascaded
 
         def parallel_offset(geom, distance, resolution=16, join_style=1,
diff --git a/shapely/impl.py b/shapely/impl.py
index d35a6da..9b206e9 100644
--- a/shapely/impl.py
+++ b/shapely/impl.py
@@ -88,6 +88,7 @@ IMPL330 = {
     'simplify': (UnaryTopologicalOp, 'simplify'),
     'topology_preserve_simplify':
         (UnaryTopologicalOp, 'topology_preserve_simplify'),
+    'normalize': (UnaryTopologicalOp, 'normalize'),
     #
     'difference': (BinaryTopologicalOp, 'difference'),
     'intersection': (BinaryTopologicalOp, 'intersection'),
diff --git a/shapely/iterops.py b/shapely/iterops.py
index a0c7deb..4011e6d 100644
--- a/shapely/iterops.py
+++ b/shapely/iterops.py
@@ -1,7 +1,9 @@
 """
 Iterative forms of operations
 """
+import warnings
 
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.topology import Delegating
 
 
@@ -11,6 +13,10 @@ class IterOp(Delegating):
     """
 
     def __call__(self, context, iterator, value=True):
+        warnings.warn(
+            "The '{0}' function is deprecated and will be removed in "
+            "Shapely 2.0".format(self._name),
+            ShapelyDeprecationWarning, stacklevel=2)
         if context._geom is None:
             raise ValueError("Null geometry supports no operations")
         for item in iterator:
diff --git a/shapely/ops.py b/shapely/ops.py
index 7c50c7f..ccbbc86 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -129,9 +129,9 @@ class CollectionOperator(object):
             "Use 'unary_union()' instead.",
             ShapelyDeprecationWarning, stacklevel=2)
         try:
-            L = len(geoms)
             if isinstance(geoms, BaseMultipartGeometry):
                 geoms = geoms.geoms
+            L = len(geoms)
         except TypeError:
             geoms = [geoms]
             L = 1
@@ -148,9 +148,9 @@ class CollectionOperator(object):
         prefered method for dissolving many polygons.
         """
         try:
-            L = len(geoms)
             if isinstance(geoms, BaseMultipartGeometry):
                 geoms = geoms.geoms
+            L = len(geoms)
         except TypeError:
             geoms = [geoms]
             L = 1
@@ -429,14 +429,20 @@ class SplitOp(object):
         assert(isinstance(line, LineString))
         assert(isinstance(splitter, LineString) or isinstance(splitter, MultiLineString))
 
-        if splitter.crosses(line):
-            # The lines cross --> return multilinestring from the split
-            return line.difference(splitter)
-        elif splitter.relate_pattern(line, '1********'):
+        # |    s\l   | Interior | Boundary | Exterior |
+        # |----------|----------|----------|----------|
+        # | Interior |  0 or F  |    *     |    *     |   At least one of these two must be 0
+        # | Boundary |  0 or F  |    *     |    *     |   So either '0********' or '[0F]**0*****'
+        # | Exterior |    *     |    *     |    *     |   No overlapping interiors ('1********')
+        relation = splitter.relate(line)
+        if relation[0] == '1':
             # The lines overlap at some segment (linear intersection of interiors)
             raise ValueError('Input geometry segment overlaps with the splitter.')
+        elif relation[0] == '0' or relation[3] == '0':
+            # The splitter crosses or touches the line's interior --> return multilinestring from the split
+            return line.difference(splitter)
         else:
-            # The lines do not cross --> return collection with identity line
+            # The splitter does not cross or touch the line's interior --> return collection with identity line
             return [line]
 
     @staticmethod
diff --git a/shapely/strtree.py b/shapely/strtree.py
index c528837..70bee19 100644
--- a/shapely/strtree.py
+++ b/shapely/strtree.py
@@ -82,8 +82,8 @@ class STRtree:
         self._geoms = list(geoms)
 
     def _init_tree_handle(self, geoms):
-        # GEOS STRtree capacity has to be > 1
-        self._tree_handle = lgeos.GEOSSTRtree_create(max(2, len(geoms)))
+        node_capacity = 10
+        self._tree_handle = lgeos.GEOSSTRtree_create(node_capacity)
         for geom in geoms:
             lgeos.GEOSSTRtree_insert(self._tree_handle, geom._geom, ctypes.py_object(geom))
 
diff --git a/shapely/topology.py b/shapely/topology.py
index 1618046..d4e56b1 100644
--- a/shapely/topology.py
+++ b/shapely/topology.py
@@ -23,6 +23,7 @@ class Validating(object):
 class Delegating(Validating):
 
     def __init__(self, name):
+        self._name = name
         self.fn = lgeos.methods[name]
 
     def _check_topology(self, err, *geoms):


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..628ef2d
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,174 @@
+name: Tests (Linux)
+
+on: [push, pull_request]
+
+jobs:
+  test:
+    name: Python ${{ matrix.python }} (GEOS ${{ matrix.geos }}, speedups ${{ matrix.speedups }}, numpy ${{ matrix.numpy || 'not installed' }})
+    # runs-on: ubuntu-latest
+    runs-on: ubuntu-16.04
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          # 2015
+          - python: 3.5
+            geos: 3.5.2
+            numpy: 1.10.4
+            speedups: 1
+          - python: 3.5
+            geos: 3.5.2
+            numpy: 1.10.4
+            speedups: 0
+          - python: 3.5
+            geos: 3.5.2
+            speedups: 0
+          # 2017
+          - python: 3.6
+            geos: 3.6.4
+            numpy: 1.13.3
+            speeedups: 1
+          - python: 3.6
+            geos: 3.6.4
+            numpy: 1.13.3
+            speeedups: 0
+          - python: 3.6
+            geos: 3.6.4
+            speeedups: 0
+          # 2018
+          - python: 3.7
+            geos: 3.7.3
+            numpy: 1.15.4
+            speedups: 1
+          - python: 3.7
+            geos: 3.7.3
+            numpy: 1.15.4
+            speedups: 0
+          - python: 3.7
+            geos: 3.7.3
+            speedups: 0
+          # 2019
+          - python: 3.8
+            geos: 3.8.1
+            numpy: 1.17.5
+            speedups: 1
+          - python: 3.8
+            geos: 3.8.1
+            numpy: 1.17.5
+            speedups: 0
+          - python: 3.8
+            geos: 3.8.1
+            speedups: 0
+          # 2020
+          - python: 3.9
+            geos: 3.9.1
+            numpy: 1.19.5
+            speedups: 1
+          - python: 3.9
+            geos: 3.9.1
+            numpy: 1.19.5
+            speedups: 0
+          - python: 3.9
+            geos: 3.9.1
+            speedups: 0
+          # dev
+          - python: 3.9
+            geos: master
+            speedups: 1
+
+    env:
+      GEOS_VERSION: ${{ matrix.geos }}
+      GEOS_INSTALL: ${{ github.workspace }}/geosinstall/geos-${{ matrix.geos }}
+      SPEEDUPS: ${{ matrix.speedups }}
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Set up Python ${{ matrix.python }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python }}
+
+      - name: Cache GEOS and pip packages
+        uses: actions/cache@v2
+        with:
+          key: ${{ runner.os }}-geos-${{ matrix.geos }}
+          path: |
+            ~/.cache/pip
+            ${{ github.workspace }}/geosinstall
+
+      - name: Install GEOS
+        shell: bash
+        run: |
+          ./ci/install_geos.sh
+          cd ${{ github.workspace }}
+
+      - name: Install python dependencies
+        shell: bash
+        run: |
+          pip install --disable-pip-version-check --upgrade pip
+          pip install --upgrade wheel
+          if [ "$GEOS_VERSION" = "master" ]; then
+            pip install --upgrade --pre Cython numpy;
+          elif [ "$SPEEDUPS" == "1" ]; then
+            pip install --install-option="--no-cython-compile" cython;
+            pip install --upgrade numpy==${{ matrix.numpy }};
+          elif [ -n "${{ matrix.numpy }}" ]; then
+            pip install --upgrade numpy==${{ matrix.numpy }};
+          else
+            pip uninstall --yes numpy;
+          fi
+          pip install --upgrade coveralls pytest-cov pytest
+          pip list
+
+      - name: Set environment variables
+        shell: bash
+        run: |
+          echo "LD_LIBRARY_PATH=$GEOS_INSTALL/lib" >> $GITHUB_ENV
+          echo $GEOS_INSTALL/bin >> $GITHUB_PATH
+
+      - name: Build and install Shapely
+        shell: bash
+        run: |
+          pip install -v -e .
+
+      - name: Overview of the Python environment (pip list)
+        shell: bash
+        run: |
+          pip list
+
+      - name: Run tests
+        shell: bash
+        continue-on-error: ${{ matrix.geos == 'master' }}
+        run: |
+          if [ "$SPEEDUPS" == "1" ]; then SPEEDUPS_FLAG=--with-speedups; else SPEEDUPS_FLAG=--without-speedups; fi
+          python -c "from shapely import geos; print(geos.geos_version_string)"
+          python -c "from shapely import speedups; print(speedups.enabled)"
+          python -m pytest --cov shapely --cov-report term-missing "${SPEEDUPS_FLAG}"
+
+      - name: Run doctests
+        if: ${{ matrix.python == '3.8' && matrix.numpy && matrix.speedups == '1' }}
+        shell: bash
+        run: |
+          python -m pytest shapely --doctest-modules
+
+      - name: Upload coverage
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          COVERALLS_PARALLEL: true
+        shell: bash
+        run: |
+          coveralls --service=github || echo "!! intermittent coveralls failure"
+
+  coveralls:
+    name: Indicate completion to coveralls.io
+    needs: test
+    runs-on: ubuntu-latest
+    container: python:3-slim
+    steps:
+    - name: Finished
+      run: |
+        pip3 install --upgrade coveralls
+        coveralls --finish || echo "!! intermittent coveralls failure"
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/tests/__init__.py b/tests/__init__.py
index 223f8b6..d7bcf52 100755
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -17,7 +17,7 @@ except ImportError:
     numpy = False
     numpy_version = 'not available'
 
-# Show some diagnostic information; handy for Travis CI
+# Show some diagnostic information; handy for CI
 print('Python version: ' + sys.version.replace('\n', ' '))
 print('GEOS version: ' + geos_version_string)
 print('Numpy version: ' + numpy_version)
diff --git a/tests/test_collection.py b/tests/test_collection.py
index e707507..09b5c11 100644
--- a/tests/test_collection.py
+++ b/tests/test_collection.py
@@ -31,7 +31,7 @@ def test_empty(geom):
     assert geom.type == "GeometryCollection"
     assert geom.type == geom.geom_type
     assert geom.is_empty
-    assert len(geom) == 0
+    assert len(geom.geoms) == 0
     assert geom.geoms == []
 
 
@@ -53,7 +53,7 @@ def test_child_with_deleted_parent():
 def test_from_geojson(geometrycollection_geojson):
     geom = shape(geometrycollection_geojson)
     assert geom.geom_type == "GeometryCollection"
-    assert len(geom) == 2
+    assert len(geom.geoms) == 2
 
     geom_types = [g.geom_type for g in geom.geoms]
     assert "Point" in geom_types
@@ -95,3 +95,22 @@ def test_geometrycollection_adapter_deprecated(geometrycollection_geojson):
     with pytest.warns(ShapelyDeprecationWarning):
         asShape(d)
 
+
+@pytest.mark.parametrize('geom', [
+    GeometryCollection(),
+    shape({"type": "GeometryCollection", "geometries": []}),
+    shape({"type": "GeometryCollection", "geometries": [
+        {"type": "Point", "coordinates": ()},
+        {"type": "LineString", "coordinates": (())}
+    ]}),
+    wkt.loads('GEOMETRYCOLLECTION EMPTY'),
+])
+def test_len_empty_deprecated(geom):
+    with pytest.warns(ShapelyDeprecationWarning, match="__len__"):
+        assert len(geom) == 0
+
+
+def test_len_deprecated(geometrycollection_geojson):
+    geom = shape(geometrycollection_geojson)
+    with pytest.warns(ShapelyDeprecationWarning, match="__len__"):
+        assert len(geom) == 2
diff --git a/tests/test_empty_polygons.py b/tests/test_empty_polygons.py
index 89f42e7..d740a21 100644
--- a/tests/test_empty_polygons.py
+++ b/tests/test_empty_polygons.py
@@ -19,4 +19,4 @@ def test_multipolygon_empty_polygon():
 
 def test_multipolygon_empty_among_polygon():
     """An empty polygon passed to MultiPolygon() is ignored."""
-    assert len(MultiPolygon([Point(0,0).buffer(1.0), Polygon()])) == 1
+    assert len(MultiPolygon([Point(0,0).buffer(1.0), Polygon()]).geoms) == 1
diff --git a/tests/test_iterops.py b/tests/test_iterops.py
index 5341896..3962933 100644
--- a/tests/test_iterops.py
+++ b/tests/test_iterops.py
@@ -1,13 +1,17 @@
 """Test operator iterations
 """
-from . import unittest
+from . import unittest, shapely20_deprecated
 from shapely import iterops
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.geometry import Point, Polygon
 from shapely.geos import TopologicalError
 
+import pytest
+
 
 class IterOpsTestCase(unittest.TestCase):
 
+    @shapely20_deprecated
     def test_iterops(self):
 
         coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0))
@@ -43,7 +47,7 @@ class IterOpsTestCase(unittest.TestCase):
                  for p in points))),
             [[(0.5, 0.5)]])
 
-
+    @shapely20_deprecated
     def test_err(self):
         # bowtie polygon.
         coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0))
@@ -55,6 +59,7 @@ class IterOpsTestCase(unittest.TestCase):
             all([isinstance(x, Polygon)
                  for x in iterops.intersects(polygon, points, True)]))
 
+    @shapely20_deprecated
     def test_topological_error(self):
         p1 = [(339, 346), (459, 346), (399, 311), (340, 277), (399, 173),
               (280, 242), (339, 415), (280, 381), (460, 207), (339, 346)]
@@ -67,3 +72,12 @@ class IterOpsTestCase(unittest.TestCase):
 
         with self.assertRaises(TopologicalError):
             list(iterops.within(polygon1, [polygon2]))
+
+    def test_iterops_deprecated(self):
+
+        coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0))
+        polygon = Polygon(coords)
+        points = [Point(0.5, 0.5), Point(2.0, 2.0)]
+
+        with pytest.warns(ShapelyDeprecationWarning, match="'contains' function"):
+            list(iterops.contains(polygon, points, True))
diff --git a/tests/test_make_valid.py b/tests/test_make_valid.py
index 78c7fd6..2166467 100644
--- a/tests/test_make_valid.py
+++ b/tests/test_make_valid.py
@@ -8,7 +8,7 @@ from tests.conftest import requires_geos_38
 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 len(valid.geoms) == 2
     assert all(geom.type == 'Polygon' for geom in valid.geoms)
 
 
diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py
index b9449e9..afbffaf 100644
--- a/tests/test_multipoint.py
+++ b/tests/test_multipoint.py
@@ -140,3 +140,9 @@ 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_len_deprecated():
+    geom = MultiPoint([[5.0, 6.0], [7.0, 8.0]])
+    with pytest.warns(ShapelyDeprecationWarning, match="__len__"):
+        assert len(geom) == 2
diff --git a/tests/test_operations.py b/tests/test_operations.py
index b1d5b88..23dfd08 100644
--- a/tests/test_operations.py
+++ b/tests/test_operations.py
@@ -92,3 +92,13 @@ class OperationsTestCase(unittest.TestCase):
         assert(empty_line.is_empty)
         with pytest.raises(ValueError):
             empty_line.interpolate(.5, normalized=True)
+
+    def test_normalize(self):
+        point = Point(1, 1)
+        result = point.normalize()
+        assert result == point
+
+        line = loads("MULTILINESTRING ((1 1, 0 0), (1 1, 1 2))")
+        result = line.normalize()
+        expected = loads("MULTILINESTRING ((1 1, 1 2), (0 0, 1 1))")
+        assert result == expected
diff --git a/tests/test_polygonize.py b/tests/test_polygonize.py
index 53584ad..a67ef8d 100755
--- a/tests/test_polygonize.py
+++ b/tests/test_polygonize.py
@@ -32,7 +32,7 @@ class PolygonizeTestCase(unittest.TestCase):
             ]
 
         result2, dangles, cuts, invalids = polygonize_full(lines2)
-        self.assertEqual(len(result2), 2)
+        self.assertEqual(len(result2.geoms), 2)
         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 b89394d..b033060 100644
--- a/tests/test_shared_paths.py
+++ b/tests/test_shared_paths.py
@@ -11,10 +11,10 @@ class SharedPaths(unittest.TestCase):
         result = shared_paths(g1, g2)
         
         self.assertTrue(isinstance(result, GeometryCollection))
-        self.assertTrue(len(result) == 2)
+        self.assertTrue(len(result.geoms) == 2)
         a, b = result.geoms
         self.assertTrue(isinstance(a, MultiLineString))
-        self.assertTrue(len(a) == 1)
+        self.assertTrue(len(a.geoms) == 1)
         self.assertEqual(a.geoms[0].coords[:], [(5, 0), (10, 0)])
         self.assertTrue(b.is_empty)
 
@@ -24,10 +24,10 @@ class SharedPaths(unittest.TestCase):
         result = shared_paths(g1, g2)
         
         self.assertTrue(isinstance(result, GeometryCollection))
-        self.assertTrue(len(result) == 2)
+        self.assertTrue(len(result.geoms) == 2)
         a, b = result.geoms
         self.assertTrue(isinstance(b, MultiLineString))
-        self.assertTrue(len(b) == 1)
+        self.assertTrue(len(b.geoms) == 1)
         self.assertEqual(b.geoms[0].coords[:], [(5, 0), (10, 0)])
         self.assertTrue(a.is_empty)
     
diff --git a/tests/test_split.py b/tests/test_split.py
index 3eaa1c8..86c77e4 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -10,7 +10,7 @@ class TestSplitGeometry(unittest.TestCase):
 	def helper(self, geom, splitter, expected_chunks):
 		s = split(geom, splitter)
 		self.assertEqual(s.type, "GeometryCollection")
-		self.assertEqual(len(s), expected_chunks)
+		self.assertEqual(len(s.geoms), expected_chunks)
 		if expected_chunks > 1:
 			# split --> expected collection that when merged is again equal to original geometry
 			if s.geoms[0].type == 'LineString':
@@ -122,6 +122,11 @@ class TestSplitLine(TestSplitGeometry):
 		self.assertTrue(splitter.touches(self.ls))
 		self.helper(self.ls, splitter, 1)
 
+		# splitter boundary touches interior of line --> return 2 segments
+		splitter = LineString([(0, 1), (1, 1)])  # touches at (1, 1)
+		self.assertTrue(splitter.touches(self.ls))
+		self.helper(self.ls, splitter, 2)
+
 	def test_split_line_with_multiline(self):
 		# crosses at one point --> return 2 segments
 		splitter = MultiLineString([[(0, 1), (1, 0)], [(0, 0), (2, -2)]])
diff --git a/tests/test_voronoi_diagram.py b/tests/test_voronoi_diagram.py
index 0d3ede6..79ec2c0 100644
--- a/tests/test_voronoi_diagram.py
+++ b/tests/test_voronoi_diagram.py
@@ -22,7 +22,7 @@ def test_no_regions():
     mp = MultiPoint(points=[(0.5, 0.5)])
     regions = voronoi_diagram(mp)
 
-    assert len(regions) == 0
+    assert len(regions.geoms) == 0
 
 
 @requires_geos_35
@@ -30,7 +30,7 @@ def test_two_regions():
     mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)])
     regions = voronoi_diagram(mp)
 
-    assert len(regions) == 2
+    assert len(regions.geoms) == 2
 
 
 @requires_geos_35
@@ -38,7 +38,7 @@ def test_edges():
     mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)])
     regions = voronoi_diagram(mp, edges=True)
 
-    assert len(regions) == 1
+    assert len(regions.geoms) == 1
     assert all(r.type == 'LineString' for r in regions.geoms)
 
 
@@ -49,7 +49,7 @@ def test_smaller_envelope():
 
     regions = voronoi_diagram(mp, envelope=poly)
 
-    assert len(regions) == 2
+    assert len(regions.geoms) == 2
     assert sum(r.area for r in regions.geoms) > poly.area
 
 
@@ -63,7 +63,7 @@ def test_larger_envelope():
 
     regions = voronoi_diagram(mp, envelope=poly)
 
-    assert len(regions) == 2
+    assert len(regions.geoms) == 2
     assert sum(r.area for r in regions.geoms) == poly.area
 
 
@@ -72,7 +72,7 @@ def test_from_polygon():
     poly = load_wkt('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
     regions = voronoi_diagram(poly)
 
-    assert len(regions) == 4
+    assert len(regions.geoms) == 4
 
 
 @requires_geos_35
@@ -80,7 +80,7 @@ def test_from_polygon_with_enough_tolerance():
     poly = load_wkt('POLYGON ((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))')
     regions = voronoi_diagram(poly, tolerance=1.0)
 
-    assert len(regions) == 2
+    assert len(regions.geoms) == 2
 
 
 @requires_geos_35
@@ -131,5 +131,5 @@ def test_from_multipoint_without_tolerace_without_floating_point_coordinates():
     """But it's fine without it."""
     mp = load_wkt('MULTIPOINT (0 0, 1 0, 1 2, 0 1)')
     regions = voronoi_diagram(mp)
-    assert len(regions) == 4
+    assert len(regions.geoms) == 4