Repository: napari/napari

Issue:
Set `use_enum_values` to False on EventedModel
## 🧰 Task
Assuming everyone is ok with it, we should remove `use_enum_values = True` from our EventedModel.Config.

this would make it so that internally, accessing the value of an Enum field on an EventedModel would give you the enum object itself (this is the pydantic default), rather than the string version.

This comes in handy when comparing a current value to its field default value.   It will be particularly annoying in settings to keep it as is.  For instance, #2933 added the first "proper" enum type to our settings... so checking whether the current `application.playback_mode` is the default value or not requires always doing some conversion (either that, or setting the default to a string, instead of an enum, which is also weird).


Discussion:
I just ran into this morning and it confused me for a little while, so I'd be in favor of going ahead with this task.

If it's helpful, here are some test cases that all fail that help to demonstrate the weirdness.

```python
class TestMode(StringEnum):
    UNKNOWN = auto()
    SOME_MODE = auto()
    ANOTHER_MODE = auto()

class ModelWithStringEnum(EventedModel):
    mode: TestMode = TestMode.UNKNOWN

def test_evented_model_with_string_enum_default():
    model = ModelWithStringEnum()
    assert model.mode == TestMode.UNKNOWN

def test_evented_model_with_string_enum_parameter():
    model = ModelWithStringEnum(mode=TestMode.SOME_MODE)
    assert model.mode == TestMode.SOME_MODE

def test_evented_model_with_string_enum_setter():
    model = ModelWithStringEnum()
    model.mode = TestMode.SOME_MODE
    assert model.mode == TestMode.SOME_MODE
```

Comparing to the string values (e.g. `'some_mode'`) pass, which I can see as a convenient behavior, but not at the cost of comparing against the enum values.

Resolving patch:
diff --git a/napari/_vispy/vispy_points_layer.py b/napari/_vispy/vispy_points_layer.py
--- a/napari/_vispy/vispy_points_layer.py
+++ b/napari/_vispy/vispy_points_layer.py
@@ -168,7 +168,7 @@ def _on_blending_change(self, event=None):
         self.node.set_gl_state(self.layer.blending)
 
         text_node = self._get_text_node()
-        text_node.set_gl_state(self.layer.text.blending)
+        text_node.set_gl_state(str(self.layer.text.blending))
         self.node.update()
 
     def close(self):
diff --git a/napari/utils/events/evented_model.py b/napari/utils/events/evented_model.py
--- a/napari/utils/events/evented_model.py
+++ b/napari/utils/events/evented_model.py
@@ -114,10 +114,6 @@ class Config:
         # whether to treat any underscore non-class var attrs as private
         # https://pydantic-docs.helpmanual.io/usage/models/#private-model-attributes
         underscore_attrs_are_private = True
-        # whether to populate models with the value property of enums, rather
-        # than the raw enum. This may be useful if you want to serialise
-        # model.dict() later
-        use_enum_values = True
         # whether to validate field defaults (default: False)
         validate_all = True
         # https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeljson
