1 # -*- test-case-name: twisted.python.test.test_deprecate -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 Deprecation framework for Twisted.
8 To mark a method or function as being deprecated do this::
10 from twisted.python.versions import Version
11 from twisted.python.deprecate import deprecated
13 @deprecated(Version("Twisted", 8, 0, 0))
14 def badAPI(self, first, second):
20 The newly-decorated badAPI will issue a warning when called. It will also have
21 a deprecation notice appended to its docstring.
23 To mark module-level attributes as being deprecated you can use::
25 badAttribute = "someValue"
29 deprecatedModuleAttribute(
30 Version("Twisted", 8, 0, 0),
31 "Use goodAttribute instead.",
32 "your.full.module.name",
35 The deprecated attributes will issue a warning whenever they are accessed. If
36 the attributes being deprecated are in the same module as the
37 L{deprecatedModuleAttribute} call is being made from, the C{__name__} global
38 can be used as the C{moduleName} parameter.
42 @type DEPRECATION_WARNING_FORMAT: C{str}
43 @var DEPRECATION_WARNING_FORMAT: The default deprecation warning string format
44 to use when one is not provided by the user.
50 'getDeprecationWarningString',
53 'deprecatedModuleAttribute',
58 from warnings import warn, warn_explicit
59 from dis import findlinestarts
61 from twisted.python.versions import getVersionString
62 from twisted.python.util import mergeFunctionMetadata
66 DEPRECATION_WARNING_FORMAT = '%(fqpn)s was deprecated in %(version)s'
69 # Notionally, part of twisted.python.reflect, but defining it there causes a
70 # cyclic dependency between this module and that module. Define it here,
71 # instead, and let reflect import it to re-expose to the public.
72 def _fullyQualifiedName(obj):
74 Return the fully qualified name of a module, class, method or function.
75 Classes and functions need to be module level ones to be correctly
81 if inspect.isclass(obj) or inspect.isfunction(obj):
82 moduleName = obj.__module__
83 return "%s.%s" % (moduleName, name)
84 elif inspect.ismethod(obj):
85 className = _fullyQualifiedName(obj.im_class)
86 return "%s.%s" % (className, name)
88 # Try to keep it looking like something in twisted.python.reflect.
89 _fullyQualifiedName.__module__ = 'twisted.python.reflect'
90 _fullyQualifiedName.__name__ = 'fullyQualifiedName'
94 def getWarningMethod():
96 Return the warning method currently used to record deprecation warnings.
102 def setWarningMethod(newMethod):
104 Set the warning method to use to record deprecation warnings.
106 The callable should take message, category and stacklevel. The return
114 def _getDeprecationDocstring(version, replacement=None):
116 Generate an addition to a deprecated object's docstring that explains its
119 @param version: the version it was deprecated.
120 @type version: L{Version}
122 @param replacement: The replacement, if specified.
123 @type replacement: C{str} or callable
125 @return: a string like "Deprecated in Twisted 27.2.0; please use
126 twisted.timestream.tachyon.flux instead."
128 doc = "Deprecated in %s" % (getVersionString(version),)
130 doc = "%s; %s" % (doc, _getReplacementString(replacement))
135 def _getReplacementString(replacement):
137 Surround a replacement for a deprecated API with some polite text exhorting
138 the user to consider it as an alternative.
140 @type replacement: C{str} or callable
142 @return: a string like "please use twisted.python.modules.getModule
145 if callable(replacement):
146 replacement = _fullyQualifiedName(replacement)
147 return "please use %s instead" % (replacement,)
151 def _getDeprecationWarningString(fqpn, version, format=None, replacement=None):
153 Return a string indicating that the Python name was deprecated in the given
156 @param fqpn: Fully qualified Python name of the thing being deprecated
159 @param version: Version that C{fqpn} was deprecated in.
160 @type version: L{twisted.python.versions.Version}
162 @param format: A user-provided format to interpolate warning values into, or
163 L{DEPRECATION_WARNING_FORMAT
164 <twisted.python.deprecate.DEPRECATION_WARNING_FORMAT>} if C{None} is
168 @param replacement: what should be used in place of C{fqpn}. Either pass in
169 a string, which will be inserted into the warning message, or a
170 callable, which will be expanded to its full import path.
171 @type replacement: C{str} or callable
173 @return: A textual description of the deprecation
177 format = DEPRECATION_WARNING_FORMAT
178 warningString = format % {
180 'version': getVersionString(version)}
182 warningString = "%s; %s" % (
183 warningString, _getReplacementString(replacement))
188 def getDeprecationWarningString(callableThing, version, format=None,
191 Return a string indicating that the callable was deprecated in the given
194 @type callableThing: C{callable}
195 @param callableThing: Callable object to be deprecated
197 @type version: L{twisted.python.versions.Version}
198 @param version: Version that C{callableThing} was deprecated in
201 @param format: A user-provided format to interpolate warning values into,
202 or L{DEPRECATION_WARNING_FORMAT
203 <twisted.python.deprecate.DEPRECATION_WARNING_FORMAT>} if C{None} is
206 @param callableThing: A callable to be deprecated.
208 @param version: The L{twisted.python.versions.Version} that the callable
211 @param replacement: what should be used in place of the callable. Either
212 pass in a string, which will be inserted into the warning message,
213 or a callable, which will be expanded to its full import path.
214 @type replacement: C{str} or callable
216 @return: A string describing the deprecation.
219 return _getDeprecationWarningString(
220 _fullyQualifiedName(callableThing), version, format, replacement)
224 def deprecated(version, replacement=None):
226 Return a decorator that marks callables as deprecated.
228 @type version: L{twisted.python.versions.Version}
229 @param version: The version in which the callable will be marked as
230 having been deprecated. The decorated function will be annotated
231 with this version, having it set as its C{deprecatedVersion}
234 @param version: the version that the callable was deprecated in.
235 @type version: L{twisted.python.versions.Version}
237 @param replacement: what should be used in place of the callable. Either
238 pass in a string, which will be inserted into the warning message,
239 or a callable, which will be expanded to its full import path.
240 @type replacement: C{str} or callable
242 def deprecationDecorator(function):
244 Decorator that marks C{function} as deprecated.
246 warningString = getDeprecationWarningString(
247 function, version, None, replacement)
249 def deprecatedFunction(*args, **kwargs):
254 return function(*args, **kwargs)
256 deprecatedFunction = mergeFunctionMetadata(
257 function, deprecatedFunction)
258 _appendToDocstring(deprecatedFunction,
259 _getDeprecationDocstring(version, replacement))
260 deprecatedFunction.deprecatedVersion = version
261 return deprecatedFunction
263 return deprecationDecorator
267 def _appendToDocstring(thingWithDoc, textToAppend):
269 Append the given text to the docstring of C{thingWithDoc}.
271 If C{thingWithDoc} has no docstring, then the text just replaces the
272 docstring. If it has a single-line docstring then it appends a blank line
273 and the message text. If it has a multi-line docstring, then in appends a
274 blank line a the message text, and also does the indentation correctly.
276 if thingWithDoc.__doc__:
277 docstringLines = thingWithDoc.__doc__.splitlines()
281 if len(docstringLines) == 0:
282 docstringLines.append(textToAppend)
283 elif len(docstringLines) == 1:
284 docstringLines.extend(['', textToAppend, ''])
286 spaces = docstringLines.pop()
287 docstringLines.extend(['',
288 spaces + textToAppend,
290 thingWithDoc.__doc__ = '\n'.join(docstringLines)
294 class _InternalState(object):
296 An L{_InternalState} is a helper object for a L{_ModuleProxy}, so that it
297 can easily access its own attributes, bypassing its logic for delegating to
298 another object that it's proxying for.
300 @ivar proxy: a L{ModuleProxy}
302 def __init__(self, proxy):
303 object.__setattr__(self, 'proxy', proxy)
306 def __getattribute__(self, name):
307 return object.__getattribute__(object.__getattribute__(self, 'proxy'),
311 def __setattr__(self, name, value):
312 return object.__setattr__(object.__getattribute__(self, 'proxy'),
317 class _ModuleProxy(object):
319 Python module wrapper to hook module-level attribute access.
321 Access to deprecated attributes first checks
322 L{_ModuleProxy._deprecatedAttributes}, if the attribute does not appear
323 there then access falls through to L{_ModuleProxy._module}, the wrapped
326 @ivar _module: Module on which to hook attribute access.
327 @type _module: C{module}
329 @ivar _deprecatedAttributes: Mapping of attribute names to objects that
330 retrieve the module attribute's original value.
331 @type _deprecatedAttributes: C{dict} mapping C{str} to
332 L{_DeprecatedAttribute}
334 @ivar _lastWasPath: Heuristic guess as to whether warnings about this
335 package should be ignored for the next call. If the last attribute
336 access of this module was a C{getattr} of C{__path__}, we will assume
337 that it was the import system doing it and we won't emit a warning for
338 the next access, even if it is to a deprecated attribute. The CPython
339 import system always tries to access C{__path__}, then the attribute
340 itself, then the attribute itself again, in both successful and failed
342 @type _lastWasPath: C{bool}
344 def __init__(self, module):
345 state = _InternalState(self)
346 state._module = module
347 state._deprecatedAttributes = {}
348 state._lastWasPath = False
353 Get a string containing the type of the module proxy and a
354 representation of the wrapped module object.
356 state = _InternalState(self)
357 return '<%s module=%r>' % (type(self).__name__, state._module)
360 def __setattr__(self, name, value):
362 Set an attribute on the wrapped module object.
364 state = _InternalState(self)
365 state._lastWasPath = False
366 setattr(state._module, name, value)
369 def __getattribute__(self, name):
371 Get an attribute from the module object, possibly emitting a warning.
373 If the specified name has been deprecated, then a warning is issued.
374 (Unless certain obscure conditions are met; see
375 L{_ModuleProxy._lastWasPath} for more information about what might quash
378 state = _InternalState(self)
379 if state._lastWasPath:
380 deprecatedAttribute = None
382 deprecatedAttribute = state._deprecatedAttributes.get(name)
384 if deprecatedAttribute is not None:
385 # If we have a _DeprecatedAttribute object from the earlier lookup,
386 # allow it to issue the warning.
387 value = deprecatedAttribute.get()
389 # Otherwise, just retrieve the underlying value directly; it's not
390 # deprecated, there's no warning to issue.
391 value = getattr(state._module, name)
392 if name == '__path__':
393 state._lastWasPath = True
395 state._lastWasPath = False
400 class _DeprecatedAttribute(object):
402 Wrapper for deprecated attributes.
404 This is intended to be used by L{_ModuleProxy}. Calling
405 L{_DeprecatedAttribute.get} will issue a warning and retrieve the
406 underlying attribute's value.
408 @type module: C{module}
409 @ivar module: The original module instance containing this attribute
412 @ivar fqpn: Fully qualified Python name for the deprecated attribute
414 @type version: L{twisted.python.versions.Version}
415 @ivar version: Version that the attribute was deprecated in
417 @type message: C{str}
418 @ivar message: Deprecation message
420 def __init__(self, module, name, version, message):
422 Initialise a deprecated name wrapper.
426 self.fqpn = module.__name__ + '.' + name
427 self.version = version
428 self.message = message
433 Get the underlying attribute value and issue a deprecation warning.
435 # This might fail if the deprecated thing is a module inside a package.
436 # In that case, don't emit the warning this time. The import system
437 # will come back again when it's not an AttributeError and we can emit
439 result = getattr(self.module, self.__name__)
440 message = _getDeprecationWarningString(self.fqpn, self.version,
441 DEPRECATION_WARNING_FORMAT + ': ' + self.message)
442 warn(message, DeprecationWarning, stacklevel=3)
447 def _deprecateAttribute(proxy, name, version, message):
449 Mark a module-level attribute as being deprecated.
451 @type proxy: L{_ModuleProxy}
452 @param proxy: The module proxy instance proxying the deprecated attributes
455 @param name: Attribute name
457 @type version: L{twisted.python.versions.Version}
458 @param version: Version that the attribute was deprecated in
460 @type message: C{str}
461 @param message: Deprecation message
463 _module = object.__getattribute__(proxy, '_module')
464 attr = _DeprecatedAttribute(_module, name, version, message)
465 # Add a deprecated attribute marker for this module's attribute. When this
466 # attribute is accessed via _ModuleProxy a warning is emitted.
467 _deprecatedAttributes = object.__getattribute__(
468 proxy, '_deprecatedAttributes')
469 _deprecatedAttributes[name] = attr
473 def deprecatedModuleAttribute(version, message, moduleName, name):
475 Declare a module-level attribute as being deprecated.
477 @type version: L{twisted.python.versions.Version}
478 @param version: Version that the attribute was deprecated in
480 @type message: C{str}
481 @param message: Deprecation message
483 @type moduleName: C{str}
484 @param moduleName: Fully-qualified Python name of the module containing
485 the deprecated attribute; if called from the same module as the
486 attributes are being deprecated in, using the C{__name__} global can
490 @param name: Attribute name to deprecate
492 module = sys.modules[moduleName]
493 if not isinstance(module, _ModuleProxy):
494 module = _ModuleProxy(module)
495 sys.modules[moduleName] = module
497 _deprecateAttribute(module, name, version, message)
500 def warnAboutFunction(offender, warningString):
502 Issue a warning string, identifying C{offender} as the responsible code.
504 This function is used to deprecate some behavior of a function. It differs
505 from L{warnings.warn} in that it is not limited to deprecating the behavior
506 of a function currently on the call stack.
508 @param function: The function that is being deprecated.
510 @param warningString: The string that should be emitted by this warning.
511 @type warningString: C{str}
515 # inspect.getmodule() is attractive, but somewhat
516 # broken in Python < 2.6. See Python bug 4845.
517 offenderModule = sys.modules[offender.__module__]
518 filename = inspect.getabsfile(offenderModule)
519 lineStarts = list(findlinestarts(offender.func_code))
520 lastLineNo = lineStarts[-1][1]
521 globals = offender.func_globals
524 category=DeprecationWarning,
527 module=offenderModule.__name__,
528 registry=globals.setdefault("__warningregistry__", {}),
531 if sys.version_info[:2] < (2, 5):
532 kwargs.pop('module_globals')
534 warn_explicit(warningString, **kwargs)