From 49c2e1225cba2727ea87ed99cf284b84ed0eca91 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Fri, 19 Jul 2024 23:23:51 -0700 Subject: [PATCH 1/3] Provide stronger warnings about mutable state default values --- docs/guides/state_management.md | 79 ++++++++++++++++++++++++--------- docs/stylesheets/extra.css | 5 ++- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/docs/guides/state_management.md b/docs/guides/state_management.md index 9220701a6..bf8676805 100644 --- a/docs/guides/state_management.md +++ b/docs/guides/state_management.md @@ -21,6 +21,63 @@ def page(): me.text(state.val) ``` +## Use immutable default values + +Similar to [regular dataclasses which disallow mutable default values](https://docs.python.org/3/library/dataclasses.html#mutable-default-values), you need to avoid mutable default values such as list and dict for state classes. Using mutable default values can result in leaking state across sessions which can be a serious privacy issue. + +You **MUST** use immutable default values _or_ use dataclasses `field` initializer _or_ not set a default value. + +???+ success "Good: immutable default value" + Setting a default value to an immutable type like str is OK. + + ```py + @me.stateclass + class State: + a: str = "abc" + ``` + +???+ failure "Bad: mutable default value" + + This will raise an exception because dataclasses prevents you from using mutable collection types like `list` as the default value because this is a common footgun. + + ```py + @me.stateclass + class State: + a: list[str] = ["abc"] + ``` + +???+ success "Good: default factory" + + If you want to set a field to a mutable default value, use default_factory in the `field` function from the dataclasses module to create a new instance of the mutable default value for each instance of the state class. + + ```py + from dataclasses import field + + @me.stateclass + class State: + a: list[str] = field(default_factory=lambda: ["abc"]) + ``` + +???+ success "Good example of no default value" + + If you want a default of an empty list, you can just not define a default value and Mesop will automatically define an empty list default value. + + For example, if you write the following: + + ```py + @me.stateclass + class State: + a: list[str] + ``` + + It's the equivalent of: + + ```py + @me.stateclass + class State: + a: list[str] = field(default_factory=list) + ``` + ## How State Works `me.stateclass` is a class decorator which tells Mesop that this class can be retrieved using the `me.state` method, which will return the state instance for the current user session. @@ -109,28 +166,6 @@ If you didn't explicitly annotate NestedState as a dataclass, then you would get ## Tips -### Set mutable default values (e.g. list) correctly - -Similar to [regular dataclasses which disallow mutable default values](https://docs.python.org/3/library/dataclasses.html#mutable-default-values), you need to avoid mutable default values such as list and dict for state classes. Allowing mutable default values could lead to erroneously sharing state across users which would be bad! - -**Bad:** Setting a mutable field directly on a state class attribute. - -```py -@me.stateclass -class State: - x: list[str] = ["a"] -``` - -**Good:** Use dataclasses `field` method to define a default factory so a new instance of the mutable value is created with each state class instance. - -```py -from dataclasses import field - -@me.stateclass -class State: - x: list[str] = field(default_factory=lambda: ["a"]) -``` - ### State performance issues Because the state class is serialized and sent back and forth between the client and server, you should try to keep the state class reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app. Instead, you should try to store large data outside of the state class (e.g. in-memory, filesystem, database, external service) and retrieve the data as needed for rendering. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 25a426c5d..008ee42d0 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -161,8 +161,9 @@ p { font-size: 0.8rem; } -.md-typeset .admonition { - font-size: 0.75rem; +.md-typeset .admonition, +.md-typeset details { + font-size: 0.8rem; } .highlight code { From 86e927c86591e18220aaf4ea634b87abdd5f4644 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Fri, 19 Jul 2024 23:30:56 -0700 Subject: [PATCH 2/3] update --- docs/guides/state_management.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/guides/state_management.md b/docs/guides/state_management.md index bf8676805..8ee204ee4 100644 --- a/docs/guides/state_management.md +++ b/docs/guides/state_management.md @@ -38,7 +38,7 @@ You **MUST** use immutable default values _or_ use dataclasses `field` initializ ???+ failure "Bad: mutable default value" - This will raise an exception because dataclasses prevents you from using mutable collection types like `list` as the default value because this is a common footgun. + The following will raise an exception because dataclasses prevents you from using mutable collection types like `list` as the default value because this is a common footgun. ```py @me.stateclass @@ -46,6 +46,14 @@ You **MUST** use immutable default values _or_ use dataclasses `field` initializ a: list[str] = ["abc"] ``` + If you set a default value to an instance of a custom type, an exception will not be raised, but you will be dangerously sharing the same mutable instance across sessions which could cause a serious privacy issue. + + ```py + @me.stateclass + class State: + a: MutableClass = MutableClass() + ``` + ???+ success "Good: default factory" If you want to set a field to a mutable default value, use default_factory in the `field` function from the dataclasses module to create a new instance of the mutable default value for each instance of the state class. From 719e3e08d8711dc229de66c592324ab910187d30 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Sat, 20 Jul 2024 21:22:32 -0700 Subject: [PATCH 3/3] Update state_management.md --- docs/guides/state_management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/state_management.md b/docs/guides/state_management.md index 8ee204ee4..5edf9dbea 100644 --- a/docs/guides/state_management.md +++ b/docs/guides/state_management.md @@ -66,7 +66,7 @@ You **MUST** use immutable default values _or_ use dataclasses `field` initializ a: list[str] = field(default_factory=lambda: ["abc"]) ``` -???+ success "Good example of no default value" +???+ success "Good: no default value" If you want a default of an empty list, you can just not define a default value and Mesop will automatically define an empty list default value.