tests and agents.md
This commit is contained in:
parent
c46b37f88e
commit
f575a84c13
45
AGENTS.md
Normal file
45
AGENTS.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# blender-python-stubs — AI agent instructions
|
||||||
|
|
||||||
|
These instructions apply to any AI coding agent working in this repository.
|
||||||
|
|
||||||
|
Type stubs generator for the Blender Python API (`bpy`, `mathutils`, `bmesh`, `gpu`, etc.). Introspects Blender's embedded Python in `--background` mode. Supported: Blender 4.0–5.1.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `uv run poe generate <version>` — generate stubs (e.g., `5.0`)
|
||||||
|
- `uv run poe typecheck-stubs <version>` — type-check generated stubs
|
||||||
|
- `uv run poe conformance <version>` — run conformance tests
|
||||||
|
- `uv run poe check` — format, lint, typecheck, test
|
||||||
|
- `uv run poe test` / `format` / `lint` — individual steps
|
||||||
|
- Run a script inside Blender: `downloads/blender-<version>-linux-x64/blender --background --python script.py`
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
- Format with **black** and lint with **ruff** after editing
|
||||||
|
- **basedpyright strict**, 0 errors is the target
|
||||||
|
- Never use `typing.Any` — use concrete types, TypedDicts, Protocols, or unions
|
||||||
|
- Use raw dict literals for TypedDicts (`{"name": ..., "type": ...}`, not `ParamData(name=..., type=...)`)
|
||||||
|
|
||||||
|
## Never bypass typing issues
|
||||||
|
|
||||||
|
Fix root causes — no `# type: ignore`, no pyright excludes/overrides, no placeholder stubs, no shims.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Add tests for every behavior/regex change — at least two examples per change
|
||||||
|
- Tests live in `tests/`, run via `python -m unittest discover -s tests -v`
|
||||||
|
|
||||||
|
## Conformance tests
|
||||||
|
|
||||||
|
- Never modify files in `conformance/<version>/` — copied verbatim from Blender docs
|
||||||
|
- On failure, fix stub generation or `overrides/<version>/<module>.json`, not the test
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- Prefer **introspection** over hardcoding — if runtime can discover it, don't hardcode
|
||||||
|
- Key files: `introspect.py` (collects data inside Blender), `generate_stubs.py` (emits stubs), `main.py` (CLI)
|
||||||
|
- `introspect.py` must work on every Blender Python (4.0 ships Python 3.10) — no newer-version syntax
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- Always add specific files by name — **never `git add -A` or `git add .`**
|
||||||
59
CLAUDE.md
59
CLAUDE.md
@ -1,59 +0,0 @@
|
|||||||
# CLAUDE.md - Project Guidelines
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Type stubs generator for the Blender Python API (`bpy`, `mathutils`, `bmesh`, `gpu`, etc.). Stubs are generated by running introspection inside Blender's embedded Python interpreter in `--background` mode. Supported Blender versions: **4.0 through 5.1** (8 versions total).
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
- `uv run poe generate <version>` — Generate stubs for a Blender version (e.g., 5.0)
|
|
||||||
- `uv run poe typecheck-stubs <version>` — Type-check generated stubs
|
|
||||||
- `uv run poe conformance <version>` — Run conformance tests against stubs
|
|
||||||
- `uv run poe check` — Run all checks (format, lint, typecheck, test)
|
|
||||||
- `uv run poe test` — Run unit tests
|
|
||||||
- `uv run poe format` — Format with black
|
|
||||||
- `uv run poe lint` — Lint with ruff
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
- Format all Python files with **black** after modifying them
|
|
||||||
- Use **basedpyright strict** mode — 0 errors is the target
|
|
||||||
- Never use `typing.Any` — use concrete types, TypedDicts, Protocols, or unions instead
|
|
||||||
- Use raw dict literals for TypedDicts, not explicit constructors (e.g., `{"name": ..., "type": ...}` not `ParamData(name=..., type=...)`)
|
|
||||||
|
|
||||||
## Type Checking Rules
|
|
||||||
|
|
||||||
- **Never use `# type: ignore`** — fix the actual typing issue instead
|
|
||||||
- **Never suppress typecheck rules** with excludes or pyright config overrides — fix the root cause
|
|
||||||
- **Never bypass issues with workarounds** — always investigate and fix root causes properly (no empty placeholder stubs, no shims)
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- **Add tests for every behavior/regex change** — at least two examples per change
|
|
||||||
- Test files live in `tests/` and run via `python -m unittest discover -s tests -v`
|
|
||||||
|
|
||||||
## Conformance Tests
|
|
||||||
|
|
||||||
- **Never modify conformance test files** — they are copied verbatim from Blender documentation
|
|
||||||
- When a conformance test fails, fix the stub generation or overrides, not the test
|
|
||||||
- Conformance tests live in `conformance/<version>/`
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- Stubs are versioned per Blender version: `dist/<version>/` (e.g., `dist/5.0/`)
|
|
||||||
- Type overrides are organized per version: `overrides/<version>/<module>.json`
|
|
||||||
- Always prefer **introspection** over hardcoding — if something can be discovered at runtime, don't hardcode it
|
|
||||||
- Key files: `introspect.py` (data collection), `generate_stubs.py` (stub output), `main.py` (CLI)
|
|
||||||
- `introspect.py` runs inside Blender's embedded Python interpreter, which varies by version (e.g., Blender 4.0 uses Python 3.10). Code in this file must be compatible with all supported Blender Python versions.
|
|
||||||
|
|
||||||
## Blender Binaries
|
|
||||||
|
|
||||||
Cached Blender executables are in `downloads/` (e.g., `downloads/blender-5.1.0-linux-x64/blender`). You can run scripts inside Blender with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
downloads/blender-5.1.0-linux-x64/blender --background --python script.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Git
|
|
||||||
|
|
||||||
- Always add specific files by name — **never use `git add -A` or `git add .`**
|
|
||||||
115
introspect.py
115
introspect.py
@ -2320,14 +2320,22 @@ RNA_TYPE_MAP: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def rna_property_to_type(prop: object) -> str:
|
def rna_property_to_type(prop: object, *, nullable_pointer: bool = True) -> str:
|
||||||
"""Map an RNA property to a PEP 484 type annotation string."""
|
"""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", "")
|
prop_type: str = getattr(prop, "type", "")
|
||||||
fixed_type: object = getattr(prop, "fixed_type", None)
|
fixed_type: object = getattr(prop, "fixed_type", None)
|
||||||
array_length: int = getattr(prop, "array_length", 0)
|
array_length: int = getattr(prop, "array_length", 0)
|
||||||
|
|
||||||
if prop_type == "pointer" and fixed_type is not None:
|
if prop_type == "pointer" and fixed_type is not None:
|
||||||
type_name: str = getattr(fixed_type, "identifier", "object")
|
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
|
return type_name
|
||||||
|
|
||||||
if prop_type == "collection" and fixed_type is not None:
|
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")
|
element_type: str = getattr(fixed_type, "identifier", "object")
|
||||||
return f"bpy_prop_collection[{element_type}]"
|
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
|
# Dynamic-length arrays have array_length=0 but is_array=True on the raw
|
||||||
# RNA property. rna_info wraps properties in InfoPropertyRNA which stores
|
# RNA property. rna_info wraps properties in InfoPropertyRNA which stores
|
||||||
# the raw prop as bl_prop; fall back to checking the prop itself.
|
# 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
|
return_type: str | None = None
|
||||||
if return_values:
|
if return_values:
|
||||||
if len(return_values) == 1:
|
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:
|
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_type = f"tuple[{', '.join(types)}]"
|
||||||
|
|
||||||
return {
|
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:
|
def _merge_screen_context_members(structs: list[StructData]) -> None:
|
||||||
"""Merge dynamic bpy.context members into the Context struct."""
|
"""Merge dynamic bpy.context members into the Context struct."""
|
||||||
known_types = {s["name"] for s in structs}
|
known_types = {s["name"] for s in structs}
|
||||||
for struct in structs:
|
for struct in structs:
|
||||||
if struct["name"] != "Context":
|
if struct["name"] != "Context":
|
||||||
continue
|
continue
|
||||||
|
_refine_context_nullability(struct)
|
||||||
rna_names = {p["name"] for p in struct["properties"]}
|
rna_names = {p["name"] for p in struct["properties"]}
|
||||||
rna_names |= {m["name"] for m in struct["methods"]}
|
rna_names |= {m["name"] for m in struct["methods"]}
|
||||||
screen_props = introspect_screen_context_members(rna_names)
|
screen_props = introspect_screen_context_members(rna_names)
|
||||||
|
|||||||
28
publish.py
28
publish.py
@ -5,7 +5,9 @@ asks for confirmation, then builds and uploads.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import configparser
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tomllib
|
import tomllib
|
||||||
@ -23,6 +25,26 @@ TEST_PYPI_JSON_URL = "https://test.pypi.org/pypi/{package}/json"
|
|||||||
TEST_PYPI_UPLOAD_URL = "https://test.pypi.org/legacy/"
|
TEST_PYPI_UPLOAD_URL = "https://test.pypi.org/legacy/"
|
||||||
|
|
||||||
|
|
||||||
|
def load_pypirc_token(use_test_pypi: bool) -> str | None:
|
||||||
|
"""Read a token from ~/.pypirc for the matching index, if present.
|
||||||
|
|
||||||
|
`uv publish` does not read ~/.pypirc (twine convention), so we surface it
|
||||||
|
through UV_PUBLISH_TOKEN. Returns None when no matching token is found.
|
||||||
|
"""
|
||||||
|
pypirc = Path.home() / ".pypirc"
|
||||||
|
if not pypirc.is_file():
|
||||||
|
return None
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
parser.read(pypirc)
|
||||||
|
except configparser.Error:
|
||||||
|
return None
|
||||||
|
section = "testpypi" if use_test_pypi else "pypi"
|
||||||
|
if not parser.has_section(section):
|
||||||
|
return None
|
||||||
|
return parser.get(section, "password", fallback=None)
|
||||||
|
|
||||||
|
|
||||||
def fetch_latest_revision(blender_version: str, use_test_pypi: bool) -> int | None:
|
def fetch_latest_revision(blender_version: str, use_test_pypi: bool) -> int | None:
|
||||||
"""Fetch the latest revision for a Blender version from PyPI.
|
"""Fetch the latest revision for a Blender version from PyPI.
|
||||||
|
|
||||||
@ -175,6 +197,11 @@ def publish_version(
|
|||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
print(f" Publishing to {publish_url}...")
|
print(f" Publishing to {publish_url}...")
|
||||||
|
env = os.environ.copy()
|
||||||
|
if "UV_PUBLISH_TOKEN" not in env:
|
||||||
|
token = load_pypirc_token(use_test_pypi)
|
||||||
|
if token:
|
||||||
|
env["UV_PUBLISH_TOKEN"] = token
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
"uv",
|
"uv",
|
||||||
@ -184,6 +211,7 @@ def publish_version(
|
|||||||
publish_url,
|
publish_url,
|
||||||
],
|
],
|
||||||
cwd=str(SCRIPT_DIR),
|
cwd=str(SCRIPT_DIR),
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print("Publish failed.", file=sys.stderr)
|
print("Publish failed.", file=sys.stderr)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from introspect import (
|
from introspect import (
|
||||||
FunctionData,
|
FunctionData,
|
||||||
|
description_hints_nullable,
|
||||||
infer_getter_return_types,
|
infer_getter_return_types,
|
||||||
parse_rst_function_sig,
|
parse_rst_function_sig,
|
||||||
infer_context_member_type,
|
infer_context_member_type,
|
||||||
@ -19,6 +20,7 @@ from introspect import (
|
|||||||
introspect_screen_context_members,
|
introspect_screen_context_members,
|
||||||
parse_docstring_types,
|
parse_docstring_types,
|
||||||
python_type_name,
|
python_type_name,
|
||||||
|
rna_property_to_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -650,5 +652,107 @@ class TestRuntimeEdgeCases(unittest.TestCase):
|
|||||||
self.assertEqual(data["variables"], [])
|
self.assertEqual(data["variables"], [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestRnaPropertyToType(unittest.TestCase):
|
||||||
|
def test_enum_flag_is_set_of_str(self) -> None:
|
||||||
|
prop = SimpleNamespace(type="enum", is_enum_flag=True, fixed_type=None)
|
||||||
|
self.assertEqual(rna_property_to_type(prop), "set[str]")
|
||||||
|
|
||||||
|
def test_enum_scalar_with_items_is_literal(self) -> None:
|
||||||
|
prop = SimpleNamespace(
|
||||||
|
type="enum",
|
||||||
|
is_enum_flag=False,
|
||||||
|
fixed_type=None,
|
||||||
|
enum_items=[
|
||||||
|
("NONE", "", ""),
|
||||||
|
("LEFTMOUSE", "", ""),
|
||||||
|
("RIGHTMOUSE", "", ""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
rna_property_to_type(prop),
|
||||||
|
'Literal["NONE", "LEFTMOUSE", "RIGHTMOUSE"]',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_enum_scalar_with_empty_items_is_str(self) -> None:
|
||||||
|
# Dynamic enums (items computed at runtime) have empty enum_items.
|
||||||
|
prop = SimpleNamespace(
|
||||||
|
type="enum", is_enum_flag=False, fixed_type=None, enum_items=[]
|
||||||
|
)
|
||||||
|
self.assertEqual(rna_property_to_type(prop), "str")
|
||||||
|
|
||||||
|
def test_enum_without_flag_attr_is_str(self) -> None:
|
||||||
|
prop = SimpleNamespace(type="enum", fixed_type=None)
|
||||||
|
self.assertEqual(rna_property_to_type(prop), "str")
|
||||||
|
|
||||||
|
def test_pointer_nullable_when_not_never_none(self) -> None:
|
||||||
|
prop = SimpleNamespace(
|
||||||
|
type="pointer",
|
||||||
|
fixed_type=SimpleNamespace(identifier="Area"),
|
||||||
|
is_never_none=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(rna_property_to_type(prop), "Area | None")
|
||||||
|
|
||||||
|
def test_pointer_non_nullable_when_never_none(self) -> None:
|
||||||
|
prop = SimpleNamespace(
|
||||||
|
type="pointer",
|
||||||
|
fixed_type=SimpleNamespace(identifier="Scene"),
|
||||||
|
is_never_none=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(rna_property_to_type(prop), "Scene")
|
||||||
|
|
||||||
|
def test_pointer_defaults_to_nullable_without_attr(self) -> None:
|
||||||
|
prop = SimpleNamespace(
|
||||||
|
type="pointer",
|
||||||
|
fixed_type=SimpleNamespace(identifier="Object"),
|
||||||
|
)
|
||||||
|
self.assertEqual(rna_property_to_type(prop), "Object | None")
|
||||||
|
|
||||||
|
def test_pointer_function_return_stays_non_nullable(self) -> None:
|
||||||
|
# Function returns get nullable_pointer=False because Blender raises
|
||||||
|
# on failure rather than returning None, even though is_never_none=False.
|
||||||
|
prop = SimpleNamespace(
|
||||||
|
type="pointer",
|
||||||
|
fixed_type=SimpleNamespace(identifier="Object"),
|
||||||
|
is_never_none=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(rna_property_to_type(prop, nullable_pointer=False), "Object")
|
||||||
|
|
||||||
|
def test_pointer_function_return_never_none_stays_non_nullable(self) -> None:
|
||||||
|
prop = SimpleNamespace(
|
||||||
|
type="pointer",
|
||||||
|
fixed_type=SimpleNamespace(identifier="Scene"),
|
||||||
|
is_never_none=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(rna_property_to_type(prop, nullable_pointer=False), "Scene")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDescriptionHintsNullable(unittest.TestCase):
|
||||||
|
def test_may_be_none_phrase(self) -> None:
|
||||||
|
self.assertTrue(
|
||||||
|
description_hints_nullable(
|
||||||
|
"The current space, may be None in background-mode"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_can_be_none_phrase(self) -> None:
|
||||||
|
self.assertTrue(
|
||||||
|
description_hints_nullable(
|
||||||
|
"The active object — can be None when no object is selected"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_may_be_null_phrase(self) -> None:
|
||||||
|
self.assertTrue(description_hints_nullable("Result may be null on failure"))
|
||||||
|
|
||||||
|
def test_case_insensitive(self) -> None:
|
||||||
|
self.assertTrue(description_hints_nullable("This MAY BE NONE in some cases"))
|
||||||
|
|
||||||
|
def test_no_hint_returns_false(self) -> None:
|
||||||
|
self.assertFalse(description_hints_nullable("The active scene"))
|
||||||
|
|
||||||
|
def test_empty_returns_false(self) -> None:
|
||||||
|
self.assertFalse(description_hints_nullable(""))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user