123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- from __future__ import annotations
- import argparse
- import pickle
- from dataclasses import dataclass
- from importlib import import_module
- from sys import version_info as _version_info
- from types import ModuleType
- from typing import Callable, Type
- try:
- from functools import cached_property # Python 3.8+
- except ImportError:
- from functools import lru_cache as _lru_cache
- def cached_property(func):
- return property(_lru_cache()(func))
- import pytest
- from multidict import MultiMapping, MutableMultiMapping
- C_EXT_MARK = pytest.mark.c_extension
- PY_38_AND_BELOW = _version_info < (3, 9)
- @dataclass(frozen=True)
- class MultidictImplementation:
- """A facade for accessing importable multidict module variants.
- An instance essentially represents a c-extension or a pure-python module.
- The actual underlying module is accessed dynamically through a property and
- is cached.
- It also has a text tag depending on what variant it is, and a string
- representation suitable for use in Pytest's test IDs via parametrization.
- """
- is_pure_python: bool
- """A flag showing whether this is a pure-python module or a C-extension."""
- @cached_property
- def tag(self) -> str:
- """Return a text representation of the pure-python attribute."""
- return "pure-python" if self.is_pure_python else "c-extension"
- @cached_property
- def imported_module(self) -> ModuleType:
- """Return a loaded importable containing a multidict variant."""
- importable_module = "_multidict_py" if self.is_pure_python else "_multidict"
- return import_module(f"multidict.{importable_module}")
- def __str__(self):
- """Render the implementation facade instance as a string."""
- return f"{self.tag}-module"
- @pytest.fixture(
- scope="session",
- params=(
- pytest.param(
- MultidictImplementation(is_pure_python=False),
- marks=C_EXT_MARK,
- ),
- MultidictImplementation(is_pure_python=True),
- ),
- ids=str,
- )
- def multidict_implementation(request: pytest.FixtureRequest) -> MultidictImplementation:
- """Return a multidict variant facade."""
- return request.param
- @pytest.fixture(scope="session")
- def multidict_module(
- multidict_implementation: MultidictImplementation,
- ) -> ModuleType:
- """Return a pre-imported module containing a multidict variant."""
- return multidict_implementation.imported_module
- @pytest.fixture(
- scope="session",
- params=("MultiDict", "CIMultiDict"),
- ids=("case-sensitive", "case-insensitive"),
- )
- def any_multidict_class_name(request: pytest.FixtureRequest) -> str:
- """Return a class name of a mutable multidict implementation."""
- return request.param
- @pytest.fixture(scope="session")
- def any_multidict_class(
- any_multidict_class_name: str,
- multidict_module: ModuleType,
- ) -> Type[MutableMultiMapping[str]]:
- """Return a class object of a mutable multidict implementation."""
- return getattr(multidict_module, any_multidict_class_name)
- @pytest.fixture(scope="session")
- def case_sensitive_multidict_class(
- multidict_module: ModuleType,
- ) -> Type[MutableMultiMapping[str]]:
- """Return a case-sensitive mutable multidict class."""
- return multidict_module.MultiDict
- @pytest.fixture(scope="session")
- def case_insensitive_multidict_class(
- multidict_module: ModuleType,
- ) -> Type[MutableMultiMapping[str]]:
- """Return a case-insensitive mutable multidict class."""
- return multidict_module.CIMultiDict
- @pytest.fixture(scope="session")
- def case_insensitive_str_class(multidict_module: ModuleType) -> Type[str]:
- """Return a case-insensitive string class."""
- return multidict_module.istr
- @pytest.fixture(scope="session")
- def any_multidict_proxy_class_name(any_multidict_class_name: str) -> str:
- """Return a class name of an immutable multidict implementation."""
- return f"{any_multidict_class_name}Proxy"
- @pytest.fixture(scope="session")
- def any_multidict_proxy_class(
- any_multidict_proxy_class_name: str,
- multidict_module: ModuleType,
- ) -> Type[MultiMapping[str]]:
- """Return an immutable multidict implementation class object."""
- return getattr(multidict_module, any_multidict_proxy_class_name)
- @pytest.fixture(scope="session")
- def case_sensitive_multidict_proxy_class(
- multidict_module: ModuleType,
- ) -> Type[MutableMultiMapping[str]]:
- """Return a case-sensitive immutable multidict class."""
- return multidict_module.MultiDictProxy
- @pytest.fixture(scope="session")
- def case_insensitive_multidict_proxy_class(
- multidict_module: ModuleType,
- ) -> Type[MutableMultiMapping[str]]:
- """Return a case-insensitive immutable multidict class."""
- return multidict_module.CIMultiDictProxy
- @pytest.fixture(scope="session")
- def multidict_getversion_callable(multidict_module: ModuleType) -> Callable:
- """Return a ``getversion()`` function for current implementation."""
- return multidict_module.getversion
- def pytest_addoption(
- parser: pytest.Parser,
- pluginmanager: pytest.PytestPluginManager,
- ) -> None:
- """Define a new ``--c-extensions`` flag.
- This lets the callers deselect tests executed against the C-extension
- version of the ``multidict`` implementation.
- """
- del pluginmanager
- parser.addoption(
- "--c-extensions", # disabled with `--no-c-extensions`
- action="store_true" if PY_38_AND_BELOW else argparse.BooleanOptionalAction,
- default=True,
- dest="c_extensions",
- help="Test C-extensions (on by default)",
- )
- if PY_38_AND_BELOW:
- parser.addoption(
- "--no-c-extensions",
- action="store_false",
- dest="c_extensions",
- help="Skip testing C-extensions (on by default)",
- )
- def pytest_collection_modifyitems(
- session: pytest.Session,
- config: pytest.Config,
- items: list[pytest.Item],
- ) -> None:
- """Deselect tests against C-extensions when requested via CLI."""
- test_c_extensions = config.getoption("--c-extensions") is True
- if test_c_extensions:
- return
- selected_tests = []
- deselected_tests = []
- for item in items:
- c_ext = item.get_closest_marker(C_EXT_MARK.name) is not None
- target_items_list = deselected_tests if c_ext else selected_tests
- target_items_list.append(item)
- config.hook.pytest_deselected(items=deselected_tests)
- items[:] = selected_tests
- def pytest_configure(config: pytest.Config) -> None:
- """Declare the C-extension marker in config."""
- config.addinivalue_line(
- "markers",
- f"{C_EXT_MARK.name}: tests running against the C-extension implementation.",
- )
- def pytest_generate_tests(metafunc):
- if "pickle_protocol" in metafunc.fixturenames:
- metafunc.parametrize(
- "pickle_protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)), scope="session"
- )
|