Repository: palantir/python-language-server
Pull request #99 (2017-08-20 13:53:25)

Better support for Windows


Patch:
diff --git a/appveyor.yml b/appveyor.yml
index 4c23835..65adda6 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -14,3 +14,6 @@ build: false  # Not a C# project
 
 test_script:
   - C:\Python27\scripts\tox
+
+cache:
+  - '%APPDATA%\pip\Cache'
diff --git a/pyls/__init__.py b/pyls/__init__.py
index 93393d1..12173a1 100644
--- a/pyls/__init__.py
+++ b/pyls/__init__.py
@@ -1,4 +1,5 @@
 # Copyright 2017 Palantir Technologies, Inc.
+import os
 from future.standard_library import install_aliases
 import pluggy
 from ._version import get_versions
@@ -11,3 +12,5 @@ PYLS = 'pyls'
 
 hookspec = pluggy.HookspecMarker(PYLS)
 hookimpl = pluggy.HookimplMarker(PYLS)
+
+IS_WIN = os.name == 'nt'
diff --git a/pyls/config.py b/pyls/config.py
index 8f88b02..3b21482 100644
--- a/pyls/config.py
+++ b/pyls/config.py
@@ -3,9 +3,8 @@ from configparser import RawConfigParser
 import logging
 import os
 import pluggy
-from urllib.parse import urlparse, unquote
 
-from . import hookspecs, PYLS
+from . import hookspecs, uris, PYLS
 
 log = logging.getLogger(__name__)
 
@@ -35,7 +34,7 @@ class Config(object):
         return self._root_uri
 
     def find_parents(self, path, names):
-        root_path = unquote(urlparse(self._root_uri).path)
+        root_path = uris.to_fs_path(self._root_uri)
         return find_parents(root_path, path, names)
 
 
diff --git a/pyls/language_server.py b/pyls/language_server.py
index ed3f251..cfd187b 100644
--- a/pyls/language_server.py
+++ b/pyls/language_server.py
@@ -2,9 +2,8 @@
 import logging
 import re
 import socketserver
+from . import uris
 from .server import JSONRPCServer
-from urllib.parse import urljoin
-from urllib.request import pathname2url
 
 log = logging.getLogger(__name__)
 
@@ -92,7 +91,7 @@ class LanguageServer(MethodJSONRPCServer):
             self.root_uri = kwargs['rootUri']
         elif 'rootPath' in kwargs:
             root_path = kwargs['rootPath']
-            self.root_uri = urljoin(u'file://', pathname2url(root_path))
+            self.root_uri = uris.from_fs_path(root_path)
         else:
             self.root_uri = ''
         self.init_opts = kwargs.get('initializationOptions')
diff --git a/pyls/plugins/definition.py b/pyls/plugins/definition.py
index 0e79a79..53431a0 100644
--- a/pyls/plugins/definition.py
+++ b/pyls/plugins/definition.py
@@ -1,6 +1,6 @@
 # Copyright 2017 Palantir Technologies, Inc.
 import logging
-from pyls import hookimpl, workspace
+from pyls import hookimpl, uris
 
 log = logging.getLogger(__name__)
 
