add optional version requirement to deprecated_method

This commit is contained in:
Abyss Watcher
2025-01-25 19:52:47 +01:00
parent 825720ed5d
commit 2ed00cc91a
+51 -5
View File
@@ -12,9 +12,11 @@ import logging
import os
import traceback
import functools
import warnings
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, TypeVar
from volatility3.framework import constants, interfaces
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
# ##
@@ -57,20 +59,64 @@ class Deprecation:
"""Deprecation related methods."""
@staticmethod
def deprecated_method(replacement: Callable, additional_information: str = ""):
def deprecated_method(
replacement: Callable,
replacement_base_class_required_version: Tuple[int, int, int] = None,
additional_information: str = "",
):
"""A decorator for marking functions as deprecated.
Args:
replacement: The replacement function overriding the deprecated API, in the form of a Callable (typically a method)
replacement_base_class_required_version: The "replacement" base class version that the deprecated method expects before proxying to it. This implies that "replacement" is a method from a class that inherits from VersionableInterface.
additional_information: Information appended at the end of the deprecation message
"""
def decorator(deprecated_func):
@functools.wraps(deprecated_func)
def wrapper(*args, **kwargs):
nonlocal replacement, additional_information
deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__name__}\" is deprecated, use \"{replacement.__module__ + '.' + replacement.__name__}\" instead. {additional_information}"
vollog.warning(deprecation_msg)
nonlocal replacement, replacement_base_class_required_version, additional_information
# Prevent version mismatches between deprecated (proxy) methods and the ones they proxy
if replacement_base_class_required_version is not None and callable(
replacement
):
# example: replacement = volatility3.MyClass.my_dummy_function
# "MyClass.my_dummy_function" -> "MyClass"
replacement_base_class_name = replacement.__qualname__.split(".")[0]
# replacement.__globals__ example: {'MyClass': <class 'volatility3.MyClass'>}
replacement_base_class = replacement.__globals__.get(
replacement_base_class_name
)
# Verify that the base class inherits from VersionableInterface
if inspect.isclass(replacement_base_class) and issubclass(
replacement_base_class,
interfaces.configuration.VersionableInterface,
):
# Construct a requirement
req = requirements.VersionRequirement(
name=replacement_base_class.__name__,
component=replacement_base_class,
version=replacement_base_class_required_version,
)
# Verify the requirement
if not req.matches_required(
req._version, req._component.version
):
full_unsat_req_path = (
deprecated_func.__module__
+ "."
+ deprecated_func.__qualname__
+ "."
+ req.name
)
# Catched by the cli and redirected to process_unsatisfied_exceptions
raise exceptions.UnsatisfiedException(
{full_unsat_req_path: req}
)
deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}"
warnings.warn(deprecation_msg, FutureWarning)
# Return the wrapped function with its original arguments
return deprecated_func(*args, **kwargs)