Repository: biolab/orange3

Issue:
Save Data: autosave is deactivated
<!-- 
Thanks for taking the time to report a bug!
If you're raising an issue about an add-on (i.e., installed via Options > Add-ons), raise an issue in the relevant add-on's issue tracker instead. See: https://github.com/biolab?q=orange3
To fix the bug, we need to be able to reproduce it. Please answer the following questions to the best of your ability.
-->


**What's wrong?**
<!-- Be specific, clear, and concise. Include screenshots if relevant. -->
<!-- If you're getting an error message, copy it, and enclose it with three backticks (```). -->
The Autosave-Checkbox in Save Data deactivates itself at every restart of Orange, though workflow and target path are in the same directory.




**How can we reproduce the problem?** 
<!-- Upload a zip with the .ows file and data. -->
<!-- Describe the steps (open this widget, click there, then add this...) -->
I think it is a Windows-Issue:

Here:
https://github.com/biolab/orange3/blob/5f06ada600eac42138e362bba566824e69e7cd3a/Orange/widgets/utils/save/owsavebase.py#L116-L120

workflow_dir is a path with backslashes: "C:\\\\Users\\\\test\\\\Desktop\\\\TestDir"
absolute_path is a path with slashes: "C:/Users/test/Desktop/TestDir"
So `absolute_path.startswith(workflow_dir.rstrip("/"))` will always be false and the absolute path is written to `self.stored_path`.



**What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: Windows 10
- Orange version: 3.32.0 dev
- How you installed Orange: clone from github



Discussion:
@mw25, thanks for another detailed report.

Almost nobody of core developers uses Windows. @thocevar could look into it ... but could you try fixing it and submitting a fix yourself? It seems it would suffice to call [os.path.normpath](https://docs.python.org/3/library/os.path.html#os.path.normcase) on both paths. On Windows, it will replace backslashes and also make the name case-insensitive, which is also necessary for this to work correctly.
Sorry for the delay in responding. I was not informed about your answer for some reason.

I will do a pull request as you suggest. 

Resolving patch:
diff --git a/Orange/widgets/utils/save/owsavebase.py b/Orange/widgets/utils/save/owsavebase.py
--- a/Orange/widgets/utils/save/owsavebase.py
+++ b/Orange/widgets/utils/save/owsavebase.py
@@ -112,12 +112,12 @@ def last_dir(self):
     def last_dir(self, absolute_path):
         """Store _absolute_path and update relative path (stored_path)"""
         self._absolute_path = absolute_path
-
+        self.stored_path = absolute_path
         workflow_dir = self.workflowEnv().get("basedir", None)
-        if workflow_dir and absolute_path.startswith(workflow_dir.rstrip("/")):
-            self.stored_path = os.path.relpath(absolute_path, workflow_dir)
-        else:
-            self.stored_path = absolute_path
+        if workflow_dir:
+            relative_path = os.path.relpath(absolute_path, start=workflow_dir)
+            if not relative_path.startswith(".."):
+                self.stored_path = relative_path
 
     def _abs_path_from_setting(self):
         """


Test patch:
diff --git a/Orange/widgets/utils/save/tests/test_owsavebase.py b/Orange/widgets/utils/save/tests/test_owsavebase.py
--- a/Orange/widgets/utils/save/tests/test_owsavebase.py
+++ b/Orange/widgets/utils/save/tests/test_owsavebase.py
@@ -359,6 +359,50 @@ class OWSave(OWSaveBase):
         widget = self.create_widget(OWSave)
         self.assertEqual(widget.default_filter(), OWSave.filters[0])
 
+    @unittest.skipUnless(sys.platform.startswith("win"), "windows path tests")
+    def test_paths_win(self):
+        class OWSave(OWSaveBase):
+            name = "Mock save"
+            filters = ["csv (*.csv)", "txt (*.txt)"]
+        widget = self.create_widget(OWSave)
+        # relative stored paths
+        for workflow_dir, filename in [("C:/Temp", "C:/Temp/abc.csv"),
+                                       ("C:/Temp", "C:/Temp/Project/abc.csv"),
+                                       ("C:/Temp/", "C:/Temp/abc.csv"),
+                                       ("C:/Temp/", "C:/Temp/Project/abc.csv"),
+                                       ("C:/Temp", "c:\\Temp\\Project\\abc.csv"),
+                                       ("c:\\Temp", "c:/Temp/Project\\abc.csv")]:
+            widget.workflowEnv = lambda bd=workflow_dir: {"basedir": bd}
+            widget.filename = filename
+            self.assertFalse(os.path.isabs(widget.stored_path))
+        # absolute stored paths
+        for workflow_dir, filename in [("C:/Temp", "C:/Folder/abc.csv"),
+                                       ("C:/Temp/Project", "C:/Temp/abc.csv"),
+                                       ("C:\\Temp\\Project", "C:\\Temp\\abc.csv")]:
+            widget.workflowEnv = lambda bd=workflow_dir: {"basedir": bd}
+            widget.filename = filename
+            self.assertTrue(os.path.isabs(widget.stored_path))
+
+    def test_paths_unix(self):
+        class OWSave(OWSaveBase):
+            name = "Mock save"
+            filters = ["csv (*.csv)", "txt (*.txt)"]
+        widget = self.create_widget(OWSave)
+        # relative stored paths
+        for workflow_dir, filename in [("/temp", "/temp/abc.csv"),
+                                       ("/temp", "/temp/project/abc.csv"),
+                                       ("/temp/", "/temp/abc.csv"),
+                                       ("/temp/", "/temp/project/abc.csv")]:
+            widget.workflowEnv = lambda bd=workflow_dir: {"basedir": bd}
+            widget.filename = filename
+            self.assertFalse(os.path.isabs(widget.stored_path))
+        # absolute stored paths
+        for workflow_dir, filename in [("/temp", "/folder/abc.csv"),
+                                       ("/temp/project", "/temp/abc.csv")]:
+            widget.workflowEnv = lambda bd=workflow_dir: {"basedir": bd}
+            widget.filename = filename
+            self.assertTrue(os.path.isabs(widget.stored_path))
+
 
 class TestOWSaveUtils(unittest.TestCase):
     def test_replace_extension(self):