This repository has been archived by the owner on May 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathinstall_dependencies.py
265 lines (207 loc) · 10.5 KB
/
install_dependencies.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# Copyright (C) 2020 Robert Guetzkow
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>
bl_info = {
"name": "Install Dependencies Example",
"author": "Robert Guetzkow",
"version": (1, 0, 4),
"blender": (2, 91, 0),
"location": "View3D > Sidebar > Example Tab",
"description": "Example add-on that installs a Python package",
"warning": "Requires installation of dependencies",
"wiki_url": "https://github.com/robertguetzkow/blender-python-examples/add-ons/install-dependencies",
"tracker_url": "https://github.com/robertguetzkow/blender-python-examples/issues",
"support": "COMMUNITY",
"category": "3D View"}
import bpy
import os
import sys
import subprocess
import importlib
from collections import namedtuple
Dependency = namedtuple("Dependency", ["module", "package", "name"])
# Declare all modules that this add-on depends on, that may need to be installed. The package and (global) name can be
# set to None, if they are equal to the module name. See import_module and ensure_and_import_module for the explanation
# of the arguments. DO NOT use this to import other parts of your Python add-on, import them as usual with an
# "import" statement.
dependencies = (Dependency(module="matplotlib", package=None, name=None),)
dependencies_installed = False
def import_module(module_name, global_name=None, reload=True):
"""
Import a module.
:param module_name: Module to import.
:param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
the global_name under which the module can be accessed.
:raises: ImportError and ModuleNotFoundError
"""
if global_name is None:
global_name = module_name
if global_name in globals():
importlib.reload(globals()[global_name])
else:
# Attempt to import the module and assign it to globals dictionary. This allow to access the module under
# the given name, just like the regular import would.
globals()[global_name] = importlib.import_module(module_name)
def install_pip():
"""
Installs pip if not already present. Please note that ensurepip.bootstrap() also calls pip, which adds the
environment variable PIP_REQ_TRACKER. After ensurepip.bootstrap() finishes execution, the directory doesn't exist
anymore. However, when subprocess is used to call pip, in order to install a package, the environment variables
still contain PIP_REQ_TRACKER with the now nonexistent path. This is a problem since pip checks if PIP_REQ_TRACKER
is set and if it is, attempts to use it as temp directory. This would result in an error because the
directory can't be found. Therefore, PIP_REQ_TRACKER needs to be removed from environment variables.
:return:
"""
try:
# Check if pip is already installed
subprocess.run([sys.executable, "-m", "pip", "--version"], check=True)
except subprocess.CalledProcessError:
import ensurepip
ensurepip.bootstrap()
os.environ.pop("PIP_REQ_TRACKER", None)
def install_and_import_module(module_name, package_name=None, global_name=None):
"""
Installs the package through pip and attempts to import the installed module.
:param module_name: Module to import.
:param package_name: (Optional) Name of the package that needs to be installed. If None it is assumed to be equal
to the module_name.
:param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
the global_name under which the module can be accessed.
:raises: subprocess.CalledProcessError and ImportError
"""
if package_name is None:
package_name = module_name
if global_name is None:
global_name = module_name
# Blender disables the loading of user site-packages by default. However, pip will still check them to determine
# if a dependency is already installed. This can cause problems if the packages is installed in the user
# site-packages and pip deems the requirement satisfied, but Blender cannot import the package from the user
# site-packages. Hence, the environment variable PYTHONNOUSERSITE is set to disallow pip from checking the user
# site-packages. If the package is not already installed for Blender's Python interpreter, it will then try to.
# The paths used by pip can be checked with `subprocess.run([bpy.app.binary_path_python, "-m", "site"], check=True)`
# Create a copy of the environment variables and modify them for the subprocess call
environ_copy = dict(os.environ)
environ_copy["PYTHONNOUSERSITE"] = "1"
subprocess.run([sys.executable, "-m", "pip", "install", package_name], check=True, env=environ_copy)
# The installation succeeded, attempt to import the module again
import_module(module_name, global_name)
class EXAMPLE_OT_dummy_operator(bpy.types.Operator):
bl_idname = "example.dummy_operator"
bl_label = "Dummy Operator"
bl_description = "This operator tries to use matplotlib."
bl_options = {"REGISTER"}
def execute(self, context):
print(matplotlib.get_backend())
return {"FINISHED"}
class EXAMPLE_PT_panel(bpy.types.Panel):
bl_label = "Example Panel"
bl_category = "Example Tab"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def draw(self, context):
layout = self.layout
for dependency in dependencies:
if dependency.name is None and hasattr(globals()[dependency.module], "__version__"):
layout.label(text=f"{dependency.module} {globals()[dependency.module].__version__}")
elif hasattr(globals()[dependency.name], "__version__"):
layout.label(text=f"{dependency.module} {globals()[dependency.name].__version__}")
else:
layout.label(text=f"{dependency.module}")
layout.operator(EXAMPLE_OT_dummy_operator.bl_idname)
classes = (EXAMPLE_OT_dummy_operator,
EXAMPLE_PT_panel)
class EXAMPLE_PT_warning_panel(bpy.types.Panel):
bl_label = "Example Warning"
bl_category = "Example Tab"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@classmethod
def poll(self, context):
return not dependencies_installed
def draw(self, context):
layout = self.layout
lines = [f"Please install the missing dependencies for the \"{bl_info.get('name')}\" add-on.",
f"1. Open the preferences (Edit > Preferences > Add-ons).",
f"2. Search for the \"{bl_info.get('name')}\" add-on.",
f"3. Open the details section of the add-on.",
f"4. Click on the \"{EXAMPLE_OT_install_dependencies.bl_label}\" button.",
f" This will download and install the missing Python packages, if Blender has the required",
f" permissions.",
f"If you're attempting to run the add-on from the text editor, you won't see the options described",
f"above. Please install the add-on properly through the preferences.",
f"1. Open the add-on preferences (Edit > Preferences > Add-ons).",
f"2. Press the \"Install\" button.",
f"3. Search for the add-on file.",
f"4. Confirm the selection by pressing the \"Install Add-on\" button in the file browser."]
for line in lines:
layout.label(text=line)
class EXAMPLE_OT_install_dependencies(bpy.types.Operator):
bl_idname = "example.install_dependencies"
bl_label = "Install dependencies"
bl_description = ("Downloads and installs the required python packages for this add-on. "
"Internet connection is required. Blender may have to be started with "
"elevated permissions in order to install the package")
bl_options = {"REGISTER", "INTERNAL"}
@classmethod
def poll(self, context):
# Deactivate when dependencies have been installed
return not dependencies_installed
def execute(self, context):
try:
install_pip()
for dependency in dependencies:
install_and_import_module(module_name=dependency.module,
package_name=dependency.package,
global_name=dependency.name)
except (subprocess.CalledProcessError, ImportError) as err:
self.report({"ERROR"}, str(err))
return {"CANCELLED"}
global dependencies_installed
dependencies_installed = True
# Register the panels, operators, etc. since dependencies are installed
for cls in classes:
bpy.utils.register_class(cls)
return {"FINISHED"}
class EXAMPLE_preferences(bpy.types.AddonPreferences):
bl_idname = __name__
def draw(self, context):
layout = self.layout
layout.operator(EXAMPLE_OT_install_dependencies.bl_idname, icon="CONSOLE")
preference_classes = (EXAMPLE_PT_warning_panel,
EXAMPLE_OT_install_dependencies,
EXAMPLE_preferences)
def register():
global dependencies_installed
dependencies_installed = False
for cls in preference_classes:
bpy.utils.register_class(cls)
try:
for dependency in dependencies:
import_module(module_name=dependency.module, global_name=dependency.name)
dependencies_installed = True
except ModuleNotFoundError:
# Don't register other panels, operators etc.
return
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in preference_classes:
bpy.utils.unregister_class(cls)
if dependencies_installed:
for cls in classes:
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()