New section: Add-on Dependencies #40
No reviewers
Labels
No Label
No Milestone
No Assignees
3 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: blender/blender-developer-docs#40
Loading…
Reference in New Issue
No description provided.
Delete Branch "dr.sybren/blender-developer-docs:pr/addon-dependencies"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A new section on how to load add-on dependencies, in a way that doesn't interfere with other add-ons. It's basically explaining what I did for Flamenco.
Thanks for this. As we discussed IRL it is better to have this as part of the Python API docs, not the developer docs.
Once we have this merged I will update the user manual to point to that.
Her are the result of some tests, I first wanted to get an idea how well this method works when add-on developers try this with various wheels.
The results of the experiment are in this repository: https://gitlab.com/ideasman42/temp-blender-wheel-test
Findings
greenlet
couldn't load it's own sub-module for some reason.xmlschema
attempted to stat a file on the file-system.Details
I picked a handful of modules from pypi (picked from lists of popular Python modules, excluding some that pull in many other dependencies).
From 10 modules:
https://pypi.org/project/appdirs
https://pypi.org/project/lxml
https://pypi.org/project/pillow
https://pypi.org/project/python-dateutil
https://pypi.org/project/scrat
https://pypi.org/project/shortuuid
https://pypi.org/project/simple-dotdict
https://pypi.org/project/tablib
https://pypi.org/project/tomlkit
https://pypi.org/project/xmlschema
Downloaded with
pip wheel -r requirements.txt -w ./my_addon/wheels
, pulled in an additional 7 dependencies.From 17 wheels, 14 loaded, although 15 would have loaded with support for multiple wheels in the
sys.path
at once.Output from test script:
@ -0,0 +88,4 @@
_log = logging.getLogger(__name__)
def load_wheel(module_name: str, submodules: Iterable[str]) -> list[ModuleType]:
The
module_name
and the wheel name don't always match, when testing wheels I found a few which didn't use the same name [PIL
/pillow
,sqlalchemy
/SQLAlchemy
,yaml
/PyYAML
] for e.g.fname_prefix
can be made an optional argument to support cases when the names don't match.@ -0,0 +185,4 @@
old_sysmod = sys.modules.copy()
try:
sys.path.insert(0, str(wheel_file))
When testing wheels I ran into cases where one wheel depends on another, if the dependencies are added here, loading those wheels worked.
This API could be extended to support dependencies indirect.
We could also lift the context manager to the top level of the intended use, so that you get something like:
That way everything within the context can 'see' each other in
sys.modules
, which should help in resolving such dependencies.Of course, how this is shaped exactly heavily depends on other factors, like the other changes you describe in your review.
@ -0,0 +192,4 @@
# held by other code will stay valid.
sys.path[:] = old_syspath
sys.modules.clear()
sys.modules.update(old_sysmod)
While in most cases this shouldn't cause issues there is a potential problem caused from having multiple versions of the same module.
When a wheel loads a built-in Python module that isn't part of the wheel which will be cleared from
sys.modules
but accessible from the wheels module.The same module may be loaded again later on, any singletons will then exist twice in memory, so if the module defines an
atexit
function, that would run multiple times when exiting for e.g.Instead of fully restoring modules, only modules which are part of this wheel file could be removed.
Checking if the
__file__
starts withwheel_file
seems to work.@ -0,0 +196,4 @@
def _wheel_filename(fname_prefix: str) -> Path:
path_pattern = "%s*.whl" % fname_prefix
The naming convention uses dash separators: https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format
So it looks like
"%s*.whl"
can be"%s-*.whl"
to avoid an error if one wheel is the prefix of another.@ -0,0 +201,4 @@
if not wheels:
raise RuntimeError("Unable to find wheel at %r" % path_pattern)
# If there are multiple wheels that match, load the last-modified one.
In the context of distributing extensions I don't see why there would ever be more than one wheel *. That could even be made into an error AFAICS.
If there is some reason to support this, the version number from each could be extracted and compared using
setuptools.wheel.parse_version
.* except in the case of different architectures although in that case we would still want only one wheel per architecture.
With regular/traditional add-ons, this was necessary to allow upgrading an add-on by simply installing the newer version. Since that didn't first delete the old add-on, you could end up with wheel files from both the old and the new version.
A better approach would be to include the exact version number in the loading function, so that the correct wheel file can be loaded explicitly. That way it's also possible to downgrade a dependency, for example. That's also the reason why I used timestamps: the last-installed file is the most important, and not necessarily the one with the highest version number.
Firstly, having looked into this I think it may be worth prioritizing wheel support that doesn't depend on temporary
sys.path
hacks - since developers will loose time whenever they run into a case which doesn't work, and generally make the experience of trying to get their add-on ported to an extension worse.When developers run into a problem the answer from Blender developers is likely something along the lines of:
"We don't know why this is broken and we're not going to investigate why individual wheels don't work with this method."
While some additional cases can be supported, wheel's that depend on other wheels for e.g. We could even extract the wheel to disk so files on the filesystem can be read, or support binary wheels by loading wheels with the appropriate architecture.
Even if we do all this there will still be enough cases that fail, time is probably better spent on having a way for extensions to install wheels to a shared location in Blender's Python installation.
Thanks for reviewing this so thoroughly.
I agree.
That's an interesting approach.
That sounds pretty good to me. I'm not sure if I have the time to be the one to build this, though. Do you think this could be a target for the Extensions project?
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Gitea.