Skip to content

Commit

Permalink
Fix invalid value in Item not being used (#985)
Browse files Browse the repository at this point in the history
* Add a test for #790 (they are now passing with traits 6.0 and 6.1)

* Add two failing tests for #983

* Revert "Move format_func, format_str and invalid traits from factory to editor (#859)"

This reverts commit 0617cea.

* Skip the test that will fail on traits<6.1.0 after reverting #859. Test should pass with traits >= 6.1.0

* Fix a comment in tests
  • Loading branch information
kitchoi authored Jul 10, 2020
1 parent 029443b commit ea9828e
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 79 deletions.
1 change: 1 addition & 0 deletions traitsui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"pyqt5": ["pyqt>=5", "pygments"],
"pyside2": ["pyside2", "shiboken2", "pygments"],
"demo": ["configobj", "docutils"],
"test": ["packaging"],
}


Expand Down
30 changes: 3 additions & 27 deletions traitsui/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,6 @@ class Editor(HasPrivateTraits):
#: The trait the editor is editing (not its value, but the trait itself):
value_trait = Property()

#: Function to use for string formatting
format_func = Callable()

#: Format string to use for formatting (used if **format_func** is not set)
format_str = Str()

#: The extended trait name of the trait containing editor invalid state
#: status:
invalid_trait_name = Str()

#: The current editor invalid state status:
invalid = Bool(False)

Expand Down Expand Up @@ -193,12 +183,7 @@ def set_focus(self):
def string_value(self, value, format_func=None):
""" Returns the text representation of a specified object trait value.
If the **format_func** attribute is set on the editor, then this method
calls that function to do the formatting. If the **format_str**
attribute is set on the editor, then this method uses that string for
formatting. If neither attribute is set, then this method just calls
the appropriate text type to format.
This simply delegates to the factory's `string_value` method.
Sub-classes may choose to override the default implementation.
Parameters
Expand All @@ -208,16 +193,7 @@ def string_value(self, value, format_func=None):
format_func : callable or None
A function that takes a value and returns a string.
"""
if self.format_func is not None:
return self.format_func(value)

if self.format_str != "":
return self.format_str % value

if format_func is not None:
return format_func(value)

return str(value)
return self.factory.string_value(value, format_func)

def restore_prefs(self, prefs):
""" Restores saved user preference information for the editor.
Expand Down Expand Up @@ -494,7 +470,7 @@ def __init__(self, parent, **traits):
raise

# Synchronize the application invalid state status with the editor's:
self.sync_value(self.invalid_trait_name, "invalid", "from")
self.sync_value(self.factory.invalid, "invalid", "from")

# ------------------------------------------------------------------------
# private methods
Expand Down
32 changes: 20 additions & 12 deletions traitsui/editor_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,6 @@ def simple_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)

def custom_editor(self, ui, object, name, description, parent):
Expand All @@ -150,9 +147,6 @@ def custom_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)

def text_editor(self, ui, object, name, description, parent):
Expand All @@ -165,9 +159,6 @@ def text_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)

def readonly_editor(self, ui, object, name, description, parent):
Expand All @@ -180,9 +171,6 @@ def readonly_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)

# -------------------------------------------------------------------------
Expand Down Expand Up @@ -211,6 +199,26 @@ def _get_toolkit_editor(cls, class_name):
raise e
return None

def string_value(self, value, format_func=None):
""" Returns the text representation of a specified object trait value.
If the **format_func** attribute is set on the editor factory, then
this method calls that function to do the formatting. If the
**format_str** attribute is set on the editor factory, then this
method uses that string for formatting. If neither attribute is
set, then this method just calls the appropriate text type to format.
"""
if self.format_func is not None:
return self.format_func(value)

if self.format_str != "":
return self.format_str % value

if format_func is not None:
return format_func(value)

return str(value)

# -------------------------------------------------------------------------
# Property getters
# -------------------------------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions traitsui/editors/array_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ def _one_dim_view(self, object, style, width, trait):
content = []
shape = object.shape
items = []
format_func = self.editor.format_func
format_str = self.editor.format_str
format_func = self.editor.factory.format_func
format_str = self.editor.factory.format_str
for i in range(shape[0]):
name = "f%d" % i
self.add_trait(
Expand Down Expand Up @@ -146,8 +146,8 @@ def _one_dim_view(self, object, style, width, trait):
def _two_dim_view(self, object, style, width, trait):
content = []
shape = object.shape
format_func = self.editor.format_func
format_str = self.editor.format_str
format_func = self.editor.factory.format_func
format_str = self.editor.factory.format_str
for i in range(shape[0]):
items = []
for j in range(shape[1]):
Expand Down
12 changes: 0 additions & 12 deletions traitsui/editors/csv_list_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,6 @@ def simple_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)

def custom_editor(self, ui, object, name, description, parent):
Expand All @@ -373,9 +370,6 @@ def custom_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)

def text_editor(self, ui, object, name, description, parent):
Expand All @@ -389,9 +383,6 @@ def text_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)

def readonly_editor(self, ui, object, name, description, parent):
Expand All @@ -405,7 +396,4 @@ def readonly_editor(self, ui, object, name, description, parent):
object=object,
name=name,
description=description,
format_func=self.format_func,
format_str=self.format_str,
invalid_trait_name=self.invalid,
)
25 changes: 13 additions & 12 deletions traitsui/qt4/ui_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,24 +848,25 @@ def _add_items(self, content, outer=None):

editor_factory = ToolkitEditorFactory()

# If the item has formatting traits set them in the editor
# factory:
if item.format_func is not None:
editor_factory.format_func = item.format_func

if item.format_str != "":
editor_factory.format_str = item.format_str

# If the item has an invalid state extended trait name, set it
# in the editor factory:
if item.invalid != "":
editor_factory.invalid = item.invalid

# Create the requested type of editor from the editor factory:
factory_method = getattr(editor_factory, item.style + "_editor")
editor = factory_method(
ui, object, name, item.tooltip, None
).trait_set(item=item, object_name=item.object)

# If the item has formatting traits set them in the editor:
if item.format_func is not None:
editor.format_func = item.format_func

if item.format_str != "":
editor.format_str = item.format_str

# If the item has an invalid state extended trait name, set it
# in the editor:
if item.invalid != "":
editor.invalid_trait_name = item.invalid

# Tell the editor to actually build the editing widget. Note that
# "inner" is a layout. This shouldn't matter as individual editors
# shouldn't be using it as a parent anyway. The important thing is
Expand Down
35 changes: 35 additions & 0 deletions traitsui/tests/editors/test_text_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import contextlib
import unittest

from packaging.version import Version

from traits import __version__ as TRAITS_VERSION
from traits.api import (
HasTraits,
Str,
Expand All @@ -32,6 +35,8 @@ class Foo(HasTraits):

name = Str()

nickname = Str()


def get_view(style, auto_set):
""" Return the default view for the Foo object.
Expand All @@ -48,6 +53,15 @@ def get_view(style, auto_set):
)


