Open26

"RuntimeError: Operator bpy.ops.PIYO.HOGE.poll() failed, context is incorrect" が出てくる流れを追ってみる

ピン留めされたアイテム
nikogolinikogoli

context を下側に掘るのに疲れたので、上側に掘ってみる

  • python と C/C++ の接続部分についてはよくわからないので、そこら辺は雰囲気で[1]
  • 文章内でのポインタと実態の区別も雰囲気で
  • Blender側の更新によって行番号がずれていることもある
脚注
  1. 接続部分は Blender 2.91/2.91/scripts/modules/bpy/ops.py, line 132, に _op_call(self.idname_py(), None, kw) があるようだ ↩︎

ピン留めされたアイテム
nikogolinikogoli

定義系

  • wmOperatorType :オペレーターの構造体?の定義 (windowmanager/WM_types.h$751)
bContextDataResult の定義

コードの場所

blenkernel/intern/context.c$264
struct bContextDataResult {
  PointerRNA ptr;
  ListBase list;
  const char **dir;
  short type; /* 0: normal, 1: seq */
};
PointerRNA の定義

コードの場所

makesrna/RNA_types.h$49
typedef struct PointerRNA {
  struct ID *owner_id;
  struct StructRNA *type;
  void *data;
} PointerRNA;
ピン留めされたアイテム
nikogolinikogoli

呼び出し側

  1. pyop_call:オペレーター実行前のチェックを行う関数で、WM_operator_poll_context を呼び出す
            (場所:python/intern/bpy_operator.c$156)
  2. WM_operator_poll_contextwm_operator_call_internal を呼び出す
            (場所:windowmanager/intern/wm_event_system.c$863)
  3. wm_operator_call_internalpoll_only=Truewm_operator_invoke を呼び出す
            (場所:windowmanager/intern/wm_event_system.c$1463)
  4. wm_operator_invokepoll_only=True のときは WM_operator_poll を呼び出す
            (場所:windowmanager/intern/wm_event_system.c$$1302)
  5. WM_operator_pollwmOperatorType -> poll に設定されている関数を呼び出す
            (場所:windowmanager/intern/wm_event_system.c$840)
nikogolinikogoli

呼び出し側でのおおまかな流れ

python 側でbpy.ops.text.copy()が実行される
   ↓
C 側で(多分)pyop_call(なんかいい感じの引数)が呼ばれ、その中で主に以下の処理が実行される

  1. bContext *C = BPY_context_get()として pythonから? context を取得する
  2. pyop_callに与えられたargsをパースして、opnameなどを取り出す
  3. ot = WM_operatortype_find(opname, true) でオペレーターTEXT_OT_copyの構造体を得る
  4. オペレーターに与えられたオプションの形式などをチェックする
  5. WM_operator_poll_context((bContext *)C, ot, context) == falseを実行[1]し、オペレーターの poll関数の結果をチェックする
  6. 具体的には、wm_operator_call_internal(C, ot, *properties=NULL, *reports=NULL, context, poll_only=true, *event=NULL)が実行される
  7. 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)を返す(はず)
  8. poll_only=trueなので、wm_operator_invokeは単にWM_operator_poll(C, ot)を実行し、その結果を返す
  9. (macro関連のよくわからない部分を無視すると[2])WM_operator_pollreturn ot->poll(C)を実行する
  10. オペレーター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" を出す

脚注
  1. ここでの context は int であり、基本的にはWM_OP_EXEC_DEFAULTになるっぽい。python (Blender)側で個別に指定した場合、argsがパースされたときにcontext_strに格納され、RNA_enum_value_from_idの処理のなかでチェックされてcontextに移されると思われる ↩︎

  2. 「オペレーターにマクロが設定されている場合はマクロ側の poll関数をチェックに使う」という処理に見える。マクロ周りは何の知識もなく、TEXT_OT_copyでも macro は設定されてないので、ここでは無視することにする ↩︎

nikogolinikogoli

オペレーター側

オペレーターは wmOperatorType を使って定義されている

  1. TEXT_OT_copy:オペレーターの設定関数で、ot->polltext_edit_poll を設定
            (場所:editors/space_text/text_ops.c$1025)
  2. text_edit_pollCTX_data_edit_text を呼ぶ
            (場所:editors/space_text/text_ops.c$171)
  3. CTX_data_edit_textmember="edit_text"ctx_data_pointer_get を呼ぶ
            (場所:blenkernel/BKE_context.c$1369)
  4. ctx_data_pointer_getmember="edit_text"ctx_data_get を呼ぶ
            (場所:blenkernel/intern/context.c$383)
  5. ctx_data_get:UI? から screen まで順番にmember="edit_text" で context を検索
            (場所:blenkernel/intern/context.c$306)
