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
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:
alias
parameter entirely.try_import()
to return import_entry
import_and_install()
to get the module from try_import and return the module to the caller along with succfile1.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