# Why is globalString not being updated by localUpdate in this add-on?

From the print statements in localUpdate it appears that changing the value of localString in the Redo panel updates globalString but globalString is not actually updated. What am I missing? (This doesn't seem to be a Blender bug, but something I'm not understanding about StringProperty or properties in general.)

NOTE: This is a a question about why the update function doesn't work, not about how to update the global without using the update function. I've simplified a fairly complex setup that has mixins that modify the local to get to this example, so while updating the global in execute would work in the example, it won't work in the final system.

To see the problem, invoke the operator by using the button in the tla Panel. When the Last Operator Adjustment (redo) panel shows up, change the value of localString in the panel. Messages will appear on the system console showing that localUpdate has run and modified globalString but if you check bpy.context.scene.globalString in the Python console after making the change, it will still have its original value of "global". Further, setting its value in the Python console causes it to change in the panel.

Here is the add-on. Copy it into the text editor and hit run.

import bpy
from bpy.types import Operator, Panel
from bpy.props import StringProperty

bl_info = {
"name" : "Op Test",
"description" : "a test",
"author" : "Marty",
"version" : (0, 0, 1),
"blender" : (2, 80, 0),
"location" : "View3D",
"support" : "COMMUNITY",
"category" : "3D View"
}

class TLA_OT_message(Operator):
bl_idname = "tla.message"
bl_label = "An Op"
bl_options = {"REGISTER", "UNDO"}

def localUpdate(self, context):
print("localUpdate called")
print(self.localString)
print(f'before {bpy.context.scene.globalString}')
context.scene.globalString = self.localString
print(f' after {bpy.context.scene.globalString}')

localString : bpy.props.StringProperty(
name="localString",
default="local",
update=localUpdate)

def execute(self, context):
self.report({"INFO"}, f"local string is {self.localString}")
return {"FINISHED"}

class TL_PT_panel(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "tla"
bl_label = "Panel"
bl_options = set()

def draw(self, context):
col = self.layout.column()
col.operator(TLA_OT_message.bl_idname, text = "push me")
col.prop(context.scene, "globalString", text="Global")

def anUpdater(self, context):
print("an Updater called")

def register():
bpy.utils.register_class(TLA_OT_message)
bpy.utils.register_class(TL_PT_panel)
bpy.types.Scene.globalString = bpy.props.StringProperty(
name="globalString",
default = "global",
update=anUpdater,
)

def unregister():
del bpy.types.Scene.globalString
bpy.utils.unregister_class(TL_PT_panel)
bpy.utils.unregister_class(TLA_OT_message)

if __name__ == "__main__":
register()


Here is what the 3D Viewport looks like after installing the addon, opening the side panel, selecting the 'tla' tab, and pushing the button:

Change "local" to "other" and the info message in the info window changes:

bpy.ops.text.run_script()
bpy.ops.tla.message()
local string is local
bpy.data.window_managers["WinMan"].(null) = "other"
bpy.ops.tla.message(localString="other")
local string is other


(Icons don't copy so here's a screenshot)

and the content of the system console at this point:

Read prefs: C:\Users\stupi\AppData\Roaming\Blender Foundation\Blender\2.93\config\userpref.blend
Info: local string is local

localUpdate called
other
before global
an Updater called
after other
Info: local string is other



Finally, checking the value of bpy.context.scene.globalString after all of that:

PYTHON INTERACTIVE CONSOLE 3.9.2 (default, Mar  1 2021, 08:18:55) [MSC v.1916 64 bit (AMD64)]

Builtin Modules:       bpy, bpy.data, bpy.ops, bpy.props, bpy.types, bpy.context, bpy.utils, bgl, blf, mathutils
Convenience Imports:   from mathutils import *; from math import *
Convenience Variables: C = bpy.context, D = bpy.data

>>> bpy.context.scene.globalString
'global'

>>>


and for completeness: my test file:

• but why do you want to show 2 fields with the "same" value (global + local)? i don't really get it...? Nov 26, 2021 at 6:36
• @Chris because you can't display class properties in a panel. The workaround I'm working on is to keep global versions in sync and display the the global versions. Eventually, once that works I'll add presets to both the panel and the operator, at least in theory. So I need to keep the global/local in sync so I can use presets in the panel and presets in the operator. Nov 26, 2021 at 16:03

Although i am not the add-on-Pro....i think it is because you are showing the global variable field too. So it takes the value from that field. If you comment out

#        col.prop(context.scene, "globalString", text="Global")


it works.

But i am sure batfinger can explain this better ;)

• The value of bpy.context.scene.globalString isn't updated this way. Nov 26, 2021 at 15:59

