1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
|
"""Some helpers for deprecation messages"""
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 "
"in Scrapy %s, use %s.%s attribute instead" % \
(cname, oldattr, version, cname, newattr), ScrapyDeprecationWarning, stacklevel=3)
def create_deprecated_class(name, new_class, clsdict=None,
warn_category=ScrapyDeprecationWarning,
warn_once=True,
old_class_path=None,
new_class_path=None,
subclass_warn_message="{cls} inherits from "\
"deprecated class {old}, please inherit "\
"from {new}.",
instance_warn_message="{cls} is deprecated, "\
"instantiate {new} instead."):
"""
Return a "deprecated" class that causes its subclasses to issue a warning.
Subclasses of ``new_class`` are considered subclasses of this class.
It also warns when the deprecated class is instantiated, but do not when
its subclasses are instantiated.
It can be used to rename a base class in a library. For example, if we
have
class OldName(SomeClass):
# ...
and we want to rename it to NewName, we can do the following::
class NewName(SomeClass):
# ...
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 DeprecatedClass(new_class.__class__):
deprecated_class = None
warned_on_subclass = False
def __new__(metacls, name, bases, clsdict_):
cls = super(DeprecatedClass, metacls).__new__(metacls, name, bases, clsdict_)
if metacls.deprecated_class is None:
metacls.deprecated_class = cls
return cls
def __init__(cls, name, bases, clsdict_):
meta = cls.__class__
old = meta.deprecated_class
if old in bases and not (warn_once and meta.warned_on_subclass):
meta.warned_on_subclass = True
msg = subclass_warn_message.format(cls=_clspath(cls),
old=_clspath(old, old_class_path),
new=_clspath(new_class, new_class_path))
if warn_once:
msg += ' (warning only on first subclass, there may be others)'
warnings.warn(msg, warn_category, stacklevel=2)
super(DeprecatedClass, cls).__init__(name, bases, clsdict_)
# see https://www.python.org/dev/peps/pep-3119/#overloading-isinstance-and-issubclass
# and https://docs.python.org/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 cls is not DeprecatedClass.deprecated_class:
# we should do the magic only if second `issubclass` argument
# is the deprecated class itself - subclasses of the
# deprecated class should not use custom `__subclasscheck__`
# method.
return super(DeprecatedClass, cls).__subclasscheck__(sub)
if not inspect.isclass(sub):
raise TypeError("issubclass() arg 1 must be a class")
mro = getattr(sub, '__mro__', ())
return any(c in {cls, new_class} for c in mro)
def __call__(cls, *args, **kwargs):
old = DeprecatedClass.deprecated_class
if cls is old:
msg = instance_warn_message.format(cls=_clspath(cls, old_class_path),
new=_clspath(new_class, new_class_path))
warnings.warn(msg, warn_category, stacklevel=2)
return super(DeprecatedClass, cls).__call__(*args, **kwargs)
deprecated_cls = DeprecatedClass(name, (new_class,), clsdict or {})
try:
frm = inspect.stack()[1]
parent_module = inspect.getmodule(frm[0])
if parent_module is not None:
deprecated_cls.__module__ = parent_module.__name__
except Exception as e:
# Sometimes inspect.stack() fails (e.g. when the first import of
# deprecated class is in jinja2 template). __module__ attribute is not
# important enough to raise an exception as users may be unable
# to fix inspect.stack() errors.
warnings.warn("Error detecting parent module: %r" % e)
return deprecated_cls
def _clspath(cls, forced=None):
if forced is not None:
return forced
return '{}.{}'.format(cls.__module__, cls.__name__)
DEPRECATION_RULES = [
('scrapy.contrib_exp.downloadermiddleware.decompression.', 'scrapy.downloadermiddlewares.decompression.'),
('scrapy.contrib_exp.iterators.', 'scrapy.utils.iterators.'),
('scrapy.contrib.downloadermiddleware.', 'scrapy.downloadermiddlewares.'),
('scrapy.contrib.exporter.', 'scrapy.exporters.'),
('scrapy.contrib.linkextractors.', 'scrapy.linkextractors.'),
('scrapy.contrib.loader.processor.', 'scrapy.loader.processors.'),
('scrapy.contrib.loader.', 'scrapy.loader.'),
('scrapy.contrib.pipeline.', 'scrapy.pipelines.'),
('scrapy.contrib.spidermiddleware.', 'scrapy.spidermiddlewares.'),
('scrapy.contrib.spiders.', 'scrapy.spiders.'),
('scrapy.contrib.', 'scrapy.extensions.'),
('scrapy.command.', 'scrapy.commands.'),
('scrapy.dupefilter.', 'scrapy.dupefilters.'),
('scrapy.linkextractor.', 'scrapy.linkextractors.'),
('scrapy.telnet.', 'scrapy.extensions.telnet.'),
('scrapy.spider.', 'scrapy.spiders.'),
('scrapy.squeue.', 'scrapy.squeues.'),
('scrapy.statscol.', 'scrapy.statscollectors.'),
('scrapy.utils.decorator.', 'scrapy.utils.decorators.'),
('scrapy.spidermanager.SpiderManager', 'scrapy.spiderloader.SpiderLoader'),
]
def update_classpath(path):
"""Update a deprecated path from an object with its new location"""
for prefix, replacement in DEPRECATION_RULES:
if path.startswith(prefix):
new_path = path.replace(prefix, replacement, 1)
warnings.warn("`{}` class is deprecated, use `{}` instead".format(path, new_path),
ScrapyDeprecationWarning)
return new_path
return path
def method_is_overridden(subclass, base_class, method_name):
"""
Return True if a method named ``method_name`` of a ``base_class``
is overridden in a ``subclass``.
>>> class Base(object):
... def foo(self):
... pass
>>> class Sub1(Base):
... pass
>>> class Sub2(Base):
... def foo(self):
... pass
>>> class Sub3(Sub1):
... def foo(self):
... pass
>>> class Sub4(Sub2):
... pass
>>> method_is_overridden(Sub1, Base, 'foo')
False
>>> method_is_overridden(Sub2, Base, 'foo')
True
>>> method_is_overridden(Sub3, Base, 'foo')
True
>>> method_is_overridden(Sub4, Base, 'foo')
True
"""
base_method = getattr(base_class, method_name)
sub_method = getattr(subclass, method_name)
return base_method.__code__ is not sub_method.__code__
|