diff --git a/manim/mobject/graphing/probability.py b/manim/mobject/graphing/probability.py index 0801fda253..76d3f78e7c 100644 --- a/manim/mobject/graphing/probability.py +++ b/manim/mobject/graphing/probability.py @@ -292,7 +292,8 @@ def __init__( ): if isinstance(bar_colors, str): logger.warning( - "Passing a string to `bar_colors` has been deprecated since v0.15.2 and will be removed after v0.17.0, the parameter must be a list. " + "Passing a string to `bar_colors` has been deprecated since v0.15.2 and will be removed after v0.17.0, the parameter must be a list. ", + stacklevel=2, ) bar_colors = list(bar_colors) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 0084cb7382..9599b2de54 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -504,6 +504,7 @@ def _insert_submobjects(self, index: int, mobjects: Sequence[Mobject]) -> Self: logger.warning( "Attempted adding some Mobject as a child more than once, " "this is not possible. Repetitions are ignored.", + stacklevel=3, ) if not self.submobjects: diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index b8e33da418..5793ccbafd 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -863,6 +863,7 @@ def _insert_submobjects( logger.warning( "Attempted adding some Mobject as a child more than once, " "this is not possible. Repetitions are ignored.", + stacklevel=3, ) if not self._submobjects: diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index d9477925bd..b39491ea2e 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -487,7 +487,7 @@ def __init__( elif font.title() in fonts_list: font = font.title() else: - logger.warning(f"Font {font} not in {fonts_list}.") + logger.warning(f"Font {font} not in {fonts_list}.", stacklevel=2) self.font = font self._font_size = float(font_size) # needs to be a float or else size is inflated when font_size = 24 @@ -1214,7 +1214,7 @@ def __init__( elif font.title() in fonts_list: font = font.title() else: - logger.warning(f"Font {font} not in {fonts_list}.") + logger.warning(f"Font {font} not in {fonts_list}.", stacklevel=2) self.font = font self._font_size = float(font_size) self.slant = slant diff --git a/manim/mobject/three_d/three_dimensions.py b/manim/mobject/three_d/three_dimensions.py index 4508cc0da6..7e61c4754a 100644 --- a/manim/mobject/three_d/three_dimensions.py +++ b/manim/mobject/three_d/three_dimensions.py @@ -295,7 +295,8 @@ def param_surface(u, v): if colorscale is None: logger.warning( "The value passed to the colorscale keyword argument was None, " - "the surface fill color has not been changed" + "the surface fill color has not been changed", + stacklevel=2, ) return self colorscale_list = list(colorscale) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 29f69d5c52..6a2d2f2fdf 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -1160,7 +1160,8 @@ def validate_run_time( f"The original {parameter_name} of {method_name}, " f"{run_time:g} seconds, is too short for the current frame " f"rate of {fps:g} FPS. Rendering with the shortest possible " - f"{parameter_name} of {seconds_per_frame:g} seconds instead." + f"{parameter_name} of {seconds_per_frame:g} seconds instead.", + stacklevel=3, ) run_time = seconds_per_frame @@ -1602,10 +1603,13 @@ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None: def embed(self) -> None: assert isinstance(self.renderer, OpenGLRenderer) if not self.session_spec.presentation.live_preview: - logger.warning("Called embed() while no live preview window is available.") + logger.warning( + "Called embed() while no live preview window is available.", + stacklevel=2, + ) return if self.renderer.file_writer.output_spec.enabled: - logger.warning("embed() is skipped while writing to a file.") + logger.warning("embed() is skipped while writing to a file.", stacklevel=2) return self.renderer.animation_start_time = 0 diff --git a/manim/utils/deprecation.py b/manim/utils/deprecation.py index 4a2f7b80d1..a919e812fc 100644 --- a/manim/utils/deprecation.py +++ b/manim/utils/deprecation.py @@ -245,7 +245,7 @@ def deprecate(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: The return value of the given callable when being passed the given arguments. """ - logger.warning(warning_msg()) + logger.warning(warning_msg(), stacklevel=3) return func(*args, **kwargs) if type(func).__name__ != "function": @@ -529,7 +529,7 @@ def deprecate_params(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: used = [param for param in params if param in kwargs] if len(used) > 0: - logger.warning(warning_msg(func, used)) + logger.warning(warning_msg(func, used), stacklevel=3) redirect_params(kwargs, used) return func(*args, **kwargs) diff --git a/tests/module/mobject/text/test_text_mobject.py b/tests/module/mobject/text/test_text_mobject.py index a94b587c80..a483119244 100644 --- a/tests/module/mobject/text/test_text_mobject.py +++ b/tests/module/mobject/text/test_text_mobject.py @@ -1,8 +1,5 @@ from __future__ import annotations -from contextlib import redirect_stdout -from io import StringIO - import pytest from manim.mobject.text.text_mobject import MarkupText, Text @@ -19,13 +16,13 @@ def test_font_size(): assert round(markuptext_string.font_size, 5) == 14.4 -def test_font_warnings(): +def test_font_warnings(manim_caplog): def warning_printed(font: str, **kwargs) -> bool: - io = StringIO() - with redirect_stdout(io): - Text("hi!", font=font, **kwargs) - txt = io.getvalue() - return "Font" in txt and "not in" in txt + # Inspect the log records rather than rendered console output: the + # latter is wrapped to the terminal width, which can split the message. + manim_caplog.clear() + Text("hi!", font=font, **kwargs) + return any("not in" in record.getMessage() for record in manim_caplog.records) # check for normal fonts (no warning) assert not warning_printed("System-ui", warn_missing_font=True) diff --git a/tests/module/utils/test_warning_stacklevel.py b/tests/module/utils/test_warning_stacklevel.py new file mode 100644 index 0000000000..5958dfc493 --- /dev/null +++ b/tests/module/utils/test_warning_stacklevel.py @@ -0,0 +1,69 @@ +"""Warnings caused by user code should be attributed to the caller. + +Each ``logger.warning`` that a user's own call can trigger passes ``stacklevel``, +so the emitted record points at the line the user wrote rather than at the line +inside Manim where the warning happens to live. +""" + +from __future__ import annotations + +import inspect +import linecache +from logging import LogRecord + +from manim import Mobject +from manim.utils.deprecation import deprecated, deprecated_params + + +@deprecated(since="v0.1.0", message="Use something else.") +def _deprecated_function() -> int: + return 1 + + +@deprecated_params(params="old", since="v0.1.0", message="Use new instead.") +def _function_with_deprecated_param(**kwargs: int) -> int: + return 1 + + +def _assert_blames_caller(record: LogRecord, source_fragment: str) -> None: + """Assert the record points at the caller's own statement. + + Checked by source text rather than a line number so the test survives + reformatting. + """ + caller = inspect.currentframe().f_back.f_code.co_name + assert record.pathname == __file__, ( + f"warning was attributed to {record.pathname}, expected the calling module" + ) + assert record.funcName == caller, ( + f"warning was attributed to {record.funcName}(), expected {caller}()" + ) + blamed_line = linecache.getline(record.pathname, record.lineno) + assert source_fragment in blamed_line, ( + f"warning pointed at {blamed_line.strip()!r}, " + f"expected the line containing {source_fragment!r}" + ) + + +def test_deprecated_function_warning_points_at_caller(manim_caplog): + _deprecated_function() + _assert_blames_caller(manim_caplog.records[0], "_deprecated_function()") + + +def test_deprecated_param_warning_points_at_caller(manim_caplog): + _function_with_deprecated_param(old=2) + _assert_blames_caller( + manim_caplog.records[0], "_function_with_deprecated_param(old=2)" + ) + + +def test_duplicate_add_warning_points_at_caller(manim_caplog): + parent, child = Mobject(), Mobject() + parent.add(child, child) + _assert_blames_caller(manim_caplog.records[0], "parent.add(child, child)") + + +def test_duplicate_add_to_back_warning_points_at_caller(manim_caplog): + parent, child = Mobject(), Mobject() + parent.add_to_back(child, child) + _assert_blames_caller(manim_caplog.records[0], "parent.add_to_back(child, child)")