Repository: Toblerity/Shapely
Pull request #864 (2020-03-11 00:41:40)

Shift AppVeyor setup to move away from wheel-building to just testing
I'd like to clean-out some wheel-building infrastructure from here to [shapely/shapely-wheels](https://github.com/shapely/shapely-wheels/).

See [shapely-wheels-PR#4](https://github.com/shapely/shapely-wheels/pull/4) for a proposed relocated home for the previous work.

The idea behind this is to get this AppVeyor setup to run Shapely's pytest on Windows, similar to what Travis CI does for Linux. It does not need to do the extra stuff to test "pip install" etc. or create artifacts. I've interleaved several versions of PYTHON, ARCH and GEOSVERSION.

Note that geos-3.4.3 is not included here since it does not compile with MSVC 2017. Also note that geos-master is also not included here, since the Travis CI setup already does a good job of this for Linux.

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

Coverage remained the same at 80.988% when pulling **573df4ce6045a4b2f9c610396b1915948de283b1 on mwtoews:appveyor** into **e477fb1d3359bf2e5751dfc6b57aaf3a5690a24c on Toblerity:master**.

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

Coverage remained the same at 80.988% when pulling **573df4ce6045a4b2f9c610396b1915948de283b1 on mwtoews:appveyor** into **e477fb1d3359bf2e5751dfc6b57aaf3a5690a24c on Toblerity:master**.


Patch:
diff --git a/.travis.yml b/.travis.yml
index de4f1a7..eb2c630 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -111,6 +111,9 @@ install:
   - pip install -v -e .[all]
 
 script:
+  - export LD_LIBRARY_PATH=$HOME/geosinstall/geos-$GEOSVERSION/lib
+  - export DYLD_LIBRARY_PATH=$HOME/geosinstall/geos-$GEOSVERSION/lib
+  - python -c "from shapely.geos import geos_version; print(geos_version)"
   - python -m pytest --cov shapely --cov-report term-missing "${SPEEDUPS_FLAG}"
 
 after_success:
diff --git a/CHANGES.txt b/CHANGES.txt
index 6662cca..42f0890 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,6 +4,7 @@ Changes
 1.7.1 (TBD)
 -----------
 
+- Fix bug in concatenation of function argtypes (#866).
 - Improved documentation of STRtree usage (#857).
 - Improved handling for empty list or list of lists in GeoJSON coordinates
   (#852).
diff --git a/appveyor.yml b/appveyor.yml
index 01b15d2..54f53a5 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -2,44 +2,24 @@ image: Visual Studio 2017
 platform: x64
 
 environment:
-  global:
-    GEOSVERSION: "3.8.1"
-
   matrix:
-#    - PYTHON: "C:\\Python27"
-#      ARCH: x86
-#      COMPILER: msvc90
-#      GEOSVERSION: "3.5.2"
-#      image: Visual Studio 2015
-#    - PYTHON: "C:\\Python27-x64"
-#      ARCH: x64
-#      COMPILER: msvc90
-#      GEOSVERSION: "3.5.2"
-#      image: Visual Studio 2015
+    # Interleave PYTHON, ARCH and GEOSVERSION
     - PYTHON: "C:\\Python35"
       ARCH: x86
       COMPILER: msvc2017
-    - PYTHON: "C:\\Python35-x64"
-      ARCH: x64
-      COMPILER: msvc2017
-    - PYTHON: "C:\\Python36"
-      ARCH: x86
-      COMPILER: msvc2017
+      GEOSVERSION: "3.5.2"
     - PYTHON: "C:\\Python36-x64"
       ARCH: x64
       COMPILER: msvc2017
+      GEOSVERSION: "3.6.4"
     - PYTHON: "C:\\Python37"
       ARCH: x86
       COMPILER: msvc2017
-    - PYTHON: "C:\\Python37-x64"
-      ARCH: x64
-      COMPILER: msvc2017
-    - PYTHON: "C:\\Python38"
-      ARCH: x86
-      COMPILER: msvc2017
+      GEOSVERSION: "3.7.3"
     - PYTHON: "C:\\Python38-x64"
       ARCH: x64
       COMPILER: msvc2017
+      GEOSVERSION: "3.8.1"
 
 
 install:
@@ -55,67 +35,36 @@ install:
 
 
 build_script:
-  - set _INITPATH=%PATH%
-
-  - ps: 'Write-Host "Configuring PATH with $env:PYTHON" -ForegroundColor Magenta'
-  - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
+  - set GEOSINSTALL=C:\projects\deps\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
+  - set CYTHONPF=C:\\projects\\deps\\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
+  - set GEOS_LIBRARY_PATH=%GEOSINSTALL%\bin\geos_c.dll
 
   - ps: 'Write-Host "Configuring MSVC compiler $env:COMPILER" -ForegroundColor Magenta'
-  - if %COMPILER%==msvc90 ( call "C:\Users\appveyor\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat" %ARCH% )
   - if %COMPILER%==msvc2017 ( call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" %ARCH% )
 
-  - ps: 'Write-Host "Cached dependency folder" -ForegroundColor Magenta'
-  - if not exist C:\projects\deps mkdir C:\projects\deps
-  - dir C:\projects\deps
+  - ps: 'Write-Host "Configuring PATH with $env:PYTHON and $env:GEOSINSTALL\bin" -ForegroundColor Magenta'
+  - set PATH=%PYTHON%;%PYTHON%\Scripts;%GEOSINSTALL%\bin;%PATH%
+  - python -m pip install --disable-pip-version-check --upgrade pip
 
-  - set GEOSINSTALL=C:\projects\deps\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
-  - set CYTHONPF=C:\\projects\\deps\\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
   - ps: 'Write-Host "Checking GEOS build $env:GEOSINSTALL" -ForegroundColor Magenta'
+  - if not exist C:\projects\deps mkdir C:\projects\deps
   - call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\build_geos.cmd
 
-  # - ps: 'Push-AppveyorArtifact $env:GEOSINSTALL\bin\geos.dll'
-  # - ps: 'Push-AppveyorArtifact $env:GEOSINSTALL\bin\geos_c.dll'
-
-  - ps: 'Write-Host "Copying and checking geos_c.dll" -ForegroundColor Magenta'
-  - cd %APPVEYOR_BUILD_FOLDER%
-  - dir
-  - if %ARCH%==x86 ( set CPDIR=DLLs_x86_VC9 )
-  - if %ARCH%==x64 ( set CPDIR=DLLs_AMD64_VC9 )
-  - xcopy %GEOSINSTALL%\bin\geos*.dll %CPDIR%
-  - cd %CPDIR%
-  - dir
-  - dumpbin /dependents geos_c.dll
-  - dumpbin /dependents %PYTHON%\python.exe
+  - ps: 'Write-Host "Checking geos_c.dll" -ForegroundColor Magenta'
   - python -c "import sys; print('Python ' + sys.version)"
-  - python -c "import ctypes; print(ctypes.CDLL('./geos_c.dll'))"
-  - cd ..
+  - python -c "import ctypes, os; print(ctypes.CDLL(os.environ['GEOS_LIBRARY_PATH']))"
 
-  - ps: 'Write-Host "Building wheel" -ForegroundColor Magenta'
+  - ps: 'Write-Host "Building extension" -ForegroundColor Magenta'
+  - cd %APPVEYOR_BUILD_FOLDER%
   - pip install -r requirements-dev.txt
-  - set GEOS_LIBRARY_PATH=%GEOSINSTALL%\bin\geos_c.dll
-  - set PATH=%GEOSINSTALL%\bin;%PATH%
-  - python setup.py build_ext -I%CYTHONPF%\\include -lgeos_c -L%CYTHONPF%\\lib bdist_wheel
+  - python setup.py build_ext --inplace -I%CYTHONPF%\\include -lgeos_c -L%CYTHONPF%\\lib
 
-  - ps: 'Write-Host "Checking pip install in a new environment" -ForegroundColor Magenta'
-  - set PATH=%_INITPATH%
-  - set GEOS_LIBRARY_PATH=
-  - '%PYTHON%\python.exe -m venv C:\projects\test'
-  - call C:\projects\test\Scripts\activate.bat
-  - pip install -r requirements-dev.txt
-  - cd dist
-  - pip install --no-index --find-links . --no-cache-dir shapely
+  - ps: 'Write-Host "Running pytest" -ForegroundColor Magenta'
+  - pytest
   - python -c "import shapely; print(shapely.__version__)"
   - python -c "from shapely.geos import geos_version_string; print(geos_version_string)"
   - python -c "from shapely import speedups; assert speedups.enabled"
 
-  - ps: 'Write-Host "Running pytest" -ForegroundColor Magenta'
-  - xcopy ..\tests tests\
-  - pytest
-
 
 cache:
-  - C:\projects\deps
-
-artifacts:
-  - path: dist\*.whl
-    name: wheel
+  - C:\projects\deps -> %APPVEYOR_BUILD_FOLDER%\ci\appveyor\build_geos.cmd
diff --git a/ci/appveyor/build_geos.cmd b/ci/appveyor/build_geos.cmd
index 05cda63..26a7c81 100644
--- a/ci/appveyor/build_geos.cmd
+++ b/ci/appveyor/build_geos.cmd
@@ -10,17 +10,16 @@ if exist %GEOSINSTALL% (
   curl -fsSO http://download.osgeo.org/geos/geos-%GEOSVERSION%.tar.bz2
   7z x geos-%GEOSVERSION%.tar.bz2
   7z x geos-%GEOSVERSION%.tar
-  cd geos-%GEOSVERSION%
+  cd geos-%GEOSVERSION% || exit /B 5
 
   pip install ninja
   cmake --version
 
   mkdir build
   cd build
-  cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=%GEOSINSTALL% ..
-  cmake --build . --config Release -j 2
-  ctest . --config Release
-  cmake --install . --config Release
+  cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=%GEOSINSTALL% .. || exit /B 1
+  cmake --build . --config Release || exit /B 2
+  ctest . --config Release || exit /B 3
+  cmake --install . --config Release || exit /B 4
   cd ..
 )
-
diff --git a/docs/manual.rst b/docs/manual.rst
index b3da710..c83db2d 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -1676,23 +1676,35 @@ linestring feature (right).
   Returns a LineString or MultiLineString geometry at a distance from the
   object on its right or its left side.
 
-  Distance must be a positive float value. The side parameter may be 'left' or
-  'right'. Left and right is determined by following the direction of given
-  geometric points of the LineString. The resolution of the offset around
-  each vertex of the object is parameterized as in the buffer method.
+  The `distance` parameter must be a positive float value.
 
-  The join style is for outside corners between line segments. Accepted integer
+  The `side` parameter may be 'left' or 'right'. Left and right are determined
+  by following the direction of the given geometric points of the LineString.
+  Right hand offsets are returned in the reverse direction of the original
+  LineString or LineRing, while left side offsets flow in the same direction.
+
+  The `resolution` of the offset around each vertex of the object is
+  parameterized as in the :meth:`buffer` method.
+
+  The `join_style` is for outside corners between line segments. Accepted integer
   values are 1 (round), 2 (mitre), and 3 (bevel). See also
   :data:`shapely.geometry.JOIN_STYLE`.
 
-  Severely mitered corners can be controlled by the mitre_limit parameter
-  (spelled in British English, en-gb). The ratio of the distance from the
-  corner to the end of the mitred offset corner is the miter ratio. Corners
-  with a ratio which exceed the limit will be beveled.
+  Severely mitered corners can be controlled by the `mitre_limit` parameter
+  (spelled in British English, en-gb). The corners of a parallel line will
+  be further from the original than most places with the mitre join style. The
+  ratio of this further distance to the specified `distance` is the miter ratio.
+  Corners with a ratio which exceed the limit will be beveled.
 
-.. note::
+  .. note::
+
+    This method may sometimes return a `MultiLineString` where a simple
+    `LineString` was expected; for example, an offset to a slightly
+    curved LineString.
+
+  .. note::
 
-  This method is only available for `LinearRing` and `LineString`  objects.
+    This method is only available for `LinearRing` and `LineString`  objects.
 
 .. plot:: code/parallel_offset.py
 
@@ -2745,4 +2757,4 @@ References
 .. _frozenset: https://docs.python.org/library/stdtypes.html#frozenset
 .. _Sorting HowTo: https://wiki.python.org/moin/HowTo/Sorting/
 .. _Python geo interface: https://gist.github.com/2217756
-.. _polylabel: https://github.com/mapbox/polylabel
\ No newline at end of file
+.. _polylabel: https://github.com/mapbox/polylabel
diff --git a/shapely/__init__.py b/shapely/__init__.py
index ffede83..9c6de1a 100644
--- a/shapely/__init__.py
+++ b/shapely/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "1.7.1dev"
+__version__ = "1.8dev"
diff --git a/shapely/geos.py b/shapely/geos.py
index d5a67d2..5b80c55 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -221,7 +221,7 @@ if geos_version >= (3, 1, 0):
                 # finishGEOS.
                 new_func.argtypes = [c_void_p]
             else:
-                new_func.argtypes = [c_void_p] + old_func.argtypes
+                new_func.argtypes = [c_void_p] + list(old_func.argtypes)
             if old_func.errcheck is not None:
                 new_func.errcheck = old_func.errcheck
 
diff --git a/shapely/ops.py b/shapely/ops.py
index 953fcd9..914b16e 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -10,6 +10,7 @@ else:
 
 from ctypes import byref, c_void_p, c_double
 
+from shapely.prepared import prep
 from shapely.geos import lgeos
 from shapely.geometry.base import geom_factory, BaseGeometry, BaseMultipartGeometry
 from shapely.geometry import asShape, asLineString, asMultiLineString, Point, MultiPoint,\
@@ -340,6 +341,11 @@ class SplitOp(object):
 
         union = poly.boundary.union(splitter)
 
+        # greatly improves split performance for big geometries with many
+        # holes (the following contains checks) with minimal overhead
+        # for common cases
+        poly = prep(poly)
+
         # some polygonized geometries may be holes, we do not want them
         # that's why we test if the original polygon (poly) contains
         # an inner point of polygonized geometry (pg)
@@ -498,39 +504,43 @@ def substring(geom, start_dist, end_dist, normalized=False):
     """
 
     assert(isinstance(geom, LineString))
-    
+
     # Filter out cases in which to return a point
     if start_dist == end_dist:
         return geom.interpolate(start_dist, normalized)
     elif not normalized and start_dist >= geom.length and end_dist >= geom.length:
         return geom.interpolate(geom.length, normalized)
     elif not normalized and -start_dist >= geom.length and -end_dist >= geom.length:
-        return geom.interpolate(0, normalized)                    
+        return geom.interpolate(0, normalized)
     elif normalized and start_dist >= 1 and end_dist >= 1:
-        return geom.interpolate(1, normalized)  
+        return geom.interpolate(1, normalized)
     elif normalized and -start_dist >= 1 and -end_dist >= 1:
         return geom.interpolate(0, normalized)
 
     start_point = geom.interpolate(start_dist, normalized)
     end_point = geom.interpolate(end_dist, normalized)
-    
+
     min_dist = min(start_dist, end_dist)
     max_dist = max(start_dist, end_dist)
     if normalized:
         min_dist *= geom.length
         max_dist *= geom.length
-    
+
     if start_dist < end_dist:
         vertex_list = [(start_point.x, start_point.y)]
     else:
         vertex_list = [(end_point.x, end_point.y)]
+
     coords = list(geom.coords)
-    for p in coords:
-        pd = geom.project(Point(p))
-        if min_dist < pd < max_dist:
-            vertex_list.append(p)
-        elif pd >= max_dist:
+    current_distance = 0
+    for p1, p2 in zip(coords, coords[1:]):
+        if min_dist < current_distance < max_dist:
+            vertex_list.append(p1)
+        elif current_distance >= max_dist:
             break
+
+        current_distance += ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) ** 0.5
+
     if start_dist < end_dist:
         vertex_list.append((end_point.x, end_point.y))
     else:
diff --git a/shapely/strtree.py b/shapely/strtree.py
index 00dea7c..4b2f482 100644
--- a/shapely/strtree.py
+++ b/shapely/strtree.py
@@ -1,35 +1,74 @@
+"""
+strtree
+=======
+
+Index geometry objects for efficient lookup of nearby or
+nearest neighbors. Home of the `STRtree` class which is
+an interface to the query-only GEOS R-tree packed using
+the Sort-Tile-Recursive algorithm [1]_.
+
+.. autoclass:: STRtree
+    :members:
+
+References
+----------
+  .. [1]  Leutenegger, Scott & Lopez, Mario & Edgington, Jeffrey. (1997).
+     "STR: A Simple and Efficient Algorithm for R-Tree Packing." Proc.
+     VLDB Conf. 497-506. 10.1109/ICDE.1997.582015.
+     https://www.cs.odu.edu/~mln/ltrs-pdfs/icase-1997-14.pdf
+"""
+
 from shapely.geos import lgeos
 import ctypes
 
 class STRtree:
     """
-    STRtree is an R-tree spatial index that is created using the
-    Sort-Tile-Recursive algorithm.
-    
-    STRtree takes a sequence of geometry objects as initialization
-    parameter. Once created, it is immutable. The query method can
-    be used to make spatial queries over the objects on the index.
-
-    >>> from shapely.geometry import Polygon
-    >>> polys = [ Polygon(((0, 0), (1, 0), (1, 1))), Polygon(((0, 1), (0, 0), (1, 0))), Polygon(((100, 100), (101, 100), (101, 101))) ]
-    >>> s = STRtree(polys)
+    An STRtree is a spatial index; specifically, an R-tree created
+    using the Sort-Tile-Recursive algorithm.
+
+    Pass a list of geometry objects to the `STRtree` constructor to
+    create a spatial index. References to these indexed objects are
+    kept and stored in the R-tree. You can query them with another
+    geometric object.
+
+    The `STRtree` is query-only, meaning that once created
+    you cannot add or remove geometries.
+
+    *New in version 1.4.0*.
+
+    Parameters
+    ----------
+    geoms : sequence of geometry objects
+        geometry objects to be indexed
+
+    Examples
+    --------
+
+    Creating an index of pologons:
+
+    >>> from shapely.strtree import STRtree
+    >>> from shapely.geometry import Polygon, Point
+    >>>
+    >>> polys = [Polygon(((0, 0), (1, 0), (1, 1))),
+    ...          Polygon(((0, 1), (0, 0), (1, 0))),
+    ...          Polygon(((100, 100), (101, 100), (101, 101)))]
+    >>> tree = STRtree(polys)
     >>> query_geom = Polygon(((-1, -1), (2, 0), (2, 2), (-1, 2)))
-    >>> result = s.query(query_geom)
+    >>> result = tree.query(query_geom)
     >>> polys[0] in result
     True
     >>> polys[1] in result
     True
     >>> polys[2] in result
     False
-    >>> # Test empty tree
-    >>> s = STRtree([])
-    >>> s.query(query_geom)
+
+    Behavior if an `STRtree` is created empty:
+
+    >>> tree = STRtree([])
+    >>> tree.query(Point(0, 0))
     []
-    >>> # Test tree with one object
-    >>> s = STRtree([polys[0]])
-    >>> result = s.query(query_geom)
-    >>> polys[0] in result
-    True
+    >>> print(tree.nearest(Point(0, 0)))
+    None
     """
 
     def __init__(self, geoms):
@@ -50,8 +89,58 @@ class STRtree:
             self._tree_handle = None
 
     def query(self, geom):
-        """Returns a list of objects on the index whose extents
-        intersect the given geometry's extents.
+        """
+        Search the index for geometry objects whose extents
+        intersect the extent of the given object.
+
+        Parameters
+        ----------
+        geom : geometry object
+            The query geometry
+
+        Returns
+        -------
+        list of geometry objects
+            All the geometry objects in the index whose extents
+            intersect the extent of `geom`.
+
+        Note
+        ----
+        A geometry object's "extent" is its the minimum xy bounding
+        rectangle.
+
+        Examples
+        --------
+
+        A buffer around a point can be used to control the extent
+        of the query.
+
+        >>> from shapely.strtree import STRtree
+        >>> from shapely.geometry import Point
+        >>> points = [Point(i, i) for i in range(10)]
+        >>> tree = STRtree(points)
+        >>> query_geom = Point(2,2).buffer(0.99)
+        >>> [o.wkt for o in tree.query(query_geom)]
+        ['POINT (2 2)']
+        >>> query_geom = Point(2, 2).buffer(1.0)
+        >>> [o.wkt for o in tree.query(query_geom)]
+        ['POINT (1 1)', 'POINT (2 2)', 'POINT (3 3)']
+
+        A subsequent search through the returned subset using the
+        desired binary predicate (eg. intersects, crosses, contains,
+        overlaps) may be necessary to further filter the results
+        according to their specific spatial relationships.
+
+        >>> [o.wkt for o in tree.query(query_geom) if o.intersects(query_geom)]
+        ['POINT (2 2)']
+
+        To get the original indices of the returned objects, create an
+        auxiliary dictionary. But use the geometry *ids* as keys since
+        the shapely geometry objects themselves are not hashable.
+
+        >>> index_by_id = dict((id(pt), i) for i, pt in enumerate(points))
+        >>> [(index_by_id[id(pt)], pt.wkt) for pt in tree.query(Point(2,2).buffer(1.0))]
+        [(1, 'POINT (1 1)'), (2, 'POINT (2 2)'), (3, 'POINT (3 3)')]
         """
         if self._n_geoms == 0:
             return []
@@ -67,9 +156,41 @@ class STRtree:
         return result
 
     def nearest(self, geom):
+        """
+        Get the nearest object in the index to a geometry object.
+
+        Parameters
+        ----------
+        geom : geometry object
+            The query geometry
+
+        Returns
+        -------
+        geometry object
+            The nearest geometry object in the index to `geom`.
+
+            Will always only return *one* object even if several
+            in the index are the minimum distance away.
+
+            `None` if the index is empty.
+
+        Examples
+        --------
+        >>> from shapely.strtree import STRtree
+        >>> from shapely.geometry import Point
+        >>> tree = STRtree([Point(i, i) for i in range(10)])
+        >>> tree.nearest(Point(2.2, 2.2)).wkt
+        'POINT (2 2)'
+
+        Will only return one object:
+
+        >>> tree = STRtree ([Point(0, 0), Point(0, 0)])
+        >>> tree.nearest(Point(0, 0)).wkt
+        'POINT (0 0)'
+        """
         if self._n_geoms == 0:
             return None
-        
+
         envelope = geom.envelope
 
         def callback(item1, item2, distance, userdata):
@@ -80,12 +201,12 @@ class STRtree:
                 lgeos.GEOSDistance(geom1._geom, geom2._geom, dist)
                 return 1
             except:
-                return 0 
-        
+                return 0
+
         item = lgeos.GEOSSTRtree_nearest_generic(self._tree_handle, ctypes.py_object(geom), envelope._geom, \
             lgeos.GEOSDistanceCallback(callback), None)
         result = ctypes.cast(item, ctypes.py_object).value
-        
+
         return result
 
 if __name__ == "__main__":
diff --git a/tools/get_appveyor_artifacts.py b/tools/get_appveyor_artifacts.py
deleted file mode 100755
index 3f1d361..0000000
--- a/tools/get_appveyor_artifacts.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Download AppVeyor artifacts for shapely
-
-See https://www.appveyor.com/docs/api/projects-builds/
-
-Created on Tue Jan 21 12:06:02 2020
-
-@author: Mike Taves
-"""
-
-import os
-import requests
-import textwrap
-
-account = 'frsci'
-slug = 'shapely'
-
-api = 'https://ci.appveyor.com/api'
-
-
-def download_job_artifacts(job_id):
-    resp = requests.get(f'{api}/buildjobs/{job_id}/artifacts')
-    for artifact in resp.json():
-        filename = artifact['fileName']
-        print(f'downloading: {filename}')
-        resp = requests.get(f'{api}/buildjobs/{job_id}/artifacts/{filename}')
-        save_path = os.path.split(filename)[-1]
-        with open(save_path, 'wb') as f:
-            f.write(resp.content)
-
-
-def main(branch='master'):
-    print(f"Gathering last build information for branch '{branch}'")
-    resp = requests.get(f'{api}/projects/{account}/{slug}/branch/{branch}')
-    data = resp.json()
-    if 'build' not in data:
-        raise ValueError(data)
-    build = data['build']
-    jobs = build.get('jobs', [])
-    if not jobs:
-        raise ValueError('no jobs found')
-    print('Found {0} jobs for last build for {1}'.format(len(jobs), branch))
-    print('Commit id: ' + str(build['commitId']))
-    print(textwrap.indent(build['message'], '> '))
-    # Check if all jobs were built successfully with one artifact
-    job_ids = []
-    for job in jobs:
-        if job.get('status') != 'success':
-            raise ValueError('not all jobs were built successfully')
-        elif job.get('artifactsCount', 0) == 0:
-            raise ValueError('not all jobs produced artifacts')
-        job_ids.append(job['jobId'])
-    for job_id in job_ids:
-        download_job_artifacts(job_id)
-
-
-if __name__ == '__main__':
-    import argparse
-    parser = argparse.ArgumentParser()
-    parser.add_argument(
-        '--branch', default='master',
-        help='branch to download artifacts from (default: %(default)s)')
-    args = parser.parse_args()
-    main(**vars(args))


Test patch:
diff --git a/tests/test_substring.py b/tests/test_substring.py
index 8917753..8634594 100644
--- a/tests/test_substring.py
+++ b/tests/test_substring.py
@@ -1,7 +1,9 @@
+import json
+
 from . import unittest
 from shapely.ops import substring
 from shapely.geos import geos_version
-from shapely.geometry import Point, LineString
+from shapely.geometry import Point, LineString, shape
 
 
 class SubstringTestCase(unittest.TestCase):
@@ -10,27 +12,27 @@ class SubstringTestCase(unittest.TestCase):
         self.point = Point(1, 1)
         self.line1 = LineString(([0, 0], [2, 0]))
         self.line2 = LineString(([3, 0], [3, 6], [4.5, 6]))
-  
+
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_startpoint(self):
         self.assertTrue(substring(self.line1, -500, -600).equals(Point(0, 0)))
         self.assertTrue(substring(self.line1, -500, -500).equals(Point(0, 0)))
         self.assertTrue(substring(self.line1, -1, -1.1, True).equals(Point(0, 0)))
-        self.assertTrue(substring(self.line1, -1.1, -1.1, True).equals(Point(0, 0)))               
+        self.assertTrue(substring(self.line1, -1.1, -1.1, True).equals(Point(0, 0)))
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_endpoint(self):
         self.assertTrue(substring(self.line1, 500, 600).equals(Point(2, 0)))
         self.assertTrue(substring(self.line1, 500, 500).equals(Point(2, 0)))
-        self.assertTrue(substring(self.line1, 1, 1.1, True).equals(Point(2, 0)))   
+        self.assertTrue(substring(self.line1, 1, 1.1, True).equals(Point(2, 0)))
         self.assertTrue(substring(self.line1, 1.1, 1.1, True).equals(Point(2, 0)))
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_midpoint(self):
-        self.assertTrue(substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0)))        
+        self.assertTrue(substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0)))
         self.assertTrue(substring(self.line1, -0.5, -0.5).equals(Point(1.5, 0)))
-        self.assertTrue(substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0)))        
-        self.assertTrue(substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)))       
+        self.assertTrue(substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0)))
+        self.assertTrue(substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)))
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_startsubstring(self):
@@ -75,7 +77,15 @@ class SubstringTestCase(unittest.TestCase):
     def test_return_substring_issue682(self):
         assert list(substring(self.line2, 0.1, 0).coords) == [(3.0, 0.1), (3.0, 0.0)]
 
+    @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
+    def test_return_substring_issue848(self):
+        line = shape(json.loads(data_issue_848))
+        cut_line = substring(line, 0.7, 0.8, normalized=True)
+        assert len(cut_line.coords) == 53
 
 def test_suite():
     loader = unittest.TestLoader()
     return loader.loadTestsFromTestCase(SubstringTestCase)
+
+
+data_issue_848 = '{"type": "LineString", "coordinates": [[-87.71314, 41.96793], [-87.71312, 41.96667], [-87.71311, 41.96643], [-87.7131, 41.96635], [-87.71309, 41.9663], [-87.71303, 41.96621], [-87.71298, 41.96615], [-87.71292, 41.96611], [-87.7128, 41.96607], [-87.71268, 41.96605], [-87.71255, 41.96605], [-87.7124, 41.96605], [-87.71219, 41.96605], [-87.71173, 41.96606], [-87.71108, 41.96607], [-87.71027, 41.96607], [-87.70884, 41.96609], [-87.70763, 41.96611], [-87.70645, 41.96612], [-87.70399, 41.96613], [-87.70267, 41.96614], [-87.70166, 41.96615], [-87.70075, 41.96615], [-87.69954, 41.96615], [-87.69873, 41.96616], [-87.69789, 41.96618], [-87.69675, 41.9662], [-87.69502, 41.96621], [-87.69411, 41.96621], [-87.69145, 41.96623], [-87.69026, 41.96624], [-87.68946, 41.96625], [-87.6885, 41.96625], [-87.68718, 41.96628], [-87.68545, 41.96631], [-87.68399, 41.96632], [-87.68271, 41.96635], [-87.68159, 41.96636], [-87.68034, 41.96638], [-87.67863, 41.96641], [-87.67766, 41.96642], [-87.67741, 41.96641], [-87.67722, 41.9664], [-87.67695, 41.96638], [-87.67665, 41.96632], [-87.67638, 41.96623], [-87.67613, 41.96612], [-87.67589, 41.96596], [-87.6757, 41.96579], [-87.67557, 41.96565], [-87.67544, 41.96547], [-87.67539, 41.96536], [-87.6753, 41.96519], [-87.67524, 41.96503], [-87.67523, 41.96491], [-87.67522, 41.96477], [-87.67521, 41.96457], [-87.6752, 41.96434], [-87.67519, 41.96371], [-87.67517, 41.96175], [-87.67513, 41.96077], [-87.67505, 41.95798], [-87.67501, 41.95666], [-87.67497, 41.95513], [-87.67496, 41.95452], [-87.67491, 41.95392], [-87.67487, 41.95302], [-87.67485, 41.95202], [-87.67484, 41.95101], [-87.67479, 41.94959], [-87.67476, 41.94859], [-87.67474, 41.94703], [-87.67468, 41.94596], [-87.67466, 41.94513], [-87.67463, 41.94494], [-87.67457, 41.94474], [-87.6745, 41.94455], [-87.67442, 41.94438], [-87.6743, 41.94424], [-87.67419, 41.94414], [-87.67405, 41.94404], [-87.67386, 41.94393], [-87.67367, 41.94386], [-87.67348, 41.9438], [-87.67334, 41.94376], [-87.67311, 41.94373], [-87.67289, 41.9437], [-87.67263, 41.94369], [-87.67234, 41.94369], [-87.6715, 41.9437], [-87.67088, 41.94371], [-87.66938, 41.94373], [-87.66749, 41.94377], [-87.66585, 41.94378], [-87.66508, 41.94379], [-87.66361, 41.94381], [-87.6591, 41.94391], [-87.65767, 41.94391], [-87.65608, 41.94393], [-87.6555, 41.94394], [-87.65521, 41.94394], [-87.65503, 41.94393], [-87.65488, 41.9439], [-87.6547, 41.94386], [-87.65454, 41.9438], [-87.65441, 41.94375], [-87.65425, 41.94364], [-87.6541, 41.94351], [-87.654, 41.94342], [-87.65392, 41.94331], [-87.65382, 41.94319], [-87.65375, 41.94306], [-87.65367, 41.94292], [-87.65361, 41.9428], [-87.65355, 41.94269], [-87.65351, 41.94257], [-87.65347, 41.94238], [-87.65345, 41.94218], [-87.65338, 41.93975], [-87.65337, 41.93939], [-87.65337, 41.93893], [-87.65336, 41.93865], [-87.65333, 41.93763], [-87.65331, 41.93717], [-87.65328, 41.93627], [-87.65327, 41.93603], [-87.65323, 41.93532], [-87.65322, 41.93491], [-87.6532, 41.93445], [-87.65314, 41.93312], [-87.65313, 41.93273], [-87.6531, 41.93218], [-87.65307, 41.93151], [-87.65305, 41.9309], [-87.65302, 41.9303], [-87.65299, 41.92951], [-87.65296, 41.9287], [-87.65295, 41.92842], [-87.65294, 41.92768], [-87.65292, 41.92715], [-87.65289, 41.92599], [-87.65288, 41.92537], [-87.65287, 41.92505], [-87.65282, 41.92352], [-87.65276, 41.92172], [-87.65274, 41.92113], [-87.65264, 41.91822], [-87.65264, 41.91808], [-87.65262, 41.91763], [-87.65261, 41.91718], [-87.65255, 41.91563], [-87.6525, 41.91406], [-87.65242, 41.91377], [-87.65234, 41.91362], [-87.65223, 41.91351], [-87.65208, 41.91339], [-87.65183, 41.91322], [-87.65093, 41.9126], [-87.65017, 41.91203], [-87.64985, 41.9118], [-87.64971, 41.91171], [-87.64957, 41.91164], [-87.64948, 41.9116], [-87.64939, 41.91158], [-87.6492, 41.91153], [-87.649, 41.9115], [-87.64883, 41.9115], [-87.64863, 41.9115], [-87.64792, 41.91151], [-87.64781, 41.9115], [-87.64768, 41.91146], [-87.64756, 41.91139], [-87.64745, 41.91122], [-87.6474, 41.91112], [-87.64739, 41.91101], [-87.64738, 41.91086], [-87.64736, 41.91071], [-87.64734, 41.91061], [-87.64728, 41.91051], [-87.64718, 41.91044], [-87.64709, 41.9104], [-87.64697, 41.91036], [-87.64682, 41.91034], [-87.64664, 41.91033], [-87.64646, 41.91033], [-87.6458, 41.91034], [-87.64523, 41.91034], [-87.64348, 41.91036], [-87.64255, 41.91039], [-87.641, 41.9104], [-87.64038, 41.9104], [-87.63975, 41.9104], [-87.6393, 41.91041], [-87.63814, 41.91042], [-87.63798, 41.91041], [-87.63787, 41.91039], [-87.63771, 41.91034], [-87.63757, 41.91027], [-87.63746, 41.91021], [-87.63736, 41.91011], [-87.6373, 41.90999], [-87.63727, 41.90986], [-87.63726, 41.90973], [-87.63725, 41.90951], [-87.63723, 41.90874], [-87.63718, 41.90758], [-87.63713, 41.90607], [-87.63711, 41.90543], [-87.63702, 41.90381], [-87.63702, 41.90368], [-87.63701, 41.90334], [-87.63699, 41.90322], [-87.63694, 41.90312], [-87.63688, 41.90299], [-87.63682, 41.90292], [-87.63671, 41.90279], [-87.63659, 41.90265], [-87.63653, 41.90255], [-87.63649, 41.90245], [-87.63646, 41.90235], [-87.63647, 41.90221], [-87.63647, 41.90211], [-87.6365, 41.90202], [-87.63653, 41.9019], [-87.63659, 41.90177], [-87.63666, 41.90156], [-87.63669, 41.90143], [-87.6367, 41.90131], [-87.6367, 41.90119], [-87.63664, 41.90029], [-87.63664, 41.90008], [-87.63662, 41.89975], [-87.63658, 41.89892], [-87.63657, 41.89867], [-87.63654, 41.89761], [-87.63654, 41.89738], [-87.63649, 41.89726], [-87.63641, 41.89715], [-87.63634, 41.89708], [-87.63623, 41.89699], [-87.63595, 41.89677], [-87.63583, 41.89667], [-87.63574, 41.89654], [-87.63569, 41.89645], [-87.63568, 41.89633], [-87.63565, 41.89542], [-87.63563, 41.89434], [-87.6356, 41.89327], [-87.63558, 41.89261], [-87.63554, 41.89147], [-87.63553, 41.89051], [-87.63548, 41.8903], [-87.6354, 41.89021], [-87.63533, 41.89012], [-87.63524, 41.89007], [-87.63508, 41.89001], [-87.63493, 41.88997], [-87.63475, 41.88994], [-87.63462, 41.88991], [-87.63447, 41.88989], [-87.63436, 41.88984], [-87.63425, 41.88979], [-87.63414, 41.8897], [-87.63407, 41.88962], [-87.63402, 41.88952], [-87.63399, 41.88943], [-87.63397, 41.88897], [-87.63396, 41.88707], [-87.63391, 41.88572], [-87.63389, 41.88441], [-87.63385, 41.8827], [-87.63384, 41.88144], [-87.63378, 41.88014], [-87.63374, 41.87872], [-87.63369, 41.87726], [-87.63369, 41.87706], [-87.63365, 41.87695], [-87.63359, 41.87691], [-87.63353, 41.87688], [-87.63345, 41.87686], [-87.63338, 41.87685], [-87.63263, 41.87685], [-87.63173, 41.87686], [-87.62925, 41.87689], [-87.62821, 41.87691], [-87.62757, 41.87693], [-87.6265, 41.87696], [-87.62635, 41.87696], [-87.62603, 41.87697], [-87.62605, 41.87831], [-87.6261, 41.87951], [-87.62616, 41.88203], [-87.62619, 41.88322], [-87.62622, 41.88443], [-87.62626, 41.88534], [-87.62625, 41.88552], [-87.62625, 41.88557], [-87.62627, 41.88562], [-87.6263, 41.88566], [-87.62635, 41.88569], [-87.62642, 41.88572], [-87.6265, 41.88573], [-87.62655, 41.88574], [-87.62661, 41.88574], [-87.62683, 41.88574], [-87.62784, 41.88574], [-87.62887, 41.88574], [-87.62948, 41.88574], [-87.62982, 41.88574], [-87.62992, 41.88574], [-87.63011, 41.88574], [-87.6302, 41.88574], [-87.63089, 41.88574], [-87.63204, 41.88574], [-87.63285, 41.88573], [-87.63391, 41.88572], [-87.63396, 41.88707], [-87.63397, 41.88897], [-87.63399, 41.88943], [-87.63402, 41.88952], [-87.63407, 41.88962], [-87.63414, 41.8897], [-87.63425, 41.88979], [-87.63436, 41.88984], [-87.63447, 41.88989], [-87.63462, 41.88991], [-87.63475, 41.88994], [-87.63493, 41.88997], [-87.63508, 41.89001], [-87.63524, 41.89007], [-87.63533, 41.89012], [-87.6354, 41.89021], [-87.63548, 41.8903], [-87.63553, 41.89051], [-87.63554, 41.89147], [-87.63558, 41.89261], [-87.6356, 41.89327], [-87.63563, 41.89434], [-87.63565, 41.89542], [-87.63568, 41.89633], [-87.63569, 41.89645], [-87.63574, 41.89654], [-87.63583, 41.89667], [-87.63595, 41.89677], [-87.63623, 41.89699], [-87.63634, 41.89708], [-87.63641, 41.89715], [-87.63649, 41.89726], [-87.63654, 41.89738], [-87.63654, 41.89761], [-87.63657, 41.89867], [-87.63658, 41.89892], [-87.63662, 41.89975], [-87.63664, 41.90008], [-87.63664, 41.90029], [-87.6367, 41.90119], [-87.6367, 41.90131], [-87.63669, 41.90143], [-87.63666, 41.90156], [-87.63659, 41.90177], [-87.63653, 41.9019], [-87.6365, 41.90202], [-87.63647, 41.90211], [-87.63647, 41.90221], [-87.63646, 41.90235], [-87.63649, 41.90245], [-87.63653, 41.90255], [-87.63659, 41.90265], [-87.63671, 41.90279], [-87.63682, 41.90292], [-87.63688, 41.90299], [-87.63694, 41.90312], [-87.63699, 41.90322], [-87.63701, 41.90334], [-87.63702, 41.90368], [-87.63702, 41.90381], [-87.63711, 41.90543], [-87.63713, 41.90607], [-87.63718, 41.90758], [-87.63723, 41.90874], [-87.63725, 41.90951], [-87.63726, 41.90973], [-87.63727, 41.90986], [-87.6373, 41.90999], [-87.63736, 41.91011], [-87.63746, 41.91021], [-87.63757, 41.91027], [-87.63771, 41.91034], [-87.63787, 41.91039], [-87.63798, 41.91041], [-87.63814, 41.91042], [-87.6393, 41.91041], [-87.63975, 41.9104], [-87.64038, 41.9104], [-87.641, 41.9104], [-87.64255, 41.91039], [-87.64348, 41.91036], [-87.64523, 41.91034], [-87.6458, 41.91034], [-87.64646, 41.91033], [-87.64664, 41.91033], [-87.64682, 41.91034], [-87.64697, 41.91036], [-87.64709, 41.9104], [-87.64718, 41.91044], [-87.64728, 41.91051], [-87.64734, 41.91061], [-87.64736, 41.91071], [-87.64738, 41.91086], [-87.64739, 41.91101], [-87.6474, 41.91112], [-87.64745, 41.91122], [-87.64756, 41.91139], [-87.64768, 41.91146], [-87.64781, 41.9115], [-87.64792, 41.91151], [-87.64863, 41.9115], [-87.64883, 41.9115], [-87.649, 41.9115], [-87.6492, 41.91153], [-87.64939, 41.91158], [-87.64948, 41.9116], [-87.64957, 41.91164], [-87.64971, 41.91171], [-87.64985, 41.9118], [-87.65017, 41.91203], [-87.65093, 41.9126], [-87.65183, 41.91322], [-87.65208, 41.91339], [-87.65223, 41.91351], [-87.65234, 41.91362], [-87.65242, 41.91377], [-87.6525, 41.91406], [-87.65255, 41.91563], [-87.65261, 41.91718], [-87.65262, 41.91763], [-87.65264, 41.91808], [-87.65264, 41.91822], [-87.65274, 41.92113], [-87.65276, 41.92172], [-87.65282, 41.92352], [-87.65287, 41.92505], [-87.65288, 41.92537], [-87.65289, 41.92599], [-87.65292, 41.92715], [-87.65294, 41.92768], [-87.65295, 41.92842], [-87.65296, 41.9287], [-87.65299, 41.92951], [-87.65302, 41.9303], [-87.65305, 41.9309], [-87.65307, 41.93151], [-87.6531, 41.93218], [-87.65313, 41.93273], [-87.65314, 41.93312], [-87.6532, 41.93445], [-87.65322, 41.93491], [-87.65323, 41.93532], [-87.65327, 41.93603], [-87.65328, 41.93627], [-87.65331, 41.93717], [-87.65333, 41.93763], [-87.65336, 41.93865], [-87.65337, 41.93893], [-87.65337, 41.93939], [-87.65338, 41.93975], [-87.65345, 41.94218], [-87.65347, 41.94238], [-87.65351, 41.94257], [-87.65355, 41.94269], [-87.65361, 41.9428], [-87.65367, 41.94292], [-87.65375, 41.94306], [-87.65382, 41.94319], [-87.65392, 41.94331], [-87.654, 41.94342], [-87.6541, 41.94351], [-87.65425, 41.94364], [-87.65441, 41.94375], [-87.65454, 41.9438], [-87.6547, 41.94386], [-87.65488, 41.9439], [-87.65503, 41.94393], [-87.65521, 41.94394], [-87.6555, 41.94394], [-87.65608, 41.94393], [-87.65767, 41.94391], [-87.6591, 41.94391], [-87.66361, 41.94381], [-87.66508, 41.94379], [-87.66585, 41.94378], [-87.66749, 41.94377], [-87.66938, 41.94373], [-87.67088, 41.94371], [-87.6715, 41.9437], [-87.67234, 41.94369], [-87.67263, 41.94369], [-87.67289, 41.9437], [-87.67311, 41.94373], [-87.67334, 41.94376], [-87.67348, 41.9438], [-87.67367, 41.94386], [-87.67386, 41.94393], [-87.67405, 41.94404], [-87.67419, 41.94414], [-87.6743, 41.94424], [-87.67442, 41.94438], [-87.6745, 41.94455], [-87.67457, 41.94474], [-87.67463, 41.94494], [-87.67466, 41.94513], [-87.67468, 41.94596], [-87.67474, 41.94703], [-87.67476, 41.94859], [-87.67479, 41.94959], [-87.67484, 41.95101], [-87.67485, 41.95202], [-87.67487, 41.95302], [-87.67491, 41.95392], [-87.67496, 41.95452], [-87.67497, 41.95513], [-87.67501, 41.95666], [-87.67505, 41.95798], [-87.67513, 41.96077], [-87.67517, 41.96175], [-87.67519, 41.96371], [-87.6752, 41.96434], [-87.67521, 41.96457], [-87.67522, 41.96477], [-87.67523, 41.96491], [-87.67524, 41.96503], [-87.6753, 41.96519], [-87.67539, 41.96536], [-87.67544, 41.96547], [-87.67557, 41.96565], [-87.6757, 41.96579], [-87.67589, 41.96596], [-87.67613, 41.96612], [-87.67638, 41.96623], [-87.67665, 41.96632], [-87.67695, 41.96638], [-87.67722, 41.9664], [-87.67741, 41.96641], [-87.67766, 41.96642], [-87.67863, 41.96641], [-87.68034, 41.96638], [-87.68159, 41.96636], [-87.68271, 41.96635], [-87.68399, 41.96632], [-87.68545, 41.96631], [-87.68718, 41.96628], [-87.6885, 41.96625], [-87.68946, 41.96625], [-87.69026, 41.96624], [-87.69145, 41.96623], [-87.69411, 41.96621], [-87.69502, 41.96621], [-87.69675, 41.9662], [-87.69789, 41.96618], [-87.69873, 41.96616], [-87.69954, 41.96615], [-87.70075, 41.96615], [-87.70166, 41.96615], [-87.70267, 41.96614], [-87.70399, 41.96613], [-87.70645, 41.96612], [-87.70763, 41.96611], [-87.70884, 41.96609], [-87.71027, 41.96607], [-87.71108, 41.96607], [-87.71173, 41.96606], [-87.71219, 41.96605], [-87.7124, 41.96605], [-87.71255, 41.96605], [-87.71268, 41.96605], [-87.7128, 41.96607], [-87.71292, 41.96611], [-87.71298, 41.96615], [-87.71303, 41.96621], [-87.71309, 41.9663], [-87.7131, 41.96635], [-87.71311, 41.96643], [-87.71312, 41.96667], [-87.71314, 41.96793]]}'