Maya Python3でインテリセンスを効かせたい

Python 3,Python,Maya

この記事はMaya Advent Calendar 2023の20日目の記事です

この記事ではMayaCommandのスタブファイルを作成についての流れを紹介します
コードをすべて紹介するにはコードの数が多すぎるため、かいつまんで何を行っていたのかの紹介になります。

できたもの

スタブファイルがほしい人はこちらからダウンロードしてください
VSCodeでの読み込み方法はREADMEに記述しています
僕のランチのために投げ銭お願いします(⋈◍>◡<◍)。✧♡

Maya-Python-IntelliSense

日本語対応もしました


初めに

スタブファイルを知らない方もいると思いますのでざっくり説明しますと、拡張子が.pyiのPythonの型情報などを記載してロジック部分を省略したファイルです。これをIDE上で使用することで型チェックや補完がうまく動作しないモジュールに対して、補完やチェックを有効にできるという便利なものです。

スタブとは、大規模なシステム開発の際に、完成済みのプログラムの動作を検証するための、完成していないプログラムの代用となるプログラムのことである。または、外部プログラムとの細かなインターフェース制御を引き受けるプログラムのことである。
引用先: スタブとは (stub): - IT用語辞典バイナリ

スタブファイルとは、クラス、変数、関数、そして最も重要なそれらの型を含む、Pythonモジュールのパブリックインターフェイスのスケルトンを含むファイルです。
ライブラリ(または任意のモジュール)のスタブファイルを書き、ライブラリモジュールと同じディレクトリに.pyiファイルとして保存します。あるいは、スタブ(.pyiファイル)をスタブ用に予約されたディレクトリ(例えば、myproject/stubs)に置きます。
引用先: mypy 1.7.1 documentation: Stub files


Maya Python3でインテリセンスを効かせたい

Maya2023以降スタブファイルがSDKにない


Autodesk_Maya_2022_5_Update_DEVKIT_Windowsにあるスタブファイル

pymelが任意インストールになってからMaya 2023以降のdevkitには、pymel含まれなくなったためMayaのスタブファイルが同梱されなくなりました。

そのため、2023以降のMayaを開発する際にIDE上でインテリセンスを効かせるには古いバージョンのdevkitを使うか、外部モジュールや拡張機能を使うといったような選択しになってきます。

VSCodeであればMayaPyが有名どころでしょうか?

とはいえ、このMayaPyも内部はMayaのDEVKITを使用したものになっているのでかなり古く、かつPython2系であるMaya2023たMaya2024はPython3でPython2と比べ型ヒントを使った開発は必須といっても過言ではない!!そんな中、Mayaのコマンドでインテリセンスが使えないのはものすごく不便です。

どうせならDocstringsも欲しいよな・・・

ということでスタブファイルを手動で作ってみた

本来であればmypy の stubgen コマンドを使用したりするんですがその場合docstringsが存在していないし...じゃあDocumentから作ればいいんじゃない?
そんな感じの見切り発車でスタート

HTMLを文字列として取得し、正規表現でtag消してとかしてたら恐ろしいほどネストが深くなったり、処理速度が尋常じゃなくやばくなったので、ちゃんと作ることにしました

開発環境の整備

作業を開始する前に開発環境を整えました。開発のベースはMaya 2024.2

開発環境

VSCode: 1.85.1
Python 3.11.6
NuGet: 6.4.0.123
Maya 2024.2 Help English

Maya Product HelpのダウンロードはDownload & Install Maya Product Help

Python Modules

black==23.3.0
flake8==6.0.0
isort==5.12.0
mypy==1.3.0
mypy-extensions==1.0.0
pyflakes==3.0.1
pyproject-flake8==6.0.0.post1
typing_extensions==4.7.1
pylint==2.17.4
pep8==1.7.1
autopep8==2.0.4
beautifulsoup4==4.12.2
pydantic==2.3.0
PyYAML==6.0.1

VSCode Extentions

全て現状リリースしているものの最新版

ms-python.black-formatter
ms-python.flake8
ms-python.isort
ms-python.mypy-type-checker
ms-python.python
ms-python.vscode-pylance

Python環境の自動構築

自前でビルドする人のすることを考え、Pythonの環境を自動構築できるようにしておきました。
とはいえ、Pythonの環境を準備してもらうのも大変なのでPython環境がないPCでも展開できるようにします。
NuGetを使って、pythonの展開とvenvの構築ができるようにします。
venvには上記に記述している、requirements.txtで必要なモジュールをpipインストールさせておきます。

