Skip to content

Instantly share code, notes, and snippets.

@theodox
Last active July 6, 2024 22:55
Show Gist options
  • Save theodox/c94d3d39397d3325b21f5ab2708f3d93 to your computer and use it in GitHub Desktop.
Save theodox/c94d3d39397d3325b21f5ab2708f3d93 to your computer and use it in GitHub Desktop.
Support functions for patching a mel command at runtime, presumably to extend the base functionality with some extra python
import maya.mel
import re
import logging
_logger = logging.getLogger("patchmel")
_logger.setLevel(logging.ERROR)
def log_patches(do_logging : bool):
"""
If `do_logging` is true, print out the text of the patched MEL procedures
during patching.
"""
if (do_logging):
_logger.setLevel (logging.DEBUG)
else:
_logger.setLevel(logging.WARN)
def get_proc_body(procName : str) -> str:
"""
Finds and returns the original text of a global MEL proc.
"""
sourcefile = maya.mel.eval(f"whatIs {procName}")
if not "found in" in sourcefile:
raise RuntimeError (f"{procName} is not an original Mel procedure")
original_mel_file = sourcefile.split("found in: ")[-1].strip()
with open(original_mel_file, "rt") as file_handle:
mel_file_text = file_handle.read()
# find the proc header. We're replacing the global procs which are accessible
# across Maya, not the local ones which Maya executes only in the local context
# (it might be possible to replace _just_ a local proc... but it might unreliable)
proc_header = re.search("global\W+proc\W+" + procName + ".*\n", mel_file_text)
counter = proc_header.end()
# scan the procedure definition to find the closing curly bracket... which may
# or may not be the first one we seee
depth = 0
found_first_paren = False
for char in mel_file_text[proc_header.end():]:
if char == "{":
depth += 1
found_first_paren = True
depth -= char == "}"
counter += 1
# sometimes there are comments before the function starts
if depth == 0 and found_first_paren:
break
return mel_file_text[proc_header.start(): counter]
def patch_proc(procedure : str, before_cb : str = None, after_cb : str = None, replace_proc: str = None):
"""
This function will patch an existing MEL command, replacing it with one that
shares the same signature. For the duration of this Maya session the patched
command will be overidden with a new one. The expected use case is to replace or
extend Maya items like menus without the need to edit the actual MEL files
in your Maya install. The trick works because MEL procs are available in a
single global memory space and can be overwritten at runtime.
There are a couple of different ways you can use the patched procedure.
1. If you provide the 'replace_proc' argument, the original Maya procedure
is replaced in its entirety. Use with caution!
2. If you provide either or both of the 'before_cb' and 'after_cb' arguments,
the code you provide will be executed just before or just after the original.
All of the "proc" arguments are strings which represent MEL with the same signature
as the original procedure; the same arguments will passed to the callbacks and/or the
replacement procedure although you are free to ignore them as needed. Most of the
time the callbacks or replacement functions will actually be implemented
in Python; generically they will look like this in Mel.
global proc mySpecialCallback(string $arg)
{
python ("import mymodule; mymodule.callback_function(\"" + $arg + "\")");
}
Note the string escaping, since what gets passed to the conmmand "python" is a MEL string.
"""
# make sure the replacement MEL functions have the expected names
before_cb_name = procedure + "_before"
after_cb_name = procedure + "_after"
if before_cb and before_cb_name not in before_cb:
raise ValueError(f"callback name should be: {before_cb_name}")
if after_cb and after_cb_name not in after_cb:
raise ValueError(f"callback name should be: {after_cb_name}")
# ... and if the replacement proc is provided, it matches the original name
if replace_proc and f"global proc {procedure}" not in replace_proc:
raise ValueError(f"callback name should be: {procedure}")
# Reset the orignal to it's Maya default, then redefine it with the suffix "_patched"
maya.mel.eval(f"source {procedure}")
original_proc = get_proc_body(procedure)
replacement_name = procedure + "_patched"
maya.mel.eval(original_proc.replace("proc " + procedure, "proc " + replacement_name))
if not "entered interactively" in maya.mel.eval("whatIs " + replacement_name):
raise RuntimeError (f"failed to redefine {procedure}")
# we need to know the signature of the original so we can forward the arguments
signature = re.search("(.*)\(.*\)", original_proc).group(0) # -> "global proc (string $a, string $b)"
argument_passthrough = re.search("\(.*\)", signature).group(0) # -> (string $a, string $b)
argument_passthrough = re.sub("(string \$)|(int \$)|(float \$)", "$", argument_passthrough)
argument_passthrough = re.sub("\[\]", "", argument_passthrough) # -> ($a, $b)
# if we we have no replacement func, we will make a new one which calls first the
# before and after callbacks as appropriate... If user did not give us
# callbacks we will make dummies below, so here just include them
# Note that if replace proc is defined, it's up to the user to forward the arguments
if not replace_proc:
replace_proc = signature
replace_proc += "{\n"
replace_proc += '\t' + before_cb_name + argument_passthrough + ";\n"
replace_proc += '\t' + replacement_name + argument_passthrough + ";\n"
replace_proc += '\t' + after_cb_name + argument_passthrough + ";\n"
replace_proc += "}\n"
# generate a local null-op if the user did not supply one or other of the callbacks
local_signature = signature.replace("global proc", "proc")
pre_callback = before_cb or local_signature.replace(procedure, before_cb_name) + "{}\n"
post_callback = after_cb or local_signature.replace(procedure, after_cb_name) + "{}\n"
_logger.debug (f"patch '{procedure}'")
for cb in (pre_callback, post_callback, replace_proc):
for line in cb.splitlines():
_logger.debug(line)
_logger.debug (f"to revert: `source {procedure}`")
# now evaluate the callbacks
maya.mel.eval(pre_callback)
maya.mel.eval(post_callback)
maya.mel.eval(replace_proc)
_logger.warn(f"patched {procedure}")
def revert_mel_proc(procName: str) -> str:
"""
Sources the original MEL procedure, restoring the factory state.
"""
maya.mel.eval(f"source {procName}")
_logger.warn(f"restored {procName}")
return maya.mel.eval(f"whatIs {procName}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment