Repository: iterative/dvc

Issue:
hdfs: pyarrow.hdfs.connect is deprecated
## Bug Report
Since 2.0.0, `pyarrow.hdfs.connect` is deprecated, as seen on [this GHA log](https://github.com/iterative/dvc/runs/1415667578?check_suite_focus=true#step:7:3952), and recommends to use [`pyarrow.fs.HadoopFileSystem`](http://arrow.apache.org/docs/python/generated/pyarrow.fs.HadoopFileSystem.html).

```
home/runner/work/dvc/dvc/dvc/tree/pool.py:53: DeprecationWarning: pyarrow.hdfs.connect is deprecated as of 2.0.0, please use pyarrow.fs.HadoopFileSystem instead.
    return self._conn_func(*self._conn_args, **self._conn_kwargs)
```

Here are the usages:
https://github.com/iterative/dvc/blob/e1b82c5222930c55886ca16a48c3a223d05b4af0/dvc/tree/hdfs.py#L51-L56
https://github.com/iterative/dvc/blob/ca2e6a004cb6268c7b62f79ba22b55c5b81d8984/tests/remotes/hdfs.py#L20
https://github.com/iterative/dvc/blob/ca2e6a004cb6268c7b62f79ba22b55c5b81d8984/tests/remotes/hdfs.py#L128



Discussion:
If no one's working on it, I'd like to try doing this issue.
@jwokaty Sure, please feel free to give it a shot. Thanks for looking into it! :pray: 
@efiop fixed at #4936 
@kgritesh, it would have been better if you'd comment here before creating a PR, given @jwokaty is interested in working on this.
@skshetry / @jwokaty  sorry for this. I somehow missed the comment that someone else has picked it up. 

Resolving patch:
diff --git a/dvc/tree/hdfs.py b/dvc/tree/hdfs.py
--- a/dvc/tree/hdfs.py
+++ b/dvc/tree/hdfs.py
@@ -46,10 +46,17 @@ def __init__(self, repo, config):
 
     @staticmethod
     def hdfs(path_info):
-        import pyarrow
+        import pyarrow.fs
+
+        # NOTE: HadoopFileSystem is not meant to be closed by us and doesn't
+        # have close() method or alternative, so we add a noop one to satisfy
+        # our connection pool.
+        class HDFSConnection(pyarrow.fs.HadoopFileSystem):
+            def close(self):
+                pass
 
         return get_connection(
-            pyarrow.hdfs.connect,
+            HDFSConnection,
             path_info.host,
             path_info.port,
             user=path_info.user,
@@ -61,7 +68,7 @@ def open(self, path_info, mode="r", encoding=None):
 
         try:
             with self.hdfs(path_info) as hdfs, closing(
-                hdfs.open(path_info.path, mode="rb")
+                hdfs.open_input_stream(path_info.path)
             ) as fd:
                 if mode == "rb":
                     yield fd
@@ -78,31 +85,51 @@ def exists(self, path_info, use_dvcignore=True):
         assert not isinstance(path_info, list)
         assert path_info.scheme == "hdfs"
         with self.hdfs(path_info) as hdfs:
-            return hdfs.exists(path_info.path)
+            import pyarrow.fs
 
-    def walk_files(self, path_info, **kwargs):
-        if not self.exists(path_info):
+            file_info = hdfs.get_file_info(path_info.path)
+            return file_info.type != pyarrow.fs.FileType.NotFound
+
+    def _walk(self, hdfs, root, topdown=True):
+        import posixpath
+
+        from pyarrow.fs import FileSelector, FileType
+
+        dirs = deque()
+        nondirs = deque()
+
+        selector = FileSelector(root)
+        for entry in hdfs.get_file_info(selector):
+            if entry.type == FileType.Directory:
+                dirs.append(entry.base_name)
+            else:
+                nondirs.append(entry.base_name)
+
+        if topdown:
+            yield root, dirs, nondirs
+
+        for dname in dirs:
+            # NOTE: posixpath.join() is slower
+            yield from self._walk(
+                hdfs, f"{root}{posixpath.sep}{dname}", topdown=topdown
+            )
+
+        if not topdown:
+            yield root, dirs, nondirs
+
+    def walk(self, path_info, **kwargs):
+        if not self.isdir(path_info):
             return
 
-        root = path_info.path
-        dirs = deque([root])
+        with self.hdfs(path_info) as hdfs:
+            for root, dnames, fnames in self._walk(
+                hdfs, path_info.path, **kwargs
+            ):
+                yield path_info.replace(path=root), dnames, fnames
 
-        with self.hdfs(self.path_info) as hdfs:
-            if not hdfs.exists(root):
-                return
-            while dirs:
-                try:
-                    entries = hdfs.ls(dirs.pop(), detail=True)
-                    for entry in entries:
-                        if entry["kind"] == "directory":
-                            dirs.append(urlparse(entry["name"]).path)
-                        elif entry["kind"] == "file":
-                            path = urlparse(entry["name"]).path
-                            yield path_info.replace(path=path)
-                except OSError:
-                    # When searching for a specific prefix pyarrow raises an
-                    # exception if the specified cache dir does not exist
-                    pass
+    def walk_files(self, path_info, **kwargs):
+        for root, _, fnames in self.walk(path_info):
+            yield from (root / fname for fname in fnames)
 
     def remove(self, path_info):
         if path_info.scheme != "hdfs":
@@ -111,34 +138,49 @@ def remove(self, path_info):
         if self.exists(path_info):
             logger.debug(f"Removing {path_info.path}")
             with self.hdfs(path_info) as hdfs:
-                hdfs.rm(path_info.path)
+                import pyarrow.fs
+
+                file_info = hdfs.get_file_info(path_info.path)
+                if file_info.type == pyarrow.fs.FileType.Directory:
+                    hdfs.delete_dir(path_info.path)
+                else:
+                    hdfs.delete_file(path_info.path)
 
     def makedirs(self, path_info):
         with self.hdfs(path_info) as hdfs:
-            # NOTE: hdfs.mkdir creates parents by default
-            hdfs.mkdir(path_info.path)
+            # NOTE: fs.create_dir creates parents by default
+            hdfs.create_dir(path_info.path)
 
     def copy(self, from_info, to_info, **_kwargs):
+        # NOTE: hdfs.copy_file is not supported yet in pyarrow
         with self.hdfs(to_info) as hdfs:
             # NOTE: this is how `hadoop fs -cp` works too: it copies through
             # your local machine.
-            with hdfs.open(from_info.path, "rb") as from_fobj:
+            with closing(hdfs.open_input_stream(from_info.path)) as from_fobj:
                 tmp_info = to_info.parent / tmp_fname(to_info.name)
                 try:
-                    with hdfs.open(tmp_info.path, "wb") as tmp_fobj:
-                        tmp_fobj.upload(from_fobj)
-                    hdfs.rename(tmp_info.path, to_info.path)
+                    with closing(
+                        hdfs.open_output_stream(tmp_info.path)
+                    ) as tmp_fobj:
+                        tmp_fobj.write(from_fobj.read())
+                    hdfs.move(tmp_info.path, to_info.path)
                 except Exception:
                     self.remove(tmp_info)
                     raise
 
     def isfile(self, path_info):
         with self.hdfs(path_info) as hdfs:
-            return hdfs.isfile(path_info.path)
+            import pyarrow.fs
+
+            file_info = hdfs.get_file_info(path_info.path)
+            return file_info.type == pyarrow.fs.FileType.File
 
     def isdir(self, path_info):
         with self.hdfs(path_info) as hdfs:
-            return hdfs.isdir(path_info.path)
+            import pyarrow.fs
+
+            file_info = hdfs.get_file_info(path_info.path)
+            return file_info.type == pyarrow.fs.FileType.Directory
 
     def hadoop_fs(self, cmd, user=None):
         cmd = "hadoop fs -" + cmd
@@ -182,7 +224,8 @@ def get_file_hash(self, path_info):
         )
 
         with self.hdfs(path_info) as hdfs:
-            hash_info.size = hdfs.info(path_info.path)["size"]
+            file_info = hdfs.get_file_info(path_info.path)
+            hash_info.size = file_info.size
 
         return hash_info
 
@@ -200,14 +243,16 @@ def _upload(
                     total=total,
                     disable=no_progress_bar,
                 ) as wrapped:
-                    hdfs.upload(tmp_file, wrapped)
-            hdfs.rename(tmp_file, to_info.path)
+                    with hdfs.open_output_stream(tmp_file) as sobj:
+                        sobj.write(wrapped.read())
+            hdfs.move(tmp_file, to_info.path)
 
     def _download(
         self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
     ):
         with self.hdfs(from_info) as hdfs:
-            total = hdfs.info(from_info.path)["size"]
+            file_info = hdfs.get_file_info(from_info.path)
+            total = file_info.size
             with open(to_file, "wb+") as fobj:
                 with Tqdm.wrapattr(
                     fobj,
@@ -216,4 +261,5 @@ def _download(
                     total=total,
                     disable=no_progress_bar,
                 ) as wrapped:
-                    hdfs.download(from_info.path, wrapped)
+                    with hdfs.open_input_stream(from_info.path) as sobj:
+                        wrapped.write(sobj.read())


Test patch:
diff --git a/tests/remotes/hdfs.py b/tests/remotes/hdfs.py
--- a/tests/remotes/hdfs.py
+++ b/tests/remotes/hdfs.py
@@ -1,6 +1,7 @@
 import locale
 import os
 import platform
+import subprocess
 import uuid
 from contextlib import contextmanager
 
@@ -15,25 +16,31 @@
 class HDFS(Base, URLInfo):  # pylint: disable=abstract-method
     @contextmanager
     def _hdfs(self):
-        import pyarrow
+        import pyarrow.fs
 
-        conn = pyarrow.hdfs.connect(self.host, self.port)
-        try:
-            yield conn
-        finally:
-            conn.close()
+        conn = pyarrow.fs.HadoopFileSystem(self.host, self.port)
+        yield conn
 
     def is_file(self):
         with self._hdfs() as _hdfs:
-            return _hdfs.isfile(self.path)
+            import pyarrow.fs
+
+            file_info = _hdfs.get_file_info(self.path)
+            return file_info.type == pyarrow.fs.FileType.File
 
     def is_dir(self):
         with self._hdfs() as _hdfs:
-            return _hdfs.isfile(self.path)
+            import pyarrow.fs
+
+            file_info = _hdfs.get_file_info(self.path)
+            return file_info.type == pyarrow.fs.FileType.Directory
 
     def exists(self):
         with self._hdfs() as _hdfs:
-            return _hdfs.exists(self.path)
+            import pyarrow.fs
+
+            file_info = _hdfs.get_file_info(self.path)
+            return file_info.type != pyarrow.fs.FileType.NotFound
 
     def mkdir(self, mode=0o777, parents=False, exist_ok=False):
         assert mode == 0o777
@@ -41,13 +48,12 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
         assert not exist_ok
 
         with self._hdfs() as _hdfs:
-            # NOTE: hdfs.mkdir always creates parents
-            _hdfs.mkdir(self.path)
+            # NOTE: fs.create_dir creates parents by default
+            _hdfs.create_dir(self.path)
 
     def write_bytes(self, contents):
         with self._hdfs() as _hdfs:
-            # NOTE: hdfs.open only supports 'rb', 'wb' or 'ab'
-            with _hdfs.open(self.path, "wb") as fobj:
+            with _hdfs.open_output_stream(self.path) as fobj:
                 fobj.write(contents)
 
     def write_text(self, contents, encoding=None, errors=None):
@@ -58,8 +64,7 @@ def write_text(self, contents, encoding=None, errors=None):
 
     def read_bytes(self):
         with self._hdfs() as _hdfs:
-            # NOTE: hdfs.open only supports 'rb', 'wb' or 'ab'
-            with _hdfs.open(self.path, "rb") as fobj:
+            with _hdfs.open_input_stream(self.path) as fobj:
                 return fobj.read()
 
     def read_text(self, encoding=None, errors=None):
@@ -115,10 +120,19 @@ def _get(url, tar, target):
     os.environ["HADOOP_HOME"] = hadoop_home
     os.environ["PATH"] += f":{hadoop_home}/bin:{hadoop_home}/sbin"
 
+    # NOTE: must set CLASSPATH to connect using pyarrow.fs.HadoopFileSystem
+    result = subprocess.run(
+        [f"{hadoop_home}/bin/hdfs", "classpath", "--glob"],
+        universal_newlines=True,
+        stdout=subprocess.PIPE,
+        check=False,
+    )
+    os.environ["CLASSPATH"] = result.stdout
+
 
 @pytest.fixture(scope="session")
 def hdfs_server(hadoop, docker_compose, docker_services):
-    import pyarrow
+    import pyarrow.fs
 
     port = docker_services.port_for("hdfs", 8020)
     web_port = docker_services.port_for("hdfs", 50070)
@@ -127,12 +141,9 @@ def _check():
         try:
             # NOTE: just connecting or even opening something is not enough,
             # we need to make sure that we are able to write something.
-            conn = pyarrow.hdfs.connect("127.0.0.1", port)
-            try:
-                with conn.open(str(uuid.uuid4()), "wb") as fobj:
-                    fobj.write(b"test")
-            finally:
-                conn.close()
+            conn = pyarrow.fs.HadoopFileSystem("hdfs://127.0.0.1", port)
+            with conn.open_output_stream(str(uuid.uuid4())) as fobj:
+                fobj.write(b"test")
             return True
         except (pyarrow.ArrowException, OSError):
             return False