@@ -14,7 +14,7 @@ def pyls_definitions(document, position):
     ]
 
     return [{
-        'uri': workspace.get_uri_like(document.uri, d.module_path) if d.module_path else document.uri,
+        'uri': uris.uri_with(document.uri, path=d.module_path) if d.module_path else document.uri,
         'range': {
             'start': {'line': d.line - 1, 'character': d.column},
             'end': {'line': d.line - 1, 'character': d.column + len(d.name)}
diff --git a/pyls/plugins/references.py b/pyls/plugins/references.py
index c77e26c..7292c8b 100644
--- a/pyls/plugins/references.py
+++ b/pyls/plugins/references.py
@@ -1,6 +1,6 @@
 # Copyright 2017 Palantir Technologies, Inc.
 import logging
-from pyls import hookimpl, workspace
+from pyls import hookimpl, uris
 
 log = logging.getLogger(__name__)
 
@@ -15,7 +15,7 @@ def pyls_references(document, position, exclude_declaration=False):
         usages = [d for d in usages if not d.is_definition()]
 
     return [{
-        'uri': workspace.get_uri_like(document.uri, d.module_path) if d.module_path else document.uri,
+        'uri': uris.uri_with(document.uri, path=d.module_path) if d.module_path else document.uri,
         'range': {
             'start': {'line': d.line - 1, 'character': d.column},
             'end': {'line': d.line - 1, 'character': d.column + len(d.name)}
diff --git a/pyls/uris.py b/pyls/uris.py
new file mode 100644
index 0000000..f0e2feb
--- /dev/null
+++ b/pyls/uris.py
@@ -0,0 +1,129 @@
+# Copyright 2017 Palantir Technologies, Inc.
+"""A collection of URI utilities with logic built on the VSCode URI library.
+
+https://github.com/Microsoft/vscode-uri/blob/e59cab84f5df6265aed18ae5f43552d3eef13bb9/lib/index.ts
+"""
+import re
+from urllib import parse
+from pyls import IS_WIN
+
+RE_DRIVE_LETTER_PATH = re.compile(r'^\/[a-zA-z]:')
+
+
+def urlparse(uri):
+    """Parse and decode the parts of a URI."""
+    scheme, netloc, path, params, query, fragment = parse.urlparse(uri)
+    return (
+        parse.unquote(scheme),
+        parse.unquote(netloc),
+        parse.unquote(path),
+        parse.unquote(params),
+        parse.unquote(query),
+        parse.unquote(fragment)
+    )
+
+
+def urlunparse(parts):
+    """Unparse and encode parts of a URI."""
+    scheme, netloc, path, params, query, fragment = parts
+
+    # Avoid encoding the windows drive letter colon
+    if RE_DRIVE_LETTER_PATH.match(path):
+        quoted_path = path[:3] + parse.quote(path[3:])
+    else:
+        quoted_path = parse.quote(path)
+
+    return parse.urlunparse((
+        parse.quote(scheme),
+        parse.quote(netloc),
+        quoted_path,
+        parse.quote(params),
+        parse.quote(query),
+        parse.quote(fragment)
+    ))
+
+
+def to_fs_path(uri):
+    """Returns the filesystem path of the given URI.
+
+    Will handle UNC paths and normalize windows drive letters to lower-case. Also
+    uses the platform specific path separator. Will *not* validate the path for
+    invalid characters and semantics. Will *not* look at the scheme of this URI.
+    """
+    # scheme://netloc/path;parameters?query#fragment
+    scheme, netloc, path, _params, _query, _fragment = urlparse(uri)
+
+    if netloc and path and scheme == 'file':
+        # unc path: file://shares/c$/far/boo
+        value = "//{netloc}{path}".format(netloc=netloc, path=path)
+
+    elif RE_DRIVE_LETTER_PATH.match(path):
+        # windows drive letter: file:///C:/far/boo
+        value = path[1].lower() + path[2:]
+
+    else:
+        # Other path
+        value = path
+
+    if IS_WIN:
+        value = value.replace('/', '\\')
+
+    return value
+
+
+def from_fs_path(path):
+    """Returns a URI for the given filesystem path."""
+    scheme = 'file'
+    params, query, fragment = '', '', ''
+    path, netloc = _normalize_win_path(path)
+    return urlunparse((scheme, netloc, path, params, query, fragment))
+
+
+def uri_with(uri, scheme=None, netloc=None, path=None, params=None, query=None, fragment=None):
+    """Return a URI with the given part(s) replaced.
+
+    Parts are decoded / encoded.
+    """
+    old_scheme, old_netloc, old_path, old_params, old_query, old_fragment = urlparse(uri)
+    path, _netloc = _normalize_win_path(path)
+    return urlunparse((
+        scheme or old_scheme,
+        netloc or old_netloc,
+        path or old_path,
+        params or old_params,
+        query or old_query,
+        fragment or old_fragment
+    ))
+
+
+def _normalize_win_path(path):
+    netloc = ''
+
+    # normalize to fwd-slashes on windows,
+    # on other systems bwd-slaches are valid
+    # filename character, eg /f\oo/ba\r.txt
+    if IS_WIN:
+        path = path.replace('\\', '/')
+
+    # check for authority as used in UNC shares
+    # or use the path as given
+    if path[:2] == '//':
+        idx = path.index('/', 2)
+        if idx == -1:
+            netloc = path[2:]
+        else:
+            netloc = path[2:idx]
+            path = path[idx:]
+    else:
+        path = path
+
+    # Ensure that path starts with a slash
+    # or that it is at least a slash
+    if not path.startswith('/'):
+        path = '/' + path
+
+    # Normalize drive paths to lower case
+    if RE_DRIVE_LETTER_PATH.match(path):
+        path = path[0] + path[1].lower() + path[2:]
+
+    return path, netloc
diff --git a/pyls/workspace.py b/pyls/workspace.py
index ed1f807..6b9e2c0 100644
--- a/pyls/workspace.py
+++ b/pyls/workspace.py
@@ -4,11 +4,10 @@ import logging
 import os
 import re
 import sys
-from urllib.parse import urlparse, urlunparse, quote, unquote
 
 import jedi
 
-from . import config, lsp
+from . import config, lsp, uris
 
 log = logging.getLogger(__name__)
 
@@ -24,19 +23,28 @@ class Workspace(object):
     M_SHOW_MESSAGE = 'window/showMessage'
 
     def __init__(self, root_uri, lang_server=None):
-        self._url_parsed = urlparse(root_uri)
-        self.root = unquote(self._url_parsed.path)
+        self._root_uri = root_uri
+        self._root_uri_scheme = uris.urlparse(self._root_uri)[0]
+        self._root_path = uris.to_fs_path(self._root_uri)
         self._docs = {}
         self._lang_server = lang_server
 
+    @property
+    def root_path(self):
+        return self._root_path
+
+    @property
+    def root_uri(self):
+        return self._root_uri
+
     def is_local(self):
-        return (self._url_parsed.scheme == '' or self._url_parsed.scheme == 'file') and os.path.exists(self.root)
+        return (self._root_uri_scheme == '' or self._root_uri_scheme == 'file') and os.path.exists(self._root_path)
 
     def get_document(self, doc_uri):
         return self._docs[doc_uri]
 
     def put_document(self, doc_uri, content, version=None):
-        path = unquote(urlparse(doc_uri).path)
+        path = uris.to_fs_path(doc_uri)
         self._docs[doc_uri] = Document(
             doc_uri, content, sys_path=self.syspath_for_path(path), version=version
         )
@@ -66,13 +74,13 @@ class Workspace(object):
         Since the workspace root may not be the root of the Python project we instead
         append the closest parent directory containing a setup.py file.
         """
-        files = config.find_parents(self.root, path, ['setup.py']) or []
+        files = config.find_parents(self._root_path, path, ['setup.py']) or []
         path = [os.path.dirname(setup_py) for setup_py in files]
         path.extend(sys.path)
         return path
 
     def is_in_workspace(self, path):
-        return not self.root or os.path.commonprefix((self.root, path))
+        return not self._root_path or os.path.commonprefix((self._root_path, path))
 
 
 class Document(object):
@@ -80,7 +88,7 @@ class Document(object):
     def __init__(self, uri, source=None, version=None, local=True, sys_path=None):
         self.uri = uri
         self.version = version
-        self.path = unquote(urlparse(uri).path)
+        self.path = uris.to_fs_path(uri)
         self.filename = os.path.basename(self.path)
 
         self._local = local
@@ -175,20 +183,3 @@ class Document(object):
             kwargs['line'] = position['line'] + 1
             kwargs['column'] = position['character']
         return jedi.Script(**kwargs)
-
-
-def get_uri_like(doc_uri, path):
-    """Replace the path in a uri. Little bit hacky!
-
-    Due to https://github.com/PythonCharmers/python-future/issues/273 we have to
-    cast all parts to the same type since jedi can return str and urlparse returns
-    unicode objects.
-    """
-    parts = list(urlparse(doc_uri))
-    if path[0] != '/' and ':' in path:  # fix path for windows
-        drivespec, path = path.split(':', 1)
-        path = '/' + drivespec + ':' + quote(path.replace('\\', '/'))
-    else:
-        path = quote(path)
-    parts[2] = path
-    return urlunparse([str(p) for p in parts])


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
index e69de29..d56b349 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright 2017 Palantir Technologies, Inc.
+import pytest
+from pyls import IS_WIN
+
+unix_only = pytest.mark.skipif(IS_WIN, reason="Unix only")
+windows_only = pytest.mark.skipif(not IS_WIN, reason="Windows only")
diff --git a/test/fixtures.py b/test/fixtures.py
index d746d16..bdac817 100644
--- a/test/fixtures.py
+++ b/test/fixtures.py
@@ -1,17 +1,14 @@
 # Copyright 2017 Palantir Technologies, Inc.
 import os
 import pytest
+from pyls import uris
 from pyls.config import Config
 from pyls.python_ls import PythonLanguageServer
 from pyls.workspace import Workspace
 from io import StringIO
 from urllib.parse import urljoin
-from urllib.request import pathname2url
 
 
-def path2uri(path):
-    return urljoin(u"file://", pathname2url(path))
-
 @pytest.fixture
 def pyls(tmpdir):
     """ Return an initialized python LS """
@@ -21,7 +18,7 @@ def pyls(tmpdir):
 
     ls.m_initialize(
         processId=1,
-        rootUri=path2uri(str(tmpdir)),
+        rootUri=uris.from_fs_path(str(tmpdir)),
         initializationOptions={}
     )
 
@@ -31,10 +28,10 @@ def pyls(tmpdir):
 @pytest.fixture
 def workspace():
     """Return a workspace."""
-    return Workspace(path2uri(os.path.dirname(__file__)))
+    return Workspace(uris.from_fs_path(os.path.dirname(__file__)))
 
 
 @pytest.fixture
 def config(workspace):
     """Return a config object."""
-    return Config(path2uri(os.path.dirname(__file__)), {})
+    return Config(workspace.root_uri, {})
diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py
index 5c30a21..50cf63b 100644
--- a/test/plugins/test_completion.py
+++ b/test/plugins/test_completion.py
@@ -1,8 +1,9 @@
 # Copyright 2017 Palantir Technologies, Inc.
+from pyls import uris
 from pyls.plugins.completion import pyls_completions
 from pyls.workspace import Document
 
-DOC_URI = __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
 print sys.stdin.read()
 
diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py
index 4683d25..1405546 100644
--- a/test/plugins/test_definitions.py
+++ b/test/plugins/test_definitions.py
@@ -1,8 +1,9 @@
 # Copyright 2017 Palantir Technologies, Inc.
+from pyls import uris
 from pyls.plugins.definition import pyls_definitions
 from pyls.workspace import Document
 
-DOC_URI = __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """def a():
     pass
 
diff --git a/test/plugins/test_format.py b/test/plugins/test_format.py
index 21b703b..d944b51 100644
--- a/test/plugins/test_format.py
+++ b/test/plugins/test_format.py
@@ -1,8 +1,9 @@
 # Copyright 2017 Palantir Technologies, Inc.
+from pyls import uris
 from pyls.plugins.format import pyls_format_document, pyls_format_range
 from pyls.workspace import Document
 
-DOC_URI = __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """A = [
     'h',   'w',
 
diff --git a/test/plugins/test_hover.py b/test/plugins/test_hover.py
index f64dc89..5d3baf8 100644
--- a/test/plugins/test_hover.py
+++ b/test/plugins/test_hover.py
@@ -1,8 +1,9 @@
 # Copyright 2017 Palantir Technologies, Inc.
+from pyls import uris
 from pyls.plugins.hover import pyls_hover
 from pyls.workspace import Document
 
-DOC_URI = __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """
 
 def main():
diff --git a/test/plugins/test_lint.py b/test/plugins/test_lint.py
index 2a5880f..eb8ff6a 100644
--- a/test/plugins/test_lint.py
+++ b/test/plugins/test_lint.py
@@ -2,11 +2,12 @@
 import os
 import shutil
 import tempfile
+from pyls import uris
 from pyls.config import Config
 from pyls.workspace import Document, Workspace
 from pyls.plugins import pycodestyle_lint, pyflakes_lint
 
-DOC_URI = __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
 
 def hello():
@@ -51,11 +52,11 @@ def test_pycodestyle_config():
     """
     # Create a workspace in tmp
     tmp = tempfile.mkdtemp()
-    workspace = Workspace(tmp)
-    doc_uri = 'file://' + tmp + '/' + 'test.py'
+    workspace = Workspace(uris.from_fs_path(tmp))
+    doc_uri = uris.from_fs_path(os.path.join(tmp, 'test.py'))
     workspace.put_document(doc_uri, DOC)
     doc = workspace.get_document(doc_uri)
-    config = Config(workspace.root, {})
+    config = Config(workspace.root_uri, {})
 
     # Make sure we get a warning for 'indentation contains tabs'
     diags = pycodestyle_lint.pyls_lint(config, workspace, doc)
diff --git a/test/plugins/test_references.py b/test/plugins/test_references.py
index cbad0fc..374657b 100644
--- a/test/plugins/test_references.py
+++ b/test/plugins/test_references.py
@@ -3,6 +3,7 @@ import os
 import pytest
 import shutil
 import tempfile
+from pyls import uris
 from pyls.workspace import Document, Workspace
 from pyls.plugins.references import pyls_references
 
@@ -23,13 +24,13 @@ Test1()
 @pytest.fixture
 def tmp_workspace():
     tmp = tempfile.mkdtemp()
-    workspace = Workspace(tmp)
+    workspace = Workspace(uris.from_fs_path(tmp))
 
     def create_file(name, content):
         fn = os.path.join(tmp, name)
         with open(fn, 'w') as f:
             f.write(content)
-        workspace.put_document('file://' + fn, content)
+        workspace.put_document(uris.from_fs_path(fn), content)
 
     create_file(DOC1_NAME, DOC1)
     create_file(DOC2_NAME, DOC2)
@@ -41,7 +42,7 @@ def tmp_workspace():
 def test_references(tmp_workspace):
     # Over 'Test1' in class Test1():
     position = {'line': 0, 'character': 8}
-    DOC1_URI = 'file://' + os.path.join(tmp_workspace.root, DOC1_NAME)
+    DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME))
     doc1 = Document(DOC1_URI)
 
     refs = pyls_references(doc1, position)
diff --git a/test/plugins/test_signature.py b/test/plugins/test_signature.py
index d5d7a90..da1c05b 100644
--- a/test/plugins/test_signature.py
+++ b/test/plugins/test_signature.py
@@ -1,8 +1,9 @@
 # Copyright 2017 Palantir Technologies, Inc.
+from pyls import uris
 from pyls.plugins.signature import pyls_signature_help
 from pyls.workspace import Document
 
-DOC_URI = __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
 
 def main(param1, param2):
diff --git a/test/plugins/test_symbols.py b/test/plugins/test_symbols.py
index a9982fa..3d8d47d 100644
--- a/test/plugins/test_symbols.py
+++ b/test/plugins/test_symbols.py
@@ -1,9 +1,10 @@
 # Copyright 2017 Palantir Technologies, Inc.
+from pyls import uris
 from pyls.plugins.symbols import pyls_document_symbols
 from pyls.lsp import SymbolKind
 from pyls.workspace import Document
 
-DOC_URI = __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
 
 a = 'hello'
diff --git a/test/test_document.py b/test/test_document.py
index 393e8d1..0a8c1f9 100644
--- a/test/test_document.py
+++ b/test/test_document.py
@@ -1,8 +1,10 @@
 # Copyright 2017 Palantir Technologies, Inc.
+import os
 import pytest
+from pyls import uris
 from pyls.workspace import Document
 
-DOC_URI = "file://" + __file__
+DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
 
 def main():
@@ -17,7 +19,6 @@ def doc():
 
 def test_document_props(doc):
     assert doc.uri == DOC_URI
-    assert doc.path == __file__
     assert doc.source == DOC
 
 
diff --git a/test/test_uris.py b/test/test_uris.py
new file mode 100644
index 0000000..37dae33
--- /dev/null
+++ b/test/test_uris.py
@@ -0,0 +1,50 @@
+# Copyright 2017 Palantir Technologies, Inc.
+import pytest
+from pyls import uris
+from test import unix_only, windows_only
+
+
+@unix_only
+@pytest.mark.parametrize('uri,path', [
+    ('file:///foo/bar#frag', '/foo/bar'),
+    ('file:/foo/bar#frag', '/foo/bar'),
+    ('file:/foo/space%20%3Fbar#frag', '/foo/space ?bar'),
+])
+def test_to_fs_path(uri, path):
+    assert uris.to_fs_path(uri) == path
+
+
+@windows_only
+@pytest.mark.parametrize('uri,path', [
+    ('file:///c:/far/boo', 'c:\\far\\boo'),
+    ('file:///C:/far/boo', 'c:\\far\\boo'),
+    ('file:///C:/far/space%20%3Fboo', 'c:\\far\\space ?boo'),
+])
+def test_win_to_fs_path(uri, path):
+    assert uris.to_fs_path(uri) == path
+
+
+@unix_only
+@pytest.mark.parametrize('path,uri', [
+    ('/foo/bar', 'file:///foo/bar'),
+    ('/foo/space ?bar', 'file:///foo/space%20%3Fbar'),
+])
+def test_from_fs_path(path, uri):
+    assert uris.from_fs_path(path) == uri
+
+
+@windows_only
+@pytest.mark.parametrize('path,uri', [
+    ('c:\\far\\boo', 'file:///c:/far/boo'),
+    ('C:\\far\\space ?boo', 'file:///c:/far/space%20%3Fboo')
+])
+def test_win_from_fs_path(path, uri):
+    assert uris.from_fs_path(path) == uri
+
+
+@pytest.mark.parametrize('uri,kwargs,new_uri', [
+    ('file:///foo/bar', {'path': '/baz/boo'}, 'file:///baz/boo'),
+    ('file:///D:/hello%20world.py', {'path': 'D:/hello universe.py'}, 'file:///d:/hello%20universe.py')
+])
+def test_uri_with(uri, kwargs, new_uri):
+    assert uris.uri_with(uri, **kwargs) == new_uri
diff --git a/test/test_workspace.py b/test/test_workspace.py
index a66c192..d7c462f 100644
--- a/test/test_workspace.py
+++ b/test/test_workspace.py
@@ -1,9 +1,9 @@
 # Copyright 2017 Palantir Technologies, Inc.
 import os
 import pytest
-from pyls import workspace
+from pyls import uris, workspace
 
-DOC_URI = 'file://' + __file__
+DOC_URI = uris.from_fs_path(__file__)
 
 
 def test_local(pyls):
@@ -34,16 +34,8 @@ def test_bad_get_document(pyls):
         pyls.workspace.get_document("BAD_URI")
 
 
-def test_uri_like():
-    assert workspace.get_uri_like('file:///some-path', '/my/path') == 'file:///my/path'
-    win_doc_uri = r'file:///D:/hello%20world.py'
-    win_doc_path = r'D:\hello world.py'
-    win_uri = workspace.get_uri_like(win_doc_uri, win_doc_path)
-    assert win_uri == win_doc_uri
-
-
 def test_non_root_project(pyls):
-    repo_root = os.path.join(pyls.workspace.root, 'repo-root')
+    repo_root = os.path.join(pyls.workspace.root_path, 'repo-root')
     os.mkdir(repo_root)
     project_root = os.path.join(repo_root, 'project-root')
     os.mkdir(project_root)
@@ -51,19 +43,7 @@ def test_non_root_project(pyls):
     with open(os.path.join(project_root, 'setup.py'), 'w+') as f:
         f.write('# setup.py')
 
-    test_uri = 'file://' + os.path.join(project_root, 'hello/test.py')
+    test_uri = uris.from_fs_path(os.path.join(project_root, 'hello/test.py'))
     pyls.workspace.put_document(test_uri, 'assert True')
     test_doc = pyls.workspace.get_document(test_uri)
     assert project_root in pyls.workspace.syspath_for_path(test_doc.path)
-
-
-def test_urlencoded_paths():
-    root_uri = "file:///Encoded%20%3FSpace/"
-    file_uri = root_uri + "test.py"
-
-    ws = workspace.Workspace(root_uri)
-    assert ws.root == "/Encoded ?Space/"
-
-    ws.put_document(file_uri, "")
-    doc = ws.get_document(file_uri)
-    assert doc.path == '/Encoded ?Space/test.py'