Skip to content

Commit

Permalink
Replace reapply() with bare apply() (#3160)
Browse files Browse the repository at this point in the history
Modifies the API for how styles are applied, changing the API for `apply()` to accept list of a style property names; if no names are provided, all properties should be applied.
  • Loading branch information
HalfWhitt authored Feb 17, 2025
1 parent 06baf8e commit b5c6feb
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 183 deletions.
1 change: 1 addition & 0 deletions changes/3160.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Travertino's ``BaseStyle.reapply()`` (and thus Toga's ``Pack.reapply()``) has been deprecated; the correct usage is now to call `.apply()` with no arguments. User code is unlikely to ever call this method, but Toga <= 0.4.8 calls it extensively, so users who update Travertino but not Toga will receive DeprecationWarnings.
122 changes: 49 additions & 73 deletions core/src/toga/style/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,6 @@ def update(self, **properties):
}
super().update(**properties)

# Pack.alignment is still an actual property, despite being deprecated, so we need
# to suppress deprecation warnings when reapply is called.
def reapply(self, *args, **kwargs):
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
super().reapply(*args, **kwargs)

_DEPRECATED_PROPERTIES = {
# Map each deprecated property name to its replacement.
# alignment / align_items is handled separately.
Expand Down Expand Up @@ -261,74 +254,57 @@ def __delitem__(self, name):
# End backwards compatibility
######################################################################

def apply(self, name: str, value: object = NOT_PROVIDED) -> None:
######################################################################
# 2025-02: Backwards compatibility for Toga < 0.5.0
######################################################################

if value is not NOT_PROVIDED:
warnings.warn(
(
"The value parameter to Pack.apply() is deprecated. The instance "
"will use its own current value for the property named."
),
DeprecationWarning,
stacklevel=2,
)

######################################################################
# End backwards compatibility
######################################################################

def apply(self, *names: list[str]) -> None:
if self._applicator:
if name == "text_align":
if (value := self.text_align) is None:
if self.text_direction == RTL:
value = RIGHT
else:
value = LEFT
self._applicator.set_text_align(value)
elif name == "text_direction":
if self.text_align is None:
self._applicator.set_text_align(
RIGHT if self.text_direction == RTL else LEFT
)
elif name == "color":
self._applicator.set_color(self.color)
elif name == "background_color":
self._applicator.set_background_color(self.background_color)
elif name == "visibility":
value = self.visibility
if value == VISIBLE:
# If visibility is being set to VISIBLE, look up the chain to see if
# an ancestor is hidden.
widget = self._applicator.widget
while widget := widget.parent:
if widget.style._hidden:
value = HIDDEN
break

self._applicator.set_hidden(value == HIDDEN)
elif name in (
"font_family",
"font_size",
"font_style",
"font_variant",
"font_weight",
):
self._applicator.set_font(
Font(
self.font_family,
self.font_size,
style=self.font_style,
variant=self.font_variant,
weight=self.font_weight,
for name in names or self._PROPERTIES:
if name == "text_align":
if (value := self.text_align) is None:
if self.text_direction == RTL:
value = RIGHT
else:
value = LEFT
self._applicator.set_text_align(value)
elif name == "text_direction":
if self.text_align is None:
self._applicator.set_text_align(
RIGHT if self.text_direction == RTL else LEFT
)
elif name == "color":
self._applicator.set_color(self.color)
elif name == "background_color":
self._applicator.set_background_color(self.background_color)
elif name == "visibility":
value = self.visibility
if value == VISIBLE:
# If visibility is being set to VISIBLE, look up the chain to
# see if an ancestor is hidden.
widget = self._applicator.widget
while widget := widget.parent:
if widget.style._hidden:
value = HIDDEN
break

self._applicator.set_hidden(value == HIDDEN)
elif name in (
"font_family",
"font_size",
"font_style",
"font_variant",
"font_weight",
):
self._applicator.set_font(
Font(
self.font_family,
self.font_size,
style=self.font_style,
variant=self.font_variant,
weight=self.font_weight,
)
)
)
else:
# Any other style change will cause a change in layout geometry,
# so perform a refresh.
self._applicator.refresh()
else:
# Any other style change will cause a change in layout geometry, so
# perform a refresh.
self._applicator.refresh()

def layout(self, viewport: Any) -> None:
# self._debug("=" * 80)
Expand Down
24 changes: 7 additions & 17 deletions core/tests/style/pack/test_apply.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
from unittest.mock import call

import pytest

from toga.colors import rgb
from toga.fonts import Font
from toga.style.pack import (
CENTER,
HIDDEN,
LEFT,
RIGHT,
ROW,
RTL,
VISIBLE,
Pack,
Expand All @@ -20,37 +17,37 @@

def test_set_default_right_textalign_when_rtl():
root = ExampleNode("app", style=Pack(text_direction=RTL))
root.style.reapply()
root.style.apply()
# Two calls; one caused by text_align, one because text_direction
# implies a change to text alignment.
assert root._impl.set_text_align.mock_calls == [call(RIGHT), call(RIGHT)]


def test_set_default_left_textalign_when_no_rtl():
root = ExampleNode("app", style=Pack())
root.style.reapply()
root.style.apply()
# Two calls; one caused by text_align, one because text_direction
# implies a change to text alignment.
assert root._impl.set_text_align.mock_calls == [call(LEFT), call(LEFT)]


def test_set_center_text_align():
root = ExampleNode("app", style=Pack(text_align="center"))
root.style.reapply()
root.style.apply()
root._impl.set_text_align.assert_called_once_with(CENTER)


def test_set_color():
color = "#ffffff"
root = ExampleNode("app", style=Pack(color=color))
root.style.reapply()
root.style.apply()
root._impl.set_color.assert_called_once_with(rgb(255, 255, 255))


def test_set_background_color():
color = "#ffffff"
root = ExampleNode("app", style=Pack(background_color=color))
root.style.reapply()
root.style.apply()
root._impl.set_background_color.assert_called_once_with(rgb(255, 255, 255))


Expand All @@ -65,7 +62,7 @@ def test_set_font():
font_weight="bold",
),
)
root.style.reapply()
root.style.apply()
root._impl.set_font.assert_called_with(
Font("Roboto", 12, style="normal", variant="small-caps", weight="bold")
)
Expand All @@ -74,7 +71,7 @@ def test_set_font():

def test_set_visibility_hidden():
root = ExampleNode("app", style=Pack(visibility=HIDDEN))
root.style.reapply()
root.style.apply()
root._impl.set_hidden.assert_called_once_with(True)


Expand Down Expand Up @@ -121,10 +118,3 @@ def assert_hidden_called(grandparent_value, parent_value, child_value):
# Show grandparent again; the other two should reappear.
grandparent.style.visibility = VISIBLE
assert_hidden_called(False, False, False)


def test_apply_deprecated_signature():
"""Calling apply() with a second argument raises a DeprecationWarning."""
style = Pack()
with pytest.warns(DeprecationWarning):
style.apply("direction", ROW)
9 changes: 5 additions & 4 deletions core/tests/widgets/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1244,14 +1244,15 @@ def test_tab_index(widget):
assert attribute_value(widget, "tab_index") == tab_index


def test_one_reapply_during_init():
"""Style's reapply() should be called exactly once during widget initialization."""
def test_one_apply_during_init():
"""Style's apply() should be called exactly once during widget initialization."""

class MockedPack(Pack):
reapply = Mock()
apply = Mock()

ExampleWidget(style=MockedPack())
MockedPack.reapply.assert_called_once()
# Make sure it was called with no arguments, to apply all properties.
MockedPack.apply.assert_called_once_with()


def test_widget_with_no_create():
Expand Down
49 changes: 36 additions & 13 deletions travertino/src/travertino/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,37 +315,42 @@ def _applicator(self, value):

if value is not None:
try:
self.reapply()
# This is backwards compatibility for Toga, which (at least as of
# 0.4.8), assigns style and applicator before the widget's
# implementation is available.
self.apply()
######################################################################
# 10-2024: Backwards compatibility for Toga < 0.5.0
######################################################################
except Exception:
warn(
"Failed to apply style when assigning applicator, or when "
"assigning a new style once applicator is present. Node should be "
"sufficiently initialized to apply its style before it is assigned "
"an applicator. This will be an exception in a future version.",
"an applicator. This will be an exception in a future version.\n"
"This error probably means you've updated Travertino to 0.5.0 but "
"are still using Toga <= 0.4.8; to fix, either update Toga to "
">= 0.5.0, or pin Travertino to 0.3.0.",
RuntimeWarning,
stacklevel=2,
)

def reapply(self):
for name in self._PROPERTIES:
self.apply(name)
######################################################################
# End backwards compatibility
######################################################################

def copy(self, applicator=None):
"""Create a duplicate of this style declaration."""
dup = self.__class__()
dup.update(**self)

######################################################################
# 10-2024: Backwards compatibility for Toga <= 0.4.8
# 10-2024: Backwards compatibility for Toga < 0.5.0
######################################################################

if applicator is not None:
warn(
"Providing an applicator to BaseStyle.copy() is deprecated. Set "
"applicator afterward on the returned copy.",
"applicator afterward on the returned copy.\n"
"This error probably means you've updated Travertino to 0.5.0 but are "
"still using Toga <= 0.4.8; to fix, either update Toga to >= 0.5.0, or "
"pin Travertino to 0.3.0.",
DeprecationWarning,
stacklevel=2,
)
Expand Down Expand Up @@ -455,11 +460,26 @@ def __str__(self):
# Backwards compatibility
######################################################################

def reapply(self):
warn(
"BaseStyle.reapply() is deprecated; call .apply with no arguments "
"instead.\n"
"This error probably means you've updated Travertino to 0.5.0 but are "
"still using Toga <= 0.4.8; to fix, either update Toga to >= 0.5.0, or pin "
"Travertino to 0.3.0.",
DeprecationWarning,
stacklevel=2,
)
self.apply()

@classmethod
def validated_property(cls, name, choices, initial=None):
warn(
"Defining style properties with class methods is deprecated; use class "
"attributes instead.",
"attributes instead.\n"
"This error probably means you've updated Travertino to 0.5.0 but are "
"still using Toga <= 0.4.8; to fix, either update Toga to >= 0.5.0, or pin "
"Travertino to 0.3.0.",
DeprecationWarning,
stacklevel=2,
)
Expand All @@ -478,7 +498,10 @@ def validated_property(cls, name, choices, initial=None):
def directional_property(cls, name):
warn(
"Defining style properties with class methods is deprecated; use class "
"attributes instead.",
"attributes instead.\n"
"This error probably means you've updated Travertino to 0.5.0 but are "
"still using Toga <= 0.4.8; to fix, either update Toga to >= 0.5.0, or pin "
"Travertino to 0.3.0.",
DeprecationWarning,
stacklevel=2,
)
Expand Down
8 changes: 4 additions & 4 deletions travertino/src/travertino/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ def applicator(self, applicator):

if applicator:
# This needs to happen *before* assigning the applicator to the style,
# below, because as part of receiving the applicator, the style will
# reapply itself. How this happens will vary with applicator
# implementation, but will probably need access to the node.
# below, because as part of receiving the applicator, the style will apply
# itself. How this happens will vary with applicator implementation, but
# will probably need access to the node.
applicator.node = self

self._applicator = applicator
# This triggers style.reapply():
# This triggers style.apply():
self.style._applicator = applicator

@property
Expand Down
7 changes: 4 additions & 3 deletions travertino/tests/test_choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from travertino.constants import GOLDENROD, NONE, REBECCAPURPLE, TOP
from travertino.declaration import BaseStyle, Choices, validated_property

from .utils import mock_attr, prep_style_class
from .utils import apply_dataclass, mock_apply


@prep_style_class
@mock_apply
@apply_dataclass
class Style(BaseStyle):
none: str = validated_property(NONE, REBECCAPURPLE, initial=NONE)
allow_string: str = validated_property(string=True, initial="start")
Expand All @@ -33,7 +34,7 @@ class Style(BaseStyle):
with catch_warnings():
filterwarnings("ignore", category=DeprecationWarning)

@mock_attr("apply")
@mock_apply
class DeprecatedStyle(BaseStyle):
pass

Expand Down
Loading

0 comments on commit b5c6feb

Please sign in to comment.