├── tests ├── __init__.py ├── setup.cfg ├── conftest.py ├── test_decorators.py ├── test_conftest.py ├── test_node_properties.py ├── test_dag_node_instantiation.py ├── test_node_instantiation.py ├── test_node_manipulation.py ├── test_attribute_retrieval.py ├── test_node_naming.py ├── test_node_comparisons.py ├── test_dag_node_methods.py ├── test_attribute_properties.py ├── test_node_attribute.py ├── test_cmd.py ├── test_attribute_reference.py ├── test_attribute_value.py ├── test_attribute_connections.py ├── test_attribute_addition.py └── test_dag_node_properties.py ├── .gitattributes ├── requirements_2.7.txt ├── requirements_3.7.txt ├── .gitignore ├── bin ├── lint ├── gencmd ├── venvsetup ├── _test.py ├── test ├── _lint.py └── _gencmd.py ├── requirements_3.9.txt ├── src └── mayax │ ├── __init__.py │ ├── strtype.py │ ├── _about.py │ ├── exceptions.py │ ├── decorators.py │ ├── utils.py │ ├── math.py │ ├── api.py │ └── node.py ├── setup.cfg ├── LICENSE ├── install.py ├── pyproject.toml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /requirements_2.7.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | -------------------------------------------------------------------------------- /requirements_3.7.txt: -------------------------------------------------------------------------------- 1 | -r requirements_2.7.txt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .venv 3 | __pycache__ 4 | *.pyc 5 | -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source .venv/3.9/Scripts/activate 6 | 7 | python bin/_lint.py 8 | -------------------------------------------------------------------------------- /bin/gencmd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source .venv/3.9/Scripts/activate 6 | 7 | python bin/_gencmd.py $@ 8 | -------------------------------------------------------------------------------- /requirements_3.9.txt: -------------------------------------------------------------------------------- 1 | -r requirements_3.7.txt 2 | 3 | black 4 | pycodestyle 5 | pydocstyle 6 | pylint 7 | 8 | beautifulsoup4 9 | -------------------------------------------------------------------------------- /src/mayax/__init__.py: -------------------------------------------------------------------------------- 1 | """Useful functionality to interact with Maya.""" 2 | 3 | from ._about import * 4 | from .api import * 5 | -------------------------------------------------------------------------------- /tests/setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max-line-length = 120 3 | 4 | [pydocstyle] 5 | inherit = true 6 | add-ignore = D100,D101,D102,D103,D104,D105,D106,D107 # missing docstrings 7 | -------------------------------------------------------------------------------- /src/mayax/strtype.py: -------------------------------------------------------------------------------- 1 | """Add a compatible string type for Python 2 and 3.""" 2 | 3 | # pylint: disable=invalid-name 4 | 5 | try: 6 | STR_TYPE = basestring 7 | except NameError: 8 | STR_TYPE = str 9 | -------------------------------------------------------------------------------- /src/mayax/_about.py: -------------------------------------------------------------------------------- 1 | """About info.""" 2 | 3 | __author__ = 'Adrian Chirieac' 4 | __version__ = '1.0.0' 5 | __license__ = 'MIT' 6 | 7 | __all__ = [ 8 | '__author__', 9 | '__version__', 10 | '__license__', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """pytest fixtures.""" 2 | 3 | import pytest 4 | 5 | from maya import cmds 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def mayaEmptyScene(): 10 | cmds.file(newFile=True, force=True) 11 | 12 | 13 | @pytest.fixture 14 | def scenePath(tmpdir): 15 | return '{}/scene.ma'.format(tmpdir) 16 | -------------------------------------------------------------------------------- /src/mayax/exceptions.py: -------------------------------------------------------------------------------- 1 | """Maya exceptions.""" 2 | 3 | 4 | class MayaError(Exception): 5 | """Base class for all Maya exceptions.""" 6 | 7 | 8 | class MayaNodeError(MayaError): 9 | """Raised by the `Node` class.""" 10 | 11 | 12 | class MayaAttributeError(MayaError, AttributeError): 13 | """Raised by the `Attribute` class.""" 14 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_undoManyOperationsAtOnce(): 7 | @mx.undoable 8 | def createCubes(): 9 | cmds.polyCube() 10 | cmds.polyCube() 11 | cmds.polyCube() 12 | 13 | createCubes() 14 | assert len(cmds.ls(type='polyCube')) == 3 15 | cmds.undo() 16 | assert not cmds.ls(type='polyCube') 17 | -------------------------------------------------------------------------------- /tests/test_conftest.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | 4 | # Every test should run within an empty Maya scene ------------------------------------------------- 5 | 6 | 7 | def test_should_createPolyCube(): 8 | assert cmds.polyCube() 9 | 10 | 11 | def test_should_haveEmptyScene(): 12 | assert not cmds.ls(type='polyCube') 13 | 14 | 15 | # -------------------------------------------------------------------------------------------------- 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max-line-length = 100 3 | ignore = E203, W503 4 | 5 | 6 | [pydocstyle] 7 | inherit = false 8 | match = .*\.py # change the default to include test files 9 | convention = numpy 10 | add-ignore = D413, # Missing blank line after last section 11 | D106, # Missing docstring in public nested class 12 | D105, # Missing docstring in magic method 13 | D107, # Missing docstring in __init__ 14 | -------------------------------------------------------------------------------- /src/mayax/decorators.py: -------------------------------------------------------------------------------- 1 | """Decorators.""" 2 | 3 | import functools 4 | 5 | from maya import cmds 6 | 7 | 8 | def undoable(func): 9 | """Allow an entire function/method to be undoed.""" 10 | 11 | @functools.wraps(func) 12 | def funcWrapper(*args, **kwargs): 13 | try: 14 | cmds.undoInfo(openChunk=True, chunkName=func.__name__) 15 | return func(*args, **kwargs) 16 | finally: 17 | cmds.undoInfo(closeChunk=True, chunkName=func.__name__) 18 | 19 | return funcWrapper 20 | -------------------------------------------------------------------------------- /tests/test_node_properties.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_checkNodeExistence_given_validNode(): 7 | node = mx.Node(cmds.polyCube(name='theCube')[0]) 8 | 9 | assert node.exists 10 | 11 | 12 | def test_should_checkNodeExistence_given_deletedNode(): 13 | node = mx.Node(cmds.polyCube(name='theCube')[0]) 14 | 15 | cmds.delete('theCube') 16 | 17 | assert not node.exists 18 | 19 | 20 | def test_should_getNodeType(): 21 | node = mx.Node(cmds.createNode('multMatrix')) 22 | assert node.type == 'multMatrix' 23 | -------------------------------------------------------------------------------- /src/mayax/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions that makes working with Maya easier.""" 2 | 3 | import os 4 | 5 | from maya import cmds 6 | 7 | 8 | def getCurrentProjectDirectory(): 9 | """Return the path for the selected project directory.""" 10 | return cmds.workspace(query=True, rootDirectory=True) 11 | 12 | 13 | def getModulesDirectory(): 14 | """Return the path for the modules directory.""" 15 | return os.path.normpath( 16 | os.path.join( 17 | cmds.internalVar(userAppDir=True), 18 | cmds.about(version=True), 19 | 'modules', 20 | ) 21 | ) 22 | -------------------------------------------------------------------------------- /tests/test_dag_node_instantiation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | 5 | import mayax as mx 6 | 7 | 8 | def test_should_getInstanceFromScene(): 9 | cmds.createNode('unknownDag', name='dagNode') 10 | assert isinstance(mx.DagNode('dagNode'), mx.DagNode) 11 | 12 | 13 | def test_should_getInstanceFromScene_when_usingNodeClass(): 14 | cmds.createNode('unknownDag', name='dagNode') 15 | assert isinstance(mx.Node('dagNode'), mx.DagNode) 16 | 17 | 18 | def test_should_raiseMayaNodeError_given_none(): 19 | with pytest.raises(mx.MayaNodeError): 20 | mx.DagNode(None) 21 | 22 | 23 | def test_should_raiseMayaNodeError_given_dgNode(): 24 | with pytest.raises(mx.MayaNodeError): 25 | mx.DagNode('lambert1') 26 | -------------------------------------------------------------------------------- /src/mayax/math.py: -------------------------------------------------------------------------------- 1 | """Useful math classes and functions.""" 2 | 3 | import maya.api.OpenMaya as om 4 | 5 | 6 | class Vector(om.MVector): 7 | """3D vector with double-precision coordinates. 8 | 9 | See ``maya.api.OpenMaya.MVector`` for more info. 10 | """ 11 | 12 | 13 | class Matrix(om.MMatrix): 14 | """4x4 matrix with double-precision elements. 15 | 16 | See ``maya.api.OpenMaya.MMatrix`` for more info. 17 | """ 18 | 19 | 20 | class Quaternion(om.MQuaternion): 21 | """Quaternion math. 22 | 23 | See ``maya.api.OpenMaya.MQuaternion`` for more info. 24 | """ 25 | 26 | 27 | class EulerRotation(om.MEulerRotation): 28 | """Euler rotation math. 29 | 30 | See ``maya.api.OpenMaya.MEulerRotation`` for more info. 31 | """ 32 | -------------------------------------------------------------------------------- /src/mayax/api.py: -------------------------------------------------------------------------------- 1 | """The API.""" 2 | 3 | from .decorators import undoable 4 | from .exceptions import ( 5 | MayaAttributeError, 6 | MayaError, 7 | MayaNodeError, 8 | ) 9 | from .math import ( 10 | EulerRotation, 11 | Matrix, 12 | Quaternion, 13 | Vector, 14 | ) 15 | from .node import ( 16 | Attribute, 17 | DagNode, 18 | Node, 19 | ) 20 | from .strtype import STR_TYPE 21 | from .utils import ( 22 | getCurrentProjectDirectory, 23 | getModulesDirectory, 24 | ) 25 | from . import cmd 26 | 27 | __all__ = [ 28 | 'Attribute', 29 | 30 | 'undoable', 31 | 32 | 'MayaAttributeError', 33 | 'MayaError', 34 | 'MayaNodeError', 35 | 36 | 'EulerRotation', 37 | 'Matrix', 38 | 'Quaternion', 39 | 'Vector', 40 | 41 | 'DagNode', 42 | "Node", 43 | 44 | 'STR_TYPE', 45 | 46 | 'getCurrentProjectDirectory', 47 | 'getModulesDirectory', 48 | 49 | 'cmd', 50 | ] 51 | -------------------------------------------------------------------------------- /tests/test_node_instantiation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | from maya.api import OpenMaya as om 5 | 6 | import mayax as mx 7 | 8 | 9 | def test_should_getInstanceFromScene_given_name(): 10 | cmds.createNode('transform') 11 | assert mx.Node('transform1') 12 | 13 | 14 | def test_should_getInstanceFromScene_given_node(): 15 | cmds.createNode('transform') 16 | assert mx.Node(mx.Node('transform1')) 17 | 18 | 19 | def test_should_getInstanceFromScene_given_mobject(): 20 | cmds.createNode('transform') 21 | mobject = om.MGlobal.getSelectionListByName('transform1').getDependNode(0) 22 | assert mx.Node(mobject) 23 | 24 | 25 | def test_should_raiseMayaNodeError_given_none(): 26 | with pytest.raises(mx.MayaNodeError): 27 | mx.Node(None) 28 | 29 | 30 | def test_should_raiseMayaNodeError_given_nonexistentName(): 31 | with pytest.raises(mx.MayaNodeError): 32 | mx.Node('nonexistentNodeName') 33 | -------------------------------------------------------------------------------- /bin/venvsetup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | VENV_DIR=".venv" 6 | 7 | VENV_DIR_PY39="${VENV_DIR}/3.9" 8 | VENV_DIR_PY37="${VENV_DIR}/3.7" 9 | VENV_DIR_PY27="${VENV_DIR}/2.7" 10 | 11 | PIP_PATH_PY39="${VENV_DIR_PY39}/Scripts/pip" 12 | PIP_PATH_PY37="${VENV_DIR_PY37}/Scripts/pip" 13 | PIP_PATH_PY27="${VENV_DIR_PY27}/Scripts/pip" 14 | 15 | read -p "Python 3.9 (path): " python39 16 | read -p "Python 3.7 (path): " python37 17 | read -p "Python 2.7 (path): " python27 18 | 19 | rm -rf .venv 20 | 21 | echo -e "\nCreating virtual environment for Python 3.9...\n" 22 | 23 | "${python39}" -m venv "${VENV_DIR_PY39}" 24 | "${PIP_PATH_PY39}" install -r requirements_3.9.txt 25 | 26 | echo -e "\nCreating virtual environment for Python 3.7...\n" 27 | 28 | "${python37}" -m venv "${VENV_DIR_PY37}" 29 | "${PIP_PATH_PY37}" install -r requirements_3.7.txt 30 | 31 | echo -e "\nCreating virtual environment for Python 2.7...\n" 32 | 33 | virtualenv "${VENV_DIR_PY27}" --python="${python27}" 34 | "${PIP_PATH_PY27}" install -r requirements_2.7.txt 35 | 36 | echo -e "\nDONE\n" 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Adrian Chirieac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/_test.py: -------------------------------------------------------------------------------- 1 | """Test the code using the Maya's Python interpreter (mayapy).""" 2 | 3 | import os 4 | import sys 5 | 6 | SRC_DIR = os.environ['SRC_DIR'] 7 | PYTEST_LOCATION = os.environ['PYTEST_LOCATION'] 8 | PYTEST_TEMP_DIR = os.environ['PYTEST_TEMP_DIR'] 9 | MAYA_APP_DIR = os.environ['MAYA_APP_DIR'] 10 | 11 | 12 | def main(): 13 | """Prepare the testing environment.""" 14 | sys.path.append(SRC_DIR) 15 | sys.path.append(PYTEST_LOCATION) 16 | sys.path.append(MAYA_APP_DIR) 17 | 18 | runTests(sys.argv[1:]) 19 | 20 | 21 | def runTests(args): 22 | """Run the tests.""" 23 | import pytest 24 | import maya.standalone 25 | 26 | with open(os.path.join(MAYA_APP_DIR, 'userSetup.py'), 'w') as setupFile: 27 | setupFile.write( 28 | '\n'.join( 29 | [ 30 | 'from maya import cmds', 31 | 'cmds.loadPlugin("quatNodes")', 32 | ] 33 | ) 34 | ) 35 | 36 | maya.standalone.initialize(name='mayax_test') 37 | 38 | result = pytest.main(['--basetemp', PYTEST_TEMP_DIR] + args) 39 | 40 | maya.standalone.uninitialize() 41 | 42 | sys.exit(result) 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | """Drop this file in a Maya viewport to install MayaX.""" 2 | 3 | import os 4 | import sys 5 | 6 | from maya import cmds 7 | 8 | 9 | def onMayaDroppedPythonFile(*_): 10 | """Install when the file is dropped in the viewport.""" 11 | rootPath = os.path.dirname(os.path.realpath(__file__)) 12 | sourcePath = os.path.join(rootPath, 'src') 13 | mayaModulesPath = os.path.normpath( 14 | os.path.join( 15 | cmds.internalVar(userAppDir=True), 16 | cmds.about(version=True), 17 | 'modules', 18 | ) 19 | ) 20 | moduleFilename = os.path.join(mayaModulesPath, 'mayax.mod') 21 | 22 | if sourcePath not in sys.path: 23 | sys.path.append(sourcePath) 24 | 25 | from mayax import __version__ 26 | 27 | if not os.path.exists(mayaModulesPath): 28 | os.makedirs(mayaModulesPath) 29 | 30 | with open(moduleFilename, mode='w') as moduleFile: 31 | moduleContent = '\n'.join( 32 | [ 33 | '+ MayaX {} {}'.format(__version__, rootPath), 34 | 'scripts: src', 35 | ] 36 | ) 37 | 38 | moduleFile.write(moduleContent) 39 | 40 | cmds.confirmDialog( 41 | title='MayaX', 42 | message='MayaX was installed.', 43 | button='OK', 44 | icon='information', 45 | ) 46 | -------------------------------------------------------------------------------- /tests/test_node_manipulation.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_deleteNodeFromScene(): 7 | cmds.createNode('transform') 8 | assert cmds.ls('transform1') 9 | mx.Node('transform1').delete() 10 | assert not cmds.ls('transform1') 11 | 12 | 13 | def test_should_duplicateNode(): 14 | cmds.createNode('transform') 15 | assert cmds.ls('transform1') 16 | assert not cmds.ls('transform2') 17 | duplicatedNode = mx.Node('transform1').duplicate() 18 | assert isinstance(duplicatedNode, mx.Node) 19 | assert cmds.ls('transform1') 20 | assert cmds.ls('transform2') 21 | 22 | 23 | def test_should_duplicateNode_given_name(): 24 | cmds.createNode('transform') 25 | assert cmds.ls('transform1') 26 | assert not cmds.ls('duplicatedTransform') 27 | duplicatedNode = mx.Node('transform1').duplicate(name='duplicatedTransform') 28 | assert isinstance(duplicatedNode, mx.Node) 29 | assert cmds.ls('transform1') 30 | assert cmds.ls('duplicatedTransform') 31 | 32 | 33 | def test_should_selectNode(): 34 | cmds.createNode('transform') 35 | cmds.createNode('transform') 36 | cmds.select(clear=True) 37 | assert not cmds.ls(selection=True) 38 | mx.Node('transform1').select() 39 | assert len(cmds.ls(selection=True)) == 1 40 | assert cmds.ls(selection=True)[0] == 'transform1' 41 | 42 | 43 | def test_should_addNodeToActiveSelection(): 44 | cmds.createNode('transform') 45 | cmds.createNode('transform') 46 | assert len(cmds.ls(selection=True)) == 1 47 | assert cmds.ls(selection=True)[0] == 'transform2' 48 | mx.Node('transform1').select(add=True) 49 | assert len(cmds.ls(selection=True)) == 2 50 | assert cmds.ls(selection=True)[0] == 'transform2' 51 | assert cmds.ls(selection=True)[1] == 'transform1' 52 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # args: 4 | # -maya 5 | # -k 6 | # -cov (coverage) 7 | 8 | set -e 9 | 10 | MAYA_LOCATION="C:/Program Files/Autodesk" 11 | 12 | export SRC_DIR=$(realpath "src") 13 | export TEST_DATA_DIR=$(realpath "_test_data") 14 | 15 | export MAYA_APP_DIR="${TEST_DATA_DIR}/maya" 16 | export PYTEST_TEMP_DIR="${TEST_DATA_DIR}/pytest" 17 | 18 | BLUE="\033[0;94m" 19 | NOCOLOR="\033[0m" 20 | BOLD="\033[1m" 21 | 22 | # ------------------------------------------------------------------------------ 23 | 24 | mayaVersion="" 25 | pytestArgs="" 26 | 27 | while [[ $# -gt 0 ]] 28 | do 29 | argName=$1; shift 30 | 31 | if [[ $1 && $1 != -* ]] 32 | then 33 | argValue=$1; shift 34 | fi 35 | 36 | if [[ "${argName}" == "-maya" ]] 37 | then 38 | mayaVersion="${argValue}" 39 | elif [[ "${argName}" == "-cov" ]] 40 | then 41 | pytestArgs="${pytestArgs} --cov mayax --cov-branch --cov-report html" 42 | else 43 | pytestArgs="${pytestArgs} ${argName} ${argValue}" 44 | fi 45 | done 46 | 47 | # ------------------------------------------------------------------------------ 48 | 49 | testMaya() 50 | { 51 | mayaVersion=$1 52 | mayapy="${MAYA_LOCATION}/Maya${mayaVersion}/bin/mayapy.exe" 53 | 54 | echo "" 55 | echo -e "${BLUE}-------------------------${NOCOLOR}" 56 | echo -e "${BLUE}--- Testing Maya ${BOLD}${mayaVersion}${BLUE} ---${NOCOLOR}" 57 | echo -e "${BLUE}-------------------------${NOCOLOR}" 58 | echo "" 59 | 60 | rm -rf "${TEST_DATA_DIR}" 61 | 62 | mkdir "${TEST_DATA_DIR}" 63 | mkdir "${MAYA_APP_DIR}" 64 | mkdir "${PYTEST_TEMP_DIR}" 65 | 66 | pyVersion=$("${mayapy}" -c "import sys; print('{}.{}'.format(sys.version_info[0], sys.version_info[1]))") 67 | 68 | export PYTEST_LOCATION=$(realpath ".venv/${pyVersion}/Lib/site-packages") 69 | 70 | "${mayapy}" bin/_test.py ${pytestArgs} 71 | 72 | rm -r "${TEST_DATA_DIR}" 73 | } 74 | 75 | if [[ "${mayaVersion}" == "" ]] 76 | then 77 | testMaya "2019" 78 | testMaya "2020" 79 | testMaya "2022" 80 | testMaya "2023" 81 | else 82 | testMaya "${mayaVersion}" 83 | fi 84 | -------------------------------------------------------------------------------- /tests/test_attribute_retrieval.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | 5 | import mayax as mx 6 | 7 | 8 | def test_should_getInstance_given_node_and_attributeName(): 9 | node = mx.Node(cmds.createNode('transform')) 10 | assert mx.Attribute(node, 'translate') 11 | 12 | 13 | def test_should_getInstance_given_nodeName_and_attributeName(): 14 | node = mx.Node(cmds.createNode('transform')) 15 | assert mx.Attribute('transform1', 'translate') 16 | assert mx.Attribute(node.uniqueName, 'translate') 17 | 18 | 19 | def test_should_getInstance_given_attributeFullName(): 20 | mx.Node(cmds.createNode('transform', name='node')) 21 | assert mx.Attribute('node.translate') 22 | 23 | 24 | def test_should_getInstance_given_transformNodeName_and_shapeAttributeName(): 25 | locatorName = cmds.spaceLocator()[0] 26 | assert mx.Attribute(locatorName, 'localPositionX') 27 | 28 | 29 | def test_should_getInstance_given_arrayAttributeName(): 30 | node = mx.Node(cmds.createNode('multMatrix')) 31 | assert mx.Attribute(node, 'matrixIn') 32 | assert mx.Attribute(node, 'matrixIn').type == 'TdataCompound' 33 | 34 | 35 | def test_should_getInstance_given_arrayAttributeName_and_index(): 36 | node = mx.Node(cmds.createNode('multMatrix')) 37 | assert mx.Attribute(node, 'matrixIn[0]') 38 | 39 | 40 | def test_should_raiseMayaNodeError_given_nonexistentNodeName(): 41 | with pytest.raises(mx.MayaNodeError): 42 | assert mx.Attribute('nonexistentNode', 'translate') 43 | 44 | 45 | def test_should_raiseMayaAttributeError_given_onlyNodeName(): 46 | node = mx.Node(cmds.createNode('transform')) 47 | with pytest.raises(mx.MayaAttributeError): 48 | assert mx.Attribute(node.uniqueName) 49 | 50 | 51 | def test_should_raiseMayaAttributeError_given_nonexistentAttributeName(): 52 | node = mx.Node(cmds.createNode('transform')) 53 | with pytest.raises(mx.MayaAttributeError): 54 | assert mx.Attribute(node.uniqueName, 'nonexistentAttrName') 55 | 56 | 57 | def test_should_raiseAttributeError_given_nonexistentAttributeName(): 58 | node = mx.Node(cmds.createNode('transform')) 59 | with pytest.raises(AttributeError): 60 | assert mx.Attribute(node.uniqueName, 'nonexistentAttrName') 61 | -------------------------------------------------------------------------------- /tests/test_node_naming.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | 5 | import mayax as mx 6 | 7 | 8 | def test_should_getName(): 9 | cmds.createNode('transform') 10 | assert mx.Node('transform1').name == 'transform1' 11 | 12 | 13 | def test_should_getName_given_dgNode(): 14 | assert mx.Node('lambert1').name == 'lambert1' 15 | 16 | 17 | def test_should_getName_given_nodeWithNonUniqueName(): 18 | cmds.createNode('transform', name='parent1') 19 | cmds.createNode('transform', name='child', parent='parent1') 20 | cmds.createNode('transform', name='parent2') 21 | cmds.createNode('transform', name='child', parent='parent2') 22 | assert mx.Node('parent1|child').name == 'child' 23 | 24 | 25 | def test_should_getUniqueName(): 26 | cmds.createNode('transform', name='parent1') 27 | cmds.createNode('transform', name='child', parent='parent1') 28 | node = mx.Node('child') 29 | 30 | assert node.name == 'child' 31 | assert node.uniqueName == 'child' 32 | 33 | cmds.createNode('transform', name='parent2') 34 | cmds.createNode('transform', name='child', parent='parent2') 35 | 36 | assert node.name == 'child' 37 | assert node.uniqueName == 'parent1|child' 38 | 39 | 40 | def test_should_setName(): 41 | cmds.createNode('transform') 42 | node = mx.Node('transform1') 43 | assert node.name == 'transform1' 44 | node.name = 'group' 45 | assert node.name == 'group' 46 | 47 | 48 | def test_should_setNameUsingRenameMethod(): 49 | cmds.createNode('transform') 50 | node = mx.Node('transform1') 51 | assert node.name == 'transform1' 52 | node.rename('group') 53 | assert node.name == 'group' 54 | 55 | 56 | def test_should_renameUsingExtraFlags(): 57 | cmds.polyCube() 58 | cube = mx.Node('pCube1') 59 | cubeShape = mx.Node('pCubeShape1') 60 | 61 | assert cube.name == 'pCube1' 62 | assert cubeShape.name == 'pCubeShape1' 63 | 64 | cube.rename('Cube', ignoreShape=True) 65 | 66 | assert cube.name == 'Cube' 67 | assert cubeShape.name == 'pCubeShape1' 68 | 69 | 70 | def test_should_raiseMayaNodeError_when_nodeBecomesInvalid(): 71 | cmds.createNode('transform') 72 | node = mx.Node('transform1') 73 | cmds.delete('transform1') 74 | with pytest.raises(mx.MayaNodeError): 75 | node.name = 'newName' 76 | -------------------------------------------------------------------------------- /bin/_lint.py: -------------------------------------------------------------------------------- 1 | """Lint the code.""" 2 | 3 | import os 4 | import colorama 5 | 6 | 7 | def main(): 8 | """Run linting commands.""" 9 | print('\n=== pylint: mayax =============================================================\n') 10 | pylint1 = os.system('pylint src/mayax') 11 | print('\n=== pylint: tests =============================================================\n') 12 | pylint2 = os.system('pylint tests') 13 | 14 | print('\n=== pycodestyle: mayax ========================================================\n') 15 | pycodestyle1 = os.system('pycodestyle src/mayax') 16 | print('\n=== pycodestyle: tests ========================================================\n') 17 | pycodestyle2 = os.system('pycodestyle tests') 18 | 19 | print('\n=== pydocstyle: mayax =========================================================\n') 20 | pydocstyle1 = os.system('pydocstyle src/mayax') 21 | print('\n=== pydocstyle: tests =========================================================\n') 22 | pydocstyle2 = os.system('pydocstyle tests') 23 | 24 | print('\n===============================================================================\n') 25 | 26 | colorama.init() 27 | 28 | print( 29 | 'pylint: {}mayax ({}){}, {}tests ({})'.format( 30 | colorama.Fore.RED if pylint1 else colorama.Fore.GREEN, 31 | 'FAIL' if pylint1 else 'OK', 32 | colorama.Fore.RESET, 33 | colorama.Fore.RED if pylint2 else colorama.Fore.GREEN, 34 | 'FAIL' if pylint2 else 'OK', 35 | ) 36 | ) 37 | 38 | print(colorama.Fore.RESET) 39 | 40 | print( 41 | 'pycodestyle: {}mayax ({}){}, {}tests ({})'.format( 42 | colorama.Fore.RED if pycodestyle1 else colorama.Fore.GREEN, 43 | 'FAIL' if pycodestyle1 else 'OK', 44 | colorama.Fore.RESET, 45 | colorama.Fore.RED if pycodestyle2 else colorama.Fore.GREEN, 46 | 'FAIL' if pycodestyle2 else 'OK', 47 | ) 48 | ) 49 | 50 | print(colorama.Fore.RESET) 51 | 52 | print( 53 | 'pydocstyle: {}mayax ({}){}, {}tests ({})'.format( 54 | colorama.Fore.RED if pydocstyle1 else colorama.Fore.GREEN, 55 | 'FAIL' if pydocstyle1 else 'OK', 56 | colorama.Fore.RESET, 57 | colorama.Fore.RED if pydocstyle2 else colorama.Fore.GREEN, 58 | 'FAIL' if pydocstyle2 else 'OK', 59 | ) 60 | ) 61 | 62 | 63 | if __name__ == '__main__': 64 | main() 65 | -------------------------------------------------------------------------------- /tests/test_node_comparisons.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_compareNode_given_self(): 7 | node = mx.Node(cmds.createNode('transform')) 8 | nodeSelf = node 9 | assert node == nodeSelf 10 | assert not bool(node != nodeSelf) 11 | 12 | 13 | def test_should_compareNode_given_sameNode(): 14 | node = mx.Node(cmds.createNode('transform')) 15 | sameNode = mx.Node(node.uniqueName) 16 | assert node == sameNode 17 | assert not bool(node != sameNode) 18 | 19 | 20 | def test_should_compareNode_given_sameNodeName(): 21 | node = mx.Node(cmds.createNode('transform')) 22 | assert node == node.uniqueName 23 | assert not bool(node != node.uniqueName) 24 | 25 | 26 | def test_should_compareNode_given_differentNode(): 27 | node1 = mx.Node(cmds.createNode('transform')) 28 | node2 = mx.Node(cmds.createNode('transform')) 29 | assert node1 != node2 30 | assert not bool(node1 == node2) 31 | 32 | 33 | def test_should_compareNode_given_differentNodeName(): 34 | node1 = mx.Node(cmds.createNode('transform')) 35 | node2 = mx.Node(cmds.createNode('transform')) 36 | assert node1 != node2.uniqueName 37 | assert not bool(node1 == node2.uniqueName) 38 | 39 | 40 | def test_should_compareNode_given_nonexistentNodeName(): 41 | node = mx.Node(cmds.createNode('transform')) 42 | assert node != 'nonexistentNodeName' 43 | assert not bool(node == 'nonexistentNodeName') 44 | 45 | 46 | def test_should_compareNode_given_nonUniqueNodeName(): 47 | mx.Node(cmds.createNode('transform', name='parent1')) 48 | mx.Node(cmds.createNode('transform', name='child', parent='parent1')) 49 | mx.Node(cmds.createNode('transform', name='parent2')) 50 | mx.Node(cmds.createNode('transform', name='child', parent='parent2')) 51 | 52 | assert mx.Node('parent1|child') != 'child' 53 | 54 | 55 | def test_should_compareNode_given_object(): 56 | node = mx.Node(cmds.createNode('transform')) 57 | obj = object() 58 | assert node != obj 59 | assert not bool(node == obj) 60 | 61 | 62 | def test_should_compareNode_given_similarObject(): 63 | # This that NotImplemented is returned in the equality functions for unknown objects. 64 | class CustomObject(object): 65 | def __eq__(self, other): 66 | return True 67 | 68 | def __ne__(self, other): 69 | return False 70 | 71 | node = mx.Node(cmds.createNode('transform')) 72 | obj = CustomObject() 73 | assert node == obj 74 | assert not bool(node != obj) 75 | -------------------------------------------------------------------------------- /tests/test_dag_node_methods.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_freezeTransformations(): 7 | node = mx.Node(cmds.createNode('transform')) 8 | node.translate = mx.Vector(15.0, 20.0, 6.0) 9 | node.rotate = mx.Vector(90, 20, 0) 10 | node.scale = mx.Vector(1, 2, 3) 11 | 12 | assert node.translate == mx.Vector(15.0, 20.0, 6.0) 13 | assert node.rotate == mx.Vector(90, 20, 0) 14 | assert node.scale == mx.Vector(1, 2, 3) 15 | 16 | node.freezeTransform() 17 | 18 | assert node.translate == mx.Vector(0, 0, 0) 19 | assert node.rotate == mx.Vector(0, 0, 0) 20 | assert node.scale == mx.Vector(1, 1, 1) 21 | 22 | 23 | def test_should_freezeTransformations_and_preserve(): 24 | node = mx.Node(cmds.polyCube()[0]) 25 | cmds.group(node.uniqueName) 26 | node.translate = mx.Vector(15.0, 20.0, 6.0) 27 | 28 | assert node.translate == mx.Vector(15.0, 20.0, 6.0) 29 | assert node.rotatePivot == mx.Vector(0, 0, 0) 30 | 31 | node.freezeTransform() 32 | 33 | assert node.translate == mx.Vector(0, 0, 0) 34 | assert node.rotatePivot == mx.Vector(15.0, 20.0, 6.0) 35 | 36 | 37 | def test_should_freezeOnlyTranslation(): 38 | node = mx.Node(cmds.createNode('transform')) 39 | node.translate = mx.Vector(15.0, 20.0, 6.0) 40 | node.rotate = mx.Vector(90, 20, 0) 41 | 42 | assert node.translate == mx.Vector(15.0, 20.0, 6.0) 43 | 44 | node.freezeTransform(translate=True) 45 | 46 | assert node.translate == mx.Vector(0, 0, 0) 47 | assert node.rotate == mx.Vector(90, 20, 0) 48 | 49 | 50 | def test_should_resetTransformations(): 51 | node = mx.Node(cmds.createNode('transform')) 52 | node.translate = mx.Vector(15.0, 20.0, 6.0) 53 | node.rotate = mx.Vector(90, 20, 0) 54 | node.scale = mx.Vector(1, 2, 3) 55 | 56 | assert node.translate == mx.Vector(15.0, 20.0, 6.0) 57 | assert node.rotate == mx.Vector(90, 20, 0) 58 | assert node.scale == mx.Vector(1, 2, 3) 59 | 60 | node.resetTransform() 61 | 62 | assert node.translate == mx.Vector(0, 0, 0) 63 | assert node.rotate == mx.Vector(0, 0, 0) 64 | assert node.scale == mx.Vector(1, 1, 1) 65 | assert node.rotatePivot == mx.Vector(0, 0, 0) 66 | 67 | 68 | def test_should_resetOnlyRotation(): 69 | node = mx.Node(cmds.createNode('transform')) 70 | node.translate = mx.Vector(15.0, 20.0, 6.0) 71 | node.rotate = mx.Vector(90, 20, 0) 72 | 73 | assert node.rotate == mx.Vector(90, 20, 0) 74 | 75 | node.resetTransform(rotate=True) 76 | 77 | assert node.translate == mx.Vector(15.0, 20.0, 6.0) 78 | assert node.rotate == mx.Vector(0, 0, 0) 79 | -------------------------------------------------------------------------------- /tests/test_attribute_properties.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_getName(): 7 | node = mx.Node(cmds.createNode('transform')) 8 | 9 | assert mx.Attribute(node, 'translate').name == 'translate' 10 | 11 | 12 | def test_should_getFullName(): 13 | node = mx.Node(cmds.createNode('transform')) 14 | 15 | assert mx.Attribute(node, 'translate').fullName == 'transform1.translate' 16 | 17 | 18 | def test_should_getNode(): 19 | node = mx.Node(cmds.createNode('transform')) 20 | 21 | assert mx.Attribute(node, 'translate').node == node 22 | 23 | 24 | def test_should_getType(): 25 | node = mx.Node(cmds.createNode('transform')) 26 | 27 | cmds.addAttr(node.uniqueName, longName='testAttr', dataType='string') 28 | 29 | assert mx.Attribute(node, 'testAttr').type == 'string' 30 | assert mx.Attribute(node, 'visibility').type == 'bool' 31 | assert mx.Attribute(node, 'translate').type == 'double3' 32 | assert mx.Attribute(node, 'translateX').type == 'doubleLinear' 33 | assert mx.Attribute(node, 'scaleX').type == 'double' 34 | 35 | 36 | def test_should_getLockedState(): 37 | node = mx.Node(cmds.createNode('transform')) 38 | 39 | assert not mx.Attribute(node, 'visibility').locked 40 | 41 | cmds.setAttr('{}.visibility'.format(node.uniqueName), lock=True) 42 | 43 | assert mx.Attribute(node, 'visibility').locked 44 | 45 | 46 | def test_should_setLockedState(): 47 | node = mx.Node(cmds.createNode('transform')) 48 | 49 | mx.Attribute(node, 'visibility').locked = True 50 | 51 | assert cmds.getAttr('{}.visibility'.format(node.uniqueName), lock=True) 52 | 53 | mx.Attribute(node, 'visibility').locked = False 54 | 55 | assert not cmds.getAttr('{}.visibility'.format(node.uniqueName), lock=True) 56 | 57 | 58 | def test_should_getKeyableState(): 59 | node = mx.Node(cmds.createNode('transform')) 60 | 61 | assert mx.Attribute(node, 'visibility').keyable 62 | 63 | cmds.setAttr('{}.visibility'.format(node.uniqueName), keyable=False) 64 | 65 | assert not mx.Attribute(node, 'visibility').keyable 66 | 67 | 68 | def test_should_setKeyableState(): 69 | node = mx.Node(cmds.createNode('transform')) 70 | 71 | mx.Attribute(node, 'visibility').keyable = True 72 | 73 | assert cmds.getAttr('{}.visibility'.format(node.uniqueName), keyable=True) 74 | 75 | mx.Attribute(node, 'visibility').keyable = False 76 | 77 | assert not cmds.getAttr('{}.visibility'.format(node.uniqueName), keyable=True) 78 | 79 | 80 | def test_should_getChannelBoxState(): 81 | node = mx.Node(cmds.createNode('transform')) 82 | 83 | assert not mx.Attribute(node, 'visibility').channelBox 84 | 85 | cmds.setAttr('{}.visibility'.format(node.uniqueName), channelBox=True) 86 | 87 | assert mx.Attribute(node, 'visibility').channelBox 88 | 89 | 90 | def test_should_setChannelBoxState(): 91 | node = mx.Node(cmds.createNode('transform')) 92 | 93 | mx.Attribute(node, 'visibility').channelBox = True 94 | 95 | assert cmds.getAttr('{}.visibility'.format(node.uniqueName), channelBox=True) 96 | 97 | mx.Attribute(node, 'visibility').channelBox = False 98 | 99 | assert not cmds.getAttr('{}.visibility'.format(node.uniqueName), channelBox=True) 100 | -------------------------------------------------------------------------------- /tests/test_node_attribute.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | 5 | import mayax as mx 6 | 7 | 8 | def test_should_getAttribute(): 9 | node = mx.Node(cmds.createNode('transform')) 10 | assert isinstance(node['translate'], mx.Attribute) 11 | 12 | 13 | def test_should_deleteAttribute(): 14 | node = mx.Node(cmds.createNode('transform', name='testNode')) 15 | cmds.addAttr(node.name, longName='testAttr') 16 | 17 | assert cmds.attributeQuery('testAttr', node='testNode', exists=True) 18 | 19 | node.deleteAttr('testAttr') 20 | 21 | assert not cmds.attributeQuery('testAttr', node='testNode', exists=True) 22 | 23 | 24 | def test_should_returnAttribute_when_adding(): 25 | node = mx.Node(cmds.createNode('transform')) 26 | attr = node.addAttr('testAttr', 'test') 27 | assert isinstance(attr, mx.Attribute) 28 | 29 | 30 | def test_should_checkAttributeExistence_given_existingAttributeName(): 31 | node = mx.Node(cmds.createNode('transform')) 32 | assert node.hasAttr('translate') 33 | 34 | 35 | def test_should_checkAttributeExistence_given_nonexistingAttributeName(): 36 | node = mx.Node(cmds.createNode('transform')) 37 | assert not node.hasAttr('nonexistentAttrName') 38 | 39 | 40 | def test_should_checkAttributeExistence_given_arrayAttributeName(): 41 | node = mx.Node(cmds.createNode('multMatrix')) 42 | assert node.hasAttr('matrixIn') 43 | 44 | 45 | def test_should_checkAttributeExistence_given_arrayAttributeName_and_index(): 46 | node = mx.Node(cmds.createNode('multMatrix')) 47 | assert node.hasAttr('matrixIn[0]') 48 | 49 | 50 | def test_should_magicallyGetAttribute(): 51 | node = mx.Node(cmds.createNode('transform')) 52 | cmds.move(15.0, 10.5, 1.0, node.uniqueName) 53 | assert node.translateX == 15.0 54 | assert node.translateY == 10.5 55 | assert node.translateZ == 1.0 56 | assert node.visibility 57 | 58 | 59 | def test_should_magicallySetAttribute(): 60 | node = mx.Node(cmds.createNode('transform')) 61 | cmds.move(15.0, 0.0, 0.0, node.uniqueName) 62 | assert node['translateX'].value == 15 63 | assert node['visibility'].value 64 | node.translateX = 10 65 | node.visibility = False 66 | assert node['translateX'].value == 10 67 | assert not node['visibility'].value 68 | 69 | 70 | def test_should_setClassAttribute_given_conflictingNames(): 71 | node = mx.Node(cmds.createNode('transform')) 72 | node.testAttr = 'test' 73 | cmds.addAttr(node.uniqueName, longName='testAttr', dataType='string') 74 | cmds.setAttr('{}.testAttr'.format(node.uniqueName), 'empty', type='string') 75 | assert node.testAttr == 'test' 76 | assert node['testAttr'].value == 'empty' 77 | node.testAttr = 'dumbo' 78 | assert node.testAttr == 'dumbo' 79 | assert node['testAttr'].value == 'empty' 80 | 81 | 82 | def test_should_setClassAttribute_given_conflictingPropertyNames(): 83 | node = mx.Node(cmds.createNode('transform')) 84 | 85 | cmds.addAttr(node.uniqueName, longName='name', dataType='string') 86 | cmds.setAttr('{}.name'.format(node.uniqueName), 'empty', type='string') 87 | 88 | assert node.name == 'transform1' 89 | assert node['name'].value == 'empty' 90 | 91 | node.name = 'dumbo' 92 | 93 | assert node.name == 'dumbo' 94 | assert node['name'].value == 'empty' 95 | 96 | 97 | def test_should_raiseMayaNodeError_given_insufficientArguments_when_addingAttribute(): 98 | node = mx.Node(cmds.createNode('transform')) 99 | with pytest.raises(mx.MayaNodeError): 100 | node.addAttr('testAttr') 101 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | skip-string-normalization = true 4 | fast = true 5 | 6 | # -------------------------------------------------------------------------------------------------- 7 | 8 | [tool.pylint.main] 9 | # Python code to execute, usually for sys.path manipulation such as pygtk.require(). 10 | init-hook = "import os; import sys; sys.path.append(os.path.abspath('src'))" 11 | 12 | # List of plugins (as comma separated values of python modules names) to load, 13 | # usually to register additional checkers. 14 | load-plugins = [ 15 | "pylint.extensions.bad_builtin", 16 | "pylint.extensions.check_elif", 17 | "pylint.extensions.comparetozero", 18 | "pylint.extensions.emptystring", 19 | "pylint.extensions.mccabe", # Design checker 20 | "pylint.extensions.overlapping_exceptions", 21 | "pylint.extensions.redefined_variable_type", 22 | "pylint.extensions.docparams", 23 | "pylint.extensions.docstyle", 24 | ] 25 | 26 | [tool.pylint."messages control"] 27 | disable = [ 28 | "missing-docstring", # pydocstyle will be used to check for missing docstrings 29 | "too-few-public-methods", 30 | "raise-missing-from", # to support Python 2 31 | "consider-using-f-string", # to support Python 2 32 | "useless-object-inheritance", # to support Python 2 33 | "super-with-arguments", # to support Python 2 34 | ] 35 | 36 | [tool.pylint.basic] 37 | # Naming style matching correct module names 38 | module-naming-style = "snake_case" 39 | 40 | # Naming style matching correct constant names 41 | const-naming-style = "UPPER_CASE" 42 | 43 | # Naming style matching correct variable names 44 | variable-naming-style = "camelCase" 45 | 46 | # Naming style matching correct argument names 47 | argument-naming-style = "camelCase" 48 | 49 | # Naming style matching correct class names 50 | class-naming-style = "PascalCase" 51 | 52 | # Naming style matching correct class attribute names 53 | class-attribute-naming-style = "any" 54 | 55 | # Naming style matching correct inline iteration names 56 | inlinevar-naming-style = "camelCase" 57 | 58 | # Regular expression matching correct function names. Overrides function-naming-style 59 | # Add custom regular expression to allow the following: 60 | # camelCase 61 | # test_should_camelCase[_given_camelCase][_if_camelCase][_when_camelCase][_and_camelCase] 62 | function-rgx = "(([a-z_][a-zA-Z0-9]{1,50})|(__[a-z][a-zA-Z0-9_]+__)|(test_should_([a-z][a-zA-Z0-9]+)(_and_([a-z][a-zA-Z0-9]+))?(_given_([a-z][a-zA-Z0-9]+))?(_if_([a-z][a-zA-Z0-9]+))?(_when_([a-z][a-zA-Z0-9]+))?(_and_([a-z][a-zA-Z0-9]+))?))$" 63 | 64 | # Regular expression matching correct method names. Overrides method-naming-style 65 | # see function-rgx 66 | method-rgx = "(((_)?[a-z_][a-zA-Z0-9]{1,40})|(__[a-z][a-zA-Z0-9_]+__)|(test_should_([a-z][a-zA-Z0-9]+)(_and_([a-z][a-zA-Z0-9]+))?(_given_([a-z][a-zA-Z0-9]+))?(_if_([a-z][a-zA-Z0-9]+))?(_when_([a-z][a-zA-Z0-9]+))?(_and_([a-z][a-zA-Z0-9]+))?))$" 67 | 68 | # Regular expression matching correct attribute names. Overrides attr-naming-style 69 | # Use rgx instead of `attr-naming-style=camelCase` to allow names 70 | # starting with double underscore, like __camelCase 71 | attr-rgx = "((_)?[a-z_][A-Za-z0-9]{2,30}|(__.*__))$" 72 | 73 | # Good variable names which should always be accepted, separated by a comma 74 | good-names = ["i", "j", "k", "e", "ex", "Run", "_"] 75 | 76 | [tool.pylint.format] 77 | # Maximum number of characters on a single line. 78 | max-line-length = 100 79 | 80 | [tool.pylint.reports] 81 | # Set the output format. Available formats are text, parseable, colorized, json 82 | # and msvs (visual studio).You can also give a reporter class, eg 83 | # mypackage.mymodule.MyReporterClass. 84 | output-format = "colorized" 85 | 86 | [tool.pylint.typecheck] 87 | # List of module names for which member attributes should not be checked 88 | # (useful for modules/projects where namespaces are manipulated during runtime 89 | # and thus existing member attributes cannot be deduced by static analysis. It 90 | # supports qualified module names, as well as Unix pattern matching. 91 | ignored-modules = ["PySide2", "shiboken2", "maya"] 92 | 93 | # List of members which are set dynamically and missed by pylint inference 94 | # system, and so shouldn't trigger E1101 when accessed. Python regular 95 | # expressions are accepted. 96 | generated-members = ["fget", "fset"] 97 | 98 | [tool.pylint.similarities] 99 | # Minimum lines number of a similarity. 100 | min-similarity-lines = 5 101 | -------------------------------------------------------------------------------- /tests/test_cmd.py: -------------------------------------------------------------------------------- 1 | import mayax as mx 2 | 3 | 4 | def test_should_returnNode(): 5 | locator = mx.cmd.group(empty=True) 6 | 7 | assert locator.uniqueName == 'null1' 8 | 9 | 10 | def test_should_returnNodeList(): 11 | locator = mx.cmd.spaceLocator() 12 | 13 | assert len(locator) == 1 14 | assert locator[0].uniqueName == 'locator1' 15 | 16 | 17 | def test_should_returnOriginalList(): 18 | locator = mx.cmd.spaceLocator() 19 | result = mx.cmd.xform(locator, query=True, translation=True, worldSpace=True) 20 | 21 | assert result == [0, 0, 0] 22 | 23 | 24 | def test_should_passNodeAsArgument(): 25 | child1 = mx.cmd.createNode('transform') 26 | child2 = mx.cmd.createNode('transform') 27 | mx.cmd.group(child1, child2) 28 | 29 | assert child1.parent.uniqueName == 'group1' 30 | assert child2.parent.uniqueName == 'group1' 31 | 32 | 33 | def test_should_passNodeAsArgument_given_nodeWithNonUniqueName(): 34 | parent1 = mx.cmd.createNode('transform', name='parent1') 35 | child1 = mx.cmd.createNode('transform', name='child', parent=parent1) 36 | 37 | parent2 = mx.cmd.createNode('transform', name='parent2') 38 | mx.cmd.createNode('transform', name='child', parent=parent2) 39 | 40 | mx.cmd.select(child1) 41 | 42 | assert mx.cmd.ls(selection=True)[0].uniqueName == 'parent1|child' 43 | 44 | 45 | def test_should_passNodeAsKeywordArgument(): 46 | parent = mx.cmd.createNode('transform') 47 | child = mx.cmd.createNode('transform', parent=parent) 48 | 49 | assert child.parent.uniqueName == 'transform1' 50 | 51 | 52 | def test_should_passNodeAsKeywordArgument_given_nodeWithNonUniqueName(): 53 | parent1 = mx.cmd.createNode('transform', name='parent1') 54 | child1 = mx.cmd.createNode('transform', name='child', parent=parent1) 55 | 56 | parent2 = mx.cmd.createNode('transform', name='parent2') 57 | mx.cmd.createNode('transform', name='child', parent=parent2) 58 | 59 | child = mx.cmd.createNode('transform', parent=child1) 60 | 61 | assert child.parent.uniqueName == 'parent1|child' 62 | 63 | 64 | def test_should_passNodeListAsKeywordArgument(): 65 | obj1 = mx.cmd.createNode('transform') 66 | obj2 = mx.cmd.createNode('transform') 67 | 68 | container = mx.cmd.container(addNode=[obj1, obj2]) 69 | containerNodes = mx.cmd.container(container, query=True, nodeList=True) 70 | 71 | assert containerNodes[0].uniqueName == 'transform1' 72 | assert containerNodes[1].uniqueName == 'transform2' 73 | 74 | 75 | def test_should_passNodeListAsKeywordArgument_given_nodeWithNonUniqueName(): 76 | parent1 = mx.cmd.createNode('transform', name='parent1') 77 | child1 = mx.cmd.createNode('transform', name='child', parent=parent1) 78 | 79 | parent2 = mx.cmd.createNode('transform', name='parent2') 80 | child2 = mx.cmd.createNode('transform', name='child', parent=parent2) 81 | 82 | container = mx.cmd.container(addNode=[child1, child2]) 83 | containerNodes = mx.cmd.container(container, query=True, nodeList=True) 84 | 85 | assert containerNodes[0].uniqueName == 'parent1|child' 86 | assert containerNodes[1].uniqueName == 'parent2|child' 87 | 88 | 89 | def test_should_returnAttribute(): 90 | root = mx.cmd.createNode('transform', name='root') 91 | leaf = mx.cmd.createNode('transform', name='leaf') 92 | 93 | leaf.addAttr('root', root) 94 | 95 | connectionInfo = mx.cmd.connectionInfo('root.message', getExactSource=True) 96 | 97 | assert isinstance(connectionInfo, mx.Attribute) 98 | assert connectionInfo.fullName == 'root.message' 99 | 100 | 101 | def test_should_returnAttributeList(): 102 | root = mx.cmd.createNode('transform', name='root') 103 | leaf1 = mx.cmd.createNode('transform', name='leaf1') 104 | leaf2 = mx.cmd.createNode('transform', name='leaf2') 105 | 106 | leaf1.addAttr('root', root) 107 | leaf2.addAttr('root', root) 108 | 109 | connectedAttributes = mx.cmd.listConnections('root.message', plugs=True) 110 | 111 | assert len(connectedAttributes) == 2 112 | assert isinstance(connectedAttributes[0], mx.Attribute) 113 | assert isinstance(connectedAttributes[1], mx.Attribute) 114 | assert connectedAttributes[0].fullName == 'leaf2.root' 115 | assert connectedAttributes[1].fullName == 'leaf1.root' 116 | 117 | 118 | def test_should_passAttributeAsArgument(): 119 | root = mx.cmd.createNode('transform', name='root') 120 | leaf = mx.cmd.createNode('transform', name='leaf') 121 | 122 | leaf.addAttr('root', root) 123 | 124 | connections = mx.cmd.listConnections(mx.Attribute(root, 'message')) 125 | 126 | assert len(connections) == 1 127 | assert isinstance(connections[0], mx.Node) 128 | assert connections[0] == leaf 129 | -------------------------------------------------------------------------------- /tests/test_attribute_reference.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_addReferenceAttribute_given_type(): 7 | node = mx.Node(cmds.createNode('network')) 8 | assert mx.Attribute(node, 'refAttr', type='message') 9 | assert mx.Attribute(node, 'refAttr') 10 | assert mx.Attribute(node, 'refAttr').type == 'message' 11 | 12 | 13 | def test_should_addReferenceAttribute_given_pyType(): 14 | node = mx.Node(cmds.createNode('network')) 15 | assert mx.Attribute(node, 'refAttr', type=mx.Node) 16 | assert mx.Attribute(node, 'refAttr') 17 | assert mx.Attribute(node, 'refAttr').type == 'message' 18 | 19 | 20 | def test_should_addReferenceAttribute_given_node(): 21 | node = mx.Node(cmds.createNode('transform')) 22 | metaNode = mx.Node(cmds.createNode('network')) 23 | assert mx.Attribute(metaNode, 'refAttr', node) 24 | assert mx.Attribute(metaNode, 'refAttr') 25 | assert mx.Attribute(metaNode, 'refAttr').type == 'message' 26 | assert ( 27 | cmds.connectionInfo( 28 | '{}.refAttr'.format(metaNode.uniqueName), 29 | sourceFromDestination=True, 30 | ) 31 | == '{}.message'.format(node.uniqueName) 32 | ) 33 | 34 | 35 | def test_should_addReferenceAttribute_given_nodeName_and_pyType(): 36 | node = mx.Node(cmds.createNode('transform')) 37 | metaNode = mx.Node(cmds.createNode('network')) 38 | assert mx.Attribute(metaNode, 'refAttr', node.uniqueName, type=mx.Node) 39 | assert mx.Attribute(metaNode, 'refAttr') 40 | assert mx.Attribute(metaNode, 'refAttr').type == 'message' 41 | assert ( 42 | cmds.connectionInfo( 43 | '{}.refAttr'.format(metaNode.uniqueName), 44 | sourceFromDestination=True, 45 | ) 46 | == '{}.message'.format(node.uniqueName) 47 | ) 48 | 49 | 50 | # -------------------------------------------------------------------------------------------------- 51 | 52 | 53 | def test_should_getNoneReference_given_unreferencedAttribute(): 54 | metaNode = mx.Node(cmds.createNode('network')) 55 | attr = mx.Attribute(metaNode, 'refAttr', type=mx.Node) 56 | assert attr.value is None 57 | 58 | 59 | def test_should_getNodeInstance_given_referencedAttribute(): 60 | node = mx.Node(cmds.createNode('transform')) 61 | metaNode = mx.Node(cmds.createNode('network')) 62 | attr = mx.Attribute(metaNode, 'refAttr', node) 63 | assert isinstance(attr.value, mx.Node) 64 | assert attr.value == node 65 | 66 | 67 | # -------------------------------------------------------------------------------------------------- 68 | 69 | 70 | def test_should_setUnreferencedAttribute_given_node(): 71 | node = mx.Node(cmds.createNode('transform')) 72 | metaNode = mx.Node(cmds.createNode('network')) 73 | attr = mx.Attribute(metaNode, 'refAttr', type=mx.Node) 74 | assert attr.value is None 75 | attr.value = node 76 | assert isinstance(attr.value, mx.Node) 77 | assert attr.value == node 78 | 79 | 80 | def test_should_setUnreferencedAttribute_given_nodeName(): 81 | node = mx.Node(cmds.createNode('transform')) 82 | metaNode = mx.Node(cmds.createNode('network')) 83 | attr = mx.Attribute(metaNode, 'refAttr', type=mx.Node) 84 | assert attr.value is None 85 | attr.value = node.uniqueName 86 | assert isinstance(attr.value, mx.Node) 87 | assert attr.value == node 88 | 89 | 90 | def test_should_setUnreferencedAttribute_given_noneType(): 91 | metaNode = mx.Node(cmds.createNode('network')) 92 | attr = mx.Attribute(metaNode, 'refAttr', type=mx.Node) 93 | assert attr.value is None 94 | attr.value = None 95 | assert attr.value is None 96 | 97 | 98 | def test_should_setReferencedAttribute_given_node(): 99 | node1 = mx.Node(cmds.createNode('transform')) 100 | node2 = mx.Node(cmds.createNode('transform')) 101 | metaNode = mx.Node(cmds.createNode('network')) 102 | attr = mx.Attribute(metaNode, 'refAttr', node1) 103 | assert isinstance(attr.value, mx.Node) 104 | assert attr.value == node1 105 | assert attr.value != node2 106 | attr.value = node2 107 | assert attr.value == node2 108 | assert attr.value != node1 109 | 110 | 111 | def test_should_setReferencedAttribute_given_nodeName(): 112 | node1 = mx.Node(cmds.createNode('transform')) 113 | node2 = mx.Node(cmds.createNode('transform')) 114 | metaNode = mx.Node(cmds.createNode('network')) 115 | attr = mx.Attribute(metaNode, 'refAttr', node1.uniqueName, type=mx.Node) 116 | assert isinstance(attr.value, mx.Node) 117 | assert attr.value == node1 118 | assert attr.value != node2 119 | attr.value = node2.uniqueName 120 | assert attr.value == node2 121 | assert attr.value != node1 122 | 123 | 124 | def test_should_setReferencedAttribute_given_noneType(): 125 | node = mx.Node(cmds.createNode('transform')) 126 | metaNode = mx.Node(cmds.createNode('network')) 127 | attr = mx.Attribute(metaNode, 'refAttr', node) 128 | assert isinstance(attr.value, mx.Node) 129 | assert attr.value == node 130 | attr.value = None 131 | assert attr.value is None 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MayaX 2 | 3 | ## Table of Contents 4 | 5 | - [Introduction](#introduction) 6 | - [Installation](#installation) 7 | - [Usage](#usage) 8 | - [API](#api) 9 | - [Node Properties](#node-properties) 10 | - [Node methods](#node-methods) 11 | - [Attribute Properties](#attribute-properties) 12 | - [Attribute methods](#attribute-methods) 13 | - [Misc](#misc) 14 | 15 | 16 | ## Introduction 17 | 18 | **MayaX** provides useful functionality to interact with **Maya**, mainly making it easier to work with **nodes** in an **object-oriented** way. It supports *Maya 2019* and up (maybe lower than 2019 too, but it wasn't tested). 19 | 20 | ```python 21 | import mayax as mx 22 | 23 | # retrieve object from scene 24 | torus = mx.Node('pTorus1') 25 | 26 | # create new node 27 | cube = mx.cmd.polyCube()[0] 28 | 29 | # connect nodes 30 | torus['worldMatrix'].connect(cube['offsetParentMatrix']) 31 | 32 | # set attribute value 33 | cube.translate = mx.Vector(0, 5, 0) 34 | 35 | # set attribute's properties 36 | cube['translate'].locked = True 37 | 38 | # add node reference as attribute 39 | # (atttribute of type `message`, connected to `torus.message`) 40 | cube.addAttr('parentDriver', torus) 41 | 42 | # retrieve node reference 43 | cube.parentDriver.worldPosition = mx.Vector(2, 5, 5) 44 | ``` 45 | 46 | The goal is to not replace `maya.cmds`, but to augmented it with some OOP features. Instead of always working with nodes' names, which can be frustrating at times, you would work with Maya's nodes as they were Python objects. Changing the name or parent of a node it will not make your variable obsolete anymore since the variable will still point to the same node. 47 | 48 | 49 | ## Installation 50 | 51 | **a.** Drag `install.py` script into Maya's viewport. 52 | 53 | **b.** Manually create the `mayax.mod` module file and put it in your favorite location: 54 | 55 | + MayaX 1.0.0 56 | scripts: src 57 | 58 | **c.** Copy `src/mayax` into your project. 59 | 60 | 61 | ## Usage 62 | 63 | - `import mayax as mx` 64 | 65 | - Use `mx.Node('objectFromScene')` to retrieve an existing node using its name. 66 | 67 | - Use `mx.cmd.*` namespace to call the Maya's commands in order to have them accept and return instances of `mx.Node`. 68 | 69 | - Retrieve and set the values for the node's attributes's as you would for regular Python objects (`node.attributeName = value`). 70 | 71 | - Do operations on the attributes themselves by accessing them using the notation `node['attributeName']`. 72 | 73 | - Add attributes using [node.addAttr()](#add-attr) method. 74 | 75 | 76 | ## API 77 | 78 | ### Node Properties 79 | 80 | - `exists`: Check if the node still exists. 81 | 82 | - `name`: Get/set the node's name. 83 | 84 | - `uniqueName`: Get the node's unique name. 85 | 86 | - `type`: Get the node's type. 87 | 88 | - `pathName`: Get the full path from the root of the dag to this object. 89 | 90 | - `parent`: Get/set the node's parent. 91 | 92 | - `children`: Get the node's children. 93 | 94 | - `descendents`: Get the node's descendents. 95 | 96 | - `shapes`: Get the node's shapes. 97 | 98 | - `worldPosition`: Get/set the node's world position. 99 | 100 | - `worldRotation`: Get/set the node's world rotation. 101 | 102 | - `worldScale`: Get/set the node's world scale. 103 | 104 | - `worldMatrix`: Get/set the node's world matrix. 105 | 106 | 107 | ### Node Methods 108 | 109 | - `rename(name, **kwargs)`: Rename the node. 110 | 111 | Use `kwargs` to pass extra flags used by `maya.cmds.rename`. 112 | For simple cases use the `name` property *setter*. 113 | 114 | - `delete()`: Delete the node. 115 | 116 | - `duplicate(**kwargs)`: Duplicate the node. 117 | 118 | Use `kwargs` to pass extra flags used by `maya.cmds.duplicate`. 119 | 120 | - `select(**kwargs)`: Select the node. 121 | 122 | Use `kwargs` to pass extra flags used by `maya.cmds.select`. 123 | 124 | 125 | - `addAttr(name, value=None, type=None, **kwargs)`: Add an attribute to the node. 126 | 127 | Use `kwargs` to pass extra flags used by `maya.cmds.addAttr`. 128 | 129 | ```python 130 | # add an attribute by inferring its type from the value 131 | node.addAttr('name', 25.5) 132 | node.addAttr('name', nodeInstance) 133 | 134 | # add an attribute by providing the type 135 | node.addAttr('name', type='float') 136 | node.addAttr('name', nodeInstance, type='message') 137 | 138 | # add an attribute by providing a Python's type 139 | node.addAttr('name', type=float) 140 | node.addAttr('name', type=mx.Node) 141 | node.addAttr('name', type=mx.Vector) 142 | ``` 143 | 144 | - `deleteAttr(name)`: Delete the provided attribute. 145 | 146 | - `hasAttr(name)`: Check if the node has an attribute. 147 | 148 | - `setParent(value, **kwargs)`: Set the node's parent. 149 | 150 | Use `kwargs` to pass extra flags used by `maya.cmds.parent`. 151 | For simple cases use the `parent` property *setter*. 152 | 153 | - `findChildren(name)`: Find children by name. 154 | 155 | - `worldPositionAt(time)`: Get the world position at specified time. 156 | 157 | - `worldRotationAt(time)`: Get the world rotation at specified time. 158 | 159 | - `worldScaleAt(time)`: Get the world scale at specified time. 160 | 161 | - `freezeTransform(**kwargs)`: Make the current transformations be the zero position. 162 | 163 | Use `kwargs` to pass extra flags used by `maya.cmds.makeIdentity`. 164 | 165 | - `resetTransform(**kwargs)`: Reset transformations back to zero (return to first or last "frozen" position). 166 | 167 | Use `kwargs` to pass extra flags used by `maya.cmds.makeIdentity`. 168 | 169 | 170 | ### Attribute Properties 171 | 172 | - `name`: Get the attribute's name. 173 | 174 | - `fullName`: Get the attribute's name with the node's name in it. 175 | 176 | - `node`: Get the attribute's node. 177 | 178 | - `type`: Get the attribute's type. 179 | 180 | - `value`: Get/set the attribute's value. 181 | 182 | - `locked`: Get/set the attribute's lock state. 183 | 184 | - `keyable`: Get/set the attribute's keyable state. 185 | 186 | - `channelBox`: Get/set the attribute's channelBox state. 187 | 188 | ### Attribute Methods 189 | 190 | - `valueAt(time)`: Get the attribute's value at the specified time. 191 | 192 | - `delete()`: Delete the attribute. 193 | 194 | - `connect(attr, **kwargs)`: Connect the attribute to another. 195 | 196 | Use `kwargs` to pass extra flags used by `maya.cmds.connectAttr`. 197 | 198 | - `disconnect(attr, **kwargs)`: Disconnect the attribute from another. 199 | 200 | Use `kwargs` to pass extra flags used by `maya.cmds.disconnectAttr`. 201 | 202 | - `disconnectInput()`: Disconnect the attribute's input. 203 | 204 | - `disconnectOutput()`: Disconnect the attribute's output. 205 | 206 | - `input(**kwargs)`: Get the attribute's input connection. 207 | 208 | Use `kwargs` to pass extra flags used by `maya.cmds.listConnections`. 209 | 210 | - `outputs(**kwargs)`: Get the attribute's output connections. 211 | 212 | Use `kwargs` to pass extra flags used by `maya.cmds.listConnections`. 213 | 214 | 215 | ### Misc 216 | 217 | - `getCurrentProjectDirectory()`: Return the path for the selected project directory. 218 | 219 | - `getModulesDirectory()`: Return the path for the modules directory. 220 | -------------------------------------------------------------------------------- /tests/test_attribute_value.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | from maya.api import OpenMaya as om 5 | 6 | import mayax as mx 7 | 8 | 9 | def test_should_getFloatValue(): 10 | node = mx.Node(cmds.createNode('transform')) 11 | cmds.move(15.0, 20.0, 6.0, node.uniqueName) 12 | assert isinstance(mx.Attribute(node, 'translateX').value, float) 13 | assert mx.Attribute(node, 'translateX').value == 15.0 14 | assert mx.Attribute(node, 'translateY').value == 20.0 15 | assert mx.Attribute(node, 'translateZ').value == 6.0 16 | 17 | 18 | def test_should_getIntValue(): 19 | node = mx.Node(cmds.createNode('transform')) 20 | cmds.addAttr(node.uniqueName, longName='testAttr', attributeType='long') 21 | cmds.setAttr('{}.testAttr'.format(node.uniqueName), 32) 22 | attr = mx.Attribute(node, 'testAttr') 23 | assert isinstance(attr.value, int) 24 | assert attr.value == 32 25 | 26 | 27 | def test_should_getBoolValue(): 28 | node = mx.Node(cmds.createNode('transform')) 29 | attr = mx.Attribute(node, 'visibility') 30 | assert isinstance(attr.value, bool) 31 | assert attr.value 32 | 33 | 34 | def test_should_getStringValue(): 35 | node = mx.Node(cmds.createNode('transform')) 36 | cmds.addAttr(node.uniqueName, longName='testAttr', dataType='string') 37 | cmds.setAttr('{}.testAttr'.format(node.uniqueName), 'blablah', type='string') 38 | attr = mx.Attribute(node, 'testAttr') 39 | assert isinstance(attr.value, mx.STR_TYPE) 40 | assert attr.value == 'blablah' 41 | 42 | 43 | def test_should_getVectorValue(): 44 | node = mx.Node(cmds.createNode('transform')) 45 | cmds.move(15.0, 20.15, 6.6, node.uniqueName) 46 | attrValue = mx.Attribute(node, 'translate').value 47 | assert isinstance(attrValue, mx.Vector) 48 | assert attrValue.x == 15.0 49 | assert attrValue.y == 20.15 50 | assert attrValue.z == 6.6 51 | 52 | 53 | def test_should_getMatrixValue(): 54 | node = mx.Node(cmds.createNode('transform')) 55 | cmds.move(15.0, 20.15, 6.6, node.uniqueName) 56 | attrValue = mx.Attribute(node, 'worldMatrix').value 57 | assert isinstance(attrValue, mx.Matrix) 58 | assert attrValue.getElement(3, 0) == 15 59 | assert attrValue.getElement(3, 1) == 20.15 60 | assert attrValue.getElement(3, 2) == 6.6 61 | assert attrValue.getElement(3, 3) == 1 62 | 63 | 64 | def test_should_getQuaternionValue(): 65 | node = mx.Node(cmds.createNode('eulerToQuat')) 66 | cmds.setAttr('{}.inputRotate'.format(node.uniqueName), 15.0, 20.15, 6.6) 67 | attrValue = mx.Attribute(node, 'outputQuat').value 68 | assert isinstance(attrValue, mx.Quaternion) 69 | assert round(attrValue.x, 3) == 0.118 70 | assert round(attrValue.y, 3) == 0.181 71 | assert round(attrValue.z, 3) == 0.033 72 | assert round(attrValue.w, 3) == 0.976 73 | 74 | 75 | def test_should_getColorValue(): 76 | node = mx.Node(cmds.createNode('ambientLight')) 77 | 78 | cmds.setAttr('{}.color'.format(node.uniqueName), 0.2, 0.35, 0.9) 79 | 80 | attrValue = mx.Attribute(node, 'color').value 81 | 82 | assert isinstance(attrValue, tuple) 83 | assert attrValue == pytest.approx((0.2, 0.35, 0.9)) 84 | 85 | 86 | def test_should_getValue_given_time(): 87 | node = mx.Node(cmds.createNode('transform')) 88 | attr = mx.Attribute(node, 'translateX') 89 | 90 | cmds.setKeyframe(node.uniqueName, attribute='translateX', time=1, value=10) 91 | cmds.setKeyframe(node.uniqueName, attribute='translateX', time=5, value=50) 92 | 93 | assert attr.value == 10 94 | assert attr.valueAt(1) == 10 95 | assert attr.valueAt(50) == 50 96 | 97 | 98 | # -------------------------------------------------------------------------------------------------- 99 | 100 | 101 | def test_should_setFloatValue(): 102 | node = mx.Node(cmds.createNode('transform')) 103 | cmds.move(15.0, 0.0, 0.0, node.uniqueName) 104 | attr = mx.Attribute(node, 'translateX') 105 | assert attr.value == 15.0 106 | attr.value = 32.45 107 | assert attr.value == 32.45 108 | 109 | 110 | def test_should_setIntValue(): 111 | node = mx.Node(cmds.createNode('transform')) 112 | cmds.addAttr(node.uniqueName, longName='testAttr', attributeType='long') 113 | cmds.setAttr('{}.testAttr'.format(node.uniqueName), 32) 114 | attr = mx.Attribute(node, 'testAttr') 115 | assert attr.value == 32 116 | attr.value = 999 117 | assert attr.value == 999 118 | 119 | 120 | def test_should_setBoolValue(): 121 | node = mx.Node(cmds.createNode('transform')) 122 | attr = mx.Attribute(node, 'visibility') 123 | assert attr.value 124 | attr.value = False 125 | assert not attr.value 126 | attr.value = True 127 | assert attr.value 128 | 129 | 130 | def test_should_setStringValue(): 131 | node = mx.Node(cmds.createNode('transform')) 132 | cmds.addAttr(node.uniqueName, longName='testAttr', dataType='string') 133 | cmds.setAttr('{}.testAttr'.format(node.uniqueName), 'blablah', type='string') 134 | attr = mx.Attribute(node, 'testAttr') 135 | assert attr.value == 'blablah' 136 | attr.value = 'hello' 137 | assert attr.value == 'hello' 138 | 139 | 140 | def test_should_setVectorValue_given_vector(): 141 | node = mx.Node(cmds.createNode('transform')) 142 | attr = mx.Attribute(node, 'translate') 143 | assert not attr.value.x 144 | assert not attr.value.y 145 | assert not attr.value.z 146 | attr.value = mx.Vector(20.0, 13, 7.5) 147 | assert attr.value.x == 20.0 148 | assert attr.value.y == 13.0 149 | assert attr.value.z == 7.5 150 | 151 | 152 | def test_should_setVectorValue_given_openMayaVector(): 153 | node = mx.Node(cmds.createNode('transform')) 154 | attr = mx.Attribute(node, 'translate') 155 | assert not attr.value.x 156 | assert not attr.value.y 157 | assert not attr.value.z 158 | attr.value = om.MVector(20.0, 13, 7.5) 159 | assert attr.value.x == 20.0 160 | assert attr.value.y == 13.0 161 | assert attr.value.z == 7.5 162 | 163 | 164 | def test_should_setVectorValue_given_tuple(): 165 | node = mx.Node(cmds.createNode('transform')) 166 | attr = mx.Attribute(node, 'translate') 167 | assert not attr.value.x 168 | assert not attr.value.y 169 | assert not attr.value.z 170 | attr.value = (20.0, 13, 7.5) 171 | assert attr.value.x == 20.0 172 | assert attr.value.y == 13.0 173 | assert attr.value.z == 7.5 174 | 175 | 176 | def test_should_setVectorValue_given_list(): 177 | node = mx.Node(cmds.createNode('transform')) 178 | attr = mx.Attribute(node, 'translate') 179 | assert not attr.value.x 180 | assert not attr.value.y 181 | assert not attr.value.z 182 | attr.value = [20.0, 13, 7.5] 183 | assert attr.value.x == 20.0 184 | assert attr.value.y == 13.0 185 | assert attr.value.z == 7.5 186 | 187 | 188 | def test_should_setMatrixValue_given_matrix(): 189 | node = mx.Node(cmds.createNode('transform')) 190 | cmds.addAttr(node.uniqueName, longName='testMatrix', attributeType='matrix') 191 | attr = mx.Attribute(node, 'testMatrix') 192 | assert not attr.value.getElement(3, 0) 193 | assert not attr.value.getElement(3, 1) 194 | assert not attr.value.getElement(3, 2) 195 | assert attr.value.getElement(3, 3) == 1 196 | attr.value = mx.Matrix([(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (20, 33, 45, 1)]) 197 | assert attr.value.getElement(3, 0) == 20 198 | assert attr.value.getElement(3, 1) == 33 199 | assert attr.value.getElement(3, 2) == 45 200 | assert attr.value.getElement(3, 3) == 1 201 | 202 | 203 | def test_should_setMatrixValue_given_openMayaMatrix(): 204 | node = mx.Node(cmds.createNode('transform')) 205 | cmds.addAttr(node.uniqueName, longName='testMatrix', attributeType='matrix') 206 | attr = mx.Attribute(node, 'testMatrix') 207 | assert not attr.value.getElement(3, 0) 208 | assert not attr.value.getElement(3, 1) 209 | assert not attr.value.getElement(3, 2) 210 | assert attr.value.getElement(3, 3) == 1 211 | attr.value = om.MMatrix([(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (20, 33, 45, 1)]) 212 | assert attr.value.getElement(3, 0) == 20 213 | assert attr.value.getElement(3, 1) == 33 214 | assert attr.value.getElement(3, 2) == 45 215 | assert attr.value.getElement(3, 3) == 1 216 | 217 | 218 | def test_should_setMatrixValue_given_emptyArrayAttribute_and_matrix(): 219 | node = mx.Node(cmds.createNode('multMatrix')) 220 | attr = mx.Attribute(node, 'matrixIn[0]') 221 | 222 | assert not attr.value.getElement(3, 0) 223 | assert not attr.value.getElement(3, 1) 224 | assert not attr.value.getElement(3, 2) 225 | assert attr.value.getElement(3, 3) == 1 226 | 227 | attr.value = mx.Matrix([(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (20, 33, 45, 1)]) 228 | 229 | assert attr.value.getElement(3, 0) == 20 230 | assert attr.value.getElement(3, 1) == 33 231 | assert attr.value.getElement(3, 2) == 45 232 | assert attr.value.getElement(3, 3) == 1 233 | 234 | 235 | def test_should_setQuaternionValue_given_quaternion(): 236 | node = mx.Node(cmds.createNode('quatToEuler')) 237 | assert not cmds.getAttr('{}.outputRotateX'.format(node.uniqueName)) 238 | assert not cmds.getAttr('{}.outputRotateY'.format(node.uniqueName)) 239 | assert not cmds.getAttr('{}.outputRotateZ'.format(node.uniqueName)) 240 | mx.Attribute(node, 'inputQuat').value = mx.Quaternion(45, 90, 0, 0) 241 | assert cmds.getAttr('{}.outputRotateX'.format(node.uniqueName)) == 180 242 | assert not cmds.getAttr('{}.outputRotateY'.format(node.uniqueName)) 243 | assert round(cmds.getAttr('{}.outputRotateZ'.format(node.uniqueName)), 3) == 126.870 244 | 245 | 246 | def test_should_setQuaternionValue_given_openMayaQuaternion(): 247 | node = mx.Node(cmds.createNode('quatToEuler')) 248 | assert not cmds.getAttr('{}.outputRotateX'.format(node.uniqueName)) 249 | assert not cmds.getAttr('{}.outputRotateY'.format(node.uniqueName)) 250 | assert not cmds.getAttr('{}.outputRotateZ'.format(node.uniqueName)) 251 | mx.Attribute(node, 'inputQuat').value = om.MQuaternion(45, 90, 0, 0) 252 | assert cmds.getAttr('{}.outputRotateX'.format(node.uniqueName)) == 180 253 | assert not cmds.getAttr('{}.outputRotateY'.format(node.uniqueName)) 254 | assert round(cmds.getAttr('{}.outputRotateZ'.format(node.uniqueName)), 3) == 126.870 255 | 256 | 257 | def test_should_setColorValue_given_tuple(): 258 | node = mx.Node(cmds.createNode('ambientLight')) 259 | 260 | assert cmds.getAttr('{}.color'.format(node.uniqueName))[0] == (1, 1, 1) 261 | 262 | mx.Attribute(node, 'color').value = (0.2, 0.35, 0.9) 263 | 264 | assert cmds.getAttr('{}.color'.format(node.uniqueName))[0] == pytest.approx((0.2, 0.35, 0.9)) 265 | -------------------------------------------------------------------------------- /tests/test_attribute_connections.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | 5 | import mayax as mx 6 | 7 | 8 | def test_should_connectAttribute(): 9 | source = mx.Node(cmds.createNode('transform', name='source')) 10 | destination = mx.Node(cmds.createNode('transform', name='destination')) 11 | sourceAttr = mx.Attribute(source, 'translateX') 12 | destinationAttr = mx.Attribute(destination, 'translateY') 13 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 14 | assert not sourceAttr.value 15 | assert not destinationAttr.value 16 | sourceAttr.connect(destinationAttr) 17 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 18 | sourceAttr.value = 20 19 | assert sourceAttr.value == 20 20 | assert destinationAttr.value == 20 21 | 22 | 23 | def test_should_connectAttribute_given_extraFlags(): 24 | source = mx.Node(cmds.createNode('transform', name='source')) 25 | destination = mx.Node(cmds.createNode('transform', name='destination')) 26 | sourceAttr = mx.Attribute(source, 'translateX') 27 | destinationAttr = mx.Attribute(destination, 'translateY') 28 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 29 | assert not destinationAttr.locked 30 | sourceAttr.connect(destinationAttr, force=True, lock=True) 31 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 32 | assert destinationAttr.locked 33 | 34 | 35 | def test_should_connectAttribute_given_arrayAttribute(): 36 | source = mx.Node(cmds.createNode('transform', name='source')) 37 | destination = mx.Node(cmds.createNode('multMatrix', name='destination')) 38 | sourceAttr = mx.Attribute(source, 'worldMatrix') 39 | destinationAttr = mx.Attribute(destination, 'matrixIn[0]') 40 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 41 | sourceAttr.connect(destinationAttr) 42 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 43 | 44 | 45 | def test_should_disconnectAttribute(): 46 | source = mx.Node(cmds.createNode('transform', name='source')) 47 | destination = mx.Node(cmds.createNode('transform', name='destination')) 48 | sourceAttr = mx.Attribute(source, 'translateX') 49 | destinationAttr = mx.Attribute(destination, 'translateY') 50 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 51 | sourceAttr.connect(destinationAttr) 52 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 53 | sourceAttr.disconnect(destinationAttr) 54 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 55 | 56 | 57 | def test_should_disconnectAttributeInput(): 58 | source = mx.Node(cmds.createNode('transform', name='source')) 59 | destination = mx.Node(cmds.createNode('transform', name='destination')) 60 | 61 | sourceAttr = mx.Attribute(source, 'translate') 62 | destinationAttr = mx.Attribute(destination, 'translate') 63 | 64 | sourceAttr.connect(destinationAttr) 65 | 66 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 67 | 68 | destinationAttr.disconnectInput() 69 | 70 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 71 | 72 | 73 | def test_should_disconnectAttributeOutput_given_singleConnection(): 74 | source = mx.Node(cmds.createNode('transform', name='source')) 75 | destination = mx.Node(cmds.createNode('transform', name='destination')) 76 | 77 | sourceAttr = mx.Attribute(source, 'translate') 78 | destinationAttr = mx.Attribute(destination, 'translate') 79 | 80 | sourceAttr.connect(destinationAttr) 81 | 82 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 83 | 84 | sourceAttr.disconnectOutput() 85 | 86 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr.fullName) 87 | 88 | 89 | def test_should_disconnectAttributeOutput_given_multipleConnections(): 90 | source = mx.Node(cmds.createNode('transform', name='source')) 91 | destination1 = mx.Node(cmds.createNode('transform', name='destination1')) 92 | destination2 = mx.Node(cmds.createNode('transform', name='destination2')) 93 | 94 | sourceAttr = mx.Attribute(source, 'translate') 95 | destinationAttr1 = mx.Attribute(destination1, 'translate') 96 | destinationAttr2 = mx.Attribute(destination2, 'translate') 97 | 98 | sourceAttr.connect(destinationAttr1) 99 | sourceAttr.connect(destinationAttr2) 100 | 101 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr1.fullName) 102 | assert cmds.isConnected(sourceAttr.fullName, destinationAttr2.fullName) 103 | 104 | sourceAttr.disconnectOutput() 105 | 106 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr1.fullName) 107 | assert not cmds.isConnected(sourceAttr.fullName, destinationAttr2.fullName) 108 | 109 | 110 | def test_should_getInputConnection_given_attributeWithNoConnections(): 111 | node = mx.Node(cmds.createNode('transform')) 112 | attr = mx.Attribute(node, 'translate') 113 | 114 | assert not attr.input() 115 | 116 | 117 | def test_should_getInputConnection_given_attributeWithConnectionsOnBothSides(): 118 | node = mx.Node(cmds.createNode('transform', name='node')) 119 | inputNode = mx.Node(cmds.createNode('transform', name='inputNode')) 120 | outputNode = mx.Node(cmds.createNode('transform', name='outputNode')) 121 | 122 | attr = mx.Attribute(node, 'translate') 123 | inputAttr = mx.Attribute(inputNode, 'translate') 124 | outputAttr = mx.Attribute(outputNode, 'translate') 125 | 126 | inputAttr.connect(attr) 127 | attr.connect(outputAttr) 128 | 129 | inputConnection = attr.input() 130 | 131 | assert isinstance(inputConnection, mx.Node) 132 | assert inputConnection == inputNode 133 | 134 | 135 | def test_should_getInputConnection_given_specifiedType(): 136 | node = mx.Node(cmds.createNode('transform', name='node')) 137 | inputNode = mx.Node(cmds.createNode('multiplyDivide', name='inputNode')) 138 | 139 | attr = mx.Attribute(node, 'translate') 140 | inputAttr = mx.Attribute(inputNode, 'output') 141 | 142 | inputAttr.connect(attr) 143 | inputConnection = attr.input(type='multiplyDivide') 144 | 145 | assert isinstance(inputConnection, mx.Node) 146 | assert inputConnection == inputNode 147 | 148 | 149 | def test_should_getInputConnection_given_specifiedType_and_noValidConnection(): 150 | node = mx.Node(cmds.createNode('transform', name='node')) 151 | inputNode = mx.Node(cmds.createNode('transform', name='inputNode')) 152 | 153 | attr = mx.Attribute(node, 'translate') 154 | inputAttr = mx.Attribute(inputNode, 'translate') 155 | 156 | inputAttr.connect(attr) 157 | inputConnection = attr.input(type='multiplyDivide') 158 | 159 | assert not inputConnection 160 | 161 | 162 | def test_should_getInputConnectionAttribute_given_plugs(): 163 | node = mx.Node(cmds.createNode('transform', name='node')) 164 | inputNode = mx.Node(cmds.createNode('transform', name='inputNode')) 165 | 166 | attr = mx.Attribute(node, 'translate') 167 | inputAttr = mx.Attribute(inputNode, 'translate') 168 | 169 | inputAttr.connect(attr) 170 | inputConnection = attr.input(plugs=True) 171 | 172 | assert isinstance(inputConnection, mx.Attribute) 173 | assert inputConnection.fullName == 'inputNode.translate' 174 | 175 | 176 | def test_should_getOutputConnections_given_attributeWithNoConnections(): 177 | node = mx.Node(cmds.createNode('transform')) 178 | outputs = mx.Attribute(node, 'translate').outputs() 179 | 180 | assert not outputs 181 | assert isinstance(outputs, list) 182 | 183 | 184 | def test_should_getOutputConnections_given_attributeWithConnectionsOnBothSides(): 185 | node = mx.Node(cmds.createNode('transform', name='node')) 186 | inputNode = mx.Node(cmds.createNode('transform', name='inputNode')) 187 | outputNode = mx.Node(cmds.createNode('transform', name='outputNode')) 188 | 189 | attr = mx.Attribute(node, 'translate') 190 | inputAttr = mx.Attribute(inputNode, 'translate') 191 | outputAttr = mx.Attribute(outputNode, 'translate') 192 | 193 | inputAttr.connect(attr) 194 | attr.connect(outputAttr) 195 | 196 | outputs = attr.outputs() 197 | 198 | assert len(outputs) == 1 199 | assert isinstance(outputs[0], mx.Node) 200 | assert outputs[0] == outputNode 201 | 202 | 203 | def test_should_getOutputConnections_given_attributeWithMultipleOutputConnections(): 204 | node = mx.Node(cmds.createNode('transform', name='node')) 205 | outputNode1 = mx.Node(cmds.createNode('transform', name='outputNode1')) 206 | outputNode2 = mx.Node(cmds.createNode('transform', name='outputNode2')) 207 | 208 | attr = mx.Attribute(node, 'translate') 209 | outputAttr1 = mx.Attribute(outputNode1, 'translate') 210 | outputAttr2 = mx.Attribute(outputNode2, 'translate') 211 | 212 | attr.connect(outputAttr1) 213 | attr.connect(outputAttr2) 214 | 215 | outputs = attr.outputs() 216 | 217 | assert len(outputs) == 2 218 | assert isinstance(outputs[0], mx.Node) 219 | assert isinstance(outputs[1], mx.Node) 220 | assert outputs[0] == outputNode2 221 | assert outputs[1] == outputNode1 222 | 223 | 224 | def test_should_getOutputConnections_given_specifiedType(): 225 | node = mx.Node(cmds.createNode('transform', name='node')) 226 | outputNode1 = mx.Node(cmds.createNode('transform', name='outputNode1')) 227 | outputNode2 = mx.Node(cmds.createNode('multiplyDivide', name='outputNode2')) 228 | 229 | attr = mx.Attribute(node, 'translate') 230 | outputAttr1 = mx.Attribute(outputNode1, 'translate') 231 | outputAttr2 = mx.Attribute(outputNode2, 'input1') 232 | 233 | attr.connect(outputAttr1) 234 | attr.connect(outputAttr2) 235 | 236 | outputs = attr.outputs(type='multiplyDivide') 237 | 238 | assert len(outputs) == 1 239 | assert isinstance(outputs[0], mx.Node) 240 | assert outputs[0] == outputNode2 241 | 242 | 243 | def test_should_getOutputConnections_given_specifiedType_and_noValidConnections(): 244 | node = mx.Node(cmds.createNode('transform', name='node')) 245 | outputNode1 = mx.Node(cmds.createNode('transform', name='outputNode1')) 246 | outputNode2 = mx.Node(cmds.createNode('transform', name='outputNode2')) 247 | 248 | attr = mx.Attribute(node, 'translate') 249 | outputAttr1 = mx.Attribute(outputNode1, 'translate') 250 | outputAttr2 = mx.Attribute(outputNode2, 'translate') 251 | 252 | attr.connect(outputAttr1) 253 | attr.connect(outputAttr2) 254 | 255 | outputs = attr.outputs(type='multiplyDivide') 256 | 257 | assert not outputs 258 | assert isinstance(outputs, list) 259 | 260 | 261 | def test_should_getOutputConnectionsAttributes_given_plugs(): 262 | node = mx.Node(cmds.createNode('transform', name='node')) 263 | outputNode1 = mx.Node(cmds.createNode('transform', name='outputNode1')) 264 | outputNode2 = mx.Node(cmds.createNode('transform', name='outputNode2')) 265 | 266 | attr = mx.Attribute(node, 'translate') 267 | outputAttr1 = mx.Attribute(outputNode1, 'translate') 268 | outputAttr2 = mx.Attribute(outputNode2, 'translate') 269 | 270 | attr.connect(outputAttr1) 271 | attr.connect(outputAttr2) 272 | 273 | outputs = attr.outputs(plugs=True) 274 | 275 | assert len(outputs) == 2 276 | assert isinstance(outputs[0], mx.Attribute) 277 | assert isinstance(outputs[1], mx.Attribute) 278 | assert outputs[0].fullName == 'outputNode2.translate' 279 | assert outputs[1].fullName == 'outputNode1.translate' 280 | 281 | 282 | def test_should_raiseMayaAttributeError_given_invalidExtraFlag_when_connecting(): 283 | source = mx.Node(cmds.createNode('transform', name='source')) 284 | destination = mx.Node(cmds.createNode('transform', name='destination')) 285 | sourceAttr = mx.Attribute(source, 'translateX') 286 | destinationAttr = mx.Attribute(destination, 'translateY') 287 | with pytest.raises(mx.MayaAttributeError): 288 | sourceAttr.connect(destinationAttr, invalidExtraFlag=True) 289 | 290 | 291 | def test_should_raiseMayaAttributeError_given_incompatibleAttributes_when_connecting(): 292 | source = mx.Node(cmds.createNode('transform', name='source')) 293 | destination = mx.Node(cmds.createNode('transform', name='destination')) 294 | sourceAttr = mx.Attribute(source, 'translateX') 295 | destinationAttr = mx.Attribute(destination, 'translate') 296 | with pytest.raises(mx.MayaAttributeError): 297 | sourceAttr.connect(destinationAttr) 298 | 299 | 300 | def test_should_raiseMayaAttributeError_given_notConnectedAttributes_when_disconnecting(): 301 | source = mx.Node(cmds.createNode('transform', name='source')) 302 | destination = mx.Node(cmds.createNode('transform', name='destination')) 303 | sourceAttr = mx.Attribute(source, 'translateX') 304 | destinationAttr = mx.Attribute(destination, 'translateY') 305 | with pytest.raises(mx.MayaAttributeError): 306 | sourceAttr.disconnect(destinationAttr) 307 | -------------------------------------------------------------------------------- /tests/test_attribute_addition.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from maya import cmds 4 | 5 | import mayax as mx 6 | 7 | 8 | class SomeVector(mx.Vector): 9 | pass 10 | 11 | 12 | class SomeMatrix(mx.Matrix): 13 | pass 14 | 15 | 16 | def test_should_deleteAttribute(): 17 | node = mx.Node(cmds.createNode('transform', name='testNode')) 18 | cmds.addAttr(node.name, longName='testAttr') 19 | 20 | assert cmds.attributeQuery('testAttr', node='testNode', exists=True) 21 | 22 | mx.Attribute(node, 'testAttr').delete() 23 | 24 | assert not cmds.attributeQuery('testAttr', node='testNode', exists=True) 25 | 26 | 27 | # -------------------------------------------------------------------------------------------------- 28 | 29 | 30 | def test_should_addAttribute_given_extraFlags(): 31 | node = mx.Node(cmds.createNode('transform')) 32 | attr = mx.Attribute(node, 'testAttr', value='blah blah', sn='blah', nn='Nice Blah', k=True) 33 | assert cmds.getAttr(attr.fullName, type=True) == 'string' 34 | assert cmds.addAttr(attr.fullName, query=True, longName=True) == 'testAttr' 35 | assert cmds.addAttr(attr.fullName, query=True, shortName=True) == 'blah' 36 | assert cmds.addAttr(attr.fullName, query=True, niceName=True) == 'Nice Blah' 37 | assert cmds.addAttr(attr.fullName, query=True, keyable=True) 38 | 39 | 40 | def test_should_raiseMayaAttributeError_given_existingAttributeName_when_adding(): 41 | node = mx.Node(cmds.createNode('transform')) 42 | with pytest.raises(mx.MayaAttributeError): 43 | mx.Attribute(node, 'translateX', 0.0) 44 | 45 | 46 | def test_should_raiseMayaAttributeError_given_existingClassAttributeName_when_adding(): 47 | node = mx.Node(cmds.createNode('transform')) 48 | node.testAttr = 'test' 49 | with pytest.raises(mx.MayaAttributeError): 50 | mx.Attribute(node, 'testAttr', 'test') 51 | 52 | 53 | def test_should_raiseMayaAttributeError_given_invalidType(): 54 | node = mx.Node(cmds.createNode('transform')) 55 | with pytest.raises(mx.MayaAttributeError): 56 | mx.Attribute(node, 'testAttr', type='__invalidtype__') 57 | 58 | 59 | def test_should_raiseMayaAttributeError_given_uknownValue_and_noType(): 60 | node = mx.Node(cmds.createNode('transform')) 61 | with pytest.raises(mx.MayaAttributeError): 62 | mx.Attribute(node, 'testAttr', object) 63 | 64 | 65 | def test_should_raiseMayaAttributeError_given_unknownExtraFlag(): 66 | node = mx.Node(cmds.createNode('transform')) 67 | with pytest.raises(mx.MayaAttributeError): 68 | mx.Attribute(node, 'testAttr', 0, unkownFlag=0) 69 | 70 | 71 | # -------------------------------------------------------------------------------------------------- 72 | 73 | 74 | def test_should_addFloatAttribute_given_type(): 75 | node = mx.Node(cmds.createNode('transform')) 76 | mx.Attribute(node, 'testAttr', type='float') 77 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'float' 78 | 79 | 80 | def test_should_addFloatAttribute_given_pyType(): 81 | node = mx.Node(cmds.createNode('transform')) 82 | mx.Attribute(node, 'testAttr', type=float) 83 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'float' 84 | 85 | 86 | def test_should_addFloatAttribute_given_zeroValue(): 87 | node = mx.Node(cmds.createNode('transform')) 88 | mx.Attribute(node, 'testAttr', 0.0) 89 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'float' 90 | assert not cmds.getAttr('{}.testAttr'.format(node.uniqueName)) 91 | 92 | 93 | def test_should_addFloatAttribute_given_nonZeroValue(): 94 | node = mx.Node(cmds.createNode('transform')) 95 | mx.Attribute(node, 'testAttr', 20.5) 96 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'float' 97 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName)) == 20.5 98 | 99 | 100 | # -------------------------------------------------------------------------------------------------- 101 | 102 | 103 | def test_should_addIntAttribute_given_type(): 104 | node = mx.Node(cmds.createNode('transform')) 105 | mx.Attribute(node, 'testAttr', type='long') 106 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'long' 107 | 108 | 109 | def test_should_addIntAttribute_given_pyType(): 110 | node = mx.Node(cmds.createNode('transform')) 111 | mx.Attribute(node, 'testAttr', type=int) 112 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'long' 113 | 114 | 115 | def test_should_addIntAttribute_given_zeroValue(): 116 | node = mx.Node(cmds.createNode('transform')) 117 | mx.Attribute(node, 'testAttr', 0) 118 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'long' 119 | assert not cmds.getAttr('{}.testAttr'.format(node.uniqueName)) 120 | 121 | 122 | def test_should_addIntAttribute_given_nonZeroValue(): 123 | node = mx.Node(cmds.createNode('transform')) 124 | mx.Attribute(node, 'testAttr', 11) 125 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'long' 126 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName)) == 11 127 | 128 | 129 | # -------------------------------------------------------------------------------------------------- 130 | 131 | 132 | def test_should_addBoolAttribute_given_type(): 133 | node = mx.Node(cmds.createNode('transform')) 134 | mx.Attribute(node, 'testAttr', type='bool') 135 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'bool' 136 | 137 | 138 | def test_should_addBoolAttribute_given_pyType(): 139 | node = mx.Node(cmds.createNode('transform')) 140 | mx.Attribute(node, 'testAttr', type=bool) 141 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'bool' 142 | 143 | 144 | def test_should_addBoolAttribute_given_trueValue(): 145 | node = mx.Node(cmds.createNode('transform')) 146 | mx.Attribute(node, 'testAttr', True) 147 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'bool' 148 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName)) 149 | 150 | 151 | def test_should_addBoolAttribute_given_falseValue(): 152 | node = mx.Node(cmds.createNode('transform')) 153 | mx.Attribute(node, 'testAttr', False) 154 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'bool' 155 | assert not cmds.getAttr('{}.testAttr'.format(node.uniqueName)) 156 | 157 | 158 | # -------------------------------------------------------------------------------------------------- 159 | 160 | 161 | def test_should_addStringAttribute_given_type(): 162 | node = mx.Node(cmds.createNode('transform')) 163 | mx.Attribute(node, 'testAttr', type='string') 164 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'string' 165 | 166 | 167 | def test_should_addStringAttribute_given_pyType(): 168 | node = mx.Node(cmds.createNode('transform')) 169 | mx.Attribute(node, 'testAttr', type=str) 170 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'string' 171 | 172 | 173 | def test_should_addStringAttribute_given_emptyValue(): 174 | node = mx.Node(cmds.createNode('transform')) 175 | mx.Attribute(node, 'testAttr', '') 176 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'string' 177 | assert not cmds.getAttr('{}.testAttr'.format(node.uniqueName)) 178 | 179 | 180 | def test_should_addStringAttribute_given_nonEmptyValue(): 181 | node = mx.Node(cmds.createNode('transform')) 182 | mx.Attribute(node, 'testAttr', 'blah blah') 183 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'string' 184 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName)) == 'blah blah' 185 | 186 | 187 | # -------------------------------------------------------------------------------------------------- 188 | 189 | 190 | def test_should_addVectorAttribute_given_type(): 191 | node = mx.Node(cmds.createNode('transform')) 192 | assert mx.Attribute(node, 'testAttr', type='double3') 193 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'double3' 194 | 195 | 196 | def test_should_addVectorAttribute_given_pyType(): 197 | node = mx.Node(cmds.createNode('transform')) 198 | assert mx.Attribute(node, 'testAttr', type=mx.Vector) 199 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'double3' 200 | 201 | 202 | def test_should_addVectorAttribute_given_pyTypeSubclass(): 203 | node = mx.Node(cmds.createNode('transform')) 204 | assert mx.Attribute(node, 'testAttr', type=SomeVector) 205 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'double3' 206 | 207 | 208 | def test_should_addVectorAttribute_given_zeroValue(): 209 | node = mx.Node(cmds.createNode('transform')) 210 | assert mx.Attribute(node, 'vectorAttr', mx.Vector(0, 0, 0)) 211 | assert cmds.getAttr('{}.vectorAttr'.format(node.uniqueName), type=True) == 'double3' 212 | assert cmds.getAttr('{}.vectorAttrX'.format(node.uniqueName), type=True) == 'double' 213 | assert cmds.getAttr('{}.vectorAttrY'.format(node.uniqueName), type=True) == 'double' 214 | assert cmds.getAttr('{}.vectorAttrZ'.format(node.uniqueName), type=True) == 'double' 215 | assert not cmds.getAttr('{}.vectorAttrX'.format(node.uniqueName)) 216 | assert not cmds.getAttr('{}.vectorAttrY'.format(node.uniqueName)) 217 | assert not cmds.getAttr('{}.vectorAttrZ'.format(node.uniqueName)) 218 | 219 | 220 | def test_should_addVectorAttribute_given_nonZeroValue(): 221 | node = mx.Node(cmds.createNode('transform')) 222 | assert mx.Attribute(node, 'vectorAttr', mx.Vector(10.0, 12, 7.5)) 223 | assert cmds.getAttr('{}.vectorAttr'.format(node.uniqueName), type=True) == 'double3' 224 | assert cmds.getAttr('{}.vectorAttrX'.format(node.uniqueName), type=True) == 'double' 225 | assert cmds.getAttr('{}.vectorAttrY'.format(node.uniqueName), type=True) == 'double' 226 | assert cmds.getAttr('{}.vectorAttrZ'.format(node.uniqueName), type=True) == 'double' 227 | assert cmds.getAttr('{}.vectorAttrX'.format(node.uniqueName)) == 10.0 228 | assert cmds.getAttr('{}.vectorAttrY'.format(node.uniqueName)) == 12.0 229 | assert cmds.getAttr('{}.vectorAttrZ'.format(node.uniqueName)) == 7.5 230 | 231 | 232 | def test_should_addVectorAttribute_given_shortName(): 233 | node = mx.Node(cmds.createNode('transform')) 234 | assert mx.Attribute(node, 'vectorAttr', mx.Vector(10.0, 12, 7.5), shortName='va') 235 | assert cmds.getAttr('{}.va'.format(node.uniqueName), type=True) == 'double3' 236 | assert cmds.getAttr('{}.vaX'.format(node.uniqueName), type=True) == 'double' 237 | assert cmds.getAttr('{}.vaY'.format(node.uniqueName), type=True) == 'double' 238 | assert cmds.getAttr('{}.vaZ'.format(node.uniqueName), type=True) == 'double' 239 | assert cmds.getAttr('{}.vaX'.format(node.uniqueName)) == 10.0 240 | assert cmds.getAttr('{}.vaY'.format(node.uniqueName)) == 12.0 241 | assert cmds.getAttr('{}.vaZ'.format(node.uniqueName)) == 7.5 242 | 243 | 244 | def test_should_addVectorAttribute_given_subclassedVector(): 245 | node = mx.Node(cmds.createNode('transform')) 246 | assert mx.Attribute(node, 'vectorAttr', SomeVector(10.0, 12, 7.5)) 247 | assert cmds.getAttr('{}.vectorAttr'.format(node.uniqueName), type=True) == 'double3' 248 | assert cmds.getAttr('{}.vectorAttrX'.format(node.uniqueName), type=True) == 'double' 249 | assert cmds.getAttr('{}.vectorAttrY'.format(node.uniqueName), type=True) == 'double' 250 | assert cmds.getAttr('{}.vectorAttrZ'.format(node.uniqueName), type=True) == 'double' 251 | assert cmds.getAttr('{}.vectorAttrX'.format(node.uniqueName)) == 10.0 252 | assert cmds.getAttr('{}.vectorAttrY'.format(node.uniqueName)) == 12.0 253 | assert cmds.getAttr('{}.vectorAttrZ'.format(node.uniqueName)) == 7.5 254 | 255 | 256 | # -------------------------------------------------------------------------------------------------- 257 | 258 | 259 | def test_should_addMatrixAttribute_given_type(): 260 | node = mx.Node(cmds.createNode('transform')) 261 | assert mx.Attribute(node, 'testAttr', type='matrix') 262 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'matrix' 263 | 264 | 265 | def test_should_addMatrixAttribute_given_pyType(): 266 | node = mx.Node(cmds.createNode('transform')) 267 | assert mx.Attribute(node, 'testAttr', type=mx.Matrix) 268 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'matrix' 269 | 270 | 271 | def test_should_addMatrixAttribute_given_pyTypeSubclass(): 272 | node = mx.Node(cmds.createNode('transform')) 273 | assert mx.Attribute(node, 'testAttr', type=SomeMatrix) 274 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'matrix' 275 | 276 | 277 | def test_should_addMatrixAttribute_given_zeroValue(): 278 | mtx = mx.Matrix([ 279 | (1, 0, 0, 0), 280 | (0, 1, 0, 0), 281 | (0, 0, 1, 0), 282 | (0, 0, 0, 1) 283 | ]) 284 | node = mx.Node(cmds.createNode('transform')) 285 | assert mx.Attribute(node, 'testAttr', mtx) 286 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'matrix' 287 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName)) == [ 288 | 1, 0, 0, 0, 289 | 0, 1, 0, 0, 290 | 0, 0, 1, 0, 291 | 0, 0, 0, 1 292 | ] 293 | 294 | 295 | def test_should_addMatrixAttribute_given_nonZeroValue(): 296 | mtx = mx.Matrix([ 297 | (1, 0, 0, 0), 298 | (0, 1, 0, 0), 299 | (0, 0, 1, 0), 300 | (20, 33, 45, 1) 301 | ]) 302 | node = mx.Node(cmds.createNode('transform')) 303 | assert mx.Attribute(node, 'testAttr', mtx) 304 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'matrix' 305 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName)) == [ 306 | 1, 0, 0, 0, 307 | 0, 1, 0, 0, 308 | 0, 0, 1, 0, 309 | 20, 33, 45, 1 310 | ] 311 | 312 | 313 | def test_should_addMatrixAttribute_given_subclassedMatrix(): 314 | node = mx.Node(cmds.createNode('transform')) 315 | assert mx.Attribute(node, 'testAttr', SomeMatrix()) 316 | assert cmds.getAttr('{}.testAttr'.format(node.uniqueName), type=True) == 'matrix' 317 | 318 | 319 | # -------------------------------------------------------------------------------------------------- 320 | 321 | 322 | def test_should_addMessageAttribute_given_type(): 323 | node = mx.Node(cmds.createNode('transform')) 324 | mx.Attribute(node, 'messageAttr', type='message') 325 | assert cmds.getAttr('{}.messageAttr'.format(node.uniqueName), type=True) == 'message' 326 | 327 | 328 | def test_should_addMessageAttribute_given_pyType(): 329 | node = mx.Node(cmds.createNode('transform')) 330 | mx.Attribute(node, 'messageAttr', type=mx.Node) 331 | assert cmds.getAttr('{}.messageAttr'.format(node.uniqueName), type=True) == 'message' 332 | -------------------------------------------------------------------------------- /tests/test_dag_node_properties.py: -------------------------------------------------------------------------------- 1 | from maya import cmds 2 | 3 | import mayax as mx 4 | 5 | 6 | def test_should_getParent(): 7 | cmds.createNode('unknownDag', name='dagNode') 8 | assert mx.DagNode('dagNode').parent.uniqueName == 'transform1' 9 | 10 | 11 | def test_should_getUniqueParent(): 12 | cmds.createNode('transform', name='root1') 13 | cmds.createNode('transform', name='controls', parent='root1') 14 | cmds.createNode('transform', name='arm_ctrl', parent='root1|controls') 15 | 16 | cmds.createNode('transform', name='root2') 17 | cmds.createNode('transform', name='controls', parent='root2') 18 | cmds.createNode('transform', name='arm_ctrl', parent='root2|controls') 19 | 20 | assert mx.DagNode('root1|controls|arm_ctrl').parent.uniqueName == 'root1|controls' 21 | 22 | 23 | def test_should_getNoParent_if_notChild(): 24 | cmds.createNode('unknownDag', name='dagNode') 25 | assert not mx.DagNode('transform1').parent 26 | 27 | 28 | def test_should_setParent_given_name(): 29 | cmds.createNode('transform', name='parent') 30 | cmds.createNode('transform', name='child') 31 | assert not mx.DagNode('child').parent 32 | mx.DagNode('child').parent = 'parent' 33 | assert mx.DagNode('child').parent.uniqueName == 'parent' 34 | 35 | 36 | def test_should_setParent_given_node(): 37 | cmds.createNode('transform', name='parent') 38 | cmds.createNode('transform', name='child') 39 | assert not mx.DagNode('child').parent 40 | mx.DagNode('child').parent = mx.Node('parent') 41 | assert mx.DagNode('child').parent.uniqueName == 'parent' 42 | 43 | 44 | def test_should_setWorldParent_given_none(): 45 | cmds.createNode('transform', name='parent') 46 | cmds.createNode('transform', name='child', parent='parent') 47 | assert mx.DagNode('child').parent.uniqueName == 'parent' 48 | mx.DagNode('child').parent = None 49 | assert not mx.DagNode('child').parent 50 | 51 | 52 | def test_should_setWorldParent_given_unparentedNode(): 53 | cmds.createNode('transform', name='child') 54 | node = mx.DagNode('child') 55 | node.parent = None 56 | assert not node.parent 57 | 58 | 59 | def test_should_reparentShape(): 60 | cmds.createNode('transform', name='newCube') 61 | cmds.polyCube() 62 | assert mx.Node('pCubeShape1').parent.uniqueName == 'pCube1' 63 | mx.DagNode('pCubeShape1').setParent('newCube', shape=True, relative=True) 64 | assert mx.Node('pCubeShape1').parent.uniqueName == 'newCube' 65 | 66 | 67 | def test_should_getChildren_given_oneChild(): 68 | cmds.createNode('transform', name='parent') 69 | cmds.createNode('transform', name='child', parent='parent') 70 | assert len(mx.DagNode('parent').children) == 1 71 | assert mx.DagNode('parent').children[0].uniqueName == 'child' 72 | 73 | 74 | def test_should_getChildren_given_twoChildren(): 75 | cmds.createNode('transform', name='parent') 76 | cmds.createNode('transform', name='child1', parent='parent') 77 | cmds.createNode('transform', name='child2', parent='parent') 78 | assert len(mx.DagNode('parent').children) == 2 79 | assert mx.DagNode('parent').children[0].uniqueName == 'child1' 80 | assert mx.DagNode('parent').children[1].uniqueName == 'child2' 81 | 82 | 83 | def test_should_getChildren_given_identicalChildNameUnderDifferentParent(): 84 | cmds.createNode('transform', name='parent1') 85 | cmds.createNode('transform', name='child', parent='parent1') 86 | cmds.createNode('transform', name='parent2') 87 | cmds.createNode('transform', name='child', parent='parent2') 88 | assert len(mx.DagNode('parent1').children) == 1 89 | assert mx.DagNode('parent1').children[0].uniqueName == 'parent1|child' 90 | 91 | 92 | def test_should_getNoChildren_given_emptyParent(): 93 | cmds.createNode('transform', name='parent') 94 | assert isinstance(mx.DagNode('parent').children, list) 95 | assert not mx.DagNode('parent').children 96 | 97 | 98 | def test_should_getDescendents_given_jointsChain(): 99 | cmds.joint(name='root') 100 | cmds.joint(name='knee') 101 | cmds.joint(name='ankle') 102 | 103 | children = mx.DagNode('root').children 104 | descendents = mx.DagNode('root').descendents 105 | 106 | assert len(children) == 1 107 | assert children[0].uniqueName == 'knee' 108 | 109 | assert len(descendents) == 2 110 | assert descendents[0].uniqueName == 'ankle' 111 | assert descendents[1].uniqueName == 'knee' 112 | 113 | 114 | def test_should_getNoDescendents_given_emptyParent(): 115 | cmds.createNode('transform', name='parent') 116 | descendents = mx.DagNode('parent').descendents 117 | 118 | assert not descendents 119 | assert isinstance(descendents, list) 120 | 121 | 122 | def test_should_getShapes_given_nodeWithOneShape(): 123 | cmds.polyCube(name='cube') 124 | 125 | shapes = mx.DagNode('cube').shapes 126 | 127 | assert len(shapes) == 1 128 | assert shapes[0].name == 'cubeShape' 129 | 130 | 131 | def test_should_getShapes_given_nodeWithTwoShapes(): 132 | cmds.polyCube(name='cube') 133 | cmds.polySphere(name='sphere') 134 | 135 | mx.DagNode('sphereShape').setParent('cube', shape=True, relative=True) 136 | 137 | shapes = mx.DagNode('cube').shapes 138 | 139 | assert len(shapes) == 2 140 | assert shapes[0].name == 'cubeShape' 141 | assert shapes[1].name == 'sphereShape' 142 | 143 | 144 | def test_should_getNoShapes_given_emptyNode(): 145 | cmds.createNode('transform', name='parent') 146 | 147 | shapes = mx.DagNode('parent').shapes 148 | 149 | assert isinstance(shapes, list) 150 | assert not shapes 151 | 152 | 153 | def test_should_findChildren_given_specificName(): 154 | cmds.createNode('transform', name='parent') 155 | cmds.createNode('transform', name='child1', parent='parent') 156 | cmds.createNode('transform', name='child2', parent='parent') 157 | 158 | foundChildren = mx.DagNode('parent').findChildren('child1') 159 | 160 | assert len(foundChildren) == 1 161 | assert foundChildren[0].uniqueName == 'child1' 162 | 163 | 164 | def test_should_findChildren_given_wildcardName(): 165 | cmds.createNode('transform', name='parent') 166 | cmds.createNode('transform', name='child1', parent='parent') 167 | cmds.createNode('transform', name='child2', parent='parent') 168 | cmds.createNode('transform', name='extraChild', parent='parent') 169 | 170 | foundChildren = mx.DagNode('parent').findChildren('child*') 171 | 172 | assert len(foundChildren) == 2 173 | assert foundChildren[0].uniqueName == 'child1' 174 | assert foundChildren[1].uniqueName == 'child2' 175 | 176 | 177 | def test_should_findNoChildren_given_inexistentName(): 178 | cmds.createNode('transform', name='parent') 179 | cmds.createNode('transform', name='child1', parent='parent') 180 | cmds.createNode('transform', name='child2', parent='parent') 181 | 182 | foundChildren = mx.DagNode('parent').findChildren('iDoNotExist') 183 | 184 | assert not foundChildren 185 | assert isinstance(foundChildren, list) 186 | 187 | 188 | # -------------------------------------------------------------------------------------------------- 189 | 190 | 191 | def test_should_getWorldPosition(): 192 | cmds.createNode('transform', name='parent') 193 | cmds.createNode('transform', name='child', parent='parent') 194 | cmds.move(10, 20, 45, 'parent') 195 | cmds.move(1, 2, 15, 'child', relative=True) 196 | 197 | worldPosition = mx.DagNode('child').worldPosition 198 | 199 | assert isinstance(worldPosition, mx.Vector) 200 | assert worldPosition == mx.Vector(11, 22, 60) 201 | 202 | 203 | def test_should_getWorldPosition_given_frozenObject(): 204 | cmds.createNode('transform', name='parent') 205 | cmds.createNode('transform', name='frozenChild', parent='parent') 206 | cmds.move(10, 20, 45, 'parent') 207 | cmds.move(1, 2, 15, 'frozenChild', relative=True) 208 | 209 | node = mx.DagNode('frozenChild') 210 | node.freezeTransform() 211 | 212 | assert node.translate.isEquivalent(mx.Vector(0, 0, 0), 0.001) 213 | assert node.worldPosition.isEquivalent(mx.Vector(11, 22, 60), 0.001) 214 | 215 | assert isinstance(node.worldPosition, mx.Vector) 216 | 217 | 218 | def test_should_getWorldPosition_given_time(): 219 | cmds.createNode('transform', name='parent') 220 | cmds.createNode('transform', name='child', parent='parent') 221 | cmds.move(10, 20, 45, 'parent') 222 | cmds.move(1, 2, 15, 'child', relative=True) 223 | 224 | node = mx.DagNode('child') 225 | 226 | cmds.setKeyframe(node.uniqueName) 227 | cmds.setKeyframe(node.uniqueName, attribute='translateX', time=5, value=55) 228 | 229 | worldPosition = node.worldPosition 230 | futureWorldPosition = node.worldPositionAt(5) 231 | 232 | assert isinstance(futureWorldPosition, mx.Vector) 233 | assert worldPosition == mx.Vector(11, 22, 60) 234 | assert futureWorldPosition == mx.Vector(65, 22, 60) 235 | 236 | 237 | def test_should_getWorldPosition_given_time_and_frozenObject(): 238 | cmds.createNode('transform', name='parent') 239 | cmds.createNode('transform', name='frozenChild', parent='parent') 240 | cmds.move(10, 20, 45, 'parent') 241 | cmds.move(1, 2, 15, 'frozenChild', relative=True) 242 | 243 | node = mx.DagNode('frozenChild') 244 | node.freezeTransform() 245 | 246 | cmds.setKeyframe(node.uniqueName) 247 | cmds.setKeyframe(node.uniqueName, attribute='translateX', time=5, value=55) 248 | cmds.setKeyframe(node.uniqueName, attribute='translateY', time=5, value=-4) 249 | cmds.setKeyframe(node.uniqueName, attribute='translateZ', time=5, value=3) 250 | 251 | worldPosition = node.worldPosition 252 | futureWorldPosition = node.worldPositionAt(5) 253 | 254 | assert node.translate.isEquivalent(mx.Vector(0, 0, 0), 0.001) 255 | 256 | assert worldPosition.isEquivalent(mx.Vector(11, 22, 60), 0.001) 257 | assert futureWorldPosition.isEquivalent(mx.Vector(66, 18, 63), 0.001) 258 | 259 | assert isinstance(worldPosition, mx.Vector) 260 | assert isinstance(futureWorldPosition, mx.Vector) 261 | 262 | 263 | def test_should_setWorldPosition(): 264 | cmds.createNode('transform', name='parent') 265 | cmds.createNode('transform', name='child', parent='parent') 266 | cmds.move(10, 20, 45, 'parent') 267 | 268 | node = mx.DagNode('child') 269 | node.worldPosition = mx.Vector(11, 22, 60) 270 | 271 | assert node.translate == mx.Vector(1, 2, 15) 272 | assert node.worldPosition == mx.Vector(11, 22, 60) 273 | 274 | 275 | def test_should_setWorldPosition_given_locatorTransform(): 276 | locator = cmds.spaceLocator(name='child')[0] 277 | cmds.createNode('transform', name='parent') 278 | cmds.parent(locator, 'parent') 279 | 280 | cmds.move(10, 20, 45, 'parent') 281 | 282 | node = mx.DagNode('child') 283 | node.worldPosition = mx.Vector(11, 22, 60) 284 | 285 | assert node.translate == mx.Vector(1, 2, 15) 286 | assert node.worldPosition == mx.Vector(11, 22, 60) 287 | 288 | 289 | def test_should_setWorldPosition_given_frozenObject(): 290 | cmds.createNode('transform', name='parent') 291 | cmds.createNode('transform', name='frozenChild', parent='parent') 292 | cmds.move(10, 20, 45, 'parent') 293 | cmds.move(1, 2, 15, 'frozenChild', relative=True) 294 | 295 | node = mx.DagNode('frozenChild') 296 | node.freezeTransform() 297 | 298 | node.worldPosition = mx.Vector(15, 33, -5) 299 | 300 | assert node.translate == mx.Vector(4, 11, -65) 301 | assert node.worldPosition == mx.Vector(15, 33, -5) 302 | 303 | 304 | def test_should_getWorldRotation(): 305 | cmds.createNode('transform', name='parent') 306 | cmds.createNode('transform', name='child', parent='parent') 307 | cmds.rotate(10, 20, 45, 'parent') 308 | cmds.rotate(1, 2, 15, 'child', relative=True) 309 | 310 | node = mx.DagNode('child') 311 | 312 | assert isinstance(node.worldRotation, mx.Vector) 313 | assert node.worldRotation.isEquivalent(mx.Vector(16.05, 18.67, 60.97), 0.1) 314 | 315 | 316 | def test_should_getWorldRotation_given_time(): 317 | cmds.createNode('transform', name='parent') 318 | cmds.createNode('transform', name='child', parent='parent') 319 | cmds.rotate(10, 20, 45, 'parent') 320 | cmds.rotate(1, 2, 15, 'child', relative=True) 321 | 322 | node = mx.DagNode('child') 323 | 324 | cmds.setKeyframe(node.uniqueName) 325 | cmds.setKeyframe(node.uniqueName, attribute='rotateX', time=5, value=55) 326 | 327 | worldRotation = node.worldRotation 328 | futureWorldRotation = node.worldRotationAt(5) 329 | 330 | assert isinstance(futureWorldRotation, mx.Vector) 331 | assert worldRotation.isEquivalent(mx.Vector(16.05, 18.67, 60.97), 0.1) 332 | assert futureWorldRotation.isEquivalent(mx.Vector(70.05, 18.67, 60.97), 0.1) 333 | 334 | 335 | def test_should_setWorldRotation(): 336 | cmds.createNode('transform', name='parent') 337 | cmds.createNode('transform', name='child', parent='parent') 338 | cmds.rotate(10, 20, 45, 'parent') 339 | 340 | node = mx.DagNode('child') 341 | node.worldRotation = mx.Vector(16, 18, 60) 342 | 343 | assert node.rotate.isEquivalent(mx.Vector(1.22, 1.10, 14.27), 0.1) 344 | assert node.worldRotation.isEquivalent(mx.Vector(16, 18, 60)) 345 | 346 | 347 | def test_should_getWorldScale(): 348 | cmds.createNode('transform', name='parent') 349 | cmds.createNode('transform', name='child', parent='parent') 350 | cmds.scale(4, 1, 1.5, 'parent') 351 | cmds.scale(1, 2, 15, 'child', relative=True) 352 | 353 | worldScale = mx.DagNode('child').worldScale 354 | 355 | assert isinstance(worldScale, mx.Vector) 356 | assert worldScale == mx.Vector(4, 2, 22.5) 357 | 358 | 359 | def test_should_getWorldScale_given_time(): 360 | cmds.createNode('transform', name='parent') 361 | cmds.createNode('transform', name='child', parent='parent') 362 | cmds.scale(4, 1, 1.5, 'parent') 363 | cmds.scale(1, 2, 15, 'child', relative=True) 364 | 365 | node = mx.DagNode('child') 366 | 367 | cmds.setKeyframe(node.uniqueName) 368 | cmds.setKeyframe(node.uniqueName, attribute='scaleX', time=5, value=3) 369 | 370 | worldScale = node.worldScale 371 | futureWorldScale = node.worldScaleAt(5) 372 | 373 | assert isinstance(futureWorldScale, mx.Vector) 374 | assert worldScale == mx.Vector(4, 2, 22.5) 375 | assert futureWorldScale == mx.Vector(12, 2, 22.5) 376 | 377 | 378 | def test_should_setWorldScale_given_noParent(): 379 | cmds.createNode('transform', name='obj') 380 | 381 | node = mx.DagNode('obj') 382 | node.worldScale = mx.Vector(4, 5, 1) 383 | 384 | assert node.scale.isEquivalent(mx.Vector(4, 5, 1), 0.001) 385 | assert node.worldScale.isEquivalent(mx.Vector(4, 5, 1), 0.001) 386 | 387 | 388 | def test_should_setWorldScale_given_scaledParent(): 389 | cmds.createNode('transform', name='parent') 390 | cmds.createNode('transform', name='child', parent='parent') 391 | cmds.scale(2, 1, 3, 'parent') 392 | 393 | node = mx.DagNode('child') 394 | node.worldScale = mx.Vector(4, 5, 1) 395 | 396 | assert node.scale.isEquivalent(mx.Vector(2, 5, 0.333), 0.001) 397 | assert node.worldScale.isEquivalent(mx.Vector(4, 5, 1), 0.001) 398 | 399 | 400 | def test_should_setWorldScale_given_scaledParentJoint_and_unitChildScale(): 401 | cmds.joint(position=mx.Vector(-2, 0, 0), name='parentJoint') 402 | cmds.joint(position=mx.Vector(4, 0, 0), name='childJoint') 403 | 404 | cmds.scale(2, 1, 3, 'parentJoint') 405 | 406 | node = mx.DagNode('childJoint') 407 | node.worldScale = mx.Vector(1, 1, 1) 408 | 409 | assert node.scale.isEquivalent(mx.Vector(1, 1, 1), 0.001) 410 | assert node.worldScale.isEquivalent(mx.Vector(1, 1, 1), 0.001) 411 | 412 | 413 | def test_should_getWorldMatrix(): 414 | cmds.createNode('transform', name='parent') 415 | cmds.createNode('transform', name='child', parent='parent') 416 | 417 | cmds.move(2, 4, 3, 'parent') 418 | cmds.rotate(-11, 12, 30, 'parent') 419 | cmds.scale(2, 2, 2, 'parent') 420 | 421 | cmds.move(5, 6, 13, 'child') 422 | cmds.rotate(11, 45, 0, 'child') 423 | cmds.scale(2, 2, 2, 'child') 424 | 425 | worldMatrix = mx.DagNode('child').worldMatrix 426 | 427 | assert isinstance(worldMatrix, mx.Matrix) 428 | assert list(worldMatrix) == cmds.getAttr('child.worldMatrix') 429 | 430 | 431 | def test_should_setWorldMatrix(): 432 | cmds.createNode('transform', name='parent') 433 | cmds.createNode('transform', name='child', parent='parent') 434 | cmds.createNode('transform', name='target') 435 | 436 | cmds.move(2, 4, 3, 'parent') 437 | cmds.rotate(-11, 12, 30, 'parent') 438 | cmds.scale(2, 2, 2, 'parent') 439 | 440 | cmds.move(5, 6, 13, 'target') 441 | cmds.rotate(11, 45, 0, 'target') 442 | cmds.scale(2, 2, 2, 'target') 443 | 444 | child = mx.DagNode('child') 445 | target = mx.DagNode('target') 446 | 447 | child.worldMatrix = target.worldMatrix 448 | 449 | assert child.worldPosition.isEquivalent(mx.Vector(5, 6, 13), 0.001) 450 | assert child.worldRotation.isEquivalent(mx.Vector(11, 45, 0), 0.001) 451 | assert child.worldScale.isEquivalent(mx.Vector(2, 2, 2), 0.001) 452 | -------------------------------------------------------------------------------- /bin/_gencmd.py: -------------------------------------------------------------------------------- 1 | """Generate the wrapping for the maya.cmds.""" 2 | 3 | import os 4 | import sys 5 | import re 6 | import json 7 | import urllib.request 8 | import textwrap 9 | import colorama 10 | from bs4 import BeautifulSoup 11 | 12 | 13 | ARG_GEN_DATA = 'data' 14 | 15 | CACHE_DOCS = True 16 | CACHE_DIR = '__gencmd_cache__' 17 | 18 | CMDS_URL = 'http://help.autodesk.com/cloudhelp/2023/ENU/Maya-Tech-Docs/CommandsPython/' 19 | 20 | CMDS_MOD_DESCRIPTION = 'Wrap `maya.cmds` to accept and return instances of `Node`.' 21 | CMDS_MOD_PATH = 'src/mayax/cmd.py' 22 | CMDS_DATA_PATH = 'cmd_data.json' 23 | 24 | 25 | # -------------------------------------------------------------------------------------------------- 26 | 27 | 28 | def main(args): 29 | """Script execution entry point.""" 30 | kwargs = parseCommandLineArguments(args) 31 | 32 | if ARG_GEN_DATA in kwargs: 33 | generateCommandsData() 34 | else: 35 | generateCommandsModule() 36 | 37 | 38 | def parseCommandLineArguments(args): 39 | """Parse command line arguments and return a dictionary.""" 40 | kwargs = {} 41 | 42 | for arg in args: 43 | argParts = arg.split('=') 44 | 45 | key = argParts[0] 46 | value = argParts[1] if len(argParts) == 2 else None 47 | 48 | kwargs[key] = value 49 | 50 | return kwargs 51 | 52 | 53 | def generateCommandsModule(): 54 | """Generate the wrapped commands module.""" 55 | commandsData = getCommandsData() 56 | 57 | if not commandsData: 58 | printCommandsDataNotFoundWarning() 59 | return 60 | 61 | commands = commandsData['commands'] 62 | obsoluteCommands = [] 63 | 64 | with open(CMDS_MOD_PATH, 'w', encoding='utf-8') as cmdFile: 65 | cmdFile.write(f'"""{CMDS_MOD_DESCRIPTION}"""\n\n') 66 | 67 | cmdFile.write('# AUTO-GENERATED. Use `bin/gencmd` to update.\n') 68 | cmdFile.write( 69 | '# pylint: disable=redefined-builtin,too-many-lines,too-complex,too-many-branches\n\n' 70 | ) 71 | 72 | cmdFile.write('from maya import cmds\n\n') 73 | cmdFile.write('from .node import Node, Attribute, MayaNodeError, MayaAttributeError\n') 74 | cmdFile.write('from .strtype import STR_TYPE\n') 75 | 76 | cmdFile.write(f'\n\n{getWrapFunction()}\n') 77 | 78 | for cmd in commands: 79 | if cmd['obsolete']: 80 | obsoluteCommands.append(cmd['name']) 81 | continue 82 | 83 | wrappedCmd = wrapCommand(cmd) 84 | 85 | cmdFile.write(wrappedCmd) 86 | 87 | commandsCount = len(commands) - len(obsoluteCommands) 88 | 89 | print(f'{colorama.Fore.GREEN}-------\nSUCCESS\n-------') 90 | print(f'{colorama.Fore.LIGHTGREEN_EX}Commands generated: {commandsCount}') 91 | print(f'{colorama.Fore.LIGHTBLUE_EX}Obsolute commands ignored: {len(obsoluteCommands)}') 92 | print(f'{obsoluteCommands}{colorama.Fore.RESET}') 93 | 94 | 95 | def generateCommandsData(): 96 | """Generate the commands data for doing the wrapping.""" 97 | commands = getAllCommands() 98 | commandsData = {'commands': commands} 99 | 100 | saveCommandsData(commandsData) 101 | 102 | print(f'{colorama.Fore.GREEN}-------\nSUCCESS\n-------') 103 | print(f'{colorama.Fore.LIGHTGREEN_EX}Commands saved: {len(commands)}{colorama.Fore.RESET}') 104 | 105 | 106 | # -------------------------------------------------------------------------------------------------- 107 | 108 | 109 | def getCommandsData(): 110 | """Get the commands from the data file.""" 111 | try: 112 | with open(CMDS_DATA_PATH, 'r', encoding='utf-8') as dataFile: 113 | return json.load(dataFile) 114 | except IOError: 115 | return None 116 | except (KeyError, ValueError): 117 | print(f'{colorama.Fore.RED}Invalid commands data file!{colorama.Fore.RESET}') 118 | 119 | sys.exit() 120 | 121 | 122 | def saveCommandsData(data): 123 | """Save the commands data to a file.""" 124 | with open(CMDS_DATA_PATH, 'w', encoding='utf-8') as dataFile: 125 | json.dump(data, dataFile, indent=4, sort_keys=True, separators=(',', ': ')) 126 | 127 | dataFile.write('\n') 128 | 129 | 130 | def printCommandsDataNotFoundWarning(): 131 | """Print a warning if no commands data file was found.""" 132 | print( 133 | f'{colorama.Fore.YELLOW}No commands data found. ' 134 | f'Run `{__file__} {ARG_GEN_DATA}`.{colorama.Fore.RESET}' 135 | ) 136 | 137 | 138 | def getCommandHtml(cmdName): 139 | """Get the command's HTML documentation.""" 140 | html = '' 141 | 142 | if CACHE_DOCS: 143 | try: 144 | with open(f'{CACHE_DIR}/{cmdName}.html', 'r', encoding='utf-8') as cachedFile: 145 | html = cachedFile.read() 146 | except IOError: 147 | pass 148 | 149 | if not html: 150 | with urllib.request.urlopen(f'{CMDS_URL}{cmdName}.html') as url: 151 | html = url.read().decode() 152 | 153 | if CACHE_DOCS: 154 | if not os.path.exists(CACHE_DIR): 155 | os.makedirs(CACHE_DIR) 156 | 157 | with open(f'{CACHE_DIR}/{cmdName}.html', 'w', encoding='utf-8') as cachedFile: 158 | cachedFile.write(html) 159 | 160 | return html 161 | 162 | 163 | def getAllCommands(): 164 | """Get a list of all commands.""" 165 | html = getCommandHtml('index_all') 166 | parser = BeautifulSoup(html, features="html.parser") 167 | 168 | commands = [] 169 | 170 | for tag in parser.find_all('a'): 171 | cmdName = tag['href'].replace('.html', '') 172 | cmdInfo = getCommandInfo(cmdName) 173 | 174 | commands.append(cmdInfo) 175 | 176 | print(cmdName) 177 | 178 | return commands 179 | 180 | 181 | def getCommandInfo(cmdName): 182 | """Get the command info.""" 183 | cmdInfo = { 184 | 'name': cmdName, 185 | 'description': '', 186 | 'synopsis': '', 187 | 'categories': [], 188 | # 'flags': [], 189 | 'returnTypes': [], 190 | 'obsolete': False, 191 | } 192 | 193 | html = getCommandHtml(cmdName) 194 | 195 | cmdInfo['obsolete'] = getCommandObsolete(html, cmdName) 196 | 197 | if cmdInfo['obsolete']: 198 | return cmdInfo 199 | 200 | cmdInfo['categories'] = getCommandCategories(html) 201 | cmdInfo['description'] = getCommandDescription(html) 202 | cmdInfo['synopsis'] = getCommandSynopsis(html) 203 | # cmdInfo['flags'] = getCommandFlags(html) 204 | cmdInfo['returnTypes'] = getCommandReturnTypes(html) 205 | 206 | return cmdInfo 207 | 208 | 209 | def getCommandObsolete(html, cmdName): 210 | """Get the command's obsolete state.""" 211 | parser = BeautifulSoup(html, features="html.parser") 212 | 213 | if parser.find(string=re.compile(rf'{cmdName} \(Obsolete\)')): 214 | return True 215 | 216 | return False 217 | 218 | 219 | def getCommandDescription(html): 220 | """Get the command's description.""" 221 | description = '' 222 | 223 | startCut = '

