AllenMoh
AllenMoh

Reputation: 476

Python decorating a method in a class and inheritance

I am writing a test automation framework for a GUI application and I wanted to use decorators as a way to catch pop-ups that are generated by methods within the class (for example login)

I have a _BaseWindow class that keeps track of the elements of the GUI that are in every window (eg: menu bar, popups), which is inherited by a MainWindow class. The MainWindow class keeps track of buttons on the main menu, as well the dialog generated when one of the buttons is clicked. For example, if you click the login button on the main menu, the login dialog is loaded.

class _BaseWindow(object):
    def __init__(self):
        self.window = "windowName"
        self.popup = None

    def _catch_popups(self, method):
        from functools import wraps
        @wraps(method)
        def wrapper(*args, **kwargs):
            # get a list of the open windows before the method is run
            before = getwindowlist()

            retval = method(*args, **kwargs)

            # get a list of the open windows after the method is run
            after = getwindowlist()

            for window in after:
                if window not in before:
                    self.popup = window
                    break

            return retval
        return wrapper

class MainWindow(_BaseWindow):
    def __init__(self):
        self.dialog = None
        self.logged_in = False

        # buttons
        self.login_button = "btnLogin"

        super(MainWindow, self).__init__()

    def click_login(self):
        if not self.dialog:
            mouseclick(self.window, self.login_button)
            self.dialog = LoginDialog()

class LoginDialog(_BaseWindow):
    def __init__(self):
        # buttons
        self.button_ok = "btnLoginOK"
        self.button_cancel = "btnLoginCancel"
        # text input
        self.input_username = "txtLoginUsername"
        self.input_password = "txtLoginPassword"

        super(LoginDialog, self).__init__()

    @MainWindow._catch_popups
    def perform_login(self, username, password):
        input_text(self.input_username, username)
        input_text(self.input_password, password)
        mouseclick(self.window, self.button_ok)

When I try to test this I get:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "objects/gui.py", line 185, in <module>
    class LoginDialog(_BaseWindow):
  File "objects/gui.py", line 236, in LoginDialog
    @MainWindow._catch_popups
TypeError: unbound method _catch_popups() must be called with 
MainWindow instance as first argument (got function instance instead)

When I try to specify the MainWindow instance as:

@MainWindow._catch_popups(self)

I get:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "objects/gui.py", line 185, in <module>
    class LoginDialog(_BaseWindow):
  File "objects/gui.py", line 236, in LoginDialog
    @MainWindow._catch_popups(self)
NameError: name 'self' is not defined

Any help in understanding this would be most appreciated.

Upvotes: 1

Views: 507

Answers (1)

Jared Goguen
Jared Goguen

Reputation: 9010

There's an error in your structure here. Instance methods that are defined in the class body are created at the same time as the class and, as such, are unbound methods. Thus, at the time of creation, there is no self to reference. Your _catch_popups method requires an instance.

The natural solution is to change _catch_popups to be a staticmethod that returns an instance method.

@staticmethod
def _catch_popups(method):
    from functools import wraps
    @wraps(method)
    def wrapper(self, *args, **kwargs):
        before = getwindowlist()
        retval = method(self, *args, **kwargs) # notice that self is passed
        after = getwindowlist()
        try: self.popup = next(w for w in after if w not in before)
        return retval
    return wrapper

Here, self is explicitly passed to method because it is still an unbound instance method when it is being passed to the decorator.


As suggested in your comment, you want to add the popup to the window instance. To do this you could update the following methods:

def click_login(self):
    if not self.dialog:
        mouseclick(self.window, self.login_button)
        self.dialog = LoginDialog(self) # pass window to dialog __init__

class LoginDialog(_BaseWindow):
    def __init__(self, parent_window):
        # original code
        self.parent = parent_window

@staticmethod
def _catch_popups(method):
    def wrapper(self, *args, **kwargs):
        # original code
        try: self.parent.popup = ... # add to parent rather than self
        return retval
    return wrapper

Upvotes: 3

Related Questions