2

I've been working on a PyQt5 application over the past year and it's grown to the point where I'm running into some issues. I have several classes that depend on "service" type objects (single instances) such as subclassed QAbstractItemModel and a QNetworkAccessManager where these are classes where you generally create a single instance for.

The way I've been including these dependencies in the classes that need them is I have a module for example called network_manager.py that looks something like this:

# network_manager.py
# import statements...

class NetworkManager(QNetworkAccessManager):
    # class code...

network_manager = NetworkManager()

And then in another module where a class needs to use the network manager I've been doing the following:

# another_module.py
from network_manager import network_manager

class SomeClass:
    # class can now use network_manager via the import

This worked well for awhile, but I started applying this same idea to my QAbstractItemModel classes and as those classes grew, they became more complex with their own custom signals, etc. I started to get some weird bugs where signals were not triggering slots. My main.py looks something like this:

# main.py
from download_model import download_model  # same idea as above/single instance

class MainWindow(QMainwindow):
    # class uses/depends on download_model

def main():
    app = QApplication()
    # rest of setup code...
    window = MainWindow()
    window.show()
    app.exec_()

if __name__ == '__main__':
    main()

It turns out this bug I was getting was because the imports at the top of main.py cause (in this case) the DownloadModel class to instantiate. This DownloadModel instance sets up custom signals inside __init__(). This occurs BEFORE my QApplication instance has even been created which can cause strange bugs. My solution? I moved the import statements that are causing these single instance creations to inside MainWindow __init__().

I thought maybe I should just get MainWindow() out of main.py to fix the issue of having to have my imports inside methods/functions, but the issue would still be there. If MainWindow were in main_window.py, I'd still need to import that class in main.py which in turn would import the download_model dependency causing the same issue where I have to delay imports. Maybe I'm overthinking it and this is a common practice. But it seems like a code smell to me.

So what IS the "right" way to inject these single instance dependencies into classes that need them? It doesn't make sense that client code has to provide those dependencies itself. For example, one of my classes is called DownloadRequest and it depends on/uses the network_manager instance. The client code does something like:

req = DownloadRequest('http://example.com/video.avi')

It wouldn't make sense in the business logic to have to pass the network manager instance each time. I guess you could use factories and not let clients directly use DownloadRequest

I was then thinking of trying something like this:

# download_request.py
from network_manager import NetworkManager

class DownloadRequest:
    def __init__(self, url, network_manager=NetworkManager)
        self.nm = NetworkManager()
        ...

Of course I then would have to make sure NetworkManager is instantiated only the first time and every other time it's instantiated (in other classes that use the same keyword parameter DI) it just returns the same instance. This can be accomplished easily using the borg singleton pattern.

I also thought of trying to make some stupid simple service locator module like the following:

# service.py
# import statements...

network_manager = None
download_model = None

def getService(serviceName):
    if serviceName == 'NetworkManager':
        if network_manager is None:
            global network_manager
            network_manager = NetworkManager()
        return network_manager
    ...

and then in a module that requires the service:

# module_that_needs_service.py
from service import getService

class SomeClass:
   def __init__(self)
      self.nm = getService('NetworkManager')

But this just defeats the purpose of dependency injection and makes the class more difficult to test if you want to test a different network manager for example.

I still do like the simplicity of how I've been doing it all along with creating the instance right after the class statement and then importing that instance into any module where it's needed. Maybe this idea is ok and I just need to fix my initliazation code in main.py. I'm open to any suggestions.

So what is the most ideal way of injecting these "global" single instance objects in classes that need them in Python?

Thanks for any input and suggestions.

Wallboy
  • 154
  • 5
  • 14

0 Answers0