Repository: vaexio/vaex

Issue:
[BUG-REPORT]
Thank you for reaching out and helping us improve Vaex!

Before you submit a new Issue, please read through the [documentation](https://docs.vaex.io/en/latest/). Also, make sure you search through the Open and Closed Issues - your problem may already be discussed or addressed.

**Description**
vaex.open() can't accept list of file paths string as the documentation states, it fails in the aliases check (cant hash a list)


Code:
 `df = vaex.open(['./my_file.hdf5'])`

Error:
TypeError: unhashable type: 'list'

Code line problem:
Line 147  @__init__.py :` if path in aliases:`

Python can't use a list as a dictionary key, so we have an error



Discussion:
Can you please give a code example, and answer the questions that the form requires. Help us help you kind of thing. 
Also please add a meaningful title, so other people with similar issues can find this. 
Thanks.
So I think the error is clear.. `vaex.open` does not support lists... You can use glob expressions instead. If you want to pass a list of files to open, please use `vaex.open_many()`. 

Please reopen if the issue persists.
Yap, this is clear, but the documentation states a list of file is OK. 
So its not clear if its a bug, or wrong documentation:

> or list path (str) – local or absolute path to file, or glob string, or list of paths
Yeah, I think the docs are incorrect - or there was something that was meant to be implemented, but it is not there yet. Let's keep this open until it is fixed (documentation or implementation).

Resolving patch:
diff --git a/packages/vaex-core/vaex/__init__.py b/packages/vaex-core/vaex/__init__.py
--- a/packages/vaex-core/vaex/__init__.py
+++ b/packages/vaex-core/vaex/__init__.py
@@ -159,82 +159,88 @@ def open(path, convert=False, shuffle=False, fs_options={}, fs=None, *args, **kw
     import vaex
     import vaex.convert
     try:
-        path = vaex.file.stringyfy(path)
-        if path in aliases:
-            path = aliases[path]
-        path = vaex.file.stringyfy(path)
-        if path.startswith("http://") or path.startswith("ws://") or \
-           path.startswith("vaex+http://") or path.startswith("vaex+ws://"):  # TODO: think about https and wss
-            server, name = path.rsplit("/", 1)
-            url = urlparse(path)
-            if '?' in name:
-                name = name[:name.index('?')]
-            extra_args = {key: values[0] for key, values in parse_qs(url.query).items()}
-            if 'token' in extra_args:
-                kwargs['token'] = extra_args['token']
-            if 'token_trusted' in extra_args:
-                kwargs['token_trusted'] = extra_args['token_trusted']
-            client = vaex.connect(server, **kwargs)
-            return client[name]
-        if path.startswith("cluster"):
-            import vaex.enterprise.distributed
-            return vaex.enterprise.distributed.open(path, *args, **kwargs)
+        if not isinstance(path, (list, tuple)):
+            # remote and clusters only support single path, not a list
+            path = vaex.file.stringyfy(path)
+            if path in aliases:
+                path = aliases[path]
+            path = vaex.file.stringyfy(path)
+            if path.startswith("http://") or path.startswith("ws://") or \
+            path.startswith("vaex+http://") or path.startswith("vaex+ws://"):  # TODO: think about https and wss
+                server, name = path.rsplit("/", 1)
+                url = urlparse(path)
+                if '?' in name:
+                    name = name[:name.index('?')]
+                extra_args = {key: values[0] for key, values in parse_qs(url.query).items()}
+                if 'token' in extra_args:
+                    kwargs['token'] = extra_args['token']
+                if 'token_trusted' in extra_args:
+                    kwargs['token_trusted'] = extra_args['token_trusted']
+                client = vaex.connect(server, **kwargs)
+                return client[name]
+            if path.startswith("cluster"):
+                import vaex.enterprise.distributed
+                return vaex.enterprise.distributed.open(path, *args, **kwargs)
+
+        import vaex.file
+        import glob
+        if isinstance(path, str):
+            paths = [path]
         else:
-            import vaex.file
-            import glob
-            if isinstance(path, str):
-                paths = [path]
+            paths = path
+        filenames = []
+        for path in paths:
+            path = vaex.file.stringyfy(path)
+            if path in aliases:
+                path = aliases[path]
+            path = vaex.file.stringyfy(path)
+            naked_path, options = vaex.file.split_options(path)
+            if glob.has_magic(naked_path):
+                filenames.extend(list(sorted(vaex.file.glob(path, **kwargs))))
             else:
-                paths = path
-            filenames = []
-            for path in paths:
-                naked_path, options = vaex.file.split_options(path)
-                if glob.has_magic(naked_path):
-                    filenames.extend(list(sorted(vaex.file.glob(path, **kwargs))))
-                else:
-                    filenames.append(path)
-            df = None
-            if len(filenames) == 0:
-                raise IOError(f'File pattern did not match anything {path}')
-            filename_hdf5 = vaex.convert._convert_name(filenames, shuffle=shuffle)
-            filename_hdf5_noshuffle = vaex.convert._convert_name(filenames, shuffle=False)
-            if len(filenames) == 1:
-                path = filenames[0]
-                # # naked_path, _ = vaex.file.split_options(path, fs_options)
-                _, ext, _ = vaex.file.split_ext(path)
-                if ext == '.csv':  # special case for csv
-                    return vaex.from_csv(path, fs_options=fs_options, fs=fs, convert=convert, **kwargs)
+                filenames.append(path)
+        df = None
+        if len(filenames) == 0:
+            raise IOError(f'File pattern did not match anything {path}')
+        filename_hdf5 = vaex.convert._convert_name(filenames, shuffle=shuffle)
+        filename_hdf5_noshuffle = vaex.convert._convert_name(filenames, shuffle=False)
+        if len(filenames) == 1:
+            path = filenames[0]
+            # # naked_path, _ = vaex.file.split_options(path, fs_options)
+            _, ext, _ = vaex.file.split_ext(path)
+            if ext == '.csv':  # special case for csv
+                return vaex.from_csv(path, fs_options=fs_options, fs=fs, convert=convert, **kwargs)
+            if convert:
+                path_output = convert if isinstance(convert, str) else filename_hdf5
+                vaex.convert.convert(
+                    path_input=path, fs_options_input=fs_options, fs_input=fs,
+                    path_output=path_output, fs_options_output=fs_options, fs_output=fs,
+                    *args, **kwargs
+                )
+                ds = vaex.dataset.open(path_output, fs_options=fs_options, fs=fs, **kwargs)
+            else:
+                ds = vaex.dataset.open(path, fs_options=fs_options, fs=fs, **kwargs)
+            df = vaex.from_dataset(ds)
+            if df is None:
+                if os.path.exists(path):
+                    raise IOError('Could not open file: {}, did you install vaex-hdf5? Is the format supported?'.format(path))
+        elif len(filenames) > 1:
+            if convert not in [True, False]:
+                filename_hdf5 = convert
+            else:
+                filename_hdf5 = vaex.convert._convert_name(filenames, shuffle=shuffle)
+            if os.path.exists(filename_hdf5) and convert:  # also check mtime
+                df = vaex.open(filename_hdf5)
+            else:
+                dfs = []
+                for filename in filenames:
+                    dfs.append(vaex.open(filename, convert=bool(convert), shuffle=shuffle, **kwargs))
+                df = vaex.concat(dfs)
                 if convert:
-                    path_output = convert if isinstance(convert, str) else filename_hdf5
-                    vaex.convert.convert(
-                        path_input=path, fs_options_input=fs_options, fs_input=fs,
-                        path_output=path_output, fs_options_output=fs_options, fs_output=fs,
-                        *args, **kwargs
-                    )
-                    ds = vaex.dataset.open(path_output, fs_options=fs_options, fs=fs, **kwargs)
-                else:
-                    ds = vaex.dataset.open(path, fs_options=fs_options, fs=fs, **kwargs)
-                df = vaex.from_dataset(ds)
-                if df is None:
-                    if os.path.exists(path):
-                        raise IOError('Could not open file: {}, did you install vaex-hdf5? Is the format supported?'.format(path))
-            elif len(filenames) > 1:
-                if convert not in [True, False]:
-                    filename_hdf5 = convert
-                else:
-                    filename_hdf5 = vaex.convert._convert_name(filenames, shuffle=shuffle)
-                if os.path.exists(filename_hdf5) and convert:  # also check mtime
+                    if shuffle:
+                        df = df.shuffle()
+                    df.export_hdf5(filename_hdf5)
                     df = vaex.open(filename_hdf5)
-                else:
-                    dfs = []
-                    for filename in filenames:
-                        dfs.append(vaex.open(filename, convert=bool(convert), shuffle=shuffle, **kwargs))
-                    df = vaex.concat(dfs)
-                    if convert:
-                        if shuffle:
-                            df = df.shuffle()
-                        df.export_hdf5(filename_hdf5)
-                        df = vaex.open(filename_hdf5)
 
         if df is None:
             raise IOError('Unknown error opening: {}'.format(path))


Test patch:
diff --git a/tests/open_test.py b/tests/open_test.py
--- a/tests/open_test.py
+++ b/tests/open_test.py
@@ -12,6 +12,7 @@
     pytest.skip("skipping windows, since it has issues re-opening files", allow_module_level=True)
 
 csv2 = os.path.join(path, 'data', 'small2.csv')
+csv3 = os.path.join(path, 'data', 'small2.csv')
 h51 = os.path.join(path, 'data', 'small2.csv.hdf5')
 h52 = os.path.join(path, 'data', 'small3.csv.hdf5')
 target_path = os.path.join(path, 'data', 'output')
@@ -98,6 +99,13 @@ def test_open():
     assert not os.path.exists(target)
 
 
+def test_open_list():
+    df2 = vaex.open(csv2)
+    df3 = vaex.open(csv3)
+    df = vaex.open([csv2, csv3])
+    assert df.x.tolist() == (df2.x.tolist() + df3.x.tolist())
+
+
 def _cleanup_generated_files():
     for hdf5_file in glob.glob(os.path.join(path, 'data', '*.yaml')):
         os.remove(hdf5_file)