This commit is contained in:
2025-01-26 19:24:23 -08:00
parent 32cd60e92b
commit d1dde0dbc6
4155 changed files with 29170 additions and 216373 deletions

View File

@@ -92,3 +92,34 @@ def ignore_invalid(condition=True):
with ignore_invalid():
line_string_nan = shapely.LineString([(np.nan, np.nan), (np.nan, np.nan)])
class ArrayLike:
"""
Simple numpy Array like class that implements the
ufunc protocol.
"""
def __init__(self, array):
self._array = np.asarray(array)
def __len__(self):
return len(self._array)
def __getitem(self, key):
return self._array[key]
def __iter__(self):
return self._array.__iter__()
def __array__(self):
return np.asarray(self._array)
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
if method == "__call__":
inputs = [
arg._array if isinstance(arg, self.__class__) else arg for arg in inputs
]
return self.__class__(ufunc(*inputs, **kwargs))
else:
return NotImplemented

View File

@@ -2,6 +2,7 @@ import numpy as np
import pytest
from shapely import LineString
from shapely.tests.common import line_string, line_string_z, point, point_z
class TestCoords:
@@ -84,3 +85,18 @@ class TestXY:
assert list(x) == [0.0, 1.0]
assert len(y) == 2
assert list(y) == [0.0, 1.0]
@pytest.mark.parametrize("geom", [point, point_z, line_string, line_string_z])
def test_coords_array_copy(geom):
"""Test CoordinateSequence.__array__ method."""
coord_seq = geom.coords
assert np.array(coord_seq) is not np.array(coord_seq)
assert np.array(coord_seq, copy=True) is not np.array(coord_seq, copy=True)
# Behaviour of copy=False is different between NumPy 1.x and 2.x
if int(np.version.short_version.split(".", 1)[0]) >= 2:
with pytest.raises(ValueError, match="A copy is always created"):
np.array(coord_seq, copy=False)
else:
assert np.array(coord_seq, copy=False) is np.array(coord_seq, copy=False)

View File

