Repository: fsspec/filesystem_spec (Python)

Issue:
Regression: LocalFileSystem now tries to resolve links
<details>
<summary> Test Script </summary>

```py
import contextlib
import os

import fsspec

def cleanup(*files):
    for file in files:
        with contextlib.suppress(FileNotFoundError):
            os.unlink(file)

cleanup("/tmp/test", "/tmp/link")


# Create a file, symlink it to another path and
# delete the original
with open("/tmp/test", "w") as stream:
    stream.write("hey")
os.symlink("/tmp/test", "/tmp/link")
os.unlink("/tmp/test")

fs = fsspec.filesystem("file")

print("(fsspec) /tmp/test exists:", fs.exists("/tmp/test"))
print("(fsspec) /tmp/link exists:", fs.exists("/tmp/link"))

print("(os) /tmp/test exists:", os.path.exists("/tmp/test"))
print("(os) /tmp/link exists:", os.path.exists("/tmp/link"))
```

</details>

With the script above, `fsspec==2021.07.0` returns `True` for `fs.exists()` on a symlink where the original file it is linked to is deleted. After https://github.com/intake/filesystem_spec/commit/63a668fb4a60d41efc66c5f6445b6a500d5e4396, it now returns `False` since it tries to resolve the links via `follow_links=True`. 

I personally find the change to make sense, considering this is how the `os.path.exists` behaves but I believe it should be explicitly mentioned in the changelog and as well as an option to opt-out from it. Perhaps a keyword option called `follow_symlinks=...` to the `info` and `exists` or a method called `lexists()` (similiar to [os.path.lexists](https://docs.python.org/3/library/os.path.html#os.path.lexists)). 


Discussion:
Would be happy to see `lexists` added. I suppose it would only make any sense for local and maybe ssh.
Is that an OK plan? Should we close this?
The issue is not resolved yet, so I don't think we should close it. I'll try to come up with a patch



Patch:
diff --git a/fsspec/implementations/local.py b/fsspec/implementations/local.py
index 202f6036e..ee7756063 100644
--- a/fsspec/implementations/local.py
+++ b/fsspec/implementations/local.py
@@ -105,6 +105,9 @@ def info(self, path, **kwargs):
                 result["size"] = 0
         return result
 
+    def lexists(self, path, **kwargs):
+        return osp.lexists(path)
+
     def cp_file(self, path1, path2, **kwargs):
         path1 = self._strip_protocol(path1).rstrip("/")
         path2 = self._strip_protocol(path2).rstrip("/")
diff --git a/fsspec/spec.py b/fsspec/spec.py
index e06b82f2e..80562ee80 100644
--- a/fsspec/spec.py
+++ b/fsspec/spec.py
@@ -579,6 +579,11 @@ def exists(self, path, **kwargs):
             # any exception allowed bar FileNotFoundError?
             return False
 
+    def lexists(self, path, **kwargs):
+        """If there is a file at the given path (including
+        broken links)"""
+        return self.exists(path)
+
     def info(self, path, **kwargs):
         """Give details of entry at path
 


Test patch:
diff --git a/fsspec/implementations/tests/test_local.py b/fsspec/implementations/tests/test_local.py
index 389b8c133..2779018d8 100644
--- a/fsspec/implementations/tests/test_local.py
+++ b/fsspec/implementations/tests/test_local.py
@@ -527,6 +527,35 @@ def test_linked_files(tmpdir):
         assert f.read() == data
 
 
+def test_linked_files_exists(tmpdir):
+    origin = tmpdir / "original"
+    copy_file = tmpdir / "copy"
+
+    fs = LocalFileSystem()
+    fs.touch(origin)
+
+    try:
+        os.symlink(origin, copy_file)
+    except OSError:
+        if WIN:
+            pytest.xfail("Ran on win without admin permissions")
+        else:
+            raise
+
+    assert fs.exists(copy_file)
+    assert fs.lexists(copy_file)
+
+    os.unlink(origin)
+
+    assert not fs.exists(copy_file)
+    assert fs.lexists(copy_file)
+
+    os.unlink(copy_file)
+
+    assert not fs.exists(copy_file)
+    assert not fs.lexists(copy_file)
+
+
 def test_linked_directories(tmpdir):
     tmpdir = str(tmpdir)