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