File: deprecate.py

package info (click to toggle)
python-scrapy 1.5.1-1%2Bdeb10u1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 4,404 kB
  • sloc: python: 25,793; xml: 199; makefile: 95; sh: 33
file content (190 lines) | stat: -rw-r--r-- 7,866 bytes parent folder | download
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__