Skip to content

Commit

Permalink
Merge pull request #143 from HalfWhitt/style-mapping-abc
Browse files Browse the repository at this point in the history
Add union operators to BaseStyle
  • Loading branch information
freakboy3742 committed Mar 6, 2024
2 parents 6adad4c + 5d37e73 commit ad49bf1
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 8 deletions.
19 changes: 15 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@
*~
.*.sw[op]
*.egg-info
*.dist-info
.eggs
.coverage
.coverage.*
coverage.xml
dist
build
logs
_build
local
distribute-*
.coverage
docs/env
local
*venv*
.idea
Pipfile*
.tox
.DS_Store
pip-selfcheck.json
pyvenv.cfg
.vscode
venv*
.idea
.envrc
pip-wheel-metadata
1 change: 1 addition & 0 deletions changes/143.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BaseStyle now supports |, |=, and 'in' operators.
53 changes: 50 additions & 3 deletions src/travertino/declaration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import defaultdict
from typing import Mapping
from warnings import filterwarnings, warn

from .colors import color
Expand Down Expand Up @@ -142,6 +143,9 @@ def __delete__(self, obj):
else:
obj.apply(self.name, self.initial)

def is_set_on(self, obj):
return hasattr(obj, f"_{self.name}")


class directional_property:
DIRECTIONS = [TOP, RIGHT, BOTTOM, LEFT]
Expand Down Expand Up @@ -199,6 +203,11 @@ def __delete__(self, obj):
for direction in self.DIRECTIONS:
del obj[self.format(direction)]

def is_set_on(self, obj):
return any(
hasattr(obj, self.format(direction)) for direction in self.DIRECTIONS
)


class BaseStyle:
"""A base class for style declarations.
Expand Down Expand Up @@ -282,19 +291,57 @@ def __delitem__(self, name):
else:
raise KeyError(name)

def keys(self):
return {
name
for name in self._PROPERTIES[self.__class__]
if hasattr(self, f"_{name}")
}

def items(self):
return [
(name, value)
for name in self._PROPERTIES[self.__class__]
if (value := getattr(self, f"_{name}", None)) is not None
]

def keys(self):
return {
def __len__(self):
return sum(
1 for name in self._PROPERTIES[self.__class__] if hasattr(self, f"_{name}")
)

def __contains__(self, name):
return name in self._ALL_PROPERTIES[self.__class__] and (
getattr(self.__class__, name).is_set_on(self)
)

def __iter__(self):
yield from (
name
for name in self._PROPERTIES[self.__class__]
if hasattr(self, f"_{name}")
}
)

def __or__(self, other):
if isinstance(other, BaseStyle):
if self.__class__ is not other.__class__:
return NotImplemented
elif not isinstance(other, Mapping):
return NotImplemented

result = self.copy()
result.update(**other)
return result

def __ior__(self, other):
if isinstance(other, BaseStyle):
if self.__class__ is not other.__class__:
return NotImplemented
elif not isinstance(other, Mapping):
return NotImplemented

self.update(**other)
return self

######################################################################
# Get the rendered form of the style declaration
Expand Down
114 changes: 113 additions & 1 deletion tests/test_declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ class DeprecatedStyle(BaseStyle):
DeprecatedStyle.directional_property("thing%s")


class StyleSubclass(Style):
pass


class DeprecatedStyleSubclass(DeprecatedStyle):
pass


class Sibling(BaseStyle):
pass


def test_invalid_style():
with pytest.raises(ValueError):
# Define an invalid initial value on a validated property
Expand Down Expand Up @@ -504,14 +516,17 @@ def test_dict(StyleClass):
thing=(30, 40, 50, 60),
)

assert style.keys() == {
expected_keys = {
"explicit_const",
"explicit_value",
"thing_bottom",
"thing_left",
"thing_right",
"thing_top",
}

assert style.keys() == expected_keys

assert sorted(style.items()) == sorted(
[
("explicit_const", "value2"),
Expand All @@ -523,6 +538,20 @@ def test_dict(StyleClass):
]
)

# Properties that are set are in the keys.
for name in expected_keys:
assert name in style

# Directional properties with one or more of the aliased properties set also count.
assert "thing" in style

# Valid properties that haven't been set are not in the keys.
assert "implicit" not in style
assert "explicit_none" not in style

# Neither are invalid properties.
assert "invalid_property" not in style

# A property can be set, retrieved and cleared using the attribute name
style["thing-bottom"] = 10
assert style["thing-bottom"] == 10
Expand Down Expand Up @@ -550,6 +579,89 @@ def test_dict(StyleClass):
del style["no-such-property"]


@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle])
@pytest.mark.parametrize("instantiate", [True, False])
def test_union_operators(StyleClass, instantiate):
"""Styles support | and |= with dicts and with their own class."""
left = StyleClass(explicit_value=VALUE1, implicit=VALUE2)

style_dict = {"thing_top": 5, "implicit": VALUE3}
right = StyleClass(**style_dict) if instantiate else style_dict

# Standard operator
result = left | right

# Original objects unchanged
assert left["explicit_value"] == VALUE1
assert left["implicit"] == VALUE2

assert right["thing_top"] == 5
assert right["implicit"] == VALUE3

# Unshared properties assigned
assert result["explicit_const"] == VALUE1
assert result["thing_top"] == 5

# Common property overridden by second operand
assert result["implicit"] == VALUE3

# In-place version
left |= right

# Common property updated on lefthand
assert left["explicit_value"] == VALUE1
assert left["implicit"] == VALUE3

# Righthand unchanged
assert right["thing_top"] == 5
assert right["implicit"] == VALUE3


@pytest.mark.parametrize(
"StyleClass, OtherClass",
[
(Style, StyleSubclass),
(Style, Sibling),
(Style, int),
(Style, list),
(DeprecatedStyle, DeprecatedStyleSubclass),
(DeprecatedStyle, Sibling),
(DeprecatedStyle, int),
(DeprecatedStyle, list),
],
)
def test_union_operators_invalid_type(StyleClass, OtherClass):
"""Styles do not support | or |= with other style classes or with non-mappings."""

left = StyleClass()
right = OtherClass()

with pytest.raises(TypeError, match=r"unsupported operand type"):
left | right

with pytest.raises(TypeError, match=r"unsupported operand type"):
left |= right


@pytest.mark.parametrize("StyleClass", [Style, DeprecatedStyle])
@pytest.mark.parametrize(
"right, error",
[
({"implicit": "bogus_value"}, ValueError),
({"bogus_key": 3.12}, NameError),
],
)
def test_union_operators_invalid_key_value(StyleClass, right, error):
"""Operators will accept any mapping, but invalid keys/values are still an error."""
left = StyleClass()

with pytest.raises(error):
left | right

with pytest.raises(error):
left |= right


def test_deprecated_class_methods():
class OldStyle(BaseStyle):
pass
Expand Down

0 comments on commit ad49bf1

Please sign in to comment.