classification
Title: IDLE: configdialog -- factor out VarTrace class
Type: enhancement Stage: resolved
Components: IDLE Versions: Python 3.7, Python 3.6
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: terry.reedy Nosy List: csabella, terry.reedy
Priority: normal Keywords:

Created on 2017-07-05 06:03 by terry.reedy, last changed 2017-07-28 22:39 by terry.reedy. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 2872 merged csabella, 2017-07-25 22:55
PR 2903 merged terry.reedy, 2017-07-26 23:12
PR 2914 merged csabella, 2017-07-27 14:08
PR 2935 merged terry.reedy, 2017-07-28 18:43
PR 2936 merged terry.reedy, 2017-07-28 19:42
PR 2937 merged terry.reedy, 2017-07-28 21:02
Messages (22)
msg297703 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-05 06:03
configdialog.ConfigDialog creates about 20 tk Variables corresponding to configuration options.  It defines a var_changed_xyz callback for each xyz variable.  6 have a common template.  ConfigDialog turns tracing on in attach_var_callbacks and off in remove_var_callbacks.  Each of these two functions has the callbacks set hard-coded.

Proposal: Factor out what can.  Register callbacks in set when var created.  Iterate register to attach and remove callbacks.  Something like

def IVariable:
    changes = <instance of changes>  # set externally somehow
    var = set()

    def register(self, callback)
        if isinstance(callback, tuple):
            self.callback = default_callback
            self.args = callback
        else:
            self.callback = callback
        self.vars.add()

    def default_callback(self):  # Used for 6 vars.
        changes.add_item(*self.args, self.get())

    @classmethod
    def attach(cls):
        for var in cls.vars:
        var.trace_add('write', self.callback)

    @classmethod
    def remove(cls):
        for var in cls.vars:
            var.trace_remove('write', var.trace_info()[0][1])
        cls.vars = set()

To get String/Int/BooleanVars, maybe this will work.  (I have only toyed with multiple inheritance.)

class IString(tk.StringVar, IVariable):
    def __init__(self, callback).
        StringVar.__init()  # Possibly not needed with no value to pass.
        self.register(callback)
msg298886 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-23 03:42
This issue is really about managing the set of var,callback pairs. The idea of wrapping vars just clouded the issue.  

class VarTrace:
    def __init__(self):
        self.tracers = []  # or set if need to delete
    def add(self, var, callback):
        if isinstance(callback, tuple):
            callback = self.make_callback(var, callback)
        self.vartrace.add((var, callback))
        return var
    def make_callback(self, var, args):  # Used for 6 vars.
        def var_callback(*params):
            changes.add_item(*args, var.get())
        return var_callback
    def attach(self):
        for var, callback in self.tracers:
            var.trace_add('write', callback)
    def remove(cls):  # detach?
        for var, _ in self.tracers:
            var.trace_remove('write', var.trace_info()[0][1]) # doc this

# A functionalist would define a function that returns a sequence of closures.

tracers = VarTrace()

# write tests, then patch creation of tested vars, retest, patch rest.

-        self.font_name = StringVar(parent)
+        self.font_name = tracers.add(StringVar(parent), var_changed_font)
msg298921 - (view) Author: Cheryl Sabella (csabella) * Date: 2017-07-24 00:33
Would you like this change for fonts and indent before the tests for the other tabs are ready or would you like to have tests for all the tabs first?
msg298929 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-24 06:50
Step 1 is to add the class and then add tests.  The tests can use artificial examples.  Say 3 vars, 2 callbacks, with the 3rd done as default.  I want to start this now -- see below.  Let me know if you start working on this so we don't duplicate work. 

I just determined that calling trace_add with the same var,c pair is a bad idea.

import tkinter as tk
r = tk.Tk()
v = tk.StringVar(r)
def c(*args): print("hello", args)
print(v.trace_add('write', c))
print(v.trace_add('write', c))
v.set('a')

2545142942664c
2545153771784c
hello ('PY_VAR0', '', 'write')
hello ('PY_VAR0', '', 'write')

The class needs two lists: untraced and traced.  Attach should pop from untraced and append to traced.  The reverse for detach (remove renamed).  
One of the test tracers should increment an int var, so we can test that after calling attach twice, there is one one increment.

I will take care of looking at the doc or code to refresh my memory of why the detach is written the way it is.

Step 2, putting this in use, is a prerequisite for writing proper tests for extracted tab page classes, which are a prerequisite for putting the class in use.  So I am inclined to convert font vars when a tested class is ready and temporarily have old and new attach functions called.

