Class Composition Between Two Classes

Hello,

I have created two classes (for now). One class is being used as the main (Testing), and the other is being used as a composition class (Login). When I attempted to run the main class, I had initially attempted to do it via the following lines:

if __name__ == "__main__":
        
    app = Testing()
    app.mainloop()

Doing it this way, it failed. After many iterations of different set ups, I finally tried the following way (adding the ‘tk’ in front of the mainloop()) and it appears to work … not sure why:

if __name__ == "__main__":
        
    app = Testing()
    tk.mainloop()

Not sure why it works this way. Can someone please give a valid explanation.
Another issue that I came across is that if I set the variable testing to ‘True’, the code appears to execute as expected. However, if I set it to ‘False’, then I get the following exception:

RuntimeError: Too early to run the main loop: no default root window

Here is my test code snippet:

import tkinter as tk
import datetime

# This list contains the authorized users to run tests on this test station.
authorizedUserList = {'James': 'CA456', 'Robert': 'CA841', 'Roger': 'CA341'}

class Login(tk.Tk):
    
    def __init__(self):
                
        super().__init__()
         
        self.verified = False
        
        tk.Tk.title(self,'PN: F4581-01 Test')   # Window title (not real pn)
        tk.Tk.geometry(self, '280x150') 
        
        login_info = tk.LabelFrame(self, padx = 15, pady = 10,
                                text = "Login Info")
        
        tk.Label(login_info, text = "First name").grid(row = 0)
        tk.Label(login_info, text = "Password").grid(row = 1)

        self.username = tk.Entry(login_info)
        self.username.grid(row = 0, column = 1, sticky = tk.W)
        self.password = tk.Entry(login_info, show = '*')
        self.password.grid(row = 1, column = 1, sticky = tk.W)

        self.btn_start_test = tk.Button(self, text = "Start Test", command = self.verifyUser)
        self.btn_start_test.bind("<Return>", self.verifyUser) # Add 'Enter' key as additional input
                                                              # to 'Start Test' button
        login_info.pack(padx = 10, pady = 10)
        self.btn_start_test.pack(padx = 10, pady = 10, side = tk.BOTTOM)
        self.username.focus_set() # Put cursor in 'First Name' cell entry

    def verifyUser(self, *args):

        name = self.username.get().title()
        password = self.password.get().upper()

        if name in authorizedUserList:

            if authorizedUserList[name] == password:
                
                print('\nCredentials verified.') # for testing purposes
                self.verified = True
               
        else:
            print('Unauthorized user! Try Again.')

        self.clear_login()
        #return self.verified

    def clear_login(self):
        self.username.delete(0, tk.END)
        self.password.delete(0, tk.END)

class Testing:

    def __init__(self):

        #super().__init__()

        testing = True
        
        # This is the testing loop
        while testing:

            self.login = Login() # Instantiate class object 'Login' via composition
            print(self.login)
            #self.valid_user = self.login() #verifyUser().verified
            print('Entered while loop and passed login')
            #print(self.valid_user)

##            if(self.login.verifyUser() == True):
##                
##                print('Test start time:', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
##                print('\nBeging testing ...')
##
##            else:
##                print('Wrong credentials!  Please try again.')

            testing = False # Force a stop to the while loop
    

if __name__ == "__main__":
        
    app = Testing()
    tk.mainloop()

Any help / clarification would be appreciated.

By the way, to test the code snippet in the affirmative, you may type the following:
First name: james
Password: ca456

… this is from a combination as defined in the dictionary at the top.

tk.mainloop() takes the registered tk.Tk ‘windows’ and starts running them. The alternative for a single ‘window’ would be to run window.mainloop().

In your case, you have Testing but you can’t access your Login object to call mainloop() on it.

If we simplify the Testing object we can access the tk.Tk and call mainloop():

...
class Testing:
    def __init__(self):
        self.login = Login()


if __name__ == "__main__":
    app = Testing()
    app.login.mainloop()

This functionally works out the same way as your existing version.

For the testing = False issue, you’re hitting that since in your original code, if testing is False a tk.Tk object isn’t created. You need at least one in order to call tk.mainloop().

2 Likes

Hello,

thank you for your response.

