diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a6506a --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Python bytecode +__pycache__/ +*.pyc + +# Other common Python ignores (optional, but good practice) +*.pyo +*.pyd +*.egg-info/ +dist/ +build/ +*.egg +venv/ +env/ \ No newline at end of file diff --git a/__init__.py b/__init__.py index 08707f3..8d8aba4 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,43 @@ +import logging + +# Configure a base logger for your package if you don't have one +# This is optional, but good practice. +package_logger = logging.getLogger(__name__) # This will be 'your_custom_node_package_name' +if not package_logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + package_logger.addHandler(handler) + package_logger.setLevel(logging.INFO) + + +# Attempt to import ComfyUI server and nodes modules - needed for the open_py_feature +try: + from server import PromptServer +except ImportError: + try: + import main + if hasattr(main, 'server_instance'): + PromptServer = main.server_instance.__class__ + PromptServer.instance = main.server_instance + package_logger.info("Successfully imported PromptServer instance via main.server_instance.") + else: + if 'PromptServer' in globals() and hasattr(globals()['PromptServer'], 'instance'): + PromptServer = globals()['PromptServer'] + package_logger.info("Used globally available PromptServer instance.") + else: + raise ImportError("PromptServer instance not found in main or global scope.") + except Exception as e: + package_logger.error(f"Failed to import PromptServer: {e}. The 'Open Py File' API route might not be available.") + PromptServer = None + +try: + import nodes +except ImportError: + package_logger.error("Failed to import 'nodes' module from ComfyUI. 'Open Py File' feature might not work correctly.") + nodes = None + +# --- Your existing imports for other nodes --- from .text_append_node import NODE_CLASS_MAPPINGS as TEXT_APPEND_NODE_CLASS_MAPPINGS from .text_append_node import NODE_DISPLAY_NAME_MAPPINGS as TEXT_APPEND_NODE_DISPLAY_NAME_MAPPINGS from .vramdebugplus import NODE_CLASS_MAPPINGS as VRAM_DEBUG_PLUS_NODE_CLASS_MAPPINGS @@ -16,9 +56,18 @@ from .LoadImagePlus import NODE_CLASS_MAPPINGS as LOAD_IMAGE_PLUS_NODE_CLASS_MAP from .LoadImagePlus import NODE_DISPLAY_NAME_MAPPINGS as LOAD_IMAGE_PLUS_NODE_DISPLAY_NAME_MAPPINGS from .LoadVideoPlus import NODE_CLASS_MAPPINGS as LOAD_VIDEO_PLUS_NODE_CLASS_MAPPINGS from .LoadVideoPlus import NODE_DISPLAY_NAME_MAPPINGS as LOAD_VIDEO_PLUS_NODE_DISPLAY_NAME_MAPPINGS +# --- End of your existing imports --- + +# Import and initialize the "Open Py File" feature +from . import open_py_feature # Use 'from .' to ensure it's relative to the current package +if PromptServer and hasattr(PromptServer, 'instance') and nodes: + open_py_feature.init_open_py_feature(PromptServer.instance, nodes) +else: + package_logger.warning("'Open Py File' feature could not be initialized due to missing PromptServer or nodes module.") -NODE_CLASS_MAPPINGS = { +# --- Your existing NODE_CLASS_MAPPINGS and NODE_DISPLAY_NAME_MAPPINGS --- +_NODE_CLASS_MAPPINGS = { **TEXT_APPEND_NODE_CLASS_MAPPINGS, **VRAM_DEBUG_PLUS_NODE_CLASS_MAPPINGS, **TENSOR_DEBUG_PLUS_NODE_CLASS_MAPPINGS, @@ -30,7 +79,7 @@ NODE_CLASS_MAPPINGS = { **LOAD_VIDEO_PLUS_NODE_CLASS_MAPPINGS, } -NODE_DISPLAY_NAME_MAPPINGS = { +_NODE_DISPLAY_NAME_MAPPINGS = { **TEXT_APPEND_NODE_DISPLAY_NAME_MAPPINGS, **VRAM_DEBUG_PLUS_NODE_DISPLAY_NAME_MAPPINGS, **TENSOR_DEBUG_PLUS_NODE_DISPLAY_NAME_MAPPINGS, @@ -41,5 +90,17 @@ NODE_DISPLAY_NAME_MAPPINGS = { **LOAD_IMAGE_PLUS_NODE_DISPLAY_NAME_MAPPINGS, **LOAD_VIDEO_PLUS_NODE_DISPLAY_NAME_MAPPINGS, } +# --- End of your existing mappings --- -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file +# Define WEB_DIRECTORY for the JavaScript file(s) +# Ensure your open_py.js is in a 'js' subdirectory within this custom node package. +WEB_DIRECTORY = "./js" + +# Final export (ensure WEB_DIRECTORY is included if ComfyUI checks __all__ for it, +# though it often discovers it as a top-level variable regardless) +NODE_CLASS_MAPPINGS = _NODE_CLASS_MAPPINGS +NODE_DISPLAY_NAME_MAPPINGS = _NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] + +package_logger.info("Custom node package initialized, including 'Open Py File' feature if server and nodes modules were available.") \ No newline at end of file diff --git a/js/open_py.js b/js/open_py.js new file mode 100644 index 0000000..4082372 --- /dev/null +++ b/js/open_py.js @@ -0,0 +1,80 @@ +import { app } from "../../../../scripts/app.js"; // Path relative to js folder in custom_nodes +import { api } from "../../../../scripts/api.js"; // Path relative to js folder in custom_nodes + +async function handleOpenPyFile(node) { + const nodeTypeName = node.type; + if (!nodeTypeName) { + console.error("Node type name is undefined for node:", node.title); + app.ui.dialog.show("Error: Could not identify node type."); + return; + } + + console.log(`Requesting to open Python file for node type: ${nodeTypeName}`); + + try { + const response = await api.fetchApi('/open_node_source', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ node_type: nodeTypeName }), + }); + + const result = await response.json(); + if (!response.ok) { + const errorMessage = result.error || response.statusText || 'Could not open file.'; + console.error("Error opening Python file:", errorMessage, "Status:", response.status, "Response:", result); + app.ui.dialog.show(`Error: ${errorMessage}`); + return; + } + + const successMessage = result.message || "File open request sent successfully."; + console.log(successMessage, "Response:", result); + // Optionally, show a brief success message. + // The file opening is the primary feedback, so this might be redundant. + // app.ui.dialog.show(successMessage); + + } catch (error) { + console.error("Failed to send 'open py' request or parse response:", error); + app.ui.dialog.show("Error: Failed to communicate with server to open file. Check console for details."); + } +} + +app.registerExtension({ + name: "comfy.helper.openPyFile", // Unique name for the extension + async setup() { + console.log("ComfyUI 'Open Py File' Extension: Setup complete."); + }, + beforeRegisterNodeDef(nodeType, nodeData, appInstance) { + // Get the original getExtraMenuOptions function from the node's prototype + const original_getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; + + nodeType.prototype.getExtraMenuOptions = function(_, options) { + // `this` refers to the specific node instance that was right-clicked + + if (original_getExtraMenuOptions) { + original_getExtraMenuOptions.apply(this, arguments); + } + + if (!Array.isArray(options)) { + console.warn("Node context menu options is not an array for node type:", nodeType.comfyClass, "Options:", options); + } + + options.unshift( + { + content: "Open Py File 📜", + callback: () => { + console.log("Open Py File clicked for node:", this.title, "(Type:", this.type, ", ID:", this.id + ")"); + handleOpenPyFile(this); + } + } + ); + + if (options.length > 1 && original_getExtraMenuOptions) { + if (options[1] !== null) { + options.splice(1, 0, null); + } + } + }; + } +}); \ No newline at end of file diff --git a/open_py_feature.py b/open_py_feature.py new file mode 100644 index 0000000..62576ce --- /dev/null +++ b/open_py_feature.py @@ -0,0 +1,122 @@ +import asyncio +from aiohttp import web +import os +import subprocess +import sys +import inspect +import logging + +# Specific logger for this feature +feature_logger = logging.getLogger(__name__) # This will use 'your_custom_node_package_name.open_py_feature' +if not feature_logger.handlers: # Avoid adding multiple handlers if reloaded + handler = logging.StreamHandler() # Or your preferred handler + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + feature_logger.addHandler(handler) + feature_logger.setLevel(logging.INFO) # Set your desired log level + +# Global references to be set by the init function +PROMPT_SERVER_INSTANCE = None +NODES_MODULE = None + +def get_node_filepath(node_type_name_str): + if not NODES_MODULE or not hasattr(NODES_MODULE, 'NODE_CLASS_MAPPINGS'): + feature_logger.error("NODES_MODULE.NODE_CLASS_MAPPINGS is not available. Cannot retrieve node file path.") + return None + + if node_type_name_str in NODES_MODULE.NODE_CLASS_MAPPINGS: + node_class_obj = NODES_MODULE.NODE_CLASS_MAPPINGS[node_type_name_str] + try: + filepath = inspect.getfile(node_class_obj) + if filepath and os.path.basename(filepath) == "__init__.py": + module_dir = os.path.dirname(filepath) + potential_file = os.path.join(module_dir, node_class_obj.__name__ + ".py") + if os.path.exists(potential_file): + feature_logger.info(f"Resolved {node_class_obj.__name__} from __init__.py to {potential_file}") + return potential_file + return filepath + except TypeError: + feature_logger.error(f"Cannot get file for node type '{node_type_name_str}': Not a user-defined class, module, or function.") + return None + except Exception as e: + feature_logger.error(f"Unexpected error in inspect.getfile for '{node_type_name_str}': {e}") + return None + else: + feature_logger.warning(f"Node type '{node_type_name_str}' not found in NODE_CLASS_MAPPINGS.") + return None + +def open_file_os_agnostic(filepath): + try: + feature_logger.info(f"Attempting to open file: {filepath} using system default.") + if sys.platform == "win32": + os.startfile(filepath) + elif sys.platform == "darwin": + subprocess.run(['open', filepath], check=True) + else: + subprocess.run(['xdg-open', filepath], check=True) + feature_logger.info(f"Successfully initiated opening of file: {filepath}") + except FileNotFoundError: + feature_logger.error(f"File not found: {filepath}") + raise + except subprocess.CalledProcessError as e: + feature_logger.error(f"Command failed for opening file '{filepath}': {e}") + raise + except Exception as e: + feature_logger.error(f"An unexpected error occurred while trying to open '{filepath}': {e}") + raise + +async def open_node_source_route_handler(request: web.Request): + try: + if not request.can_read_body: + feature_logger.warning("API call to /api/open_node_source: Request body is missing or unreadable.") + return web.json_response({"error": "Request body is missing or unreadable"}, status=400) + + try: + data = await request.json() + except ValueError: + feature_logger.warning("API call to /api/open_node_source: Invalid JSON in request body.") + return web.json_response({"error": "Invalid JSON in request body"}, status=400) + + node_type_name = data.get('node_type') + + if not node_type_name: + feature_logger.warning("API call to /api/open_node_source missing 'node_type'.") + return web.json_response({"error": "Missing 'node_type' in request body"}, status=400) + + feature_logger.info(f"Received request to open source for node type: {node_type_name}") + + if not NODES_MODULE: + feature_logger.error("Cannot process /api/open_node_source: 'nodes' module reference unavailable.") + return web.json_response({"error": "Server configuration error: Node registry not loaded correctly for feature."}, status=500) + + filepath = get_node_filepath(node_type_name) + + if filepath: + feature_logger.info(f"Found source file for '{node_type_name}' at: {filepath}") + try: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, open_file_os_agnostic, filepath) + return web.json_response({"message": f"Request to open '{os.path.basename(filepath)}' sent successfully."}) + except Exception as e: + feature_logger.error(f"Failed to open file '{filepath}' for node '{node_type_name}': {e}") + return web.json_response({"error": f"Could not open file: {str(e)}"}, status=500) + else: + feature_logger.warning(f"Could not find source file for node type: {node_type_name}") + return web.json_response({"error": f"Source file not found for node type: {node_type_name}"}, status=404) + except Exception as e: + feature_logger.exception(f"Critical error in /api/open_node_source route handler: {e}") + return web.json_response({"error": f"An internal server error occurred: {str(e)}"}, status=500) + +def init_open_py_feature(prompt_server_instance, nodes_module_ref): + global PROMPT_SERVER_INSTANCE, NODES_MODULE + PROMPT_SERVER_INSTANCE = prompt_server_instance + NODES_MODULE = nodes_module_ref + + if PROMPT_SERVER_INSTANCE and hasattr(PROMPT_SERVER_INSTANCE, 'app') and NODES_MODULE: + PROMPT_SERVER_INSTANCE.app.router.add_post('/api/open_node_source', open_node_source_route_handler) + feature_logger.info("Successfully registered POST '/api/open_node_source' API route for 'Open Py File' feature.") + else: + if not PROMPT_SERVER_INSTANCE or not hasattr(PROMPT_SERVER_INSTANCE, 'app'): + feature_logger.error("PromptServer instance or app not available. API route for 'Open Py File' will not be available.") + if not NODES_MODULE: + feature_logger.error("Nodes module reference not available. File path resolution will fail for 'Open Py File' feature.") \ No newline at end of file