Rename warn_when_subclassed to deprecated_base_class; add some magic for issubclass and isinstance checks to work if subclasses of non-deprecated class are checked against deprecated class.

This commit is contained in:
Mikhail Korobov 2013-12-30 20:59:10 +06:00
parent 04788673e7
commit 39458b7843
4 changed files with 191 additions and 16 deletions

View File

@ -7,7 +7,7 @@ from scrapy import log
from scrapy.http import Request
from scrapy.utils.trackref import object_ref
from scrapy.utils.url import url_is_from_spider
from scrapy.utils.deprecate import warn_when_subclassed
from scrapy.utils.deprecate import deprecated_base_class
class Spider(object_ref):
@ -66,10 +66,7 @@ class Spider(object_ref):
class BaseSpider(Spider):
__metaclass__ = warn_when_subclassed(
superclass=Spider,
message="scrapy.spider.BaseSpider was deprecated. Please inherit from scrapy.spider.Spider."
)
__metaclass__ = deprecated_base_class(Spider, "scrapy.spider.BaseSpider was deprecated. Please inherit from scrapy.spider.Spider.")
class ObsoleteClass(object):

View File

@ -2,6 +2,7 @@ import gzip
import inspect
import warnings
from cStringIO import StringIO
from scrapy.utils.trackref import object_ref
from twisted.trial import unittest
@ -141,12 +142,54 @@ class BaseSpiderDeprecationTest(unittest.TestCase):
def test_basespider_is_deprecated(self):
with warnings.catch_warnings(record=True) as w:
class MySpider(BaseSpider):
class MySpider1(BaseSpider):
pass
self.assertEqual(len(w), 1)
self.assertEqual(w[0].category, ScrapyDeprecationWarning)
self.assertEqual(w[0].lineno, inspect.getsourcelines(MySpider)[1])
self.assertEqual(w[0].lineno, inspect.getsourcelines(MySpider1)[1])
def test_basespider_issubclass(self):
class MySpider2(Spider):
pass
class MySpider2a(MySpider2):
pass
class Foo(object):
pass
class Foo2(object_ref):
pass
assert issubclass(MySpider2, BaseSpider)
assert issubclass(MySpider2a, BaseSpider)
assert not issubclass(Foo, BaseSpider)
assert not issubclass(Foo2, BaseSpider)
def test_basespider_isinstance(self):
class MySpider3(Spider):
name = 'myspider3'
class MySpider3a(MySpider3):
pass
class Foo(object):
pass
class Foo2(object_ref):
pass
assert isinstance(MySpider3(), BaseSpider)
assert isinstance(MySpider3a(), BaseSpider)
assert not isinstance(Foo(), BaseSpider)
assert not isinstance(Foo2(), BaseSpider)
def test_crawl_spider(self):
assert issubclass(CrawlSpider, Spider)
assert issubclass(CrawlSpider, BaseSpider)
assert isinstance(CrawlSpider(name='foo'), Spider)
assert isinstance(CrawlSpider(name='foo'), BaseSpider)
if __name__ == '__main__':

View File

