diff --git a/README.md b/README.md index 7d61fe6..f54a50c 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ Formatting -
- GitHub closed pull requests + GitHub closed pull requests

@@ -43,6 +42,17 @@ It also aims to prevent re-duplication and re-writing of code inside of a projec + + + + + + + + + +
✅ Preview of images and other elements✅ Attribute editor
+ # 📖 How to ## Adding more classes/nodes @@ -109,6 +119,7 @@ Key elements that a node **CAN** have: * A `HELP` attribute to explain how the node works * The `INPUTS_DICT` dictionary, if the node needs inputs * The `OUTPUTS_DICT` dictionary, if the node needs outputs +* The `INTERNALS_DICT` dictionary, if the node needs inputs/previews through GUI Other considerations: * The `import` statements are kept inside the run method, so no ImportError is met when editing nodes outside the software they are meant for. @@ -176,7 +187,7 @@ Example: `main.py -f environ_to_yaml` Finally, we can use the `-s` argument to set all the attributes we want to change in the scene prior to running. This flag accepts as many arguments as we need. Here is an example setting one node's attribute prior to running: -Example: `main.py -f environ_to_yaml -s OPEN_FILE.out_bool true` +Example: `main.py -f environ_to_yaml -s OPEN_FILE.internal_bool true` # ▶️ Execution logic In a scene, the execution starts from nodes that are recognized as "starting nodes". @@ -248,6 +259,7 @@ logic_scene.run_all_nodes() # 📈 Analytics Automatically generated once a week +(For personal use, they are not gathered from any other user) ![](docs/analytics/recent_usage.png) ![](docs/analytics/most_used.png) ![](docs/analytics/errored.png) diff --git a/constants.py b/constants.py index 1f705a1..31a47d2 100644 --- a/constants.py +++ b/constants.py @@ -5,6 +5,7 @@ __license__ = "MIT License" +from enum import auto, Enum import logging import os @@ -58,6 +59,8 @@ INPUT = "INPUT" OUTPUT = "OUTPUT" +INTERNAL = "INTERNAL" + START = "START" COMPLETED = "COMPLETED" @@ -70,6 +73,25 @@ ERROR = "ERROR" +# -------------------------------- GUI INPUT / PREVIEW TYPES -------------------------------- # +class InputsGUI(Enum): + STR_INPUT = auto() + MULTILINE_STR_INPUT = auto() + INT_INPUT = auto() + FLOAT_INPUT = auto() + BOOL_INPUT = auto() + OPTION_INPUT = ["A", "B", "C"] + TUPLE_INPUT = auto() + DICT_INPUT = auto() + LIST_INPUT = auto() + + +class PreviewsGUI(Enum): + STR_PREVIEW = auto() + MULTILINE_STR_PREVIEW = auto() + IMAGE_PREVIEW = auto() + + # -------------------------------- LOGGING PREFS -------------------------------- # LOGGING_LEVEL = logging.INFO CONSOLE_LOG_FORMATTER = logging.Formatter( diff --git a/docs/attribute_editor.gif b/docs/attribute_editor.gif index 3cbe6d5..836ae33 100644 Binary files a/docs/attribute_editor.gif and b/docs/attribute_editor.gif differ diff --git a/docs/preview.png b/docs/preview.png index e07b860..ee45e72 100644 Binary files a/docs/preview.png and b/docs/preview.png differ diff --git a/docs/previews.gif b/docs/previews.gif new file mode 100644 index 0000000..4819e1d Binary files /dev/null and b/docs/previews.gif differ diff --git a/graphic/graphic_node.py b/graphic/graphic_node.py index 779e3f8..8e5ff37 100644 --- a/graphic/graphic_node.py +++ b/graphic/graphic_node.py @@ -5,7 +5,7 @@ __license__ = "MIT License" -import ast +from functools import partial import math from PySide2 import QtWidgets @@ -29,6 +29,7 @@ def __init__(self, logic_node, color_name): # SHAPE self.extra_header = 0 + self.extra_bottom = 0 self.node_width = 450 # INIT @@ -51,15 +52,14 @@ def __init__(self, logic_node, color_name): constants.STRIPE_HEIGHT, QtCore.Qt.TransformationMode.SmoothTransformation ) self.class_icon = QtWidgets.QGraphicsPixmapItem(class_pixmap, parent=self) + self.class_text = QtWidgets.QGraphicsTextItem(parent=self) self.proxy_help_btn = QtWidgets.QGraphicsProxyWidget(parent=self) - self.input_widget = None - self.proxy_input_widget = QtWidgets.QGraphicsProxyWidget(parent=self) - self.selection_marquee = QtWidgets.QGraphicsPathItem(parent=self) self.selection_marquee.hide() + self.error_marquee = QtWidgets.QGraphicsPathItem(parent=self) self.error_marquee.setFlag(QtWidgets.QGraphicsItem.ItemStacksBehindParent) self.error_marquee.hide() @@ -80,19 +80,30 @@ def __init__(self, logic_node, color_name): # ATTRIBUTES self.graphic_attributes = [] - # SPECIAL / INPUT - self.input_datatype = None - if hasattr(self.logic_node, "INPUT_TYPE"): - self.input_datatype = self.logic_node.INPUT_TYPE - self.setScale(0.7) + # DIRECT INPUT WIDGETS + self.input_widgets = [] + self.proxy_input_widgets = [] + + self.has_gui_inputs = False + if self.logic_node.get_gui_internals_inputs(): + self.has_gui_inputs = True + + # PREVIEW WIDGETS + self.preview_widgets = [] + self.proxy_preview_widgets = [] + + self.has_gui_previews = False + if self.logic_node.get_gui_internals_previews(): + self.has_gui_previews = True # SETUP self.setup_node() self.add_graphic_attributes() - self.setup_widget() + self.setup_input_widgets() + self.setup_preview_widgets() self.make_shape() self.place_graphic_attributes() - self.update_attribute_from_widget() + self.update_attributes_from_widgets() self.setup_extras() # Connect @@ -106,6 +117,7 @@ def node_height(self): constants.HEADER_HEIGHT + self.extra_header + (self.logic_node.get_max_in_or_out_count()) * constants.STRIPE_HEIGHT + + self.extra_bottom ) total_h += constants.STRIPE_HEIGHT @@ -141,25 +153,74 @@ def guess_width_to_use(self): longest_in_name + longest_out_name + 2 * constants.CHAMFER_RADIUS + 20, ) - # Re-shape widget - if self.input_datatype: + # Re-shape input / preview widgets + if self.has_gui_inputs: self.node_width = max( self.node_width, - self.input_widget.width() + 2 * constants.CHAMFER_RADIUS, + max( + w.width() + 2 * constants.CHAMFER_RADIUS for w in self.input_widgets + ), ) - self.input_widget.setFixedSize( + for i in range(len(self.input_widgets)): + self.input_widgets[i].setFixedSize( + max( + self.node_width - 2 * constants.CHAMFER_RADIUS, + self.input_widgets[i].width(), + ), + self.input_widgets[i].height(), + ) + + self.proxy_input_widgets.append( + QtWidgets.QGraphicsProxyWidget(parent=self) + ) + self.proxy_input_widgets[i].setWidget(self.input_widgets[i]) + self.proxy_input_widgets[i].setPos( + constants.CHAMFER_RADIUS, constants.HEADER_HEIGHT + ) + if i: + prev_widget = self.proxy_input_widgets[i - 1].widget() + self.proxy_input_widgets[i].setPos( + constants.CHAMFER_RADIUS, + prev_widget.pos().y() + prev_widget.height(), + ) + + if self.has_gui_previews: + self.node_width = max( + self.node_width, max( - self.node_width - 2 * constants.CHAMFER_RADIUS, - self.input_widget.width(), + w.width() + 2 * constants.CHAMFER_RADIUS + for w in self.preview_widgets ), - self.input_widget.height(), ) - self.proxy_input_widget.setWidget(self.input_widget) - self.proxy_input_widget.moveBy( - constants.CHAMFER_RADIUS, constants.HEADER_HEIGHT - ) + for i in range(len(self.preview_widgets)): + self.preview_widgets[i].setFixedSize( + max( + self.node_width - 2 * constants.CHAMFER_RADIUS, + self.preview_widgets[i].width(), + ), + self.preview_widgets[i].height(), + ) + + self.proxy_preview_widgets.append( + QtWidgets.QGraphicsProxyWidget(parent=self) + ) + self.proxy_preview_widgets[i].setWidget(self.preview_widgets[i]) + self.proxy_preview_widgets[i].setPos( + constants.CHAMFER_RADIUS, + constants.HEADER_HEIGHT + + +self.extra_header + + (self.logic_node.get_max_in_or_out_count()) + * constants.STRIPE_HEIGHT + + constants.CHAMFER_RADIUS, + ) + if i: + prev_widget = self.proxy_preview_widgets[i - 1].widget() + self.proxy_preview_widgets[i].setPos( + constants.CHAMFER_RADIUS, + prev_widget.pos().y() + prev_widget.height(), + ) def setup_node(self): """ @@ -336,189 +397,336 @@ def place_graphic_attributes(self): for attr in self.graphic_attributes: attr.setup_graphics() - def setup_widget(self): + def setup_input_widgets(self): """ - For special nodes that take input, setup a widget to receive the input + For special nodes that take input, setup widgets to receive the input """ - if not self.input_datatype: + if not self.has_gui_inputs: return - # Default widget - self.input_widget = QtWidgets.QLineEdit(parent=None) - self.input_widget.setFixedSize( - 150, - int(constants.HEADER_HEIGHT), - ) + # Set type of widget depending on the type of input neede + gui_internals_inputs = self.logic_node.get_gui_internals_inputs() - # Set type of widget depending on the type of input needed - if self.input_datatype == "str": - self.input_widget.setStyleSheet( - "background:transparent; color:white; border:1px solid white;" - ) - self.input_widget.setPlaceholderText("str here") + for attr_name in gui_internals_inputs: + gui_input_type = gui_internals_inputs.get(attr_name).get("gui_type") - if self.logic_node.get_attribute_value("out_str"): - self.input_widget.setText( - self.logic_node.get_attribute_value("out_str") + if gui_input_type == constants.InputsGUI.STR_INPUT: + new_input_widget = QtWidgets.QLineEdit(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 150, + int(constants.HEADER_HEIGHT), ) - - self.input_widget.textChanged.connect(self.update_attribute_from_widget) - - elif self.input_datatype == "multiline_str": - self.input_widget = QtWidgets.QPlainTextEdit(parent=None) - self.input_widget.setStyleSheet( - "background:transparent; color:white; border:1px solid white;" - ) - self.input_widget.setPlaceholderText("Text here") - - if self.logic_node.get_attribute_value("out_str"): - self.input_widget.setText( - self.logic_node.get_attribute_value("out_str") + new_input_widget.setStyleSheet( + "background:transparent; color:white; border:1px solid white;" ) - - self.input_widget.textChanged.connect(self.update_attribute_from_widget) - self.input_widget.setFixedSize( - 300, - int(4 * constants.HEADER_HEIGHT), - ) - - elif self.input_datatype == "dict": - self.input_widget = QtWidgets.QPlainTextEdit(parent=None) - self.input_widget.setStyleSheet( - "background:transparent; color:white; border:1px solid white;" - ) - self.input_widget.setPlaceholderText("Paste dict here") - - if self.logic_node.get_attribute_value("out_dict"): - self.input_widget.setText( - self.logic_node.get_attribute_value("out_dict") + new_input_widget.setPlaceholderText("str here") + if self.logic_node.get_attribute_value(attr_name): + new_input_widget.setText( + self.logic_node.get_attribute_value(attr_name) + ) + new_input_widget.textChanged.connect( + partial( + self.update_attributes_from_widgets, + attr_name, + ) ) - - self.input_widget.textChanged.connect(self.update_attribute_from_widget) - self.input_widget.setFixedSize( - 300, - int(4 * constants.HEADER_HEIGHT), - ) - - elif self.input_datatype == "list": - self.input_widget.setStyleSheet( - "background:transparent; color:white; border:1px solid white;" - ) - self.input_widget.setPlaceholderText("Paste list here") - - if self.logic_node.get_attribute_value("out_list"): - self.input_widget.setText( - self.logic_node.get_attribute_value("out_list") + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.MULTILINE_STR_INPUT: + new_input_widget = QtWidgets.QPlainTextEdit(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 300, + int(4 * constants.HEADER_HEIGHT), ) - - self.input_widget.textChanged.connect(self.update_attribute_from_widget) - - elif self.input_datatype == "bool": - self.input_widget = QtWidgets.QCheckBox("False", parent=None) - self.input_widget.setStyleSheet( - "QCheckBox::indicator{border : 1px solid white;}" - "QCheckBox::indicator:checked{ background:rgba(255,255,200,150); }" - "QCheckBox{ background:transparent; color:white}" - ) - self.input_widget.stateChanged.connect( - lambda: self.input_widget.setText( - ["False", "True"][self.input_widget.isChecked()] + new_input_widget.setStyleSheet( + "background:transparent; color:white; border:1px solid white;" ) - ) - if self.logic_node.get_attribute_value("out_bool"): - self.input_widget.setChecked( - self.logic_node.get_attribute_value("out_bool") + new_input_widget.setPlaceholderText("Text here") + if self.logic_node.get_attribute_value(attr_name): + new_input_widget.setText( + self.logic_node.get_attribute_value(attr_name) + ) + new_input_widget.textChanged.connect( + partial( + self.update_attributes_from_widgets, + attr_name, + ) ) - - self.input_widget.stateChanged.connect(self.update_attribute_from_widget) - self.input_widget.setFixedSize( - 150, - int(constants.HEADER_HEIGHT), - ) - - elif self.input_datatype == "int": - self.input_widget = QtWidgets.QSpinBox(parent=None) - self.input_widget.setMaximum(int(1e6)) - self.input_widget.setMinimum(int(-1e6)) - self.input_widget.setStyleSheet( - "background:transparent; color:white; border:1px solid white;" - ) - - if self.logic_node.get_attribute_value("out_int"): - self.input_widget.setValue( - self.logic_node.get_attribute_value("out_int") + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.DICT_INPUT: + new_input_widget = QtWidgets.QPlainTextEdit(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 300, + int(4 * constants.HEADER_HEIGHT), ) - - self.input_widget.valueChanged.connect(self.update_attribute_from_widget) - self.input_widget.setFixedSize( - 150, - int(constants.HEADER_HEIGHT), - ) - - elif self.input_datatype == "float": - self.input_widget = QtWidgets.QDoubleSpinBox(parent=None) - self.input_widget.setMaximum(1e6) - self.input_widget.setMinimum(-1e6) - self.input_widget.setStyleSheet( - "background:transparent; color:white; border:1px solid white;" - ) - - if self.logic_node.get_attribute_value("out_float"): - self.input_widget.setValue( - self.logic_node.get_attribute_value("out_float") + new_input_widget.setStyleSheet( + "background:transparent; color:white; border:1px solid white;" ) + new_input_widget.setPlaceholderText("Text here") + if self.logic_node.get_attribute_value(attr_name): + new_input_widget.setText( + self.logic_node.get_attribute_value(attr_name) + ) + new_input_widget.textChanged.connect( + partial( + self.update_attributes_from_widgets, + attr_name, + ) + ) + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.LIST_INPUT: + new_input_widget = QtWidgets.QLineEdit(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 150, + int(constants.HEADER_HEIGHT), + ) + new_input_widget.setStyleSheet( + "background:transparent; color:white; border:1px solid white;" + ) + new_input_widget.setPlaceholderText("list here") + if self.logic_node.get_attribute_value(attr_name): + new_input_widget.setText( + self.logic_node.get_attribute_value(attr_name) + ) + new_input_widget.textChanged.connect( + partial( + self.update_attributes_from_widgets, + attr_name, + ) + ) + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.BOOL_INPUT: + new_input_widget = QtWidgets.QCheckBox("False", parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize(150, int(constants.HEADER_HEIGHT)) + new_input_widget.setStyleSheet( + "QCheckBox::indicator{border : 1px solid white;}" + "QCheckBox::indicator:checked{ background:rgba(255,255,200,150); }" + "QCheckBox{ background:transparent; color:white}" + ) + if self.logic_node.get_attribute_value(attr_name): + new_input_widget.setChecked( + self.logic_node.get_attribute_value(attr_name) + ) + new_input_widget.stateChanged.connect( + lambda: new_input_widget.setText( + ["False", "True"][new_input_widget.isChecked()] + ) + ) + new_input_widget.clicked.connect( + partial(self.update_attributes_from_widgets, attr_name) + ) + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.INT_INPUT: + new_input_widget = QtWidgets.QSpinBox(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 150, + int(constants.HEADER_HEIGHT), + ) + new_input_widget.setStyleSheet( + "background:transparent; color:white; border:1px solid white;" + ) + new_input_widget.setMaximum(int(1e6)) + new_input_widget.setMinimum(int(-1e6)) + if self.logic_node.get_attribute_value("out_int"): + new_input_widget.setValue( + self.logic_node.get_attribute_value("out_int") + ) + new_input_widget.valueChanged.connect( + partial(self.update_attributes_from_widgets, attr_name) + ) + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.FLOAT_INPUT: + new_input_widget = QtWidgets.QDoubleSpinBox(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 150, + int(constants.HEADER_HEIGHT), + ) + new_input_widget.setStyleSheet( + "background:transparent; color:white; border:1px solid white;" + ) + new_input_widget.setMaximum(int(1e6)) + new_input_widget.setMinimum(int(-1e6)) + if self.logic_node.get_attribute_value("out_int"): + new_input_widget.setValue( + self.logic_node.get_attribute_value("out_int") + ) + new_input_widget.valueChanged.connect( + partial(self.update_attributes_from_widgets, attr_name) + ) + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.OPTION_INPUT: + new_input_widget = QtWidgets.QComboBox(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 150, + int(constants.HEADER_HEIGHT), + ) + new_input_widget.adjustSize() + new_input_widget.setFixedSize( + new_input_widget.width() + 20, new_input_widget.height() + ) + new_input_widget.setStyleSheet( + "QComboBox { background:transparent; color:white; border:1px solid white; }" + "QWidget:item { color: black; background:white; }" + ) + new_input_widget.addItems( + gui_internals_inputs.get(attr_name).get( + "options", constants.InputsGUI.OPTION_INPUT.value + ) + ) + if self.logic_node.get_attribute_value("out_str"): + new_input_widget.setCurrentText( + self.logic_node.get_attribute_value("out_str") + ) + new_input_widget.currentIndexChanged.connect( + partial(self.update_attributes_from_widgets, attr_name) + ) + self.input_widgets.append(new_input_widget) + + elif gui_input_type == constants.InputsGUI.TUPLE_INPUT: + new_input_widget = QtWidgets.QLineEdit(parent=None) + new_input_widget.setObjectName(attr_name) + new_input_widget.setToolTip(f"Internal attribute: {attr_name}") + new_input_widget.setFixedSize( + 150, + int(constants.HEADER_HEIGHT), + ) + new_input_widget.setStyleSheet( + "background:transparent; color:white; border:1px solid white;" + ) + new_input_widget.setPlaceholderText("tuple here") + if self.logic_node.get_attribute_value(attr_name): + new_input_widget.setText( + self.logic_node.get_attribute_value(attr_name) + ) + new_input_widget.textChanged.connect( + partial( + self.update_attributes_from_widgets, + attr_name, + ) + ) + self.input_widgets.append(new_input_widget) - self.input_widget.valueChanged.connect(self.update_attribute_from_widget) - self.input_widget.setFixedSize( - 150, - int(constants.HEADER_HEIGHT), - ) - - elif self.input_datatype == "option": - self.input_widget = QtWidgets.QComboBox(parent=None) - self.input_widget.setStyleSheet( - "QComboBox { background:transparent; color:white; border:1px solid white; }" - "QWidget:item { color: black; background:white; }" - ) - self.input_widget.addItems(self.logic_node.INPUT_OPTIONS) + # Set font of the widgets + for w in self.input_widgets: + w.setFont(QtGui.QFont(constants.NODE_FONT, constants.HEADER_HEIGHT * 0.4)) - if self.logic_node.get_attribute_value("out_str"): - self.input_widget.setCurrentText( - self.logic_node.get_attribute_value("out_str") - ) + # Measure + self.extra_header = sum([w.height() for w in self.input_widgets]) + 5 - self.input_widget.currentIndexChanged.connect( - self.update_attribute_from_widget - ) - self.input_widget.setFixedSize( - 150, - int(constants.HEADER_HEIGHT), - ) - self.input_widget.adjustSize() - self.input_widget.setFixedSize( - self.input_widget.width() + 20, self.input_widget.height() - ) + def setup_preview_widgets(self): + """ + For special nodes that can display previews, setup widgets + """ + if not self.has_gui_previews: + return - elif self.input_datatype == "tuple": - self.input_widget.setStyleSheet( - "background:transparent; color:white; border:1px solid white;" - ) - self.input_widget.setPlaceholderText("tuple here") + # Separator label + previews_label = QtWidgets.QLabel(parent=None) + previews_label.setFixedSize( + 150, + int(constants.STRIPE_HEIGHT), + ) + previews_label.setStyleSheet( + "background:transparent; color:white; border:none;" + ) + previews_label.setText("- PREVIEWS -") + self.preview_widgets.append(previews_label) + + # Set type of widget depending on the type of preview needed + gui_internals_previews = self.logic_node.get_gui_internals_previews() + + for attr_name in gui_internals_previews: + gui_preview_type = gui_internals_previews.get(attr_name).get("gui_type") + + if gui_preview_type == constants.PreviewsGUI.STR_PREVIEW: + new_preview_widget = QtWidgets.QLineEdit(parent=None) + new_preview_widget.setObjectName(attr_name) + new_preview_widget.setToolTip(f"Internal attribute: {attr_name}") + new_preview_widget.setReadOnly(True) + new_preview_widget.setFixedSize( + 150, + int(constants.STRIPE_HEIGHT), + ) + new_preview_widget.setStyleSheet( + "background:transparent; color:white; border:1px dotted white;" + ) - if self.logic_node.get_attribute_value("out_tuple"): - self.input_widget.setText( - self.logic_node.get_attribute_value("out_tuple") + if self.logic_node.get_attribute_value(attr_name): + new_preview_widget.setText( + self.logic_node.get_attribute_value(attr_name) + ) + else: + new_preview_widget.setText(f"[{attr_name}]") + + self.preview_widgets.append(new_preview_widget) + + elif gui_preview_type == constants.PreviewsGUI.MULTILINE_STR_PREVIEW: + new_preview_widget = QtWidgets.QPlainTextEdit(parent=None) + new_preview_widget.setObjectName(attr_name) + new_preview_widget.setToolTip(f"Internal attribute: {attr_name}") + new_preview_widget.setReadOnly(True) + new_preview_widget.setFixedSize( + 300, + int(4 * constants.HEADER_HEIGHT), + ) + new_preview_widget.setStyleSheet( + "background:transparent; color:white; border:1px dotted white;" ) - self.input_widget.textChanged.connect(self.update_attribute_from_widget) + if self.logic_node.get_attribute_value(attr_name): + new_preview_widget.setPlaceholderText( + self.logic_node.get_attribute_value(attr_name) + ) + else: + new_preview_widget.setPlaceholderText(f"[{attr_name}]") + + self.preview_widgets.append(new_preview_widget) + + elif gui_preview_type == constants.PreviewsGUI.IMAGE_PREVIEW: + new_preview_widget = QtWidgets.QLabel(parent=None) + new_preview_widget.setObjectName(attr_name) + new_preview_widget.setToolTip(f"Internal attribute: {attr_name}") + new_preview_widget.setFixedSize( + constants.STRIPE_HEIGHT * 10, + constants.STRIPE_HEIGHT * 10, + ) + new_preview_widget.setAlignment( + QtCore.Qt.AlignVCenter and QtCore.Qt.AlignCenter + ) + new_preview_widget.setStyleSheet( + "background:transparent; color:white; border:none;" + ) + if self.logic_node.get_attribute_value(attr_name): + pass # TODO + else: + new_preview_widget.setText(f"[{attr_name}]") - # Set font of the widget - self.input_widget.setFont( - QtGui.QFont(constants.NODE_FONT, constants.HEADER_HEIGHT * 0.4) - ) + self.preview_widgets.append(new_preview_widget) # Measure - self.extra_header = self.input_widget.height() + 5 + self.extra_bottom = sum([w.height() for w in self.preview_widgets]) def setup_extras(self): """ @@ -531,11 +739,13 @@ def setup_extras(self): ctx_icon.setSharedRenderer(self.extras_renderer) ctx_icon.setPos(0, -25) ctx_icon.setElementId("context") + elif "InputFromCtx" in self.logic_node.class_name: ctx_icon = QtSvg.QGraphicsSvgItem(parentItem=self) ctx_icon.setSharedRenderer(self.extras_renderer) ctx_icon.setPos(0, -25) ctx_icon.setElementId("in") + elif "OutputToCtx" in self.logic_node.class_name: ctx_icon = QtSvg.QGraphicsSvgItem(parentItem=self) ctx_icon.setSharedRenderer(self.extras_renderer) @@ -551,7 +761,8 @@ def reset(self): self.badge_icon.hide() self.error_marquee.hide() self.additional_info_text.hide() - self.update_attribute_from_widget() + self.update_attributes_from_widgets() + self.clear_previews() def show_result(self): """ @@ -609,70 +820,109 @@ def show_result(self): ) self.badge_icon.setToolTip(html_text) + if constants.IN_SCREEN_ERRORS: self.additional_info_text.setHtml(html_text) self.additional_info_text.show() - # CHANGE ATTRIBUTES ---------------------- - def update_attribute_from_widget(self): - if self.input_datatype == "str": - text = self.proxy_input_widget.widget().text() - if text: - self.logic_node.set_special_attr_value("out_str", text) - else: - self.logic_node.clear_special_attr_value("out_str") - elif self.input_datatype == "multiline_str": - text = self.proxy_input_widget.widget().toPlainText() - if text: - self.logic_node.set_special_attr_value("out_str", text) - else: - self.logic_node.clear_special_attr_value("out_str") - elif self.input_datatype == "dict": - text = self.proxy_input_widget.widget().toPlainText() - if text: - try: - eval_dict = ast.literal_eval(text) - if eval_dict and isinstance(eval_dict, dict): - self.logic_node.set_special_attr_value("out_dict", eval_dict) - except (ValueError, SyntaxError): - pass - else: - self.logic_node.clear_special_attr_value("out_dict") - elif self.input_datatype == "list": - text = self.proxy_input_widget.widget().text() - if text: - try: - eval_list = ast.literal_eval(text) - if eval_list and isinstance(eval_list, list): - self.logic_node.set_special_attr_value("out_list", eval_list) - except (ValueError, SyntaxError): - pass - else: - self.logic_node.clear_special_attr_value("out_list") - elif self.input_datatype == "tuple": - text = self.proxy_input_widget.widget().text() - if text: - try: - eval_tuple = ast.literal_eval(text) - if eval_tuple and isinstance(eval_tuple, tuple): - self.logic_node.set_special_attr_value("out_tuple", eval_list) - except (ValueError, SyntaxError): - pass - else: - self.logic_node.clear_special_attr_value() - elif self.input_datatype == "bool": - checked = self.proxy_input_widget.widget().isChecked() - self.logic_node.set_special_attr_value("out_bool", checked) - elif self.input_datatype == "int": - val = self.proxy_input_widget.widget().value() - self.logic_node.set_special_attr_value("out_int", val) - elif self.input_datatype == "float": - val = self.proxy_input_widget.widget().value() - self.logic_node.set_special_attr_value("out_float", val) - self.logic_node.set_special_attr_value("out_int", int(val)) - elif self.input_datatype == "option": - val = self.proxy_input_widget.widget().currentText() - self.logic_node.set_special_attr_value("out_str", val) + # Previews + if self.logic_node.success == constants.SUCCESSFUL: + self.update_previews_from_attributes() + + # CHANGE ATTRIBUTES FROM INPUT WIDGETS ---------------------- + def update_attributes_from_widgets(self, *args): + gui_internal_attr_name = args[0] if args else None + for w in self.input_widgets: + if ( + w.objectName() == gui_internal_attr_name + or gui_internal_attr_name is None + ): + value = None + + if isinstance(w, QtWidgets.QLineEdit): + value = w.text() or None + if value is None: + self.logic_node[w.objectName()].clear() + continue + self.logic_node.set_attribute_from_str(w.objectName(), value) + elif isinstance(w, QtWidgets.QPlainTextEdit): + value = w.toPlainText() or None + if value is None: + self.logic_node[w.objectName()].clear() + continue + self.logic_node.set_attribute_from_str(w.objectName(), value) + elif isinstance(w, QtWidgets.QCheckBox): + value = w.isChecked() + self.logic_node[w.objectName()].set_value(value) + elif isinstance(w, QtWidgets.QSpinBox): + value = w.value() + self.logic_node[w.objectName()].set_value(value) + elif isinstance(w, QtWidgets.QDoubleSpinBox): + value = w.value() + self.logic_node[w.objectName()].set_value(value) + elif isinstance(w, QtWidgets.QComboBox): + value = w.currentText() + self.logic_node[w.objectName()].set_value(value) + + if self.scene(): + GS.attribute_editor_refresh_node_requested.emit(self.logic_node.uuid) + + # SHOW PREVIEWS ---------------------- + def update_previews_from_attributes(self): + for w in self.preview_widgets: + if not w.objectName(): + continue + + value = self.logic_node.get_attribute_value(w.objectName()) + + if isinstance(w, QtWidgets.QLineEdit): + w.setText(value) + elif isinstance(w, QtWidgets.QPlainTextEdit): + w.setPlainText(value) + elif isinstance(w, QtWidgets.QCheckBox): + w.setChecked(value) + elif isinstance(w, QtWidgets.QSpinBox): + w.setValue(value) + elif isinstance(w, QtWidgets.QDoubleSpinBox): + w.setValue(value) + elif isinstance(w, QtWidgets.QComboBox): + w.setCurrentText(value) + elif isinstance(w, QtWidgets.QLabel): + im2 = value.convert("RGB") + data = im2.tobytes("raw", "RGB") + image = QtGui.QImage( + data, im2.width, im2.height, QtGui.QImage.Format_RGB888 + ) + pix = QtGui.QPixmap.fromImage(image) + w.setPixmap( + pix.scaled(w.width(), w.height(), QtCore.Qt.KeepAspectRatio) + ) + + if self.scene(): + GS.attribute_editor_refresh_node_requested.emit(self.logic_node.uuid) + + def clear_previews(self): + for w in self.preview_widgets: + if not w.objectName(): + continue + + self.logic_node[w.objectName()].clear() + + if isinstance(w, QtWidgets.QLineEdit): + w.setText(f"[{w.objectName()}]") + elif isinstance(w, QtWidgets.QPlainTextEdit): + w.setPlainText(f"[{w.objectName()}]") + elif isinstance(w, QtWidgets.QCheckBox): + w.setChecked(False) + elif isinstance(w, QtWidgets.QSpinBox): + w.setValue(0) + elif isinstance(w, QtWidgets.QDoubleSpinBox): + w.setValue(0.0) + elif isinstance(w, QtWidgets.QComboBox): + w.setCurrentIndex(0) + elif isinstance(w, QtWidgets.QLabel): + w.clear() + w.setText(f"[{w.objectName()}]") if self.scene(): GS.attribute_editor_refresh_node_requested.emit(self.logic_node.uuid) diff --git a/graphic/widgets/class_searcher.py b/graphic/widgets/class_searcher.py index 5bb3808..5305581 100644 --- a/graphic/widgets/class_searcher.py +++ b/graphic/widgets/class_searcher.py @@ -36,7 +36,7 @@ def __init__(self, *args, **kwargs): def make_connections(self): self.search_bar.textChanged.connect(self.filter_classes) - self.class_list.itemDoubleClicked.connect(self.send_signal) + self.class_list.itemDoubleClicked.connect(self.send_node_creation_signal) def reset(self): self.search_bar.clear() @@ -61,10 +61,9 @@ def keyPressEvent(self, event: QtWidgets.QWidget.event): modifiers = QtWidgets.QApplication.keyboardModifiers() if event.key() == QtCore.Qt.Key_Return and not modifiers: - self.send_signal() + self.send_node_creation_signal() - def send_signal(self): - # TODO rename this one + def send_node_creation_signal(self): if self.class_list.selectedItems(): GS.node_creation_requested.emit( self.pos(), diff --git a/graphic/widgets/main_window.py b/graphic/widgets/main_window.py index d4e37b1..8bce790 100644 --- a/graphic/widgets/main_window.py +++ b/graphic/widgets/main_window.py @@ -131,7 +131,7 @@ def add_scenes_recursive(entries_dict: dict, menu: QtWidgets.QMenu): """ menu.setToolTipsVisible(True) for key in entries_dict: - nice_name = key.capitalize().replace("_", " ") + nice_name = key.title().replace("_", " ") libs_menu = menu.addMenu(nice_name) libs_menu.setIcon(QtGui.QIcon("icons:folder.png")) scenes_list = entries_dict[key] @@ -141,7 +141,7 @@ def add_scenes_recursive(entries_dict: dict, menu: QtWidgets.QMenu): for elem in scenes_list: if isinstance(elem, tuple): scene_name, full_path = elem - nice_name = scene_name.capitalize().replace("_", " ") + nice_name = scene_name.title().replace("_", " ") ac = QtWidgets.QAction(nice_name, parent=menu) ac.setToolTip(full_path) ac.triggered.connect(partial(self.load_scene, full_path)) @@ -189,10 +189,10 @@ def populate_tree(self): for m in sorted(all_classes): node_lib_path = all_classes[m]["node_lib_path"] node_lib_name = all_classes[m]["node_lib_name"] - node_lib_nice_name = node_lib_name.capitalize().replace("_", " ") + node_lib_nice_name = node_lib_name.title().replace("_", " ") module_filename = all_classes[m]["module_filename"] module_full_path = all_classes[m]["module_full_path"] - module_nice_name = m.capitalize().replace("_", " ") + module_nice_name = m.title().replace("_", " ") color = all_classes[m].get("color", constants.DEFAULT_NODE_COLOR) if node_lib_name not in top_level_items: @@ -217,7 +217,9 @@ def populate_tree(self): ), ) class_item.setData(0, QtCore.Qt.UserRole, name) - if cls.NICE_NAME: + if ( + hasattr(cls, "NICE_NAME") and cls.NICE_NAME + ): # TODO inheritance not working here? class_item.setText(0, cls.NICE_NAME) icon = QtGui.QIcon(cls.ICON_PATH) @@ -246,6 +248,8 @@ def populate_tree(self): node_class = node_file_item.child(i) node_class.setFont(0, QtGui.QFont("arial", 14)) + self.ui.nodes_tree.sortByColumn(0, QtCore.Qt.AscendingOrder) + def filter_nodes_by_name(self): """ Set node classes as hidden/visible based on user input. diff --git a/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx b/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx index 94a18e3..ca973af 100644 --- a/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx +++ b/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx @@ -6,32 +6,32 @@ nodes: - CreateTempFile_1: class_name: CreateTempFile - x_pos: -979 - y_pos: -150 + x_pos: -1014 + y_pos: -135 - DictToYaml_1: class_name: DictToYaml x_pos: -637 y_pos: -333 - GetEntireEnviron_1: class_name: GetEntireEnviron - x_pos: -1028 - y_pos: -314 + x_pos: -1149 + y_pos: -367 - SetStrOutputToCtx_1: class_name: SetStrOutputToCtx x_pos: -176 y_pos: -27 -- StrInput_3: +- StrInput_2: class_name: StrInput node_attributes: - out_str: yaml_filepath - x_pos: -532 - y_pos: -152 -- TextFileExtensionSelect_1: + internal_str: yaml_filepath + x_pos: -846 + y_pos: 175 +- TextFileExtensionSelect_2: class_name: TextFileExtensionSelect node_attributes: - out_str: .yml - x_pos: -1280 - y_pos: -139 + internal_str: .yml + x_pos: -1441 + y_pos: -55 # Connections section: connections to be done between nodes connections: @@ -39,9 +39,9 @@ connections: - CreateTempFile_1.tempfile_path -> SetStrOutputToCtx_1.out_parent_attr_value - DictToYaml_1.COMPLETED -> SetStrOutputToCtx_1.START - GetEntireEnviron_1.environ_dict -> DictToYaml_1.in_dict -- StrInput_3.out_str -> SetStrOutputToCtx_1.out_parent_attr_name -- TextFileExtensionSelect_1.out_str -> CreateTempFile_1.suffix +- StrInput_2.out_str -> SetStrOutputToCtx_1.out_parent_attr_name +- TextFileExtensionSelect_2.out_str -> CreateTempFile_1.suffix -# Context created at: 2023-01-24 15:18:22.210039 -# Created by: jaime.rvq \ No newline at end of file +# Context modified at: 2024-04-12 13:39:22.998043 +# Modified by: jaime.rvq \ No newline at end of file diff --git a/lib/base_node_lib/nodes_general_library/datetime_nodes.py b/lib/base_node_lib/nodes_general_library/datetime_nodes.py index b3ab147..f850563 100644 --- a/lib/base_node_lib/nodes_general_library/datetime_nodes.py +++ b/lib/base_node_lib/nodes_general_library/datetime_nodes.py @@ -6,22 +6,19 @@ import datetime +from all_nodes.constants import InputsGUI from all_nodes.logic.logic_node import GeneralLogicNode -from all_nodes.logic.logic_node import OptionInput from all_nodes import utils LOGGER = utils.get_logger(__name__) -class DatetimeNow(GeneralLogicNode): - NICE_NAME = "Datetime now" - HELP = "Get a datetime object as of right now" - - OUTPUTS_DICT = {"datetime_object": {"type": datetime.datetime}} - - def run(self): - self.set_output("datetime_object", datetime.datetime.now()) +DATE_FORMATS = [ + "%d/%m/%Y, %H:%M:%S", # European + "%m/%d/%Y, %H:%M:%S", # American + "%Y.%m.%d_%H.%M.%S", # Technical +] class StrfDatetime(GeneralLogicNode): @@ -41,10 +38,47 @@ def run(self): ) -class DatetimeFormatsSelect(OptionInput): - INPUT_OPTIONS = [ - "%d/%m/%Y, %H:%M:%S", # European - "%m/%d/%Y, %H:%M:%S", # American - "%Y.%m.%d_%H.%M.%S", # Technical - ] +class DatetimeFormatsSelect(GeneralLogicNode): NICE_NAME = "Datetime formats" + + INTERNALS_DICT = { + "internal_datetime_format_str": { + "type": str, + "gui_type": InputsGUI.OPTION_INPUT, + "options": DATE_FORMATS, + }, + } + + OUTPUTS_DICT = {"out_datetime_format_str": {"type": str}} + + def run(self): + self.set_output( + "out_datetime_format_str", + self.get_attribute_value("internal_datetime_format_str"), + ) + + +class DatetimeNow(GeneralLogicNode): + NICE_NAME = "Datetime now" + HELP = "Get a datetime object as of right now, as well as a formatted string" + + INTERNALS_DICT = { + "internal_datetime_str": { + "type": str, + "gui_type": InputsGUI.OPTION_INPUT, + "options": DATE_FORMATS, + }, + } + + OUTPUTS_DICT = { + "datetime_object": {"type": datetime.datetime}, + "out_datetime_str": {"type": str}, + } + + def run(self): + datetime_object = datetime.datetime.now() + datetime_formatting = self.get_attribute_value("internal_datetime_str") + self.set_output( + "out_datetime_str", datetime_object.strftime(datetime_formatting) + ) + self.set_output("datetime_object", datetime_object) diff --git a/lib/base_node_lib/nodes_general_library/debug_nodes.py b/lib/base_node_lib/nodes_general_library/debug_nodes.py index 8af4a25..3a96b68 100644 --- a/lib/base_node_lib/nodes_general_library/debug_nodes.py +++ b/lib/base_node_lib/nodes_general_library/debug_nodes.py @@ -6,6 +6,7 @@ import time +from all_nodes.constants import PreviewsGUI from all_nodes.logic.logic_node import GeneralLogicNode from all_nodes import utils @@ -78,6 +79,10 @@ class IntAddition(GeneralLogicNode): OUTPUTS_DICT = {"out_total": {"type": int}} + INTERNALS_DICT = { + "internal_str": {"type": str, "gui_type": PreviewsGUI.STR_PREVIEW}, + } + def run(self): int_0 = self.get_attribute_value("in_int_0") int_1 = self.get_attribute_value("in_int_1") @@ -88,3 +93,35 @@ def run(self): total = int_0 + int_1 + (int_2 or 0) + (int_3 or 0) + (int_4 or 0) self.set_output("out_total", total) + self.set_attribute_value("internal_str", f"Total: {total}") + + +class StringPreview(GeneralLogicNode): + INPUTS_DICT = { + "in_str": {"type": str}, + } + + INTERNALS_DICT = { + "internal_str": {"type": str, "gui_type": PreviewsGUI.STR_PREVIEW}, + } + + def run(self): + self.set_attribute_value("internal_str", self.get_attribute_value("in_str")) + + +class MultilineStringPreview(GeneralLogicNode): + INPUTS_DICT = { + "in_str": {"type": str}, + } + + INTERNALS_DICT = { + "internal_multiline_str": { + "type": str, + "gui_type": PreviewsGUI.MULTILINE_STR_PREVIEW, + }, + } + + def run(self): + self.set_attribute_value( + "internal_multiline_str", self.get_attribute_value("in_str") + ) diff --git a/lib/base_node_lib/nodes_general_library/file_extension_nodes.py b/lib/base_node_lib/nodes_general_library/file_extension_nodes.py index ac92679..1958bea 100644 --- a/lib/base_node_lib/nodes_general_library/file_extension_nodes.py +++ b/lib/base_node_lib/nodes_general_library/file_extension_nodes.py @@ -5,18 +5,43 @@ __license__ = "MIT License" -from all_nodes.logic.logic_node import OptionInput +from all_nodes.constants import InputsGUI +from all_nodes.logic.logic_node import GeneralLogicNode from all_nodes import utils LOGGER = utils.get_logger(__name__) -class ImageFileExtensionSelect(OptionInput): - INPUT_OPTIONS = [".png", ".exr", ".jpg"] +class ImageFileExtensionSelect(GeneralLogicNode): NICE_NAME = "Image file extension select" + INTERNALS_DICT = { + "internal_str": { + "type": str, + "gui_type": InputsGUI.OPTION_INPUT, + "options": [".png", ".exr", ".jpg"], + }, + } -class TextFileExtensionSelect(OptionInput): - INPUT_OPTIONS = [".txt", ".json", ".yml"] + OUTPUTS_DICT = {"out_str": {"type": str}} + + def run(self): + self.set_output("out_str", self.get_attribute_value("internal_str")) + + +class TextFileExtensionSelect(GeneralLogicNode): NICE_NAME = "Text file extension select" + + INTERNALS_DICT = { + "internal_str": { + "type": str, + "gui_type": InputsGUI.OPTION_INPUT, + "options": [".txt", ".json", ".yml"], + }, + } + + OUTPUTS_DICT = {"out_str": {"type": str}} + + def run(self): + self.set_output("out_str", self.get_attribute_value("internal_str")) diff --git a/lib/base_node_lib/nodes_general_library/general_input_nodes.py b/lib/base_node_lib/nodes_general_library/general_input_nodes.py new file mode 100644 index 0000000..2146dab --- /dev/null +++ b/lib/base_node_lib/nodes_general_library/general_input_nodes.py @@ -0,0 +1,152 @@ +# -*- coding: UTF-8 -*- +__author__ = "Jaime Rivera " +__copyright__ = "Copyright 2022, Jaime Rivera" +__credits__ = [] +__license__ = "MIT License" + + +from all_nodes.constants import InputsGUI +from all_nodes.logic.logic_node import GeneralLogicNode +from all_nodes import utils + + +LOGGER = utils.get_logger(__name__) + + +class StrInput(GeneralLogicNode): + NICE_NAME = "String input" + + OUTPUTS_DICT = {"out_str": {"type": str}} + + INTERNALS_DICT = { + "internal_str": {"type": str, "gui_type": InputsGUI.STR_INPUT}, + } + + def run(self): + self.set_output("out_str", self.get_attribute_value("internal_str")) + + +class MultilineStrInput(GeneralLogicNode): + NICE_NAME = "Multiline string input" + HELP = "" + + OUTPUTS_DICT = {"out_str": {"type": str}} + + INTERNALS_DICT = { + "internal_str": {"type": str, "gui_type": InputsGUI.MULTILINE_STR_INPUT}, + } + + def run(self): + self.set_output("out_str", self.get_attribute_value("internal_str")) + + +class BoolInput(GeneralLogicNode): + NICE_NAME = "Boolean input" + HELP = "" + + OUTPUTS_DICT = {"out_bool": {"type": bool}} + + INTERNALS_DICT = { + "internal_bool": {"type": bool, "gui_type": InputsGUI.BOOL_INPUT}, + } + + def run(self): + self.set_output("out_bool", self.get_attribute_value("internal_bool")) + + +class DictInput(GeneralLogicNode): + NICE_NAME = "Dictionary input" + HELP = "" + + OUTPUTS_DICT = {"out_dict": {"type": dict}} + + INTERNALS_DICT = { + "internal_dict": {"type": dict, "gui_type": InputsGUI.DICT_INPUT}, + } + + def run(self): + internal_dict = self.get_attribute_value("internal_dict") + if internal_dict is None: + self.fail( + "Looks like the dictionary was not properly formed! (Check the syntax)" + ) + self.set_output("out_dict", self.get_attribute_value("internal_dict")) + + +class ListInput(GeneralLogicNode): + NICE_NAME = "List input" + HELP = "" + + OUTPUTS_DICT = {"out_list": {"type": list}} + + INTERNALS_DICT = { + "internal_list": {"type": list, "gui_type": InputsGUI.LIST_INPUT}, + } + + def run(self): + internal_list = self.get_attribute_value("internal_list") + if internal_list is None: + self.fail("Looks like the list was not properly formed! (Check the syntax)") + self.set_output("out_list", self.get_attribute_value("internal_list")) + + +class TupleInput(GeneralLogicNode): + NICE_NAME = "Tuple input" + HELP = "" + + OUTPUTS_DICT = {"out_tuple": {"type": tuple}} + + INTERNALS_DICT = { + "internal_tuple": {"type": tuple, "gui_type": InputsGUI.TUPLE_INPUT}, + } + + def run(self): + internal_tuple = self.get_attribute_value("internal_tuple") + if internal_tuple is None: + self.fail( + "Looks like the tuple was not properly formed! (Check the syntax)" + ) + self.set_output("out_tuple", self.get_attribute_value("internal_tuple")) + + +class IntInput(GeneralLogicNode): + NICE_NAME = "Integer input" + HELP = "" + + OUTPUTS_DICT = {"out_int": {"type": int}} + + INTERNALS_DICT = { + "internal_int": {"type": int, "gui_type": InputsGUI.INT_INPUT}, + } + + def run(self): + self.set_output("out_int", self.get_attribute_value("internal_int")) + + +class FloatInput(GeneralLogicNode): + NICE_NAME = "Float input" + HELP = "" + + OUTPUTS_DICT = {"out_float": {"type": float}} + + INTERNALS_DICT = { + "internal_float": {"type": float, "gui_type": InputsGUI.FLOAT_INPUT}, + } + + def run(self): + self.set_output("out_float", self.get_attribute_value("internal_float")) + + +class OptionInput(GeneralLogicNode): + OUTPUTS_DICT = {"out_str": {"type": str}} + + INTERNALS_DICT = { + "internal_str": { + "type": str, + "gui_type": InputsGUI.OPTION_INPUT, + "options": ["1", "2", "3"], + }, + } + + def run(self): + self.set_output("out_str", self.get_attribute_value("internal_str")) diff --git a/lib/base_node_lib/nodes_general_library/special_input_nodes.py b/lib/base_node_lib/nodes_general_library/special_input_nodes.py deleted file mode 100644 index 0fbddb4..0000000 --- a/lib/base_node_lib/nodes_general_library/special_input_nodes.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: UTF-8 -*- -__author__ = "Jaime Rivera " -__copyright__ = "Copyright 2022, Jaime Rivera" -__credits__ = [] -__license__ = "MIT License" - - -from all_nodes.logic.logic_node import SpecialInputNode -from all_nodes import utils - - -LOGGER = utils.get_logger(__name__) - - -class StrInput(SpecialInputNode): - INPUT_TYPE = "str" - - NICE_NAME = "String input" - HELP = "" - - OUTPUTS_DICT = {"out_str": {"type": str}} - - -class MultilineStrInput(SpecialInputNode): - INPUT_TYPE = "multiline_str" - - NICE_NAME = "Multiline string input" - HELP = "" - - OUTPUTS_DICT = {"out_str": {"type": str}} - - -class BoolInput(SpecialInputNode): - INPUT_TYPE = "bool" - - NICE_NAME = "Boolean input" - HELP = "" - - OUTPUTS_DICT = {"out_bool": {"type": bool}} - - -class DictInput(SpecialInputNode): - INPUT_TYPE = "dict" - - NICE_NAME = "Dictionary input" - HELP = "" - - OUTPUTS_DICT = {"out_dict": {"type": dict}} - - -class ListInput(SpecialInputNode): - INPUT_TYPE = "list" - - NICE_NAME = "List input" - HELP = "" - - OUTPUTS_DICT = {"out_list": {"type": list}} - - -class TupleInput(SpecialInputNode): - INPUT_TYPE = "tuple" - - NICE_NAME = "Tuple input" - HELP = "" - - OUTPUTS_DICT = {"out_tuple": {"type": tuple}} - - -class IntInput(SpecialInputNode): - INPUT_TYPE = "int" - - NICE_NAME = "Integer input" - HELP = "" - - OUTPUTS_DICT = {"out_int": {"type": int}} - - -class FloatInput(SpecialInputNode): - INPUT_TYPE = "float" - - NICE_NAME = "Float input" - HELP = "" - - OUTPUTS_DICT = {"out_float": {"type": float}, "out_int": {"type": int}} diff --git a/lib/base_node_lib/pillow_imaging/__init__.py b/lib/base_node_lib/pillow_imaging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/base_node_lib/pillow_imaging/icons/image.png b/lib/base_node_lib/pillow_imaging/icons/image.png new file mode 100644 index 0000000..70d92f3 Binary files /dev/null and b/lib/base_node_lib/pillow_imaging/icons/image.png differ diff --git a/lib/base_node_lib/pillow_imaging/pillow_general_nodes.py b/lib/base_node_lib/pillow_imaging/pillow_general_nodes.py new file mode 100644 index 0000000..cd17e15 --- /dev/null +++ b/lib/base_node_lib/pillow_imaging/pillow_general_nodes.py @@ -0,0 +1,89 @@ +# -*- coding: UTF-8 -*- +__author__ = "Jaime Rivera " +__copyright__ = "Copyright 2022, Jaime Rivera" +__credits__ = [] +__license__ = "MIT License" + + +import PIL + +from all_nodes.constants import InputsGUI, PreviewsGUI +from all_nodes.logic.logic_node import GeneralLogicNode +from all_nodes import utils + + +LOGGER = utils.get_logger(__name__) + + +class PIL_ImageOpen(GeneralLogicNode): + INPUTS_DICT = {"in_path": {"type": str}} + OUTPUTS_DICT = {"out_image": {"type": PIL.Image.Image}} + + def run(self): + self.set_output( + "out_image", + PIL.Image.open(self.get_attribute_value("in_path")), + ) + + +class PIL_ImagePreview(GeneralLogicNode): + INPUTS_DICT = {"in_image": {"type": PIL.Image.Image}} + + INTERNALS_DICT = { + "internal_image": { + "type": PIL.Image.Image, + "gui_type": PreviewsGUI.IMAGE_PREVIEW, + } + } + + def run(self): + self.set_attribute_value("internal_image", self.get_attribute_value("in_image")) + + +class PIL_DirectOpen(GeneralLogicNode): + OUTPUTS_DICT = { + "out_image": {"type": PIL.Image.Image}, + "out_image_path": {"type": str}, + } + + INTERNALS_DICT = { + "internal_str_image_path": { + "type": str, + "gui_type": InputsGUI.STR_INPUT, + }, + "internal_image": { + "type": PIL.Image.Image, + "gui_type": PreviewsGUI.IMAGE_PREVIEW, + }, + } + + def run(self): + # Get inputs + path = self.get_attribute_value("internal_str_image_path") + opened_image = PIL.Image.open(path) + + # Set outputs + self.set_output( + "out_image", + opened_image, + ) + self.set_output( + "out_image_path", + path, + ) + + # Display previews + self.set_attribute_value("internal_image", opened_image) + + +class PIL_OpenFromUrl(GeneralLogicNode): + INPUTS_DICT = {"in_url": {"type": str}} + + OUTPUTS_DICT = {"out_image": {"type": PIL.Image.Image}} + + def run(self): + import requests + + data = requests.get(self.get_attribute_value("in_url"), stream=True).raw + img = PIL.Image.open(data) + self.set_output("out_image", img) diff --git a/lib/base_node_lib/pillow_imaging/styles.yml b/lib/base_node_lib/pillow_imaging/styles.yml new file mode 100644 index 0000000..9a751b5 --- /dev/null +++ b/lib/base_node_lib/pillow_imaging/styles.yml @@ -0,0 +1,5 @@ +## Registry of the styles (colors and icons) for this lib + +pillow_general_nodes: + color: '#007513' + default_icon: image diff --git a/lib/example_scene_lib/debug_scenes/datetime_example.yml b/lib/example_scene_lib/datetime_example.yml similarity index 55% rename from lib/example_scene_lib/debug_scenes/datetime_example.yml rename to lib/example_scene_lib/datetime_example.yml index c45015f..b6337cf 100644 --- a/lib/example_scene_lib/debug_scenes/datetime_example.yml +++ b/lib/example_scene_lib/datetime_example.yml @@ -7,28 +7,35 @@ nodes: - DatetimeFormatsSelect_1: class_name: DatetimeFormatsSelect node_attributes: - out_str: '%Y.%m.%d_%H.%M.%S' - x_pos: -1948 - y_pos: -428 + internal_datetime_format_str: '%d/%m/%Y, %H:%M:%S' + x_pos: -2039 + y_pos: -396 - DatetimeNow_1: class_name: DatetimeNow + node_attributes: + internal_datetime_str: '%d/%m/%Y, %H:%M:%S' x_pos: -2039 y_pos: -570 - PrintToConsole_1: class_name: PrintToConsole - x_pos: -1119 - y_pos: -545 + x_pos: -1083 + y_pos: -617 - StrfDatetime_1: class_name: StrfDatetime x_pos: -1616 y_pos: -528 +- StringPreview_1: + class_name: StringPreview + x_pos: -1084 + y_pos: -389 # Connections section: connections to be done between nodes connections: -- DatetimeFormatsSelect_1.out_str -> StrfDatetime_1.datetime_formatting +- DatetimeFormatsSelect_1.out_datetime_format_str -> StrfDatetime_1.datetime_formatting - DatetimeNow_1.datetime_object -> StrfDatetime_1.datetime_object - StrfDatetime_1.datetime_formatted -> PrintToConsole_1.in_object_0 +- StrfDatetime_1.datetime_formatted -> StringPreview_1.in_str -# Scene modified at: 2024-01-22 01:31:41.529472 -# Modified by: Jaime \ No newline at end of file +# Scene modified at: 2024-04-26 15:23:52.012754 +# Modified by: jaime.rvq \ No newline at end of file diff --git a/lib/example_scene_lib/debug_scenes/env_variables.yml b/lib/example_scene_lib/env_variables.yml similarity index 73% rename from lib/example_scene_lib/debug_scenes/env_variables.yml rename to lib/example_scene_lib/env_variables.yml index e41fffc..b966539 100644 --- a/lib/example_scene_lib/debug_scenes/env_variables.yml +++ b/lib/example_scene_lib/env_variables.yml @@ -1,17 +1,9 @@ # SCENE env_variables # ------------------- -# Description: Example of getting env vars with fallback values +# Description: # Nodes section: overall list of nodes to be created nodes: -- VAR_1: - class_name: GetEnvVariable - x_pos: -1032 - y_pos: -318 -- VAR_2: - class_name: GetEnvVariable - x_pos: -1021 - y_pos: -56 - PrintToConsole_1: class_name: PrintToConsole x_pos: -556 @@ -19,30 +11,38 @@ nodes: - StrInput_1: class_name: StrInput node_attributes: - out_str: PROJECT - x_pos: -1353 - y_pos: -304 + internal_str: PROJECT + x_pos: -1410 + y_pos: -361 - StrInput_2: class_name: StrInput node_attributes: - out_str: NO_PROJECT - x_pos: -1353 - y_pos: -204 + internal_str: NO_PROJECT + x_pos: -1406 + y_pos: -196 - StrInput_3: class_name: StrInput node_attributes: - out_str: USERNAME - x_pos: -1353 - y_pos: -4 + internal_str: USERNAME + x_pos: -1409 + y_pos: -7 +- VAR_1: + class_name: GetEnvVariable + x_pos: -1032 + y_pos: -318 +- VAR_2: + class_name: GetEnvVariable + x_pos: -1021 + y_pos: -56 # Connections section: connections to be done between nodes connections: -- VAR_1.env_variable_value -> PrintToConsole_1.in_object_0 -- VAR_2.env_variable_value -> PrintToConsole_1.in_object_1 - StrInput_1.out_str -> VAR_1.env_variable_name - StrInput_2.out_str -> VAR_1.fallback_value - StrInput_3.out_str -> VAR_2.env_variable_name +- VAR_1.env_variable_value -> PrintToConsole_1.in_object_0 +- VAR_2.env_variable_value -> PrintToConsole_1.in_object_1 -# Scene created at: 2023-01-15 17:37:59.201005 -# Created by: jaime.rvq \ No newline at end of file +# Scene modified at: 2024-04-17 12:59:33.053113 +# Modified by: jaime.rvq \ No newline at end of file diff --git a/lib/example_scene_lib/environ_to_yaml.yml b/lib/example_scene_lib/environ_to_yaml.yml index d251825..5750dee 100644 --- a/lib/example_scene_lib/environ_to_yaml.yml +++ b/lib/example_scene_lib/environ_to_yaml.yml @@ -10,31 +10,38 @@ nodes: y_pos: -38 - EnvironToYmlCtx_1: class_name: EnvironToYmlCtx - x_pos: -906 - y_pos: -306 + x_pos: -1528 + y_pos: -278 - OPEN_FILE: class_name: BoolInput node_attributes: - out_bool: false - x_pos: -914 - y_pos: -28 + internal_bool: false + x_pos: -1279 + y_pos: -70 - PrintToConsole_1: class_name: PrintToConsole - x_pos: -277 - y_pos: -120 + x_pos: -128 + y_pos: -117 - StartFile_1: class_name: StartFile - x_pos: -312 - y_pos: -302 + x_pos: -126 + y_pos: -265 +- StringPreview_1: + class_name: StringPreview + x_pos: -1076 + y_pos: -406 # Connections section: connections to be done between nodes connections: - BasicIf_1.path_1 -> StartFile_1.START - BasicIf_1.path_2 -> PrintToConsole_1.START +- EnvironToYmlCtx_1.COMPLETED -> StringPreview_1.START - EnvironToYmlCtx_1.yaml_filepath -> PrintToConsole_1.in_object_0 - EnvironToYmlCtx_1.yaml_filepath -> StartFile_1.file_path +- EnvironToYmlCtx_1.yaml_filepath -> StringPreview_1.in_str - OPEN_FILE.out_bool -> BasicIf_1.in_bool +- StringPreview_1.COMPLETED -> BasicIf_1.START -# Scene modified at: 2024-01-22 01:30:06.284002 -# Created by: Jaime \ No newline at end of file +# Scene modified at: 2024-04-24 15:57:36.743853 +# Modified by: jaime.rvq \ No newline at end of file diff --git a/lib/example_scene_lib/debug_scenes/fail_scene.yml b/lib/example_scene_lib/fail_scene.yml similarity index 100% rename from lib/example_scene_lib/debug_scenes/fail_scene.yml rename to lib/example_scene_lib/fail_scene.yml diff --git a/lib/example_scene_lib/pillow_url_image.yml b/lib/example_scene_lib/pillow_url_image.yml new file mode 100644 index 0000000..d3c4d43 --- /dev/null +++ b/lib/example_scene_lib/pillow_url_image.yml @@ -0,0 +1,29 @@ +# SCENE pillow_url_image +# ---------------------- +# Description: + +# Nodes section: overall list of nodes to be created +nodes: +- PIL_ImagePreview_1: + class_name: PIL_ImagePreview + x_pos: -1357 + y_pos: -440 +- PIL_OpenFromUrl_1: + class_name: PIL_OpenFromUrl + x_pos: -1754 + y_pos: -440 +- StrInput_1: + class_name: StrInput + node_attributes: + internal_str: https://birdsupplies.com/cdn/shop/articles/HORMONAL_BEHAVIOR_IN_PARROTS_HOW_TO_PET_A_PARROT.png + x_pos: -2126 + y_pos: -440 + +# Connections section: connections to be done between nodes +connections: +- PIL_OpenFromUrl_1.out_image -> PIL_ImagePreview_1.in_image +- StrInput_1.out_str -> PIL_OpenFromUrl_1.in_url + + +# Scene created at: 2024-04-16 10:53:38.282351 +# Created by: jaime.rvq \ No newline at end of file diff --git a/lib/example_scene_lib/debug_scenes/simple_regex.yml b/lib/example_scene_lib/simple_regex.yml similarity index 64% rename from lib/example_scene_lib/debug_scenes/simple_regex.yml rename to lib/example_scene_lib/simple_regex.yml index 22a9e73..1225718 100644 --- a/lib/example_scene_lib/debug_scenes/simple_regex.yml +++ b/lib/example_scene_lib/simple_regex.yml @@ -1,29 +1,29 @@ # SCENE simple_regex # ------------------ -# Description: Simple regex pattern match +# Description: # Nodes section: overall list of nodes to be created nodes: - PrintToConsole_1: class_name: PrintToConsole - x_pos: 129 - y_pos: -141 + x_pos: 217 + y_pos: -139 - RegexMatch_1: class_name: RegexMatch - x_pos: -155 - y_pos: -120 + x_pos: -145 + y_pos: -122 - StrInput_1: class_name: StrInput node_attributes: - out_str: TEST_123_TEST - x_pos: -438 - y_pos: -152 + internal_str: TEST_123_TEST + x_pos: -514 + y_pos: -158 - StrInput_2: class_name: StrInput node_attributes: - out_str: .+\d{3}.+$ - x_pos: -437 - y_pos: -35 + internal_str: .+\d{3}.+$ + x_pos: -510 + y_pos: -4 # Connections section: connections to be done between nodes connections: @@ -32,5 +32,5 @@ connections: - StrInput_2.out_str -> RegexMatch_1.pattern -# Scene created at: 2023-01-02 10:59:39.085681 -# Created by: jaime.rvq \ No newline at end of file +# Scene modified at: 2024-04-26 15:23:02.489478 +# Modified by: jaime.rvq \ No newline at end of file diff --git a/logic/class_registry.py b/logic/class_registry.py index 489589c..ecd8e20 100644 --- a/logic/class_registry.py +++ b/logic/class_registry.py @@ -23,10 +23,10 @@ # -------------------------------- NODE CLASSES -------------------------------- # CLASSES_TO_SKIP = [ + "InputsGUI", + "PreviewsGUI", "GeneralLogicNode", "Run", - "SpecialInputNode", - "OptionInput", "GrabInputFromCtx", "SetOutputToCtx", ] # Classes to skip when gathering all usable clases, populating widgets... @@ -68,20 +68,23 @@ def register_node_lib(full_path): ) loaded_module = importlib.util.module_from_spec(loaded_spec) loaded_spec.loader.exec_module(loaded_module) - class_memebers = inspect.getmembers(loaded_module, inspect.isclass) - if not class_memebers: + class_members = inspect.getmembers(loaded_module, inspect.isclass) + if not class_members: return classes_dict[module_name] = dict() module_classes = list() class_counter = 0 - for name, cls in class_memebers: + for name, cls in class_members: if name in CLASSES_TO_SKIP: continue - # Icon for this class # TODO Refactor this out default_icon = node_styles.get(module_name, dict()).get("default_icon") - icon_path = "icons:nodes.png" if not cls.IS_CONTEXT else "icons:cubes.png" + icon_path = "icons:nodes.png" + if ( + hasattr(cls, "IS_CONTEXT") and cls.IS_CONTEXT + ): # TODO inheritance not working here? + icon_path = "icons:cubes.png" if QtCore.QFile.exists(f"icons:{name}.png"): icon_path = f"icons:{name}.png" elif QtCore.QFile.exists(f"icons:{name}.svg"): diff --git a/logic/logic_node.py b/logic/logic_node.py index de427cd..1b6a1d5 100644 --- a/logic/logic_node.py +++ b/logic/logic_node.py @@ -47,14 +47,16 @@ class GeneralLogicNode: FILEPATH = "" - NICE_NAME = None + NICE_NAME = "" HELP = "" + IS_CONTEXT = False + CONTEXT_DEFINITION_FILE = "" + INPUTS_DICT = {} OUTPUTS_DICT = {} - IS_CONTEXT = False - CONTEXT_DEFINITION_FILE = None + INTERNALS_DICT = {} # Internal attrs not exposed (for GUI input / preview mostly) VALID_NAMING_PATTERN = "^[A-Z]+[a-zA-Z0-9_]*$" @@ -72,11 +74,11 @@ def __init__(self): # Context it belongs to self.context = None - # Is context + # Specific for contexts self.internal_scene = None self.build_internal() - # Run + # Execution self.success = constants.NOT_RUN self.fail_log = [] @@ -94,12 +96,12 @@ def __init__(self): # UTILITY ---------------------- @staticmethod - def name_is_valid(name): + def name_is_valid(name) -> bool: if re.match(GeneralLogicNode.VALID_NAMING_PATTERN, name): return True return False - def rename(self, new_name): + def rename(self, new_name: str) -> bool: if self.node_name == new_name: return True @@ -112,20 +114,21 @@ def rename(self, new_name): return False - def force_rename(self, new_name): + def force_rename(self, new_name: str) -> bool: LOGGER.debug( "Forcing renaming of node '{}' to '{}'".format(self.node_name, new_name) ) self.node_name = new_name return True - def get_max_in_or_out_count(self): + def get_max_in_or_out_count(self) -> int: + """Get the biggest number of either input attributes or output attributes""" return max(len(self.get_input_attrs()), len(self.get_output_attrs())) - def get_node_full_dict(self): + def get_node_full_dict(self) -> dict: out_dict = dict() - # Class namde, node name... + # Class name, node name... out_dict["class_name"] = self.class_name out_dict["node_name"] = self.node_name out_dict["full_name"] = self.full_name @@ -135,6 +138,11 @@ def get_node_full_dict(self): # Attributes out_dict["node_attributes"] = dict() for attr in self.all_attributes: + if self.INTERNALS_DICT.get(attr.attribute_name, {}).get("gui_type") in set( + constants.PreviewsGUI + ): + continue # No need to register preview attrs + out_dict["node_attributes"][attr.dot_name] = dict() out_dict["node_attributes"][attr.dot_name][ "attribute_name" @@ -178,7 +186,14 @@ def get_node_full_dict(self): return out_dict - def get_node_basic_dict(self): + def get_node_basic_dict(self) -> dict: + """Get a dict that represents the main data needed to 'rebuild' a node. + + It is retrieved as dict so it can also be serialized + + Returns: + dict: with node main data + """ out_dict = dict() out_dict[self.node_name] = dict() @@ -186,6 +201,11 @@ def get_node_basic_dict(self): out_dict[self.node_name]["node_attributes"] = dict() for attr in self.all_attributes: + if self.INTERNALS_DICT.get(attr.attribute_name, {}).get("gui_type") in set( + constants.PreviewsGUI + ): + continue # No need to register preview attrs + if ( attr.attribute_name not in [constants.START, constants.COMPLETED] and attr.value is not None @@ -193,6 +213,8 @@ def get_node_basic_dict(self): out_dict[self.node_name]["node_attributes"][ attr.attribute_name ] = attr.value + + # If no node attributes were registered, no need to keep this key if not out_dict[self.node_name]["node_attributes"]: out_dict[self.node_name].pop("node_attributes") @@ -203,6 +225,7 @@ def get_out_connections(self): for attr in self.get_output_attrs(): for connected in attr.connected_attributes: connections_list.append([attr.dot_name, connected.dot_name]) + return connections_list def out_connected_nodes(self): @@ -210,9 +233,15 @@ def out_connected_nodes(self): for attr in self.get_output_attrs(): for connected_attr in attr.connected_attributes: connected_nodes.add(connected_attr.parent_node) + return connected_nodes - def in_connected_nodes_recursive(self): + def in_connected_nodes_recursive(self) -> list: + """Recursively get the full 'chain' of all the nodes connected to this node's inputs + + Returns: + list: list of nodes + """ in_connected_nodes = list() in_immediate_nodes = set() @@ -227,10 +256,30 @@ def in_connected_nodes_recursive(self): return in_connected_nodes - def check_cycles(self, node_to_check): + def check_cycles(self, node_to_check: GeneralLogicNode) -> bool: in_connected_nodes = self.in_connected_nodes_recursive() return node_to_check in in_connected_nodes + def get_gui_internals_inputs(self) -> dict: + gui_internals_inputs = dict() + for attr_name in self.INTERNALS_DICT: + if self.INTERNALS_DICT[attr_name].get("gui_type") in set( + constants.InputsGUI + ): + gui_internals_inputs[attr_name] = self.INTERNALS_DICT[attr_name] + + return gui_internals_inputs + + def get_gui_internals_previews(self) -> dict: + gui_internals_previews = dict() + for attr_name in self.INTERNALS_DICT: + if self.INTERNALS_DICT[attr_name].get("gui_type") in set( + constants.PreviewsGUI + ): + gui_internals_previews[attr_name] = self.INTERNALS_DICT[attr_name] + + return gui_internals_previews + # PROPERTIES ---------------------- @property def full_name(self): @@ -257,13 +306,13 @@ def create_attributes(self): """ Populate this node with the attributes that have been defined. """ + # -------------- INPUTS -------------- # # Add a special "Run control" START attribute that will be in all nodes start_attr = GeneralLogicAttribute( self, constants.START, constants.INPUT, Run, is_optional=True ) self.all_attributes.append(start_attr) - # Add all in and out attributes that have been defined for input_attribute_name in self.INPUTS_DICT: in_attr = GeneralLogicAttribute( self, @@ -275,6 +324,8 @@ def create_attributes(self): ), ) self.all_attributes.append(in_attr) + + # -------------- OUTPUTS -------------- # for output_attribute_name in self.OUTPUTS_DICT: out_attr = GeneralLogicAttribute( self, @@ -293,6 +344,19 @@ def create_attributes(self): ) self.all_attributes.append(completed_attr) + # -------------- INTERNAL -------------- # + for internal_attribute_name in self.INTERNALS_DICT: + internal_attr = GeneralLogicAttribute( + self, + internal_attribute_name, + constants.INTERNAL, + self.INTERNALS_DICT[internal_attribute_name]["type"], + is_optional=self.INTERNALS_DICT[internal_attribute_name].get( + "optional", False + ), + ) + self.all_attributes.append(internal_attr) + def get_input_attrs(self): """ Get all the input attributes of the node. @@ -329,7 +393,9 @@ def set_attribute_value(self, attribute_name: str, value): """ if attribute_name not in self.all_attribute_names: raise RuntimeError( - "Error! No valid attribute {} in the node".format(attribute_name) + "Error! No valid attribute '{}' in the node {}".format( + attribute_name, self.node_name + ) ) for attribute in self.all_attributes: @@ -388,7 +454,9 @@ def set_output(self, attribute_name: str, value): def set_attribute_from_str(self, attribute_name: str, value_str: str): if attribute_name not in self.all_attribute_names: LOGGER.error( - "Error! No valid attribute {} in the node".format(attribute_name) + "Error! No valid attribute '{}' in the node {}".format( + attribute_name, self.node_name + ) ) return @@ -418,10 +486,12 @@ def set_attribute_from_str(self, attribute_name: str, value_str: str): "true": True, }.get(value_str) ) - elif attribute.data_type in (dict, list, object): - attribute.set_value( - ast.literal_eval(value_str) - ) # TODO revisit this for malformed lists, dicts, etc + elif attribute.data_type in (dict, list, object, tuple): + try: + attribute.set_value(ast.literal_eval(value_str)) + except (SyntaxError, ValueError) as e: + attribute.clear() + LOGGER.debug(e) else: LOGGER.error( "Cannot set value {} to type {}, not defined how to cast from string".format( @@ -432,7 +502,9 @@ def set_attribute_from_str(self, attribute_name: str, value_str: str): def get_attribute_value(self, attribute_name: str): if attribute_name not in self.all_attribute_names: LOGGER.error( - "Error! No valid attribute {} in the node".format(attribute_name) + "Error! No valid attribute '{}' in the node {}".format( + attribute_name, self.node_name + ) ) return @@ -584,11 +656,11 @@ def _run(self, execute_connected=True): self.internal_scene.run_all_nodes( spawn_thread=False ) # TODO maybe this can be improved? / recursive - internal_failures = self.internal_scene.gather_failed_nodes() + internal_failures = self.internal_scene.gather_failed_nodes_logs() if internal_failures: for f in internal_failures: self.fail(f) - internal_errors = self.internal_scene.gather_errored_nodes() + internal_errors = self.internal_scene.gather_errored_nodes_logs() if internal_errors: for e in internal_errors: self.error(e) @@ -822,6 +894,15 @@ def get_node_html_help(self): + "

