pythonmayapymel

Is there a way to indent Python without affecting the code


I'm trying to write a user interface in Maya, and it's getting incredibly confusing with multiple levels of parents and no indents. The basic code (without any functionality) is currently around 400 lines and it's taking a while to find the bits I need.

For example take this following code without comments:

#Earlier user interface

py.rowColumnLayout( numberOfColumns = 5 )
py.text( label="", width = 1 )
py.text( label="Column 1", enable = False, width = 250 )
py.text( label="", width = 1 )
py.text( label="Column 2" enable = False, width = 250 )
py.text( label="", width = 1 )

py.text( label="" )
py.rowColumnLayout( numberOfColumns = 4 )
py.text( label="   Input data:", align="left" )
py.text( label="" )
py.text( label="" )
py.text( label="" )
py.textField( text = "Text here" )
py.text( label="" )
py.text( label="" )
py.text( label="" )
py.setParent( ".." )

py.text( label="" )
py.rowColumnLayout( numberOfColumns = 4 )
py.rowColumnLayout( numberOfColumns = 5 )
py.radioButton( label = "Read file from path", width = 100 )
py.text( label="" )
py.button( label = "Browse" )
py.text( label="" )
py.button( label = "Validate" )
py.setParent( ".." )
py.text( label="" )
py.text( label="" )
py.text( label="" )
py.setParent( ".." )
py.setParent( ".." )

However, this is how it'd look with indents

py.rowColumnLayout( numberOfColumns = 5 )
    py.text( label="", width = 1 )
    py.text( label="Column 1", enable = False, width = 250 )
    py.text( label="", width = 1 )
    py.text( label="Column 2" enable = False, width = 250 )
    py.text( label="", width = 1 )

    py.text( label="" )
    py.rowColumnLayout( numberOfColumns = 4 )
        py.text( label="   Input data:", align="left" )
        py.text( label="" )
        py.text( label="" )
        py.text( label="" )
        py.textField( text = "Text here" )
        py.text( label="" )
        py.text( label="" )
        py.text( label="" )
        py.setParent( ".." )

    py.text( label="" )
    py.rowColumnLayout( numberOfColumns = 4 )
        py.rowColumnLayout( numberOfColumns = 5 )
            py.radioButton( label = "Read file from path", width = 100 )
            py.text( label="" )
            py.button( label = "Browse" )
            py.text( label="" )
            py.button( label = "Validate" )
            py.setParent( ".." )
        py.text( label="" )
        py.text( label="" )
        py.text( label="" )
        py.setParent( ".." )
    py.setParent( ".." )

Is there any way at all I'd be able to write it with indents but make it ignore them all on execution? I saw the question asking if you can write python without indents, but I'm kind of needing the opposite.

Note: The output values of some of the py.* functions will also need to be assigned to variables, just haven't been yet as the layout needs to get sorted first.


