株式会社デジタル・フロンティア-Digital Frontier

Header

Main

  • TOP
  • DF TALK
  • Eclipse+pydevdによる Maya Pythonリモートデバッグの紹介

Eclipse+pydevdによる Maya Pythonリモートデバッグの紹介

2017/3/21

Tag: ,,

唐突ではありますが、printデバッグは好きですか?

僕は好きでした。
頭の中をプログラムのことで一杯に満たしつつプログラムを書く。動かしてみると、ちょっとプログラムが思った挙動にならなくて、頭の中でプログラムの挙動を反芻してみる。でもイマイチ要領を得ないので print文で途中の状態を出力してみて、どこで挙動が違っているのか探る。
見えない部分をちょっとずつ探るのが、単純にゲーム感覚で楽しかったのです。

…現在はというと、便利なデバッグ環境を知ってしまいとてもそんな頃には戻れないのですが。

そんなわけで、そんなちょっと便利な環境をご紹介です。
もし printデバッグが好物で、便利で時短できる環境を望まないマゾい諸氏が居られましたら、御免なさい。
今回、そういうプレイは非推奨です。

あ、自己紹介が遅れました。僕は最近TDとして仕事を引き受けております、神原と申します。
ちなみに「MayaでPythonスクリプトを書いているけど、リモートデバッグどうすればできるの?」くらいの方が対象となりますので、あしからず。

準備編

まずは、基本的な環境を用意していただきたいです。
Maya, Eclipse, PyDev。今回はこの3点が必要になります。
(PyDevはEclipseのプラグイン環境なので、基本的には2点+αという形ですけど…)

Mayaは既にあるものとして、Eclipse環境をインストールされていらっしゃらない方は、まずEclipseの導入をお願いいたします。
(2017.3月現在は、Eclipse(Neon)が最新ですが、PyDev環境が使えればOKなので、大概のEclipseで動くと思われます。)

ダウンロードはhttp://eclipse.org/downloads/から。

インストール方法は、PyDev環境含めて「eclipse + PyDevでPythonする。」というブログ記事によくまとまっていましたので、こういった記事を参考にされれば問題なく導入できると思います。

Pythonデバッグ環境のこと

Pythonにはデフォルトで pdbというデバッグモジュールが提供されているんです。
この pdbはインターフェースが CLI(コマンドライン型)で、逐一キー入力でデバッグ操作を行うという作りなのですが、それでも慣れてしまえば本来そこそこ使えるツールです。…ただ困ったことに Mayaに限っては、入力がいちいちダイアログボックスでのお伺いになっており、入力フォーカスがダイアログとMayaを行ったり来たりしてしまうので非常に使いづらいです。
またPySideなどでツールをGUI化することが多いと思うのですが、この場合見るべきウィンドウは4枚(Mayaのメインウィンドウ、スクリプトエディタのログ領域、PySideのGUI、そして…pdbのダイアログ)、しかもデバッグで処理を止めている期間中はダイアログ以外のウィンドウ処理はブロックされています。(つまり全く操作を受け付けず、移動もできない。)

pdb_debug01
(※上記の場合だと、Mayaでシーンを用意してツールを操作しブレイクポイントで停止したものの、ツールのウィンドウがスクリプトエディタを隠していてデバッグ情報を確認できない。こうなるとツールのウィンドウがスクリプトエディタを隠さない位置に動かして、再度ブレイクポイントで止めないといけなくなる。)

この環境だとウィンドウを重ならないように工夫していても、たまに画面が重なってしまった途端動かせもしないので状態を確認することもままならず…なんて強制的にハメを喰らうことがしばしば起こります。(そもそもプログラムに集中したいのに、そんな些末事に気を削がれるのが辛いです。)

そんなわけで、楽々なステップ実行、クリックでブレイクポイントを追加しまくり、変数状態も常に表示させたまま作業が可能なデバッグ環境が理想なのです。(pydevdは設定次第で、PC2台でリッチなリモート・デバッグ環境も作れます。)

pydevdの良いところ

