Skip to content

Commit 678cdf6

Browse files
Move aliasing logic from Core into Travertino (#3213)
* Moved property aliasing logic from Toga into Travertino * Test (and further fix) __contains__ checks * Move deprecation check into method * Added change notes * Check alias validity for __contains__ * Removed paired_property; simplified Condition; made aliased_property more versatile * Fix annotations for 3.9 * Pulled out parent class, fixed core coverage * Changed separate __get__ methods back to dictionary * Fixed missed test failure... * Switch condition order for better Black formatting * Remove redundant assignment * Remove unneccesary deprecated param * removed vestigial comment and simplified alias deprecation-checking * Removed match() main_name param; hard-coded alignment_property initialization * Typo fixes in strings * Added Travertino tests * Added some missing docstrings * Test for error message, and make message clearer * Underscored compat classes, added comments, simplified logic * Fix deprecation message * Check deprecation message * Fixed typos / reworded comments * Simplify conditional and add one more comment * Simplify logic MORE * Two minor looping optimisations. --------- Co-authored-by: Russell Keith-Magee <[email protected]>
1 parent 952d2b5 commit 678cdf6

File tree

9 files changed

+494
-146
lines changed

9 files changed

+494
-146
lines changed

changes/3213.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Travertino now has an ``aliased_property`` descriptor to support declaration of property name aliases in styles.

changes/3213.misc.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pack's aliases, deprecated names, and hyphenated style names now function correctly with ``name in style``.

core/src/toga/style/pack.py

+148-144
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
VISIBLE,
3737
)
3838
from travertino.layout import BaseBox
39+
from travertino.properties.aliased import Condition, aliased_property
3940
from travertino.properties.shorthand import directional_property
4041
from travertino.properties.validated import validated_property
4142
from travertino.size import BaseIntrinsicSize
@@ -57,9 +58,123 @@
5758

5859
PACK = "pack"
5960

60-
# Used in backwards compatibility section below
61-
ALIGNMENT = "alignment"
62-
ALIGN_ITEMS = "align_items"
61+
######################################################################
62+
# 2024-12: Backwards compatibility for Toga < 0.5.0
63+
######################################################################
64+
65+
66+
class _AlignmentCondition(Condition):
67+
def __init__(self, main_value, /, **properties):
68+
super().__init__(**properties)
69+
self.main_value = main_value
70+
71+
def match(self, style, main_name=None):
72+
# main_name can't be accessed the "normal" way without causing a loop; we need
73+
# to access the private stored value.
74+
return (
75+
super().match(style) and getattr(style, f"_{main_name}") == self.main_value
76+
)
77+
78+
79+
class _alignment_property(validated_property):
80+
# Alignment is deprecated in favor of align_items, but the two share a complex
81+
# relationship because they don't use the same set of values; translating from one
82+
# to the other may require knowing the value of direction and text_direction as
83+
# well.
84+
85+
# Both names exist as actual properties on the style object. If one of them has been
86+
# set, that one is the source of truth; if the other name is requested, its value
87+
# is computed / translated. They can never both be set at the same time; setting
88+
# one deletes any value stored in the other.
89+
90+
def __set_name__(self, owner, name):
91+
# Hard-coded because it's only called on alignment, not align_items.
92+
93+
self.name = "alignment"
94+
owner._BASE_ALL_PROPERTIES[owner].add("alignment")
95+
self.other = "align_items"
96+
self.derive = {
97+
_AlignmentCondition(CENTER): CENTER,
98+
_AlignmentCondition(START, direction=COLUMN, text_direction=LTR): LEFT,
99+
_AlignmentCondition(START, direction=COLUMN, text_direction=RTL): RIGHT,
100+
_AlignmentCondition(START, direction=ROW): TOP,
101+
_AlignmentCondition(END, direction=COLUMN, text_direction=LTR): RIGHT,
102+
_AlignmentCondition(END, direction=COLUMN, text_direction=RTL): LEFT,
103+
_AlignmentCondition(END, direction=ROW): BOTTOM,
104+
}
105+
106+
# Replace the align_items validated_property with another instance of this
107+
# class. This is needed so accessing or setting either one will properly
108+
# reference the other.
109+
owner.align_items = _alignment_property(START, CENTER, END)
110+
owner.align_items.name = "align_items"
111+
owner.align_items.other = "alignment"
112+
owner.align_items.derive = {
113+
# Invert each condition so that it maps in the opposite direction.
114+
_AlignmentCondition(result, **condition.properties): condition.main_value
115+
for condition, result in self.derive.items()
116+
}
117+
118+
def __get__(self, obj, objtype=None):
119+
if obj is None:
120+
return self
121+
122+
self.warn_if_deprecated()
123+
124+
if hasattr(obj, f"_{self.other}"):
125+
# If the other property is set, attempt to translate.
126+
for condition, value in self.derive.items():
127+
if condition.match(obj, main_name=self.other):
128+
return value
129+
130+
# If the other property isn't set (or no condition is valid), access this
131+
# property as usual.
132+
return super().__get__(obj)
133+
134+
def __set__(self, obj, value):
135+
# This won't be executed until @dataclass is added
136+
if value is self: # pragma: no cover
137+
# This happens during autogenerated dataclass __init__ when no value is
138+
# supplied.
139+
return
140+
141+
self.warn_if_deprecated()
142+
143+
# Delete the other property when setting this one.
144+
try:
145+
delattr(obj, f"_{self.other}")
146+
except AttributeError:
147+
pass
148+
super().__set__(obj, value)
149+
150+
def __delete__(self, obj):
151+
self.warn_if_deprecated()
152+
153+
# Delete the other property too.
154+
try:
155+
delattr(obj, f"_{self.other}")
156+
except AttributeError:
157+
pass
158+
super().__delete__(obj)
159+
160+
def is_set_on(self, obj):
161+
self.warn_if_deprecated()
162+
163+
# Counts as set if *either* of the two properties is set.
164+
return super().is_set_on(obj) or hasattr(obj, f"_{self.other}")
165+
166+
def warn_if_deprecated(self):
167+
if self.name == "alignment":
168+
warnings.warn(
169+
"Pack.alignment is deprecated. Use Pack.align_items instead.",
170+
DeprecationWarning,
171+
stacklevel=3,
172+
)
173+
174+
175+
######################################################################
176+
# End backwards compatibility
177+
######################################################################
63178

