def _create_buttons(self, box): """Create radio buttons""" def intspin(): s = QSpinBox(self) s.setMinimum(2) s.setMaximum(10) s.setFixedWidth(60) s.setAlignment(Qt.AlignRight) s.setContentsMargins(0, 0, 0, 0) return s, s.valueChanged def widthline(validator): s = QLineEdit(self) s.setFixedWidth(60) s.setAlignment(Qt.AlignRight) s.setValidator(validator) s.setContentsMargins(0, 0, 0, 0) return s, s.textChanged def manual_cut_editline(text="", enabled=True) -> QLineEdit: edit = QLineEdit( text=text, placeholderText="e.g. 0.0, 0.5, 1.0", toolTip='<p style="white-space:pre">' + 'Enter cut points as a comma-separate list of \n' 'strictly increasing numbers e.g. 0.0, 0.5, 1.0).</p>', enabled=enabled, ) edit.setValidator(IncreasingNumbersListValidator()) edit.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) @edit.textChanged.connect def update(): validator = edit.validator() if validator is not None and edit.text().strip(): state, _, _ = validator.validate(edit.text(), 0) else: state = QValidator.Acceptable palette = edit.palette() colors = { QValidator.Intermediate: (Qt.yellow, Qt.black), QValidator.Invalid: (Qt.red, Qt.black), }.get(state, None) if colors is None: palette = QPalette() else: palette.setColor(QPalette.Base, colors[0]) palette.setColor(QPalette.Text, colors[1]) cr = edit.cursorRect() p = edit.mapToGlobal(cr.bottomRight()) edit.setPalette(palette) if state != QValidator.Acceptable and edit.isVisible(): validator.show_tip(edit, p, edit.toolTip(), textFormat=Qt.RichText) else: validator.show_tip(edit, p, "") return edit, edit.textChanged children = [] def button(id_, *controls, stretch=True): layout = QHBoxLayout() desc = Options[id_] button = QRadioButton(desc.label) button.setToolTip(desc.tooltip) self.button_group.addButton(button, id_) layout.addWidget(button) if controls: if stretch: layout.addStretch(1) for c, signal in controls: layout.addWidget(c) if signal is not None: @signal.connect def arg_changed(): self.button_group.button(id_).setChecked(True) self.update_hints(id_) children.append(layout) button_box.layout().addLayout(layout) return (*controls, (None, ))[0][0] button_box = gui.vBox(box) button_box.layout().setSpacing(0) button_box.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)) self.button_group = QButtonGroup(self) self.button_group.idClicked.connect(self.update_hints) button(Methods.Keep) button(Methods.Remove) self.binning_spin = button(Methods.Binning, intspin()) validator = QDoubleValidator() validator.setBottom(0) self.width_line = button(Methods.FixedWidth, widthline(validator)) self.width_time_unit = u = QComboBox(self) u.setContentsMargins(0, 0, 0, 0) u.addItems([unit + "(s)" for unit in time_units]) validator = QIntValidator() validator.setBottom(1) self.width_time_line = button(Methods.FixedWidthTime, widthline(validator), (u, u.currentTextChanged)) self.freq_spin = button(Methods.EqualFreq, intspin()) self.width_spin = button(Methods.EqualWidth, intspin()) button(Methods.MDL) self.copy_to_custom = FixedSizeButton( text="CC", toolTip="Copy the current cut points to manual mode") self.copy_to_custom.clicked.connect(self._copy_to_manual) self.threshold_line = button(Methods.Custom, manual_cut_editline(), (self.copy_to_custom, None), stretch=False) button(Methods.Default) maxheight = max(w.sizeHint().height() for w in children) for w in children: w.itemAt(0).widget().setFixedHeight(maxheight) button_box.layout().addStretch(1)
def __init__(self): super().__init__() #: input data self.data = None self.class_var = None #: Current variable discretization state self.var_state = {} #: Saved variable discretization settings (context setting) self.saved_var_states = {} self.method = Methods.Default self.k = 5 self.cutpoints = () box = gui.vBox(self.controlArea, self.tr("Default Discretization")) self._default_method_ = 0 self.default_bbox = rbox = gui.radioButtons( box, self, "_default_method_", callback=self._default_disc_changed) self.default_button_group = bg = rbox.findChild(QButtonGroup) bg.buttonClicked[int].connect(self.set_default_method) rb = gui.hBox(rbox) self.left = gui.vBox(rb) right = gui.vBox(rb) rb.layout().setStretch(0, 1) rb.layout().setStretch(1, 1) self.options = [ (Methods.Default, self.tr("Default")), (Methods.Leave, self.tr("Leave numeric")), (Methods.MDL, self.tr("Entropy-MDL discretization")), (Methods.EqualFreq, self.tr("Equal-frequency discretization")), (Methods.EqualWidth, self.tr("Equal-width discretization")), (Methods.Remove, self.tr("Remove numeric variables")), (Methods.Custom, self.tr("Manual")), ] for id_, opt in self.options[1:]: t = gui.appendRadioButton(rbox, opt) bg.setId(t, id_) t.setChecked(id_ == self.default_method) [right, self.left][opt.startswith("Equal")].layout().addWidget(t) def _intbox(parent, attr, callback): box = gui.indentedBox(parent) s = gui.spin( box, self, attr, minv=2, maxv=10, label="Num. of intervals:", callback=callback) s.setMaximumWidth(60) s.setAlignment(Qt.AlignRight) gui.rubber(s.box) return box.box self.k_general = _intbox(self.left, "default_k", self._default_disc_changed) self.k_general.layout().setContentsMargins(0, 0, 0, 0) def manual_cut_editline(text="", enabled=True) -> QLineEdit: edit = QLineEdit( text=text, placeholderText="e.g. 0.0, 0.5, 1.0", toolTip="Enter fixed discretization cut points (a comma " "separated list of strictly increasing numbers e.g. " "0.0, 0.5, 1.0).", enabled=enabled, ) @edit.textChanged.connect def update(): validator = edit.validator() if validator is not None: state, _, _ = validator.validate(edit.text(), 0) else: state = QValidator.Acceptable palette = edit.palette() colors = { QValidator.Intermediate: (Qt.yellow, Qt.black), QValidator.Invalid: (Qt.red, Qt.black), }.get(state, None) if colors is None: palette = QPalette() else: palette.setColor(QPalette.Base, colors[0]) palette.setColor(QPalette.Text, colors[1]) cr = edit.cursorRect() p = edit.mapToGlobal(cr.bottomRight()) edit.setPalette(palette) if state != QValidator.Acceptable and edit.isVisible(): show_tip(edit, p, edit.toolTip(), textFormat=Qt.RichText) else: show_tip(edit, p, "") return edit self.manual_cuts_edit = manual_cut_editline( text=", ".join(map(str, self.default_cutpoints)), enabled=self.default_method == Methods.Custom, ) def set_manual_default_cuts(): text = self.manual_cuts_edit.text() self.default_cutpoints = tuple( float(s.strip()) for s in text.split(",") if s.strip()) self._default_disc_changed() self.manual_cuts_edit.editingFinished.connect(set_manual_default_cuts) validator = IncreasingNumbersListValidator() self.manual_cuts_edit.setValidator(validator) ibox = gui.indentedBox(right, orientation=Qt.Horizontal) ibox.layout().addWidget(self.manual_cuts_edit) right.layout().addStretch(10) self.left.layout().addStretch(10) self.connect_control( "default_cutpoints", lambda values: self.manual_cuts_edit.setText(", ".join(map(str, values))) ) vlayout = QHBoxLayout() box = gui.widgetBox( self.controlArea, "Individual Attribute Settings", orientation=vlayout, spacing=8 ) # List view with all attributes self.varview = ListViewSearch( selectionMode=QListView.ExtendedSelection, uniformItemSizes=True, ) self.varview.setItemDelegate(DiscDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed ) vlayout.addWidget(self.varview) # Controls for individual attr settings self.bbox = controlbox = gui.radioButtons( box, self, "method", callback=self._disc_method_changed ) vlayout.addWidget(controlbox) self.variable_button_group = bg = controlbox.findChild(QButtonGroup) for id_, opt in self.options[:5]: b = gui.appendRadioButton(controlbox, opt) bg.setId(b, id_) self.k_specific = _intbox(controlbox, "k", self._disc_method_changed) gui.appendRadioButton(controlbox, "Remove attribute", id=Methods.Remove) b = gui.appendRadioButton(controlbox, "Manual", id=Methods.Custom) self.manual_cuts_specific = manual_cut_editline( text=", ".join(map(str, self.cutpoints)), enabled=self.method == Methods.Custom ) self.manual_cuts_specific.setValidator(validator) b.toggled[bool].connect(self.manual_cuts_specific.setEnabled) def set_manual_cuts(): text = self.manual_cuts_specific.text() points = [t for t in text.split(",") if t.split()] self.cutpoints = tuple(float(t) for t in points) self._disc_method_changed() self.manual_cuts_specific.editingFinished.connect(set_manual_cuts) self.connect_control( "cutpoints", lambda values: self.manual_cuts_specific.setText(", ".join(map(str, values))) ) ibox = gui.indentedBox(controlbox, orientation=Qt.Horizontal) self.copy_current_to_manual_button = b = FixedSizeButton( text="CC", toolTip="Copy the current cut points to manual mode", enabled=False ) b.clicked.connect(self._copy_to_manual) ibox.layout().addWidget(self.manual_cuts_specific) ibox.layout().addWidget(b) gui.rubber(controlbox) controlbox.setEnabled(False) bg.button(self.method) self.controlbox = controlbox gui.auto_apply(self.buttonsArea, self, "autosend") self._update_spin_positions()
class OWDiscretize(widget.OWWidget): # pylint: disable=too-many-instance-attributes name = "Discretize" description = "Discretize numeric variables" category = "Transform" icon = "icons/Discretize.svg" keywords = ["bin", "categorical", "nominal", "ordinal"] priority = 2130 class Inputs: data = Input("Data", Table, doc="Input data table") class Outputs: data = Output("Data", Table, doc="Table with categorical features") settings_version = 3 #: Default setting (key DefaultKey) and specific settings for variables; # if variable is not in the dict, it uses default var_hints: Dict[KeyType, VarHint] = Setting( {DefaultKey: DefaultHint}, schema_only=True) autosend = Setting(True) want_main_area = False def __init__(self): super().__init__() #: input data self.data = None #: Cached discretized variables self.discretized_vars: Dict[KeyType, DiscreteVariable] = {} # Indicates that buttons, spins, edit and combos are being changed # programmatically (when interface is changed due to selection change), # so this should not trigger update of hints and invalidation of # discretization in `self.discretized_vars`. self.__interface_update = False box = gui.hBox(self.controlArea, True, spacing=8) self._create_var_list(box) self._create_buttons(box) gui.auto_apply(self.buttonsArea, self, "autosend") gui.rubber(self.buttonsArea) self.varview.select_default() def _create_var_list(self, box): """Create list view with variables""" # If we decide to not elide, remove the `uniformItemSize` argument self.varview = ListViewSearch( selectionMode=QListView.ExtendedSelection, uniformItemSizes=True) self.varview.setModel( DiscDomainModel( valid_types=(ContinuousVariable, TimeVariable), order=DiscDomainModel.MIXED )) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed) self.varview.default_view.selectionModel().selectionChanged.connect( self._default_selected) self._update_default_model() box.layout().addWidget(self.varview) def _create_buttons(self, box): """Create radio buttons""" def intspin(): s = QSpinBox(self) s.setMinimum(2) s.setMaximum(10) s.setFixedWidth(60) s.setAlignment(Qt.AlignRight) s.setContentsMargins(0, 0, 0, 0) return s, s.valueChanged def widthline(validator): s = QLineEdit(self) s.setFixedWidth(60) s.setAlignment(Qt.AlignRight) s.setValidator(validator) s.setContentsMargins(0, 0, 0, 0) return s, s.textChanged def manual_cut_editline(text="", enabled=True) -> QLineEdit: edit = QLineEdit( text=text, placeholderText="e.g. 0.0, 0.5, 1.0", toolTip='<p style="white-space:pre">' + 'Enter cut points as a comma-separate list of \n' 'strictly increasing numbers e.g. 0.0, 0.5, 1.0).</p>', enabled=enabled, ) edit.setValidator(IncreasingNumbersListValidator()) edit.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) @edit.textChanged.connect def update(): validator = edit.validator() if validator is not None and edit.text().strip(): state, _, _ = validator.validate(edit.text(), 0) else: state = QValidator.Acceptable palette = edit.palette() colors = { QValidator.Intermediate: (Qt.yellow, Qt.black), QValidator.Invalid: (Qt.red, Qt.black), }.get(state, None) if colors is None: palette = QPalette() else: palette.setColor(QPalette.Base, colors[0]) palette.setColor(QPalette.Text, colors[1]) cr = edit.cursorRect() p = edit.mapToGlobal(cr.bottomRight()) edit.setPalette(palette) if state != QValidator.Acceptable and edit.isVisible(): validator.show_tip(edit, p, edit.toolTip(), textFormat=Qt.RichText) else: validator.show_tip(edit, p, "") return edit, edit.textChanged children = [] def button(id_, *controls, stretch=True): layout = QHBoxLayout() desc = Options[id_] button = QRadioButton(desc.label) button.setToolTip(desc.tooltip) self.button_group.addButton(button, id_) layout.addWidget(button) if controls: if stretch: layout.addStretch(1) for c, signal in controls: layout.addWidget(c) if signal is not None: @signal.connect def arg_changed(): self.button_group.button(id_).setChecked(True) self.update_hints(id_) children.append(layout) button_box.layout().addLayout(layout) return (*controls, (None, ))[0][0] button_box = gui.vBox(box) button_box.layout().setSpacing(0) button_box.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)) self.button_group = QButtonGroup(self) self.button_group.idClicked.connect(self.update_hints) button(Methods.Keep) button(Methods.Remove) self.binning_spin = button(Methods.Binning, intspin()) validator = QDoubleValidator() validator.setBottom(0) self.width_line = button(Methods.FixedWidth, widthline(validator)) self.width_time_unit = u = QComboBox(self) u.setContentsMargins(0, 0, 0, 0) u.addItems([unit + "(s)" for unit in time_units]) validator = QIntValidator() validator.setBottom(1) self.width_time_line = button(Methods.FixedWidthTime, widthline(validator), (u, u.currentTextChanged)) self.freq_spin = button(Methods.EqualFreq, intspin()) self.width_spin = button(Methods.EqualWidth, intspin()) button(Methods.MDL) self.copy_to_custom = FixedSizeButton( text="CC", toolTip="Copy the current cut points to manual mode") self.copy_to_custom.clicked.connect(self._copy_to_manual) self.threshold_line = button(Methods.Custom, manual_cut_editline(), (self.copy_to_custom, None), stretch=False) button(Methods.Default) maxheight = max(w.sizeHint().height() for w in children) for w in children: w.itemAt(0).widget().setFixedHeight(maxheight) button_box.layout().addStretch(1) def _update_default_model(self): """Update data in the model showing default settings""" model = self.varview.default_view.model() model.setData(model.index(0), self.var_hints[DefaultKey], Qt.UserRole) def _set_mdl_button(self): """Disable MDL discretization for data with non-discrete class""" mdl_button = self.button_group.button(Methods.MDL) if self.data is None or self.data.domain.has_discrete_class: mdl_button.setEnabled(True) else: if mdl_button.isChecked(): self._check_button(Methods.Keep, True) mdl_button.setEnabled(False) def _check_button(self, method_id: Methods, checked: bool): """Checks the given button""" self.button_group.button(method_id).setChecked(checked) def _uncheck_all_buttons(self): """Uncheck all radio buttons""" group = self.button_group button = group.checkedButton() if button is not None: group.setExclusive(False) button.setChecked(False) group.setExclusive(True) def _set_radio_enabled(self, method_id: Methods, value: bool): """Enable/disable radio button and related controls""" if self.button_group.button(method_id).isChecked() and not value: self._uncheck_all_buttons() self.button_group.button(method_id).setEnabled(value) for control_name in Options[method_id].controls: getattr(self, control_name).setEnabled(value) def _get_values(self, method_id: Methods) -> Tuple[Union[int, float, str]]: """Return parameters from controls pertaining to the given method""" controls = Options[method_id].controls values = [] for control_name in controls: control = getattr(self, control_name) if isinstance(control, QSpinBox): values.append(control.value()) elif isinstance(control, QComboBox): values.append(control.currentIndex()) else: values.append(control.text()) return tuple(values) def _set_values(self, method_id: Methods, values: Tuple[Union[str, int, float]]): """ Set controls pertaining to the given method to parameters from hint """ controls = Options[method_id].controls for control_name, value in zip(controls, values): control = getattr(self, control_name) if isinstance(control, QSpinBox): control.setValue(value) elif isinstance(control, QComboBox): control.setCurrentIndex(value) else: control.setText(value) def varkeys_for_selection(self) -> List[KeyType]: """ Return list of KeyType's for selected variables (for indexing var_hints) If 'Default settings' are selected, this returns DefaultKey """ model = self.varview.model() varkeys = [variable_key(model[index.row()]) for index in self.varview.selectionModel().selectedRows()] return varkeys or [DefaultKey] # default settings are selected def update_hints(self, method_id: Methods): """ Callback for radio buttons and for controls regulating parameters This function: - updates `var_hints` for all selected methods - invalidates (removes) `discretized_vars` for affected variables - calls _update_discretizations to compute and commit new discretization - calls deferred commit Data for list view models is updated in _update_discretizations """ if self.__interface_update: return method_id = Methods(method_id) args = self._get_values(method_id) keys = self.varkeys_for_selection() if method_id == Methods.Default: for key in keys: if key in self.var_hints: del self.var_hints[key] else: self.var_hints.update(dict.fromkeys(keys, VarHint(method_id, args))) if keys == [DefaultKey]: invalidate = set(self.discretized_vars) - set(self.var_hints) else: invalidate = keys for key in invalidate: del self.discretized_vars[key] if keys == [DefaultKey]: self._update_default_model() self._update_discretizations() self.commit.deferred() def _update_discretizations(self): """ Compute invalidated (missing) discretizations Also set data for list view models for all invalidated variables """ if self.data is None: return default_hint = self.var_hints[DefaultKey] model = self.varview.model() for index, var in enumerate(model): key = variable_key(var) if key in self.discretized_vars: continue # still valid var_hint = self.var_hints.get(key) points, dvar = self._discretize_var(var, var_hint or default_hint) self.discretized_vars[key] = dvar values = getattr(dvar, "values", ()) model.setData(model.index(index), DiscDesc(var_hint, points, values), Qt.UserRole) def _discretize_var(self, var: ContinuousVariable, hint: VarHint) \ -> Tuple[str, Optional[Variable]]: """ Discretize using method and data in the hint. Returns a description (list of points or error/warning) and a - discrete variable - same variable (if kept numeric) - None (if removed or errored) """ if isinstance(var, TimeVariable): if hint.method_id in (Methods.FixedWidth, Methods.Custom): return ": <keep, time var>", var else: if hint.method_id == Methods.FixedWidthTime: return ": <keep, not time>", var function = Options[hint.method_id].function dvar = function(self.data, var, *hint.args) if isinstance(dvar, str): return f" <{dvar}>", None # error if dvar is None: return "", None # removed elif dvar is var: return "", var # no transformation thresholds = dvar.compute_value.points if len(thresholds) == 0: return " <removed>", None return ": " + ", ".join(map(var.repr_val, thresholds)), dvar def _copy_to_manual(self): """ Callback for 'CC' button Sets selected variables' method to "Custom" and copies thresholds to their VarHints. Variables that are not discretized (for any reason) are skipped. Discretizations are invalidated and then updated (`_update_discretizations`). If all selected variables have the same thresholds, it copies it to the line edit. Otherwise it unchecks all radio buttons to keep the interface consistent. """ varkeys = self.varkeys_for_selection() texts = set() for key in varkeys: dvar = self.discretized_vars.get(key) fmt = self.data.domain[key[0]].repr_val if isinstance(dvar, DiscreteVariable): text = ", ".join(map(fmt, dvar.compute_value.points)) texts.add(text) self.var_hints[key] = VarHint(Methods.Custom, (text, )) del self.discretized_vars[key] try: self.__interface_update = True if len(texts) == 1: self.threshold_line.setText(texts.pop()) else: self._uncheck_all_buttons() finally: self.__interface_update = False self._update_discretizations() self.commit.deferred() def _default_selected(self, selected): """Callback for selecting 'Default setting'""" if not selected: # Prevent infinite recursion (with _var_selection_changed) return self.varview.selectionModel().clearSelection() self._update_interface() set_enabled = self._set_radio_enabled set_enabled(Methods.Default, False) set_enabled(Methods.FixedWidth, True) set_enabled(Methods.FixedWidthTime, True) set_enabled(Methods.Custom, True) self.copy_to_custom.setEnabled(False) def _var_selection_changed(self, _): """Callback for changed selection in listview with variables""" selected = self.varview.selectionModel().selectedIndexes() if not selected: # Prevent infinite recursion (with _default_selected) return self.varview.default_view.selectionModel().clearSelection() self._update_interface() set_enabled = self._set_radio_enabled vars_ = [self.data.domain[name] for name, _ in self.varkeys_for_selection()] no_time = not any(isinstance(var, TimeVariable) for var in vars_) all_time = all(isinstance(var, TimeVariable) for var in vars_) set_enabled(Methods.Default, True) set_enabled(Methods.FixedWidth, no_time) set_enabled(Methods.Custom, no_time) self.copy_to_custom.setEnabled(no_time) set_enabled(Methods.FixedWidthTime, all_time) def _update_interface(self): """ Update the user interface according to selection - If VarHints for all selected variables are the same, check the corresponding radio button and fill the corresponding controls; - otherwise, uncheck all radios. """ if self.__interface_update: return try: self.__interface_update = True keys = self.varkeys_for_selection() mset = list(unique_everseen(map(self.var_hints.get, keys))) if len(mset) != 1: self._uncheck_all_buttons() return if mset == [None]: method_id, args = Methods.Default, () else: method_id, args = mset.pop() self._check_button(method_id, True) self._set_values(method_id, args) finally: self.__interface_update = False @Inputs.data def set_data(self, data: Optional[Table]): self.discretized_vars = {} self.data = data self.varview.model().set_domain(None if data is None else data.domain) self._update_discretizations() self._update_default_model() self.varview.select_default() self._set_mdl_button() self.commit.now() @gui.deferred def commit(self): if self.data is None: self.Outputs.data.send(None) return def part(variables: List[Variable]) -> List[Variable]: return [dvar for dvar in (self.discretized_vars.get(variable_key(v), v) for v in variables) if dvar] d = self.data.domain domain = Domain(part(d.attributes), part(d.class_vars), part(d.metas)) output = self.data.transform(domain) self.Outputs.data.send(output) def send_report(self): dmodel = self.varview.default_view.model() desc = dmodel.data(dmodel.index(0)) self.report_items((tuple(desc.split(": ", maxsplit=1)), )) model = self.varview.model() reported = [] for row in range(model.rowCount()): name = model[row].name desc = model.data(model.index(row), Qt.UserRole) if desc.hint is not None: name = f"{name} ({format_desc(desc.hint)})" reported.append((name, ', '.join(desc.values))) self.report_items("Variables", reported) @classmethod def migrate_settings(cls, settings, version): if version is None or version < 2: # was stored as int indexing Methods (but offset by 1) default = settings.pop("default_method", 0) default = Methods(default + 1) settings["default_method_name"] = default.name if version is None or version < 3: method_name = settings.pop("default_method_name", DefaultHint.method_id.name) k = settings.pop("default_k", 3) cut_points = settings.pop("default_cutpoints", ()) method_id = getattr(Methods, method_name) if method_id in (Methods.EqualFreq, Methods.EqualWidth): args = (k, ) elif method_id == Methods.Custom: args = (cut_points, ) else: args = () default_hint = VarHint(method_id, args) var_hints = {DefaultKey: default_hint} for context in settings.pop("context_settings", []): values = context.values if "saved_var_states" not in values: continue var_states, _ = values.pop("saved_var_states") for (tpe, name), dstate in var_states.items(): key = (name, tpe == 4) # time variable == 4 method = dstate.method method_name = type(method).__name__.replace("Leave", "Keep") if method_name == "Default": continue if method_name == "Custom": args = (", ".join(f"{x:g}" for x in method.points), ) else: args = tuple(method) var_hints[key] = VarHint(getattr(Methods, method_name), args) settings["var_hints"] = var_hints