EclipseはGUIベースのIDE環境なので、とにかく情報が多数表示できます。
Mayaが止まっていても Eclipseは動いているので、変数状態も常に確認しながらデバッグを行うことが出来ます。ウィンドウが重なって情報が見えないなんてつまらない問題は起こりませんから、プログラムのロジックに集中してデバッグできる環境が手に入ります。

pydevdの悪い(?)ところ

若干 Mayaのスクリプトエディタと相性が悪いのか、スクリプトエディタの行数と Eclipseに表示中のスクリプト上のカーソル位置がズレます。
基本的にデバッグする場合そこそこ長いプログラムを対象にすると思いますし、ほとんどの場合それらはscriptsフォルダに保存してあるものだと思います。実際はそこまで気にする部分でもありませんが利用頻度が低いと馴染み辛い部分だと思うので、悪いところとして挙げておきます。
あと、たまにプロセスのアタッチ時になにかの処理で空振っているのか Eclipse上に表示するべきスクリプトファイルの選択を要求されないことがあります。

単純に言って、少しクセがあるのでその辺りで万人受けしない気がします。

とはいえ、デバッグが安心して行えるメリットは大きいです。

pydevdを設定する

pydevdを使うには、EclipseにPyDevを導入した上で pydevdモジュールにPYTHONPATHを通す必要があります。
なんのことやら?…と思った方は、すみません。これから、その辺りを順に説明してみたいと思います。

まず pydevdモジュール。
これは Eclipseに PyDevがインストールされるのと一緒に PCにインストールされます。ただ、場所が少々判り辛いです…
Windows環境での説明になりますが、初期設定の状態だと通常 “C:\Users\(ユーザー名)\.p2\pool\plugins\” フォルダ下に Eclipseに導入されたプラグイン環境のファイル群が収まります。
この中で “pysrc” について検索を掛けてください。すると pysrcフォルダが見つかります。
(Eclipseやプラグイン環境のインストール先やWindowsのドキュメント場所などをカスタマイズしている場合は、変更先を探して戴く必要があります。)

この pysrcフォルダの中に、pydevdモジュールとその関連パッケージが詰まっていますので、ここを Mayaの Python環境から見えるようにする(スクリプトエディタ上での “import pydevd” の実行を有効な記述にする)必要があります。

見えるようにする設定として必要なのが、「PYTHONPATHを通す」作業です。

PYTHONPATHを通す

Autodeskのヘルプは正直判りにくいです。
前提が多過ぎるせいか、説明されていなかったり…そもそも情報があるのかすら判らなかったり。とにかく、軽く情報の海で遭難する感覚を味わうことができます。

たとえば、このページを読んでも、PYTHONPATHのことを理解できる人は少ないように思います。

また、そもそもコマンドプロンプトのことを知らない人に「環境設定変数」の概念や機能を説明するのはかなり骨の折れる話です。
ですので、思い切って “userSetup.py” におまじないを書きこんで、対象フォルダに置いてください。とだけ言います。

64bit環境の場合の対象フォルダは、基本以下のどれかです。

  • ドキュメント\maya\(バージョン番号)-x64\ja_JP\scripts
  • ドキュメント\maya\(バージョン番号)-x64\prefs\scripts
  • ドキュメント\maya\(バージョン番号)-x64\scripts
  • ドキュメント\maya\scripts

maya_scripts_folder
※画像は、Maya2014 Windows 64bit版の場合です。
※prefsフォルダを開き忘れていました、ゴメンナサイ;

そもそも “userSetup.py” が無かった場合は、新しく作成して該当フォルダに保存してください。
書き込むおまじないは、以下の通りです。

import sys

# Insert PYTHONPATH: "pydevd"
sys.path.append( r'(pysrcフォルダのフルパス)' )

print("!! userSetup.py loaded, Now !!")

「r'(pysrcフォルダのフルパス) ‘」の部分は、例えば r’C:\Users\(ユーザー名)\.p2\pool\plugins\org.python.pydev_5.5.0.201701191708\pysrc’ みたいな文字列になるはずです。検索して、出てきた pysrcフォルダを右クリック~プロパティを開いて、”場所” の項目に表示されている文字列を各自ご自身の環境でコピペしてください。
pysrc_property

