├── requirements └── dev.txt ├── tests ├── conftest.py └── maya │ ├── conftest.py │ ├── test_node.py │ ├── test_api.py │ ├── test_plug.py │ └── test_dag.py ├── README.md ├── src └── maya_fn │ ├── __init__.py │ ├── node.py │ ├── api.py │ ├── dg.py │ ├── dag.py │ └── plug.py ├── LICENSE ├── tox.ini └── .gitignore /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | tox 2 | pytest -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Global test suite config.""" 2 | -------------------------------------------------------------------------------- /tests/maya/conftest.py: -------------------------------------------------------------------------------- 1 | """Maya test suite config.""" 2 | 3 | import atexit 4 | import os 5 | import pytest 6 | 7 | import maya.standalone 8 | 9 | maya.standalone.initialize() 10 | atexit.register(maya.standalone.uninitialize) 11 | 12 | from maya import cmds 13 | 14 | 15 | @pytest.fixture(scope="function") 16 | def new_scene(): 17 | """Open a new scene before running this test.""" 18 | 19 | cmds.file(new=True, force=True) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maya FunctionSet (maya_fn) 2 | A package that decompose core maya.cmds and maya.api features to a set of simple functions. 3 | 4 | # Tests 5 | The recommended approach for running tests is to setup a virtual environment. Edit the `activate.bat` script to include the `/src` directory and set a `MAYA_LOCATION` environment variable, as shown below. 6 | 7 | ``` 8 | python -m virtualenv venv 9 | venv\Scripts\activate.bat 10 | pip install -r requirements/dev.txt 11 | ``` 12 | 13 | ``` 14 | # activate.bat 15 | set "PYTHONPATH=%VIRTUAL_ENV%\..\src" 16 | set "MAYA_LOCATION=C:\Program Files\Autodesk\Maya2020" 17 | ``` 18 | 19 | You can execute the following `tox` environments: 20 | 21 | - `tox` runs all environments, including: `maya` 22 | - `tox -e maya` runs the maya tests 23 | - `tox -e black` runs Black on the code. 24 | - `tox -e lint` runs flake8 and pydocstyles on the code. 25 | -------------------------------------------------------------------------------- /src/maya_fn/__init__.py: -------------------------------------------------------------------------------- 1 | """Maya Function Set. 2 | 3 | A set of wrappers around a set of maya.cmds and maya.api behaviors. 4 | """ 5 | 6 | import functools 7 | 8 | from maya import cmds 9 | 10 | import maya_fn.dg as dg # noqa 11 | import maya_fn.dag as dag # noqa 12 | import maya_fn.node as node # noqa 13 | 14 | from maya_fn.plug import plug # noqa 15 | 16 | __author__ = "Ryan Rorter" 17 | __version__ = "0.0.1" 18 | __license__ = "MIT" 19 | 20 | 21 | def undoable(name): 22 | """Create an undo chunk every time the decorated function is called.""" 23 | 24 | def wrapper(func): 25 | @functools.wraps(func) 26 | def wrapped(*args, **kwargs): 27 | cmds.undoInfo(chunkName=name, openChunk=True) 28 | try: 29 | return func(*args, **kwargs) 30 | finally: 31 | cmds.undoInfo(closeChunk=True) 32 | 33 | return wrapped 34 | 35 | return wrapper 36 | -------------------------------------------------------------------------------- /tests/maya/test_node.py: -------------------------------------------------------------------------------- 1 | """Attribute function set test suite.""" 2 | 3 | import pytest 4 | 5 | from maya import cmds 6 | 7 | import maya_fn 8 | 9 | 10 | def test_add_attr(): 11 | node = cmds.createNode("transform", parent=cmds.createNode("transform")) 12 | node = maya_fn.dag.full_path(node) 13 | 14 | expected = maya_fn.plug(node, "foobar") 15 | actual = maya_fn.node.add_attr(node, ln="foobar") 16 | 17 | assert expected == actual 18 | 19 | with pytest.raises(ValueError): 20 | maya_fn.node.add_attr(node) 21 | 22 | expected = ["|persp.foobar", "|front.foobar", "|top.foobar", "|side.foobar"] 23 | actual = maya_fn.node.add_attr("persp", "front", "top", "side", ln="foobar") 24 | 25 | assert expected == actual 26 | 27 | 28 | def test_add_compound_attr(): 29 | node = cmds.createNode("transform") 30 | node = maya_fn.dag.full_path(node) 31 | 32 | expected = maya_fn.plug(node, "fizz", "buzz") 33 | 34 | maya_fn.node.add_attr(node, ln="fizz", at="compound", nc=1) 35 | actual = maya_fn.node.add_attr(node, ln="buzz", p="fizz") 36 | 37 | assert expected == actual 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ryan Porter 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 | -------------------------------------------------------------------------------- /src/maya_fn/node.py: -------------------------------------------------------------------------------- 1 | """Maya attribute function set.""" 2 | 3 | from maya import cmds 4 | 5 | import maya_fn.plug 6 | 7 | __all__ = [ 8 | "add_attr", 9 | "of_type", 10 | ] 11 | 12 | 13 | def of_type(nodes, node_type): 14 | """Yield the nodes of the given type.""" 15 | 16 | return cmds.ls(list(nodes), type=node_type, long=True) 17 | 18 | 19 | def add_attr(*args, **kwargs): 20 | """Add an attribute to the node(s) and return the new plug(s). 21 | 22 | See cmds.addAttr for a list of flags (kwargs). 23 | 24 | Returns: 25 | str | list[str] 26 | """ 27 | 28 | attr_name = kwargs.get("longName") or kwargs.get("ln") 29 | parent = kwargs.get("parent") or kwargs.get("p") 30 | 31 | if not attr_name: 32 | raise ValueError("An attribute name was not specified.") 33 | 34 | cmds.addAttr(*args, **kwargs) 35 | 36 | values = [parent, attr_name] if parent else [attr_name] 37 | 38 | nodes = cmds.ls(args, long=True) 39 | plugs = [maya_fn.plug(node, *values) for node in nodes] 40 | 41 | if len(plugs) == 1: 42 | return plugs[0] 43 | else: 44 | return plugs 45 | 46 | 47 | get = maya_fn.api.get_object 48 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = maya 3 | skipsdist = True 4 | 5 | 6 | [flake8] 7 | max-line-length=119 8 | ignore = W291, W293, W503 9 | 10 | 11 | [pydocstyle] 12 | add_ignore = D202, D413 13 | 14 | 15 | [testenv:maya] 16 | deps = 17 | pytest 18 | pytest-cov 19 | whitelist_externals = 20 | echo 21 | mayapy 22 | setenv = 23 | PYTHONDONTWRITEBYTECODE = 1 24 | PYTHONPATH={envsitepackagesdir};{toxinidir}/src 25 | PATH={envsitepackagesdir};{env:PATH} 26 | commands = 27 | "{env:MAYA_LOCATION}/bin/mayapy.exe" -m pytest \ 28 | --cov=src \ 29 | --cov-report term-missing \ 30 | -p no:warnings \ 31 | -p no:cacheprovider \ 32 | -xv \ 33 | {posargs:./tests/maya} 34 | 35 | 36 | [testenv:black] 37 | whitelist_externals = 38 | black 39 | setenv = 40 | PYTHONDONTWRITEBYTECODE = 1 41 | commands = 42 | black --line-length 88 ./src ./tests 43 | install_commands = 44 | pip3 install black[python27] 45 | 46 | 47 | [testenv:lint] 48 | deps = 49 | flake8 50 | pydocstyle 51 | setenv = 52 | PYTHONDONTWRITEBYTECODE = 1 53 | passenv = PYTHONPATH 54 | commands = 55 | python -m flake8 ./src 56 | python -m pydocstyle ./src 57 | -------------------------------------------------------------------------------- /tests/maya/test_api.py: -------------------------------------------------------------------------------- 1 | """API function set test suite.""" 2 | 3 | import pytest 4 | 5 | from maya.api import OpenMaya 6 | 7 | import maya_fn.api 8 | 9 | 10 | def test_get_dag_path(): 11 | """Given a valid DAG object, the function returns a valid MDagPath.""" 12 | 13 | dag = maya_fn.api.get_dag_path("persp") 14 | 15 | assert isinstance(dag, OpenMaya.MDagPath), "Wrong object type returned" 16 | assert dag.isValid(), "Invalid DAG path returned" 17 | assert OpenMaya.MFnDagNode(dag).name() == "persp", "Wrong object returned" 18 | 19 | 20 | def test_get_dag_path_errors_on_dg_node(): 21 | """Given a valid, DG object, the function raises a TypeError.""" 22 | 23 | with pytest.raises(TypeError): 24 | maya_fn.api.get_dag_path("time1") 25 | 26 | 27 | def test_get_depend_node(): 28 | node = maya_fn.api.get_object("time1") 29 | 30 | assert node is not None 31 | assert not node.isNull() 32 | 33 | 34 | def test_get_depend_node_with_object_that_does_not_exist(): 35 | with pytest.raises(LookupError): 36 | maya_fn.api.get_object("foobar") 37 | 38 | 39 | @pytest.mark.parametrize("value", [None, 123], ids=["Null", "Integer"]) 40 | def test_get_depend_node_with_invalid_type(value): 41 | with pytest.raises(ValueError): 42 | maya_fn.api.get_object(value) 43 | 44 | 45 | def test_get_plug(): 46 | """Given a valid plug, the function returns a valid MPlug.""" 47 | 48 | plug = maya_fn.api.get_plug("persp.message") 49 | 50 | assert isinstance(plug, OpenMaya.MPlug), "Wrong object type returned" 51 | assert not plug.isNull, "Null MPlug returned" 52 | assert plug.name() == "persp.message", "Wrong object returned" 53 | 54 | 55 | def test_get_plug_errors(): 56 | """Given an invalid plug, the function raises an error.""" 57 | 58 | with pytest.raises(LookupError): 59 | maya_fn.api.get_plug("persp.foobar") 60 | 61 | with pytest.raises(TypeError): 62 | maya_fn.api.get_plug("persp") 63 | -------------------------------------------------------------------------------- /src/maya_fn/api.py: -------------------------------------------------------------------------------- 1 | """Maya API functions.""" 2 | 3 | from maya.api import OpenMaya 4 | 5 | 6 | __all__ = [ 7 | "get_dag_path", 8 | "get_object", 9 | "get_plug", 10 | ] 11 | 12 | 13 | def get_dag_path(obj): 14 | """Return the MDagPath of the given object. 15 | 16 | Args: 17 | obj (Any): An object in the current Maya scene. 18 | 19 | Returns: 20 | maya.api.OpenMaya.MDagPath 21 | 22 | Raises: 23 | LookupError: If the given object does not exist. 24 | TypeError: If the given object is not a DAG node. 25 | ValueError: If the given object is not selectable. 26 | """ 27 | 28 | dag = get_object(obj) 29 | 30 | try: 31 | return OpenMaya.MDagPath.getAPathTo(dag) 32 | except RuntimeError: 33 | raise TypeError("Object '{}' is not a DAG node.".format(obj)) 34 | 35 | 36 | def get_object(obj): 37 | """Return the MObject of the given object. 38 | 39 | Args: 40 | obj (Any): An object in the current Maya scene. 41 | 42 | Returns: 43 | maya.api.OpenMaya.MObject 44 | 45 | Raises: 46 | LookupError: If the given object does not exist. 47 | ValueError: If the given object is not selectable. 48 | """ 49 | 50 | return _get_selection(obj).getDependNode(0) 51 | 52 | 53 | def get_plug(obj): 54 | """Return the MPlug of the given plug. 55 | 56 | Args: 57 | obj (Any): A plug in the current Maya scene. 58 | 59 | Returns: 60 | maya.api.OpenMaya.MPlug 61 | 62 | Raises: 63 | LookupError: If the given object does not exist. 64 | TypeError: If the given object is not a plug. 65 | ValueError: If the given object is not selectable. 66 | """ 67 | 68 | try: 69 | return _get_selection(obj).getPlug(0) 70 | except TypeError: 71 | raise TypeError("Object '{}' is not a plug.".format(obj)) 72 | 73 | 74 | def _get_selection(obj): 75 | """Return a selection list for the given object.""" 76 | 77 | sel = OpenMaya.MSelectionList() 78 | 79 | try: 80 | sel.add(obj) 81 | except RuntimeError: 82 | raise LookupError("Object '{}' does not exist.".format(obj)) 83 | except TypeError: 84 | raise ValueError( 85 | "Cannot select a(n) {} object '{}' - " 86 | "expected a string, MObject, MDagPath, or MPlug.".format( 87 | type(obj).__name__, obj 88 | ) 89 | ) 90 | 91 | return sel 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /tests/maya/test_plug.py: -------------------------------------------------------------------------------- 1 | """Plug function set suite for plug functions.""" 2 | 3 | import pytest 4 | 5 | from maya import cmds 6 | from maya.api import OpenMaya 7 | 8 | import maya_fn 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "args,expected", 13 | [ 14 | (("node", "attr"), "node.attr"), 15 | (("node", "attr", "X"), "node.attrX"), 16 | (("node", "attr", 1), "node.attr[1]"), 17 | (("node", "attr", 1, 2), "node.attr[1][2]"), 18 | (("node", "parent", "child", 1, "attr", "X"), "node.parent.child[1].attrX"), 19 | ], 20 | ids=[ 21 | "simple attribute", 22 | "triple attribute", 23 | "array attribute index", 24 | "matrix attribute indexes", 25 | "all of the above", 26 | ], 27 | ) 28 | def test_get_plug(new_scene, args, expected): 29 | assert maya_fn.plug(*args) == expected 30 | 31 | 32 | def test_get_plug_array(): 33 | node = cmds.createNode("network") 34 | root = maya_fn.plug(node, "values") 35 | 36 | cmds.addAttr(node, longName="values", multi=True) 37 | cmds.setAttr(maya_fn.plug(root, 0), 1.0) 38 | cmds.setAttr(maya_fn.plug(root, 1), 1.0) 39 | cmds.setAttr(maya_fn.plug(root, 3), 1.0) 40 | 41 | expected = [0, 1, 3] 42 | actual = list(maya_fn.plug.indices(root)) 43 | 44 | assert actual == expected, "plug_indices returned the wrong values" 45 | 46 | expected = ["network1.values[0]", "network1.values[1]", "network1.values[3]"] 47 | actual = list(maya_fn.plug.elements(root)) 48 | 49 | assert actual == expected, "plug_elements returned the wrong values" 50 | 51 | 52 | def test_get_plug_array_errors(): 53 | node = cmds.createNode("network") 54 | root = maya_fn.plug(node, "values") 55 | 56 | cmds.addAttr(node, longName="values") 57 | 58 | with pytest.raises(TypeError): 59 | list(maya_fn.plug.indices(root)) 60 | 61 | with pytest.raises(TypeError): 62 | list(maya_fn.plug.elements(root)) 63 | 64 | 65 | def test_get_connections(): 66 | node = cmds.createNode("transform") 67 | node = cmds.createNode("transform", parent=node) 68 | node = cmds.createNode("transform", parent=node) 69 | 70 | node = maya_fn.dag.full_path(node) 71 | 72 | src = "|persp.visibility" 73 | dst = maya_fn.plug(node, "visibility") 74 | 75 | expected = None 76 | actual = maya_fn.plug.source(dst) 77 | assert actual == expected, "plug_source returned the wrong values" 78 | 79 | cmds.connectAttr(src, dst) 80 | 81 | expected = src 82 | actual = maya_fn.plug.source(dst) 83 | 84 | assert actual == expected, "plug_source returned the wrong values" 85 | 86 | expected = [dst] 87 | actual = maya_fn.plug.destinations(src) 88 | 89 | assert actual == expected, "plug_destinations returned the wrong values" 90 | 91 | 92 | def test_get_parts_on_dag_node(): 93 | node = cmds.createNode("transform") 94 | node = cmds.createNode("transform", parent=node) 95 | node = maya_fn.dag.full_path(node) 96 | 97 | plug = maya_fn.node.add_attr(node, ln="fizz", at="compound", nc=1) 98 | plug = maya_fn.node.add_attr(node, ln="buzz", p="fizz") 99 | 100 | expected = node 101 | actual = maya_fn.plug.node(plug) 102 | 103 | assert expected == actual 104 | 105 | expected = "fizz.buzz" 106 | actual = maya_fn.plug.attr(plug) 107 | 108 | assert expected == actual 109 | 110 | 111 | def test_get_parts_on_dg_node(): 112 | node = cmds.createNode("network") 113 | 114 | plug = maya_fn.node.add_attr(node, ln="fizz", at="compound", nc=1) 115 | plug = maya_fn.node.add_attr(node, ln="buzz", p="fizz") 116 | 117 | expected = node 118 | actual = maya_fn.plug.node(plug) 119 | 120 | assert expected == actual 121 | 122 | expected = "fizz.buzz" 123 | actual = maya_fn.plug.attr(plug) 124 | 125 | assert expected == actual 126 | -------------------------------------------------------------------------------- /src/maya_fn/dg.py: -------------------------------------------------------------------------------- 1 | """Dependency node utilities.""" 2 | 3 | import six 4 | 5 | from maya import cmds 6 | 7 | import maya_fn.plug 8 | 9 | __all__ = [ 10 | "create", 11 | ] 12 | 13 | 14 | def create(node_type, name=None, **kwargs): 15 | """Create a new node in the graph, with connections/values. 16 | 17 | Args: 18 | node_type (str): Type of node to create. 19 | name (str): Optional name for the new node. 20 | kwargs (dict[str, Any]): Map of attribute name -> value to set/connect 21 | 22 | Returns: 23 | str 24 | """ 25 | 26 | name = name or "{}#".format(node_type) 27 | node = cmds.createNode(node_type, name=name, skipSelect=True) 28 | 29 | for attr, value in kwargs.items(): 30 | if cmds.attributeQuery(attr, node=node, writable=True): 31 | _set_or_connect_attr(node, attr, value) 32 | else: 33 | _connect_attr(node, attr, value) 34 | 35 | return node 36 | 37 | 38 | def _connect_attr(node, attr, value): 39 | """Connect the given output attribute. 40 | 41 | Args: 42 | node (str): Name of a DG node. 43 | attr (str): Attribute to connect. 44 | value (Any): Destination plug(s). 45 | """ 46 | 47 | if not cmds.attributeQuery(attr, node=node, exists=True): 48 | return 49 | 50 | plug = maya_fn.plug(node, attr) 51 | 52 | if isinstance(value, dict): 53 | for k, v in value.items(): 54 | _connect_attr(node, maya_fn.plug(attr, k), v) 55 | elif isinstance(value, (list, tuple)): 56 | (num_attrs,) = cmds.attributeQuery(attr, node=node, numberOfChildren=True) or [ 57 | 0 58 | ] 59 | num_items = len(value) 60 | 61 | if num_items == num_attrs: 62 | children = cmds.attributeQuery(attr, node=node, listChildren=True) 63 | 64 | for child, val in zip(children, value): 65 | _connect_attr(node, child, val) 66 | return 67 | 68 | if cmds.attributeQuery(attr, node=node, multi=True): 69 | for i, v in enumerate(value): 70 | _connect_attr(node, maya_fn.plug(attr, i), v) 71 | return 72 | 73 | for v in value: 74 | _connect_attr(node, attr, v) 75 | elif isinstance(value, six.string_types): 76 | if cmds.objExists(value): 77 | cmds.connectAttr(plug, value) 78 | else: 79 | raise RuntimeError((node, attr, value)) 80 | else: 81 | raise RuntimeError((node, attr, value)) 82 | 83 | 84 | def _set_or_connect_attr(node, attr, value): 85 | """Set or connect the given input attribute. 86 | 87 | Args: 88 | node (str): Name of a DG node. 89 | attr (str): Attribute to set or connect. 90 | value (Any): Source plug(s) or value(s) 91 | """ 92 | 93 | if not cmds.attributeQuery(attr, node=node, exists=True): 94 | return 95 | 96 | plug = maya_fn.plug(node, attr) 97 | 98 | if isinstance(value, dict): 99 | for k, v in value.items(): 100 | _set_or_connect_attr(node, maya_fn.plug(attr, k), v) 101 | elif isinstance(value, (list, tuple)): 102 | if cmds.getAttr(plug, type=True) == "matrix" and len(value) == 16: 103 | cmds.setAttr(plug, value, type="matrix") 104 | return 105 | 106 | num_items = len(value) 107 | (num_attrs,) = cmds.attributeQuery(attr, node=node, numberOfChildren=True) or [ 108 | 0 109 | ] 110 | 111 | if num_items == num_attrs: 112 | children = cmds.attributeQuery(attr, node=node, listChildren=True) 113 | 114 | for child, val in zip(children, value): 115 | _set_or_connect_attr(node, child, val) 116 | return 117 | 118 | if cmds.attributeQuery(attr, node=node, multi=True): 119 | for i, v in enumerate(value): 120 | _set_or_connect_attr(node, maya_fn.plug(attr, i), v) 121 | return 122 | 123 | raise RuntimeError((node, attr, value)) 124 | elif isinstance(value, six.string_types): 125 | if cmds.objExists(value): 126 | cmds.connectAttr(value, plug) 127 | elif cmds.getAttr(plug, type=True) == "string": 128 | cmds.setAttr(plug, value, type="string") 129 | else: 130 | raise RuntimeError((node, attr, value)) 131 | else: 132 | cmds.setAttr(plug, value) 133 | -------------------------------------------------------------------------------- /src/maya_fn/dag.py: -------------------------------------------------------------------------------- 1 | """DAG node utilities.""" 2 | 3 | from maya import cmds 4 | from maya.api import OpenMaya 5 | 6 | import maya_fn.api 7 | 8 | __all__ = [ 9 | "ancestors", 10 | "children", 11 | "descendents", 12 | "full_path", 13 | "name", 14 | "parent", 15 | "path", 16 | "shapes", 17 | "siblings", 18 | ] 19 | 20 | 21 | def ancestors(dag_node): 22 | """Return the ancestors of the given dag node, depth first. 23 | 24 | Args: 25 | dag_node (str): DAG node in the current scene. 26 | 27 | Returns: 28 | list[str] 29 | """ 30 | 31 | return list(_iter_parents(dag_node))[::-1] 32 | 33 | 34 | def child(dag_node, dag_name): 35 | """Return the child of the given dag node.""" 36 | 37 | return dag_node + "|" + dag_name 38 | 39 | 40 | def children(dag_node): 41 | """Return the children transforms of the given node. 42 | 43 | Args: 44 | dag_node (str): DAG node in the current scene. 45 | 46 | Returns: 47 | list[str] 48 | """ 49 | 50 | return [ 51 | child.fullPathName() 52 | for child in _iter_children(dag_node) 53 | if child.node().hasFn(OpenMaya.MFn.kTransform) 54 | ] 55 | 56 | 57 | def descendents(dag_node): 58 | """Yield the descendents of the given dag node, depth first. 59 | 60 | Args: 61 | dag_node (str): DAG node in the current scene. 62 | 63 | Yields: 64 | str 65 | """ 66 | 67 | for child in children(dag_node): 68 | yield child 69 | 70 | for each in descendents(child): 71 | yield each 72 | 73 | 74 | def full_path(dag_node): 75 | """Return the full path of the given dag node. 76 | 77 | Args: 78 | dag_node (str): DAG node in the current scene. 79 | 80 | Returns: 81 | str 82 | """ 83 | 84 | return maya_fn.api.get_dag_path(dag_node).fullPathName() 85 | 86 | 87 | get = maya_fn.api.get_dag_path 88 | 89 | 90 | def name(dag_node): 91 | """Return the name of the given dag node. 92 | 93 | Args: 94 | dag_node (str): DAG node in the current scene. 95 | 96 | Returns: 97 | str 98 | """ 99 | 100 | # Split the full path name because partial path name may not be unique. 101 | return maya_fn.api.get_dag_path(dag_node).fullPathName().split("|")[-1] 102 | 103 | 104 | def parent(dag_node): 105 | """Return the parent of the given dag node. 106 | 107 | Args: 108 | dag_node (str): DAG path of a node in the current scene. 109 | 110 | Returns: 111 | str | None 112 | """ 113 | 114 | dag_path = maya_fn.api.get_dag_path(dag_node) 115 | dag_path.pop() 116 | 117 | return dag_path.fullPathName() or None 118 | 119 | 120 | def partial_path(dag_node): 121 | """Return the partial path of the given dag node. 122 | 123 | Args: 124 | dag_node (str): DAG node in the current scene. 125 | 126 | Returns: 127 | list[str] 128 | """ 129 | 130 | return maya_fn.api.get_dag_path(dag_node).partialPathName() 131 | 132 | 133 | path = maya_fn.api.get_dag_path 134 | 135 | 136 | def shapes(dag_node): 137 | """Return the shape nodes for the given node. 138 | 139 | Args: 140 | dag_node (str): DAG path of a transform in the current scene. 141 | 142 | Returns: 143 | list[str] 144 | """ 145 | 146 | return [ 147 | child.fullPathName() 148 | for child in _iter_children(dag_node) 149 | if child.node().hasFn(OpenMaya.MFn.kShape) 150 | ] 151 | 152 | 153 | def siblings(dag_node): 154 | """Return the siblings of the given dag node. 155 | 156 | Args: 157 | dag_node (str): DAG node in the current scene. 158 | 159 | Returns: 160 | list[str] 161 | """ 162 | 163 | dag_path = path(dag_node) 164 | 165 | _parent = parent(dag_node) 166 | 167 | if not _parent: 168 | _siblings = cmds.ls(assemblies=True, long=True) 169 | else: 170 | _siblings = [each.fullPathName() for each in _iter_children(_parent)] 171 | 172 | _siblings.remove(dag_path.fullPathName()) 173 | 174 | return _siblings 175 | 176 | 177 | def _iter_children(dag_node): 178 | """Yield the children of the given node.""" 179 | 180 | dag_path = maya_fn.api.get_dag_path(dag_node) 181 | 182 | for i in range(dag_path.childCount()): 183 | yield OpenMaya.MDagPath.getAPathTo(dag_path.child(i)) 184 | 185 | 186 | def _iter_parents(dag_node): 187 | """Yield the children of the given node.""" 188 | 189 | dag_path = maya_fn.api.get_dag_path(dag_node) 190 | 191 | while dag_path.length(): 192 | dag_path = dag_path.pop() 193 | 194 | if dag_path.length(): 195 | yield dag_path.fullPathName() 196 | else: 197 | break 198 | -------------------------------------------------------------------------------- /src/maya_fn/plug.py: -------------------------------------------------------------------------------- 1 | """Maya Plug functions.""" 2 | 3 | import inspect 4 | import six 5 | 6 | from maya import cmds 7 | from maya.api import OpenMaya 8 | 9 | import maya_fn.api 10 | 11 | __all__ = [ 12 | "plug", 13 | ] 14 | 15 | 16 | def attr(plug): 17 | """Return the attribute of the given plug. 18 | 19 | Args: 20 | plug (str): Path to an plug. 21 | 22 | Returns: 23 | str 24 | """ 25 | 26 | plug = maya_fn.api.get_plug(plug) 27 | 28 | return plug.partialName( 29 | includeNonMandatoryIndices=True, 30 | includeInstancedIndices=True, 31 | useFullAttributePath=True, 32 | useLongNames=True, 33 | ) 34 | 35 | 36 | def destinations(plug): 37 | """Return the outputs of the given plug. 38 | 39 | Args: 40 | plug (str): Path to an plug. 41 | 42 | Returns: 43 | list[str] 44 | """ 45 | 46 | plug = maya_fn.api.get_plug(plug) 47 | 48 | plugs = plug.connectedTo(False, True) 49 | 50 | return cmds.ls([p.name() for p in plugs], long=True) 51 | 52 | 53 | def downstream(plug): 54 | """Return the nodes downstream the given plug. 55 | 56 | Args: 57 | plug (str): Path to an plug. 58 | 59 | Returns: 60 | list[str] 61 | """ 62 | 63 | return [node(each) for each in destinations(plug)] 64 | 65 | 66 | def elements(plug): 67 | """Yield the elements of the given array plug. 68 | 69 | Args: 70 | plug (str): Path to an array plug. 71 | 72 | Yields: 73 | str 74 | 75 | Raises: 76 | TypeError: If the given plug is not an array. 77 | """ 78 | 79 | plug = _get_array_plug(plug) 80 | 81 | for i in plug.getExistingArrayAttributeIndices(): 82 | yield plug.elementByLogicalIndex(i).name() 83 | 84 | 85 | get = maya_fn.api.get_plug 86 | 87 | 88 | def indices(plug): 89 | """Yield the indices of the given array plug. 90 | 91 | Args: 92 | plug (str): Path to an array plug. 93 | 94 | Yields: 95 | int 96 | 97 | Raises: 98 | TypeError: If the given plug is not an array. 99 | """ 100 | 101 | plug = _get_array_plug(plug) 102 | 103 | for i in plug.getExistingArrayAttributeIndices(): 104 | yield i 105 | 106 | 107 | def make(*args): 108 | """Return the plug built up from the given arguments. 109 | 110 | Args: 111 | *args (str | int): Token(s) to build the plug name from. 112 | 113 | Returns: 114 | str 115 | """ 116 | 117 | parts = [] 118 | 119 | for arg in args: 120 | if isinstance(arg, int): 121 | parts[-1] = "{}[{}]".format(parts[-1], arg) 122 | elif isinstance(arg, six.string_types) and len(arg) == 1: 123 | parts[-1] = "{}{}".format(parts[-1], arg) 124 | else: 125 | parts.append(arg) 126 | 127 | return ".".join(parts) 128 | 129 | 130 | def node(plug): 131 | """Return the node of the given plug. 132 | 133 | Args: 134 | plug (str): Path to an plug. 135 | 136 | Returns: 137 | str 138 | """ 139 | 140 | plug = maya_fn.api.get_plug(plug) 141 | obj = plug.node() 142 | 143 | if obj.hasFn(OpenMaya.MFn.kDagNode): 144 | return OpenMaya.MFnDagNode(obj).fullPathName() 145 | else: 146 | return OpenMaya.MFnDependencyNode(obj).name() 147 | 148 | 149 | def source(plug): 150 | """Return the source of the given plug. 151 | 152 | Args: 153 | plug (str): Path to an plug. 154 | 155 | Returns: 156 | str | None 157 | """ 158 | 159 | plug = maya_fn.api.get_plug(plug) 160 | 161 | plugs = plug.connectedTo(True, False) 162 | plugs = [make(node(p), attr(p)) for p in plugs] 163 | 164 | if plugs: 165 | return plugs[0] 166 | else: 167 | return None 168 | 169 | 170 | def split(plug): 171 | """Return the node and attribute of the given plug.""" 172 | 173 | return node(plug), attr(plug) 174 | 175 | 176 | def upstream(plug): 177 | """Return the node upstream the given plug. 178 | 179 | Args: 180 | plug (str): Path to an plug. 181 | 182 | Returns: 183 | list[str] 184 | """ 185 | 186 | p = source(plug) 187 | 188 | return p if p is None else source(p) 189 | 190 | 191 | def _get_array_plug(plug): 192 | """Return the given array plug.""" 193 | 194 | plug = maya_fn.api.get_plug(plug) 195 | 196 | if not plug.isArray: 197 | raise TypeError("'{}' is not an array plug.".format(plug.name())) 198 | 199 | return plug 200 | 201 | 202 | __functions__ = dict( 203 | __call__=staticmethod(make), 204 | **{ 205 | obj.__name__: staticmethod(obj) 206 | for obj in locals().values() 207 | if inspect.isfunction(obj) 208 | } 209 | ) 210 | 211 | plug = type("plug", (), __functions__)() 212 | -------------------------------------------------------------------------------- /tests/maya/test_dag.py: -------------------------------------------------------------------------------- 1 | """DAG function set test suite.""" 2 | 3 | import pytest 4 | 5 | from maya import cmds 6 | 7 | import maya_fn 8 | 9 | 10 | def test_get_ancestores(): 11 | """Given a valid DAG object, the function returns all parent transforms.""" 12 | 13 | x = cmds.createNode("transform", name="x") 14 | a = cmds.createNode("transform", name="a", parent=x) 15 | b = cmds.createNode("transform", name="b", parent=a) 16 | c = cmds.createNode("transform", name="c", parent=b) 17 | 18 | expected = sorted(cmds.ls([x, a, b], long=True), key=lambda n: n.count("|")) 19 | actual = maya_fn.dag.ancestors(c) 20 | 21 | assert actual == expected 22 | 23 | assert maya_fn.dag.ancestors(x) == [] 24 | 25 | 26 | def test_get_children(): 27 | """Given a valid DAG object, the function returns the full path of its children.""" 28 | 29 | root = cmds.createNode("transform") 30 | 31 | cmds.createNode("locator", parent=root) 32 | cmds.createNode("transform", parent=root) 33 | cmds.createNode("transform", parent=root) 34 | 35 | expected = cmds.listRelatives(root, children=True, type="transform", fullPath=True) 36 | actual = maya_fn.dag.children(root) 37 | 38 | assert set(actual) == set(expected), "get_children returned the wrong results" 39 | 40 | 41 | def test_get_descendents(): 42 | """Given a valid DAG object, the function returns the full path of its descendents.""" 43 | 44 | cmds.file(new=True, force=True) 45 | 46 | x = cmds.createNode("transform", name="x") 47 | a = cmds.createNode("transform", name="a", parent=x) 48 | b = cmds.createNode("transform", name="b", parent=a) 49 | c = cmds.createNode("transform", name="c", parent=b) 50 | 51 | expected = sorted(cmds.ls([a, b, c], long=True), key=lambda n: n.count("|")) 52 | actual = list(maya_fn.dag.descendents(x)) 53 | 54 | assert actual == expected, "descendents returned the wrong results" 55 | 56 | 57 | def test_get_full_path(): 58 | """Given a valid DAG object, the function returns the its full name.""" 59 | 60 | root = cmds.createNode("transform") 61 | child = cmds.createNode("transform", parent=root) 62 | child = cmds.createNode("transform", parent=child) 63 | child = cmds.createNode("transform", parent=child) 64 | 65 | expected = cmds.ls(child, long=True)[0] 66 | actual = maya_fn.dag.full_path(child) 67 | 68 | assert actual == expected, "get_full_path returned the wrong results" 69 | 70 | 71 | def test_get_name(): 72 | """Given a valid DAG object, the function returns the its short name.""" 73 | 74 | root = cmds.createNode("transform") 75 | child = cmds.createNode("transform", parent=root) 76 | child = cmds.createNode("transform", parent=child) 77 | child = cmds.createNode("transform", name="foobar", parent=child) 78 | (child,) = cmds.ls(child, long=True) 79 | 80 | cmds.duplicate(root) 81 | 82 | expected = "foobar" 83 | actual = maya_fn.dag.name(child) 84 | 85 | assert actual == expected, "get_name returned the wrong results" 86 | 87 | 88 | def test_get_parent(): 89 | """Given a valid DAG object, the function returns the full path of its shapes.""" 90 | 91 | root = cmds.createNode("transform") 92 | child = cmds.createNode("transform", parent=root) 93 | shape = cmds.createNode("locator", parent=child) 94 | 95 | assert maya_fn.dag.parent(root) is None 96 | assert maya_fn.dag.parent(child) == maya_fn.dag.path(root).fullPathName() 97 | assert maya_fn.dag.parent(shape) == maya_fn.dag.path(child).fullPathName() 98 | 99 | 100 | def test_get_shapes(): 101 | """Given a valid, DG object, the function raises a TypeError.""" 102 | 103 | root = cmds.createNode("transform") 104 | 105 | cmds.createNode("locator", parent=root) 106 | cmds.createNode("transform", parent=root) 107 | cmds.createNode("transform", parent=root) 108 | 109 | expected = cmds.listRelatives(root, children=True, type="shape", fullPath=True) 110 | actual = maya_fn.dag.shapes(root) 111 | 112 | assert set(actual) == set(expected), "get_children returned the wrong results" 113 | 114 | 115 | def test_get_siblings(): 116 | """Given a valid DAG object, the function returns all sibling DAG nodes.""" 117 | 118 | cmds.file(new=True, force=True) 119 | 120 | root = cmds.createNode("transform", name="root") 121 | x = cmds.createNode("transform", name="x", parent=root) 122 | a = cmds.createNode("transform", name="a", parent=x) 123 | b = cmds.createNode("transform", name="b", parent=x) 124 | c = cmds.createNode("transform", name="c", parent=x) 125 | y = cmds.createNode("transform", name="y", parent=root) 126 | z = cmds.createNode("transform", name="z", parent=y) 127 | 128 | expected = set(cmds.ls([b, c], long=True)) 129 | actual = set(maya_fn.dag.siblings(a)) 130 | 131 | assert actual == expected 132 | assert maya_fn.dag.siblings(z) == [] 133 | 134 | assert set(maya_fn.dag.siblings(root)) == {"|persp", "|top", "|front", "|side"} 135 | --------------------------------------------------------------------------------