@@ -47,7 +47,7 @@ def test_format_point():
("g", xy2, "POINT (-169.910918 -18.997564)", False),
("0.2g", xy2, "POINT (-169.91 -19)", False),
]
# without precsions test GEOS rounding_precision=-1; different than Python
# without precisions test GEOS rounding_precision=-1; different than Python
test_list += [
("f", (1, 2), f"POINT ({1:.16f} {2:.16f})", False),
("F", xyz3, "POINT Z ({:.16f} {:.16f} {:.16f})".format(*xyz3), False),
@@ -84,23 +84,28 @@ def test_format_polygon():
assert format(poly, "X") == poly.wkb_hex
# Use f-strings with extra characters and rounding precision
assert f"<{poly:.2f}>" == (
"<POLYGON ((10.00 0.00, 7.07 -7.07, 0.00 -10.00, -7.07 -7.07, "
"-10.00 -0.00, -7.07 7.07, -0.00 10.00, 7.07 7.07, 10.00 0.00))>"
)
if geos_version < (3, 13, 0):
assert f"<{poly:.2f}>" == (
"<POLYGON ((10.00 0.00, 7.07 -7.07, 0.00 -10.00, -7.07 -7.07, "
"-10.00 -0.00, -7.07 7.07, -0.00 10.00, 7.07 7.07, 10.00 0.00))>"
)
else:
assert f"<{poly:.2f}>" == (
"<POLYGON ((10.00 0.00, 7.07 -7.07, 0.00 -10.00, -7.07 -7.07, "
"-10.00 0.00, -7.07 7.07, 0.00 10.00, 7.07 7.07, 10.00 0.00))>"
)
# 'g' format varies depending on GEOS version
if geos_version < (3, 10, 0):
expected_2G = (
assert f"{poly:.2G}" == (
"POLYGON ((10 0, 7.1 -7.1, 1.6E-14 -10, -7.1 -7.1, "
"-10 -3.2E-14, -7.1 7.1, -4.6E-14 10, 7.1 7.1, 10 0))"
)
else:
expected_2G = (
assert f"{poly:.2G}" == (
"POLYGON ((10 0, 7.07 -7.07, 0 -10, -7.07 -7.07, "
"-10 0, -7.07 7.07, 0 10, 7.07 7.07, 10 0))"
)
assert f"{poly:.2G}" == expected_2G
# check empty
empty = Polygon()

View File

@@ -101,7 +101,9 @@ class TestPoint:
# Test 2D points
p = Point(1.0, 2.0)
assert p.x == 1.0
assert type(p.x) is float
assert p.y == 2.0
assert type(p.y) is float
assert p.coords[:] == [(1.0, 2.0)]
assert str(p) == p.wkt
assert p.has_z is False
@@ -114,6 +116,7 @@ class TestPoint:
assert str(p) == p.wkt
assert p.has_z is True
assert p.z == 3.0
assert type(p.z) is float
# Coordinate access
p = Point((3.0, 4.0))

View File

@@ -3,6 +3,7 @@ import unittest
import pytest
import shapely
from shapely import geos_version
from shapely.errors import TopologicalError
from shapely.geometry import GeometryCollection, LineString, MultiPoint, Point, Polygon
from shapely.wkt import loads
@@ -79,8 +80,11 @@ class OperationsTestCase(unittest.TestCase):
"POLYGON ((40 100, 80 100, 80 60, 40 60, 40 100), (60 60, 80 60, 80 40, 60 40, 60 60))"
)
assert not invalid_polygon.is_valid
with pytest.raises((TopologicalError, shapely.GEOSException)):
invalid_polygon.relate(invalid_polygon)
if geos_version < (3, 13, 0):
with pytest.raises((TopologicalError, shapely.GEOSException)):
invalid_polygon.relate(invalid_polygon)
else: # resolved with RelateNG
assert invalid_polygon.relate(invalid_polygon) == "2FFF1FFF2"
def test_hausdorff_distance(self):
point = Point(1, 1)

View File

@@ -5,6 +5,7 @@ import unittest
import pytest
import shapely
from shapely import geos_version
from shapely.geometry import Point, Polygon
@@ -67,8 +68,15 @@ class PredicatesTestCase(unittest.TestCase):
(339, 207),
]
with pytest.raises(shapely.GEOSException):
Polygon(p1).within(Polygon(p2))
g1 = Polygon(p1)
g2 = Polygon(p2)
assert not g1.is_valid
assert not g2.is_valid
if geos_version < (3, 13, 0):
with pytest.raises(shapely.GEOSException):
g1.within(g2)
else: # resolved with RelateNG
assert not g1.within(g2)
def test_relate_pattern(self):

View File

@@ -17,6 +17,7 @@ from shapely import (
from shapely.testing import assert_geometries_equal
from shapely.tests.common import (
all_types,
ArrayLike,
empty,
empty_line_string,
empty_point,
@@ -981,6 +982,17 @@ def test_oriented_envelope_pre_geos_312(geometry, expected):
assert_geometries_equal(actual, expected, normalize=True, tolerance=1e-3)
def test_oriented_evelope_array_like():
# https://github.com/shapely/shapely/issues/1929
# because we have a custom python implementation, need to ensure this has
# the same capabilities as numpy ufuncs to work with array-likes
geometries = [Point(1, 1).buffer(1), Point(2, 2).buffer(1)]
actual = shapely.oriented_envelope(ArrayLike(geometries))
assert isinstance(actual, ArrayLike)
expected = shapely.oriented_envelope(geometries)
assert_geometries_equal(np.asarray(actual), expected)
@pytest.mark.skipif(shapely.geos_version < (3, 11, 0), reason="GEOS < 3.11")
def test_concave_hull_kwargs():
p = Point(10, 10)

View File

@@ -255,7 +255,7 @@ def test_set_nan():
def test_set_nan_same_objects():
# You can't put identical objects in a set.
# x = float("nan"); set([x, x]) also retuns a set with 1 element
# x = float("nan"); set([x, x]) also returns a set with 1 element
a = set([line_string_nan] * 10)
assert len(a) == 1

View File

@@ -287,6 +287,15 @@ def test_to_wkt_none():
assert shapely.to_wkt(None) is None
def test_to_wkt_array_with_empty_z():
# See GH-2004
empty_wkt = ["POINT Z EMPTY", None, "POLYGON Z EMPTY"]
empty_geoms = shapely.from_wkt(empty_wkt)
if shapely.geos_version < (3, 9, 0):
empty_wkt = ["POINT EMPTY", None, "POLYGON EMPTY"]
assert list(shapely.to_wkt(empty_geoms)) == empty_wkt
def test_to_wkt_exceptions():
with pytest.raises(TypeError):
shapely.to_wkt(1)

View File

@@ -178,7 +178,7 @@ def test_shared_paths_non_linestring():
def _prepare_input(geometry, prepare):
"""Prepare without modifying inplace"""
"""Prepare without modifying in-place"""
if prepare:
geometry = shapely.transform(geometry, lambda x: x) # makes a copy
shapely.prepare(geometry)

View File

@@ -1,5 +1,6 @@
import os
import sys
from inspect import cleandoc
from itertools import chain
from string import ascii_letters, digits
from unittest import mock
@@ -83,13 +84,22 @@ class SomeClass:
"""
expected_docstring = """Docstring that will be mocked.
def expected_docstring(**kwds):
doc = """Docstring that will be mocked.
{indent}A multiline.
{indent}.. note:: 'func' requires at least GEOS {version}.
{indent}Some description.
{indent}"""
{indent}""".format(
**kwds
)
if sys.version_info[:2] >= (3, 13):
# There are subtle differences between inspect.cleandoc() and
# _PyCompile_CleanDoc(). Most significantly, the latter does not remove
# leading or trailing blank lines.
return cleandoc(doc) + "\n"
return doc
@pytest.mark.parametrize("version", ["3.7.0", "3.7.1", "3.6.2"])
@@ -104,7 +114,7 @@ def test_requires_geos_not_ok(version, mocked_geos_version):
with pytest.raises(shapely.errors.UnsupportedGEOSVersionError):
wrapped()
assert wrapped.__doc__ == expected_docstring.format(version=version, indent=" " * 4)
assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 4)
@pytest.mark.parametrize("version", ["3.6.0", "3.8.0"])
@@ -112,7 +122,7 @@ def test_requires_geos_doc_build(version, mocked_geos_version, sphinx_doc_build)
"""The requires_geos decorator always adapts the docstring."""
wrapped = requires_geos(version)(func)
assert wrapped.__doc__ == expected_docstring.format(version=version, indent=" " * 4)
assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 4)
@pytest.mark.parametrize("version", ["3.6.0", "3.8.0"])
@@ -120,7 +130,7 @@ def test_requires_geos_method(version, mocked_geos_version, sphinx_doc_build):
"""The requires_geos decorator adjusts methods docstrings correctly"""
wrapped = requires_geos(version)(SomeClass.func)
assert wrapped.__doc__ == expected_docstring.format(version=version, indent=" " * 8)
assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 8)
@multithreading_enabled

View File

@@ -298,7 +298,7 @@ def test_is_ccw(geom, expected):
def _prepare_with_copy(geometry):
"""Prepare without modifying inplace"""
"""Prepare without modifying in-place"""
geometry = shapely.transform(geometry, lambda x: x) # makes a copy
shapely.prepare(geometry)
return geometry

View File

@@ -110,18 +110,30 @@ def test_points():
"POINT EMPTY",
"POINT EMPTY",
"POINT (4 4)",
None,
"POINT EMPTY",
]
)
typ, result, offsets = shapely.to_ragged_array(arr)
expected = np.array(
[[0, 0], [1, 1], [np.nan, np.nan], [np.nan, np.nan], [4, 4], [np.nan, np.nan]]
[
[0, 0],
[1, 1],
[np.nan, np.nan],
[np.nan, np.nan],
[4, 4],
[np.nan, np.nan],
[np.nan, np.nan],
]
)
assert typ == shapely.GeometryType.POINT
assert len(result) == len(arr)
assert_allclose(result, expected)
assert len(offsets) == 0
geoms = shapely.from_ragged_array(typ, result)
# in a roundtrip, missing geometries come back as empty
arr[-2] = shapely.from_wkt("POINT EMPTY")
assert_geometries_equal(geoms, arr)
@@ -133,6 +145,7 @@ def test_linestrings():
"LINESTRING EMPTY",
"LINESTRING EMPTY",
"LINESTRING (10 10, 20 20, 10 40)",
None,
"LINESTRING EMPTY",
]
)
@@ -151,13 +164,15 @@ def test_linestrings():
[10.0, 40.0],
]
)
expected_offsets = np.array([0, 3, 7, 7, 7, 10, 10])
expected_offsets = np.array([0, 3, 7, 7, 7, 10, 10, 10])
assert typ == shapely.GeometryType.LINESTRING
assert_allclose(coords, expected)
assert len(offsets) == 1
assert_allclose(offsets[0], expected_offsets)
result = shapely.from_ragged_array(typ, coords, offsets)
# in a roundtrip, missing geometries come back as empty
arr[-2] = shapely.from_wkt("LINESTRING EMPTY")
assert_geometries_equal(result, arr)
@@ -169,6 +184,7 @@ def test_polygons():
"POLYGON EMPTY",
"POLYGON EMPTY",
"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))",
None,
"POLYGON EMPTY",
]
)
@@ -197,7 +213,7 @@ def test_polygons():
]
)
expected_offsets1 = np.array([0, 5, 10, 14, 19])
expected_offsets2 = np.array([0, 1, 3, 3, 3, 4, 4])
expected_offsets2 = np.array([0, 1, 3, 3, 3, 4, 4, 4])
assert typ == shapely.GeometryType.POLYGON
assert_allclose(coords, expected)
@@ -206,6 +222,8 @@ def test_polygons():
assert_allclose(offsets[1], expected_offsets2)
result = shapely.from_ragged_array(typ, coords, offsets)
# in a roundtrip, missing geometries come back as empty
arr[-2] = shapely.from_wkt("POLYGON EMPTY")
assert_geometries_equal(result, arr)
@@ -217,6 +235,7 @@ def test_multipoints():
"MULTIPOINT EMPTY",
"MULTIPOINT EMPTY",
"MULTIPOINT (30 10, 10 30, 40 40)",
None,
"MULTIPOINT EMPTY",
]
)
@@ -233,7 +252,7 @@ def test_multipoints():
[40.0, 40.0],
]
)
expected_offsets = np.array([0, 4, 5, 5, 5, 8, 8])
expected_offsets = np.array([0, 4, 5, 5, 5, 8, 8, 8])
assert typ == shapely.GeometryType.MULTIPOINT
assert_allclose(coords, expected)
@@ -241,6 +260,8 @@ def test_multipoints():
assert_allclose(offsets[0], expected_offsets)
result = shapely.from_ragged_array(typ, coords, offsets)
# in a roundtrip, missing geometries come back as empty
arr[-2] = shapely.from_wkt("MULTIPOINT EMPTY")
assert_geometries_equal(result, arr)
@@ -252,6 +273,7 @@ def test_multilinestrings():
"MULTILINESTRING EMPTY",
"MULTILINESTRING EMPTY",
"MULTILINESTRING ((35 10, 45 45), (15 40, 10 20), (30 10, 10 30, 40 40))",
None,
"MULTILINESTRING EMPTY",
]
)
@@ -278,7 +300,7 @@ def test_multilinestrings():
]
)
expected_offsets1 = np.array([0, 3, 6, 10, 12, 14, 17])
expected_offsets2 = np.array([0, 1, 3, 3, 3, 6, 6])
expected_offsets2 = np.array([0, 1, 3, 3, 3, 6, 6, 6])
assert typ == shapely.GeometryType.MULTILINESTRING
assert_allclose(coords, expected)
@@ -287,6 +309,8 @@ def test_multilinestrings():
assert_allclose(offsets[1], expected_offsets2)
result = shapely.from_ragged_array(typ, coords, offsets)
# in a roundtrip, missing geometries come back as empty
arr[-2] = shapely.from_wkt("MULTILINESTRING EMPTY")
assert_geometries_equal(result, arr)
@@ -298,6 +322,7 @@ def test_multipolygons():
"MULTIPOLYGON EMPTY",
"MULTIPOLYGON EMPTY",
"MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)))",
None,
"MULTIPOLYGON EMPTY",
]
)
@@ -335,7 +360,7 @@ def test_multipolygons():
)
expected_offsets1 = np.array([0, 5, 9, 13, 19, 23, 27])
expected_offsets2 = np.array([0, 2, 3, 5, 6])
expected_offsets3 = np.array([0, 1, 3, 3, 3, 4, 4])
expected_offsets3 = np.array([0, 1, 3, 3, 3, 4, 4, 4])
assert typ == shapely.GeometryType.MULTIPOLYGON
assert_allclose(coords, expected)
@@ -345,6 +370,8 @@ def test_multipolygons():
assert_allclose(offsets[2], expected_offsets3)
result = shapely.from_ragged_array(typ, coords, offsets)
# in a roundtrip, missing geometries come back as empty
arr[-2] = shapely.from_wkt("MULTIPOLYGON EMPTY")
assert_geometries_equal(result, arr)

View File

@@ -22,14 +22,14 @@ SET_OPERATIONS = (
shapely.intersection,
shapely.symmetric_difference,
shapely.union,
# shapely.coverage_union is tested seperately
# shapely.coverage_union is tested separately
)
REDUCE_SET_OPERATIONS = (
(shapely.intersection_all, shapely.intersection),
(shapely.symmetric_difference_all, shapely.symmetric_difference),
(shapely.union_all, shapely.union),
# shapely.coverage_union_all, shapely.coverage_union) is tested seperately
# shapely.coverage_union_all, shapely.coverage_union) is tested separately
)
# operations that support fixed precision
@@ -53,6 +53,13 @@ non_polygon_types = [
@pytest.mark.parametrize("a", all_types)
@pytest.mark.parametrize("func", SET_OPERATIONS)
def test_set_operation_array(a, func):
if (
func is shapely.difference
and a.geom_type == "GeometryCollection"
and shapely.get_num_geometries(a) == 2
and shapely.geos_version == (3, 9, 5)
):
pytest.xfail("GEOS 3.9.5 crashes with mixed collection")
actual = func(a, point)
assert isinstance(actual, Geometry)
@@ -270,7 +277,7 @@ def test_set_operation_prec_reduce_all_none(n, func, related_func):
@pytest.mark.parametrize("n", range(1, 4))
def test_coverage_union_reduce_1dim(n):
"""
This is tested seperately from other set operations as it differs in two ways:
This is tested separately from other set operations as it differs in two ways:
1. It expects only non-overlapping polygons
2. It expects GEOS 3.8.0+
"""
@@ -307,7 +314,7 @@ def test_coverage_union_overlapping_inputs():
other = Polygon([(1, 0), (0.9, 1), (2, 1), (2, 0), (1, 0)])
if shapely.geos_version >= (3, 12, 0):
# Return mostly unchaged output
# Return mostly unchanged output
result = shapely.coverage_union(polygon, other)
expected = shapely.multipolygons([polygon, other])
assert_geometries_equal(result, expected, normalize=True)

View File

@@ -10,7 +10,7 @@ import pytest
from numpy.testing import assert_array_equal
import shapely
from shapely import box, geos_version, MultiPoint, Point, STRtree
from shapely import box, geos_version, LineString, MultiPoint, Point, STRtree
from shapely.errors import UnsupportedGEOSVersionError
from shapely.testing import assert_geometries_equal
from shapely.tests.common import (
@@ -368,39 +368,51 @@ def test_query_with_partially_prepared_inputs(tree):
@pytest.mark.parametrize(
"predicate",
"predicate,expected",
[
# intersects is intentionally omitted; it does not raise an exception
pytest.param(
"intersects",
[1],
marks=pytest.mark.xfail(geos_version < (3, 13, 0), reason="GEOS < 3.13"),
),
pytest.param(
"within",
[],
marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"),
),
pytest.param(
"contains",
[],
marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"),
),
"overlaps",
"crosses",
"touches",
("overlaps", []),
("crosses", [1]),
("touches", []),
pytest.param(
"covers",
[],
marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"),
),
pytest.param(
"covered_by",
[],
marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"),
),
pytest.param(
"contains_properly",
[],
marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"),
),
],
)
def test_query_predicate_errors(tree, predicate):
def test_query_predicate_errors(tree, predicate, expected):
with ignore_invalid():
line_nan = shapely.linestrings([1, 1], [1, float("nan")])
with pytest.raises(shapely.GEOSException):
tree.query(line_nan, predicate=predicate)
if geos_version < (3, 13, 0):
with pytest.raises(shapely.GEOSException):
tree.query(line_nan, predicate=predicate)
else:
assert_array_equal(tree.query(line_nan, predicate=predicate), expected)
### predicate == 'intersects'
@@ -928,12 +940,12 @@ def test_query_crosses_polygons(poly_tree, geometry, expected):
# box contains points but touches only those at edges
(box(3, 3, 6, 6), [3, 6]),
([box(3, 3, 6, 6)], [[0, 0], [3, 6]]),
# buffer completely contains point in tree
# polygon completely contains point in tree
(shapely.buffer(Point(3, 3), 1), []),
([shapely.buffer(Point(3, 3), 1)], [[], []]),
# buffer intersects 2 points but touches only one
(shapely.buffer(Point(0, 1), 1), [1]),
([shapely.buffer(Point(0, 1), 1)], [[0], [1]]),
# linestring intersects 2 points but touches only one
(LineString([(-1, -1), (1, 1)]), [1]),
([LineString([(-1, -1), (1, 1)])], [[0], [1]]),
# multipoints intersect but not valid relation
(MultiPoint([[5, 5], [7, 7]]), []),
([MultiPoint([[5, 5], [7, 7]])], [[], []]),