“userSetup.py” をしかるべき場所に置いたら、Mayaを起動してみてください。
ちゃんと読み込んでくれていれば、”Output Window”(Maya起動時に一緒に開く白いウィンドウ)に、”!! userSetup.py loaded, Now !!” のメッセージが表示されます。
outputwindow
表示されない場合は、おまじないが見えていないので、Mayaが読んでくれる Scriptsフォルダを探して下さい。
(ドキュメントフォルダの中にある Scriptsフォルダに順番に入れて Mayaを起動してみて下さい。幾つか入れれば、当たりがあるはずです。)

当たりが出た方、おめでとうございます。

import pydevd。そして、ザ・ワールドッ!

早速ですが、止めてみましょう。

と、その前に。
まずは、ちゃんと pydevd が読めるか確認します。
以下の2行をスクリプトエディタに入力し、実行してみて下さい。

import pydevd
reload(pydevd)

結果スクリプトログに、以下の行が出れば準備完了。
import_pydevd01

表示内容的には、以下のようなものです。(warningの内容は、デバッガ処理を加速させるのに使えるcythonがないけどね。という話で、それほど重要ではないです。)

import pydevd
reload(pydevd)
# warning: Debugger speedups using cython not found. Run '"C:\Program Files\Autodesk\(バージョン番号)\bin\maya.exe" "C:\Users\(ユーザー名)\.p2\pool\plugins\org.python.pydev_5.5.0.201701191708\pysrc\setup_cython.py" build_ext --inplace' to build.

pydevd.settrace()。ここでッ止まれッ!

準備が整えば「pydevd.settrace()」と記述することで、記述行でスクリプトの実行を強制的に中断できます。(pdb.set_trace()と同じです。)

