Fixed #22078 -- Fixed crash of Feed with decorated methods.
This commit is contained in:
parent
7e4656e4b2
commit
8c0886b068
|
@ -1,3 +1,5 @@
|
||||||
|
from inspect import getattr_static, unwrap
|
||||||
|
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
|
@ -82,10 +84,21 @@ class Feed:
|
||||||
# Check co_argcount rather than try/excepting the function and
|
# Check co_argcount rather than try/excepting the function and
|
||||||
# catching the TypeError, because something inside the function
|
# catching the TypeError, because something inside the function
|
||||||
# may raise the TypeError. This technique is more accurate.
|
# may raise the TypeError. This technique is more accurate.
|
||||||
|
func = unwrap(attr)
|
||||||
try:
|
try:
|
||||||
code = attr.__code__
|
code = func.__code__
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
code = attr.__call__.__code__
|
func = unwrap(attr.__call__)
|
||||||
|
code = func.__code__
|
||||||
|
# If function doesn't have arguments and it is not a static method,
|
||||||
|
# it was decorated without using @functools.wraps.
|
||||||
|
if not code.co_argcount and not isinstance(
|
||||||
|
getattr_static(self, func.__name__, None), staticmethod
|
||||||
|
):
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"Feed method {attname!r} decorated by {func.__name__!r} needs to "
|
||||||
|
f"use @functools.wraps."
|
||||||
|
)
|
||||||
if code.co_argcount == 2: # one argument is 'self'
|
if code.co_argcount == 2: # one argument is 'self'
|
||||||
return attr(obj)
|
return attr(obj)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from django.contrib.syndication import views
|
from django.contrib.syndication import views
|
||||||
from django.utils import feedgenerator
|
from django.utils import feedgenerator
|
||||||
from django.utils.timezone import get_fixed_timezone
|
from django.utils.timezone import get_fixed_timezone
|
||||||
|
@ -5,6 +7,23 @@ from django.utils.timezone import get_fixed_timezone
|
||||||
from .models import Article, Entry
|
from .models import Article, Entry
|
||||||
|
|
||||||
|
|
||||||
|
def wraps_decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
value = f(*args, **kwargs)
|
||||||
|
return f"{value} -- decorated by @wraps."
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def common_decorator(f):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
value = f(*args, **kwargs)
|
||||||
|
return f"{value} -- common decorated."
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class TestRss2Feed(views.Feed):
|
class TestRss2Feed(views.Feed):
|
||||||
title = "My blog"
|
title = "My blog"
|
||||||
description = "A more thorough description of my blog."
|
description = "A more thorough description of my blog."
|
||||||
|
@ -47,11 +66,45 @@ class TestRss2FeedWithCallableObject(TestRss2Feed):
|
||||||
ttl = TimeToLive()
|
ttl = TimeToLive()
|
||||||
|
|
||||||
|
|
||||||
class TestRss2FeedWithStaticMethod(TestRss2Feed):
|
class TestRss2FeedWithDecoratedMethod(TestRss2Feed):
|
||||||
|
class TimeToLive:
|
||||||
|
@wraps_decorator
|
||||||
|
def __call__(self):
|
||||||
|
return 800
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@wraps_decorator
|
||||||
|
def feed_copyright():
|
||||||
|
return "Copyright (c) 2022, John Doe"
|
||||||
|
|
||||||
|
ttl = TimeToLive()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def categories():
|
def categories():
|
||||||
return ("javascript", "vue")
|
return ("javascript", "vue")
|
||||||
|
|
||||||
|
@wraps_decorator
|
||||||
|
def title(self):
|
||||||
|
return "Overridden title"
|
||||||
|
|
||||||
|
@wraps_decorator
|
||||||
|
def item_title(self, item):
|
||||||
|
return f"Overridden item title: {item.title}"
|
||||||
|
|
||||||
|
@wraps_decorator
|
||||||
|
def description(self, obj):
|
||||||
|
return "Overridden description"
|
||||||
|
|
||||||
|
@wraps_decorator
|
||||||
|
def item_description(self):
|
||||||
|
return "Overridden item description"
|
||||||
|
|
||||||
|
|
||||||
|
class TestRss2FeedWithWrongDecoratedMethod(TestRss2Feed):
|
||||||
|
@common_decorator
|
||||||
|
def item_description(self, item):
|
||||||
|
return f"Overridden item description: {item.title}"
|
||||||
|
|
||||||
|
|
||||||
class TestRss2FeedWithGuidIsPermaLinkTrue(TestRss2Feed):
|
class TestRss2FeedWithGuidIsPermaLinkTrue(TestRss2Feed):
|
||||||
def item_guid_is_permalink(self, item):
|
def item_guid_is_permalink(self, item):
|
||||||
|
|
|
@ -202,11 +202,38 @@ class SyndicationFeedTest(FeedTestCase):
|
||||||
chan = doc.getElementsByTagName("rss")[0].getElementsByTagName("channel")[0]
|
chan = doc.getElementsByTagName("rss")[0].getElementsByTagName("channel")[0]
|
||||||
self.assertChildNodeContent(chan, {"ttl": "700"})
|
self.assertChildNodeContent(chan, {"ttl": "700"})
|
||||||
|
|
||||||
def test_rss2_feed_with_static_methods(self):
|
def test_rss2_feed_with_decorated_methods(self):
|
||||||
response = self.client.get("/syndication/rss2/with-static-methods/")
|
response = self.client.get("/syndication/rss2/with-decorated-methods/")
|
||||||
doc = minidom.parseString(response.content)
|
doc = minidom.parseString(response.content)
|
||||||
chan = doc.getElementsByTagName("rss")[0].getElementsByTagName("channel")[0]
|
chan = doc.getElementsByTagName("rss")[0].getElementsByTagName("channel")[0]
|
||||||
self.assertCategories(chan, ["javascript", "vue"])
|
self.assertCategories(chan, ["javascript", "vue"])
|
||||||
|
self.assertChildNodeContent(
|
||||||
|
chan,
|
||||||
|
{
|
||||||
|
"title": "Overridden title -- decorated by @wraps.",
|
||||||
|
"description": "Overridden description -- decorated by @wraps.",
|
||||||
|
"ttl": "800 -- decorated by @wraps.",
|
||||||
|
"copyright": "Copyright (c) 2022, John Doe -- decorated by @wraps.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
items = chan.getElementsByTagName("item")
|
||||||
|
self.assertChildNodeContent(
|
||||||
|
items[0],
|
||||||
|
{
|
||||||
|
"title": (
|
||||||
|
f"Overridden item title: {self.e1.title} -- decorated by @wraps."
|
||||||
|
),
|
||||||
|
"description": "Overridden item description -- decorated by @wraps.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rss2_feed_with_wrong_decorated_methods(self):
|
||||||
|
msg = (
|
||||||
|
"Feed method 'item_description' decorated by 'wrapper' needs to use "
|
||||||
|
"@functools.wraps."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
||||||
|
self.client.get("/syndication/rss2/with-wrong-decorated-methods/")
|
||||||
|
|
||||||
def test_rss2_feed_guid_permalink_false(self):
|
def test_rss2_feed_guid_permalink_false(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -7,7 +7,14 @@ urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"syndication/rss2/with-callable-object/", feeds.TestRss2FeedWithCallableObject()
|
"syndication/rss2/with-callable-object/", feeds.TestRss2FeedWithCallableObject()
|
||||||
),
|
),
|
||||||
path("syndication/rss2/with-static-methods/", feeds.TestRss2FeedWithStaticMethod()),
|
path(
|
||||||
|
"syndication/rss2/with-decorated-methods/",
|
||||||
|
feeds.TestRss2FeedWithDecoratedMethod(),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"syndication/rss2/with-wrong-decorated-methods/",
|
||||||
|
feeds.TestRss2FeedWithWrongDecoratedMethod(),
|
||||||
|
),
|
||||||
path("syndication/rss2/articles/<int:entry_id>/", feeds.TestGetObjectFeed()),
|
path("syndication/rss2/articles/<int:entry_id>/", feeds.TestGetObjectFeed()),
|
||||||
path(
|
path(
|
||||||
"syndication/rss2/guid_ispermalink_true/",
|
"syndication/rss2/guid_ispermalink_true/",
|
||||||
|
|
Loading…
Reference in New Issue