"RuntimeError: Operator bpy.ops.PIYO.HOGE.poll() failed, context is incorrect" が出てくる流れを追ってみる
具体例として、テキストエディタの「コピー」を対象にして調べてみる
python での関数は "bpy.ops.text.copy()"
調べる場所は公式が提供している Blender のコード
定義系
- wmOperatorType :オペレーターの構造体?の定義 (windowmanager/WM_types.h$751)
bContextDataResult の定義
struct bContextDataResult {
PointerRNA ptr;
ListBase list;
const char **dir;
short type; /* 0: normal, 1: seq */
};
PointerRNA の定義
typedef struct PointerRNA {
struct ID *owner_id;
struct StructRNA *type;
void *data;
} PointerRNA;
呼び出し側
-
pyop_call:オペレーター実行前のチェックを行う関数で、
WM_operator_poll_context
を呼び出す
(場所:python/intern/bpy_operator.c$156) -
WM_operator_poll_context:
wm_operator_call_internal
を呼び出す
(場所:windowmanager/intern/wm_event_system.c$863) -
wm_operator_call_internal:
poll_only=True
でwm_operator_invoke
を呼び出す
(場所:windowmanager/intern/wm_event_system.c$1463) -
wm_operator_invoke:
poll_only=True
のときはWM_operator_poll
を呼び出す
(場所:windowmanager/intern/wm_event_system.c$$1302) -
WM_operator_poll:
wmOperatorType -> poll
に設定されている関数を呼び出す
(場所:windowmanager/intern/wm_event_system.c$840)
呼び出し側でのおおまかな流れ
python 側でbpy.ops.text.copy()
が実行される
↓
C 側で(多分)pyop_call(なんかいい感じの引数)
が呼ばれ、その中で主に以下の処理が実行される
-
bContext *C = BPY_context_get()
として pythonから? context を取得する -
pyop_call
に与えられたargs
をパースして、opname
などを取り出す -
ot = WM_operatortype_find(opname, true)
でオペレーターTEXT_OT_copy
の構造体を得る - オペレーターに与えられたオプションの形式などをチェックする
-
WM_operator_poll_context((bContext *)C, ot, context) == false
を実行[1]し、オペレーターの poll関数の結果をチェックする - 具体的には、
wm_operator_call_internal(C, ot, *properties=NULL, *reports=NULL, context, poll_only=true, *event=NULL)
が実行される -
wm_operator_call_internal
の処理の詳細はWM_OP_EXEC_DEFAULT
系のオプション(context引数)に応じて変化するが、最終的にはwm_operator_invoke(C, ot, *event=NULL, *properties=NULL, *reports=NULL, poll_only=true, use_last_properties=true)
を返す(はず) -
poll_only=true
なので、wm_operator_invoke
は単にWM_operator_poll(C, ot)
を実行し、その結果を返す - (macro関連のよくわからない部分を無視すると[2])
WM_operator_poll
はreturn ot->poll(C)
を実行する - オペレーター
TEXT_OT_copy
の構造体に設定された poll関数(今回ならtext_edit_poll()
)がC
を引数として実行され、結果が返される
↓
poll関数が 0 を返すと、WM_operator_poll_context((bContext *)C, ot, context)==false
が成立し、pyop_call
はBleder 側に "context is incorrect" を出す
オペレーター側
オペレーターは wmOperatorType を使って定義されている
-
TEXT_OT_copy:オペレーターの設定関数で、
ot->poll
にtext_edit_poll
を設定
(場所:editors/space_text/text_ops.c$1025) -
text_edit_poll:
CTX_data_edit_text
を呼ぶ
(場所:editors/space_text/text_ops.c$171) -
CTX_data_edit_text:
member="edit_text"
でctx_data_pointer_get
を呼ぶ
(場所:blenkernel/BKE_context.c$1369) -
ctx_data_pointer_get:
member="edit_text"
でctx_data_get
を呼ぶ
(場所:blenkernel/intern/context.c$383) -
ctx_data_get:UI? から screen まで順番に
member="edit_text"
で context を検索
(場所:blenkernel/intern/context.c$306)
オペレーター側でのおおまかな流れ
「呼び出し側でのおおまかな流れ」の10の続きのようなもの
(TEXT_OT_copy
の構造体が作られたときに、poll
に text_edit_poll
が設定されている)
↓
-
pyop_call(なんかいい感じの引数)
が実行された結果、なんやかんやしたあとtext_edit_poll(*C)
が実行される - 今回の poll関数では、
*text = CTX_data_edit_text(C)
が実行される -
CTX_data_edit_text
は単にctx_data_pointer_get(C, "edit_text")
を実行してその結果を返すだけ -
ctx_data_pointer_get
では、まず&bContextDataResult
として変数result
が設定される - そして、"edit_text"の探索結果を格納する場所として
result
を用いて、ctx_data_get(*C, member="edit_text", &result)
を実行する -
ctx_data_get
が返してきたresult
の型をチェックし、問題なければresult.ptr.data
を返す
↓
ctx_data_get
で "edit_text" を見つけることができなければNULL
が返され、*text
が正しく取得できなかったということでtext_edit_poll(*C)
は 0 を返す
↓
poll関数が 0 を返したということで、pyop_call
はBleder 側に "context is incorrect" を出す
BPY_context_get
bContext *BPY_context_get(void)
{
return bpy_context_module->ptr.data;
}
bpy_context_module
は BPy_init_modulesの中で呼ばれていて、_bpy.context
として設定された BPy_StructRNA のことっぽい
↓
BPY_context_get
は python側のbpy.context
のデータを取ってきている?
pyop_call のbContext *C = BPY_context_get()
についたコメントを読む限りそんな気がする
XXX TODO: work out a better solution for passing on context,
could make a tuple from self and pack the name and Context into it.
「"context is incorrect"になるとき」に関する現時点での情報
-
"context is incorrect" になるとき
↑←ctx_data_get(*C, member="edit_text", &result)
が false になるとき
=ctx_data_get
が "edit_text "を発見できず、0 を返すとき
-
ctx_data_get
に関連するよくわからないこと- Blender 起動時は
#ifdef WITH_PYTHON
の方で処理を行うのかどうか -
&C->wm.store->entries
、というかstore->entries
に何が入っているのか
(というかwm.store
自体が何を store するところなのか)
- Blender 起動時は
#ifdef WITH_PYTHON の場合
#ifdef WITH_PYTHON
if (CTX_py_dict_get(C)) {
if (BPY_context_member_get(C, member, result)) {
return 1;
}
}
#endif
-
BPY_context_member_get(*C, member="edit_text", &result)
が true になれば pollチェック成功 - false になれば "context is incorrect" を出す方向に進む
CTX_py_dict_get に関するあれこれ
定義は以下の通り
void *CTX_py_dict_get(const bContext *C)
{
return C->data.py_context;
}
data.py_context
は、pyop_call
における以下のような部分で設定されている (ソース)
(英文コメントはソースのまま)
context_dict
は、args をパースして得た " optional args " らしい
/* ... */
/* 呼び出し側でのおおまかな流れの 4 までの処理 */
/* ... */
/**
* It might be that there is already a Python context override. We don't want to remove that
* except when this operator call sets a new override explicitly. This is necessary so that
* called operator runs in the same context as the calling code by default.
*/
struct bContext_PyState context_py_state;
if (context_dict != NULL) {
CTX_py_state_push(C, &context_py_state, (void *)context_dict);
Py_INCREF(context_dict); /* 参照カウントの追加 */
}
/* ... */
/* WM_operator_poll_context による poll関数呼び出しとかいろいろ*/
/* ... */
if (context_dict != NULL) {
PyObject *context_dict_test = CTX_py_dict_get(C);
if (context_dict_test != context_dict) {
Py_DECREF(context_dict_test);
}
/* Restore with original context dict,
* probably NULL but need this for nested operator calls. */
Py_DECREF(context_dict);
CTX_py_state_pop(C, &context_py_state);
}
/* ... */
/* ... 最後の処理とかいろいろ */
CTX_py_state_push
は以下のように定義されている(ソース)
void CTX_py_state_push(bContext *C, struct bContext_PyState *pystate, void *value)
{
pystate->py_context = C->data.py_context;
pystate->py_context_orig = C->data.py_context_orig;
C->data.py_context = value;
C->data.py_context_orig = value;
}
CTX_py_state_push
によってもともとのC->data.py_context
は context_py_state に移され、代わりに context_dict の内容がC->data.py_context
に格納される、という感じ
py_context に関連するソースの状況
-
py_context
を操作するのは、CTX_py_state_push
とCTX_py_state_pop
だけ -
py_context
の呼び出しは、いくつかの関数で行われている -
CTX_py_state_push
もCTX_py_state_pop
を呼び出すのは、pyop_call
(とその仲間)だけ
これを見ると、必ずCTX_py_state_push
は実行される = context_dict
は必ず NULL ではない が正解に見える
つまり、「ユーザーの行動に関係なくpyop_call()
に渡された args の中には常に context の辞書が入っていて、それをC->data.py_context
に設定した上で poll関数を呼んでいる」
CTX_py_dict_get と py_context に関する整理
-
C->data.py_context
は基本的にNULL- pop の部分のコメントはそんな感じ
- コードとしても、オペレーターが呼ばれない限り初期設定から変更されない(はず)
- オペレーターが呼ばれると、python側から与えられた(たぶん)辞書形式の context の内容が
C->data.py_context
に設定される- オペレーターを呼んだとき、python側は(たぶん)辞書形式の context を(たぶん) args として
pyop_call
に渡す -
pyop_call
は受け取った context 辞書をcontext_dict
に格納する - さらに
CTX_py_state_push
を実行し、もとのC->data.py_context
はcontext_py_state
に退避させ、context_dict
の内容を新たにC->data.py_context
に設定する
- オペレーターを呼んだとき、python側は(たぶん)辞書形式の context を(たぶん) args として
- poll関数のチェックには、新しい
C->data.py_context
、つまり python側が与えた context が使われる[1] - poll関数などのチェックが終わると、
CTX_py_state_pop
によってC->data.py_context
は以前の内容に戻される[2]
なので、元の話に戻るとif (CTX_py_dict_get(C))
のチェックは必ず成功する、つまり
- "context is incorrect" が出る
↑←ctx_data_get(*C, member="edit_text", &result)
が false になる
↑←BPY_context_member_get(*C, member="edit_text", &result)
が false になる
ということになり、BPY_context_member_get
がポイントになる
BPY_context_member_get
BPY_context_member_get の定義
int BPY_context_member_get(bContext *C, const char *member, bContextDataResult *result)
{
PyGILState_STATE gilstate;
const bool use_gil = !PyC_IsInterpreterActive();
PyObject *pyctx;
PyObject *item;
PointerRNA *ptr = NULL;
bool done = false;
if (use_gil) {
gilstate = PyGILState_Ensure();
}
pyctx = (PyObject *)CTX_py_dict_get(C);
item = PyDict_GetItemString(pyctx, member);
if (item == NULL) {
/* pass */
}
else if (item == Py_None) {
done = true;
}
else if (BPy_StructRNA_Check(item)) {
ptr = &(((BPy_StructRNA *)item)->ptr);
// result->ptr = ((BPy_StructRNA *)item)->ptr;
CTX_data_pointer_set_ptr(result, ptr);
CTX_data_type_set(result, CTX_DATA_TYPE_POINTER);
done = true;
}
else if (PySequence_Check(item)) {
PyObject *seq_fast = PySequence_Fast(item, "bpy_context_get sequence conversion");
if (seq_fast == NULL) {
PyErr_Print();
PyErr_Clear();
}
else {
const int len = PySequence_Fast_GET_SIZE(seq_fast);
PyObject **seq_fast_items = PySequence_Fast_ITEMS(seq_fast);
int i;
for (i = 0; i < len; i++) {
PyObject *list_item = seq_fast_items[i];
if (BPy_StructRNA_Check(list_item)) {
#if 0
CollectionPointerLink *link = MEM_callocN(sizeof(CollectionPointerLink),
"bpy_context_get");
link->ptr = ((BPy_StructRNA *)item)->ptr;
BLI_addtail(&result->list, link);
#endif
ptr = &(((BPy_StructRNA *)list_item)->ptr);
CTX_data_list_add_ptr(result, ptr);
}
else {
CLOG_INFO(BPY_LOG_CONTEXT,
1,
"'%s' list item not a valid type in sequence type '%s'",
member,
Py_TYPE(item)->tp_name);
}
}
Py_DECREF(seq_fast);
CTX_data_type_set(result, CTX_DATA_TYPE_COLLECTION);
done = true;
}
}
if (done == false) {
if (item) {
CLOG_INFO(BPY_LOG_CONTEXT, 1, "'%s' not a valid type", member);
}
else {
CLOG_INFO(BPY_LOG_CONTEXT, 1, "'%s' not found\n", member);
}
}
else {
CLOG_INFO(BPY_LOG_CONTEXT, 2, "'%s' found", member);
}
if (use_gil) {
PyGILState_Release(gilstate);
}
return done;
}
BPY_context_member_get の流れ
- Global Interpeter Lock のチェック
-
CTX_py_dict_get(C)
を実行し、C->data.py_context
設定されている辞書を変数pyctx
に格納する - 格納した辞書から文字列
member
をキーとしてオブジェクトを取り出し、変数item
に格納する -
BPy_StructRNA_Check
およびPySequence_Check
で item の型をチェックし、処理を分ける
BPy_StructRNA_Check(item)=true
◆item が『単一のデータ』である場合:-
item
を BPy_StructRNA 型にキャストし、その ptr メンバへのポインタ値を変数ptr
に格納する - 引数として与えられていた result を持ってきて、
CTX_data_pointer_set_ptr(result, ptr)
を実行。といってもresult->ptr = *ptr
、つまりresult->ptr = item->ptr
のように検索結果を result に移しているだけ - さらに
CTX_data_type_set(result, CTX_DATA_TYPE_POINTER)
を実行。これもresult->type = CTX_DATA_TYPE_POINTER
を行い、result の type メンバに必要な値を設定しているだけ - log 出して
done(bool)
を返して終了
PySequence_Check(item)=true
◆ item が『データのリスト』である場合:- まず、
PySequence_Fast
を使って item をシーケンス(リスト)として扱える用に調整する -
PySequence_Fast_ITEMS
を使って、リストとして操作できる?配列を取得 - item のリストから順番に要素を取り出す。ここでは、リストの中に X1~X3 の要素があり、まず最初に X1 が取り出されたとする
- 、
BPy_StructRNA_Check
で X1 の型をチェックし、X1 を BPy_StructRNA 型にキャストし、その ptr メンバへのポインタ値を変数ptr
に格納する -
CTX_data_list_add_ptr(result, ptr)
を実行し、link
として CollectionPointerLink を用意してlink->ptr = X1->ptr
となるように処理 - そして
BLI_addtail(&result->list, link)
を実行し、list->last = link
として最後尾に link を追加する (つまりresult->list->last->ptr = X1->ptr
となる) - X2 と X3 についても同様に処理する (その結果、
result->list
に X1~X3 のデータがまとめられる、はず) - 全ての要素について処理が終わると、
CTX_data_type_set(result, CTX_DATA_TYPE_COLLECTION)
を実行し、result->type = CTX_DATA_TYPE_COLLECTION
して、result の type メンバに必要な値を設定する - log 出して
done(bool)
を返して終了
BPY_context_member_get のまとめ:"edit_text"を探索する場合
-
結局のところ、仕事は3つ
- "edit_text" を探す。見つけたら item に入れておく
- result に 見つけた item の ptr メンバの情報を入れる
- result に適切なデータの型の情報を入れる
-
"edit_text"は、
C->data.py_context
に設定されている辞書から探すが、これはおそらくbpy.context.copy()
の結果と同じもの
→ Blender 側が作る context 辞書のキー"edit_text" に結びついた値が探索結果となる
-
まとめ
bpy.context.edit_text
が None でなければ、BPY_context_member_get
は 1 を返す?
blank
Blender の Python コンソールにて実験
>>> bpy.ops.text.copy() #コンソールでは poll() が fail するので実行できない
Traceback (most recent call last):
File "<blender_console>", line 1, in <module>
File "C:\Program Files\Blender Foundation\Blender 2.91\2.91\scripts\modules\bpy\ops.py", line 132, in __call__
ret = _op_call(self.idname_py(), None, kw)
RuntimeError: Operator bpy.ops.text.copy.poll() failed, context is incorrect
>>> override = C.copy() # context 辞書を取得してみる
>>> override["edit_text"] # コンソールで取得した context 辞書には edit_text は入っていない
Traceback (most recent call last):
File "<blender_console>", line 1, in <module>
KeyError: 'edit_text'
>>> D.texts[0]
bpy.data.texts['ui_panel_simple.py']
>>> override["edit_text"] = D.texts[0] # context 辞書にむりやり edit_text を設定する
>>> bpy.ops.text.copy(override) # コンソールからでも pollチェックに成功した!!
{'FINISHED'}
(オペレーターの実行自体にも成功していた)
addon に組み込みユーザー設定パネルの context で実行した場合でも、同じような結果になった
適当に作ったやつ
import bpy
from bpy.props import *
from bpy.ops import *
bl_info = {
"name": "Test:text.copy() with overridden context",
"author": "nikogoli",
"version": (0, 1),
"blender": (2, 91, 0),
"location": "None",
"description": "",
"warning": "",
"support": "TESTING",
"wiki_url": "",
"tracker_url": "",
"category": "Custom"
}
# オペレーター
class TestingTempOperator(bpy.types.Operator):
bl_idname = "wm.testing_temp_operator"
bl_label = "Testing Temp Operator"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
bpy.ops.text.copy()
context.parent_panel.text_cont = "Operator's poll() successes"
except RuntimeError:
context.parent_panel.text_cont = "Operator's poll() fails"
override = context.copy()
override["edit_text"] = bpy.data.texts[0]
try:
bpy.ops.text.copy(override)
context.parent_panel.text_over = "Operator's poll() successes"
except RuntimeError:
context.parent_panel.text_over = "Operator's poll() fails"
return {'FINISHED'}
# アドオン設定
class AddonPreferences(bpy.types.AddonPreferences):
bl_idname = __name__
text_clip : StringProperty(name="Clipboard", default="")
text_cont : StringProperty(name="default context", default="")
text_over : StringProperty(name="overridden context", default="")
def draw(self, context):
self.text_clip = context.window_manager.clipboard
row = self.layout.row()
row.label(text="Texts in Clipboard: "+self.text_clip)
row.context_pointer_set("parent_panel", self)
row.operator(TestingTempOperator.bl_idname, text="Call bpy.ops.text.copy()")
self.layout.prop(self, "text_cont")
self.layout.prop(self, "text_over")
#---------------------------------------------------
classes = [
TestingTempOperator,
AddonPreferences,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
#bpy.types.VIEW3D_PT_tools_object_options.append(menu_func)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
#bpy.types.VIEW3D_PT_tools_object_options.remove(menu_func)
if __name__ == "__main__":
register()
ボタンを押すと素の context と override した context で bpy.ops.text.copy() を呼ぶ
これでいいんだっけ? 「必要っぽいものを override に突っ込んでおく」方法がだめだから area を置き換えるのが通例になってると理解していたんだけど・・・
area を置き換える方法でも上手くいく どういうこと????????
#ifdef WITH_PYTHON
で動いているという想定が間違いなんだろうか
>>> bpy.ops.text.copy() #コンソールでは poll() が fail するので実行できない
Traceback (most recent call last):
File "<blender_console>", line 1, in <module>
File "C:\Program Files\Blender Foundation\Blender 2.91\2.91\scripts\modules\bpy\ops.py", line 132, in __call__
ret = _op_call(self.idname_py(), None, kw)
RuntimeError: Operator bpy.ops.text.copy.poll() failed, context is incorrect
>>> for a in C.screen.areas: #テキストエディタのデータを取得
... if a.type=="TEXT_EDITOR":
... A = a
...
>>> override["area"] = A # context 辞書の area の値を差し替え
>>> override["area"].type
'TEXT_EDITOR'
>>> bpy.ops.text.copy(override) # コンソールからでも pollチェックに成功する
{'FINISHED'}
temp
前提
- context のチェックは、「python の context → C 側での context の検索」で行われる
仮説
-
'edit_text'
を override した場合、「python の context の検索」が成功 → poll 成功 -
'area'
を override した場合、「C側 の context の検索」が override された area で行われ、その結果テキストエディタで検索されて成功する → poll 成功
仮説の想定
- override した pyhon の context の
'area'
が、C側で context を検索する area を決定する
そんなことある?