手順を、以下に示します。(参考:remote debugger(@本家PyDevマニュアルページ)

スクリプト中断~Eclipseに移行させる為には、事前に Eclipseを起動してデバッガを待機状態にさせておく必要があります。
まずは、Eclipseを起動してデバッグ・パースペクティブを出します。

パースペクティブを開くボタンを押して。
eclipse_open_perspective01
デバッグを選んで、OKボタンを押します。
eclipse_open_perspective02
画面が、デバッグパースペクティブに遷移。
eclipse_open_perspective03-1
 虫の下にPの文字が付いているアイコンがあるので、それを押します。
(もしくは、PyDevメニュ~Start Debug Serverを選択。)
start_pydev_server01
デバッグタブに、サーバ状況の表示が出てくる。
start_pydev_server02
Eclipseがこの状態の時に Maya側で「pydevd.settrace()」行が実行されると、Eclipseに処理が移ります。
試しに Eclipseでデバッグサーバを実行させ、Mayaのスクリプトエディタで以下を実行してください。

import pydevd
reload(pydevd)

print "The",
print "World!"
pydevd.settrace()

print "And,",
print "re-start script."

Eclipseが、Mayaのスクリプト実行に反応してファイルを選べ。というようにオープン・ダイアログを出すと思います。
settrace_select_pyfile01
ここで読み込むべきものは、先ほどスクリプトエディタで実行したスクリプト・ファイルなのですが、スクリプトは今スクリプトエディタ上にしかないので、ここはキャンセルしてそのままEclipse に処理を戻します。
Debug(デバッグ)タブの項目に、<Module>[<Maya Console>:9] の表示が出ていると思います。
settrace_select_pyfile02

start_pydev_server02b
たまにこんな表示で止まることがありますが、止まった場所がsettrace関数内になっている場合があるというだけなので、慌てる必要はありません。
この場合はF7キーを数回押して下さい。関数を抜けて、自分の書いたスクリプトのところまで戻ればOKです。
 
※以下のような表示が、F7キーを押すごとに減っていくので、
 <module>[<maya console>:行数]の表示に戻るまで押します。
 
  settrace[threading.py:92] <— これらが上から順に消える
  patch_threads[pydevd.py:882]
  _locked_settrace[pydevd.py:1188]
  settrace[threading.py:1099]
  <module>[<maya console>:7]

この数字は停止行です(pydevdの認識では、スクリプトの9行目で停止中)。この停止行自体はpydevd内の認識で間違いないのですが、Maya のスクリプトエディタの表示行数と認識が一致しておらず、実際より2~3行先の場所を指しているようです。

なんにしても settrace()行で止まっているのは間違いないので、ここは読み替えて、スクリプトエディタの行数で6行目に居ると思ってください。
start_pydev_server03
さて、それではこれよりデバッガでステップ実行していきます。
ステップ実行には、F5~F7キーを使います。
以下のような機能割り当てです。

  • F5 .. ステップイン
    (関数があれば、関数に入ったところまで進める)
  • F6 .. ステップオーバー
    (関数があったら呼び出して、結果を貰うところまで進める)
  • F7 .. ステップアウト
    (関数を1つ抜けるところまで進める)

今回関数呼び出しはないので、単純に1行ずつ進みます。
いま残っているのは以下の2行ですが、

print "And,",
print "re-start script."

まだログ領域にはなにも出力されていません。
1回目にF6キー押し下げ後、以下のようになりました。
start_pydev_server04
2回目にF6キー押し下げ後、以下のようになりました。
start_pydev_server05
「ん??」って感じなんですけど、変数の代入とか内部的な処理はダイレクトに扱われているのですが、どうもprint文のような処理だと間にバッファリングが入るせいなのか時差を感じる時があります。(内部的にprint文にデータは渡して処理しているはずなのに、画面にはまだ反映されない。ということが起こる。)

こういった部分に少々クセのようなものを感じますが、Eclipseのリモートデバッグ環境を利用するならスクリプトエディタのログ出力よりEclipse の変数情報表示が重要です。
また、実際の処理を追う場合もファイルに書き出したものを対象と捉えているので、その辺りも問題は少ないと思っています。
そのあたりを踏まえつつ、今度はモジュール(scriptsフォルダに置いたファイル)の処理をデバッグする手順を説明します。

ひとまず、デバッグサーバの握っている処理をMayaに返してあげたいので、トレース終了のボタンを押しましょう。
start_pydev_server06

モジュールを止めてみる

(ここまで読んで下さった方、もう一頑張りお願いいたします。)

ようやく本番ですよ。
まずは、デバッグしたいモジュールを用意します。

今回は簡単なDAG階層追跡とPySideでのツリー表示を行うサンプルを用意しました。
以下の3ファイルを、ドキュメント\maya\scriptsフォルダ内に作成したsampleUIフォルダに収めて下さい。

ファイル名は、上から “__init__.py”, “testUI.py”, “formUI.ui” です。
※Python記述時の文字コードは utf-8 を推奨します。(ただMaya向けにということなら、cp932を別途推奨します。)

>> __init__.py

# -*- coding: utf-8 -*-

import os.path
import sys
import PySide.QtGui as QtGui

import testUI


# implementation: dialog basic

def loadUi(uifile, parent=None, baseinstance=None):
    
    '''
    load .ui-file
    '''
    
    from PySide import QtCore
    from PySide.QtUiTools import QUiLoader

    loader = QUiLoader(baseinstance)
    ui = loader.load(uifile, parent)
    QtCore.QMetaObject.connectSlotsByName(ui)
    return ui


class formUI(QtGui.QDialog):
    
    def __init__(self, app=None):
        """
        init
        """
        super(formUI,self).__init__()
        
        self.ui = None
        self.app = app
        self.conf = None
        
        self.setWindowTitle('formUI')
        
        #load UI
        self.ui = loadUi(os.path.join(os.path.dirname(__file__), 'formUI.ui'), self)
        
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.ui)
        self.setLayout(layout)
        
        
        # implementation: user dialog
        self.testUI = testUI.TestUI(self.ui)
        
        
    def __canExit(self):
        """
        user select(exit or cancel)
        """
        return self.msg( u'', u'close this dialog?', QtGui.QMessageBox.Question, QtGui.QMessageBox.Yes|QtGui.QMessageBox.No, QtGui.QMessageBox.No)
        
        
    def reject(self):
        """
        catch reject
        """
        
        if self.__canExit() == QtGui.QMessageBox.Yes:
            #dialog close
            super(formUI,self).reject()
            
        else:
            #close cancel
            pass
            
            
    def closeEvent(self, event):
        """
        catch close-event
        """
        
        event.ignore()
        if self.__canExit() == QtGui.QMessageBox.Yes:
            event.accept()
            
        else:
            event.ignore()
            
            
    def msg(self, msg, msg2, icon, buttons, defaultButton):
        """
        message print
        """
        
        msgBox = QtGui.QMessageBox()
        msgBox.setIcon(icon)
        msgBox.setText(msg)
        msgBox.setInformativeText(msg2)
        msgBox.setStandardButtons(buttons)
        msgBox.setDefaultButton(defaultButton)
        res = msgBox.exec_()
        return res


