Files
MetaFusion/scripts/ReloadModules.py
2025-05-02 00:14:28 +08:00

742 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tool - Module Reload Tool
Provides module reloading functionality for the plugin, supporting hot updates
用法说明:
1. 在Maya中直接运行:
import scripts.ReloadModules
scripts.ReloadModules.main()
2. 从命令行重新加载所有模块:
import scripts.ReloadModules
scripts.ReloadModules.reload_all()
3. 重新加载特定模块:
import scripts.ReloadModules
scripts.ReloadModules.ModuleReloader.reload_module('module_name')
4. 在开发过程中使用:
- 当修改了代码后,运行此模块可以热更新所有更改
- 无需重启Maya即可测试新功能
- 支持UI和命令行两种方式
注意事项:
- 某些深度依赖的模块可能需要手动重启Maya
- 如果遇到导入错误,请检查模块路径是否正确
- 重新加载可能不会影响已经创建的对象实例
"""
import sys
import importlib
import os
import traceback
import shutil
from maya import cmds
# 设置工具路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
# 设置脚本路径
SCRIPTS_PATH = os.path.dirname(os.path.abspath(__file__))
if SCRIPTS_PATH not in sys.path:
sys.path.insert(0, SCRIPTS_PATH)
# 导入主配置 - 多级尝试导入策略
def import_main_config():
"""尝试多种方式导入主配置文件"""
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH, Config
# 尝试直接导入
try:
import config
importlib.reload(config) # 确保获取最新配置
# 从主配置文件导入路径和设置
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
print(f"成功从主配置文件导入配置")
return True
except ImportError as e:
print(f"直接导入主配置文件失败: {str(e)}")
# 尝试从上级目录导入
parent_dir = os.path.dirname(TOOL_PATH)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
try:
import config
importlib.reload(config) # 确保获取最新配置
# 从主配置文件导入路径和设置
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
print(f"成功从上级目录导入主配置文件")
return True
except ImportError as e:
print(f"从上级目录导入主配置文件失败: {str(e)}")
if parent_dir in sys.path:
sys.path.remove(parent_dir)
return False
# 尝试导入主配置
if not import_main_config():
print("警告: 无法导入主配置文件,使用默认配置")
# 使用默认路径
TOOL_NAME = "Delos"
TOOL_VERSION = "Alpha v1.0.0"
TOOL_AUTHOR = "Virtuos Games"
TOOL_PATH = TOOL_PATH
UI_PATH = os.path.join(SCRIPTS_PATH, 'ui')
STYLE_FILE = os.path.join(SCRIPTS_PATH, 'ui', 'style.qss')
ICONS_PATH = os.path.join(TOOL_PATH, 'icons')
ASSETS_PATH = os.path.join(TOOL_PATH, 'assets')
DNA_FILE_PATH = os.path.join(TOOL_PATH, 'assets', 'dna')
# 尝试导入Config模块
try:
from scripts.utils import Config
except ImportError:
try:
from utils import Config
except ImportError:
try:
import Config
except ImportError:
print("警告: 无法导入Config模块")
Config = None
# 创建默认Config对象
class DefaultConfig:
# 使用TOOL_PATH而不是ROOT_DIR
DNA_CONFIG = {
'DNA_PATH': os.path.join(TOOL_PATH, 'assets', 'dna'),
'DNA_FILE_PATH': os.path.join(TOOL_PATH, 'assets', 'dna', 'default.dna'),
'DNA_VERSION': 'DNAv4',
'LOD_LEVELS': [0, 1, 2, 3, 4],
'DEFAULT_MESH_INDICES': [0],
'GUI_PATH': os.path.join(TOOL_PATH, 'ui'),
'ASSEMBLE_SCRIPT': os.path.join(TOOL_PATH, 'scripts', 'assemble.py')
}
DNA_LIB_PATHS = [
os.path.join(TOOL_PATH, 'lib'),
os.path.join(TOOL_PATH, 'lib', 'windows' if sys.platform == 'win32' else 'linux')
]
Config = DefaultConfig()
# 尝试导入 Qt 模块
# 首先尝试使用项目中的 Qt.py 兼容层
try:
# 添加父目录到路径中以确保可以导入 Qt.py
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if parent_dir not in sys.path:
sys.path.append(parent_dir)
# 尝试导入 Qt.py
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
# 获取 Maya 主窗口
def getMayaMainWindow():
from maya import OpenMayaUI as omui
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using Qt.py compatibility layer")
except ImportError:
# 如果无法导入 Qt.py尝试直接导入 PySide2/PySide6
try:
from PySide2 import QtWidgets, QtCore, QtGui
from shiboken2 import wrapInstance
from maya import OpenMayaUI as omui
def getMayaMainWindow():
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using PySide2")
except ImportError:
try:
from PySide6 import QtWidgets, QtCore, QtGui
from shiboken6 import wrapInstance
from maya import OpenMayaUI as omui
def getMayaMainWindow():
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using PySide6")
except ImportError:
# 如果所有导入尝试都失败,设置标志并显示警告
HAS_QT = False
cmds.warning("Failed to import Qt modules. UI functionality will be limited to command line only.")
def clean_pycache(root_dir):
"""Delete all __pycache__ directories and .pyc files under the given root directory
Args:
root_dir (str): Root directory to search for __pycache__ folders and .pyc files
Returns:
int: Number of __pycache__ directories and .pyc files removed
"""
count = 0
try:
# 首先确保目录存在
if not os.path.exists(root_dir) or not os.path.isdir(root_dir):
print(f"Warning: Directory does not exist or is not a directory: {root_dir}")
return count
print(f"Cleaning __pycache__ in directory: {root_dir}")
# 收集所有需要删除的路径,避免在遍历时修改目录结构
pycache_dirs = []
pyc_files = []
# 首先收集所有 __pycache__ 目录和 .pyc 文件
for root, dirs, files in os.walk(root_dir):
# 收集 __pycache__ 目录
if "__pycache__" in dirs:
pycache_path = os.path.join(root, "__pycache__")
pycache_dirs.append(pycache_path)
# 收集 .pyc 文件
for file in files:
if file.endswith(".pyc"):
file_path = os.path.join(root, file)
pyc_files.append(file_path)
# 优先使用批处理命令删除 __pycache__ 目录(更可靠)
if os.name == 'nt': # Windows
# 创建临时批处理文件
temp_bat = os.path.join(os.environ.get('TEMP', '.'), 'clean_pycache_temp.bat')
with open(temp_bat, 'w') as f:
f.write('@echo off\n')
# 添加删除 __pycache__ 目录的命令
for pycache_path in pycache_dirs:
f.write(f'if exist "{pycache_path}" rd /s /q "{pycache_path}"\n')
# 添加删除 .pyc 文件的命令
for file_path in pyc_files:
f.write(f'if exist "{file_path}" del /f /q "{file_path}"\n')
# 执行批处理文件
os.system(f'"{temp_bat}"')
# 删除临时批处理文件
try:
os.remove(temp_bat)
except:
pass
# 验证是否删除成功
remaining_pycache = 0
for pycache_path in pycache_dirs:
if not os.path.exists(pycache_path):
count += 1
else:
remaining_pycache += 1
remaining_pyc = 0
for file_path in pyc_files:
if not os.path.exists(file_path):
count += 1
else:
remaining_pyc += 1
if remaining_pycache > 0 or remaining_pyc > 0:
print(f"Warning: {remaining_pycache} __pycache__ directories and {remaining_pyc} .pyc files could not be removed")
else: # Unix/Linux
# 使用系统命令删除 __pycache__ 目录
for pycache_path in pycache_dirs:
if os.system(f'rm -rf "{pycache_path}"') == 0:
print(f"Removed __pycache__ directory: {pycache_path}")
count += 1
else:
print(f"Failed to remove {pycache_path}")
# 使用系统命令删除 .pyc 文件
for file_path in pyc_files:
if os.system(f'rm -f "{file_path}"') == 0:
print(f"Removed .pyc file: {file_path}")
count += 1
else:
print(f"Failed to remove {file_path}")
except Exception as error:
print(f"Error cleaning __pycache__ directories: {str(error)}")
print(f"Removed {count} files/directories: {len(pycache_dirs)} __pycache__ directories and {len(pyc_files)} .pyc files")
traceback.print_exc()
return count
class ModuleReloader(object):
"""Class for reloading modules in the Plugin"""
@staticmethod
def get_package_modules(package_name):
"""
Get all modules in a package
Args:
package_name (str): Name of the package
Returns:
list: List of module names
"""
package_modules = []
try:
# 尝试导入包
package = importlib.import_module(package_name)
package_path = os.path.dirname(package.__file__)
# 遍历包目录查找模块
for root, dirs, files in os.walk(package_path):
for file in files:
if file.endswith('.py') and file != '__init__.py':
# 计算相对路径
rel_path = os.path.relpath(os.path.join(root, file), package_path)
# 转换为模块路径格式
module_path = os.path.splitext(rel_path)[0].replace(os.sep, '.')
# 构建完整模块名
module_name = f"{package_name}.{module_path}"
package_modules.append(module_name)
# 添加子包
for dir_name in dirs:
init_file = os.path.join(root, dir_name, '__init__.py')
if os.path.exists(init_file):
rel_path = os.path.relpath(os.path.join(root, dir_name), package_path)
module_name = f"{package_name}.{rel_path.replace(os.sep, '.')}"
package_modules.append(module_name)
return package_modules
except Exception as e:
print(f"错误: 无法导入包 {package_name}: {str(e)}")
traceback.print_exc()
return []
@staticmethod
def reload_module(module_name):
"""Reload a specific module
Args:
module_name (str): Name of the module to reload
Returns:
bool: True if successful, False otherwise
"""
try:
# 尝试导入模块
try:
module = __import__(module_name, fromlist=["*"])
except ImportError:
# 如果失败,尝试将点替换为斜杠
module_path = module_name.replace('.', '/')
if os.path.exists(os.path.join(TOOL_PATH, module_path + '.py')):
sys_path_modified = False
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
sys_path_modified = True
module = __import__(module_name, fromlist=["*"])
if sys_path_modified:
sys.path.remove(TOOL_PATH)
else:
raise ImportError(f"Module {module_name} not found")
# 重新加载模块
importlib.reload(module)
print(f"成功重新加载模块: {module_name}")
return True
except Exception as e:
print(f"重新加载模块 {module_name} 时出错: {str(e)}")
traceback.print_exc()
return False
@classmethod
def reload_all_modules(cls):
"""
Reload all Plugin modules
Returns:
dict: Results of reloading each module
"""
# 首先声明全局变量
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH
results = {}
# 清理缓存文件
clean_pycache(TOOL_PATH)
# 首先尝试重新加载主配置文件
try:
# 尝试直接导入主配置
if 'config' in sys.modules:
config_module = sys.modules['config']
importlib.reload(config_module)
print("成功重新加载主配置文件")
results['config'] = True
TOOL_NAME = config_module.TOOL_NAME
TOOL_VERSION = config_module.TOOL_VERSION
TOOL_AUTHOR = config_module.TOOL_AUTHOR
TOOL_PATH = config_module.TOOL_PATH
SCRIPTS_PATH = config_module.SCRIPTS_PATH
UI_PATH = config_module.UI_PATH
STYLE_FILE = config_module.STYLE_FILE
ICONS_PATH = config_module.ICONS_PATH
ASSETS_PATH = config_module.ASSETS_PATH
DNA_FILE_PATH = config_module.DNA_FILE_PATH
else:
# 尝试重新导入
import_main_config()
results['config'] = True
except Exception as e:
print(f"重新加载主配置文件时出错: {str(e)}")
results['config'] = False
# 定义要重新加载的模块
modules_to_reload = [
"scripts.utils.Config", # 首先重新加载配置
"scripts.utils",
"scripts.ui",
"scripts.Main"
]
# 获取所有子模块
all_modules = []
for module in modules_to_reload:
all_modules.append(module)
try:
submodules = cls.get_package_modules(module)
if submodules:
all_modules.extend(submodules)
except Exception as e:
print(f"获取模块 {module} 的子模块时出错: {str(e)}")
# 去除重复项并排序
all_modules = sorted(list(set(all_modules)))
# 按照依赖关系对模块进行排序
# 确保配置模块先重新加载
priority_modules = []
normal_modules = []
for module in all_modules:
if "Config" in module or module.endswith(".config"):
priority_modules.append(module)
else:
normal_modules.append(module)
# 先重新加载优先级模块,然后是普通模块
for module in priority_modules + normal_modules:
results[module] = cls.reload_module(module)
return results
def reload_all():
"""Reload all Plugin modules and display results"""
print("\n" + "-"*50)
print("Reloading Plugin modules...")
print("-"*50)
# 获取插件根目录 - 使用config中定义的TOOL_PATH
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
# 如果无法导入config则使用相对路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
print("Cleaning __pycache__ directories...")
pycache_count = clean_pycache(TOOL_PATH)
print(f"Removed {pycache_count} __pycache__ directories and .pyc files")
# Reload modules
results = ModuleReloader.reload_all_modules()
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
print("\nReload Summary:")
print(f"Successfully reloaded {success_count} of {total_count} modules")
if success_count < total_count:
print("\nFailed modules:")
for module, success in results.items():
if not success:
print(f" - {module}")
print("-"*50)
return results
def show_reload_ui():
"""Show a UI for reloading modules"""
# 检查是否有Qt模块可用
if not 'HAS_QT' in globals() or not HAS_QT:
cmds.warning("Qt modules not available. Falling back to command line reload.")
return reload_all()
try:
# 创建对话框
dialog = QtWidgets.QDialog(getMayaMainWindow())
dialog.setWindowTitle("Plugin Reload UI")
dialog.setMinimumWidth(450)
dialog.setWindowFlags(dialog.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
# 创建布局
layout = QtWidgets.QVBoxLayout(dialog)
# 创建信息标签
info_label = QtWidgets.QLabel("Reload Plugin modules, no need to restart Maya.")
layout.addWidget(info_label)
# 创建模块选择区域
module_group = QtWidgets.QGroupBox("Select Modules to Reload")
module_layout = QtWidgets.QVBoxLayout(module_group)
# 创建全选复选框
all_modules_cb = QtWidgets.QCheckBox("All Modules")
all_modules_cb.setChecked(True)
module_layout.addWidget(all_modules_cb)
# 创建模块类别复选框
module_cbs = {}
module_categories = [
("Core Modules", [
"scripts",
"scripts.Main",
"scripts.ReloadModules"
]
),
("UI Modules", [
"scripts.ui",
"scripts.ui.toolbar",
"scripts.ui.geometry",
"scripts.ui.rigging",
"scripts.ui.behaviour",
"scripts.ui.definition"
]
),
("Utility Modules", [
"scripts.utils",
"scripts.utils.utils_toolbar",
"scripts.utils.utils_geometry",
"scripts.utils.utils_rigging",
"scripts.utils.utils_behaviour",
"scripts.utils.utils_definition"
]
),
("Config", ["config"])
]
for category, modules in module_categories:
category_cb = QtWidgets.QCheckBox(category)
category_cb.setChecked(True)
category_cb.setEnabled(False)
module_layout.addWidget(category_cb)
module_cbs[category] = (category_cb, modules)
# 全选/取消全选逻辑
def toggle_all_modules():
checked = all_modules_cb.isChecked()
for category, (cb, _) in module_cbs.items():
cb.setChecked(checked)
cb.setEnabled(not checked)
all_modules_cb.toggled.connect(toggle_all_modules)
layout.addWidget(module_group)
# 创建操作按钮区域
button_layout = QtWidgets.QHBoxLayout()
# 创建重载按钮
reload_button = QtWidgets.QPushButton("Reload Selected Modules")
reload_button.setMinimumHeight(30)
button_layout.addWidget(reload_button)
# 创建清理缓存按钮
clean_button = QtWidgets.QPushButton("Clean Caches Only")
clean_button.setMinimumHeight(30)
button_layout.addWidget(clean_button)
layout.addLayout(button_layout)
# 创建结果文本区域
results_text = QtWidgets.QTextEdit()
results_text.setReadOnly(True)
results_text.setMinimumHeight(300)
layout.addWidget(results_text)
# 创建关闭按钮
close_button = QtWidgets.QPushButton("Close")
close_button.setMinimumHeight(30)
close_button.clicked.connect(dialog.close)
layout.addWidget(close_button)
# 重载并更新UI的函数
def reload_and_update_ui(text_widget):
text_widget.clear()
text_widget.append("Reloading modules...\n")
# 获取插件根目录 - 使用config中定义的TOOL_PATH
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
# 如果无法导入config则使用相对路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
text_widget.append(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
text_widget.append("Cleaning __pycache__ directories...")
pycache_count = clean_pycache(TOOL_PATH)
text_widget.append(f"Removed {pycache_count} __pycache__ directories and .pyc files\n")
# 确定要重载的模块
results = {}
if all_modules_cb.isChecked():
# 重载所有模块
results = ModuleReloader.reload_all_modules()
else:
# 重载选定的模块类别
selected_categories = []
for category, (cb, modules) in module_cbs.items():
if cb.isChecked():
selected_categories.extend(modules)
if not selected_categories:
text_widget.append("No module was selected, the operation was canceled.")
return
# 获取所有模块
all_modules = []
for package in selected_categories:
if package.startswith("scripts."):
all_modules.extend(ModuleReloader.get_package_modules(package))
else:
all_modules.append(package)
# 重载选定的模块
results = {}
for module in all_modules:
results[module] = ModuleReloader.reload_module(module)
# 更新UI显示结果
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
text_widget.append(f"Successfully reloaded {success_count}/{total_count} modules\n")
# 显示成功重载的模块
text_widget.append("Successfully reloaded modules:")
for module, success in results.items():
if success:
text_widget.append(f" - {module}")
# 显示失败的模块(如果有)
if success_count < total_count:
text_widget.append("\nFailed to reload modules:")
for module, success in results.items():
if not success:
text_widget.append(f" - {module}")
# 提示用户重载完成
text_widget.append("\nReload operation completed!")
# 仅清理缓存的函数
def clean_cache_only(text_widget):
text_widget.clear()
text_widget.append("Cleaning caches only...\n")
# 获取插件根目录
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
text_widget.append(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
pycache_count = clean_pycache(TOOL_PATH)
text_widget.append(f"Removed {pycache_count} __pycache__ directories and .pyc files")
text_widget.append("\nCache cleaning operation completed!")
# 连接按钮信号
reload_button.clicked.connect(lambda: reload_and_update_ui(results_text))
clean_button.clicked.connect(lambda: clean_cache_only(results_text))
# 显示对话框
dialog.show()
except Exception as e:
traceback.print_exc()
cmds.warning(f"Failed to show UI: {str(e)}. Falling back to command line reload.")
return reload_all()
# Main function to be called from Maya
def main():
"""Main function to be called from Maya"""
try:
if HAS_QT:
show_reload_ui()
else:
reload_all()
print("Modules reloaded successfully!")
return True
except Exception as e:
print(f"Error during module reload: {str(e)}")
traceback.print_exc()
return False
# Allow running this script directly
if __name__ == "__main__":
main()