feat: Add 'Open Py File' context menu to nodes

This commit is contained in:
KewkLW
2025-05-11 14:51:14 -07:00
parent aa74a2d2e1
commit 74c33d4ecc
4 changed files with 279 additions and 3 deletions
+13
View File
@@ -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/
+64 -3
View File
@@ -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']
# 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.")
+80
View File
@@ -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);
}
}
};
}
});
+122
View File
@@ -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.")