Import a module from an absolute path without modifying sys.modules

Viewed 843

So, I know I can load a python module given a filepath to a module as such

def import_module_from_fpath(module_fpath):
    from os.path import basename, splitext, isdir, join, exists, dirname, split
    import platform
    if isdir(module_fpath):
        module_fpath = join(module_fpath, '__init__.py')
    print('module_fpath = {!r}'.format(module_fpath))
    if not exists(module_fpath):
        raise ImportError('module_fpath={!r} does not exist'.format(
            module_fpath))
    python_version = platform.python_version()
    modname = splitext(basename(module_fpath))[0]
    if modname == '__init__':
        modname = split(dirname(module_fpath))[1]
    if python_version.startswith('2.7'):
        import imp
        module = imp.load_source(modname, module_fpath)
    elif python_version.startswith('3'):
        import importlib.machinery
        loader = importlib.machinery.SourceFileLoader(modname, module_fpath)
        module = loader.load_module()
        # module = loader.exec_module(modname)
    else:
        raise AssertionError('invalid python version={!r}'.format(
            python_version))
return module

However, this always seems to populate an entry in sys.modules?

I have the case where I have two different modules in different paths. Both modules are not reachable from the PYTHONPATH. I want to be able to load these modules locally in different functions and not have them interfere with each other. The kicker to this problem is both modules have the same name (different versions). Thus, I don't want sys.modules to cache anything in between.

I'm not sure if its safe to simply delete the module name from sys.modules after I load them. It seems like it might be from a quick test I did:

    import shutil
    import ubelt as ub
    test_root = ub.ensure_app_cache_dir('test_fpath_import')
    # Clear the directory
    shutil.rmtree(test_root)
    test_root = ub.ensure_app_cache_dir('test_fpath_import')

    # -----
    # Define two temporary modules with the same name that are not in sys.path
    import sys, os, os.path
    from os.path import join

    # Even though they have the same name they have different values
    mod1_fpath = ub.ensuredir((test_root, 'path1', 'testmod'))
    ub.writeto(join(mod1_fpath, '__init__.py'), 'version = 1\nfrom . import sibling\na1 = 1')
    ub.writeto(join(mod1_fpath, 'sibling.py'), 'spam = \"ham\"\nb1 = 2')

    # Even though they have the same name they have different values
    mod2_fpath = ub.ensuredir((test_root, 'path2', 'testmod'))
    ub.writeto(join(mod2_fpath, '__init__.py'), 'version = 2\nfrom . import sibling\na2 = 3')
    ub.writeto(join(mod2_fpath, 'sibling.py'), 'spam = \"jam\"\nb2 = 4')

    # -----
    # Neither module should be importable through the normal mechanism
    try:
        import testmod
        assert False, 'should fail'
    except ImportError as ex:
        pass

    mod1 = import_module_from_fpath(mod1_fpath)
    print('mod1.version = {!r}'.format(mod1.version))
    print('mod1.version = {!r}'.format(mod1.version))
    print(mod1.version == 1, 'mod1 version is 1')
    print('mod1.a1 = {!r}'.format(mod1.a1))

    mod2 = import_module_from_fpath(mod2_fpath)
    print('mod2.version = {!r}'.format(mod2.version))
    print(mod2.version == 2, 'mod2 version is 2')
    print('mod2.a2 = {!r}'.format(mod1.a2))

    # BUT Notice how mod1 is mod2
    print(mod1 is mod2)

    # mod1 has attributes from mod1 and mod2
    print('mod1.a1 = {!r}'.format(mod1.a1))
    print('mod1.a2 = {!r}'.format(mod1.a2))
    print('mod2.a1 = {!r}'.format(mod2.a1))
    print('mod2.a2 = {!r}'.format(mod2.a2))

    # Both are version 2
    print('mod1.version = {!r}'.format(mod1.version))
    print('mod2.version = {!r}'.format(mod2.version))

    # However sibling always remains at version1 (ham)
    print('mod2.sibling.spam = {!r}'.format(mod2.sibling.spam))

    # now importing testmod works because it reads from sys.modules
    import testmod

    # reloading mod1 overwrites attrs again
    mod1 = ut.import_module_from_fpath(mod1_fpath)

    # Removing both from sys.modules
    del sys.modules['testmod']
    del sys.modules['testmod.sibling']
    mod2 = ut.import_module_from_fpath(mod2_fpath)

    print(not hasattr(mod2, 'a1'),
        'mod2 no longer has a1 and it reloads itself correctly')

The above script illustrates what happens when trying to load two modules different modules with the same name: They overwrite / complement each other. However, if I do this import and then directly delete all imported modules it seems to do the "right" thing.

My questions are:

  1. Is it really doing the "right" thing? Or is there some case that my script doesn't test for.

  2. Is there a way to import a module locally without it being added to sys.modules (aka the global namespace)? (is sys.modules the only thing that makes up the global namespace for modules?)

(I only have tested the above code in Python 3, not sure if the old imp modules does the same thing as importlib).

0 Answers
Related