aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/multidict/tests/conftest.py
blob: 0d003950cd721b236ef0fde73a37a48d55e0ae04 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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"
        )