1st issue:
I tried your suggestion, and it does work. My interpretation from your response is that because we are using the tk module via inheritance in the Login class, we have to incorporate it somehow into the mainloop(). In my original method, I used ‘tk’ explicitly and your method we use it implicitly since the Login class has tk.Tk as a master. However, I also noticed that the Login class had its first letter lower case. When I tried it as app.Login.mainloop(), it failed (with ‘L’ as upper case - take away is that the composition class has to be entered all lower case). My conjecture (correct me if I’m wrong), is that if you’re going to use a class as a composition, and that class has a module class as a master, you have to somehow include the inherited master class into the main loop call? In your method, we do so implicitly by including that class (the Login class). Please advise.

2nd issue:
Ok, here it appears that I have to incorporate some class, be it a dummy class which does nothing potentially, has to be included so that it at least makes a reference to the ‘tk.Tk’ class. Is this correct?

Now, in a practical application, there would be many more classes that may or may not have tk.Tk class. Would these need to be included as part of the mainloop() as well somehow or only one would suffice?

Again, thank you for your insight and helping this novice understand python a little better.

For 1:

tkinter.mainloop() is a method that can will run the ‘main loop’ for tkinter. It runs window.mainloop() for the main (i think first) ‘window’. You have one window so tkinter.mainloop() does the same thing as running .mainloop() on your instance of Login.

You controlled the casing of the Login variable. For example:

login = Login()

# now i have a Login instance named 'login'

LOGIN = Login()
# now i have another Login instance named LOGIN

# I can now do login.<thing> and LOGIN.<thing>, etc. 
# I don't recommend having both, but I'm just showing for demonstration purposes. 

I’m not 100% what you mean by ‘master’ in your descriptions. You simply have a class Testing with a login property on it.

In Login’s case:

class Login(tk.Tk):
    ...

denotes that Login inherits from tk.Tk. Therefore Login is a child class of tk.Tk. tk.Tk is also a parent class of Login.

For 2:

You have Login which inherits from tk.Tk. So you can do whatever you need in Login. Each window of the application would likely be its own child of tk.Tk.

1 Like

Hello,

thank you for the additional clarification.

Issue 1
I referenced the tk.Tk class as the ‘master’ class because the code snippet is based on this module.
Additionally, in my test snippet:
Login - Base class (inheriting from the tk.Tk classes —> Login(tk.Tk): )
Testing - Derived class
(although ‘Derived’ / ‘Base’ nomenclature is more appropriately used for inheritance, I am using this naming convention loosely because the Testing class is using the Login class via composition)

Regarding the lower case login, ok, I understand now. This is because I instantiated the Login class as: self.login = Login() … here ‘login’ is lower case. For mainloop call, we do not use the ‘else’. This slipped by but now I got it.

Issue 2:
Ok, understood.

Issue 3 (New one):
Stay tuned … I am working on expanding this code snippet. So might have additional questions.

For now, all cleared up. Thank you very much. Much appreciated.

************************************** UPDATE ***************************************

I have a question for the def init(self) method in the Testing class. I notice that when I run the program, it appears that python ram rods through the entirety of the init method before the GUI appears. I am accustomed to the C, procedural language. But with classes, the behavior is a bit different. How can I stop the init method from ram rodding through the entirety of the body code? What I wanted was to run the program and wait for the result of the GUI results before proceeding with the remainder of the code.

class Testing:

    def __init__(self):

        self.login = Login() # Instantiate class object 'Login' via composition
        
      **** Would like to obtain login verification before continuing with the rest of the code here ***
        # Verify valid user
             ------------------------------------------------------------------

             Body of additional __init__(self) method

             ------------------------------------------------------------------

… but entirety of the init method appears to execute before GUI appears.

Doing

login = Login()

(sort of) prepares the window to be shown… but doesn’t necessarily show it.

This definitively shows it:

login.mainloop()

The problem then is that mainloop() will wait until the window closes to continue the code execution.

(As an aside:

mainloop() is actually more/less looping over and over looking for events to process with regard to the given window.

Generally we use event-driven programming with GUIs such that things like clicking a button, etc. cause a piece of code to be executed. This works because internally the click would generate an event that the main-loop would notice and act on by calling the callback function you gave.

So I’ll give you a solution that may work to do what you’re saying, but consider event-driven programming since it is the recommended way to work here.

)

