feat: Add 'Open Py File' context menu to nodes
This commit is contained in:
+13
@@ -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
@@ -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.")
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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.")
|
||||
Reference in New Issue
Block a user