#!/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()