From 6cb36e05f5f1bf935fbcbdfed9f8cfb72c6320ee Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 28 Sep 2020 15:25:33 +0200 Subject: [PATCH 1/3] Create node on file drop in canvas --- Orange/widgets/data/owfile.py | 36 +++++++++++++++++-- Orange/widgets/data/owpythonscript.py | 34 ++++++++++++++++-- Orange/widgets/data/tests/test_owfile.py | 18 +++++++++- .../widgets/data/tests/test_owpythonscript.py | 17 ++++++++- Orange/widgets/model/owloadmodel.py | 18 +++++++++- .../widgets/model/tests/test_owloadmodel.py | 14 +++++++- Orange/widgets/unsupervised/owdistancefile.py | 16 ++++++++- .../unsupervised/tests/test_owdistancefile.py | 15 ++++++++ setup.py | 6 ++++ 9 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 Orange/widgets/unsupervised/tests/test_owdistancefile.py diff --git a/Orange/widgets/data/owfile.py b/Orange/widgets/data/owfile.py index 86d650a4793..17ece783229 100644 --- a/Orange/widgets/data/owfile.py +++ b/Orange/widgets/data/owfile.py @@ -2,13 +2,15 @@ import logging from itertools import chain from urllib.parse import urlparse -from typing import List +from typing import List, Dict, Any import numpy as np from AnyQt.QtWidgets import \ QStyle, QComboBox, QMessageBox, QGridLayout, QLabel, \ QLineEdit, QSizePolicy as Policy, QCompleter -from AnyQt.QtCore import Qt, QTimer, QSize +from AnyQt.QtCore import Qt, QTimer, QSize, QUrl + +from orangewidget.workflow.drophandler import SingleUrlDropHandler from Orange.data.table import Table, get_sample_datasets_dir from Orange.data.io import FileFormat, UrlReader, class_from_qualified_name @@ -28,7 +30,6 @@ # module's namespace so that old saved settings still work from Orange.widgets.utils.filedialogs import RecentPath - log = logging.getLogger(__name__) @@ -571,5 +572,34 @@ def workflowEnvChanged(self, key, value, oldvalue): self.update_file_list(key, value, oldvalue) +class OWFileDropHandler(SingleUrlDropHandler): + WIDGET = OWFile + + def canDropUrl(self, url: QUrl) -> bool: + if url.isLocalFile(): + try: + FileFormat.get_reader(url.toLocalFile()) + return True + except Exception: # noqa # pylint:disable=broad-except + return False + else: + return url.scheme().lower() in ("http", "https", "ftp") + + def parametersFromUrl(self, url: QUrl) -> Dict[str, Any]: + if url.isLocalFile(): + path = url.toLocalFile() + r = RecentPath(os.path.abspath(path), None, None, + os.path.basename(path)) + return { + "recent_paths": [r], + "source": OWFile.LOCAL_FILE, + } + else: + return { + "recent_urls": [url.toString()], + "source": OWFile.URL, + } + + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWFile).run() diff --git a/Orange/widgets/data/owpythonscript.py b/Orange/widgets/data/owpythonscript.py index 8b402599207..58dff3101ab 100644 --- a/Orange/widgets/data/owpythonscript.py +++ b/Orange/widgets/data/owpythonscript.py @@ -8,7 +8,7 @@ from functools import reduce from unittest.mock import patch -from typing import Optional, List, TYPE_CHECKING +from typing import Optional, List, Dict, Any, TYPE_CHECKING from AnyQt.QtWidgets import ( QPlainTextEdit, QListView, QSizePolicy, QMenu, QSplitter, QLineEdit, @@ -20,9 +20,12 @@ QSyntaxHighlighter, QTextCharFormat, QTextCursor, QKeySequence, ) from AnyQt.QtCore import ( - Qt, QRegularExpression, QByteArray, QItemSelectionModel, QSize + Qt, QRegularExpression, QByteArray, QItemSelectionModel, QSize, + QMimeDatabase ) +from orangewidget.workflow.drophandler import SingleFileDropHandler + from Orange.data import Table from Orange.base import Learner, Model from Orange.util import interleave @@ -800,5 +803,32 @@ def migrate_settings(cls, settings, version): settings["scriptLibrary"] = library +class OWPythonScriptDropHandler(SingleFileDropHandler): + WIDGET = OWPythonScript + + def canDropFile(self, path: str) -> bool: + md = QMimeDatabase() + mt = md.mimeTypeForFile(path) + return mt.inherits("text/x-python") + + def parametersFromFile(self, path: str) -> Dict[str, Any]: + with open(path, "rt") as f: + content = f.read() + + item: '_ScriptData' = { + "name": os.path.basename(path), + "script": content, + "filename": path, + } + params = { + "__version__": OWPythonScript.settings_version, + "scriptLibrary": [ + item, + ], + "scriptText": content + } + return params + + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWPythonScript).run() diff --git a/Orange/widgets/data/tests/test_owfile.py b/Orange/widgets/data/tests/test_owfile.py index 13793f3737c..1c409a0abca 100644 --- a/Orange/widgets/data/tests/test_owfile.py +++ b/Orange/widgets/data/tests/test_owfile.py @@ -24,7 +24,7 @@ from Orange.data.io import TabReader from Orange.tests import named_file -from Orange.widgets.data.owfile import OWFile +from Orange.widgets.data.owfile import OWFile, OWFileDropHandler from Orange.widgets.utils.filedialogs import dialog_formats, format_filter, RecentPath from Orange.widgets.tests.base import WidgetTest from Orange.widgets.utils.domaineditor import ComboDelegate, VarTypeDelegate, VarTableModel @@ -666,5 +666,21 @@ def read(): self.assertIn(WARNING_MSG, str(self.widget.Warning.load_warning)) +class TestOWFileDropHandler(unittest.TestCase): + def test_canDropUrl(self): + handler = OWFileDropHandler() + self.assertTrue(handler.canDropUrl(QUrl("https://example.com/test.tab"))) + self.assertTrue(handler.canDropUrl(QUrl.fromLocalFile("test.tab"))) + + def test_parametersFromUrl(self): + handler = OWFileDropHandler() + r = handler.parametersFromUrl(QUrl("https://example.com/test.tab")) + self.assertEqual(r["source"], OWFile.URL) + self.assertEqual(r["recent_urls"], ["https://example.com/test.tab"]) + r = handler.parametersFromUrl(QUrl.fromLocalFile("test.tab")) + self.assertEqual(r["source"], OWFile.LOCAL_FILE) + self.assertEqual(r["recent_paths"][0].basename, "test.tab") + + if __name__ == "__main__": unittest.main() diff --git a/Orange/widgets/data/tests/test_owpythonscript.py b/Orange/widgets/data/tests/test_owpythonscript.py index 6c505737e76..b2f6f0a06c1 100644 --- a/Orange/widgets/data/tests/test_owpythonscript.py +++ b/Orange/widgets/data/tests/test_owpythonscript.py @@ -1,6 +1,7 @@ # Test methods with long descriptive names can omit docstrings # pylint: disable=missing-docstring import sys +import unittest from AnyQt.QtCore import QMimeData, QUrl, QPoint, Qt from AnyQt.QtGui import QDragEnterEvent, QDropEvent @@ -8,7 +9,8 @@ from Orange.data import Table from Orange.classification import LogisticRegressionLearner from Orange.tests import named_file -from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content, Script +from Orange.widgets.data.owpythonscript import OWPythonScript, \ + read_file_content, Script, OWPythonScriptDropHandler from Orange.widgets.tests.base import WidgetTest, DummySignalManager from Orange.widgets.widget import OWWidget @@ -260,3 +262,16 @@ def test_restore(self): "__version__": 2 }) self.assertEqual(w.libraryListSource[0].name, "A") + + +class TestOWPythonScriptDropHandler(unittest.TestCase): + def test_canDropFile(self): + handler = OWPythonScriptDropHandler() + self.assertTrue(handler.canDropFile(__file__)) + self.assertFalse(handler.canDropFile("test.tab")) + + def test_parametersFromFile(self): + handler = OWPythonScriptDropHandler() + r = handler.parametersFromFile(__file__) + item = r["scriptLibrary"][0] + self.assertEqual(item["filename"], __file__) diff --git a/Orange/widgets/model/owloadmodel.py b/Orange/widgets/model/owloadmodel.py index dd6edabc189..eb2403b2b0d 100644 --- a/Orange/widgets/model/owloadmodel.py +++ b/Orange/widgets/model/owloadmodel.py @@ -1,13 +1,16 @@ import os import pickle +from typing import Any, Dict from AnyQt.QtWidgets import QSizePolicy, QStyle, QFileDialog from AnyQt.QtCore import QTimer +from orangewidget.workflow.drophandler import SingleFileDropHandler + from Orange.base import Model from Orange.widgets import widget, gui from Orange.widgets.model import owsavemodel -from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin +from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin, RecentPath from Orange.widgets.utils import stdpaths from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.widget import Msg, Output @@ -88,5 +91,18 @@ def open_file(self): self.Outputs.model.send(model) +class OWLoadModelDropHandler(SingleFileDropHandler): + WIDGET = OWLoadModel + + def canDropFile(self, path: str) -> bool: + return path.endswith(".pkcls") + + def parametersFromFile(self, path: str) -> Dict[str, Any]: + r = RecentPath(os.path.abspath(path), None, None, + os.path.basename(path)) + parameters = {"recent_paths": [r]} + return parameters + + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWLoadModel).run() diff --git a/Orange/widgets/model/tests/test_owloadmodel.py b/Orange/widgets/model/tests/test_owloadmodel.py index e9528f03fa0..c606c1b251f 100644 --- a/Orange/widgets/model/tests/test_owloadmodel.py +++ b/Orange/widgets/model/tests/test_owloadmodel.py @@ -11,7 +11,7 @@ from orangewidget.utils.filedialogs import RecentPath from Orange.data import Table from Orange.classification.naive_bayes import NaiveBayesLearner -from Orange.widgets.model.owloadmodel import OWLoadModel +from Orange.widgets.model.owloadmodel import OWLoadModel, OWLoadModelDropHandler from Orange.widgets.tests.base import WidgetTest @@ -136,5 +136,17 @@ def test_open_moved_workflow(self, load): os.remove(file_name) +class TestOWLoadModelDropHandler(unittest.TestCase): + def test_canDropFile(self): + handler = OWLoadModelDropHandler() + self.assertTrue(handler.canDropFile("test.pkcls")) + self.assertFalse(handler.canDropFile("test.txt")) + + def test_parametersFromFile(self): + handler = OWLoadModelDropHandler() + res = handler.parametersFromFile("test.pkcls") + self.assertEqual(res["recent_paths"][0].basename, "test.pkcls") + + if __name__ == "__main__": unittest.main() diff --git a/Orange/widgets/unsupervised/owdistancefile.py b/Orange/widgets/unsupervised/owdistancefile.py index 30079771451..5bb1003f698 100644 --- a/Orange/widgets/unsupervised/owdistancefile.py +++ b/Orange/widgets/unsupervised/owdistancefile.py @@ -3,10 +3,12 @@ from AnyQt.QtWidgets import QSizePolicy, QStyle, QMessageBox, QFileDialog from AnyQt.QtCore import QTimer +from orangewidget.workflow.drophandler import SingleFileDropHandler + from Orange.misc import DistMatrix from Orange.widgets import widget, gui from Orange.data import get_sample_datasets_dir -from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin +from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin, RecentPath from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.widget import Output @@ -144,5 +146,17 @@ def send_report(self): self.report_items([("File name", self.loaded_file)]) +class OWDistanceFileDropHandler(SingleFileDropHandler): + WIDGET = OWDistanceFile + + def parametersFromFile(self, path): + r = RecentPath(os.path.abspath(path), None, None, + os.path.basename(path)) + return {"recent_paths": [r]} + + def canDropFile(self, path: str) -> bool: + return os.path.splitext(path)[1].lower() == ".dst" + + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWDistanceFile).run() diff --git a/Orange/widgets/unsupervised/tests/test_owdistancefile.py b/Orange/widgets/unsupervised/tests/test_owdistancefile.py new file mode 100644 index 00000000000..a1e34ad0f76 --- /dev/null +++ b/Orange/widgets/unsupervised/tests/test_owdistancefile.py @@ -0,0 +1,15 @@ +import unittest + +from Orange.widgets.unsupervised.owdistancefile import OWDistanceFileDropHandler + + +class TestOWDistanceFileDropHandler(unittest.TestCase): + def test_canDropFile(self): + handler = OWDistanceFileDropHandler() + self.assertTrue(handler.canDropFile("test.dst")) + self.assertFalse(handler.canDropFile("test.bin")) + + def test_parametersFromFile(self): + handler = OWDistanceFileDropHandler() + r = handler.parametersFromFile("test.dst") + self.assertEqual(r["recent_paths"][0].basename, "test.dst") diff --git a/setup.py b/setup.py index 685051d5a05..ce964288bf7 100755 --- a/setup.py +++ b/setup.py @@ -97,6 +97,12 @@ "orange.canvas.help": ( "html-index = Orange.widgets:WIDGET_HELP_PATH", ), + "orangecanvas.document.interactions.DropHandler": ( + "File = Orange.widgets.data.owfile:OWFileDropHandler", + "Load Model = Orange.widgets.model.owloadmodel:OWLoadModelDropHandler", + "Distance File = Orange.widgets.unsupervised.owdistancefile:OWDistanceFileDropHandler", + "Python Script = Orange.widgets.data.owpythonscript:OWPythonScriptDropHandler", + ), "gui_scripts": ( "orange-canvas = Orange.canvas.__main__:main", ), From 97c2ae9cb4c7ba20191fb93d14e35922ef145a38 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Tue, 13 Oct 2020 15:27:39 +0200 Subject: [PATCH 2/3] mainwindow: Specify the drop handler plugin group --- Orange/canvas/mainwindow.py | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Orange/canvas/mainwindow.py b/Orange/canvas/mainwindow.py index 7a8fc67a95f..bd44137b64e 100644 --- a/Orange/canvas/mainwindow.py +++ b/Orange/canvas/mainwindow.py @@ -3,6 +3,7 @@ QFormLayout, QCheckBox, QLineEdit, QWidget, QVBoxLayout, QLabel ) from orangecanvas.application.settings import UserSettingsDialog, FormLayout +from orangecanvas.document.interactions import PluginDropHandler from orangecanvas.document.usagestatistics import UsageStatistics from orangecanvas.utils.overlay import NotificationOverlay @@ -107,6 +108,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.notification_overlay = NotificationOverlay(self.scheme_widget) self.notification_server = None + self.scheme_widget.setDropHandlers([ + PluginDropHandler("orange.canvas.drophandler") + ]) def open_canvas_settings(self): # type: () -> None diff --git a/setup.py b/setup.py index ce964288bf7..6abd2c214d8 100755 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ "orange.canvas.help": ( "html-index = Orange.widgets:WIDGET_HELP_PATH", ), - "orangecanvas.document.interactions.DropHandler": ( + "orange.canvas.drophandler": ( "File = Orange.widgets.data.owfile:OWFileDropHandler", "Load Model = Orange.widgets.model.owloadmodel:OWLoadModelDropHandler", "Distance File = Orange.widgets.unsupervised.owdistancefile:OWDistanceFileDropHandler", From e8006ec4b226e4414c6ee9d293d03083d65e5720 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 17 May 2021 12:57:21 +0200 Subject: [PATCH 3/3] tox.ini: Add minimal deps for lint --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 358558529c4..099ab84b6cb 100644 --- a/tox.ini +++ b/tox.ini @@ -63,7 +63,11 @@ commands = changedir = {toxinidir} skip_install = true whitelist_externals = bash -deps = pylint +deps = + orange-widget-base + anyqt + PyQt5==5.12.* + pylint commands = bash .github/workflows/check_pylint_diff.sh