Skip to content

Commit

Permalink
Add "horizontal" and "vertical" aliases for align_items and justify_c…
Browse files Browse the repository at this point in the history
…ontent (#3113)

* Add "horizontal" and "vertical" aliases for align_items and justify_content

* Work around Travertino _ALL_PROPERTIES version differences

* Improve wording

Co-authored-by: Russell Keith-Magee <[email protected]>

---------

Co-authored-by: Russell Keith-Magee <[email protected]>
  • Loading branch information
mhsmith and freakboy3742 authored Jan 21, 2025
1 parent b1926ee commit 1bc7002
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 86 deletions.
1 change: 1 addition & 0 deletions changes/3111.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``align_items`` and ``justify_content`` properties now have the aliases ``horizontal_align_items``, ``vertical_align_items``, ``horizontal_align_content`` and ``vertical_align_content`` that explicitly describe layout behavior in the named direction.
14 changes: 11 additions & 3 deletions core/src/toga/style/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ class StyleProperty:
def __set_name__(self, mixin_cls, name):
self.name = name

def __repr__(self):
return f"<StyleProperty {self.name!r}>"

def __get__(self, widget, mixin_cls):
return self if widget is None else getattr(widget.style, self.name)

Expand All @@ -21,8 +24,13 @@ def style_mixin(style_cls):
"""
}

for name in dir(style_cls):
if not name.startswith("_") and isinstance(getattr(style_cls, name), property):
mixin_dict[name] = StyleProperty()
try:
_all_properties = style_cls._BASE_ALL_PROPERTIES
except AttributeError:
# Travertino 0.3 compatibility
_all_properties = style_cls._ALL_PROPERTIES

for name in _all_properties[style_cls]:
mixin_dict[name] = StyleProperty()

return type(style_cls.__name__ + "Mixin", (), mixin_dict)
45 changes: 37 additions & 8 deletions core/src/toga/style/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def _hidden(self) -> bool:
######################################################################

def update(self, **properties):
# Set direction first, as it may change the interpretation of direction-based
# property aliases in _update_property_name.
if direction := properties.pop("direction", None):
self.direction = direction

properties = {
self._update_property_name(name.replace("-", "_")): value
for name, value in properties.items()
Expand All @@ -128,7 +133,7 @@ def reapply(self, *args, **kwargs):
warnings.filterwarnings("ignore", category=DeprecationWarning)
super().reapply(*args, **kwargs)

DEPRECATED_PROPERTIES = {
_DEPRECATED_PROPERTIES = {
# Map each deprecated property name to its replacement.
# alignment / align_items is handled separately.
"padding": "margin",
Expand All @@ -138,22 +143,38 @@ def reapply(self, *args, **kwargs):
"padding_left": "margin_left",
}

@classmethod
def _update_property_name(cls, name):
if new_name := cls.DEPRECATED_PROPERTIES.get(name):
cls._warn_deprecated(name, new_name, stacklevel=4)
_ALIASES = {
"horizontal_align_content": {ROW: "justify_content"},
"horizontal_align_items": {COLUMN: "align_items"},
"vertical_align_content": {COLUMN: "justify_content"},
"vertical_align_items": {ROW: "align_items"},
}

def _update_property_name(self, name):
if aliases := self._ALIASES.get(name):
try:
name = aliases[self.direction]
except KeyError:
raise AttributeError(
f"{name!r} is not supported on a {self.direction}"
) from None

if new_name := self._DEPRECATED_PROPERTIES.get(name):
self._warn_deprecated(name, new_name, stacklevel=4)
name = new_name

return name

@staticmethod
def _warn_deprecated(old_name, new_name, stacklevel=3):
def _warn_deprecated(self, old_name, new_name, stacklevel=3):
msg = f"Pack.{old_name} is deprecated; use {new_name} instead"
warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel)

# Dot lookup

def __getattribute__(self, name):
if name.startswith("_"):
return super().__getattribute__(name)

# Align_items and alignment are paired. Both can never be set at the same time;
# if one is requested, and the other one is set, compute the requested value
# from the one that is set.
Expand Down Expand Up @@ -197,7 +218,7 @@ def __getattribute__(self, name):
# Only CENTER remains
return CENTER

return super().__getattribute__(type(self)._update_property_name(name))
return super().__getattribute__(self._update_property_name(name))

def __setattr__(self, name, value):
# Only one of these can be set at a time.
Expand Down Expand Up @@ -979,3 +1000,11 @@ def __css__(self) -> str:
# 'font_family', 'font_style', 'font_variant', 'font_weight', 'font_size'
# FONT_CHOICES
# ])

try:
_all_properties = Pack._BASE_ALL_PROPERTIES
except AttributeError:
# Travertino 0.3 compatibility
_all_properties = Pack._ALL_PROPERTIES

_all_properties[Pack].update(Pack._ALIASES)
48 changes: 48 additions & 0 deletions core/tests/style/pack/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from toga.style.pack import Pack


def with_init(**kwargs):
return Pack(**kwargs)


def with_update(**kwargs):
style = Pack()
style.update(**kwargs)
return style


def with_setattr(**kwargs):
style = Pack()
for name, value in kwargs.items():
setattr(style, name, value)
return style


def with_setitem(**kwargs):
style = Pack()
for name, value in kwargs.items():
style[name] = value
return style


def with_setitem_hyphen(**kwargs):
style = Pack()
for name, value in kwargs.items():
style[name.replace("_", "-")] = value
return style


def getitem(obj, name):
return obj[name]


def getitem_hyphen(obj, name):
return obj[name.replace("_", "-")]


def delitem(obj, name):
del obj[name]


def delitem_hyphen(obj, name):
del obj[name.replace("_", "-")]
108 changes: 108 additions & 0 deletions core/tests/style/pack/test_aliases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import pytest
from pytest import raises

from toga.style.pack import CENTER, COLUMN, END, ROW

from . import (
delitem,
delitem_hyphen,
getitem,
getitem_hyphen,
with_init,
with_setattr,
with_setitem,
with_setitem_hyphen,
with_update,
)


@pytest.mark.parametrize(
"css_name, row_alias, column_alias, default",
[
(
"align_items",
"vertical_align_items",
"horizontal_align_items",
None,
),
(
"justify_content",
"horizontal_align_content",
"vertical_align_content",
"start",
),
],
)
@pytest.mark.parametrize(
"style_with",
(with_init, with_update, with_setattr, with_setitem, with_setitem_hyphen),
)
@pytest.mark.parametrize("get_fn", (getattr, getitem, getitem_hyphen))
@pytest.mark.parametrize("del_fn", (delattr, delitem, delitem_hyphen))
def test_align(css_name, row_alias, column_alias, default, style_with, get_fn, del_fn):
"""The `vertical_align` and `horizontal_align` aliases work correctly."""
# Row alias
style = style_with(**{row_alias: CENTER})
assert get_fn(style, css_name) == CENTER

del_fn(style, row_alias)
assert get_fn(style, css_name) == default

style = style_with(**{css_name: CENTER})
assert get_fn(style, row_alias) == CENTER

del_fn(style, css_name)
assert get_fn(style, row_alias) == default

# Column alias
style = style_with(**{"direction": COLUMN, column_alias: CENTER})
assert get_fn(style, css_name) == CENTER

del_fn(style, column_alias)
assert get_fn(style, css_name) == default

style = style_with(**{"direction": COLUMN, css_name: CENTER})
assert get_fn(style, column_alias) == CENTER

del_fn(style, css_name)
assert get_fn(style, column_alias) == default

# Column alias is not accepted in a row, and vice versa.
def assert_invalid_alias(alias, direction):
style = style_with(direction=direction)
raises_kwargs = dict(
expected_exception=AttributeError,
match=f"'{alias}' is not supported on a {direction}",
)

with raises(**raises_kwargs):
get_fn(style, alias)
with raises(**raises_kwargs):
setattr(style, alias, END)
with raises(**raises_kwargs):
del_fn(style, alias)
with raises(**raises_kwargs):
style.update(**{"direction": direction, alias: END})
with raises(**raises_kwargs):
style.update(**{alias: END, "direction": direction})

assert_invalid_alias(column_alias, ROW)
assert_invalid_alias(row_alias, COLUMN)

# Consistent values of direction and alias can be updated together, regardless of
# argument order.
style = style_with(direction=COLUMN)
style.update(**{"direction": ROW, row_alias: CENTER})
assert get_fn(style, row_alias) == CENTER
assert get_fn(style, css_name) == CENTER
style.update(**{column_alias: END, "direction": COLUMN})
assert get_fn(style, column_alias) == END
assert get_fn(style, css_name) == END

style = style_with(direction=ROW)
style.update(**{"direction": COLUMN, column_alias: CENTER})
assert get_fn(style, column_alias) == CENTER
assert get_fn(style, css_name) == CENTER
style.update(**{row_alias: END, "direction": ROW})
assert get_fn(style, row_alias) == END
assert get_fn(style, css_name) == END
62 changes: 15 additions & 47 deletions core/tests/style/pack/test_deprecated_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,17 @@
Pack,
)


def with_init(name, value):
return Pack(**{name: value})


def with_update(name, value):
style = Pack()
style.update(**{name: value})
return style


def with_setattr(name, value):
style = Pack()
setattr(style, name, value)
return style


def with_setitem(name, value):
style = Pack()
style[name] = value
return style


def with_setitem_hyphen(name, value):
style = Pack()
style[name.replace("_", "-")] = value
return style


def getitem(obj, name):
return obj[name]


def getitem_hyphen(obj, name):
return obj[name.replace("_", "-")]


def delitem(obj, name):
del obj[name]


def delitem_hyphen(obj, name):
del obj[name.replace("_", "-")]
from . import (
delitem,
delitem_hyphen,
getitem,
getitem_hyphen,
with_init,
with_setattr,
with_setitem,
with_setitem_hyphen,
with_update,
)


@pytest.mark.parametrize(
Expand All @@ -80,7 +48,7 @@ def test_padding_margin(old_name, new_name, value, default, style_with, get_fn,
"""Padding (with deprecation warning) and margin map to each other."""
# Set the old name, then check the new name
with pytest.warns(DeprecationWarning):
style = style_with(old_name, value)
style = style_with(**{old_name: value})
assert get_fn(style, new_name) == value

# Delete the old name, check new name
Expand All @@ -89,7 +57,7 @@ def test_padding_margin(old_name, new_name, value, default, style_with, get_fn,
assert get_fn(style, new_name) == default

# Set the new name, then check the old name
style = style_with(new_name, value)
style = style_with(**{new_name: value})
with pytest.warns(DeprecationWarning):
assert get_fn(style, old_name) == value

Expand Down Expand Up @@ -128,7 +96,7 @@ def test_alignment_align_items(
"""Alignment (with deprecation warning) and align_items map to each other."""
# Set alignment, check align_items
with pytest.warns(DeprecationWarning):
style = style_with("alignment", alignment)
style = style_with(alignment=alignment)
style.update(direction=direction, text_direction=text_direction)

assert get_fn(style, "align_items") == align_items
Expand All @@ -139,7 +107,7 @@ def test_alignment_align_items(
assert get_fn(style, "align_items") is None

# Set align_items, check alignment
style = style_with("align_items", align_items)
style = style_with(align_items=align_items)
style.update(direction=direction, text_direction=text_direction)

with pytest.warns(DeprecationWarning):
Expand Down
Loading

0 comments on commit 1bc7002

Please sign in to comment.