We may discover a need or at least a use for other VarTrace methods when writing tests.
msg299012 - (view) Author: Cheryl Sabella (csabella) * Date: 2017-07-24 18:29
I would like to work on this, but I don't think I'd be able to have something ready for a few days, so I don't want to hold you up if you would like it sooner.
msg299014 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-24 19:53
I am going to continue with the tests.
msg299270 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-26 23:10
New changeset 45bf723c6c591ec56a18dad8150ae89797450d8b by Terry Jan Reedy (csabella) in branch 'master':
bpo-30853:  IDLE: Factor a VarTrace class from configdialog.ConfigDialog. (#2872)
https://github.com/python/cpython/commit/45bf723c6c591ec56a18dad8150ae89797450d8b
msg299271 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-26 23:15
I will include new blurb with PR that uses this with font vars.  I might do this tonight.

Test coverage of class is 100%.
msg299274 - (view) Author: Cheryl Sabella (csabella) * Date: 2017-07-26 23:34
Thanks!

I can try the font vars if you like.

One question I keep forgetting to ask -- I can't figure out how remove_var_callbacks gets called.  I've grepped for the name and didn't find it anywhere.  I don't know what I'm missing.
msg299280 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-27 00:53
New changeset 0243bea55dc340067247e635442f2a227705315a by Terry Jan Reedy in branch '3.6':
[3.6] bpo-30853:  IDLE: Factor a VarTrace class from configdialog.ConfigDialog. (GH-2872) (#2903)
https://github.com/python/cpython/commit/0243bea55dc340067247e635442f2a227705315a
msg299283 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-27 01:30
Go ahead.  If it works with font, add general tab.

CD.remove_var_callbacks is currently only called in test_configdialog.tearDownModule.  I added the call to prevent getting a TclError for each callback after the test finished.  .destroy does not destroy callbacks.

I presume remove_var_callbacks was written to be called in self.cancel. Then perhaps someone discovered that there was no visible effect of deleting it, perhaps because TclErrors are caught and ignored.  I want to put it, or the new equivalent, into cancel, before destroy.  And make  sure to add to teardownmodule.
msg299286 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-27 01:46
See in you can install blurb into 3.6 or even 3.5, (pip should work) and run it to add the blurb file.  I will fill in the body.
msg299312 - (view) Author: Cheryl Sabella (csabella) * Date: 2017-07-27 14:21
The font vars went well, so I also added general vars.

I created the blurb.  I didn't know if it would work with no body, so I just put a placeholder.

I did have another question.  Instead of just one tracers.add() method, I was wondering if there's an advantage to having one called add_callback(var, callback) and a separate one called add_default(var, config)?  Design-wise, I don't know which is the preferred way, but changing the existing code seemed a little hackish.
msg299313 - (view) Author: Cheryl Sabella (csabella) * Date: 2017-07-27 14:25
Just as an FYI with blurb, I installed it in the same venv as coverage, so I was able to run it with 3.7.  It's really cool.
msg299359 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-28 03:45
A minor change.  Complete the path by adding the other var-trace pairs and after checking carefully, I will be ready to push.

I prefer one easy to remember short-name function to two longer-named functions.  However, the docstring should specify the tuple as idleConf config-type, section, and option names.  Either *add* or *make_callback* could check len = 3 and callback[0] in idleConf.config_types, with a test added.

"changing the existing code seemed a little hackish": I don't understand exactly what you think is hackish.

There might be people who would prefer (possibly as less hakish)

def add(tkvar, parent, cb_info):
    var = tkvar(parent)
    if isinstance(callback, tuple):
        callback = self.make_callback(var, cb_info)
    else:
        callback = cb_info
    self.untraced.append((var, callback))
    return var

I don't know if this would make the add calls readable, but is does factor the var calls into one place, replaces () with ',' (at the cost of passing one more reference), and avoids the hackish return of an input.  It also simplifies the code in a way by making it 'flatter' instead of nested.  Did I miss something?  What do you think?

 The parameter name change is an independent idea that should satisfy someone who objects to calling something an x that is not an x.
msg299382 - (view) Author: Cheryl Sabella (csabella) * Date: 2017-07-28 12:02
Instead 'hackish', maybe I should have used 'magic'.  The overloading just wasn't obvious to me, meaning I have:

self.font_bold = tracers.add(BooleanVar(parent), self.var_changed_font)
self.space_num = tracers.add(IntVar(parent), ('main', 'Indent', 'num-spaces'))

We defined VarTrace as being (var, callback) pairs and the second example isn't sending a function.  So, even though I understand what we're doing, I wanted to ask about using different names for my own education.  I was even thinking of a different interface --

add(var, callback=default, config=None)

If config was specified even for the non-default callbacks, then each var could have its config defined at create time instead of in the var_changed* function.  This wouldn't work for theme/keys `name` and `name2` though (I think that's the only one with two add_option calls).  If the callback didn't have a changes.add_option, then it can send None for config.

I hadn't thought of separating `parent`, but I like that idea.  It fits in with the rest of how the widgets are created. 

So, if both changes were incorporated:

self.font_bold = tracers.add(parent, BooleanVar, 
                         callback=self.var_changed_font, 
                         config=('main', 'EditorWindow', 'font-bold'))
self.space_num = tracers.add(parent, IntVar, 
                         callback=default, 
                         config=('main', 'Indent', 'num-spaces'))

Maybe that expands VarTrace too much?  Or maybe instead of (var, callback) pairs, it's a dictionary?  tracers = {var: (callback, config)}

And then the non-default var_changed methods could use:
changes.add_option(*tracers[var].config, value)

Wouldn't work for var_changed_font because that has the three add_option calls.
msg299388 - (view) Author: Cheryl Sabella (csabella) * Date: 2017-07-28 14:20
"Either *add* or *make_callback* could check len = 3 and callback[0] in idleConf.config_types, with a test added."

I didn't add this because I wasn't sure what you wanted to happen if it wasn't right.  I suspect it should fail gracefully (not break IDLE), but in what way?
msg299420 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-28 18:41
New changeset 5b59154c0d3d91c0766b9177f6b737b1abcbf3f6 by Terry Jan Reedy (csabella) in branch 'master':
bpo-30853: IDLE: Convert font and general vars to use VarTrace (#2914)
https://github.com/python/cpython/commit/5b59154c0d3d91c0766b9177f6b737b1abcbf3f6
msg299428 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-28 19:42
New changeset 02f88d2a411a6a789b33be281adfc3570c49efd5 by Terry Jan Reedy in branch '3.6':
[3.6] bpo-30853: IDLE: Convert font and general vars to use VarTrace (GH-2914) (#2935)
https://github.com/python/cpython/commit/02f88d2a411a6a789b33be281adfc3570c49efd5
msg299430 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-28 19:51
IDLE *will* fail if idleConf.SetOption is called with a wrong config-type (KeyError) or the wrong number of other arguments (TypeError).  I did not add the check because it does not cover the non-default callbacks (the majority), let alone all the other idleConf calls.  The important thing is to make sure that every callback gets called in a test (and indeed, eventually, that every line is executed).  Enhancing the doc string should be enough.
msg299438 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-28 21:00
New changeset 5d0f30aae5fccc99690923fc5c7cb58de8ad7eec by Terry Jan Reedy in branch 'master':
bpo-30853: IDLE - touch-up configdialog.VarTrace and tests. (#2936)
https://github.com/python/cpython/commit/5d0f30aae5fccc99690923fc5c7cb58de8ad7eec
msg299439 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-28 22:36
New changeset ecc80b3f1b56f1e4df9e592f8527e622a6b45e01 by Terry Jan Reedy in branch '3.6':
[3.6] bpo-30853: IDLE - touch-up configdialog.VarTrace and tests. (GH-2936) (#2937)
https://github.com/python/cpython/commit/ecc80b3f1b56f1e4df9e592f8527e622a6b45e01
History
Date User Action Args
2017-07-28 22:39:16terry.reedysettitle: IDLE: configdialog -- factor out Tracer subclass -> IDLE: configdialog -- factor out VarTrace class
2017-07-28 22:37:27terry.reedysetstatus: open -> closed
resolution: fixed
stage: needs patch -> resolved
2017-07-28 22:36:32terry.reedysetmessages: + msg299439
2017-07-28 21:02:46terry.reedysetpull_requests: + pull_request2989
2017-07-28 21:00:05terry.reedysetmessages: + msg299438
2017-07-28 19:51:07terry.reedysetmessages: + msg299430
2017-07-28 19:42:45terry.reedysetmessages: + msg299428
2017-07-28 19:42:17terry.reedysetpull_requests: + pull_request2988
2017-07-28 18:43:05terry.reedysetpull_requests: + pull_request2987
2017-07-28 18:41:03terry.reedysetmessages: + msg299420
2017-07-28 14:20:25csabellasetmessages: + msg299388
2017-07-28 12:02:06csabellasetmessages: + msg299382
2017-07-28 03:45:31terry.reedysetmessages: + msg299359
2017-07-27 14:25:21csabellasetmessages: + msg299313
2017-07-27 14:21:02csabellasetmessages: + msg299312
2017-07-27 14:08:04csabellasetpull_requests: + pull_request2966
2017-07-27 01:46:48terry.reedysetmessages: + msg299286
2017-07-27 01:30:15terry.reedysetmessages: + msg299283
2017-07-27 00:53:15terry.reedysetmessages: + msg299280
2017-07-26 23:34:21csabellasetmessages: + msg299274
2017-07-26 23:15:28terry.reedysetstage: test needed -> needs patch
messages: + msg299271
versions: + Python 3.6, Python 3.7
2017-07-26 23:12:34terry.reedysetpull_requests: + pull_request2954
2017-07-26 23:10:00terry.reedysetmessages: + msg299270
2017-07-26 18:58:42terry.reedylinkissue31004 dependencies
2017-07-25 22:55:09csabellasetpull_requests: + pull_request2923
2017-07-24 19:53:37terry.reedysetmessages: + msg299014
2017-07-24 18:29:25csabellasetmessages: + msg299012
2017-07-24 06:50:07terry.reedysetmessages: + msg298929
2017-07-24 00:33:58csabellasetmessages: + msg298921
2017-07-23 03:42:29terry.reedysetnosy: + csabella

messages: + msg298886
title: IDLE: configdialog -- factor out Variable subclass -> IDLE: configdialog -- factor out Tracer subclass
2017-07-05 06:03:26terry.reedycreate