Search code examples
pythonpython-importlib

Finding classes in an external directory without modifying sys.path


I have a function that takes in an external path that holds some python files and discovers any classes that exist within those files. It returns a map of the class name and the class itself so that I can create an object from it, if I want. The following code below works how I intended it to, but I am trying to do it without appending the sys.path.

def find_plugins(path):
    sys.path.append(path)   
    plugin_map = {}
    current_module_name = os.path.splitext(os.path.basename(path))[0]
    for file in glob.glob(path + '/*.py'):
        name = os.path.splitext(os.path.basename(file))[0]

        if name.startswith('__'):
            continue

        module = importlib.import_module(name, package=current_module_name)
        for member in dir(module):
            plugin_class = getattr(module, member)
            if plugin_class and inspect.isclass(plugin_class):
                plugin_map[member] = plugin_class
    return plugin_map

If I remove the sys.path line, I get the following stack trace:

ModuleNotFoundError: No module named 'plugin'

where 'plugin' is the plugin.py file in the path provided to the function. Is it even possible to do this without appending the sys.path?


Solution

  • You could do it using the technique shown in the accepted answer to the question
    How to import a module given the full path which doesn't require manipulating sys.path.

    import importlib.util
    import inspect
    from pathlib import Path
    
    
    def find_plugins(path):
        plugins = {}
        for module_path in Path(path).glob('*.py'):
            module_name = module_path.stem
            if not module_name.startswith('__'):
                # Load module.
                spec = importlib.util.spec_from_file_location(module_name, module_path)
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)  # Execute module in its own namespace.
                # Extract classes defined in module.
                for member in dir(module):
                    plugin_class = getattr(module, member)
                    if plugin_class and inspect.isclass(plugin_class):
                        plugins[member] = plugin_class
    
        return plugins
    
    
    if __name__ == '__main__':
        from pprint import pprint
    
        plugins_folder = '/path/to/plugins'
        plugins = find_plugins(plugins_folder)
    
        pprint(plugins)