diff --git a/conformance/5.0/test_bpy_data.py b/conformance/5.0/test_bpy_data.py new file mode 100644 index 0000000..6b6415b --- /dev/null +++ b/conformance/5.0/test_bpy_data.py @@ -0,0 +1,26 @@ +import bpy + +# Print all objects. +for obj in bpy.data.objects: + print(obj.name) + + +# Print all scene names in a list. +print(bpy.data.scenes.keys()) + + +# Remove mesh Cube. +if "Cube" in bpy.data.meshes: + mesh = bpy.data.meshes["Cube"] + print("removing mesh", mesh) + bpy.data.meshes.remove(mesh) + + +# Write images into a file next to the blend. +import os + +with open(os.path.splitext(bpy.data.filepath)[0] + ".txt", "w") as fs: + for image in bpy.data.images: + fs.write( + "{:s} {:d} x {:d}\n".format(image.filepath, image.size[0], image.size[1]) + ) diff --git a/conformance/5.0/test_bpy_msgbus_example_use.py b/conformance/5.0/test_bpy_msgbus_example_use.py new file mode 100644 index 0000000..b15b393 --- /dev/null +++ b/conformance/5.0/test_bpy_msgbus_example_use.py @@ -0,0 +1,26 @@ +import bpy + +# Any Python object can act as the subscription's owner. +owner = object() + +assert bpy.context.object is not None +subscribe_to = bpy.context.object.location + + +def msgbus_callback(*args: int): + # This will print: + # Something changed! (1, 2, 3) + print("Something changed!", args) + + +bpy.msgbus.subscribe_rna( + key=subscribe_to, + owner=owner, + args=(1, 2, 3), + notify=msgbus_callback, +) + +assert bpy.context.object is not None +subscribe_to = bpy.context.object.path_resolve("name", False) + +subscribe_to = (bpy.types.Object, "location") diff --git a/conformance/5.0/test_bpy_ops_execution_context.py b/conformance/5.0/test_bpy_ops_execution_context.py new file mode 100644 index 0000000..06dc086 --- /dev/null +++ b/conformance/5.0/test_bpy_ops_execution_context.py @@ -0,0 +1,14 @@ +# Collection add popup. +import bpy +from bpy import context + +bpy.ops.object.collection_instance_add("INVOKE_DEFAULT") + +# Maximize 3d view in all windows. +for window in context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == "VIEW_3D": + with context.temp_override(window=window, area=area): + bpy.ops.screen.screen_full_area() + break diff --git a/conformance/5.0/test_bpy_ops_keywords_and_positional_arguments.py b/conformance/5.0/test_bpy_ops_keywords_and_positional_arguments.py new file mode 100644 index 0000000..b9e9c2c --- /dev/null +++ b/conformance/5.0/test_bpy_ops_keywords_and_positional_arguments.py @@ -0,0 +1,9 @@ +# Calling an operator. +import bpy + +bpy.ops.mesh.subdivide(number_cuts=3, smoothness=0.5) + + +# Check poll() to avoid exception. +if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode="EDIT") diff --git a/conformance/5.0/test_bpy_ops_overriding_context.py b/conformance/5.0/test_bpy_ops_overriding_context.py new file mode 100644 index 0000000..db50c3f --- /dev/null +++ b/conformance/5.0/test_bpy_ops_overriding_context.py @@ -0,0 +1,8 @@ +# Remove all objects in scene rather than the selected ones. +import bpy +from bpy import context + +context_override = context.copy() +context_override["selected_objects"] = list(context.scene.objects) +with context.temp_override(**context_override): + bpy.ops.object.delete() diff --git a/conformance/5.0/test_bpy_types_context.py b/conformance/5.0/test_bpy_types_context.py new file mode 100644 index 0000000..1279ca4 --- /dev/null +++ b/conformance/5.0/test_bpy_types_context.py @@ -0,0 +1,27 @@ +import bpy +from bpy import context + +# Reload the current file and select all. +bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath) +window = context.window_manager.windows[0] +with context.temp_override(window=window): + bpy.ops.mesh.primitive_uv_sphere_add() + # The context override is needed so it's possible to set edit-mode. + bpy.ops.object.mode_set(mode="EDIT") + +win_active = context.window +win_other = None +for win_iter in context.window_manager.windows: + if win_iter != win_active: + win_other = win_iter + break + +# Add cube in the other window. +with context.temp_override(window=win_other): + bpy.ops.mesh.primitive_cube_add() + +my_objects = [context.scene.camera] + +with context.temp_override(selected_objects=my_objects) as override: + override.logging_set(True) # Enable logging. + bpy.ops.object.delete() diff --git a/conformance/5.0/test_bpy_types_object.py b/conformance/5.0/test_bpy_types_object.py new file mode 100644 index 0000000..ecfdad2 --- /dev/null +++ b/conformance/5.0/test_bpy_types_object.py @@ -0,0 +1,20 @@ +import bpy + +view_layer = bpy.context.view_layer + +# Create new light data-block. +light_data = bpy.data.lights.new(name="New Light", type="POINT") + +# Create new object with our light data-block. +light_object = bpy.data.objects.new(name="New Light", object_data=light_data) + +# Link light object to the active collection of current view layer, +# so that it'll appear in the current scene. +view_layer.active_layer_collection.collection.objects.link(light_object) + +# Place light to a specified location. +light_object.location = (5.0, 5.0, 5.0) + +# And finally select it and make it active. +light_object.select_set(True) +view_layer.objects.active = light_object diff --git a/conformance/5.1/test_bpy_data.py b/conformance/5.1/test_bpy_data.py new file mode 100644 index 0000000..6b6415b --- /dev/null +++ b/conformance/5.1/test_bpy_data.py @@ -0,0 +1,26 @@ +import bpy + +# Print all objects. +for obj in bpy.data.objects: + print(obj.name) + + +# Print all scene names in a list. +print(bpy.data.scenes.keys()) + + +# Remove mesh Cube. +if "Cube" in bpy.data.meshes: + mesh = bpy.data.meshes["Cube"] + print("removing mesh", mesh) + bpy.data.meshes.remove(mesh) + + +# Write images into a file next to the blend. +import os + +with open(os.path.splitext(bpy.data.filepath)[0] + ".txt", "w") as fs: + for image in bpy.data.images: + fs.write( + "{:s} {:d} x {:d}\n".format(image.filepath, image.size[0], image.size[1]) + ) diff --git a/conformance/5.1/test_bpy_msgbus_example_use.py b/conformance/5.1/test_bpy_msgbus_example_use.py new file mode 100644 index 0000000..b15b393 --- /dev/null +++ b/conformance/5.1/test_bpy_msgbus_example_use.py @@ -0,0 +1,26 @@ +import bpy + +# Any Python object can act as the subscription's owner. +owner = object() + +assert bpy.context.object is not None +subscribe_to = bpy.context.object.location + + +def msgbus_callback(*args: int): + # This will print: + # Something changed! (1, 2, 3) + print("Something changed!", args) + + +bpy.msgbus.subscribe_rna( + key=subscribe_to, + owner=owner, + args=(1, 2, 3), + notify=msgbus_callback, +) + +assert bpy.context.object is not None +subscribe_to = bpy.context.object.path_resolve("name", False) + +subscribe_to = (bpy.types.Object, "location") diff --git a/conformance/5.1/test_bpy_ops_execution_context.py b/conformance/5.1/test_bpy_ops_execution_context.py new file mode 100644 index 0000000..06dc086 --- /dev/null +++ b/conformance/5.1/test_bpy_ops_execution_context.py @@ -0,0 +1,14 @@ +# Collection add popup. +import bpy +from bpy import context + +bpy.ops.object.collection_instance_add("INVOKE_DEFAULT") + +# Maximize 3d view in all windows. +for window in context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == "VIEW_3D": + with context.temp_override(window=window, area=area): + bpy.ops.screen.screen_full_area() + break diff --git a/conformance/5.1/test_bpy_ops_keywords_and_positional_arguments.py b/conformance/5.1/test_bpy_ops_keywords_and_positional_arguments.py new file mode 100644 index 0000000..b9e9c2c --- /dev/null +++ b/conformance/5.1/test_bpy_ops_keywords_and_positional_arguments.py @@ -0,0 +1,9 @@ +# Calling an operator. +import bpy + +bpy.ops.mesh.subdivide(number_cuts=3, smoothness=0.5) + + +# Check poll() to avoid exception. +if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode="EDIT") diff --git a/conformance/5.1/test_bpy_ops_overriding_context.py b/conformance/5.1/test_bpy_ops_overriding_context.py new file mode 100644 index 0000000..db50c3f --- /dev/null +++ b/conformance/5.1/test_bpy_ops_overriding_context.py @@ -0,0 +1,8 @@ +# Remove all objects in scene rather than the selected ones. +import bpy +from bpy import context + +context_override = context.copy() +context_override["selected_objects"] = list(context.scene.objects) +with context.temp_override(**context_override): + bpy.ops.object.delete() diff --git a/conformance/5.1/test_bpy_types_context.py b/conformance/5.1/test_bpy_types_context.py new file mode 100644 index 0000000..6d6b241 --- /dev/null +++ b/conformance/5.1/test_bpy_types_context.py @@ -0,0 +1,36 @@ +import bpy +from bpy import context + +# Example inserting keyframe for the hovered property. +active_property = bpy.context.property +if active_property: + datablock, data_path, index = active_property + datablock.keyframe_insert(data_path=data_path, index=index, frame=1) + +# Reload the current file and select all. +bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath) +window = context.window_manager.windows[0] +with context.temp_override(window=window): + bpy.ops.mesh.primitive_uv_sphere_add() + # The context override is needed so it's possible to set edit-mode. + bpy.ops.object.mode_set(mode="EDIT") + +win_active = context.window +win_other = None +for win_iter in context.window_manager.windows: + if win_iter != win_active: + win_other = win_iter + break + +# Add cube in the other window. +with context.temp_override(window=win_other): + bpy.ops.mesh.primitive_cube_add() + +my_objects = [context.scene.camera] + +with context.temp_override(selected_objects=my_objects) as override: + override.logging_set( + True, # Enable logging. + hide_missing=True, # Don't show failed attempts. + ) + bpy.ops.object.delete() diff --git a/conformance/5.1/test_bpy_types_object.py b/conformance/5.1/test_bpy_types_object.py new file mode 100644 index 0000000..ecfdad2 --- /dev/null +++ b/conformance/5.1/test_bpy_types_object.py @@ -0,0 +1,20 @@ +import bpy + +view_layer = bpy.context.view_layer + +# Create new light data-block. +light_data = bpy.data.lights.new(name="New Light", type="POINT") + +# Create new object with our light data-block. +light_object = bpy.data.objects.new(name="New Light", object_data=light_data) + +# Link light object to the active collection of current view layer, +# so that it'll appear in the current scene. +view_layer.active_layer_collection.collection.objects.link(light_object) + +# Place light to a specified location. +light_object.location = (5.0, 5.0, 5.0) + +# And finally select it and make it active. +light_object.select_set(True) +view_layer.objects.active = light_object diff --git a/generate_stubs.py b/generate_stubs.py index cebf69a..90ba69d 100644 --- a/generate_stubs.py +++ b/generate_stubs.py @@ -384,7 +384,9 @@ def generate_property_stub( else: result += f"{indent} ...\n" result += f"{indent}@{prop['name']}.setter\n" - result += f"{indent}def {prop['name']}(self, value: {setter_type}) -> None: ...\n" + result += ( + f"{indent}def {prop['name']}(self, value: {setter_type}) -> None: ...\n" + ) return result result = f"{indent}{prop['name']}: {prop['type']}\n" @@ -553,6 +555,31 @@ def collect_inherited_info( return cache +def _generate_context_dict(context_struct: StructData) -> str: + """Generate a ContextDict TypedDict from Context properties. + + This TypedDict mirrors all Context properties so that Context.copy() + can return a precisely typed dict instead of dict[str, object]. + """ + lines: list[str] = ["class ContextDict(TypedDict):"] + lines.append(' """Dictionary returned by Context.copy() with all context members."""') + method_names = {m["name"] for m in context_struct["methods"]} + for prop in context_struct["properties"]: + if prop["name"] in method_names: + continue + lines.append(f" {prop['name']}: {prop['type']}") + lines.append("") + return "\n".join(lines) + + +def _patch_context_copy_return_type(context_struct: StructData) -> None: + """Change Context.copy() return type from dict[str, object] to ContextDict.""" + for method in context_struct["methods"]: + if method["name"] == "copy": + method["return_type"] = "ContextDict" + break + + def generate_types_stub( structs: list[StructData], python_version: str = "3.11", doc: str = "" ) -> str: @@ -570,7 +597,7 @@ def generate_types_stub( all_type_strs_parts.append(m["return_type"]) all_type_strs = " ".join(all_type_strs_parts) - typing_imports = ["Generic", "TypeVar"] + typing_imports = ["Generic", "TypedDict", "TypeVar"] if "Literal[" in all_type_strs: typing_imports.append("Literal") if re.search(r"\bSelf\b", all_type_strs): @@ -615,6 +642,11 @@ def generate_types_stub( sorted_structs = topological_sort_structs(structs) inherited_map = collect_inherited_info(sorted_structs) + # Patch Context.copy() return type and generate ContextDict TypedDict + context_struct = next((s for s in sorted_structs if s["name"] == "Context"), None) + if context_struct: + _patch_context_copy_return_type(context_struct) + for struct in sorted_structs: info = inherited_map.get(struct["name"], _InheritedInfo(set(), set())) # Force writable properties to readonly when they override a parent's @@ -622,6 +654,10 @@ def generate_types_stub( for prop in struct["properties"]: if not prop["is_readonly"] and prop["name"] in info.readonly_props: prop["is_readonly"] = True + # Emit ContextDict TypedDict right before the Context class + if struct["name"] == "Context": + parts.append("") + parts.append(_generate_context_dict(struct)) parts.append("") parts.append(generate_struct_stub(struct, info.methods))