55A widget for manual editing of a domain's attributes.
66
77"""
8+ from __future__ import annotations
89import warnings
910from xml .sax .saxutils import escape
10- from itertools import zip_longest , repeat , chain
11+ from itertools import zip_longest , repeat , chain , groupby
1112from collections import namedtuple , Counter
1213from functools import singledispatch , partial
14+ from operator import itemgetter
1315from typing import (
1416 Tuple , List , Any , Optional , Union , Dict , Sequence , Iterable , NamedTuple ,
1517 FrozenSet , Type , Callable , TypeVar , Mapping , Hashable , cast , Set
1618)
1719
1820import numpy as np
1921import pandas as pd
22+
2023from AnyQt .QtWidgets import (
2124 QWidget , QListView , QTreeView , QVBoxLayout , QHBoxLayout , QFormLayout ,
2225 QLineEdit , QAction , QActionGroup , QGroupBox ,
3538)
3639from AnyQt .QtCore import pyqtSignal as Signal , pyqtSlot as Slot
3740
41+ from orangecanvas .utils import assocf
3842from orangewidget .utils .listview import ListViewSearch
3943
4044import Orange .data
4650from Orange .util import frompyfunc
4751from Orange .widgets import widget , gui
4852from Orange .widgets .settings import Setting
49- from Orange .widgets .utils import itemmodels , ftry , disconnected
53+ from Orange .widgets .utils import itemmodels , ftry , disconnected , unique_everseen as unique
5054from Orange .widgets .utils .buttons import FixedSizeButton
5155from Orange .widgets .utils .itemmodels import signal_blocking
5256from Orange .widgets .utils .widgetpreview import WidgetPreview
6266MAX_HINTS = 1000
6367
6468
65- def unique (sequence : Iterable [H ]) -> Iterable [H ]:
66- """
67- Return unique elements in `sequence`, preserving their (first seen) order.
68- """
69- # depending on Python >= 3.6 'ordered' dict implementation detail.
70- return iter (dict .fromkeys (sequence ))
71-
72-
7369class _DataType :
7470 def __eq__ (self , other ):
7571 """Equal if `other` has the same type and all elements compare equal."""
@@ -83,19 +79,6 @@ def __ne__(self, other):
8379 def __hash__ (self ):
8480 return hash ((type (self ), super ().__hash__ ()))
8581
86- def name_type (self ):
87- """
88- Returns a tuple with name and type of the variable.
89- It is used since it is forbidden to use names of variables in settings.
90- """
91- type_number = {
92- "Categorical" : 0 ,
93- "Real" : 2 ,
94- "Time" : 3 ,
95- "String" : 4
96- }
97- return self .name , type_number [type (self ).__name__ ]
98-
9982
10083#: An ordered sequence of key, value pairs (variable annotations)
10184AnnotationsType = Tuple [Tuple [str , str ], ...]
@@ -2029,9 +2012,17 @@ class Outputs:
20292012 class Error (widget .OWWidget .Error ):
20302013 duplicate_var_name = widget .Msg ("A variable name is duplicated." )
20312014
2015+ class Warning (widget .OWWidget .Warning ):
2016+ transform_restore_failed = widget .Msg (
2017+ "Failed to restore transform {} for column {}"
2018+ )
2019+ cat_mapping_does_not_apply = widget .Msg (
2020+ "Categories mapping for {} does not apply to current input"
2021+ )
2022+
20322023 settings_version = 4
20332024
2034- _domain_change_hints = Setting ({}, schema_only = True )
2025+ _domain_change_hints : dict = Setting ({}, schema_only = True )
20352026 _merge_dialog_settings = Setting ({}, schema_only = True )
20362027 output_table_name = Setting ("" , schema_only = True )
20372028
@@ -2122,8 +2113,8 @@ def clear(self):
21222113 self .data = None
21232114 self .variables_model .clear ()
21242115 self .clear_editor ()
2125-
21262116 self ._merge_dialog_settings = {}
2117+ self .Warning .clear ()
21272118
21282119 def reset_selected (self ):
21292120 """Reset the currently selected variable to its original state."""
@@ -2178,30 +2169,63 @@ def setup_model(self, data: Orange.data.Table):
21782169 for i , d in enumerate (columns ):
21792170 model .setData (model .index (i ), d , Qt .EditRole )
21802171
2172+ def _sanitize_transform (
2173+ self , var : Variable , trs : Sequence [Transform ]
2174+ ) -> tuple [Sequence [Transform ], Sequence [tuple [Msg , str ]]]:
2175+ def does_categories_mapping_apply (
2176+ var : Categorical , tr : CategoriesMapping ) -> bool :
2177+ return set (var .categories ) \
2178+ == set (ci for ci , _ in tr .mapping if ci is not None )
2179+ msgs = []
2180+ if isinstance (var , Categorical ):
2181+ trs_ = []
2182+ for tr in trs :
2183+ if isinstance (tr , CategoriesMapping ):
2184+ if does_categories_mapping_apply (var , tr ):
2185+ trs_ .append (tr )
2186+ else :
2187+
2188+ msgs .append ((self .Warning .cat_mapping_does_not_apply , var .name ))
2189+ else :
2190+ trs_ .append (tr )
2191+ return trs_ , msgs
2192+ else :
2193+ return trs , msgs
2194+
21812195 def _restore (self ):
21822196 """
21832197 Restore the edit transform from saved state.
21842198 """
21852199 model = self .variables_model
21862200 hints = self ._domain_change_hints
21872201 first_key = None
2202+ msgs = []
21882203 for i in range (model .rowCount ()):
21892204 midx = model .index (i , 0 )
21902205 coldesc = model .data (midx , Qt .EditRole ) # type: DataVector
2191- tr , key = self ._restore_transform (coldesc .vtype )
2192- if tr :
2193- model .setData (midx , tr , TransformRole )
2194- if first_key is None :
2195- first_key = key
2206+ res = self ._find_stored_transform (coldesc .vtype )
2207+ if res :
2208+ key , tr = res
2209+ if tr :
2210+ self ._store_transform (coldesc .vtype , tr , key )
2211+ tr , msgs_ = self ._sanitize_transform (coldesc .vtype , tr )
2212+ model .setData (midx , tr , TransformRole )
2213+ msgs .extend (msgs_ )
2214+ if first_key is None :
2215+ first_key = key
21962216 # Reduce the number of hints to MAX_HINTS, but keep all current hints
21972217 # Current hints start with `first_key`.
21982218 while len (hints ) > MAX_HINTS and \
2199- (key := next (iter (hints ))) is not first_key :
2219+ (key := next (iter (hints ))) != first_key :
22002220 del hints [key ] # pylint: disable=unsupported-delete-operation
22012221
2222+ # Show warnings for non-applicable transforms
2223+ for msg , names in groupby (msgs , key = itemgetter (0 )):
2224+ msg (", " .join (map (itemgetter (1 ), names )))
2225+
22022226 # Restore the current variable selection
22032227 selected_rows = [i for i , vec in enumerate (model )
2204- if vec .vtype .name_type ()[ 0 ] in self ._selected_items ]
2228+ if vec .vtype .name in self ._selected_items ]
22052229 if not selected_rows and model .rowCount ():
22062230 selected_rows = [0 ]
22072231 itemmodels .select_rows (self .variables_view , selected_rows )
@@ -2257,8 +2281,9 @@ def _on_variable_changed(self):
22572281 self ._store_transform (var , transform )
22582282 self ._invalidate ()
22592283
2260- def _store_transform (self , var , transform , deconvar = None ):
2261- # type: (Variable, List[Transform]) -> None
2284+ def _store_transform (
2285+ self , var : Variable , transform : Iterable [Transform ], deconvar = None
2286+ ) -> None :
22622287 deconvar = deconvar or deconstruct (var )
22632288 # Remove the existing key (if any) to put the new one at the end,
22642289 # to make sure it comes after the sentinel
@@ -2267,25 +2292,32 @@ def _store_transform(self, var, transform, deconvar=None):
22672292 self ._domain_change_hints [deconvar ] = \
22682293 [deconstruct (t ) for t in transform ]
22692294
2270- def _restore_transform (self , var ):
2271- # type: (Variable) -> List[Transform]
2295+ def _find_stored_transform (
2296+ self , var : Variable
2297+ ) -> Tuple [tuple , Sequence [Transform ]] | None :
2298+ """Find stored transform for `var`."""
2299+ def reconstruct_transform (tr_ : list [tuple ]) -> list [Transform ]:
2300+ trs = []
2301+ for t in tr_ :
2302+ try :
2303+ trs .append (cast (Transform , reconstruct (* t )))
2304+ except (AttributeError , TypeError , NameError ):
2305+ self .Warning .transform_restore_failed (
2306+ str (t ), var .name , exc_info = True ,
2307+ )
2308+ return trs
2309+
2310+ hints = self ._domain_change_hints
22722311 key = deconstruct (var )
2273- tr_ = self ._domain_change_hints .get (key , [])
2274- tr = []
2312+ tr = hints .get (key ) # exact match
2313+ if tr is not None :
2314+ return key , reconstruct_transform (tr )
22752315
2276- for t in tr_ :
2277- try :
2278- tr .append (reconstruct (* t ))
2279- except (NameError , TypeError ) as err :
2280- warnings .warn (
2281- f"Failed to restore transform: { t } , { err } " ,
2282- UserWarning , stacklevel = 2
2283- )
2284- if tr :
2285- self ._store_transform (var , tr , key )
2286- else :
2287- key = None
2288- return tr , key
2316+ # match by name and type only
2317+ item = assocf (hints .items (),
2318+ lambda k : k [0 ] == key [0 ] and k [1 ][0 ] == var .name )
2319+ if item is not None :
2320+ return item [0 ], reconstruct_transform (item [1 ])
22892321
22902322 def _invalidate (self ):
22912323 self ._set_modified (True )
0 commit comments