190 lines
5.3 KiB
Python
190 lines
5.3 KiB
Python
import os
|
|
import sys
|
|
from inspect import cleandoc
|
|
from itertools import chain
|
|
from string import ascii_letters, digits
|
|
from unittest import mock
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import shapely
|
|
from shapely.decorators import multithreading_enabled, requires_geos
|
|
|
|
|
|
@pytest.fixture
|
|
def mocked_geos_version():
|
|
with mock.patch.object(shapely.lib, "geos_version", new=(3, 10, 1)):
|
|
yield "3.10.1"
|
|
|
|
|
|
@pytest.fixture
|
|
def sphinx_doc_build():
|
|
os.environ["SPHINX_DOC_BUILD"] = "1"
|
|
yield
|
|
del os.environ["SPHINX_DOC_BUILD"]
|
|
|
|
|
|
def test_version():
|
|
assert isinstance(shapely.__version__, str)
|
|
|
|
|
|
def test_geos_version():
|
|
expected = "{}.{}.{}".format(*shapely.geos_version)
|
|
actual = shapely.geos_version_string
|
|
|
|
# strip any beta / dev qualifiers
|
|
if any(c.isalpha() for c in actual):
|
|
if actual[-1].isnumeric():
|
|
actual = actual.rstrip(digits)
|
|
actual = actual.rstrip(ascii_letters)
|
|
|
|
assert actual == expected
|
|
|
|
|
|
def test_geos_capi_version():
|
|
expected = "{}.{}.{}-CAPI-{}.{}.{}".format(
|
|
*(shapely.geos_version + shapely.geos_capi_version)
|
|
)
|
|
|
|
# split into component parts and strip any beta / dev qualifiers
|
|
(
|
|
actual_geos_version,
|
|
actual_geos_api_version,
|
|
) = shapely.geos_capi_version_string.split("-CAPI-")
|
|
|
|
if any(c.isalpha() for c in actual_geos_version):
|
|
if actual_geos_version[-1].isnumeric():
|
|
actual_geos_version = actual_geos_version.rstrip(digits)
|
|
actual_geos_version = actual_geos_version.rstrip(ascii_letters)
|
|
actual_geos_version = actual_geos_version.rstrip(ascii_letters)
|
|
|
|
assert f"{actual_geos_version}-CAPI-{actual_geos_api_version}" == expected
|
|
|
|
|
|
def func():
|
|
"""Docstring that will be mocked.
|
|
A multiline.
|
|
|
|
Some description.
|
|
"""
|
|
|
|
|
|
class SomeClass:
|
|
def func(self):
|
|
"""Docstring that will be mocked.
|
|
A multiline.
|
|
|
|
Some description.
|
|
"""
|
|
|
|
|
|
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}""".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.10.0", "3.10.1", "3.9.2"])
|
|
def test_requires_geos_ok(version, mocked_geos_version):
|
|
wrapped = requires_geos(version)(func)
|
|
wrapped()
|
|
assert wrapped is func
|
|
|
|
|
|
@pytest.mark.parametrize("version", ["3.10.2", "3.11.0", "3.11.1"])
|
|
def test_requires_geos_not_ok(version, mocked_geos_version):
|
|
wrapped = requires_geos(version)(func)
|
|
with pytest.raises(shapely.errors.UnsupportedGEOSVersionError):
|
|
wrapped()
|
|
|
|
assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 4)
|
|
|
|
|
|
@pytest.mark.parametrize("version", ["3.9.0", "3.10.0"])
|
|
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(version=version, indent=" " * 4)
|
|
|
|
|
|
@pytest.mark.parametrize("version", ["3.9.0", "3.10.0"])
|
|
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(version=version, indent=" " * 8)
|
|
|
|
|
|
@multithreading_enabled
|
|
def set_first_element(value, *args, **kwargs):
|
|
for arg in chain(args, kwargs.values()):
|
|
if hasattr(arg, "__setitem__"):
|
|
arg[0] = value
|
|
return arg
|
|
|
|
|
|
def test_multithreading_enabled_raises_arg():
|
|
arr = np.empty((1,), dtype=object)
|
|
|
|
# set_first_element cannot change the input array
|
|
with pytest.raises(ValueError):
|
|
set_first_element(42, arr)
|
|
|
|
# afterwards, we can
|
|
arr[0] = 42
|
|
assert arr[0] == 42
|
|
|
|
|
|
def test_multithreading_enabled_raises_kwarg():
|
|
arr = np.empty((1,), dtype=object)
|
|
|
|
# set_first_element cannot change the input array
|
|
with pytest.raises(ValueError):
|
|
set_first_element(42, arr=arr)
|
|
|
|
# writable flag goes to original state
|
|
assert arr.flags.writeable
|
|
|
|
|
|
def test_multithreading_enabled_preserves_flag():
|
|
arr = np.empty((1,), dtype=object)
|
|
arr.flags.writeable = False
|
|
|
|
# set_first_element cannot change the input array
|
|
with pytest.raises(ValueError):
|
|
set_first_element(42, arr)
|
|
|
|
# writable flag goes to original state
|
|
assert not arr.flags.writeable
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"args,kwargs",
|
|
[
|
|
((np.empty((1,), dtype=float),), {}), # float-dtype ndarray is untouched
|
|
((), {"a": np.empty((1,), dtype=float)}),
|
|
(([1],), {}), # non-ndarray is untouched
|
|
((), {"a": [1]}),
|
|
((), {"out": np.empty((1,), dtype=object)}), # ufunc kwarg 'out' is untouched
|
|
(
|
|
(),
|
|
{"where": np.empty((1,), dtype=object)},
|
|
), # ufunc kwarg 'where' is untouched
|
|
],
|
|
)
|
|
def test_multithreading_enabled_ok(args, kwargs):
|
|
result = set_first_element(42, *args, **kwargs)
|
|
assert result[0] == 42
|