Welcome, guest | Sign In | My Account | Store | Cart

This code is a little variation of my other trick:

https://code.activestate.com/recipes/580721-tkinter-remote-debugging

It makes more easy to create tests for Tkinter.

Install rpyc:

pip install rpyc

Save the code below to a file named for example tkinter_selenium.py.

This is the usage:

python tkinter_selenium.py [-h] [-p PORT] filename

where filename is the path to main file of Tkinter application, and port is an optional port number for the remote interpreter. Otherwise it uses default port.

Then in another python interpreter you can interact with the application. For example, write:

import rpyc
c = rpyc.classic.connect("localhost")
c.execute("""
from Tkinter import Button, Toplevel
import tkMessageBox 

responsive_button = Button(Toplevel(), text="It's responsive", command = lambda:tkMessageBox.showinfo("alert window", "It's responsive!"))

responsive_button.pack()
""")

responsive_button = c.eval("responsive_button")
responsive_button.invoke()

(This example only works for Python 2. For python 3 use "tkinter" instead of "Tkinter" and so on)

Use port keyword argument to "repyc.classic.connect" if you want a different port number than default port. For example:

import rpyc
c = rpyc.classic.connect("localhost", port=8000)

For the selection of tkinter widgets, I have this other trick:

https://code.activestate.com/recipes/580738-tkinter-selectors

Using this remote debugging utility and selectors makes easy to test tkinter applications similar to selenium.

This utility could be used not only for Tkinter applications. It could be used also for wxpython, pygtk and pyqt applications.

NOTE: Interact with remote application using python of same version. If the application is running using a Python 2 interpreter, use a python 2 interpreter for remote interaction. Similarly use a python 3 interpreter for remote interaction with a python 3 application.

Python, 82 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# Author: Miguel Martinez Lopez

from rpyc.utils.server import ThreadedServer
from rpyc.utils.classic import DEFAULT_SERVER_PORT
from rpyc.core.service import Service, ModuleNamespace

from rpyc.lib.compat import execute, is_py3k

import sys
import os
import threading
import argparse
    
EXECUTED_PYTHON_FILE = False

def exec_python(filepath, namespace):
    global EXECUTED_PYTHON_FILE

    if EXECUTED_PYTHON_FILE:
        raise Exception("exec_python can be used only one time")

    EXECUTED_PYTHON_FILE = True

    filepath = os.path.abspath(filepath)
    sys.path = [os.path.dirname(filepath)] + sys.path[1:]

    namespace["__file__"] = filepath
    namespace["__name__"] = "__main__"

    with open(filepath, 'rb') as file:
        exec(compile(file.read(), filepath, 'exec'), namespace)


class PublicService(Service):
    exposed_namespace = {}

    def on_connect(self):
        self._conn._config.update(dict(
            allow_all_attrs = True,
            allow_pickle = True,
            allow_getattr = True,
            allow_setattr = True,
            allow_delattr = True,
            import_custom_exceptions = True,
            instantiate_custom_exceptions = True,
            instantiate_oldstyle_exceptions = True,
        ))

        # shortcuts
        self._conn.modules = ModuleNamespace(self._conn.root.getmodule)
        self._conn.eval = self._conn.root.eval
        self._conn.execute = self._conn.root.execute
        self._conn.namespace = self._conn.root.namespace
        if is_py3k:
            self._conn.builtin = self._conn.modules.builtins
        else:
            self._conn.builtin = self._conn.modules.__builtin__
        self._conn.builtins = self._conn.builtin

    def exposed_execute(self, text):
        """execute arbitrary code (using ``exec``)"""
        execute(text, self.exposed_namespace)
    def exposed_eval(self, text):
        """evaluate arbitrary code (using ``eval``)"""
        return eval(text, self.exposed_namespace)
    def exposed_getmodule(self, name):
        """imports an arbitrary module"""
        return __import__(name, None, None, "*")
    def exposed_getconn(self):
        """returns the local connection instance to the other side"""
        return self._conn

parser = argparse.ArgumentParser(description='Remote debugging and testing')
parser.add_argument('filename', help="Path to script")
parser.add_argument('-p', '--port', action="store", dest="port", default=DEFAULT_SERVER_PORT, help="Remote interpreter port", type=int)

args = parser.parse_args()
thread = threading.Thread(target=lambda: ThreadedServer(PublicService, hostname = "localhost", port=args.port).start())
thread.daemon=True
thread.start()

exec_python(args.filename, PublicService.exposed_namespace)