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