From e24b649771f35429c0e4e83254d67a24c1102069 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 8 Oct 2026 12:02:25 -0500 Subject: [PATCH] Reject a ParallaxGroup depth of 0 when it's set ParallaxGroup divides the group offset by each layer's depth, so a depth of 0 raised ZeroDivisionError the first time the offset changed, far from where the depth was set. Setting a depth of 0, in any of the four places a depth can be set, now raises a ValueError saying to use float("inf") for a layer that doesn't scroll (#1551). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + arcade/future/background/groups.py | 19 +++++++- tests/unit/test_background_parallax_group.py | 49 ++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_background_parallax_group.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 59dfa6b3cb..49aa1a0905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. ## Unreleased ### Fixes +- Fixed `arcade.future.background.ParallaxGroup` raising `ZeroDivisionError` the first time its offset changed after a layer was given a depth of 0. Setting a depth of 0 now raises a `ValueError` right away, which says to use `float("inf")` for a layer that doesn't scroll ([#1551](https://github.com/pythonarcade/arcade/issues/1551)). - GUI: Fixed a gap between `UIFlatButton`'s border and the edge of the button, half the border's width, which also showed on `UIDropdown` ([#2868](https://github.com/pythonarcade/arcade/issues/2868)). - GUI: Passing a single style such as `UIFlatButton.UIStyle()` as a widget's `style` raised a confusing `TypeError` about attribute names. It now raises a `TypeError` saying a dict of styles for each state is needed, and how to start from the widget's `DEFAULT_STYLE` ([#2817](https://github.com/pythonarcade/arcade/issues/2817)). - GUI: Fixed `UITextArea`: diff --git a/arcade/future/background/groups.py b/arcade/future/background/groups.py index e1722441c2..d89aca7028 100644 --- a/arcade/future/background/groups.py +++ b/arcade/future/background/groups.py @@ -89,6 +89,15 @@ def add_from_file( self.add(background) +def _check_depth(depth: float) -> float: + """Raise a ValueError for a depth of 0, which ParallaxGroup can't divide by.""" + if depth == 0: + raise ValueError( + "A ParallaxGroup depth can't be 0. For a layer that doesn't scroll, use float('inf')." + ) + return depth + + class ParallaxGroup: """ The ParallaxBackground holds a list of backgrounds and a list of depths. @@ -96,6 +105,9 @@ class ParallaxGroup: each Background's offset will be set inversely proportional to its depth. This creates the effect of Backgrounds with greater depths appearing further away. The depth does not affect the positioning of layers at all. + + A depth can't be 0. For a layer that doesn't scroll, such as a distant sky, + use ``float("inf")``. """ def __init__( @@ -108,6 +120,8 @@ def __init__( if len(self._backgrounds) != len(self._depths): raise ValueError("The number of backgrounds does not equal the number of depth values") + for depth in self._depths: + _check_depth(depth) self._pos = (0.0, 0.0) self._offset = (0.0, 0.0) @@ -136,11 +150,12 @@ def __getitem__(self, item: int): def __setitem__(self, key: int, value: Background | float): if isinstance(value, float | int): - self._depths[key] = value + self._depths[key] = _check_depth(value) else: self._backgrounds[key] = value def add(self, item: Background, depth: float = 1.0): + _check_depth(depth) if item not in self._backgrounds: self._backgrounds.append(item) self._depths.append(depth) @@ -153,7 +168,7 @@ def remove(self, item: Background): self._depths.pop(index) def change_depth(self, item: Background, new_depth: float): - self._depths[self._backgrounds.index(item)] = new_depth + self._depths[self._backgrounds.index(item)] = _check_depth(new_depth) def extend(self, items: list[Background], depths: list[float]): for index, item in enumerate(items): diff --git a/tests/unit/test_background_parallax_group.py b/tests/unit/test_background_parallax_group.py new file mode 100644 index 0000000000..1e6dc713d1 --- /dev/null +++ b/tests/unit/test_background_parallax_group.py @@ -0,0 +1,49 @@ +"""ParallaxGroup offsets each layer by the group offset divided by its depth (#1551).""" + +import math + +import pytest + +from arcade.future.background import Background, ParallaxGroup + +TEXTURE = ":resources:images/cybercity_background/far-buildings.png" + + +def _layer(): + return Background.from_file(TEXTURE) + + +def test_offset_is_divided_by_depth(window): + near, far = _layer(), _layer() + group = ParallaxGroup([near, far], [1.0, 4.0]) + group.offset = (100.0, 40.0) + assert near.texture.offset == pytest.approx((100.0, 40.0)) + assert far.texture.offset == pytest.approx((25.0, 10.0)) + + +def test_infinite_depth_doesnt_scroll(window): + sky = _layer() + group = ParallaxGroup() + group.add(sky, math.inf) + group.offset = (100.0, 40.0) + assert sky.texture.offset == pytest.approx((0.0, 0.0)) + + +def test_depth_zero_is_rejected(window): + layer = _layer() + with pytest.raises(ValueError, match="can't be 0"): + ParallaxGroup([layer], [0.0]) + + group = ParallaxGroup() + with pytest.raises(ValueError, match="can't be 0"): + group.add(layer, 0) + + group.add(layer, 2.0) + with pytest.raises(ValueError, match="can't be 0"): + group.change_depth(layer, 0.0) + with pytest.raises(ValueError, match="can't be 0"): + group[0] = 0.0 + # The rejected depths weren't stored, so changing the offset still works + assert group[0] == (layer, 2.0) + group.offset = (10.0, 0.0) + assert layer.texture.offset == pytest.approx((5.0, 0.0))