Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace reapply() with bare apply() #3160

Merged
merged 18 commits into from
Feb 17, 2025
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 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 @@ -316,37 +316,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 @@ -456,11 +461,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 @@ -479,7 +499,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