64179

65180
class Pack(BaseStyle):
@@ -77,9 +192,6 @@ class IntrinsicSize(BaseIntrinsicSize):
77192
visibility: str = validated_property(VISIBLE, HIDDEN, initial=VISIBLE)
78193
direction: str = validated_property(ROW, COLUMN, initial=ROW)
79194
align_items: str | None = validated_property(START, CENTER, END)
80-
alignment: str | None = validated_property(
81-
LEFT, RIGHT, TOP, BOTTOM, CENTER
82-
) # Deprecated
83195
justify_content: str | None = validated_property(START, CENTER, END, initial=START)
84196
gap: int = validated_property(integer=True, initial=0)
85197

@@ -109,153 +221,48 @@ class IntrinsicSize(BaseIntrinsicSize):
109221
font_weight: str = validated_property(*FONT_WEIGHTS, initial=NORMAL)
110222
font_size: int = validated_property(integer=True, initial=SYSTEM_DEFAULT_FONT_SIZE)
111223

112-
@classmethod
113-
def _debug(cls, *args: str) -> None: # pragma: no cover
114-
print(" " * cls._depth, *args)
224+
######################################################################
225+
# Directional aliases
226+
######################################################################
115227

116-
@property
117-
def _hidden(self) -> bool:
118-
"""Does this style declaration define an object that should be hidden."""
119-
return self.visibility == HIDDEN
228+
horizontal_align_content: str | None = aliased_property(
229+
source={Condition(direction=ROW): "justify_content"}
230+
)
231+
horizontal_align_items: str | None = aliased_property(
232+
source={Condition(direction=COLUMN): "align_items"}
233+
)
234+
vertical_align_content: str | None = aliased_property(
235+
source={Condition(direction=COLUMN): "justify_content"}
236+
)
237+
vertical_align_items: str | None = aliased_property(
238+
source={Condition(direction=ROW): "align_items"}
239+
)
120240

121241
######################################################################
122242
# 2024-12: Backwards compatibility for Toga < 0.5.0
123243
######################################################################
124244

125-
def update(self, **properties):
126-
# Set direction first, as it may change the interpretation of direction-based
127-
# property aliases in _update_property_name.
128-
if direction := properties.pop("direction", None):
129-
self.direction = direction
245+
padding: int | tuple[int] = aliased_property(source="margin", deprecated=True)
246+
padding_top: int = aliased_property(source="margin_top", deprecated=True)
247+
padding_right: int = aliased_property(source="margin_right", deprecated=True)
248+
padding_bottom: int = aliased_property(source="margin_bottom", deprecated=True)
249+
padding_left: int = aliased_property(source="margin_left", deprecated=True)
130250

