Skip to content

Commit eddd78c

Browse files
Change the way dependencies are installed
1 parent 2496ec2 commit eddd78c

2 files changed

Lines changed: 117 additions & 103 deletions

File tree

__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
)
88
from .install_deps import (
99
dependencies,
10+
required_dependencies,
1011
SFR_OT_CheckDependencies,
1112
SFR_OT_InstallDependencies,
1213
SFR_OT_OpenAddonPrefs,
@@ -26,7 +27,7 @@
2627
bl_info = {
2728
"name": "Super Fast Render (SFR)",
2829
"author": "Kevin Lorengel, Chris Bond (Kamikaze)",
29-
"version": (3, 0, 0),
30+
"version": (3, 0, 1),
3031
"blender": (2, 92, 0),
3132
"location": "Properties > Render > Super Fast Render",
3233
"description": "SFR optimizes your scene, so you render faster!",
@@ -145,6 +146,8 @@ def register():
145146
# so that users can revert back to a working version
146147
addon_updater_ops.register(bl_info)
147148

149+
dependencies.check_dependencies()
150+
148151
# register the example panel, to show updater buttons
149152
for cls in classes:
150153
addon_updater_ops.make_annotations(

install_deps.py

Lines changed: 113 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,101 @@
11
import bpy
2-
import sys
2+
import importlib
3+
import os
34
import subprocess
5+
import sys
46
from 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

792
class 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

151159
dependencies = 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\nCheck the System Console for details.")
183194

184195
if dependencies.error:
185196
return {'CANCELLED'}

0 commit comments

Comments
 (0)