test_misc.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import os
  2. import sys
  3. from inspect import cleandoc
  4. from itertools import chain
  5. from string import ascii_letters, digits
  6. from unittest import mock
  7. import numpy as np
  8. import pytest
  9. import shapely
  10. from shapely.decorators import multithreading_enabled, requires_geos
  11. @pytest.fixture
  12. def mocked_geos_version():
  13. with mock.patch.object(shapely.lib, "geos_version", new=(3, 10, 1)):
  14. yield "3.10.1"
  15. @pytest.fixture
  16. def sphinx_doc_build():
  17. os.environ["SPHINX_DOC_BUILD"] = "1"
  18. yield
  19. del os.environ["SPHINX_DOC_BUILD"]
  20. def test_version():
  21. assert isinstance(shapely.__version__, str)
  22. def test_geos_version():
  23. expected = "{}.{}.{}".format(*shapely.geos_version)
  24. actual = shapely.geos_version_string
  25. # strip any beta / dev qualifiers
  26. if any(c.isalpha() for c in actual):
  27. if actual[-1].isnumeric():
  28. actual = actual.rstrip(digits)
  29. actual = actual.rstrip(ascii_letters)
  30. assert actual == expected
  31. def test_geos_capi_version():
  32. expected = "{}.{}.{}-CAPI-{}.{}.{}".format(
  33. *(shapely.geos_version + shapely.geos_capi_version)
  34. )
  35. # split into component parts and strip any beta / dev qualifiers
  36. (
  37. actual_geos_version,
  38. actual_geos_api_version,
  39. ) = shapely.geos_capi_version_string.split("-CAPI-")
  40. if any(c.isalpha() for c in actual_geos_version):
  41. if actual_geos_version[-1].isnumeric():
  42. actual_geos_version = actual_geos_version.rstrip(digits)
  43. actual_geos_version = actual_geos_version.rstrip(ascii_letters)
  44. actual_geos_version = actual_geos_version.rstrip(ascii_letters)
  45. assert f"{actual_geos_version}-CAPI-{actual_geos_api_version}" == expected
  46. def func():
  47. """Docstring that will be mocked.
  48. A multiline.
  49. Some description.
  50. """
  51. class SomeClass:
  52. def func(self):
  53. """Docstring that will be mocked.
  54. A multiline.
  55. Some description.
  56. """
  57. def expected_docstring(**kwds):
  58. doc = """Docstring that will be mocked.
  59. {indent}A multiline.
  60. {indent}.. note:: 'func' requires at least GEOS {version}.
  61. {indent}Some description.
  62. {indent}""".format(**kwds)
  63. if sys.version_info[:2] >= (3, 13):
  64. # There are subtle differences between inspect.cleandoc() and
  65. # _PyCompile_CleanDoc(). Most significantly, the latter does not remove
  66. # leading or trailing blank lines.
  67. return cleandoc(doc) + "\n"
  68. return doc
  69. @pytest.mark.parametrize("version", ["3.10.0", "3.10.1", "3.9.2"])
  70. def test_requires_geos_ok(version, mocked_geos_version):
  71. wrapped = requires_geos(version)(func)
  72. wrapped()
  73. assert wrapped is func
  74. @pytest.mark.parametrize("version", ["3.10.2", "3.11.0", "3.11.1"])
  75. def test_requires_geos_not_ok(version, mocked_geos_version):
  76. wrapped = requires_geos(version)(func)
  77. with pytest.raises(shapely.errors.UnsupportedGEOSVersionError):
  78. wrapped()
  79. assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 4)
  80. @pytest.mark.parametrize("version", ["3.9.0", "3.10.0"])
  81. def test_requires_geos_doc_build(version, mocked_geos_version, sphinx_doc_build):
  82. """The requires_geos decorator always adapts the docstring."""
  83. wrapped = requires_geos(version)(func)
  84. assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 4)
  85. @pytest.mark.parametrize("version", ["3.9.0", "3.10.0"])
  86. def test_requires_geos_method(version, mocked_geos_version, sphinx_doc_build):
  87. """The requires_geos decorator adjusts methods docstrings correctly"""
  88. wrapped = requires_geos(version)(SomeClass.func)
  89. assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 8)
  90. @multithreading_enabled
  91. def set_first_element(value, *args, **kwargs):
  92. for arg in chain(args, kwargs.values()):
  93. if hasattr(arg, "__setitem__"):
  94. arg[0] = value
  95. return arg
  96. def test_multithreading_enabled_raises_arg():
  97. arr = np.empty((1,), dtype=object)
  98. # set_first_element cannot change the input array
  99. with pytest.raises(ValueError):
  100. set_first_element(42, arr)
  101. # afterwards, we can
  102. arr[0] = 42
  103. assert arr[0] == 42
  104. def test_multithreading_enabled_raises_kwarg():
  105. arr = np.empty((1,), dtype=object)
  106. # set_first_element cannot change the input array
  107. with pytest.raises(ValueError):
  108. set_first_element(42, arr=arr)
  109. # writable flag goes to original state
  110. assert arr.flags.writeable
  111. def test_multithreading_enabled_preserves_flag():
  112. arr = np.empty((1,), dtype=object)
  113. arr.flags.writeable = False
  114. # set_first_element cannot change the input array
  115. with pytest.raises(ValueError):
  116. set_first_element(42, arr)
  117. # writable flag goes to original state
  118. assert not arr.flags.writeable
  119. @pytest.mark.parametrize(
  120. "args,kwargs",
  121. [
  122. ((np.empty((1,), dtype=float),), {}), # float-dtype ndarray is untouched
  123. ((), {"a": np.empty((1,), dtype=float)}),
  124. (([1],), {}), # non-ndarray is untouched
  125. ((), {"a": [1]}),
  126. ((), {"out": np.empty((1,), dtype=object)}), # ufunc kwarg 'out' is untouched
  127. (
  128. (),
  129. {"where": np.empty((1,), dtype=object)},
  130. ), # ufunc kwarg 'where' is untouched
  131. ],
  132. )
  133. def test_multithreading_enabled_ok(args, kwargs):
  134. result = set_first_element(42, *args, **kwargs)
  135. assert result[0] == 42