nikogolinikogoli

オペレーター側でのおおまかな流れ

「呼び出し側でのおおまかな流れ」の10の続きのようなもの

(TEXT_OT_copyの構造体が作られたときに、polltext_edit_pollが設定されている)
  ↓

  1. pyop_call(なんかいい感じの引数)が実行された結果、なんやかんやしたあとtext_edit_poll(*C)が実行される
  2. 今回の poll関数では、*text = CTX_data_edit_text(C)が実行される
  3. CTX_data_edit_text は単に ctx_data_pointer_get(C, "edit_text")を実行してその結果を返すだけ
  4. ctx_data_pointer_get では、まず&bContextDataResult として変数resultが設定される
  5. そして、"edit_text"の探索結果を格納する場所としてresultを用いて、ctx_data_get(*C, member="edit_text", &result)を実行する
  6. 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" を出す

nikogolinikogoli

BPY_context_get

python/intern/bpy_interface.c$256
bContext *BPY_context_get(void)
{
  return bpy_context_module->ptr.data;
}

bpy_context_moduleBPy_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.

nikogolinikogoli

「"context is incorrect"になるとき」に関する現時点での情報

  • "context is incorrect" になるとき
     ↑←ctx_data_get(*C, member="edit_text", &result)が false になるとき
        = ctx_data_getが "edit_text "を発見できず、0 を返すとき
      

  • ctx_data_getに関連するよくわからないこと

    1. Blender 起動時は#ifdef WITH_PYTHONの方で処理を行うのかどうか
    2. &C->wm.store->entries、というかstore->entriesに何が入っているのか
      (というか wm.store 自体が何を store するところなのか)
nikogolinikogoli

#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" を出す方向に進む
nikogolinikogoli

CTX_py_dict_get に関するあれこれ

定義は以下の通り

blenkernel/intern/context.c$239
void *CTX_py_dict_get(const bContext *C)
{
  return C->data.py_context;
}

data.py_contextは、pyop_callにおける以下のような部分で設定されている (ソース)
(英文コメントはソースのまま)
context_dictは、args をパースして得た " optional args " らしい