window = None
def start():
    
    global window
    
    parent = None
    window = formUI(parent)
    if window.ui:
        print 'formUI execute.'
        window.show()


if __name__ == '__main__':
    pass

>> testUI.py

# -*- coding: utf-8 -*-

from PySide import QtCore, QtGui
import maya.cmds as cmds

class TestUI(object):
    
    """
    testUI
    """
    
    def __init__(self, ui=None):
        
        self.ui = ui
        self.ui.pushButton.clicked.connect(self.__on_click_pb1)
        
        
    def __append(self, parent, value):
        item = QtGui.QTreeWidgetItem(parent, [value])
        return item
        
        
    def __on_click_pb1(self):
        nodes = list(set(cmds.ls(assemblies=True)) - set([u'persp', u'top', u'front', u'side']))
        for node in nodes:
            item = self.__append(self.ui.treeWidget, node)
            self.__tree_analyze(node, item)
            
            
    def __tree_analyze(self, parent, parentitem):
        children = cmds.listRelatives(parent)
        for child in children:
            childitem = self.__append(parentitem, child)
            self.__tree_analyze(child, childitem)

>> formUI.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Form</class>
 <widget class="QWidget" name="Form">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>694</width>
    <height>431</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout_2">
   <item>
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
      <widget class="QTreeWidget" name="treeWidget">
       <property name="dragDropMode">
        <enum>QAbstractItemView::NoDragDrop</enum>
       </property>
       <property name="defaultDropAction">
        <enum>Qt::IgnoreAction</enum>
       </property>
       <property name="selectionMode">
        <enum>QAbstractItemView::ExtendedSelection</enum>
       </property>
       <property name="rootIsDecorated">
        <bool>true</bool>
       </property>
       <property name="itemsExpandable">
        <bool>true</bool>
       </property>
       <property name="expandsOnDoubleClick">
        <bool>true</bool>
       </property>
       <property name="columnCount">
        <number>1</number>
       </property>
       <attribute name="headerDefaultSectionSize">
        <number>100</number>
       </attribute>
       <column>
        <property name="text">
         <string>node(s)</string>
        </property>
       </column>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="pushButton">
       <property name="text">
        <string>PushButton</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

それでは実際にデバッグしたいと思いますが、サンプルのプログラムは、シーンに置かれたDAGオブジェクトの階層構造を表示するものですので、まず何でもよいのでシーンにオブジェクトを配置してください。

次に、Eclipseを起動してデバッグサーバを開始させてください。
そして、以下のコードをスクリプトエディタに入力、実行します。

import pydevd
import sampleUI
import sampleUI.testUI as testUI
reload(pydevd)
reload(sampleUI)
reload(testUI)

pydevd.settrace()
sampleUI.start()

特に問題がなければ、pydevd.settrace()でスクリプトが停止。
Eclipseが開くべきファイルを要求してきますので、ここでデバッグしたいファイル(testUI.py)を指定します。
testUI.pyを開いたら、24行目にブレイクポイントを設定してください。
幾つか方法がありますが、24という数字のすぐ左側の空白をダブルクリックすれば、マークが付きます。

add_breakpoint

マークを付けたら、そのまま処理を続行するだけでよいので、F8キーを押して下さい。(F8は停止した処理を再開します。)
処理を再開したらデバッグ対象のUIを表示しますが、Mayaの裏に隠れていると思うのでタスクバーのMayaアイコンにカーソルを合わせてしばらく待ち、formUIというウィンドウを選びましょう。

