From 0dfdd77bdb3c223ab080555486cd610e5d765950 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 5 Oct 2026 16:40:04 -0500 Subject: [PATCH] Clean up warnings in the test suite The test suite printed 240 warnings. All are gone: - Run examples and tutorials with runpy.run_path(run_name="__main__") instead of SourceFileLoader.load_module(), which Python 3.15 removes. The old code also ran each file twice, since load_module() already executes the module before exec_module() ran it again. - Pass lists, not generators, to parametrize (deprecated in pytest). - Ignore draw_text's PerformanceWarning in the tests and examples that use draw_text on purpose. - Fix the OpenGL __del__ methods raising AttributeError when creating the object failed before the OpenGL handle was set, for example a texture with 5 components. Seven classes had the same pattern. - test_container_size_zero now checks for its "Container size is 0" warning, and test_works_in_box_layout gives its layout a size. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + arcade/gl/backends/opengl/buffer.py | 3 +++ arcade/gl/backends/opengl/compute_shader.py | 3 +++ arcade/gl/backends/opengl/framebuffer.py | 3 +++ arcade/gl/backends/opengl/program.py | 3 +++ arcade/gl/backends/opengl/texture.py | 3 +++ arcade/gl/backends/opengl/texture_array.py | 3 +++ arcade/gl/backends/opengl/vertex_array.py | 3 +++ tests/integration/examples/test_examples.py | 11 ++++---- tests/integration/tutorials/test_tutorials.py | 11 ++++---- tests/unit/draw/test_drawing_primitives.py | 4 +++ tests/unit/gl/test_gl_gc.py | 27 +++++++++++++++++++ .../gui/test_interactive_sprite_widget.py | 2 +- .../gui/test_layouting_box_main_algorithm.py | 4 ++- tests/unit/text/test_text.py | 2 ++ tests/unit/text/test_text_error_handling.py | 2 ++ 16 files changed, 73 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9990e30835..cdd13a130b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - `anchor_x="center"` cut off the left half of the text, and `anchor_x="right"` made an empty sprite. - The sprite was placed too high. It now covers the same area as an `arcade.Text` with the same arguments at the same position. - An empty string raised `ValueError`. It now makes a transparent sprite, as documented. +- Fixed an `AttributeError` being printed as "Exception ignored in `__del__`" when creating an OpenGL texture, framebuffer, buffer, program, compute shader, texture array, or vertex array failed, for example with an invalid number of texture components. ### New Features - Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check. diff --git a/arcade/gl/backends/opengl/buffer.py b/arcade/gl/backends/opengl/buffer.py index e6245c7485..176fd9bb23 100644 --- a/arcade/gl/backends/opengl/buffer.py +++ b/arcade/gl/backends/opengl/buffer.py @@ -83,6 +83,9 @@ def __repr__(self): return f"" def __del__(self): + # Creating the object can fail before the OpenGL object exists + if not hasattr(self, "_glo"): + return # Intercept garbage collection if we are using Context.gc() if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: self._ctx.objects.append(self) diff --git a/arcade/gl/backends/opengl/compute_shader.py b/arcade/gl/backends/opengl/compute_shader.py index f3c88a4ea3..7f853e1060 100644 --- a/arcade/gl/backends/opengl/compute_shader.py +++ b/arcade/gl/backends/opengl/compute_shader.py @@ -165,6 +165,9 @@ def __setitem__(self, key, value): uniform.setter(value) def __del__(self): + # Creating the object can fail before the OpenGL object exists + if not hasattr(self, "_glo"): + return if self._ctx.gc_mode == "context_gc" and self._glo > 0: self._ctx.objects.append(self) diff --git a/arcade/gl/backends/opengl/framebuffer.py b/arcade/gl/backends/opengl/framebuffer.py index eb9fac5a6f..38fae0dd5b 100644 --- a/arcade/gl/backends/opengl/framebuffer.py +++ b/arcade/gl/backends/opengl/framebuffer.py @@ -103,6 +103,9 @@ def __init__( weakref.finalize(self, OpenGLFramebuffer.delete_glo, ctx, fbo_id) def __del__(self): + # Creating the object can fail before the OpenGL object exists + if not hasattr(self, "_glo"): + return # Intercept garbage collection if we are using Context.gc() if self._ctx.gc_mode == "context_gc" and not self.is_default and self._glo.value > 0: self._ctx.objects.append(self) diff --git a/arcade/gl/backends/opengl/program.py b/arcade/gl/backends/opengl/program.py index cde9cead01..3a786d27ce 100644 --- a/arcade/gl/backends/opengl/program.py +++ b/arcade/gl/backends/opengl/program.py @@ -164,6 +164,9 @@ def __init__( weakref.finalize(self, OpenGLProgram.delete_glo, self._ctx, glo) def __del__(self): + # Creating the object can fail before the OpenGL object exists + if not hasattr(self, "_glo"): + return # Intercept garbage collection if we are using Context.gc() if self._ctx.gc_mode == "context_gc" and self._glo > 0: self._ctx.objects.append(self) diff --git a/arcade/gl/backends/opengl/texture.py b/arcade/gl/backends/opengl/texture.py index 0de0e65a91..e01a7d030b 100644 --- a/arcade/gl/backends/opengl/texture.py +++ b/arcade/gl/backends/opengl/texture.py @@ -195,6 +195,9 @@ def resize(self, size: tuple[int, int]): self._texture_2d(None) def __del__(self): + # Creating the object can fail before the OpenGL object exists + if not hasattr(self, "_glo"): + return # Intercept garbage collection if we are using Context.gc() if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: self._ctx.objects.append(self) diff --git a/arcade/gl/backends/opengl/texture_array.py b/arcade/gl/backends/opengl/texture_array.py index 153cc3a884..84349e2054 100644 --- a/arcade/gl/backends/opengl/texture_array.py +++ b/arcade/gl/backends/opengl/texture_array.py @@ -200,6 +200,9 @@ def resize(self, size: tuple[int, int]): self._texture_2d_array(None) def __del__(self): + # Creating the object can fail before the OpenGL object exists + if not hasattr(self, "_glo"): + return # Intercept garbage collection if we are using Context.gc() if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: self._ctx.objects.append(self) diff --git a/arcade/gl/backends/opengl/vertex_array.py b/arcade/gl/backends/opengl/vertex_array.py index dfc2ea0862..062936ea96 100644 --- a/arcade/gl/backends/opengl/vertex_array.py +++ b/arcade/gl/backends/opengl/vertex_array.py @@ -77,6 +77,9 @@ def __repr__(self) -> str: return f"" def __del__(self) -> None: + # Creating the object can fail before the OpenGL object exists + if not hasattr(self, "glo"): + return # Intercept garbage collection if we are using Context.gc() if self._ctx.gc_mode == "context_gc" and self.glo.value > 0: self._ctx.objects.append(self) diff --git a/tests/integration/examples/test_examples.py b/tests/integration/examples/test_examples.py index dbfc709395..7b7098df73 100644 --- a/tests/integration/examples/test_examples.py +++ b/tests/integration/examples/test_examples.py @@ -6,7 +6,7 @@ import io import inspect import os -from importlib.machinery import SourceFileLoader +import runpy from pathlib import Path import arcade @@ -73,9 +73,11 @@ def find_main_function(module): return None +# Some examples demonstrate draw_text, which warns that it's slow +@pytest.mark.filterwarnings("ignore::arcade.exceptions.PerformanceWarning") @pytest.mark.parametrize( "module_path, file_path, allow_stdout", - list_examples(), + list(list_examples()), ) def test_examples(window_proxy, module_path, file_path, allow_stdout): """Run all examples""" @@ -84,9 +86,8 @@ def test_examples(window_proxy, module_path, file_path, allow_stdout): stdout = io.StringIO() with contextlib.redirect_stdout(stdout): - # Manually load the module as __main__ so it runs on import - loader = SourceFileLoader("__main__", str(file_path)) - loader.exec_module(loader.load_module()) + # Run the example as __main__, as `python -m` would + runpy.run_path(str(file_path), run_name="__main__") # Reset the global clock's tick speed # is this a good argument against a global scope clock? diff --git a/tests/integration/tutorials/test_tutorials.py b/tests/integration/tutorials/test_tutorials.py index e7ee269f60..8be9e6e2d3 100644 --- a/tests/integration/tutorials/test_tutorials.py +++ b/tests/integration/tutorials/test_tutorials.py @@ -5,7 +5,7 @@ import io import os import contextlib -from importlib.machinery import SourceFileLoader +import runpy from pathlib import Path import sys @@ -23,9 +23,11 @@ def find_tutorials(): yield path, path.stem in ALLOW_STDOUT +# Some tutorial steps use draw_text, which warns that it's slow +@pytest.mark.filterwarnings("ignore::arcade.exceptions.PerformanceWarning") @pytest.mark.parametrize( "file_path, allow_stdout", - find_tutorials(), + list(find_tutorials()), ) def test_tutorials(window_proxy, file_path, allow_stdout): """Run all tutorials""" @@ -35,10 +37,9 @@ def test_tutorials(window_proxy, file_path, allow_stdout): os.environ["ARCADE_TEST"] = "TRUE" stdout = io.StringIO() with contextlib.redirect_stdout(stdout): - # Manually load the module as __main__ so it runs on import + # Run the tutorial as __main__, as `python -m` would os.chdir(file_path.parent) - loader = SourceFileLoader("__main__", str(file_path)) - loader.exec_module(loader.load_module()) + runpy.run_path(str(file_path), run_name="__main__") if not allow_stdout: output = stdout.getvalue() diff --git a/tests/unit/draw/test_drawing_primitives.py b/tests/unit/draw/test_drawing_primitives.py index 8c3d81ced9..dd404d816a 100644 --- a/tests/unit/draw/test_drawing_primitives.py +++ b/tests/unit/draw/test_drawing_primitives.py @@ -1,6 +1,10 @@ +import pytest + import arcade +# Uses draw_text, which warns that it's slow +@pytest.mark.filterwarnings("ignore::arcade.exceptions.PerformanceWarning") def test_draw_primitives(window): """ Render the screen. diff --git a/tests/unit/gl/test_gl_gc.py b/tests/unit/gl/test_gl_gc.py index aba1b69784..7482c9c8ff 100644 --- a/tests/unit/gl/test_gl_gc.py +++ b/tests/unit/gl/test_gl_gc.py @@ -1,4 +1,8 @@ import gc +import sys + +import pytest + import arcade from arcade.gl import geometry @@ -72,6 +76,29 @@ def test_auto_gc(ctx): create_resources(ctx) +def test_failed_creation_collected_quietly(ctx): + """Objects whose creation failed don't raise errors when garbage collected""" + errors = [] + old_hook = sys.unraisablehook + old_gc_mode = ctx.gc_mode + sys.unraisablehook = errors.append + try: + for gc_mode in ("auto", "context_gc"): + ctx.gc_mode = gc_mode + with pytest.raises(ValueError): + ctx.texture((10, 10), components=5) + with pytest.raises(ValueError): + ctx.framebuffer() + with pytest.raises(ValueError): + ctx.framebuffer(color_attachments=[ctx.texture((10, 10)), ctx.texture((10, 11))]) + gc.collect() + ctx.gc() + finally: + sys.unraisablehook = old_hook + ctx.gc_mode = old_gc_mode + assert [f"{error.exc_type.__name__}: {error.exc_value}" for error in errors] == [] + + def create_resources(ctx: arcade.ArcadeContext): # Texture created, freed = ctx.stats.texture diff --git a/tests/unit/gui/test_interactive_sprite_widget.py b/tests/unit/gui/test_interactive_sprite_widget.py index a8edd9853e..6dc7bae42c 100644 --- a/tests/unit/gui/test_interactive_sprite_widget.py +++ b/tests/unit/gui/test_interactive_sprite_widget.py @@ -91,7 +91,7 @@ def test_explicit_size_overrides_sprite(window): def test_works_in_box_layout(ui): """Widget should be usable inside a UIBoxLayout.""" - layout = UIBoxLayout(vertical=False, space_between=10, size_hint=None) + layout = UIBoxLayout(vertical=False, space_between=10, width=500, height=500, size_hint=None) widget_a = _make_widget() widget_b = _make_widget() layout.add(widget_a) diff --git a/tests/unit/gui/test_layouting_box_main_algorithm.py b/tests/unit/gui/test_layouting_box_main_algorithm.py index c111028d0a..4fb2518636 100644 --- a/tests/unit/gui/test_layouting_box_main_algorithm.py +++ b/tests/unit/gui/test_layouting_box_main_algorithm.py @@ -1,3 +1,4 @@ +import pytest from pytest import approx from arcade.gui import UIWidget @@ -40,7 +41,8 @@ def test_container_size_zero(window): ] # WHEN - sizes = _box_axis_algorithm(entries, 0) + with pytest.warns(UserWarning, match="Container size is 0"): + sizes = _box_axis_algorithm(entries, 0) # THEN assert sizes == [50, 50, 50] diff --git a/tests/unit/text/test_text.py b/tests/unit/text/test_text.py index 993fe75a94..e3e4af5ec6 100644 --- a/tests/unit/text/test_text.py +++ b/tests/unit/text/test_text.py @@ -4,6 +4,8 @@ import arcade +# Tests draw_text, which warns that it's slow +@pytest.mark.filterwarnings("ignore::arcade.exceptions.PerformanceWarning") def test_text(window): window.background_color = arcade.color.AMAZON diff --git a/tests/unit/text/test_text_error_handling.py b/tests/unit/text/test_text_error_handling.py index 7e756ed757..274da85d3e 100644 --- a/tests/unit/text/test_text_error_handling.py +++ b/tests/unit/text/test_text_error_handling.py @@ -21,6 +21,8 @@ def test_text_instance_raise_multiline_error(window): ) +# Tests draw_text, which warns that it's slow +@pytest.mark.filterwarnings("ignore::arcade.exceptions.PerformanceWarning") def test_text_function_raise_multiline_error(window): with pytest.raises(ValueError) as e: _ = arcade.draw_text("Initial text", 0, 0, width=0, multiline=True)