From 7451dcef15087e6f131c150d22b214e5c9a0302c Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Wed, 10 Jan 2024 21:01:32 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Rewire=20pytest=20fixtures=20avo?= =?UTF-8?q?iding=20import=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch also refactors and reduces the duplication of the previously existing fixtures for retrieving different multidict module implementations and makes the c-extension testing controllable by a CLI option on the pytest level. Fixes #837 --- .github/workflows/ci-cd.yml | 8 +- .mypy.ini | 2 +- ...kle.0 => cimultidict-c-extension.pickle.0} | 0 ...kle.1 => cimultidict-c-extension.pickle.1} | Bin ...kle.2 => cimultidict-c-extension.pickle.2} | Bin ...kle.3 => cimultidict-c-extension.pickle.3} | Bin ...kle.4 => cimultidict-c-extension.pickle.4} | Bin ...kle.5 => cimultidict-c-extension.pickle.5} | Bin ...kle.0 => cimultidict-pure-python.pickle.0} | 0 ...kle.1 => cimultidict-pure-python.pickle.1} | Bin ...kle.2 => cimultidict-pure-python.pickle.2} | Bin ...kle.3 => cimultidict-pure-python.pickle.3} | Bin ...kle.4 => cimultidict-pure-python.pickle.4} | Bin ...kle.5 => cimultidict-pure-python.pickle.5} | Bin tests/conftest.py | 221 ++++++++- tests/gen_pickles.py | 29 +- ...ickle.0 => multidict-c-extension.pickle.0} | 0 ...ickle.1 => multidict-c-extension.pickle.1} | Bin ...ickle.2 => multidict-c-extension.pickle.2} | Bin ...ickle.3 => multidict-c-extension.pickle.3} | Bin ...ickle.4 => multidict-c-extension.pickle.4} | Bin ...ickle.5 => multidict-c-extension.pickle.5} | Bin ...ickle.0 => multidict-pure-python.pickle.0} | 0 ...ickle.1 => multidict-pure-python.pickle.1} | Bin ...ickle.2 => multidict-pure-python.pickle.2} | Bin ...ickle.3 => multidict-pure-python.pickle.3} | Bin ...ickle.4 => multidict-pure-python.pickle.4} | Bin ...ickle.5 => multidict-pure-python.pickle.5} | Bin tests/test_abc.py | 50 +- tests/test_copy.py | 61 +-- tests/test_guard.py | 34 +- tests/test_istr.py | 1 - tests/test_multidict.py | 280 ++++++----- tests/test_mutable_multidict.py | 446 ++++++++++++------ tests/test_mypy.py | 2 + tests/test_pickle.py | 76 +-- tests/test_types.py | 100 ++-- tests/test_update.py | 110 ++--- tests/test_version.py | 409 ++++++++-------- 39 files changed, 1054 insertions(+), 775 deletions(-) rename tests/{cimultidict.pickle.0 => cimultidict-c-extension.pickle.0} (100%) rename tests/{cimultidict.pickle.1 => cimultidict-c-extension.pickle.1} (100%) rename tests/{cimultidict.pickle.2 => cimultidict-c-extension.pickle.2} (100%) rename tests/{cimultidict.pickle.3 => cimultidict-c-extension.pickle.3} (100%) rename tests/{cimultidict.pickle.4 => cimultidict-c-extension.pickle.4} (100%) rename tests/{cimultidict.pickle.5 => cimultidict-c-extension.pickle.5} (100%) rename tests/{pycimultidict.pickle.0 => cimultidict-pure-python.pickle.0} (100%) rename tests/{pycimultidict.pickle.1 => cimultidict-pure-python.pickle.1} (100%) rename tests/{pycimultidict.pickle.2 => cimultidict-pure-python.pickle.2} (100%) rename tests/{pycimultidict.pickle.3 => cimultidict-pure-python.pickle.3} (100%) rename tests/{pycimultidict.pickle.4 => cimultidict-pure-python.pickle.4} (100%) rename tests/{pycimultidict.pickle.5 => cimultidict-pure-python.pickle.5} (100%) rename tests/{multidict.pickle.0 => multidict-c-extension.pickle.0} (100%) rename tests/{multidict.pickle.1 => multidict-c-extension.pickle.1} (100%) rename tests/{multidict.pickle.2 => multidict-c-extension.pickle.2} (100%) rename tests/{multidict.pickle.3 => multidict-c-extension.pickle.3} (100%) rename tests/{multidict.pickle.4 => multidict-c-extension.pickle.4} (100%) rename tests/{multidict.pickle.5 => multidict-c-extension.pickle.5} (100%) rename tests/{pymultidict.pickle.0 => multidict-pure-python.pickle.0} (100%) rename tests/{pymultidict.pickle.1 => multidict-pure-python.pickle.1} (100%) rename tests/{pymultidict.pickle.2 => multidict-pure-python.pickle.2} (100%) rename tests/{pymultidict.pickle.3 => multidict-pure-python.pickle.3} (100%) rename tests/{pymultidict.pickle.4 => multidict-pure-python.pickle.4} (100%) rename tests/{pymultidict.pickle.5 => multidict-pure-python.pickle.5} (100%) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index eae3898a4..0099296a9 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -261,12 +261,11 @@ jobs: - name: Self-install run: python -Im pip install '${{ steps.wheel-file.outputs.path }}' - name: Run unittests - env: - MULTIDICT_NO_EXTENSIONS: ${{ matrix.no-extensions }} run: >- - python -m pytest tests -vv + python -Im pytest tests -vv --cov-report xml --junitxml=.test-results/pytest/test.xml + --${{ matrix.no-extensions == 'Y' && 'no-' || '' }}c-extensions - name: Produce markdown test summary from JUnit if: >- !cancelled() @@ -284,11 +283,10 @@ jobs: if: >- !cancelled() && failure() - env: - MULTIDICT_NO_EXTENSIONS: ${{ matrix.no-extensions }} run: >- # `exit 1` makes sure that the job remains red with flaky runs python -Im pytest --no-cov -vvvvv --lf -rA + --${{ matrix.no-extensions == 'Y' && 'no-' || '' }}c-extensions && exit 1 shell: bash - name: Prepare coverage artifact diff --git a/.mypy.ini b/.mypy.ini index 242e990da..410d17424 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -26,4 +26,4 @@ warn_unused_ignores = True warn_return_any = True [mypy-test_multidict] -disable_error_code = misc +disable_error_code = call-arg diff --git a/tests/cimultidict.pickle.0 b/tests/cimultidict-c-extension.pickle.0 similarity index 100% rename from tests/cimultidict.pickle.0 rename to tests/cimultidict-c-extension.pickle.0 diff --git a/tests/cimultidict.pickle.1 b/tests/cimultidict-c-extension.pickle.1 similarity index 100% rename from tests/cimultidict.pickle.1 rename to tests/cimultidict-c-extension.pickle.1 diff --git a/tests/cimultidict.pickle.2 b/tests/cimultidict-c-extension.pickle.2 similarity index 100% rename from tests/cimultidict.pickle.2 rename to tests/cimultidict-c-extension.pickle.2 diff --git a/tests/cimultidict.pickle.3 b/tests/cimultidict-c-extension.pickle.3 similarity index 100% rename from tests/cimultidict.pickle.3 rename to tests/cimultidict-c-extension.pickle.3 diff --git a/tests/cimultidict.pickle.4 b/tests/cimultidict-c-extension.pickle.4 similarity index 100% rename from tests/cimultidict.pickle.4 rename to tests/cimultidict-c-extension.pickle.4 diff --git a/tests/cimultidict.pickle.5 b/tests/cimultidict-c-extension.pickle.5 similarity index 100% rename from tests/cimultidict.pickle.5 rename to tests/cimultidict-c-extension.pickle.5 diff --git a/tests/pycimultidict.pickle.0 b/tests/cimultidict-pure-python.pickle.0 similarity index 100% rename from tests/pycimultidict.pickle.0 rename to tests/cimultidict-pure-python.pickle.0 diff --git a/tests/pycimultidict.pickle.1 b/tests/cimultidict-pure-python.pickle.1 similarity index 100% rename from tests/pycimultidict.pickle.1 rename to tests/cimultidict-pure-python.pickle.1 diff --git a/tests/pycimultidict.pickle.2 b/tests/cimultidict-pure-python.pickle.2 similarity index 100% rename from tests/pycimultidict.pickle.2 rename to tests/cimultidict-pure-python.pickle.2 diff --git a/tests/pycimultidict.pickle.3 b/tests/cimultidict-pure-python.pickle.3 similarity index 100% rename from tests/pycimultidict.pickle.3 rename to tests/cimultidict-pure-python.pickle.3 diff --git a/tests/pycimultidict.pickle.4 b/tests/cimultidict-pure-python.pickle.4 similarity index 100% rename from tests/pycimultidict.pickle.4 rename to tests/cimultidict-pure-python.pickle.4 diff --git a/tests/pycimultidict.pickle.5 b/tests/cimultidict-pure-python.pickle.5 similarity index 100% rename from tests/pycimultidict.pickle.5 rename to tests/cimultidict-pure-python.pickle.5 diff --git a/tests/conftest.py b/tests/conftest.py index bd3b4de02..3f6a67b28 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,25 +1,222 @@ +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._compat import USE_EXTENSIONS +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" -OPTIONAL_CYTHON = ( - () - if USE_EXTENSIONS - else pytest.mark.skip(reason="No extensions available") + +@pytest.fixture( + scope="session", + params=( + MultidictImplementation(is_pure_python=False), + pytest.param( + MultidictImplementation(is_pure_python=True), + marks=C_EXT_MARK, + ), + ), + ids=str, ) +def multidict_implementation(request) -> 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( # type: ignore[call-overload] + +@pytest.fixture( scope="session", - params=[ - pytest.param("multidict._multidict", marks=OPTIONAL_CYTHON), # type: ignore - "multidict._multidict_py", - ], + params=("MultiDict", "CIMultiDict"), + ids=("case-sensitive", "case-insensitive"), ) -def _multidict(request): - return pytest.importorskip(request.param) +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_mark = item.get_closest_marker(C_EXT_MARK.name) + + target_items_list = deselected_tests if c_ext_mark 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): diff --git a/tests/gen_pickles.py b/tests/gen_pickles.py index 028a01f8c..0f4ffac0d 100644 --- a/tests/gen_pickles.py +++ b/tests/gen_pickles.py @@ -1,31 +1,24 @@ import pickle -from multidict._compat import USE_EXTENSIONS -from multidict._multidict_py import CIMultiDict as PyCIMultiDict # noqa -from multidict._multidict_py import MultiDict as PyMultiDict # noqa +import multidict -try: - from multidict._multidict import ( # type: ignore # noqa - CIMultiDict, - MultiDict, - ) -except ImportError: - pass - -def write(name, proto): - cls = globals()[name] +def write(tag, cls, proto): d = cls([("a", 1), ("a", 2)]) - with open("{}.pickle.{}".format(name.lower(), proto), "wb") as f: + file_basename = f"{cls.__name__.lower()}-{tag}" + with open(f"{file_basename}.pickle.{proto}", "wb") as f: pickle.dump(d, f, proto) def generate(): - if not USE_EXTENSIONS: - raise RuntimeError("C Extension is required") + _impl_map = { + "c-extension": multidict._multidict, + "pure-python": multidict._multidict_py, + } for proto in range(pickle.HIGHEST_PROTOCOL + 1): - for name in ("MultiDict", "CIMultiDict", "PyMultiDict", "PyCIMultiDict"): - write(name, proto) + for tag, impl in _impl_map.items(): + for cls in impl.CIMultiDict, impl.MultiDict: + write(tag, cls, proto) if __name__ == "__main__": diff --git a/tests/multidict.pickle.0 b/tests/multidict-c-extension.pickle.0 similarity index 100% rename from tests/multidict.pickle.0 rename to tests/multidict-c-extension.pickle.0 diff --git a/tests/multidict.pickle.1 b/tests/multidict-c-extension.pickle.1 similarity index 100% rename from tests/multidict.pickle.1 rename to tests/multidict-c-extension.pickle.1 diff --git a/tests/multidict.pickle.2 b/tests/multidict-c-extension.pickle.2 similarity index 100% rename from tests/multidict.pickle.2 rename to tests/multidict-c-extension.pickle.2 diff --git a/tests/multidict.pickle.3 b/tests/multidict-c-extension.pickle.3 similarity index 100% rename from tests/multidict.pickle.3 rename to tests/multidict-c-extension.pickle.3 diff --git a/tests/multidict.pickle.4 b/tests/multidict-c-extension.pickle.4 similarity index 100% rename from tests/multidict.pickle.4 rename to tests/multidict-c-extension.pickle.4 diff --git a/tests/multidict.pickle.5 b/tests/multidict-c-extension.pickle.5 similarity index 100% rename from tests/multidict.pickle.5 rename to tests/multidict-c-extension.pickle.5 diff --git a/tests/pymultidict.pickle.0 b/tests/multidict-pure-python.pickle.0 similarity index 100% rename from tests/pymultidict.pickle.0 rename to tests/multidict-pure-python.pickle.0 diff --git a/tests/pymultidict.pickle.1 b/tests/multidict-pure-python.pickle.1 similarity index 100% rename from tests/pymultidict.pickle.1 rename to tests/multidict-pure-python.pickle.1 diff --git a/tests/pymultidict.pickle.2 b/tests/multidict-pure-python.pickle.2 similarity index 100% rename from tests/pymultidict.pickle.2 rename to tests/multidict-pure-python.pickle.2 diff --git a/tests/pymultidict.pickle.3 b/tests/multidict-pure-python.pickle.3 similarity index 100% rename from tests/pymultidict.pickle.3 rename to tests/multidict-pure-python.pickle.3 diff --git a/tests/pymultidict.pickle.4 b/tests/multidict-pure-python.pickle.4 similarity index 100% rename from tests/pymultidict.pickle.4 rename to tests/multidict-pure-python.pickle.4 diff --git a/tests/pymultidict.pickle.5 b/tests/multidict-pure-python.pickle.5 similarity index 100% rename from tests/pymultidict.pickle.5 rename to tests/multidict-pure-python.pickle.5 diff --git a/tests/test_abc.py b/tests/test_abc.py index 4636b3bc1..e18ad83f8 100644 --- a/tests/test_abc.py +++ b/tests/test_abc.py @@ -3,43 +3,6 @@ import pytest from multidict import MultiMapping, MutableMultiMapping -from multidict._compat import USE_EXTENSIONS -from multidict._multidict_py import CIMultiDict as PyCIMultiDict -from multidict._multidict_py import CIMultiDictProxy as PyCIMultiDictProxy -from multidict._multidict_py import MultiDict as PyMultiDict # noqa: E402 -from multidict._multidict_py import MultiDictProxy as PyMultiDictProxy - -if USE_EXTENSIONS: - from multidict._multidict import ( # type: ignore - CIMultiDict, - CIMultiDictProxy, - MultiDict, - MultiDictProxy, - ) - - -@pytest.fixture( - params=([MultiDict, CIMultiDict] if USE_EXTENSIONS else []) - + [PyMultiDict, PyCIMultiDict], - ids=(["MultiDict", "CIMultiDict"] if USE_EXTENSIONS else []) - + ["PyMultiDict", "PyCIMultiDict"], -) -def cls(request): - return request.param - - -@pytest.fixture( - params=( - [(MultiDictProxy, MultiDict), (CIMultiDictProxy, CIMultiDict)] - if USE_EXTENSIONS - else [] - ) - + [(PyMultiDictProxy, PyMultiDict), (PyCIMultiDictProxy, PyCIMultiDict)], - ids=(["MultiDictProxy", "CIMultiDictProxy"] if USE_EXTENSIONS else []) - + ["PyMultiDictProxy", "PyCIMultiDictProxy"], -) -def proxy_classes(request): - return request.param def test_abc_inheritance(): @@ -116,15 +79,14 @@ def test_abc_popall(): B().popall("key") -def test_multidict_inheritance(cls): - assert issubclass(cls, MultiMapping) - assert issubclass(cls, MutableMultiMapping) +def test_multidict_inheritance(any_multidict_class): + assert issubclass(any_multidict_class, MultiMapping) + assert issubclass(any_multidict_class, MutableMultiMapping) -def test_proxy_inheritance(proxy_classes): - proxy, _ = proxy_classes - assert issubclass(proxy, MultiMapping) - assert not issubclass(proxy, MutableMultiMapping) +def test_proxy_inheritance(any_multidict_proxy_class): + assert issubclass(any_multidict_proxy_class, MultiMapping) + assert not issubclass(any_multidict_proxy_class, MutableMultiMapping) def test_generic_type_in_runtime(): diff --git a/tests/test_copy.py b/tests/test_copy.py index 564cdde59..cd926cdc1 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -1,48 +1,8 @@ import copy -import pytest -from multidict._compat import USE_EXTENSIONS -from multidict._multidict_py import CIMultiDict as PyCIMultiDict -from multidict._multidict_py import CIMultiDictProxy as PyCIMultiDictProxy -from multidict._multidict_py import MultiDict as PyMultiDict # noqa: E402 -from multidict._multidict_py import MultiDictProxy as PyMultiDictProxy - -if USE_EXTENSIONS: - from multidict._multidict import ( # type: ignore - CIMultiDict, - CIMultiDictProxy, - MultiDict, - MultiDictProxy, - ) - - -@pytest.fixture( - params=([MultiDict, CIMultiDict] if USE_EXTENSIONS else []) - + [PyMultiDict, PyCIMultiDict], - ids=(["MultiDict", "CIMultiDict"] if USE_EXTENSIONS else []) - + ["PyMultiDict", "PyCIMultiDict"], -) -def cls(request): - return request.param - - -@pytest.fixture( - params=( - [(MultiDictProxy, MultiDict), (CIMultiDictProxy, CIMultiDict)] - if USE_EXTENSIONS - else [] - ) - + [(PyMultiDictProxy, PyMultiDict), (PyCIMultiDictProxy, PyCIMultiDict)], - ids=(["MultiDictProxy", "CIMultiDictProxy"] if USE_EXTENSIONS else []) - + ["PyMultiDictProxy", "PyCIMultiDictProxy"], -) -def proxy_classes(request): - return request.param - - -def test_copy(cls): - d = cls() +def test_copy(any_multidict_class): + d = any_multidict_class() d["foo"] = 6 d2 = d.copy() d2["foo"] = 7 @@ -50,11 +10,10 @@ def test_copy(cls): assert d2["foo"] == 7 -def test_copy_proxy(proxy_classes): - proxy_cls, dict_cls = proxy_classes - d = dict_cls() +def test_copy_proxy(any_multidict_class, any_multidict_proxy_class): + d = any_multidict_class() d["foo"] = 6 - p = proxy_cls(d) + p = any_multidict_proxy_class(d) d2 = p.copy() d2["foo"] = 7 assert d["foo"] == 6 @@ -62,8 +21,8 @@ def test_copy_proxy(proxy_classes): assert d2["foo"] == 7 -def test_copy_std_copy(cls): - d = cls() +def test_copy_std_copy(any_multidict_class): + d = any_multidict_class() d["foo"] = 6 d2 = copy.copy(d) d2["foo"] = 7 @@ -71,9 +30,9 @@ def test_copy_std_copy(cls): assert d2["foo"] == 7 -def test_ci_multidict_clone(cls): - d = cls(foo=6) - d2 = cls(d) +def test_ci_multidict_clone(any_multidict_class): + d = any_multidict_class(foo=6) + d2 = any_multidict_class(d) d2["foo"] = 7 assert d["foo"] == 6 assert d2["foo"] == 7 diff --git a/tests/test_guard.py b/tests/test_guard.py index 823cc1afb..225da67c8 100644 --- a/tests/test_guard.py +++ b/tests/test_guard.py @@ -1,38 +1,34 @@ -import pytest - -from multidict._compat import USE_EXTENSIONS -from multidict._multidict_py import MultiDict as PyMultiDict # noqa: E402 - -if USE_EXTENSIONS: - from multidict._multidict import MultiDict # type: ignore +from typing import Type +import pytest -@pytest.fixture( - params=([MultiDict] if USE_EXTENSIONS else []) + [PyMultiDict], - ids=(["MultiDict"] if USE_EXTENSIONS else []) + ["PyMultiDict"], -) -def cls(request): - return request.param +from multidict import MultiMapping -def test_guard_items(cls): - md = cls({"a": "b"}) +def test_guard_items( + case_sensitive_multidict_class: Type[MultiMapping[str]], +) -> None: + md = case_sensitive_multidict_class({"a": "b"}) it = iter(md.items()) md["a"] = "c" with pytest.raises(RuntimeError): next(it) -def test_guard_keys(cls): - md = cls({"a": "b"}) +def test_guard_keys( + case_sensitive_multidict_class: Type[MultiMapping[str]], +) -> None: + md = case_sensitive_multidict_class({"a": "b"}) it = iter(md.keys()) md["a"] = "c" with pytest.raises(RuntimeError): next(it) -def test_guard_values(cls): - md = cls({"a": "b"}) +def test_guard_values( + case_sensitive_multidict_class: Type[MultiMapping[str]], +) -> None: + md = case_sensitive_multidict_class({"a": "b"}) it = iter(md.values()) md["a"] = "c" with pytest.raises(RuntimeError): diff --git a/tests/test_istr.py b/tests/test_istr.py index caae397f2..09ec4ff4c 100644 --- a/tests/test_istr.py +++ b/tests/test_istr.py @@ -17,7 +17,6 @@ class IStrMixin: - cls = Type[istr] def test_ctor(self): diff --git a/tests/test_multidict.py b/tests/test_multidict.py index 706fc93e7..55ab977c8 100644 --- a/tests/test_multidict.py +++ b/tests/test_multidict.py @@ -1,12 +1,13 @@ +from __future__ import annotations + import gc import operator import sys import weakref from collections import deque from collections.abc import Mapping -from functools import reduce +from types import ModuleType from typing import ( - Any, Callable, Dict, Iterable, @@ -16,106 +17,112 @@ Set, Tuple, Type, - TypeVar, Union, + cast, ) import pytest import multidict -from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy - -_MultiDictClasses = Union[Type[MultiDict[str]], Type[CIMultiDict[str]]] +from multidict import CIMultiDict, MultiDict, MultiMapping, MutableMultiMapping def chained_callable( - module: object, callables: Union[str, Iterable[str]] -) -> Callable[..., Any]: + module: ModuleType, + callables: Iterable[str], +) -> Callable[..., MultiMapping[int | str] | MutableMultiMapping[int | str]]: """ - Returns callable that will get and call all given objects in module in - exact order. If `names` is a single object's name function will return - object itself. - - Will treat `names` of type `str` as a list of single element. + Return callable that will get and call all given objects in module in + exact order. """ - callables = (callables,) if isinstance(callables, str) else callables - _callable, *rest = (getattr(module, name) for name in callables) - def chained_call(*args: object, **kwargs: object) -> Any: - return reduce(lambda res, c: c(res), rest, _callable(*args, **kwargs)) + def chained_call( + *args: object, + **kwargs: object, + ) -> MultiMapping[int | str] | MutableMultiMapping[int | str]: + nonlocal callables - return chained_call if len(rest) > 0 else _callable # type: ignore[no-any-return] + callable_chain = (getattr(module, name) for name in callables) + first_callable = next(callable_chain) + value = first_callable(*args, **kwargs) + for element in callable_chain: + value = element(value) -@pytest.fixture(scope="function") -def cls(request: Any, _multidict: Any) -> Any: - return chained_callable(_multidict, request.param) - + return cast( + Union[ + MultiMapping[Union[int, str]], + MutableMultiMapping[Union[int, str]], + ], + value, + ) -@pytest.fixture(scope="function") -def classes(request: Any, _multidict: Any) -> Any: - return tuple(chained_callable(_multidict, n) for n in request.param) + return chained_call -@pytest.mark.parametrize("cls", ["MultiDict", "CIMultiDict"], indirect=True) -def test_exposed_names( - cls: Union[Type[MultiDict[object]], Type[CIMultiDict[object]]] -) -> None: - name = cls.__name__ +@pytest.fixture(scope="function") +def cls( # type: ignore[misc] + request: pytest.FixtureRequest, + multidict_module: ModuleType, +) -> Callable[..., MultiMapping[int | str] | MutableMultiMapping[int | str]]: + """Make a callable from multidict module, requested by name.""" + return chained_callable(multidict_module, request.param) - while name.startswith("_"): - name = name[1:] - assert name in multidict.__all__ # type: ignore[attr-defined] +def test_exposed_names(any_multidict_class_name: str) -> None: + assert any_multidict_class_name in multidict.__all__ # type: ignore[attr-defined] @pytest.mark.parametrize( - "cls, key_cls", - [("MultiDict", str), (("MultiDict", "MultiDictProxy"), str)], + ("cls", "key_cls"), + [ + (("MultiDict",), str), + ( + ("MultiDict", "MultiDictProxy"), + str, + ), + ], indirect=["cls"], ) def test__iter__types( - cls: Type[MultiDict[Union[str, int]]], key_cls: Type[object] + cls: Type[MultiDict[Union[str, int]]], + key_cls: Type[object], ) -> None: d = cls([("key", "one"), ("key2", "two"), ("key", 3)]) for i in d: assert type(i) is key_cls, (type(i), key_cls) -_ClsPair = TypeVar( - "_ClsPair", - Tuple[Type[MultiDict[str]], Type[MultiDictProxy[str]]], - Tuple[Type[CIMultiDict[str]], Type[CIMultiDictProxy[str]]], -) - - -@pytest.mark.parametrize( - "classes", - [("MultiDict", "MultiDictProxy"), ("CIMultiDict", "CIMultiDictProxy")], - indirect=True, -) -def test_proxy_copy(classes: _ClsPair) -> None: - dict_cls, proxy_cls = classes - d1 = dict_cls(key="value", a="b") - p1 = proxy_cls(d1) +def test_proxy_copy( + any_multidict_class: Type[MutableMultiMapping[str]], + any_multidict_proxy_class: Type[MultiMapping[str]], +) -> None: + d1 = any_multidict_class(key="value", a="b") + p1 = any_multidict_proxy_class(d1) - d2 = p1.copy() + d2 = p1.copy() # type: ignore[attr-defined] assert d1 == d2 assert d1 is not d2 -@pytest.mark.parametrize( - "cls", - ["MultiDict", "CIMultiDict", "MultiDictProxy", "CIMultiDictProxy"], - indirect=True, -) -def test_subclassing(cls: Any) -> None: - class MyClass(cls): # type: ignore[valid-type,misc] +def test_multidict_subclassing( + any_multidict_class: Type[MutableMultiMapping[str]], +) -> None: + class DummyMultidict(any_multidict_class): # type: ignore[valid-type,misc] + pass + + +def test_multidict_proxy_subclassing( + any_multidict_proxy_class: Type[MultiMapping[str]], +) -> None: + class DummyMultidictProxy( + any_multidict_proxy_class, # type: ignore[valid-type,misc] + ): pass class BaseMultiDictTest: - def test_instantiate__empty(self, cls: _MultiDictClasses) -> None: + def test_instantiate__empty(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls() empty: Mapping[str, str] = {} assert d == empty @@ -126,12 +133,12 @@ def test_instantiate__empty(self, cls: _MultiDictClasses) -> None: assert cls() != list() # type: ignore[comparison-overlap] with pytest.raises(TypeError, match=r"(2 given)"): - cls(("key1", "value1"), ("key2", "value2")) # type: ignore[arg-type,call-arg] # noqa: E501 + cls(("key1", "value1"), ("key2", "value2")) # type: ignore[call-arg] # noqa: E501 @pytest.mark.parametrize("arg0", [[("key", "value1")], {"key": "value1"}]) def test_instantiate__from_arg0( self, - cls: _MultiDictClasses, + cls: Type[MutableMultiMapping[str]], arg0: Union[List[Tuple[str, str]], Dict[str, str]], ) -> None: d = cls(arg0) @@ -142,7 +149,10 @@ def test_instantiate__from_arg0( assert list(d.values()) == ["value1"] assert list(d.items()) == [("key", "value1")] - def test_instantiate__with_kwargs(self, cls: _MultiDictClasses) -> None: + def test_instantiate__with_kwargs( + self, + cls: Type[MutableMultiMapping[str]], + ) -> None: d = cls([("key", "value1")], key2="value2") assert d == {"key": "value1", "key2": "value2"} @@ -162,13 +172,17 @@ def test_instantiate__from_generator( assert sorted(d.values()) == [0, 1] assert sorted(d.items()) == [("0", 0), ("1", 1)] - def test_instantiate__from_list_of_lists(self, cls: _MultiDictClasses) -> None: + def test_instantiate__from_list_of_lists( + self, + cls: Type[MutableMultiMapping[str]], + ) -> None: # Should work at runtime, but won't type check. - d = cls([["key", "value1"]]) # type: ignore[list-item] + d = cls([["key", "value1"]]) # type: ignore[call-arg] assert d == {"key": "value1"} def test_instantiate__from_list_of_custom_pairs( - self, cls: _MultiDictClasses + self, + cls: Type[MutableMultiMapping[str]], ) -> None: class Pair: def __len__(self) -> int: @@ -183,10 +197,10 @@ def __getitem__(self, pos: int) -> str: raise IndexError # Works at runtime, but won't type check. - d = cls([Pair()]) # type: ignore[list-item] + d = cls([Pair()]) assert d == {"key": "value1"} - def test_getone(self, cls: _MultiDictClasses) -> None: + def test_getone(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")], key="value2") assert d.getone("key") == "value1" @@ -202,14 +216,20 @@ def test_getone(self, cls: _MultiDictClasses) -> None: def test__iter__( self, - cls: Union[Type[MultiDict[Union[str, int]]], Type[CIMultiDict[Union[str, int]]]] + cls: Union[ + Type[MultiDict[Union[str, int]]], + Type[CIMultiDict[Union[str, int]]], + ], ) -> None: d = cls([("key", "one"), ("key2", "two"), ("key", 3)]) assert list(d) == ["key", "key2", "key"] def test_keys__contains( self, - cls: Union[Type[MultiDict[Union[str, int]]], Type[CIMultiDict[Union[str, int]]]] + cls: Union[ + Type[MultiDict[Union[str, int]]], + Type[CIMultiDict[Union[str, int]]], + ], ) -> None: d = cls([("key", "one"), ("key2", "two"), ("key", 3)]) @@ -222,7 +242,10 @@ def test_keys__contains( def test_values__contains( self, - cls: Union[Type[MultiDict[Union[str, int]]], Type[CIMultiDict[Union[str, int]]]] + cls: Union[ + Type[MultiDict[Union[str, int]]], + Type[CIMultiDict[Union[str, int]]], + ], ) -> None: d = cls([("key", "one"), ("key", "two"), ("key", 3)]) @@ -236,7 +259,10 @@ def test_values__contains( def test_items__contains( self, - cls: Union[Type[MultiDict[Union[str, int]]], Type[CIMultiDict[Union[str, int]]]] + cls: Union[ + Type[MultiDict[Union[str, int]]], + Type[CIMultiDict[Union[str, int]]], + ], ) -> None: d = cls([("key", "one"), ("key", "two"), ("key", 3)]) @@ -248,58 +274,67 @@ def test_items__contains( assert ("foo", "bar") not in d.items() - def test_cannot_create_from_unaccepted(self, cls: _MultiDictClasses) -> None: + def test_cannot_create_from_unaccepted( + self, + cls: Type[MutableMultiMapping[str]], + ) -> None: with pytest.raises(TypeError): - cls([(1, 2, 3)]) # type: ignore[list-item] + cls([(1, 2, 3)]) # type: ignore[call-arg] - def test_keys_is_set_less(self, cls: _MultiDictClasses) -> None: + def test_keys_is_set_less(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert d.keys() < {"key", "key2"} - def test_keys_is_set_less_equal(self, cls: _MultiDictClasses) -> None: + def test_keys_is_set_less_equal(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert d.keys() <= {"key"} - def test_keys_is_set_equal(self, cls: _MultiDictClasses) -> None: + def test_keys_is_set_equal(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert d.keys() == {"key"} - def test_keys_is_set_greater(self, cls: _MultiDictClasses) -> None: + def test_keys_is_set_greater(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert {"key", "key2"} > d.keys() - def test_keys_is_set_greater_equal(self, cls: _MultiDictClasses) -> None: + def test_keys_is_set_greater_equal( + self, + cls: Type[MutableMultiMapping[str]], + ) -> None: d = cls([("key", "value1")]) assert {"key"} >= d.keys() - def test_keys_is_set_not_equal(self, cls: _MultiDictClasses) -> None: + def test_keys_is_set_not_equal(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert d.keys() != {"key2"} - def test_eq(self, cls: _MultiDictClasses) -> None: + def test_eq(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert {"key": "value1"} == d - def test_eq2(self, cls: _MultiDictClasses) -> None: + def test_eq2(self, cls: Type[MutableMultiMapping[str]]) -> None: d1 = cls([("key", "value1")]) d2 = cls([("key2", "value1")]) assert d1 != d2 - def test_eq3(self, cls: _MultiDictClasses) -> None: + def test_eq3(self, cls: Type[MutableMultiMapping[str]]) -> None: d1 = cls([("key", "value1")]) d2 = cls() assert d1 != d2 - def test_eq_other_mapping_contains_more_keys(self, cls: _MultiDictClasses) -> None: + def test_eq_other_mapping_contains_more_keys( + self, + cls: Type[MutableMultiMapping[str]], + ) -> None: d1 = cls(foo="bar") d2 = dict(foo="bar", bar="baz") @@ -325,7 +360,7 @@ def __len__(self) -> int: # type: ignore[return] def test_eq_bad_mapping_getitem( self, - cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]] + cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]], ) -> None: class BadMapping(Mapping[str, int]): def __getitem__(self, key: str) -> int: # type: ignore[return] @@ -342,60 +377,60 @@ def __len__(self) -> int: with pytest.raises(ZeroDivisionError): d1 == d2 - def test_ne(self, cls: _MultiDictClasses) -> None: + def test_ne(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert d != {"key": "another_value"} - def test_and(self, cls: _MultiDictClasses) -> None: + def test_and(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert {"key"} == d.keys() & {"key", "key2"} - def test_and2(self, cls: _MultiDictClasses) -> None: + def test_and2(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert {"key"} == {"key", "key2"} & d.keys() - def test_or(self, cls: _MultiDictClasses) -> None: + def test_or(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert {"key", "key2"} == d.keys() | {"key2"} - def test_or2(self, cls: _MultiDictClasses) -> None: + def test_or2(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1")]) assert {"key", "key2"} == {"key2"} | d.keys() - def test_sub(self, cls: _MultiDictClasses) -> None: + def test_sub(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1"), ("key2", "value2")]) assert {"key"} == d.keys() - {"key2"} - def test_sub2(self, cls: _MultiDictClasses) -> None: + def test_sub2(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1"), ("key2", "value2")]) assert {"key3"} == {"key", "key2", "key3"} - d.keys() - def test_xor(self, cls: _MultiDictClasses) -> None: + def test_xor(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1"), ("key2", "value2")]) assert {"key", "key3"} == d.keys() ^ {"key2", "key3"} - def test_xor2(self, cls: _MultiDictClasses) -> None: + def test_xor2(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls([("key", "value1"), ("key2", "value2")]) assert {"key", "key3"} == {"key2", "key3"} ^ d.keys() @pytest.mark.parametrize("_set, expected", [({"key2"}, True), ({"key"}, False)]) def test_isdisjoint( - self, cls: _MultiDictClasses, _set: Set[str], expected: bool + self, cls: Type[MutableMultiMapping[str]], _set: Set[str], expected: bool ) -> None: d = cls([("key", "value1")]) assert d.keys().isdisjoint(_set) == expected - def test_repr_issue_410(self, cls: _MultiDictClasses) -> None: + def test_repr_issue_410(self, cls: Type[MutableMultiMapping[str]]) -> None: d = cls() try: @@ -412,7 +447,7 @@ def test_repr_issue_410(self, cls: _MultiDictClasses) -> None: @pytest.mark.parametrize("other", [{"other"}]) def test_op_issue_410( self, - cls: _MultiDictClasses, + cls: Type[MutableMultiMapping[str]], op: Callable[[object, object], object], other: Set[str], ) -> None: @@ -426,7 +461,7 @@ def test_op_issue_410( assert sys.exc_info()[1] == e - def test_weakref(self, cls: _MultiDictClasses) -> None: + def test_weakref(self, cls: Type[MutableMultiMapping[str]]) -> None: called = False def cb(wr: object) -> None: @@ -442,7 +477,7 @@ def cb(wr: object) -> None: def test_iter_length_hint_keys( self, - cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]] + cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]], ) -> None: md = cls(a=1, b=2) it = iter(md.keys()) @@ -450,7 +485,7 @@ def test_iter_length_hint_keys( def test_iter_length_hint_items( self, - cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]] + cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]], ) -> None: md = cls(a=1, b=2) it = iter(md.items()) @@ -458,7 +493,7 @@ def test_iter_length_hint_items( def test_iter_length_hint_values( self, - cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]] + cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]], ) -> None: md = cls(a=1, b=2) it = iter(md.values()) @@ -466,7 +501,7 @@ def test_iter_length_hint_values( def test_ctor_list_arg_and_kwds( self, - cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]] + cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]], ) -> None: arg = [("a", 1)] obj = cls(arg, b=2) @@ -475,7 +510,7 @@ def test_ctor_list_arg_and_kwds( def test_ctor_tuple_arg_and_kwds( self, - cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]] + cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]], ) -> None: arg = (("a", 1),) obj = cls(arg, b=2) @@ -484,7 +519,7 @@ def test_ctor_tuple_arg_and_kwds( def test_ctor_deque_arg_and_kwds( self, - cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]] + cls: Union[Type[MultiDict[int]], Type[CIMultiDict[int]]], ) -> None: arg = deque([("a", 1)]) obj = cls(arg, b=2) @@ -493,9 +528,19 @@ def test_ctor_deque_arg_and_kwds( class TestMultiDict(BaseMultiDictTest): - @pytest.fixture(params=["MultiDict", ("MultiDict", "MultiDictProxy")]) - def cls(self, request: Any, _multidict: Any) -> Any: - return chained_callable(_multidict, request.param) + @pytest.fixture( + params=[ + ("MultiDict",), + ("MultiDict", "MultiDictProxy"), + ], + ) + def cls( # type: ignore[misc] + self, + request: pytest.FixtureRequest, + multidict_module: ModuleType, + ) -> Callable[..., MultiMapping[int | str] | MutableMultiMapping[int | str]]: + """Make a case-sensitive multidict class/proxy constructor.""" + return chained_callable(multidict_module, request.param) def test__repr__(self, cls: Type[MultiDict[str]]) -> None: d = cls() @@ -522,7 +567,8 @@ def test_getall(self, cls: Type[MultiDict[str]]) -> None: assert d.getall("some_key", default) is default def test_preserve_stable_ordering( - self, cls: Type[MultiDict[Union[str, int]]] + self, + cls: Type[MultiDict[Union[str, int]]], ) -> None: d = cls([("a", 1), ("b", "2"), ("a", 3)]) s = "&".join("{}={}".format(k, v) for k, v in d.items()) @@ -548,9 +594,19 @@ def test_values__repr__(self, cls: Type[MultiDict[str]]) -> None: class TestCIMultiDict(BaseMultiDictTest): - @pytest.fixture(params=["CIMultiDict", ("CIMultiDict", "CIMultiDictProxy")]) - def cls(self, request: Any, _multidict: Any) -> Any: - return chained_callable(_multidict, request.param) + @pytest.fixture( + params=[ + ("CIMultiDict",), + ("CIMultiDict", "CIMultiDictProxy"), + ], + ) + def cls( # type: ignore[misc] + self, + request: pytest.FixtureRequest, + multidict_module: ModuleType, + ) -> Callable[..., MultiMapping[int | str] | MutableMultiMapping[int | str]]: + """Make a case-insensitive multidict class/proxy constructor.""" + return chained_callable(multidict_module, request.param) def test_basics(self, cls: Type[CIMultiDict[str]]) -> None: d = cls([("KEY", "value1")], KEY="value2") diff --git a/tests/test_mutable_multidict.py b/tests/test_mutable_multidict.py index 3d4d16ac0..3cacec25a 100644 --- a/tests/test_mutable_multidict.py +++ b/tests/test_mutable_multidict.py @@ -1,40 +1,43 @@ import string import sys +from typing import Type import pytest +from multidict import MultiMapping, MutableMultiMapping -class TestMutableMultiDict: - @pytest.fixture - def cls(self, _multidict): - return _multidict.MultiDict - - @pytest.fixture - def proxy_cls(self, _multidict): - return _multidict.MultiDictProxy - - @pytest.fixture - def istr(self, _multidict): - return _multidict.istr - def test_copy(self, cls): - d1 = cls(key="value", a="b") +class TestMutableMultiDict: + def test_copy( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d1 = case_sensitive_multidict_class(key="value", a="b") d2 = d1.copy() assert d1 == d2 assert d1 is not d2 - def test__repr__(self, cls): - d = cls() - assert str(d) == "<%s()>" % cls.__name__ + def test__repr__( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() + assert str(d) == "<%s()>" % case_sensitive_multidict_class.__name__ - d = cls([("key", "one"), ("key", "two")]) + d = case_sensitive_multidict_class([("key", "one"), ("key", "two")]) - expected = "<%s('key': 'one', 'key': 'two')>" % cls.__name__ + expected = ( + f"<{case_sensitive_multidict_class.__name__}" + "('key': 'one', 'key': 'two')>" + ) assert str(d) == expected - def test_getall(self, cls): - d = cls([("key", "value1")], key="value2") + def test_getall( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class([("key", "value1")], key="value2") assert len(d) == 2 assert d.getall("key") == ["value1", "value2"] @@ -45,8 +48,11 @@ def test_getall(self, cls): default = object() assert d.getall("some_key", default) is default - def test_add(self, cls): - d = cls() + def test_add( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() assert d == {} d["key"] = "one" @@ -65,8 +71,11 @@ def test_add(self, cls): assert 3 == len(d) assert d.getall("foo") == ["bar"] - def test_extend(self, cls): - d = cls() + def test_extend( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() assert d == {} d.extend([("key", "one"), ("key", "two")], key=3, foo="bar") @@ -79,7 +88,7 @@ def test_extend(self, cls): assert ("key", 3) in itms assert ("foo", "bar") in itms - other = cls(bar="baz") + other = case_sensitive_multidict_class(bar="baz") assert other == {"bar": "baz"} d.extend(other) @@ -94,24 +103,34 @@ def test_extend(self, cls): with pytest.raises(TypeError): d.extend("foo", "bar") - def test_extend_from_proxy(self, cls, proxy_cls): - d = cls([("a", "a"), ("b", "b")]) - proxy = proxy_cls(d) + def test_extend_from_proxy( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + case_sensitive_multidict_proxy_class: Type[MultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class([("a", "a"), ("b", "b")]) + proxy = case_sensitive_multidict_proxy_class(d) - d2 = cls() + d2 = case_sensitive_multidict_class() d2.extend(proxy) assert [("a", "a"), ("b", "b")] == list(d2.items()) - def test_clear(self, cls): - d = cls([("key", "one")], key="two", foo="bar") + def test_clear( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class([("key", "one")], key="two", foo="bar") d.clear() assert d == {} assert list(d.items()) == [] - def test_del(self, cls): - d = cls([("key", "one"), ("key", "two")], foo="bar") + def test_del( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class([("key", "one"), ("key", "two")], foo="bar") assert list(d.keys()) == ["key", "key", "foo"] del d["key"] @@ -121,37 +140,52 @@ def test_del(self, cls): with pytest.raises(KeyError, match="key"): del d["key"] - def test_set_default(self, cls): - d = cls([("key", "one"), ("key", "two")], foo="bar") + def test_set_default( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class([("key", "one"), ("key", "two")], foo="bar") assert "one" == d.setdefault("key", "three") assert "three" == d.setdefault("otherkey", "three") assert "otherkey" in d assert "three" == d["otherkey"] - def test_popitem(self, cls): - d = cls() + def test_popitem( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() d.add("key", "val1") d.add("key", "val2") assert ("key", "val1") == d.popitem() assert [("key", "val2")] == list(d.items()) - def test_popitem_empty_multidict(self, cls): - d = cls() + def test_popitem_empty_multidict( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() with pytest.raises(KeyError): d.popitem() - def test_pop(self, cls): - d = cls() + def test_pop( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() d.add("key", "val1") d.add("key", "val2") assert "val1" == d.pop("key") assert {"key": "val2"} == d - def test_pop2(self, cls): - d = cls() + def test_pop2( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() d.add("key", "val1") d.add("key2", "val2") d.add("key", "val3") @@ -159,22 +193,31 @@ def test_pop2(self, cls): assert "val1" == d.pop("key") assert [("key2", "val2"), ("key", "val3")] == list(d.items()) - def test_pop_default(self, cls): - d = cls(other="val") + def test_pop_default( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class(other="val") assert "default" == d.pop("key", "default") assert "other" in d - def test_pop_raises(self, cls): - d = cls(other="val") + def test_pop_raises( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class(other="val") with pytest.raises(KeyError, match="key"): d.pop("key") assert "other" in d - def test_replacement_order(self, cls): - d = cls() + def test_replacement_order( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() d.add("key1", "val1") d.add("key2", "val2") d.add("key1", "val3") @@ -186,39 +229,59 @@ def test_replacement_order(self, cls): assert expected == list(d.items()) - def test_nonstr_key(self, cls): - d = cls() + def test_nonstr_key( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() with pytest.raises(TypeError): d[1] = "val" - def test_istr_key(self, cls, istr): - d = cls() - d[istr("1")] = "val" - assert type(list(d.keys())[0]) is istr - - def test_str_derived_key(self, cls): + def test_istr_key( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + case_insensitive_str_class: Type[str], + ) -> None: + d = case_sensitive_multidict_class() + d[case_insensitive_str_class("1")] = "val" + assert type(list(d.keys())[0]) is case_insensitive_str_class + + def test_str_derived_key( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: class A(str): pass - d = cls() + d = case_sensitive_multidict_class() d[A("1")] = "val" assert type(list(d.keys())[0]) is A - def test_istr_key_add(self, cls, istr): - d = cls() - d.add(istr("1"), "val") - assert type(list(d.keys())[0]) is istr - - def test_str_derived_key_add(self, cls): + def test_istr_key_add( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + case_insensitive_str_class: Type[str], + ) -> None: + d = case_sensitive_multidict_class() + d.add(case_insensitive_str_class("1"), "val") + assert type(list(d.keys())[0]) is case_insensitive_str_class + + def test_str_derived_key_add( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: class A(str): pass - d = cls() + d = case_sensitive_multidict_class() d.add(A("1"), "val") assert type(list(d.keys())[0]) is A - def test_popall(self, cls): - d = cls() + def test_popall( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() d.add("key1", "val1") d.add("key2", "val2") d.add("key1", "val3") @@ -226,18 +289,27 @@ def test_popall(self, cls): assert ["val1", "val3"] == ret assert {"key2": "val2"} == d - def test_popall_default(self, cls): - d = cls() + def test_popall_default( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() assert "val" == d.popall("key", "val") - def test_popall_key_error(self, cls): - d = cls() + def test_popall_key_error( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_sensitive_multidict_class() with pytest.raises(KeyError, match="key"): d.popall("key") - def test_large_multidict_resizing(self, cls): + def test_large_multidict_resizing( + self, + case_sensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: SIZE = 1024 - d = cls() + d = case_sensitive_multidict_class() for i in range(SIZE): d["key" + str(i)] = i @@ -248,20 +320,11 @@ def test_large_multidict_resizing(self, cls): class TestCIMutableMultiDict: - @pytest.fixture - def cls(self, _multidict): - return _multidict.CIMultiDict - - @pytest.fixture - def proxy_cls(self, _multidict): - return _multidict.CIMultiDictProxy - - @pytest.fixture - def istr(self, _multidict): - return _multidict.istr - - def test_getall(self, cls): - d = cls([("KEY", "value1")], KEY="value2") + def test_getall( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class([("KEY", "value1")], KEY="value2") assert d != {"KEY": "value1"} assert len(d) == 2 @@ -271,53 +334,74 @@ def test_getall(self, cls): with pytest.raises(KeyError, match="some_key"): d.getall("some_key") - def test_ctor(self, cls): - d = cls(k1="v1") + def test_ctor( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class(k1="v1") assert "v1" == d["K1"] assert ("k1", "v1") in d.items() - def test_setitem(self, cls): - d = cls() + def test_setitem( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() d["k1"] = "v1" assert "v1" == d["K1"] assert ("k1", "v1") in d.items() - def test_delitem(self, cls): - d = cls() + def test_delitem( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() d["k1"] = "v1" assert "K1" in d del d["k1"] assert "K1" not in d - def test_copy(self, cls): - d1 = cls(key="KEY", a="b") + def test_copy( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d1 = case_insensitive_multidict_class(key="KEY", a="b") d2 = d1.copy() assert d1 == d2 assert d1.items() == d2.items() assert d1 is not d2 - def test__repr__(self, cls): - d = cls() - assert str(d) == "<%s()>" % cls.__name__ + def test__repr__( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() + assert str(d) == "<%s()>" % case_insensitive_multidict_class.__name__ - d = cls([("KEY", "one"), ("KEY", "two")]) + d = case_insensitive_multidict_class([("KEY", "one"), ("KEY", "two")]) - expected = "<%s('KEY': 'one', 'KEY': 'two')>" % cls.__name__ + expected = ( + f"<{case_insensitive_multidict_class.__name__}" + "('KEY': 'one', 'KEY': 'two')>" + ) assert str(d) == expected - def test_add(self, cls): - d = cls() + def test_add( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() assert d == {} d["KEY"] = "one" assert ("KEY", "one") in d.items() - assert d == cls({"Key": "one"}) + assert d == case_insensitive_multidict_class({"Key": "one"}) assert d.getall("key") == ["one"] d["KEY"] = "two" assert ("KEY", "two") in d.items() - assert d == cls({"Key": "two"}) + assert d == case_insensitive_multidict_class({"Key": "two"}) assert d.getall("key") == ["two"] d.add("KEY", "one") @@ -335,8 +419,11 @@ def test_add(self, cls): assert 4 == len(d) assert d.getall("test") == ["test"] - def test_extend(self, cls): - d = cls() + def test_extend( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() assert d == {} d.extend([("KEY", "one"), ("key", "two")], key=3, foo="bar") @@ -348,7 +435,7 @@ def test_extend(self, cls): assert ("key", 3) in itms assert ("foo", "bar") in itms - other = cls(Bar="baz") + other = case_insensitive_multidict_class(Bar="baz") assert other == {"Bar": "baz"} d.extend(other) @@ -365,24 +452,37 @@ def test_extend(self, cls): with pytest.raises(TypeError): d.extend("foo", "bar") - def test_extend_from_proxy(self, cls, proxy_cls): - d = cls([("a", "a"), ("b", "b")]) - proxy = proxy_cls(d) + def test_extend_from_proxy( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + case_insensitive_multidict_proxy_class: Type[MultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class([("a", "a"), ("b", "b")]) + proxy = case_insensitive_multidict_proxy_class(d) - d2 = cls() + d2 = case_insensitive_multidict_class() d2.extend(proxy) assert [("a", "a"), ("b", "b")] == list(d2.items()) - def test_clear(self, cls): - d = cls([("KEY", "one")], key="two", foo="bar") + def test_clear( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class([("KEY", "one")], key="two", foo="bar") d.clear() assert d == {} assert list(d.items()) == [] - def test_del(self, cls): - d = cls([("KEY", "one"), ("key", "two")], foo="bar") + def test_del( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class( + [("KEY", "one"), ("key", "two")], + foo="bar", + ) del d["key"] assert d == {"foo": "bar"} @@ -391,16 +491,25 @@ def test_del(self, cls): with pytest.raises(KeyError, match="key"): del d["key"] - def test_set_default(self, cls): - d = cls([("KEY", "one"), ("key", "two")], foo="bar") + def test_set_default( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class( + [("KEY", "one"), ("key", "two")], + foo="bar", + ) assert "one" == d.setdefault("key", "three") assert "three" == d.setdefault("otherkey", "three") assert "otherkey" in d assert ("otherkey", "three") in d.items() assert "three" == d["OTHERKEY"] - def test_popitem(self, cls): - d = cls() + def test_popitem( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() d.add("KEY", "val1") d.add("key", "val2") @@ -409,57 +518,83 @@ def test_popitem(self, cls): assert isinstance(pair[0], str) assert [("key", "val2")] == list(d.items()) - def test_popitem_empty_multidict(self, cls): - d = cls() + def test_popitem_empty_multidict( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() with pytest.raises(KeyError): d.popitem() - def test_pop(self, cls): - d = cls() + def test_pop( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() d.add("KEY", "val1") d.add("key", "val2") assert "val1" == d.pop("KEY") assert {"key": "val2"} == d - def test_pop_lowercase(self, cls): - d = cls() + def test_pop_lowercase( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class() d.add("KEY", "val1") d.add("key", "val2") assert "val1" == d.pop("key") assert {"key": "val2"} == d - def test_pop_default(self, cls): - d = cls(OTHER="val") + def test_pop_default( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class(OTHER="val") assert "default" == d.pop("key", "default") assert "other" in d - def test_pop_raises(self, cls): - d = cls(OTHER="val") + def test_pop_raises( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d = case_insensitive_multidict_class(OTHER="val") with pytest.raises(KeyError, match="KEY"): d.pop("KEY") assert "other" in d - def test_extend_with_istr(self, cls, istr): - us = istr("aBc") - d = cls() + def test_extend_with_istr( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + case_insensitive_str_class: Type[str], + ) -> None: + us = case_insensitive_str_class("aBc") + d = case_insensitive_multidict_class() d.extend([(us, "val")]) assert [("aBc", "val")] == list(d.items()) - def test_copy_istr(self, cls, istr): - d = cls({istr("Foo"): "bar"}) + def test_copy_istr( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + case_insensitive_str_class: Type[str], + ) -> None: + d = case_insensitive_multidict_class({case_insensitive_str_class("Foo"): "bar"}) d2 = d.copy() assert d == d2 - def test_eq(self, cls): - d1 = cls(Key="val") - d2 = cls(KEY="val") + def test_eq( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + d1 = case_insensitive_multidict_class(Key="val") + d2 = case_insensitive_multidict_class(KEY="val") assert d1 == d2 @@ -467,8 +602,11 @@ def test_eq(self, cls): sys.implementation.name == "pypy", reason="getsizeof() is not implemented on PyPy", ) - def test_sizeof(self, cls): - md = cls() + def test_sizeof( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + md = case_insensitive_multidict_class() s1 = sys.getsizeof(md) for i in string.ascii_lowercase: for j in string.ascii_uppercase: @@ -481,29 +619,41 @@ def test_sizeof(self, cls): sys.implementation.name == "pypy", reason="getsizeof() is not implemented on PyPy", ) - def test_min_sizeof(self, cls): - md = cls() + def test_min_sizeof( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: + md = case_insensitive_multidict_class() assert sys.getsizeof(md) < 1024 - def test_issue_620_items(self, cls): + def test_issue_620_items( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: # https://github.com/aio-libs/multidict/issues/620 - d = cls({"a": "123, 456", "b": "789"}) + d = case_insensitive_multidict_class({"a": "123, 456", "b": "789"}) before_mutation_items = d.items() d["c"] = "000" # This causes an error on pypy. list(before_mutation_items) - def test_issue_620_keys(self, cls): + def test_issue_620_keys( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: # https://github.com/aio-libs/multidict/issues/620 - d = cls({"a": "123, 456", "b": "789"}) + d = case_insensitive_multidict_class({"a": "123, 456", "b": "789"}) before_mutation_keys = d.keys() d["c"] = "000" # This causes an error on pypy. list(before_mutation_keys) - def test_issue_620_values(self, cls): + def test_issue_620_values( + self, + case_insensitive_multidict_class: Type[MutableMultiMapping[str]], + ) -> None: # https://github.com/aio-libs/multidict/issues/620 - d = cls({"a": "123, 456", "b": "789"}) + d = case_insensitive_multidict_class({"a": "123, 456", "b": "789"}) before_mutation_values = d.values() d["c"] = "000" # This causes an error on pypy. diff --git a/tests/test_mypy.py b/tests/test_mypy.py index 62bb62e52..38751fdb9 100644 --- a/tests/test_mypy.py +++ b/tests/test_mypy.py @@ -194,6 +194,7 @@ def test_setitem() -> None: d1[key] = "b" d2[key] = "b" + def test_delitem() -> None: d1: multidict.MultiDict[str] = multidict.MultiDict({"a": "b"}) d2: multidict.CIMultiDict[str] = multidict.CIMultiDict({"a": "b"}) @@ -248,6 +249,7 @@ def test_update_mapping() -> None: d1.update({key: "b"}) d2.update({key: "b"}) + def test_popone() -> None: d1: multidict.MultiDict[str] = multidict.MultiDict({"a": "b"}) d2: multidict.CIMultiDict[str] = multidict.CIMultiDict({"a": "b"}) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 3a85956e9..c1c2522c4 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -3,78 +3,36 @@ import pytest -from multidict._compat import USE_EXTENSIONS -from multidict._multidict_py import CIMultiDict as PyCIMultiDict -from multidict._multidict_py import CIMultiDictProxy as PyCIMultiDictProxy -from multidict._multidict_py import MultiDict as PyMultiDict # noqa: E402 -from multidict._multidict_py import MultiDictProxy as PyMultiDictProxy - -if USE_EXTENSIONS: - from multidict._multidict import ( # type: ignore - CIMultiDict, - CIMultiDictProxy, - MultiDict, - MultiDictProxy, - ) - - here = Path(__file__).resolve().parent -@pytest.fixture( - params=(["MultiDict", "CIMultiDict"] if USE_EXTENSIONS else []) - + ["PyMultiDict", "PyCIMultiDict"] -) -def cls_name(request): - return request.param - - -@pytest.fixture( - params=([MultiDict, CIMultiDict] if USE_EXTENSIONS else []) - + [PyMultiDict, PyCIMultiDict], - ids=(["MultiDict", "CIMultiDict"] if USE_EXTENSIONS else []) - + ["PyMultiDict", "PyCIMultiDict"], -) -def cls(request): - return request.param - - -@pytest.fixture( - params=( - [(MultiDictProxy, MultiDict), (CIMultiDictProxy, CIMultiDict)] - if USE_EXTENSIONS - else [] - ) - + [(PyMultiDictProxy, PyMultiDict), (PyCIMultiDictProxy, PyCIMultiDict)], - ids=(["MultiDictProxy", "CIMultiDictProxy"] if USE_EXTENSIONS else []) - + ["PyMultiDictProxy", "PyCIMultiDictProxy"], -) -def proxy_classes(request): - return request.param - - -def test_pickle(cls, pickle_protocol): - d = cls([("a", 1), ("a", 2)]) +def test_pickle(any_multidict_class, pickle_protocol): + d = any_multidict_class([("a", 1), ("a", 2)]) pbytes = pickle.dumps(d, pickle_protocol) obj = pickle.loads(pbytes) assert d == obj - assert isinstance(obj, cls) + assert isinstance(obj, any_multidict_class) -def test_pickle_proxy(proxy_classes): - proxy_cls, dict_cls = proxy_classes - d = dict_cls([("a", 1), ("a", 2)]) - proxy = proxy_cls(d) +def test_pickle_proxy(any_multidict_class, any_multidict_proxy_class): + d = any_multidict_class([("a", 1), ("a", 2)]) + proxy = any_multidict_proxy_class(d) with pytest.raises(TypeError): pickle.dumps(proxy) -def test_load_from_file(pickle_protocol, cls_name): - cls = globals()[cls_name] - d = cls([("a", 1), ("a", 2)]) - fname = "{}.pickle.{}".format(cls_name.lower(), pickle_protocol) +def test_load_from_file(any_multidict_class, multidict_implementation, pickle_protocol): + multidict_class_name = any_multidict_class.__name__ + pickle_file_basename = "-".join( + ( + multidict_class_name.lower(), + multidict_implementation.tag, + ) + ) + d = any_multidict_class([("a", 1), ("a", 2)]) + fname = f"{pickle_file_basename}.pickle.{pickle_protocol}" p = here / fname with p.open("rb") as f: obj = pickle.load(f) assert d == obj - assert isinstance(obj, cls) + assert isinstance(obj, any_multidict_class) diff --git a/tests/test_types.py b/tests/test_types.py index 3ae2cbb84..ceaa391e3 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -4,62 +4,65 @@ import pytest -def test_proxies(_multidict): - assert issubclass(_multidict.CIMultiDictProxy, _multidict.MultiDictProxy) +def test_proxies(multidict_module): + assert issubclass( + multidict_module.CIMultiDictProxy, + multidict_module.MultiDictProxy, + ) -def test_dicts(_multidict): - assert issubclass(_multidict.CIMultiDict, _multidict.MultiDict) +def test_dicts(multidict_module): + assert issubclass(multidict_module.CIMultiDict, multidict_module.MultiDict) -def test_proxy_not_inherited_from_dict(_multidict): - assert not issubclass(_multidict.MultiDictProxy, _multidict.MultiDict) +def test_proxy_not_inherited_from_dict(multidict_module): + assert not issubclass(multidict_module.MultiDictProxy, multidict_module.MultiDict) -def test_dict_not_inherited_from_proxy(_multidict): - assert not issubclass(_multidict.MultiDict, _multidict.MultiDictProxy) +def test_dict_not_inherited_from_proxy(multidict_module): + assert not issubclass(multidict_module.MultiDict, multidict_module.MultiDictProxy) -def test_multidict_proxy_copy_type(_multidict): - d = _multidict.MultiDict(key="val") - p = _multidict.MultiDictProxy(d) - assert isinstance(p.copy(), _multidict.MultiDict) +def test_multidict_proxy_copy_type(multidict_module): + d = multidict_module.MultiDict(key="val") + p = multidict_module.MultiDictProxy(d) + assert isinstance(p.copy(), multidict_module.MultiDict) -def test_cimultidict_proxy_copy_type(_multidict): - d = _multidict.CIMultiDict(key="val") - p = _multidict.CIMultiDictProxy(d) - assert isinstance(p.copy(), _multidict.CIMultiDict) +def test_cimultidict_proxy_copy_type(multidict_module): + d = multidict_module.CIMultiDict(key="val") + p = multidict_module.CIMultiDictProxy(d) + assert isinstance(p.copy(), multidict_module.CIMultiDict) -def test_create_multidict_proxy_from_nonmultidict(_multidict): +def test_create_multidict_proxy_from_nonmultidict(multidict_module): with pytest.raises(TypeError): - _multidict.MultiDictProxy({}) + multidict_module.MultiDictProxy({}) -def test_create_multidict_proxy_from_cimultidict(_multidict): - d = _multidict.CIMultiDict(key="val") - p = _multidict.MultiDictProxy(d) +def test_create_multidict_proxy_from_cimultidict(multidict_module): + d = multidict_module.CIMultiDict(key="val") + p = multidict_module.MultiDictProxy(d) assert p == d -def test_create_multidict_proxy_from_multidict_proxy_from_mdict(_multidict): - d = _multidict.MultiDict(key="val") - p = _multidict.MultiDictProxy(d) +def test_create_multidict_proxy_from_multidict_proxy_from_mdict(multidict_module): + d = multidict_module.MultiDict(key="val") + p = multidict_module.MultiDictProxy(d) assert p == d - p2 = _multidict.MultiDictProxy(p) + p2 = multidict_module.MultiDictProxy(p) assert p2 == p -def test_create_cimultidict_proxy_from_cimultidict_proxy_from_ci(_multidict): - d = _multidict.CIMultiDict(key="val") - p = _multidict.CIMultiDictProxy(d) +def test_create_cimultidict_proxy_from_cimultidict_proxy_from_ci(multidict_module): + d = multidict_module.CIMultiDict(key="val") + p = multidict_module.CIMultiDictProxy(d) assert p == d - p2 = _multidict.CIMultiDictProxy(p) + p2 = multidict_module.CIMultiDictProxy(p) assert p2 == p -def test_create_cimultidict_proxy_from_nonmultidict(_multidict): +def test_create_cimultidict_proxy_from_nonmultidict(multidict_module): with pytest.raises( TypeError, match=( @@ -67,11 +70,11 @@ def test_create_cimultidict_proxy_from_nonmultidict(_multidict): "not " ), ): - _multidict.CIMultiDictProxy({}) + multidict_module.CIMultiDictProxy({}) -def test_create_ci_multidict_proxy_from_multidict(_multidict): - d = _multidict.MultiDict(key="val") +def test_create_ci_multidict_proxy_from_multidict(multidict_module): + d = multidict_module.MultiDict(key="val") with pytest.raises( TypeError, match=( @@ -79,31 +82,32 @@ def test_create_ci_multidict_proxy_from_multidict(_multidict): "not " ), ): - _multidict.CIMultiDictProxy(d) + multidict_module.CIMultiDictProxy(d) @pytest.mark.skipif( sys.version_info >= (3, 9), reason="Python 3.9 uses GenericAlias which is different" ) -def test_generic_exists(_multidict) -> None: - assert _multidict.MultiDict[int] is _multidict.MultiDict - assert _multidict.MultiDictProxy[int] is _multidict.MultiDictProxy - assert _multidict.CIMultiDict[int] is _multidict.CIMultiDict - assert _multidict.CIMultiDictProxy[int] is _multidict.CIMultiDictProxy +def test_generic_exists(multidict_module) -> None: + assert multidict_module.MultiDict[int] is multidict_module.MultiDict + assert multidict_module.MultiDictProxy[int] is multidict_module.MultiDictProxy + assert multidict_module.CIMultiDict[int] is multidict_module.CIMultiDict + assert multidict_module.CIMultiDictProxy[int] is multidict_module.CIMultiDictProxy @pytest.mark.skipif( sys.version_info < (3, 9), reason="Python 3.9 is required for GenericAlias" ) -def test_generic_alias(_multidict) -> None: - - assert _multidict.MultiDict[int] == types.GenericAlias(_multidict.MultiDict, (int,)) - assert _multidict.MultiDictProxy[int] == types.GenericAlias( - _multidict.MultiDictProxy, (int,) +def test_generic_alias(multidict_module) -> None: + assert multidict_module.MultiDict[int] == types.GenericAlias( + multidict_module.MultiDict, (int,) + ) + assert multidict_module.MultiDictProxy[int] == types.GenericAlias( + multidict_module.MultiDictProxy, (int,) ) - assert _multidict.CIMultiDict[int] == types.GenericAlias( - _multidict.CIMultiDict, (int,) + assert multidict_module.CIMultiDict[int] == types.GenericAlias( + multidict_module.CIMultiDict, (int,) ) - assert _multidict.CIMultiDictProxy[int] == types.GenericAlias( - _multidict.CIMultiDictProxy, (int,) + assert multidict_module.CIMultiDictProxy[int] == types.GenericAlias( + multidict_module.CIMultiDictProxy, (int,) ) diff --git a/tests/test_update.py b/tests/test_update.py index 4bacdbce7..f45532785 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -1,97 +1,68 @@ from collections import deque +from typing import Type -import pytest +from multidict import MultiMapping -from multidict._compat import USE_EXTENSIONS -from multidict._multidict_py import CIMultiDict as PyCIMultiDict -from multidict._multidict_py import MultiDict as PyMultiDict # noqa: E402 -if USE_EXTENSIONS: - from multidict._multidict import CIMultiDict, MultiDict # type: ignore - - -@pytest.fixture( - params=([MultiDict, CIMultiDict] if USE_EXTENSIONS else []) - + [PyMultiDict, PyCIMultiDict], - ids=(["MultiDict", "CIMultiDict"] if USE_EXTENSIONS else []) - + ["PyMultiDict", "PyCIMultiDict"], -) -def cls(request): - return request.param - - -@pytest.fixture -def md_cls(_multidict): - return _multidict.MultiDict - - -@pytest.fixture -def ci_md_cls(_multidict): - return _multidict.CIMultiDict - - -@pytest.fixture -def istr(_multidict): - return _multidict.istr - - -def test_update_replace(cls): - obj1 = cls([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) - obj2 = cls([("a", 4), ("b", 5), ("a", 6)]) +def test_update_replace(any_multidict_class: Type[MultiMapping[str]]) -> None: + obj1 = any_multidict_class([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) + obj2 = any_multidict_class([("a", 4), ("b", 5), ("a", 6)]) obj1.update(obj2) expected = [("a", 4), ("b", 5), ("a", 6), ("c", 10)] assert list(obj1.items()) == expected -def test_update_append(cls): - obj1 = cls([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) - obj2 = cls([("a", 4), ("a", 5), ("a", 6)]) +def test_update_append(any_multidict_class: Type[MultiMapping[str]]) -> None: + obj1 = any_multidict_class([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) + obj2 = any_multidict_class([("a", 4), ("a", 5), ("a", 6)]) obj1.update(obj2) expected = [("a", 4), ("b", 2), ("a", 5), ("c", 10), ("a", 6)] assert list(obj1.items()) == expected -def test_update_remove(cls): - obj1 = cls([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) - obj2 = cls([("a", 4)]) +def test_update_remove(any_multidict_class: Type[MultiMapping[str]]) -> None: + obj1 = any_multidict_class([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) + obj2 = any_multidict_class([("a", 4)]) obj1.update(obj2) expected = [("a", 4), ("b", 2), ("c", 10)] assert list(obj1.items()) == expected -def test_update_replace_seq(cls): - obj1 = cls([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) +def test_update_replace_seq(any_multidict_class: Type[MultiMapping[str]]) -> None: + obj1 = any_multidict_class([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) obj2 = [("a", 4), ("b", 5), ("a", 6)] obj1.update(obj2) expected = [("a", 4), ("b", 5), ("a", 6), ("c", 10)] assert list(obj1.items()) == expected -def test_update_replace_seq2(cls): - obj1 = cls([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) +def test_update_replace_seq2(any_multidict_class: Type[MultiMapping[str]]) -> None: + obj1 = any_multidict_class([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) obj1.update([("a", 4)], b=5, a=6) expected = [("a", 4), ("b", 5), ("a", 6), ("c", 10)] assert list(obj1.items()) == expected -def test_update_append_seq(cls): - obj1 = cls([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) +def test_update_append_seq(any_multidict_class: Type[MultiMapping[str]]) -> None: + obj1 = any_multidict_class([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) obj2 = [("a", 4), ("a", 5), ("a", 6)] obj1.update(obj2) expected = [("a", 4), ("b", 2), ("a", 5), ("c", 10), ("a", 6)] assert list(obj1.items()) == expected -def test_update_remove_seq(cls): - obj1 = cls([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) +def test_update_remove_seq(any_multidict_class: Type[MultiMapping[str]]) -> None: + obj1 = any_multidict_class([("a", 1), ("b", 2), ("a", 3), ("c", 10)]) obj2 = [("a", 4)] obj1.update(obj2) expected = [("a", 4), ("b", 2), ("c", 10)] assert list(obj1.items()) == expected -def test_update_md(md_cls): - d = md_cls() +def test_update_md( + case_sensitive_multidict_class: Type[MultiMapping[str]], +) -> None: + d = case_sensitive_multidict_class() d.add("key", "val1") d.add("key", "val2") d.add("key2", "val3") @@ -101,19 +72,24 @@ def test_update_md(md_cls): assert [("key", "val"), ("key2", "val3")] == list(d.items()) -def test_update_istr_ci_md(ci_md_cls, istr): - d = ci_md_cls() - d.add(istr("KEY"), "val1") +def test_update_istr_ci_md( + case_insensitive_multidict_class: Type[MultiMapping[str]], + case_insensitive_str_class: str, +) -> None: + d = case_insensitive_multidict_class() + d.add(case_insensitive_str_class("KEY"), "val1") d.add("key", "val2") d.add("key2", "val3") - d.update({istr("key"): "val"}) + d.update({case_insensitive_str_class("key"): "val"}) assert [("key", "val"), ("key2", "val3")] == list(d.items()) -def test_update_ci_md(ci_md_cls): - d = ci_md_cls() +def test_update_ci_md( + case_insensitive_multidict_class: Type[MultiMapping[str]], +) -> None: + d = case_insensitive_multidict_class() d.add("KEY", "val1") d.add("key", "val2") d.add("key2", "val3") @@ -123,24 +99,30 @@ def test_update_ci_md(ci_md_cls): assert [("Key", "val"), ("key2", "val3")] == list(d.items()) -def test_update_list_arg_and_kwds(cls): - obj = cls() +def test_update_list_arg_and_kwds( + any_multidict_class: Type[MultiMapping[str]], +) -> None: + obj = any_multidict_class() arg = [("a", 1)] obj.update(arg, b=2) assert list(obj.items()) == [("a", 1), ("b", 2)] assert arg == [("a", 1)] -def test_update_tuple_arg_and_kwds(cls): - obj = cls() +def test_update_tuple_arg_and_kwds( + any_multidict_class: Type[MultiMapping[str]], +) -> None: + obj = any_multidict_class() arg = (("a", 1),) obj.update(arg, b=2) assert list(obj.items()) == [("a", 1), ("b", 2)] assert arg == (("a", 1),) -def test_update_deque_arg_and_kwds(cls): - obj = cls() +def test_update_deque_arg_and_kwds( + any_multidict_class: Type[MultiMapping[str]], +) -> None: + obj = any_multidict_class() arg = deque([("a", 1)]) obj.update(arg, b=2) assert list(obj.items()) == [("a", 1), ("b", 2)] diff --git a/tests/test_version.py b/tests/test_version.py index 067d6210c..e004afa11 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,199 +1,222 @@ -from typing import Type +from typing import Callable, Type import pytest from multidict import MultiMapping -from multidict._compat import USE_EXTENSIONS -from multidict._multidict_py import CIMultiDict as _CIMultiDict -from multidict._multidict_py import MultiDict as _MultiDict # noqa: E402 -from multidict._multidict_py import getversion as _getversion - -if USE_EXTENSIONS: - from multidict._multidict import ( # type: ignore - CIMultiDict, - MultiDict, - getversion, - ) - - -class VersionMixin: - cls: Type[MultiMapping[str]] - - def getver(self, md): - raise NotImplementedError - - def test_getversion_bad_param(self): - with pytest.raises(TypeError): - self.getver(1) - - def test_ctor(self): - m1 = self.cls() - v1 = self.getver(m1) - m2 = self.cls() - v2 = self.getver(m2) - assert v1 != v2 - - def test_add(self): - m = self.cls() - v = self.getver(m) - m.add("key", "val") - assert self.getver(m) > v - - def test_delitem(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - del m["key"] - assert self.getver(m) > v - - def test_delitem_not_found(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - with pytest.raises(KeyError): - del m["notfound"] - assert self.getver(m) == v - - def test_setitem(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m["key"] = "val2" - assert self.getver(m) > v - - def test_setitem_not_found(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m["notfound"] = "val2" - assert self.getver(m) > v - - def test_clear(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.clear() - assert self.getver(m) > v - - def test_setdefault(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.setdefault("key2", "val2") - assert self.getver(m) > v - - def test_popone(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.popone("key") - assert self.getver(m) > v - - def test_popone_default(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.popone("key2", "default") - assert self.getver(m) == v - - def test_popone_key_error(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - with pytest.raises(KeyError): - m.popone("key2") - assert self.getver(m) == v - - def test_pop(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.pop("key") - assert self.getver(m) > v - - def test_pop_default(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.pop("key2", "default") - assert self.getver(m) == v - - def test_pop_key_error(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - with pytest.raises(KeyError): - m.pop("key2") - assert self.getver(m) == v - - def test_popall(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.popall("key") - assert self.getver(m) > v - - def test_popall_default(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.popall("key2", "default") - assert self.getver(m) == v - - def test_popall_key_error(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - with pytest.raises(KeyError): - m.popall("key2") - assert self.getver(m) == v - - def test_popitem(self): - m = self.cls() - m.add("key", "val") - v = self.getver(m) - m.popitem() - assert self.getver(m) > v - - def test_popitem_key_error(self): - m = self.cls() - v = self.getver(m) - with pytest.raises(KeyError): - m.popitem() - assert self.getver(m) == v - - -if USE_EXTENSIONS: - - class TestMultiDict(VersionMixin): - - cls = MultiDict - - def getver(self, md): - return getversion(md) - - -if USE_EXTENSIONS: - - class TestCIMultiDict(VersionMixin): - cls = CIMultiDict - def getver(self, md): - return getversion(md) - - -class TestPyMultiDict(VersionMixin): - - cls = _MultiDict # type: ignore[assignment] - - def getver(self, md): - return _getversion(md) - - -class TestPyCIMultiDict(VersionMixin): - - cls = _CIMultiDict # type: ignore[assignment] - - def getver(self, md): - return _getversion(md) +def test_getversion_bad_param(multidict_getversion_callable): + with pytest.raises(TypeError): + multidict_getversion_callable(1) + + +def test_ctor( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m1 = any_multidict_class() + v1 = multidict_getversion_callable(m1) + m2 = any_multidict_class() + v2 = multidict_getversion_callable(m2) + assert v1 != v2 + + +def test_add( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + v = multidict_getversion_callable(m) + m.add("key", "val") + assert multidict_getversion_callable(m) > v + + +def test_delitem( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + del m["key"] + assert multidict_getversion_callable(m) > v + + +def test_delitem_not_found( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + with pytest.raises(KeyError): + del m["notfound"] + assert multidict_getversion_callable(m) == v + + +def test_setitem( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m["key"] = "val2" + assert multidict_getversion_callable(m) > v + + +def test_setitem_not_found( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m["notfound"] = "val2" + assert multidict_getversion_callable(m) > v + + +def test_clear( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.clear() + assert multidict_getversion_callable(m) > v + + +def test_setdefault( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.setdefault("key2", "val2") + assert multidict_getversion_callable(m) > v + + +def test_popone( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.popone("key") + assert multidict_getversion_callable(m) > v + + +def test_popone_default( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.popone("key2", "default") + assert multidict_getversion_callable(m) == v + + +def test_popone_key_error( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + with pytest.raises(KeyError): + m.popone("key2") + assert multidict_getversion_callable(m) == v + + +def test_pop( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.pop("key") + assert multidict_getversion_callable(m) > v + + +def test_pop_default( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.pop("key2", "default") + assert multidict_getversion_callable(m) == v + + +def test_pop_key_error( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + with pytest.raises(KeyError): + m.pop("key2") + assert multidict_getversion_callable(m) == v + + +def test_popall( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.popall("key") + assert multidict_getversion_callable(m) > v + + +def test_popall_default( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.popall("key2", "default") + assert multidict_getversion_callable(m) == v + + +def test_popall_key_error( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + with pytest.raises(KeyError): + m.popall("key2") + assert multidict_getversion_callable(m) == v + + +def test_popitem( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + m.add("key", "val") + v = multidict_getversion_callable(m) + m.popitem() + assert multidict_getversion_callable(m) > v + + +def test_popitem_key_error( + any_multidict_class: Type[MultiMapping[str]], + multidict_getversion_callable: Callable, +) -> None: + m = any_multidict_class() + v = multidict_getversion_callable(m) + with pytest.raises(KeyError): + m.popitem() + assert multidict_getversion_callable(m) == v