Skip to content

Feature/python3 support #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Python Language Server
======================

A Python 2.7 implementation of the `Language Server Protocol`_ making use of Jedi_, pycodestyle_, Pyflakes_ and YAPF_.
A Python 2.7 and 3.6 implementation of the `Language Server Protocol`_ making use of Jedi_, pycodestyle_, Pyflakes_ and YAPF_.

Features
--------
Expand Down
4 changes: 3 additions & 1 deletion pyls/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copyright 2017 Palantir Technologies, Inc.

from future.standard_library import install_aliases
from ._version import get_versions

install_aliases()
__version__ = get_versions()['version']
del get_versions
4 changes: 2 additions & 2 deletions pyls/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import logging
import time
import sys
import language_server
from python_ls import PythonLanguageServer
from . import language_server
from .python_ls import PythonLanguageServer


def add_arguments(parser):
Expand Down
23 changes: 14 additions & 9 deletions pyls/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
import logging
import re
import SocketServer
import socketserver

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -36,7 +36,7 @@ def to_rpc(self):
}


class JSONRPCServer(SocketServer.StreamRequestHandler, object):
class JSONRPCServer(socketserver.StreamRequestHandler, object):
""" Read/Write JSON RPC messages """

_msg_id = None
Expand All @@ -45,13 +45,18 @@ def __init__(self, rfile, wfile):
self.rfile = rfile
self.wfile = wfile

def shutdown(self):
# TODO: we should handle this much better
self.rfile.close()
self.wfile.close()

def handle(self):
# VSCode wants us to keep the connection open, so let's handle messages in a loop
while True:
# TODO: need to time out here eventually??
try:
self._handle_rpc_call()
except EOFError:
except Exception:
break

def handle_json(self, msg):
Expand All @@ -67,7 +72,9 @@ def handle_json(self, msg):
log.debug("Got message: %s", msg)

# Else pass the message with params as kwargs
return getattr(self, method)(**msg.get('params', {}))
# Params may be a dictionary or undefined, so force them to be a dictionary
params = msg.get('params', {}) or {}
return getattr(self, method)(**params)

def call(self, method, params=None):
""" Call a remote method, for now we ignore the response... """
Expand Down Expand Up @@ -101,15 +108,13 @@ def _handle_rpc_call(self):
response['error'] = e.to_rpc()
except Exception as e:
log.exception("Caught internal error")
err = JSONRPCError(JSONRPCError.INTERNAL_ERROR, str(e.message))
err = JSONRPCError(JSONRPCError.INTERNAL_ERROR, str(e))
response['error'] = err.to_rpc()

log.debug("Responding to msg %s with %s", self._msg_id, response)
self._write_message(response)

def _content_length(self, line):
if len(line) < 2 or line[-2:] != "\r\n":
raise ValueError("Line endings must be \\r\\n not %s")
if line.startswith("Content-Length: "):
_, value = line.split("Content-Length: ")
value = value.strip()
Expand All @@ -126,8 +131,8 @@ def _read_message(self):

content_length = self._content_length(line)

# Read until end of the beginning of the JSON request
while line and line != "\r\n":
# Blindly consume all header lines
while line and line.strip():
line = self.rfile.readline()

if not line:
Expand Down
12 changes: 6 additions & 6 deletions pyls/language_server.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
import SocketServer
from jsonrpc import JSONRPCServer
from workspace import Workspace
import socketserver
from .jsonrpc import JSONRPCServer
from .workspace import Workspace

log = logging.getLogger(__name__)


def start_tcp_lang_server(bind_addr, port, handler_class):
if not issubclass(handler_class, LanguageServer):
raise ValueError("Handler class must be a subclass of LanguageServer")
server = SocketServer.ThreadingTCPServer((bind_addr, port), handler_class)
server = socketserver.ThreadingTCPServer((bind_addr, port), handler_class)
try:
log.info("Serving %s on (%s, %s)", handler_class.__name__, bind_addr, port)
server.serve_forever()
Expand Down Expand Up @@ -65,7 +65,7 @@ def m___cancel_request(self, **kwargs):
pass

def m_shutdown(self, **kwargs):
self.server.shutdown()
self.shutdown()

def m_exit(self, **kwargs):
self.server.shutdown()
self.shutdown()
4 changes: 2 additions & 2 deletions pyls/providers/code_lens.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import JediProvider
from .base import JediProvider