131-
properties = {
132-
self._update_property_name(name.replace("-", "_")): value
133-
for name, value in properties.items()
134-
}
135-
super().update(**properties)
136-
137-
_DEPRECATED_PROPERTIES = {
138-
# Map each deprecated property name to its replacement.
139-
# alignment / align_items is handled separately.
140-
"padding": "margin",
141-
"padding_top": "margin_top",
142-
"padding_right": "margin_right",
143-
"padding_bottom": "margin_bottom",
144-
"padding_left": "margin_left",
145-
}
146-
147-
_ALIASES = {
148-
"horizontal_align_content": {ROW: "justify_content"},
149-
"horizontal_align_items": {COLUMN: "align_items"},
150-
"vertical_align_content": {COLUMN: "justify_content"},
151-
"vertical_align_items": {ROW: "align_items"},
152-
}
153-
154-
def _update_property_name(self, name):
155-
if aliases := self._ALIASES.get(name):
156-
try:
157-
name = aliases[self.direction]
158-
except KeyError:
159-
raise AttributeError(
160-
f"{name!r} is not supported on a {self.direction}"
161-
) from None
162-
163-
if new_name := self._DEPRECATED_PROPERTIES.get(name):
164-
self._warn_deprecated(name, new_name, stacklevel=4)
165-
name = new_name
166-
167-
return name
168-
169-
def _warn_deprecated(self, old_name, new_name, stacklevel=3):
170-
msg = f"Pack.{old_name} is deprecated; use {new_name} instead"
171-
warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel)
172-
173-
# Dot lookup
174-
175-
def __getattribute__(self, name):
176-
if name.startswith("_"):
177-
return super().__getattribute__(name)
178-
179-
# Align_items and alignment are paired. Both can never be set at the same time;
180-
# if one is requested, and the other one is set, compute the requested value
181-
# from the one that is set.
182-
if name == ALIGN_ITEMS and (alignment := super().__getattribute__(ALIGNMENT)):
183-
if alignment == CENTER:
184-
return CENTER
185-
186-
if self.direction == ROW:
187-
if alignment == TOP:
188-
return START
189-
if alignment == BOTTOM:
190-
return END
191-
192-
# No remaining valid combinations
193-
return None
194-
195-
# direction must be COLUMN
196-
if alignment == LEFT:
197-
return START if self.text_direction == LTR else END
198-
if alignment == RIGHT:
199-
return START if self.text_direction == RTL else END
200-
201-
# No remaining valid combinations
202-
return None
203-
204-
if name == ALIGNMENT:
205-
# Warn, whether it's set or not.
206-
self._warn_deprecated(ALIGNMENT, ALIGN_ITEMS)
207-
208-
if align_items := super().__getattribute__(ALIGN_ITEMS):
209-
if align_items == START:
210-
if self.direction == COLUMN:
211-
return LEFT if self.text_direction == LTR else RIGHT
212-
return TOP # for ROW
213-
214-
if align_items == END:
215-
if self.direction == COLUMN:
216-
return RIGHT if self.text_direction == LTR else LEFT
217-
return BOTTOM # for ROW
218-
219-
# Only CENTER remains
220-
return CENTER
221-
222-
return super().__getattribute__(self._update_property_name(name))
223-
224-
def __setattr__(self, name, value):
225-
# Only one of these can be set at a time.
226-
if name == ALIGN_ITEMS:
227-
super().__delattr__(ALIGNMENT)
228-
elif name == ALIGNMENT:
229-
self._warn_deprecated(ALIGNMENT, ALIGN_ITEMS)
230-
super().__delattr__(ALIGN_ITEMS)
231-
232-
super().__setattr__(self._update_property_name(name), value)
233-
234-
def __delattr__(self, name):
235-
# If one of the two is being deleted, delete the other also.
236-
if name == ALIGN_ITEMS:
237-
super().__delattr__(ALIGNMENT)
238-
elif name == ALIGNMENT:
239-
self._warn_deprecated(ALIGNMENT, ALIGN_ITEMS)
240-
super().__delattr__(ALIGN_ITEMS)
241-
242-
super().__delattr__(self._update_property_name(name))
243-
244-
# Index notation
245-
246-
def __getitem__(self, name):
247-
return super().__getitem__(self._update_property_name(name.replace("-", "_")))
248-
249-
def __setitem__(self, name, value):
250-
super().__setitem__(self._update_property_name(name.replace("-", "_")), value)
251-
252-
def __delitem__(self, name):
253-
super().__delitem__(self._update_property_name(name.replace("-", "_")))
251+
alignment: str | None = _alignment_property(TOP, RIGHT, BOTTOM, LEFT, CENTER)
254252

255253
######################################################################
256254
# End backwards compatibility
257255
######################################################################
258256

257+
@classmethod
258+
def _debug(cls, *args: str) -> None: # pragma: no cover
259+
print(" " * cls._depth, *args)
260+
261+
@property
262+
def _hidden(self) -> bool:
263+
"""Does this style declaration define an object that should be hidden."""
264+
return self.visibility == HIDDEN
265+
259266
def apply(self, *names: list[str]) -> None:
260267
if self._applicator:
261268
for name in names or self._PROPERTIES:
@@ -939,6 +946,3 @@ def __css__(self) -> str:
939946
css.append(f"font-variant: {self.font_variant};")
940947

941948
return " ".join(css)
942-
943-
944-
Pack._BASE_ALL_PROPERTIES[Pack].update(Pack._ALIASES)

core/tests/style/pack/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,13 @@ def delitem(obj, name):
4646

4747
def delitem_hyphen(obj, name):
4848
del obj[name.replace("_", "-")]
49+
50+
51+
def assert_name_in(name, style):
52+
assert name in style
53+
assert name.replace("_", "-") in style
54+
55+
56+
def assert_name_not_in(name, style):
57+
assert name not in style
58+
assert name.replace("_", "-") not in style

0 commit comments

Comments
 (0)