11import bpy
2- import sys
2+ import importlib
3+ import os
34import subprocess
5+ import sys
46from bpy .types import Operator
7+ from collections import namedtuple
8+
9+
10+ # functions inspired by https://github.com/robertguetzkow/blender-python-examples
11+ Dependency = namedtuple ("Dependency" , ["module" , "package" , "name" , "skip_import" ])
12+
13+ required_dependencies = (
14+ Dependency (module = "cv2" , package = "opencv-python" , name = "cv2" , skip_import = False ),
15+ Dependency (module = None , package = "opencv-contrib-python" , name = None , skip_import = True ),
16+ Dependency (module = "numpy" , package = "numpy" , name = "numpy" , skip_import = False ),
17+ Dependency (module = "skimage" , package = "scikit-image" , name = "skimage" , skip_import = False ),
18+ )
19+
20+
21+ def import_module (module_name , global_name = None ):
22+ """
23+ Import a module.
24+ :param module_name: Module to import.
25+ :param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
26+ This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
27+ the global_name under which the module can be accessed.
28+ :raises: ImportError and ModuleNotFoundError
29+ """
30+
31+ if global_name is None :
32+ global_name = module_name
33+
34+ if global_name in globals ():
35+ importlib .reload (globals ()[global_name ])
36+ else :
37+ # Attempt to import the module and assign it to globals dictionary. This allow to access the module under
38+ # the given name, just like the regular import would.
39+ globals ()[global_name ] = importlib .import_module (module_name )
40+
41+
42+ def install_pip ():
43+ """
44+ Installs pip if not already present. Please note that ensurepip.bootstrap() also calls pip, which adds the
45+ environment variable PIP_REQ_TRACKER. After ensurepip.bootstrap() finishes execution, the directory doesn't exist
46+ anymore. However, when subprocess is used to call pip, in order to install a package, the environment variables
47+ still contain PIP_REQ_TRACKER with the now nonexistent path. This is a problem since pip checks if PIP_REQ_TRACKER
48+ is set and if it is, attempts to use it as temp directory. This would result in an error because the
49+ directory can't be found. Therefore, PIP_REQ_TRACKER needs to be removed from environment variables.
50+ :return:
51+ """
52+
53+ try :
54+ # Check if pip is already installed
55+ subprocess .run ([sys .executable , "-m" , "pip" , "--version" ], check = True )
56+ except subprocess .CalledProcessError :
57+ import ensurepip
58+
59+ ensurepip .bootstrap ()
60+ os .environ .pop ("PIP_REQ_TRACKER" , None )
61+
62+
63+ def install_module (module_name , package_name = None ):
64+ """
65+ Installs the package through pip.
66+ :param module_name: Module to install.
67+ :param package_name: (Optional) Name of the package that needs to be installed. If None it is assumed to be equal
68+ to the module_name.
69+ :param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
70+ This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
71+ the global_name under which the module can be accessed.
72+ :raises: subprocess.CalledProcessError and ImportError
73+ """
74+
75+ if package_name is None :
76+ package_name = module_name
77+
78+ # Blender disables the loading of user site-packages by default. However, pip will still check them to determine
79+ # if a dependency is already installed. This can cause problems if the packages is installed in the user
80+ # site-packages and pip deems the requirement satisfied, but Blender cannot import the package from the user
81+ # site-packages. Hence, the environment variable PYTHONNOUSERSITE is set to disallow pip from checking the user
82+ # site-packages. If the package is not already installed for Blender's Python interpreter, it will then try to.
83+ # The paths used by pip can be checked with `subprocess.run([sys.executable, "-m", "site"], check=True)`
84+
85+ # Create a copy of the environment variables and modify them for the subprocess call
86+ environ_copy = dict (os .environ )
87+ environ_copy ["PYTHONNOUSERSITE" ] = "1"
88+
89+ subprocess .run ([sys .executable , "-m" , "pip" , "install" , package_name ], check = True , env = environ_copy )
590
691
792class Dependencies_check_singleton (object ):
893 def __init__ (self ):
994 self ._checked = False
95+ self ._needs_install = False
1096 self ._error = False
1197 self ._success = False
1298
13- self ._needs_cv2 = False
14- self ._needs_numpy = False
15- self ._needs_skimage = False
16-
1799 # Properties
18100
19101 @property
@@ -30,58 +112,23 @@ def success(self):
30112
31113 @property
32114 def needs_install (self ):
33- return True in [
34- self .needs_cv2 ,
35- self .needs_numpy ,
36- self .needs_skimage ,
37- ]
38-
39- @property
40- def needs_cv2 (self ):
41- return self ._needs_cv2
42-
43- @property
44- def needs_numpy (self ):
45- return self ._needs_numpy
46-
47- @property
48- def needs_skimage (self ):
49- return self ._needs_skimage
115+ return self ._needs_install
50116
51117 # Methods
52118
53119 def check_dependencies (self ):
54120 self ._checked = False
55- self ._error = False
56- self ._success = False
57-
58- self ._needs_cv2 = False
59- self ._needs_numpy = False
60- self ._needs_skimage = False
61121
62122 try :
63- print ("Checking for cv2..." )
64- import cv2
65- print ("cv2 found." )
66- except ImportError :
67- print ("cv2 NOT found." )
68- self ._needs_cv2 = True
69-
70- try :
71- print ("Checking for numpy..." )
72- import numpy
73- print ("numpy found." )
74- except ImportError :
75- print ("numpy NOT found." )
76- self ._needs_numpy = True
77-
78- try :
79- print ("Checking for scikit-image..." )
80- from skimage import io
81- print ("scikit-image found." )
82- except ImportError :
83- print ("scikit-image NOT found." )
84- self ._needs_skimage = True
123+ for dependency in required_dependencies :
124+ if dependency .skip_import : continue
125+ print (f"Checking for { dependency .module } ..." )
126+ import_module (dependency .module , dependency .name )
127+ print (f"Found { dependency .module } ." )
128+ self ._needs_install = False
129+ except ModuleNotFoundError :
130+ print ("One or more dependencies need to be installed." )
131+ self ._needs_install = True
85132
86133 self ._checked = True
87134
@@ -90,63 +137,24 @@ def install_dependencies(self):
90137 self ._success = False
91138
92139 # Update pip
93- path_to_python = sys .executable if bpy .app .version > (
94- 2 , 90 ) else bpy .app .binary_path_python
95- try :
96- print ("Updating pip..." )
97- subprocess .run ([path_to_python , "-m" , "ensurepip" ], check = True )
98- subprocess .run ([path_to_python , "-m" , "pip" ,
99- "install" , "--upgrade" , "pip" ], check = True )
100- print ("Successfully updated pip." )
101- except subprocess .CalledProcessError as e :
102- self ._error = True
103- print ("Error updating pip!" , e .__str__ ())
104- # raise
105- return
106-
107- # Install cv2
108- if self .needs_cv2 :
109- try :
110- print ("Installing cv2..." )
111- subprocess .run ([path_to_python , "-m" , "pip" , "install" ,
112- "--upgrade" , "opencv-python" ], check = True )
113- subprocess .run ([path_to_python , "-m" , "pip" , "install" ,
114- "--upgrade" , "opencv-contrib-python" ], check = True )
115- import cv2
116- self ._needs_cv2 = False
117- except (subprocess .CalledProcessError , ImportError ) as e :
118- self ._error = True
119- print ("Error installing cv2!" , e .__str__ ())
120- return
121-
122- # Install numpy
123- if self .needs_numpy :
124- try :
125- print ("Installing numpy..." )
126- subprocess .run ([path_to_python , "-m" , "pip" ,
127- "install" , "--upgrade" , "numpy" ], check = True )
128- import numpy
129- self ._needs_numpy = False
130- except (subprocess .CalledProcessError , ImportError ) as e :
131- self ._error = True
132- print ("Error installing numpy!" , e .__str__ ())
133- return
140+ print ("Ensuring pip is installed..." )
141+ install_pip ()
134142
135- # Install scikit-image
136- if self .needs_skimage :
143+ for dependency in required_dependencies :
144+ package_name = dependency .package if dependency .package is not None else dependency .module
145+ print (f"Installing { package_name } ..." )
137146 try :
138- print ("Installing scikit-image..." )
139- subprocess .run ([path_to_python , "-m" , "pip" , "install" ,
140- "--upgrade" , "scikit-image" ], check = True )
141- from skimage import io
142- self ._needs_skimage = False
143- except (subprocess .CalledProcessError , ImportError ) as e :
147+ install_module (module_name = dependency .module , package_name = dependency .package )
148+ except (subprocess .CalledProcessError , ImportError ) as err :
144149 self ._error = True
145- print ("Error installing scikit-image!" , e .__str__ ())
146- return
150+ print (f"Error installing { package_name } !" )
151+ print (str (err ))
152+ raise ValueError (package_name )
147153
148154 self ._success = True
149155
156+ self .check_dependencies ()
157+
150158
151159dependencies = Dependencies_check_singleton ()
152160
@@ -179,7 +187,10 @@ def poll(cls, context):
179187 return dependencies .needs_install
180188
181189 def execute (self , context ):
182- dependencies .install_dependencies ()
190+ try :
191+ dependencies .install_dependencies ()
192+ except ValueError as ve :
193+ self .report ({"ERROR" }, f"Error installing package { ve .args [0 ]} .\n \n Check the System Console for details." )
183194
184195 if dependencies .error :
185196 return {'CANCELLED' }
0 commit comments