" ) + help_text += ( + "

INTERNALS_DICT:

" + + pprint.pformat(self.INTERNALS_DICT, indent=2) + .replace(">", ">") + .replace("<", "<") + .replace("\n", "
") + + "

" + ) + return help_text @@ -921,12 +1002,16 @@ def connect_to_other(self, other_attribute: GeneralLogicAttribute) -> bool: """ # Checks ------------------------- # Cycles check - if self.parent_node.check_cycles( - other_attribute.parent_node - ) or other_attribute.parent_node.check_cycles(self.parent_node): - connection_warning = "Cannot connect, cycle detected!" - LOGGER.warning(connection_warning) - return (False, connection_warning) + if self.connector_type == constants.INPUT: + if other_attribute.parent_node.check_cycles(self.parent_node): + connection_warning = "Cannot connect, cycle detected!" + LOGGER.warning(connection_warning) + return (False, connection_warning) + elif self.connector_type == constants.OUTPUT: + if self.parent_node.check_cycles(other_attribute.parent_node): + connection_warning = "Cannot connect, cycle detected!" + LOGGER.warning(connection_warning) + return (False, connection_warning) # Different nodes check if not self.parent_node != other_attribute.parent_node: @@ -1021,8 +1106,10 @@ def get_datatype_str(self) -> str: str: with a representation fo the datatype """ type = str(self.data_type) - if "." in str(self.data_type): - return re.search("'.+\.(.+)'", type).group(1) + if re.match("", type): + return re.match("", type).group(1) + elif re.match(" list: failed_log = [] for node in self.all_logic_nodes: if node.success in [constants.FAILED, constants.ERROR]: @@ -428,7 +428,7 @@ def gather_failed_nodes(self): failed_log.append(node.full_name + ": " + line) return failed_log - def gather_errored_nodes(self): + def gather_errored_nodes_logs(self) -> list: errored_log = [] for node in self.all_logic_nodes: if node.success in [constants.FAILED, constants.ERROR]: diff --git a/requirements.txt b/requirements.txt index a9106ef..2b1ed50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ colorama matplotlib pandas +Pillow pymongo[srv] PySide2>5, <6 PyYAML diff --git a/test/test_logic_scene.py b/test/test_logic_scene.py index 91d1ae1..bcff921 100644 --- a/test/test_logic_scene.py +++ b/test/test_logic_scene.py @@ -31,20 +31,20 @@ def test_logic_scene(self): logic_scene.add_node_by_name("FailNode") self.assertEqual(logic_scene.node_count(), 4) - def test_run_node_graph_starting_at_node_2(self): - utils.print_test_header("test_run_node_graph_starting_at_node_2") + def test_run_node_graph_starting_at_node(self): + utils.print_test_header("test_run_node_graph_starting_at_node") logic_scene = LogicScene() n_1 = logic_scene.add_node_by_name("StrInput") - n_1.set_attribute_value("out_str", "test_people") + n_1.set_attribute_value("internal_str", "test_people") n_2 = logic_scene.add_node_by_name("GetDictKey") n_2.set_attribute_value("in_dict", LogicSceneTesting.DICT_EXAMPLE) n_2["key"].connect_to_other(n_1["out_str"]) n_3 = logic_scene.add_node_by_name("PrintToConsole") n_3["in_object_0"].connect_to_other(n_2["out"]) n_1.run_chain() - self.assertTrue(n_1.success) - self.assertTrue(n_2.success) + self.assertEqual(n_1.success, constants.SUCCESSFUL) + self.assertEqual(n_2.success, constants.SUCCESSFUL) def test_locate_starting_nodes(self): utils.print_test_header("test_locate_starting_nodes") @@ -97,7 +97,7 @@ def test_write_scene_to_file(self): logic_scene = LogicScene() n_1 = logic_scene.add_node_by_name("StrInput") - n_1.set_attribute_value("out_str", "TEST") + n_1.set_attribute_value("internal_str", "TEST") n_2 = logic_scene.add_node_by_name("PrintToConsole") n_1["out_str"].connect_to_other(n_2["in_object_0"]) n_2[constants.START].connect_to_other(n_1[constants.COMPLETED]) @@ -106,6 +106,7 @@ def test_write_scene_to_file(self): temp = tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) temp.close() logic_scene.save_to_file(temp.name) + self.assertTrue(os.path.isfile(temp.name)) def test_load_scene_from_file(self): @@ -158,6 +159,9 @@ def test_execute_scene_from_alias(self): logic_scene.load_from_file("simple_regex") logic_scene.run_all_nodes_batch() + assert len(logic_scene.gather_failed_nodes_logs()) == 0 + assert len(logic_scene.gather_errored_nodes_logs()) == 0 + def test_execute_scene_from_alias_2(self): utils.print_test_header("test_execute_scene_from_alias_2") @@ -165,6 +169,29 @@ def test_execute_scene_from_alias_2(self): logic_scene.load_from_file("environ_to_yaml") logic_scene.run_all_nodes_batch() + assert len(logic_scene.gather_failed_nodes_logs()) == 0 + assert len(logic_scene.gather_errored_nodes_logs()) == 0 + + def test_execute_scene_from_alias_3(self): + utils.print_test_header("test_execute_scene_from_alias_3") + + logic_scene = LogicScene() + logic_scene.load_from_file("fail_scene") + logic_scene.run_all_nodes_batch() + + assert len(logic_scene.gather_failed_nodes_logs()) == 2 + assert len(logic_scene.gather_errored_nodes_logs()) == 4 + + def test_execute_scene_from_alias_4(self): + utils.print_test_header("test_execute_scene_from_alias_4") + + logic_scene = LogicScene() + logic_scene.load_from_file("datetime_example") + logic_scene.run_all_nodes_batch() + + assert len(logic_scene.gather_failed_nodes_logs()) == 0 + assert len(logic_scene.gather_errored_nodes_logs()) == 0 + def test_rename_node_incorrect(self): utils.print_test_header("test_rename_node_incorrect") diff --git a/test/test_node.py b/test/test_node.py index 06685e8..29ace4f 100644 --- a/test/test_node.py +++ b/test/test_node.py @@ -8,7 +8,7 @@ from all_nodes.lib.base_node_lib.nodes_general_library import file_reading_nodes from all_nodes.lib.base_node_lib.nodes_general_library import file_writing_nodes from all_nodes.lib.base_node_lib.nodes_general_library import general_nodes -from all_nodes.lib.base_node_lib.nodes_general_library import special_input_nodes +from all_nodes.lib.base_node_lib.nodes_general_library import general_input_nodes from all_nodes import utils @@ -76,11 +76,16 @@ def test_getting_internal_dict(self): """ utils.print_test_header("test_getting_internal_dict") - n_1 = special_input_nodes.StrInput() + n_1 = general_input_nodes.StrInput() n_2 = general_nodes.GetDictKey() - n_1.set_attribute_value("out_str", "test") + + n_1.set_attribute_value("internal_str", "test") n_1.connect_attribute("out_str", n_2, "key") + n_2.set_attribute_value("in_dict", {"test": 100}) n_1.run_chain() + + assert n_2["out"].get_value() == 100 + pprint(n_2.get_node_full_dict()) def test_inputs_checked_run(self): diff --git a/utils.py b/utils.py index a65b8b7..adb5ec2 100644 --- a/utils.py +++ b/utils.py @@ -15,12 +15,19 @@ # -------------------------------- LOGGING -------------------------------- # -def get_logger(logger_name): +def get_logger(logger_name: str) -> logging.Logger: + """Get a logger with a specific formatting + + Args: + logger_name (str) + + Returns: + logging.Logger + """ logger = logging.getLogger(logger_name) logger.setLevel(constants.LOGGING_LEVEL) console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(constants.LOGGING_LEVEL) - console_handler.setLevel(constants.LOGGING_LEVEL) console_handler.setFormatter(constants.CONSOLE_LOG_FORMATTER) logger.addHandler(console_handler) return logger @@ -35,9 +42,7 @@ def print_separator(message): Args: message (str): Message to display in the separator """ - print("\n", end="") - print(message.upper()) - print("-" * len(message)) + LOGGER.info(f"\n{message.upper()}\n{'-' * len(message)}") def print_test_header(message): @@ -46,11 +51,12 @@ def print_test_header(message): Args: message (str): Message to display in the separator """ - message = "TEST STARTED - " + message - print("\n", end="") - print(f"{Fore.GREEN}+-{'-' * len(message)}-+") - print(f"{Fore.GREEN}| {message} | ") - print(f"{Fore.GREEN}+-{'-' * len(message)}-+ {Style.RESET_ALL}") + header = f"TEST STARTED - {message}" + print( + f"{Fore.GREEN}+-{'-' * len(header)}-+\n" + f"{Fore.GREEN}| {header} |\n" + f"{Fore.GREEN}+-{'-' * len(header)}-+ {Style.RESET_ALL}" + ) # -------------------------------- UTILITY -------------------------------- #