Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* ------------------------------------------------------------------------
*
* Copyright by KNIME AG, Zurich, Switzerland
* Website: http://www.knime.com; Email: contact@knime.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, Version 3, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses>.
*
* Additional permission under GNU GPL version 3 section 7:
*
* KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs.
* Hence, KNIME and ECLIPSE are both independent programs and are not
* derived from each other. Should, however, the interpretation of the
* GNU GPL Version 3 ("License") under any applicable laws result in
* KNIME and ECLIPSE being a combined program, KNIME AG herewith grants
* you the additional permission to use and propagate KNIME together with
* ECLIPSE with only the license terms in place for ECLIPSE applying to
* ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the
* license terms of ECLIPSE themselves allow for the respective use and
* propagation of ECLIPSE together with KNIME.
*
* Additional permission relating to nodes for KNIME that extend the Node
* Extension (and in particular that are based on subclasses of NodeModel,
* NodeDialog, and NodeView) and that only interoperate with KNIME through
* standard APIs ("Nodes"):
* Nodes are deemed to be separate and independent programs and to not be
* covered works. Notwithstanding anything to the contrary in the
* License, the License does not apply to Nodes, you are not required to
* license Nodes under the License, and you are granted a license to
* prepare and propagate Nodes, in each case even if such Nodes are
* propagated with or for interoperation with KNIME. The owner of a Node
* may freely choose the license terms applicable to such Node, including
* when such Node is propagated with or for interoperation with KNIME.
* ---------------------------------------------------------------------
*/
package org.knime.python3.nodes.ports;

import static org.junit.Assert.assertSame;

import org.junit.Test;
import org.knime.core.node.port.inactive.InactiveBranchPortObject;
import org.knime.core.node.port.inactive.InactiveBranchPortObjectSpec;
import org.knime.python3.nodes.ports.PythonPortObjects.PythonInactivePortObject;
import org.knime.python3.nodes.ports.PythonPortObjects.PythonInactivePortObjectSpec;
import org.knime.python3.nodes.ports.converters.PortObjectConversionContext;

