Skip to content

Commit

Permalink
Adapt multidict C ext test implementation (#25)
Browse files Browse the repository at this point in the history
* Adapt multidict implementation

* migrate tests

* cleanup

* Revert "fix tests"

This reverts commit ce4da7f.

* Revert "Revert "fix tests""

This reverts commit 428a16f.

* fix missing extension files under windows

* fix missing extension files under windows

* maybe should have been manifest

* Update tests/conftest.py

Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>

* windows produces .pyd

* Update tests/conftest.py

Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>

* naming

* revert MANIFEST.in changes

---------

Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
  • Loading branch information
bdraco and webknjaz authored Oct 6, 2024
1 parent b762a3c commit 8a68f5a
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 170 deletions.
12 changes: 7 additions & 5 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,13 @@ jobs:
sed -i.bak 's/^\s\{2\}Cython\.Coverage$//g' .coveragerc
shell: bash
- name: Run unittests
env:
PROPCACHE_NO_EXTENSIONS: ${{ matrix.no-extensions }}
run: >-
python -Im
pytest
-v
--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()
Expand All @@ -324,11 +323,14 @@ jobs:
if: >-
!cancelled()
&& failure()
env:
PROPCACHE_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
pytest
--no-cov
-vvvvv
--lf
-rA
--${{ matrix.no-extensions == 'Y' && 'no-' || '' }}c-extensions
&& exit 1
shell: bash
- name: Send coverage data to Codecov
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ include_package_data = True
# (see notes for the asterisk/`*` meaning)
* =
*.so
*.pyd
*.pyx

[options.exclude_package_data]
Expand Down
133 changes: 133 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import argparse
from dataclasses import dataclass
from functools import cached_property
from importlib import import_module
from sys import version_info as _version_info
from types import ModuleType
from typing import List, Type, Union

import pytest

C_EXT_MARK = pytest.mark.c_extension
PY_38_AND_BELOW = _version_info < (3, 9)


@dataclass(frozen=True)
class PropcacheImplementation:
"""A facade for accessing importable propcache 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 propcache variant."""
importable_module = "_helpers_py" if self.is_pure_python else "_helpers_c"
return import_module(f"propcache.{importable_module}")

def __str__(self) -> str:
"""Render the implementation facade instance as a string."""
return f"{self.tag}-module"


@pytest.fixture(
scope="session",
params=(
pytest.param(
PropcacheImplementation(is_pure_python=False),
marks=C_EXT_MARK,
),
PropcacheImplementation(is_pure_python=True),
),
ids=str,
)
def propcache_implementation(request: pytest.FixtureRequest) -> PropcacheImplementation:
"""Return a propcache variant facade."""
return request.param


@pytest.fixture(scope="session")
def propcache_module(
propcache_implementation: PropcacheImplementation,
) -> ModuleType:
"""Return a pre-imported module containing a propcache variant."""
return propcache_implementation.imported_module


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 ``propcache`` implementation.
"""
del pluginmanager

arg_parse_action: Union[str, Type[argparse.Action]]
if PY_38_AND_BELOW:
arg_parse_action = "store_true"
else:
arg_parse_action = argparse.BooleanOptionalAction # type: ignore[attr-defined, unused-ignore] # noqa

parser.addoption(
"--c-extensions", # disabled with `--no-c-extensions`
action=arg_parse_action,
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: List[pytest.Item] = []
deselected_tests: List[pytest.Item] = []

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.",
)
147 changes: 68 additions & 79 deletions tests/test_cached_property.py
Original file line number Diff line number Diff line change
@@ -1,134 +1,123 @@
import platform
from operator import not_

import pytest

from propcache import _helpers, _helpers_py
from propcache._helpers import cached_property

IS_PYPY = platform.python_implementation() == "PyPy"
def test_cached_property(propcache_module) -> None:
class A:
def __init__(self):
self._cache = {}

@propcache_module.cached_property
def prop(self):
return 1

class CachedPropertyMixin:
cached_property = NotImplemented
a = A()
assert a.prop == 1

def test_cached_property(self) -> None:
class A:
def __init__(self):
self._cache = {}

@self.cached_property # type: ignore[misc]
def prop(self):
return 1
def test_cached_property_class(propcache_module) -> None:
class A:
def __init__(self):
"""Init."""
# self._cache not set because its never accessed in this test

a = A()
assert a.prop == 1
@propcache_module.cached_property
def prop(self):
"""Docstring."""

def test_cached_property_class(self) -> None:
class A:
def __init__(self):
"""Init."""
# self._cache not set because its never accessed in this test
assert isinstance(A.prop, propcache_module.cached_property)
assert A.prop.__doc__ == "Docstring."

@self.cached_property # type: ignore[misc]
def prop(self):
"""Docstring."""

assert isinstance(A.prop, self.cached_property)
assert A.prop.__doc__ == "Docstring."
def test_cached_property_without_cache(propcache_module) -> None:
class A:

def test_cached_property_without_cache(self) -> None:
class A:
__slots__ = ()

__slots__ = ()
def __init__(self):
pass

def __init__(self):
pass
@propcache_module.cached_property
def prop(self):
"""Mock property."""

@self.cached_property # type: ignore[misc]
def prop(self):
"""Mock property."""
a = A()

a = A()
with pytest.raises(AttributeError):
a.prop = 123

with pytest.raises(AttributeError):
a.prop = 123

def test_cached_property_check_without_cache(self) -> None:
class A:
def test_cached_property_check_without_cache(propcache_module) -> None:
class A:

__slots__ = ()
__slots__ = ()

def __init__(self):
pass
def __init__(self):
pass

@self.cached_property # type: ignore[misc]
def prop(self):
"""Mock property."""
@propcache_module.cached_property
def prop(self):
"""Mock property."""

a = A()
with pytest.raises((TypeError, AttributeError)):
assert a.prop == 1
a = A()
with pytest.raises((TypeError, AttributeError)):
assert a.prop == 1


class A:
def __init__(self):
self._cache = {}
def test_cached_property_caching(propcache_module) -> None:

@cached_property
def prop(self):
"""Docstring."""
return 1
class A:
def __init__(self):
self._cache = {}

@propcache_module.cached_property
def prop(self):
"""Docstring."""
return 1

def test_cached_property():
a = A()
assert 1 == a.prop


def test_cached_property_class():
assert isinstance(A.prop, cached_property)
assert "Docstring." == A.prop.__doc__


class TestPyCachedProperty(CachedPropertyMixin):
cached_property = _helpers_py.cached_property # type: ignore[assignment]
def test_cached_property_class_docstring(propcache_module) -> None:

class A:
def __init__(self):
"""Init."""

if (
not _helpers.NO_EXTENSIONS
and not IS_PYPY
and hasattr(_helpers, "cached_property_c")
):
@propcache_module.cached_property
def prop(self):
"""Docstring."""

class TestCCachedProperty(CachedPropertyMixin):
cached_property = _helpers.cached_property_c # type: ignore[assignment, attr-defined, unused-ignore] # noqa: E501
assert isinstance(A.prop, propcache_module.cached_property)
assert "Docstring." == A.prop.__doc__


def test_set_name():
def test_set_name(propcache_module) -> None:
"""Test that the __set_name__ method is called and checked."""

class A:

@cached_property
@propcache_module.cached_property
def prop(self):
"""Docstring."""

A.prop.__set_name__(A, "prop")

with pytest.raises(
TypeError, match=r"Cannot assign the same cached_property to two "
):
match = r"Cannot assign the same cached_property to two "
with pytest.raises(TypeError, match=match):
A.prop.__set_name__(A, "something_else")


def test_get_without_set_name():
def test_get_without_set_name(propcache_module) -> None:
"""Test that get without __set_name__ fails."""
cp = cached_property(not_)
cp = propcache_module.cached_property(not_)

class A:
"""A class."""

A.cp = cp
with pytest.raises(TypeError, match=r"Cannot use cached_property instance "):
_ = A().cp
A.cp = cp # type: ignore[attr-defined]
match = r"Cannot use cached_property instance "
with pytest.raises(TypeError, match=match):
_ = A().cp # type: ignore[attr-defined]
Loading

0 comments on commit 8a68f5a

Please sign in to comment.