tests and agents.md
This commit is contained in:
+111
-4
@@ -2320,14 +2320,22 @@ RNA_TYPE_MAP: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def rna_property_to_type(prop: object) -> str:
|
||||
"""Map an RNA property to a PEP 484 type annotation string."""
|
||||
def rna_property_to_type(prop: object, *, nullable_pointer: bool = True) -> str:
|
||||
"""Map an RNA property to a PEP 484 type annotation string.
|
||||
|
||||
``nullable_pointer`` controls whether pointer types are widened to ``T | None``
|
||||
based on the RNA ``is_never_none`` flag. Set to False for function return
|
||||
values: Blender's RNA marks most function returns ``is_never_none=False``
|
||||
even though they raise on failure rather than returning None.
|
||||
"""
|
||||
prop_type: str = getattr(prop, "type", "")
|
||||
fixed_type: object = getattr(prop, "fixed_type", None)
|
||||
array_length: int = getattr(prop, "array_length", 0)
|
||||
|
||||
if prop_type == "pointer" and fixed_type is not None:
|
||||
type_name: str = getattr(fixed_type, "identifier", "object")
|
||||
if nullable_pointer and not getattr(prop, "is_never_none", False):
|
||||
return f"{type_name} | None"
|
||||
return type_name
|
||||
|
||||
if prop_type == "collection" and fixed_type is not None:
|
||||
@@ -2342,6 +2350,18 @@ def rna_property_to_type(prop: object) -> str:
|
||||
element_type: str = getattr(fixed_type, "identifier", "object")
|
||||
return f"bpy_prop_collection[{element_type}]"
|
||||
|
||||
if prop_type == "enum":
|
||||
if getattr(prop, "is_enum_flag", False):
|
||||
return "set[str]"
|
||||
# rna_info exposes enum_items as list[tuple[identifier, name, description]];
|
||||
# an empty list means a dynamic enum (items computed at runtime).
|
||||
raw_items = cast("list[tuple[str, str, str]]", getattr(prop, "enum_items", []))
|
||||
identifiers = [item[0] for item in raw_items if item]
|
||||
if identifiers:
|
||||
quoted = ", ".join(f'"{v}"' for v in identifiers)
|
||||
return f"Literal[{quoted}]"
|
||||
return "str"
|
||||
|
||||
# Dynamic-length arrays have array_length=0 but is_array=True on the raw
|
||||
# RNA property. rna_info wraps properties in InfoPropertyRNA which stores
|
||||
# the raw prop as bl_prop; fall back to checking the prop itself.
|
||||
@@ -2423,9 +2443,11 @@ def rna_function_to_data(func_info: object) -> FunctionData:
|
||||
return_type: str | None = None
|
||||
if return_values:
|
||||
if len(return_values) == 1:
|
||||
return_type = rna_property_to_type(return_values[0])
|
||||
return_type = rna_property_to_type(return_values[0], nullable_pointer=False)
|
||||
else:
|
||||
types = [rna_property_to_type(rv) for rv in return_values]
|
||||
types = [
|
||||
rna_property_to_type(rv, nullable_pointer=False) for rv in return_values
|
||||
]
|
||||
return_type = f"tuple[{', '.join(types)}]"
|
||||
|
||||
return {
|
||||
@@ -2694,12 +2716,97 @@ def _rna_struct_to_data(
|
||||
}
|
||||
|
||||
|
||||
_NULLABLE_DOC_HINTS = ("may be none", "can be none", "may be null", "can be null")
|
||||
|
||||
|
||||
def description_hints_nullable(description: str) -> bool:
|
||||
"""Return True if the RST description explicitly hints the value may be None."""
|
||||
lowered = description.lower()
|
||||
return any(hint in lowered for hint in _NULLABLE_DOC_HINTS)
|
||||
|
||||
|
||||
def _find_view3d_override(ctx: object) -> dict[str, object]:
|
||||
"""Find a (window, area, region) override that simulates a 3D viewport.
|
||||
|
||||
Returns kwargs for ``bpy.context.temp_override``. Empty dict if no 3D
|
||||
viewport is reachable (degenerate startup state).
|
||||
"""
|
||||
wm = getattr(ctx, "window_manager", None)
|
||||
windows = getattr(wm, "windows", None) if wm is not None else None
|
||||
if not windows:
|
||||
return {}
|
||||
for window in windows:
|
||||
screen = getattr(window, "screen", None)
|
||||
areas = getattr(screen, "areas", None) if screen is not None else None
|
||||
if not areas:
|
||||
continue
|
||||
for area in areas:
|
||||
if getattr(area, "type", None) != "VIEW_3D":
|
||||
continue
|
||||
for region in getattr(area, "regions", []) or []:
|
||||
if getattr(region, "type", None) == "WINDOW":
|
||||
return {"window": window, "area": area, "region": region}
|
||||
return {}
|
||||
|
||||
|
||||
def _refine_context_nullability(struct: StructData) -> None:
|
||||
"""Strip ``| None`` from Context pointer properties that are never None.
|
||||
|
||||
Blender's RNA reports ``is_never_none=False`` for most ``bpy.context.*``
|
||||
pointers, but several (``scene``, ``view_layer``, ``window_manager`` …,
|
||||
plus UI-structural ones like ``area``, ``region``, ``region_data``) are
|
||||
populated whenever a script runs from a normal 3D viewport — which is
|
||||
what Blender's documentation assumes. In ``--background`` mode the
|
||||
viewport context is empty, so we probe inside a ``temp_override`` that
|
||||
simulates one.
|
||||
|
||||
Members that stay None even inside the override (``gizmo_group``,
|
||||
``asset``, ``region_popup``) are correctly UI-event-dependent and stay
|
||||
nullable.
|
||||
|
||||
The probe is overridden by Blender's own RST documentation: if the
|
||||
description explicitly says "may be None" (or similar), the property
|
||||
stays nullable even when the probe found a value.
|
||||
"""
|
||||
bpy = importlib.import_module("bpy")
|
||||
ctx = getattr(bpy, "context", None)
|
||||
if ctx is None:
|
||||
return
|
||||
override = _find_view3d_override(ctx)
|
||||
temp_override = getattr(ctx, "temp_override", None)
|
||||
if not override or temp_override is None:
|
||||
# Fall back to a plain probe — better than nothing on older Blenders
|
||||
# or in headless setups without windows.
|
||||
probe_ctx: object = ctx
|
||||
cm = None
|
||||
else:
|
||||
cm = temp_override(**override)
|
||||
cm.__enter__()
|
||||
probe_ctx = bpy.context # ``bpy.context`` reflects the override
|
||||
try:
|
||||
for prop in struct["properties"]:
|
||||
if not prop["type"].endswith(" | None"):
|
||||
continue
|
||||
if description_hints_nullable(prop["description"]):
|
||||
continue
|
||||
try:
|
||||
value = getattr(probe_ctx, prop["name"])
|
||||
except Exception:
|
||||
continue
|
||||
if value is not None:
|
||||
prop["type"] = prop["type"].removesuffix(" | None")
|
||||
finally:
|
||||
if cm is not None:
|
||||
cm.__exit__(None, None, None)
|
||||
|
||||
|
||||
def _merge_screen_context_members(structs: list[StructData]) -> None:
|
||||
"""Merge dynamic bpy.context members into the Context struct."""
|
||||
known_types = {s["name"] for s in structs}
|
||||
for struct in structs:
|
||||
if struct["name"] != "Context":
|
||||
continue
|
||||
_refine_context_nullability(struct)
|
||||
rna_names = {p["name"] for p in struct["properties"]}
|
||||
rna_names |= {m["name"] for m in struct["methods"]}
|
||||
screen_props = introspect_screen_context_members(rna_names)
|
||||
|
||||
Reference in New Issue
Block a user