Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- 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.
- Text sprites went blank when the texture atlas rebuilt itself, which happens when it fills up after some textures were freed. The atlas redraws textures from their images, and a text sprite's image was empty. `sprite.texture.image` now also contains the text.
- 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.
Expand Down
3 changes: 3 additions & 0 deletions arcade/gl/backends/opengl/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ def __repr__(self):
return f"<Buffer {self._glo.value}>"

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)
Expand Down
3 changes: 3 additions & 0 deletions arcade/gl/backends/opengl/compute_shader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions arcade/gl/backends/opengl/framebuffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions arcade/gl/backends/opengl/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions arcade/gl/backends/opengl/texture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions arcade/gl/backends/opengl/texture_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions arcade/gl/backends/opengl/vertex_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def __repr__(self) -> str:
return f"<VertexArray {self.glo.value}>"

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)
Expand Down
11 changes: 6 additions & 5 deletions tests/integration/examples/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import io
import inspect
import os
from importlib.machinery import SourceFileLoader
import runpy
from pathlib import Path

import arcade
Expand Down Expand Up @@ -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"""
Expand All @@ -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?
Expand Down
11 changes: 6 additions & 5 deletions tests/integration/tutorials/test_tutorials.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import io
import os
import contextlib
from importlib.machinery import SourceFileLoader
import runpy
from pathlib import Path
import sys

Expand All @@ -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"""
Expand All @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/draw/test_drawing_primitives.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/gl/test_gl_gc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import gc
import sys

import pytest

import arcade
from arcade.gl import geometry

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/gui/test_interactive_sprite_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/gui/test_layouting_box_main_algorithm.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from pytest import approx

from arcade.gui import UIWidget
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/text/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions tests/unit/text/test_text_error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading