Skip to content

Blender 5.1 compatibility fixes #193

Description

@kelitraynaud

Hi! I ran 2.6.7 on Blender 5.1.1 (Python 3.13) and hit three more issues. I've patched all of them locally with Claude, let me know if this can help you to push a new version.

TL;DR

# | Severity | File | Symptom on Blender 5.1.1 -- | -- | -- | -- 1 | Blocker | core/utilities.py, resources/extensions/instance_assets.py | AttributeError: 'Action' object has no attribute 'fcurves' 2 | Blocker | dependencies/rpc/factory.py | SyntaxError: expected 'else' after 'if' expression when calling any remote Unreal method 3 | Cosmetic | dependencies/rpc/factory.py | SyntaxWarning: invalid escape sequence '\.'

Issues 1 and 2 each prevent any export. Issue 3 is just a warning under Python 3.12+.


1. action.fcurves removed in Blender 5.0

Blender 5.0 finalised the slotted-action migration started in 4.4 and removed the legacy proxy properties action.fcurves, action.groups, and action.id_root. F-Curves now live on the channelbag of each action slot: https://developer.blender.org/docs/release_notes/5.0/python_api/ https://developer.blender.org/docs/release_notes/4.4/upgrading/slotted_actions/

Send2UE accesses action.fcurves directly in six places:

  • core/utilities.pyget_custom_property_fcurve_data, remove_object_scale_keyframes, round_keyframes, scale_object_actions
  • resources/extensions/instance_assets.pypost_import (anim sequence branch)

Fix

I added two small back-compat helpers at the top of core/utilities.py and routed every existing call through them:

python
def _iter_action_fcurves(action):
    """Yield all F-Curves of an Action; transparent on 4.x and 5.x."""
    if action is None:
        return
    if bpy.app.version[0] >= 5:
        try:
            for layer in action.layers:
                for strip in layer.strips:
                    for channelbag in strip.channelbags:
                        for fcurve in channelbag.fcurves:
                            yield fcurve
            return
        except AttributeError:
            pass
    for fcurve in action.fcurves:
        yield fcurve


def _remove_action_fcurve(action, fcurve):
    """Remove an F-Curve from an Action; transparent on 4.x and 5.x."""
    if bpy.app.version[0] >= 5:
        try:
            for layer in action.layers:
                for strip in layer.strips:
                    for channelbag in strip.channelbags:
                        if fcurve in list(channelbag.fcurves):
                            channelbag.fcurves.remove(fcurve)
                            return
            return
        except AttributeError:
            pass
    action.fcurves.remove(fcurve)

For instance, remove_object_scale_keyframes becomes:

python
for action in actions:
    scale_fcurves = [fc for fc in _iter_action_fcurves(action) if fc.data_path == 'scale']
    for fcurve in scale_fcurves:
        _remove_action_fcurve(action, fcurve)

instance_assets.py imports _iter_action_fcurves from send2ue.core.utilities and uses it in the for fcurve in action.fcurves loop inside post_import.


2. _get_docstring / _get_code in dependencies/rpc/factory.py

When the user clicks Send to Unreal, validation calls a remote method (e.g. UnrealRemoteCalls.is_using_legacy_fbx_importer()). On Blender 5.1.1 the call fails twice in a row:

2a) Client-side exec() in _get_docstring

python
exec('\n'.join(code))
function_instance = locals().copy().get(function_name)
return function_instance.__doc__

This re-execs the function source just to read __doc__. Python 3.13's parser raises SyntaxError: expected 'else' after 'if' expression on several UnrealRemoteCalls method sources (the methods themselves are fine — it's the round-trip through inspect.getsourcetextwrap.dedent → split-and-filter → join that produces something the new parser doesn't like in edge cases involving comprehensions and conditional expressions).

The exec is unnecessary: at the only call site we already hold the function object. I changed it to:

python
@staticmethod
def _get_docstring(code, function_name, function=None):
    if function is not None and getattr(function, "__doc__", None) is not None:
        return function.__doc__
    # Fallback for the rare case where the function object isn't supplied:
    # extract the first triple-quoted block from the source without exec.
    import re
    source = '\n'.join(code)
    m = re.search(r'(?P<q>\"{3}|\'{3})(?P<body>.*?)(?P=q)', source, re.DOTALL)
    return m.group('body') if m else None

…and the single caller forwards the function:

python
doc_string = self._get_docstring(code, function.__name__, function=function)

2b) Server-side exec() chokes on the stripped docstring

After 2a was fixed, Send to Unreal then crashed on the server side:

File "send2ue/dependencies/rpc/base_server.py", line 208, in add_new_callable
    exec(code)
File "<string>", line 3
    Checks to see if a directory exist in unreal.
              ^^^^^^^^
SyntaxError: expected 'else' after 'if' expression

The culprit is the docstring-stripping logic in _get_code:

python
if doc_string:
    code = '\n'.join(code).replace(doc_string, '')
    code = [line for line in code.split('\n')
            if not all([char == '"' or char == "'" for char in line.strip()])]

function.__doc__ preserves the source's leading indentation, but inspect.getsource is fed through textwrap.dedent first, so the indentation no longer matches. str.replace silently fails to remove anything. Then the second filter strips lines composed entirely of triple-quotes — which means the opening and closing """ get removed but the body stays. The body becomes naked text in the function (Checks to see if a directory exist in unreal.) and Python 3.13 sees the if and demands an else.

The simplest robust fix is to stop trying to strip docstrings. They're valid Python and add only a handful of bytes per RPC call. I replaced the block with:

python
# Docstrings are valid Python; leaving them in is harmless and avoids a
# fragile indentation-sensitive string replace.
_ = doc_string
return code

After this, Send to Unreal completes end-to-end on Blender 5.1.1.


3. SyntaxWarning: invalid escape sequence '\.'

Just re.split('\.|\(| ', line.strip()) at factory.py:126 — needs an r prefix. Not blocking, but noisy in the console on Python 3.12+.

python
if key in re.split(r'\.|\(| ', line.strip()):

Testing

I tested on:

  • Blender 5.1.1 with Python 3.13, Unreal 5.7.4

Static mesh, skeletal mesh, animation export, and instance-assets extension all work.

Kelit

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions