diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 60d2d65f068..67aca356f57 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -1,9 +1,44 @@ """Internal utilities; not for external use""" +# Some functions in this module are derived from functions in pandas. For +# reference, here is a copy of the pandas copyright notice: + +# BSD 3-Clause License + +# Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team +# All rights reserved. + +# Copyright (c) 2011-2022, Open source contributors. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations import contextlib import functools import importlib +import inspect import io import itertools import math @@ -972,3 +1007,46 @@ def module_available(module: str) -> bool: Whether the module is installed. """ return importlib.util.find_spec(module) is not None + + +def find_stack_level(test_mode=False) -> int: + """Find the first place in the stack that is not inside xarray. + + This is unless the code emanates from a test, in which case we would prefer + to see the xarray source. + + This function is taken from pandas. + + Parameters + ---------- + test_mode : bool + Flag used for testing purposes to switch off the detection of test + directories in the stack trace. + + Returns + ------- + stacklevel : int + First level in the stack that is not part of xarray. + """ + import xarray as xr + + pkg_dir = os.path.dirname(xr.__file__) + test_dir = os.path.join(pkg_dir, "tests") + + # https://stackoverflow.com/questions/17407119/python-inspect-stack-is-slow + frame = inspect.currentframe() + n = 0 + while frame: + fname = inspect.getfile(frame) + if fname.startswith(pkg_dir) and (not fname.startswith(test_dir) or test_mode): + frame = frame.f_back + n += 1 + else: + break + return n + + +def emit_user_level_warning(message, category=None): + """Emit a warning at the user level by inspecting the stack trace.""" + stacklevel = find_stack_level() + warnings.warn(message, category=category, stacklevel=stacklevel) diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 1e14e8dc38e..0226f62d45a 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -71,7 +71,10 @@ NON_NANOSECOND_WARNING = ( "Converting non-nanosecond precision {case} values to nanosecond precision. " "This behavior can eventually be relaxed in xarray, as it is an artifact from " - "pandas which is now beginning to support non-nanosecond precision values." + "pandas which is now beginning to support non-nanosecond precision values. " + "This warning is caused by passing non-nanosecond np.datetime64 or " + "np.timedelta64 values to the DataArray or Variable constructor; it can be " + "silenced by converting the values to nanosecond precision ahead of time." ) @@ -191,14 +194,14 @@ def _as_nanosecond_precision(data): isinstance(dtype, pd.DatetimeTZDtype) and dtype.unit != "ns" ) if non_ns_datetime64 or non_ns_datetime_tz_dtype: - warnings.warn(NON_NANOSECOND_WARNING.format(case="datetime")) + utils.emit_user_level_warning(NON_NANOSECOND_WARNING.format(case="datetime")) if isinstance(dtype, pd.DatetimeTZDtype): nanosecond_precision_dtype = pd.DatetimeTZDtype("ns", dtype.tz) else: nanosecond_precision_dtype = "datetime64[ns]" return data.astype(nanosecond_precision_dtype) elif dtype.kind == "m" and dtype != np.dtype("timedelta64[ns]"): - warnings.warn(NON_NANOSECOND_WARNING.format(case="timedelta")) + utils.emit_user_level_warning(NON_NANOSECOND_WARNING.format(case="timedelta")) return data.astype("timedelta64[ns]") else: return data diff --git a/xarray/tests/test_utils.py b/xarray/tests/test_utils.py index c4b6294603f..ae0c083d0fc 100644 --- a/xarray/tests/test_utils.py +++ b/xarray/tests/test_utils.py @@ -266,3 +266,13 @@ def test_infix_dims_errors(supplied, all_): ) def test_iterate_nested(nested_list, expected): assert list(iterate_nested(nested_list)) == expected + + +def test_find_stack_level(): + assert utils.find_stack_level() == 1 + assert utils.find_stack_level(test_mode=True) == 2 + + def f(): + return utils.find_stack_level(test_mode=True) + + assert f() == 3