diff --git a/napari/utils/misc.py b/napari/utils/misc.py
--- a/napari/utils/misc.py
+++ b/napari/utils/misc.py
@@ -267,6 +267,16 @@ def __str__(self):
         """
         return self.value
 
+    def __eq__(self, other):
+        if type(self) is type(other):
+            return self is other
+        elif isinstance(other, str):
+            return str(self) == other
+        return NotImplemented
+
+    def __hash__(self):
+        return hash(str(self))
+
 
 camel_to_snake_pattern = re.compile(r'(.)([A-Z][a-z]+)')
 camel_to_spaces_pattern = re.compile(


Test patch:
diff --git a/napari/layers/points/_tests/test_points.py b/napari/layers/points/_tests/test_points.py
--- a/napari/layers/points/_tests/test_points.py
+++ b/napari/layers/points/_tests/test_points.py
@@ -9,6 +9,7 @@
 from napari._tests.utils import check_layer_world_data_extent
 from napari.layers import Points
 from napari.layers.points._points_utils import points_to_squares
+from napari.layers.utils._text_constants import Anchor
 from napari.layers.utils.color_manager import ColorProperties
 from napari.utils.colormaps.standardize_color import transform_color
 
@@ -710,7 +711,7 @@ def test_set_text_with_kwarg_dict(properties):
         'color': [0, 0, 0, 1],
         'rotation': 10,
         'translation': [5, 5],
-        'anchor': 'upper_left',
+        'anchor': Anchor.UPPER_LEFT,
         'size': 10,
         'visible': True,
     }
diff --git a/napari/layers/shapes/_tests/test_shapes.py b/napari/layers/shapes/_tests/test_shapes.py
--- a/napari/layers/shapes/_tests/test_shapes.py
+++ b/napari/layers/shapes/_tests/test_shapes.py
@@ -7,7 +7,7 @@
 
 from napari._tests.utils import check_layer_world_data_extent
 from napari.layers import Shapes
-from napari.layers.utils._text_constants import TextMode
+from napari.layers.utils._text_constants import Anchor, TextMode
 from napari.utils.colormaps.standardize_color import transform_color
 
 
@@ -277,7 +277,7 @@ def test_set_text_with_kwarg_dict(properties):
         'color': [0, 0, 0, 1],
         'rotation': 10,
         'translation': [5, 5],
-        'anchor': 'upper_left',
+        'anchor': Anchor.UPPER_LEFT,
         'size': 10,
         'visible': True,
     }
diff --git a/napari/layers/utils/_tests/test_text.py b/napari/layers/utils/_tests/test_text_manager.py
similarity index 100%
rename from napari/layers/utils/_tests/test_text.py
rename to napari/layers/utils/_tests/test_text_manager.py
diff --git a/napari/utils/_tests/test_misc.py b/napari/utils/_tests/test_misc.py
--- a/napari/utils/_tests/test_misc.py
+++ b/napari/utils/_tests/test_misc.py
@@ -111,6 +111,31 @@ class OtherEnum(StringEnum):
     with pytest.raises(ValueError):
         TestEnum(OtherEnum.SOMETHING)
 
+    # test string conversion
+    assert str(TestEnum.THING) == 'thing'
+
+    # test direct comparison with a string
+    assert TestEnum.THING == 'thing'
+    assert 'thing' == TestEnum.THING
+    assert TestEnum.THING != 'notathing'
+    assert 'notathing' != TestEnum.THING
+
+    # test comparison with another enum with same value names
+    class AnotherTestEnum(StringEnum):
+        THING = auto()
+        ANOTHERTHING = auto()
+
+    assert TestEnum.THING != AnotherTestEnum.THING
+
+    # test lookup in a set
+    assert TestEnum.THING in {TestEnum.THING, TestEnum.OTHERTHING}
+    assert TestEnum.THING not in {TestEnum.OTHERTHING}
+    assert TestEnum.THING in {'thing', TestEnum.OTHERTHING}
+    assert TestEnum.THING not in {
+        AnotherTestEnum.THING,
+        AnotherTestEnum.ANOTHERTHING,
+    }
+
 
 def test_abspath_or_url():
     relpath = "~" + sep + "something"
diff --git a/napari/utils/events/_tests/test_evented_model.py b/napari/utils/events/_tests/test_evented_model.py
--- a/napari/utils/events/_tests/test_evented_model.py
+++ b/napari/utils/events/_tests/test_evented_model.py
@@ -1,4 +1,5 @@
 import inspect
+from enum import auto
 from typing import ClassVar
 from unittest.mock import Mock
 
@@ -11,6 +12,7 @@
 
 from napari.utils.events import EmitterGroup, EventedModel
 from napari.utils.events.custom_types import Array
+from napari.utils.misc import StringEnum
 
 
 def test_creating_empty_evented_model():
@@ -318,3 +320,56 @@ def my_function():
 
     # check that equality checking works as expected
     assert o1 == o1
+
+
+# The following tests ensure that StringEnum field values can be
+# compared against the enum constants and not their string value.
+# For more context see the GitHub issue:
+# https://github.com/napari/napari/issues/3062
+class SomeStringEnum(StringEnum):
+    NONE = auto()
+    SOME_VALUE = auto()
+    ANOTHER_VALUE = auto()
+
+
+class ModelWithStringEnum(EventedModel):
+    enum_field: SomeStringEnum = SomeStringEnum.NONE
+
+
+def test_evented_model_with_string_enum_default():
+    model = ModelWithStringEnum()
+    assert model.enum_field == SomeStringEnum.NONE
+
+
+def test_evented_model_with_string_enum_parameter():
+    model = ModelWithStringEnum(enum_field=SomeStringEnum.SOME_VALUE)
+    assert model.enum_field == SomeStringEnum.SOME_VALUE
+
+
+def test_evented_model_with_string_enum_parameter_as_str():
+    model = ModelWithStringEnum(enum_field='some_value')
+    assert model.enum_field == SomeStringEnum.SOME_VALUE
+
+
+def test_evented_model_with_string_enum_setter():
+    model = ModelWithStringEnum()
+    model.enum_field = SomeStringEnum.SOME_VALUE
+    assert model.enum_field == SomeStringEnum.SOME_VALUE
+
+
+def test_evented_model_with_string_enum_setter_as_str():
+    model = ModelWithStringEnum()
+    model.enum_field = 'some_value'
+    assert model.enum_field == SomeStringEnum.SOME_VALUE
+
+
+def test_evented_model_with_string_enum_parse_raw():
+    model = ModelWithStringEnum(enum_field=SomeStringEnum.SOME_VALUE)
+    deserialized_model = ModelWithStringEnum.parse_raw(model.json())
+    assert deserialized_model.enum_field == model.enum_field
+
+
+def test_evented_model_with_string_enum_parse_obj():
+    model = ModelWithStringEnum(enum_field=SomeStringEnum.SOME_VALUE)
+    deserialized_model = ModelWithStringEnum.parse_obj(model.dict())
+    assert deserialized_model.enum_field == model.enum_field