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

Prototype: safe-guard against mutable default values #647

Closed
wants to merge 1 commit into from
Closed
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
19 changes: 16 additions & 3 deletions mesop/dataclass_utils/dataclass_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import base64
import json
from dataclasses import asdict, dataclass, field, is_dataclass
from dataclasses import Field, asdict, dataclass, field, is_dataclass
from datetime import datetime
from io import StringIO
from typing import Any, Type, TypeVar, cast, get_origin, get_type_hints
Expand Down Expand Up @@ -32,15 +32,28 @@ def _check_has_pandas():
_has_pandas = _check_has_pandas()


def is_primitive(obj: Any):
return isinstance(obj, (int, float, bool, str, type(None), complex, bytes))


def dataclass_with_defaults(cls: Type[C]) -> Type[C]:
"""
Provides defaults for every attribute in a dataclass (recursively) so
Mesop developers don't need to manually set default values
"""
pass
for name in cls.__dict__:
if name.startswith("__") and name.endswith("__"):
continue
# If default is already set, make sure it's a primitive value
if not isinstance(cls.__dict__[name], Field) and not is_primitive(
cls.__dict__[name]
):
raise Exception(
"Detected potentially mutable type | name", name, "type", type.__name__
)
annotations = get_type_hints(cls)
for name, type_hint in annotations.items():
if name not in cls.__dict__: # Skip if default already set
if name not in cls.__dict__:
if type_hint == int:
setattr(cls, name, field(default=0))
elif type_hint == float:
Expand Down
41 changes: 41 additions & 0 deletions mesop/dataclass_utils/dataclass_utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pandas as pd
import pytest

import mesop.protos.ui_pb2 as pb
from mesop.dataclass_utils.dataclass_utils import (
dataclass_with_defaults,
serialize_dataclass,
Expand Down Expand Up @@ -286,5 +287,45 @@ def test_serialize_deserialize_state_with_list_dict():
assert new_state == state


def test_raises_exception_for_mutable_default_value_default_list():
with pytest.raises(Exception) as exc_info:

@dataclass_with_defaults
class StateWithMutableDefaultValue:
mutable_list: list[str] = []

assert "Detected potentially mutable type" in str(exc_info.value)


def test_raises_exception_for_mutable_default_value_default_list_no_type_annotation():
with pytest.raises(Exception) as exc_info:

@dataclass_with_defaults
class StateWithMutableDefaultValue:
mutable_list = []

assert "Detected potentially mutable type" in str(exc_info.value)


def test_raises_exception_for_mutable_default_value_proto():
with pytest.raises(Exception) as exc_info:

@dataclass_with_defaults
class StateWithMutableProto:
proto: pb.Style = pb.Style()

assert "Detected potentially mutable type" in str(exc_info.value)


def test_proto_works():
with pytest.raises(Exception) as exc_info:

@dataclass_with_defaults
class StateWithMutableProto:
proto: pb.Style

assert "FAIL" in str(exc_info.value)


if __name__ == "__main__":
raise SystemExit(pytest.main([__file__]))
1 change: 1 addition & 0 deletions mesop/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mesop.examples import index as index
from mesop.examples import integrations as integrations
from mesop.examples import many_checkboxes as many_checkboxes
from mesop.examples import mutable_state as mutable_state
from mesop.examples import navigate_advanced as navigate_advanced
from mesop.examples import nested as nested
from mesop.examples import on_load as on_load
Expand Down
41 changes: 41 additions & 0 deletions mesop/examples/mutable_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import mesop as me
import mesop.components.button.button_pb2 as button_pb


class Bar:
val: str
pass


class Foo:
val: str


b = button_pb.ButtonType()
b.color = "abc"
print("~ b", b)
c = button_pb.ButtonType()
print("~ c", c)


@me.stateclass
class State:
button: button_pb.ButtonType
a: str
b: Bar


s = State()
s.button.color = "foo"
print("s.button (1)", s.button)
s.b.val = "hi"
print(s)
print("---")
b = State()
print("b.button (2)", b.button)
print(b)


@me.page()
def page():
me.text("mutable state")
Loading