summaryrefslogtreecommitdiff
path: root/django/utils/deprecation.py
diff options
context:
space:
mode:
Diffstat (limited to 'django/utils/deprecation.py')
-rw-r--r--django/utils/deprecation.py79
1 files changed, 79 insertions, 0 deletions
diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py
index 4aa1183216..89c9f71e55 100644
--- a/django/utils/deprecation.py
+++ b/django/utils/deprecation.py
@@ -30,6 +30,85 @@ class RemovedInDjango70Warning(PendingDeprecationWarning):
RemovedAfterNextVersionWarning = RemovedInDjango70Warning
+def warn_about_external_use(
+ message,
+ category,
+ *,
+ skip_name_prefixes=None,
+ skip_frames=0,
+ internal_modules=None,
+):
+ """Issue a warning when a deprecated feature is used outside of Django.
+
+ Skip the warning when called from within Django, to avoid cascading
+ warnings when one deprecated feature is implemented on top of another.
+
+ Examine the stack to determine the "effective caller" (the code using the
+ deprecated feature). By default, this is the third frame from the top
+ (ignoring this helper plus the call site).
+
+ Provide `skip_name_prefixes` (a string or tuple of strings) to skip
+ additional frames by prefix-matching each frame's fully qualified name
+ (dotted module path plus qualname). `skip_name_prefixes` can be used to
+ skip specific functions, all methods in a class, or everything in a module.
+ Skipping stops at the first non-matching frame. Useful when a shared helper
+ is called through multiple paths of varying depth with a common prefix.
+
+ Provide `skip_frames` (an integer) to skip a fixed number of additional
+ frames (e.g., exactly one decorator or shared helper function). If both
+ options are provided, `skip_name_prefixes` is applied first.
+
+ Provide `internal_modules` (a tuple of names, defaulting to ("django",)) to
+ customize what counts as "internal". A frame is internal when its fully
+ qualified name starts with one of these names followed by a dot.
+
+ The warning is issued only if the effective caller (the first non-skipped
+ frame) is outside Django, attributed to that frame. If all frames are
+ skipped, it falls back to the base of the stack.
+
+ Note: To unconditionally issue a warning identifying the first caller
+ outside Django as its source, don't use this function. Instead, use::
+
+ warnings.warn(..., skip_file_prefixes=django_file_prefixes())
+
+ to avoid unnecessary stack inspection overhead.
+ """
+
+ if internal_modules is None:
+ internal_modules = ("django",)
+ if not isinstance(internal_modules, tuple):
+ raise TypeError("internal_modules must be a tuple of module names")
+ internal_prefixes = tuple(f"{mod}." for mod in internal_modules)
+
+ def get_fq_name(frame):
+ mod_name = frame.f_globals.get("__name__", "__main__")
+ return f"{mod_name}.{frame.f_code.co_qualname}"
+
+ def back(frame, level):
+ if frame is not None:
+ return frame.f_back, level + 1
+ return None, level
+
+ frame, level = inspect.currentframe(), 0
+ try:
+ # Back two frames: ignore warn_about_external_use() and its caller.
+ frame, level = back(*back(frame, level))
+
+ if skip_name_prefixes is not None:
+ while frame and get_fq_name(frame).startswith(skip_name_prefixes):
+ frame, level = back(frame, level)
+
+ for _ in range(skip_frames):
+ frame, level = back(frame, level)
+
+ is_internal = frame and get_fq_name(frame).startswith(internal_prefixes)
+ finally:
+ del frame
+
+ if not is_internal:
+ warnings.warn(message, category=category, stacklevel=level + 1)
+
+
class warn_about_renamed_method:
def __init__(
self, class_name, old_method_name, new_method_name, deprecation_warning