In the development of add-ons, their modules should be abstracted as much as possible. For a very simple reason – functional, created for a current add-on, will likely need in the next add-on, and possibly not even in one. In the add-on release, the problem of accessing to such common functionality modules is easily solved – all the necessary modules are included in a single package and distributed together. However, during the add-ons development, such modules are much easier to store separately, in one instance, without associating them with any particular package, and import if necessary the desired modules to the desired add-on.
In accordance with the packages import rules, Python allows to refer modules in the following ways:
- Direct import, if the module is located inside the current package.
To have the module inside the package, it is necessary to copy it to the package directory. This is the easiest and, at the same time, most suboptimal way with maximum code duplication. If an error is detected in one module copy, the correction should be made in all. If one module copy is improved – added a new class or method, changes affects only the current copy, and to introduce changes to all the other module copies needs much effort.
- If the module is located outside the package, it can be still imported with adding the path to the module directory to the environment variable sys.path.
Debug script for multifile add-ons uses this way. This way is free of the first way flaws, but it also has some downsides. The module is stored in a separate location as a single copy, and all changes and amendments are automatically available in all add-ons, uses this module. There are no problems with code duplication. However, in this case, when you create an add-on release, you need to search through all the imported modules and copy all of them at the redistributable package.
- Аccessing the desired module by creation a symbolic link to it inside the package.
The most convenient way, free of the above drawbacks. Python works correctly with symbolic links on Windows and Linux. No code duplication, no real copies – all changes and corrections are made directly in the source module through a symbolic link. At the same time, the archiver correctly takes not the symbolic link but the real copy of the module, zipping the package to create a distributed release.
As an example let’s take a simple generic class working with a two-dimensional vector:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import math class Vector2d(): def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return "Vector2d({x},{y})".format(x = self.x, y = self.y) def add(self, vec2): if isinstance(vec2, Vector2d): return Vector2d(self.x + vec2.x, self.y + vec2.y) def subtract(self, vec2): if isinstance(vec2, Vector2d): return Vector2d(self.x - vec2.x, self.y - vec2.y) def length(self): return math.sqrt(self.x ** 2 + self.y ** 2) |
It can be used in tons of add-ons, so place it in a separate module Vector2d.py in the directory d:\Python\Vector2d\.
Let’s create a separate add-on SampleProject, with the sole task to demonstrate the Vector2d module import with the symbolic link use.
- Create the directory d:\Python\SampleProject\
- Create the __init__.py file in it
To connect the previously created Vectro2d module to add-on package, let’s create a symbolic link to this module in SampleProject directory.
On Windows, symbolic links are created with mklink command (for Linux the command ln should be used) or via file manager, works with symbolic links (like the Far). Attention – on Windows symbolic links are supported since Vista. The users of older systems need to use 1 or 2 methods of working with modules.
Create a command file link.cmd with the following code:
1 |
mklink "d:\Python\SampleProject\Vector2d.py" "d:\Python\Vector2d\Vector2d.py" |
After file execution in the directory d:\Python\SampleProject symbolic link Vector2d.py is created. This link refers to the module d:\Python\Vector2d\Vector2d.py.
To test this, let’s modify the __init__.py file. Put in it the universal code for multifile add-ons with a few changes. At first, append Vectror2d module name to the modulesNames list.
1 |
modulesNames = ['Vector2d'] |
Add to the function “register” a few additional lines of code. This function is called during the add-on activation, and the added code will be executed too.
1 2 3 4 5 6 |
v1 = Vector2d.Vector2d(1, 0) v2 = Vector2d.Vector2d(2, 0) print('--------------------------') print(v1.add(v2)) print('--------------------------') |
Remind you that this is a bad practice, there should be no additional code in __init__.py in the real add-on. This code is added only to demonstrate that with using symbolic links “it works”. This code creates two vectors v1 and v2 and outputs the result of the simple operation (addition) with these two vectors.
The final __init__.py code:
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 |
bl_info = { 'name': 'SampleProject', 'category': 'All', 'version': (0, 0, 1), 'blender': (2, 78, 0) } modulesNames = ['Vector2d'] import sys import importlib modulesFullNames = [] for currentModuleName in modulesNames: if 'DEBUG_MODE' in sys.argv: modulesFullNames.append('{}'.format(currentModuleName)) else: modulesFullNames.append('{}.{}'.format(__name__, currentModuleName)) for currentModuleName in modulesFullNames: if currentModuleName in sys.modules: importlib.reload(sys.modules[currentModuleName]) else: globals()[currentModuleName] = importlib.import_module(currentModuleName) def register(): for currentModuleName in modulesFullNames: if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'register'): sys.modules[currentModuleName].register() v1 = Vector2d.Vector2d(1, 0) v2 = Vector2d.Vector2d(2, 0) print('--------------------------') print(v1.add(v2)) print('--------------------------') def unregister(): for currentModuleName in modulesFullNames: if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'unregister'): sys.modules[currentModuleName].unregister() if __name__ == "__main__": register() |
To test it, start Blender and in debug script specify the add-on directory:
1 |
filesDir = "d:\Python\SampleProject" |
and execute it by pressing the Run Script button. Open the System Console window to see the result of vectors adding.
After testing module importing through symbolic links in debug mode, let’s try to build a release package.
Pack the directory d:\Python\SampleProject\ with a zip archiver. Then if you open resulted *.zip file, you can notice that Vector2d.py file packaged not as a symbolic link (size 0 bytes) but as a real file (size 568 bytes).
Install add-on from the created *.zip archive and activate it.
After the add-on activation, the System Console window displays the result of vectors addition, which means that the add-on is installed and works correctly.