現在展開したいるフォルダをVSCodeで開き、code-workspaceの作成、pyproject.tomlやsetting.jsonなど必要な設定をしておきます。

pyproject.tomlの詳しい説明はこちらを読んでください
設定したものは以下の通りです。

開発環境を整えたところでコーディングについて

ファイル構成

|-- .venv/
|-- .vscode/
|-- bin/
|-- src/
|   |-- create_pyi.py : pyiを作成するための実行部|
|   |-- create_pyi.yml : pyiを作成する際に参照するデータ群(処理を無視するファイルなど)
|   |-- maya2023.yml : Maya2023に関するデータ
|   |-- maya2024.yml : Maya2024に関するデータ
|   `-- models.py : Maya本体や各Functionに関するデータクラスなど
|--  pyproject.toml
|--  setup.bat

ソースコード

全てのコードを説明するのは大変ですので、ソースコードの確認はこちらから
Autodesk-Maya-Python-IntelliSense

ざっくり処理を説明するとMayaのPythonCommandのドキュメントは一つのコマンドごとに1つのHTMLを持っているため、すべてのHTMLを一つずつfor文で回します
その中で必要な要素をデータクラスなどに保持し、そのデータを元に関数を作り、アノテーションとDocstringsを作成するといった方法です

HTMLはh2タグごとに項目が分かれているので以下の項目を必要に応じてデータを取得、編集しています

  • hSynopsis
  • hReturn
  • hKeywords
  • hFlags
  • hExamples
  • hRelated
  • hNotes

引数やそのコマンドの説明が書かれたhSynopsisからdef addPP(attribute: string) -> list[string]:を作成するのですが
中にはMaya独自の型などもあるのでそういった本来存在しない方はtyping.NewTypeを使って新しい型を定義しています。addPPattributeの引数がstringになっています
Pythonではstrが文字列ですがstring = NewType("string", str)を定義し、stringという型を新しく定義しています。

現状は引数はLongNameしか使えませんが将来的にはShortNameを含めたり、ShortNameのみのスタブファイルを作れるようになど
欲しい人に合わせて切り替えられたりしてもいいかもなんて思って一応、データクラスにはshortNameを定義しています。

class ArgumentData:
    longName: str
    shortName: str
    type: str
    docs: str
    properties: list[str]

HTMLはそのまま文字列で使用すると恐ろしいことになります。そういう時はBeautifulSoupを使うと便利ですので今回のツールでも使用しています

(こんなこと言ってますがBeautifulSoupがめんどくさいから文字列から正規表現で...以下略)

for文で回す際にHTMLを文字列として取得し、BeasutifulSoupで読み込み

        ...

    def extract_html_content(self, file_path: Path | str) -> None:
        try:
            with open(file_path, "r", encoding="utf-8") as file:
                self.html_content = file.read()
        except Exception as e:
            raise e
        ...

    @property
    def soup(self) -> BeautifulSoup:
        return BeautifulSoup(self.html_content, "html.parser")

        ...
    def create_code_text(self) -> None:
        ...

        for iter in self.document_root.iterdir():
            self.function_name = iter.stem
            if iter.stem not in self.option.common.ignore:
                ...
                self.extract_html_content(iter)
                ...

そのsoupを元に、必要な要素を取得し、加工し、保持するようにしました

    @property
    def synopsis(self) -> Tag | NavigableString | None:
        synopsis_section = self.soup.find("a", {"name": HTags.hSynopsis.value}).find_parent("h2")
        return synopsis_section.find_next_sibling("p")

    def extract_table_data(self) -> None:
        tables = self.soup.find_all("table")
        ...

その時取得したデータや事前に必要な情報をデータクラスに格納することで
コードの作成や、バージョン、言語の対応がスムーズできるように簡単な設計をしています。
(言語対応はまだしてませんが将来的にするかも?)

処理のためのデータクラスについて

models.pyや各yamlファイルを使用して、Mayaクラスのデータクラスやそれらをまとめて持つModelクラスを用意し
そのオブジェクトをcreate_pyi.pyにあるCreateMayaCommandPYIクラスに読み込ませ、実際の処理をCreateMayaCommandPYIで行っています。
まず、データクラスに関してざっくり説明します。

models.py

models.pyはMaya本体や各Mayaコマンドに関するデータクラスなどを持っているモジュールです

class FunctionData: 
class ArgumentData:
class NewType(BaseModel):
class NewTypes(BaseModel):
class Common(BaseModel):
class Docs(BaseModel):
class Maya(BaseModel):
class IntelliSenseOptionModel(BaseModel):
class HTags(Enum):
class Arguments(BaseModel):

FunctionDataは各Mayaコマンドの持つ引数や、Docstrings, Web上にあるドキュメントのURLを持つクラス
NewTypeはMaya独自の形を定義する際に使用する情報を持ち、NewTypesNewTypeitemsとしてlist[NewType]という形で保持させています。
そういったようなデータクラスがたくさんあります

具体的な例を1つ挙げるとclass Maya(BaseModel)maya2024.ymlのデータを入れることで必要な要素をPythonのデータクラスとして扱えるようにしています

Maya2023.ymlMaya2024.ymlは各Mayaのバージョンに合わせてCloudhelpのURL、pythonのバージョンに合わせて必要なモジュール情報など必要なデータを保持しています

version: 2023
python: "3.9.7"
help url: help.autodesk.com/cloudhelp/2023/ENU/Maya-Tech-Docs/CommandsPython
imports:
  - from __future__ import annotations
documents:
  2023:
    en:
      autodesk maya user guide 2023=1=htm (ade 2.1)=en-2
    jp:
      autodesk maya user guide 2023=1=htm (ade 2.1)=ja-2

このデータをmodels.pyにあるclass Mayaが読み込むことでコーディングの際に便利に扱えるようになり、データを切り替えることで中身が変わるのでコード自体を変えずにバージョンの対応がスムーズに行くようにしています

from argparse import ArgumentParser
from enum import Enum
from pathlib import Path
from typing import Optional

from pydantic import BaseModel, Field, validator
from typing_extensions import Self

...python
class Docs(BaseModel):
    jp: str
    en: str

class Maya(BaseModel):
    version: int
    python: str
    help_url: Path = Field(alias="help url")
    imports: list[str]
    documents: dict[str, Docs]

    class Config:
        populate_by_name = True

    @validator("imports", pre=True)
    def create_imports_list(cls, v) -> list[str]:
        return v or []

    @validator("help_url", pre=True)
    def create_help_url(cls, v) -> Path:
        return Path(v)

    @validator("documents", pre=True)
    def create_help_url(cls, v) -> Path:
        docs: dict[str, Docs] = {}
        for key, value in v.items():
            docs[str(key)] = Docs(**value)
        return docs

class IntelliSenseOptionModel(BaseModel):
    common: Common
    maya: Maya
import re
from functools import cached_property
from pathlib import Path
from typing import Any

import autopep8
import yaml
from bs4 import BeautifulSoup, NavigableString, Tag

from models import ArgumentData, Arguments, FunctionData, HTags, IntelliSenseOptionModel

class CreateMayaCommandPYI:
    ...
    def __init__(
        self,
        document_root: str | Path,
        export_path: str | Path,
        language: str,
        version: str,
        option: IntelliSenseOptionModel,
    ) -> None:
        self.option = option
    ...

    @cached_property
    def cloudhelp_url(self) -> str:
        return f"https://{self.option.maya.help_url.as_posix()}"

if __name__ == "__main__":
    ...
    with open(create_pyi, "r") as file:
        data = yaml.safe_load(file)
    with open(maya, "r") as file:
        maya_data = yaml.safe_load(file)
    data["maya"] = maya_data

    option = IntelliSenseOptionModel(**data)

    mayacmd = CreateMayaCommandPYI(
        document_root=document_dir,
        export_path=export_path,
        language=language,
        version=version,
        option=IntelliSenseOptionModel(**data),
    )
    mayacmd.run()

Docstringsに記述するURLのアドレスを取得するときにcloudhelp_urlを使用していますが読み込むソースが変わることでcloudhelp_urlの取得する値が変わるようになどそういったときに
データクラスをいくつか用意しています。
create_pyi.ymlも同じデータクラスのためのデータでバージョン関係なく、主に共通で使用するものが含めています

正直コードが長かったりするためすべての紹介はできませんがざっくり行ったことは伝えられたのではないでしょうか?
もう少し詳しいことが知りたい人はGithubのほうを見てください、プルリクエストとかあればリクエストしてください


明日は COYOTE TAチームさんの[Mayaのリファレンス機能、 ちゃんと理解して使えば怖くない! -中編-]()です