From 8ef75df9eba70979dee6771170a76328607125d1 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 27 Mar 2026 18:24:12 +0100 Subject: [PATCH] Fix mypy compatibility and add 5.0 conformance tests - Qualify bare "object" as builtins.object in bpy.types to avoid shadowing by Context.object and similar properties - Use _Sequence alias import to fix mypy "collections.abc.Sequence not defined" - Filter out variables that clash with submodule re-exports (e.g. bpy.app) - Add 5.0 conformance tests from Blender 5.0 mathutils documentation Co-Authored-By: Claude Opus 4.6 (1M context) --- conformance/5.0/test_mathutils.py | 18 ++++++ conformance/5.0/test_mathutils_color.py | 37 +++++++++++++ conformance/5.0/test_mathutils_euler.py | 35 ++++++++++++ conformance/5.0/test_mathutils_matrix.py | 35 ++++++++++++ conformance/5.0/test_mathutils_quaternion.py | 40 ++++++++++++++ conformance/5.0/test_mathutils_vector.py | 58 ++++++++++++++++++++ generate_stubs.py | 20 +++++-- 7 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 conformance/5.0/test_mathutils.py create mode 100644 conformance/5.0/test_mathutils_color.py create mode 100644 conformance/5.0/test_mathutils_euler.py create mode 100644 conformance/5.0/test_mathutils_matrix.py create mode 100644 conformance/5.0/test_mathutils_quaternion.py create mode 100644 conformance/5.0/test_mathutils_vector.py diff --git a/conformance/5.0/test_mathutils.py b/conformance/5.0/test_mathutils.py new file mode 100644 index 0000000..3838d69 --- /dev/null +++ b/conformance/5.0/test_mathutils.py @@ -0,0 +1,18 @@ +import mathutils +from math import radians + +vec = mathutils.Vector((1.0, 2.0, 3.0)) + +mat_rot = mathutils.Matrix.Rotation(radians(90.0), 4, "X") +mat_trans = mathutils.Matrix.Translation(vec) + +mat = mat_trans @ mat_rot +mat.invert() + +mat3 = mat.to_3x3() +quat1 = mat.to_quaternion() +quat2 = mat3.to_quaternion() + +quat_diff = quat1.rotation_difference(quat2) + +print(quat_diff.angle) diff --git a/conformance/5.0/test_mathutils_color.py b/conformance/5.0/test_mathutils_color.py new file mode 100644 index 0000000..bd1711f --- /dev/null +++ b/conformance/5.0/test_mathutils_color.py @@ -0,0 +1,37 @@ +import mathutils + +# Color values are represented as RGB values from 0 - 1, this is blue. +col = mathutils.Color((0.0, 0.0, 1.0)) + +# As well as r/g/b attribute access you can adjust them by h/s/v. +col.s *= 0.5 + +# You can access its components by attribute or index. +print("Color R:", col.r) +print("Color G:", col[1]) +print("Color B:", col[-1]) +print("Color HSV: {:.2f}, {:.2f}, {:.2f}".format(*col)) + + +# Components of an existing color can be set. +col[:] = 0.0, 0.5, 1.0 + +# Components of an existing color can use slice notation to get a tuple. +print("Values: {:f}, {:f}, {:f}".format(*col)) + +# Colors can be added and subtracted. +col += mathutils.Color((0.25, 0.0, 0.0)) + +# Color can be multiplied, in this example color is scaled to 0-255 +# can printed as integers. +print("Color: {:d}, {:d}, {:d}".format(*(int(c) for c in (col * 255.0)))) + +# This example prints the color as hexadecimal. +print( + "Hexadecimal: {:02x}{:02x}{:02x}".format( + int(col.r * 255), int(col.g * 255), int(col.b * 255) + ) +) + +# Direct buffer access is supported at runtime via C buffer protocol +# but not expressible in type stubs (requires Python 3.12+ __buffer__). diff --git a/conformance/5.0/test_mathutils_euler.py b/conformance/5.0/test_mathutils_euler.py new file mode 100644 index 0000000..7146ca7 --- /dev/null +++ b/conformance/5.0/test_mathutils_euler.py @@ -0,0 +1,35 @@ +import mathutils +import math + +# Create a new euler with default axis rotation order. +eul = mathutils.Euler((0.0, math.radians(45.0), 0.0), "XYZ") + +# Rotate the euler. +eul.rotate_axis("Z", math.radians(10.0)) + +# You can access its components by attribute or index. +print("Euler X", eul.x) +print("Euler Y", eul[1]) +print("Euler Z", eul[-1]) + +# Components of an existing euler can be set. +eul[:] = 1.0, 2.0, 3.0 + +# Components of an existing euler can use slice notation to get a tuple. +print("Values: {:f}, {:f}, {:f}".format(*eul)) + +# The order can be set at any time too. +eul.order = "ZYX" + +# Eulers can be used to rotate vectors. +vec = mathutils.Vector((0.0, 0.0, 1.0)) +vec.rotate(eul) + +# Often its useful to convert the euler into a matrix so it can be used as +# transformations with more flexibility. +mat_rot = eul.to_matrix() +mat_loc = mathutils.Matrix.Translation((2.0, 3.0, 4.0)) +mat = mat_loc @ mat_rot.to_4x4() + +# Direct buffer access is supported at runtime via C buffer protocol +# but not expressible in type stubs (requires Python 3.12+ __buffer__). diff --git a/conformance/5.0/test_mathutils_matrix.py b/conformance/5.0/test_mathutils_matrix.py new file mode 100644 index 0000000..9e90814 --- /dev/null +++ b/conformance/5.0/test_mathutils_matrix.py @@ -0,0 +1,35 @@ +import mathutils +import math + +# Create a location matrix. +mat_loc = mathutils.Matrix.Translation((2.0, 3.0, 4.0)) + +# Create an identity matrix. +mat_sca = mathutils.Matrix.Scale(0.5, 4, (0.0, 0.0, 1.0)) + +# Create a rotation matrix. +mat_rot = mathutils.Matrix.Rotation(math.radians(45.0), 4, "X") + +# Combine transformations. +mat_out = mat_loc @ mat_rot @ mat_sca +print(mat_out) + +# Extract components back out of the matrix as two vectors and a quaternion. +loc, rot, sca = mat_out.decompose() +print(loc, rot, sca) + +# Recombine extracted components. +mat_out2 = mathutils.Matrix.LocRotScale(loc, rot, sca) +print(mat_out2) + +# It can also be useful to access components of a matrix directly. +mat = mathutils.Matrix() +mat[0][0], mat[1][0], mat[2][0] = 0.0, 1.0, 2.0 + +mat[0][0:3] = 0.0, 1.0, 2.0 + +# Each item in a matrix is a vector so vector utility functions can be used. +mat[0].xyz = 0.0, 1.0, 2.0 + +# Direct buffer access is supported at runtime via C buffer protocol +# but not expressible in type stubs (requires Python 3.12+ __buffer__). diff --git a/conformance/5.0/test_mathutils_quaternion.py b/conformance/5.0/test_mathutils_quaternion.py new file mode 100644 index 0000000..965ef80 --- /dev/null +++ b/conformance/5.0/test_mathutils_quaternion.py @@ -0,0 +1,40 @@ +import mathutils +import math + +# A new rotation 90 degrees about the Y axis. +quat_a = mathutils.Quaternion((0.7071068, 0.0, 0.7071068, 0.0)) + +# Passing values to Quaternion's directly can be confusing so axis, angle +# is supported for initializing too. +quat_b = mathutils.Quaternion((0.0, 1.0, 0.0), math.radians(90.0)) + +print("Check quaternions match", quat_a == quat_b) + +# Like matrices, quaternions can be multiplied to accumulate rotational values. +quat_a = mathutils.Quaternion((0.0, 1.0, 0.0), math.radians(90.0)) +quat_b = mathutils.Quaternion((0.0, 0.0, 1.0), math.radians(45.0)) +quat_out = quat_a @ quat_b + +# Print the quaternion, euler degrees for mere mortals and (axis, angle). +print("Final Rotation:") +print(quat_out) +print("{:.2f}, {:.2f}, {:.2f}".format(*(math.degrees(a) for a in quat_out.to_euler()))) +print( + "({:.2f}, {:.2f}, {:.2f}), {:.2f}".format( + *quat_out.axis, math.degrees(quat_out.angle) + ) +) + +# Multiple rotations can be interpolated using the exponential map. +quat_c = mathutils.Quaternion((1.0, 0.0, 0.0), math.radians(15.0)) +exp_avg = ( + quat_a.to_exponential_map() + + quat_b.to_exponential_map() + + quat_c.to_exponential_map() +) / 3.0 +quat_avg = mathutils.Quaternion(exp_avg) +print("Average rotation:") +print(quat_avg) + +# Direct buffer access is supported at runtime via C buffer protocol +# but not expressible in type stubs (requires Python 3.12+ __buffer__). diff --git a/conformance/5.0/test_mathutils_vector.py b/conformance/5.0/test_mathutils_vector.py new file mode 100644 index 0000000..c5f1ddd --- /dev/null +++ b/conformance/5.0/test_mathutils_vector.py @@ -0,0 +1,58 @@ +import mathutils + +# Zero length vector. +vec = mathutils.Vector((0.0, 0.0, 1.0)) + +# Unit length vector. +vec_a = vec.normalized() + +vec_b = mathutils.Vector((0.0, 1.0, 2.0)) + +vec2d = mathutils.Vector((1.0, 2.0)) +vec3d = mathutils.Vector((1.0, 0.0, 0.0)) +vec4d = vec_a.to_4d() + +# Other `mathutils` types. +quat = mathutils.Quaternion() +matrix = mathutils.Matrix() + +# Comparison operators can be done on Vector classes: + +# (In)equality operators == and != test component values, e.g. 1,2,3 != 3,2,1 +vec_a == vec_b +vec_a != vec_b + +# Ordering operators >, >=, > and <= test vector length. +vec_a > vec_b +vec_a >= vec_b +vec_a < vec_b +vec_a <= vec_b + + +# Math can be performed on Vector classes. +vec_a + vec_b +vec_a - vec_b +vec_a @ vec_b +vec_a * 10.0 +matrix @ vec_a +quat @ vec_a +-vec_a + + +# You can access a vector object like a sequence. +x = vec_a[0] +len(vec) +vec_a[:] = vec_b +vec_a[:] = 1.0, 2.0, 3.0 +vec2d[:] = vec3d[:2] + + +# Vectors support 'swizzle' operations. +# See https://en.wikipedia.org/wiki/Swizzling_(computer_graphics) +vec.xyz = vec.zyx +vec.xy = vec4d.zw +vec.xyz = vec4d.wzz +vec4d.wxyz = vec.yxyx + +# Direct buffer access is supported at runtime via C buffer protocol +# but not expressible in type stubs (requires Python 3.12+ __buffer__). diff --git a/generate_stubs.py b/generate_stubs.py index b000ae4..eee389a 100644 --- a/generate_stubs.py +++ b/generate_stubs.py @@ -600,7 +600,9 @@ def generate_types_stub( if "Sequence[" in all_type_strs: # Use fully qualified import to avoid shadowing by bpy.types.Sequence # (the video sequencer strip type) + imports.append("import collections") imports.append("import collections.abc") + imports.append("from collections.abc import Sequence as _Sequence") parts: list[str] = [] if doc: @@ -629,10 +631,14 @@ def generate_types_stub( result = "\n".join(parts) class_names = {s["name"] for s in structs} result = strip_self_module_prefix(result, "bpy.types", class_names) - # Qualify all bare Sequence[ references to avoid shadowing by bpy.types.Sequence - result = re.sub( - r"(? )object\b", "builtins.object", result) return prune_unused_imports(result) @@ -755,11 +761,13 @@ def generate_module_stub( parts.append('_T = TypeVar("_T")') parts.append("") - # Variables + # Variables (skip those that clash with submodule re-exports) + sub_names = set(submodule_names or []) if module_data["variables"]: parts.append("") for var in module_data["variables"]: - parts.append(generate_variable_stub(var)) + if var["name"] not in sub_names: + parts.append(generate_variable_stub(var)) # Functions for func in module_data["functions"]: