scrapy / scrapy
1
"""Some helpers for deprecation messages"""
2

3 7
import warnings
4 7
import inspect
5 7
from scrapy.exceptions import ScrapyDeprecationWarning
6

7

8 7
def attribute(obj, oldattr, newattr, version='0.12'):
9 0
    cname = obj.__class__.__name__
10 0
    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

17 7
def 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 7
    class DeprecatedClass(new_class.__class__):
54

55 7
        deprecated_class = None
56 7
        warned_on_subclass = False
57

58 7
        def __new__(metacls, name, bases, clsdict_):
59 7
            cls = super().__new__(metacls, name, bases, clsdict_)
60 7
            if metacls.deprecated_class is None:
61 7
                metacls.deprecated_class = cls
62 7
            return cls
63

64 7
        def __init__(cls, name, bases, clsdict_):
65 7
            meta = cls.__class__
66 7
            old = meta.deprecated_class
67 7
            if old in bases and not (warn_once and meta.warned_on_subclass):
68 7
                meta.warned_on_subclass = True
69 7
                msg = subclass_warn_message.format(cls=_clspath(cls),
70
                                                   old=_clspath(old, old_class_path),
71
                                                   new=_clspath(new_class, new_class_path))
72 7
                if warn_once:
73 7
                    msg += ' (warning only on first subclass, there may be others)'
74 7
                warnings.warn(msg, warn_category, stacklevel=2)
75 7
            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 7
        def __instancecheck__(cls, inst):
81 7
            return any(cls.__subclasscheck__(c)
82
                       for c in {type(inst), inst.__class__})
83

84 7
        def __subclasscheck__(cls, sub):
85 7
            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 7
                return super().__subclasscheck__(sub)
91

92 7
            if not inspect.isclass(sub):
93 7
                raise TypeError("issubclass() arg 1 must be a class")
94

95 7
            mro = getattr(sub, '__mro__', ())
96 7
            return any(c in {cls, new_class} for c in mro)
97

98 7
        def __call__(cls, *args, **kwargs):
99 7
            old = DeprecatedClass.deprecated_class
100 7
            if cls is old:
101 7
                msg = instance_warn_message.format(cls=_clspath(cls, old_class_path),
102
                                                   new=_clspath(new_class, new_class_path))
103 7
                warnings.warn(msg, warn_category, stacklevel=2)
104 7
            return super().__call__(*args, **kwargs)
105

106 7
    deprecated_cls = DeprecatedClass(name, (new_class,), clsdict or {})
107

108 7
    try:
109 7
        frm = inspect.stack()[1]
110 7
        parent_module = inspect.getmodule(frm[0])
111 7
        if parent_module is not None:
112 7
            deprecated_cls.__module__ = parent_module.__name__
113 7
    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 7
        warnings.warn(f"Error detecting parent module: {e!r}")
119

120 7
    return deprecated_cls
121

122

123 7
def _clspath(cls, forced=None):
124 7
    if forced is not None:
125 7
        return forced
126 7
    return f'{cls.__module__}.{cls.__name__}'
127

128

129 7
DEPRECATION_RULES = [
130
    ('scrapy.telnet.', 'scrapy.extensions.telnet.'),
131
]
132

133

134 7
def update_classpath(path):
135
    """Update a deprecated path from an object with its new location"""
136 7
    for prefix, replacement in DEPRECATION_RULES:
137 7
        if isinstance(path, str) and path.startswith(prefix):
138 7
            new_path = path.replace(prefix, replacement, 1)
139 7
            warnings.warn(f"`{path}` class is deprecated, use `{new_path}` instead",
140
                          ScrapyDeprecationWarning)
141 7
            return new_path
142 7
    return path
143

144

145 7
def 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 7
    base_method = getattr(base_class, method_name)
173 7
    sub_method = getattr(subclass, method_name)
174 7
    return base_method.__code__ is not sub_method.__code__

Read our documentation on viewing source code .

Loading