python/intern/bpy_operator.c$243
/* ... */
/* 呼び出し側でのおおまかな流れの 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に格納される、という感じ

nikogolinikogoli

py_context に関連するソースの状況

  • py_contextを操作するのは、CTX_py_state_pushCTX_py_state_popだけ
  • py_contextの呼び出しは、いくつかの関数で行われている
  • CTX_py_state_pushCTX_py_state_popを呼び出すのは、pyop_call(とその仲間)だけ

これを見ると、必ずCTX_py_state_pushは実行される = context_dictは必ず NULL ではない が正解に見える

つまり、「ユーザーの行動に関係なくpyop_call()に渡された args の中には常に context の辞書が入っていて、それをC->data.py_contextに設定した上で poll関数を呼んでいる」

nikogolinikogoli

CTX_py_dict_get と py_context に関する整理

  1. C->data.py_contextは基本的にNULL
    • pop の部分のコメントはそんな感じ
    • コードとしても、オペレーターが呼ばれない限り初期設定から変更されない(はず)
  2. オペレーターが呼ばれると、python側から与えられた(たぶん)辞書形式の context の内容がC->data.py_contextに設定される
    • オペレーターを呼んだとき、python側は(たぶん)辞書形式の context を(たぶん) args としてpyop_callに渡す
    • pyop_callは受け取った context 辞書をcontext_dictに格納する
    • さらにCTX_py_state_pushを実行し、もとのC->data.py_contextcontext_py_stateに退避させ、context_dictの内容を新たにC->data.py_contextに設定する
  3. poll関数のチェックには、新しいC->data.py_context、つまり python側が与えた context が使われる[1]
  4. poll関数などのチェックが終わると、CTX_py_state_popによってC->data.py_contextは以前の内容に戻される[2]
脚注
  1. 『使われる』ときの具体的な利用法は、BPY_context_member_getの実装に依存する ↩︎

  2. なので、override した context は poll の判定などには利用されるが、オペレーターの実行においては考慮されないということになる。 ↩︎

nikogolinikogoli

なので、元の話に戻ると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がポイントになる

nikogolinikogoli

BPY_context_member_get

BPY_context_member_get の定義

ソース

source/blender/python/intern/bpy_interface.c$682
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;
}
nikogolinikogoli

BPY_context_member_get の流れ

  1. Global Interpeter Lock のチェック
  2. CTX_py_dict_get(C)を実行し、C->data.py_context設定されている辞書を変数pyctxに格納する
  3. 格納した辞書から文字列memberをキーとしてオブジェクトを取り出し、変数itemに格納する
  4. BPy_StructRNA_CheckおよびPySequence_Check で item の型をチェックし、処理を分ける

◆item が『単一のデータ』である場合:BPy_StructRNA_Check(item)=true

  1. itemを BPy_StructRNA 型にキャストし、その ptr メンバへのポインタ値を変数ptrに格納する
  2. 引数として与えられていた result を持ってきて、CTX_data_pointer_set_ptr(result, ptr)を実行。といってもresult->ptr = *ptr、つまりresult->ptr = item->ptrのように検索結果を result に移しているだけ
  3. さらにCTX_data_type_set(result, CTX_DATA_TYPE_POINTER)を実行。これもresult->type = CTX_DATA_TYPE_POINTERを行い、result の type メンバに必要な値を設定しているだけ
  4. log 出して done(bool) を返して終了

◆ item が『データのリスト』である場合:PySequence_Check(item)=true

  1. まず、PySequence_Fastを使って item をシーケンス(リスト)として扱える用に調整する
  2. PySequence_Fast_ITEMSを使って、リストとして操作できる?配列を取得
  3. item のリストから順番に要素を取り出す。ここでは、リストの中に X1~X3 の要素があり、まず最初に X1 が取り出されたとする
  4. BPy_StructRNA_Checkで X1 の型をチェックし、X1 を BPy_StructRNA 型にキャストし、その ptr メンバへのポインタ値を変数ptrに格納する
  5. CTX_data_list_add_ptr(result, ptr)を実行し、linkとして CollectionPointerLink を用意して link->ptr = X1->ptr となるように処理
  6. そしてBLI_addtail(&result->list, link)を実行し、list->last = linkとして最後尾に link を追加する (つまり result->list->last->ptr = X1->ptr となる)
  7. X2 と X3 についても同様に処理する (その結果、result->listに X1~X3 のデータがまとめられる、はず)
  8. 全ての要素について処理が終わると、CTX_data_type_set(result, CTX_DATA_TYPE_COLLECTION)を実行し、result->type = CTX_DATA_TYPE_COLLECTIONして、result の type メンバに必要な値を設定する
  9. log 出して done(bool) を返して終了
nikogolinikogoli

BPY_context_member_get のまとめ:"edit_text"を探索する場合

  • 結局のところ、仕事は3つ

    1. "edit_text" を探す。見つけたら item に入れておく
    2. result に 見つけた item の ptr メンバの情報を入れる
    3. result に適切なデータの型の情報を入れる

  • "edit_text"は、C->data.py_contextに設定されている辞書から探すが、これはおそらくbpy.context.copy()の結果と同じもの
    → Blender 側が作る context 辞書のキー"edit_text" に結びついた値が探索結果となる

  • まとめ
    bpy.context.edit_textが None でなければ、BPY_context_member_getは 1 を返す?

nikogolinikogoli

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'}

(オペレーターの実行自体にも成功していた)

nikogolinikogoli
  • Blender側、少なくともコンソールから呼ぶ場合は、#ifdef WITH_PYTHONで動いている
  • その場合、基本的には[1]bpy.context.copy()で得た辞書に「poll関数が探す名前」で適切なデータを入れておけば、"context is incorrect"を回避できる
脚注
  1. poll関数にもいろいろな形式があるので、この方法で必ず上手くいくとは限らないはず ↩︎

nikogolinikogoli

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() を呼ぶ

nikogolinikogoli

これでいいんだっけ? 「必要っぽいものを override に突っ込んでおく」方法がだめだから area を置き換えるのが通例になってると理解していたんだけど・・・

nikogolinikogoli

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'}
nikogolinikogoli

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 を決定する

そんなことある?

nikogolinikogoli

そもそも、なぜ「'area'を override する」ことが「context is incorrect の対策」なのか?ってとこに疑問を持つべきかも。override した contex はどこまで利用されるのか?

nikogolinikogoli

override["area"]を適切の行い、加えてoverride["edit_text"]を不適切に行うと、poll fail を出すことができる。

単純に#ifdef WITH_PYTHON失敗したあとも判定が継続することを忘れてただけっぽいね