300 lines
8.7 KiB
Python
300 lines
8.7 KiB
Python
"""
|
|
Jedi EPC server.
|
|
|
|
Copyright (C) 2012 Takafumi Arakaki
|
|
|
|
Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
|
|
|
This file is NOT part of GNU Emacs.
|
|
|
|
Jedi EPC server is free software: you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
Jedi EPC server is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Jedi EPC server.
|
|
If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import re
|
|
import itertools
|
|
import logging
|
|
import site
|
|
|
|
jedi = None # I will load it later
|
|
|
|
|
|
PY3 = (sys.version_info[0] >= 3)
|
|
NEED_ENCODE = not PY3
|
|
|
|
|
|
def jedi_script(source, line, column, source_path):
|
|
if NEED_ENCODE:
|
|
source = source.encode('utf-8')
|
|
source_path = source_path and source_path.encode('utf-8')
|
|
return jedi.Script(source, line, column, source_path or '')
|
|
|
|
|
|
def candidate_symbol(comp):
|
|
"""
|
|
Return a character representing completion type.
|
|
|
|
:type comp: jedi.api.Completion
|
|
:arg comp: A completion object returned by `jedi.Script.complete`.
|
|
|
|
"""
|
|
try:
|
|
return comp.type[0].lower()
|
|
except (AttributeError, TypeError):
|
|
return '?'
|
|
|
|
|
|
def candidates_description(comp):
|
|
"""
|
|
Return `comp.description` in an appropriate format.
|
|
|
|
* Avoid return a string 'None'.
|
|
* Strip off all newlines. This is required for using
|
|
`comp.description` as candidate summary.
|
|
|
|
"""
|
|
desc = comp.description
|
|
return _WHITESPACES_RE.sub(' ', desc) if desc and desc != 'None' else ''
|
|
_WHITESPACES_RE = re.compile(r'\s+')
|
|
|
|
|
|
def complete(*args):
|
|
reply = []
|
|
for comp in jedi_script(*args).complete():
|
|
reply.append(dict(
|
|
word=comp.word,
|
|
doc=comp.doc,
|
|
description=candidates_description(comp),
|
|
symbol=candidate_symbol(comp),
|
|
))
|
|
return reply
|
|
|
|
|
|
def get_in_function_call(*args):
|
|
call_def = jedi_script(*args).get_in_function_call()
|
|
if call_def:
|
|
return dict(
|
|
# p.get_code(False) should do the job. But jedi-vim use replace.
|
|
# So follow what jedi-vim does...
|
|
params=[p.get_code().replace('\n', '') for p in call_def.params],
|
|
index=call_def.index,
|
|
call_name=call_def.call_name,
|
|
)
|
|
else:
|
|
return [] # nil
|
|
|
|
|
|
def _goto(method, *args):
|
|
"""
|
|
Helper function for `goto` and `related_names`.
|
|
|
|
:arg method: `jedi.Script.goto` or `jedi.Script.related_names`
|
|
:arg args: Arguments to `jedi_script`
|
|
|
|
"""
|
|
# `definitions` is a list. Each element is an instances of
|
|
# `jedi.api_classes.BaseOutput` subclass, i.e.,
|
|
# `jedi.api_classes.RelatedName` or `jedi.api_classes.Definition`.
|
|
definitions = method(jedi_script(*args))
|
|
return [dict(
|
|
column=d.column,
|
|
line_nr=d.line_nr,
|
|
module_path=d.module_path if d.module_path != '__builtin__' else [],
|
|
module_name=d.module_name,
|
|
description=d.description,
|
|
) for d in definitions]
|
|
|
|
|
|
def goto(*args):
|
|
return _goto(jedi.Script.goto, *args)
|
|
|
|
|
|
def related_names(*args):
|
|
return _goto(jedi.Script.related_names, *args)
|
|
|
|
|
|
def definition_to_dict(d):
|
|
return dict(
|
|
doc=d.doc,
|
|
description=d.description,
|
|
desc_with_module=d.desc_with_module,
|
|
line_nr=d.line_nr,
|
|
column=d.column,
|
|
module_path=d.module_path,
|
|
name=getattr(d, 'name', []),
|
|
full_name=getattr(d, 'full_name', []),
|
|
type=getattr(d, 'type', []),
|
|
)
|
|
|
|
|
|
def get_definition(*args):
|
|
definitions = jedi_script(*args).get_definition()
|
|
return list(map(definition_to_dict, definitions))
|
|
|
|
|
|
def get_names_recursively(definition):
|
|
"""
|
|
Fetch interesting defined names in sub-scopes under `definition`.
|
|
|
|
:type names: jedi.api_classes.Definition
|
|
|
|
"""
|
|
d = definition_to_dict(definition)
|
|
# FIXME: use appropriate method to do this (when Jedi implement some)
|
|
if definition.description.startswith('class '):
|
|
ds = definition.defined_names()
|
|
return [d] + list(map(get_names_recursively, ds))
|
|
else:
|
|
return [d]
|
|
|
|
|
|
def defined_names(*args):
|
|
return list(map(get_names_recursively, jedi.api.defined_names(*args)))
|
|
|
|
|
|
def get_module_version(module):
|
|
try:
|
|
from pkg_resources import get_distribution, DistributionNotFound
|
|
try:
|
|
return get_distribution(module.__name__).version
|
|
except DistributionNotFound:
|
|
pass
|
|
except ImportError:
|
|
pass
|
|
|
|
notfound = object()
|
|
for key in ['__version__', 'version']:
|
|
version = getattr(module, key, notfound)
|
|
if version is not notfound:
|
|
return version
|
|
|
|
|
|
def get_jedi_version():
|
|
import epc
|
|
import sexpdata
|
|
return [dict(
|
|
name=module.__name__,
|
|
file=getattr(module, '__file__', []),
|
|
version=get_module_version(module) or [],
|
|
) for module in [sys, jedi, epc, sexpdata]]
|
|
|
|
|
|
def jedi_epc_server(address='localhost', port=0, port_file=sys.stdout,
|
|
sys_path=[], virtual_env=[],
|
|
debugger=None, log=None, log_level=None):
|
|
add_virtualenv_path()
|
|
for p in virtual_env:
|
|
add_virtualenv_path(p)
|
|
sys_path = map(os.path.expandvars, map(os.path.expanduser, sys_path))
|
|
sys.path = [''] + list(filter(None, itertools.chain(sys_path, sys.path)))
|
|
# Workaround Jedi's module cache. Use this workaround until Jedi
|
|
# got an API to set module paths.
|
|
# See also: https://github.com/davidhalter/jedi/issues/36
|
|
import_jedi()
|
|
import epc.server
|
|
server = epc.server.EPCServer((address, port))
|
|
server.register_function(complete)
|
|
server.register_function(get_in_function_call)
|
|
server.register_function(goto)
|
|
server.register_function(related_names)
|
|
server.register_function(get_definition)
|
|
server.register_function(defined_names)
|
|
server.register_function(get_jedi_version)
|
|
|
|
port_file.write(str(server.server_address[1])) # needed for Emacs client
|
|
port_file.write("\n")
|
|
port_file.flush()
|
|
if port_file is not sys.stdout:
|
|
port_file.close()
|
|
|
|
if log:
|
|
server.log_traceback = True
|
|
handler = logging.FileHandler(filename=log, mode='w')
|
|
if log_level:
|
|
log_level = getattr(logging, log_level.upper())
|
|
handler.setLevel(log_level)
|
|
server.logger.setLevel(log_level)
|
|
server.logger.addHandler(handler)
|
|
if debugger:
|
|
server.set_debugger(debugger)
|
|
handler = logging.StreamHandler()
|
|
handler.setLevel(logging.DEBUG)
|
|
server.logger.addHandler(handler)
|
|
server.logger.setLevel(logging.DEBUG)
|
|
|
|
server.serve_forever()
|
|
server.logger.info('exit')
|
|
return server
|
|
|
|
|
|
def import_jedi():
|
|
global jedi
|
|
import jedi
|
|
import jedi.parsing
|
|
import jedi.evaluate
|
|
import jedi.api
|
|
return jedi
|
|
|
|
|
|
def add_virtualenv_path(venv=os.getenv('VIRTUAL_ENV')):
|
|
"""Add virtualenv's site-packages to `sys.path`."""
|
|
if not venv:
|
|
return
|
|
venv = os.path.abspath(venv)
|
|
path = os.path.join(
|
|
venv, 'lib', 'python%d.%d' % sys.version_info[:2], 'site-packages')
|
|
sys.path.insert(0, path)
|
|
site.addsitedir(path)
|
|
|
|
|
|
def main(args=None):
|
|
import argparse
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
description=__doc__)
|
|
parser.add_argument(
|
|
'--address', default='localhost')
|
|
parser.add_argument(
|
|
'--port', default=0, type=int)
|
|
parser.add_argument(
|
|
'--port-file', '-f', default='-', type=argparse.FileType('wt'),
|
|
help='file to write port on. default is stdout.')
|
|
parser.add_argument(
|
|
'--sys-path', '-p', default=[], action='append',
|
|
help='paths to be inserted at the top of `sys.path`.')
|
|
parser.add_argument(
|
|
'--virtual-env', '-v', default=[], action='append',
|
|
help='paths to be used as if VIRTUAL_ENV is set to it.')
|
|
parser.add_argument(
|
|
'--log', help='save server log to this file.')
|
|
parser.add_argument(
|
|
'--log-level',
|
|
choices=['CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG'],
|
|
help='logging level for log file.')
|
|
parser.add_argument(
|
|
'--pdb', dest='debugger', const='pdb', action='store_const',
|
|
help='start pdb when error occurs.')
|
|
parser.add_argument(
|
|
'--ipdb', dest='debugger', const='ipdb', action='store_const',
|
|
help='start ipdb when error occurs.')
|
|
ns = parser.parse_args(args)
|
|
jedi_epc_server(**vars(ns))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|