deprecate.py 6.6 KB

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