blenderwright 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. {blenderwright-2.1.0 → blenderwright-2.2.0}/PKG-INFO +4 -3
  2. {blenderwright-2.1.0 → blenderwright-2.2.0}/README.md +3 -2
  3. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/__init__.py +1 -1
  4. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/blender_manifest.toml +1 -1
  5. blenderwright-2.2.0/addon/handlers/code_exec.py +176 -0
  6. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/ui_panel.py +34 -0
  7. {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/index.html +25 -9
  8. {blenderwright-2.1.0 → blenderwright-2.2.0}/pyproject.toml +1 -1
  9. blenderwright-2.2.0/src/blenderwright/tools/code_exec.py +39 -0
  10. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_code_exec_handler.py +99 -0
  11. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_ui_panel.py +20 -0
  12. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_code_exec.py +16 -0
  13. {blenderwright-2.1.0 → blenderwright-2.2.0}/uv.lock +1 -1
  14. blenderwright-2.1.0/addon/handlers/code_exec.py +0 -101
  15. blenderwright-2.1.0/src/blenderwright/tools/code_exec.py +0 -31
  16. {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/FUNDING.yml +0 -0
  17. {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/ci.yml +0 -0
  18. {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/pylint.yml +0 -0
  19. {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/release.yml +0 -0
  20. {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/test-bpy51.yml +0 -0
  21. {blenderwright-2.1.0 → blenderwright-2.2.0}/.gitignore +0 -0
  22. {blenderwright-2.1.0 → blenderwright-2.2.0}/LICENSE.md +0 -0
  23. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/dispatcher.py +0 -0
  24. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/__init__.py +0 -0
  25. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/animation.py +0 -0
  26. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/armature.py +0 -0
  27. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/booltool.py +0 -0
  28. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/camera.py +0 -0
  29. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/collections.py +0 -0
  30. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/curves.py +0 -0
  31. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/file_ops.py +0 -0
  32. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/geometry_nodes.py +0 -0
  33. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/gpencil.py +0 -0
  34. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/lighting.py +0 -0
  35. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/materials.py +0 -0
  36. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/mesh_editing.py +0 -0
  37. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/mesh_quality.py +0 -0
  38. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/modeling.py +0 -0
  39. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/objects.py +0 -0
  40. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/physics.py +0 -0
  41. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/print3d.py +0 -0
  42. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/procedural.py +0 -0
  43. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/raster.py +0 -0
  44. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/rendering.py +0 -0
  45. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/scene.py +0 -0
  46. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/sculpting.py +0 -0
  47. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/selection.py +0 -0
  48. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/sequencer.py +0 -0
  49. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/sweep.py +0 -0
  50. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/transforms.py +0 -0
  51. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/uv.py +0 -0
  52. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/viewport.py +0 -0
  53. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/render_guard.py +0 -0
  54. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/server.py +0 -0
  55. {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/thread_safety.py +0 -0
  56. {blenderwright-2.1.0 → blenderwright-2.2.0}/blend-ai-v1.0.0.zip +0 -0
  57. {blenderwright-2.1.0 → blenderwright-2.2.0}/build.ps1 +0 -0
  58. {blenderwright-2.1.0 → blenderwright-2.2.0}/build.sh +0 -0
  59. {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/.nojekyll +0 -0
  60. {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/CNAME +0 -0
  61. {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/googled2f6c71abc2d6a7b.html +0 -0
  62. {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/robots.txt +0 -0
  63. {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/shuttle-launch.mp4 +0 -0
  64. {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/sitemap.xml +0 -0
  65. {blenderwright-2.1.0 → blenderwright-2.2.0}/glama.json +0 -0
  66. {blenderwright-2.1.0 → blenderwright-2.2.0}/install-addon.sh +0 -0
  67. {blenderwright-2.1.0 → blenderwright-2.2.0}/install_addon.py +0 -0
  68. {blenderwright-2.1.0 → blenderwright-2.2.0}/launch_ollama.sh +0 -0
  69. {blenderwright-2.1.0 → blenderwright-2.2.0}/saloon.png +0 -0
  70. {blenderwright-2.1.0 → blenderwright-2.2.0}/screenshot.png +0 -0
  71. {blenderwright-2.1.0 → blenderwright-2.2.0}/scripts/generate_tool_docs.py +0 -0
  72. {blenderwright-2.1.0 → blenderwright-2.2.0}/shader-preview.png +0 -0
  73. {blenderwright-2.1.0 → blenderwright-2.2.0}/shuttle-launch.gif +0 -0
  74. {blenderwright-2.1.0 → blenderwright-2.2.0}/shuttle-launch.png +0 -0
  75. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/__init__.py +0 -0
  76. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/__main__.py +0 -0
  77. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/aliases.py +0 -0
  78. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/connection.py +0 -0
  79. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/enum_hints.py +0 -0
  80. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/ollama_chat.py +0 -0
  81. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/prompts/__init__.py +0 -0
  82. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/prompts/workflows.py +0 -0
  83. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/resources/__init__.py +0 -0
  84. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/resources/scene_info.py +0 -0
  85. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/server.py +0 -0
  86. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/strict.py +0 -0
  87. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tool_registry.py +0 -0
  88. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/__init__.py +0 -0
  89. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/animation.py +0 -0
  90. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/armature.py +0 -0
  91. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/booltool.py +0 -0
  92. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/camera.py +0 -0
  93. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/collections.py +0 -0
  94. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/curves.py +0 -0
  95. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/file_ops.py +0 -0
  96. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/geometry_nodes.py +0 -0
  97. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/gpencil.py +0 -0
  98. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/lighting.py +0 -0
  99. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/materials.py +0 -0
  100. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_editing.py +0 -0
  101. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_quality.py +0 -0
  102. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/modeling.py +0 -0
  103. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/objects.py +0 -0
  104. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/physics.py +0 -0
  105. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/print3d.py +0 -0
  106. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/rendering.py +0 -0
  107. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/scene.py +0 -0
  108. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/screenshot.py +0 -0
  109. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/sculpting.py +0 -0
  110. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/selection.py +0 -0
  111. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/sequencer.py +0 -0
  112. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/sweep.py +0 -0
  113. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/transforms.py +0 -0
  114. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/uv.py +0 -0
  115. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/viewport.py +0 -0
  116. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/toolsets.py +0 -0
  117. {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/validators.py +0 -0
  118. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/__init__.py +0 -0
  119. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/conftest.py +0 -0
  120. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/__init__.py +0 -0
  121. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/conftest.py +0 -0
  122. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_dispatcher.py +0 -0
  123. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handler_registration.py +0 -0
  124. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/__init__.py +0 -0
  125. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_camera_handler.py +0 -0
  126. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_geometry_nodes_handler.py +0 -0
  127. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_gpencil_handler.py +0 -0
  128. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_materials_handler.py +0 -0
  129. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_mesh_quality_handler.py +0 -0
  130. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_modeling_handler.py +0 -0
  131. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_objects_handler.py +0 -0
  132. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_print3d_handler.py +0 -0
  133. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_procedural_handler.py +0 -0
  134. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_raster_handler.py +0 -0
  135. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_rendering_handler.py +0 -0
  136. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_scene_handler.py +0 -0
  137. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sculpting_handler.py +0 -0
  138. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_selection_handler.py +0 -0
  139. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sequencer_handler.py +0 -0
  140. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sweep_handler.py +0 -0
  141. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_layer_consistency.py +0 -0
  142. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_module_cache_purge.py +0 -0
  143. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_render_guard.py +0 -0
  144. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_server.py +0 -0
  145. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_thread_safety.py +0 -0
  146. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_command_registry.py +0 -0
  147. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_connection.py +0 -0
  148. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_enum_hints.py +0 -0
  149. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_installer.py +0 -0
  150. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_long_names.py +0 -0
  151. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_object_param_naming.py +0 -0
  152. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_budget.py +0 -0
  153. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_chat.py +0 -0
  154. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_toolsets.py +0 -0
  155. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_vision.py +0 -0
  156. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_project_metadata.py +0 -0
  157. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_round_wasters.py +0 -0
  158. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_strict_parameters.py +0 -0
  159. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tool_disambiguation.py +0 -0
  160. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/__init__.py +0 -0
  161. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/conftest.py +0 -0
  162. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_animation.py +0 -0
  163. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_armature.py +0 -0
  164. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_auto_critique_prompt.py +0 -0
  165. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_booltool.py +0 -0
  166. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_camera.py +0 -0
  167. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_collections.py +0 -0
  168. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_curves.py +0 -0
  169. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_eevee_light_path.py +0 -0
  170. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_extensions.py +0 -0
  171. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_file_ops.py +0 -0
  172. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_geometry_nodes.py +0 -0
  173. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_gpencil.py +0 -0
  174. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_issue_31_validation.py +0 -0
  175. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_lighting.py +0 -0
  176. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_materials.py +0 -0
  177. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_mesh_editing.py +0 -0
  178. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_mesh_quality.py +0 -0
  179. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_mesh_repair.py +0 -0
  180. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_modeling.py +0 -0
  181. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_negative_scale_primitives.py +0 -0
  182. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_new_nodes.py +0 -0
  183. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_numeric_text_values.py +0 -0
  184. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_objects.py +0 -0
  185. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_physics.py +0 -0
  186. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_point_camera_at.py +0 -0
  187. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_print3d.py +0 -0
  188. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_procedural.py +0 -0
  189. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_prompts.py +0 -0
  190. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_raster.py +0 -0
  191. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_rendering.py +0 -0
  192. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_scene.py +0 -0
  193. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_screenshot.py +0 -0
  194. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_sculpting.py +0 -0
  195. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_selection.py +0 -0
  196. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_selection_respected.py +0 -0
  197. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_sequencer.py +0 -0
  198. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_shader_nodes.py +0 -0
  199. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_sweep.py +0 -0
  200. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_transforms.py +0 -0
  201. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_uv.py +0 -0
  202. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_uv_slim.py +0 -0
  203. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_viewport.py +0 -0
  204. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_toolsets.py +0 -0
  205. {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: blenderwright
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: The most intuitive and efficient MCP Server for Blender
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE.md
@@ -49,7 +49,7 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
49
49
  - **Visual feedback loop**: fast viewport screenshots via OpenGL render (~ms, not seconds) with auto-critique prompts that guide the LLM to check its own work
50
50
  - **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
51
51
  - **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
52
- - **Sandboxed code execution**: `execute_blender_code` blocks dangerous imports (`os`, `subprocess`, `socket`, etc.) and dangerous builtins (`exec`, `eval`, `open`) while allowing safe Blender operations
52
+ - **Raw Python is opt-in**: `execute_blender_code` is off until you tick "Allow raw Python" in the N-panel, per session, never saved into the .blend. When on, every snippet is echoed to the console and listed in the panel. A restricted namespace blocks obvious mistakes (`os`, `subprocess`, `exec`, `open`, ...); it is a guard against accidents, not a security boundary
53
53
  - **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
54
54
  - **Blender 4.2+ compatible**: ships as a Blender Extension; tested against Blender 5.1 with EEVEE identifier, Annotation API, sculpt stroke_method, SLIM UV unwrap, Raycast shader node, and EEVEE light path intensity controls
55
55
  - **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
@@ -349,7 +349,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
349
349
  <summary><strong>Security</strong></summary>
350
350
 
351
351
  - **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
352
- - **Sandboxed code execution**: `execute_blender_code` blocks 25 dangerous imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and removes dangerous builtins (`__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help`, `memoryview`). Safe Blender imports (`bpy`, `bmesh`, `mathutils`, `math`, `json`) are allowed.
352
+ - **Raw Python is off by default**: `execute_blender_code` refuses to run until you tick "Allow raw Python" in the blenderwright N-panel. The switch is a `WindowManager` property, so it is per Blender session and is never written into a `.blend` file. Every run is echoed to the system console and the panel shows a count and the last snippet. The 191 structured tools do not need it.
353
+ - **The restricted namespace is not a sandbox**: when raw Python is on, the import hook refuses 25 imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and the builtins `__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help` and `memoryview` are removed. That catches a model making a mistake. It does not contain an adversary: the code runs in-process with full `bpy`, and `bpy` alone can save files anywhere and run any operator. Treat "Allow raw Python" the way you would treat running a script someone else wrote: read what the model is about to run, and turn it off when you are done.
353
354
  - **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
354
355
  - **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
355
356
  - **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
@@ -31,7 +31,7 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
31
31
  - **Visual feedback loop**: fast viewport screenshots via OpenGL render (~ms, not seconds) with auto-critique prompts that guide the LLM to check its own work
32
32
  - **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
33
33
  - **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
34
- - **Sandboxed code execution**: `execute_blender_code` blocks dangerous imports (`os`, `subprocess`, `socket`, etc.) and dangerous builtins (`exec`, `eval`, `open`) while allowing safe Blender operations
34
+ - **Raw Python is opt-in**: `execute_blender_code` is off until you tick "Allow raw Python" in the N-panel, per session, never saved into the .blend. When on, every snippet is echoed to the console and listed in the panel. A restricted namespace blocks obvious mistakes (`os`, `subprocess`, `exec`, `open`, ...); it is a guard against accidents, not a security boundary
35
35
  - **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
36
36
  - **Blender 4.2+ compatible**: ships as a Blender Extension; tested against Blender 5.1 with EEVEE identifier, Annotation API, sculpt stroke_method, SLIM UV unwrap, Raycast shader node, and EEVEE light path intensity controls
37
37
  - **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
@@ -331,7 +331,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
331
331
  <summary><strong>Security</strong></summary>
332
332
 
333
333
  - **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
334
- - **Sandboxed code execution**: `execute_blender_code` blocks 25 dangerous imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and removes dangerous builtins (`__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help`, `memoryview`). Safe Blender imports (`bpy`, `bmesh`, `mathutils`, `math`, `json`) are allowed.
334
+ - **Raw Python is off by default**: `execute_blender_code` refuses to run until you tick "Allow raw Python" in the blenderwright N-panel. The switch is a `WindowManager` property, so it is per Blender session and is never written into a `.blend` file. Every run is echoed to the system console and the panel shows a count and the last snippet. The 191 structured tools do not need it.
335
+ - **The restricted namespace is not a sandbox**: when raw Python is on, the import hook refuses 25 imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and the builtins `__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help` and `memoryview` are removed. That catches a model making a mistake. It does not contain an adversary: the code runs in-process with full `bpy`, and `bpy` alone can save files anywhere and run any operator. Treat "Allow raw Python" the way you would treat running a script someone else wrote: read what the model is about to run, and turn it off when you are done.
335
336
  - **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
336
337
  - **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
337
338
  - **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
@@ -8,7 +8,7 @@ Blender's Python API.
8
8
  bl_info = {
9
9
  "name": "blenderwright",
10
10
  "author": "blenderwright",
11
- "version": (2, 1, 0),
11
+ "version": (2, 2, 0),
12
12
  "blender": (4, 2, 0),
13
13
  "location": "View3D > Sidebar > blenderwright",
14
14
  "description": "MCP Server integration for AI-assisted 3D workflows",
@@ -1,7 +1,7 @@
1
1
  schema_version = "1.0.0"
2
2
 
3
3
  id = "blenderwright"
4
- version = "2.1.0"
4
+ version = "2.2.0"
5
5
  name = "blenderwright"
6
6
  tagline = "MCP server integration for AI-assisted 3D workflows"
7
7
  maintainer = "jabberwock <88205+jabberwock@users.noreply.github.com>"
@@ -0,0 +1,176 @@
1
+ """Blender addon handler for raw Python execution (off by default).
2
+
3
+ What this is: a restricted namespace that blocks the obvious footguns
4
+ (import os/subprocess/socket..., exec/eval/open/compile) so a model
5
+ that makes a mistake fails loudly instead of touching the filesystem
6
+ or the network by accident.
7
+
8
+ What this is NOT: a security boundary. The code runs in-process inside
9
+ Blender with full `bpy`, and `bpy` alone can save files anywhere and
10
+ run any operator. A denylist cannot contain a determined adversary in
11
+ the same interpreter, and this module does not claim to.
12
+
13
+ So the real control is the switch, not the list:
14
+
15
+ - `execute_code` is refused unless the user has ticked "Allow raw
16
+ Python" in the blenderwright N-panel. The flag lives on
17
+ `bpy.types.WindowManager`, so it is per session and is never saved
18
+ into a .blend file that could be handed to someone else.
19
+ - Every snippet that runs is echoed to the system console and kept in
20
+ a short log that the panel displays, so the user can see what the
21
+ model actually did.
22
+ """
23
+
24
+ import sys
25
+ import io
26
+ import time
27
+ from collections import deque
28
+ from .. import dispatcher
29
+
30
+
31
+ # Name of the per-session switch registered on bpy.types.WindowManager
32
+ # by ui_panel.register(). Not a Scene property on purpose: Scene props
33
+ # are written into the .blend.
34
+ SWITCH_PROP = "blenderwright_allow_code_exec"
35
+
36
+ # How many recent runs the panel can show.
37
+ EXEC_LOG_SIZE = 20
38
+ _exec_log: deque = deque(maxlen=EXEC_LOG_SIZE)
39
+
40
+
41
+ def is_enabled() -> bool:
42
+ """True only if the user turned raw Python on for this Blender session.
43
+
44
+ Anything other than a real `True` (missing property, wrong type, no
45
+ window manager yet, bpy not importable) counts as off.
46
+ """
47
+ try:
48
+ import bpy
49
+ flag = getattr(bpy.context.window_manager, SWITCH_PROP, False)
50
+ except Exception:
51
+ return False
52
+ return isinstance(flag, bool) and flag
53
+
54
+
55
+ def get_exec_log() -> list:
56
+ """Copy of the recent-run log, oldest first."""
57
+ return list(_exec_log)
58
+
59
+
60
+ def _record(code: str, ok: bool) -> None:
61
+ lines = code.strip().splitlines() or [""]
62
+ _exec_log.append({
63
+ "time": time.strftime("%H:%M:%S"),
64
+ "first_line": lines[0].strip(),
65
+ "lines": len(lines),
66
+ "ok": ok,
67
+ })
68
+
69
+
70
+ # Modules refused by the import hook. A guard against accidental misuse by
71
+ # the model, not a security control: see the module docstring.
72
+ BLOCKED_MODULES = frozenset({
73
+ "os", "subprocess", "socket", "shutil", "sys", "ctypes",
74
+ "importlib", "pathlib", "signal", "multiprocessing",
75
+ "pickle", "shelve", "tempfile", "http", "urllib", "ftplib",
76
+ "smtplib", "xmlrpc", "code", "codeop", "compileall",
77
+ "webbrowser", "antigravity", "turtle", "tkinter",
78
+ })
79
+
80
+ # Builtins removed from the restricted namespace. Same caveat as above.
81
+ _REMOVED_BUILTINS = frozenset({
82
+ "__import__", "exec", "eval", "compile", "open",
83
+ "globals", "locals", "vars", "input", "breakpoint",
84
+ "exit", "quit", "help", "memoryview",
85
+ })
86
+
87
+ # Reference to the real __import__ for the safe import hook
88
+ _real_import = builtins_import = __builtins__.__import__ if hasattr(
89
+ __builtins__, "__import__"
90
+ ) else __builtins__["__import__"] if isinstance(__builtins__, dict) else __import__
91
+
92
+
93
+ def _safe_import(name, *args, **kwargs):
94
+ """Import hook that refuses the modules in BLOCKED_MODULES."""
95
+ base_module = name.split(".")[0]
96
+ if base_module in BLOCKED_MODULES:
97
+ raise ImportError(
98
+ f"Module '{name}' is blocked inside execute_blender_code. "
99
+ f"Use the structured MCP tools instead of direct Python imports."
100
+ )
101
+ return _real_import(name, *args, **kwargs)
102
+
103
+
104
+ def _build_safe_builtins():
105
+ """Build the restricted builtins dict."""
106
+ # Get all builtins as a dict
107
+ if isinstance(__builtins__, dict):
108
+ raw = dict(__builtins__)
109
+ else:
110
+ raw = {k: getattr(__builtins__, k) for k in dir(__builtins__)}
111
+
112
+ # Remove dangerous builtins
113
+ safe = {k: v for k, v in raw.items() if k not in _REMOVED_BUILTINS}
114
+
115
+ # Add our safe import hook
116
+ safe["__import__"] = _safe_import
117
+
118
+ return safe
119
+
120
+
121
+ # Build once at module load
122
+ SAFE_BUILTINS = _build_safe_builtins()
123
+
124
+
125
+ def handle_execute_code(params: dict) -> dict:
126
+ """Run Python inside Blender, if the user has allowed it this session.
127
+
128
+ - Refuses with PermissionError while "Allow raw Python" is unticked.
129
+ - Echoes the snippet to the system console (stderr) and records it
130
+ in the exec log before running it.
131
+ - Runs in the restricted namespace (see module docstring for what
132
+ that does and does not mean) and captures stdout.
133
+ """
134
+ code = params.get("code", "")
135
+ if not code:
136
+ raise ValueError("No code provided")
137
+
138
+ if not is_enabled():
139
+ raise PermissionError(
140
+ "execute_blender_code is disabled. Raw Python is off by default; "
141
+ "tick 'Allow raw Python' in the blenderwright panel "
142
+ "(View3D > Sidebar > blenderwright) to enable it for this Blender "
143
+ "session, or use the structured tools instead."
144
+ )
145
+
146
+ # Echo to the real console so the user can see what ran, regardless
147
+ # of whether the MCP client shows tool inputs. Goes to stderr so it
148
+ # is never mixed into the captured stdout returned to the model.
149
+ print(
150
+ f"[blenderwright] execute_code ({time.strftime('%H:%M:%S')}):\n{code}",
151
+ file=sys.stderr,
152
+ )
153
+
154
+ # Capture stdout
155
+ old_stdout = sys.stdout
156
+ sys.stdout = buffer = io.StringIO()
157
+
158
+ try:
159
+ exec(code, {"__builtins__": SAFE_BUILTINS})
160
+ output = buffer.getvalue()
161
+ _record(code, ok=True)
162
+ return {
163
+ "output": output.strip(),
164
+ "success": True,
165
+ }
166
+ except Exception as e:
167
+ output = buffer.getvalue()
168
+ _record(code, ok=False)
169
+ raise RuntimeError(f"{type(e).__name__}: {e}\nOutput before error: {output}")
170
+ finally:
171
+ sys.stdout = old_stdout
172
+
173
+
174
+ def register():
175
+ """Register code execution handler with the dispatcher."""
176
+ dispatcher.register_handler("execute_code", handle_execute_code)
@@ -3,6 +3,7 @@
3
3
  import bpy
4
4
 
5
5
  from . import server as addon_server
6
+ from .handlers import code_exec
6
7
 
7
8
 
8
9
  class BLENDERWRIGHT_PT_MainPanel(bpy.types.Panel):
@@ -26,6 +27,23 @@ class BLENDERWRIGHT_PT_MainPanel(bpy.types.Panel):
26
27
  layout.prop(context.scene, "blenderwright_port", text="Port")
27
28
  layout.operator("blenderwright.start_server", text="Start Server", icon="PLAY")
28
29
 
30
+ # Raw Python switch: per session, never saved into the .blend.
31
+ box = layout.box()
32
+ wm = context.window_manager
33
+ box.prop(wm, code_exec.SWITCH_PROP, text="Allow raw Python")
34
+ if getattr(wm, code_exec.SWITCH_PROP, False):
35
+ box.label(text="Model may run Python in this session", icon="ERROR")
36
+ log = code_exec.get_exec_log()
37
+ if log:
38
+ last = log[-1]
39
+ status = "ok" if last["ok"] else "failed"
40
+ box.label(text=f"{len(log)} run(s), last {last['time']} ({status}):")
41
+ box.label(text=f" {last['first_line'][:48]}")
42
+ else:
43
+ box.label(text="No runs yet this session")
44
+ else:
45
+ box.label(text="execute_blender_code is off", icon="LOCKED")
46
+
29
47
 
30
48
  class BLENDERWRIGHT_OT_StartServer(bpy.types.Operator):
31
49
  """Start the blenderwright MCP server"""
@@ -69,10 +87,26 @@ def register():
69
87
  max=65535,
70
88
  )
71
89
 
90
+ # WindowManager, not Scene: WindowManager properties are not written
91
+ # into the .blend, so the switch cannot travel with a file and is
92
+ # always off when Blender starts.
93
+ setattr(bpy.types.WindowManager, code_exec.SWITCH_PROP, bpy.props.BoolProperty(
94
+ name="Allow raw Python",
95
+ description=(
96
+ "Let execute_blender_code run model-written Python in this Blender "
97
+ "session. Off by default. The restricted namespace blocks obvious "
98
+ "mistakes but is not a security boundary: bpy itself can write files "
99
+ "and run any operator. Every run is echoed to the system console"
100
+ ),
101
+ default=False,
102
+ ))
103
+
72
104
 
73
105
  def unregister():
74
106
  if hasattr(bpy.types.Scene, "blenderwright_port"):
75
107
  del bpy.types.Scene.blenderwright_port
108
+ if hasattr(bpy.types.WindowManager, code_exec.SWITCH_PROP):
109
+ delattr(bpy.types.WindowManager, code_exec.SWITCH_PROP)
76
110
 
77
111
  for cls in reversed(classes):
78
112
  bpy.utils.unregister_class(cls)
@@ -261,15 +261,31 @@ boolean modifier (a warning will be included in the response).</p><table class="
261
261
  <tr><th>location</th><td>XYZ location to point at as [x, y, z]. Mutually exclusive with target.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article><article class="tool" id="set_active_camera" data-search="set_active_camera set the active scene camera."><h3><a href="#set_active_camera">set_active_camera</a></h3><pre class="sig"><code>set_active_camera(object_name: str)</code></pre><p>Set the active scene camera.</p><table class="args"><tbody><tr><th>object_name</th><td>Name of the camera object to make active.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article><article class="tool" id="set_camera_from_view" data-search="set_camera_from_view match the active camera to the current 3d viewport view."><h3><a href="#set_camera_from_view">set_camera_from_view</a></h3><pre class="sig"><code>set_camera_from_view()</code></pre><p>Match the active camera to the current 3D viewport view.</p><p class="ret"><span>Returns</span> Confirmation dict with the camera&#x27;s new location and rotation.</p></article><article class="tool" id="set_camera_property" data-search="set_camera_property set a property on a camera."><h3><a href="#set_camera_property">set_camera_property</a></h3><pre class="sig"><code>set_camera_property(object_name: str, property: str, value: Any)</code></pre><p>Set a property on a camera.</p><table class="args"><tbody><tr><th>object_name</th><td>Name of the camera object.</td></tr>
262
262
  <tr><th>property</th><td>Property to set. One of: lens, clip_start, clip_end, sensor_width, sensor_height, dof.use_dof, dof.focus_distance, dof.aperture_fstop, ortho_scale, shift_x, shift_y, type, sensor_fit.</td></tr>
263
263
  <tr><th>value</th><td>The value to set.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article></section>
264
- <section class="module" id="code_exec" data-module="code execution"><h2>Code Execution <span class="n">1</span></h2><article class="tool" id="execute_blender_code" data-search="execute_blender_code execute python code inside blender&#x27;s sandboxed environment.
265
-
266
- the code runs in a restricted sandbox that blocks dangerous imports
267
- (os, subprocess, socket, etc.) and dangerous builtins (exec, eval, open).
268
- safe blender imports (bpy, bmesh, mathutils, math, json) are allowed."><h3><a href="#execute_blender_code">execute_blender_code</a></h3><pre class="sig"><code>execute_blender_code(code: str)</code></pre><p>Execute Python code inside Blender&#x27;s sandboxed environment.
269
-
270
- The code runs in a restricted sandbox that blocks dangerous imports
271
- (os, subprocess, socket, etc.) and dangerous builtins (exec, eval, open).
272
- Safe Blender imports (bpy, bmesh, mathutils, math, json) are allowed.</p><table class="args"><tbody><tr><th>code</th><td>Python code to execute. bpy is available but must be imported.</td></tr></tbody></table><p class="ret"><span>Returns</span> Dict with &#x27;output&#x27; (captured stdout) and &#x27;success&#x27; boolean.</p></article></section>
264
+ <section class="module" id="code_exec" data-module="code execution"><h2>Code Execution <span class="n">1</span></h2><article class="tool" id="execute_blender_code" data-search="execute_blender_code run python inside blender. off by default; prefer the structured tools.
265
+
266
+ the user must tick &quot;allow raw python&quot; in the blenderwright n-panel
267
+ (view3d &gt; sidebar &gt; blenderwright) for the current blender session.
268
+ until they do, this tool returns an error saying so; relay that to the
269
+ user and do not retry. never ask the user to enable it just to do
270
+ something a structured tool already covers.
271
+
272
+ the code runs in a restricted namespace that blocks obvious mistakes
273
+ (import os/subprocess/socket, exec/eval/open/compile). that is a guard
274
+ against accidents, not a security boundary: the code runs in-process
275
+ with full bpy. every snippet is echoed to blender&#x27;s system console and
276
+ listed in the panel so the user can see what ran."><h3><a href="#execute_blender_code">execute_blender_code</a></h3><pre class="sig"><code>execute_blender_code(code: str)</code></pre><p>Run Python inside Blender. OFF BY DEFAULT; prefer the structured tools.
277
+
278
+ The user must tick &quot;Allow raw Python&quot; in the blenderwright N-panel
279
+ (View3D &gt; Sidebar &gt; blenderwright) for the current Blender session.
280
+ Until they do, this tool returns an error saying so; relay that to the
281
+ user and do not retry. Never ask the user to enable it just to do
282
+ something a structured tool already covers.
283
+
284
+ The code runs in a restricted namespace that blocks obvious mistakes
285
+ (import os/subprocess/socket, exec/eval/open/compile). That is a guard
286
+ against accidents, not a security boundary: the code runs in-process
287
+ with full bpy. Every snippet is echoed to Blender&#x27;s system console and
288
+ listed in the panel so the user can see what ran.</p><table class="args"><tbody><tr><th>code</th><td>Python code to execute. bpy is available but must be imported.</td></tr></tbody></table><p class="ret"><span>Returns</span> Dict with &#x27;output&#x27; (captured stdout) and &#x27;success&#x27; boolean.</p></article></section>
273
289
  <section class="module" id="collections" data-module="collections"><h2>Collections <span class="n">4</span></h2><article class="tool" id="create_collection" data-search="create_collection create a new collection, optionally nested under a parent collection."><h3><a href="#create_collection">create_collection</a></h3><pre class="sig"><code>create_collection(name: str, parent: str = &#x27;&#x27;)</code></pre><p>Create a new collection, optionally nested under a parent collection.</p><table class="args"><tbody><tr><th>name</th><td>Name for the new collection.</td></tr>
274
290
  <tr><th>parent</th><td>Optional name of parent collection to nest under. Empty string uses the scene&#x27;s root collection.</td></tr></tbody></table><p class="ret"><span>Returns</span> Dict with the created collection&#x27;s name and parent.</p></article><article class="tool" id="delete_collection" data-search="delete_collection delete a collection."><h3><a href="#delete_collection">delete_collection</a></h3><pre class="sig"><code>delete_collection(collection_name: str, delete_objects: bool = False)</code></pre><p>Delete a collection.</p><table class="args"><tbody><tr><th>collection_name</th><td>Name of the collection to delete.</td></tr>
275
291
  <tr><th>delete_objects</th><td>If True, also delete all objects in the collection. If False, objects are unlinked but kept in the scene.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article><article class="tool" id="move_to_collection" data-search="move_to_collection move objects to a collection, unlinking them from their current collections."><h3><a href="#move_to_collection">move_to_collection</a></h3><pre class="sig"><code>move_to_collection(object_names: list[str], collection_name: str)</code></pre><p>Move objects to a collection, unlinking them from their current collections.</p><table class="args"><tbody><tr><th>object_names</th><td>List of object names to move.</td></tr>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "blenderwright"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "The most intuitive and efficient MCP Server for Blender"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,39 @@
1
+ """MCP tool for running raw Python inside Blender. Off by default."""
2
+
3
+ from typing import Any
4
+
5
+ from blenderwright.server import mcp, get_connection
6
+
7
+
8
+ @mcp.tool()
9
+ def execute_blender_code(
10
+ code: str,
11
+ ) -> dict[str, Any]:
12
+ """Run Python inside Blender. OFF BY DEFAULT; prefer the structured tools.
13
+
14
+ The user must tick "Allow raw Python" in the blenderwright N-panel
15
+ (View3D > Sidebar > blenderwright) for the current Blender session.
16
+ Until they do, this tool returns an error saying so; relay that to the
17
+ user and do not retry. Never ask the user to enable it just to do
18
+ something a structured tool already covers.
19
+
20
+ The code runs in a restricted namespace that blocks obvious mistakes
21
+ (import os/subprocess/socket, exec/eval/open/compile). That is a guard
22
+ against accidents, not a security boundary: the code runs in-process
23
+ with full bpy. Every snippet is echoed to Blender's system console and
24
+ listed in the panel so the user can see what ran.
25
+
26
+ Args:
27
+ code: Python code to execute. bpy is available but must be imported.
28
+
29
+ Returns:
30
+ Dict with 'output' (captured stdout) and 'success' boolean.
31
+ """
32
+ if not code or not code.strip():
33
+ raise ValueError("No code provided")
34
+
35
+ conn = get_connection()
36
+ response = conn.send_command("execute_code", {"code": code})
37
+ if response.get("status") == "error":
38
+ raise RuntimeError(f"Blender error: {response.get('result')}")
39
+ return response.get("result")
@@ -35,6 +35,20 @@ def code_exec_handler():
35
35
  return mod
36
36
 
37
37
 
38
+ @pytest.fixture(autouse=True)
39
+ def raw_python_enabled(code_exec_handler):
40
+ """Turn the per-session switch on for the sandbox tests below.
41
+
42
+ The switch lives on bpy.types.WindowManager so it is never saved into a
43
+ .blend. The mock bpy would otherwise return a truthy MagicMock, so the
44
+ handler must only honour a real bool True; set it explicitly here.
45
+ """
46
+ import bpy
47
+ bpy.context.window_manager.blenderwright_allow_code_exec = True
48
+ yield
49
+ bpy.context.window_manager.blenderwright_allow_code_exec = False
50
+
51
+
38
52
  class TestSandboxBlocksDangerousImports:
39
53
  """Verify that dangerous modules are blocked."""
40
54
 
@@ -187,3 +201,88 @@ class TestSandboxConstants:
187
201
  def test_blocked_modules_is_set(self, code_exec_handler):
188
202
  """BLOCKED_MODULES is a set (for O(1) lookup)."""
189
203
  assert isinstance(code_exec_handler.BLOCKED_MODULES, (set, frozenset))
204
+
205
+
206
+ class TestRawPythonIsOffByDefault:
207
+ """execute_code refuses unless the user ticked the box this session."""
208
+
209
+ def test_disabled_when_switch_is_false(self, code_exec_handler):
210
+ import bpy
211
+ bpy.context.window_manager.blenderwright_allow_code_exec = False
212
+ with pytest.raises(PermissionError, match="(?i)disabled|Allow raw Python"):
213
+ code_exec_handler.handle_execute_code({"code": "print('x')"})
214
+
215
+ def test_disabled_when_switch_is_missing(self, code_exec_handler):
216
+ """No property registered (or a non-bool stand-in) means off."""
217
+ import bpy
218
+ bpy.context.window_manager.blenderwright_allow_code_exec = MagicMock()
219
+ with pytest.raises(PermissionError):
220
+ code_exec_handler.handle_execute_code({"code": "print('x')"})
221
+
222
+ def test_disabled_when_bpy_context_raises(self, code_exec_handler, monkeypatch):
223
+ """Any failure reading the switch is treated as off, never on."""
224
+
225
+ class _NoContext:
226
+ @property
227
+ def context(self):
228
+ raise AttributeError("no context yet")
229
+
230
+ monkeypatch.setitem(sys.modules, "bpy", _NoContext())
231
+ assert code_exec_handler.is_enabled() is False
232
+
233
+ def test_refusal_does_not_run_code(self, code_exec_handler):
234
+ import bpy
235
+ bpy.context.window_manager.blenderwright_allow_code_exec = False
236
+ code_exec_handler._exec_log.clear()
237
+ before = 0
238
+ with pytest.raises(PermissionError):
239
+ code_exec_handler.handle_execute_code({"code": "print('never')"})
240
+ assert len(code_exec_handler.get_exec_log()) == before
241
+
242
+ def test_refusal_message_names_the_panel(self, code_exec_handler):
243
+ import bpy
244
+ bpy.context.window_manager.blenderwright_allow_code_exec = False
245
+ with pytest.raises(PermissionError, match="blenderwright"):
246
+ code_exec_handler.handle_execute_code({"code": "print('x')"})
247
+
248
+ def test_enabled_runs(self, code_exec_handler):
249
+ result = code_exec_handler.handle_execute_code({"code": "print('on')"})
250
+ assert result["success"] is True
251
+ assert "on" in result["output"]
252
+
253
+
254
+ class TestExecLog:
255
+ """Every run is recorded so the user can see what the model did."""
256
+
257
+ def test_records_each_run(self, code_exec_handler):
258
+ code_exec_handler._exec_log.clear()
259
+ code_exec_handler.handle_execute_code({"code": "print('a')\nprint('b')"})
260
+ log = code_exec_handler.get_exec_log()
261
+ assert len(log) == 1
262
+ entry = log[-1]
263
+ assert entry["first_line"] == "print('a')"
264
+ assert entry["lines"] == 2
265
+ assert entry["ok"] is True
266
+ assert "time" in entry
267
+
268
+ def test_records_failed_run(self, code_exec_handler):
269
+ with pytest.raises(RuntimeError):
270
+ code_exec_handler.handle_execute_code({"code": "raise ValueError('x')"})
271
+ assert code_exec_handler.get_exec_log()[-1]["ok"] is False
272
+
273
+ def test_log_is_capped(self, code_exec_handler):
274
+ for i in range(code_exec_handler.EXEC_LOG_SIZE + 5):
275
+ code_exec_handler.handle_execute_code({"code": f"x = {i}"})
276
+ assert len(code_exec_handler.get_exec_log()) == code_exec_handler.EXEC_LOG_SIZE
277
+
278
+ def test_echoes_code_to_console(self, code_exec_handler, capsys):
279
+ """The snippet is printed to the real stdout, not the captured buffer."""
280
+ code_exec_handler.handle_execute_code({"code": "print('seen')"})
281
+ err = capsys.readouterr().err
282
+ assert "[blenderwright] execute_code" in err
283
+ assert "print('seen')" in err
284
+
285
+ def test_get_exec_log_returns_copy(self, code_exec_handler):
286
+ log = code_exec_handler.get_exec_log()
287
+ log.clear()
288
+ assert isinstance(code_exec_handler.get_exec_log(), list)
@@ -58,3 +58,23 @@ class TestCustomPort:
58
58
  """unregister() removes blenderwright_port from Scene."""
59
59
  # Should delete the property during unregister
60
60
  assert "del" in ui_source and "blenderwright_port" in ui_source
61
+
62
+
63
+ class TestRawPythonSwitch:
64
+ def _switch_line(self, ui_source):
65
+ lines = [ln for ln in ui_source.splitlines() if "SWITCH_PROP" in ln and ".prop(" in ln]
66
+ assert len(lines) == 1, "expected exactly one prop() call for the raw Python switch"
67
+ return lines[0]
68
+
69
+ def test_switch_is_a_checkbox_not_a_toggle_button(self, ui_source):
70
+ """A BoolProperty drawn with icon= becomes a toggle button that reads as
71
+ a one-shot 'Allow' action. No icon, so it draws as a checkbox."""
72
+ assert "icon=" not in self._switch_line(ui_source)
73
+ assert "toggle=" not in self._switch_line(ui_source)
74
+
75
+ def test_switch_lives_on_window_manager(self, ui_source):
76
+ assert "context.window_manager" in ui_source
77
+ assert "WindowManager, code_exec.SWITCH_PROP" in ui_source
78
+
79
+ def test_panel_states_off_when_unticked(self, ui_source):
80
+ assert "execute_blender_code is off" in ui_source
@@ -53,3 +53,19 @@ class TestExecuteBlenderCode:
53
53
 
54
54
  with pytest.raises(RuntimeError, match="Blender error"):
55
55
  execute_blender_code(code="print(")
56
+
57
+ def test_disabled_in_blender_raises_with_hint(self, mock_conn):
58
+ """When the user has not enabled raw Python, the error says how to."""
59
+ mock_conn.send_command.return_value = {
60
+ "status": "error",
61
+ "result": "PermissionError: execute_blender_code is disabled. "
62
+ "Tick 'Allow raw Python' in the blenderwright panel.",
63
+ }
64
+ with pytest.raises(RuntimeError, match="Allow raw Python"):
65
+ execute_blender_code(code="print('x')")
66
+
67
+ def test_docstring_says_off_by_default(self):
68
+ """The model must learn from the tool description that it is opt-in."""
69
+ doc = execute_blender_code.__doc__ or ""
70
+ assert "off by default" in doc.lower()
71
+ assert "not a security boundary" in doc.lower()
@@ -51,7 +51,7 @@ wheels = [
51
51
 
52
52
  [[package]]
53
53
  name = "blenderwright"
54
- version = "2.1.0"
54
+ version = "2.2.0"
55
55
  source = { editable = "." }
56
56
  dependencies = [
57
57
  { name = "mcp" },