Stack level for warning when one public function wraps another public function

I’m adding a warning to a public SciPy function, scipy.sparse.diags_array. This function is called by another public function, scipy.sparse.diags. The question is what stacklevel argument should be given in warnings.warn() in scipy.sparse.diags_array? For a caller of diags_array, the stack level should be 2, but for a caller of diags, it should be 3. Has anyone dealt with this and come up with a clever solution?

One suggestion I’ve seen is to have the code in diags catch the warning and reissue it, but that involves dealing with warnings filters that are not thread-safe (at least not until Python 3.14).

One possibility is to inspect the frame stack in diags_array to see if the caller is diags (from the file _construct.py). This works, but it adds a bit of overhead when a warning is generated:

[... this is in the function diags_array() ...]
if (dtype != future_dtype):
    # Generate a warning about the future change in behavior.
    import inspect
    filename, _, funcname, _, _ = inspect.getframeinfo(inspect.currentframe().f_back)
    stacklevel = 2
    if filename.endswith('_construct.py') and funcname == 'diags':
        stacklevel = 3
    warnings.warn(
        f"Input has data type {future_dtype}, but the result has been cast "
        f"to {dtype}.  In the future, the output data type will match "
        "the input. To avoid this warning, set the `dtype` parameter to "
        "`None` to have the output dtype match the input, or set it to the "
        "desired output data type. "
        "In SciPy version 1.19, the default behavior will change to "
        "always determine the output data type from the input data types.",
        FutureWarning,
        stacklevel=stacklevel
    )
[...]

I have seen that scikit-image has this clever utility scikit-image/skimage/_shared/utils.py at e3c37269a5432e69e38c5bddc3049e1bf53523dc · scikit-image/scikit-image · GitHub
I’ll let you decide if the trade off of user experience vs added complexity is worth it!

My impression is that both this inspect usage and the code @j-bowhay linked to is way too complex, fragile and high-overhead for what it’s doing. My thoughts are:

  1. Calling another public function in the same library that emits a warning isn’t all that different from one library calling a function in another library that may emit a warning. The stacklevel in the latter case isn’t going to be perfect for the end user, but it’s not a significant enough problem to worry about.
  2. If you expect the warning to be commonly hit and you care to get it right, refactor the warning-generating code out and put it in a utility function, then call it directly from both public functions so they require the same stacklevel. This should always be possible (e.g., rename public diags_array to _diags_array and call the private implementation from both diags and diags_array).