/**
* Tests conversion of inactive branch port objects and specs between KNIME and Python.
*/
public class InactivePortConversionTest {

@Test
public void testConvertInactiveSpecToPython() {
var converted = PythonPortTypeRegistry.convertPortObjectSpecToPython(InactiveBranchPortObjectSpec.INSTANCE);

assertSame(PythonInactivePortObjectSpec.INSTANCE, converted);

Check warning on line 66 in org.knime.python3.nodes.tests/src/test/java/org/knime/python3/nodes/ports/InactivePortConversionTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a message to this assertion.

See more on https://sonarcloud.io/project/issues?id=knime_knime-python&issues=AZ0K3rjXoeAGDXXgxwoH&open=AZ0K3rjXoeAGDXXgxwoH&pullRequest=92
}

@Test
public void testConvertInactiveSpecFromPython() {
var converted = PythonPortTypeRegistry.convertPortObjectSpecFromPython(PythonInactivePortObjectSpec.INSTANCE);

assertSame(InactiveBranchPortObjectSpec.INSTANCE, converted);

Check warning on line 73 in org.knime.python3.nodes.tests/src/test/java/org/knime/python3/nodes/ports/InactivePortConversionTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a message to this assertion.

See more on https://sonarcloud.io/project/issues?id=knime_knime-python&issues=AZ0K3rjXoeAGDXXgxwoI&open=AZ0K3rjXoeAGDXXgxwoI&pullRequest=92
}

@Test
public void testConvertInactiveObjectToPython() {
var converted = PythonPortTypeRegistry.convertPortObjectToPython(InactiveBranchPortObject.INSTANCE,
new PortObjectConversionContext(null, null, null));

assertSame(PythonInactivePortObject.INSTANCE, converted);

Check warning on line 81 in org.knime.python3.nodes.tests/src/test/java/org/knime/python3/nodes/ports/InactivePortConversionTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a message to this assertion.

See more on https://sonarcloud.io/project/issues?id=knime_knime-python&issues=AZ0K3rjXoeAGDXXgxwoJ&open=AZ0K3rjXoeAGDXXgxwoJ&pullRequest=92
}

@Test
public void testConvertInactiveObjectFromPython() {
var converted = PythonPortTypeRegistry.convertPortObjectFromPython(PythonInactivePortObject.INSTANCE,
new PortObjectConversionContext(null, null, null));

assertSame(InactiveBranchPortObject.INSTANCE, converted);

Check warning on line 89 in org.knime.python3.nodes.tests/src/test/java/org/knime/python3/nodes/ports/InactivePortConversionTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a message to this assertion.

See more on https://sonarcloud.io/project/issues?id=knime_knime-python&issues=AZ0K3rjXoeAGDXXgxwoK&open=AZ0K3rjXoeAGDXXgxwoK&pullRequest=92
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
from contextlib import contextmanager

import _node_backend_launcher as knb
from _ports import JavaPortTypeRegistry
Expand Down Expand Up @@ -437,6 +438,189 @@ def test_connection_port_object_to_python(self):
self.assertEqual("badabummm", obj.spec.data)
self.assertEqual("this data is not serialized", obj._transient_data)

def test_inactive_spec_from_python(self):
port = knext.Port(knext.PortType.TABLE, "Test port", "Test port")
pypos = self.registry.spec_from_python(knext.InactivePort, port, "NodeId", 0)
self.assertEqual(
"org.knime.core.node.port.inactive.InactiveBranchPortObjectSpec",
pypos.getJavaClassName(),
)

def test_inactive_spec_to_python(self):
port = knext.Port(knext.PortType.TABLE, "Test port", "Test port")
java_spec = knb._PythonPortObjectSpec(
"org.knime.core.node.port.inactive.InactiveBranchPortObjectSpec", {}
)
self.assertIs(
knext.InactivePort,
self.registry.spec_to_python(java_spec, port, callback_mock),
)

def test_inactive_object_from_python(self):
port = knext.Port(knext.PortType.TABLE, "Test port", "Test port")
out = self.registry.port_object_from_python(
knext.InactivePort, None, port, "nodeID", 0
)
self.assertEqual(
"org.knime.core.node.port.inactive.InactiveBranchPortObject",
out.getJavaClassName(),
)

def test_inactive_object_to_python(self):
port = knext.Port(knext.PortType.TABLE, "Test port", "Test port")
java_obj = self.MockFromJavaObject(
spec=None,
file_path="unused",
class_name="org.knime.core.node.port.inactive.InactiveBranchPortObject",
)
self.assertIs(
knext.InactivePort,
self.registry.port_object_to_python(java_obj, port, callback_mock),
)


class InactivePortProxyTest(unittest.TestCase):
@kn.node("Inactive output node", kn.NodeType.OTHER, "", "")
@kn.output_binary("Primary output", "Primary binary output.", id="primary")
@kn.output_binary_group(
"Secondary outputs", "Secondary binary outputs.", id="secondary"
)
class NodeWithInactiveOutputs:
def configure(self, ctx):
return (
knext.BinaryPortObjectSpec("primary"),
[kn.InactivePort, knext.BinaryPortObjectSpec("secondary")],
)

def execute(self, ctx):
return (
b"primary-output",
[kn.InactivePort, b"secondary-output"],
)

class MockJavaConfigContext:
def get_input_port_map(self):
return {}

def get_node_id(self):
return "0:1"

class MockJavaExecContext(MockJavaConfigContext):
def is_canceled(self):
return False

class _MockFileStore:
def __init__(self, file_path: str) -> None:
self._file_path = file_path

def get_key(self):
return self._file_path

def get_file_path(self):
return self._file_path

class MockJavaCallback:
def __init__(self) -> None:
self._created_files = []

def get_flow_variables(self):
return {}

def set_flow_variables(self, flow_variables):
pass

def log(self, msg, sev):
pass

def set_failure(self, message, details, invalid_settings):
raise AssertionError(message)

def create_filestore_file(self):
with tempfile.NamedTemporaryFile(delete=False) as tmp:
self._created_files.append(tmp.name)
return InactivePortProxyTest._MockFileStore(tmp.name)

def cleanup(self):
for file_path in self._created_files:
if os.path.exists(file_path):
os.remove(file_path)

@contextmanager
def _monkey_patch_java_helpers(self):
original_to_java_list = knb._to_java_list
original_create_linked_hashmap = knb._create_linked_hashmap
knb._to_java_list = lambda list_: list_
knb._create_linked_hashmap = lambda: {}
try:
yield
finally:
knb._to_java_list = original_to_java_list
knb._create_linked_hashmap = original_create_linked_hashmap

def _create_proxy(self):
java_port_type_registry: JavaPortTypeRegistry = unittest.mock.create_autospec(
JavaPortTypeRegistry
)
java_port_type_registry.can_decode_port_object.return_value = False
java_port_type_registry.can_encode_port_object.return_value = False
java_port_type_registry.can_decode_spec.return_value = False
java_port_type_registry.can_encode_spec.return_value = False

registry = knb._PortTypeRegistry("test.extension", java_port_type_registry)
proxy = knb._PythonNodeProxy(
node=type(self).NodeWithInactiveOutputs(),
port_type_registry=registry,
knime_parser=None,
extension_version="0.0.1",
)
callback = self.MockJavaCallback()
proxy.initializeJavaCallback(callback)
return proxy, callback

def test_configure_with_inactive_outputs_keeps_output_arity(self):
proxy, callback = self._create_proxy()
try:
with self._monkey_patch_java_helpers():
outputs = proxy.configure([], self.MockJavaConfigContext())
finally:
callback.cleanup()

self.assertEqual(3, len(outputs))
self.assertEqual(
"org.knime.python3.nodes.ports.PythonBinaryBlobPortObjectSpec",
outputs[0].getJavaClassName(),
)
self.assertEqual(
"org.knime.core.node.port.inactive.InactiveBranchPortObjectSpec",
outputs[1].getJavaClassName(),
)
self.assertEqual(
"org.knime.python3.nodes.ports.PythonBinaryBlobPortObjectSpec",
outputs[2].getJavaClassName(),
)

def test_execute_with_inactive_outputs_keeps_output_arity(self):
proxy, callback = self._create_proxy()
try:
with self._monkey_patch_java_helpers():
outputs = proxy.execute([], self.MockJavaExecContext())
finally:
callback.cleanup()

self.assertEqual(3, len(outputs))
self.assertEqual(
"org.knime.python3.nodes.ports.PythonBinaryBlobFileStorePortObject",
outputs[0].getJavaClassName(),
)
self.assertEqual(
"org.knime.core.node.port.inactive.InactiveBranchPortObject",
outputs[1].getJavaClassName(),
)
self.assertEqual(
"org.knime.python3.nodes.ports.PythonBinaryBlobFileStorePortObject",
outputs[2].getJavaClassName(),
)


class DescriptionParsingTest(unittest.TestCase):
def setUp(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
import org.knime.core.node.port.PortObject;
import org.knime.core.node.port.PortObjectSpec;
import org.knime.core.node.port.PortType;
import org.knime.core.node.port.inactive.InactiveBranchPortObject;
import org.knime.core.node.port.inactive.InactiveBranchPortObjectSpec;
import org.knime.core.node.port.image.ImagePortObject;
import org.knime.core.node.port.image.ImagePortObjectSpec;
import org.knime.credentials.base.Credential;
Expand Down Expand Up @@ -189,6 +191,28 @@ public interface PortObjectProvider {
PortObject getPortObject();
}

/**
* {@link PythonPortObject} specialization representing an inactive output.
*/
public static final class PythonInactivePortObject implements PythonPortObject {

public static final PythonInactivePortObject INSTANCE = new PythonInactivePortObject();

private PythonInactivePortObject() {
// singleton
}

@Override
public String getJavaClassName() {
return InactiveBranchPortObject.class.getName();
}

@Override
public String toString() {
return "PythonInactivePortObject";
}
}

/**
* {@link PythonPortObject} implementation for {@link BufferedDataTable}s used and populated on the Java side.
*/
Expand Down Expand Up @@ -507,6 +531,33 @@ public interface PortObjectSpecProvider {
PortObjectSpec getPortObjectSpec();
}

/**
* {@link PythonPortObjectSpec} specialization representing an inactive output spec.
*/
public static final class PythonInactivePortObjectSpec implements PythonPortObjectSpec {

public static final PythonInactivePortObjectSpec INSTANCE = new PythonInactivePortObjectSpec();

private PythonInactivePortObjectSpec() {
// singleton
}

@Override
public String getJavaClassName() {
return InactiveBranchPortObjectSpec.class.getName();
}

@Override
public String toJsonString() {
return "{}";
}

@Override
public String toString() {
return "PythonInactivePortObjectSpec";
}
}

/**
* {@link PythonPortObjectSpec} specialization wrapping a {@link DataTableSpec}
*/
Expand Down
Loading