Ramblings about rigging, programming and games.

Pytest, VScode and Maya

June 26, 2022

Introduction

So you’re using VScode and you’re into unit testing but unfortunately for you, you’re a maya user and you have to resort to running your tests in the command line.

Rejoice! You can actually get the VSCode test explorer to work with mayapy though the setup can be a bit hacky.

Info

This blog post assumes you’re running Windows, Python 3 and Maya 2022+ But you should be able to adapt this for any other OS, Python 2 and earlier versions of Maya (And likely other test libraries too.).

Maya Project Structure

You’ll need a project folder structured as a maya module looking similar to this. The actual template project can be found here on github.

maya-pytest-template/
├── .venv/
├── .vscode/
│   └── settings.json
├── scripts/
│   ├── maya_pytest_template/
│   │   └── __init__.py
│   └── userSetup.py
├── tests/
│   ├── __init__.py
│   ├── test_maya_pytest_template.py
│   └── conftest.py
├── mayapy.bat
├── pytest.bat
└── maya_pytest_template.mod

If you've worked with maya modules before, this should look pretty familiar with a few exceptions we'll cover in a minute.
If you haven't you can read up on that more here:

Setting up Pytest to work with Maya

Create a virtualenv

Let’s first create a virtualenv and install pytest in it. In a terminal type this:

python -m venv .venv
.venv/scripts/activate
pip install pytest

Setup mayapy.bat

Now let's create a file called mayapy.bat and set its content to this.

1rem mayapy.bat
2
3@ECHO OFF
4
5set MAYA_APP_DIR=%~dp0\temp\maya
6set MAYA_MODULE_PATH=%~dp0;%MAYA_MODULE_PATH%
7set PYTHONPATH=%~dp0\.venv\Lib\site-packages;%PYTHONPATH%
8
9for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\Maya\2023\Setup\InstallPath" /v MAYA_INSTALL_LOCATION') do set "MAYA_LOCATION=%%~b"
10"%MAYA_LOCATION%\bin\mayapy.exe" %*

Let’s break it down a bit:

We can now call mayapy.bat as we would any python interpreter except our environment is loaded properly:

./mayapy.bat -m site
sys.path = [
    'D:\\projects\\maya\\maya-pytest-template', <─────────────────────────────────┬── our project is properly added to the PYTHONPATH
    'D:\\projects\\maya\\maya-pytest-template\\.venv\\Lib\\site-packages', <──────┘
    ...
]
...

Initialize maya.standalone

While we can call mayapy.bat -m pytest to run our tests, they currently fail because maya.standalone is not initialized yet.
In this case, the tests fail because our Maya Module is not yet loaded:

./mayapy.bat -m pytest tests
====================================================== test session starts ======================================================
platform win32 -- Python 3.9.7, pytest-7.1.2, pluggy-1.0.0
rootdir: D:/projects/maya/maya-pytest-template
collected 0 items / 1 error

============================================================ ERRORS =============================================================
______________________________________ ERROR collecting tests/test_maya_pytest_template.py ______________________________________
ImportError while importing test module 'D:/projects/maya/maya-pytest-template/tests/test_maya_pytest_template.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
C:/Program Files/Autodesk/Maya2023/Python/lib/importlib/__init__.py:127: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tests/test_maya_pytest_template.py:5: in <module>
    import maya_pytest_template
E   ModuleNotFoundError: No module named 'maya_pytest_template'
==================================================== short test summary info ====================================================
ERROR tests/test_maya_pytest_template.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
======================================================= 1 error in 0.14s ========================================================

Pytest has a couple hooks that let us run some code whenever the test sessions start and end, let's set those up.
Let's write this in out conftest.py:

# tests/conftest.py

import maya.standalone
import pytest

def pytest_sessionstart(session: pytest.Session):
    maya.standalone.initialize()

def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
    maya.standalone.uninitialize()

Let’s try again.

.\mayapy.bat -m pytest tests
====================================================== test session starts ======================================================
platform win32 -- Python 3.9.7, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\projects\maya\maya-pytest-template
collected 2 items

tests\test_maya_pytest_template.py ..                                                                                      [100%] 

======================================================= 2 passed in 0.16s ======================================================= 

Success! 🎉

Note

There is a slight difference when calling pytest as a module rather than using the pytest command directly but this should have little to no impact how things work.

Making it work in VSCode

The first thing we need to do is to let VScode know that we want to use pytest as our testing library.
Let’s add this 3 lines in our workspace settings:

