123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797 |
- # -*- test-case-name: twisted.python.test.test_deprecate -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- Deprecation framework for Twisted.
- To mark a method, function, or class as being deprecated do this::
- from incremental import Version
- from twisted.python.deprecate import deprecated
- @deprecated(Version("Twisted", 8, 0, 0))
- def badAPI(self, first, second):
- '''
- Docstring for badAPI.
- '''
- ...
- @deprecated(Version("Twisted", 16, 0, 0))
- class BadClass(object):
- '''
- Docstring for BadClass.
- '''
- The newly-decorated badAPI will issue a warning when called, and BadClass will
- issue a warning when instantiated. Both will also have a deprecation notice
- appended to their docstring.
- To deprecate properties you can use::
- from incremental import Version
- from twisted.python.deprecate import deprecatedProperty
- class OtherwiseUndeprecatedClass(object):
- @deprecatedProperty(Version('Twisted', 16, 0, 0))
- def badProperty(self):
- '''
- Docstring for badProperty.
- '''
- @badProperty.setter
- def badProperty(self, value):
- '''
- Setter sill also raise the deprecation warning.
- '''
- To mark module-level attributes as being deprecated you can use::
- badAttribute = "someValue"
- ...
- deprecatedModuleAttribute(
- Version("Twisted", 8, 0, 0),
- "Use goodAttribute instead.",
- "your.full.module.name",
- "badAttribute")
- The deprecated attributes will issue a warning whenever they are accessed. If
- the attributes being deprecated are in the same module as the
- L{deprecatedModuleAttribute} call is being made from, the C{__name__} global
- can be used as the C{moduleName} parameter.
- See also L{incremental.Version}.
- @type DEPRECATION_WARNING_FORMAT: C{str}
- @var DEPRECATION_WARNING_FORMAT: The default deprecation warning string format
- to use when one is not provided by the user.
- """
- from __future__ import division, absolute_import
- __all__ = [
- 'deprecated',
- 'deprecatedProperty',
- 'getDeprecationWarningString',
- 'getWarningMethod',
- 'setWarningMethod',
- 'deprecatedModuleAttribute',
- ]
- import sys, inspect
- from warnings import warn, warn_explicit
- from dis import findlinestarts
- from functools import wraps
- from incremental import getVersionString
- from twisted.python.compat import _PY3
- DEPRECATION_WARNING_FORMAT = '%(fqpn)s was deprecated in %(version)s'
- # Notionally, part of twisted.python.reflect, but defining it there causes a
- # cyclic dependency between this module and that module. Define it here,
- # instead, and let reflect import it to re-expose to the public.
- def _fullyQualifiedName(obj):
- """
- Return the fully qualified name of a module, class, method or function.
- Classes and functions need to be module level ones to be correctly
- qualified.
- @rtype: C{str}.
- """
- try:
- name = obj.__qualname__
- except AttributeError:
- name = obj.__name__
- if inspect.isclass(obj) or inspect.isfunction(obj):
- moduleName = obj.__module__
- return "%s.%s" % (moduleName, name)
- elif inspect.ismethod(obj):
- try:
- cls = obj.im_class
- except AttributeError:
- # Python 3 eliminates im_class, substitutes __module__ and
- # __qualname__ to provide similar information.
- return "%s.%s" % (obj.__module__, obj.__qualname__)
- else:
- className = _fullyQualifiedName(cls)
- return "%s.%s" % (className, name)
- return name
- # Try to keep it looking like something in twisted.python.reflect.
- _fullyQualifiedName.__module__ = 'twisted.python.reflect'
- _fullyQualifiedName.__name__ = 'fullyQualifiedName'
- _fullyQualifiedName.__qualname__ = 'fullyQualifiedName'
- def _getReplacementString(replacement):
- """
- Surround a replacement for a deprecated API with some polite text exhorting
- the user to consider it as an alternative.
- @type replacement: C{str} or callable
- @return: a string like "please use twisted.python.modules.getModule
- instead".
- """
- if callable(replacement):
- replacement = _fullyQualifiedName(replacement)
- return "please use %s instead" % (replacement,)
- def _getDeprecationDocstring(version, replacement=None):
- """
- Generate an addition to a deprecated object's docstring that explains its
- deprecation.
- @param version: the version it was deprecated.
- @type version: L{incremental.Version}
- @param replacement: The replacement, if specified.
- @type replacement: C{str} or callable
- @return: a string like "Deprecated in Twisted 27.2.0; please use
- twisted.timestream.tachyon.flux instead."
- """
- doc = "Deprecated in %s" % (getVersionString(version),)
- if replacement:
- doc = "%s; %s" % (doc, _getReplacementString(replacement))
- return doc + "."
- def _getDeprecationWarningString(fqpn, version, format=None, replacement=None):
- """
- Return a string indicating that the Python name was deprecated in the given
- version.
- @param fqpn: Fully qualified Python name of the thing being deprecated
- @type fqpn: C{str}
- @param version: Version that C{fqpn} was deprecated in.
- @type version: L{incremental.Version}
- @param format: A user-provided format to interpolate warning values into, or
- L{DEPRECATION_WARNING_FORMAT
- <twisted.python.deprecate.DEPRECATION_WARNING_FORMAT>} if L{None} is
- given.
- @type format: C{str}
- @param replacement: what should be used in place of C{fqpn}. Either pass in
- a string, which will be inserted into the warning message, or a
- callable, which will be expanded to its full import path.
- @type replacement: C{str} or callable
- @return: A textual description of the deprecation
- @rtype: C{str}
- """
- if format is None:
- format = DEPRECATION_WARNING_FORMAT
- warningString = format % {
- 'fqpn': fqpn,
- 'version': getVersionString(version)}
- if replacement:
- warningString = "%s; %s" % (
- warningString, _getReplacementString(replacement))
- return warningString
- def getDeprecationWarningString(callableThing, version, format=None,
- replacement=None):
- """
- Return a string indicating that the callable was deprecated in the given
- version.
- @type callableThing: C{callable}
- @param callableThing: Callable object to be deprecated
- @type version: L{incremental.Version}
- @param version: Version that C{callableThing} was deprecated in
- @type format: C{str}
- @param format: A user-provided format to interpolate warning values into,
- or L{DEPRECATION_WARNING_FORMAT
- <twisted.python.deprecate.DEPRECATION_WARNING_FORMAT>} if L{None} is
- given
- @param callableThing: A callable to be deprecated.
- @param version: The L{incremental.Version} that the callable
- was deprecated in.
- @param replacement: what should be used in place of the callable. Either
- pass in a string, which will be inserted into the warning message,
- or a callable, which will be expanded to its full import path.
- @type replacement: C{str} or callable
- @return: A string describing the deprecation.
- @rtype: C{str}
- """
- return _getDeprecationWarningString(
- _fullyQualifiedName(callableThing), version, format, replacement)
- def _appendToDocstring(thingWithDoc, textToAppend):
- """
- Append the given text to the docstring of C{thingWithDoc}.
- If C{thingWithDoc} has no docstring, then the text just replaces the
- docstring. If it has a single-line docstring then it appends a blank line
- and the message text. If it has a multi-line docstring, then in appends a
- blank line a the message text, and also does the indentation correctly.
- """
- if thingWithDoc.__doc__:
- docstringLines = thingWithDoc.__doc__.splitlines()
- else:
- docstringLines = []
- if len(docstringLines) == 0:
- docstringLines.append(textToAppend)
- elif len(docstringLines) == 1:
- docstringLines.extend(['', textToAppend, ''])
- else:
- spaces = docstringLines.pop()
- docstringLines.extend(['',
- spaces + textToAppend,
- spaces])
- thingWithDoc.__doc__ = '\n'.join(docstringLines)
- def deprecated(version, replacement=None):
- """
- Return a decorator that marks callables as deprecated. To deprecate a
- property, see L{deprecatedProperty}.
- @type version: L{incremental.Version}
- @param version: The version in which the callable will be marked as
- having been deprecated. The decorated function will be annotated
- with this version, having it set as its C{deprecatedVersion}
- attribute.
- @param version: the version that the callable was deprecated in.
- @type version: L{incremental.Version}
- @param replacement: what should be used in place of the callable. Either
- pass in a string, which will be inserted into the warning message,
- or a callable, which will be expanded to its full import path.
- @type replacement: C{str} or callable
- """
- def deprecationDecorator(function):
- """
- Decorator that marks C{function} as deprecated.
- """
- warningString = getDeprecationWarningString(
- function, version, None, replacement)
- @wraps(function)
- def deprecatedFunction(*args, **kwargs):
- warn(
- warningString,
- DeprecationWarning,
- stacklevel=2)
- return function(*args, **kwargs)
- _appendToDocstring(deprecatedFunction,
- _getDeprecationDocstring(version, replacement))
- deprecatedFunction.deprecatedVersion = version
- return deprecatedFunction
- return deprecationDecorator
- def deprecatedProperty(version, replacement=None):
- """
- Return a decorator that marks a property as deprecated. To deprecate a
- regular callable or class, see L{deprecated}.
- @type version: L{incremental.Version}
- @param version: The version in which the callable will be marked as
- having been deprecated. The decorated function will be annotated
- with this version, having it set as its C{deprecatedVersion}
- attribute.
- @param version: the version that the callable was deprecated in.
- @type version: L{incremental.Version}
- @param replacement: what should be used in place of the callable.
- Either pass in a string, which will be inserted into the warning
- message, or a callable, which will be expanded to its full import
- path.
- @type replacement: C{str} or callable
- @return: A new property with deprecated setter and getter.
- @rtype: C{property}
- @since: 16.1.0
- """
- class _DeprecatedProperty(property):
- """
- Extension of the build-in property to allow deprecated setters.
- """
- def _deprecatedWrapper(self, function):
- @wraps(function)
- def deprecatedFunction(*args, **kwargs):
- warn(
- self.warningString,
- DeprecationWarning,
- stacklevel=2)
- return function(*args, **kwargs)
- return deprecatedFunction
- def setter(self, function):
- return property.setter(self, self._deprecatedWrapper(function))
- def deprecationDecorator(function):
- if _PY3:
- warningString = getDeprecationWarningString(
- function, version, None, replacement)
- else:
- # Because Python 2 sucks, we need to implement our own here -- lack
- # of __qualname__ means that we kinda have to stack walk. It maybe
- # probably works. Probably. -Amber
- functionName = function.__name__
- className = inspect.stack()[1][3] # wow hax
- moduleName = function.__module__
- fqdn = "%s.%s.%s" % (moduleName, className, functionName)
- warningString = _getDeprecationWarningString(
- fqdn, version, None, replacement)
- @wraps(function)
- def deprecatedFunction(*args, **kwargs):
- warn(
- warningString,
- DeprecationWarning,
- stacklevel=2)
- return function(*args, **kwargs)
- _appendToDocstring(deprecatedFunction,
- _getDeprecationDocstring(version, replacement))
- deprecatedFunction.deprecatedVersion = version
- result = _DeprecatedProperty(deprecatedFunction)
- result.warningString = warningString
- return result
- return deprecationDecorator
- def getWarningMethod():
- """
- Return the warning method currently used to record deprecation warnings.
- """
- return warn
- def setWarningMethod(newMethod):
- """
- Set the warning method to use to record deprecation warnings.
- The callable should take message, category and stacklevel. The return
- value is ignored.
- """
- global warn
- warn = newMethod
- class _InternalState(object):
- """
- An L{_InternalState} is a helper object for a L{_ModuleProxy}, so that it
- can easily access its own attributes, bypassing its logic for delegating to
- another object that it's proxying for.
- @ivar proxy: a L{_ModuleProxy}
- """
- def __init__(self, proxy):
- object.__setattr__(self, 'proxy', proxy)
- def __getattribute__(self, name):
- return object.__getattribute__(object.__getattribute__(self, 'proxy'),
- name)
- def __setattr__(self, name, value):
- return object.__setattr__(object.__getattribute__(self, 'proxy'),
- name, value)
- class _ModuleProxy(object):
- """
- Python module wrapper to hook module-level attribute access.
- Access to deprecated attributes first checks
- L{_ModuleProxy._deprecatedAttributes}, if the attribute does not appear
- there then access falls through to L{_ModuleProxy._module}, the wrapped
- module object.
- @ivar _module: Module on which to hook attribute access.
- @type _module: C{module}
- @ivar _deprecatedAttributes: Mapping of attribute names to objects that
- retrieve the module attribute's original value.
- @type _deprecatedAttributes: C{dict} mapping C{str} to
- L{_DeprecatedAttribute}
- @ivar _lastWasPath: Heuristic guess as to whether warnings about this
- package should be ignored for the next call. If the last attribute
- access of this module was a C{getattr} of C{__path__}, we will assume
- that it was the import system doing it and we won't emit a warning for
- the next access, even if it is to a deprecated attribute. The CPython
- import system always tries to access C{__path__}, then the attribute
- itself, then the attribute itself again, in both successful and failed
- cases.
- @type _lastWasPath: C{bool}
- """
- def __init__(self, module):
- state = _InternalState(self)
- state._module = module
- state._deprecatedAttributes = {}
- state._lastWasPath = False
- def __repr__(self):
- """
- Get a string containing the type of the module proxy and a
- representation of the wrapped module object.
- """
- state = _InternalState(self)
- return '<%s module=%r>' % (type(self).__name__, state._module)
- def __setattr__(self, name, value):
- """
- Set an attribute on the wrapped module object.
- """
- state = _InternalState(self)
- state._lastWasPath = False
- setattr(state._module, name, value)
- def __getattribute__(self, name):
- """
- Get an attribute from the module object, possibly emitting a warning.
- If the specified name has been deprecated, then a warning is issued.
- (Unless certain obscure conditions are met; see
- L{_ModuleProxy._lastWasPath} for more information about what might quash
- such a warning.)
- """
- state = _InternalState(self)
- if state._lastWasPath:
- deprecatedAttribute = None
- else:
- deprecatedAttribute = state._deprecatedAttributes.get(name)
- if deprecatedAttribute is not None:
- # If we have a _DeprecatedAttribute object from the earlier lookup,
- # allow it to issue the warning.
- value = deprecatedAttribute.get()
- else:
- # Otherwise, just retrieve the underlying value directly; it's not
- # deprecated, there's no warning to issue.
- value = getattr(state._module, name)
- if name == '__path__':
- state._lastWasPath = True
- else:
- state._lastWasPath = False
- return value
- class _DeprecatedAttribute(object):
- """
- Wrapper for deprecated attributes.
- This is intended to be used by L{_ModuleProxy}. Calling
- L{_DeprecatedAttribute.get} will issue a warning and retrieve the
- underlying attribute's value.
- @type module: C{module}
- @ivar module: The original module instance containing this attribute
- @type fqpn: C{str}
- @ivar fqpn: Fully qualified Python name for the deprecated attribute
- @type version: L{incremental.Version}
- @ivar version: Version that the attribute was deprecated in
- @type message: C{str}
- @ivar message: Deprecation message
- """
- def __init__(self, module, name, version, message):
- """
- Initialise a deprecated name wrapper.
- """
- self.module = module
- self.__name__ = name
- self.fqpn = module.__name__ + '.' + name
- self.version = version
- self.message = message
- def get(self):
- """
- Get the underlying attribute value and issue a deprecation warning.
- """
- # This might fail if the deprecated thing is a module inside a package.
- # In that case, don't emit the warning this time. The import system
- # will come back again when it's not an AttributeError and we can emit
- # the warning then.
- result = getattr(self.module, self.__name__)
- message = _getDeprecationWarningString(self.fqpn, self.version,
- DEPRECATION_WARNING_FORMAT + ': ' + self.message)
- warn(message, DeprecationWarning, stacklevel=3)
- return result
- def _deprecateAttribute(proxy, name, version, message):
- """
- Mark a module-level attribute as being deprecated.
- @type proxy: L{_ModuleProxy}
- @param proxy: The module proxy instance proxying the deprecated attributes
- @type name: C{str}
- @param name: Attribute name
- @type version: L{incremental.Version}
- @param version: Version that the attribute was deprecated in
- @type message: C{str}
- @param message: Deprecation message
- """
- _module = object.__getattribute__(proxy, '_module')
- attr = _DeprecatedAttribute(_module, name, version, message)
- # Add a deprecated attribute marker for this module's attribute. When this
- # attribute is accessed via _ModuleProxy a warning is emitted.
- _deprecatedAttributes = object.__getattribute__(
- proxy, '_deprecatedAttributes')
- _deprecatedAttributes[name] = attr
- def deprecatedModuleAttribute(version, message, moduleName, name):
- """
- Declare a module-level attribute as being deprecated.
- @type version: L{incremental.Version}
- @param version: Version that the attribute was deprecated in
- @type message: C{str}
- @param message: Deprecation message
- @type moduleName: C{str}
- @param moduleName: Fully-qualified Python name of the module containing
- the deprecated attribute; if called from the same module as the
- attributes are being deprecated in, using the C{__name__} global can
- be helpful
- @type name: C{str}
- @param name: Attribute name to deprecate
- """
- module = sys.modules[moduleName]
- if not isinstance(module, _ModuleProxy):
- module = _ModuleProxy(module)
- sys.modules[moduleName] = module
- _deprecateAttribute(module, name, version, message)
- def warnAboutFunction(offender, warningString):
- """
- Issue a warning string, identifying C{offender} as the responsible code.
- This function is used to deprecate some behavior of a function. It differs
- from L{warnings.warn} in that it is not limited to deprecating the behavior
- of a function currently on the call stack.
- @param function: The function that is being deprecated.
- @param warningString: The string that should be emitted by this warning.
- @type warningString: C{str}
- @since: 11.0
- """
- # inspect.getmodule() is attractive, but somewhat
- # broken in Python < 2.6. See Python bug 4845.
- offenderModule = sys.modules[offender.__module__]
- filename = inspect.getabsfile(offenderModule)
- lineStarts = list(findlinestarts(offender.__code__))
- lastLineNo = lineStarts[-1][1]
- globals = offender.__globals__
- kwargs = dict(
- category=DeprecationWarning,
- filename=filename,
- lineno=lastLineNo,
- module=offenderModule.__name__,
- registry=globals.setdefault("__warningregistry__", {}),
- module_globals=None)
- warn_explicit(warningString, **kwargs)
- def _passedArgSpec(argspec, positional, keyword):
- """
- Take an I{inspect.ArgSpec}, a tuple of positional arguments, and a dict of
- keyword arguments, and return a mapping of arguments that were actually
- passed to their passed values.
- @param argspec: The argument specification for the function to inspect.
- @type argspec: I{inspect.ArgSpec}
- @param positional: The positional arguments that were passed.
- @type positional: L{tuple}
- @param keyword: The keyword arguments that were passed.
- @type keyword: L{dict}
- @return: A dictionary mapping argument names (those declared in C{argspec})
- to values that were passed explicitly by the user.
- @rtype: L{dict} mapping L{str} to L{object}
- """
- result = {}
- unpassed = len(argspec.args) - len(positional)
- if argspec.keywords is not None:
- kwargs = result[argspec.keywords] = {}
- if unpassed < 0:
- if argspec.varargs is None:
- raise TypeError("Too many arguments.")
- else:
- result[argspec.varargs] = positional[len(argspec.args):]
- for name, value in zip(argspec.args, positional):
- result[name] = value
- for name, value in keyword.items():
- if name in argspec.args:
- if name in result:
- raise TypeError("Already passed.")
- result[name] = value
- elif argspec.keywords is not None:
- kwargs[name] = value
- else:
- raise TypeError("no such param")
- return result
- def _passedSignature(signature, positional, keyword):
- """
- Take an L{inspect.Signature}, a tuple of positional arguments, and a dict of
- keyword arguments, and return a mapping of arguments that were actually
- passed to their passed values.
- @param signature: The signature of the function to inspect.
- @type signature: L{inspect.Signature}
- @param positional: The positional arguments that were passed.
- @type positional: L{tuple}
- @param keyword: The keyword arguments that were passed.
- @type keyword: L{dict}
- @return: A dictionary mapping argument names (those declared in
- C{signature}) to values that were passed explicitly by the user.
- @rtype: L{dict} mapping L{str} to L{object}
- """
- result = {}
- kwargs = None
- numPositional = 0
- for (n, (name, param)) in enumerate(signature.parameters.items()):
- if param.kind == inspect.Parameter.VAR_POSITIONAL:
- # Varargs, for example: *args
- result[name] = positional[n:]
- numPositional = len(result[name]) + 1
- elif param.kind == inspect.Parameter.VAR_KEYWORD:
- # Variable keyword args, for example: **my_kwargs
- kwargs = result[name] = {}
- elif param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD,
- inspect.Parameter.POSITIONAL_ONLY):
- if n < len(positional):
- result[name] = positional[n]
- numPositional += 1
- elif param.kind == inspect.Parameter.KEYWORD_ONLY:
- if name not in keyword:
- if param.default == inspect.Parameter.empty:
- raise TypeError("missing keyword arg {}".format(name))
- else:
- result[name] = param.default
- else:
- raise TypeError("'{}' parameter is invalid kind: {}".format(
- name, param.kind))
- if len(positional) > numPositional:
- raise TypeError("Too many arguments.")
- for name, value in keyword.items():
- if name in signature.parameters.keys():
- if name in result:
- raise TypeError("Already passed.")
- result[name] = value
- elif kwargs is not None:
- kwargs[name] = value
- else:
- raise TypeError("no such param")
- return result
- def _mutuallyExclusiveArguments(argumentPairs):
- """
- Decorator which causes its decoratee to raise a L{TypeError} if two of the
- given arguments are passed at the same time.
- @param argumentPairs: pairs of argument identifiers, each pair indicating
- an argument that may not be passed in conjunction with another.
- @type argumentPairs: sequence of 2-sequences of L{str}
- @return: A decorator, used like so::
- @_mutuallyExclusiveArguments([["tweedledum", "tweedledee"]])
- def function(tweedledum=1, tweedledee=2):
- "Don't pass tweedledum and tweedledee at the same time."
- @rtype: 1-argument callable taking a callable and returning a callable.
- """
- def wrapper(wrappee):
- if getattr(inspect, "signature", None):
- # Python 3
- spec = inspect.signature(wrappee)
- _passed = _passedSignature
- else:
- # Python 2
- spec = inspect.getargspec(wrappee)
- _passed = _passedArgSpec
- @wraps(wrappee)
- def wrapped(*args, **kwargs):
- arguments = _passed(spec, args, kwargs)
- for this, that in argumentPairs:
- if this in arguments and that in arguments:
- raise TypeError(
- ("The %r and %r arguments to %s "
- "are mutually exclusive.") %
- (this, that, _fullyQualifiedName(wrappee)))
- return wrappee(*args, **kwargs)
- return wrapped
- return wrapper
|