Synopsis

' 224 | endCut = '

Return value

' 225 | 226 | html = html[html.find(startCut) + len(startCut) :] 227 | html = html[: html.find(endCut)] 228 | 229 | parser = BeautifulSoup(html, features="html.parser") 230 | 231 | parser.find(id='synopsis').decompose() 232 | parser.find('p').decompose() 233 | 234 | description = str(parser) 235 | 236 | description = [x.strip() for x in description.split('\n\n') if x][0] 237 | description = [x.strip() for x in description.split('

') if x][0] 238 | 239 | description = stripHtml(description) 240 | description.strip() 241 | 242 | description = description.split('.')[0].strip() + '.' 243 | 244 | return description 245 | 246 | 247 | def getCommandCategories(html): 248 | """Get the command's categories test.""" 249 | categories = [] 250 | 251 | parser = BeautifulSoup(html, features="html.parser") 252 | 253 | for tag in parser.find(id='banner').find_all('a', href=re.compile('^cat')): 254 | categories.append(tag.string) 255 | 256 | return categories 257 | 258 | 259 | def getCommandFlags(html): 260 | """Get the command's flags.""" 261 | flags = [] 262 | 263 | parser = BeautifulSoup(html, features="html.parser") 264 | 265 | flagsEl = parser.find('a', attrs={'name': 'hFlags'}) 266 | 267 | if not flagsEl: 268 | return flags 269 | 270 | flagsTable = flagsEl.parent.find_next_sibling('table') 271 | 272 | for row in flagsTable.find_all('tr', attrs={'bgcolor': '#EEEEEE'}): 273 | data = row.find_all('td') 274 | flagNames = data[0].find_all('b') 275 | descriptionTable = row.find_next_sibling().find('table') 276 | 277 | description = descriptionTable.find_all('td')[1].decode_contents() 278 | 279 | description = stripHtml(description) 280 | description = description.split('.')[0].strip() + '.' 281 | 282 | flags.append( 283 | { 284 | 'description': description, 285 | 'longName': flagNames[0].decode_contents().strip(), 286 | 'shortName': flagNames[1].decode_contents().strip(), 287 | 'type': data[1].find('i').decode_contents().strip(), 288 | } 289 | ) 290 | 291 | return flags 292 | 293 | 294 | def getCommandReturnTypes(html): 295 | """Get the command's return types.""" 296 | returnTypes = [] 297 | 298 | parser = BeautifulSoup(html, features="html.parser") 299 | 300 | returnTypesContainer = parser.find('a', attrs={'name': 'hReturn'}).parent.find_next_sibling() 301 | 302 | if returnTypesContainer.name != 'table': 303 | return returnTypes 304 | 305 | for returnTypeRowEl in returnTypesContainer.find_all('tr'): 306 | data = returnTypeRowEl.find_all('td') 307 | 308 | returnType = data[0].find('i').string 309 | description = data[1].decode_contents().strip() 310 | 311 | description = stripHtml(description) 312 | 313 | returnTypes.append( 314 | { 315 | 'description': description, 316 | 'type': returnType, 317 | } 318 | ) 319 | 320 | return returnTypes 321 | 322 | 323 | def getCommandSynopsis(html): 324 | """Get the command's synopsis.""" 325 | parser = BeautifulSoup(html, features="html.parser") 326 | 327 | synopsis = parser.find(id='synopsis').find('code').decode_contents().strip() 328 | 329 | synopsis = cleanText(synopsis) 330 | 331 | synopsis = re.sub(r'(.*?)', r'\2', synopsis) 332 | synopsis = re.sub(r'(.*?)', r'\1', synopsis) 333 | 334 | return synopsis 335 | 336 | 337 | def getCommandExamples(html): 338 | """Get the command's examples.""" 339 | parser = BeautifulSoup(html, features="html.parser") 340 | 341 | examplesEl = parser.find('a', attrs={'name': 'hExamples'}).parent.find_next_sibling('pre') 342 | 343 | return examplesEl.decode_contents() 344 | 345 | 346 | def cleanText(text): 347 | """Clean spaces, new lines, tabs, etc.""" 348 | return ' '.join(text.split()) 349 | 350 | 351 | def stripHtml(html): 352 | """Strip HTML from text and replace it with reStructuredText.""" 353 | html = cleanText(html) 354 | 355 | html = re.sub(r'(.*?)', r'`\1`', html) 356 | html = re.sub(r'(.*?)', r'`\1`', html) 357 | html = re.sub(r'(.*?)', r'`\1`', html) 358 | html = re.sub(r'(.*?)', r'`\1`', html) 359 | html = re.sub(r'(.*?)', r'`\1`', html) 360 | 361 | html = re.sub(r'
(.*?)
', r'``\1``', html) 362 | html = html.replace('
', '')
363 | 
364 |     html = html.replace('

', '') 365 | 366 | html = cleanText(html) 367 | 368 | html = html.replace('
', '\n') 369 | 370 | html = stripHtmlList(html) 371 | 372 | return html.strip() 373 | 374 | 375 | def stripHtmlList(html, innerPosition=0): 376 | """Strip HTML list from text and replace it with reStructuredText.""" 377 | parser = BeautifulSoup(html, features="html.parser") 378 | 379 | listEl = parser.find(('ul', 'ol')) 380 | 381 | if not listEl: 382 | return html 383 | 384 | listTag = listEl.name 385 | listIndex = html.find(f'<{listTag}') 386 | listContent = '' 387 | 388 | if not innerPosition: 389 | listContent += '\n' 390 | 391 | orderedListSeq = 1 392 | 393 | for listItem in listEl.find_all('li', recursive=False): 394 | innerListEl = listItem.find(('ul', 'ol')) 395 | innerListHtml = '' 396 | 397 | if innerListEl: 398 | innerListHtml = str(innerListEl) 399 | innerListEl.decompose() 400 | 401 | itemContent = listItem.decode_contents().strip() 402 | 403 | if itemContent: 404 | listContent += '\n{}{} {}'.format( 405 | ' ' * 4 * innerPosition, 406 | f'{orderedListSeq}.' if listTag == 'ol' else '-', 407 | itemContent, 408 | ) 409 | 410 | if innerListHtml: 411 | listContent += stripHtmlList(innerListHtml, innerPosition + 1) 412 | 413 | if listTag == 'ol': 414 | orderedListSeq += 1 415 | 416 | if not innerPosition: 417 | listContent += '\n\n' 418 | 419 | listEl.decompose() 420 | 421 | html = str(parser) 422 | 423 | html = html[:listIndex] + listContent + html[listIndex:] 424 | 425 | parser = BeautifulSoup(html, features="html.parser") 426 | 427 | if parser.find(('ul', 'ol')): 428 | return stripHtmlList(html) 429 | 430 | return html 431 | 432 | 433 | # -------------------------------------------------------------------------------------------------- 434 | 435 | 436 | def getWrapFunction(): 437 | """Get the wrap function used to wrap the commands.""" 438 | return ( 439 | 'def _wrapCommand(cmdFn, args, kwargs):\n' 440 | ' args = [\n' 441 | ' value.uniqueName if isinstance(value, Node)\n' 442 | ' else value.fullName if isinstance(value, Attribute)\n' 443 | ' else value\n' 444 | ' for value in args\n' 445 | ' ]\n' 446 | '\n' 447 | ' for k in kwargs:\n' 448 | ' if isinstance(kwargs[k], Node):\n' 449 | ' kwargs[k] = kwargs[k].uniqueName\n' 450 | ' elif isinstance(kwargs[k], list):\n' 451 | ' kwargs[k] = [\n' 452 | ' value.uniqueName if isinstance(value, Node) else value\n' 453 | ' for value in kwargs[k]\n' 454 | ' ]\n' 455 | '\n' 456 | ' result = cmdFn(*args, **kwargs)\n' 457 | '\n' 458 | ' if isinstance(result, STR_TYPE):\n' 459 | ' try:\n' 460 | ' if result.find(\'.\') != -1:\n' 461 | ' result = Attribute(result)\n' 462 | ' else:\n' 463 | ' result = Node(result)\n' 464 | ' except (MayaNodeError, MayaAttributeError):\n' 465 | ' pass\n' 466 | ' elif isinstance(result, list):\n' 467 | ' for i, value in enumerate(result):\n' 468 | ' if not isinstance(value, STR_TYPE):\n' 469 | ' continue\n' 470 | '\n' 471 | ' try:\n' 472 | ' if value.find(\'.\') != -1:\n' 473 | ' result[i] = Attribute(value)\n' 474 | ' else:\n' 475 | ' result[i] = Node(value)\n' 476 | ' except (MayaNodeError, MayaAttributeError):\n' 477 | ' pass\n' 478 | '\n' 479 | ' return result' 480 | ) 481 | 482 | 483 | def wrapCommand(cmd): 484 | """Wrap the command.""" 485 | # flags = '' 486 | # for flag in cmd['flags']: 487 | # flags += ( 488 | # ' {} ({}) : {}\n' 489 | # ' {}\n' 490 | # ).format( 491 | # flag['longName'], 492 | # flag['shortName'], 493 | # flag['type'], 494 | # wrapText(flag['description'], width=90, subsequentIndent=' ' * 8), 495 | # ) 496 | 497 | return ( 498 | '\n\n' 499 | 'def {0}(*args, **kwargs): # noqa\n' 500 | ' """{1}\n' 501 | '\n' 502 | ' {2}\n' 503 | '\n' 504 | ' {3}\n' 505 | ' """\n' 506 | ' return _wrapCommand(cmds.{0}, args, kwargs)\n' 507 | ).format( 508 | cmd['name'], 509 | wrapText(cmd['description'], width=90, subsequentIndent=' ' * 4), 510 | wrapText(cmd['synopsis'], width=90, subsequentIndent=' ' * 4), 511 | f'{CMDS_URL}{cmd["name"]}.html', 512 | ) 513 | 514 | 515 | def wrapText(text, width, initialIndent='', subsequentIndent=''): 516 | """Wrap text to fit `width`, keeping new lines.""" 517 | return '\n'.join( 518 | [ 519 | textwrap.fill( 520 | line.strip(), 521 | width=width, 522 | initial_indent=subsequentIndent if i else initialIndent, 523 | subsequent_indent=subsequentIndent, 524 | ).replace( 525 | '\n', '\n ' if line.startswith('- ') else '\n' 526 | ) # reStructuredText list 527 | for i, line in enumerate(text.splitlines()) 528 | ] 529 | ) 530 | 531 | 532 | # -------------------------------------------------------------------------------------------------- 533 | 534 | 535 | if __name__ == '__main__': 536 | main(sys.argv[1:]) 537 | -------------------------------------------------------------------------------- /src/mayax/node.py: -------------------------------------------------------------------------------- 1 | """Wrap a maya node.""" 2 | 3 | from __future__ import absolute_import 4 | 5 | import math 6 | 7 | from maya import cmds 8 | from maya.api import OpenMaya as om 9 | 10 | from .exceptions import MayaNodeError, MayaAttributeError 11 | from .math import Vector, Matrix, Quaternion 12 | from .strtype import STR_TYPE 13 | 14 | 15 | class Node(object): 16 | """Wrap a Maya node. 17 | 18 | Parameters 19 | ---------- 20 | node : str or Node or MObject 21 | The name (or instance) of an existing node. 22 | 23 | Raises 24 | ------ 25 | MayaNodeError 26 | When something went wrong. 27 | """ 28 | 29 | def __new__(cls, node): 30 | """Create an object instance based on the provided `node`.""" 31 | pyClass = cls 32 | mobject = None 33 | nodeFn = None 34 | nameFn = None 35 | uniqueNameFn = None 36 | 37 | if node is None: 38 | raise MayaNodeError('Invalid node name.') 39 | 40 | if isinstance(node, om.MObject): 41 | mobject = node 42 | elif isinstance(node, Node): 43 | node = node.uniqueName 44 | 45 | if not mobject: 46 | try: 47 | mobject = om.MGlobal.getSelectionListByName(node).getDependNode(0) 48 | except RuntimeError: 49 | raise MayaNodeError( 50 | 'Maya node "{}" does not exist (or is not unique).'.format(node) 51 | ) 52 | 53 | if mobject.hasFn(om.MFn.kDagNode): 54 | nodeFn = om.MFnDagNode(mobject) 55 | nameFn = nodeFn.name 56 | uniqueNameFn = nodeFn.partialPathName 57 | else: 58 | nodeFn = om.MFnDependencyNode(mobject) 59 | nameFn = nodeFn.name 60 | uniqueNameFn = nameFn 61 | 62 | pyClass = cls.__findPyClass(nodeFn) 63 | 64 | if not pyClass or not issubclass(pyClass, cls): 65 | raise MayaNodeError('The provided node is not of type "{}".'.format(cls.__name__)) 66 | 67 | obj = object.__new__(pyClass) 68 | 69 | obj.__dict__['apiObject'] = mobject 70 | obj.__dict__['apiObjectHandle'] = om.MObjectHandle(mobject) 71 | obj.__dict__['_Node__nameFn'] = nameFn 72 | obj.__dict__['_Node__uniqueNameFn'] = uniqueNameFn 73 | 74 | return obj 75 | 76 | @classmethod 77 | def __findPyClass(cls, nodeFn): 78 | nodeType = nodeFn.typeName 79 | 80 | # 1: Search for exact node wrap. 81 | if cls.__name__.lower() == nodeType.lower(): 82 | return cls 83 | 84 | # 2: Search for specialized node. 85 | if nodeFn.type() == om.MFn.kDagNode: 86 | return DagNode 87 | if cls is Node: 88 | return cls 89 | 90 | return None 91 | 92 | def __getitem__(self, name): 93 | return Attribute(self, name) 94 | 95 | def __getattr__(self, name): 96 | return self[name].value 97 | 98 | def __setattr__(self, name, value): 99 | isPyAttribute = ( 100 | hasattr(self.__class__, name) or name in self.__dict__ or not self.hasAttr(name) 101 | ) 102 | 103 | if isPyAttribute: 104 | super(Node, self).__setattr__(name, value) 105 | else: 106 | self[name].value = value 107 | 108 | def __eq__(self, other): 109 | if isinstance(other, Node): 110 | return self.apiObjectHandle == other.apiObjectHandle 111 | 112 | if isinstance(other, STR_TYPE): 113 | try: 114 | return self.apiObjectHandle == Node(other).apiObjectHandle 115 | except MayaNodeError: 116 | return False 117 | 118 | return NotImplemented 119 | 120 | def __ne__(self, other): 121 | if isinstance(other, Node): 122 | return self.apiObjectHandle != other.apiObjectHandle 123 | 124 | if isinstance(other, STR_TYPE): 125 | try: 126 | return self.apiObjectHandle != Node(other).apiObjectHandle 127 | except MayaNodeError: 128 | return True 129 | 130 | return NotImplemented 131 | 132 | def __repr__(self): 133 | return "{}('{}')".format(self.__class__.__name__, self.uniqueName) 134 | 135 | def __str__(self): 136 | return self.uniqueName 137 | 138 | @property 139 | def exists(self): 140 | """Check if the node still exists.""" 141 | return self.apiObjectHandle.isValid() 142 | 143 | @property 144 | def name(self): 145 | """Get the node's name.""" 146 | if self.exists: 147 | return self.__nameFn() 148 | 149 | raise MayaNodeError('Node is no longer valid.') 150 | 151 | @name.setter 152 | def name(self, name): 153 | self.rename(name) 154 | 155 | def rename(self, name, **kwargs): 156 | """Rename the node. 157 | 158 | Use `kwargs` to pass extra flags used by ``maya.cmds.rename``. 159 | 160 | For simple cases use the `name` property setter. 161 | """ 162 | cmds.rename(self.uniqueName, name, **kwargs) 163 | 164 | @property 165 | def uniqueName(self): 166 | """Get the node's unique name. 167 | 168 | For extra rename functionality use `rename()`. 169 | """ 170 | if self.exists: 171 | return self.__uniqueNameFn() 172 | 173 | raise MayaNodeError('Node is no longer valid.') 174 | 175 | @property 176 | def type(self): 177 | """str: The node's type.""" 178 | return cmds.nodeType(self.uniqueName) 179 | 180 | def delete(self): 181 | """Delete the node.""" 182 | cmds.delete(self.uniqueName) 183 | 184 | def duplicate(self, **kwargs): 185 | """Duplicate the node. 186 | 187 | Use `kwargs` to pass extra flags used by ``maya.cmds.duplicate``. 188 | """ 189 | return Node(cmds.duplicate(self.uniqueName, **kwargs)[0]) 190 | 191 | def select(self, **kwargs): 192 | """Select the node. 193 | 194 | Use `kwargs` to pass extra flags used by ``maya.cmds.select``. 195 | """ 196 | cmds.select(self.uniqueName, **kwargs) 197 | 198 | def addAttr(self, name, value=None, type=None, **kwargs): # pylint: disable=W0622 199 | """Add an attribute to the node. 200 | 201 | See `Attribute` class for more info. 202 | 203 | Returns 204 | ------- 205 | Attribute 206 | The attribute instance. 207 | 208 | Raises 209 | ------ 210 | MayaNodeError 211 | If insufficient arguments are provided. 212 | MayaAttributeError 213 | If the attribute couldn't be added. 214 | """ 215 | createAttr = value is not None or type or 'dataType' in kwargs or 'attributeType' in kwargs 216 | if not createAttr: 217 | raise MayaNodeError('Value or type must be provided.') 218 | 219 | return Attribute(self, name, value, type, **kwargs) 220 | 221 | def deleteAttr(self, name): 222 | """Delete the provided attribute.""" 223 | Attribute(self, name).delete() 224 | 225 | def hasAttr(self, name): 226 | """Check if the node has an attribute.""" 227 | try: 228 | Attribute(self, name) 229 | return True 230 | except MayaAttributeError: 231 | return False 232 | 233 | 234 | # -------------------------------------------------------------------------------------------------- 235 | # Attribute 236 | # -------------------------------------------------------------------------------------------------- 237 | 238 | 239 | _PY_ATTRIBUTE_TYPES = { 240 | str: 'string', 241 | float: 'float', 242 | int: 'long', 243 | bool: 'bool', 244 | Vector: 'double3', 245 | Matrix: 'matrix', 246 | Node: 'message', 247 | } 248 | _ATTRIBUTE_TYPES = [ 249 | 'bool', 250 | 'long', 251 | 'short', 252 | 'byte', 253 | 'char', 254 | 'enum', 255 | 'float', 256 | 'double', 257 | 'doubleAngle', 258 | 'doubleLinear', 259 | 'compound', 260 | 'message', 261 | 'time', 262 | 'matrix', 263 | 'fltMatrix', 264 | 'reflectance', 265 | 'spectrum', 266 | 'float2', 267 | 'float3', 268 | 'double2', 269 | 'double3', 270 | 'long2', 271 | 'long3', 272 | 'short2', 273 | 'short3', 274 | ] 275 | _DATA_TYPES = [ 276 | 'string', 277 | 'stringArray', 278 | 'matrix', 279 | 'reflectanceRGB', 280 | 'spectrumRGB', 281 | 'float2', 282 | 'float3', 283 | 'double2', 284 | 'double3', 285 | 'long2', 286 | 'long3', 287 | 'short2', 288 | 'short3', 289 | 'doubleArray', 290 | 'floatArray', 291 | 'Int32Array', 292 | 'vectorArray', 293 | 'pointArray', 294 | 'nurbsCurve', 295 | 'nurbsSurface', 296 | 'mesh', 297 | 'lattice', 298 | ] 299 | 300 | 301 | class Attribute(object): 302 | """Wrap a Maya attribute. 303 | 304 | Parameters 305 | ---------- 306 | node : str or `Node` 307 | The node that holds the attribute. 308 | name : str 309 | The attribute's long name. 310 | value : any 311 | The value of the attribute. Used only when adding a new attribute. 312 | type : string 313 | The attribute type (see ``maya.cmds.addAttr``). Used only when adding a new attribute. 314 | kwargs : 315 | Extra flags used by ``maya.cmds.addAttr``. 316 | 317 | Raises 318 | ------ 319 | MayaAttributeError 320 | When something went wrong. 321 | """ 322 | 323 | def __init__(self, node, name=None, value=None, type=None, **kwargs): # pylint: disable=W0622 324 | if isinstance(node, STR_TYPE): 325 | if name: 326 | node = Node(node) 327 | else: 328 | attrParts = node.split('.', 1) 329 | 330 | if len(attrParts) == 2: 331 | node = Node(attrParts[0]) 332 | name = attrParts[1] 333 | else: 334 | raise MayaAttributeError( 335 | 'Attribute name could not be retrieved from "{}".'.format(node) 336 | ) 337 | 338 | create = value is not None or type or 'dataType' in kwargs or 'attributeType' in kwargs 339 | 340 | if create: 341 | Attribute.__addAttr(node, name, value, type, kwargs) 342 | else: 343 | try: 344 | cmds.getAttr('{}.{}'.format(node.uniqueName, name.split('[')[0]), size=True) 345 | except ValueError: 346 | raise MayaAttributeError( 347 | 'Attribute "{}.{}" does not exist.'.format(node.uniqueName, name) 348 | ) 349 | 350 | self.__node = node 351 | self.__name = name 352 | 353 | if create and value is not None: 354 | self.value = value 355 | 356 | @staticmethod 357 | def __addAttr(node, name, value, attrType, kwargs): 358 | if name in node.__dict__: 359 | raise MayaAttributeError( 360 | 'Node object already has an attribute named "{}".'.format(name) 361 | ) 362 | 363 | kwargs['longName'] = name 364 | 365 | Attribute.__inferType(value, attrType, kwargs) 366 | 367 | try: 368 | cmds.addAttr(node.uniqueName, **kwargs) 369 | 370 | if 'attributeType' in kwargs and kwargs['attributeType'] == 'double3': 371 | childKwargs = {'parent': name} 372 | 373 | childKwargs['longName'] = name + 'X' 374 | if 'shortName' in kwargs: 375 | childKwargs['shortName'] = kwargs['shortName'] + 'X' 376 | cmds.addAttr(node.uniqueName, **childKwargs) 377 | 378 | childKwargs['longName'] = name + 'Y' 379 | if 'shortName' in kwargs: 380 | childKwargs['shortName'] = kwargs['shortName'] + 'Y' 381 | cmds.addAttr(node.uniqueName, **childKwargs) 382 | 383 | childKwargs['longName'] = name + 'Z' 384 | if 'shortName' in kwargs: 385 | childKwargs['shortName'] = kwargs['shortName'] + 'Z' 386 | cmds.addAttr(node.uniqueName, **childKwargs) 387 | except (RuntimeError, TypeError) as e: 388 | raise MayaAttributeError(e) 389 | 390 | @staticmethod 391 | def __inferType(value, attrType, kwargs): 392 | if not attrType and 'dataType' not in kwargs and 'attributeType' not in kwargs: 393 | attrType = type(value) 394 | 395 | if isinstance(attrType, type): 396 | if issubclass(attrType, Node): 397 | attrType = Node 398 | elif issubclass(attrType, Vector): 399 | attrType = Vector 400 | elif issubclass(attrType, Matrix): 401 | attrType = Matrix 402 | 403 | if attrType in _PY_ATTRIBUTE_TYPES: 404 | attrType = _PY_ATTRIBUTE_TYPES[attrType] 405 | else: 406 | raise MayaAttributeError('Type could not be inferred from "{}".'.format(attrType)) 407 | 408 | if attrType in _ATTRIBUTE_TYPES: 409 | kwargs['attributeType'] = attrType 410 | elif attrType in _DATA_TYPES: 411 | kwargs['dataType'] = attrType 412 | else: 413 | raise MayaAttributeError('Invalid type "{}".'.format(attrType)) 414 | 415 | def __repr__(self): 416 | return "{}('{}')".format(self.__class__.__name__, self.fullName) 417 | 418 | def __str__(self): 419 | return self.fullName 420 | 421 | @property 422 | def name(self): 423 | """str: The attribute's name.""" 424 | return self.__name 425 | 426 | @property 427 | def fullName(self): 428 | """str: The attribute's name with the node's name in it.""" 429 | return '{}.{}'.format(self.__node.uniqueName, self.__name) 430 | 431 | @property 432 | def node(self): 433 | """`Node`: The attribute's node.""" 434 | return self.__node 435 | 436 | @property 437 | def type(self): 438 | """str: The attribute's type.""" 439 | return cmds.getAttr(self.fullName, type=True) 440 | 441 | @property 442 | def value(self): 443 | """any: The attribute's value.""" 444 | return self.valueAt(None) 445 | 446 | @value.setter 447 | def value(self, value): 448 | if self.type == 'message': 449 | if value: 450 | if isinstance(value, STR_TYPE): 451 | value = Node(value) 452 | value['message'].connect(self, force=True) 453 | elif self.value: 454 | self.value['message'].disconnect(self) 455 | else: 456 | args = (value,) 457 | kwargs = {} 458 | 459 | if isinstance(value, STR_TYPE): 460 | kwargs['type'] = 'string' 461 | elif isinstance(value, (om.MVector, tuple, list)) and self.type == 'float3': 462 | kwargs['type'] = 'float3' 463 | args = tuple(value) 464 | elif isinstance(value, (om.MVector, tuple, list)) and self.type == 'double3': 465 | kwargs['type'] = 'double3' 466 | args = tuple(value) 467 | elif isinstance(value, om.MMatrix): 468 | kwargs['type'] = 'matrix' 469 | args = tuple(value) 470 | elif isinstance(value, om.MQuaternion): 471 | args = tuple(value) 472 | 473 | cmds.setAttr(self.fullName, *args, **kwargs) 474 | 475 | def valueAt(self, time): 476 | """Get the attribute's value.""" 477 | cmdFlags = {} 478 | 479 | if time is not None: 480 | cmdFlags['time'] = time 481 | 482 | if self.type == 'message': 483 | nodeName = cmds.connectionInfo(self.fullName, sourceFromDestination=True) 484 | 485 | if nodeName: 486 | return Node(nodeName.split('.')[0]) 487 | 488 | return None 489 | 490 | value = cmds.getAttr(self.fullName, **cmdFlags) 491 | 492 | if self.type == 'float3': 493 | value = value[0] 494 | elif self.type == 'double3': 495 | value = Vector(value[0]) 496 | elif self.type == 'matrix': 497 | if value is None: # to fix Maya 2019 498 | value = Matrix.kIdentity 499 | else: 500 | value = Matrix(value) 501 | elif self.type == 'TdataCompound': 502 | isQuat = ( 503 | len(value) 504 | and len(value[0]) == 4 505 | and cmds.attributeQuery( 506 | self.__name + 'W', 507 | node=self.__node.uniqueName, 508 | exists=True, 509 | ) 510 | ) 511 | if isQuat: 512 | value = Quaternion(value[0]) # pylint: disable=redefined-variable-type 513 | 514 | return value 515 | 516 | @property 517 | def locked(self): 518 | """bool: The attribute's lock state.""" 519 | return cmds.getAttr(self.fullName, lock=True) 520 | 521 | @locked.setter 522 | def locked(self, value): 523 | cmds.setAttr(self.fullName, lock=value) 524 | 525 | @property 526 | def keyable(self): 527 | """bool: The attribute's keyable state.""" 528 | return cmds.getAttr(self.fullName, keyable=True) 529 | 530 | @keyable.setter 531 | def keyable(self, value): 532 | cmds.setAttr(self.fullName, keyable=value) 533 | 534 | @property 535 | def channelBox(self): 536 | """bool: The attribute's channelBox state.""" 537 | return cmds.getAttr(self.fullName, channelBox=True) 538 | 539 | @channelBox.setter 540 | def channelBox(self, value): 541 | cmds.setAttr(self.fullName, channelBox=value) 542 | 543 | def connect(self, attr, **kwargs): 544 | """Connect the attribute to another. 545 | 546 | Parameters 547 | ---------- 548 | attr : Attribute 549 | The destination attribute receiving the connection. 550 | kwargs : 551 | Extra flags used by ``maya.cmds.connectAttr``. 552 | 553 | Raises 554 | ------ 555 | MayaAttributeError 556 | If the connection was not successful. 557 | """ 558 | try: 559 | cmds.connectAttr(self.fullName, attr.fullName, **kwargs) 560 | except (RuntimeError, TypeError) as e: 561 | raise MayaAttributeError(e) 562 | 563 | def delete(self): 564 | """Delete the attribute.""" 565 | cmds.deleteAttr(self.fullName) 566 | 567 | def disconnect(self, attr, **kwargs): 568 | """Disconnect the attribute from another. 569 | 570 | Parameters 571 | ---------- 572 | attr : Attribute 573 | The destination attribute to disconnect from. 574 | kwargs : 575 | Extra flags used by ``maya.cmds.disconnectAttr``. 576 | 577 | Raises 578 | ------ 579 | MayaAttributeError 580 | If the disconnection was not successful. 581 | """ 582 | try: 583 | cmds.disconnectAttr(self.fullName, attr.fullName, **kwargs) 584 | except (RuntimeError, TypeError) as e: 585 | raise MayaAttributeError(e) 586 | 587 | def disconnectInput(self): 588 | """Disconnect the attribute's input.""" 589 | inputAttr = self.input(plugs=True) 590 | 591 | if inputAttr: 592 | inputAttr.disconnect(self) 593 | 594 | def disconnectOutput(self): 595 | """Disconnect the attribute's output.""" 596 | outputs = self.outputs(plugs=True) 597 | 598 | for attr in outputs: 599 | self.disconnect(attr) 600 | 601 | def input(self, **kwargs): 602 | """Get the attribute's input connection. 603 | 604 | Use `kwargs` to pass extra flags used by ``maya.cmds.listConnections``. 605 | """ 606 | kwargs['destination'] = False 607 | connections = cmds.listConnections(self.fullName, **kwargs) 608 | 609 | if connections: 610 | if 'plugs' in kwargs: 611 | return Attribute(connections[0]) 612 | 613 | return Node(connections[0]) 614 | 615 | return None 616 | 617 | def outputs(self, **kwargs): 618 | """Get the attribute's output connections. 619 | 620 | Use `kwargs` to pass extra flags used by ``maya.cmds.listConnections``. 621 | """ 622 | kwargs['source'] = False 623 | connections = cmds.listConnections(self.fullName, **kwargs) 624 | 625 | if connections: 626 | if 'plugs' in kwargs: 627 | return [Attribute(attrFullName) for attrFullName in connections] 628 | 629 | return [Node(nodeName) for nodeName in connections] 630 | 631 | return [] 632 | 633 | 634 | # -------------------------------------------------------------------------------------------------- 635 | # Specialized Nodes 636 | # -------------------------------------------------------------------------------------------------- 637 | 638 | 639 | class DagNode(Node): 640 | """Maintain data related to Maya's Directed Acyclic Graph (DAG).""" 641 | 642 | @property 643 | def pathName(self): 644 | """Return the full path from the root of the dag to this object.""" 645 | if not self.exists: 646 | raise MayaNodeError('Node is no longer valid.') 647 | 648 | return om.MFnDagNode(self.apiObject).fullPathName() 649 | 650 | @property 651 | def parent(self): 652 | """`Node`: Get/set the node's parent.""" 653 | try: 654 | return Node(cmds.listRelatives(self.uniqueName, parent=True, path=True)[0]) 655 | except TypeError: 656 | return None 657 | 658 | @parent.setter 659 | def parent(self, value): 660 | self.setParent(value) 661 | 662 | def setParent(self, value, **kwargs): 663 | """Set the node's parent. 664 | 665 | Use `kwargs` to pass extra flags used by ``maya.cmds.parent``. 666 | 667 | For simple cases use the `parent` property setter. 668 | """ 669 | if value: 670 | if isinstance(value, Node): 671 | value = value.uniqueName 672 | cmds.parent(self.uniqueName, value, **kwargs) 673 | elif self.parent: 674 | cmds.parent(self.uniqueName, world=True) 675 | 676 | @property 677 | def children(self): 678 | """list: Get the node's children.""" 679 | children = cmds.listRelatives(self.uniqueName, children=True, path=True) 680 | 681 | if children: 682 | return [Node(child) for child in children] 683 | 684 | return [] 685 | 686 | @property 687 | def descendents(self): 688 | """list: Get the node's descendents.""" 689 | descendents = cmds.listRelatives(self.uniqueName, allDescendents=True, path=True) 690 | 691 | if descendents: 692 | return [Node(descendent) for descendent in descendents] 693 | 694 | return [] 695 | 696 | @property 697 | def shapes(self): 698 | """list: Get the node's shapes.""" 699 | shapes = cmds.listRelatives(self.uniqueName, shapes=True, path=True) 700 | 701 | if shapes: 702 | return [Node(shape) for shape in shapes] 703 | 704 | return [] 705 | 706 | def findChildren(self, name): 707 | """Find children by name.""" 708 | children = cmds.ls('{}|{}'.format(self.pathName, name)) 709 | 710 | return [Node(child) for child in children] 711 | 712 | @property 713 | def worldPosition(self): 714 | """`Vector`: Return the node's world position.""" 715 | return Vector(cmds.xform(self.uniqueName, query=True, rotatePivot=True, worldSpace=True)) 716 | 717 | @worldPosition.setter 718 | def worldPosition(self, value): 719 | value -= Vector(cmds.xform(self.uniqueName, query=True, rotatePivot=True)) 720 | cmds.xform(self.uniqueName, worldSpace=True, translation=value) 721 | 722 | @property 723 | def worldRotation(self): 724 | """`Vector`: Return the node's world rotation.""" 725 | return Vector(cmds.xform(self.uniqueName, query=True, rotation=True, worldSpace=True)) 726 | 727 | @worldRotation.setter 728 | def worldRotation(self, value): 729 | cmds.xform(self.uniqueName, worldSpace=True, rotation=value) 730 | 731 | @property 732 | def worldScale(self): 733 | """`Vector`: Return the node's world scale.""" 734 | return Vector(cmds.xform(self.uniqueName, query=True, scale=True, worldSpace=True)) 735 | 736 | @worldScale.setter 737 | def worldScale(self, value): 738 | scale = self.scale 739 | worldMatrix = self.worldMatrix 740 | 741 | transformationMatrix = om.MTransformationMatrix(worldMatrix) 742 | transformationMatrix.setScale(value, om.MSpace.kTransform) 743 | 744 | scaleRatio = om.MTransformationMatrix( 745 | transformationMatrix.asMatrix() * worldMatrix.inverse() 746 | ).scale(om.MSpace.kTransform) 747 | 748 | cmds.xform( 749 | self.uniqueName, 750 | scale=( 751 | scale.x * scaleRatio[0], 752 | scale.y * scaleRatio[1], 753 | scale.z * scaleRatio[2], 754 | ), 755 | ) 756 | 757 | @property 758 | def worldMatrix(self): 759 | """`Matrix`: Return the node's world matrix.""" 760 | return Matrix(cmds.xform(self.uniqueName, query=True, matrix=True, worldSpace=True)) 761 | 762 | @worldMatrix.setter 763 | def worldMatrix(self, value): 764 | cmds.xform(self.uniqueName, matrix=value, worldSpace=True) 765 | 766 | def worldPositionAt(self, time): 767 | """Get the world position at specified time.""" 768 | worldMatrix = self['worldMatrix'].valueAt(time) 769 | rotatePivot = self['rotatePivot'].valueAt(time) 770 | 771 | return Vector(om.MPoint(rotatePivot) * worldMatrix) 772 | 773 | def worldRotationAt(self, time): 774 | """Get the world rotation at specified time.""" 775 | transformationMatrix = om.MTransformationMatrix(self['worldMatrix'].valueAt(time)) 776 | rotation = transformationMatrix.rotation() 777 | 778 | return Vector( 779 | math.degrees(rotation.x), 780 | math.degrees(rotation.y), 781 | math.degrees(rotation.z), 782 | ) 783 | 784 | def worldScaleAt(self, time): 785 | """Get the world scale at specified time.""" 786 | transformationMatrix = om.MTransformationMatrix(self['worldMatrix'].valueAt(time)) 787 | 788 | return Vector(transformationMatrix.scale(om.MSpace.kWorld)) 789 | 790 | def freezeTransform(self, **kwargs): 791 | """Make the current transformations be the zero position. 792 | 793 | Use `kwargs` to pass extra flags used by ``maya.cmds.makeIdentity``. 794 | """ 795 | kwargs['apply'] = True 796 | cmds.makeIdentity(self.uniqueName, **kwargs) 797 | 798 | def resetTransform(self, **kwargs): 799 | """Reset transformations back to zero (return to first or last "frozen" position). 800 | 801 | Use `kwargs` to pass extra flags used by ``maya.cmds.makeIdentity``. 802 | """ 803 | kwargs['apply'] = False 804 | cmds.makeIdentity(self.uniqueName, **kwargs) 805 | --------------------------------------------------------------------------------