// .vscode/settings.json

{
  "python.testing.unittestEnabled": false,
  "python.testing.pytestEnabled": true,
  "python.testing.pytestArgs": ["tests"]
}

Tip

You can also use the Python: Configure tests task from the command palette to automatically add these entries to your workspace settings.

Fixing Discovery Errors

If we now go to the testing tab we are greeted by a “Pytest Discovery Error”.

This is because we’re importing maya libraries in both our conftest.py and our different tests.
At this point, vscode is still using the interpreter from our virtualenv and has no knowledge of those libraries.
We will properly address this when we’ll want to actually run the tests but for now, let’s refactor our tests a bit so that they can be discovered by any python interpreter.

A simple way to address this problem is to add a safeguard around all our maya imports:

# tests/conftest.py

import pytest

# Whether the tests are running in mayapy or not.
HAS_MAYA: bool
try:
    import maya.standalone
except ImportError:
    HAS_MAYA = False
else:
    HAS_MAYA = True

def pytest_sessionstart(session: pytest.Session):
    # Initialize maya standalone only if it is running in mayapy.
    if HAS_MAYA:
        maya.standalone.initialize()

def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
    # Uninitialize maya standalone only if it is running in mayapy.
    if HAS_MAYA:
        maya.standalone.uninitialize()
# tests/test_maya_pytest_template.py

from .conftest import HAS_MAYA

# Safeguard maya imports to avoid test discovery errors
if HAS_MAYA:
    import maya_pytest_template
    from maya import cmds

def test_version():
    assert maya_pytest_template.__version__ == "0.0.1"

def test_create_node():
    node = cmds.createNode("transform")
    assert node == "transform1"

And just like that, we have our tests discovered! But we can’t run them yet.

Running the tests

Now is the time where we need to make VScode actually use mayapy.

Unfortunately we can’t specify a different interpreter for tests than for the rest of VScode.
And since our dependencies exist in our virtualenv - not in mayapy - we definitely want to keep that interpreter as the one used for most of vscode’s.

Luckily, vscode lets us use a custom pytest executable and we can hack around that to trick vscode into running our mayapy.bat script.

To do that, we’ll need to make another bat file, let’s call it pytest.bat:

rem pytest.bat

@ECHO OFF

".\mayapy.bat" -m pytest %*

Simple enough.

We can use pytest.bat like we would use the regular pytest executable now:

.\pytest.bat tests
====================================================== test session starts ======================================================
platform win32 -- Python 3.9.7, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\projects\maya\maya-pytest-template
collected 2 items

tests\test_maya_pytest_template.py ..                                                                                      [100%]

======================================================= 2 passed in 0.08s =======================================================

Cool!
Now let’s tell vscode to use our new pytest.bat file as its pytest executable.
In our workspace settings let’s add this line:

// .vscode/settings.json

{
  // Previous config ...
  "python.testing.pytestPath": "${workspaceFolder}/pytest.bat"
}

And voilà!

Tests Passing

Debugging the tests

Now that our tests are running, the last step is to be able to debug them within VScode as well.

This part is going to be less hacky as VScode has support for custom debug configurations through the launch.json file.

To generate a new configuration:

Now that the launch.json file has been created, we need to make a few modifications to it.
Here’s the final file we’ll use:

// .vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "MayaPy: Current File",
      "type": "python",
      "request": "launch",
      "program": "${file}",
      "console": "internalConsole",
      "justMyCode": true,
      "python": "${workspaceFolder}/mayapy.bat",
      "purpose": ["debug-test", "debug-in-terminal"]
    }
  ]
}

Here’s the modifications we made to the one vscode generated:

  1. Changed the name to MayaPy: Current File
  2. Set console to internalConsole to use the debug console instead of the terminal (Optional)
  3. Set python to use our mayapy.bat
  4. Set the purpose to ["debug-test", "debug-in-terminal"] This one does two things
    1. debug-test makes this configuration the one that vscode will use when debugging the tests.

    2. debug-in-terminal tells vscode to use this configuration when using the Debug Python File task.

      Debug Python File

      This one is not necessary for what we’re doing but it is a pretty handy feature and there’s no reason not to include it.

      Warning

      maya.standalone.initialize is not called automatically with this so you will need to manually call it in the file you’re debugging.

That’s all folks

We can now Discover, Run and Debug Maya tests as if we were real python developers… 🙃

Back to top