Solution

  • This is an excellent use case that technical artists like us face everyday while building UI in Maya.

    For PyMEL based UI:

    This comes built into PyMEL. You don't have to create a context manager. The layout commands themselves are context managers. You only have to add a with keyword before every layout command call like so:

    # Do this when using PyMEL for your UI code
    import pymel.core as pm
    
    # ...
    
    with pm.rowColumnLayout( numberOfColumns = 5 ):
        pm.text( label="", width = 1 )
        pm.text( label="Column 1", enable = False, width = 250 )
        pm.text( label="", width = 1 )
        pm.text( label="Column 2", enable = False, width = 250 )
        pm.text( label="", width = 1 )
    
        pm.text( label="" )
    
        with pm.rowColumnLayout( numberOfColumns = 4 ):
            pm.text( label="   Input data:", align="left" )
            pm.text( label="" )
            pm.text( label="" )
            pm.text( label="" )
            pm.textField( text = "Text here" )
            pm.text( label="" )
            pm.text( label="" )
            pm.text( label="" )        
    
        pm.text( label="" )
        with pm.rowColumnLayout( numberOfColumns = 4 ):
            with pm.rowColumnLayout( numberOfColumns = 5 ):
                pm.radioButton( label = "Read file from path", width = 100 )
                pm.text( label="" )
                pm.button( label = "Browse" )
                pm.text( label="" )
                pm.button( label = "Validate" )
    
            pm.text( label="" )
            pm.text( label="" )
            pm.text( label="" )
    

    For maya.cmds based UI:

    One quick solution would be to make a dummy context manager. You could do something like this

    # Do this when using Maya's cmds for your UI code
    import maya.cmds as cmds
    
    # ...
    
    from contextlib import contextmanager
    @contextmanager
    def neat_indent():
        # OPTIONAL: This is also an opportunity to do something before the block of code runs!
        try:
            # During this is where your indented block will execute
            # Leave it empty
            yield
        finally:
            # OPTIONAL: This is where you can write code that executes AFTER your indented block executes.
            pass
    

    This way your code doesn't have to change too much. Just add your context manager function with the with keyword in the beginning of every intended indent!

    cmds.rowColumnLayout( numberOfColumns = 5 )
    with neat_indent():
        cmds.text( label="", width = 1 )
        cmds.text( label="Column 1", enable = False, width = 250 )
        cmds.text( label="", width = 1 )
        cmds.text( label="Column 2", enable = False, width = 250 )
        cmds.text( label="", width = 1 )
    
        cmds.text( label="" )
    
        cmds.rowColumnLayout( numberOfColumns = 4 )
        with neat_indent():
            cmds.text( label="   Input data:", align="left" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.textField( text = "Text here" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.setParent( ".." )
    
        cmds.text( label="" )
        cmds.rowColumnLayout( numberOfColumns = 4 )
        with neat_indent():
            cmds.rowColumnLayout( numberOfColumns = 5 )
            with neat_indent():
                cmds.radioButton( label = "Read file from path", width = 100 )
                cmds.text( label="" )
                cmds.button( label = "Browse" )
                cmds.text( label="" )
                cmds.button( label = "Validate" )
                cmds.setParent( ".." )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.setParent( ".." )
        cmds.setParent( ".." )
    

    The context manager we created, neat_indent(), also gives you the opportunity to write code that wraps your indent blocks. A practical example here, is that in the end of every indent you find yourself writing py.setParent(".."). You can just throw this into the finally section of the context manager:

    from contextlib import contextmanager
    @contextmanager
    def neat_indent(parent=None):
        # OPTIONAL: This is also an opportunity to do something before the block of code runs!
        try:
            # During this is where your indented block will execute
            # Leave it empty
            yield
        finally:
            # OPTIONAL: This is where you can write code that executes AFTER your indented block executes.
            if parent:
                cmds.setParent(parent)
    

    Your code will make more sense now:

    cmds.rowColumnLayout( numberOfColumns = 5 )
    with neat_indent(".."):
        cmds.text( label="", width = 1 )
        cmds.text( label="Column 1", enable = False, width = 250 )
        cmds.text( label="", width = 1 )
        cmds.text( label="Column 2", enable = False, width = 250 )
        cmds.text( label="", width = 1 )
    
        cmds.text( label="" )
    
        cmds.rowColumnLayout( numberOfColumns = 4 )
        with neat_indent(".."):
            cmds.text( label="   Input data:", align="left" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.textField( text = "Text here" )
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.text( label="" )        
    
        cmds.text( label="" )
        cmds.rowColumnLayout( numberOfColumns = 4 )
        with neat_indent(".."):
            cmds.rowColumnLayout( numberOfColumns = 5 )
            with neat_indent(".."):
                cmds.radioButton( label = "Read file from path", width = 100 )
                cmds.text( label="" )
                cmds.button( label = "Browse" )
                cmds.text( label="" )
                cmds.button( label = "Validate" )
    
            cmds.text( label="" )
            cmds.text( label="" )
            cmds.text( label="" )
    

    Context managers are powerful. In this post I have used the contextmanager decorator from contextlib standard library module. You can read about this technique here. About with in general here.

    Also, for this very purpose (one of the purposes) of making UI development in Maya cleaner and more Pythonic @theodox created the mGui module. Check it out.