Here is a possible way to do what you’re asking, with more hints embedded as comments. I’ve simplified the code a bit as an example. Upon calling try_login() it’ll pop up a login window. Once you close the window, the code execution will continue.

import tkinter as tk
import time


class Login(tk.Tk):
    def __init__(self):
        super().__init__()

        login_info = tk.LabelFrame(self, padx=15, pady=10, text="Login Info")

        tk.Label(login_info, text="Username").grid(row=0)
        tk.Label(login_info, text="Password").grid(row=1)

        # These string vars will be available after this object's tk.Tk is destroyed.
        # Other attributes will likely be unavailable.
        self.username_stringvar = tk.StringVar()
        self.password_stringvar = tk.StringVar()

        self.username = tk.Entry(login_info, textvariable=self.username_stringvar)
        self.username.grid(row=0, column=1, sticky=tk.W)

        self.password = tk.Entry(
            login_info, show="*", textvariable=self.password_stringvar
        )
        self.password.grid(row=1, column=1, sticky=tk.W)

        login_info.pack(padx=10, pady=10)
        self.username.focus_set()


class Testing:
    def _run_login_till_destroyed(self):
        """
        This will create a Login object and run it until it is closed.

        Then it will return the object. Note that only certain attributes on it will
        be available as the object's tk.Tk has been destroyed.
        """
        login = Login()
        try:
            # Simulate what login.mainloop() would more/less do
            while login.winfo_exists():
                login.update()
                login.update_idletasks()
                time.sleep(0.1)

                # If you need to do something in this loop, you can do it here (though I wouldn't recommend it).
                # Consider event-driven programming or using something like tkinter's after method instead.

        except tk.TclError as ex:
            # We expect the application to wind up being destroyed and trigger an exception like this.
            # Unfortunately, its just a tk.TclError and not something more unique to catch
            if not "application has been destroyed" in str(ex):
                # If its some other TclError, raise it
                raise

        # At this point only certain things will be accessible on the Login object
        return login

    def try_login(self):
        # This line will wait for the user to close the window to continue code execution
        # If you want to run something while the window is still open, you could modify the
        # loop in _run_login_till_destroyed to do something else... though honestly the better
        # option would be to use event-driven programming.
        login = self._run_login_till_destroyed()

        # Now the window has been closed, and we can check the values of the stringvars
        if login.username_stringvar.get() != "test_user":
            print("Username did not match")

        if login.password_stringvar.get() != "test_pass":
            print("Password did not match")


if __name__ == "__main__":
    app = Testing()
    app.try_login()

I still highly recommend looking into event-driven programming.

1 Like

Hello,

ok, I implemented your suggested code into my application (the top method only). Got it working. I also created a new class of which I strictly use to hold variables which are used as flags between classes. I merely make sure to use this class as an inherited class for all other classes. It is a useful technique that I found online and appears to work really well. Apparently, someone had posted their issue in a forum. It wasn’t until two months later that someone stumbled upon the thread and made this suggestion which ended up fixing that person’s SW issue.

class InterClassFlags:

     flag_1 = False  # etc.

Since I can use the result during the username/password verification method in the Login() class, I can pass this variable along (outside) as my test variable flag. For the closing window issue, I replaced the ‘raise’ with a ‘pass’ so that an error in red letters is not generated (tkinter.TclError: can’t invoke “winfo” command: application has been destroyed).

In my application test snippet, I don’t plan on closing the login window after a successful login. I actually plan on keeping it as part of a larger GUI. Perhaps on the upper left-hand corner. After the login is successful, the rest of the GUI can become highlighted active, and the login unhighlighted (turns grey), …, the general idea.

In any case, thank you for pointing me along in the right direction. Your input has been invaluable.
As always, much obliged. :raised_hands:

P.S

Yes, I will look into event-driven programming. This is similar to asynchronous hardware interrupts of which I am familiar from embedded programming. It appears that this is popular with OOP. First, I have to learn tkinter module (though some of it goes hand in hand), … :smiley:

Again, thank you a bunch. :100:

1 Like