Repository: biolab/orange3

Issue:
Opening data without column names and Variable.make
A user tried to open multiple unannotated files, which were opened with default feature names (Feature X) and couldn't be concatenated afterwards. Such are files if you use export csv from Matlab.

I checked, and `FileFormat.data_table` explicitly creates anonymous features without `Variable.make`, which makes them incompatible. Why is Orange being so strict (even tests pass if I change it)?

I also checked what happens with the domain if the user edits it in the bottom part of OWFile. There, `.make` is used for all Variable types except for conversion into a DiscreteVariable. 

Does anyone remembers why domain editor creates DiscreteVariable without make? I think it is inconsistent to use .make at file reading and not to use if a user choses to create a variable manually. I know discrete variables are a pain as Variables values depend on file reading order.

For starters, I propose making all non-nominal anonymous features with `.make`.


Discussion:
> Why is Orange being so strict (even tests pass if I change it)?

+1 for using make for files without headers

> Does anyone remembers why domain editor creates DiscreteVariable without make?

It used to create all variables without make until https://github.com/biolab/orange3/commit/29ccd88ca59d30fa521223854458a6c18199928f. I chose to change only String/Numeric paths as they are without side effects. Using make for Categorical can introduce new values to existing vars, so simply switching one column to categorical and back to string type can result in another column having some extra values.

Resolving patch:
diff --git a/Orange/data/io.py b/Orange/data/io.py
--- a/Orange/data/io.py
+++ b/Orange/data/io.py
@@ -170,9 +170,20 @@ def guess_data_type(orig_values, namask=None):
 
 
 def sanitize_variable(valuemap, values, orig_values, coltype, coltype_kwargs,
-                      domain_vars, existing_var, new_var_name, data=None):
+                      domain_vars=None, existing_var=None, new_var_name=None, data=None, name=None):
     assert issubclass(coltype, Variable)
 
+    if name is None or existing_var is not None or new_var_name is not None:
+        name = existing_var.strip() if existing_var else new_var_name
+        raise DeprecationWarning("Arguments 'existing_var' and 'new_var_name' are "\
+                                 "deprecated since 3.16; use 'name' instead")
+
+    if domain_vars is not None:
+        raise DeprecationWarning("Argument 'domain_vars' is deprecated since 3.16")
+
+    if data is not None:
+        raise DeprecationWarning("Argument 'data' is deprecated since 3.16")
+
     def get_number_of_decimals(values):
         len_ = len
         ndecimals = max((len_(value) - value.find(".")
@@ -183,12 +194,7 @@ def get_number_of_decimals(values):
     if issubclass(coltype, DiscreteVariable) and valuemap is not None:
         coltype_kwargs.update(values=valuemap)
 
-    if existing_var:
-        # Use existing variable if available
-        var = coltype.make(existing_var.strip(), **coltype_kwargs)
-    else:
-        # Never use existing for un-named variables
-        var = coltype(new_var_name, **coltype_kwargs)
+    var = coltype.make(name, **coltype_kwargs)
 
     if isinstance(var, DiscreteVariable):
         # Map discrete data to 'ints' (or at least what passes as int around
@@ -714,16 +720,14 @@ def _equal_length(lst):
 
             cols, domain_vars = append_to
 
-            existing_var, new_var_name = None, None
             if domain_vars is not None:
-                existing_var = names and names[col]
-                if not existing_var:
-                    new_var_name = next(NAMEGEN)
+                var_name = names and names[col]
+                if not var_name:
+                    var_name = next(NAMEGEN)
 
-            if domain_vars is not None:
                 values, var = sanitize_variable(
                     valuemap, values, orig_values, coltype, coltype_kwargs,
-                    domain_vars, existing_var, new_var_name, data)
+                    name=var_name)
             else:
                 var = None
             if domain_vars is not None:
diff --git a/Orange/data/table.py b/Orange/data/table.py
--- a/Orange/data/table.py
+++ b/Orange/data/table.py
@@ -1629,8 +1629,7 @@ def guessed_var(i, var_name):
             orig_vals = M[:, i]
             val_map, vals, var_type = Orange.data.io.guess_data_type(orig_vals)
             values, variable = Orange.data.io.sanitize_variable(
-                val_map, vals, orig_vals, var_type,
-                {}, _metas, None, var_name)
+                val_map, vals, orig_vals, var_type, {}, name=var_name)
             M[:, i] = values
             return variable
 


Test patch:
diff --git a/Orange/tests/test_io.py b/Orange/tests/test_io.py
--- a/Orange/tests/test_io.py
+++ b/Orange/tests/test_io.py
@@ -6,8 +6,11 @@
 import tempfile
 import shutil
 
-from Orange.data.io import FileFormat, TabReader, CSVReader, PickleReader
+from Orange.data import ContinuousVariable
+from Orange.data.io import FileFormat, TabReader, CSVReader, PickleReader, \
+    sanitize_variable
 from Orange.data.table import get_sample_datasets_dir
+from Orange.version import version
 
 
 class WildcardReader(FileFormat):
@@ -100,3 +103,12 @@ def test_open_bad_pickle(self):
         reader = PickleReader("")
         with unittest.mock.patch("pickle.load", return_value=None):
             self.assertRaises(TypeError, reader.read, "foo")
+
+
+class TestIo(unittest.TestCase):
+    def test_sanitize_variable_deprecated_params(self):
+        """In version 3.18 deprecation warnings in function 'sanitize_variable'
+        should be removed along with unused parameters."""
+        if version > "3.18":
+            _, _ = sanitize_variable(None, None, None, ContinuousVariable,
+                                     {}, name="name", data="data")
diff --git a/Orange/tests/test_tab_reader.py b/Orange/tests/test_tab_reader.py
--- a/Orange/tests/test_tab_reader.py
+++ b/Orange/tests/test_tab_reader.py
@@ -143,6 +143,17 @@ def test_read_data_no_header(self):
         self.assertTrue(table.domain[0].is_continuous)
         self.assertEqual(table.domain[0].name, 'Feature 1')
 
+    def test_read_data_no_header_feature_reuse(self):
+        samplefile = """\
+        0.1\t0.2\t0.3
+        1.1\t1.2\t1.5
+        """
+        file = io.StringIO(samplefile)
+        t1 = read_tab_file(file)
+        file = io.StringIO(samplefile)
+        t2 = read_tab_file(file)
+        self.assertEqual(t1.domain[0], t2.domain[0])
+
     def test_reuse_variables(self):
         file1 = io.StringIO("\n".join("xd dbac"))
         t1 = read_tab_file(file1)