diff --git a/scrapy/spider.py b/scrapy/spider.py index a141266a2..8ecfae2a0 100644 --- a/scrapy/spider.py +++ b/scrapy/spider.py @@ -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 deprecated_base_class +from scrapy.utils.deprecate import create_deprecated_class class Spider(object_ref): @@ -65,8 +65,7 @@ class Spider(object_ref): __repr__ = __str__ -class BaseSpider(Spider): - __metaclass__ = deprecated_base_class(Spider, "scrapy.spider.BaseSpider was deprecated. Please inherit from scrapy.spider.Spider.") +BaseSpider = create_deprecated_class('BaseSpider', Spider) class ObsoleteClass(object): diff --git a/scrapy/tests/test_utils_deprecate.py b/scrapy/tests/test_utils_deprecate.py index f5ba214b3..a0ba3bad9 100644 --- a/scrapy/tests/test_utils_deprecate.py +++ b/scrapy/tests/test_utils_deprecate.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import inspect import unittest import warnings -from scrapy.utils.deprecate import deprecated_base_class +from scrapy.utils.deprecate import create_deprecated_class class MyWarning(UserWarning): pass @@ -19,17 +19,13 @@ class WarnWhenSubclassedTest(unittest.TestCase): def test_no_warning_on_definition(self): with warnings.catch_warnings(record=True) as w: - - class Deprecated(NewName): - __metaclass__ = deprecated_base_class(NewName, "message") + Deprecated = create_deprecated_class('Deprecated', NewName) self.assertEqual(w, []) def test_warning_on_subclassing(self): with warnings.catch_warnings(record=True) as w: - - class Deprecated(NewName): - __metaclass__ = deprecated_base_class(NewName, "message", MyWarning) + Deprecated = create_deprecated_class('Deprecated', NewName, MyWarning) class UserClass(Deprecated): pass @@ -37,28 +33,28 @@ class WarnWhenSubclassedTest(unittest.TestCase): self.assertEqual(len(w), 1) msg = w[0] assert issubclass(msg.category, MyWarning) - self.assertEqual(str(msg.message), "message") + self.assertEqual( + str(msg.message), + "Base class scrapy.tests.test_utils_deprecate.Deprecated of " + "scrapy.tests.test_utils_deprecate.UserClass was deprecated. " + "Please inherit from scrapy.tests.test_utils_deprecate.NewName." + ) 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) + Deprecated = create_deprecated_class('Deprecated', 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) + 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) + DeprecatedName = create_deprecated_class('DeprecatedName', NewName) class UpdatedUserClass1(NewName): pass @@ -88,9 +84,7 @@ class WarnWhenSubclassedTest(unittest.TestCase): def test_isinstance(self): with warnings.catch_warnings(record=True): - - class DeprecatedName(NewName): - __metaclass__ = deprecated_base_class(NewName, "message", MyWarning) + DeprecatedName = create_deprecated_class('DeprecatedName', NewName) class UpdatedUserClass2(NewName): pass @@ -114,14 +108,3 @@ class WarnWhenSubclassedTest(unittest.TestCase): 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) diff --git a/scrapy/utils/deprecate.py b/scrapy/utils/deprecate.py index 24a0ccbd7..37641cf8e 100644 --- a/scrapy/utils/deprecate.py +++ b/scrapy/utils/deprecate.py @@ -4,6 +4,7 @@ import warnings import inspect from scrapy.exceptions import ScrapyDeprecationWarning + def attribute(obj, oldattr, newattr, version='0.12'): cname = obj.__class__.__name__ warnings.warn("%s.%s attribute is deprecated and will be no longer supported " @@ -11,15 +12,12 @@ def attribute(obj, oldattr, newattr, version='0.12'): (cname, oldattr, version, cname, newattr), ScrapyDeprecationWarning, stacklevel=3) -def deprecated_base_class(new_class, message=None, category=ScrapyDeprecationWarning): +def create_deprecated_class(name, new_class, warn_category=ScrapyDeprecationWarning, message=None): """ - Return a metaclass that causes classes to issue a warning when - they are subclassed. + Return a "deprecated" class that causes its subclasses to issue a warning. + Subclasses of ``new_class`` are considered subclasses of this class. - 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 + It can be used to rename a base class in a library. For example, if we have class OldName(SomeClass): @@ -30,30 +28,26 @@ def deprecated_base_class(new_class, message=None, category=ScrapyDeprecationWar class NewName(SomeClass): # ... - class OldName(NewName): - __metaclass__ = deprecated_base_class(NewName, "OldName is deprecated. Please inherit from NewName.") + OldName = create_deprecated_class('OldName', 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): + + deprecated = {} + class DeprecatedClass(type): + def __init__(cls, name, bases, clsdict): - - 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) + if 'cls' in deprecated: + if message is not None: + msg = message + else: + msg = "Base class {0} of {1} was deprecated. Please inherit from {2}."\ + .format(_clspath(deprecated['cls']), _clspath(cls), _clspath(new_class)) + warnings.warn(msg, warn_category, stacklevel=2) + super(DeprecatedClass, 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 @@ -70,5 +64,16 @@ def deprecated_base_class(new_class, message=None, category=ScrapyDeprecationWar candidates = {cls, new_class} return any(c in candidates for c in mro) - return Metaclass + deprecated_cls = DeprecatedClass(name, (new_class,), {}) + deprecated['cls'] = deprecated_cls + frm = inspect.stack()[1] + parent_module = inspect.getmodule(frm[0]) + if parent_module is not None: + deprecated_cls.__module__ = parent_module.__name__ + + return deprecated_cls + + +def _clspath(cls): + return '{}.{}'.format(cls.__module__, cls.__name__)