diff --git a/ci-scripts/ci-runner.bash b/ci-scripts/ci-runner.bash index 378b2a2d23..74685b1fa7 100644 --- a/ci-scripts/ci-runner.bash +++ b/ci-scripts/ci-runner.bash @@ -142,7 +142,7 @@ if [ $CI_GENERIC -eq 1 ]; then # Run unit tests for the public release echo "[INFO] Running unit tests with generic settings" checked_exec ./test_reframe.py --workers=auto --forked \ - -W=error::reframe.core.exceptions.ReframeDeprecationWarning -ra + -W=error::reframe.core.warnings.ReframeDeprecationWarning -ra checked_exec ! ./bin/reframe.py --system=generic -l 2>&1 | \ grep -- '--- Logging error ---' elif [ $CI_TUTORIAL -eq 1 ]; then @@ -174,7 +174,7 @@ else echo "[INFO] Running unit tests with ${backend}" TMPDIR=$tempdir checked_exec ./test_reframe.py --workers=auto --forked \ --rfm-user-config=config/cscs-ci.py \ - -W=error::reframe.core.exceptions.ReframeDeprecationWarning \ + -W=error::reframe.core.warnings.ReframeDeprecationWarning \ --rfm-user-system=dom:${backend} -ra done export PATH=$PATH_save @@ -182,7 +182,7 @@ else echo "[INFO] Running unit tests" TMPDIR=$tempdir checked_exec ./test_reframe.py --workers=auto --forked \ --rfm-user-config=config/cscs-ci.py \ - -W=error::reframe.core.exceptions.ReframeDeprecationWarning -ra + -W=error::reframe.core.warnings.ReframeDeprecationWarning -ra fi if [ $CI_EXITCODE -eq 0 ]; then diff --git a/reframe/core/config.py b/reframe/core/config.py index 3991142457..80cc4a7b73 100644 --- a/reframe/core/config.py +++ b/reframe/core/config.py @@ -19,10 +19,9 @@ import reframe.utility as util import reframe.utility.osext as osext import reframe.utility.typecheck as types -from reframe.core.exceptions import (ConfigError, - ReframeDeprecationWarning, - ReframeFatalError) +from reframe.core.exceptions import ConfigError, ReframeFatalError from reframe.core.logging import getlogger +from reframe.core.warnings import ReframeDeprecationWarning from reframe.utility import ScopedDict diff --git a/reframe/core/deferrable.py b/reframe/core/deferrable.py index e3274bda64..dcc937fcad 100644 --- a/reframe/core/deferrable.py +++ b/reframe/core/deferrable.py @@ -6,8 +6,6 @@ import builtins import functools -from reframe.core.exceptions import user_deprecation_warning - def deferrable(func): '''Function decorator for converting a function to a deferred diff --git a/reframe/core/exceptions.py b/reframe/core/exceptions.py index ba5485a30f..c20d801564 100644 --- a/reframe/core/exceptions.py +++ b/reframe/core/exceptions.py @@ -270,13 +270,6 @@ class DependencyError(ReframeError): '''Raised when a dependency problem is encountered.''' -class ReframeDeprecationWarning(DeprecationWarning): - '''Warning raised for deprecated features of the framework.''' - - -warnings.filterwarnings('default', category=ReframeDeprecationWarning) - - def user_frame(tb): if not inspect.istraceback(tb): raise ValueError('could not retrieve frame: argument not a traceback') @@ -326,24 +319,3 @@ def format_user_frame(frame): exc_str = ''.join(traceback.format_exception(exc_type, exc_value, tb)) return 'unexpected error: %s\n%s' % (exc_value, exc_str) - - -def user_deprecation_warning(message): - '''Raise a deprecation warning at the user stack frame that eventually - calls this function. - - As "user stack frame" is considered a stack frame that is outside the - :py:mod:`reframe` base module. - ''' - - # Unroll the stack and issue the warning from the first stack frame that is - # outside the framework. - stack_level = 1 - for s in inspect.stack(): - module = inspect.getmodule(s.frame) - if module is None or not module.__name__.startswith('reframe'): - break - - stack_level += 1 - - warnings.warn(message, ReframeDeprecationWarning, stacklevel=stack_level) diff --git a/reframe/core/fields.py b/reframe/core/fields.py index 07e459ad2f..af53237de6 100644 --- a/reframe/core/fields.py +++ b/reframe/core/fields.py @@ -13,7 +13,7 @@ import re import reframe.utility.typecheck as types -from reframe.core.exceptions import user_deprecation_warning +from reframe.core.warnings import user_deprecation_warning from reframe.utility import ScopedDict diff --git a/reframe/core/meta.py b/reframe/core/meta.py index 45a67045df..538b2393be 100644 --- a/reframe/core/meta.py +++ b/reframe/core/meta.py @@ -7,7 +7,7 @@ # Met-class for creating regression tests. # -from reframe.core.exceptions import user_deprecation_warning +from reframe.core.warnings import user_deprecation_warning class RegressionTestMeta(type): diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 6ec215e368..0b6292646f 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -34,9 +34,10 @@ from reframe.core.deferrable import _DeferredExpression from reframe.core.exceptions import (BuildError, DependencyError, PipelineError, SanityError, - PerformanceError, user_deprecation_warning) + PerformanceError) from reframe.core.meta import RegressionTestMeta from reframe.core.schedulers import Job +from reframe.core.warnings import user_deprecation_warning # Dependency kinds diff --git a/reframe/core/systems.py b/reframe/core/systems.py index 21f33fe31f..17fa370eca 100644 --- a/reframe/core/systems.py +++ b/reframe/core/systems.py @@ -155,7 +155,7 @@ def launcher(self): Please use :attr:`launcher_type` instead. ''' - from reframe.core.exceptions import user_deprecation_warning + from reframe.core.warnings import user_deprecation_warning user_deprecation_warning("the 'launcher' attribute is deprecated; " "please use 'launcher_type' instead") diff --git a/reframe/core/warnings.py b/reframe/core/warnings.py new file mode 100644 index 0000000000..4cd898e818 --- /dev/null +++ b/reframe/core/warnings.py @@ -0,0 +1,65 @@ +import contextlib +import inspect +import warnings + +from reframe.core.exceptions import ReframeFatalError + + +class ReframeDeprecationWarning(DeprecationWarning): + '''Warning raised for deprecated features of the framework.''' + + +warnings.filterwarnings('default', category=ReframeDeprecationWarning) + + +_format_warning_orig = warnings.formatwarning + + +def _format_warning(message, category, filename, lineno, line=None): + import reframe.core.runtime as rt + import reframe.utility.color as color + + if category != ReframeDeprecationWarning: + return _format_warning_orig(message, category, filename, lineno, line) + + if line is None: + # Read in the line from the file + with open(filename) as fp: + try: + line = fp.readlines()[lineno-1] + except IndexError: + line = '' + + message = f'{filename}:{lineno}: WARNING: {message}\n{line}\n' + + # Ignore coloring if runtime has not been initialized; this can happen + # when generating the documentation of deprecated APIs + with contextlib.suppress(ReframeFatalError): + if rt.runtime().get_option('general/0/colorize'): + message = color.colorize(message, color.YELLOW) + + return message + + +warnings.formatwarning = _format_warning + + +def user_deprecation_warning(message): + '''Raise a deprecation warning at the user stack frame that eventually + calls this function. + + As "user stack frame" is considered a stack frame that is outside the + :py:mod:`reframe` base module. + ''' + + # Unroll the stack and issue the warning from the first stack frame that is + # outside the framework. + stack_level = 1 + for s in inspect.stack(): + module = inspect.getmodule(s.frame) + if module is None or not module.__name__.startswith('reframe'): + break + + stack_level += 1 + + warnings.warn(message, ReframeDeprecationWarning, stacklevel=stack_level) diff --git a/reframe/frontend/cli.py b/reframe/frontend/cli.py index 64a148667e..49907596c1 100644 --- a/reframe/frontend/cli.py +++ b/reframe/frontend/cli.py @@ -24,8 +24,9 @@ import reframe.utility.osext as osext from reframe.core.exceptions import ( EnvironError, ConfigError, ReframeError, - ReframeDeprecationWarning, ReframeFatalError, format_exception + ReframeFatalError, format_exception ) +from reframe.core.warnings import ReframeDeprecationWarning from reframe.frontend.executors import Runner, generate_testcases from reframe.frontend.executors.policies import (SerialExecutionPolicy, AsynchronousExecutionPolicy) diff --git a/unittests/resources/checks_unlisted/deprecated_test.py b/unittests/resources/checks_unlisted/deprecated_test.py index 6a95963377..398bff51b1 100644 --- a/unittests/resources/checks_unlisted/deprecated_test.py +++ b/unittests/resources/checks_unlisted/deprecated_test.py @@ -1,6 +1,7 @@ import reframe as rfm import reframe.utility.sanity as sn -from reframe.core.exceptions import user_deprecation_warning + +from reframe.core.warnings import user_deprecation_warning @rfm.simple_test diff --git a/unittests/test_config.py b/unittests/test_config.py index c59c5aacb2..bee766b910 100644 --- a/unittests/test_config.py +++ b/unittests/test_config.py @@ -8,8 +8,9 @@ import pytest import reframe.core.config as config -from reframe.core.exceptions import (ConfigError, ReframeDeprecationWarning) +from reframe.core.exceptions import ConfigError from reframe.core.systems import System +from reframe.core.warnings import ReframeDeprecationWarning def test_load_config_fallback(monkeypatch): diff --git a/unittests/test_fields.py b/unittests/test_fields.py index c79e864f73..5763af784c 100644 --- a/unittests/test_fields.py +++ b/unittests/test_fields.py @@ -8,7 +8,7 @@ import pytest import reframe.core.fields as fields -from reframe.core.exceptions import ReframeDeprecationWarning +from reframe.core.warnings import ReframeDeprecationWarning from reframe.utility import ScopedDict diff --git a/unittests/test_loader.py b/unittests/test_loader.py index f8af8631d7..873e4da77d 100644 --- a/unittests/test_loader.py +++ b/unittests/test_loader.py @@ -8,9 +8,9 @@ import reframe as rfm from reframe.core.exceptions import (ConfigError, NameConflictError, - ReframeDeprecationWarning, RegressionTestLoadError) from reframe.core.systems import System +from reframe.core.warnings import ReframeDeprecationWarning from reframe.frontend.loader import RegressionCheckLoader diff --git a/unittests/test_warnings.py b/unittests/test_warnings.py new file mode 100644 index 0000000000..dc84e96cf5 --- /dev/null +++ b/unittests/test_warnings.py @@ -0,0 +1,51 @@ +import pytest +import warnings + +import reframe.core.runtime as rt +import reframe.core.warnings as warn +import reframe.utility.color as color +import unittests.fixtures as fixtures + + +@pytest.fixture(params=['colors', 'nocolors']) +def with_colors(request): + with rt.temp_runtime(fixtures.BUILTIN_CONFIG_FILE, 'generic', + {'general/colorize': request.param == 'colors'}): + yield request.param == 'colors' + + +def test_deprecation_warning(): + with pytest.warns(warn.ReframeDeprecationWarning): + warn.user_deprecation_warning('deprecated') + + +def test_deprecation_warning_formatting(with_colors): + message = warnings.formatwarning( + 'deprecated', warn.ReframeDeprecationWarning, 'file', 10, 'a = 1' + ) + expected = 'file:10: WARNING: deprecated\na = 1\n' + if with_colors: + expected = color.colorize(expected, color.YELLOW) + + assert message == expected + + +def test_deprecation_warning_formatting_noline(tmp_path, with_colors): + srcfile = tmp_path / 'file' + srcfile.touch() + + message = warnings.formatwarning( + 'deprecated', warn.ReframeDeprecationWarning, srcfile, 10 + ) + expected = f'{srcfile}:10: WARNING: deprecated\n\n' + if with_colors: + expected = color.colorize(expected, color.YELLOW) + + assert message == expected + + +def test_random_warning_formatting(): + message = warnings.formatwarning( + 'deprecated', UserWarning, 'file', 10, 'a = 1' + ) + assert message == f'file:10: UserWarning: deprecated\n a = 1\n'