pythonpython-importpython-importlib

How do I carry over an import carried out in a 2nd file back to the calling file in python?


The code below is a utility I wrote that installs and restarts a script automatically without having the user need to parse through errors to install. Ideal usage would be below:

#file1.py
from file2 import import_and_install
succ = import_and_install("numpy") #import numpy
succ |= import_and_install("cv2", "opencv-python") #import cv2
succ |= import_and_install("openpyxl", alias="xl") #import pyxl

However I soon learned that the imports executed within file2 do no carry over back to file1 after it has been called resulting in me needing to use the below code instead

#alternate_file1.py
try:
    print("IMPORT LIBS")
    print("")
    import numpy
    import cv2
    import openpyxl as xl
except:
    succ = import_and_install("numpy") #import numpy
    succ |= import_and_install("cv2", "opencv-python") #import cv2
    succ |= import_and_install("openpyxl", alias="xl") #import numpy
    if( not succ == 4096):
        exit(1)

How do I get the dynamic imports to carry back over to the original file1? If I copy the contents of file2 over to file1 everything works as intended, but I have to copy code all the time which is annoying.

#file2.py
import os
import sys
import importlib
import subprocess
import traceback

def try_import(import_name, alt_install_name=None, from_pkg=None, alias=None):
    succ = 4096
    #allow different import strategies, and force install attempt if import fails
    try:
        if from_pkg is None:
            to_import = import_name
        elif not (from_pkg is None):
            to_import = from_pkg
        else:
            raise AttributeError("Not a valid package")
    
        import_entry = importlib.import_module(to_import)
        if not (from_pkg is None):
            import_entry = import_entry.__dict__[import_name]
        
        print(import_entry)
        print(to_import)
        if(not (alias is None)):
            globals()[alias] = import_entry
        else:
            globals()[import_name] = import_entry

    except AttributeError:
        print("Not a valid package")
        succ |= 1
    except ImportError:
        print("can't find the " + to_import + " module.")
        succ |= 2
    
    if not (alt_install_name is None):
        to_install = alt_install_name
    else:
        to_install = to_import
        
    return succ, to_install
    
def try_install(to_install):
    #if an importerror occured ask user to install missing package
    succ = 4096
    should_install = input("Would you like to install? (y/n): ")
    if should_install.lower() in ('y', 'yes'):
        try:
            ret = subprocess.check_call([sys.executable, "-m", "pip", 'install', to_install])
        except ChildProcessError:
            print("Failed to install package with return code " + str(ret) + ". Try again manually")
            succ |= 4
        else:
            print("Install attempted.")
            restart_script()

    else:
        print("You can't run the script until you install the package")
        succ |= 8
        
    return succ

def restart_script():
        print("Restarting script. If restart fails try again manually.")
        print('\a')
        if not (os.name == 'nt'):
            os.execl(sys.executable, 'python', traceback.extract_stack()[0].filename, *sys.argv[1:])
        else:
            p = subprocess.call([sys.executable, os.path.realpath(traceback.extract_stack()[0].filename), *sys.argv], shell=True, start_new_session=True)
            sys.exit(0)

def import_and_install(import_name, alt_name=None, from_pkg=None, alias=None):
    succ, to_install = try_import(import_name, alt_name, from_pkg, alias)
    
    if( succ == (4096 | 2) ):
        succ |= try_install(to_install)

    return succ

