diff --git a/pyworkflow/pyworkflow/node_factory.py b/pyworkflow/pyworkflow/node_factory.py index 3fde202..578a5aa 100644 --- a/pyworkflow/pyworkflow/node_factory.py +++ b/pyworkflow/pyworkflow/node_factory.py @@ -47,6 +47,7 @@ def manipulation_node(node_key, node_info): else: return None + def custom_node(filename, node_key, node_info): try: package = __import__('custom_nodes.' + filename) diff --git a/vp/vp/urls.py b/vp/vp/urls.py index 85bbe74..88caaaf 100644 --- a/vp/vp/urls.py +++ b/vp/vp/urls.py @@ -39,7 +39,6 @@ path('admin/', admin.site.urls), path('info/', views.info), path('nodes/', views.retrieve_nodes_for_user), - path('custom_nodes/', views.retrieve_custom_nodes_for_user), path('node/', include('node.urls')), path('workflow/', include('workflow.urls')) ] diff --git a/vp/vp/views.py b/vp/vp/views.py index 2c55761..e8e9f58 100644 --- a/vp/vp/views.py +++ b/vp/vp/views.py @@ -2,9 +2,11 @@ from rest_framework.decorators import api_view from drf_yasg.utils import swagger_auto_schema from pyworkflow import Node +from modulefinder import ModuleFinder import os import inspect +import sys @swagger_auto_schema(method='get', responses={200:'JSON response with data'}) @@ -42,78 +44,84 @@ def retrieve_nodes_for_user(request): """ data = dict() - # Iterate through node 'types' + # Iterate through installed Nodes for parent in Node.__subclasses__(): key = getattr(parent, "display_name", parent.__name__) data[key] = list() # Iterate through node 'keys' for child in parent.__subclasses__(): - # TODO: check attribute-scope is handled correctly - child_node = { - 'name': child.name, - 'node_key': child.__name__, - 'node_type': parent.__name__, - 'num_in': child.num_in, - 'num_out': child.num_out, - 'color': child.color or parent.color, - 'doc': child.__doc__, - 'options': {k: v.get_value() for k, v in child.options.items()}, - 'option_types': child.option_types, - 'download_result': getattr(child, "download_result", False) - } - - data[key].append(child_node) + node = extract_node_info(parent, child) + data[key].append(node) + + # Check for any installed Custom Nodes + # TODO: Workflow loading excluded in middleware for this route + # Should probably have a way to access the 'custom_node` dir dynamically + custom_node_path = os.path.join(os.getcwd(), '../pyworkflow/custom_nodes') + data['CustomNode'] = import_custom_node(custom_node_path) return JsonResponse(data) -@swagger_auto_schema(method='get', - operation_summary='Retrieve a list of custom Nodes', - operation_description='Retrieves a list of custom Nodes, in JSON.', - responses={ - 200: 'List of installed Nodes, in JSON', - }) -@api_view(['GET']) -def retrieve_custom_nodes_for_user(request): - data = dict() +def check_missing_packages(node_path): + finder = ModuleFinder(node_path) + finder.run_script(node_path) + + uninstalled = list() + for missing_package in finder.badmodules.keys(): + if missing_package not in sys.modules: + uninstalled.append(missing_package) + + return uninstalled + + +def extract_node_info(parent, child): + # TODO: check attribute(s) accessing is handled correctly + return { + 'name': child.name, + 'node_key': child.__name__, + 'node_type': str(parent), + 'num_in': child.num_in, + 'num_out': child.num_out, + 'color': child.color or parent.color or 'black', + 'doc': child.__doc__, + 'options': {k: v.get_value() for k, v in child.options.items()}, + 'option_types': child.option_types, + 'download_result': getattr(child, "download_result", False) + } - # TODO: Workflow loading excluded in middleware for this route - # Should probably have a way to access the 'custom_node` dir dynamically - custom_node_path = os.path.join(os.getcwd(), '../pyworkflow/custom_nodes') +def import_custom_node(root_path): + # Get list of files in path try: - nodes = os.listdir(custom_node_path) + files = os.listdir(root_path) except OSError as e: - return JsonResponse({"message": str(e)}, status=500) + return None - for node in nodes: - # Parse file type - node_name, ext = os.path.splitext(node) + data = list() + for file in files: + # Check file is not a dir + node_path = os.path.join(root_path, file) + if not os.path.isfile(node_path): + continue + + node, ext = os.path.splitext(file) try: - package = __import__('custom_nodes.' + node_name) - module = getattr(package, node_name) - except ModuleNotFoundError as e: - # TODO: This will only catch the first missing package. Can we get more? - data[node_name] = f"Please install missing packages and restart the server. Missing '{e.name}'" + package = __import__('custom_nodes.' + node) + module = getattr(package, node) + except ModuleNotFoundError: + data.append({ + "name": node, + "missing_packages": check_missing_packages(node_path) + }) continue for name, klass in inspect.getmembers(module): if inspect.isclass(klass) and klass.__module__.startswith('custom_nodes.'): - custom_node = { - 'name': klass.name, - 'node_key': name, - 'node_type': node_name, - 'num_in': klass.num_in, - 'num_out': klass.num_out, - 'color': klass.color or 'black', - 'doc': klass.__doc__, - 'options': {k: v.get_value() for k, v in klass.options.items()}, - 'option_types': klass.option_types, - 'download_result': getattr(klass, "download_result", False) - } - - data[node_name] = custom_node - - return JsonResponse(data, safe=False) + custom_node = extract_node_info(node, klass) + data.append(custom_node) + + return data + +