log = logging.getLogger(__name__)

Expand All @@ -13,7 +13,7 @@ def run(self, doc_uri, position, exclude_declaration=False):

if exclude_declaration:
# Filter out if the usage is the actual declaration of the thing
usages = filter(lambda d: not d.is_definition(), usages)
usages = [d for d in usages if not d.is_definition()]

return [{
'uri': self.workspace.get_uri_like(doc_uri, d.module_path),
Expand Down
4 changes: 2 additions & 2 deletions pyls/providers/completion.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import JediProvider
from .base import JediProvider
from pyls.vscode import CompletionItemKind

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -30,7 +30,7 @@ def sort_text(definition):
"""
mod = definition.description.split(":", 1)[1].strip()

if mod.startswith("__"):
if definition.in_builtin_module():
# It's a builtin, put it last
return 'z' + definition.name

Expand Down
4 changes: 2 additions & 2 deletions pyls/providers/definition.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import JediProvider
from .base import JediProvider

log = logging.getLogger(__name__)

Expand All @@ -11,7 +11,7 @@ class JediDefinitionsProvider(JediProvider):
def run(self, doc_uri, position):
definitions = self.jedi_script(doc_uri, position).goto_definitions()

definitions = filter(lambda d: d.is_definition(), definitions)
definitions = [d for d in definitions if d.is_definition()]

return [{
'uri': self.workspace.get_uri_like(doc_uri, d.module_path),
Expand Down
2 changes: 1 addition & 1 deletion pyls/providers/format.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import BaseProvider
from .base import BaseProvider
from yapf.yapflib.yapf_api import FormatCode

log = logging.getLogger(__name__)
Expand Down
4 changes: 2 additions & 2 deletions pyls/providers/hover.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import JediProvider
from .base import JediProvider

log = logging.getLogger(__name__)

Expand All @@ -14,7 +14,7 @@ def run(self, doc_uri, position):
word = document.word_at_position(position)

# Find an exact match for a completion
completions = filter(lambda c: c.name == word, completions)
completions = [c for c in completions if c.name == word]

if len(completions) == 0:
# :(
Expand Down
4 changes: 2 additions & 2 deletions pyls/providers/lint.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Copyright 2017 Palantir Technologies, Inc.
from ConfigParser import RawConfigParser
from configparser import RawConfigParser
import logging
import pycodestyle
from pyflakes import api as pyflakes_api
from base import BaseProvider
from .base import BaseProvider

log = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions pyls/providers/references.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import JediProvider
from .base import JediProvider

log = logging.getLogger(__name__)

Expand All @@ -13,7 +13,7 @@ def run(self, doc_uri, position, exclude_declaration=False):

if exclude_declaration:
# Filter out if the usage is the actual declaration of the thing
usages = filter(lambda d: not d.is_definition(), usages)
usages = [d for d in usages if not d.is_definition()]

return [{
'uri': self.workspace.get_uri_like(doc_uri, d.module_path),
Expand Down
2 changes: 1 addition & 1 deletion pyls/providers/signature.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import JediProvider
from .base import JediProvider

log = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion pyls/providers/symbols.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from base import JediProvider
from .base import JediProvider
from pyls.vscode import SymbolKind

log = logging.getLogger(__name__)
Expand Down
22 changes: 11 additions & 11 deletions pyls/python_ls.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
from language_server import LanguageServer

from providers.completion import JediCompletionProvider
from providers.definition import JediDefinitionsProvider
from providers.format import YapfFormatter
from providers.lint import PyCodeStyleLinter, PyflakesLinter
from providers.hover import JediDocStringHoverProvider
from providers.references import JediReferencesProvider
from providers.signature import JediSignatureProvider
from providers.symbols import JediDocumentSymbolsProvider
from vscode import TextDocumentSyncKind
from .language_server import LanguageServer

from .providers.completion import JediCompletionProvider
from .providers.definition import JediDefinitionsProvider
from .providers.format import YapfFormatter
from .providers.lint import PyCodeStyleLinter, PyflakesLinter
from .providers.hover import JediDocStringHoverProvider
from .providers.references import JediReferencesProvider
from .providers.signature import JediSignatureProvider
from .providers.symbols import JediDocumentSymbolsProvider
from .vscode import TextDocumentSyncKind

log = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions pyls/workspace.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright 2017 Palantir Technologies, Inc.
import os
import re
from urlparse import urlparse, urlunparse
from urllib.parse import urlparse, urlunparse

# TODO: this is not the best e.g. we capture numbers
RE_START_WORD = re.compile('[A-Za-z_0-9]*$')
Expand Down Expand Up @@ -35,7 +35,7 @@ def find_config_files(self, document, names):
curdir = os.path.dirname(document.path)

while curdir != os.path.dirname(self.root) and curdir != '/':
existing = filter(os.path.exists, [os.path.join(curdir, n) for n in names])
existing = list(filter(os.path.exists, [os.path.join(curdir, n) for n in names]))
if existing:
return existing
curdir = os.path.dirname(curdir)
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=[
'configparser',
'future',
'pycodestyle',
'pyflakes',
'jedi==0.10.0',
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from pyls.python_ls import PythonLanguageServer
from pyls.workspace import Workspace
from StringIO import StringIO
from io import StringIO


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion test/providers/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ def test_completion_ordering(workspace):
items = {c['label']: c['sortText'] for c in completions}

# Assert that builtins come after our own functions even if alphabetically they're before
assert items['hello'] < items['assert']
assert items['hello'] < items['dict']
# And that 'hidden' functions come after unhidden ones
assert items['hello'] < items['_a_hello']
10 changes: 5 additions & 5 deletions test/providers/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_pycodestyle(workspace):

# One we're expecting is:
msg = 'E402 module level import not at top of file'
mod_import = filter(lambda d: d['message'] == msg, diags)[0]
mod_import = [d for d in diags if d['message'] == msg][0]

assert mod_import['code'] == 'E402'
assert mod_import['range']['start'] == {'line': 5, 'character': 0}
Expand Down Expand Up @@ -60,22 +60,22 @@ def test_pycodestyle_config():

# Make sure we get a warning for 'indentation contains tabs'
diags = provider.run(doc_uri)
assert len(filter(lambda d: d['code'] == 'W191', diags)) > 0
assert len([d for d in diags if d['code'] == 'W191']) > 0

content = {
'setup.cfg': ('[pycodestyle]\nignore = W191', True),
'pep8.cfg': ('[pep8]\nignore = W191', True),
'tox.ini': ('', False)
}

for conf_file, (content, working) in content.items():
for conf_file, (content, working) in list(content.items()):
# Now we'll add config file to ignore it
with open(os.path.join(tmp, conf_file), 'w+') as f:
f.write(content)

# And make sure we don't get any warnings
diags = provider.run(doc_uri)
assert len(filter(lambda d: d['code'] == 'W191', diags)) == 0 if working else 1
assert len([d for d in diags if d['code'] == 'W191']) == 0 if working else 1

os.unlink(os.path.join(tmp, conf_file))

Expand All @@ -90,7 +90,7 @@ def test_pyflakes(workspace):

# One we're expecting is:
msg = '\'sys\' imported but unused'
unused_import = filter(lambda d: d['message'] == msg, diags)[0]
unused_import = [d for d in diags if d['message'] == msg][0]

assert unused_import['range']['start'] == {'line': 0, 'character': 0}

Expand Down
6 changes: 3 additions & 3 deletions test/providers/test_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ def test_references(tmp_workspace):
assert len(no_def_refs) == 1

# Make sure our definition is correctly located
doc1_ref = filter(lambda u: u['uri'] == DOC1_URI, refs)[0]
doc1_ref = [u for u in refs if u['uri'] == DOC1_URI][0]
assert doc1_ref['range']['start'] == {'line': 0, 'character': 6}
assert doc1_ref['range']['end'] == {'line': 0, 'character': 11}

# Make sure our import is correctly located
doc2_import_ref = filter(lambda u: u['uri'] != DOC1_URI, refs)[0]
doc2_import_ref = [u for u in refs if u['uri'] != DOC1_URI][0]
assert doc2_import_ref['range']['start'] == {'line': 0, 'character': 18}
assert doc2_import_ref['range']['end'] == {'line': 0, 'character': 23}

doc2_usage_ref = filter(lambda u: u['uri'] != DOC1_URI, refs)[1]
doc2_usage_ref = [u for u in refs if u['uri'] != DOC1_URI][1]
assert doc2_usage_ref['range']['start'] == {'line': 2, 'character': 0}
assert doc2_usage_ref['range']['end'] == {'line': 2, 'character': 5}
Loading