Skip to content

Commit

Permalink
Switch request handling to thread locals
Browse files Browse the repository at this point in the history
This is safer in general, and especially in ASGI environment. If ASGI (which ships its own version) is not available, we use threading local

Move to pytest runner to run async tests (though not on ASGI)
  • Loading branch information
yakky committed Nov 14, 2020
1 parent 1c3cffd commit d90d427
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 22 deletions.
1 change: 1 addition & 0 deletions changes/115.bugfix
@@ -0,0 +1 @@
Switch request handling to thread locals
1 change: 1 addition & 0 deletions cms_helper.py
Expand Up @@ -11,6 +11,7 @@
META_USE_TWITTER_PROPERTIES=True,
META_USE_SCHEMAORG_PROPERTIES=True,
FILE_UPLOAD_TEMP_DIR=mkdtemp(),
TEST_RUNNER="app_helper.pytest_runner.PytestTestRunner",
)

try:
Expand Down
22 changes: 9 additions & 13 deletions meta/models.py
@@ -1,9 +1,10 @@
import contextlib
import warnings
from copy import copy

from django.conf import settings as dj_settings

from . import settings
from .utils import get_request, set_request

NEED_REQUEST_OBJECT_ERR_MSG = """
Meta models needs request objects when initializing if sites framework is not used.
Expand Down Expand Up @@ -62,7 +63,7 @@ def _retrieve_data(self, request, metadata):
"""
Build the data according to the metadata configuration
"""
with self._set_request(request):
with set_request(request):
for field, value in metadata.items():
if value:
data = self._get_meta_value(field, value)
Expand Down Expand Up @@ -109,20 +110,15 @@ def as_meta(self, request=None):
setattr(meta, field, generaldesc)
return meta

@contextlib.contextmanager
def _set_request(self, request):
"""
Context processor that sets the request on the current instance
"""
self._request = request
yield
delattr(self, "_request")

def get_request(self):
"""
Retrieve request from current instance
"""
return getattr(self, "_request", None)
warnings.warn(
"use meta.utils.get_request function, ModelMeta.get_request will be removed in version 3.0",
PendingDeprecationWarning,
)
return get_request()

def get_author(self):
"""
Expand Down Expand Up @@ -186,7 +182,7 @@ def build_absolute_uri(self, url):
"""
Return the full url for the provided url argument
"""
request = self.get_request()
request = get_request()
if request:
return request.build_absolute_uri(url)

Expand Down
23 changes: 23 additions & 0 deletions meta/utils.py
@@ -0,0 +1,23 @@
import contextlib

try:
from asgiref.local import Local
except ImportError:
from threading import local as Local # noqa: N812
_thread_locals = Local()


@contextlib.contextmanager
def set_request(request):
"""
Context processor that sets the request on the current instance
"""
_thread_locals._request = request
yield


def get_request():
"""
Retrieve request from current instance
"""
return getattr(_thread_locals, "_request", None)
4 changes: 4 additions & 0 deletions requirements-test.txt
Expand Up @@ -4,3 +4,7 @@ coveralls>=2.0
mock>=1.0.1
pillow
django-app-helper>=2.0.1

pytest
pytest-django
pytest-asyncio
54 changes: 54 additions & 0 deletions tests/test_asgi.py
@@ -0,0 +1,54 @@
from datetime import timedelta

import pytest
from asgiref.sync import sync_to_async
from django.test import AsyncRequestFactory
from django.utils.text import slugify
from django.utils.timezone import now

from tests.example_app.models import Post


@sync_to_async
def get_post(title):
post, __ = Post.objects.get_or_create(
title=title,
og_title=f"og {title}",
twitter_title="twitter {title}",
schemaorg_title="schemaorg {title}",
slug=slugify(title),
abstract="post abstract",
meta_description="post meta",
meta_keywords="post keyword1,post keyword 2",
date_published_end=now() + timedelta(days=2),
text="post text",
image_url="/path/to/image",
)
return post


@sync_to_async
def delete_post(post):
post.delete()


@sync_to_async
def get_meta(post, request=None):
return post.as_meta(request)


@pytest.mark.asyncio
@pytest.mark.django_db
async def test_mixin_on_asgi():
post = await get_post("first post")
meta = await get_meta(post)
assert meta.title == "first post"


@pytest.mark.asyncio
@pytest.mark.django_db
async def test_mixin_on_asgi_request():
request = AsyncRequestFactory().get("/")
post = await get_post("first post")
meta = await get_meta(post, request)
assert meta.title == "first post"
19 changes: 10 additions & 9 deletions tests/test_mixin.py
Expand Up @@ -14,9 +14,10 @@
class TestMeta(BaseTestCase):
post = None

def setUp(self):
super().setUp()
self.post = Post.objects.create(
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.post, __ = Post.objects.get_or_create(
title="a title",
og_title="og title",
twitter_title="twitter title",
Expand All @@ -25,16 +26,16 @@ def setUp(self):
abstract="post abstract",
meta_description="post meta",
meta_keywords="post keyword1,post keyword 2",
author=self.user,
author=cls.user,
date_published_end=timezone.now() + timedelta(days=2),
text="post text",
image_url="/path/to/image",
)
self.post.main_image = self.create_django_image_object()
self.post.save()
self.image_url = self.post.main_image.url
self.image_width = self.post.main_image.width
self.image_height = self.post.main_image.height
cls.post.main_image, __ = cls.create_django_image()
cls.post.save()
cls.image_url = cls.post.main_image.url
cls.image_width = cls.post.main_image.width
cls.image_height = cls.post.main_image.height

@override_settings(META_SITE_PROTOCOL="http")
def test_as_meta(self):
Expand Down
6 changes: 6 additions & 0 deletions tox.ini
Expand Up @@ -150,3 +150,9 @@ ignore =
*.mo
ignore-bad-ideas =
*.mo

[pytest]
DJANGO_SETTINGS_MODULE = cms_helper
python_files = test_*.py
traceback = short
addopts = --reuse-db

0 comments on commit d90d427

Please sign in to comment.