def get_text(editor):
""" Return the text from the widget for checking.
"""
if is_current_backend_qt4():
return editor.control.text()
else:
raise unittest.SkipTest("Not implemented for the current toolkit.")


def set_text(editor, text):
""" Imitate user changing the text on the text box to a new value. Note
that this is equivalent to "clear and insert", which excludes confirmation
Expand Down Expand Up @@ -234,3 +248,24 @@ def test_custom_auto_set_false_update_text(self):
process_cascade_events()

self.assertEqual(foo.name, "NEW\n")

@unittest.skipUnless(
Version(TRAITS_VERSION) >= Version("6.1.0"),
"This test requires traits >= 6.1.0"
)
def test_format_func_used(self):
# Regression test for enthought/traitsui#790
# The test will fail with traits < 6.1.0 because the bug
# is fixed in traits, see enthought/traitsui#980 for moving those
# relevant code to traitsui.
foo = Foo(name="william", nickname="bill")
view = View(
Item("name", format_func=lambda s: s.upper()),
Item("nickname"),
)
with store_exceptions_on_all_threads(), \
create_ui(foo, dict(view=view)) as ui:
name_editor, = ui.get_editors("name")
nickname_editor, = ui.get_editors("nickname")
self.assertEqual(get_text(name_editor), "WILLIAM")
self.assertEqual(get_text(nickname_editor), "bill")
28 changes: 28 additions & 0 deletions traitsui/tests/test_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ class UserObject(HasTraits):
#: An event user value
user_event = Event()

#: A state that is to be synchronized with the editor.
invalid_state = Bool()


def create_editor(
context=None,
Expand Down Expand Up @@ -399,6 +402,31 @@ def test_parse_extended_name(self):

editor.dispose()

# Test synchronizing built-in trait values between factory
# and editor.

def test_factory_sync_invalid_state(self):
# Test when object's trait that sets the invalid state changes,
# the invalid state on the editor changes
factory = StubEditorFactory(invalid="invalid_state")
user_object = UserObject(invalid_state=False)
context = {
"object": user_object,
}
editor = create_editor(context=context, factory=factory)
editor.prepare(None)
self.addCleanup(editor.dispose)

with self.assertTraitChanges(editor, "invalid", count=1):
user_object.invalid_state = True

self.assertTrue(editor.invalid)

with self.assertTraitChanges(editor, "invalid", count=1):
user_object.invalid_state = False

self.assertFalse(editor.invalid)

# Testing sync_value "from" ---------------------------------------------

def test_sync_value_from(self):
Expand Down
26 changes: 26 additions & 0 deletions traitsui/tests/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import unittest

from traits.api import Property
from traits.has_traits import HasTraits, HasStrictTraits
from traits.trait_types import Str, Int
import traitsui
Expand Down Expand Up @@ -52,6 +53,20 @@ class DisallowNewTraits(HasStrictTraits):
traits_view = View(Item("x"), spring)


class MaybeInvalidTrait(HasTraits):

name = Str()

name_is_invalid = Property(depends_on="name")

traits_view = View(
Item("name", invalid="name_is_invalid")
)

def _get_name_is_invalid(self):
return len(self.name) < 10


class TestUI(unittest.TestCase):

@skip_if_not_wx
Expand Down Expand Up @@ -215,3 +230,14 @@ def test_no_spring_trait(self):
pass

self.assertTrue("spring" not in obj.traits())

@skip_if_null
def test_invalid_state(self):
# Regression test for enthought/traitsui#983
obj = MaybeInvalidTrait(name="Name long enough to be valid")
with create_ui(obj) as ui:
editor, = ui.get_editors("name")
self.assertFalse(editor.invalid)

obj.name = "too short"
self.assertTrue(editor.invalid)
Loading

0 comments on commit ea9828e

Please sign in to comment.