conftest.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. from __future__ import annotations
  2. import argparse
  3. import pickle
  4. from dataclasses import dataclass
  5. from importlib import import_module
  6. from sys import version_info as _version_info
  7. from types import ModuleType
  8. from typing import Callable, Type
  9. try:
  10. from functools import cached_property # Python 3.8+
  11. except ImportError:
  12. from functools import lru_cache as _lru_cache
  13. def cached_property(func):
  14. return property(_lru_cache()(func))
  15. import pytest
  16. from multidict import MultiMapping, MutableMultiMapping
  17. C_EXT_MARK = pytest.mark.c_extension
  18. PY_38_AND_BELOW = _version_info < (3, 9)
  19. @dataclass(frozen=True)
  20. class MultidictImplementation:
  21. """A facade for accessing importable multidict module variants.
  22. An instance essentially represents a c-extension or a pure-python module.
  23. The actual underlying module is accessed dynamically through a property and
  24. is cached.
  25. It also has a text tag depending on what variant it is, and a string
  26. representation suitable for use in Pytest's test IDs via parametrization.
  27. """
  28. is_pure_python: bool
  29. """A flag showing whether this is a pure-python module or a C-extension."""
  30. @cached_property
  31. def tag(self) -> str:
  32. """Return a text representation of the pure-python attribute."""
  33. return "pure-python" if self.is_pure_python else "c-extension"
  34. @cached_property
  35. def imported_module(self) -> ModuleType:
  36. """Return a loaded importable containing a multidict variant."""
  37. importable_module = "_multidict_py" if self.is_pure_python else "_multidict"
  38. return import_module(f"multidict.{importable_module}")
  39. def __str__(self):
  40. """Render the implementation facade instance as a string."""
  41. return f"{self.tag}-module"
  42. @pytest.fixture(
  43. scope="session",
  44. params=(
  45. pytest.param(
  46. MultidictImplementation(is_pure_python=False),
  47. marks=C_EXT_MARK,
  48. ),
  49. MultidictImplementation(is_pure_python=True),
  50. ),
  51. ids=str,
  52. )
  53. def multidict_implementation(request: pytest.FixtureRequest) -> MultidictImplementation:
  54. """Return a multidict variant facade."""
  55. return request.param
  56. @pytest.fixture(scope="session")
  57. def multidict_module(
  58. multidict_implementation: MultidictImplementation,
  59. ) -> ModuleType:
  60. """Return a pre-imported module containing a multidict variant."""
  61. return multidict_implementation.imported_module
  62. @pytest.fixture(
  63. scope="session",
  64. params=("MultiDict", "CIMultiDict"),
  65. ids=("case-sensitive", "case-insensitive"),
  66. )
  67. def any_multidict_class_name(request: pytest.FixtureRequest) -> str:
  68. """Return a class name of a mutable multidict implementation."""
  69. return request.param
  70. @pytest.fixture(scope="session")
  71. def any_multidict_class(
  72. any_multidict_class_name: str,
  73. multidict_module: ModuleType,
  74. ) -> Type[MutableMultiMapping[str]]:
  75. """Return a class object of a mutable multidict implementation."""
  76. return getattr(multidict_module, any_multidict_class_name)
  77. @pytest.fixture(scope="session")
  78. def case_sensitive_multidict_class(
  79. multidict_module: ModuleType,
  80. ) -> Type[MutableMultiMapping[str]]:
  81. """Return a case-sensitive mutable multidict class."""
  82. return multidict_module.MultiDict
  83. @pytest.fixture(scope="session")
  84. def case_insensitive_multidict_class(
  85. multidict_module: ModuleType,
  86. ) -> Type[MutableMultiMapping[str]]:
  87. """Return a case-insensitive mutable multidict class."""
  88. return multidict_module.CIMultiDict
  89. @pytest.fixture(scope="session")
  90. def case_insensitive_str_class(multidict_module: ModuleType) -> Type[str]:
  91. """Return a case-insensitive string class."""
  92. return multidict_module.istr
  93. @pytest.fixture(scope="session")
  94. def any_multidict_proxy_class_name(any_multidict_class_name: str) -> str:
  95. """Return a class name of an immutable multidict implementation."""
  96. return f"{any_multidict_class_name}Proxy"
  97. @pytest.fixture(scope="session")
  98. def any_multidict_proxy_class(
  99. any_multidict_proxy_class_name: str,
  100. multidict_module: ModuleType,
  101. ) -> Type[MultiMapping[str]]:
  102. """Return an immutable multidict implementation class object."""
  103. return getattr(multidict_module, any_multidict_proxy_class_name)
  104. @pytest.fixture(scope="session")
  105. def case_sensitive_multidict_proxy_class(
  106. multidict_module: ModuleType,
  107. ) -> Type[MutableMultiMapping[str]]:
  108. """Return a case-sensitive immutable multidict class."""
  109. return multidict_module.MultiDictProxy
  110. @pytest.fixture(scope="session")
  111. def case_insensitive_multidict_proxy_class(
  112. multidict_module: ModuleType,
  113. ) -> Type[MutableMultiMapping[str]]:
  114. """Return a case-insensitive immutable multidict class."""
  115. return multidict_module.CIMultiDictProxy
  116. @pytest.fixture(scope="session")
  117. def multidict_getversion_callable(multidict_module: ModuleType) -> Callable:
  118. """Return a ``getversion()`` function for current implementation."""
  119. return multidict_module.getversion
  120. def pytest_addoption(
  121. parser: pytest.Parser,
  122. pluginmanager: pytest.PytestPluginManager,
  123. ) -> None:
  124. """Define a new ``--c-extensions`` flag.
  125. This lets the callers deselect tests executed against the C-extension
  126. version of the ``multidict`` implementation.
  127. """
  128. del pluginmanager
  129. parser.addoption(
  130. "--c-extensions", # disabled with `--no-c-extensions`
  131. action="store_true" if PY_38_AND_BELOW else argparse.BooleanOptionalAction,
  132. default=True,
  133. dest="c_extensions",
  134. help="Test C-extensions (on by default)",
  135. )
  136. if PY_38_AND_BELOW:
  137. parser.addoption(
  138. "--no-c-extensions",
  139. action="store_false",
  140. dest="c_extensions",
  141. help="Skip testing C-extensions (on by default)",
  142. )
  143. def pytest_collection_modifyitems(
  144. session: pytest.Session,
  145. config: pytest.Config,
  146. items: list[pytest.Item],
  147. ) -> None:
  148. """Deselect tests against C-extensions when requested via CLI."""
  149. test_c_extensions = config.getoption("--c-extensions") is True
  150. if test_c_extensions:
  151. return
  152. selected_tests = []
  153. deselected_tests = []
  154. for item in items:
  155. c_ext = item.get_closest_marker(C_EXT_MARK.name) is not None
  156. target_items_list = deselected_tests if c_ext else selected_tests
  157. target_items_list.append(item)
  158. config.hook.pytest_deselected(items=deselected_tests)
  159. items[:] = selected_tests
  160. def pytest_configure(config: pytest.Config) -> None:
  161. """Declare the C-extension marker in config."""
  162. config.addinivalue_line(
  163. "markers",
  164. f"{C_EXT_MARK.name}: tests running against the C-extension implementation.",
  165. )
  166. def pytest_generate_tests(metafunc):
  167. if "pickle_protocol" in metafunc.fixturenames:
  168. metafunc.parametrize(
  169. "pickle_protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)), scope="session"
  170. )