如何根据调用者为警告分配堆栈级别?

问题描述 投票:0回答:3

我有一个 Python 类,它在内部发出警告

__init__()
。它还提供了用于打开和读取文件的工厂类方法:

from warnings import warn

class MyWarning(Warning):
    """Warning issued when an invalid name is found."""
    pass

class MyClass:
    def __init__(self, names):
        # Simplified; actual code is longer
        if is_invalid(names):
            names = fix_names(names)
            warn(f'{names!r} contains invalid element(s)',
                MyWarning, stacklevel=2)
        self._names = names

    @classmethod
    def from_file(cls, filename):
        with open(filename) as file:
            names = extract_names(file)
        return cls(names)

stacklevel=2
使警告引用对
MyClass()
的调用,而不是
warn()
语句本身。当用户代码直接实例化 MyClass 时,这会起作用。但是,当
MyClass.from_file()
发出警告时,
MyWarning
指的是
return cls(names)
,而不是调用
from_file()
的用户代码。

如何确保工厂方法也发出指向调用者的警告?我考虑过的一些选择:

  1. _stacklevel
    添加一个“隐藏”的
    __init__()
    参数,并在
    _stacklevel=2
    内部用
    from_file()
    实例化MyClass。
    • 这非常丑陋,并且向 API 公开了内部行为。
  2. 添加“隐藏”
    _stacklevel
    类属性,并在
    __init__()
    内访问它。然后在
    from_file()
    中临时修改这个属性
    • 也超级丑。
  3. 添加一个
    _set_names()
    方法来检查/修复名称并在需要时发出警告。然后在构造函数中调用这个方法。对于
    from_file()
    ,首先用空参数实例化MyClass,然后直接调用
    _set_names()
    以确保MyWarning指向调用者。
    • 仍然很hacky,并且在调用
      _set_names()
      时有效地调用
      from_file()
      两次。
  4. 捕获并重新抛出警告,类似于异常链。
    • 听起来不错,但我不知道该怎么做。

我阅读了

warning
模块文档,但它对安全捕获和重新抛出警告几乎没有提供帮助。使用
warnings.simplefilter()
将警告转换为异常会中断
MyClass()
并迫使我再次调用它。

python python-3.x constructor warnings
3个回答
2
投票

您可以使用类似于捕获异常的方式捕获警告

warnings.catch_warnings()
:

import warnings

class MyWarning(Warning):
    """Warning issued when an invalid name is found."""
    pass

class MyClass:
    def __init__(self, names):
        # Simplified; actual code is longer
        if is_invalid(names):
            names = fix_names(names)
            warn(f'{names!r} contains invalid element(s)',
                MyWarning, stacklevel=2)
        self._names = names

    @classmethod
    def from_file(cls, filename):
        with open(filename) as file:
            names = extract_names(file)
        with warnings.catch_warnings(record=True) as cx_manager:
            inst = cls(names)

        #re-report warnings with the stack-level we want
        for warning in cx_manager:
            warnings.warn(warning.message, warning.category, stacklevel=2)

        return inst

请记住

warnings.catch_warnings()
文档中的以下注释:

注意 catch_warnings 管理器通过替换然后恢复模块的

showwarning()
函数和过滤器规范的内部列表来工作。这意味着上下文管理器正在修改全局状态,因此不是线程安全的。


1
投票

大卫是对的,

warnings.catch_warnings(record=True)
可能就是你想要的。虽然我会把它写成函数装饰器:

def reissue_warnings(func):
    def inner(*args, **kwargs):
        with warnings.catch_warnings(record = True) as warning_list:
            result = func(*args, **kwargs)
        for warning in warning_list:
            warnings.warn(warning.message, warning.category, stacklevel = 2)
        return result
    return inner

然后在你的例子中:

class MyClass:
    def __init__(self, names):
        # ...

    @classmethod
    @reissue_warnings
    def from_file(cls, filename):
        with open(filename) as file:
            names = extract_names(file)
        return cls(names)

inst = MyClass(['some', 'names'])   # 58: MyWarning: ['some', 'names'] contains invalid element(s)
inst = MyClass.from_file('example') # 59: MyWarning: ['example'] contains invalid element(s)

这种方式还允许您跨多个功能干净地收集和重新发出警告:

class Test:
    def a(self):
        warnings.warn("This is a warning issued from a()")
        
    @reissue_warnings
    def b(self):
        self.a()
    
    @reissue_warnings
    def c(self):
        warnings.warn("This is a warning issued from c()")
        self.b()
        
    @reissue_warnings
    def d(self):
        self.c()
        
test = Test()
test.d() # Line 59
# 59: UserWarning: This is a warning issued from c()
# 59: UserWarning: This is a warning issued from a()

0
投票

这对我有用:

def get_warning_stack_level(module_filenames: Union[str, List[str]]) -> int:
    """Calculate the stack level for warnings.

    The stack level is the number of stack frames between the caller of the
    function and the user code past the sequence of modules given in
    module_filenames.

    This is used to provide the appropriate stack level to warnings.warn or
    warnings.warn_explicit so that the warning appears in user code rather
    than in the library itself.

    The calculation is based on the system call stack.

    Args:
        module_filenames: The filenames of the top-most modules after which the
            user code starts. If a single filename is provided, it is wrapped
            in a list. The modules should be given in the bottom-up order.

    Returns:
        The stack level to be used in warnings.warn or warnings.warn_explicit.
    """

    if isinstance(module_filenames, str):
        module_filenames = [module_filenames]

    modules = list(module_filenames)
    target = modules.pop(0)

    stack = sys._getframe().f_back
    stack_level = 1
    found = False

    while stack is not None:
        filename = stack.f_code.co_filename
        matches = filename.endswith((target, f"{target}.py"))

        if not found and matches:  # the sequence has started
            found = True
        elif found and not matches:
            if not modules:  # the sequence has ended
                break

            target = modules.pop(0)
            matches = filename.endswith((target, f"{target}.py"))
            if not matches:
                raise ValueError("The module sequence has been found, but broken.")

        stack = stack.f_back
        stack_level += 1

    if not found:
        raise ValueError("The module sequence has not started in the stack.")

    if not stack:
        raise ValueError(
            "The module sequence has been found, "
            "but we have ran out of stack "
            "before finding the top-most module."
        )

    return stack_level
© www.soinside.com 2019 - 2024. All rights reserved.