It is because localUpdate is an instance method. localString is a class annotation adding a property to all instances but at the time is defined, instance methods are not yet registered in the class. (At least that's my guess)

So you have to move localUpdate out of the class definition :

This works :

import bpy
from bpy.types import Operator, Panel
from bpy.props import StringProperty

bl_info = {
"name" : "Op Test",
"description" : "a test",
"author" : "Marty",
"version" : (0, 0, 1),
"blender" : (2, 80, 0),
"location" : "View3D",
"support" : "COMMUNITY",
"category" : "3D View"
}

def localUpdate(self, context):
print("localUpdate called")
print(self.localString)
print(f'before {bpy.context.scene.globalString}')
context.scene.globalString = self.localString
print(f' after {bpy.context.scene.globalString}')

class TLA_OT_message(Operator):
bl_idname = "tla.message"
bl_label = "An Op"
bl_options = {"REGISTER", "UNDO"}

localString : bpy.props.StringProperty(
name="localString",
default="local",
update=localUpdate)

def execute(self, context):
self.report({"INFO"}, f"local string is {self.localString}")
return {"FINISHED"}

class TL_PT_panel(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "tla"
bl_label = "Panel"
bl_options = set()

def draw(self, context):
col = self.layout.column()
col.operator(TLA_OT_message.bl_idname, text = "push me")
col.prop(context.scene, "globalString", text="Global")

def anUpdater(self, context):
print("an Updater called")

def register():
bpy.utils.register_class(TLA_OT_message)
bpy.utils.register_class(TL_PT_panel)
bpy.types.Scene.globalString = bpy.props.StringProperty(
name="globalString",
default = "global",
update=anUpdater,
)

def unregister():
del bpy.types.Scene.globalString
bpy.utils.unregister_class(TL_PT_panel)
bpy.utils.unregister_class(TLA_OT_message)

if __name__ == "__main__":
register()

• python noob here: why can python access self.localString in a global func def localUpdate(self, context) ? I know from other programming languages that properties of classes "normally" cannot be accessed from outside the class - except class variables or global variables, but even then there should be the class instance written before like instance.property = blabla or Classname.property = blubb. Looks like i have a basic misunderstanding here... :( Nov 26, 2021 at 8:11
• Yeah that's because of the way properties are registered in the Blender API : they're not instance attributes (foo = "this is an attribute"), they are (type) class annotations (bar: bpy.props.StringProperty()), meaning that they are dynamically added to Operator class instances when the operator is created and executed at run time using a button in the layout or the search operator. In the process the update method's parameter are dynamically populated, whereas in a regular class instance attribute the self is implicitly added. eg if you have an instance method method on class A Nov 26, 2021 at 9:03
• Calling a = A() and then a.method() is the same as calling A.method(a) (Again that's how I understand it I may be totally wrong ><) Nov 26, 2021 at 9:03
• You also have to understand that we are using an API, meaning this is just an interface to the actual Blender codebase, you can see it as a keyboard and mouse peripherals, keyboard and mouse are just an interface to your computer, and you don't need to see or understand what signals goes to the computer and comes back to actually manage your computer. But the core of the computation is done in the computer, not the interface. So what you're defining is python, yes, but it's also calling a bunch of C / C++ code under the hood that you don't need to know about for it to work Nov 26, 2021 at 9:09
• ok, thank you. My misunderstanding was my assumption this would be a "normal" property. Thank you for clarification. Nov 26, 2021 at 9:11

In the end, the answer is "You can't get there from here". More precisely, when you add an update function to a property in a class, Blender massages that property at registration time, in such a way that the context argument to the update function becomes the window manager, and not the context you need to look up the property.

This is why the info window gets the message bpy.data.window_managers["WinMan"].(null) = "other" in my example.

It doesn't matter whether the update function is defined within the class or not, it just doesn't work. For now, I have a workaround that works for presets, although it does require a bit of excess cleverness.

import bpy
from bpy.types import Operator, Panel
from bpy.props import StringProperty

bl_info = {
"name" : "Op Test",
"description" : "a test",
"author" : "Marty",
"version" : (0, 0, 1),
"blender" : (2, 80, 0),
"location" : "View3D",
"support" : "COMMUNITY",
"category" : "3D View"
}

class TLA_OT_message(Operator):
bl_idname = "tla.message"
bl_label = "An Op"
bl_options = {"REGISTER", "UNDO", "PRESET"}

need_update = False

localString : bpy.props.StringProperty(
name="localString",
default="local",
)

def updateGlobal(self, context):
context.scene.globalString = self.localString

@classmethod
def requireUpdate(cls):
cls.need_update = True

def execute(self, context):
if TLA_OT_message.need_update:
self.localString = context.scene.globalString
self.report({"INFO"}, f"local string is {self.localString}")
self.updateGlobal(context)
# The call to updateGlobal will cause the global's anUpdater
# function to be called, causing need_update to be set to True
# but we don't need an update so we override that change before
# leaving the function.
TLA_OT_message.need_update = False
return {"FINISHED"}

class TLA_PT_panel(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "tla"
bl_label = "Panel"
bl_options = set()

def draw(self, context):
col = self.layout.column()
col.operator(TLA_OT_message.bl_idname, text = "push me")
col.prop(context.scene, "globalString", text="Global")

def anUpdater(self, context):
TLA_OT_message.requireUpdate()

def register():
bpy.utils.register_class(TLA_OT_message)
bpy.utils.register_class(TLA_PT_panel)
bpy.types.Scene.globalString = bpy.props.StringProperty(
name="globalString",
default = "global",
update=anUpdater,
)

def unregister():
del bpy.types.Scene.globalString
bpy.utils.unregister_class(TLA_PT_panel)
bpy.utils.unregister_class(TLA_OT_message)

if __name__ == "__main__":
register()
$$$$
`