834 lines
28 KiB
Python
834 lines
28 KiB
Python
![]() |
# -*- 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", 22, 10, 0))
|
||
|
def badAPI(self, first, second):
|
||
|
'''
|
||
|
Docstring for badAPI.
|
||
|
'''
|
||
|
...
|
||
|
|
||
|
@deprecated(Version("Twisted", 22, 10, 0))
|
||
|
class BadClass:
|
||
|
'''
|
||
|
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:
|
||
|
|
||
|
@deprecatedProperty(Version("Twisted", 22, 10, 0))
|
||
|
def badProperty(self):
|
||
|
'''
|
||
|
Docstring for badProperty.
|
||
|
'''
|
||
|
|
||
|
@badProperty.setter
|
||
|
def badProperty(self, value):
|
||
|
'''
|
||
|
Setter sill also raise the deprecation warning.
|
||
|
'''
|
||
|
|
||
|
|
||
|
While it's best to avoid this as it adds performance overhead to *any* usage of
|
||
|
the module, to mark module-level attributes as being deprecated you can use::
|
||
|
|
||
|
badAttribute = "someValue"
|
||
|
|
||
|
...
|
||
|
|
||
|
deprecatedModuleAttribute(
|
||
|
Version("Twisted", 22, 10, 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.
|
||
|
|
||
|
|
||
|
To mark an optional, keyword parameter of a function or method as deprecated
|
||
|
without deprecating the function itself, you can use::
|
||
|
|
||
|
@deprecatedKeywordParameter(Version("Twisted", 22, 10, 0), "baz")
|
||
|
def someFunction(foo, bar=0, baz=None):
|
||
|
...
|
||
|
|
||
|
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 annotations
|
||
|
|
||
|
__all__ = [
|
||
|
"deprecated",
|
||
|
"deprecatedProperty",
|
||
|
"getDeprecationWarningString",
|
||
|
"getWarningMethod",
|
||
|
"setWarningMethod",
|
||
|
"deprecatedModuleAttribute",
|
||
|
"deprecatedKeywordParameter",
|
||
|
]
|
||
|
|
||
|
|
||
|
import inspect
|
||
|
import sys
|
||
|
from dis import findlinestarts
|
||
|
from functools import wraps
|
||
|
from types import ModuleType
|
||
|
from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
||
|
from warnings import warn, warn_explicit
|
||
|
|
||
|
from incremental import Version, getVersionString
|
||
|
from typing_extensions import ParamSpec
|
||
|
|
||
|
_P = ParamSpec("_P")
|
||
|
_R = TypeVar("_R")
|
||
|
|
||
|
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 f"{moduleName}.{name}"
|
||
|
elif inspect.ismethod(obj):
|
||
|
return f"{obj.__module__}.{obj.__qualname__}"
|
||
|
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 f"please use {replacement} instead"
|
||
|
|
||
|
|
||
|
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 = f"Deprecated in {getVersionString(version)}"
|
||
|
if replacement:
|
||
|
doc = f"{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 = "{}; {}".format(
|
||
|
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 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:
|
||
|
trailer = docstringLines[-1]
|
||
|
spaces = ""
|
||
|
if not trailer.strip():
|
||
|
# Deal with differences between Python 3.13 and older versions.
|
||
|
spaces = docstringLines.pop()
|
||
|
docstringLines.extend(["", spaces + textToAppend, spaces])
|
||
|
docstringLines = [l.lstrip(" ") for l in docstringLines]
|
||
|
thingWithDoc.__doc__ = "\n".join(docstringLines)
|
||
|
|
||
|
|
||
|
def deprecated(
|
||
|
version: Version, replacement: str | Callable[..., object] | None = None
|
||
|
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
||
|
"""
|
||
|
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 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: Callable[_P, _R]) -> Callable[_P, _R]:
|
||
|
"""
|
||
|
Decorator that marks C{function} as deprecated.
|
||
|
"""
|
||
|
warningString = getDeprecationWarningString(
|
||
|
function, version, None, replacement
|
||
|
)
|
||
|
|
||
|
@wraps(function)
|
||
|
def deprecatedFunction(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||
|
warn(warningString, DeprecationWarning, stacklevel=2)
|
||
|
return function(*args, **kwargs)
|
||
|
|
||
|
_appendToDocstring(
|
||
|
deprecatedFunction, _getDeprecationDocstring(version, replacement)
|
||
|
)
|
||
|
deprecatedFunction.deprecatedVersion = version # type: ignore[attr-defined]
|
||
|
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 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, # type: ignore[attr-defined]
|
||
|
DeprecationWarning,
|
||
|
stacklevel=2,
|
||
|
)
|
||
|
return function(*args, **kwargs)
|
||
|
|
||
|
return deprecatedFunction
|
||
|
|
||
|
def setter(self, function):
|
||
|
return property.setter(self, self._deprecatedWrapper(function))
|
||
|
|
||
|
def deprecationDecorator(function):
|
||
|
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 # type: ignore[attr-defined]
|
||
|
|
||
|
result = _DeprecatedProperty(deprecatedFunction)
|
||
|
result.warningString = warningString # type: ignore[attr-defined]
|
||
|
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:
|
||
|
"""
|
||
|
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:
|
||
|
"""
|
||
|
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) -> str:
|
||
|
"""
|
||
|
Get a string containing the type of the module proxy and a
|
||
|
representation of the wrapped module object.
|
||
|
"""
|
||
|
state = _InternalState(self)
|
||
|
return f"<{type(self).__name__} module={state._module!r}>"
|
||
|
|
||
|
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:
|
||
|
"""
|
||
|
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 = cast(ModuleType, _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 offender: 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.
|
||
|
# In Python 3.13 line numbers returned by findlinestarts
|
||
|
# can be None for bytecode that does not map to source
|
||
|
# lines.
|
||
|
offenderModule = sys.modules[offender.__module__]
|
||
|
warn_explicit(
|
||
|
warningString,
|
||
|
category=DeprecationWarning,
|
||
|
filename=inspect.getabsfile(offenderModule),
|
||
|
lineno=max(
|
||
|
lineNumber
|
||
|
for _, lineNumber in findlinestarts(offender.__code__)
|
||
|
if lineNumber is not None
|
||
|
),
|
||
|
module=offenderModule.__name__,
|
||
|
registry=offender.__globals__.setdefault("__warningregistry__", {}),
|
||
|
module_globals=None,
|
||
|
)
|
||
|
|
||
|
|
||
|
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: Dict[str, object] = {}
|
||
|
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(f"missing keyword arg {name}")
|
||
|
else:
|
||
|
result[name] = param.default
|
||
|
else:
|
||
|
raise TypeError(f"'{name}' parameter is invalid kind: {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):
|
||
|
spec = inspect.signature(wrappee)
|
||
|
_passed = _passedSignature
|
||
|
|
||
|
@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
|
||
|
|
||
|
|
||
|
_Tc = TypeVar("_Tc", bound=Callable[..., Any])
|
||
|
|
||
|
|
||
|
def deprecatedKeywordParameter(
|
||
|
version: Version, name: str, replacement: Optional[str] = None
|
||
|
) -> Callable[[_Tc], _Tc]:
|
||
|
"""
|
||
|
Return a decorator that marks a keyword parameter of a callable
|
||
|
as deprecated. A warning will be emitted if a caller supplies
|
||
|
a value for the parameter, whether the caller uses a keyword or
|
||
|
positional syntax.
|
||
|
|
||
|
@type version: L{incremental.Version}
|
||
|
@param version: The version in which the parameter will be marked as
|
||
|
having been deprecated.
|
||
|
|
||
|
@type name: L{str}
|
||
|
@param name: The name of the deprecated parameter.
|
||
|
|
||
|
@type replacement: L{str}
|
||
|
@param replacement: Optional text indicating what should be used in
|
||
|
place of the deprecated parameter.
|
||
|
|
||
|
@since: Twisted 21.2.0
|
||
|
"""
|
||
|
|
||
|
def wrapper(wrappee: _Tc) -> _Tc:
|
||
|
warningString = _getDeprecationWarningString(
|
||
|
f"The {name!r} parameter to {_fullyQualifiedName(wrappee)}",
|
||
|
version,
|
||
|
replacement=replacement,
|
||
|
)
|
||
|
|
||
|
doc = "The {!r} parameter was deprecated in {}".format(
|
||
|
name,
|
||
|
getVersionString(version),
|
||
|
)
|
||
|
if replacement:
|
||
|
doc = doc + "; " + _getReplacementString(replacement)
|
||
|
doc += "."
|
||
|
|
||
|
params = inspect.signature(wrappee).parameters
|
||
|
if (
|
||
|
name in params
|
||
|
and params[name].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||
|
):
|
||
|
parameterIndex = list(params).index(name)
|
||
|
|
||
|
def checkDeprecatedParameter(*args, **kwargs):
|
||
|
if len(args) > parameterIndex or name in kwargs:
|
||
|
warn(warningString, DeprecationWarning, stacklevel=2)
|
||
|
return wrappee(*args, **kwargs)
|
||
|
|
||
|
else:
|
||
|
|
||
|
def checkDeprecatedParameter(*args, **kwargs):
|
||
|
if name in kwargs:
|
||
|
warn(warningString, DeprecationWarning, stacklevel=2)
|
||
|
return wrappee(*args, **kwargs)
|
||
|
|
||
|
decorated = cast(_Tc, wraps(wrappee)(checkDeprecatedParameter))
|
||
|
_appendToDocstring(decorated, doc)
|
||
|
return decorated
|
||
|
|
||
|
return wrapper
|