Bpy.context not working as expected

I had a script which included bpy.context . Once I’d built the script up with various other functions, it was no longer working properly.

Using advice from here:https://blender.stackexchange.com/questions/141330/problem-with-bpy-context-selected-objects/141342#141342 I changed various lines to use bpy.context.scene.objects if o.select_get() instead of bpy.context.selected_objects .

That worked nicely when I substituted into several of my def functions. Unfortunately in one function I’m relying on a built-in module (not sure if that is the correct term):

from bl_operators.uvcalc_smart_project import main as uv_smart_project
uv_smart_project(bpy.context, .02, 30, 0.03, True, True)

That one no longer works either, but as it is a built-in module I can’t easily go in and update the module to use o.select_get() . I get the error:

ob for ob in context.selected_editable_objects
AttributeError: 'Context' object has no attribute 'selected_editable_objects'

If, after the main script fails, I run the two problematic lines in a separate text block, then it runs fine.

I tried an override like this:

for window in context.window_manager.windows:
    screen = window.screen

    for area in screen.areas:
        if area.type == 'VIEW_3D':
            override = {'window': window, 'screen': screen, 'area': area}
            uv_smart_project(override, .02, 30, 0.03, True, True)

But that gives me 'dict' object has no attribute 'mode' and I can’t see how to add the mode to the override (if that is even a thing).

Is there a way to set the context correctly?

Use context.copy() to get all available context members before you pass it as an override.

for window in context.window_manager.windows:
    screen = window.screen

    for area in screen.areas:
        if area.type == 'VIEW_3D':
            override = bpy.context.copy()
            override.update(window=window, screen=screen, area=area)
            uv_smart_project(override, .02, 30, 0.03, True, True)

If the issue stems from missing dict keys, you just need to copy the context dict before passing the override. But it’s all a guessing game without seeing the actual script or at least a reproducible snippet.

Edit:

I managed to reproduce it. Looks like override won’t work since somewhere in the smart project script it uses direct access to context member instead of key access, which a dict provides.

You can create a stub class and update its attributes, then pass the class itself as override.

import bpy
from bl_operators.uvcalc_smart_project import main

context = bpy.context

def run():

    for window in context.window_manager.windows:
        screen = window.screen

        for area in screen.areas:
            if area.type == 'VIEW_3D':
            
                tmp = bpy.context.copy()
                tmp.update(window=window, screen=screen, area=area)

                class Override:
                    pass

                for key, val in tmp.items():
                    setattr(Override, key, val)

                main(Override, .02, 30, 0.03, True, True)
                return

if __name__ == '__main__':
    run()

That is seriously helpful of you. Thanks iceythe.

Unfortunately I still get an error. This time it is:
AttributeError: type object 'Override' has no attribute 'selected_editable_objects'

If I run just this part of the script directly after the error (it is a separate text block), it runs fine. So I’m still stuck with the issue that for some reason the context or selection set or something is not being passed through to the module/function properly.

Any suggestions?

Sounds like the context is being restricted for some reason. Are you running the function from a timer or a different thread?

selected_editable_objects will in this case be a list of selected mesh objects, so if you stored a list of these from earlier in the script you can just add it (let’s call it my_objects) to the class.

class Override:
    pass

for key, val in tmp.items():
    setattr(Override, key, val)

setattr(Override, 'selected_editable_objects', my_objects)

Thanks, I’ll try that.

Actually I’ve just found that it does work after all. Not quite as intended, but hopefully I can work out why that is.

The function that seems to make it not work is at the beginning of my script, where I delete everything in the scene and save/reopen the file to clear all unused data blocks. After that function, files are imported, cleaned up and so on. If I comment this function out, your initial script works.

This is the clear function, in case it tells you anything useful about why it wouldn’t work.

def ClearAll():

    for c in context.scene.collection.children:
        context.scene.collection.children.unlink(c)
    
    for c in bpy.data.collections:
        if not c.users:
            bpy.data.collections.remove(c)

    for o in bpy.data.objects:
        try:
            bpy.data.objects.remove(o)
        except:
            print('All objects deleted')

    bpy.ops.wm.save_mainfile()
    CurrentPath = bpy.path.abspath('//')
    CurrentFile = bpy.path.basename(context.blend_data.filepath)
    print(CurrentPath)
    print(CurrentFile)
    bpy.ops.wm.open_mainfile(filepath=CurrentPath+CurrentFile)

Anyway, I’ll try your most recent bit of advice, above, and see if that makes it work with the ClearAll function enabled.

The issue is that you’re re-opening the blend file with bpy.ops.wm.open_mainfile().

If you put print(len(bpy.context.copy())) before and after ClearAll() you’ll notice the context members has shrunk from over 60 to less than 20. It probably means the context hasn’t had a chance to list all available entries at execution time.

If you just want to clear orphaned data and you’re running 2.80, you can use the outliner’s purge instead. It’s been rewritten to clear orphans without saving/reloading the blend file.

import bpy

context = bpy.context

def override():
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'OUTLINER':
                override_dict = {}
                override_dict.update(area=area)
                return override_dict

def ClearAll():

    for c in context.scene.collection.children:
        context.scene.collection.children.unlink(c)
    
    for c in bpy.data.collections:
        if not c.users:
            bpy.data.collections.remove(c)

    for o in bpy.data.objects:
        try:
            bpy.data.objects.remove(o)
        except:
            print('All objects deleted')

    override_dict = override()
    if override_dict is not None:
        bpy.ops.outliner.orphans_purge(override_dict)


print("context:", len(bpy.context.copy()), "members")
ClearAll()
print("context:", len(bpy.context.copy()), "members")

It works! Thank you so much iceythe. I’ve been struggling with this for so long that I had almost given up hope.

That’s awesome. Glad to help!

I wasn’t sure if you were on 2.80, but in case you weren’t I wrote a purge function that works in both versions. By the sound of it you probably don’t need it, but I’ll post it anyway.

import bpy

def purge_orphans():
    bpc = bpy.types.bpy_prop_collection

    for data in [eval("bpy.data.%s" % i) for i in dir(bpy.data)]:
        if type(data) is bpc and hasattr(data, 'remove'):
            remove = data.remove
            for obj in data:
                if not obj.users:
                    remove(obj)

if __name__ == '__main__':
    purge_orphans()
1 Like

Well hopefully that helps somebody. I’m on 2.80 (and loving it) but have linked to this thread on Stack Exchange so others may stumble across your 2.79 script.