Last active
July 6, 2024 22:55
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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