resume_after

UIにはひとつボタンが付いているので、それを押して下さい。
押した直後に、Eclipseに処理が移りましたか?移っていれば、ちゃんとブレイクポイントが利いている証拠です。

add_breakpoint02

このように止めたい場所にブレイクポイントを設定してツールを実行し、Eclipseでデバッグという流れで作業を進めることができます。

では、またF8キーを押して処理を進めてみます。
すると、どうも思ったように動作しないのではないかと思います。(というのも、ちょっとバグを仕込んであります)
スクリプトエディタのログを見ると、tracebackで32行目に問題があるようです。

trackback01

ちなみに、僕は下のような階層を作っていますが、サンプルのUI上にはまったくそんな風には表示されていません。(処理途中でエラー停止したので、当たり前ではあるのですが…)

trackback02

とりあえず、一旦UIを終了して、もう一度1からデバッグをしたいと思います。

また24行目から、進めてみます。
F6を1回押して、nodesに値を収集してみます。そのままカーソルをnodesに合わせると、中身を確認できますが、僕の場合は’group1′, ‘pCylinder1’が取得されています。

trace01

F6を更に2回押し、F5を1回押すと、1つ下の関数に入りますので、更にF6を1回押します。そのままカーソルをchildrenに合わせると、中身を確認できますが、僕の場合は’pCube1′, ‘group2’が取得されています。

trace02

更に1つ下の階層に潜り、childrenを取得します。(F6を更に2回、F5を1回、F6を1回。)
僕の場合は’pCubeShape1’が取得されました。

先にこのプログラムの問題を明かしてしまうと、cmds.listRelatives関数は子階層の値が取れなかった場合に Noneを返すのですが、その事に気付かず当初このコードを書きました。
意図した訳でもないのですが、ちょうどよいデバッグサンプルになったわけです。

更に1つ下の階層に潜り、childrenを取得しようとするとどうなるのでしょうか?(F6を更に2回、F5を1回、F6を1回…)

「for child in None:」という処理になってしまうので、これが原因で先ほどのtraceback「TypeError: ‘NoneType’ object is not iterable」となった次第です。
ですので、for文を回す前に childrenが有効かチェックする必要がありました。
ここに、「if children:」の追加が必要です。
(また、これはもう一か所のforループ「for node in nodes:」にも当てはまるので、ここも修正します。)

修正したコードは、以下の通りです。
>> testUI.py

# -*- coding: utf-8 -*-

from PySide import QtCore, QtGui
import maya.cmds as cmds

class TestUI(object):
    
    """
    testUI
    """
    
    def __init__(self, ui=None):
        
        self.ui = ui
        self.ui.pushButton.clicked.connect(self.__on_click_pb1)
        
        
    def __append(self, parent, value):
        item = QtGui.QTreeWidgetItem(parent, [value])
        return item
        
        
    def __on_click_pb1(self):
        nodes = list(set(cmds.ls(assemblies=True)) - set([u'persp', u'top', u'front', u'side']))
        if nodes:
            for node in nodes:
                item = self.__append(self.ui.treeWidget, node)
                self.__tree_analyze(node, item)
                
                
    def __tree_analyze(self, parent, parentitem):
        children = cmds.listRelatives(parent)
        if children:
            for child in children:
                childitem = self.__append(parentitem, child)
                self.__tree_analyze(child, childitem)

コードを修正して、もう一度実行してみます。
今度は無事に動きました。

finish

このようにEclipse+PyDev(pydevd)のリモートデバッグ機能を使うと、意外と便利そうなデバッガ環境が手に入ります。
デバッグが苦痛で…という方。こういう機能の利用は如何でしょうか?


※免責事項※
本記事内で公開している全ての手法・コードの有用性、安全性について、当方は一切の保証を与えるものではありません。
これらのコードを使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご使用ください。

コメント

コメントはありません

コメントフォーム

コメントは承認制ですので、即時に反映されません。ご了承ください。

CAPTCHA


 

*