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

Add "horizontal" and "vertical" aliases for align_items and justify_content #3113

Merged
merged 3 commits into from
Jan 21, 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/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
Comment on lines +27 to +31
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like MicroPython doesn't support the __init_subclass__ method used by BaseStyle, so this will have to be revisited once we start actually running Travertino on MicroPython rather than simply importing it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh -_-

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually turns out it's super simple, I can just replace the subclass_init with a pair of 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
Loading