Skip to content

Commit 1bc7002

Browse files
Add "horizontal" and "vertical" aliases for align_items and justify_content (#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]>
1 parent b1926ee commit 1bc7002

File tree

8 files changed

+269
-86
lines changed

8 files changed

+269
-86
lines changed

changes/3111.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

core/src/toga/style/mixin.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ class StyleProperty:
22
def __set_name__(self, mixin_cls, name):
33
self.name = name
44

5+
def __repr__(self):
6+
return f"<StyleProperty {self.name!r}>"
7+
58
def __get__(self, widget, mixin_cls):
69
return self if widget is None else getattr(widget.style, self.name)
710

@@ -21,8 +24,13 @@ def style_mixin(style_cls):
2124
"""
2225
}
2326

24-
for name in dir(style_cls):
25-
if not name.startswith("_") and isinstance(getattr(style_cls, name), property):
26-
mixin_dict[name] = StyleProperty()
27+
try:
28+
_all_properties = style_cls._BASE_ALL_PROPERTIES
29+
except AttributeError:
30+
# Travertino 0.3 compatibility
31+
_all_properties = style_cls._ALL_PROPERTIES
32+
33+
for name in _all_properties[style_cls]:
34+
mixin_dict[name] = StyleProperty()
2735

2836
return type(style_cls.__name__ + "Mixin", (), mixin_dict)

core/src/toga/style/pack.py

+37-8
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ def _hidden(self) -> bool:
115115
######################################################################
116116

117117
def update(self, **properties):
118+
# Set direction first, as it may change the interpretation of direction-based
119+
# property aliases in _update_property_name.
120+
if direction := properties.pop("direction", None):
121+
self.direction = direction
122+
118123
properties = {
119124
self._update_property_name(name.replace("-", "_")): value
120125
for name, value in properties.items()
@@ -128,7 +133,7 @@ def reapply(self, *args, **kwargs):
128133
warnings.filterwarnings("ignore", category=DeprecationWarning)
129134
super().reapply(*args, **kwargs)
130135

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

141-
@classmethod
142-
def _update_property_name(cls, name):
143-
if new_name := cls.DEPRECATED_PROPERTIES.get(name):
144-
cls._warn_deprecated(name, new_name, stacklevel=4)
146+
_ALIASES = {
147+
"horizontal_align_content": {ROW: "justify_content"},
148+
"horizontal_align_items": {COLUMN: "align_items"},
149+
"vertical_align_content": {COLUMN: "justify_content"},
150+
"vertical_align_items": {ROW: "align_items"},
151+
}
152+
153+
def _update_property_name(self, name):
154+
if aliases := self._ALIASES.get(name):
155+
try:
156+
name = aliases[self.direction]
157+
except KeyError:
158+
raise AttributeError(
159+
f"{name!r} is not supported on a {self.direction}"
160+
) from None
161+
162+
if new_name := self._DEPRECATED_PROPERTIES.get(name):
163+
self._warn_deprecated(name, new_name, stacklevel=4)
145164
name = new_name
146165

147166
return name
148167

149-
@staticmethod
150-
def _warn_deprecated(old_name, new_name, stacklevel=3):
168+
def _warn_deprecated(self, old_name, new_name, stacklevel=3):
151169
msg = f"Pack.{old_name} is deprecated; use {new_name} instead"
152170
warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel)
153171

154172
# Dot lookup
155173

156174
def __getattribute__(self, name):
175+
if name.startswith("_"):
176+
return super().__getattribute__(name)
177+
157178
# Align_items and alignment are paired. Both can never be set at the same time;
158179
# if one is requested, and the other one is set, compute the requested value
159180
# from the one that is set.
@@ -197,7 +218,7 @@ def __getattribute__(self, name):
197218
# Only CENTER remains
198219
return CENTER
199220

200-
return super().__getattribute__(type(self)._update_property_name(name))
221+
return super().__getattribute__(self._update_property_name(name))
201222

202223
def __setattr__(self, name, value):
203224
# Only one of these can be set at a time.
@@ -979,3 +1000,11 @@ def __css__(self) -> str:
9791000
# 'font_family', 'font_style', 'font_variant', 'font_weight', 'font_size'
9801001
# FONT_CHOICES
9811002
# ])
1003+
1004+
try:
1005+
_all_properties = Pack._BASE_ALL_PROPERTIES
1006+
except AttributeError:
1007+
# Travertino 0.3 compatibility
1008+
_all_properties = Pack._ALL_PROPERTIES
1009+
1010+
_all_properties[Pack].update(Pack._ALIASES)

core/tests/style/pack/__init__.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from toga.style.pack import Pack
2+
3+
4+
def with_init(**kwargs):
5+
return Pack(**kwargs)
6+
7+
8+
def with_update(**kwargs):
9+
style = Pack()
10+
style.update(**kwargs)
11+
return style
12+
13+
14+
def with_setattr(**kwargs):
15+
style = Pack()
16+
for name, value in kwargs.items():
17+
setattr(style, name, value)
18+
return style
19+
20+
21+
def with_setitem(**kwargs):
22+
style = Pack()
23+
for name, value in kwargs.items():
24+
style[name] = value
25+
return style
26+
27+
28+
def with_setitem_hyphen(**kwargs):
29+
style = Pack()
30+
for name, value in kwargs.items():
31+
style[name.replace("_", "-")] = value
32+
return style
33+
34+
35+
def getitem(obj, name):
36+
return obj[name]
37+
38+
39+
def getitem_hyphen(obj, name):
40+
return obj[name.replace("_", "-")]
41+
42+
43+
def delitem(obj, name):
44+
del obj[name]
45+
46+
47+
def delitem_hyphen(obj, name):
48+
del obj[name.replace("_", "-")]

core/tests/style/pack/test_aliases.py

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import pytest
2+
from pytest import raises
3+
4+
from toga.style.pack import CENTER, COLUMN, END, ROW
5+
6+
from . import (
7+
delitem,
8+
delitem_hyphen,
9+
getitem,
10+
getitem_hyphen,
11+
with_init,
12+
with_setattr,
13+
with_setitem,
14+
with_setitem_hyphen,
15+
with_update,
16+
)
17+
18+
19+
@pytest.mark.parametrize(
20+
"css_name, row_alias, column_alias, default",
21+
[
22+
(
23+
"align_items",
24+
"vertical_align_items",
25+
"horizontal_align_items",
26+
None,
27+
),
28+
(
29+
"justify_content",
30+
"horizontal_align_content",
31+
"vertical_align_content",
32+
"start",
33+
),
34+
],
35+
)
36+
@pytest.mark.parametrize(
37+
"style_with",
38+
(with_init, with_update, with_setattr, with_setitem, with_setitem_hyphen),
39+
)
40+
@pytest.mark.parametrize("get_fn", (getattr, getitem, getitem_hyphen))
41+
@pytest.mark.parametrize("del_fn", (delattr, delitem, delitem_hyphen))
42+
def test_align(css_name, row_alias, column_alias, default, style_with, get_fn, del_fn):
43+
"""The `vertical_align` and `horizontal_align` aliases work correctly."""
44+
# Row alias
45+
style = style_with(**{row_alias: CENTER})
46+
assert get_fn(style, css_name) == CENTER
47+
48+
del_fn(style, row_alias)
49+
assert get_fn(style, css_name) == default
50+
51+
style = style_with(**{css_name: CENTER})
52+
assert get_fn(style, row_alias) == CENTER
53+
54+
del_fn(style, css_name)
55+
assert get_fn(style, row_alias) == default
56+
57+
# Column alias
58+
style = style_with(**{"direction": COLUMN, column_alias: CENTER})
59+
assert get_fn(style, css_name) == CENTER
60+
61+
del_fn(style, column_alias)
62+
assert get_fn(style, css_name) == default
63+
64+
style = style_with(**{"direction": COLUMN, css_name: CENTER})
65+
assert get_fn(style, column_alias) == CENTER
66+
67+
del_fn(style, css_name)
68+
assert get_fn(style, column_alias) == default
69+
70+
# Column alias is not accepted in a row, and vice versa.
71+
def assert_invalid_alias(alias, direction):
72+
style = style_with(direction=direction)
73+
raises_kwargs = dict(
74+
expected_exception=AttributeError,
75+
match=f"'{alias}' is not supported on a {direction}",
76+
)
77+
78+
with raises(**raises_kwargs):
79+
get_fn(style, alias)
80+
with raises(**raises_kwargs):
81+
setattr(style, alias, END)
82+
with raises(**raises_kwargs):
83+
del_fn(style, alias)
84+
with raises(**raises_kwargs):
85+
style.update(**{"direction": direction, alias: END})
86+
with raises(**raises_kwargs):
87+
style.update(**{alias: END, "direction": direction})
88+
89+
assert_invalid_alias(column_alias, ROW)
90+
assert_invalid_alias(row_alias, COLUMN)
91+
92+
# Consistent values of direction and alias can be updated together, regardless of
93+
# argument order.
94+
style = style_with(direction=COLUMN)
95+
style.update(**{"direction": ROW, row_alias: CENTER})
96+
assert get_fn(style, row_alias) == CENTER
97+
assert get_fn(style, css_name) == CENTER
98+
style.update(**{column_alias: END, "direction": COLUMN})
99+
assert get_fn(style, column_alias) == END
100+
assert get_fn(style, css_name) == END
101+
102+
style = style_with(direction=ROW)
103+
style.update(**{"direction": COLUMN, column_alias: CENTER})
104+
assert get_fn(style, column_alias) == CENTER
105+
assert get_fn(style, css_name) == CENTER
106+
style.update(**{row_alias: END, "direction": ROW})
107+
assert get_fn(style, row_alias) == END
108+
assert get_fn(style, css_name) == END

core/tests/style/pack/test_deprecated_properties.py

+15-47
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,17 @@
1515
Pack,
1616
)
1717

18-
19-
def with_init(name, value):
20-
return Pack(**{name: value})
21-
22-
23-
def with_update(name, value):
24-
style = Pack()
25-
style.update(**{name: value})
26-
return style
27-
28-
29-
def with_setattr(name, value):
30-
style = Pack()
31-
setattr(style, name, value)
32-
return style
33-
34-
35-
def with_setitem(name, value):
36-
style = Pack()
37-
style[name] = value
38-
return style
39-
40-
41-
def with_setitem_hyphen(name, value):
42-
style = Pack()
43-
style[name.replace("_", "-")] = value
44-
return style
45-
46-
47-
def getitem(obj, name):
48-
return obj[name]
49-
50-
51-
def getitem_hyphen(obj, name):
52-
return obj[name.replace("_", "-")]
53-
54-
55-
def delitem(obj, name):
56-
del obj[name]
57-
58-
59-
def delitem_hyphen(obj, name):
60-
del obj[name.replace("_", "-")]
18+
from . import (
19+
delitem,
20+
delitem_hyphen,
21+
getitem,
22+
getitem_hyphen,
23+
with_init,
24+
with_setattr,
25+
with_setitem,
26+
with_setitem_hyphen,
27+
with_update,
28+
)
6129

6230

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

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

9159
# Set the new name, then check the old name
92-
style = style_with(new_name, value)
60+
style = style_with(**{new_name: value})
9361
with pytest.warns(DeprecationWarning):
9462
assert get_fn(style, old_name) == value
9563

@@ -128,7 +96,7 @@ def test_alignment_align_items(
12896
"""Alignment (with deprecation warning) and align_items map to each other."""
12997
# Set alignment, check align_items
13098
with pytest.warns(DeprecationWarning):
131-
style = style_with("alignment", alignment)
99+
style = style_with(alignment=alignment)
132100
style.update(direction=direction, text_direction=text_direction)
133101

134102
assert get_fn(style, "align_items") == align_items
@@ -139,7 +107,7 @@ def test_alignment_align_items(
139107
assert get_fn(style, "align_items") is None
140108

141109
# Set align_items, check alignment
142-
style = style_with("align_items", align_items)
110+
style = style_with(align_items=align_items)
143111
style.update(direction=direction, text_direction=text_direction)
144112

145113
with pytest.warns(DeprecationWarning):

0 commit comments

Comments
 (0)