1"""This modules defines CSVListEditor. 2 3A CSVListEditor provides an editor for lists of simple data types. 4It allows the user to edit the list in a text field, using commas 5(or optionally some other character) to separate the elements. 6""" 7 8# ------------------------------------------------------------------------------ 9# 10# Copyright (c) 2011, Enthought, Inc. 11# All rights reserved. 12# 13# This software is provided without warranty under the terms of the BSD 14# license included in LICENSE.txt and may be redistributed only 15# under the conditions described in the aforementioned license. The license 16# is also available online at http://www.enthought.com/licenses/BSD.txt 17# 18# Author: Warren Weckesser 19# Date: July 2011 20# 21# ------------------------------------------------------------------------------ 22 23from traits.api import Str, Int, Float, Enum, Range, Bool, TraitError, Either 24from traits.trait_handlers import RangeTypes 25 26from .text_editor import TextEditor 27from ..helper import enum_values_changed 28 29 30def _eval_list_str(s, sep=",", item_eval=None, ignore_trailing_sep=True): 31 """Convert a string into a list. 32 33 Parameters 34 ---------- 35 s : str 36 The string to be converted. 37 sep : str or None 38 `sep` is the text separator of list items. If `sep` is None, 39 each contiguous stretch of whitespace is a separator. 40 item_eval : callable or None 41 `item_eval` is used to evaluate the list elements. If `item_eval` 42 is None, the list will be a list substrings of `s`. 43 ignore_trailing_sep : bool 44 If `ignore_trailing_sep` is False, it is an error to have a separator 45 at the end of the list (i.e. 'foo, bar,' is invalid). 46 If `ignore_trailing_sep` is True, a separator at the end of the 47 string `s` is ignored. 48 49 Returns 50 ------- 51 values : list 52 List of converted values from the string. 53 """ 54 if item_eval is None: 55 item_eval = lambda x: x 56 s = s.strip() 57 if sep is not None and ignore_trailing_sep and s.endswith(sep): 58 s = s[: -len(sep)] 59 s = s.rstrip() 60 if s == "": 61 values = [] 62 else: 63 values = [item_eval(x.strip()) for x in s.split(sep)] 64 return values 65 66 67def _format_list_str(values, sep=",", item_format=str): 68 """Convert a list to a string. 69 70 Each item in the list `values` is converted to a string with the 71 function `item_format`, and these are joined with `sep` plus a space. 72 If `sep` is None, a single space is used to join the items. 73 74 Parameters 75 ---------- 76 values : list 77 The list of values to be represented as a string. 78 sep : str 79 String used to join the items. A space is also added after 80 `sep`. 81 item_format : callable 82 Converts its single argument to a string. 83 84 Returns 85 ------- 86 s : str 87 The result of converting the list to a string. 88 """ 89 if sep is None: 90 joiner = " " 91 else: 92 joiner = sep + " " 93 s = joiner.join(item_format(x) for x in values) 94 return s 95 96 97def _validate_range_value(range_object, object, name, value): 98 """Validate a Range value. 99 100 This function is used by the CSVListEditor to validate a value 101 when editing a list of ranges where the Range is dynamic (that 102 is, one or both of the 'low' and 'high' values are strings that 103 refer to other traits in `object`. 104 105 The function implements the same validation logic as in the method 106 traits.trait_types.BaseRange._set(), but does not call the 107 set_value() method; instead it simply returns the valid value. 108 If the value is not valid, range_object.error(...) is called. 109 110 Parameters 111 ---------- 112 range_object : instance of traits.trait_types.Range 113 114 object : instance of HasTraits 115 This is the HasTraits object that holds the traits 116 to which the one or both of range_object.low and 117 range_object.high refer. 118 119 name : str 120 The name of the List trait in `object`. 121 122 value : object (e.g. int, float, str) 123 The value to be validated. 124 125 Returns 126 ------- 127 value : object 128 The validated value. It might not be the same 129 type as the input argument (e.g. if the range type 130 is float and the input value is an int, the return 131 value will be a float). 132 """ 133 low = eval(range_object._low) 134 high = eval(range_object._high) 135 if low is None and high is None: 136 if isinstance(value, RangeTypes): 137 return value 138 else: 139 new_value = range_object._typed_value(value, low, high) 140 141 satisfies_low = ( 142 low is None 143 or low < new_value 144 or ((not range_object._exclude_low) and (low == new_value)) 145 ) 146 147 satisfies_high = ( 148 high is None 149 or high > new_value 150 or ((not range_object._exclude_high) and (high == new_value)) 151 ) 152 153 if satisfies_low and satisfies_high: 154 return value 155 156 # Note: this is the only explicit use of 'object' and 'name'. 157 range_object.error(object, name, value) 158 159 160def _prepare_method(cls, parent): 161 """ Unbound implementation of the prepare editor method to add a 162 change notification hook in the items of the list before calling 163 the parent prepare method of the parent class. 164 165 """ 166 name = cls.extended_name 167 if name != "None": 168 cls.context_object.on_trait_change( 169 cls._update_editor, name + "[]", dispatch="ui" 170 ) 171 super(cls.__class__, cls).prepare(parent) 172 173 174def _dispose_method(cls): 175 """ Unbound implementation of the dispose editor method to remove 176 the change notification hook in the items of the list before calling 177 the parent dispose method of the parent class. 178 179 """ 180 if cls.ui is None: 181 return 182 183 name = cls.extended_name 184 if name != "None": 185 cls.context_object.on_trait_change( 186 cls._update_editor, name + "[]", remove=True 187 ) 188 super(cls.__class__, cls).dispose() 189 190 191class CSVListEditor(TextEditor): 192 """A text editor for a List. 193 194 This editor provides a single line of input text of comma separated 195 values. (Actually, the default separator is a comma, but this can 196 changed.) The editor can only be used with List traits whose inner 197 trait is one of Int, Float, Str, Enum, or Range. 198 199 The 'simple', 'text', 'custom' and readonly styles are based on 200 TextEditor. The 'readonly' style provides the same formatting in the 201 text field as the other editors, but the user cannot change the value. 202 203 Like other Traits editors, the background of the text field will turn 204 red if the user enters an incorrectly formatted list or if the values 205 do not match the type of the inner trait. This validation only occurs 206 while editing the text field. If, for example, the inner trait is 207 Range(low='lower', high='upper'), a change in 'upper' will not trigger 208 the validation code of the editor. 209 210 The editor removes whitespace of entered items with strip(), so for 211 Str types, the editor should not be used if whitespace at the beginning 212 or end of the string must be preserved. 213 214 Parameters 215 ---------- 216 sep : str or None, optional 217 The separator of the values in the list. If None, each contiguous 218 sequence of whitespace is a separator. 219 Default is ','. 220 221 ignore_trailing_sep : bool, optional 222 If this is False, a line containing a trailing separator is invalid. 223 Default is True. 224 225 auto_set : bool 226 If True, then every keystroke sets the value of the trait. 227 228 enter_set : bool 229 If True, the user input sets the value when the Enter key is pressed. 230 231 Example 232 ------- 233 The following will display a window containing a single input field. 234 Entering, say, '0, .5, 1' in this field will result in the list 235 x = [0.0, 0.5, 1.0]. 236 """ 237 238 #: The separator of the element in the list. 239 sep = Either(None, Str, default=",") 240 241 #: If False, it is an error to have a trailing separator. 242 ignore_trailing_sep = Bool(True) 243 244 #: Include some of the TextEditor API: 245 246 #: Is user input set on every keystroke? 247 auto_set = Bool(True) 248 249 #: Is user input set when the Enter key is pressed? 250 enter_set = Bool(False) 251 252 def _funcs(self, object, name): 253 """Create the evalution and formatting functions for the editor. 254 255 Parameters 256 ---------- 257 object : instance of HasTraits 258 This is the object that has the List trait for which we are 259 creating an editor. 260 261 name : str 262 Name of the List trait on `object`. 263 264 Returns 265 ------- 266 evaluate, fmt_func : callable, callable 267 The functions for converting a string to a list (`evaluate`) 268 and a list to a string (`fmt_func`). These are the functions 269 that are ultimately given as the keyword arguments 'evaluate' 270 and 'format_func' of the TextEditor that will be generated by 271 the CSVListEditor editor factory functions. 272 """ 273 t = getattr(object, name) 274 # Get the list of inner traits. Only a single inner trait is allowed. 275 it_list = t.trait.inner_traits() 276 if len(it_list) > 1: 277 raise TraitError( 278 "Only one inner trait may be specified when " 279 "using a CSVListEditor." 280 ) 281 282 # `it` is the single inner trait. This will be an instance of 283 # traits.traits.CTrait. 284 it = it_list[0] 285 # The following 'if' statement figures out the appropriate evaluation 286 # function (evaluate) and formatting function (fmt_func) for the 287 # given inner trait. 288 if ( 289 it.is_trait_type(Int) 290 or it.is_trait_type(Float) 291 or it.is_trait_type(Str) 292 ): 293 evaluate = lambda s: _eval_list_str( 294 s, 295 sep=self.sep, 296 item_eval=it.trait_type.evaluate, 297 ignore_trailing_sep=self.ignore_trailing_sep, 298 ) 299 fmt_func = lambda vals: _format_list_str(vals, sep=self.sep) 300 elif it.is_trait_type(Enum): 301 values, mapping, inverse_mapping = enum_values_changed(it) 302 evaluate = lambda s: _eval_list_str( 303 s, 304 sep=self.sep, 305 item_eval=mapping.__getitem__, 306 ignore_trailing_sep=self.ignore_trailing_sep, 307 ) 308 fmt_func = lambda vals: _format_list_str( 309 vals, sep=self.sep, item_format=inverse_mapping.__getitem__ 310 ) 311 elif it.is_trait_type(Range): 312 # Get the type of the values from the default value. 313 # range_object will be an instance of traits.trait_types.Range. 314 range_object = it.handler 315 if range_object.default_value_type == 8: 316 # range_object.default_value is callable. 317 defval = range_object.default_value(object) 318 else: 319 # range_object.default_value *is* the default value. 320 defval = range_object.default_value 321 typ = type(defval) 322 323 if range_object.validate is None: 324 # This will be the case for dynamic ranges. 325 item_eval = lambda s: _validate_range_value( 326 range_object, object, name, typ(s) 327 ) 328 else: 329 # Static ranges have a validate method. 330 item_eval = lambda s: range_object.validate( 331 object, name, typ(s) 332 ) 333 334 evaluate = lambda s: _eval_list_str( 335 s, 336 sep=self.sep, 337 item_eval=item_eval, 338 ignore_trailing_sep=self.ignore_trailing_sep, 339 ) 340 fmt_func = lambda vals: _format_list_str(vals, sep=self.sep) 341 else: 342 raise TraitError( 343 "To use a CSVListEditor, the inner trait of the " 344 "List must be Int, Float, Range, Str or Enum." 345 ) 346 347 return evaluate, fmt_func 348 349 def simple_editor(self, ui, object, name, description, parent): 350 """ Generates an editor using the "simple" style. 351 """ 352 self.evaluate, self.format_func = self._funcs(object, name) 353 return self.simple_editor_class( 354 parent, 355 factory=self, 356 ui=ui, 357 object=object, 358 name=name, 359 description=description, 360 ) 361 362 def custom_editor(self, ui, object, name, description, parent): 363 """ Generates an editor using the "custom" style. 364 """ 365 self.evaluate, self.format_func = self._funcs(object, name) 366 return self.custom_editor_class( 367 parent, 368 factory=self, 369 ui=ui, 370 object=object, 371 name=name, 372 description=description, 373 ) 374 375 def text_editor(self, ui, object, name, description, parent): 376 """ Generates an editor using the "text" style. 377 """ 378 self.evaluate, self.format_func = self._funcs(object, name) 379 return self.text_editor_class( 380 parent, 381 factory=self, 382 ui=ui, 383 object=object, 384 name=name, 385 description=description, 386 ) 387 388 def readonly_editor(self, ui, object, name, description, parent): 389 """ Generates an "editor" that is read-only. 390 """ 391 self.evaluate, self.format_func = self._funcs(object, name) 392 return self.readonly_editor_class( 393 parent, 394 factory=self, 395 ui=ui, 396 object=object, 397 name=name, 398 description=description, 399 ) 400