Solution

  • The primary issue with your implementation is that globals() is per module, and not for the program as a whole. From the documentation:

    globals() Return the dictionary implementing the current module namespace. For code within functions, this is set when the function is defined and remains the same regardless of where the function is called.

    So in the example given, there is a files1.globals() and files2.globals(). This block of code in try_import():

            if(not (alias is None)):
                globals()[alias] = import_entry
            else:
                globals()[import_name] = import_entry
    

    is only adding the imported module to the namespace of the file2 module, so file1 can't access it.

    What you need to do is return the module object from import_and_install() and assign it to a name within file1.py. The alias parameter doesn't really serve any purpose anymore. Instead, you can just use the desired alias for as the name you store the module in.

    Here I have made the following modifications:

    1. Removed the alias parameter entirely.
    2. Removed the block that set the module into globals() as it only affected file2.
    3. Updated try_import() to return import_entry
    4. Modified import_and_install() to get the module from try_import and return the module to the caller along with succ
    5. Updated file1.py to handle both return values, and split the bitwise OR operations onto separate lines.

    file1.py

    #file1.py
    from file2 import import_and_install
    import sys
    import cv2 as test
    succ, numpy = import_and_install("numpy") #import numpy
    succ_next, cv2 = import_and_install("cv2", "opencv-python") #import cv2
    succ |= succ_next
    succ_next, xl = import_and_install("openpyxl") #import pyxl
    succ |= succ_next
    
    
    print(sys.modules["numpy"])
    print(sys.modules["cv2"])
    print(sys.modules["openpyxl"])
    
    
    print(f"numpy {numpy=}  {numpy.__version__}")
    print(numpy.array([1, 2, 3, 4, 5, 6]))
    
    print(f"cv2 {cv2=}  {cv2.__version__}")
    
    print(f"openpyxl {xl=}  {xl.__version__}")
    

    file2.py

    #file2.py
    import os
    import sys
    import importlib
    import subprocess
    import traceback
    
    def try_import(import_name, alt_install_name=None, from_pkg=None):
        succ = 4096
        #allow different import strategies, and force install attempt if import fails
        try:
            if from_pkg is None:
                to_import = import_name
            elif not (from_pkg is None):
                to_import = from_pkg
            else:
                raise AttributeError("Not a valid package")
        
            import_entry = importlib.import_module(to_import)
            if not (from_pkg is None):
                import_entry = import_entry.__dict__[import_name]
            
            print(import_entry)
            print(to_import)
    
    
        except AttributeError:
            print("Not a valid package")
            succ |= 1
        except ImportError:
            print("can't find the " + to_import + " module.")
            succ |= 2
        
        if not (alt_install_name is None):
            to_install = alt_install_name
        else:
            to_install = to_import
            
        return succ, to_install, import_entry
        
    def try_install(to_install):
        #if an importerror occured ask user to install missing package
        succ = 4096
        should_install = input("Would you like to install? (y/n): ")
        if should_install.lower() in ('y', 'yes'):
            try:
                ret = subprocess.check_call([sys.executable, "-m", "pip", 'install', to_install])
            except ChildProcessError:
                print("Failed to install package with return code " + str(ret) + ". Try again manually")
                succ |= 4
            else:
                print("Install attempted.")
                restart_script()
    
        else:
            print("You can't run the script until you install the package")
            succ |= 8
            
        return succ
    
    def restart_script():
            print("Restarting script. If restart fails try again manually.")
            print('\a')
            if not (os.name == 'nt'):
                os.execl(sys.executable, 'python', traceback.extract_stack()[0].filename, *sys.argv[1:])
            else:
                p = subprocess.call([sys.executable, os.path.realpath(traceback.extract_stack()[0].filename), *sys.argv], shell=True, start_new_session=True)
                sys.exit(0)
    
    def import_and_install(import_name, alt_name=None, from_pkg=None):
        succ, to_install, module = try_import(import_name, alt_name, from_pkg)
        
        if( succ == (4096 | 2) ):
            succ |= try_install(to_install)
    
        return succ, module
    

    output

    $ python file1.py
    <module 'numpy' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\numpy\\__init__.py'>
    numpy
    <module 'cv2' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\cv2\\__init__.py'>
    cv2
    <module 'openpyxl' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\openpyxl\\__init__.py'>
    openpyxl
    <module 'numpy' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\numpy\\__init__.py'>
    <module 'cv2' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\cv2\\__init__.py'>
    <module 'openpyxl' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\openpyxl\\__init__.py'>
    numpy numpy=<module 'numpy' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\numpy\\__init__.py'>  1.26.4
    [1 2 3 4 5 6]
    cv2 cv2=<module 'cv2' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\cv2\\__init__.py'>  4.10.0
    openpyxl xl=<module 'openpyxl' from 'C:\\Users\\markm\\PythonVenv\\ImportPlayground\\.venv\\Lib\\site-packages\\openpyxl\\__init__.py'>  3.1.3