Forum Archive

Dynamically Load Modules

blmacbeth

I have an app I'm working on which is supposed to load games dynamically from a sub folder. This is the basic setup.

Main Folder/
    games/
        game1.py
        ...
        gameN.py
    main.py

In main.py I want to load all games in the games subfolder and their respective pyui file. This is so I can create new games and the main ui file can grab them. This will make extending my app with new games fairly painless.

I have tried using the __import__() function, but I have not had much luck. Do y'all have any ideas?

ccc
import importlib
module_name = 'requests'  # NOT requests.py
mod = importlib.import_module(module_name)
briarfox

You may want to look at runpy from the standard library.

https://docs.python.org/2/library/runpy.html

blmacbeth

After searching the Internet and StackOverflow I found a person with a similar problem. I looked at the answers and found this within the comments from the original poster who had found a solution on his own. It's not pretty and can do bad things if I'm not careful, but it works.

## Load Games modules
path = os.path.dirname(os.path.abspath(__file__))+'/games'

modules = [f.split('.')[0] for f in os.listdir(path) if f.endswith(".py")]

## I hate myself for this :(
for module in modules:
    cmd = 'from %s import *' % module 
    exec(cmd)

If you have any suggestions to make this better/safer, please let me know. But for now, this works as needed.

JonB

Shellista had a nice mechanism for dynamic imports. The key was to use importlib.import_module, which returns the module, which you insert into your global dict. Below are the relevant functions from the ModuleInstaller (which did a lot more, also downloading modules if needed). The key is to make sure you add the path to sys.path first, then use import_module and add the result to globals() so you can reference it. Ahh, I see ccc already mentioned this module.

    def _global_import(self, modulename):
        module = importlib.import_module(modulename)
        globals()[modulename] = module

    def _add_module_path(self):
        '''Add the installed module path to sys.path'''
        mod_path = self.full_install_path + os.sep
        if not os.path.exists(mod_path):
            os.makedirs(mod_path)

        if not mod_path in sys.path:
            sys.path.insert(0, self.full_install_path + os.sep)

    def try_import(self):
        self._add_module_path()
        self._global_import(self.module_name)

Also..are you sure you want to import modules, rather than run them? Unless the scripts and pyuis are written to allow them to be imported, I think you will have problems; for instance actions won't be assigned correctly.

blmacbeth

Thanks @JonB.

I have my games written so that each ui has a class associated with it, which is named the same as the module so as to avoid "collisions". And each class has the actions in it.

dgelessus

Sorry to say this, but my personal experiences with the import mechanisms of ShellistaExt and Shellista/dev-modular were not the best. In most cases I kept getting "package not found" type errors that were hard or impossible to fix, and the only way I could successfully use non-builtin commands was with an absolutely clean Python environment (i. e. full app restart).

Since the purpose of blmacbeth's script seems to be running other scripts as if they were the main script (rather than importing them as a library), runpy is probably the easiest option. Or for full control over the environment in which the scripts are run, use exec:

import os
import sys

with open(os.path.join(os.path.dirname(sys.argv[0]), "games", "nameofthegame.py")) as f:
    custom_env = {}
    exec(f.read(), custom_env)

This way you can even do some advanced things by manually compiling the code before execing it, but I doubt you'd need those kinds of features for this script.

ccc

I liked @briarfox's runpy idea best. Here is a working example:

import os, runpy

path = os.path.join(os.path.dirname(__file__), 'games')
python_scripts = [f for f in os.listdir(path) if f.lower().endswith('.py')]
for i, ps in enumerate(python_scripts):
    print('{:>3} - {}'.format(i, ps[:-3]))
while True:
    selection = raw_input('Which game do you want to run: ')
    if selection.lower() in ('', 'q', 'quit'):  # enter nothing to quit
        break
    try:
        runpy.run_path(os.path.join(path, python_scripts[int(selection)]))
    except (IndexError, ValueError):
        print('Please pick one of the numbers above!!')