@ -3,7 +3,7 @@ from __future__ import absolute_import
import inspect
import unittest
import warnings
from scrapy.utils.deprecate import warn_when_subclassed
from scrapy.utils.deprecate import deprecated_base_class
class MyWarning(UserWarning):
pass
@ -21,7 +21,7 @@ class WarnWhenSubclassedTest(unittest.TestCase):
with warnings.catch_warnings(record=True) as w:
class Deprecated(NewName):
__metaclass__ = warn_when_subclassed(NewName, "message")
__metaclass__ = deprecated_base_class(NewName, "message")
self.assertEqual(w, [])
@ -29,7 +29,7 @@ class WarnWhenSubclassedTest(unittest.TestCase):
with warnings.catch_warnings(record=True) as w:
class Deprecated(NewName):
__metaclass__ = warn_when_subclassed(NewName, "message", MyWarning)
__metaclass__ = deprecated_base_class(NewName, "message", MyWarning)
class UserClass(Deprecated):
pass
@ -39,3 +39,89 @@ class WarnWhenSubclassedTest(unittest.TestCase):
assert issubclass(msg.category, MyWarning)
self.assertEqual(str(msg.message), "message")
self.assertEqual(msg.lineno, inspect.getsourcelines(UserClass)[1])
def test_warning_auto_message(self):
with warnings.catch_warnings(record=True) as w:
class Deprecated(NewName):
__metaclass__ = deprecated_base_class(NewName)
class UserClass2(Deprecated):
pass
msg = str(w[0].message)
self.assertIn("scrapy.tests.test_utils_deprecate.NewName", msg)
# this doesn't work:
# self.assertIn("scrapy.tests.test_utils_deprecate.Deprecated", msg)
def test_issubclass(self):
with warnings.catch_warnings(record=True):
class DeprecatedName(NewName):
__metaclass__ = deprecated_base_class(NewName, "message", MyWarning)
class UpdatedUserClass1(NewName):
pass
class UpdatedUserClass1a(NewName):
pass
class OutdatedUserClass1(DeprecatedName):
pass
class UnrelatedClass(object):
pass
class OldStyleClass:
pass
assert issubclass(UpdatedUserClass1, NewName)
assert issubclass(UpdatedUserClass1a, NewName)
assert issubclass(UpdatedUserClass1, DeprecatedName)
assert issubclass(UpdatedUserClass1a, DeprecatedName)
assert issubclass(OutdatedUserClass1, DeprecatedName)
assert not issubclass(UnrelatedClass, DeprecatedName)
assert not issubclass(OldStyleClass, DeprecatedName)
assert not issubclass(OldStyleClass, DeprecatedName)
self.assertRaises(TypeError, issubclass, object(), DeprecatedName)
def test_isinstance(self):
with warnings.catch_warnings(record=True):
class DeprecatedName(NewName):
__metaclass__ = deprecated_base_class(NewName, "message", MyWarning)
class UpdatedUserClass2(NewName):
pass
class UpdatedUserClass2a(NewName):
pass
class OutdatedUserClass2(DeprecatedName):
pass
class UnrelatedClass(object):
pass
class OldStyleClass:
pass
assert isinstance(UpdatedUserClass2(), NewName)
assert isinstance(UpdatedUserClass2a(), NewName)
assert isinstance(UpdatedUserClass2(), DeprecatedName)
assert isinstance(UpdatedUserClass2a(), DeprecatedName)
assert isinstance(OutdatedUserClass2(), DeprecatedName)
assert not isinstance(UnrelatedClass(), DeprecatedName)
assert not isinstance(OldStyleClass(), DeprecatedName)
def test_invalid_usage(self):
class SomeClass(object):
pass
def define_invalid():
class DeprecatedName(NewName):
__metaclass__ = deprecated_base_class(SomeClass)
self.assertRaises(ValueError, define_invalid)

View File

@ -1,7 +1,7 @@
"""Some helpers for deprecation messages"""
import warnings
import inspect
from scrapy.exceptions import ScrapyDeprecationWarning
def attribute(obj, oldattr, newattr, version='0.12'):
@ -11,15 +11,64 @@ def attribute(obj, oldattr, newattr, version='0.12'):
(cname, oldattr, version, cname, newattr), ScrapyDeprecationWarning, stacklevel=3)
def warn_when_subclassed(superclass, message, category=ScrapyDeprecationWarning):
def deprecated_base_class(new_class, message=None, category=ScrapyDeprecationWarning):
"""
Return a metaclass that causes classes to
issue a warning when they are subclassed.
Return a metaclass that causes classes to issue a warning when
they are subclassed.
In addition to that, subclasses of ``new_class`` are considered subclasses
of a class this metaclass is applied to.
It can be used to rename a base class of some user classes, e.g. if we
have
class OldName(SomeClass):
# ...
and we want to rename it to NewName, we can do the following::
class NewName(SomeClass):
# ...
class OldName(NewName):
__metaclass__ = deprecated_base_class(NewName, "OldName is deprecated. Please inherit from NewName.")
Then, if user class inherits from OldName, warning is issued. Also, if
some code uses ``issubclass(sub, OldName)`` or ``isinstance(sub(), OldName)``
checks they'll still return True if sub is a subclass of NewName instead of
OldName.
"""
class Metaclass(type):
def __init__(cls, name, bases, clsdict):
if len(cls.mro()) > len(superclass.mro()) + 1:
warnings.warn(message, category, stacklevel=2)
if not issubclass(cls, new_class):
raise ValueError("first parameter of `warn_when_subclassed` must be a superclass of %s" % cls)
warn_message = message
if warn_message is None:
# XXX: how to get a name of deprecated base class?
cls_name = cls.__module__ + '.' + name
new_name = new_class.__module__ + '.' + new_class.__name__
warn_message = "Base class of %s was deprecated. Please inherit from %s." % (cls_name, new_name)
if len(cls.mro()) > len(new_class.mro()) + 1:
warnings.warn(warn_message, category, stacklevel=2)
super(Metaclass, cls).__init__(name, bases, clsdict)
# see http://www.python.org/dev/peps/pep-3119/#overloading-isinstance-and-issubclass
# and http://docs.python.org/2/reference/datamodel.html#customizing-instance-and-subclass-checks
# for implementation details
def __instancecheck__(cls, inst):
return any(cls.__subclasscheck__(c)
for c in {type(inst), inst.__class__})
def __subclasscheck__(cls, sub):
if not inspect.isclass(sub):
raise TypeError("issubclass() arg 1 must be a class")
mro = getattr(sub, '__mro__', ())
candidates = {cls, new_class}
return any(c in candidates for c in mro)
return Metaclass