diff options
Diffstat (limited to 'django/utils/deprecation.py')
| -rw-r--r-- | django/utils/deprecation.py | 79 |
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 |
